swift-localization
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.
More from hocgin/agent-skills
swift-composable-architecture
Use when building, refactoring, debugging, or testing iOS/macOS features using The Composable Architecture (TCA). Covers feature structure, effects, dependencies, navigation patterns, and testing with TestStore.
15swift-private-bundle
Use when working with private Swift Package Manager dependencies from github.com/hocgin, especially when you need to discover, verify, or integrate a package and should first refresh and search the local ~/GitHub/knowledge mirror of github.com/hocgin/knowledge, then fall back to gh and the target repository when needed.
6swift-sqlite-data
Use when working with SQLiteData library (@Table, @FetchAll, @FetchOne macros) for SQLite persistence, queries, writes, migrations, or CloudKit private database sync.
6article-writer
AI驱动的智能写作系统,专注于创作高质量、低AI检测率的文章内容
5wrangler
Cloudflare Workers CLI for deploying, developing, and managing Workers, KV, R2, D1, Vectorize, Hyperdrive, Workers AI, Containers, Queues, Workflows, Pipelines, and Secrets Store. Load before running wrangler commands to ensure correct syntax and best practices.
4xlsx
Use this skill any time a spreadsheet file is the primary input or output. This means any task where the user wants to: open, read, edit, or fix an existing .xlsx, .xlsm, .csv, or .tsv file (e.g., adding columns, computing formulas, formatting, charting, cleaning messy data); create a new spreadsheet from scratch or from other data sources; or convert between tabular file formats. Trigger especially when the user references a spreadsheet file by name or path — even casually (like \"the xlsx in my downloads\") — and wants something done to it or produced from it. Also trigger for cleaning or restructuring messy tabular data files (malformed rows, misplaced headers, junk data) into proper spreadsheets. The deliverable must be a spreadsheet file. Do NOT trigger when the primary deliverable is a Word document, HTML report, standalone Python script, database pipeline, or Google Sheets API integration, even if tabular data is involved.
4