mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-22 13:42:12 -03:00
249 lines
7.2 KiB
Python
249 lines
7.2 KiB
Python
"""Regression tests for localization data and usage.
|
|
|
|
These tests validate three key aspects of the localisation setup:
|
|
|
|
* Every locale file is valid JSON and contains the expected sections.
|
|
* All locales expose the same translation keys as the English reference.
|
|
* Static JavaScript/HTML sources only reference available translation keys.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
from typing import Dict, Iterable, Set
|
|
|
|
import pytest
|
|
|
|
ROOT_DIR = Path(__file__).resolve().parents[2]
|
|
LOCALES_DIR = ROOT_DIR / "locales"
|
|
STATIC_JS_DIR = ROOT_DIR / "static" / "js"
|
|
TEMPLATES_DIR = ROOT_DIR / "templates"
|
|
|
|
EXPECTED_LOCALES = (
|
|
"en",
|
|
"zh-CN",
|
|
"zh-TW",
|
|
"ja",
|
|
"ru",
|
|
"de",
|
|
"fr",
|
|
"es",
|
|
"ko",
|
|
"he",
|
|
)
|
|
|
|
REQUIRED_SECTIONS = {"common", "header", "loras", "recipes", "modals"}
|
|
|
|
SINGLE_WORD_TRANSLATION_KEYS = {
|
|
"loading",
|
|
"error",
|
|
"success",
|
|
"warning",
|
|
"info",
|
|
"cancel",
|
|
"save",
|
|
"delete",
|
|
}
|
|
|
|
FALSE_POSITIVES = {
|
|
"checkpoint",
|
|
"civitai_api_key",
|
|
"div",
|
|
"embedding",
|
|
"lora",
|
|
"show_only_sfw",
|
|
"model",
|
|
"type",
|
|
"name",
|
|
"value",
|
|
"id",
|
|
"class",
|
|
"style",
|
|
"src",
|
|
"href",
|
|
"data",
|
|
"width",
|
|
"height",
|
|
"size",
|
|
"format",
|
|
"version",
|
|
"url",
|
|
"path",
|
|
"file",
|
|
"folder",
|
|
"image",
|
|
"text",
|
|
"number",
|
|
"boolean",
|
|
"array",
|
|
"object",
|
|
"non.existent.key",
|
|
}
|
|
|
|
SPECIAL_UI_HELPER_KEYS = {
|
|
"uiHelpers.workflow.loraAdded",
|
|
"uiHelpers.workflow.loraReplaced",
|
|
"uiHelpers.workflow.loraFailedToSend",
|
|
"uiHelpers.workflow.recipeAdded",
|
|
"uiHelpers.workflow.recipeReplaced",
|
|
"uiHelpers.workflow.recipeFailedToSend",
|
|
}
|
|
|
|
JS_TRANSLATION_PATTERNS = (
|
|
r"\btranslate\s*\(\s*['\"]([a-zA-Z0-9._-]+)['\"]",
|
|
r"\bshowToast\s*\(\s*['\"]([a-zA-Z0-9._-]+)['\"]",
|
|
r"\bt\s*\(\s*['\"]([a-zA-Z0-9._-]+)['\"]",
|
|
)
|
|
|
|
HTML_TRANSLATION_PATTERN = (
|
|
r"(?:\{\{|\{%)[^}]*\bt\s*\(\s*['\"]([a-zA-Z0-9._-]+)['\"][^}]*(?:\}\}|%\})"
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def loaded_locales() -> Dict[str, dict]:
|
|
"""Load locale JSON once per test module."""
|
|
locales: Dict[str, dict] = {}
|
|
|
|
for locale in EXPECTED_LOCALES:
|
|
path = LOCALES_DIR / f"{locale}.json"
|
|
if not path.exists():
|
|
pytest.fail(f"Locale file {path.name} is missing", pytrace=False)
|
|
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError as exc: # pragma: no cover - explicit failure message
|
|
pytest.fail(f"Locale file {path.name} contains invalid JSON: {exc}", pytrace=False)
|
|
|
|
if not isinstance(data, dict):
|
|
pytest.fail(
|
|
f"Locale file {path.name} must contain a JSON object at the top level",
|
|
pytrace=False,
|
|
)
|
|
|
|
locales[locale] = data
|
|
|
|
return locales
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def english_translation_keys(loaded_locales: Dict[str, dict]) -> Set[str]:
|
|
return collect_translation_keys(loaded_locales["en"])
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def static_code_translation_keys() -> Set[str]:
|
|
return gather_static_translation_keys()
|
|
|
|
|
|
def collect_translation_keys(data: dict, prefix: str = "") -> Set[str]:
|
|
"""Recursively collect translation keys from a locale dictionary."""
|
|
keys: Set[str] = set()
|
|
|
|
for key, value in data.items():
|
|
full_key = f"{prefix}.{key}" if prefix else key
|
|
if isinstance(value, dict):
|
|
keys.update(collect_translation_keys(value, full_key))
|
|
else:
|
|
keys.add(full_key)
|
|
|
|
return keys
|
|
|
|
|
|
def gather_static_translation_keys() -> Set[str]:
|
|
"""Collect translation keys referenced in static JavaScript and HTML templates."""
|
|
keys: Set[str] = set()
|
|
|
|
if STATIC_JS_DIR.exists():
|
|
for file_path in STATIC_JS_DIR.rglob("*.js"):
|
|
keys.update(filter_translation_keys(extract_i18n_keys_from_js(file_path)))
|
|
|
|
if TEMPLATES_DIR.exists():
|
|
for file_path in TEMPLATES_DIR.rglob("*.html"):
|
|
keys.update(filter_translation_keys(extract_i18n_keys_from_html(file_path)))
|
|
|
|
keys.update(SPECIAL_UI_HELPER_KEYS)
|
|
|
|
return keys
|
|
|
|
|
|
def filter_translation_keys(raw_keys: Iterable[str]) -> Set[str]:
|
|
"""Filter out obvious false positives and non-translation identifiers."""
|
|
filtered: Set[str] = set()
|
|
for key in raw_keys:
|
|
if key in FALSE_POSITIVES:
|
|
continue
|
|
if "." not in key and key not in SINGLE_WORD_TRANSLATION_KEYS:
|
|
continue
|
|
filtered.add(key)
|
|
return filtered
|
|
|
|
|
|
def extract_i18n_keys_from_js(file_path: Path) -> Set[str]:
|
|
"""Extract translation keys from JavaScript sources."""
|
|
content = file_path.read_text(encoding="utf-8")
|
|
# Remove single-line and multi-line comments to avoid false positives.
|
|
content = re.sub(r"//.*$", "", content, flags=re.MULTILINE)
|
|
content = re.sub(r"/\*.*?\*/", "", content, flags=re.DOTALL)
|
|
|
|
matches: Set[str] = set()
|
|
for pattern in JS_TRANSLATION_PATTERNS:
|
|
matches.update(re.findall(pattern, content))
|
|
return matches
|
|
|
|
|
|
def extract_i18n_keys_from_html(file_path: Path) -> Set[str]:
|
|
"""Extract translation keys from HTML templates."""
|
|
content = file_path.read_text(encoding="utf-8")
|
|
content = re.sub(r"<!--.*?-->", "", content, flags=re.DOTALL)
|
|
|
|
matches: Set[str] = set(re.findall(HTML_TRANSLATION_PATTERN, content))
|
|
|
|
# Inspect inline script tags as JavaScript.
|
|
for script_body in re.findall(r"<script[^>]*>(.*?)</script>", content, flags=re.DOTALL):
|
|
for pattern in JS_TRANSLATION_PATTERNS:
|
|
matches.update(re.findall(pattern, script_body))
|
|
|
|
return matches
|
|
|
|
|
|
@pytest.mark.parametrize("locale", EXPECTED_LOCALES)
|
|
def test_locale_files_have_expected_structure(locale: str, loaded_locales: Dict[str, dict]) -> None:
|
|
"""Every locale must contain the required sections."""
|
|
data = loaded_locales[locale]
|
|
missing_sections = sorted(REQUIRED_SECTIONS - data.keys())
|
|
assert not missing_sections, f"{locale} locale is missing sections: {missing_sections}"
|
|
|
|
|
|
@pytest.mark.parametrize("locale", EXPECTED_LOCALES[1:])
|
|
def test_locale_keys_match_english(
|
|
locale: str, loaded_locales: Dict[str, dict], english_translation_keys: Set[str]
|
|
) -> None:
|
|
"""Locales must expose the same translation keys as English."""
|
|
locale_keys = collect_translation_keys(loaded_locales[locale])
|
|
|
|
missing_keys = sorted(english_translation_keys - locale_keys)
|
|
extra_keys = sorted(locale_keys - english_translation_keys)
|
|
|
|
assert not missing_keys, (
|
|
f"{locale} is missing translation keys: {missing_keys[:10]}"
|
|
+ ("..." if len(missing_keys) > 10 else "")
|
|
)
|
|
assert not extra_keys, (
|
|
f"{locale} defines unexpected translation keys: {extra_keys[:10]}"
|
|
+ ("..." if len(extra_keys) > 10 else "")
|
|
)
|
|
|
|
|
|
def test_static_sources_only_use_existing_translations(
|
|
english_translation_keys: Set[str], static_code_translation_keys: Set[str]
|
|
) -> None:
|
|
"""Static code must not reference unknown translation keys."""
|
|
missing_keys = sorted(static_code_translation_keys - english_translation_keys)
|
|
assert not missing_keys, (
|
|
"Static sources reference missing translation keys: "
|
|
f"{missing_keys[:20]}" + ("..." if len(missing_keys) > 20 else "")
|
|
)
|