#!/usr/bin/env python3 """ Synchronise locales/*.json avec locales/en.json (référence des clés). - Garde les chaînes déjà présentes et non vides dans la locale cible. - Traduit les clés manquantes (Google via deep-translator). Usage : depuis memento-note/ avec le venv : .venv-i18n/bin/python scripts/sync_locales_from_en.py """ from __future__ import annotations import json import sys import time from pathlib import Path from deep_translator import GoogleTranslator ROOT = Path(__file__).resolve().parents[1] LOCALES = ROOT / "locales" # Codes Google Translate pour deep-translator (source=en) LANG_TARGETS = { "fr": "fr", "es": "es", "de": "de", "fa": "fa", "it": "it", "pt": "pt", "ru": "ru", "zh": "zh-CN", "ja": "ja", "ko": "ko", "ar": "ar", "hi": "hi", "nl": "nl", "pl": "pl", } def flatten_leaves(obj: dict, prefix: str = "") -> dict[str, str]: out: dict[str, str] = {} for k, v in obj.items(): path = f"{prefix}.{k}" if prefix else k if isinstance(v, dict): out.update(flatten_leaves(v, path)) elif isinstance(v, str): out[path] = v else: raise TypeError(f"Valeur non supportée à {path}: {type(v)}") return out def unflatten_leaves(flat: dict[str, str]) -> dict: root: dict = {} for path, val in flat.items(): parts = path.split(".") cur = root for p in parts[:-1]: cur = cur.setdefault(p, {}) cur[parts[-1]] = val return root def translate_unique(texts: list[str], target_code: str, batch_size: int = 35) -> dict[str, str]: translator = GoogleTranslator(source="en", target=target_code) mapping: dict[str, str] = {} for i in range(0, len(texts), batch_size): batch = texts[i : i + batch_size] try: outs = translator.translate_batch(batch) except Exception as e: print(f" batch erreur ({e}), traduction unitaire…", flush=True) outs = [] for t in batch: try: outs.append(translator.translate(t)) except Exception: outs.append(t) time.sleep(0.15) if len(outs) != len(batch): outs = batch # fallback for src, dst in zip(batch, outs): mapping[src] = dst if isinstance(dst, str) else src time.sleep(0.6) print(f" … {min(i + batch_size, len(texts))}/{len(texts)}", flush=True) return mapping def merge_locale(en_flat: dict[str, str], loc_flat: dict[str, str], target_code: str) -> dict[str, str]: text_to_keys: dict[str, list[str]] = {} result: dict[str, str] = {} for key, en_val in en_flat.items(): loc_val = loc_flat.get(key) if isinstance(loc_val, str) and loc_val.strip(): result[key] = loc_val else: text_to_keys.setdefault(en_val, []).append(key) if not text_to_keys: return result unique = list(text_to_keys.keys()) print(f" {len(unique)} textes uniques à traduire ({sum(len(v) for v in text_to_keys.values())} clés)", flush=True) trans_map = translate_unique(unique, target_code) for src, keys in text_to_keys.items(): tr = trans_map.get(src, src) for k in keys: result[k] = tr return result def main() -> int: en_path = LOCALES / "en.json" if not en_path.exists(): print("locales/en.json introuvable", file=sys.stderr) return 1 en_obj = json.loads(en_path.read_text(encoding="utf-8")) en_flat = flatten_leaves(en_obj) print(f"Référence en.json : {len(en_flat)} clés feuilles", flush=True) skip = {"en"} for code, google_target in LANG_TARGETS.items(): if code in skip: continue path = LOCALES / f"{code}.json" if not path.exists(): print(f"Absence de {path.name}, ignoré", flush=True) continue loc_obj = json.loads(path.read_text(encoding="utf-8")) loc_flat = flatten_leaves(loc_obj) before_missing = sum(1 for k in en_flat if k not in loc_flat or not str(loc_flat.get(k, "")).strip()) if before_missing == 0: print(f"\n=== {code}.json — déjà complet ({len(en_flat)} clés), rien à faire", flush=True) continue print(f"\n=== {code}.json ({google_target}) — manquantes avant: {before_missing}", flush=True) merged_flat = merge_locale(en_flat, loc_flat, google_target) # Couverture complète des clés en out_flat = dict(en_flat) out_flat.update(merged_flat) missing_after = sum(1 for k in en_flat if k not in out_flat or not str(out_flat[k]).strip()) if missing_after: print(f"ERREUR: encore {missing_after} clés vides pour {code}", file=sys.stderr) return 1 tree = unflatten_leaves(out_flat) path.write_text(json.dumps(tree, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") print(f" Écrit {path.name}", flush=True) print("\nTerminé.", flush=True) return 0 if __name__ == "__main__": raise SystemExit(main())