#!/usr/bin/env python3 """Read-only drift check for shared i18n key coverage. Compares `shared/i18n/key-manifest.json` against `shared/i18n/lobby.json` and fails fast when keyspaces drift between frontend/backend contract sections. """ from __future__ import annotations import json from pathlib import Path import sys from typing import Any REPO_ROOT = Path(__file__).resolve().parents[1] CATALOG_PATH = REPO_ROOT / "shared" / "i18n" / "lobby.json" MANIFEST_PATH = REPO_ROOT / "shared" / "i18n" / "key-manifest.json" def _load_json(path: Path) -> dict[str, Any]: with path.open("r", encoding="utf-8") as handle: return json.load(handle) def _as_set(value: Any) -> set[str]: if not isinstance(value, list): return set() return {str(item) for item in value} def _require_translations( errors: dict[str, Any], locales: set[str], label: str, failures: list[str], ) -> None: for key, translations in errors.items(): if not isinstance(translations, dict): failures.append(f"{label}.{key} must be an object of locale->message") continue for locale in sorted(locales): value = translations.get(locale) if not isinstance(value, str) or not value.strip(): failures.append(f"{label}.{key} missing non-empty '{locale}' translation") def main() -> int: catalog = _load_json(CATALOG_PATH) manifest = _load_json(MANIFEST_PATH) failures: list[str] = [] manifest_locales = _as_set(manifest.get("locales")) catalog_locales = _as_set(catalog.get("locales", {}).get("supported")) if manifest_locales != catalog_locales: failures.append( "locale set mismatch: " f"manifest={sorted(manifest_locales)} catalog={sorted(catalog_locales)}" ) frontend_manifest = _as_set(manifest.get("frontend_error_keys")) frontend_catalog = set(catalog.get("frontend", {}).get("errors", {}).keys()) if frontend_manifest != frontend_catalog: failures.append( "frontend error key mismatch: " f"manifest-only={sorted(frontend_manifest - frontend_catalog)} " f"catalog-only={sorted(frontend_catalog - frontend_manifest)}" ) backend_code_manifest = _as_set(manifest.get("backend_error_codes")) backend_code_catalog = set(catalog.get("backend", {}).get("error_codes", {}).keys()) if backend_code_manifest != backend_code_catalog: failures.append( "backend error code mismatch: " f"manifest-only={sorted(backend_code_manifest - backend_code_catalog)} " f"catalog-only={sorted(backend_code_catalog - backend_code_manifest)}" ) backend_key_manifest = _as_set(manifest.get("backend_error_keys")) backend_key_catalog = set(catalog.get("backend", {}).get("errors", {}).keys()) if backend_key_manifest != backend_key_catalog: failures.append( "backend error key mismatch: " f"manifest-only={sorted(backend_key_manifest - backend_key_catalog)} " f"catalog-only={sorted(backend_key_catalog - backend_key_manifest)}" ) backend_to_frontend = catalog.get("contract", {}).get("backend_to_frontend_error_keys", {}) if not isinstance(backend_to_frontend, dict): failures.append("contract.backend_to_frontend_error_keys must be an object") backend_to_frontend = {} for code in sorted(backend_code_catalog): frontend_key = backend_to_frontend.get(code) if frontend_key is None: failures.append(f"missing contract mapping for backend code '{code}'") continue if frontend_key not in frontend_catalog: failures.append( f"mapping for backend code '{code}' points to unknown frontend key '{frontend_key}'" ) allowed_contract_only_codes = _as_set(manifest.get("allowed_contract_only_backend_codes")) unknown_mapping_codes = set(backend_to_frontend.keys()) - backend_code_catalog disallowed_unknown_mapping_codes = unknown_mapping_codes - allowed_contract_only_codes if disallowed_unknown_mapping_codes: failures.append( "contract contains mappings for unknown backend codes: " f"{sorted(disallowed_unknown_mapping_codes)}" ) _require_translations(catalog.get("frontend", {}).get("errors", {}), manifest_locales, "frontend.errors", failures) _require_translations(catalog.get("backend", {}).get("errors", {}), manifest_locales, "backend.errors", failures) if failures: print("i18n drift check FAILED") for failure in failures: print(f" - {failure}") return 1 print("i18n drift check OK") print(f" - locales: {sorted(manifest_locales)}") print(f" - frontend error keys: {len(frontend_catalog)}") print(f" - backend error codes: {len(backend_code_catalog)}") print(f" - backend error keys: {len(backend_key_catalog)}") return 0 if __name__ == "__main__": raise SystemExit(main())