| name | swift-localization |
| description | 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. |
| license | MIT |
Swift Localization Best Practices
Comprehensive guide for implementing localization in Swift and SwiftUI applications using modern Apple frameworks and type-safe patterns.
Core Principles
1. Never Hardcode User-Facing Strings
All text visible to users must be localized, including:
- UI labels, buttons, and navigation titles
- Error messages and alerts
- Placeholder text and hints
- Accessibility labels and hints
- Status messages and notifications
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)
2. Use Type-Safe Localization Keys
Create a centralized enum structure (commonly named L10n) using LocalizedStringResource for compile-time safety.
Implementation Pattern
Structure: L10n.swift
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"
)
}
}
Key Naming Convention
Use dot-separated namespaces that mirror your code structure:
{feature}.{component}.{element}.{property}- Examples:
dashboard.currentWeight- Simple valuecommon.button.save- Reusable buttonsettings.section.personalization.title- Nested section titleaddEntry.error.invalidWeight- Error message
Parameterized Strings
For 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"
)
}
}
}
Usage in SwiftUI Views
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)))
Accessibility Localization
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))
String Catalogs (.xcstrings)
File Structure
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"
}
Managing String Catalogs in Xcode
- Build the project after adding new
LocalizedStringResourceentries - Xcode will detect them - Open
Localizable.xcstringsin Xcode - Use the String Catalog editor to add translations
- Xcode shows:
- ✓ Translated strings
- ⚠️ Untranslated strings (needs attention)
- States:
new,translated,needs_review
Adding New Languages
- In Xcode: Project Settings → Info → Localizations →
+ - Select language (e.g., Spanish, French, German)
- Xcode adds the language to all
.xcstringsfiles - Translate strings in the String Catalog editor
Common Patterns
Conditional Text
enum 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)
Error Messages
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)
}
Pluralization
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"
}
}
}
}
}
}
}
Date Formatting
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
Migration Strategy
Converting Hardcoded Strings
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:
- Add to appropriate enum section
- Use descriptive key name
- Provide clear default value
Replace in views:
// Before Text("Current Weight") // After Text(L10n.Dashboard.currentWeight)Build and verify:
- Build project to generate
.xcstringsentries - Open String Catalog to verify keys appear
- Add translations for all supported languages
- Build project to generate
Handling Legacy NSLocalizedString
If 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.
Testing Localization
Pseudo-localization
Test string length and layout issues:
- Xcode → Product → Scheme → Edit Scheme
- Run → Options → App Language → "Double-Length Pseudo-language"
- Strings appear doubled to simulate longer translations
Language Testing
- Simulator: Settings → Language & Region → Preferred Languages
- Xcode scheme: Edit Scheme → Run → App Language → Select language
- Verify:
- All strings are translated
- No truncation or layout issues
- Right-to-left (RTL) languages display correctly
Automated Checks
// 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")
}
Best Practices Summary
✅ Do:
- Organize L10n enum by feature/screen
- Use
LocalizedStringResourcefor type safety - Provide descriptive default values
- Localize accessibility strings separately
- Use functions for parameterized strings
- Let Foundation handle date/number formatting
- Test with pseudo-localization and RTL languages
❌ Don't:
- Hardcode user-facing strings directly in views
- Use string concatenation for sentences (breaks translation)
- Assume English word order works in all languages
- Skip accessibility localization
- Use generic key names like "title1", "label2"
- Forget to build after adding new keys
Tooling
Xcode String Catalog Editor
- Filter: Search/filter strings by state (new, translated, needs review)
- Bulk edit: Select multiple strings to mark as reviewed
- Export/Import: File → Export/Import Localizations for external translation
Command Line
# 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
Resources
- Apple Localization Documentation
- LocalizedStringResource API
- String Catalogs
- WWDC: Discover String Catalogs
Quick Reference
| 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.