ai-docs/assets/skills/swift-localization/SKILL.md

5.1 KiB

name description globs
Swift Localization Localization patterns using String Catalogs and modern APIs
**/*.swift
**/*.xcstrings

Localization with String Catalogs

Use String Catalogs (.xcstrings files) for localization in modern Swift projects.

Language Support

At minimum, support the base development language (typically English). Add additional locales based on your app's target markets. Common additions include Spanish and French variants.

How String Catalogs Work

Automatic Extraction

SwiftUI Text views with string literals are automatically extracted:

// Automatically added to String Catalog
Text("Hello, World!")
Text("Welcome back, \(user.name)!")

Manual Extraction for Non-Text Strings

For strings outside of Text views, use String(localized:):

// Use String(localized:) for alerts, buttons, accessibility
let title = String(localized: "Delete Item")
let message = String(localized: "Are you sure you want to delete this item?")

// With comments for translators
let greeting = String(
    localized: "greeting_message",
    defaultValue: "Hello!",
    comment: "Greeting shown on the home screen"
)

Never Use NSLocalizedString

// BAD - Old API
let text = NSLocalizedString("Hello", comment: "Greeting")

// GOOD - Modern API
let text = String(localized: "Hello")

String Interpolation

String Catalogs handle interpolation automatically:

// In Swift
Text("You have \(count) items")

// In String Catalog, translators see:
// "You have %lld items"
// They can reorder: "Items: %lld" for languages that need different order

Pluralization

Use automatic grammar agreement for plurals:

// Automatic pluralization
Text("^[\(count) item](inflect: true)")

// Result:
// count = 1: "1 item"
// count = 5: "5 items"

For complex pluralization rules, define in String Catalog with plural variants.

Formatting Numbers, Dates, Currency

Always use formatters - they respect locale automatically:

// Numbers
Text(price, format: .currency(code: "USD"))
Text(percentage, format: .percent)
Text(count, format: .number)

// Dates
Text(date, format: .dateTime.month().day().year())
Text(date, format: .relative(presentation: .named))

// Measurements
let distance = Measurement(value: 5, unit: UnitLength.miles)
Text(distance, format: .measurement(width: .abbreviated))

Localized String Keys

Use meaningful keys for complex strings:

// For simple UI text, use the text itself
Text("Settings")
Text("Cancel")

// For complex or contextual strings, use keys
Text("home.welcome.title")  // Key in String Catalog
Text("profile.empty.message")

Accessibility Labels

Localize all accessibility content:

Image(systemName: "heart.fill")
    .accessibilityLabel(String(localized: "Favorite"))

Button { } label: {
    Image(systemName: "trash")
}
.accessibilityLabel(String(localized: "Delete"))
.accessibilityHint(String(localized: "Removes this item permanently"))

String Catalog Organization

File Structure

App/
├── Localizable.xcstrings     # Main strings
├── InfoPlist.xcstrings       # Info.plist strings (app name, permissions)
└── Intents.xcstrings         # Siri/Shortcuts strings (if applicable)

Comments for Translators

Add comments to help translators understand context:

Text("Save", comment: "Button to save the current document")
Text("Save", comment: "Menu item to save all changes")

// These become separate entries with context

Common Patterns

Error Messages

enum AppError: LocalizedError {
    case networkUnavailable
    case invalidData
    
    var errorDescription: String? {
        switch self {
        case .networkUnavailable:
            return String(localized: "error.network.unavailable")
        case .invalidData:
            return String(localized: "error.data.invalid")
        }
    }
}

Attributed Strings

var attributedGreeting: AttributedString {
    var string = AttributedString(localized: "Welcome to **MyApp**!")
    // Markdown formatting is preserved
    return string
}

Dynamic Strings from Server

For server-provided strings that need localization:

// Use a mapping approach
let serverKey = response.messageKey  // e.g., "subscription_expired"
let localizedMessage = String(localized: String.LocalizationValue(serverKey))

Testing Localization

Preview with Different Locales

#Preview {
    ContentView()
        .environment(\.locale, Locale(identifier: "es-MX"))
}

#Preview {
    ContentView()
        .environment(\.locale, Locale(identifier: "fr-CA"))
}

Pseudo-Localization

Enable in scheme to find truncation and layout issues:

  1. Edit Scheme → Run → Options
  2. Set "Application Language" to a pseudo-language
  3. Look for strings that don't expand properly

Export/Import for Translation

# Export for translators
xcodebuild -exportLocalizations -project MyApp.xcodeproj -localizationPath ./Localizations

# Import translations
xcodebuild -importLocalizations -project MyApp.xcodeproj -localizationPath ./Localizations/es-MX.xcloc