From f93822cebf6258c3a31e03745183aa2c8bb56f4d Mon Sep 17 00:00:00 2001 From: tanaka takashi Date: Sat, 27 Jun 2026 14:49:12 +0900 Subject: マルチ言語対応。MCP設定画面の言語切り替え表示にバグあり MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_i18n.py | 110 +++++++++++++++++++++++++++++++++++++++++++ tests/test_i18n_rendering.py | 72 ++++++++++++++++++++++++++++ 2 files changed, 182 insertions(+) create mode 100644 tests/test_i18n.py create mode 100644 tests/test_i18n_rendering.py (limited to 'tests') diff --git a/tests/test_i18n.py b/tests/test_i18n.py new file mode 100644 index 0000000..48f8b9f --- /dev/null +++ b/tests/test_i18n.py @@ -0,0 +1,110 @@ +"""Tests that i18n keys referenced in code and templates exist in JSON files.""" + +from __future__ import annotations + +import json +import re +from pathlib import Path + +import pytest + +from ollama_web import i18n, prompts + + +_SRC = Path(__file__).resolve().parent.parent / "src" / "ollama_web" + + +def _json_keys(data: dict, prefix: str = "") -> set[str]: + """Return all dot-separated keys in a nested dict.""" + keys: set[str] = set() + for key, value in data.items(): + full = f"{prefix}.{key}" if prefix else key + keys.add(full) + if isinstance(value, dict): + keys.update(_json_keys(value, full)) + return keys + + +def _extract_template_keys(path: Path) -> set[str]: + """Find t('...') calls in Jinja2 templates.""" + text = path.read_text(encoding="utf-8") + return set(re.findall(r"{{\s*t\(['\"]([^'\"]+)['\"]", text)) + + +def _extract_js_keys(path: Path) -> set[str]: + """Find t("...") / t('...') calls in JavaScript files.""" + text = path.read_text(encoding="utf-8") + return set(re.findall(r"\bt\(['\"]([^'\"]+)['\"]", text)) + + +def _extract_python_t_keys(path: Path) -> set[str]: + """Find t(...) calls in Python source.""" + text = path.read_text(encoding="utf-8") + return set(re.findall(r"\bt\(['\"]([^'\"]+)['\"]", text)) + + +def _extract_python_prompt_keys(path: Path) -> set[str]: + """Find get_prompt(...) calls in Python source.""" + text = path.read_text(encoding="utf-8") + return set(re.findall(r"\bget_prompt\(['\"]([^'\"]+)['\"]", text)) + + +def _referenced_message_keys() -> set[str]: + """Collect every message key referenced from templates, JS and Python.""" + referenced: set[str] = set() + for path in _SRC.rglob("*.html"): + referenced.update(_extract_template_keys(path)) + for path in _SRC.rglob("*.js"): + referenced.update(_extract_js_keys(path)) + for path in _SRC.rglob("*.py"): + referenced.update(_extract_python_t_keys(path)) + return referenced + + +def _referenced_prompt_keys() -> set[str]: + """Collect every prompt key referenced from Python.""" + referenced: set[str] = set() + for path in _SRC.rglob("*.py"): + referenced.update(_extract_python_prompt_keys(path)) + return referenced + + +@pytest.mark.parametrize("lang", ["ja", "en"]) +def test_all_message_keys_exist(lang: str): + """Every referenced message key must exist for each supported language.""" + messages = i18n.get_messages(lang) + available = _json_keys(messages) + missing = _referenced_message_keys() - available + assert not missing, f"Missing keys in i18n/messages/{lang}.json: {sorted(missing)}" + + +@pytest.mark.parametrize("lang", ["ja", "en"]) +def test_all_prompt_keys_exist(lang: str): + """Every referenced prompt key must exist for each supported language.""" + prompt_data = prompts.get_prompts(lang) + available = _json_keys(prompt_data) + missing = _referenced_prompt_keys() - available + assert not missing, f"Missing keys in prompts/{lang}.json: {sorted(missing)}" + + +@pytest.mark.parametrize("lang", ["ja", "en"]) +def test_json_files_are_valid(lang: str): + """Both localization JSON files must be valid and parseable.""" + assert isinstance(i18n.get_messages(lang), dict) + assert isinstance(prompts.get_prompts(lang), dict) + + +def test_t_returns_expected_strings(): + """Basic sanity checks for the t() helper.""" + assert i18n.t("common.send") == "送信" + assert i18n.t("common.send", lang="en") == "Send" + assert i18n.t("missing.key", fallback="fallback") == "fallback" + assert i18n.t("missing.key") == "missing.key" + + +def test_get_prompt_returns_expected_strings(): + """Basic sanity checks for the get_prompt() helper.""" + assert prompts.get_prompt("tool_system") == prompts.get_prompts("ja")["tool_system"] + assert prompts.get_prompt("tool_system", lang="en") == prompts.get_prompts("en")["tool_system"] + assert prompts.get_prompt("missing.prompt", fallback="fallback") == "fallback" + assert prompts.get_prompt("missing.prompt") == "missing.prompt" diff --git a/tests/test_i18n_rendering.py b/tests/test_i18n_rendering.py new file mode 100644 index 0000000..95015d5 --- /dev/null +++ b/tests/test_i18n_rendering.py @@ -0,0 +1,72 @@ +"""Tests that page rendering respects the configured language consistently.""" + +from __future__ import annotations + +import pytest +from starlette.testclient import TestClient + +from ollama_web.app import create_app +from ollama_web.config import settings +from ollama_web.i18n import get_messages, t + + +@pytest.fixture +def reset_language(): + """Reset language to the initial value after each test.""" + original = settings.language + yield + settings.language = original + if hasattr(get_messages, "cache_clear"): + get_messages.cache_clear() + + +def _render_index() -> str: + """Render index.html and return the HTML body.""" + app = create_app() + with TestClient(app) as client: + response = client.post("/api/auth/login", json={"pin": settings.pin}) + assert response.status_code == 200 + response = client.get("/") + return response.text + + +def test_english_language_consistency(reset_language): + """When settings.language='en', all UI text including MCP must be English.""" + settings.language = "en" + html = _render_index() + # HTML-side labels + assert 'lang="en"' in html + assert "MCP Settings" in html + assert "MCP Server Settings" in html + assert "+ Add server" in html + # Japanese labels must not appear anywhere + assert "MCP設定" not in html + assert "MCPサーバー設定" not in html + assert "+ サーバー追加" not in html + assert "セッション" not in html + assert "モデル:" not in html + + +def test_japanese_language_consistency(reset_language): + """When settings.language='ja', all UI text including MCP must be Japanese.""" + settings.language = "ja" + html = _render_index() + # HTML-side labels + assert 'lang="ja"' in html + assert "MCP設定" in html + assert "MCPサーバー設定" in html + assert "+ サーバー追加" in html + # English labels must not appear anywhere + assert "MCP Settings" not in html + assert "MCP Server Settings" not in html + assert "+ Add server" not in html + assert "Sessions" not in html + assert "Model:" not in html + + +def test_t_helper_respects_language_override(): + """The t() helper must return the requested language string.""" + assert t("common.send", lang="en") == "Send" + assert t("common.send", lang="ja") == "送信" + assert t("mcp.server_settings", lang="en") == "MCP Server Settings" + assert t("mcp.server_settings", lang="ja") == "MCPサーバー設定" -- cgit v1.2.3