Translate Xcode String Catalogs (.xcstrings)
A String Catalog holds every language in one JSON file that only Xcode should be editing by hand. KAERIS adds the new languages to that same file and leaves everything else in it as it was.
How a String Catalog is built
One file, strings, then the key, then localizations, then the language. Each language holds a stringUnit or variations for plurals. The file also carries comment, extractionState and shouldTranslate. A key with no entry for the source language is its own source text.
{
"sourceLanguage" : "en",
"strings" : {
"Welcome back, %@!" : {
"comment" : "Greeting on the home screen",
"extractionState" : "manual",
"localizations" : {
"en" : { "stringUnit" : { "state" : "translated", "value" : "Welcome back, %@!" } }
}
},
"%lld items" : {
"localizations" : {
"en" : {
"variations" : {
"plural" : {
"one" : { "stringUnit" : { "state" : "translated", "value" : "%lld item" } },
"other" : { "stringUnit" : { "state" : "translated", "value" : "%lld items" } }
}
}
}
}
},
"Cancel" : { }
},
"version" : "1.0"
}
What to watch for in .xcstrings translations
Do not read it as plain JSON
Flattened as generic JSON it becomes keys like
strings.welcome.localizations.en…, which looks translated and which Xcode cannot read. KAERIS has its own reader for the format.Plural variations per language
English has
oneandother; Russian also needsfewandmany, Arabic all six. The README describes the expansion;%lldstays where it was.What Xcode wrote must stay
Comments, extraction states, languages already in the file and anything marked
shouldTranslate: falseare carried through untouched.Format specifiers
%@,%lld,%1$@are compared with the source after translation.
A real run: translated to German and checked
A real run, 27 October 2026, nothing edited by hand: kaeris translate Localizable.xcstrings --langs de with kaeris 0.2.35, 2.4 seconds, model openai/gpt-4.1-mini (the service's fallback model answered this job). Below, the German file it wrote and what kaeris check says about it.
{
"sourceLanguage": "en",
"strings": {
"Welcome back, %@!": {
"comment": "Greeting on the home screen",
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Welcome back, %@!"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Willkommen zurück, %@!"
}
}
}
},
"%lld items": {
"localizations": {
"en": {
"variations": {
"plural": {
"one": {
"stringUnit": {
"state": "translated",
"value": "%lld item"
}
},
"other": {
"stringUnit": {
"state": "translated",
"value": "%lld items"
}
}
}
}
},
"de": {
"variations": {
"plural": {
"one": {
"stringUnit": {
"state": "translated",
"value": "%lld Artikel"
}
},
"other": {
"stringUnit": {
"state": "translated",
"value": "%lld Artikel"
}
}
}
}
}
}
},
"Cancel": {
"localizations": {
"de": {
"stringUnit": {
"state": "translated",
"value": "Abbrechen"
}
}
}
}
},
"version": "1.0"
}
$ kaeris check --source Localizable.xcstrings --langs de ✓ de: complete & placeholder-safe ✓ all locales complete & placeholder-safe
What KAERIS does with it
One file, updated in place
kaeris translate Localizable.xcstrings --langs de,fr,jawrites the languages into the catalog. There is no file per language.Plurals as ICU for the model
In the check, a plural variation is read as an ICU plural, so a lost form is visible.
Free check for this format
The free check on /check reads this format and runs without a key or an account. The terminal command
kaeris checkreads it too: it takes all 13 formats since kaeris 0.2.35, with the same rules and nothing sent anywhere.
Run it in three steps
Install the CLI
Run
pip install kaeris.Translate the catalog
Run
kaeris translate Localizable.xcstrings --langs de,fr,ja. The catalog is updated in place.Check the result
Upload the catalog on the free check page; it reads source and translations from the one file.
pip install kaeris kaeris translate Localizable.xcstrings --langs de,fr,ja --verify
Questions
Does it overwrite my existing translations?
Languages already in the file, comments and extraction states are carried through untouched, as the CLI README says.
How are plurals handled?
Plural variations are expanded to the categories each language needs: English one/other becomes one/few/many/other in Russian and six forms in Arabic.
Is it free?
Yes, without an account: 10,000 characters per file and up to 8 languages per run. A free API key raises the file limit to 50,000 characters. Lifetime is $79 once, on your own OpenRouter key; monthly plans start at $19. Pricing
Can I run it from the terminal or in CI?
Yes. pip install kaeris, then kaeris translate Localizable.xcstrings --langs de,ja. See also iOS. The GitHub Action runs the same translation in a pull request. CLI docs · GitHub Action
Keep going
CLI reference · Check reference · Translate in the browser · Free check · Convert formats · Pseudo-localization
Other formats: JSON · YAML · .po · ARB · XLIFF · .strings · .properties · .resx · CSV · Markdown · Fluent · Android XML