String Catalogs + AI translation pipeline for 5 languages: Spanish, French, German, Japanese, Chinese (Simplified) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
247 lines
5.6 KiB
Markdown
247 lines
5.6 KiB
Markdown
# App Localization Design
|
|
|
|
## Overview
|
|
|
|
Localize SportsTime for international markets using Xcode String Catalogs with AI-generated translations.
|
|
|
|
## Scope
|
|
|
|
**What's localized:**
|
|
- UI text (buttons, labels, menus, error messages) — ~200 strings
|
|
- Locale-aware formatting (dates, distances, durations, numbers)
|
|
|
|
**What stays in English:**
|
|
- Team names (Yankees, Lakers)
|
|
- Stadium names (Fenway Park, Madison Square Garden)
|
|
- League names (MLB, NBA, NHL, NFL)
|
|
|
|
**Target Languages:**
|
|
- Spanish (es)
|
|
- French (fr)
|
|
- German (de)
|
|
- Japanese (ja)
|
|
- Chinese Simplified (zh-Hans)
|
|
|
|
**Translation Approach:**
|
|
- AI-generated translations initially
|
|
- Iterate based on user feedback
|
|
- Easy to swap for professional translations later
|
|
|
|
## Architecture
|
|
|
|
### File Structure
|
|
|
|
```
|
|
SportsTime/
|
|
├── Resources/
|
|
│ └── Localizable.xcstrings # Single String Catalog for the app
|
|
├── Scripts/
|
|
│ └── translate.py # AI translation script
|
|
```
|
|
|
|
### String Key Convention
|
|
|
|
Use descriptive keys rather than English text:
|
|
|
|
```swift
|
|
// Good - descriptive key
|
|
Text("trip.creation.title") // "Plan Your Trip"
|
|
Text("button.save") // "Save"
|
|
Text("error.network.offline") // "No internet connection"
|
|
|
|
// Avoid - English as key
|
|
Text("Plan Your Trip") // Fragile if English text changes
|
|
```
|
|
|
|
### SwiftUI Integration
|
|
|
|
```swift
|
|
// Static text - automatic lookup
|
|
Text("trip.detail.header")
|
|
|
|
// Dynamic text with interpolation
|
|
Text("trip.stops.count \(count)") // Uses stringsdict for pluralization
|
|
|
|
// Programmatic access
|
|
let message = String(localized: "error.generic")
|
|
```
|
|
|
|
## String Extraction Process
|
|
|
|
### Phase 1: Audit Existing Strings
|
|
|
|
Identify hardcoded strings in:
|
|
- View files: `Text()`, `Label()`, `Button()`
|
|
- Alerts: `.alert("...", isPresented:)`
|
|
- Error messages in ViewModels
|
|
- Navigation titles: `.navigationTitle("...")`
|
|
|
|
### Phase 2: Replace Strings
|
|
|
|
Replace hardcoded strings with localized references:
|
|
|
|
```swift
|
|
// Before
|
|
Text("No trips planned yet")
|
|
|
|
// After
|
|
Text("home.empty.title")
|
|
```
|
|
|
|
### Phase 3: Populate String Catalog
|
|
|
|
Add English strings to `Localizable.xcstrings`:
|
|
|
|
```json
|
|
{
|
|
"sourceLanguage": "en",
|
|
"strings": {
|
|
"home.empty.title": {
|
|
"localizations": {
|
|
"en": { "stringUnit": { "value": "No trips planned yet" } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## AI Translation Pipeline
|
|
|
|
### Translation Script Usage
|
|
|
|
```bash
|
|
python Scripts/translate.py --target es,fr,de,ja,zh-Hans
|
|
```
|
|
|
|
### How It Works
|
|
|
|
1. Parse `Localizable.xcstrings` JSON
|
|
2. For each target language, find strings missing translations
|
|
3. Batch strings (10-20 per API call) with context
|
|
4. Write translations back to the String Catalog
|
|
5. Commit the updated file to git
|
|
|
|
### Prompt Strategy
|
|
|
|
```
|
|
You are translating a sports trip planning iOS app.
|
|
The app helps users plan road trips to attend MLB, NBA, NHL, NFL games.
|
|
|
|
Translate to Spanish (Latin America, informal "tú" form).
|
|
Keep sport names (NBA, MLB) and team names in English.
|
|
Keep translations concise for UI buttons and labels.
|
|
|
|
Strings to translate:
|
|
- home.empty.title: "No trips planned yet"
|
|
- button.create_trip: "Plan a Trip"
|
|
- trip.stops.count: "%d stops"
|
|
```
|
|
|
|
### Pluralization
|
|
|
|
String Catalogs handle plurals natively:
|
|
|
|
```json
|
|
"trip.stops.count": {
|
|
"localizations": {
|
|
"es": {
|
|
"variations": {
|
|
"plural": {
|
|
"one": { "stringUnit": { "value": "%d parada" } },
|
|
"other": { "stringUnit": { "value": "%d paradas" } }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Quality Safeguards
|
|
|
|
- Translations committed to git — reviewable in PRs
|
|
- String Catalog shows "Needs Review" state for AI-generated strings
|
|
- Easy to mark specific strings for professional review later
|
|
|
|
## Locale-Aware Formatting
|
|
|
|
### Dates and Times
|
|
|
|
```swift
|
|
// Relative dates ("Tomorrow", "In 3 days")
|
|
game.date.formatted(.relative(presentation: .named))
|
|
|
|
// Game date with weekday
|
|
game.date.formatted(.dateTime.weekday(.wide).month().day())
|
|
// English: "Saturday, March 15"
|
|
// German: "Samstag, 15. März"
|
|
// Japanese: "3月15日土曜日"
|
|
```
|
|
|
|
### Distances
|
|
|
|
```swift
|
|
let distance = Measurement(value: miles, unit: UnitLength.miles)
|
|
distance.formatted(.measurement(width: .abbreviated, usage: .road))
|
|
// US: "245 mi"
|
|
// Germany: "394 km"
|
|
```
|
|
|
|
### Driving Duration
|
|
|
|
```swift
|
|
let duration = Duration.seconds(driveTimeSeconds)
|
|
duration.formatted(.units(allowed: [.hours, .minutes], width: .abbreviated))
|
|
// English: "4 hr, 15 min"
|
|
// French: "4 h 15 min"
|
|
```
|
|
|
|
### Numbers
|
|
|
|
```swift
|
|
let score = 1500
|
|
score.formatted(.number)
|
|
// US: "1,500"
|
|
// Germany: "1.500"
|
|
```
|
|
|
|
## Implementation Plan
|
|
|
|
### Phases
|
|
|
|
1. **Setup** — Create `Localizable.xcstrings`, add translation script to `Scripts/`
|
|
2. **Extract** — Audit and extract all hardcoded strings (~200)
|
|
3. **Translate** — Run AI translation for 5 target languages
|
|
4. **Format Audit** — Verify all dates/distances use Foundation formatters
|
|
5. **Test** — QA each language in Simulator
|
|
6. **Ship** — App Store metadata localization
|
|
|
|
### Testing Strategy
|
|
|
|
Test in Simulator with scheme environment variable:
|
|
|
|
```
|
|
Product → Scheme → Edit Scheme → Run → Arguments
|
|
-AppleLanguages (es)
|
|
-AppleLocale es_MX
|
|
```
|
|
|
|
Key test cases per language:
|
|
- All screens render without truncation
|
|
- Plurals work correctly (0, 1, 2, many items)
|
|
- Dates/distances format correctly
|
|
- Right-to-left layout works if Arabic added later
|
|
|
|
### App Store Metadata
|
|
|
|
Localize separately in App Store Connect:
|
|
- App name (can stay "SportsTime" globally)
|
|
- Subtitle, description, keywords, screenshots
|
|
|
|
### Maintenance Workflow
|
|
|
|
When adding new strings:
|
|
1. Add to Swift code with descriptive key
|
|
2. Xcode auto-adds key to String Catalog with English value
|
|
3. Run `translate.py` to generate translations
|
|
4. Commit updated String Catalog
|