#!/usr/bin/env python3 """Füllt Locale-JSONs via deep-translator (Fallback wenn MyMemory/Google limitiert).""" from __future__ import annotations import argparse import json import re import sys import time from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout from pathlib import Path from deep_translator import GoogleTranslator ROOT = Path(__file__).resolve().parents[1] LOCALES_DIR = ROOT / "frontend" / "src" / "i18n" / "locales" CACHE_FILE = Path(__file__).resolve().parent / ".i18n-translate-cache.json" LOCALE_TARGETS = { "fr": "fr", "es": "es", "it": "it", "pl": "pl", "ja": "ja", "zh": "zh-CN", "th": "th", "tl": "tl", "fil": "tl", } SKIP = {"de", "de-extended", "de-CH", "en-US", "en-GB", "en-AU"} FILL_ORDER = ["fr", "es", "it", "pl", "ja", "zh", "th", "tl", "fil"] PLACEHOLDER_RE = re.compile(r"\{[^}]+\}") def deep_merge(base, override): if not isinstance(base, dict) or isinstance(base, list): return override if override is not None else base result = dict(base) for key, value in (override or {}).items(): if ( isinstance(value, dict) and not isinstance(value, list) and isinstance(result.get(key), dict) and not isinstance(result.get(key), list) ): result[key] = deep_merge(result[key], value) else: result[key] = value return result def flatten(obj, prefix=""): out = {} for key, value in (obj or {}).items(): next_key = f"{prefix}.{key}" if prefix else key if isinstance(value, dict) and not isinstance(value, list): out.update(flatten(value, next_key)) elif isinstance(value, str): out[next_key] = value return out def set_by_path(obj, dot_path, value): parts = dot_path.split(".") cur = obj for part in parts[:-1]: if part not in cur or not isinstance(cur[part], dict): cur[part] = {} cur = cur[part] cur[parts[-1]] = value def build_overrides(de_flat, target_flat): out = {} for key, value in target_flat.items(): if value != de_flat.get(key): set_by_path(out, key, value) return out def protect_placeholders(text): tokens = [] def repl(m): token = f"__PH{len(tokens)}__" tokens.append(m.group(0)) return token return PLACEHOLDER_RE.sub(repl, text), tokens def restore_placeholders(text, tokens): out = text for i, token in enumerate(tokens): out = out.replace(f"__PH{i}__", token) return out def load_cache(): if CACHE_FILE.exists(): return json.loads(CACHE_FILE.read_text(encoding="utf-8")) return {} def save_cache(cache): CACHE_FILE.write_text(json.dumps(cache, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def fill_de_ch(de_flat, merged_flat): for key in de_flat: if merged_flat.get(key) == de_flat[key]: merged_flat[key] = de_flat[key].replace("ß", "ss") def translate_text(translator, cache, text, from_lang, to_lang, delay, timeout=20): cache_key = f"{from_lang}|{to_lang}|{text}" if cache_key in cache: return cache[cache_key] safe, tokens = protect_placeholders(text) def _call(): return translator.translate(safe) last_err = None for attempt in range(3): try: with ThreadPoolExecutor(max_workers=1) as pool: raw = pool.submit(_call).result(timeout=timeout) out = restore_placeholders(raw, tokens) cache[cache_key] = out time.sleep(delay) return out except (FuturesTimeout, Exception) as e: last_err = e time.sleep(2 + attempt * 2) raise last_err def fill_locale(code, en_flat, cache, delay, dry_run): target = LOCALE_TARGETS[code] de = json.loads((LOCALES_DIR / "de.json").read_text(encoding="utf-8")) de_flat = flatten(de) locale_path = LOCALES_DIR / f"{code}.json" locale_json = json.loads(locale_path.read_text(encoding="utf-8")) merged_flat = flatten(deep_merge(json.loads(json.dumps(de)), locale_json)) if code == "de-CH": fill_de_ch(de_flat, merged_flat) else: keys_to_fix = [k for k in de_flat if merged_flat.get(k) == de_flat[k]] unique = {} for k in keys_to_fix: from_lang = "en" if en_flat.get(k) and en_flat[k] != de_flat[k] else "de" text = en_flat[k] if from_lang == "en" else de_flat[k] unique.setdefault((from_lang, text), []).append(k) print(f"[{code}] {len(keys_to_fix)} keys, {len(unique)} unique → {target}", flush=True) by_source = {"en": [], "de": []} for (from_lang, text), keys in unique.items(): by_source[from_lang].append((text, keys)) done = 0 for from_lang in ("en", "de"): items = by_source[from_lang] if not items: continue translator = GoogleTranslator(source=from_lang, target=target) for text, keys in items: cache_key = f"{from_lang}|{target}|{text}" try: if dry_run: translated = f"[{target}] {text[:30]}" elif cache_key in cache: translated = cache[cache_key] else: translated = translate_text(translator, cache, text, from_lang, target, delay) for k in keys: merged_flat[k] = translated done += 1 if done % 50 == 0: print(f"[{code}] {done}/{len(unique)}", flush=True) save_cache(cache) except Exception as e: print(f"[{code}] skip: {text[:40]}… ({e})", file=sys.stderr, flush=True) if from_lang == "en" and en_flat: for k in keys: merged_flat[k] = text save_cache(cache) overrides = build_overrides(de_flat, merged_flat) if not dry_run: locale_path.write_text( json.dumps(overrides, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" ) still_de = sum(1 for k in de_flat if merged_flat.get(k) == de_flat[k]) print(f"[{code}] overrides={len(flatten(overrides))}, stillDe={still_de}", flush=True) def main(): parser = argparse.ArgumentParser() parser.add_argument("--locale") parser.add_argument("--all", action="store_true") parser.add_argument("--from-locale", dest="from_locale", help="Bei --all: ab dieser Locale fortsetzen") parser.add_argument("--delay", type=float, default=0.35) parser.add_argument("--dry-run", action="store_true") args = parser.parse_args() codes = FILL_ORDER if args.all else [args.locale] if args.locale else None if codes and args.from_locale: if args.from_locale not in FILL_ORDER: parser.error(f"Unknown locale: {args.from_locale}") codes = FILL_ORDER[FILL_ORDER.index(args.from_locale) :] if not codes: parser.error("Usage: --locale | --all") de = json.loads((LOCALES_DIR / "de.json").read_text(encoding="utf-8")) en_us = json.loads((LOCALES_DIR / "en-US.json").read_text(encoding="utf-8")) en_flat = flatten(deep_merge(de, en_us)) cache = load_cache() for code in codes: if code in SKIP or code not in LOCALE_TARGETS: continue fill_locale(code, en_flat, cache, args.delay, args.dry_run) time.sleep(5) save_cache(cache) print("Done.") if __name__ == "__main__": main()