Localization in iOS: Swift & iOS lessons from production
Building an iOS app that reaches users across borders means more than translating text—it's handling currencies, dates, plurals, and cultural conventions that vary by region. We've learned that localization in iOS can feel fragile when done ad-hoc, but with String Catalogs and a structured approach, it becomes a reliable part of your build pipeline. In this article, we'll walk through how to implement localization in iOS using modern tools and practices we've refined across production apps serving multiple markets.
String Catalogs in Xcode
String Catalogs replaced the legacy localization workflow (separate .strings files per language) starting in Xcode 15. Instead of managing parallel files and hand-editing XML, you now maintain a single unified catalog that Xcode uses to generate localized strings at build time.
To create a String Catalog, right-click your project in Xcode, select New File, and choose Strings File. Name it Localizable. Xcode creates a Localizable.xcstrings file in JSON format. This is your single source of truth.
Open the catalog and you'll see a structured editor. On the left, you see all string keys. On the right, you manage translations and metadata. The UI shows:
- The original string (usually in English)
- Translations for each configured language
- Context notes for translators
- Device-specific variants (phone vs. pad)
- Pluralization rules
In your Swift code, use the String(localized:) initializer or the @LocalizedStringResource macro in SwiftUI:
let message = String(localized: "welcome_title")
let formatted = String(localized: "items_added", defaultValue: "Items added")
When you build, Xcode scans your code for localization calls, extracts strings, and populates the catalog. If a key already exists, Xcode skips it. If it's new, Xcode adds it as a candidate for translation. This automated extraction is a game-changer—you don't manually maintain a list of keys.
Configure which languages your app supports in the project settings under Localizations. Add each language you ship in, and Xcode creates entries in the catalog for each one.
Automatic extraction of SwiftUI strings
One of the biggest wins with String Catalogs is how they work seamlessly with SwiftUI. Every time you use a localized string literal in SwiftUI, the build process extracts it automatically.
SwiftUI's Text view has built-in support:
struct ProductDetail: View {
var body: some View {
VStack {
Text("product_title")
Text("in_stock")
Text("add_to_cart")
}
}
}
When you build, those three strings are extracted into the catalog. You don't need to do anything else—no manual registration, no separate process.
For dynamic strings (where content comes from data), use string interpolation with localization:
let count = 5
Text("You have \(count) items")
This extracts the template and keeps the interpolation intact. Translators see the placeholder and understand that a number will be substituted.
Where extraction gets trickier is with conditionals. If you hide a string behind a complex conditional, the extractor might miss it:
// Risky: extractor may not find this
if shouldShowMessage {
Text("thank_you_message")
}
To guarantee extraction, place the string at the top level or use a property:
struct CheckoutView: View {
@State var isProcessing = false
var body: some View {
VStack {
Text("checkout_title")
if isProcessing {
ProgressView()
Text("processing_payment")
}
}
}
}
Both strings are extracted because they're direct children of the view hierarchy. The build process scans the syntax tree and finds them.
For strings constructed programmatically (rare, but it happens), use the @LocalizedStringResource attribute to hint the extractor:
@LocalizedStringResource var dynamicKey: LocalizedStringResource
dynamicKey = "some_key"
Text(dynamicKey)
This pattern isn't common in well-designed apps because translators can't translate a key that's chosen at runtime. If you find yourself doing this, reconsider the design.
Pluralization rules
Handling plurals across languages is more complex than English's simple "1 item" vs. "N items" rule. Some languages have three or more plural forms, and the rules vary by language.
String Catalogs handle this with built-in plural support. In the catalog editor, select a string and mark it as requiring pluralization. Xcode prompts you to specify the plural variable (usually a count).
In your code, use a dictionary syntax with the ^[...] notation:
let itemCount = 3
let message = String(localized: "item_count ^[\(itemCount) item](inflect: true)")
Text(message)
The inflect: true flag tells the system to apply language-specific plural rules. For English, this produces "3 items". For languages with complex rules (Russian has six forms), the system selects the correct form automatically.
In the String Catalog editor, you'll see a Plural section where you specify variations for each language. English typically has two (singular and plural), but French has two, Polish has three, and Russian has six.
Here's what the catalog JSON looks like under the hood (you rarely edit this directly):
// Pseudo-representation of catalog structure
{
"item_count": {
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "^[\(count) item](inflect: true)"
}
},
"ru": {
"stringUnit": {
"state": "translated",
"value": "^[\(count) предмет](inflect: true)"
}
}
}
}
}
Don't hardcode plurals in conditional logic. Always use the catalog's plural system because translators need to see the rules and adjust them per language.
Test with pseudo-languages
Before shipping to real translators and users, test your localization with pseudo-languages. Xcode supports several:
- Accented English: replaces vowels with accented versions (e → é), making it obvious which strings are localized
- Pseudo Right-to-Left: reverses text direction to catch layout issues
- Double-length Pseudo-language: doubles string length to find UI overflow
Enable pseudo-languages in your scheme. Open the scheme editor, go to Run > Options, and set App Language to a pseudo variant. Build and run.
With Accented English enabled, any string showing plain ASCII (like "Welcome") instead of "Wélcömé" is missing localization. This catches extraction failures and hardcoded strings:
// Bad: this won't localize
Text("Welcome")
// Good: this will show "Wélcömé" in Accented English
Text("welcome_title")
The double-length variant catches UI that breaks when strings are longer (common when translating to German or French). Run through your entire app and check for clipped labels, truncated buttons, and text overlaps.
The pseudo RTL variant surfaces layout bugs early. Text may not reverse correctly, and some UI elements may be positioned incorrectly. Test all user flows, not just the happy path.
Automate this: run your UI tests against pseudo-languages as part of CI. Set the language, take screenshots, and compare against baselines. This catches regressions before translation work begins.
Common mistakes
- Forgetting to localize format strings: If you build a string with
String(format:), the format template itself needs to be localized because placeholder order varies by language. Use interpolation within a localized template instead. - Hardcoding numbers and currency: Never hardcode currency symbols or number formatting. Use
NumberFormatterand locale-aware formatting so that 1000 displays as "1,000" in English but "1.000" in German. - Extracting strings after strings have translations: If you add a new string to your code and build, Xcode adds it to the catalog as a candidate. If you've already translated strings, the new one won't have translations until you send it to your translation vendor. Track which strings are candidates vs. translated and manage the handoff carefully.
- Not testing RTL languages: If you support Arabic or Hebrew, test extensively with pseudo RTL. Many apps have broken navigation, inverted images, and misaligned text in RTL mode.
- Using images with text baked in: Localize image assets too. If your app logo or marketing image contains text, you need localized variants or use dynamic text overlays.
Key takeaways
- String Catalogs (
.xcstrings) unified the localization workflow in Xcode 15+. Maintain a single catalog per app, not parallel .strings files per language. - Automatic extraction in SwiftUI means you declare localized strings once in code, and Xcode populates the catalog during build. No manual key registration needed.
- Use the catalog's plural system for any string with a count variable. Don't implement plurals with conditionals—translators can't adjust rules per language that way.
- Test with pseudo-languages (Accented English, pseudo RTL, double-length) before sending strings to translators. Catch extraction gaps and UI layout issues early.
- Treat localization as a build artifact, not an afterthought. Version your catalog alongside code, and coordinate with translation vendors on the export/import cycle.






Comments
Loading comments…