Copilot, Claude, Cursor, and others all read from ~/.agents/. The npx skills CLI handles fan-out to tool-specific directories.
229 lines
5.1 KiB
Markdown
229 lines
5.1 KiB
Markdown
---
|
|
name: Swift Localization
|
|
description: Localization patterns using String Catalogs and modern APIs
|
|
globs: ["**/*.swift", "**/*.xcstrings"]
|
|
---
|
|
|
|
# Localization with String Catalogs
|
|
|
|
Use **String Catalogs** (`.xcstrings` files) for localization in modern Swift projects.
|
|
|
|
## Required Language Support
|
|
|
|
At minimum, support these languages:
|
|
|
|
- **English (en)** - Base language
|
|
- **Spanish - Mexico (es-MX)**
|
|
- **French - Canada (fr-CA)**
|
|
|
|
## How String Catalogs Work
|
|
|
|
### Automatic Extraction
|
|
|
|
SwiftUI `Text` views with string literals are automatically extracted:
|
|
|
|
```swift
|
|
// 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:)`:
|
|
|
|
```swift
|
|
// 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
|
|
|
|
```swift
|
|
// BAD - Old API
|
|
let text = NSLocalizedString("Hello", comment: "Greeting")
|
|
|
|
// GOOD - Modern API
|
|
let text = String(localized: "Hello")
|
|
```
|
|
|
|
## String Interpolation
|
|
|
|
String Catalogs handle interpolation automatically:
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
// 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:
|
|
|
|
```swift
|
|
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:
|
|
|
|
```swift
|
|
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
|
|
|
|
```swift
|
|
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
|
|
|
|
```swift
|
|
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:
|
|
|
|
```swift
|
|
// 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
|
|
|
|
```swift
|
|
#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
|
|
|
|
```bash
|
|
# Export for translators
|
|
xcodebuild -exportLocalizations -project MyApp.xcodeproj -localizationPath ./Localizations
|
|
|
|
# Import translations
|
|
xcodebuild -importLocalizations -project MyApp.xcodeproj -localizationPath ./Localizations/es-MX.xcloc
|
|
```
|