Files
2026-08-28 11:43:34 -04:00

154 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Comprehensive i18n validation for ObsiGate.
Validates:
1. JSON validity for both locale files
2. Key alignment between EN and FR (no missing/mismatched keys)
3. Empty or truncated values
4. Non-string values (nested objects by accident)
5. Untranslated keys (FR == EN, excluding system keys)
6. HTML i18n references all have corresponding EN keys
7. Auto-generated key health (truncation, nonsense values)
Usage: python scripts/validate-i18n.py [--fix]
"""
import json
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent.parent
FRONTEND = ROOT / "frontend"
EN_PATH = FRONTEND / "locales" / "en.json"
FR_PATH = FRONTEND / "locales" / "fr.json"
HTML_PATH = FRONTEND / "index.html"
EXIT_CODE = 0
def fail(msg: str):
global EXIT_CODE
print(f" FAIL: {msg}")
EXIT_CODE = 1
def ok(msg: str):
print(f" OK: {msg}")
def main():
# 1. Load JSON
print("=== 1. JSON Validity ===")
try:
en = json.loads(EN_PATH.read_text(encoding="utf-8"))
print(f" EN: {len(en)} keys, valid JSON")
except Exception as e:
fail(f"EN invalid: {e}")
sys.exit(1)
try:
fr = json.loads(FR_PATH.read_text(encoding="utf-8"))
print(f" FR: {len(fr)} keys, valid JSON")
except Exception as e:
fail(f"FR invalid: {e}")
sys.exit(1)
# 2. Key alignment
print("\n=== 2. Key Alignment ===")
en_only = set(en.keys()) - set(fr.keys())
fr_only = set(fr.keys()) - set(en.keys())
if en_only:
for k in sorted(en_only):
fail(f"EN-only key: {k}")
if fr_only:
for k in sorted(fr_only):
fail(f"FR-only key: {k}")
if not en_only and not fr_only:
ok(f"Keys aligned: {len(en)} = {len(fr)}")
# 3. Empty / non-string values
print("\n=== 3. Value Health ===")
empty_en = [k for k, v in en.items() if isinstance(v, str) and not v.strip()]
empty_fr = [k for k, v in fr.items() if isinstance(v, str) and not v.strip()]
non_str_en = [(k, type(v).__name__) for k, v in en.items() if not isinstance(v, str)]
non_str_fr = [(k, type(v).__name__) for k, v in fr.items() if not isinstance(v, str)]
if empty_en:
for k in empty_en:
fail(f"Empty EN: {k}")
if empty_fr:
for k in empty_fr:
fail(f"Empty FR: {k}")
if non_str_en:
for k, t in non_str_en:
fail(f"Non-string EN[{k}]: {t}")
if non_str_fr:
for k, t in non_str_fr:
fail(f"Non-string FR[{k}]: {t}")
if not empty_en and not empty_fr and not non_str_en and not non_str_fr:
ok("All values non-empty strings")
# 4. Untranslated keys
print("\n=== 4. Untranslated Keys (FR == EN) ===")
untranslated = []
system_keys = {"This key is auto-generated"}
for k in en:
if k in fr and isinstance(en[k], str) and isinstance(fr[k], str):
if en[k] == fr[k] and en[k].strip() and en[k] not in system_keys:
untranslated.append(k)
if untranslated:
for k in sorted(untranslated):
fail(f"Untranslated: {k} = \"{en[k][:80]}\"")
else:
ok("No untranslated keys (excluding system keys)")
# 5. Auto-key health
print("\n=== 5. Auto-Key Health ===")
auto_en = {k: v for k, v in en.items() if k.startswith("auto.")}
truncated = []
for k, v in auto_en.items():
if not isinstance(v, str):
continue
if not v.endswith((".", ")", "]", "}", '"', "!", "?", "▾", "…")) and len(v) < 80:
# Possible truncation — but check if it's a short label
if len(v) < 30 and v[0].isupper():
truncated.append((k, v))
if truncated:
for k, v in truncated:
fail(f"Possibly truncated auto-key: {k} = \"{v}\"")
else:
ok(f"{len(auto_en)} auto-keys, none suspicious")
# 6. HTML i18n references
print("\n=== 6. HTML i18n References ===")
html = HTML_PATH.read_text(encoding="utf-8")
refs = set()
refs |= set(re.findall(r'data-i18n="([^"]+)"', html))
refs |= set(re.findall(r'data-i18n-title="([^"]+)"', html))
refs |= set(re.findall(r'data-i18n-placeholder="([^"]+)"', html))
refs |= set(re.findall(r"t\('([^']+)'\)", html))
# Filter false positives (JavaScript keywords, short strings)
false_positives = {"input", "true", "false", "null", "undefined", "length"}
missing = sorted(r for r in refs if r not in en and r not in false_positives)
if missing:
for k in missing:
fail(f"HTML ref missing from EN: {k}")
else:
ok(f"{len(refs)} HTML refs, all in EN")
# 7. Summary
print(f"\n=== Summary ===")
if EXIT_CODE == 0:
print("ALL CHECKS PASSED — i18n production-ready")
else:
print(f"{EXIT_CODE} issue(s) found")
sys.exit(EXIT_CODE)
if __name__ == "__main__":
main()