How to Build Localization in SwiftUI
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
-
Automatic
LocalizedStringKeypromotion. AnyStringliteral you pass to a SwiftUI view initialiser (likeText("inbox.title")) is silently typed asLocalizedStringKey. At runtime, SwiftUI looks up the key in your String Catalog for the device's current locale. No extra wrapping needed. -
Interpolated keys and plural rules.
Text("inbox.unread_count \(stats.unreadCount)")produces a composite keyinbox.unread_count %lldin 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 viaNSLocalizedStringplural rules under the hood. -
String(localized:)for non-view code. When you need a plainString— e.g., foraccessibilityLabelor a log message — useString(localized: "key", comment: "…"). Thecommentparameter is surfaced to translators in the catalog, improving translation quality. -
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. -
Locale preview variants. Injecting
.environment(\.locale, Locale(identifier: "ar_SA"))and\.layoutDirection, .rightToLeftin a#Previewlets 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
-
String Catalogs require Xcode 15+ / iOS 17 SDK. If your minimum deployment target is iOS 16 you can still use the new
.xcstringsformat — Xcode converts it to.strings/.stringsdictat build time — but test on iOS 16 devices to confirm plural rules export correctly. -
Concatenating strings kills localization. Never do
Text("Hello, ") + Text(name)hoping translators can work with it; word order varies per language. Always use a single interpolated key:Text("greeting \(name)")so the translator can reorder the placeholder. -
Missing
comment:labels slow down translation and hurt quality. The Xcode String Catalog shows comments to translators. Without context, a word like"action.like"is ambiguous (verb vs noun). Always fill in thecomment:parameter inString(localized:comment:).
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.