Files
scarf/tools/merge-translations.py
Alan Wizemann 1726a613a5 feat(i18n): add translations for zh-Hans, de, fr, es, ja, pt-BR
Ships first-pass AI translations for six locales on top of the existing
English base, plus a simple JSON-per-locale contributor workflow so new
languages can land as a single PR.

- 518 keys translated per locale (proper nouns / brand names / format-
  only strings left to fall back to English by design — see the
  "Non-blocking (intentional verbatim)" section of scarf/docs/I18N.md).
- Per-locale source-of-truth lives in tools/translations/<locale>.json;
  tools/merge-translations.py writes them into Localizable.xcstrings
  and is idempotent (re-runnable as translators iterate).
- InfoPlist.xcstrings (macOS microphone permission prompt) translated
  for all six locales.
- knownRegions expanded: zh-Hans, de, fr now join by es, ja, pt-BR.
- CONTRIBUTING.md gains an "Adding a Language" section documenting the
  fork → JSON → merge → PR flow. Native-speaker reviews welcome.

Closes #13 (the original ask: Simplified Chinese support).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 18:16:41 -07:00

91 lines
2.7 KiB
Python

#!/usr/bin/env python3
"""
Merge per-locale translation JSON files into Localizable.xcstrings.
Each JSON under tools/translations/<locale>.json is a flat
{ "English source key": "Translation" } map. Keys absent from the JSON
fall through to English at runtime — that's the desired behavior for
proper nouns, format-only strings, and technical terminology.
Usage:
python3 tools/merge-translations.py
Re-runnable: rewrites the per-locale stringUnit entries each time, so
translators can iterate on a JSON and re-merge.
"""
from __future__ import annotations
import json
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
CATALOG = REPO_ROOT / "scarf" / "scarf" / "Localizable.xcstrings"
TRANSLATIONS_DIR = REPO_ROOT / "tools" / "translations"
LOCALES = ["zh-Hans", "de", "fr", "es", "ja", "pt-BR"]
def load_json(path: Path) -> dict:
with path.open("r", encoding="utf-8") as f:
return json.load(f)
def save_json(path: Path, data: dict) -> None:
with path.open("w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2, sort_keys=True)
f.write("\n")
def main() -> int:
catalog = load_json(CATALOG)
source_keys = set(catalog.get("strings", {}).keys())
applied: dict[str, int] = {}
skipped_unknown: dict[str, list[str]] = {}
for locale in LOCALES:
path = TRANSLATIONS_DIR / f"{locale}.json"
if not path.exists():
print(f"[skip] {locale}: no file at {path}")
continue
translations = load_json(path)
applied[locale] = 0
skipped_unknown[locale] = []
for source, target in translations.items():
if source not in source_keys:
skipped_unknown[locale].append(source)
continue
entry = catalog["strings"].setdefault(source, {})
entry.setdefault("localizations", {})
entry["localizations"][locale] = {
"stringUnit": {
"state": "translated",
"value": target,
}
}
applied[locale] += 1
save_json(CATALOG, catalog)
# Summary
print("Merge summary:")
for locale in LOCALES:
if locale in applied:
extras = len(skipped_unknown.get(locale, []))
print(f" {locale:8} applied={applied[locale]:4} unknown-keys-skipped={extras}")
any_unknown = any(skipped_unknown.values())
if any_unknown:
print("\nKeys present in translation files but missing from the catalog:")
for locale, unknowns in skipped_unknown.items():
for k in unknowns:
print(f" [{locale}] {k!r}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())