要整合 Mac 应用中的重复消息,请为每种共享含义设置一个本地化键和一个 Swift 定义,并在该消息出现的所有位置使用它。将参数、复数规则和译者上下文放在一起;仅匹配英文文本并不足够。
本指南面向一位 Mac 开发者,他需要在 SwiftUI 和 Interface Builder 窗口中维护选择摘要。具体任务是以一致的方式显示诸如 2 items selected in Batch A. 的消息,而无需在 XIB 和 Swift 代码中分别维护句子模板。下文中的所有数量和标签均为虚构示例。
确定哪些重复消息应该归为一组
编辑目录之前,先定义消息约定:句子的含义、接受哪些值,以及出现的位置。本例的约定如下:
| 约定要素 | 定义 |
|---|---|
| 含义 | 指定集合中当前选中的项目数量 |
| 键 | selection.summary |
| 表 | Localizable |
| Bundle | 主应用 bundle |
| 参数 1 | Int64 类型的选中数量 |
| 参数 2 | 字面集合标签,例如 Batch A |
| 变化形式 | 根据选中数量确定复数形式 |
| 上下文 | 在 SwiftUI 和 AppKit 标签中显示的完整状态句子 |
将此约定用作整合标准。当两处消息的含义、参数和语法作用一致时,它们可以共用同一条消息。选中项目数和项目总数应保持区分,即使两者目前都显示为 2 items。名为 Select 的按钮也需要自己的消息,因为它表达的是一个操作。
Apple 支持使用显式键和独立默认值来消除歧义。这样,你可以按用途命名消息,同时保留易读的源文本。注释用于提供上下文;键用于标识条目。String 的显式键初始化器。
表和 bundle 也属于查找约定的一部分。位于其他表中的键是另一个查找目标。Apple 说明可通过 table 或 tableName 参数选择表,并建议通过 LocalizedStringResource 传递可本地化文本。本例中,将辅助方法和目录放在应用目标中,并使用主 bundle。为应用准备待翻译文本。
了解提取与运行时格式化的界限
提取发生在 Xcode 构建期间:它会发现受支持的可本地化表达式,并将其源文本与目录同步。运行时本地化则发生在应用解析消息并替换实际值时。在目录中找到条目,说明提取过程发现了它;这并不能证明每个控件都能正确显示它。
Interface Builder 有自己的资源工作流。Apple 介绍了与 storyboard 或 XIB 配套的目录,而 Swift 源代码通常会写入 Localizable 表。文档描述的工作流并未提供跨资源别名,无法仅因可见文本相同就合并消息。一种实用且受支持的做法是在界面加载后,由 Swift 设置共用的本地化消息。这种实现方式基于 Apple 文档中的 API。探索字符串目录。
SwiftUI 也区分字面量本地化键和普通字符串值。例如,Text("Summary") 会请求本地化,而传入 String 变量则会直接显示其值,不会执行查找。将运行时字符串包装在 LocalizedStringKey 中会请求查找,但不应将此与证明提取过程发现了它可能的取值混为一谈。LocalizedStringKey。
因此,下面的设计会将可提取的字面量保留在一个辅助方法中。它通过 LocalizedStringResource 传递数量和标签,并在需要完整字符串时解析该资源。Apple 将此资源类型描述为延迟执行本地化查找的一种方式。它适用于 macOS 13 及更高版本,本例的最低目标版本就是 macOS 13。LocalizedStringResource。
在 Swift 中只定义一次共用消息
创建一个使用 SwiftUI 的 macOS 应用项目,将部署目标设为 macOS 13 或更高版本,并将 Localizable.xcstrings 添加到应用目标中。当前 Xcode 文档中的创建路径为 File > New > File from Template;搜索 string,然后选择 String Catalog。使用字符串目录本地化文本并设置其变化形式。
用以下完整程序替换生成的应用入口点。删除原有的应用入口点,确保目标中只有一个 @main 声明。SwiftUI 窗口可以独立运行,不依赖 XIB;接下来一节将使用所包含的 AppKit 子类来适配现有的 Interface Builder 标签。
import AppKit
import Foundation
import SwiftUI
enum SelectionMessages {
static func summary(
count: Int64,
collectionLabel: String
) -> LocalizedStringResource {
LocalizedStringResource(
"selection.summary",
defaultValue: "\(count) items selected in \(collectionLabel).",
table: "Localizable",
comment: "Complete selection-status sentence used in SwiftUI and an AppKit label. Argument 1 is the selected-item count; argument 2 is a literal collection label. Pluralize by argument 1."
)
}
}
struct SelectionDemoView: View {
private let counts: [Int64] = [0, 1, 2]
private let collectionLabel = "Batch A"
var body: some View {
VStack(alignment: .leading, spacing: 12) {
ForEach(counts, id: \.self) { count in
Text(String(localized: SelectionMessages.summary(
count: count,
collectionLabel: collectionLabel
)))
}
}
.padding(24)
.frame(minWidth: 360, minHeight: 160)
}
}
final class SelectionSummaryField: NSTextField {
override func awakeFromNib() {
super.awakeFromNib()
// Synthetic initial state for the Interface Builder example.
updateSelection(count: 2, collectionLabel: "Batch A")
}
func updateSelection(count: Int64, collectionLabel: String) {
stringValue = String(localized: SelectionMessages.summary(
count: count,
collectionLabel: collectionLabel
))
}
}
@main
struct SelectionDemoApp: App {
var body: some Scene {
WindowGroup {
SelectionDemoView()
}
}
}
静态键用于标识消息;插值默认值提供源句子和运行时参数。该初始化器还接受表、bundle 和译者注释。默认 bundle 为主 bundle。LocalizedStringResource 的显式键初始化器。
标签 Batch A 是插入句子中的示例内容,它不是另一个消息键。这样,集合标签就能与周围的界面文本分开。如果实际应用使用预定义的翻译后集合名称,请先通过各自的定义本地化这些名称,再将其传入此处。
不要将句子拆成分别本地化的片段,例如数量、items selected in 和标签。将完整句子纳入约定,就能让本地化人员在同一处调整措辞和参数顺序。
让 Interface Builder 标签使用同一定义
对于现有的 XIB 或 storyboard,请使用静态 NSTextField 标签显示摘要。将其自定义类设为 SelectionSummaryField,并将模块设为应用目标。清除其设计时字符串值中的旧句子,因为此标签现在会从共用的 Swift 定义获取文本。
界面加载时,子类会调用 super.awakeFromNib() 并设置虚构的初始摘要。Apple 将 awakeFromNib() 说明为界面加载完成后的钩子,此时出口和操作连接均已建立,并要求调用超类实现。awakeFromNib()。
在实际窗口中,每当选择发生变化时,都对已连接的标签调用 updateSelection(count:collectionLabel:),并使用与 SwiftUI 相同的模型值。将虚构的初始赋值替换为窗口的实际初始化流程。加载时的一次性赋值无法跟踪之后的选择变化。
AppKit 适配器会设置 stringValue,这是 Apple 文档中为 NSTextField 赋值文本的方式。这样,布局仍由 Interface Builder 管理,而消息则由 Swift 管理。请为完整句子留出足够宽度,或启用换行。NSTextField。
让无关的静态界面文本继续使用现有资源工作流。这次迁移只涉及一条共用消息;无需将所有标签都移到 Swift 中。还要检查是否有绑定或其他控制器赋值会在之后用旧措辞替换摘要。
配置复数形式,同时保持参数不变
选择 Product > Build,打开 Localizable.xcstrings,并找到 selection.summary。按住 Control 键并点按该条目,选择 Vary by Plural;如果 Xcode 询问要根据哪个参数变化,请选择数字数量参数。Apple 文档介绍了这种对插值消息的处理流程。使用字符串目录本地化文本并设置其变化形式。
对于英语源本地化,单数形式应生成 1 item selected in Batch A.,其他形式则应生成诸如 2 items selected in Batch A. 的句子。保留 Xcode 提供的两个参数占位符。修改周围的措辞,不要用示例数量或标签替换占位符。
| 虚构输入 | 配置复数形式后的预期英文结果 |
|---|---|
数量 0,标签 Batch A |
0 items selected in Batch A. |
数量 1,标签 Batch A |
1 item selected in Batch A. |
数量 2,标签 Batch A |
2 items selected in Batch A. |
数量 2,标签 Batch B |
2 items selected in Batch B. |
这些是说明性的验收用例,并非实测结果。在配置复数形式之前,无论数量是多少,默认源句子都仍会包含 items。提取会保留插值消息,但不会自行修正语法。
最后一行中变化的标签有助于诊断问题:如果显示仍是 Batch A,则可能是某个形式中硬编码了示例值,或调用方传入了过时状态。如果标签消失,请检查该形式中的占位符,再考虑更改辅助方法的签名。
移除旧条目前,验证每条显示路径
分别测试 SwiftUI 窗口和实际的 Interface Builder 窗口。两处都使用相同的数量和标签。检查初始加载、之后的选择变化,以及较长的虚构标签(例如 Batch A Extended)。确认措辞一致,且句子易于阅读。
对于每种受支持的本地化,选择 Product > Scheme > Edit Scheme,选中 Run,打开 Options,然后设置 App Language。再次运行应用,并检查两条显示路径。Apple 说明了这些方案设置可用于测试本地化,而无需更改整个系统的语言。运行应用时测试本地化。
如果结果不正确,请按以下顺序排查:
- **缺少条目:**确认目录属于应用目标,并构建相关方案。检查是否已启用
Use Compiler to Extract Swift Strings。 - **意外显示源文本:**检查键、表、bundle 和译文,不要将可读的回退文本误认为成功。
- **只有 Interface Builder 标签不同:**检查其自定义类、模块、加载流程以及之后发生的赋值。
- **只有后续值不同:**检查两个界面是否都收到了更新后的模型值。
- **复数措辞有误:**检查用于设置变化形式的数字参数,以及所有适用的形式。
Apple 说明了编译器提取设置,并解释说,如果源码中不再出现已翻译条目,该条目可能会标记为 Stale。此状态是清理提示,并不能证明资源已弃用。删除条目及其译文之前,请检查旧界面赋值和 Swift 调用方。探索字符串目录。
当两个界面都从同一个定义获取相同且带有上下文的消息、参数完整保留,并且旧句子模板已没有任何调用方时,整合就完成了。将约定和验收用例保留在辅助方法旁边,确保今后修改措辞时,两个窗口共享的含义仍然一致。
来源
为应用准备待翻译文本 — Apple
使用字符串目录本地化文本并设置其变化形式 — Apple
探索字符串目录 — Apple
LocalizedStringKey — Apple
LocalizedStringResource — Apple
init(localized:defaultValue:table:bundle:locale:comment:) — Apple
NSTextField — Apple
awakeFromNib() — Apple
运行应用时测试本地化 — Apple