Loading...
Loading...
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
npx skill4agent add charleswiltgen/axiom axiom-localization.xcstrings.strings.stringsdict.strings.stringsdict.xcstrings#bundleLocalizedStringResource.stringsLocalizable.xcstringsTextLabelButtonString(localized:)NSLocalizedStringCFCopyLocalizedString.storyboard.xib.xcstrings{
"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"
}String// ✅ Automatically localizable
Text("Welcome to WWDC!")
Label("Thanks for shopping with us!", systemImage: "bag")
Button("Checkout") { }
// Xcode extracts these strings to String CatalogLocalizedStringKey// 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")commentimport 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."
)LocalizedStringResource// 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]Info.plistInfoPlist.strings// InfoPlist.strings (Spanish)
"CFBundleName" = "Mi Aplicación";
"NSCameraUsageDescription" = "La app necesita acceso a la cámara para tomar fotos.";// Xcode automatically creates plural variations
Text("\(count) items")
// With custom formatting
Text("\(visitorCount) Recent Visitors"){
"strings" : {
"%lld Recent Visitors" : {
"localizations" : {
"en" : {
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld Recent Visitor"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld Recent Visitors"
}
}
}
}
}
}
}
}
}<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><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")songCountalbumCount// Same code, different strings per device
Text("Bird Food Shop"){
"Bird Food Shop" : {
"localizations" : {
"en" : {
"variations" : {
"device" : {
"applewatch" : {
"stringUnit" : {
"value" : "Bird Food"
}
},
"other" : {
"stringUnit" : {
"value" : "Bird Food Shop"
}
}
}
}
}
}
}
}Text("Application Settings")// ✅ Automatically mirrors for Arabic/Hebrew
HStack {
Image(systemName: "chevron.right")
Text("Next")
}
// iPhone (English): [>] Next
// iPhone (Arabic): Next [<]// ✅ Correct - mirrors automatically
.padding(.leading, 16)
.frame(maxWidth: .infinity, alignment: .leading)
// ❌ Wrong - doesn't mirror
.padding(.left, 16)
.frame(maxWidth: .infinity, alignment: .left)// ✅ Directional - mirrors for RTL
Image(systemName: "chevron.forward")
// ✅ Non-directional - never mirrors
Image(systemName: "star.fill")
// Custom images
Image("backButton")
.flipsForRightToLeftLayoutDirection(true)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"// ❌ Wrong - breaks in other locales
formatter.dateFormat = "MM/dd/yyyy"
// ✅ Correct - adapts to locale
formatter.dateStyle = .shortlet 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
}
}
}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)"
]
)
}
}.strings.xcstrings.strings.strings.stringsdict.xcstringsLocalizable.xcstringsString(localized:).strings.stringsNSLocalizedString.strings.stringsdict.strings.xcstrings// ❌ 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")// ❌ 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")String.localizedStringWithFormat// ✅ Correct
Text("\(count) items")
// ❌ Wrong
Text(String.localizedStringWithFormat(NSLocalizedString("%d items", comment: ""), count))import Foundation // Required for #bundle
Text("My Collections", bundle: #bundle, comment: "Section title")#bundle.module.xcstrings#bundle// ❌ Typo - fails silently at runtime
Text("App.HomeScren.Title") // Missing 'e' in Screen// ✅ Type-safe - compiler catches typos
Text(.appHomeScreenTitle)| String Type | Generated Symbol Type | Usage Example |
|---|---|---|
| No placeholders | Static property | |
| With placeholders | Function with labeled arguments | |
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)"The text label on a button to cancel the deletion of a collection"
<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>.main// ❌ Wrong - uses main bundle, strings not found
Text("My Collections", comment: "Section title")#bundle// ✅ Correct - automatically uses package/framework bundle
Text("My Collections", bundle: #bundle, comment: "Section title").module// Main app
Text("My Collections",
tableName: "Discover",
comment: "Section title")
// Framework or Swift Package
Text("My Collections",
tableName: "Discover",
bundle: #bundle,
comment: "Section title")LocalizedStringResourceText(.welcomeMessage) // From Localizable.xcstrings// From Discover.xcstrings
Text(Discover.featuredCollection)
// From Settings.xcstrings
Text(Settings.privacyPolicy)TextButtonString(localized:)// ✅ String extraction workflow
Text("Welcome to WWDC!", comment: "Main welcome message")// ✅ 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 |
// Before
Text("Welcome to WWDC!", comment: "Main welcome message")
// After refactoring
Text(.welcomeToWWDC)Text()String(localized:)bundle: #bundle#bundle