Initial commit: 25 scripts Python sauvegardés
Date: 2026-08-28 11:43:34 EDT
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
# i18n Audit Scripts
|
||||
|
||||
Helper Python scripts for auditing ObsiGate i18n coverage.
|
||||
|
||||
## audit_french_in_html.py
|
||||
|
||||
Scans index.html for French text without data-i18n:
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
html = Path("frontend/index.html").read_text(encoding="utf-8")
|
||||
lines = html.split("\n")
|
||||
|
||||
for i, line in enumerate(lines, 1):
|
||||
if 'data-i18n' in line:
|
||||
continue
|
||||
# Find text between tags with French chars
|
||||
texts = re.findall(r'>([^<>{}\n]{4,80})<', line)
|
||||
for t in texts:
|
||||
if any(c in t for c in 'éèêëàâùûôîïçÉÈÊËÀ'):
|
||||
print(f"L{i}: {t.strip()[:120]}")
|
||||
```
|
||||
|
||||
## audit_en_fr_mismatch.py
|
||||
|
||||
Finds untranslated keys (EN == FR) and French chars in EN values:
|
||||
|
||||
```python
|
||||
import json
|
||||
|
||||
en = json.load(open("frontend/locales/en.json"))
|
||||
fr = json.load(open("frontend/locales/fr.json"))
|
||||
|
||||
identical = []
|
||||
french_in_en = []
|
||||
|
||||
for k in en:
|
||||
ev = en[k]; fv = fr.get(k, "")
|
||||
if isinstance(ev, str) and isinstance(fv, str):
|
||||
if ev == fv and len(ev) > 3 and not ev.startswith('<'):
|
||||
identical.append((k, ev))
|
||||
if any(c in ev for c in 'éèêëàâùûôîïçÉÈÊËÀ'):
|
||||
french_in_en.append((k, ev))
|
||||
|
||||
print(f"Identical EN=FR: {len(identical)}")
|
||||
print(f"French chars in EN: {len(french_in_en)}")
|
||||
```
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user