
Ios Localization
- 3k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
ios-localization is an agent skill for implementing localization and internationalization in iOS and macOS apps using String Catalogs, FormatStyle, and RTL-safe SwiftUI patterns.
About
iOS Localization and Internationalization teaches software engineers to localize iOS 26 plus apps with String Catalogs, generated localizable symbols, modern string types, FormatStyle, and right-to-left layout rules. The skill covers automatic string extraction in SwiftUI, String(localized:) for programmatic strings, LocalizedStringResource for widgets and App Intents, plural variants in xcstrings, locale-aware number and date formatting, and RTL testing with layoutDirection overrides. It documents common mistakes like concatenating localized strings, hard-coding date formats, using fixed-width layouts, and relying on NSLocalizedString in new Swift code. A review checklist verifies user-facing strings, pluralization, FormatStyle usage, leading and trailing alignment, German and Arabic testing, and generated symbol keys. Developers invoke it when adding multi-language support, setting up String Catalogs, handling plural forms, formatting currencies for locales, or fixing RTL layouts for Arabic and Hebrew markets.
- Documents String Catalogs, generated symbols, and LocalizedStringResource decision guide.
- Covers pluralization, device variations, and inflection syntax in xcstrings.
- FormatStyle section mandates locale-aware dates, numbers, currency, and measurements.
- RTL rules require leading and trailing alignment instead of left and right offsets.
- Review checklist includes pseudolocalization and Dynamic Type spacing guidance.
Ios Localization by the numbers
- 2,982 all-time installs (skills.sh)
- +134 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #48 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
ios-localization capabilities & compatibility
- Capabilities
- string catalogs · pluralization · locale formatting · rtl layout · generated symbols · swiftui extraction · localization review
- Use cases
- frontend · translation
What ios-localization says it does
String Catalogs are the recommended Xcode 15+ workflow for new localization work.
Never hard-code date, number, or measurement formats. Use `FormatStyle`
Localization mistakes cause App Store rejections in non-English markets
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill ios-localizationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
How do I add multi-language support with correct plurals, locale formatting, and RTL layouts in a SwiftUI app?
Implement String Catalogs, pluralization, FormatStyle locale formatting, and RTL-safe SwiftUI layouts for iOS and macOS apps.
Who is it for?
iOS developers adding String Catalogs, plural rules, and locale formatting before App Store release in multiple regions.
Skip if: Android or React Native localization, or backend-only APIs with no user-facing UI strings.
When should I use this skill?
Use when adding multi-language support, setting up String Catalogs, enabling generated symbols, handling plurals, formatting dates and currencies per locale, or fixing RTL layouts.
What you get
Localized xcstrings entries, generated symbols, locale-aware FormatStyle usage, and RTL-tested SwiftUI screens ready for translation.
- .xcstrings String Catalog files
- localized SwiftUI views
- RTL and pluralization test guidance
Files
iOS Localization & Internationalization
Localize iOS 26+ apps using String Catalogs, modern string types, FormatStyle, and RTL-aware layout. Localization mistakes cause App Store rejections in non-English markets, mistranslated UI, and broken layouts. Ship with correct localization from the start.
Contents
- String Catalogs (.xcstrings)
- String Catalogs (Xcode 15+) and Generated Symbols (Xcode 26+)
- String Types -- Decision Guide
- String Interpolation in Localized Strings
- Pluralization
- FormatStyle -- Locale-Aware Formatting
- Right-to-Left (RTL) Layout
- Common Mistakes
- Localization Review Checklist
- References
String Catalogs (.xcstrings)
String Catalogs are the recommended Xcode 15+ workflow for new localization work. They keep localizable strings, pluralization rules, and device variations together in an Xcode-managed JSON file with a visual editor. Legacy .strings and .stringsdict files can coexist during migration, but new Swift and SwiftUI code should default to String Catalogs.
Why String Catalogs exist:
.stringsfiles required manual key management and fell out of sync.stringsdictrequired complex XML for plurals- String Catalogs auto-extract strings from code, track translation state, and support plurals natively
How automatic extraction works:
Xcode scans for these patterns on each build:
// 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 XcodeXcode adds discovered keys to the String Catalog automatically. Mark translations as Needs Review, Translated, or Stale in the editor.
For detailed String Catalog workflows, migration, and testing strategies, see references/string-catalogs.md.
String Catalogs (Xcode 15+) and Generated Symbols (Xcode 26+)
For generated-symbol or migration answers, start by stating: "String Catalogs are the recommended Xcode 15+ localization workflow. Xcode 26 generated symbols are a separate typed-access layer on top of String Catalogs." Then explain generated symbols, plurals, or migration details. Do not describe catalogs themselves as requiring Xcode 26 or iOS 17.
Enable: Build Settings > Localization > Generate String Catalog Symbols → Yes (on by default in new Xcode 26 projects). Requires catalog format version 1.1.
Workflow: Add a key manually via the (+) button in the String Catalog editor — manual keys have the Generate Swift Symbol checkbox enabled by default. Auto-extracted keys can also opt in via Refactor > Convert Strings to Symbols. Use stable manual keys for generated-symbol strings. Avoid source-copy-derived keys for API-facing strings because wording edits can rename generated identifiers and churn call sites.
// Generated from key "room_available" in Localizable.xcstrings
Text(.roomAvailable)
// Parameterized key "landmarks_count" with %1$(count)lld
Text(.landmarksCount(count: 42))
// Non-default table "Booking.xcstrings"
Text(.Booking.confirmBookingCta)Xcode derives symbol names by camelCasing the key: settings.notifications.toggle → .settingsNotificationsToggle. You can convert existing extracted strings to symbols via Refactor > Convert Strings to Symbols (reversible).
Generated symbols are internal. For cross-module access, create a public wrapper extension. For heavier multi-module setups, use xcstrings-tool instead.
For the full generated symbols reference — extraction states, symbol derivation rules, and cross-module patterns — see references/string-catalogs.md.
String Types -- Decision Guide
LocalizedStringKey (SwiftUI default)
SwiftUI views accept LocalizedStringKey for their text parameters. String literals are implicitly converted -- no extra work needed.
// These all create a LocalizedStringKey lookup automatically:
Text("Welcome back")
Label("Profile", systemImage: "person")
Button("Delete") { deleteItem() }
.navigationTitle("Home")Use LocalizedStringKey when passing strings directly to SwiftUI view initializers. Do not construct LocalizedStringKey manually in most cases.
String(localized:) -- Modern NSLocalizedString replacement
Use for any localized string outside a SwiftUI view initializer. Returns a plain String. The literal/interpolated initializer is available iOS 15+; resolving a LocalizedStringResource is iOS 16+.
// 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")For Swift package localization failures, answer with this explicit resource checklist before bundle debugging: 1. Package.swift declares defaultLocalization. 2. The target resources list processes the catalog location, such as .process("Resources"). 3. Localizable.xcstrings is actually inside that processed target-resource path. Only after those pass, debug lookup with bundle: .module or Text(..., bundle: .module).
Existing NSLocalizedString literal keys can still be exported or migrated by Xcode tooling, but new Swift code should prefer String(localized:), SwiftUI literals, LocalizedStringResource, or generated symbols.
LocalizedStringResource -- Pass localization info without resolving
Use when a string must be carried as a localizable value for later resolution, especially for App Intents, widgets, notifications, generated localizable symbols, and system APIs that accept LocalizedStringResource directly. Use String(localized:) when code needs the resolved string immediately. Available iOS 16+.
// 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)
}When to use each type
| Context | Type | Why |
|---|---|---|
| SwiftUI view text parameters | LocalizedStringKey (implicit) | SwiftUI handles lookup automatically |
| Computed strings in view models / services | String(localized:) | Returns resolved String for logic |
| App Intents, widgets, system APIs | LocalizedStringResource | Framework resolves at display time |
| Error messages shown to users | String(localized:) | Resolved in catch blocks |
| Logging / analytics (not user-facing) | Plain String | No localization needed |
String Interpolation in Localized Strings
Interpolated values in localized strings become positional arguments that translators can reorder.
// 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.")In the String Catalog, this appears with %@ and %lld placeholders that translators can reorder:
- English:
"Welcome, %@! You have %lld new messages." - Japanese:
"%@さん、新しいメッセージが%lld件あります。"
Type-safe interpolation (preferred over format specifiers):
// Interpolation provides type safety
String(localized: "Score: \(score, format: .number)")
String(localized: "Due: \(date, format: .dateTime.month().day())")Pluralization
String Catalogs handle pluralization natively -- no .stringsdict XML required.
Setup in String Catalog
When a localized string contains an integer interpolation, Xcode detects it and offers plural variants in the String Catalog editor. Supply translations for each CLDR plural category:
| 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 |
English uses only one and other. Arabic uses all six. Always supply other as the fallback.
// Code -- single interpolation triggers plural support
Text("\(unreadCount) unread messages")
// String Catalog entries (English):
// one: "%lld unread message"
// other: "%lld unread messages"Device Variations
String Catalogs support device-specific text (iPhone vs iPad vs Mac):
// 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"Grammar Agreement (iOS 15+)
Use ^[...] inflection syntax for automatic grammatical agreement:
// 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"FormatStyle -- Locale-Aware Formatting
Never hard-code date, number, or measurement formats. Use FormatStyle (iOS 15+) so formatting adapts to the user's locale automatically.
Locale-aware formatting matters even in single-language apps because user locale affects separators, calendars, currency, units, names, and list formatting. When giving user-facing formatting advice, explicitly recommend testing or previewing output under multiple locales such as en_US, de_DE, ar_SA, and ja_JP.
ios-localization owns FormatStyle guidance when the issue is locale-aware user-facing display, including numbers, dates, currency, units, names, lists, calendars, separators, and locale preview/testing. For custom FormatStyle, ParseableFormatStyle, parsing, Date.IntervalFormatStyle, URL.FormatStyle, or reusable formatter API design, route to swift-formatstyle; keep ios-localization advice to locale risks and testing unless implementation is explicitly requested.
Dates
let 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())Numbers
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)Measurements
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, PersonName, Lists
// 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)For the complete FormatStyle reference, custom styles, and RTL layout, see references/formatstyle-locale.md.
Right-to-Left (RTL) Layout
SwiftUI automatically mirrors layouts for RTL languages (Arabic, Hebrew, Urdu, Persian). Most views require zero changes.
What SwiftUI auto-mirrors
HStackchildren reverse order.leading/.trailingalignment and padding swap sidesNavigationStackback button moves to trailing edgeListdisclosure indicators flip- Text alignment follows reading direction
What needs manual attention
// 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)Layout rules
- DO use
.leading/.trailing-- they auto-flip for RTL - DON'T use
.left/.right-- they are fixed and break RTL - DO use
HStack/VStack-- they respect layout direction - DON'T use absolute
offset(x:)for directional positioning
Common Mistakes
DON'T: Use NSLocalizedString in new Swift code
// LEGACY -- Xcode can export literal keys, but new Swift code should use modern APIs
let title = NSLocalizedString("welcome_title", comment: "Welcome screen title")DO: Use String(localized:) or let SwiftUI handle it
// CORRECT
let title = String(localized: "welcome_title",
defaultValue: "Welcome!",
comment: "Welcome screen title")
// Or in SwiftUI, just:
Text("Welcome!")DON'T: Concatenate localized strings
// WRONG -- word order varies by language
let greeting = String(localized: "Hello") + ", " + name + "!"DO: Use string interpolation
// CORRECT -- translators can reorder placeholders
let greeting = String(localized: "Hello, \(name)!")DON'T: Hard-code date/number formats
// WRONG -- US-only format
let formatter = DateFormatter()
formatter.dateFormat = "MM/dd/yyyy" // Meaningless in most countriesDO: Use FormatStyle
// CORRECT -- adapts to user locale
Text(date, format: .dateTime.month().day().year())DON'T: Use fixed-width layouts
// WRONG -- German text is ~30% longer than English
Text(title).frame(width: 120)DO: Use flexible layouts
// CORRECT
Text(title).fixedSize(horizontal: false, vertical: true)
// Or use VStack/wrapping that accommodates expansionDON'T: Use .left / .right for alignment
// WRONG -- does not flip for RTL
HStack { Spacer(); text }.padding(.left, 16)DO: Use .leading / .trailing
// CORRECT
HStack { Spacer(); text }.padding(.leading, 16)DON'T: Put user-facing strings as plain String outside SwiftUI
// WRONG -- not localized
let errorMessage = "Something went wrong"
showAlert(message: errorMessage)DO: Use LocalizedStringResource for deferred resolution
// CORRECT
let errorMessage = LocalizedStringResource("Something went wrong")
showAlert(message: String(localized: errorMessage))DON'T: Use natural-language text as the key for manually-managed strings
// WRONG -- typo silently creates a new key, stales the old one, no compiler error
Text("Wlecome Back") // was "Welcome Back" -- silent localization breakDO: Use stable symbol-style keys and enable generated symbols
// CORRECT -- key is stable; UI text lives in the catalog's default value
Text(.welcomeBack) // generated from key "welcome_back" in String Catalog
// Or without generated symbols:
String(localized: "welcome_back", defaultValue: "Welcome Back")DON'T: Skip pseudolocalization testing
Testing only in English hides truncation, layout, and RTL bugs.
DO: Test with German (long) and Arabic (RTL) at minimum
Use Xcode scheme settings to override the app language without changing device locale.
Review Checklist
- [ ] All user-facing strings use localization (
LocalizedStringKeyin SwiftUI orString(localized:)) - [ ] No string concatenation for user-visible text
- [ ] Dates and numbers use
FormatStyle, not hardcoded formats - [ ] Pluralization handled via String Catalog plural variants (not manual if/else)
- [ ] Layout uses
.leading/.trailing, not.left/.right - [ ] UI tested with long text (German) and RTL (Arabic)
- [ ] String Catalog includes all target languages
- [ ] Images needing RTL mirroring use
.flipsForRightToLeftLayoutDirection(true) - [ ] App Intents and widgets use
LocalizedStringResource - [ ] No
NSLocalizedStringusage in new code - [ ] Comments provided for ambiguous keys (context for translators)
- [ ]
@ScaledMetricused for spacing that must scale with Dynamic Type - [ ] Currency formatting uses explicit currency code, not locale default
- [ ] Pseudolocalization tested (accented, right-to-left, double-length)
- [ ] Manually-managed keys use stable symbol-style names, not English text as the key
- [ ] Generate String Catalog Symbols enabled for targets with manually-managed keys
- [ ] Ensure localized string types are Sendable; use @MainActor for locale-change UI updates
References
- FormatStyle patterns: references/formatstyle-locale.md
- String Catalogs guide: references/string-catalogs.md
{
"skill_name": "ios-localization",
"evals": [
{
"id": 0,
"name": "generated-symbols-plurals",
"prompt": "I am updating an iOS 26 SwiftUI booking app for localization. We have a new Localizable.xcstrings file, a manually managed key named room_available whose English value is \"Book this room\", and a parameterized key named landmarks_count for \"42 landmarks\". Give me a concise migration plan and Swift snippets that use Xcode generated localizable symbols, plural-friendly placeholders, and modern string APIs. Also say what to do with old NSLocalizedString calls.",
"expected_output": "A concise migration plan that uses String Catalogs, generated symbols, correct positional named placeholders, modern String(localized:) / LocalizedStringResource guidance, plural variants, and legacy NSLocalizedString migration notes.",
"files": [],
"expectations": [
"Frames String Catalogs as the Xcode 15+ recommended workflow and does not claim they require iOS 17.",
"Mentions enabling Generate String Catalog Symbols and using stable manually managed keys.",
"Uses generated symbol examples such as Text(.roomAvailable) and Text(.landmarksCount(count: 42)).",
"Uses a positional named placeholder such as %1$(count)lld for the parameterized generated symbol.",
"Distinguishes String(localized:) for resolved strings from LocalizedStringResource for deferred or system-framework resolution.",
"Treats NSLocalizedString as legacy for new Swift code while acknowledging literal keys can still be exported or migrated."
]
},
{
"id": 1,
"name": "package-bundle-localization",
"prompt": "Please review this Swift package localization setup. SharedUI has Resources/Localizable.xcstrings and Package.swift includes .process(\"Resources\"). Inside the package target we wrote Text(\"Save\") and String(localized: \"settings.title\") with no bundle argument. In the app target the same keys work, but in the package they fall back to English. What should we change?",
"expected_output": "A review that explains package resources need explicit bundle lookup, recommends bundle: .module for Swift Package strings and SwiftUI Text, and keeps the guidance scoped to localization rather than package architecture.",
"files": [],
"expectations": [
"States that code outside the main app bundle needs an explicit bundle for localized resources.",
"Recommends String(localized: \"settings.title\", bundle: .module) or equivalent for Swift Package code.",
"Recommends SwiftUI Text with an explicit package bundle, such as Text(\"Save\", bundle: .module), or an equivalent localized label wrapper.",
"Does not claim SwiftUI Text inside a package automatically uses .module.",
"Mentions verifying the .xcstrings file is included in the package target resources.",
"Keeps the answer focused on localization bundle lookup rather than unrelated package architecture."
]
},
{
"id": 2,
"name": "formatstyle-boundary",
"prompt": "I am not adding new languages yet. I need to design a custom ParseableFormatStyle for follower counts like 12.5K, audit Date.IntervalFormatStyle usage, and format URLs for display. Should the iOS localization skill own this, or is there a better skill/domain? Give a short routing answer with the minimum localization advice that still matters.",
"expected_output": "A boundary answer that routes deep standalone FormatStyle API design to swift-formatstyle while giving minimal locale-facing localization cautions.",
"files": [],
"expectations": [
"Routes custom ParseableFormatStyle, Date.IntervalFormatStyle, and URL.FormatStyle design to the swift-formatstyle skill or domain.",
"Explains that ios-localization covers FormatStyle when the task is locale-aware display for localization and internationalization.",
"Keeps minimum localization advice, such as not hardcoding user-facing formats and respecting user locale.",
"Does not expand into a full custom FormatStyle implementation.",
"Does not collapse swift-formatstyle's standalone formatting scope into ios-localization."
]
}
]
}
FormatStyle & Locale-Aware Formatting
Comprehensive reference for locale-aware formatting in iOS 15+ using FormatStyle. Never hard-code date, number, or measurement formats -- these break in every locale except the one you tested. ios-localization owns FormatStyle guidance when the issue is locale-aware user-facing display: numbers, dates, currency, units, names, lists, calendars, separators, and locale preview/testing. Locale-aware formatting matters even in single-language apps; explicitly recommend testing or previewing user-facing output under multiple locales. Use the swift-formatstyle skill for broader standalone FormatStyle API design.
Contents
- Date Formatting
- Number Formatting
- Measurement Formatting
- Duration Formatting
- PersonNameComponents Formatting
- ByteCountFormatStyle
- ListFormatStyle
- Custom FormatStyle Implementation
- Forcing a Specific Locale
- RTL Layout Deep Dive
- `@ScaledMetric for Dynamic Type`
- Layout Testing with Accessibility Inspector
- Quick Reference Table
Migration from Legacy Formatters
FormatStyle (iOS 15+) replaces the older Formatter subclasses. If you encounter legacy code, migrate to FormatStyle:
| Legacy | Modern replacement |
|---|---|
DateFormatter | .formatted(.dateTime...) or Date.FormatStyle |
NumberFormatter | .formatted(.number...) or IntegerFormatStyle / FloatingPointFormatStyle |
DateComponentsFormatter | Duration.formatted(.units(...)) or .time(pattern:) |
MeasurementFormatter | Measurement.formatted(.measurement(...)) |
DateIntervalFormatter | (start..<end).formatted(date:time:) |
PersonNameComponentsFormatter | .formatted(.name(style:)) |
ByteCountFormatter | .formatted(.byteCount(style:)) |
ListFormatter | .formatted(.list(type:)) |
FormatStyle is value-type, Sendable, composable, and works directly in SwiftUI Text views. The legacy formatters are reference types that require manual locale and calendar configuration.
Date Formatting
Preset date and time styles
let date = Date.now
// Date only
date.formatted(date: .numeric, time: .omitted) // "1/15/2026" (US) / "15.01.2026" (DE)
date.formatted(date: .abbreviated, time: .omitted) // "Jan 15, 2026" (US) / "15. Jan. 2026" (DE)
date.formatted(date: .long, time: .omitted) // "January 15, 2026" (US) / "15. Januar 2026" (DE)
date.formatted(date: .complete, time: .omitted) // "Thursday, January 15, 2026" (US)
// Time only
date.formatted(date: .omitted, time: .shortened) // "3:30 PM" (US) / "15:30" (DE)
date.formatted(date: .omitted, time: .standard) // "3:30:45 PM" (US) / "15:30:45" (DE)
date.formatted(date: .omitted, time: .complete) // includes time zone
// Combined
date.formatted(date: .long, time: .shortened) // "January 15, 2026 at 3:30 PM"
date.formatted() // platform defaultComponent-based date formatting
Build custom date formats by composing components. The system reorders components for each locale.
// Month and day
date.formatted(.dateTime.month().day()) // "Jan 15" (US) / "15 Jan" (UK)
// Full date with weekday
date.formatted(.dateTime.weekday(.wide).month(.wide).day().year())
// "Thursday, January 15, 2026" (US) / "Donnerstag, 15. Januar 2026" (DE)
// Month name styles
date.formatted(.dateTime.month(.wide)) // "January"
date.formatted(.dateTime.month(.abbreviated)) // "Jan"
date.formatted(.dateTime.month(.narrow)) // "J"
date.formatted(.dateTime.month(.twoDigits)) // "01"
// Day styles
date.formatted(.dateTime.day(.twoDigits)) // "15"
date.formatted(.dateTime.day(.ordinalOfDayInMonth)) // "3" (third Thursday)
// Year
date.formatted(.dateTime.year(.defaultDigits)) // "2026"
date.formatted(.dateTime.year(.twoDigits)) // "26"
// Hour/minute
date.formatted(.dateTime.hour().minute()) // "3:30 PM" (US, 12h) / "15:30" (DE, 24h)
date.formatted(.dateTime.hour(.defaultDigits(amPM: .omitted)).minute()) // "3:30"SwiftUI date display
// Automatic format
Text(event.date, format: .dateTime.month().day().year())
// Date range
Text(event.start...event.end) // "Jan 15 - Jan 20, 2026"
// Relative (auto-updates)
Text(event.date, style: .relative) // "2 hours ago", "in 3 days"
Text(event.date, style: .timer) // counts up/down live
Text(event.date, style: .offset) // "+2 hours" / "-3 days"Relative date formatting
// Relative (named style)
let relative = date.formatted(.relative(presentation: .named))
// "yesterday", "today", "tomorrow", "last Friday", "in 2 weeks"
// Relative (numeric style)
let relativeNum = date.formatted(.relative(presentation: .numeric))
// "1 day ago", "in 2 days", "3 weeks ago"
// Relative with specific units
let relativeCustom = date.formatted(.relative(presentation: .named, unitsStyle: .wide))Date ranges and intervals
let start = Date.now
let end = Calendar.current.date(byAdding: .day, value: 5, to: start)!
// Range formatting
(start..<end).formatted(date: .abbreviated, time: .omitted)
// "Jan 15 - 20, 2026" (smart about shared month/year)
// Duration of interval
(start..<end).formatted(.components(style: .wide))
// "5 days"ISO 8601 (for APIs, not for display)
// For serialization to APIs -- NOT for user-facing display
date.formatted(.iso8601) // "2026-01-15T15:30:45Z"
date.formatted(.iso8601.dateSeparator(.dash).timeSeparator(.colon))Number Formatting
Integer and decimal
let value = 1234567
value.formatted() // "1,234,567" (US) / "1.234.567" (DE) / "1 234 567" (FR)
value.formatted(.number.grouping(.never)) // "1234567"
value.formatted(.number.precision(.significantDigits(3))) // "1,230,000"
let decimal = 3.14159
decimal.formatted(.number.precision(.fractionLength(2))) // "3.14"
decimal.formatted(.number.precision(.fractionLength(0...3))) // "3.142"
// Notation
value.formatted(.number.notation(.compactName)) // "1.2M" (US) / "1,2 Mio." (DE)
value.formatted(.number.notation(.scientific)) // "1.234567E6"Rounding
let num = 3.456
// Round to 2 fraction digits
num.formatted(.number.precision(.fractionLength(2)).rounded(rule: .up)) // "3.46"
num.formatted(.number.precision(.fractionLength(2)).rounded(rule: .down)) // "3.45"
num.formatted(.number.precision(.fractionLength(2)).rounded(rule: .toNearestOrEven)) // "3.46"Percent
let ratio = 0.856
ratio.formatted(.percent) // "86%" (US) / "86 %" (FR)
ratio.formatted(.percent.precision(.fractionLength(1))) // "85.6%"
// Integer percentage
let score = 92
score.formatted(.percent) // "9,200%" -- probably not what you want!
// For integer percentages, divide first:
(Double(score) / 100).formatted(.percent) // "92%"Currency
Always specify the currency code explicitly. The locale controls formatting (symbol position, decimal separator), but the currency code determines the currency.
let price = Decimal(29.99)
// Explicit currency code (recommended)
price.formatted(.currency(code: "USD")) // "$29.99" (US) / "29,99 $US" (FR) / "US$29.99" (AU)
price.formatted(.currency(code: "EUR")) // "EUR29.99" (US) / "29,99 EUR" (DE) / "29,99 EUR" (FR)
price.formatted(.currency(code: "JPY")) // "JPY30" (no decimals for yen)
// Narrow symbol (when space is limited)
price.formatted(.currency(code: "USD").presentation(.narrow)) // "$29.99" even in non-US locales
// In SwiftUI
Text(price, format: .currency(code: order.currencyCode))Important: Use Decimal (not Double) for monetary values to avoid floating-point precision errors.
Ordinal numbers
let position = 3
position.formatted(.number.notation(.ordinal)) // "3rd" (EN) / "3." (DE) / "3e" (FR)Measurement Formatting
The system auto-converts units based on locale (metric vs imperial) unless you opt out.
Length / distance
let distance = Measurement(value: 5, unit: UnitLength.kilometers)
distance.formatted(.measurement(width: .wide))
// US: "3.1 miles" (auto-converts to imperial!)
// DE: "5 Kilometer"
// JP: "5 km" (with .abbreviated)
distance.formatted(.measurement(width: .abbreviated)) // "3.1 mi" (US) / "5 km" (DE)
distance.formatted(.measurement(width: .narrow)) // "3.1mi" (US) / "5km" (DE)
// Prevent auto-conversion (keep original unit)
distance.formatted(.measurement(width: .wide, usage: .asProvided))
// US: "5 kilometers" (keeps km even in US locale)Weight / mass
let weight = Measurement(value: 75, unit: UnitMass.kilograms)
weight.formatted(.measurement(width: .wide))
// US: "165.3 pounds" / DE: "75 Kilogramm"
weight.formatted(.measurement(width: .abbreviated, usage: .personWeight))
// Uses locale-appropriate unit for body weightTemperature
let temp = Measurement(value: 22, unit: UnitTemperature.celsius)
temp.formatted(.measurement(width: .abbreviated))
// US: "72 F" (auto-converts!) / FR: "22 C" / DE: "22 C"
// Weather-specific (ensures locale-correct unit)
temp.formatted(.measurement(width: .abbreviated, usage: .weather))Speed
let speed = Measurement(value: 100, unit: UnitSpeed.kilometersPerHour)
speed.formatted(.measurement(width: .abbreviated))
// US: "62.1 mph" / DE: "100 km/h"Volume
let volume = Measurement(value: 500, unit: UnitVolume.milliliters)
volume.formatted(.measurement(width: .abbreviated, usage: .drink))
// US: "16.9 fl oz" / DE: "500 ml"Duration Formatting
Time pattern
let dur = Duration.seconds(3661) // 1 hour, 1 minute, 1 second
dur.formatted(.time(pattern: .hourMinuteSecond)) // "1:01:01"
dur.formatted(.time(pattern: .hourMinute)) // "1:01"
dur.formatted(.time(pattern: .minuteSecond)) // "61:01"Units style (iOS 16+)
dur.formatted(.units(allowed: [.hours, .minutes], width: .wide))
// "1 hour, 1 minute" (EN) / "1 Stunde, 1 Minute" (DE)
dur.formatted(.units(allowed: [.hours, .minutes], width: .abbreviated))
// "1 hr, 1 min" (EN) / "1 Std., 1 Min." (DE)
dur.formatted(.units(allowed: [.hours, .minutes], width: .narrow))
// "1h 1m"
// Maximum unit count
dur.formatted(.units(allowed: [.hours, .minutes, .seconds],
width: .abbreviated,
maximumUnitCount: 2))
// "1 hr, 1 min" (drops seconds)PersonNameComponents Formatting
Respects locale conventions for name ordering (given-family vs family-given).
var name = PersonNameComponents()
name.givenName = "John"
name.familyName = "Appleseed"
name.namePrefix = "Dr."
name.nickname = "Johnny"
name.formatted(.name(style: .long)) // "Dr. John Appleseed" (US) / "Appleseed John" (JP)
name.formatted(.name(style: .medium)) // "John Appleseed" (US) / "Appleseed John" (JP)
name.formatted(.name(style: .short)) // "John" (US) / "Appleseed" (JP)
name.formatted(.name(style: .abbreviated)) // "JA" (initials)
// In SwiftUI
Text(name, format: .name(style: .medium))ByteCountFormatStyle
Format file sizes with locale-appropriate units.
let bytes: Int64 = 1_536_000
bytes.formatted(.byteCount(style: .file)) // "1.5 MB"
bytes.formatted(.byteCount(style: .memory)) // "1.46 MB" (uses 1024-based)
bytes.formatted(.byteCount(style: .binary)) // "1.46 MB"
// Specific allowed units
bytes.formatted(.byteCount(style: .file, allowedUnits: [.kb])) // "1,536 kB"ListFormatStyle
Join arrays into grammatically correct lists.
let fruits = ["Apples", "Oranges", "Bananas"]
fruits.formatted(.list(type: .and))
// EN: "Apples, Oranges, and Bananas"
// FR: "Apples, Oranges et Bananas"
// AR: "Apples وOranges وBananas"
fruits.formatted(.list(type: .or))
// EN: "Apples, Oranges, or Bananas"
// With member formatting
let prices = [Decimal(1.99), Decimal(2.49), Decimal(3.99)]
prices.formatted(.list(memberStyle: .currency(code: "USD"), type: .and))
// "$1.99, $2.49, and $3.99"
// Two items
["Red", "Blue"].formatted(.list(type: .and))
// "Red and Blue" (no Oxford comma for two items)Custom FormatStyle Implementation
Create a reusable FormatStyle for domain-specific formatting.
struct AbbreviatedCountStyle: FormatStyle {
func format(_ value: Int) -> String {
switch value {
case ..<1_000:
return "\(value)"
case 1_000..<1_000_000:
let k = Double(value) / 1_000.0
return k.formatted(.number.precision(.fractionLength(0...1))) + "K"
case 1_000_000..<1_000_000_000:
let m = Double(value) / 1_000_000.0
return m.formatted(.number.precision(.fractionLength(0...1))) + "M"
default:
let b = Double(value) / 1_000_000_000.0
return b.formatted(.number.precision(.fractionLength(0...1))) + "B"
}
}
}
extension FormatStyle where Self == AbbreviatedCountStyle {
static var abbreviatedCount: AbbreviatedCountStyle { .init() }
}
// Usage
let followers = 12_500
followers.formatted(.abbreviatedCount) // "12.5K"
// In SwiftUI
Text(followers, format: .abbreviatedCount)Custom ParseableFormatStyle (for input parsing)
struct AbbreviatedCountStyle: ParseableFormatStyle {
var parseStrategy: AbbreviatedCountParseStrategy { .init() }
func format(_ value: Int) -> String { /* same as above */ }
}
struct AbbreviatedCountParseStrategy: ParseStrategy {
func parse(_ value: String) throws -> Int {
let cleaned = value.uppercased().trimmingCharacters(in: .whitespaces)
if cleaned.hasSuffix("K") {
guard let num = Double(cleaned.dropLast()) else { throw parseError }
return Int(num * 1_000)
}
// ... handle M, B, plain numbers
guard let num = Int(cleaned) else { throw parseError }
return num
}
private var parseError: some Error {
DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: "Invalid count"))
}
}Forcing a Specific Locale
Occasionally you need a specific locale (server APIs, fixed-format exports). Use .locale() modifier:
// Force US format for API serialization (not user display)
let usPrice = price.formatted(.currency(code: "USD").locale(Locale(identifier: "en_US")))
// Force a date format for an API
let apiDate = date.formatted(.iso8601) // Prefer ISO 8601 for APIs
// Force German format for a German-language PDF export
let deDate = date.formatted(.dateTime.month(.wide).day().year().locale(Locale(identifier: "de_DE")))Warning: Never force a locale for user-facing UI. Always let the system locale drive user-visible formatting.
RTL Layout Deep Dive
How SwiftUI auto-mirrors
SwiftUI respects layoutDirection from the environment. When the user's language is RTL:
1. HStack: Children render right-to-left 2. Leading/Trailing: .leading = right side, .trailing = left side 3. Padding: .padding(.leading, 16) applies to right side 4. NavigationStack: Back button appears on trailing (left) side 5. Lists: Disclosure chevrons point left 6. ScrollView: Horizontal scrolling starts from the right 7. Text alignment: Default alignment follows reading direction
Image flipping
// Directional images SHOULD flip
Image(systemName: "chevron.forward")
.flipsForRightToLeftLayoutDirection(true)
Image(systemName: "arrow.right")
.flipsForRightToLeftLayoutDirection(true)
Image("progress-arrow")
.flipsForRightToLeftLayoutDirection(true)
// These should NOT flip:
// - Logos and brand marks
// - Photos and illustrations
// - Clock faces (clockwise is universal)
// - Music notation
// - Checkmarks
// - Mathematical symbols (+, -, =)
// - Media playback controls (play triangle always points right)
// SF Symbols with .rtl variant auto-flip (e.g., text.alignleft has text.alignright)
// Check SF Symbols app for RTL variantsEnvironment-based testing
// Preview with RTL
#Preview("Arabic RTL") {
ContentView()
.environment(\.layoutDirection, .rightToLeft)
.environment(\.locale, Locale(identifier: "ar"))
}
// Preview with both directions side by side
#Preview("LTR vs RTL") {
HStack(spacing: 0) {
ContentView()
.environment(\.layoutDirection, .leftToRight)
.frame(maxWidth: .infinity)
Divider()
ContentView()
.environment(\.layoutDirection, .rightToLeft)
.environment(\.locale, Locale(identifier: "ar"))
.frame(maxWidth: .infinity)
}
}Semantic content attributes (UIKit interop)
When mixing UIKit views via UIViewRepresentable, set semantic content attribute:
class MyUIView: UIView {
override var semanticContentAttribute: UISemanticContentAttribute {
// .forceLeftToRight for phone numbers, code
// .forceRightToLeft to force RTL
// .unspecified to follow system (default)
.unspecified
}
}Bidirectional text
When mixing LTR and RTL text (e.g., English brand names in Arabic text), Unicode bidirectional algorithm handles it automatically. For edge cases:
// Force LTR for specific content within RTL context
Text("\u{200E}+1 (555) 123-4567") // LTR mark before phone number
// Or use environment override on a specific view
Text(phoneNumber)
.environment(\.layoutDirection, .leftToRight)Common RTL pitfalls
| Issue | Wrong | Correct |
|---|---|---|
| Fixed position | .padding(.left, 16) | .padding(.leading, 16) |
| Absolute offset | .offset(x: -20) for "move left" | Use alignment or .padding(.trailing) |
| Text alignment | .multilineTextAlignment(.left) | .multilineTextAlignment(.leading) |
| Corner radius | Only rounding top-left/top-right | Round leading/trailing corners |
| Swipe gestures | "Swipe right to delete" | "Swipe to leading edge" -- or use system gestures |
@ScaledMetric for Dynamic Type
Use @ScaledMetric to make custom spacing, icon sizes, and padding scale with the user's Dynamic Type setting.
struct ProfileRow: View {
@ScaledMetric(relativeTo: .body) private var avatarSize = 44.0
@ScaledMetric(relativeTo: .body) private var spacing = 12.0
var body: some View {
HStack(spacing: spacing) {
AvatarView()
.frame(width: avatarSize, height: avatarSize)
VStack(alignment: .leading) {
Text(name).font(.headline)
Text(subtitle).font(.subheadline)
}
}
}
}relativeTo parameter
@ScaledMetric scales proportionally to a text style. Choose the text style that the metric logically accompanies:
| Text style | Base size | Use for |
|---|---|---|
.body | 17pt | General spacing, icons next to body text |
.caption | 12pt | Small icons, fine spacing |
.title | 28pt | Large icons, hero spacing |
.largeTitle | 34pt | Hero images, splash elements |
Testing Dynamic Type
// Preview with large text
#Preview("Accessibility XXL") {
ContentView()
.dynamicTypeSize(.accessibility3)
}
// Preview matrix
#Preview("Dynamic Type Sizes") {
ScrollView {
ForEach(DynamicTypeSize.allCases, id: \.self) { size in
ContentView()
.dynamicTypeSize(size)
.padding()
.border(Color.gray)
}
}
}Layout Testing with Accessibility Inspector
Accessibility Inspector (Xcode > Open Developer Tool > Accessibility Inspector) provides:
1. Audit: Scans running app for accessibility issues including truncated text 2. Inspection: Shows exact font sizes and Dynamic Type response 3. Settings: Override Dynamic Type size, Bold Text, Reduce Motion on device without changing system settings
Quick test workflow
1. Launch app in Simulator 2. Open Accessibility Inspector, target the Simulator 3. Use the Settings panel to set Dynamic Type to "Accessibility XXL" 4. Navigate through every screen -- look for truncated text, overlapping elements, broken layouts 5. Switch to RTL (set language to Arabic in scheme options) 6. Repeat navigation -- check all alignment and reading order
Quick Reference Table
| Data type | FormatStyle | Example output (US) |
|---|---|---|
Date | .dateTime.month().day().year() | "Jan 15, 2026" |
Date range | (start..<end).formatted(date:time:) | "Jan 15 - 20, 2026" |
Date relative | .relative(presentation: .named) | "yesterday" |
Int | .number | "1,234,567" |
Int ordinal | .number.notation(.ordinal) | "3rd" |
Int compact | .number.notation(.compactName) | "1.2M" |
Double | .number.precision(.fractionLength(2)) | "3.14" |
Double | .percent | "85.6%" |
Decimal | .currency(code: "USD") | "$29.99" |
Measurement | .measurement(width: .abbreviated) | "5 km" / "3.1 mi" |
Duration | .time(pattern: .hourMinuteSecond) | "1:01:01" |
Duration | .units(width: .abbreviated) | "1 hr, 1 min" |
PersonNameComponents | .name(style: .medium) | "John Doe" |
Int64 (bytes) | .byteCount(style: .file) | "1.5 MB" |
[String] | .list(type: .and) | "A, B, and C" |
String Catalogs (.xcstrings) -- Detailed Reference
Contents
- What is a String Catalog?
- Creating a String Catalog
- Automatic String Extraction
- Manual Key Management
- Handling Strings in Non-SwiftUI Code
- Bundle Access Patterns
- Multi-Module / SPM Localization
- Pluralization in String Catalogs
- Device Variations
- Exporting for Translators (XLIFF / xcloc)
- String Catalog JSON Structure
- Generated Localizable Symbols (Xcode 26+)
- Testing Strategies
- Migration from .strings / .stringsdict
- Best Practices
What is a String Catalog?
A String Catalog is a single Xcode-managed .xcstrings file (JSON-based) that holds localizable strings in a target, along with translations, plural forms, and device variations. In Xcode 15 and later, String Catalogs are the recommended workflow for new localization work because they replace much of the manual synchronization previously required across .strings and .stringsdict files.
Availability: Xcode 15+, all Apple platforms. String Catalogs are the recommended Xcode 15+ workflow for app localization. Xcode 26 adds generated localizable symbols on top of String Catalogs; do not describe catalogs themselves as requiring Xcode 26 or iOS 17.
Creating a String Catalog
1. File > New > File > String Catalog 2. Name it Localizable.xcstrings (the default table name, matching the legacy Localizable.strings) 3. Place it in the target's source directory 4. Add target languages in Project > Info > Localizations
For a non-default table name (e.g., Onboarding.xcstrings), reference it explicitly:
String(localized: "welcome.title", table: "Onboarding")Automatic String Extraction
On every build, Xcode scans source files and extracts strings from known localizable initializers. Extraction is compiler-driven -- it recognizes these patterns:
SwiftUI (LocalizedStringKey)
Text("Hello, world") // extracted
Label("Settings", systemImage: "gear") // extracted
Button("Save") { } // extracted
Toggle("Enable notifications", isOn: $on) // extracted
.navigationTitle("Home") // extracted
Section("Account") { } // extracted
// NOT extracted -- computed or variable strings
Text(viewModel.title) // not extracted (runtime value)
Text(verbatim: "v1.2.3") // not extracted (verbatim skips localization)Foundation (String(localized:))
String(localized: "No results found") // extracted
String(localized: "error.title",
defaultValue: "Something went wrong",
comment: "Generic error alert title") // extracted with default + commentLocalizedStringResource
LocalizedStringResource("Order placed") // extracted
static var title: LocalizedStringResource = "Title" // extractedWhat is NOT extracted
let x: String = "Not localized" // plain String assignment
print("debug info") // not user-facing
NSLocalizedString("legacy", comment: "") // legacy API; Xcode can export literal keysPrefer String(localized:), SwiftUI localizable literals, or LocalizedStringResource in new Swift code so String Catalog syncing and generated-symbol workflows stay straightforward. If automatic extraction misses a string, add it manually in the String Catalog editor.
Manual Key Management
Open the .xcstrings file in Xcode to use the visual editor:
- Add key: Click + at the bottom of the key list
- Remove key: Select key, press Delete (marks as Stale, removed on next build if no code reference)
- Edit comment: Select key, edit the Comment field (provides translator context)
- Mark state: Right-click a translation to set Needs Review / Reviewed
- Vary by plural: Select a key, click Vary > Plural to add plural categories
- Vary by device: Select a key, click Vary > Device to add iPhone/iPad/Mac variants
Key naming conventions
For manually-managed strings, use stable symbol-style keys rather than English text as the key. This prevents silent localization breaks when UI copy changes (a typo or rewording just creates a new key and stales the old one — no compiler error). With Xcode 26's generated symbols, stable keys also produce readable, predictable Swift accessors.
onboarding.welcome.title -> "Welcome"
onboarding.welcome.subtitle -> "Get started in minutes"
settings.notifications.toggle -> "Enable Notifications"
error.network.title -> "Connection Error"
error.network.message -> "Check your internet and try again"Use String(localized:defaultValue:) when you want a structured key that differs from the English text:
let title = String(localized: "error.network.title",
defaultValue: "Connection Error",
comment: "Title for network error alert")For SwiftUI auto-extracted strings, the literal text IS the key by default. This is fine for simple views. For any string you manage manually — shared keys, keys referenced across modules, or keys where copy changes frequently — use a stable key instead.
Handling Strings in Non-SwiftUI Code
View models, services, and utilities
class OrderService {
func statusMessage(for order: Order) -> String {
switch order.status {
case .shipped:
return String(localized: "order.status.shipped",
defaultValue: "Your order has shipped!",
comment: "Order status when item is in transit")
case .delivered:
return String(localized: "order.status.delivered",
defaultValue: "Delivered on \(order.deliveryDate!, format: .dateTime.month().day())",
comment: "Order status with delivery date")
case .processing:
return String(localized: "order.status.processing",
defaultValue: "Processing your order...",
comment: "Order status while being prepared")
}
}
}Specifying table and bundle
// From a specific table
String(localized: "greeting",
table: "Onboarding",
comment: "First-launch greeting")
// From a specific bundle (framework or Swift package)
String(localized: "button.save",
table: "SharedUI",
bundle: .module,
comment: "Save button in shared component")Bundle Access Patterns
Main app
// Uses Bundle.main by default -- no bundle argument needed
String(localized: "Hello")Swift Package (SPM)
// .module refers to the package's resource bundle
String(localized: "Hello", bundle: .module)
// In SwiftUI, pass the package bundle explicitly for package resources
Text("Hello", bundle: .module)Framework
// Reference the framework's bundle
let frameworkBundle = Bundle(for: MyFrameworkClass.self)
String(localized: "Hello",
bundle: .init(frameworkBundle.bundleURL))Multi-Module / SPM Localization
Each Swift package target that contains user-facing strings needs its own String Catalog.
Package.swift setup
let package = Package(
name: "SharedUI",
defaultLocalization: "en",
targets: [
.target(
name: "SharedUI",
dependencies: [],
resources: [
.process("Resources") // Localizable.xcstrings goes here
]
)
]
)Directory structure
Sources/
SharedUI/
Resources/
Localizable.xcstrings <- String Catalog for this module
Views/
ButtonStyles.swiftAccessing strings from the package
// Inside the package -- bundle: .module resolves package-owned resources
public struct SaveButton: View {
public var body: some View {
Button(String(localized: "Save", bundle: .module)) { }
}
}Important: Code outside the main app bundle needs an explicit bundle. Use bundle: .module in Swift packages, Bundle(for:) in frameworks, or the current-target bundle macro when available.
For Swift package localization failures, answer with this explicit resource checklist before bundle debugging: 1. Package.swift declares defaultLocalization. 2. The target resources list processes the catalog location, such as .process("Resources"). 3. Localizable.xcstrings is actually inside that processed target-resource path. Only after those pass, debug lookup with bundle: .module or Text(..., bundle: .module).
Pluralization in String Catalogs
Setup
1. Write code with integer interpolation:
Text("\(itemCount) items in your cart")2. Build the project -- Xcode adds the key to the String Catalog 3. Open the String Catalog, select the key 4. Click "Vary by Plural" in the inspector 5. Fill in plural forms for each language
English plural forms
one: "%1$(itemCount)lld item in your cart"
other: "%1$(itemCount)lld items in your cart"Arabic plural forms (all six categories)
zero: "لا توجد عناصر في سلتك"
one: "عنصر واحد في سلتك"
two: "عنصران في سلتك"
few: "%lld عناصر في سلتك" (3-10)
many: "%lld عنصرًا في سلتك" (11-99)
other: "%lld عنصر في سلتك" (100+)Multiple plural variables
When a string has two integer interpolations, the String Catalog shows a matrix of plural combinations:
Text("\(photoCount) photos in \(albumCount) albums")
// English needs: one/one, one/other, other/one, other/otherDevice Variations
Enable "Vary by Device" for a key to provide different text on iPhone, iPad, Apple Watch, Mac, Apple TV, and Apple Vision Pro.
// Code is the same everywhere:
Text("Tap to continue")
// String Catalog provides:
// iPhone: "Tap to continue"
// iPad: "Tap or click to continue"
// Mac: "Click to continue"
// Vision: "Look and tap to continue"Exporting for Translators (XLIFF / xcloc)
Export
1. Product > Export Localizations... (or xcodebuild -exportLocalizations) 2. Select target languages 3. Xcode creates .xcloc bundles (one per language) 4. Send .xcloc files to translators (they contain XLIFF 1.2 inside)
Command-line export
xcodebuild -exportLocalizations \
-project MyApp.xcodeproj \
-localizationPath ./Localizations \
-exportLanguage de -exportLanguage ja -exportLanguage arImport
1. Product > Import Localizations... 2. Select the completed .xcloc file 3. Xcode merges translations into the String Catalog 4. Review changes in the diff viewer
Command-line import
xcodebuild -importLocalizations \
-project MyApp.xcodeproj \
-localizationPath ./Localizations/de.xclocString Catalog JSON Structure
The .xcstrings file is Xcode-managed JSON. Understanding the observed structure can help with parser-backed validation or careful batch updates, but prefer Xcode's editor/export/import workflows for normal localization changes and validate any automated edit before committing.
{
"sourceLanguage": "en",
"version": "1.0",
"strings": {
"Welcome, %@!": {
"comment": "Greeting shown on home screen with user name",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Welcome, %@!"
}
},
"de": {
"stringUnit": {
"state": "translated",
"value": "Willkommen, %@!"
}
}
}
},
"room_available": {
"comment": "Button label on room search results",
"extractionState": "manual",
"localizations": {
"en": {
"stringUnit": {
"state": "translated",
"value": "Book this room"
}
}
}
},
"%1$(count)lld items": {
"localizations": {
"en": {
"variations": {
"plural": {
"one": {
"stringUnit": {
"state": "translated",
"value": "%1$(count)lld item"
}
},
"other": {
"stringUnit": {
"state": "translated",
"value": "%1$(count)lld items"
}
}
}
}
}
}
}
}
}Note the "room_available" key above: it uses "extractionState": "manual" and a stable symbol-style key with the English text in "value", not in the key itself. Use stable manual keys for generated-symbol strings. Avoid source-copy-derived keys for API-facing strings because wording edits can rename generated identifiers and churn call sites.
Translation states
"new"-- Xcode extracted the key but no translation exists"translated"-- Translation provided"needs_review"-- Marked for review (source string changed or manual flag)"stale"-- Key no longer found in code (removed on next clean build)
Extraction states
The extractionState field (separate from translation state) tracks how a key entered the catalog:
| Value | Meaning |
|---|---|
extracted_with_value | Xcode found the string in source code and extracted it automatically |
manual | Added by hand via the (+) button — not discovered from code. Xcode will never update or remove manual keys during build sync |
stale | Previously extracted from code, but Xcode can no longer find it. Orphaned translations still exist |
migrated | Converted from a legacy .strings or .stringsdict file |
The manual state is significant: manual keys have the Generate Swift Symbol checkbox enabled by default, so they automatically produce compiler-checked LocalizedStringResource accessors when the build setting is on. Auto-extracted keys can also generate symbols — enable the checkbox per-key or use Refactor > Convert Strings to Symbols.
Generated Localizable Symbols (Xcode 26+)
For generated-symbol or migration answers, start by stating: "String Catalogs are the recommended Xcode 15+ localization workflow. Xcode 26 generated symbols are a separate typed-access layer on top of String Catalogs." Then explain generated symbols, plurals, or migration details. Do not describe catalogs themselves as requiring Xcode 26 or iOS 17.
Enabling symbol generation
1. Build Settings > Localization > Generate String Catalog Symbols → Yes (on by default in new Xcode 26 projects) 2. The catalog must use format version "1.1" — Xcode 26 writes this automatically when symbol generation metadata is present 3. Each key has a Generate Swift Symbol checkbox in the String Catalog editor. Manual keys (added via the (+) button) have this enabled by default. Auto-extracted keys can opt in via Refactor > Convert Strings to Symbols, which enables the checkbox
How Xcode derives symbol names
Xcode camelCases the key name, lowercasing the first segment:
| Catalog key | Generated symbol |
|---|---|
room_available | .roomAvailable |
settings.notifications.toggle | .settingsNotificationsToggle |
TITLE | .title |
Keys with format specifiers become functions. Use positional named placeholders such as %1$(name)lld for descriptive argument labels; bare %lld produces generic labels:
| Catalog key | Format | Generated symbol |
|---|---|---|
landmarks_count | %1$(count)lld | .landmarksCount(count: Int) |
greeting | %@ | .greeting(_ param1: String) |
You can rename parameters during refactoring for more descriptive signatures.
Using generated symbols
// Simple key — static property
Text(.roomAvailable)
// Parameterized key — function
Text(.landmarksCount(count: 42))
// Non-default table (Booking.xcstrings)
Text(.Booking.confirmBookingCta)
// In non-SwiftUI code
let title = String(localized: .roomAvailable)
let attributed = AttributedString(localized: .greeting(userName))Code completion supports generated symbols — type . and choose from the menu.
Refactoring existing strings to symbols
Select one or more keys in the String Catalog editor, Control-click, and choose Refactor > Convert Strings to Symbols. Xcode replaces string literal usage in code with the generated symbol. This is reversible via Convert Symbols to Strings.
Cross-module limitations
Generated symbols are declared internal. Code in other modules cannot access them directly. Default to a public wrapper; reach for xcstrings-tool if the wrapper becomes unwieldy across many modules:
- Public wrapper (default): Create a public extension on
LocalizedStringResourcethat delegates to the internal symbols - [xcstrings-tool](https://github.com/liamnichols/xcstrings-tool): A Swift Package Plugin that generates public constants from
.xcstringsfiles — use this for heavier multi-module setups where maintaining manual wrappers becomes tedious
For Swift Packages, the generated symbols use the .module bundle automatically. The internal visibility means only code within the same package target can reference them.
Testing Strategies
Scheme language override
Edit Scheme > Run > Options > App Language. Choose any added language to launch the app in that locale without changing the device/simulator system language.
Pseudolocalization options
Xcode provides built-in pseudolocalization modes (Edit Scheme > Run > Options > App Language):
| Option | Effect | Catches |
|---|---|---|
| Accented Pseudolanguage | Adds accents: "Hello" -> "[Hellо]" | Hardcoded strings (unlocalized text is obvious) |
| Right-to-Left Pseudolanguage | Forces RTL layout | Layout mirroring bugs |
| Double-Length Pseudolanguage | Doubles all strings | Truncation and overflow |
| Bounded String Pseudolanguage | Wraps strings in brackets | Missing localizations |
UI tests with locale override
func testGermanLayout() {
let app = XCUIApplication()
app.launchArguments += ["-AppleLanguages", "(de)"]
app.launchArguments += ["-AppleLocale", "de_DE"]
app.launch()
// Verify no truncation on key screens
let saveButton = app.buttons["Speichern"]
XCTAssertTrue(saveButton.exists)
XCTAssertTrue(saveButton.isHittable)
}Snapshot testing per locale
Use a snapshot testing library to capture screenshots in multiple locales and compare them for layout regressions:
let locales = ["en_US", "de_DE", "ar_SA", "ja_JP"]
for locale in locales {
app.launchArguments = ["-AppleLanguages", "(\(locale.prefix(2)))"]
app.launch()
// Capture and compare snapshot
}Translation coverage validation
Check that all keys are translated before release:
# Parse the .xcstrings JSON and check for "new" or empty states
python3 -c "
import json, sys
with open('Localizable.xcstrings') as f:
data = json.load(f)
missing = []
for key, info in data['strings'].items():
for lang, loc in info.get('localizations', {}).items():
unit = loc.get('stringUnit', {})
if unit.get('state') in ('new', None) or not unit.get('value'):
missing.append(f'{lang}: {key}')
if missing:
print('Missing translations:')
for m in missing: print(f' {m}')
sys.exit(1)
print('All translations complete.')
"Migration from .strings / .stringsdict
Automatic migration
1. Select the .strings file in the project navigator 2. Right-click > Migrate to String Catalog... 3. Xcode creates a .xcstrings file with all existing keys and translations 4. Verify in the String Catalog editor 5. Remove the old .strings / .stringsdict files from the target
Manual migration
If automatic migration fails (complex bundle setups, CocoaPods):
1. Create a new Localizable.xcstrings 2. Build to extract keys from code 3. Copy translations from old .strings files into the String Catalog editor 4. Copy plural rules from .stringsdict into plural variants 5. Remove old files
Migration checklist
- [ ] All
.stringskeys present in the new String Catalog - [ ] All
.stringsdictplural rules converted to String Catalog plural variants - [ ] Bundle references updated (if custom bundle was used)
- [ ] Build succeeds with no missing-localization warnings
- [ ] Test every language the app supports
- [ ] Remove old
.stringsand.stringsdictfiles from the target - [ ] Commit the
.xcstringsfile (it is JSON, diffs well in version control)
Coexistence
String Catalogs and .strings files can coexist in the same target during migration. Xcode resolves keys from the String Catalog first, then falls back to .strings. Remove legacy files after verifying the migration.
Best Practices
1. One String Catalog per target -- keep Localizable.xcstrings as the single source of truth for each target. 2. Use comments -- provide context for every ambiguous key. Translators cannot see your UI. 3. Review extraction on every build -- new keys appear with state "new". Translate them promptly. 4. Version control the .xcstrings file -- it is JSON and diffs clearly. Review translation changes in PRs. 5. Automate coverage checks -- integrate translation-coverage validation in CI to catch missing translations before release. 6. Export regularly -- send updated .xcloc bundles to translators after each sprint or feature merge. 7. Test with pseudolocalizations in CI -- run UI tests with double-length and RTL pseudo-languages to catch layout issues early. 8. Prefer stable keys with generated symbols -- for manually-managed strings, use symbol-style keys and enable Generate String Catalog Symbols to get compile-time safety and autocompletion.
Related skills
FAQ
What string type should SwiftUI views use?
SwiftUI text parameters accept LocalizedStringKey implicitly from string literals without extra wrappers.
How should dates and numbers be formatted?
Use FormatStyle so formatting adapts to the user locale instead of hard-coded patterns.
What alignment should RTL layouts use?
Use leading and trailing alignment and padding because they auto-flip for RTL languages.
Is Ios Localization safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.