```html SwiftUI: How to Build Localization (iOS 17+, 2026)

How to Build Localization in SwiftUI

iOS 17+ Xcode 16+ Intermediate APIs: LocalizedStringKey Updated: May 11, 2026
TL;DR

SwiftUI promotes plain string literals to LocalizedStringKey automatically, so you get free localization just by adding a String Catalog (Localizable.xcstrings) and translating your strings inside it. Use String(localized:) when you need a plain String value in logic code.

import SwiftUI

struct WelcomeView: View {
    var body: some View {
        VStack(spacing: 16) {
            // Plain string literals become LocalizedStringKey automatically
            Text("welcome.title")
            Text("welcome.subtitle")
            Button("action.get_started") {
                // action
            }
        }
    }
}

#Preview { WelcomeView() }

Full implementation

The modern approach (Xcode 15+) uses a String Catalog (Localizable.xcstrings) instead of the old .strings/.stringsdict pair. Xcode automatically extracts every LocalizedStringKey usage from your SwiftUI views during build. Below is a complete example covering a screen with dynamic counts, formatted values, and a locale-aware preview — the most common real-world requirements.

import SwiftUI

// MARK: - Model

struct InboxStats {
    var unreadCount: Int
    var senderName: String
    var lastDate: Date
}

// MARK: - View

struct InboxView: View {
    let stats: InboxStats

    // String(localized:) for logic — returns a plain String
    private var accessibilityLabel: String {
        String(localized: "inbox.accessibility \(stats.unreadCount)",
               comment: "VoiceOver label for unread badge")
    }

    var body: some View {
        NavigationStack {
            VStack(alignment: .leading, spacing: 20) {

                // 1. Simple keyed string — Xcode extracts this automatically
                Text("inbox.title")
                    .font(.largeTitle.bold())

                // 2. Interpolated LocalizedStringKey — plural-aware via String Catalog
                Text("inbox.unread_count \(stats.unreadCount)")
                    .font(.headline)
                    .foregroundStyle(stats.unreadCount > 0 ? .red : .secondary)
                    .accessibilityLabel(accessibilityLabel)

                // 3. Formatted date using locale-sensitive format style
                Text(
                    "inbox.last_message \(stats.lastDate.formatted(.dateTime.day().month().year()))"
                )
                .font(.subheadline)
                .foregroundStyle(.secondary)

                Divider()

                // 4. Sender attribution using named interpolation
                Text("inbox.from \(stats.senderName)")
                    .font(.body)

                Spacer()

                // 5. Button label as LocalizedStringKey
                Button {
                    // mark all read action
                } label: {
                    Label("inbox.mark_all_read", systemImage: "envelope.open")
                        .frame(maxWidth: .infinity)
                }
                .buttonStyle(.borderedProminent)
                .disabled(stats.unreadCount == 0)
                .accessibilityHint(Text("inbox.mark_all_read.hint"))
            }
            .padding()
            .navigationTitle(Text("inbox.nav_title"))
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

// MARK: - Previews

#Preview("English — unread") {
    InboxView(stats: InboxStats(
        unreadCount: 3,
        senderName: "Alice",
        lastDate: .now
    ))
    .environment(\.locale, Locale(identifier: "en_US"))
}

#Preview("German — none") {
    InboxView(stats: InboxStats(
        unreadCount: 0,
        senderName: "Alice",
        lastDate: .now
    ))
    .environment(\.locale, Locale(identifier: "de_DE"))
}

#Preview("Arabic RTL") {
    InboxView(stats: InboxStats(
        unreadCount: 12,
        senderName: "أحمد",
        lastDate: .now
    ))
    .environment(\.locale, Locale(identifier: "ar_SA"))
    .environment(\.layoutDirection, .rightToLeft)
}

How it works

  1. Automatic LocalizedStringKey promotion. Any String literal you pass to a SwiftUI view initialiser (like Text("inbox.title")) is silently typed as LocalizedStringKey. At runtime, SwiftUI looks up the key in your String Catalog for the device's current locale. No extra wrapping needed.
  2. Interpolated keys and plural rules. Text("inbox.unread_count \(stats.unreadCount)") produces a composite key inbox.unread_count %lld in the catalog. In the Xcode editor you can add one / other (and language-specific) plural variants for that key — Xcode picks the right form at runtime via NSLocalizedString plural rules under the hood.
  3. String(localized:) for non-view code. When you need a plain String — e.g., for accessibilityLabel or a log message — use String(localized: "key", comment: "…"). The comment parameter is surfaced to translators in the catalog, improving translation quality.
  4. Locale-sensitive format styles. Date.formatted(.dateTime.day().month().year()) uses the active locale automatically, so German users see 12. Mai 2026 without any extra code.
  5. Locale preview variants. Injecting .environment(\.locale, Locale(identifier: "ar_SA")) and \.layoutDirection, .rightToLeft in a #Preview lets you catch truncation and mirroring bugs before a real device is needed.

Variants

Switching locale at runtime (language picker)

import SwiftUI

// Store the user's chosen locale and inject it at the root.
// Note: changing AppleLanguages requires a restart on real devices,
// but injecting via environment works for in-app demos / onboarding.

@main
struct MyApp: App {
    @AppStorage("appLocale") private var localeID: String = "en"

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.locale, Locale(identifier: localeID))
        }
    }
}

// Picker to switch:
struct LocalePicker: View {
    @AppStorage("appLocale") private var localeID: String = "en"
    let supported = ["en", "de", "fr", "ar", "ja"]

    var body: some View {
        Picker("language.picker.label", selection: $localeID) {
            ForEach(supported, id: \.self) { id in
                Text(Locale(identifier: id).localizedString(forIdentifier: id) ?? id)
                    .tag(id)
            }
        }
    }
}

Markdown in localized strings

SwiftUI's Text renders a subset of Markdown when the source is a LocalizedStringKey. Add bold or italic directly in your catalog value: "welcome.body" = "Sign in with **Apple** or _email_.". Because the Markdown is in the catalog (not the source), translators can adjust emphasis without a code change. This works automatically — no extra modifiers needed.

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement localization in SwiftUI for iOS 17+.
Use LocalizedStringKey, String Catalogs (.xcstrings), and String(localized:comment:).
Support plural rules for count-bearing strings.
Make it accessible (VoiceOver labels with localized strings).
Add a #Preview with realistic sample data for English, German, and Arabic (RTL).

In the Soarias Build phase, drop this prompt into the active file context so Claude Code can extract your existing string literals and populate the String Catalog keys automatically.

Related

FAQ

Does this work on iOS 16?

Yes — LocalizedStringKey has been available since SwiftUI's debut, and String Catalogs (.xcstrings) are back-deployed at build time by Xcode 15+. Xcode converts the catalog to classic .strings and .stringsdict files when building for older SDKs, so plural rules work on iOS 16 and below. Just set your minimum deployment target normally.

How do I localize values inside @AppStorage or view-model properties?

Use String(localized: "my.key", bundle: .main) in your view model or anywhere outside a SwiftUI view body. The bundle: parameter ensures the lookup targets your app's String Catalog, which matters when your code lives in a Swift Package. Avoid storing raw English strings in @AppStorage; store keys and resolve them at display time instead.

What is the UIKit equivalent?

In UIKit you call NSLocalizedString("my.key", comment: "…") to get a String, then assign it to label.text. SwiftUI removes this boilerplate: Text("my.key") does the lookup automatically. The underlying catalog format is identical, so UIKit and SwiftUI targets in the same app can share a single Localizable.xcstrings file.

Last reviewed: 2026-05-11 by the Soarias team.

```