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:
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
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 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:
Text("Settings") .font(.headline)
TextField("Enter your name", text: $name)
Button("Submit") { ... }
Text("Hello, (userName)!")
After:
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 "%@"" ):
// Before Text("No results for "(searchText)"")
// After Text(<Module>Strings.Search.noResults(searchText))
Conditional plurals:
// 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" ):
Text(<Module>Strings.Message.detail(userName, fileCount))
Step 5: Validate Changes
-
Build the project to catch missing keys
-
Run validation script to check consistency:
python scripts/validate_strings.py /path/to/<ModuleName>
AI-Powered Translation
When translating strings to non-English locales:
-
Read the English source string
-
Consider context from the key name (e.g., search.noResults = search UI)
-
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
-
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
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:
python scripts/sync_translations.py /path/to/<ModuleName> --report
Add missing keys as placeholders:
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