Files
webui/tests/test_language_precedence.py

263 lines
6.8 KiB
Python

import json
import pathlib
import re
import subprocess
import textwrap
REPO_ROOT = pathlib.Path(__file__).parent.parent.resolve()
I18N_JS = (REPO_ROOT / "static" / "i18n.js").read_text(encoding="utf-8")
BOOT_JS = (REPO_ROOT / "static" / "boot.js").read_text(encoding="utf-8")
PANELS_JS = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8")
def _run_i18n_case(script_expr: str) -> dict:
wrapped_expr = f"(() => ({script_expr}))()"
script = textwrap.dedent(
f"""
const fs = require('fs');
const vm = require('vm');
const src = fs.readFileSync({json.dumps(str(REPO_ROOT / "static" / "i18n.js"))}, 'utf8');
const storage = {{}};
const ctx = {{
localStorage: {{
getItem: (k) => Object.prototype.hasOwnProperty.call(storage, k) ? storage[k] : null,
setItem: (k, v) => {{ storage[k] = String(v); }},
}},
document: {{
documentElement: {{ lang: '' }},
querySelectorAll: () => [],
}},
}};
vm.createContext(ctx);
vm.runInContext(src, ctx);
const out = vm.runInContext({json.dumps(wrapped_expr)}, ctx);
process.stdout.write(JSON.stringify(out));
"""
)
proc = subprocess.run(["node", "-e", script], check=True, capture_output=True, text=True)
return json.loads(proc.stdout)
def _extract_call_arglists(src: str, fn_name: str) -> list[str]:
token = f"{fn_name}("
out = []
search_from = 0
while True:
start = src.find(token, search_from)
if start < 0:
return out
i = start + len(token)
depth = 1
in_single = False
in_double = False
in_backtick = False
escape = False
while i < len(src):
ch = src[i]
if escape:
escape = False
i += 1
continue
if in_single:
if ch == "\\":
escape = True
elif ch == "'":
in_single = False
i += 1
continue
if in_double:
if ch == "\\":
escape = True
elif ch == '"':
in_double = False
i += 1
continue
if in_backtick:
if ch == "\\":
escape = True
elif ch == "`":
in_backtick = False
i += 1
continue
if ch == "'":
in_single = True
elif ch == '"':
in_double = True
elif ch == "`":
in_backtick = True
elif ch == "(":
depth += 1
elif ch == ")":
depth -= 1
if depth == 0:
out.append(src[start + len(token) : i])
break
i += 1
search_from = start + len(token)
def _split_top_level_args(arg_src: str) -> list[str]:
args = []
cur = []
paren = 0
brace = 0
bracket = 0
in_single = False
in_double = False
in_backtick = False
escape = False
for ch in arg_src:
if escape:
cur.append(ch)
escape = False
continue
if in_single:
cur.append(ch)
if ch == "\\":
escape = True
elif ch == "'":
in_single = False
continue
if in_double:
cur.append(ch)
if ch == "\\":
escape = True
elif ch == '"':
in_double = False
continue
if in_backtick:
cur.append(ch)
if ch == "\\":
escape = True
elif ch == "`":
in_backtick = False
continue
if ch == "'":
in_single = True
cur.append(ch)
continue
if ch == '"':
in_double = True
cur.append(ch)
continue
if ch == "`":
in_backtick = True
cur.append(ch)
continue
if ch == "(":
paren += 1
cur.append(ch)
continue
if ch == ")":
paren -= 1
cur.append(ch)
continue
if ch == "{":
brace += 1
cur.append(ch)
continue
if ch == "}":
brace -= 1
cur.append(ch)
continue
if ch == "[":
bracket += 1
cur.append(ch)
continue
if ch == "]":
bracket -= 1
cur.append(ch)
continue
if ch == "," and paren == 0 and brace == 0 and bracket == 0:
args.append("".join(cur).strip())
cur = []
continue
cur.append(ch)
if cur:
args.append("".join(cur).strip())
return args
def _has_precedence_call(src: str, first_arg: str) -> bool:
expected_second = {
"localStorage.getItem('hermes-lang')",
'localStorage.getItem("hermes-lang")',
}
for arg_src in _extract_call_arglists(src, "resolvePreferredLocale"):
args = _split_top_level_args(arg_src)
if len(args) < 2:
continue
first = re.sub(r"\s+", "", args[0])
second = re.sub(r"\s+", "", args[1])
if first == first_arg and second in expected_second:
return True
return False
def test_i18n_exposes_locale_resolvers():
assert "function resolveLocale(" in I18N_JS
assert "function resolvePreferredLocale(" in I18N_JS
def test_locale_alias_resolution_and_precedence_logic():
result = _run_i18n_case(
"""
{
zhCn: resolveLocale('zh-CN'),
zhTw: resolveLocale('zh_TW'),
enUs: resolveLocale('EN-us'),
esMx: resolveLocale('es-MX'),
bad: resolveLocale('xx-YY'),
preferred1: resolvePreferredLocale('zh-CN', 'en'),
preferred2: resolvePreferredLocale('xx-YY', 'zh-Hant'),
preferred3: resolvePreferredLocale('', 'xx-YY'),
}
"""
)
assert result["zhCn"] == "zh"
assert result["zhTw"] == "zh-Hant"
assert result["enUs"] == "en"
assert result["esMx"] == "es"
assert result["bad"] is None
assert result["preferred1"] == "zh"
assert result["preferred2"] == "zh-Hant"
assert result["preferred3"] == "en"
def test_set_locale_normalizes_alias_and_persists_canonical_key():
result = _run_i18n_case(
"""
{
...(setLocale('zh-CN'), {}),
saved: localStorage.getItem('hermes-lang'),
htmlLang: document.documentElement.lang,
}
"""
)
assert result["saved"] == "zh"
assert result["htmlLang"] == "zh-CN"
def test_boot_and_settings_panel_use_shared_locale_precedence():
assert _has_precedence_call(BOOT_JS, "s.language")
assert _has_precedence_call(PANELS_JS, "settings.language")