This guide describes exactly how to add missing machine translations for a locale in Localizable.xcstrings using local-localizer — an on-device Apple Intelligence tool that requires no API keys or network access.
git checkout main && git pull
git checkout -b translations/<locale-name>
# e.g. git checkout -b translations/chinese-traditional
Run this script to count how many strings are missing or in needs_review for the target locale:
python3 - <<'EOF'
import json
LANG = "zh-Hant-TW" # <-- change to your target locale
with open("Localizable.xcstrings", encoding="utf-8") as f:
data = json.load(f)
strings = data.get("strings", {})
missing, total = [], 0
for key, val in strings.items():
if not key:
continue
total += 1
locs = val.get("localizations", {})
if LANG not in locs:
missing.append(key)
else:
loc = locs[LANG]
if "stringUnit" in loc and loc["stringUnit"].get("state") not in ("translated",):
missing.append(key)
print(f"Total keys : {total}")
print(f"Missing : {len(missing)}")
EOF
Before generating new strings it is important to understand the register and style of what is already there. Run:
python3 - <<'EOF'
import json
LANG = "zh-Hant-TW"
with open("Localizable.xcstrings", encoding="utf-8") as f:
data = json.load(f)
samples = []
for key, val in data["strings"].items():
if not key:
continue
locs = val.get("localizations", {})
if LANG in locs:
unit = locs[LANG].get("stringUnit", {})
if unit.get("state") == "translated":
samples.append((key, unit.get("value", "")))
if len(samples) >= 30:
break
for k, v in samples:
print(f"EN: {k}")
print(f"{LANG}: {v}\n")
EOF
Use the output to decide the appropriate --tone value (professional, formal, informal, polite, neutral).
Create scripts/<locale>-glossary.json to:
Example for zh-Hant-TW:
{
"doNotTranslate": [
"Meshtastic",
"LoRa",
"MQTT",
"BLE",
"GPS",
"TAK",
"CoT",
"PKC",
"SNR",
"GPIO",
"DFU",
"API",
"Wi-Fi",
"Bluetooth",
"IAQ",
"ATAK",
"WinTAK",
"mPWRD-OS",
"iOS",
"macOS",
"CarPlay",
"SiriKit"
],
"tone": {
"default": "professional",
"zh-Hant-TW": "professional"
}
}
Adjust the doNotTranslate list and tone based on the locale. For European languages, formal selects the polite second-person form (Sie/vous/Lei/usted). For Japanese/Korean, use polite.
git clone https://github.com/JoshuaSullivan/local-localizer.git ~/Source/local-localizer
cd ~/Source/local-localizer
swift build -c release
mkdir -p ~/bin
cp .build/release/local-localizer ~/bin/local-localizer
Make sure
~/binis on yourPATH, or call the binary with its full path.
Always verify the work plan before translating:
cd /path/to/Meshtastic-Apple
~/bin/local-localizer Localizable.xcstrings \
--locales zh-Hant-TW \
--glossary scripts/zh-Hant-TW-glossary.json \
--dry-run
Check that:
~/bin/local-localizer Localizable.xcstrings \
--locales zh-Hant-TW \
--glossary scripts/zh-Hant-TW-glossary.json
Do not pass
--state translated. The default state isneeds_review, which is correct for machine translations — it signals to native speakers that the strings need human review before shipping.
This runs entirely on-device using Apple Intelligence. Expect roughly 1–2 seconds per string. For ~500 strings, allow 10–20 minutes. The process writes results to the file incrementally so it is safe to interrupt and resume.
After completion, verify full coverage and that all new strings are needs_review:
python3 - <<'EOF'
import json
LANG = "zh-Hant-TW"
with open("Localizable.xcstrings", encoding="utf-8") as f:
data = json.load(f)
missing, needs_review, translated = [], 0, 0
for key, val in data["strings"].items():
if not key:
continue
locs = val.get("localizations", {})
if LANG not in locs:
missing.append(key)
else:
loc = locs[LANG]
if "stringUnit" in loc:
state = loc["stringUnit"].get("state")
if state == "translated":
translated += 1
elif state == "needs_review":
needs_review += 1
else:
missing.append(key)
elif "variations" in loc:
translated += 1 # plural variations are always fine
print(f"Confirmed translated : {translated}")
print(f"Needs review : {needs_review}")
print(f"Still missing : {len(missing)}")
if missing:
print("Missing keys:", missing)
EOF
Expected output: Still missing: 0. If any keys remain, add them manually (common for plural-only variation entries or strings containing unsupported characters).
Confirm that the pre-existing translated strings were not downgraded to needs_review by comparing against the previous commit:
python3 - <<'EOF'
import json, subprocess
LANG = "zh-Hant-TW"
old = json.loads(subprocess.check_output(["git", "show", "HEAD:Localizable.xcstrings"]))
with open("Localizable.xcstrings", encoding="utf-8") as f:
new = json.load(f)
regressions = []
for key, val in old["strings"].items():
locs = val.get("localizations", {})
if LANG in locs:
old_state = locs[LANG].get("stringUnit", {}).get("state")
new_state = new["strings"].get(key, {}).get("localizations", {}).get(LANG, {}).get("stringUnit", {}).get("state")
if old_state == "translated" and new_state != "translated":
regressions.append(key)
print(f"Regressions (translated → other): {len(regressions)}")
if regressions:
print(regressions[:20])
EOF
git add Localizable.xcstrings scripts/<locale>-glossary.json
git commit -m "Add <Language> (<locale>) localisation strings
Add <N> missing <Language> translations to Localizable.xcstrings covering
all user-visible strings. Translations generated using local-localizer with
Apple on-device Foundation Models. All machine-translated strings are marked
needs_review for native speaker review."
git push -u origin translations/<locale-name>
gh pr create --base main \
--title "Add <Language> (<locale>) localisation strings" \
--body "..."
needs_reviewneeds_review state in Xcode flags the strings for review before shipping.%lld generating one/other/few etc.) are handled automatically by local-localizer for .xcstrings files. Chinese/Japanese/Korean only need other.--overwrite unless you specifically want to re-translate already-confirmed translated strings.%@, %lld, %d) are preserved by local-localizer's built-in placeholder protection.