Initial commit: 25 scripts Python sauvegardés

Date: 2026-08-28 11:43:34 EDT
This commit is contained in:
2026-08-28 11:43:34 -04:00
parent 0ba9701e0f
commit 994ea86d47
204 changed files with 70104 additions and 2 deletions
@@ -0,0 +1,81 @@
"""
Auto-tag index.html with data-i18n attributes.
Reads the frontend/locales/fr.json file to build a reverse map
(French text → locale key), then scans index.html for elements
with French text content and inserts data-i18n="key" attributes.
Usage: python3 i18n-auto-tag.py
Run from the ObsiGate project root.
"""
import re, json
with open('frontend/locales/fr.json', 'r', encoding='utf-8') as f:
fr = json.load(f)
# Build reverse map: French text → key (prefer shorter keys for duplicates)
text_to_key = {}
for key, val in fr.items():
if key.startswith('_'): continue
if val not in text_to_key or len(key) < len(text_to_key[val]):
text_to_key[val] = key
with open('frontend/index.html', 'r', encoding='utf-8') as f:
html = f.read()
before = len(re.findall(r'data-i18n(?:-[a-z]+)?=\"', html))
# Tags to scan for text content
TAGS = 'span|p|h[1-6]|label|button|a|li|td|th|strong|summary|legend|caption|dd|dt|small|em|b|u|div|option|figcaption|blockquote'
def replace_match(m):
tag = m.group(1)
attrs = m.group(2)
text = m.group(3).strip()
full_inner = m.group(3)
if 'data-i18n=' in attrs:
return m.group(0)
key = text_to_key.get(text)
if not key:
return m.group(0)
if attrs.strip():
new_open = f'<{tag} {attrs.strip()} data-i18n="{key}">'
else:
new_open = f'<{tag} data-i18n="{key}">'
return f'{new_open}{full_inner}</{tag}>'
pattern = rf'<({TAGS})([^>]*)>([^<{{]{{3,300}})</\1>'
html = re.sub(pattern, replace_match, html)
# Also add data-i18n-placeholder for inputs
def replace_placeholder(m):
attrs = m.group(1)
text = m.group(2)
if 'data-i18n-placeholder=' in attrs or 'data-i18n=' in attrs:
return m.group(0)
key = text_to_key.get(text)
if not key:
return m.group(0)
return f'<input{attrs} data-i18n-placeholder="{key}" placeholder="{text}"'
placeholder_pattern = r'<input([^>]*?)placeholder="([^"]{3,120})"'
html = re.sub(placeholder_pattern, replace_placeholder, html)
after = len(re.findall(r'data-i18n(?:-[a-z]+)?=\"', html))
# Verify no concatenated attributes
bad = re.findall(r'<[a-z]+data-i18n=', html)
if bad:
print(f'WARNING: {len(bad)} concatenated attributes found!')
for b in bad[:5]:
print(f' {b}')
with open('frontend/index.html', 'w', encoding='utf-8') as f:
f.write(html)
print(f'data-i18n attributes: {before} → {after} (+{after - before})')
print(f'Concatenated: {len(bad)}')
@@ -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()