mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-08 02:14:37 +00:00
1726a613a5
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>
91 lines
2.7 KiB
Python
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())
|