| name | axiom-localization |
| description | Use when localizing apps, using String Catalogs, generating type-safe symbols (Xcode 26+), handling plurals, RTL layouts, locale-aware formatting, or migrating from .strings files - comprehensive i18n patterns for Xcode 15-26 |
| license | MIT |
| metadata | {"version":"1.1.0","last-updated":"2025-12-16"} |
Localization & Internationalization
Comprehensive guide to app localization using String Catalogs. Apple Design Award Inclusivity winners always support multiple languages with excellent RTL (Right-to-Left) support.
Overview
String Catalogs (.xcstrings) are Xcode 15's unified format for managing app localization. They replace legacy .strings and .stringsdict files with a single JSON-based format that's easier to maintain, diff, and integrate with translation workflows.
This skill covers String Catalogs, SwiftUI/UIKit localization APIs, plural handling, RTL support, locale-aware formatting, and migration strategies from legacy formats.
When to Use This Skill
- Setting up String Catalogs in Xcode 15+
- Localizing SwiftUI and UIKit apps
- Handling plural forms correctly (critical for many languages)
- Supporting RTL languages (Arabic, Hebrew)
- Formatting dates, numbers, and currencies by locale
- Migrating from legacy
.strings/.stringsdict files
- Preparing App Shortcuts and App Intents for localization
- Debugging missing translations or incorrect plural forms
System Requirements
- Xcode 15+ for String Catalogs (
.xcstrings)
- Xcode 26+ for automatic symbol generation,
#bundle macro, and AI-powered comment generation
- iOS 15+ for
LocalizedStringResource
- iOS 16+ for App Shortcuts localization
- Earlier iOS versions use legacy
.strings files
Part 1: String Catalogs (WWDC 2023/10155)
Creating a String Catalog
Method 1: Xcode Navigator
- File → New → File
- Choose "String Catalog"
- Name it (e.g.,
Localizable.xcstrings)
- Add to target
Method 2: Automatic Extraction
Xcode 15 can automatically extract strings from:
- SwiftUI views (string literals in
Text, Label, Button)
- Swift code (
String(localized:))
- Objective-C (
NSLocalizedString)
- C (
CFCopyLocalizedString)
- Interface Builder files (
.storyboard, .xib)
- Info.plist values
- App Shortcuts phrases
Build Settings Required:
- "Use Compiler to Extract Swift Strings" → Yes
- "Localization Prefers String Catalogs" → Yes
String Catalog Structure
Each entry has:
- Key: Unique identifier (default: the English string)
- Default Value: Fallback if translation missing
- Comment: Context for translators
- String Table: Organization container (default: "Localizable")
Example .xcstrings JSON:
{
"sourceLanguage" : "en",
"strings" : {
"Thanks for shopping with us!" : {
"comment" : "Label above checkout button",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Thanks for shopping with us!"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "¡Gracias por comprar con nosotros!"
}
}
}
}
},
"version"
Translation States
Xcode tracks state for each translation:
- New (⚪) - String hasn't been translated yet
- Needs Review (🟡) - Source changed, translation may be outdated
- Reviewed (✅) - Translation approved and current
- Stale (🔴) - String no longer found in source code
Workflow:
- Developer adds string → New
- Translator adds translation → Reviewed
- Developer changes source → Needs Review
- Translator updates → Reviewed
- Developer removes code → Stale
Part 2: SwiftUI Localization
LocalizedStringKey (Automatic)
SwiftUI views with String parameters automatically support localization:
Text("Welcome to WWDC!")
Label("Thanks for shopping with us!", systemImage: "bag")
Button("Checkout") { }
How it works: SwiftUI uses LocalizedStringKey internally, which looks up strings in String Catalogs.
String(localized:) with Comments
For explicit localization in Swift code:
let title = String(localized: "Welcome to WWDC!")
let title = String(localized: "Welcome to WWDC!",
comment: "Notification banner title")
let title = String(localized: "Welcome to WWDC!",
table: "WWDCNotifications",
comment: "Notification banner title")
let title = String(localized: "WWDC_NOTIFICATION_TITLE",
defaultValue: "Welcome to WWDC!",
comment: "Notification banner title")
Best practice: Always include comment to give translators context.
LocalizedStringResource (Deferred Localization)
For passing localizable strings to other functions:
import Foundation
struct CardView: View {
let title: LocalizedStringResource
let subtitle: LocalizedStringResource
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 10.0)
VStack {
Text(title)
Text(subtitle)
}
.padding()
}
}
}
CardView(
title: "Recent Purchases",
subtitle: "Items you've ordered in the past week."
)
Key difference: LocalizedStringResource defers lookup until used, allowing custom views to be fully localizable.
AttributedString with Markdown
let subtitle = AttributedString(localized: "**Bold** and _italic_ text")
Part 3: UIKit & Foundation
NSLocalizedString Macro
let title = NSLocalizedString("Recent Purchases", comment: "Button Title")
let title = NSLocalizedString("Recent Purchases",
tableName: "Shopping",
comment: "Button Title")
let title = NSLocalizedString("Recent Purchases",
tableName: nil,
bundle: .main,
value: "",
comment: "Button Title")
Bundle.localizedString
let customBundle = Bundle(for: MyFramework.self)
let text = customBundle.localizedString(forKey: "Welcome",
value: nil,
table: "MyFramework")
Custom Macros
#define MyLocalizedString(key, comment) \
[myBundle localizedStringForKey:key value:nil table:nil]
Info.plist Localization
Localize app name, permissions, etc.:
- Select
Info.plist
- Editor → Add Localization
- Create
InfoPlist.strings for each language:
// InfoPlist.strings (Spanish)
"CFBundleName" = "Mi Aplicación";
"NSCameraUsageDescription" = "La app necesita acceso a la cámara para tomar fotos.";
Part 4: Pluralization
Different languages have different plural rules:
- English: 2 forms (one, other)
- Russian: 3 forms (one, few, many)
- Polish: 3 forms (one, few, other)
- Arabic: 6 forms (zero, one, two, few, many, other)
SwiftUI Plural Handling
Text("\(count) items")
Text("\(visitorCount) Recent Visitors")
In String Catalog:
{
"strings" : {
"%lld Recent Visitors" : {
"localizations" : {
"en" : {
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld Recent Visitor"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld Recent Visitors"
}
}
}
}
}
XLIFF Export Format
When exporting for translation (File → Export Localizations):
Legacy (stringsdict):
<trans-unit id="/%lld Recent Visitors:dict/NSStringLocalizedFormatKey:dict/:string">
<source>%#@recentVisitors@</source>
</trans-unit>
<trans-unit id="/%lld Recent Visitors:dict/recentVisitors:dict/one:dict/:string">
<source>%lld Recent Visitor</source>
<target>%lld Visitante Recente</target>
</trans-unit>
String Catalog (cleaner):
<trans-unit id="%lld Recent Visitors|==|plural.one">
<source>%lld Recent Visitor</source>
<target>%lld Visitante Recente</target>
</trans-unit>
<trans-unit id="%lld Recent Visitors|==|plural.other">
<source>%lld Recent Visitors</source>
<target>%lld Visitantes Recentes</target>
</trans-unit>
Substitutions with Plural Variables
let message = String(localized: "\(songCount) songs on \(albumCount) albums")
Xcode creates variations for each variable's plural form:
songCount: one, other
albumCount: one, other
- Total combinations: 2 × 2 = 4 translation entries
Part 5: Device & Width Variations
Device-Specific Strings
Different text for different platforms:
Text("Bird Food Shop")
String Catalog variations:
{
"Bird Food Shop" : {
"localizations" : {
"en" : {
"variations" : {
"device" : {
"applewatch" : {
"stringUnit" : {
"value" : "Bird Food"
}
},
"other" : {
"stringUnit" : {
"value" : "Bird Food Shop"
}
}
}
}
}
}
}
}
Result:
- iPhone/iPad: "Bird Food Shop"
- Apple Watch: "Bird Food" (shorter for small screen)
Width Variations
For dynamic type and size classes:
Text("Application Settings")
String Catalog can provide shorter text for narrow widths.
Part 6: RTL Support
Layout Mirroring
SwiftUI automatically mirrors layouts for RTL languages:
HStack {
Image(systemName: "chevron.right")
Text("Next")
}
Leading/Trailing vs Left/Right
Always use semantic directions:
.padding(.leading, 16)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.left, 16)
.frame(maxWidth: .infinity, alignment: .left)
Images and Icons
Mark images that should/shouldn't flip:
Image(systemName: "chevron.forward")
Image(systemName: "star.fill")
Image("backButton")
.flipsForRightToLeftLayoutDirection(true)
Testing in RTL Mode
Xcode Scheme:
- Edit Scheme → Run → Options
- Application Language: Arabic / Hebrew
- OR: App Language → Right-to-Left Pseudolanguage
Simulator:
Settings → General → Language & Region → Preferred Language Order
SwiftUI Preview:
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.layoutDirection, .rightToLeft)
.environment(\.locale, Locale(identifier: "ar"))
}
}
Part 7: Locale-Aware Formatting
DateFormatter
let formatter = DateFormatter()
formatter.locale = Locale.current
formatter.dateStyle = .long
formatter.timeStyle = .short
let dateString = formatter.string(from: Date())
Never hardcode date format strings:
formatter.dateFormat = "MM/dd/yyyy"
formatter.dateStyle = .short
NumberFormatter for Currency
let formatter = NumberFormatter()
formatter.locale = Locale.current
formatter.numberStyle = .currency
let priceString = formatter.string(from: 29.99)
MeasurementFormatter
let distance = Measurement(value: 100, unit: UnitLength.meters)
let formatter = MeasurementFormatter()
formatter.locale = Locale.current
let distanceString = formatter.string(from: distance)
Locale-Specific Sorting
let names = ["Ångström", "Zebra", "Apple"]
let sorted = names.sorted { (lhs, rhs) in
lhs.localizedStandardCompare(rhs) == .orderedAscending
}
Part 8: App Shortcuts Localization
Phrases with Parameters
import AppIntents
struct ShowTopDonutsIntent: AppIntent {
static var title: LocalizedStringResource = "Show Top Donuts"
@Parameter(title: "Timeframe")
var timeframe: Timeframe
static var parameterSummary: some ParameterSummary {
Summary("\(.applicationName) Trends for \(\.$timeframe)") {
\.$timeframe
}
}
}
String Catalog automatically extracts:
- Intent title
- Parameter names
- Phrase templates with placeholders
Localized phrases:
English: "Food Truck Trends for this week"
Spanish: "Tendencias de Food Truck para esta semana"
AppShortcutsProvider Localization
struct FoodTruckShortcuts: AppShortcutsProvider {
static var appShortcuts: [AppShortcut] {
AppShortcut(
intent: ShowTopDonutsIntent(),
phrases: [
"\(.applicationName) Trends for \(\.$timeframe)",
"Show trending donuts for \(\.$timeframe) in \(.applicationName)",
"Give me trends for \(\.$timeframe) in \(.applicationName)"
]
)
}
}
Xcode extracts all 3 phrases into String Catalog for translation.
Part 9: Migration from Legacy
Converting .strings to .xcstrings
Automatic migration:
- Select
.strings file in Navigator
- Editor → Convert to String Catalog
- Xcode creates
.xcstrings and preserves translations
Manual approach:
- Create new String Catalog
- Build project (Xcode extracts strings from code)
- Import translations via File → Import Localizations (XLIFF)
- Delete old
.strings files
Converting .stringsdict
Plural files automatically merge:
- Keep
.strings and .stringsdict together
- Convert → Both merge into single
.xcstrings
- Plural variations preserved
Gradual Migration Strategy
Phase 1: New code uses String Catalogs
- Create
Localizable.xcstrings
- Write new code with
String(localized:)
- Keep legacy
.strings files for old code
Phase 2: Migrate existing strings
- Convert one
.strings table at a time
- Test translations after each conversion
- Update code using old
NSLocalizedString calls
Phase 3: Remove legacy files
- Delete
.strings and .stringsdict files
- Verify all strings in String Catalog
- Submit to App Store
Coexistence: .strings and .xcstrings work together - Xcode checks both.
Common Mistakes
Hardcoded Strings
Text("Welcome")
let title = "Settings"
Text("Welcome")
let title = String(localized: "Settings")
Concatenating Localized Strings
let message = String(localized: "You have") + " \(count) " + String(localized: "items")
let message = String(localized: "You have \(count) items")
Why wrong: Some languages put numbers before nouns, some after.
Missing Plural Forms
Text("\(count) item(s)")
Text("\(count) items")
Ignoring RTL
.padding(.left, 20)
HStack {
backButton
Spacer()
title
}
.padding(.leading, 20)
HStack {
backButton
Spacer()
title
}
Wrong Date/Number Formats
let formatter = DateFormatter()
formatter.dateFormat = "MM/dd/yyyy"
formatter.dateStyle = .short
formatter.locale = Locale.current
Forgetting Comments
String(localized: "Confirm")
String(localized: "Confirm", comment: "Button to confirm delete action")
Impact: "Confirm" could mean "verify" or "acknowledge" - context matters for accurate translation.
Troubleshooting
Strings not appearing in String Catalog
Cause: Build settings not enabled
Solution:
- Build Settings → "Use Compiler to Extract Swift Strings" → Yes
- Clean Build Folder (Cmd+Shift+K)
- Build project
Translations not showing in app
Cause 1: Language not added to project
- Project → Info → Localizations → + button
- Add target language
Cause 2: String marked as "Stale"
- Remove stale strings or verify code still uses them
Plural forms incorrect
Cause: Using String.localizedStringWithFormat instead of String Catalog
Solution: Use String Catalog's automatic plural handling:
Text("\(count) items")
Text(String.localizedStringWithFormat(NSLocalizedString("%d items", comment: ""), count))
XLIFF export missing strings
Cause: "Localization Prefers String Catalogs" not set
Solution:
- Build Settings → "Localization Prefers String Catalogs" → Yes
- Export Localizations again
Generated symbols not appearing (Xcode 26+)
Cause 1: Build setting not enabled
Solution:
- Build Settings → "Generate String Catalog Symbols" → Yes
- Clean Build Folder (Cmd+Shift+K)
- Rebuild project
Cause 2: String not manually added to catalog
Solution: Symbols only generate for manually-added strings (+ button in String Catalog). Auto-extracted strings don't generate symbols.
#bundle macro not working (Xcode 26+)
Cause: Wrong syntax or missing import
Solution:
import Foundation
Text("My Collections", bundle: #bundle, comment: "Section title")
Verify you're using #bundle not .module.
Refactoring to symbols fails (Xcode 26+)
Cause 1: String not in String Catalog
- Ensure string exists in
.xcstrings file
- Build project to refresh catalog
- Try refactoring again
Cause 2: Build setting not enabled
- Enable "Generate String Catalog Symbols" in Build Settings
- Clean and rebuild
Part 10: Xcode 26 Localization Enhancements
Xcode 26 introduces type-safe localization with generated symbols, automatic comment generation using on-device AI, and improved Swift Package support with the #bundle macro. Based on WWDC 2025 session 225 "Explore localization with Xcode".
Generated Symbols (Type-Safe Localization)
The problem: String-based localization fails silently when typos occur.
Text("App.HomeScren.Title")
The solution: Xcode 26 generates type-safe symbols from manually-added strings.
How It Works
- Add strings manually to String Catalog using the + button
- Enable build setting: "Generate String Catalog Symbols" (ON by default in new projects)
- Use symbols instead of strings
Text(.appHomeScreenTitle)
Symbol Generation Rules
| String Type | Generated Symbol Type | Usage Example |
|---|
| No placeholders | Static property | Text(.introductionTitle) |
| With placeholders | Function with labeled arguments | .subtitle(friendsPosts: 42) |
Key naming conversion:
App.HomeScreen.Title → .appHomeScreenTitle
- Periods removed, camel-cased
- Available on
LocalizedStringResource
Code Examples
struct ContentView: View {
var body: some View {
NavigationStack {
Text(.introductionTitle)
.navigationSubtitle(.subtitle(friendsPosts: 42))
}
}
}
let message = String(localized: .curatedCollection)
struct CollectionDetailEditingView: View {
let title: LocalizedStringResource
init(title: LocalizedStringResource) {
self.title = title
}
var body: some View {
Text(title)
}
}
CollectionDetailEditingView(title: .editingTitle)
Automatic Comment Generation
Xcode 26 uses an on-device model to automatically generate contextual comments for localizable strings.
Enabling the Feature
- Open Xcode Settings → Editing
- Enable "automatically generate string catalog comments"
- New strings added to code automatically receive generated comments
Example
For a button string, Xcode generates:
"The text label on a button to cancel the deletion of a collection"
This context helps translators understand where and how the string is used.
XLIFF Export
Auto-generated comments are marked in exported XLIFF files:
<trans-unit id="Grand Canyon" xml:space="preserve">
<source>Grand Canyon</source>
<target state="new">Grand Canyon</target>
<note from="auto-generated">Suggestion for searching landmarks</note>
</trans-unit>
Benefits:
- Saves developer time writing translator context
- Provides consistent, clear descriptions
- Improves translation quality
Swift Package & Framework Localization
The Problem
SwiftUI uses the .main bundle by default. Swift Packages and frameworks need to reference their own bundle:
Text("My Collections", comment: "Section title")
The Solution: #bundle Macro (NEW in Xcode 26)
The #bundle macro automatically references the correct bundle for the current target:
Text("My Collections", bundle: #bundle, comment: "Section title")
Key advantages:
- Works in main app, frameworks, and Swift Packages
- Backwards-compatible with older OS versions
- Eliminates manual
.module bundle management
With Custom Table Names
Text("My Collections",
tableName: "Discover",
comment: "Section title")
Text("My Collections",
tableName: "Discover",
bundle: #bundle,
comment: "Section title")
Custom Table Symbol Access
When using multiple String Catalogs for organization:
Default "Localizable" Table
Symbols are directly accessible on LocalizedStringResource:
Text(.welcomeMessage)
Note: Xcode automatically resolves symbols from the default "Localizable" table. Explicit table selection is rarely needed—use it only for debugging or testing specific catalogs.
Custom Tables
Symbols are nested in the table namespace:
Text(Discover.featuredCollection)
Text(Settings.privacyPolicy)
Organization strategy for large apps:
- Localizable.xcstrings - Core app strings
- FeatureName.xcstrings - Feature-specific strings (e.g., Onboarding, Settings, Discover)
- Benefits: Easier to manage, clearer ownership, better XLIFF organization
Two Localization Workflows
Xcode 26 supports two complementary workflows:
Workflow 1: String Extraction (Recommended for new projects)
Process:
- Write strings directly in code
- Use SwiftUI views (
Text, Button) and String(localized:)
- Xcode automatically extracts to String Catalog
- Leverage automatic comment generation
Pros: Simple initial setup, immediate start
Cons: Less control over string organization
Text("Welcome to WWDC!", comment: "Main welcome message")
Workflow 2: Generated Symbols (Recommended as complexity grows)
Process:
- Manually add strings to String Catalog
- Reference via type-safe symbols
- Organize into custom tables
Pros: Better control, type safety, easier to maintain across frameworks
Cons: Requires planning string catalog structure upfront
Text(.welcomeMessage)
| Workflow | Best For | Trade-offs |
|---|
| String Extraction | New projects, simple apps, prototyping | Automatic extraction, less control over organization |
| Generated Symbols | Large apps, frameworks, multiple teams | Type safety, better organization, requires upfront planning |
Refactoring Between Workflows
Xcode 26 allows converting between workflows without manual rewriting.
Converting Strings to Symbols
- Right-click on a string literal in code
- Select "Refactor > Convert Strings to Symbols"
- Preview all affected locations
- Customize symbol names before confirming
- Apply to entire table or individual strings
Example:
Text("Welcome to WWDC!", comment: "Main welcome message")
Text(.welcomeToWWDC)
Benefits:
- Batch conversion of entire String Catalogs
- Preview changes before applying
- Maintain localization without code rewrites
Implementation Checklist
After adopting Xcode 26 generated symbols, verify:
Build Configuration:
String Catalog Setup:
Swift Package Integration:
Refactoring & Migration:
Optional Features:
Testing:
Resources
WWDC: 2025-225, 2023-10155, 2022-10110
Docs: /xcode/localization, /xcode/localizing-and-varying-text-with-a-string-catalog
Skills: axiom-app-intents-ref, axiom-hig, axiom-accessibility-diag