Localize Your App

iOS App Localization: The Complete Guide to Xcode String Catalogs

Apple’s current localization path is a string catalog. Add a .xcstrings file, turn on Use Compiler to Extract Swift Strings, build so Xcode populates the catalog, add a language, then generate translations. Catalogs shipped in Xcode 15. Export Localizations can pull localized content from code, string catalogs, Interface Builder files, and localized assets. That is the happy path. The rest of this page is what that path does not say.

Recommended setup

Put user-facing text through a localizable API at the call site Xcode can see. For Swift that means String(localized:) and the SwiftUI surfaces the compiler already extracts when the setting above is on: Text, and AttributedString(localized:) interpolations. NSLocalizedString is still a valid lookup. It is not what the catalog workflow is built around. Compiler extraction plus String(localized:) is.

Build the app to populate the catalog. Discovery is a compile/build step, not a save-the-file step. For strings with variables, pass the string through a localizable API first so Xcode can discover it and add it. A constructed String that you localize later is invisible.

String(localized:) supports defaultValue. When no localized version is found, the runtime uses that default. If no localization exists at all, it falls back to the key. Interpolation is localized when the value is initialized inside String(localized:) or SwiftUI Text(...). Numeric interpolation uses the user’s locale unless you pass a locale. Comments are translator context in the catalog and survive export/import in principle. Apple’s docs do not pin down comment syntax or the full set of recognized call sites. Treat comments as required context, not as a specified extraction grammar.

The compiler flag is the switch that makes discovery automatic. With it enabled, export builds the project first and extracts SwiftUI, String(localized:), and AttributedString(localized:) interpolations. Without it you are back to manual catalog entry.

Migrating from .strings and .stringsdict

Xcode can convert existing localization resources into a string catalog, including legacy text in .strings and pluralized content that lived in .stringsdict. After a clean conversion, the catalog is the place you manage strings.

What migration does convert, in typical projects: keys and values from the old files, and plural data from .stringsdict into catalog plural variants. There is no Apple document that lists every item it does not convert. Recurring failures are setup-dependent: files in Base.lproj, odd bundle layout, and build-configured Info.plist arrangements that block automatic collection. .stringsdict in Base.lproj is a known way to make migration fail or leave the catalog inconsistent. The workaround described for a failed Xcode 15 migration is to remove and re-add localized files correctly, then migrate again.

Xcode 16 release notes fixed plural-propagation behavior in catalogs and InfoPlist localization file handling at build time. Read that as: the conversion path is still moving. After you migrate, confirm keys, plurals, and InfoPlist entries in the catalog before you delete the legacy files.

Plurals

In the legacy model, plural forms lived in .stringsdict as a separate plural-rule resource, not in the main strings file. In a catalog, the source string is discovered from code first. Control-click the key and choose Vary by Plural. Xcode adds plural variants to the source localization. You then enter translations for those variants in the language sidebar.

Xcode 16 fixed a bug where varying a source-language string by plural did not correctly use plural cases when propagating to languages that already had translations. If you created variants while other languages were already filled in, re-check those languages. Do not assume the variants copied forward.

What actually goes wrong

Strings not extracted or not appearing

Empty export or an empty catalog of Swift strings: verify Use Compiler to Extract Swift Strings. Export builds first and only then extracts SwiftUI, String(localized:), and AttributedString(localized:) interpolations. Wrappers, delayed interpolation, and “build a String, localize later” never reach the extractor. Apple does not publish a full extraction grammar.

Missing locale rows in a catalog may not fall back to the source language the way a .strings lookup sometimes did. You can get the key, or defaultValue, instead of English. If a string is in source code and not in the catalog, it never hit a recognized localizable API at build time.

Stale or duplicate keys after migration

Partial migrations leave duplicate keys and leftover legacy files. .stringsdict or other localization files in Base.lproj are a documented cause of failed Xcode 15 migrations. Path and import/export behavior tied to INFOPLIST_FILE can produce stale or mismatched resource paths, so the catalog and the bundle disagree about which file owns a key. Xcode 16’s localization fixes are evidence that duplicate and stale catalog state was an active problem area, not a one-off.

After migrating, treat leftover files in Base.lproj as blockers. Delete old resources only once the catalog actually contains the keys you still ship.

Plural variations not applying

