i18n & l10n

在使用国际化和本地化时,经常遇到两个术语:“i18n” 和 “l10n”,它们分别是“国际化”和“本地化”的缩写。为什么?嗯,这很简单:

这两者都旨在使键入更容易,并且在存储文件目录时尤为常见。例如,您可能会看到存储在 i18n 目录中的语言文件。这是最容易的部分。稍微令人困惑的部分是这两个词之间的区别:


Internationalization 开启国际化

<aside> 💡 Xcode 中的一个重要设置:转到【Product】菜单,按住 Option 键,然后选择【Run】。然后转到弹窗中的【Option】选项卡,然后选中 “本地化调试 Localization Debugging ” 旁边的框【显示非本地化字符串 Show non-localized strings】。

</aside>

此选项将自动调整应用程序在模拟器中的外观,以便任何未本地化的文本都将以大写字母显示。这一开始可能会让人困惑,尤其是前面所说的“国际化和本地化”是两回事。但是,一旦完成了应用程序的国际化,其实也已经完成了本地化(因为你已经添加了原始语言)。

该选项开启后:


Localizing to English 英文本地化

<aside> 💡 第一步是要从应用程序中提取英文字符串,并将它们存储在其他地方。这些将保留为英文,但一旦完成,我们的应用程序将国际化,它将准备好与我们想要添加的任何其他语言一起使用。

</aside>

1. 创建字符串文件

现在,查看本地化文件可以看到:

  1. 左边是一个键字符串,在本例中为 “Filters”,这是我们在 SwiftUI 代码中使用的字符串名称
  2. 右边是一个值字符串,在本例中是 “Fish” ,选择了一个特别的词只是为了看起来效果明显
  3. 最后是一个分号( ; )分号是必需的,如果不添加,Xcode 会抛出完全无用的错误,它只会说“输入数据的格式无效”,而无法说明哪行

2. 初始化

前期,在初始本地化中,相当一部分工作是要把相同的字符串作为键和值。也就是先把要本地化的词全部摘出来,方便后续修改。查找和提取代码中的所有字符串看似非常麻烦,所以 Xcode 附带了一个非常有用的命令行工具,使这个过程变得容易。

处理所有子文件夹的文件

注意 genstrings -SwiftUI *.swift 命令只会查找当前文件夹内的后缀为 swift 的文件,子文件夹内的文件无法被找到。这样很不方便,如果想要对当前文件夹及其子文件夹中的所有 Swift 文件执行 genstrings 命令,可以使用以下命令:

find . -name "*.swift" -print0 | xargs -0 genstrings -SwiftUI

这个命令会递归地查找当前目录及其子目录中的所有 Swift 文件,并对每个文件执行 genstrings -SwiftUI 命令。需要注意的是,如果你的文件名或路径中包含空格或其他特殊字符,使用 find 和 xargs 的组合命令会更加安全可靠。

3. 清理增补

尽管用了 genstrings ,还会有相当多的 string 由于某种原因没有被添加。需要人为检查增补,将遗漏的添加到 Localizable.strings

4. 语言字符压力测试

另一个有用的 Xcode 功能是:返回【Product】菜单,按住 Option,然后再次选择【Run】。在【Option】选项里,将 “App Language” 从 “System Language” 更改为 “Double-Length Pseudolanguage 双倍长度伪语言”,它在长列表靠近底部的位置。

设置完成后,再次运行该应用程序。应该看到的是,应用程序的大多数字符串都是两次编写的:例如,Filters 标题现在是 Filters Filters。此设置旨在通过生成人为的长字符串来对布局进行压力测试,其想法是,如果 UI 在长度为两倍时仍能正常工作,则应适用于任何数据。

5. 解决英语复数的问题

可以使用 Stringsdict 文件模板,创建一个新文件,并将其命名为 Localizable.stringsdict。然后在 Localizable.stringsdict 文件上,右键单击,并选择 Open As > Source Code。这将向您显示模板文件背后的原始底层 XML,它更易于阅读、理解和编辑。

后续比较复杂,请参照【代码片段收集】,直接复制去用即可。

6. 本地化用户数据 NSLocalizedString

有些数据位于我们的数据和用户的数据之间,它们可能无法自动国际化。例如:用户点击按钮创建新对象时,弹窗的输入框里预制的字符串 “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.stringsLocalizable.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";

7. 解决文本无法本地化的问题

// 本地化对以下这样的静态文本是有效的,因为 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.")
    }
}

9. 整理 Localizable.strings 文件内容

完成本地化英文后,现在的文件存在一些问题:

后面的其他语言的本地化,需要复制现有的 Localizable.strings 文件,所以应该先清理该文件,可按以下步骤清理:

  1. 删除所有“No comment provided by engineer”注释
  2. 重新排列字符串行,并且将相近的字符串,合理地归组在一起
  3. 在这些组上方添加注释,描述它们的使用文件位置