---
name: ios-localization
description: "i18n and l10n for iOS and macOS: String Catalogs (.xcstrings), generated localizable symbols with compile-time-checked keys, stable key naming, LocalizedStringKey, LocalizedStringResource, String(localized:), plurals, device variations, locale-aware FormatStyle for numbers, dates, currency and measurements, right-to-left layout, Dynamic Type, localization testing. Use when implementing or reviewing localization, adding languages, setting up catalogs, formatting values per locale, testing translations or making UI work in Arabic or Hebrew. Not for custom or parseable FormatStyle, parsing, interval or URL format styles (swift-formatstyle)."
metadata:
  source: multi-agent-pipeline
---

# iOS Localization

This skill targets iOS 26+ apps built with String Catalogs, the modern Swift
string types, `FormatStyle` and layout that respects writing direction. Each API
below keeps its own, often much older, availability.

Retrofitting localization late is expensive. Hard-coded copy, glued-together
sentences and fixed widths show up as rejected builds in non-English
storefronts, mistranslated screens and clipped layouts. Treat every visible
string and every formatted value as localizable from the first commit.

Deep material lives in two references:

- [references/string-catalogs.md](references/string-catalogs.md): catalog
  creation, extraction rules, key management, bundles and Swift packages,
  plurals, device variants, translator export and import, the JSON format,
  generated symbols, testing and migration.
- [references/formatstyle-locale.md](references/formatstyle-locale.md):
  locale-aware formatting for every value type, legacy formatter migration, RTL
  in depth, `@ScaledMetric` and layout checks with Accessibility Inspector.

## String Catalogs

A String Catalog is a JSON file managed by Xcode, edited through a visual
table, that keeps source strings, translations, plural forms and per-device
variants in one place. Catalogs have been the recommended way to localize
since Xcode 15. Legacy `.strings` and `.stringsdict` files can live next to a
catalog while a project migrates, but new Swift and SwiftUI code should start
in a catalog.

Why the switch: `.strings` files needed hand-maintained keys that silently
drifted from the code, and `.stringsdict` plural rules were long XML
dictionaries. A catalog is filled in by the build, tracks the state of each
translation, and models plurals directly.

Every build, Xcode scans the source for known localizable call patterns and
adds any key it finds. Keys then show a state in the editor such as Needs
Review, Translated or Stale.

```swift
struct TripHeader: View {
    @State private var alertsOn = true

    var body: some View {
        VStack {
            Text("Your upcoming trips")                           // extracted
            Label("Boarding pass", systemImage: "ticket")       // extracted
            Button("Check in") { }                               // extracted
            Toggle("Gate alerts", isOn: $alertsOn)               // extracted
        }
    }
}

let banner = String(localized: "Seat changed")                   // extracted
let hint = LocalizedStringResource("Arrive two hours early")     // extracted
let debugTag = "trip-header"   // plain String: never extracted, never translated
```

For a SwiftUI initializer the literal itself becomes the key.

## Generated Symbols (Xcode 26) on Top of Catalogs

When a question is about generated symbols or about migrating to them, say
this first: catalogs are the Xcode 15+ workflow, and generated symbols are a
separate typed-access layer that Xcode 26 builds on those catalogs. Do not say
catalogs require Xcode 26, and do not tie them to iOS 17.

- Turn it on with Build Settings > Localization > Generate String Catalog
  Symbols = `Yes`. New Xcode 26 projects already have it on.
- The catalog must use format version `1.1`.
- A key added by hand with the (+) button has Generate Swift Symbol ticked by
  default. Keys that came from extraction opt in through Refactor > Convert
  Strings to Symbols, which can be undone later.
- Pick stable, hand-written keys for anything reached through a symbol. A key
  derived from the English copy renames its symbol the moment someone edits
  the wording, and every call site churns.

```swift
// Localizable.xcstrings: "cart_empty" -> "Your cart is empty"
Text(.cartEmpty)

// "seats_left" -> "%1$(remaining)lld seats left"
Text(.seatsLeft(remaining: 3))

// Separate table Payments.xcstrings, key "apply_voucher_cta"
Text(.Payments.applyVoucherCta)
```

Symbols are the key in camelCase: `profile.privacy.header` becomes
`.profilePrivacyHeader`. They are generated with `internal` access, so another
module needs a small public wrapper. When wrappers pile up across many modules,
the open-source `xcstrings-tool` package plugin generates public accessors.

## Picking a String Type

### LocalizedStringKey

Text parameters of SwiftUI views are `LocalizedStringKey`, and a string literal
converts to it implicitly, so lookup just happens:

```swift
NavigationStack {
    List {
        Label("Saved places", systemImage: "mappin")
        Button("Clear history") { }
    }
    .navigationTitle("Places")
}
```

Pass literals straight into SwiftUI initializers. Building a
`LocalizedStringKey` by hand is rarely needed.

### String(localized:)

`String(localized:)` replaces `NSLocalizedString`. It returns a resolved
`String` for code that is not a SwiftUI initializer argument. The literal and
interpolation form needs iOS 15; resolving a `LocalizedStringResource` needs
iOS 16.

