fix i18n review comments and locale test robustness
This commit is contained in:
committed by
Nathan Esquenazi
parent
c4efe96725
commit
204dc23c6b
@@ -10,6 +10,66 @@ def read(path: Path) -> str:
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def extract_locale_block(src: str, locale_key: str) -> str:
|
||||
start_match = re.search(rf"\b{re.escape(locale_key)}\s*:\s*\{{", src)
|
||||
assert start_match, f"{locale_key} locale block not found"
|
||||
|
||||
start = start_match.end() - 1 # "{"
|
||||
depth = 0
|
||||
in_single = False
|
||||
in_double = False
|
||||
in_backtick = False
|
||||
escape = False
|
||||
|
||||
for i in range(start, len(src)):
|
||||
ch = src[i]
|
||||
|
||||
if escape:
|
||||
escape = False
|
||||
continue
|
||||
|
||||
if in_single:
|
||||
if ch == "\\":
|
||||
escape = True
|
||||
elif ch == "'":
|
||||
in_single = False
|
||||
continue
|
||||
|
||||
if in_double:
|
||||
if ch == "\\":
|
||||
escape = True
|
||||
elif ch == '"':
|
||||
in_double = False
|
||||
continue
|
||||
|
||||
if in_backtick:
|
||||
if ch == "\\":
|
||||
escape = True
|
||||
elif ch == "`":
|
||||
in_backtick = False
|
||||
continue
|
||||
|
||||
if ch == "'":
|
||||
in_single = True
|
||||
continue
|
||||
if ch == '"':
|
||||
in_double = True
|
||||
continue
|
||||
if ch == "`":
|
||||
in_backtick = True
|
||||
continue
|
||||
|
||||
if ch == "{":
|
||||
depth += 1
|
||||
continue
|
||||
if ch == "}":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return src[start + 1 : i]
|
||||
|
||||
raise AssertionError(f"{locale_key} locale block braces are not balanced")
|
||||
|
||||
|
||||
def test_chinese_locale_block_exists():
|
||||
src = read(REPO / "static" / "i18n.js")
|
||||
assert "\n zh: {" in src
|
||||
@@ -35,17 +95,9 @@ def test_chinese_locale_includes_representative_translations():
|
||||
|
||||
def test_chinese_locale_covers_english_keys():
|
||||
src = read(REPO / "static" / "i18n.js")
|
||||
en_match = re.search(r"\n en: \{([\s\S]*?)\n \},\n\n es: \{", src)
|
||||
zh_match = re.search(
|
||||
r"\n zh: \{([\s\S]*?)\n \},\n\n // Traditional Chinese \(zh-Hant\)",
|
||||
src,
|
||||
)
|
||||
assert en_match, "English locale block not found"
|
||||
assert zh_match, "Chinese locale block not found"
|
||||
|
||||
key_pattern = re.compile(r"^\s{4}([a-zA-Z0-9_]+):", re.MULTILINE)
|
||||
en_keys = set(key_pattern.findall(en_match.group(1)))
|
||||
zh_keys = set(key_pattern.findall(zh_match.group(1)))
|
||||
en_keys = set(key_pattern.findall(extract_locale_block(src, "en")))
|
||||
zh_keys = set(key_pattern.findall(extract_locale_block(src, "zh")))
|
||||
|
||||
missing = sorted(en_keys - zh_keys)
|
||||
assert not missing, f"Chinese locale missing keys: {missing}"
|
||||
@@ -53,13 +105,7 @@ def test_chinese_locale_covers_english_keys():
|
||||
|
||||
def test_chinese_locale_has_no_duplicate_keys():
|
||||
src = read(REPO / "static" / "i18n.js")
|
||||
zh_match = re.search(
|
||||
r"\n zh: \{([\s\S]*?)\n \},\n\n // Traditional Chinese \(zh-Hant\)",
|
||||
src,
|
||||
)
|
||||
assert zh_match, "Chinese locale block not found"
|
||||
|
||||
key_pattern = re.compile(r"^\s{4}([a-zA-Z0-9_]+):", re.MULTILINE)
|
||||
keys = key_pattern.findall(zh_match.group(1))
|
||||
keys = key_pattern.findall(extract_locale_block(src, "zh"))
|
||||
duplicates = sorted(k for k, count in Counter(keys).items() if count > 1)
|
||||
assert not duplicates, f"Chinese locale has duplicate keys: {duplicates}"
|
||||
|
||||
@@ -39,6 +39,180 @@ def _run_i18n_case(script_expr: str) -> dict:
|
||||
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
|
||||
@@ -84,5 +258,5 @@ def test_set_locale_normalizes_alias_and_persists_canonical_key():
|
||||
|
||||
|
||||
def test_boot_and_settings_panel_use_shared_locale_precedence():
|
||||
assert re.search(r"resolvePreferredLocale\(s\.language\s*,\s*localStorage\.getItem\('hermes-lang'\)\)", BOOT_JS)
|
||||
assert re.search(r"resolvePreferredLocale\(settings\.language\s*,\s*localStorage\.getItem\('hermes-lang'\)\)", PANELS_JS)
|
||||
assert _has_precedence_call(BOOT_JS, "s.language")
|
||||
assert _has_precedence_call(PANELS_JS, "settings.language")
|
||||
|
||||
@@ -46,7 +46,9 @@ def test_login_page_uses_simplified_chinese_for_zh_cn_alias():
|
||||
assert "\u767b\u5f55" in html
|
||||
assert "\u8f93\u5165\u5bc6\u7801\u7ee7\u7eed\u4f7f\u7528" in html
|
||||
finally:
|
||||
post("/api/settings", {"language": prev_lang})
|
||||
restored, restore_status = post("/api/settings", {"language": prev_lang})
|
||||
assert restore_status == 200
|
||||
assert restored.get("language") == prev_lang
|
||||
|
||||
|
||||
def test_login_page_uses_traditional_chinese_for_zh_hant():
|
||||
@@ -61,4 +63,6 @@ def test_login_page_uses_traditional_chinese_for_zh_hant():
|
||||
assert "\u8f38\u5165\u5bc6\u78bc\u7e7c\u7e8c\u4f7f\u7528" in html
|
||||
assert "\u5bc6\u78bc\u932f\u8aa4" in html
|
||||
finally:
|
||||
post("/api/settings", {"language": prev_lang})
|
||||
restored, restore_status = post("/api/settings", {"language": prev_lang})
|
||||
assert restore_status == 200
|
||||
assert restored.get("language") == prev_lang
|
||||
|
||||
Reference in New Issue
Block a user