Intended path: source string in the catalog, Vary by Plural, translations per variant. Two failure modes show up in practice. First, plural cases not propagating into already-translated languages when you vary the source string—the Xcode 16 fix. Second, Xcode getting confused when another format specifier appears before the plural variable. The catalog editor documents the happy path and not these.

If English looks right and other languages ignore the plural, inspect specifier order and whether variants were added before or after those translations existed. Re-enter variants on the translated side if propagation did not run.

SwiftUI interpolation

Interpolation is not localized unless it is initialized inside String(localized:) or SwiftUI Text(). Building an interpolated String and handing that to Text localizes the wrong thing, or nothing. Numeric interpolation follows the user’s locale unless you supply one.

defaultValue on String(localized:) can contain interpolations. If a catalog entry is missing and runtime fallback looks like a raw key, use Text(LocalizedStringResource(...)) or String(localized:defaultValue:) so the fallback is the default value. That does not invent a catalog row; it only makes the miss survivable.

Info.plist / InfoPlist localization

Localize Info.plist keys with InfoPlist.xcstrings. After each build, Xcode adds known localizable keys from Info.plist into that catalog. If collection never happens, the usual cause is a broken build configuration, not a missing API call.

Xcode 16 renames files such as TargetName-InfoPlist.strings to InfoPlist.strings at build time, including when the Info.plist is generated by the build system. A wrong InfoPlist.strings file being used at runtime is an older failure mode; InfoPlist.xcstrings is the layout that also corrects default-language behavior in that situation. Do not mix a generated Info.plist, a renamed strings file, and a catalog and assume they merge.

Export / import XLIFF round trip

Product → Export Localizations is the handoff. Export extracts from code, catalogs, Interface Builder, and localized assets. You send XLIFF out and import translations back. As of Xcode 16, strings marked Don’t Translate are emitted with translate=no in XLIFF.

The original path in exported XLIFF has been tied to INFOPLIST_FILE since Xcode 9.3. Projects that assume a stable path then import into the wrong file or appear to drop strings. Align INFOPLIST_FILE with what export emits before blaming the catalog. Comments are supposed to travel as translator context; the exact propagation rules are under-specified, so verify they survived the round trip.

Keeping many languages in sync

A catalog plus Export Localizations keeps one source of keys. Drift still happens: new keys after a build, plural variants that did not propagate, InfoPlist entries that only appear once the catalog is rebuilt. When you maintain every App Store language, generate the missing translations from the current catalog or XLIFF in one pass with Localize Your App, re-import, rebuild, and confirm extraction and plural variants before you ship.

For the file-level detail, see the guides on translating Localizable.strings and String Catalogs. The complete app localization guide covers language choice and cost, and the App Store language list covers the store side.

Frequently asked questions

Why is my String Catalog empty after I add it?

Build with Use Compiler to Extract Swift Strings enabled. Xcode populates .xcstrings from String(localized:), SwiftUI Text, and AttributedString(localized:) interpolations at build time, not when you save the file.

Do I still need NSLocalizedString if I use String Catalogs?

No for new Swift. NSLocalizedString remains a legacy lookup; the catalog workflow is String(localized:) plus compiler extraction. Existing NSLocalizedString call sites can still resolve keys after migration.

What does migration from .strings/.stringsdict not convert?

Apple does not publish a full exclusion list. Files in Base.lproj, awkward bundle layout, and some Info.plist build setups commonly block or skew conversion. Fix layout, migrate again, then drop the legacy files.

Why don’t plural variants show up in already-translated languages?

Vary by Plural on the source string did not always propagate plural cases into existing translations; Xcode 16 fixed that. Also avoid a format specifier before the plural variable. Re-check translated languages after varying.

How do I localize Info.plist keys in a catalog project?

Add InfoPlist.xcstrings. After each build Xcode inserts known localizable Info.plist keys. If nothing appears, fix the build configuration. Xcode 16 also renames TargetName-InfoPlist.strings to InfoPlist.strings at build time.

Why does XLIFF import miss strings or write to the wrong file?

Export’s original path has been tied to INFOPLIST_FILE since Xcode 9.3. Don’t Translate becomes translate=no in Xcode 16 XLIFF. Match INFOPLIST_FILE to the export, then re-import.

Everything in iOS localization

Xcode String Catalogs, legacy .strings and .stringsdict, and what breaks when you migrate between them.

Ship your app in 50 languages by tonight

Upload your localization file, review side by side, download ready-to-import files for every language. One-time credits from $9 — no subscription.

No subscription. Credits never expire.

Related guides