```swift
let title = String(localized: "Refund requested")
let note = String(localized: "refund.pending.note",
                  defaultValue: "Refunds take up to 5 business days")
let label = String(localized: "Download", table: "Offline", bundle: .module)
let verb = String(localized: "Book",
                  comment: "Verb on a button that reserves a table")
```

A Swift package that still shows English falls into one of three setup gaps.
Check them in order before touching bundle lookup:

1. `Package.swift` sets `defaultLocalization`.
2. The target's `resources` processes the folder that holds the catalog, for
   example `.process("Resources")`.
3. `Localizable.xcstrings` really sits inside that processed folder.

Only once all three hold, debug lookup with `bundle: .module` in
`String(localized:)` and `Text(_:bundle:)`.

Xcode can still export and migrate literal-key `NSLocalizedString` calls, but
new code uses `String(localized:)`, SwiftUI literals,
`LocalizedStringResource` or generated symbols.

### LocalizedStringResource

`LocalizedStringResource` (iOS 16+) carries a localizable value that someone
else resolves later: App Intents, widgets, notifications, generated symbols
and system APIs that accept it. When you need the text now, use
`String(localized:)`.

```swift
struct StartTimerIntent: AppIntent {
    static let title: LocalizedStringResource = "Start Focus Timer"
    func perform() async throws -> some IntentResult { .result() }
}

struct StepsWidget: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: "steps", provider: StepsProvider()) { entry in
            StepsView(entry: entry)
        }
        .configurationDisplayName(LocalizedStringResource("Daily Steps"))
    }
}

func showToast(_ message: LocalizedStringResource) {
    let text = String(localized: message)   // resolved in the current locale
    toastCenter.present(text)
}
```

### Which type where

| Context | Type | Why |
|---|---|---|
| SwiftUI view text parameters | `LocalizedStringKey` (implicit) | SwiftUI does the lookup |
| Strings built in view models or services | `String(localized:)` | Logic needs the resolved value |
| App Intents, widgets, system APIs | `LocalizedStringResource` | The framework resolves at display time |
| Error text shown to the user | `String(localized:)` | Resolved inside the `catch` |
| Logs and analytics | `String` | Never shown to users |

## Interpolation

Interpolated values become positional arguments, so a translator can move them
around the sentence:

```swift
let summary = String(localized: "\(guestName) booked \(nights) nights")
// Catalog key: "%@ booked %lld nights"
// Japanese can place the arguments in its own order.
```

Give interpolations a format instead of raw specifiers:
`\(distance, format: .number)` or `\(departure, format: .dateTime.hour().minute())`.

## Plurals

Catalogs model plurals directly; no `.stringsdict`. Interpolate an integer,
build, and the editor offers plural variants for that key. CLDR defines six
categories: zero, one, two, few, many, other. English only uses one and other.
Arabic uses all six: zero, one, two (dual), few (3 to 10), many (11 to 99) and
other (100 and up). Always fill `other`; it is the fallback.

```swift
Text("\(pendingInvites) pending invites")
// en one:   "%lld pending invite"
// en other: "%lld pending invites"
```

### Device variations

Vary by Device lets one key read differently per device, for instance "Tap to
edit" on iPhone, "Tap or click to edit" on iPad and "Click to edit" on Mac.

### Grammar agreement (iOS 15+)

Automatic grammar agreement fixes gender and number for supported languages
with the inflection markup `^[...](inflect: true)`:

```swift
Text("Added ^[\(bottleCount) bottle](inflect: true) to your cellar")
// English: 1 bottle / 3 bottles; Spanish forms agree automatically.
```

## Locale-Aware Formatting with FormatStyle

Never hard-code date, number or measurement patterns; use `FormatStyle`
(iOS 15+). Locale still matters in a one-language app: decimal and grouping
separators, calendars, currency symbols, units, name order and list
conjunctions all follow the user's region. Whenever you advise on user-facing
formatting, also tell the reader to preview or test under several locales such
as `en_US`, `de_DE`, `ar_SA` and `ja_JP`.

Scope boundary: this skill owns `FormatStyle` for locale-aware display
(numbers, dates, currency, units, names, lists, calendars, separators, and
locale previews and tests). Custom `FormatStyle` types, `ParseableFormatStyle`,
parsing, `Date.IntervalFormatStyle`, `URL.FormatStyle` and reusable formatter
API design belong to `swift-formatstyle`. Here, only point out locale risks and
testing for those, unless the user explicitly asks for an implementation.

