mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 10:36:35 +00:00
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>
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user