SwiftUI String Catalog 工程化:从字符串提取到可测试的多语言界面
SwiftUI 的本地化常常在第一天显得非常简单:把 Text("Settings") 写进界面,Xcode 构建后自动提取到 String Catalog。真正的复杂度会在第二种语言上线后出现——运行时生成的字符串没有进入 catalog、复数规则被字符串拼接破坏、Swift Package 一直显示英文、德语按钮把布局撑坏,而测试只覆盖了开发者自己的语言。
解决这些问题的关键不是增加更多 NSLocalizedString,而是把“用户可见文本在什么时机、由哪个 bundle、按什么 locale 解析”设计成明确契约。
字面量直接交给 SwiftUI
接受 LocalizedStringKey 的 SwiftUI API 会把字符串字面量视为本地化键,包括 Text、Button、Label、导航标题和 alert 标题:
Button("Save changes") {
save()
}
Text("Welcome back",
comment: "Greeting shown above the account dashboard.")
不要把字面量提前包成 String(localized:) 再交给 Text。这样会过早解析,也会失去视图环境中 locale 覆盖的优势。只有明确不应翻译的内容,例如调试标识或协议固定值,才使用 Text(verbatim:)。
项目必须先创建 .xcstrings 文件;Xcode 不会凭空生成 catalog。随后每次构建会扫描本地化 API,把新键同步到 catalog。键可以是自然语言,也可以是 settings.save_button 这类不透明标识,选择一种与现有项目一致的策略即可。
String 变量不会自动翻译
下面的代码看似与字面量相同,实际调用的是普通 String 重载:
let title: String = feature.title
Text(title) // 直接显示,不查询 String Catalog
如果值来自有限的业务状态,不要把待翻译键塞进运行时字符串。让模型暴露 LocalizedStringResource,把解析推迟到显示阶段:
enum SyncState {
case idle, syncing, complete
var title: LocalizedStringResource {
switch self {
case .idle: "Ready to sync"
case .syncing: "Syncing"
case .complete: "Sync complete"
}
}
}
Text(state.title)
这样 Xcode 能在构建时发现所有分支,模型也不会把已经按某个 locale 解析的 String 长期缓存起来。
插值,不要拼接句子
语言的词序不同,不能把几个独立翻译片段用 + 组装:
// 不推荐:翻译者无法重排完整句子
Text("Created by " + author.name)
// 推荐:catalog 得到带占位符的完整句子
Text("Created by \(author.name)")
插值会保留可本地化格式,让翻译者调整变量位置。为包含多个占位符的文本添加 comment,说明每个值的语义,而不是假设变量名会出现在翻译工具里。
复数也不应该写成 count == 1 ? ... : ...。先把带整数插值的完整文本传给本地化 API,再在 String Catalog 中启用 Vary by Plural。英语可能只有 one 和 other,其他语言可能需要更多形式,业务代码不应硬编码语法规则。
日期、数字和货币由 locale 排版
硬编码 MM/dd/yyyy 或在数字前拼接货币符号,会把某一地区的习惯强加给所有用户。优先使用 FormatStyle:
Text(order.date, format: .dateTime.year().month().day())
Text(order.total, format: .currency(code: order.currencyCode))
Text(progress, format: .percent.precision(.fractionLength(0)))
代码选择显示哪些字段,locale 决定字段顺序、分隔符、数字和货币格式。列表也应使用 formatted() 生成符合语言习惯的连接词,而不是固定 joined(separator: ", ")。
Package 和 Framework 必须指定资源 bundle
应用 target 中可以依赖主 bundle,但 Swift Package 或 framework 的本地化资源不在那里。组件库中的文本应明确使用当前模块资源:
Text(
"Save to Favorites",
bundle: #bundle,
comment: "Button that bookmarks an item."
)
遗漏 bundle 通常不会崩溃,只会安静地回退到键本身,因此很容易在只测英文时漏掉。每个可复用模块都应该拥有自己的 catalog,并在示例应用中切换语言验证。
布局必须允许翻译改变尺寸和方向
英文短按钮变成长德语、阿拉伯语切换为从右到左、中文行高与拉丁文字不同,都是正常输入。使用 .leading 与 .trailing,避免给文字固定宽高,并让按钮可换行或在空间不足时切换布局:
ViewThatFits {
HStack { actionButtons }
VStack { actionButtons }
}
字体使用语义 text style,而不是固定点数。图标有方向含义时检查其 RTL 行为;纯视觉顺序不要被写死成 left/right。
把语言矩阵放进预览与 CI
在预览中注入 locale,可以快速发现截断和硬编码文本:
#Preview("German") {
CheckoutView(order: .preview)
.environment(\.locale, Locale(identifier: "de"))
.environment(\.layoutDirection, .leftToRight)
}
#Preview("Arabic") {
CheckoutView(order: .preview)
.environment(\.locale, Locale(identifier: "ar"))
.environment(\.layoutDirection, .rightToLeft)
}
预览不是完整测试。CI 还应构建所有 catalog、检查缺失翻译,并至少用一种长文本语言和一种 RTL 语言运行关键页面截图或 UI 测试。需要交付翻译团队时,可以通过 Xcode 或 xcodebuild -exportLocalizations 导出 XLIFF,再把回传内容合并进项目。
一个可执行的发布检查表
每次加入用户可见文本时,检查以下几点:
- 字面量直接进入支持本地化的 SwiftUI API;
- 模型中的可翻译值使用
LocalizedStringResource,不是运行时键字符串; - 动态句子使用插值,复数交给 catalog;
- 日期、数字和货币使用 locale-aware FormatStyle;
- Package 文本指定正确 bundle;
- translator comment 能解释歧义与占位符;
- 长文本、RTL 与 Dynamic Type 不会破坏布局;
- 发布构建不存在未翻译或失效的键。
String Catalog 解决的是集中管理,不会自动修复错误的数据类型、句子结构和布局假设。把解析延迟到界面、保持完整句子、明确资源归属,并把多语言纳入测试矩阵,SwiftUI 本地化才能从“翻译了几个字符串”变成可靠的产品能力。