```swift
let flight = Date.now
flight.formatted(date: .long, time: .shortened)
// en_US "June 3, 2026 at 4:05 PM", de_DE "3. Juni 2026 um 16:05"; ja_JP uses year-month-day order
flight.formatted(.dateTime.month(.wide).day().year())
Text(flight, format: .dateTime.weekday().hour().minute())

let visitors = 1_284_503
visitors.formatted()                                    // "1,284,503" / "1.284.503"
3.14159.formatted(.number.precision(.fractionLength(2)))
0.18.formatted(.percent)                                 // "18%"
Decimal(59.9).formatted(.currency(code: "USD"))
Decimal(59.9).formatted(.currency(code: "EUR"))          // symbol position follows locale

Measurement(value: 42, unit: UnitLength.kilometers)
    .formatted(.measurement(width: .wide))               // miles in en_US
Measurement(value: 21, unit: UnitTemperature.celsius)
    .formatted(.measurement(width: .abbreviated))        // Fahrenheit in en_US

Duration.seconds(5_025).formatted(.time(pattern: .hourMinuteSecond))   // "1:23:45"
var author = PersonNameComponents(); author.givenName = "Aiko"; author.familyName = "Tanaka"
author.formatted(.name(style: .long))                    // order differs en_US vs ja_JP
["tea", "coffee", "juice"].formatted(.list(type: .and))  // "tea, coffee, and juice"; French joins with "et" and no serial comma
```

Full coverage of every style is in
[references/formatstyle-locale.md](references/formatstyle-locale.md).

## Right-to-Left Layout

SwiftUI mirrors layout for Arabic, Hebrew, Urdu and Persian on its own; most
views need nothing. It flips `HStack` order, `.leading` and `.trailing`
alignment and padding, puts the `NavigationStack` back button on the trailing
side, mirrors `List` disclosure chevrons, and aligns text to the reading
direction.

```swift
#Preview("Checkout, Arabic") {
    CheckoutView()
        .environment(\.layoutDirection, .rightToLeft)
        .environment(\.locale, Locale(identifier: "ar"))
}

Image(systemName: "arrowshape.forward")          // directional: mirror it
    .flipsForRightToLeftLayoutDirection(true)

Text(verbatim: supportHotline)                    // phone numbers and code stay LTR
    .environment(\.layoutDirection, .leftToRight)
```

Leave logos, photos, clocks and musical notation unmirrored.

Rules: prefer `.leading` and `.trailing` (they flip) over `.left` and `.right`
(they do not); build with `HStack` and `VStack`; do not use absolute
`offset(x:)` to place things in a direction.

## Common Mistakes

1. `NSLocalizedString` in new Swift code. It is legacy; use
   `String(localized:defaultValue:comment:)` or a `Text` literal.
2. Building a sentence from localized fragments. Word order changes between
   languages; localize one interpolated string.
3. `DateFormatter` with a fixed `dateFormat` such as `"MM/dd/yyyy"`. Use
   `Text(date, format: .dateTime.month().day().year())` or another FormatStyle.
4. Fixed widths like `.frame(width: 120)` around text. German runs roughly 30%
   longer than English; let text wrap with
   `.fixedSize(horizontal: false, vertical: true)` or flexible stacks.
5. `.padding(.left, 12)`. Use `.padding(.leading, 12)`.
6. Passing user-facing text around as `String` outside SwiftUI. Carry a
   `LocalizedStringResource` and resolve it with `String(localized:)` where it
   is shown.
7. English sentences as keys for strings you manage by hand. A typo creates a
   brand new key, the old one goes stale, and the compiler says nothing. Use
   stable symbol-style keys, for example `order_shipped` read as
   `Text(.orderShipped)`, or
   `String(localized: "order_shipped", defaultValue: "Your order is on its way")`.
8. Testing only in English. Truncation, layout and RTL bugs stay hidden;
   run the pseudolanguages.
9. Not testing German (long text) and Arabic (RTL) at minimum. Switch the app
   language in the scheme options instead of changing the device locale.

## Review Checklist

- [ ] Every user-facing string is localized (`LocalizedStringKey` or
      `String(localized:)`)
- [ ] No string concatenation for visible text
- [ ] Dates and numbers go through `FormatStyle`, never fixed patterns
- [ ] Plurals use catalog plural variants, not `if count == 1`
- [ ] `.leading` and `.trailing` instead of `.left` and `.right`
- [ ] UI checked with long text (German) and RTL (Arabic)
- [ ] The catalog lists every shipping language
- [ ] Directional images use `.flipsForRightToLeftLayoutDirection(true)`
- [ ] App Intents and widgets take `LocalizedStringResource`
- [ ] No `NSLocalizedString` in new code
- [ ] Ambiguous keys carry translator comments
- [ ] Spacing that must track Dynamic Type uses `@ScaledMetric`
- [ ] Currency always names an explicit currency code instead of relying on the
      locale default
- [ ] Pseudolocalization run: accented, right-to-left, double-length
- [ ] Hand-managed keys are stable symbol-style names, not English sentences
- [ ] Generate String Catalog Symbols is on for targets with manual keys
- [ ] Localized string types are `Sendable`; UI reacting to a locale change
      updates on `@MainActor`

## References

- [references/string-catalogs.md](references/string-catalogs.md): catalogs,
  keys, bundles, packages, plurals, translator workflow, JSON, symbols,
  testing and migration.
- [references/formatstyle-locale.md](references/formatstyle-locale.md):
  FormatStyle per value type, forcing a locale, RTL, `@ScaledMetric` and
  Accessibility Inspector.
