--- name: app-localization description: | iOS/macOS app localization management for Tuist-based projects with .strings files. Use when: (1) Adding new translation keys to modules, (2) Validating .strings files for missing/duplicate keys, (3) Syncing translations across languages, (4) AI-powered translation from English to other locales, (5) Checking placeholder consistency (%@, %d), (6) Generating localization reports, (7) Updating Swift code to use localized strings instead of hardcoded text. --- # App Localization Manage iOS/macOS .strings files in Tuist-based projects. ## Project Structure ``` / ├── Resources/ │ ├── en.lproj/Localizable.strings # Primary language (English) │ ├── .lproj/Localizable.strings # Additional locales │ └── ... ├── Derived/ │ └── Sources/ │ └── TuistStrings+.swift # Generated by Tuist └── Sources/ └── **/*.swift # Uses Strings.Section.key ``` After editing .strings files, run `tuist generate` to regenerate type-safe accessors. ## Complete Localization Workflow ### Step 1: Identify Hardcoded Strings Find hardcoded strings in Swift files: ```bash # Find Text("...") patterns with hardcoded strings grep -rn 'Text("[A-Z]' /Sources/ grep -rn 'title: "[A-Z]' /Sources/ grep -rn 'label: "[A-Z]' /Sources/ grep -rn 'placeholder: "[A-Z]' /Sources/ ``` ### Step 2: Add Translation Keys Add keys to **all** language files: **en.lproj/Localizable.strings** (primary): ``` /* Section description */ "section.key.name" = "English value"; "section.key.withParam" = "Value with %@"; ``` **Other locales** (translate appropriately): ``` "section.key.name" = ""; "section.key.withParam" = " %@"; ``` ### Step 3: Generate Type-Safe Accessors ```bash tuist generate ``` This creates `Derived/Sources/TuistStrings+.swift` with accessors: - `Strings.Section.keyName` (static property) - `Strings.Section.keyWithParam(value)` (static function for %@ params) See [references/tuist-strings-patterns.md](references/tuist-strings-patterns.md) for detailed patterns. ### Step 4: Update Swift Code Replace hardcoded strings with generated accessors. #### Pattern Mapping | Hardcoded Pattern | Localized Pattern | |-------------------|-------------------| | `Text("Title")` | `Text(Strings.Section.title)` | | `Text("Hello, \(name)")` | `Text(Strings.Section.hello(name))` | | `title: "Submit"` | `title: Strings.Action.submit` | | `placeholder: "Enter..."` | `placeholder: Strings.Field.placeholder` | #### Example Transformations **Before**: ```swift Text("Settings") .font(.headline) TextField("Enter your name", text: $name) Button("Submit") { ... } Text("Hello, \(userName)!") ``` **After**: ```swift Text(Strings.Section.settings) .font(.headline) TextField(Strings.Field.namePlaceholder, text: $name) Button(Strings.Action.submit) { ... } Text(Strings.Greeting.hello(userName)) ``` #### Handling Parameters and Plurals **String with parameter** (key: `"search.noResults" = "No results for \"%@\""`): ```swift // Before Text("No results for \"\(searchText)\"") // After Text(Strings.Search.noResults(searchText)) ``` **Conditional plurals**: ```swift // Keys: // "item.count" = "%d item" // "item.countPlural" = "%d items" // Swift: let label = count == 1 ? Strings.Item.count(count) : Strings.Item.countPlural(count) ``` **Multiple parameters** (key: `"message.detail" = "%@ uploaded %d files"`): ```swift Text(Strings.Message.detail(userName, fileCount)) ``` ### Step 5: Validate Changes 1. Build the project to catch missing keys 2. Run validation script to check consistency: ```bash python scripts/validate_strings.py /path/to/ ``` ## AI-Powered Translation When translating strings to non-English locales: 1. Read the English source string 2. Consider context from the key name (e.g., `search.noResults` = search UI) 3. Translate appropriately for the target locale: - **zh-Hans**: Simplified Chinese, formal but friendly - **zh-Hant**: Traditional Chinese - **ja**: Japanese, polite form (desu/masu style) - **ko**: Korean, polite form (hamnida/yo style) - **de/fr/es/etc.**: Appropriate regional conventions 4. Preserve all placeholders exactly (%@, %d, %ld, etc.) **Translation context by UI element**: - Labels: Keep concise - Buttons: Action-oriented verbs - Placeholders: Instructive tone - Error messages: Helpful and clear - Confirmations: Clear consequences ## Validation Scripts ### Validate .strings Files ```bash python scripts/validate_strings.py /path/to/ ``` Checks for: - Missing keys between languages - Duplicate keys - Placeholder mismatches (%@, %d, %ld) - Untranslated strings (value = English) ### Sync Missing Translations Report missing keys: ```bash python scripts/sync_translations.py /path/to/ --report ``` Add missing keys as placeholders: ```bash python scripts/sync_translations.py /path/to/ --sync ``` ## Key Naming Convention Pattern: `"domain.context.element"` → `Strings.Domain.Context.element` ### Domain-Focused Naming (User Mental Model) Keys should reflect **what the user is doing**, not technical UI components: | User Mental Model | Key Pattern | Generated Accessor | |-------------------|-------------|-------------------| | "I'm looking at my profile" | `"profile.name"` | `Strings.Profile.name` | | "I'm testing a build" | `"betaBuild.whatToTest"` | `Strings.BetaBuild.whatToTest` | | "I'm adding a tester" | `"testerGroup.addTester"` | `Strings.TesterGroup.addTester` | | "Something went wrong with sync" | `"sync.error.failed"` | `Strings.Sync.Error.failed` | ### Good vs Bad Examples | Bad (Technical) | Good (Domain-Focused) | |-----------------|----------------------| | `button.save` | `profile.save` | | `field.email` | `registration.email` | | `placeholder.search` | `appSelector.searchPlaceholder` | | `error.network` | `sync.connectionFailed` | | `label.title` | `settings.title` | | `alert.confirm` | `build.expireConfirm` | ### Structure by Feature/Screen Organize keys by the feature or screen where they appear: ``` /* Profile Section */ "profile.title" = "Profile"; "profile.name" = "Name"; "profile.save" = "Save Changes"; "profile.saveSuccess" = "Profile updated"; /* Beta Builds */ "betaBuild.title" = "Beta Builds"; "betaBuild.whatToTest" = "What to Test"; "betaBuild.submitForReview" = "Submit for Review"; "betaBuild.expireConfirm" = "Expire this build?"; /* Tester Groups */ "testerGroup.create" = "Create Group"; "testerGroup.addTester" = "Add Tester"; "testerGroup.empty" = "No testers yet"; ``` This mirrors how users think: "I'm in Beta Builds, submitting for review" → `betaBuild.submitForReview` ## .strings File Format ``` /* Comment describing the section */ "key.name" = "Value"; "key.with.parameter" = "Hello, %@!"; "key.with.number" = "%d items"; "key.with.multiple" = "%1$@ has %2$d items"; ``` Rules: - Keys must be unique within a file - Values are UTF-8 encoded - Escape quotes with backslash: `\"` - Line ends with semicolon - Use positional parameters (%1$@, %2$d) when order differs between languages