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
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Comprehensive guide to app localization using String Catalogs. Apple Design Award Inclusivity winners always support multiple languages with excellent RTL (Right-to-Left) support.
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.
.strings/.stringsdict files.xcstrings)#bundle macro, and AI-powered comment generationLocalizedStringResource.strings filesMethod 1: Xcode Navigator
Localizable.xcstrings)Method 2: Automatic Extraction
Xcode 15 can automatically extract strings from:
Text, Label, Button)String(localized:))NSLocalizedString)CFCopyLocalizedString).storyboard, .xib)Build Settings Required:
Each entry has:
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" : "1.0"
}
Xcode tracks state for each translation:
Workflow:
SwiftUI views with String parameters automatically support localization:
// ✅ Automatically localizable
Text("Welcome to WWDC!")
Label("Thanks for shopping with us!", systemImage: "bag")
Button("Checkout") { }
// Xcode extracts these strings to String Catalog
How it works: SwiftUI uses LocalizedStringKey internally, which looks up strings in String Catalogs.
For explicit localization in Swift code:
// Basic
let title = String(localized: "Welcome to WWDC!")
// With comment for translators
let title = String(localized: "Welcome to WWDC!",
comment: "Notification banner title")
// With custom table
let title = String(localized: "Welcome to WWDC!",
table: "WWDCNotifications",
comment: "Notification banner title")
// With default value (key ≠ English text)
let title = String(localized: "WWDC_NOTIFICATION_TITLE",
defaultValue: "Welcome to WWDC!",
comment: "Notification banner title")
Best practice: Always include comment to give translators context.
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) // Resolved at render time
Text(subtitle)
}
.padding()
}
}
}
// Usage
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.
// Markdown formatting is preserved across localizations
let subtitle = AttributedString(localized: "**Bold** and _italic_ text")
// Basic
let title = NSLocalizedString("Recent Purchases", comment: "Button Title")
// With table
let title = NSLocalizedString("Recent Purchases",
tableName: "Shopping",
comment: "Button Title")
// With bundle
let title = NSLocalizedString("Recent Purchases",
tableName: nil,
bundle: .main,
value: "",
comment: "Button Title")
let customBundle = Bundle(for: MyFramework.self)
let text = customBundle.localizedString(forKey: "Welcome",
value: nil,
table: "MyFramework")
// Objective-C
#define MyLocalizedString(key, comment) \
[myBundle localizedStringForKey:key value:nil table:nil]
Localize app name, permissions, etc.:
Info.plistInfoPlist.strings for each language:// InfoPlist.strings (Spanish)
"CFBundleName" = "Mi Aplicación";
"NSCameraUsageDescription" = "La app necesita acceso a la cámara para tomar fotos.";
Different languages have different plural rules:
// Xcode automatically creates plural variations
Text("\(count) items")
// With custom formatting
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"
}
}
}
}
}
}
}
}
}
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>
// Multiple variables with different plural forms
let message = String(localized: "\(songCount) songs on \(albumCount) albums")
Xcode creates variations for each variable's plural form:
songCount: one, otheralbumCount: one, otherDifferent text for different platforms:
// Same code, different strings per device
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:
For dynamic type and size classes:
Text("Application Settings")
String Catalog can provide shorter text for narrow widths.
SwiftUI automatically mirrors layouts for RTL languages:
// ✅ Automatically mirrors for Arabic/Hebrew
HStack {
Image(systemName: "chevron.right")
Text("Next")
}
// iPhone (English): [>] Next
// iPhone (Arabic): Next [<]
Always use semantic directions:
// ✅ Correct - mirrors automatically
.padding(.leading, 16)
.frame(maxWidth: .infinity, alignment: .leading)
// ❌ Wrong - doesn't mirror
.padding(.left, 16)
.frame(maxWidth: .infinity, alignment: .left)
Mark images that should/shouldn't flip:
// ✅ Directional - mirrors for RTL
Image(systemName: "chevron.forward")
// ✅ Non-directional - never mirrors
Image(systemName: "star.fill")
// Custom images
Image("backButton")
.flipsForRightToLeftLayoutDirection(true)
Xcode Scheme:
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"))
}
}
let formatter = DateFormatter()
formatter.locale = Locale.current // ✅ Use current locale
formatter.dateStyle = .long
formatter.timeStyle = .short
let dateString = formatter.string(from: Date())
// US: "January 15, 2024 at 3:30 PM"
// France: "15 janvier 2024 à 15:30"
// Japan: "2024年1月15日 15:30"
Never hardcode date format strings:
// ❌ Wrong - breaks in other locales
formatter.dateFormat = "MM/dd/yyyy"
// ✅ Correct - adapts to locale
formatter.dateStyle = .short
let formatter = NumberFormatter()
formatter.locale = Locale.current
formatter.numberStyle = .currency
let priceString = formatter.string(from: 29.99)
// US: "$29.99"
// UK: "£29.99"
// Japan: "¥30" (rounds to integer)
// France: "29,99 €" (comma decimal, space before symbol)
let distance = Measurement(value: 100, unit: UnitLength.meters)
let formatter = MeasurementFormatter()
formatter.locale = Locale.current
let distanceString = formatter.string(from: distance)
// US: "328 ft" (converts to imperial)
// Metric countries: "100 m"
let names = ["Ångström", "Zebra", "Apple"]
// ✅ Locale-aware sort
let sorted = names.sorted { (lhs, rhs) in
lhs.localizedStandardCompare(rhs) == .orderedAscending
}
// Sweden: ["Ångström", "Apple", "Zebra"] (Å comes first in Swedish)
// US: ["Ångström", "Apple", "Zebra"] (Å treated as A)
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:
Localized phrases:
English: "Food Truck Trends for this week"
Spanish: "Tendencias de Food Truck para esta semana"
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.
Automatic migration:
.strings file in Navigator.xcstrings and preserves translationsManual approach:
.strings filesPlural files automatically merge:
.strings and .stringsdict together.xcstringsPhase 1: New code uses String Catalogs
Localizable.xcstringsString(localized:).strings files for old codePhase 2: Migrate existing strings
.strings table at a timeNSLocalizedString callsPhase 3: Remove legacy files
.strings and .stringsdict filesCoexistence: .strings and .xcstrings work together - Xcode checks both.
// ❌ Wrong - not localizable
Text("Welcome")
let title = "Settings"
// ✅ Correct - localizable
Text("Welcome") // SwiftUI auto-localizes
let title = String(localized: "Settings")
// ❌ Wrong - word order varies by language
let message = String(localized: "You have") + " \(count) " + String(localized: "items")
// ✅ Correct - single localizable string with substitution
let message = String(localized: "You have \(count) items")
Why wrong: Some languages put numbers before nouns, some after.
// ❌ Wrong - grammatically incorrect for many languages
Text("\(count) item(s)")
// ✅ Correct - proper plural handling
Text("\(count) items") // Xcode creates plural variations
// ❌ Wrong - breaks in RTL languages
.padding(.left, 20)
HStack {
backButton
Spacer()
title
}
// ✅ Correct - mirrors automatically
.padding(.leading, 20)
HStack {
backButton // Appears on right in RTL
Spacer()
title
}
// ❌ Wrong - US-only format
let formatter = DateFormatter()
formatter.dateFormat = "MM/dd/yyyy"
// ✅ Correct - adapts to locale
formatter.dateStyle = .short
formatter.locale = Locale.current
// ❌ Wrong - translator has no context
String(localized: "Confirm")
// ✅ Correct - clear context
String(localized: "Confirm", comment: "Button to confirm delete action")
Impact: "Confirm" could mean "verify" or "acknowledge" - context matters for accurate translation.
Cause: Build settings not enabled
Solution:
Cause 1: Language not added to project
Cause 2: String marked as "Stale"
Cause: Using String.localizedStringWithFormat instead of String Catalog
Solution: Use String Catalog's automatic plural handling:
// ✅ Correct
Text("\(count) items")
// ❌ Wrong
Text(String.localizedStringWithFormat(NSLocalizedString("%d items", comment: ""), count))
Cause: "Localization Prefers String Catalogs" not set
Solution:
Cause 1: Build setting not enabled
Solution:
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.
Cause: Wrong syntax or missing import
Solution:
import Foundation // Required for #bundle
Text("My Collections", bundle: #bundle, comment: "Section title")
Verify you're using #bundle not .module.
Cause 1: String not in String Catalog
.xcstrings fileCause 2: Build setting not enabled
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".
The problem: String-based localization fails silently when typos occur.
// ❌ Typo - fails silently at runtime
Text("App.HomeScren.Title") // Missing 'e' in Screen
The solution: Xcode 26 generates type-safe symbols from manually-added strings.
// ✅ Type-safe - compiler catches typos
Text(.appHomeScreenTitle)
| 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 → .appHomeScreenTitleLocalizedStringResource// SwiftUI views
struct ContentView: View {
var body: some View {
NavigationStack {
Text(.introductionTitle)
.navigationSubtitle(.subtitle(friendsPosts: 42))
}
}
}
// Foundation String
let message = String(localized: .curatedCollection)
// Custom views with LocalizedStringResource
struct CollectionDetailEditingView: View {
let title: LocalizedStringResource
init(title: LocalizedStringResource) {
self.title = title
}
var body: some View {
Text(title)
}
}
CollectionDetailEditingView(title: .editingTitle)
Xcode 26 uses an on-device model to automatically generate contextual comments for localizable strings.
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.
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:
SwiftUI uses the .main bundle by default. Swift Packages and frameworks need to reference their own bundle:
// ❌ Wrong - uses main bundle, strings not found
Text("My Collections", comment: "Section title")
The #bundle macro automatically references the correct bundle for the current target:
// ✅ Correct - automatically uses package/framework bundle
Text("My Collections", bundle: #bundle, comment: "Section title")
Key advantages:
.module bundle management// Main app
Text("My Collections",
tableName: "Discover",
comment: "Section title")
// Framework or Swift Package
Text("My Collections",
tableName: "Discover",
bundle: #bundle,
comment: "Section title")
When using multiple String Catalogs for organization:
Symbols are directly accessible on LocalizedStringResource:
Text(.welcomeMessage) // From Localizable.xcstrings
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.
Symbols are nested in the table namespace:
// From Discover.xcstrings
Text(Discover.featuredCollection)
// From Settings.xcstrings
Text(Settings.privacyPolicy)
Organization strategy for large apps:
Xcode 26 supports two complementary workflows:
Process:
Text, Button) and String(localized:)Pros: Simple initial setup, immediate start
Cons: Less control over string organization
// ✅ String extraction workflow
Text("Welcome to WWDC!", comment: "Main welcome message")
Process:
Pros: Better control, type safety, easier to maintain across frameworks
Cons: Requires planning string catalog structure upfront
// ✅ Generated symbols workflow
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 |
Xcode 26 allows converting between workflows without manual rewriting.
Example:
// Before
Text("Welcome to WWDC!", comment: "Main welcome message")
// After refactoring
Text(.welcomeToWWDC)
Benefits:
After adopting Xcode 26 generated symbols, verify:
Build Configuration:
String Catalog Setup:
Swift Package Integration:
Text() and String(localized:) calls in packages use bundle: #bundle#bundle is usedRefactoring & Migration:
Optional Features:
Testing: