app-localization

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

App 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.key
After editing .strings files, run
tuist generate
to regenerate type-safe accessors.
<ModuleName>/
├── Resources/
│   ├── en.lproj/Localizable.strings       # 主语言(英文)
│   ├── <locale>.lproj/Localizable.strings # 其他语言区域
│   └── ...
├── Derived/
│   └── Sources/
│       └── TuistStrings+<ModuleName>.swift  # 由Tuist生成
└── Sources/
    └── **/*.swift  # 使用<ModuleName>Strings.Section.key
编辑.strings文件后,运行
tuist generate
重新生成类型安全访问器。

Complete Localization Workflow

完整本地化工作流

Step 1: Identify Hardcoded Strings

步骤1:识别硬编码字符串

Find hardcoded strings in Swift files:
bash
undefined
在Swift文件中查找硬编码字符串:
bash
undefined

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

Step 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 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.
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 PatternLocalized 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
硬编码模式本地化模式
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))
转换前
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:校验变更

  1. Build the project to catch missing keys
  2. Run validation script to check consistency:
bash
python scripts/validate_strings.py /path/to/<ModuleName>
  1. 构建项目以捕获缺失的键
  2. 运行校验脚本检查一致性:
bash
python scripts/validate_strings.py /path/to/<ModuleName>

AI-Powered Translation

基于AI的翻译

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
将字符串翻译为非英文语言区域时:
  1. 阅读英文源字符串
  2. 结合键名的上下文(例如
    search.noResults
    对应搜索UI)
  3. 针对目标语言区域进行适配翻译:
    • zh-Hans:简体中文,正式且友好
    • zh-Hant:繁体中文
    • ja:日语,礼貌体(です/ます体)
    • ko:韩语,礼貌体(합니다/요体)
    • de/fr/es等:符合对应地区的惯例
  4. 完全保留所有占位符(%@、%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> --report
Add 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> --sync

Key Naming Convention

键命名规范

Pattern:
"domain.context.element"
<Module>Strings.Domain.Context.element
模式:
"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 ModelKey PatternGenerated 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
键名应反映用户的操作场景,而非技术UI组件:
用户心智模型键名模式生成的访问器
"我正在查看我的个人资料"
"profile.name"
Strings.Profile.name
"我正在测试一个构建版本"
"betaBuild.whatToTest"
Strings.BetaBuild.whatToTest
"我正在添加测试人员"
"testerGroup.addTester"
Strings.TesterGroup.addTester
"同步时出现错误"
"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
错误示例(技术导向)正确示例(领域导向)
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
按字符串出现的功能或页面组织键名:
/* 个人资料章节 */
"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)