Files
srbhr--resume-matcher/scripts/check_locale_parity.py
T
wehub-resource-sync 5bdf4cc89a
Publish Docker Image / publish (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:39:36 +08:00

158 lines
5.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Verify every frontend locale file structurally matches ``en.json``.
The frontend declares ``type Messages = typeof en`` and
``const allMessages: Record<Locale, Messages>``, so a locale JSON that is
*missing* a key present in ``en.json`` - or that has a key of a *different shape*
(an object where ``en`` has a string, or vice versa) - makes ``tsc`` /
``next build`` fail. That is exactly the break that shipped to ``main`` and only
surfaced (post-merge) inside the Docker publish job - the incident that
motivated the testing work.
This check reproduces and prevents it with **pure stdlib** - no Node, npm, or
nvm - so it runs fast inside the local pre-push hook regardless of shell setup.
Exit code: 0 = all locales match; 1 = at least one locale is missing keys, has a
key whose shape (leaf vs. object) differs from ``en``, or is not valid JSON.
Extra keys (present in a locale but not in ``en``) are reported as warnings only
(they don't break the ``typeof en`` build, but signal drift worth cleaning up).
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
from typing import Any
MESSAGES_DIR = Path(__file__).resolve().parents[1] / "apps" / "frontend" / "messages"
REFERENCE = "en.json"
def _node_kind(value: Any) -> str:
"""Classify a JSON value by the type ``typeof en`` would infer for it.
``bool`` is checked before ``int`` because ``bool`` is a subclass of ``int``
in Python (``isinstance(True, int)`` is ``True``).
"""
if isinstance(value, dict):
return "object"
if isinstance(value, list):
return "array"
if isinstance(value, bool):
return "boolean"
if isinstance(value, (int, float)):
return "number"
if value is None:
return "null"
return "string"
def key_kinds(obj: Any, prefix: str = "") -> dict[str, str]:
"""Map every dotted key path to its JSON kind.
Comparing kinds - not just key presence - is what catches a locale whose
``a.b`` is an object (or a number, or an array) where ``en`` has a string:
the key path is still present, so a presence-only check passes, yet it still
breaks ``next build`` because the locale is no longer assignable to
``typeof en``. Tracking the full JSON type (rather than a coarse
branch/leaf) catches primitive/array mismatches too, and keeps this in lock-
step with the frontend in-suite guard. Recursion descends into objects only
(the only container these message files nest with).
"""
kinds: dict[str, str] = {}
if isinstance(obj, dict):
for key, value in obj.items():
path = f"{prefix}.{key}" if prefix else key
kinds[path] = _node_kind(value)
kinds.update(key_kinds(value, path))
return kinds
def _load(path: Path) -> dict[str, Any]:
"""Parse a locale JSON file; raise ``ValueError`` with a clean message on bad JSON."""
try:
return json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise ValueError(f"{path.name} is not valid JSON: {exc}") from exc
def main(argv: list[str]) -> int:
# Optional argv[1] overrides the messages dir (used by tests / the hook).
messages_dir = Path(argv[1]).resolve() if len(argv) > 1 else MESSAGES_DIR
reference_file = messages_dir / REFERENCE
if not reference_file.exists():
print(f"locale-parity: reference {reference_file} not found", file=sys.stderr)
return 1
try:
reference_kinds = key_kinds(_load(reference_file))
except ValueError as exc:
print(f"locale-parity: error: {exc}", file=sys.stderr)
return 1
reference_paths = set(reference_kinds)
failed = False
for path in sorted(messages_dir.glob("*.json")):
if path.name == REFERENCE:
continue
try:
locale_kinds = key_kinds(_load(path))
except ValueError as exc:
failed = True
print(f"locale-parity: error: {exc}", file=sys.stderr)
continue
locale_paths = set(locale_kinds)
missing = reference_paths - locale_paths
extra = locale_paths - reference_paths
mismatched = {
p
for p in reference_paths & locale_paths
if reference_kinds[p] != locale_kinds[p]
}
if missing:
failed = True
print(f"locale-parity: error: {path.name} is MISSING keys from {REFERENCE}:", file=sys.stderr)
for key in sorted(missing):
print(f" missing: {key}", file=sys.stderr)
if mismatched:
failed = True
print(
f"locale-parity: error: {path.name} has keys whose JSON type differs "
f"from {REFERENCE}; this breaks `next build`:",
file=sys.stderr,
)
for key in sorted(mismatched):
print(
f" type-mismatch: {key} "
f"({REFERENCE}={reference_kinds[key]}, {path.name}={locale_kinds[key]})",
file=sys.stderr,
)
if extra:
print(
f"locale-parity: warning: {path.name} has extra keys not in {REFERENCE} (non-fatal):",
file=sys.stderr,
)
for key in sorted(extra):
print(f" extra: {key}", file=sys.stderr)
if failed:
print(
"locale-parity: locale files are out of sync with en.json; this would "
"break `next build` (type Messages = typeof en). Fix the missing / "
"mismatched keys.",
file=sys.stderr,
)
return 1
print("locale-parity: ok all locale files match en.json")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))