iOS localization with String Catalogs

A working guide to .xcstrings — the format, its sharp edges, and how to translate a catalog into 100+ languages without breaking plurals or losing your Chinese localization.

Updated 2026-08-21

Xcode 15 replaced the Localizable.strings and .stringsdict pair with the String Catalog: a single .xcstrings file holding every key, every language, and plurals together. It is JSON, it diffs cleanly, and Xcode keeps it in sync with your source on each build. Migrating is a right-click on Localizable.strings and Migrate to String Catalog; no code changes are needed, because NSLocalizedString, Text() and String(localized:) all keep working.

Three fields decide how Xcode treats an entry. sourceLanguage declares which localization is the original. extractionState records where a key came from: extracted_with_value means Xcode found it in code, manual means you added it by hand, and stale means it no longer exists in source. Each stringUnit also carries a state of new, needs_review or translated, and tooling that writes translations without setting translated leaves the catalog looking permanently unfinished.

Plurals live in variations.plural with one unit per CLDR category, which is what replaced .stringsdict. The categories belong to the target language, not the source string: English needs one and other, Russian needs one, few, many and other, Japanese needs only other. Copying the English categories into every locale produces grammatically wrong output.

The sharpest edge is Chinese. Apple keys those localizations by script — zh-Hans and zh-Hant — while most translation tooling normalises Chinese to the region forms zh-CN and zh-TW. A tool looking up zh-CN never finds the catalog's existing zh-Hans block, so Simplified Chinese reads as untranslated and is re-translated in full on every run, while the output is written to a key Xcode ignores. Everything reports success and nothing ships.

Namespaced keys are a related trap: settings and settings.title are unrelated entries, so inferring plural structure from keys that share a prefix writes a bogus plural variation Xcode cannot render. Plural-ness is declared in the source localization, never inferred from the shape of a key. Device variations sit beside stringUnit in the same object, so a merge that replaces a localization wholesale silently deletes them.

Explore Key Pages

Related Reading