| name | ios-localization |
| description | Localizes Swift / SwiftUI apps using String Catalogs (Localizable.xcstrings) — symbol keys, manual extraction, plural and gender rules, formatting, and translation workflow. Use when adding multi-language support, auditing localization coverage, or fixing locale-sensitive bugs. |
iOS Localization
String catalog as source of truth
For iOS 17+, use String Catalogs (Localizable.xcstrings). They replace Localizable.strings, Localizable.stringsdict, and InfoPlist.strings.
Resources/
└── Localizable.xcstrings
Xcode automatically extracts string literals into the catalog on each build. You then translate per language inside the catalog editor.
Symbol keys, not English-as-key
Prefer symbol keys with extractionState set to "manual":
| Approach | Key | Pros | Cons |
|---|
| English-as-key | "Add User" | Quick start | Any rewording in English breaks all translations |
| Symbol key (preferred) | "addUser" | Stable across UX rewrites | Requires manual editing of the catalog |
In code:
Text(.addUser)
Text("addUser")
Avoid:
Text("Add User")
When a new key is added, translate it into every language the project supports before merging. Use the catalog's "Needs Review" state to track gaps.
Plurals
Define plurals directly in the String Catalog (no separate .stringsdict needed). For supported languages (English, French, German, Portuguese, Spanish, Italian), prefer automatic grammar agreement:
Text("^[\(count) item](inflect: true)")
- Renders
"1 item" / "2 items" correctly.
- For other languages or more complex pluralization, use the catalog's plural variants UI.
Formatting
Always format numbers, dates, currencies, and lists with FormatStyle — never with hand-built strings:
Text(price, format: .currency(code: "USD"))
Text(date, format: .dateTime.day().month().year())
Text(names, format: .list(type: .and))
- Avoid
String(format: "%.2f", x) — produces incorrect output in many locales.
- Avoid manual date format strings; if unavoidable for display, use
"y" rather than "yyyy" so the year is correct everywhere.
Right-to-left
- Use SwiftUI's automatic layout direction —
HStack flips correctly under RTL.
- Use
.leading / .trailing for alignment, never .left / .right.
- For directional icons (e.g., back arrows), use SF Symbols variants that flip automatically (
chevron.backward, not chevron.left).
- Test by switching the scheme's Application Language to a pseudo-RTL like Arabic or Hebrew.
Pseudolanguages for testing
Xcode supports two pseudolanguages for surfacing localization bugs without translators:
- Double-Length Pseudolanguage — surfaces truncation issues.
- Right-to-Left Pseudolanguage — surfaces RTL layout issues.
Scheme → Run → Options → App Language. Use these on every PR that touches UI.
Info.plist strings
Privacy strings (NSCameraUsageDescription etc.) and CFBundleDisplayName are localized via InfoPlist.xcstrings — a separate string catalog auto-generated by Xcode. Always translate these; an unlocalized privacy prompt looks unprofessional and may be flagged in App Review.
Translation workflow
- Add keys during feature work — leave non-default languages in "Needs Review".
- Export the catalog to XLIFF: Product → Export Localizations.
- Send XLIFF to translators (or a translation service).
- Import the returned XLIFF: Product → Import Localizations.
- Verify the catalog shows 100% translated for shipped languages before tagging the release.
For small projects without translators, ChatGPT / Claude can do a decent first pass, but always have a native speaker review marketing-visible strings (App Store description, onboarding, paywall).
Anti-patterns to flag
Text("Hello, " + name + "!") — string concatenation breaks RTL and word order. Use interpolation: Text("Hello, \(name)!").
- Hard-coded English in
Alert titles / buttons.
Locale.current.identifier used as a UI flag — use the system formatter APIs instead.
- Untranslated empty-state UI in
ContentUnavailableView.
- Privacy strings only in English.
Quick checklist