在使用国际化和本地化时,经常遇到两个术语:“i18n” 和 “l10n”,它们分别是“国际化”和“本地化”的缩写。为什么?嗯,这很简单:
这两者都旨在使键入更容易,并且在存储文件目录时尤为常见。例如,您可能会看到存储在 i18n 目录中的语言文件。这是最容易的部分。稍微令人困惑的部分是这两个词之间的区别:
<aside> 💡 Xcode 中的一个重要设置:转到【Product】菜单,按住 Option 键,然后选择【Run】。然后转到弹窗中的【Option】选项卡,然后选中 “本地化调试 Localization Debugging ” 旁边的框【显示非本地化字符串 Show non-localized strings】。
</aside>
此选项将自动调整应用程序在模拟器中的外观,以便任何未本地化的文本都将以大写字母显示。这一开始可能会让人困惑,尤其是前面所说的“国际化和本地化”是两回事。但是,一旦完成了应用程序的国际化,其实也已经完成了本地化(因为你已经添加了原始语言)。
该选项开启后:
<aside> 💡 第一步是要从应用程序中提取英文字符串,并将它们存储在其他地方。这些将保留为英文,但一旦完成,我们的应用程序将国际化,它将准备好与我们想要添加的任何其他语言一起使用。
</aside>
Localizable.strings
,Xcode 将用它来进行本地化"Filters" = "Fish";
添加到文件末尾,这时再次运行程序,就可以看到应用中的 Filters
已替换为 "Fish"
Localizable.strings
文件中编写 Filters
时,Swift 其实是将其解释为 LocalizedStringKey
对象,然后输入到 SwiftUI 中,后者会在文件中查找它后面的实际值。之后每次您在代码中输入新的字符串,其实 SwiftUI 都会自动尝试在 Localizable.strings
中查找它。现在,查看本地化文件可以看到:
Filters
”,这是我们在 SwiftUI 代码中使用的字符串名称Fish
” ,选择了一个特别的词只是为了看起来效果明显前期,在初始本地化中,相当一部分工作是要把相同的字符串作为键和值。也就是先把要本地化的词全部摘出来,方便后续修改。查找和提取代码中的所有字符串看似非常麻烦,所以 Xcode 附带了一个非常有用的命令行工具,使这个过程变得容易。
启动终端应用程序,并切换到您的 Swift 代码目录。例如运行 cd ~/Desktop/UltimatePortfolio/UltimatePortfolio
运行以下命令:genstrings -SwiftUI *.swift
。它使用 Xcode 的 genstrings 程序,在代码中查找和提取尽可能多的字符串。它并不完美,它只会跟踪存储在 Text
视图中的文本,但也算是给了一个良好的开端
处理 genstrings
在生成过程中的报错
//如果终端提示 “xcode-select: error” 出错,
可以运行 sudo xcode-select -s /Applications/Xcode.app/Contents/Developer 进行修复
//使用 genstrings 的过程中,可能会因为识别到的某些字符串无法转化而报错,例如:
Text(issue.issueCreationDate.formatted(date: .numeric, time: .omitted))
//解决这类问题的一种简单方法是:将代码改成没有问题的简单的东西。把视图中的逻辑移动到视图模型中,尽量让数据和视图分离
var issueFormattedCreationDate: String {
issueCreationDate.formatted(date: .numeric, time: .omitted)
}
//挪过去以后,视图中的代码就比较简单了
Text(issue.issueFormattedCreationDate)
处理完报错后,genstrings
可以正常运行,它将自动覆盖现有的 Localizable.strings 文件
如果 Localizable.strings
文件有提示编码不正确,可以选中文件后,在 xcode 的侧边栏 Text Encoding 选项中进行修改。当切换到 UTF-16 时,Xcode 会询问您是要 Convert
还是 Reinterpret
。老实说不知道,我尝试了 Reinterpret
到目前为止它运行良好。
打开 Localizable.strings
,会发现里面有各种各样的字符串,键和值总是相同的,因为 genstrings
不能翻译东西。另请注意,它在每个字符串之前都放置了一个注释,这是您描述如何使用密钥来帮助翻译人员提供足够信息以进行良好翻译的机会。更重要的是,我们的 Swift 代码没有变化:我们的 Swift 代码中仍然有 “Filters”、“Issues” 等,作为我们应用程序的本地化字符串键。
注意 genstrings -SwiftUI *.swift
命令只会查找当前文件夹内的后缀为 swift 的文件,子文件夹内的文件无法被找到。这样很不方便,如果想要对当前文件夹及其子文件夹中的所有 Swift 文件执行 genstrings 命令,可以使用以下命令:
find . -name "*.swift" -print0 | xargs -0 genstrings -SwiftUI
find . -name "*.swift"
会在当前目录及其子目录中查找所有扩展名为 .swift
的文件print0
选项会使 find
命令在输出每个文件名时使用 null 字符 (\\0
) 作为分隔符,而不是默认的换行符。这是为了避免文件名中包含空格等特殊字符时出现问题| xargs -0
会将 find
命令的输出作为 xargs
命令的参数。0
选项告诉 xargs
使用 null 字符作为参数分隔符genstrings -SwiftUI
是你想要执行的命令。xargs
会将 find
命令找到的所有 Swift 文件作为参数传递给 genstrings
命令这个命令会递归地查找当前目录及其子目录中的所有 Swift 文件,并对每个文件执行 genstrings -SwiftUI 命令。需要注意的是,如果你的文件名或路径中包含空格或其他特殊字符,使用 find 和 xargs 的组合命令会更加安全可靠。
尽管用了 genstrings
,还会有相当多的 string
由于某种原因没有被添加。需要人为检查增补,将遗漏的添加到 Localizable.strings
中
另一个有用的 Xcode 功能是:返回【Product】菜单,按住 Option,然后再次选择【Run】。在【Option】选项里,将 “App Language” 从 “System Language” 更改为 “Double-Length Pseudolanguage 双倍长度伪语言”,它在长列表靠近底部的位置。
设置完成后,再次运行该应用程序。应该看到的是,应用程序的大多数字符串都是两次编写的:例如,Filters 标题现在是 Filters Filters。此设置旨在通过生成人为的长字符串来对布局进行压力测试,其想法是,如果 UI 在长度为两倍时仍能正常工作,则应适用于任何数据。
可以使用 Stringsdict
文件模板,创建一个新文件,并将其命名为 Localizable.stringsdict
。然后在 Localizable.stringsdict
文件上,右键单击,并选择 Open As > Source Code。这将向您显示模板文件背后的原始底层 XML,它更易于阅读、理解和编辑。
后续比较复杂,请参照【代码片段收集】,直接复制去用即可。
有些数据位于我们的数据和用户的数据之间,它们可能无法自动国际化。例如:用户点击按钮创建新对象时,弹窗的输入框里预制的字符串 “New tag” 和 “New issue”。它们来自我们,但当将它们重命名为用户实际使用的任何内容时,它们实际上会成为用户的数据。
这不是我们可以解决 LocalizedStringKey
的问题,因为用户名不是为本地化而设计的,用户可以随心所欲地使用他们想要的任何语言。因此,我们需要在 SwiftUI 的更底层的级别,才能访问底层本地化系统: NSLocalizedString()
//例如这是当前 newTag() 方法在 DataController.swift 中的样子:
func newTag() {
let tag = Tag(context: container.viewContext)
tag.id = UUID()
tag.name = "New tag"
save()
}
这里需要用一个更好的默认名称,以便适应将来的任何本地化。这就是 NSLocalizedString()
,给它一个键名作为它的第一个参数,给它一个翻译器的注释作为它的第二个参数,它将使用 Localizable.strings
和 Localizable.stringsdict
本地化字符串,就像 SwiftUI 一样。
//因此,将当前 name 代码替换为以下代码:
tag.name = NSLocalizedString("New tag", comment: "Create a new tag")
//该 comment 参数在 UI 中实际上不可见,它只是为了使用您之前看到的 Localizable.strings 中的相同注释为翻译人员提供一些上下文。
//这里用小写的 “t” ,因为我认为它感觉更自然,无法想象用户将问题中的每个单词都大写。
//现在我们需要将“New tag”和“New issue”(小写字母“i”)添加到 Localizable.strings 中。现在添加这两个
//这确保了我们有一个可本地化的标签和问题默认名称,但当然,用户可以在之后将它们编辑成他们喜欢的任何内容
"New tag" = "New tag";
"New issue" = "New issue";
// 本地化对以下这样的静态文本是有效的,因为 Text 的视图会自动重载 LocalizedStringKey
Text("CLOSED")
// 但如果界面上是以下这种字符串插值的形式,本地化就会无效了,我们需要自己执行 LocalizedStringKey 重载
var issueStatus: String {
if completed {
return "Closed"
} else {
return "Open"
}
}
Text("**Status:** \\(issue.issueStatus)")
//为了解决这个问题,有以下一些做法:
LocalizedStringKey
使用 LocalizedStringKey
其实也分如下两种方式。但这两种方式都需要动到界面代码,不大好。
// 方式1: 调整 issueStatus 这个属性,直接让它返回 LocalizedStringKey 类型(其具体值还是和String一样不需要改)
// 1.这种方法要导入 swiftUI
import SwiftUI
// 2.返回的类型需要改成 LocalizedStringKey
var issueStatus: LocalizedStringKey {
if completed {
return ("Closed")
} else {
return ("Open")
}
}
// 3.当需要将 LocalizedStringKey 对象和别的字符串组合使用时,需要先将其转化为 Text,再插值
Text("**Status:** \\(Text(issue.issueStatus))")
// 方式2: 不调整计算属性,只是在界面使用时加上 LocalizedStringKey
Text("**Status:** \\(Text(LocalizedStringKey(issue.issueStatus)))")
使用 LocalizedStringKey
初始化设定项,可以显式地强制某些字符串显示为本地化字符串,这是一种常用的技巧。
// 例子1:
NavigationLink(value: filter) {
Label(LocalizedStringKey(filter.name), systemImage: filter.icon)
}
// 例子2:awardTitle 标题原本是返回 String 的,用在 .alert(awardTitle, isPresented: $showingAwardDetails) 上
// 发现无法自动本地化,这时可以把返回类型改成 LocalizedStringKey ,就正常了
var awardTitle: LocalizedStringKey {
if dataController.hasEarned(award: selectedAward) {
return "Unlocked: \\(selectedAward.name)"
} else {
return "Locked"
}
}
NSLocalizedString
这种方法是让 issueStatus
属性使用 NSLocalizedString
,这种方法无疑更好,它不需要动到界面视图代码。
// 这种方法的好处是可以不需要导入 SwiftUI,并且返回类型不需要改,还是 String
var issueStatus: String {
if completed {
return NSLocalizedString("Closed", comment: "This issue has been resolved by the user.")
} else {
return NSLocalizedString("Open", comment: "This issue is currently unresolved.")
}
}
完成本地化英文后,现在的文件存在一些问题:
genstrings
字母顺序出现,这几乎可以肯定没有意义后面的其他语言的本地化,需要复制现有的 Localizable.strings 文件,所以应该先清理该文件,可按以下步骤清理:
SidebarView
、ContentView
、DetailView
、IssueView
…