app-localization
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseApp Localization
应用本地化
Manage iOS/macOS .strings files in Tuist-based projects.
管理基于Tuist的iOS/macOS项目中的.strings文件。
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.keyAfter editing .strings files, run to regenerate type-safe accessors.
tuist generate<ModuleName>/
├── Resources/
│ ├── en.lproj/Localizable.strings # 主语言(英文)
│ ├── <locale>.lproj/Localizable.strings # 其他语言区域
│ └── ...
├── Derived/
│ └── Sources/
│ └── TuistStrings+<ModuleName>.swift # 由Tuist生成
└── Sources/
└── **/*.swift # 使用<ModuleName>Strings.Section.key编辑.strings文件后,运行重新生成类型安全访问器。
tuist generateComplete Localization Workflow
完整本地化工作流
Step 1: Identify Hardcoded Strings
步骤1:识别硬编码字符串
Find hardcoded strings in Swift files:
bash
undefined在Swift文件中查找硬编码字符串:
bash
undefinedFind Text("...") patterns with hardcoded strings
查找包含硬编码字符串的Text("...")模式
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/
undefinedgrep -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/
undefinedStep 2: Add Translation Keys
步骤2:添加翻译键
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> %@";为所有语言文件添加键:
en.lproj/Localizable.strings(主语言):
/* 章节描述 */
"section.key.name" = "English value";
"section.key.withParam" = "Value with %@";其他语言区域(按需翻译):
"section.key.name" = "<translated value>";
"section.key.withParam" = "<translated> %@";Step 3: Generate Type-Safe Accessors
步骤3:生成类型安全访问器
bash
tuist generateThis creates with accessors:
Derived/Sources/TuistStrings+<ModuleName>.swift- (static property)
<ModuleName>Strings.Section.keyName - (static function for %@ params)
<ModuleName>Strings.Section.keyWithParam(value)
See references/tuist-strings-patterns.md for detailed patterns.
bash
tuist generate此命令会创建,包含以下访问器:
Derived/Sources/TuistStrings+<ModuleName>.swift- (静态属性)
<ModuleName>Strings.Section.keyName - (用于%@参数的静态函数)
<ModuleName>Strings.Section.keyWithParam(value)
详细模式请参考references/tuist-strings-patterns.md。
Step 4: Update Swift Code
步骤4:更新Swift代码
Replace hardcoded strings with generated accessors.
用生成的访问器替换硬编码字符串。
Pattern Mapping
模式映射
| Hardcoded Pattern | Localized Pattern |
|---|---|
| |
| |
| |
| |
| 硬编码模式 | 本地化模式 |
|---|---|
| |
| |
| |
| |
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))转换前:
swift
Text("Settings")
.font(.headline)
TextField("Enter your name", text: $name)
Button("Submit") { ... }
Text("Hello, \(userName)!")转换后:
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))带参数的字符串(键:):
"search.noResults" = "No results for \"%@\""swift
// 转换前
Text("No results for \"\(searchText)\"")
// 转换后
Text(<Module>Strings.Search.noResults(searchText))条件复数:
swift
// 键:
// "item.count" = "%d item"
// "item.countPlural" = "%d items"
// Swift代码:
let label = count == 1
? <Module>Strings.Item.count(count)
: <Module>Strings.Item.countPlural(count)多参数(键:):
"message.detail" = "%@ uploaded %d files"swift
Text(<Module>Strings.Message.detail(userName, fileCount))Step 5: Validate Changes
步骤5:校验变更
- Build the project to catch missing keys
- Run validation script to check consistency:
bash
python scripts/validate_strings.py /path/to/<ModuleName>- 构建项目以捕获缺失的键
- 运行校验脚本检查一致性:
bash
python scripts/validate_strings.py /path/to/<ModuleName>AI-Powered Translation
基于AI的翻译
When translating strings to non-English locales:
- Read the English source string
- Consider context from the key name (e.g., = search UI)
search.noResults - 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
将字符串翻译为非英文语言区域时:
- 阅读英文源字符串
- 结合键名的上下文(例如对应搜索UI)
search.noResults - 针对目标语言区域进行适配翻译:
- zh-Hans:简体中文,正式且友好
- zh-Hant:繁体中文
- ja:日语,礼貌体(です/ます体)
- ko:韩语,礼貌体(합니다/요体)
- de/fr/es等:符合对应地区的惯例
- 完全保留所有占位符(%@、%d、%ld等)
按UI元素划分的翻译上下文:
- 标签:保持简洁
- 按钮:使用面向动作的动词
- 占位符:采用指导性语气
- 错误提示:清晰且有帮助
- 确认提示:明确告知结果
Validation Scripts
校验脚本
Validate .strings Files
校验.strings文件
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)
bash
python scripts/validate_strings.py /path/to/<ModuleName>检查内容包括:
- 不同语言间的缺失键
- 重复键
- 占位符不匹配(%@、%d、%ld)
- 未翻译的字符串(值与英文一致)
Sync Missing Translations
同步缺失的翻译
Report missing keys:
bash
python scripts/sync_translations.py /path/to/<ModuleName> --reportAdd missing keys as placeholders:
bash
python scripts/sync_translations.py /path/to/<ModuleName> --sync报告缺失的键:
bash
python scripts/sync_translations.py /path/to/<ModuleName> --report添加缺失的键作为占位符:
bash
python scripts/sync_translations.py /path/to/<ModuleName> --syncKey Naming Convention
键命名规范
Pattern: →
"domain.context.element"<Module>Strings.Domain.Context.element模式: →
"domain.context.element"<Module>Strings.Domain.Context.elementDomain-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" | | |
| "I'm testing a build" | | |
| "I'm adding a tester" | | |
| "Something went wrong with sync" | | |
键名应反映用户的操作场景,而非技术UI组件:
| 用户心智模型 | 键名模式 | 生成的访问器 |
|---|---|---|
| "我正在查看我的个人资料" | | |
| "我正在测试一个构建版本" | | |
| "我正在添加测试人员" | | |
| "同步时出现错误" | | |
Good vs Bad Examples
正反示例对比
| Bad (Technical) | Good (Domain-Focused) |
|---|---|
| |
| |
| |
| |
| |
| |
| 错误示例(技术导向) | 正确示例(领域导向) |
|---|---|
| |
| |
| |
| |
| |
| |
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按字符串出现的功能或页面组织键名:
/* 个人资料章节 */
"profile.title" = "Profile";
"profile.name" = "Name";
"profile.save" = "Save Changes";
"profile.saveSuccess" = "Profile updated";
/* Beta构建版本 */
"betaBuild.title" = "Beta Builds";
"betaBuild.whatToTest" = "What to Test";
"betaBuild.submitForReview" = "Submit for Review";
"betaBuild.expireConfirm" = "Expire this build?";
/* 测试人员组 */
"testerGroup.create" = "Create Group";
"testerGroup.addTester" = "Add Tester";
"testerGroup.empty" = "No testers yet";这种方式贴合用户的思考逻辑:“我在Beta构建版本页面,提交审核” →
betaBuild.submitForReview.strings File Format
.strings文件格式
/* 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
/* 章节描述注释 */
"key.name" = "Value";
"key.with.parameter" = "Hello, %@!";
"key.with.number" = "%d items";
"key.with.multiple" = "%1$@ has %2$d items";规则:
- 键在文件中必须唯一
- 值采用UTF-8编码
- 引号用反斜杠转义:
\" - 行末以分号结尾
- 当不同语言的参数顺序不同时,使用位置参数(%1$@、%2$d)