localization-ios
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseLocalization iOS — Expert Decisions
iOS本地化——专家决策指南
Expert decision frameworks for localization choices. Claude knows NSLocalizedString and .strings files — this skill provides judgment calls for architecture decisions and cross-language complexity.
本地化选择的专家决策框架。Claude熟悉NSLocalizedString与.strings文件——本技能为架构决策和跨语言复杂度问题提供判断依据。
Decision Trees
决策树
Runtime Language Switching
运行时语言切换
Do users need in-app language control?
├─ NO (respect system language)
│ └─ Standard localization
│ NSLocalizedString, let iOS handle it
│ Simplest and recommended
│
├─ YES (business requirement)
│ └─ Does app restart work?
│ ├─ YES → UserDefaults + AppleLanguages
│ │ Simpler, more reliable
│ └─ NO → Full runtime switching
│ Complex: Bundle swizzling or custom lookup
│
└─ Single language override only (e.g., always English)
└─ Don't localize
Bundle.main with no .lprojThe trap: Implementing runtime language switching when system language suffices. It adds complexity and can break third-party SDKs that read system locale.
用户是否需要应用内语言控制?
├─ 否(遵循系统语言)
│ └─ 标准本地化方案
│ 使用NSLocalizedString,交由iOS处理
│ 最简单且推荐的方案
│
├─ 是(业务需求)
│ └─ 应用重启是否可行?
│ ├─ 是 → UserDefaults + AppleLanguages
│ │ 更简单、更可靠
│ └─ 否 → 完整运行时切换
│ 复杂度高:需使用Bundle Swizzling或自定义查找逻辑
│
└─ 仅需单一语言覆盖(例如始终使用英语)
└─ 无需本地化
使用不带.lproj的Bundle.main误区:在系统语言足以满足需求时仍实现运行时语言切换。这会增加复杂度,还可能导致依赖系统区域设置的第三方SDK出现问题。
String Key Architecture
字符串键架构
How to structure your keys?
├─ Small app (< 100 strings)
│ └─ Flat keys with prefixes
│ "login_title", "login_email_placeholder"
│
├─ Medium app
│ └─ Hierarchical dot notation
│ "auth.login.title", "auth.login.email"
│
├─ Large app with teams
│ └─ Feature-based files
│ Auth.strings, Profile.strings, etc.
│ Each team owns their strings file
│
└─ Design system / component library
└─ Component-scoped keys
"button.primary.title", "input.error.required"如何设计字符串键结构?
├─ 小型应用(少于100个字符串)
│ └─ 带前缀的扁平键
│ "login_title", "login_email_placeholder"
│
├─ 中型应用
│ └─ 层级点符号
│ "auth.login.title", "auth.login.email"
│
├─ 多团队协作的大型应用
│ └─ 基于功能的文件拆分
│ Auth.strings、Profile.strings等
│ 每个团队负责各自的字符串文件
│
└─ 设计系统/组件库
└─ 组件作用域的键
"button.primary.title", "input.error.required"Pluralization Complexity
复数规则复杂度
Which languages do you support?
├─ Western languages only (en, es, fr, de)
│ └─ Simple plural rules
│ one, other (maybe zero)
│
├─ Slavic languages (ru, pl, uk)
│ └─ Complex plural rules
│ one, few, many, other
│ e.g., Russian: 1 файл, 2 файла, 5 файлов
│
├─ Arabic
│ └─ Six plural forms!
│ zero, one, two, few, many, other
│ MUST use stringsdict
│
└─ East Asian (zh, ja, ko)
└─ No grammatical plural
But may need counters/classifiers你需要支持哪些语言?
├─ 仅支持西方语言(英语、西班牙语、法语、德语)
│ └─ 简单复数规则
│ one、other(可能包含zero)
│
├─ 斯拉夫语言(俄语、波兰语、乌克兰语)
│ └─ 复杂复数规则
│ one、few、many、other
│ 例如俄语:1 файл、2 файла、5 файлов
│
├─ 阿拉伯语
│ └─ 六种复数形式!
│ zero、one、two、few、many、other
│ 必须使用stringsdict
│
└─ 东亚语言(中文、日语、韩语)
└─ 无语法复数
但可能需要使用量词/分类词RTL Support Level
RTL支持等级
Do you support RTL languages?
├─ NO RTL languages planned
│ └─ Still use leading/trailing
│ Future-proof your layout
│
├─ Arabic only
│ └─ Standard RTL support
│ layoutDirection + leading/trailing
│ Test thoroughly
│
├─ Arabic + Hebrew + Persian
│ └─ Each has unique considerations
│ Hebrew: different number handling
│ Persian: different numerals (۱۲۳)
│
└─ Mixed LTR/RTL content
└─ Explicit direction per component
Force LTR for code, URLs, numbers你是否支持RTL语言?
├─ 无RTL语言支持计划
│ └─ 仍使用leading/trailing布局
│ 为未来扩展做好准备
│
├─ 仅支持阿拉伯语
│ └─ 标准RTL支持
│ layoutDirection + leading/trailing
│ 需全面测试
│
├─ 支持阿拉伯语+希伯来语+波斯语
│ └─ 每种语言有独特注意事项
│ 希伯来语:数字处理方式不同
│ 波斯语:使用不同的数字(۱۲۳)
│
└─ 混合LTR/RTL内容
└─ 为每个组件设置明确方向
强制代码、URL、数字使用LTRNEVER Do
绝对禁忌
String Management
字符串管理
NEVER concatenate localized strings:
swift
// ❌ Breaks in languages with different word order
let message = NSLocalizedString("hello", comment: "") + " " + userName
// German: "Hallo" + " " + "Hans" = "Hallo Hans" ✓
// Japanese: "こんにちは" + " " + "田中" = "こんにちは 田中" ✗
// Should be "田中さん、こんにちは"
// ✅ Use format strings
let format = NSLocalizedString("greeting.format", comment: "")
let message = String(format: format, userName)
// greeting.format = "Hello, %@!" (en)
// greeting.format = "%@さん、こんにちは!" (ja)NEVER embed numbers in translation keys:
swift
// ❌ Doesn't handle plural rules
"items.1" = "1 item"
"items.2" = "2 items"
"items.3" = "3 items"
// What about 0? 100? Arabic's 6 forms?
// ✅ Use stringsdict for plurals
String.localizedStringWithFormat(
NSLocalizedString("items.count", comment: ""),
count
)NEVER assume string length:
swift
// ❌ German is ~30% longer than English
.frame(width: 100) // "Settings" fits, "Einstellungen" doesn't
// ✅ Use flexible layouts
.frame(minWidth: 80)
// Or
.fixedSize(horizontal: true, vertical: false)NEVER use left/right in layouts:
swift
// ❌ Breaks in RTL
.padding(.left, 16)
.frame(alignment: .left)
// ✅ Use leading/trailing
.padding(.leading, 16)
.frame(alignment: .leading)绝对不要拼接本地化字符串:
swift
// ❌ 在语序不同的语言中会出错
let message = NSLocalizedString("hello", comment: "") + " " + userName
// 德语:"Hallo" + " " + "Hans" = "Hallo Hans" ✓
// 日语:"こんにちは" + " " + "田中" = "こんにちは 田中" ✗
// 正确写法应为 "田中さん、こんにちは"
// ✅ 使用格式化字符串
let format = NSLocalizedString("greeting.format", comment: "")
let message = String(format: format, userName)
// greeting.format = "Hello, %@!"(英语)
// greeting.format = "%@さん、こんにちは!"(日语)绝对不要在翻译键中嵌入数字:
swift
// ❌ 无法处理复数规则
"items.1" = "1 item"
"items.2" = "2 items"
"items.3" = "3 items"
// 那0、100怎么办?阿拉伯语的6种形式呢?
// ✅ 使用stringsdict处理复数
String.localizedStringWithFormat(
NSLocalizedString("items.count", comment: ""),
count
)绝对不要假设字符串长度:
swift
// ❌ 德语词汇比英语长约30%
.frame(width: 100) // "Settings"能放下,但"Einstellungen"放不下
// ✅ 使用弹性布局
.frame(minWidth: 80)
// 或者
.fixedSize(horizontal: true, vertical: false)绝对不要在布局中使用left/right:
swift
// ❌ 在RTL环境中会失效
.padding(.left, 16)
.frame(alignment: .left)
// ✅ 使用leading/trailing
.padding(.leading, 16)
.frame(alignment: .leading)Runtime Language
运行时语言
NEVER change AppleLanguages without restart:
swift
// ❌ Partial UI update — inconsistent state
UserDefaults.standard.set(["ar"], forKey: "AppleLanguages")
// Some views updated, others not. Third-party SDKs broken.
// ✅ Require restart or use custom bundle
UserDefaults.standard.set(["ar"], forKey: "AppleLanguages")
showRestartRequiredAlert() // User restarts appNEVER forget to set locale for formatters:
swift
// ❌ Uses device locale, not app's selected language
let formatter = DateFormatter()
formatter.dateStyle = .medium
let date = formatter.string(from: Date()) // Wrong language!
// ✅ Set locale explicitly
let formatter = DateFormatter()
formatter.locale = Locale(identifier: selectedLanguage.rawValue)
formatter.dateStyle = .medium绝对不要不重启应用就修改AppleLanguages:
swift
// ❌ UI更新不完整——状态不一致
UserDefaults.standard.set(["ar"], forKey: "AppleLanguages")
// 部分视图更新,部分未更新,第三方SDK可能失效
// ✅ 要求重启应用或使用自定义Bundle
UserDefaults.standard.set(["ar"], forKey: "AppleLanguages")
showRestartRequiredAlert() // 用户重启应用绝对不要忘记为格式化器设置区域:
swift
// ❌ 使用设备区域设置,而非应用选定的语言
let formatter = DateFormatter()
formatter.dateStyle = .medium
let date = formatter.string(from: Date()) // 语言错误!
// ✅ 显式设置区域
let formatter = DateFormatter()
formatter.locale = Locale(identifier: selectedLanguage.rawValue)
formatter.dateStyle = .mediumPluralization
复数处理
NEVER use simple if/else for plurals:
swift
// ❌ Fails for Russian, Arabic, etc.
func itemsText(_ count: Int) -> String {
if count == 1 {
return "1 item"
} else {
return "\(count) items"
}
}
// Russian: 1 товар, 2 товара, 5 товаров, 21 товар, 22 товара...
// This requires CLDR plural rules
// ✅ Use stringsdict — iOS handles rules automaticallyNEVER hardcode numeral systems:
swift
// ❌ Arabic users may expect Arabic-Indic numerals
Text("\(count) items") // Shows "5 items" even in Arabic
// ✅ Use NumberFormatter with locale
let formatter = NumberFormatter()
formatter.locale = Locale(identifier: "ar")
formatter.string(from: count as NSNumber) // "٥"绝对不要使用简单的if/else处理复数:
swift
// ❌ 在俄语、阿拉伯语等语言中会失效
func itemsText(_ count: Int) -> String {
if count == 1 {
return "1 item"
} else {
return "\(count) items"
}
}
// 俄语:1 товар、2 товара、5 товаров、21 товар、22 товара...
// 这需要CLDR复数规则
// ✅ 使用stringsdict——iOS会自动处理规则绝对不要硬编码数字系统:
swift
// ❌ 阿拉伯语用户可能期望使用阿拉伯-印度数字
Text("\(count) items") // 即使在阿拉伯语环境中仍显示"5 items"
// ✅ 使用带区域设置的NumberFormatter
let formatter = NumberFormatter()
formatter.locale = Locale(identifier: "ar")
formatter.string(from: count as NSNumber) // "٥"RTL Layouts
RTL布局
NEVER use fixed directional icons:
swift
// ❌ Arrow points wrong way in RTL
Image(systemName: "arrow.right")
// ✅ Use semantic icons or flip
Image(systemName: "arrow.forward") // Semantic
// Or
Image(systemName: "arrow.right")
.flipsForRightToLeftLayoutDirection(true)NEVER force layout direction globally when it should be per-component:
swift
// ❌ Phone numbers, code, etc. should stay LTR
.environment(\.layoutDirection, .rightToLeft)
// ✅ Apply selectively
VStack {
Text(localizedContent) // Follows RTL
Text(phoneNumber)
.environment(\.layoutDirection, .leftToRight) // Always LTR
Text(codeSnippet)
.environment(\.layoutDirection, .leftToRight)
}绝对不要使用固定方向的图标:
swift
// ❌ 在RTL环境中箭头方向错误
Image(systemName: "arrow.right")
// ✅ 使用语义化图标或翻转图标
Image(systemName: "arrow.forward") // 语义化图标
// 或者
Image(systemName: "arrow.right")
.flipsForRightToLeftLayoutDirection(true)绝对不要在需要按组件设置时全局强制布局方向:
swift
// ❌ 电话号码、代码等内容应保持LTR
.environment(\.layoutDirection, .rightToLeft)
// ✅ 选择性应用
VStack {
Text(localizedContent) // 遵循RTL方向
Text(phoneNumber)
.environment(\.layoutDirection, .leftToRight) // 始终保持LTR
Text(codeSnippet)
.environment(\.layoutDirection, .leftToRight)
}Essential Patterns
核心模式
Type-Safe Localization with SwiftGen
使用SwiftGen实现类型安全的本地化
swift
// swiftgen.yml
// strings:
// inputs: Resources/en.lproj/Localizable.strings
// outputs:
// - templateName: structured-swift5
// output: Generated/Strings.swift
// Usage — compile-time safe
Text(L10n.Auth.Login.title)
Text(L10n.User.greeting(userName))
Text(L10n.Items.count(itemCount))
// Benefits:
// - Compiler catches missing keys
// - Auto-complete for strings
// - Refactoring safeswift
// swiftgen.yml
// strings:
// inputs: Resources/en.lproj/Localizable.strings
// outputs:
// - templateName: structured-swift5
// output: Generated/Strings.swift
// 使用方式——编译时安全
Text(L10n.Auth.Login.title)
Text(L10n.User.greeting(userName))
Text(L10n.Items.count(itemCount))
// 优势:
// - 编译器会捕获缺失的键
// - 自动补全字符串
// - 重构安全Stringsdict for Plurals
使用Stringsdict处理复数
xml
<!-- en.lproj/Localizable.stringsdict -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ...>
<plist version="1.0">
<dict>
<key>items.count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@items@</string>
<key>items</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>No items</string>
<key>one</key>
<string>%d item</string>
<key>other</key>
<string>%d items</string>
</dict>
</dict>
</dict>
</plist>swift
// Usage
let text = String.localizedStringWithFormat(
NSLocalizedString("items.count", comment: ""),
count
)
// 0 → "No items"
// 1 → "1 item"
// 5 → "5 items"xml
<!-- en.lproj/Localizable.stringsdict -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" ...>
<plist version="1.0">
<dict>
<key>items.count</key>
<dict>
<key>NSStringLocalizedFormatKey</key>
<string>%#@items@</string>
<key>items</key>
<dict>
<key>NSStringFormatSpecTypeKey</key>
<string>NSStringPluralRuleType</string>
<key>NSStringFormatValueTypeKey</key>
<string>d</string>
<key>zero</key>
<string>No items</string>
<key>one</key>
<string>%d item</string>
<key>other</key>
<string>%d items</string>
</dict>
</dict>
</dict>
</plist>swift
// 使用方式
let text = String.localizedStringWithFormat(
NSLocalizedString("items.count", comment: ""),
count
)
// 0 → "No items"
// 1 → "1 item"
// 5 → "5 items"RTL-Aware Layout Helpers
RTL感知的布局助手
swift
extension View {
/// Applies leading alignment that respects RTL
func alignLeading() -> some View {
self.frame(maxWidth: .infinity, alignment: .leading)
}
/// Force LTR for content that shouldn't flip (code, URLs, phone numbers)
func forceLTR() -> some View {
self.environment(\.layoutDirection, .leftToRight)
}
}
// ContentView
struct MessageCell: View {
let message: Message
var body: some View {
VStack(alignment: .leading) {
Text(message.content)
.alignLeading() // Respects RTL
Text(message.codeSnippet)
.font(.monospaced(.body)())
.forceLTR() // Code always LTR
Text(message.url)
.forceLTR() // URLs always LTR
}
}
}swift
extension View {
/// 应用遵循RTL的左对齐
func alignLeading() -> some View {
self.frame(maxWidth: .infinity, alignment: .leading)
}
/// 强制内容保持LTR(适用于代码、URL、电话号码等不应翻转的内容)
func forceLTR() -> some View {
self.environment(\.layoutDirection, .leftToRight)
}
}
// 内容视图
struct MessageCell: View {
let message: Message
var body: some View {
VStack(alignment: .leading) {
Text(message.content)
.alignLeading() // 遵循RTL方向
Text(message.codeSnippet)
.font(.monospaced(.body)())
.forceLTR() // 代码始终保持LTR
Text(message.url)
.forceLTR() // URL始终保持LTR
}
}
}Locale-Aware Formatting
区域感知的格式化
swift
struct LocalizedFormatters {
let locale: Locale
init(languageCode: String) {
self.locale = Locale(identifier: languageCode)
}
func formatDate(_ date: Date, style: DateFormatter.Style = .medium) -> String {
let formatter = DateFormatter()
formatter.locale = locale
formatter.dateStyle = style
return formatter.string(from: date)
}
func formatNumber(_ number: Double) -> String {
let formatter = NumberFormatter()
formatter.locale = locale
formatter.numberStyle = .decimal
return formatter.string(from: NSNumber(value: number)) ?? "\(number)"
}
func formatCurrency(_ amount: Double, code: String) -> String {
let formatter = NumberFormatter()
formatter.locale = locale
formatter.numberStyle = .currency
formatter.currencyCode = code
return formatter.string(from: NSNumber(value: amount)) ?? "\(amount)"
}
}swift
struct LocalizedFormatters {
let locale: Locale
init(languageCode: String) {
self.locale = Locale(identifier: languageCode)
}
func formatDate(_ date: Date, style: DateFormatter.Style = .medium) -> String {
let formatter = DateFormatter()
formatter.locale = locale
formatter.dateStyle = style
return formatter.string(from: date)
}
func formatNumber(_ number: Double) -> String {
let formatter = NumberFormatter()
formatter.locale = locale
formatter.numberStyle = .decimal
return formatter.string(from: NSNumber(value: number)) ?? "\(number)"
}
func formatCurrency(_ amount: Double, code: String) -> String {
let formatter = NumberFormatter()
formatter.locale = locale
formatter.numberStyle = .currency
formatter.currencyCode = code
return formatter.string(from: NSNumber(value: amount)) ?? "\(amount)"
}
}Quick Reference
快速参考
Plural Forms by Language
各语言复数形式
| Language | Forms | Example (1, 2, 5) |
|---|---|---|
| English | one, other | 1 item, 2 items, 5 items |
| French | one, other | 1 élément, 2 éléments |
| Russian | one, few, many, other | 1 файл, 2 файла, 5 файлов |
| Arabic | zero, one, two, few, many, other | 6 forms! |
| Japanese | other only | No grammatical plural |
| 语言 | 复数形式 | 示例(1、2、5) |
|---|---|---|
| 英语 | one、other | 1个项目、2个项目、5个项目 |
| 法语 | one、other | 1 élément、2 éléments |
| 俄语 | one、few、many、other | 1 файл、2 файла、5 файлов |
| 阿拉伯语 | zero、one、two、few、many、other | 6种形式! |
| 日语 | 仅other | 无语法复数 |
RTL Languages
RTL语言
| Language | Script Direction | Numerals |
|---|---|---|
| Arabic | RTL | Arabic-Indic (٠١٢) or Western |
| Hebrew | RTL | Western |
| Persian | RTL | Extended Arabic (۰۱۲) |
| Urdu | RTL | Extended Arabic |
| 语言 | 文字方向 | 数字系统 |
|---|---|---|
| 阿拉伯语 | RTL | 阿拉伯-印度数字(٠١٢)或西方数字 |
| 希伯来语 | RTL | 西方数字 |
| 波斯语 | RTL | 扩展阿拉伯数字(۰۱۲) |
| 乌尔都语 | RTL | 扩展阿拉伯数字 |
String Expansion Guidelines
字符串扩展指南
| Source (English) | Expansion |
|---|---|
| 1-10 chars | +200-300% |
| 11-20 chars | +80-100% |
| 21-50 chars | +60-80% |
| 51-70 chars | +50-60% |
| 70+ chars | +30% |
| 源语言(英语) | 扩展比例 |
|---|---|
| 1-10个字符 | +200-300% |
| 11-20个字符 | +80-100% |
| 21-50个字符 | +60-80% |
| 51-70个字符 | +50-60% |
| 70+个字符 | +30% |
Red Flags
危险信号
| Smell | Problem | Fix |
|---|---|---|
| String concatenation | Word order varies | Format strings |
| if count == 1 else | Wrong plural rules | stringsdict |
| .padding(.left) | Breaks RTL | .padding(.leading) |
| DateFormatter without locale | Wrong language | Set locale explicitly |
| Runtime language without restart | Inconsistent UI | Require restart |
| Fixed frame widths for text | Text truncation | Flexible layouts |
| Hardcoded "1, 2, 3" | Wrong numeral system | NumberFormatter with locale |
| 问题迹象 | 潜在问题 | 修复方案 |
|---|---|---|
| 字符串拼接 | 语序适配问题 | 使用格式化字符串 |
| if count == 1 else 逻辑 | 复数规则错误 | 使用stringsdict |
| .padding(.left) | RTL环境失效 | 使用.padding(.leading) |
| DateFormatter未设置区域 | 语言显示错误 | 显式设置区域 |
| 运行时语言切换未重启应用 | UI状态不一致 | 要求用户重启应用 |
| 文本使用固定宽度布局 | 文本被截断 | 使用弹性布局 |
| 硬编码数字“1、2、3” | 数字系统不匹配 | 使用带区域的NumberFormatter |