在 WWDC23 上,Swift 引入了一种用于 Swift 应用和包的国际化(i18n)新方式,即字符串目录(String Catalogs)。本指南涵盖了在新项目中开始使用字符串目录的基础知识:WWDC23 String Catalogs。然而,它没有涉及在单一代码库中包含多个 Swift 包的用例(并且没有示例项目)。
在本文中,我们将探讨如何设置你的单库,使每个 Swift 包都可以拥有自己的字符串目录,并在构建主应用时自动合并为一个目录,而无需费力!
这篇文章由我的朋友和同为 iOS 开发者的 Luca Ban 撰写。他将分享一些关于 Swift Catalogs 的独特见解和经验。感谢他的贡献,让我们一起阅读这篇精彩的客座文章吧!
初始设置
在这篇博客文章中,我们将创建一个包含两个本地化项目的简单 monorepo:一个 Xcode 项目和一个 Swift Package。我们的目标是分别对以下视图进行本地化(一个字符串在宿主应用中,一个字符串在 Swift Package 中),并在构建宿主应用时合并这些翻译。
VStack {
// 按钮标签在 **宿主应用** 字符串目录中本地化:
MyButton(action: {}, label: "Let's tap here!")
// 默认按钮标签在 **Swift Package** 字符串目录中本地化的备用标签:
MyButton(action: {})
}
我们将在下面的步骤 1 到 7 中完成初始设置。如果你已经有一个包含 Swift Packages 和宿主应用的 monorepo,可以直接跳到 在 HostApp 中显示 Swift 包的翻译。
-
创建 HostApp 项目和 MyComponents 包
- 使用 Xcode 创建一个 Xcode 项目(
/HostApp
)和一个 Swift 包(/MyComponents
)。 - 参考 WWDC23 视频:
- 在主应用中添加一个新的“字符串目录”。
- 添加
Text(LocalizedStringResource("Text from host app", comment: "shown on the main screen"))
。 - 构建项目,你将看到字符串目录中填充了
"Text from host app"
及其注释。 - 通过点击字符串目录左下角的
+
按钮添加一种新语言,并翻译该字符串。
- 使用 Xcode 创建一个 Xcode 项目(
-
验证初始设置
- 在 Xcode 中按住
option
键并点击RUN
按钮运行HostApp
。 - 将语言设置为新语言进行测试,并运行应用。
- 你应该会看到翻译后的字符串。
- 在 Xcode 中按住
在 MyComponents 包中添加本地化文本
-
在 MyComponents 中创建本地化按钮
- 在
MyComponents
中,创建一个按钮,该按钮可以接收LocalizedStringResource
类型的label
,并且已经在额外语言中进行了翻译的回退标签。
Swiftpublic struct MyButton: View { private let label: LocalizedStringResource? private let action: () -> Void public init( action: @escaping () -> Void, label: LocalizedStringResource? = nil ) { self.action = action self.label = label } public var body: some View { Button(action: action) { Text(label ?? LocalizedStringResource("Tap here", comment: "Button label fallback text")) } } }
- 在
-
为 MyComponents 设置字符串目录
- 在
MyComponents/Sources/MyComponents/Resources
目录下创建一个新文件夹来存放字符串目录。 - 将一个名为
Localizable.xcstring
的字符串目录文件保存在该文件夹中。 - 在
Package.swift
中添加一行代码,以表明这个 Swift 包是本地化的:
Swiftlet package = Package( name: "MyComponents", defaultLocalization: "en", // ...
- 将字符串目录打包到资源文件夹中无需额外配置,因为这是 Swift 包中任何资源的默认位置。
- 在
-
将 MyComponents 导入 HostApp
- 在 Xcode 中,转到
HostApp
目标 > 常规 > 框架、库和嵌入内容 > 添加其他… > 添加包依赖… > 添加本地… > 选择/MyComponents
文件夹。
- 在 Xcode 中,转到
集成 MyButton 并构建以添加新翻译
-
更新 HostApp 中的 ContentView
SwiftVStack(spacing: 16) { Text(LocalizedStringResource("Text from host app", comment: "shown on the main screen")) MyButton(action: {}) // 应使用 MyButton 的回退标签 MyButton(action: {}, label: "Let's tap here!") }
-
构建并添加新翻译
- 如果再次构建
HostApp
,它将扫描HostApp
和导入的MyComponents
包中的新 Swift 字符串,并将其添加到字符串目录中。 - 它现在会找到新字符串,并将其添加到字符串目录中:
- 在
HostApp
的字符串目录中,我们看到新添加的字符串 “Let’s tap here!” - 在
MyComponents
的字符串目录中,我们看到新添加的字符串 “Tap here”
- 在
- 翻译这两个字符串,并在新语言中再次运行应用。
- 如果再次构建
在 HostApp 中显示 Swift 包的翻译
尽管在 MyComponents
中本地化了 “Tap here” 的回退文本,但在没有额外设置的情况下,它不会显示在 HostApp 中。MyComponents
中打包的字符串目录似乎完全被我们的 HostApp
忽略了。
以前使用字符串字典时,我们需要在构建步骤中运行一个复杂的脚本来合并多个文件。然而,现在有一个无需运行任何额外脚本的解决方案。
你必须确保将 Swift 包中每个 LocalizedStringResource
实例限定到该模块本身。然而,添加 bundle
参数并不能直接提供一种简单的方法:
让我们用一种简洁的方法来解决这个问题。
-
将 Swift 包的
LocalizedStringResource
限定到其自身模块- 让我们为
LocalizedStringResource
的bundle
参数定义一个新选项。 - 添加一个扩展来定义
.module
:
Swiftextension LocalizedStringResource.BundleDescription { /// 方便计算 _这个_ Swift 包的 `BundleDescription` 的属性 static let module: LocalizedStringResource.BundleDescription = .atURL(Bundle.module.bundleURL) }
- 让我们为
-
使用 Bundle 更新 MyButton
- 现在,让我们在
MyButton
代码中使用这个新的bundle: .module
:
Swiftpublic struct MyButton: View { // ... public var body: some View { Button(action: action) { Text(label ?? LocalizedStringResource("Tap here", bundle: .module, comment: "Button label fallback text")) } } }
- 现在,让我们在
-
最终验证
- 再次构建并运行
HostApp
。 - 现在显示从
/MyComponents
中 Swift 包字符串目录打包的翻译! - 你可以为单库中的多个 Swift 包重复此过程。
- 你也可以使用这种方法通过 SPM 提供带有自己翻译的 Swift 包。
- 再次构建并运行
这个解决方案提供了一种简洁的方法来合并单库设置中的多个字符串目录,大大改善了需要自定义脚本的旧方法。无需在 Xcode 中进行任何额外配置。
这是我的概念验证单库,具有与本文相同的设置:github.com/mesqueeb/SwiftStringCatalogsPOC。克隆并在本地运行以进行测试。
我是 Luca Ban(GitHub/Twitter 上的 mesqueeb
),希望你喜欢这篇文章,并且它对你有所帮助。
个人简介
Luca Ban
- x: mesqueeb
- github: mesqueeb
- 我为 visionOS 制作桌游!🎲 请查看 craftingtable.studio