Loading...
Loading...
Implement, review, or improve localization and internationalization in iOS/macOS apps — String Catalogs (.xcstrings), LocalizedStringKey, LocalizedStringResource, pluralization, FormatStyle for numbers/dates/measurements, right-to-left layout, Dynamic Type, and locale-aware formatting. Use when adding multi-language support, setting up String Catalogs, handling plural forms, formatting dates/numbers/currencies for different locales, testing localizations, or making UI work correctly in RTL languages like Arabic and Hebrew.
npx skill4agent add dpearson2699/swift-ios-skills ios-localization.strings.stringsdict.strings.stringsdict// SwiftUI -- automatically extracted (LocalizedStringKey)
Text("Welcome back") // key: "Welcome back"
Label("Settings", systemImage: "gear")
Button("Save") { }
Toggle("Dark Mode", isOn: $dark)
// Programmatic -- automatically extracted
String(localized: "No items found")
LocalizedStringResource("Order placed")
// NOT extracted -- plain String, not localized
let msg = "Hello" // just a String, invisible to Xcodereferences/string-catalogs.mdLocalizedStringKey// These all create a LocalizedStringKey lookup automatically:
Text("Welcome back")
Label("Profile", systemImage: "person")
Button("Delete") { deleteItem() }
NavigationTitle("Home")LocalizedStringKeyLocalizedStringKeyString// Basic
let title = String(localized: "Welcome back")
// With default value (key differs from English text)
let msg = String(localized: "error.network",
defaultValue: "Check your internet connection")
// With table and bundle
let label = String(localized: "onboarding.title",
table: "Onboarding",
bundle: .module)
// With comment for translators
let btn = String(localized: "Save",
comment: "Button title to save the current document")// App Intents require LocalizedStringResource
struct OrderCoffeeIntent: AppIntent {
static var title: LocalizedStringResource = "Order Coffee"
}
// Widgets
struct MyWidget: Widget {
var body: some WidgetConfiguration {
StaticConfiguration(kind: "timer",
provider: Provider()) { entry in
TimerView(entry: entry)
}
.configurationDisplayName(LocalizedStringResource("Timer"))
}
}
// Pass around without resolving yet
func showAlert(title: LocalizedStringResource, message: LocalizedStringResource) {
// Resolved at display time with the user's current locale
let resolved = String(localized: title)
}| Context | Type | Why |
|---|---|---|
| SwiftUI view text parameters | | SwiftUI handles lookup automatically |
| Computed strings in view models / services | | Returns resolved |
| App Intents, widgets, system APIs | | Framework resolves at display time |
| Error messages shown to users | | Resolved in catch blocks |
| Logging / analytics (not user-facing) | Plain | No localization needed |
// English: "Welcome, Alice! You have 3 new messages."
// German: "Willkommen, Alice! Sie haben 3 neue Nachrichten."
// Japanese: "Alice さん、新しいメッセージが 3 件あります。"
let text = String(localized: "Welcome, \(name)! You have \(count) new messages.")%@%lld"Welcome, %@! You have %lld new messages.""%@さん、新しいメッセージが%lld件あります。"// Interpolation provides type safety
String(localized: "Score: \(score, format: .number)")
String(localized: "Due: \(date, format: .dateTime.month().day())").stringsdict| Category | English example | Arabic example |
|---|---|---|
| zero | (not used) | 0 items |
| one | 1 item | 1 item |
| two | (not used) | 2 items (dual) |
| few | (not used) | 3-10 items |
| many | (not used) | 11-99 items |
| other | 2+ items | 100+ items |
oneotherother// Code -- single interpolation triggers plural support
Text("\(unreadCount) unread messages")
// String Catalog entries (English):
// one: "%lld unread message"
// other: "%lld unread messages"// In String Catalog editor, enable "Vary by Device" for a key
// iPhone: "Tap to continue"
// iPad: "Tap or click to continue"
// Mac: "Click to continue"^[...]// Automatically adjusts for gender/number in supported languages
Text("^[\(count) \("photo")](inflect: true) added")
// English: "1 photo added" / "3 photos added"
// Spanish: "1 foto agregada" / "3 fotos agregadas"FormatStylelet now = Date.now
// Preset styles
now.formatted(date: .long, time: .shortened)
// US: "January 15, 2026 at 3:30 PM"
// DE: "15. Januar 2026 um 15:30"
// JP: "2026年1月15日 15:30"
// Component-based
now.formatted(.dateTime.month(.wide).day().year())
// US: "January 15, 2026"
// In SwiftUI
Text(now, format: .dateTime.month().day().year())let count = 1234567
count.formatted() // "1,234,567" (US) / "1.234.567" (DE)
count.formatted(.number.precision(.fractionLength(2)))
count.formatted(.percent) // For 0.85 -> "85%" (US) / "85 %" (FR)
// Currency
let price = Decimal(29.99)
price.formatted(.currency(code: "USD")) // "$29.99" (US) / "29,99 $US" (FR)
price.formatted(.currency(code: "EUR")) // "29,99 EUR" (DE)let distance = Measurement(value: 5, unit: UnitLength.kilometers)
distance.formatted(.measurement(width: .wide))
// US: "3.1 miles" (auto-converts!) / DE: "5 Kilometer"
let temp = Measurement(value: 22, unit: UnitTemperature.celsius)
temp.formatted(.measurement(width: .abbreviated))
// US: "72 F" (auto-converts!) / FR: "22 C"// Duration
let dur = Duration.seconds(3661)
dur.formatted(.time(pattern: .hourMinuteSecond)) // "1:01:01"
// Person names
let name = PersonNameComponents(givenName: "John", familyName: "Doe")
name.formatted(.name(style: .long)) // "John Doe" (US) / "Doe John" (JP)
// Lists
let items = ["Apples", "Oranges", "Bananas"]
items.formatted(.list(type: .and)) // "Apples, Oranges, and Bananas" (EN)
// "Apples, Oranges et Bananas" (FR)references/formatstyle-locale.mdHStack.leading.trailingNavigationStackList// Testing RTL in previews
MyView()
.environment(\.layoutDirection, .rightToLeft)
.environment(\.locale, Locale(identifier: "ar"))
// Images that should mirror (directional arrows, progress indicators)
Image(systemName: "chevron.right")
.flipsForRightToLeftLayoutDirection(true)
// Images that should NOT mirror: logos, photos, clocks, music notes
// Forced LTR for specific content (phone numbers, code)
Text("+1 (555) 123-4567")
.environment(\.layoutDirection, .leftToRight).leading.trailing.left.rightHStackVStackoffset(x:)// WRONG -- legacy API, verbose, no compiler integration with String Catalogs
let title = NSLocalizedString("welcome_title", comment: "Welcome screen title")// CORRECT
let title = String(localized: "welcome_title",
defaultValue: "Welcome!",
comment: "Welcome screen title")
// Or in SwiftUI, just:
Text("Welcome!")// WRONG -- word order varies by language
let greeting = String(localized: "Hello") + ", " + name + "!"// CORRECT -- translators can reorder placeholders
let greeting = String(localized: "Hello, \(name)!")// WRONG -- US-only format
let formatter = DateFormatter()
formatter.dateFormat = "MM/dd/yyyy" // Meaningless in most countries// CORRECT -- adapts to user locale
Text(date, format: .dateTime.month().day().year())// WRONG -- German text is ~30% longer than English
Text(title).frame(width: 120)// CORRECT
Text(title).fixedSize(horizontal: false, vertical: true)
// Or use VStack/wrapping that accommodates expansion// WRONG -- does not flip for RTL
HStack { Spacer(); text }.padding(.left, 16)// CORRECT
HStack { Spacer(); text }.padding(.leading, 16)// WRONG -- not localized
let errorMessage = "Something went wrong"
showAlert(message: errorMessage)// CORRECT
let errorMessage = LocalizedStringResource("Something went wrong")
showAlert(message: String(localized: errorMessage))LocalizedStringKeyString(localized:)FormatStyle.leading.trailing.left.right.flipsForRightToLeftLayoutDirection(true)LocalizedStringResourceNSLocalizedString@ScaledMetricLocalizedStringResourceFormatStyleString(localized:)fetchAppleDocumentation path: "/documentation/foundation/localizedstringresource"
fetchAppleDocumentation path: "/documentation/foundation/formatstyle"
fetchAppleDocumentation path: "/documentation/swiftui/localizedstringkey"