Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
Reviews, improves, and writes SwiftUI code following state management, view composition, performance, and iOS 26+ Liquid Glass best practices.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
references/localization.md
1# SwiftUI Localization Reference23Guidance for user-facing text: `Text`, `Button`, `Label`, navigation/toolbar titles, alerts, and types that carry localizable strings. For the narrower "verbatim vs localized" decision on a single `Text`, see `references/text-patterns.md`.45## Table of Contents67- [SwiftUI Localizes String Literals Automatically](#swiftui-localizes-string-literals-automatically)8- [String Catalogs](#string-catalogs)9- [Bundle for Swift Packages and Frameworks](#bundle-for-swift-packages-and-frameworks)10- [Localizing Variables and Custom Types](#localizing-variables-and-custom-types)11- [LocalizedStringResource for Non-View Types](#localizedstringresource-for-non-view-types)12- [Interpolation vs Concatenation](#interpolation-vs-concatenation)13- [Casing](#casing)14- [Formatting Dates, Numbers, and Currencies](#formatting-dates-numbers-and-currencies)15- [Layout for Localization](#layout-for-localization)16- [Reading the Current Locale](#reading-the-current-locale)17- [String(localized:) Outside SwiftUI Views](#stringlocalized-outside-swiftui-views)18- [Comments for Translators](#comments-for-translators)1920## SwiftUI Localizes String Literals Automatically2122Initializers that accept `LocalizedStringKey` (`Text`, `Button`, `Label`, `.navigationTitle`, alert titles, and so on) treat string literals as localization keys automatically. Do not wrap literals in `NSLocalizedString`, `String(localized:)`, or `LocalizedStringResource` — that resolves the string eagerly and ignores `\.locale` overrides.2324```swift25// AVOID: double work, and resolves eagerly26Text(String(localized: "start_workout"))2728// PREFER: pass the literal directly29Text("start_workout")30```3132Both opaque keys (`"start_workout"`) and natural-language strings (`"Start Workout"`) work as keys — pick whichever convention the project already uses. Use `Text(verbatim:)` only to opt a literal out of localization (e.g. a debug label interpolating a runtime value). When the argument is already a `String` variable, `Text(value)` calls the `StringProtocol` overload and skips localization on its own.3334## String Catalogs3536Most projects localize through String Catalogs (`.xcstrings`). Each build syncs new keys from code into the catalog, but the catalog file must already exist — Xcode doesn't create one automatically. If a project already uses `.strings` / `.stringsdict`, add new strings there rather than migrating. Route groups of strings to a specific catalog with `tableName:`.3738```swift39Text("Explore", tableName: "Navigation",40comment: "Tab bar item title for the Explore screen.")41```4243## Bundle for Swift Packages and Frameworks4445Apps, app extensions, and XPC services are their own main bundle, so `bundle` can be omitted. Frameworks and Swift packages need an explicit `bundle:` — without one, SwiftUI looks up strings in `Bundle.main`, the lookup fails silently, and the string appears unlocalized at runtime.4647```swift48// AVOID (inside a framework/package): searches the app's catalog49Text("Save to Favorites")5051// PREFER: #bundle resolves to the current target's bundle52Text("Save to Favorites", bundle: #bundle,53comment: "Button to bookmark a recipe.")54```5556`#bundle` is the preferred form; `Bundle.module` and `Bundle(for:)` still work but are older patterns.5758## Localizing Variables and Custom Types5960A `String` variable passed to `Text` runs the `StringProtocol` overload and is **not** localized. Wrapping it in `LocalizedStringKey(_:)` doesn't help — Xcode can't extract a literal from a runtime value, so nothing lands in the catalog. To localize a value chosen from a known set, model it with a type that exposes `LocalizedStringResource`:6162```swift63enum Category {64case appetizers, mains, desserts65var name: LocalizedStringResource {66switch self {67case .appetizers: "Appetizers"68case .mains: "Mains"69case .desserts: "Desserts"70}71}72}7374Text(category.name)75```7677When a view or model exposes user-facing text, type the property as `LocalizedStringKey` or `LocalizedStringResource` rather than `String`. Every SwiftUI view that takes localized text accepts both, so deferring resolution costs nothing at the display site and preserves locale/bundle context.7879## LocalizedStringResource for Non-View Types8081When a non-view type carries user-facing text — a model object, a tip, a queued notification — use `LocalizedStringResource` instead of `String`. It defers resolution to display time, so it honors the locale active when the value actually renders, not when it was created.8283```swift84// AVOID: resolved at creation time, can't re-render in another locale85struct Tip { let headline: String }86let tip = Tip(headline: String(localized: "Tip of the Day"))8788// PREFER: resolution deferred to display time89struct Tip { let headline: LocalizedStringResource }90let tip = Tip(headline: "Tip of the Day")91```9293Apply this when designing new types or changing user-facing text — don't sweep through existing `String` properties as part of unrelated edits.9495## Interpolation vs Concatenation9697String interpolation preserves `LocalizedStringKey` and produces a format string in the catalog (e.g. `"Welcome, %@"`). Concatenation with `+` produces a plain `String` and is not localized. Never glue separately localized fragments into a sentence — word order varies across languages.9899```swift100// AVOID: + produces String; sentence assembly breaks word order101Text("Error: " + statusMessage)102Text(String(localized: "Created by")) + Text(" ") + Text(authorName)103104// PREFER: one interpolated string translators can rearrange105Text("Error: \(statusMessage)")106Text("Created by \(authorName)")107```108109## Casing110111Bake the desired case into the string rather than transforming at runtime via `.textCase(_:)`, `.localizedUppercase`, or `.localizedCapitalized`. A runtime transform forces the same casing on every translation, leaving translators no room to adjust per language.112113```swift114// AVOID115Text("Section Header").textCase(.uppercase)116117// PREFER118Text("SECTION HEADER")119```120121This applies to localized strings; display user-entered text as-is. If a transform is unavoidable, prefer `.localizedUppercase` / `.localizedCapitalized`, which honor the user's locale.122123## Formatting Dates, Numbers, and Currencies124125Use `Text`'s `format:` parameter or `.formatted()` instead of `DateFormatter` / `NumberFormatter` with hardcoded format strings. Format styles adapt to the user's locale; hardcoded format strings don't.126127```swift128// AVOID129let f = DateFormatter(); f.dateFormat = "MM/dd/yyyy"130Text(f.string(from: workout.date))131Text("$\(product.price, specifier: "%.2f")")132133// PREFER134Text(workout.date, format: .dateTime.month().day().year())135Text(product.price, format: .currency(code: store.currencyCode))136```137138Field components (`.month()`, `.day()`) choose which fields appear; the locale decides the order. For lists, `Array.formatted()` inserts locale-correct separators and conjunctions instead of `joined(separator:)`. When `DateFormatter` is genuinely unavoidable, use `setLocalizedDateFormatFromTemplate(_:)` rather than assigning `dateFormat`.139140## Layout for Localization141142- Use `.leading` / `.trailing` instead of `.left` / `.right` — they flip for right-to-left locales.143- Don't hardcode frame widths/heights for text; translations vary in length and scripts vary in height. Use `ViewThatFits` when a layout might not fit longer translations.144- Use text styles (`.body`, `.headline`) rather than fixed point sizes, so line height adapts per script.145146```swift147// PREFER148Text(recipe.title)149.frame(maxWidth: .infinity, alignment: .leading)150151ViewThatFits {152HStack { actionButtons }153VStack { actionButtons }154}155```156157## Reading the Current Locale158159Use `@Environment(\.locale)` for locale-dependent logic in views, not `Locale.current` — the environment respects preview overrides and per-view injection.160161## String(localized:) Outside SwiftUI Views162163When you need a localized `String` outside a view, use `String(localized:)`, not `NSLocalizedString`. Don't interpolate inside `NSLocalizedString` — Xcode extracts keys from literals at build time and can't extract interpolated values. `String(localized:)` supports interpolation (it extracts the format string and treats values as runtime arguments) and is preferred over `String(format:)`, which always renders digits as 0–9 regardless of locale.164165```swift166// PREFER167let title = String(localized: "activity_summary", comment: "Dashboard header")168```169170## Comments for Translators171172Add a `comment:` describing the UI element and its purpose, especially for ambiguous strings. For interpolated strings, describe each placeholder by position — translators don't see Swift variable names. Comments can live at the call site or in the String Catalog's per-string Comment field; keep one source of truth per string.173174```swift175// AVOID: "Edit" could be a noun or a verb176Text("Edit")177178// PREFER179Text("Edit", comment: "Toolbar button that enters editing mode for the list.")180Text("Completed \(count) of \(total)",181comment: "Progress label — first variable is finished items, second is the total.")182```183184## Summary Checklist185186- [ ] String literals passed directly to `Text`/`Button`/`Label` (not wrapped in `NSLocalizedString`/`String(localized:)`)187- [ ] `bundle: #bundle` on user-facing strings inside frameworks and Swift packages188- [ ] User-facing text on models/non-view types typed as `LocalizedStringResource`, not `String`189- [ ] Interpolation (not `+`) for dynamic strings; no sentence assembly from fragments190- [ ] Case baked into the string, not applied via `.textCase`191- [ ] Dates/numbers/currencies use `format:` / `.formatted()` with locale-aware styles192- [ ] `.leading`/`.trailing` (not `.left`/`.right`); no hardcoded text frame sizes193- [ ] `@Environment(\.locale)` for locale logic in views194- [ ] `comment:` provided for ambiguous strings and interpolated placeholders195