A macOS command-line tool that machine-translates an iOS/macOS String Catalog (.xcstrings) or legacy .strings file into multiple locales using the on-device Apple Foundation Models framework.
Translations run entirely on-device. No API keys, no network, no per-call cost.
Disclaimer. Translations are produced by an AI model (Apple's on-device Foundation Models). They will contain errors: wrong words, awkward phrasing, missed nuance, broken format specifiers, mistranslated brand names. The rate of error varies by language, domain, and prompt. Always have a native speaker review the output before shipping. The author of this tool makes no representations or warranties about the correctness, fitness, or safety of any translation it produces and accepts no responsibility for any consequence of using its output. By running this tool you agree that you are responsible for reviewing and validating every translation it generates. See the LICENSE file for the full disclaimer of warranties.
- macOS 26 or later
- Apple Silicon
- Apple Intelligence enabled (System Settings → Apple Intelligence & Siri)
- Xcode 26 toolchain (for building from source)
swift build -c release
cp .build/release/local-localizer /usr/local/bin/Or use SwiftPM's installer (puts it in ~/.swiftpm/bin, which you'll need on PATH):
swift package experimental-installlocal-localizer <input> [options]
In-place modification of the input file. All locales live in the same JSON file.
local-localizer Resources/Localizable.xcstrings
local-localizer Resources/Localizable.xcstrings --locales fr,de,ja
local-localizer Resources/Localizable.xcstrings --output /tmp/translated.xcstringsPer-locale outputs are written to sibling <locale>.lproj/ directories next to the input.
local-localizer Project/en.lproj/Localizable.strings
# produces Project/fr.lproj/Localizable.strings, Project/de.lproj/..., etc.The input must live inside an .lproj directory. Source language is inferred from the parent directory name (en.lproj → en, Base.lproj → en); pass --source-locale to override.
| Flag | Default | Purpose |
|---|---|---|
<input> |
— (required) | Path to a .xcstrings or .strings file |
--locales |
the nine defaults below | Comma-separated locale identifiers |
--source-locale |
inferred | Source language for legacy .strings (ignored for .xcstrings) |
--overwrite |
off | Re-translate keys even if a translation already exists |
--output |
in-place | .xcstrings only: write to a different path |
--temperature |
0.2 |
Sampling temperature, 0.0–2.0 |
--dry-run |
off | Print the work plan, don't call the model or write files |
-v, --verbose |
off | Include source prompts in the progress log |
--glossary |
none | Path to a JSON glossary file (see below) |
--tone |
none | Default tone: formal, informal, neutral, professional, polite |
--state |
needs_review |
State to write into new translations: translated or needs_review |
--check |
off | Validate only — exit 0 if everything is current, 1 if anything is missing or needs review |
--keys |
all | Comma-separated list of keys to translate (others ignored) |
--keys-from |
none | Path to a newline-separated list of keys to translate |
--concurrency |
min(9, locale count) | Maximum simultaneous in-flight model calls |
| Display name | Identifier |
|---|---|
| French | fr |
| German | de |
| Spanish | es |
| Italian | it |
| Brazilian Portuguese | pt-BR |
| Simplified Chinese | zh-Hans |
| Traditional Chinese | zh-Hant |
| Japanese | ja |
| Korean | ko |
A JSON file with three optional sections:
{
"doNotTranslate": ["Microsoft Teams", "OneDrive", "Flipgrid"],
"tone": {
"default": "professional",
"de": "formal",
"fr": "formal",
"ja": "polite"
},
"termMappings": {
"de": { "Premium": "Pro" },
"fr": { "Sign in": "Se connecter" }
}
}doNotTranslate— terms (typically brand and product names) that must appear verbatim in every translation.tone— per-locale tone preference. Thedefaultkey applies to any locale not explicitly listed. Per-locale entries override the global--toneflag. For German/French/Italian/Spanish,formalselects the polite second-person form (Sie/vous/Lei/usted). For Japanese/Korean,politeselects the polite verb forms.termMappings— per-locale forced translations. When the source contains the left-hand term, the model is instructed to render it as the right-hand term in that locale.
.xcstrings plural-variation entries (auto-generated by Xcode for any source string with %lld) are translated into the correct CLDR plural categories per target locale:
- French / Spanish / Italian / Portuguese:
one,many,other - German:
one,other - Chinese / Japanese / Korean:
otheronly - Russian:
one,few,many,other - Arabic:
zero,one,two,few,many,other
Locales not in the hardcoded table fall back to one/other with a warning.
Format specifiers (%@, %lld, etc.) are preserved across all plural forms — every translation keeps the literal placeholder.
.xcstrings translations are written with a state field. v2 honors all four states:
translated— current; skipped on subsequent runsneeds_review— re-translated on next run (Xcode marks this when source changes)newor absent — translatedstale— skipped silently
New machine translations default to state: "needs_review" to honestly reflect that they should be reviewed before shipping. Pass --state translated to opt back to v1 behavior.
For legacy .strings, already-translated keys are skipped by file presence (no state field). Pass --overwrite to force re-translation.
Validate before shipping (CI use):
local-localizer Resources/Localizable.xcstrings --check && echo "ready to ship"Translate only specific keys after a code review changed their wording:
local-localizer Resources/Localizable.xcstrings --keys onboarding_title,onboarding_subtitle --overwrite --state translatedTranslate with brand-name preservation and formal European tone:
local-localizer Resources/Localizable.xcstrings --glossary glossary.json --locales de,fr,it,esFaster runs against many locales:
local-localizer Resources/Localizable.xcstrings --concurrency 9- Hand-review the output. These are machine translations. Default state is
needs_reviewfor exactly this reason — flip totranslatedonly after a native speaker confirms. - Plural support is
.xcstrings-only. Legacy.stringsdictplural files are out of scope. devicevariations andsubstitutionsin.xcstringsare skipped with a warning.- Multi-line sources may collapse to a single line in some target languages — the on-device model occasionally prefers a single sentence over preserving exact line layout.
- Comments matter. The developer comment is sent to the model as disambiguation context. Strings like "Open" or "Mark" translate noticeably better with a comment like "Verb, button label" than without.
- Concurrency speedup is modest (~15-25% in practice). The on-device model serializes much of its inference, so adding parallel calls helps less than it would against a cloud API.