docs: add localization design
String Catalogs + AI translation pipeline for 5 languages: Spanish, French, German, Japanese, Chinese (Simplified) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
246
docs/plans/2026-01-11-localization-design.md
Normal file
246
docs/plans/2026-01-11-localization-design.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user