Best practices for internationalizing Swift/SwiftUI applications using LocalizedStringResource, String Catalogs (.xcstrings), and type-safe localization patterns. Use when implementing multi-language support, adding new UI strings, or refactoring hardcoded text in Swift apps.
Comprehensive guide for implementing localization in Swift and SwiftUI applications using modern Apple frameworks and type-safe patterns.
All text visible to users must be localized, including:
Bad:
Text("Add Weight")
Button("Save") { ... }
.alert("Error", message: "Something went wrong")
Good:
Text(L10n.Common.addWeight)
Button(L10n.Common.saveButton) { ... }
.alert(L10n.Common.errorTitle, message: L10n.Errors.genericMessage)
Create a centralized enum structure (commonly named L10n) using LocalizedStringResource for compile-time safety.
Create a hierarchical enum structure organized by feature or screen:
import Foundation
enum L10n {
enum Common {
static let saveButton = LocalizedStringResource(
"common.button.save",
defaultValue: "Save"
)
static let cancelButton = LocalizedStringResource(
"common.button.cancel",
defaultValue: "Cancel"
)
static let errorTitle = LocalizedStringResource(
"common.alert.errorTitle",
defaultValue: "Error"
)
}
enum Dashboard {
static let navigationTitle = LocalizedStringResource(
"dashboard.navigation.title",
defaultValue: "Dashboard"
)
static func greeting(_ name: String) -> LocalizedStringResource {
LocalizedStringResource(
"dashboard.greeting",
defaultValue: "Hello, \(name)!"
)
}
}
enum Settings {
static let navigationTitle = LocalizedStringResource(
"settings.navigation.title",
defaultValue: "Settings"
)
static let themeTitle = LocalizedStringResource(
"settings.personalization.theme.title",
defaultValue: "Theme"
)
}
}
Use dot-separated namespaces that mirror your code structure:
{feature}.{component}.{element}.{property}dashboard.currentWeight - Simple valuecommon.button.save - Reusable buttonsettings.section.personalization.title - Nested section titleaddEntry.error.invalidWeight - Error messageFor strings with dynamic content, use functions that return LocalizedStringResource:
enum L10n {
enum Dashboard {
static func latestEntry(_ time: String) -> LocalizedStringResource {
LocalizedStringResource(
"dashboard.latestEntry",
defaultValue: "Latest: \(time)"
)
}
static func averageEntries(_ count: Int) -> LocalizedStringResource {
LocalizedStringResource(
"dashboard.averageEntries",
defaultValue: "Average of \(count) entries"
)
}
}
}
Convert LocalizedStringResource to String using String(localized:):
struct DashboardView: View {
var body: some View {
NavigationStack {
VStack {
Text(L10n.Dashboard.navigationTitle)
Button(L10n.Common.saveButton) {
save()
}
}
.navigationTitle(String(localized: L10n.Dashboard.navigationTitle))
}
}
}
For string interpolation or concatenation:
TextField(
String(localized: L10n.AddEntry.weightPlaceholder),
text: $weightText
)
.accessibilityLabel(String(localized: L10n.Accessibility.weightValue))
.accessibilityHint(String(localized: L10n.Accessibility.weightValueHint(unitSymbol)))
Always localize accessibility strings separately:
enum L10n {
enum Accessibility {
static let addWeightEntry = LocalizedStringResource(
"accessibility.label.addWeightEntry",
defaultValue: "Add weight entry"
)
static let addWeightEntryHint = LocalizedStringResource(
"accessibility.hint.addWeightEntry",
defaultValue: "Opens form to log a new weight"
)
static func weightValueHint(_ unitSymbol: String) -> LocalizedStringResource {
LocalizedStringResource(
"accessibility.hint.weightValue",
defaultValue: "Enter your current weight in \(unitSymbol)"
)
}
}
}
Usage:
Button {
showAddEntry = true
} label: {
Image(systemName: "plus")
}
.accessibilityLabel(String(localized: L10n.Accessibility.addWeightEntry))
.accessibilityHint(String(localized: L10n.Accessibility.addWeightEntryHint))
Modern Swift projects use .xcstrings files (String Catalogs) instead of .strings files. Xcode automatically generates entries when you use LocalizedStringResource.
Location: {ProjectName}/Localization/Localizable.xcstrings
Example structure:
{
"sourceLanguage" : "en",
"strings" : {
"common.button.save" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Save"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Guardar"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Enregistrer"
}
}
}
}
},
"version" : "1.0"
}
LocalizedStringResource entries - Xcode will detect themLocalizable.xcstrings in Xcodenew, translated, needs_review+.xcstrings filesenum L10n {
enum Settings {
static let iCloudSyncEnabled = LocalizedStringResource(
"settings.data.iCloudSync.enabled",
defaultValue: "On"
)
static let iCloudSyncDisabled = LocalizedStringResource(
"settings.data.iCloudSync.disabled",
defaultValue: "Off"
)
}
}
// Usage
Text(isEnabled ? L10n.Settings.iCloudSyncEnabled : L10n.Settings.iCloudSyncDisabled)
enum L10n {
enum AddEntry {
static let errorInvalidWeight = LocalizedStringResource(
"addEntry.error.invalidWeight",
defaultValue: "Please enter a valid weight"
)
static func errorSaveFailure(_ message: String) -> LocalizedStringResource {
LocalizedStringResource(
"addEntry.error.saveFailure",
defaultValue: "Failed to save entry: \(message)"
)
}
}
}
// Usage
.alert(L10n.Common.errorTitle, isPresented: $showingError) {
Button(L10n.Common.okButton) { showingError = false }
} message: {
Text(L10n.AddEntry.errorInvalidWeight)
}
For count-dependent strings, use string interpolation:
static func days(_ count: Int) -> LocalizedStringResource {
LocalizedStringResource(
"common.value.days",
defaultValue: "\(count) days"
)
}
In .xcstrings, you can add plural rules:
"common.value.days" : {
"localizations" : {
"en" : {
"variations" : {
"plural" : {
"one" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld day"
}
},
"other" : {
"stringUnit" : {
"state" : "translated",
"value" : "%lld days"
}
}
}
}
}
}
}
Let Foundation handle date localization:
// Good - respects user's locale
Text(date, style: .date)
Text(date, format: .dateTime.day().month().year())
// Avoid hardcoded formats
Text(dateFormatter.string(from: date)) // ❌ if format is hardcoded
Audit for hardcoded strings:
# Find potential hardcoded user-facing strings
grep -r 'Text("' --include="*.swift" .
grep -r 'Button("' --include="*.swift" .
grep -r '.alert("' --include="*.swift" .
Create L10n entries:
Replace in views:
// Before
Text("Current Weight")
// After
Text(L10n.Dashboard.currentWeight)
Build and verify:
.xcstrings entriesIf migrating from NSLocalizedString:
// Old pattern
NSLocalizedString("key", comment: "Description")
// New pattern
LocalizedStringResource("key", defaultValue: "Default Value")
Both work with .xcstrings, but LocalizedStringResource is preferred for SwiftUI.
Test string length and layout issues:
// Unit test to ensure no hardcoded strings in views
func testNoHardcodedStrings() {
let source = try! String(contentsOfFile: "DashboardView.swift")
let pattern = #"Text\("[^L]"#
let regex = try! NSRegularExpression(pattern: pattern)
let matches = regex.matches(in: source, range: NSRange(source.startIndex..., in: source))
XCTAssertEqual(matches.count, 0, "Found hardcoded Text strings")
}
✅ Do:
LocalizedStringResource for type safety❌ Don't:
# Extract strings from code (legacy .strings format)
genstrings -o en.lproj *.swift
# Export for translation
xcodebuild -exportLocalizations -project MyApp.xcodeproj -localizationPath ./Localizations
# Import translations
xcodebuild -importLocalizations -project MyApp.xcodeproj -localizationPath ./Localizations/fr.xcloc
| Task | Pattern |
|------|---------|
| Simple string | static let title = LocalizedStringResource("key", defaultValue: "Title") |
| Parameterized | static func greeting(_ name: String) -> LocalizedStringResource { ... } |
| In SwiftUI | Text(L10n.Feature.label) |
| With String conversion | String(localized: L10n.Feature.label) |
| Navigation title | .navigationTitle(String(localized: L10n.Feature.title)) |
| Accessibility | .accessibilityLabel(String(localized: L10n.Accessibility.label)) |
| Alert title | .alert(L10n.Common.errorTitle, message: ...) |
This skill provides general best practices for Swift localization. Adapt patterns to your project's specific architecture and requirements.
Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer