最近项目有了国际化(i18n)的需求,正好 WWDC23 上 Apple 推出了 String Catalog 功能。趁此机会尝试一下,并将一些经验记录下来。

如果你对 String Catalog 不太熟悉,可以先参考 老司机技术 WWDC 23 内参,或者直接观看 WWDC 的 官方视频文档 来了解更多。

好了废话不多说,开始今天的内容。

开发环境

  • Xcode:16.2
  • Swift:5.9
  • 框架:UIKit
  • 最低支持版本:iOS 13
  • 组件化工具:CocoaPods

Xcode 15 及以上版本支持 String Catalog,版本对功能无特别限制。
但由于项目最低支持版本为 iOS 13,而 String(localized:) 是 iOS 15 引入的 API,因此需要额外处理低版本兼容的问题。

此外,关于 String Catalog 在 CocoaPods 组件化项目中的使用,相关资料较少,因此这部分内容需要实际摸索一下。

项目准备工作

首先,在项目的 PROJECT 设置中添加需要支持的语言:

添加多语言

接着可以(可选)调整 Scheme 的模拟器设置,将语言和地区设置为待测试的环境:

修改模拟器语言

例如将语言设置为英语、地区为美国。此操作便于验证翻译效果,但地区设置通常不会影响文案展示。

String Catalog + CocoaPods

在主项目中使用 String Catalog 可以算作在 CocoaPods 中使用的一个子集,所以我们直接讲解如何在 CocoaPods 中使用 String Catalog。

新建文件

通过 Command+N 新建 String Catalog 文件,效果如下:

新建 String Catalog 文件

提示
文件里显示的语言,是根据所属 .xcodeproj 设置里添加的语言自动生成的。
Pods.xcodeproj 默认包含多种语言,所以 Pods 中的 .xcstrings 文件才会显示这么多的语种。
但这不会影响最终的 App。鉴于我们平时在使用时,不会将 Pods.xcodeproj 提交到 git,同时 App 的多语言设置不受 Pods 的影响,所以作为开发者你完全可以无视这一点,只从 String Catalog 挑选你关注的语种即可。

这里我新建了一个文件,叫 HomeLocalizable.xcstrings,并加了两个文案。

修改 Podspec 文件

接着,我们需要修改 podspec,把 .xcstrings 文件加到资源文件中。

说到这里,聪明的你肯定可以直接猜到,是不是可以仿照 .xcassets 类型的文件,直接将 .xcstrings 文件放到 resource_bundle 的自定义 Bundle 中。没错,确实是这样:

s.resource_bundle = {
  'HomeBundle' => [
    'Sources/Resource/**/*.xcstrings',
    'Sources/Resource/**/*.xcassets',
  ]
}

注意:
如果只将 .xcstrings 放入 Bundle 中的话会有一个问题,详见 Pod 与 String Catalog 自动化 这一小节。

然后执行 pod install,这样 .xcstrings 文件就会被包含到 HomeBundle 里了。

添加新的文言

选择我们的默认语种,点击加号即可添加新的文言。此后同样的操作在需要翻译的语种中,设置对应文言的翻译内容即可。

需要注意的是,默认语种中文言的状态(State)默认是不展示,即 Reviewed 已审核过的,和其他语种中的绿色对勾是一样的。而当我们新建一个文言后,其他语种中文言的状态则是NEW

我实际测试了一下,当我们向 NEW 状态的文言添加对应的翻译,或者修改了 NEEDS REVIEW 状态的文言所对应的翻译后,该条文言的状态将自动变为 Reviewed

另外,就算文言处于 NEEDS REVIEW 状态,只要包含了对应的翻译,那么该翻译也是可以正常显示的。不过从字面上来看这一行为有些危险,稳妥起见建议开发者确保所有文言无误后,将其标记为 Reviewed 后再使用。

展示国际化文本

这一节我们就要提到 String(localized:) 的问题了。网上大部分介绍 String Catalog 的文章都会使用 String(localized: ) 这一个新的 api,然而当我实际使时才发现,它仅限于 iOS 15 + 的版本。那么该怎么办呢?

其实我们还可以使用老的 NSLocalizedString 方法。

let text = NSLocalizedString(
    "扫码或长按小程序码", 
    tableName: "HomeLocalizable", 
    bundle: BundleToken.bundle, 
    comment: ""
)

private final class BundleToken {
  static let bundle: Bundle = {
    #if SWIFT_PACKAGE
    return Bundle.module
    #else
    guard
      let resourcePath = Bundle(for: BundleToken.self).resourcePath,
      let bundle = Bundle(path: resourcePath + "/HomeBundle.bundle") // ⬅️ Watch this.
    else {
        fatalError("Bundle not found, please make sure the Bundle name is correct!")
    }
    return bundle
    #endif
  }()
}

tableName.xcstrings 文件名,bundle 是在 podspec 里定义的 HomeBundle

这里的 BundleToken 参考了 SwiftGen 的实现,做了一些适配 CocoaPods 的改动:增加 HomeBundle

如果你好奇为什么使用老的 NSLocalizedString api 也可以,那么我们可以先 build 编译一下项目,然后在 Products 目录找到编译好了之后的应用,最后找到 HomeBundle.bundle

此时你就会发现,.xcstrings 虽然不像是 .bundle 一样是一个 “包”,但是在编译时,它会被编译为和 .xcstrings 同名的 .strings 文件,所以我们依然可以使用 NSLocalizedString api 来读取相关设置,这点在 WWDC 的视频中也有相关说明:

SwiftGen

截止文章发布的日期(2024.12.30),SwiftGen 尚未正式支持 String Catalog。(SwiftGen 目前好像进入停更的状态)

不过有一个相关的 pr:Xcode 15 String catalog support #1124。pr 的作者好心的打了一个 6.6.4 的 tag,如果你使用 mise 的话可以改到这个地址。

顺带一提,这似乎也影响到了 Tuist 对于 String Catalog 的支持。

如果你不想分叉自编译 SwiftGen 可以考虑尝试一个名为 xcstrings-tool 的新工具,Swift 开发,不支持 brew,但是支持通过 mise 安装 cli 单独使用,详见文档

一些注意

因为这是我第一次总结 i18n 相关的内容,所以也顺带记录一些问题或者注意事项。可能比较常见,比较基础。

nslocalizedstring_key

SwiftLint 中包含着一条名为 nslocalizedstring_key 的规则,文档在这里。

它建议不要动态传入 key,比如这样是不推荐的:

// 不推荐
let key = "example_key"
let text = NSLocalizedString(key, comment: "")

Pod 与 String Catalog 自动化

在 Xcode 15 中,如果设置了 Use Compiler to Extract Swift StringsYES,那么 String Catalog 文件就可以自动追踪国际化文本,比如下面这个字符串:

let test = NSLocalizedString("Test", tableName: "TestLocalizable", comment: "Test")

编译后会有一个 “Syncing Localizations” 的阶段,在该阶段这个字符串会被自动记录到 TestLocalizable.xcstrings 文件中。

但是在 pod 组件化项目中,如果该字符串在某个 pod 内,则自动追踪不会生效。原因有以下几点:

  1. “Syncing Localizations” 阶段仅会扫描当前 Scheme 所包含的 target。一般我们编译时都会选择主项目,此时是不包含 pod target 的,所以不会扫描相应的代码。
  2. Pod 的 Use Compiler to Extract Swift Strings 设置项默认为 NO,需要在 Podfile 中使用钩子手动开启(Key 为 SWIFT_EMIT_LOC_STRINGS)。
  3. 前文中,.xcstrings 被添加到 Bundle 中,这意味着它会被归入 Bundle 这个 target,比如 Pod 名为 Home,那么 .xcstrings 文件就在 Home-HomeBundle 这个 target 中。而项目代码则在 Home 这个 target 里。此时编译 Home,因为该 target 实际不包含 .xcstrings,所以字符串也不会同步到 .xcstrings 文件中。

综上,考虑到 CocoaPods 已经停止增加新功能,建议还是尽快迁移到 SPM。