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,再把回传内容合并进项目。

一个可执行的发布检查表

每次加入用户可见文本时,检查以下几点:

  1. 字面量直接进入支持本地化的 SwiftUI API;
  2. 模型中的可翻译值使用 LocalizedStringResource,不是运行时键字符串;
  3. 动态句子使用插值,复数交给 catalog;
  4. 日期、数字和货币使用 locale-aware FormatStyle;
  5. Package 文本指定正确 bundle;
  6. translator comment 能解释歧义与占位符;
  7. 长文本、RTL 与 Dynamic Type 不会破坏布局;
  8. 发布构建不存在未翻译或失效的键。

String Catalog 解决的是集中管理,不会自动修复错误的数据类型、句子结构和布局假设。把解析延迟到界面、保持完整句子、明确资源归属,并把多语言纳入测试矩阵,SwiftUI 本地化才能从“翻译了几个字符串”变成可靠的产品能力。