Files
Sportstime/docs/plans/2026-01-11-localization-design.md
Trey t 5fba9e6052 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>
2026-01-11 01:55:42 -06:00

5.6 KiB

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:

// 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

// 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:

// Before
Text("No trips planned yet")

// After
Text("home.empty.title")

Phase 3: Populate String Catalog

Add English strings to Localizable.xcstrings:

{
  "sourceLanguage": "en",
  "strings": {
    "home.empty.title": {
      "localizations": {
        "en": { "stringUnit": { "value": "No trips planned yet" } }
      }
    }
  }
}

AI Translation Pipeline

Translation Script Usage

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:

"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

// 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

let distance = Measurement(value: miles, unit: UnitLength.miles)
distance.formatted(.measurement(width: .abbreviated, usage: .road))
// US: "245 mi"
// Germany: "394 km"

Driving Duration

let duration = Duration.seconds(driveTimeSeconds)
duration.formatted(.units(allowed: [.hours, .minutes], width: .abbreviated))
// English: "4 hr, 15 min"
// French: "4 h 15 min"

Numbers

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