home / skills / tddworks / claude-skills / tuist-app-localization

tuist-app-localization skill

/skills/tuist-app-localization

This skill helps manage iOS/macOS .strings files in Tuist-based projects, validate keys, and synchronize translations across locales.

npx playbooks add skill tddworks/claude-skills --skill tuist-app-localization

Review the files below or copy the command above to add this skill to your agents.

Files (4)
SKILL.md
7.2 KB
---
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

```
<ModuleName>/
├── Resources/
│   ├── en.lproj/Localizable.strings       # Primary language (English)
│   ├── <locale>.lproj/Localizable.strings # Additional locales
│   └── ...
├── Derived/
│   └── Sources/
│       └── TuistStrings+<ModuleName>.swift  # Generated by Tuist
└── Sources/
    └── **/*.swift  # Uses <ModuleName>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]' <ModuleName>/Sources/
grep -rn 'title: "[A-Z]' <ModuleName>/Sources/
grep -rn 'label: "[A-Z]' <ModuleName>/Sources/
grep -rn 'placeholder: "[A-Z]' <ModuleName>/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" = "<translated value>";
"section.key.withParam" = "<translated> %@";
```

### Step 3: Generate Type-Safe Accessors

```bash
tuist generate
```

This creates `Derived/Sources/TuistStrings+<ModuleName>.swift` with accessors:
- `<ModuleName>Strings.Section.keyName` (static property)
- `<ModuleName>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(<Module>Strings.Section.title)` |
| `Text("Hello, \(name)")` | `Text(<Module>Strings.Section.hello(name))` |
| `title: "Submit"` | `title: <Module>Strings.Action.submit` |
| `placeholder: "Enter..."` | `placeholder: <Module>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(<Module>Strings.Section.settings)
    .font(.headline)

TextField(<Module>Strings.Field.namePlaceholder, text: $name)

Button(<Module>Strings.Action.submit) { ... }

Text(<Module>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(<Module>Strings.Search.noResults(searchText))
```

**Conditional plurals**:
```swift
// Keys:
// "item.count" = "%d item"
// "item.countPlural" = "%d items"

// Swift:
let label = count == 1
    ? <Module>Strings.Item.count(count)
    : <Module>Strings.Item.countPlural(count)
```

**Multiple parameters** (key: `"message.detail" = "%@ uploaded %d files"`):
```swift
Text(<Module>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/<ModuleName>
```

## 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/<ModuleName>
```

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/<ModuleName> --report
```

Add missing keys as placeholders:
```bash
python scripts/sync_translations.py /path/to/<ModuleName> --sync
```

## Key Naming Convention

Pattern: `"domain.context.element"` → `<Module>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

Overview

This skill manages iOS/macOS localization for Tuist-based projects that use .strings files and generated TuistStrings accessors. It streamlines adding and syncing translation keys, validating placeholders and duplicates, running AI-powered translations from English to other locales, and updating Swift code to use type-safe localized accessors. The goal is consistent, maintainable localization across modules and languages.

How this skill works

The skill inspects module Resources/*.lproj/Localizable.strings files and Sources for hardcoded strings, then validates keys across locales for missing, duplicate, or placeholder mismatches. It can generate or sync missing keys as placeholders, run AI-assisted translations from the English source while preserving placeholders, and provide transformations to replace hardcoded Swift strings with Tuist-generated accessors. It also produces reports and invokes validation scripts to catch issues before build.

When to use it

  • Adding new translation keys to a module and ensuring all locales have entries
  • Validating .strings files for missing keys, duplicates, or placeholder mismatches
  • Syncing translations across locales and creating placeholder entries for missing keys
  • AI-assisted translation from English to target locales while preserving placeholders
  • Refactoring Swift code to replace hardcoded strings with TuistStrings accessors
  • Generating localization reports for reviewers or translators

Best practices

  • Add keys first to English (primary) and mirror key names across all locales before translating
  • Use domain-focused key naming (domain.context.element) to reflect user intent
  • Preserve all placeholders exactly (%@, %d, %ld, %1$@, etc.) and use positional parameters when word order differs
  • Run tuist generate after editing .strings to update type-safe accessors and then replace hardcoded strings in Swift
  • Validate .strings files with the provided scripts to catch missing or duplicate keys and mismatched placeholders before building

Example use cases

  • Find hardcoded Text/label/title/placeholder strings in a module and replace them with generated accessors
  • Add a new UI string: add key to en.lproj and corresponding keys to other locales, then run tuist generate
  • Detect and report missing translations and sync placeholders across locales using the sync script
  • Translate an English .strings file to zh-Hans, ja, de, etc., using AI while preserving placeholders and context
  • Validate placeholder consistency across all locales to avoid runtime formatting errors
  • Generate a localization report listing untranslated or duplicate keys for reviewers

FAQ

How do I replace hardcoded Swift strings with generated accessors?

Add the key to Localizable.strings, run tuist generate to create TuistStrings accessors, then replace occurrences (e.g., Text("Title") → Text(ModuleStrings.Section.title).

How are placeholders handled during translation?

Always preserve placeholder tokens exactly. Use positional parameters (%1$@, %2$d) when target-language word order differs to ensure correct formatting.