objc系列译文(9.3):字符串本地化

725 查看

一个应用在进行多语言本地化的时候涉及到大量的工作。因为这一期的主题是字符串,所以本文主要探讨字符串的本地化。字符串本地化有两种方法:修改代码或修改 nib 文件和 storyboard。本文将专注于通过代码实现字符串的本地化。

NSLocalizedString

NSLocalizedString 这个宏是字符串本地化的核心工具。它还有三个鲜为人知的变体:NSLocalizedStringFromTableNSLocalizedStringFromTableInBundle 和 NSLocalizedStringWithDefaultValue。这些宏最终都调用 NSBundle 的 localizedStringForKey:value:table: 方法来完成任务。

使用这些宏有两个好处:一方面相比直接调用 localizedStringForKey:value:table: 方法,使用宏让代码简单易懂;另一方面,类似 genstrings 这样的工具能够监测到这些宏,从而生成供你翻译使用的字符串文件。这些工具会解析 .c 和 .m 后缀的文件,然后为其中每一个需要进行本地化的字符串都生成对应条目,并写入到生成的 .strings 文件中。

如果想让 genstrings 检测自己项目中所有的 .m 后缀文件,可以执行如下命令:

-o 选项指定了生成字符串文件的存放目录,默认情况下文件名是 Localizable.strings。需要注意的是,genstrings 默认会覆盖已存在的同名字符串文件。-a 选项可以让 genstrings 将生成的条目追加到已存在同名文件的末尾,而不会覆盖原文件。

不过一般情况下你也许想将生成文件放到另一个目录中,然后使用你喜欢的合并工具将它们与已有文件合并以保留已翻译好的条目。

字符串文件的格式非常简单,都是键值对的形式:

更复杂的操作比如在需要本地化的字符串中插入格式化占位符等,我们将在稍后谈到。

另外,字符串文件现在可以保存成 UTF-8 格式了,因为 Xcode 在构建过程中能够将它们转换成所需的 UTF-16 格式。

应用中哪些字符串需要本地化?

一般而言,所有你想以某种形式展现在用户眼前的字符串都需要本地化,包括标签和按钮上的文本,或者在运行时通过格式化字符串和数据动态生成的字符串。

在本地化字符串时,根据语法规则为每一种类型的语句定义一个可本地化的字符串是非常重要的。假设你在应用中需要显示「Paul invited you」和「You invited Paul」,那么只本地化格式化字符串「%@ invited %@」看起来是个不错的选择,这样在合适的时候把「you」本地化之后插入进去就可以完成任务。

在英语中这种做法没什么问题,但是请谨记,当把这种小伎俩应用到其他语言中时基本都会以失败而告终。以德语为例,「Paul invited you」译为「Paul hat dich eingeladen」,而「You invited Paul」则译为「Du hast Paul eingeladen」。

正确的做法是定义两个可本地化字符串「%@ invited you」和「You invited %@」,只有这样翻译器才能正确处理其他语言的特殊语法规则。

永远不要将句子分解为几个部分,而要将它们作为一个完整的可本地化字符串。如果一个句子与另一个句子的语法规则并不完全一致,那么即使它们在你的母语中看起来极为相像,也要创建两个可本地化字符串。

字符串键值最佳实践

使用 NSLocalizedString 宏的时候,第一个参数就是为每个特殊字符串指定的键值(key)。程序员经常使用母语中的单词作为键值,这样乍一看是个便利的方案,但是实际上相当糟糕,会引发非常严重的错误。

在一个字符串文件中,键值需要具有唯一性,因此任何母语中字面上具有唯一性的单词在翻译为其他语言的时候也必须具有唯一性。这一点是无法满足的,因为一个单词翻译为其他语言时经常会有多种意思,需要对应到多种文字表示。

以英文单词「run」为例,作为名词表示「跑步」,作为动词表示「奔跑」,在翻译的时候要加以区别。而且根据上下文的不同,每种具体的译法在文字上可能还会有细微变化。

一个健身应用在不同的地方用到这个单词的不同意思是很正常的,但是如果你使用下面的方法来进行本地化:

无论第二个参数指定了注释内容还是留空,你在字符串文件中都只有一个「run」的条目。而在德语中,「run」作名词时应该译为「Lauf」,作动词时则应该译为「laufen」,或者在特定情况下译为完全不同的形式比如「loslaufen」和「Los geht’s」。

好的键值应该满足两个条件:首先键值必须在每个具体的上下文中保持唯一性,其次如果我们没有翻译特定的那个上下文,那么它们不会被其他情况覆盖到而被翻译。

本文推荐使用如下的命名空间方法:

这样的键值可以区分应用中不同地方出现的单词,同时提供具体的上下文,比如是标题中的或者按钮中的。上面的例子里我们为了简便忽略了第二个参数,实际使用中如果键值本身没有提供清晰的上下文说明,你可以将进一步的说明作为第二个参数传入。同时请确保键值中只含有 ASCII 字符。

分割字符串文件

正如我们一开始提到的,NSLocalizedString 有一些变体能够提供更多字符串本地化的操作方式。NSLocalizedStringFromTable接收 key、table 和 comment 这三个参数,其中 table 参数表示该字符串对应的一个表格,genstrings 会为表中的每一个条目生成一个以条目名称(假设为 table-item)命名的独立字符串文件 table-item.strings

这样你就可以把字符串文件分割成几个小一些的文件。在一个庞大的项目或者团队中工作时,这一点显得尤为重要。同时这也让合并原有的和重新生成的字符串文件变得容易一些。

相比在每个地方调用下面的语句:

你可以自定义一个用于字符串本地化的函数来让工作变得轻松一些

为了给所有调用此函数的地方生成字符串文件,你可以在执行 genstrings 的时候加上 -s 选项:

-s 这个选项指定了本地化函数的共同前缀名称,如果你还定义了LocalizedActivityTrackerStringFromTableLocalizedActivityTrackerStringFromTableInBundle,LocalizedActivityTrackerStringWithDefaultValue等函数,以上命令也会调用它们。

运用格式化字符串

我们经常需要对一些在运行时才能最终确定下来的字符串进行本地化,格式化字符串可以完成这项工作。Foundation 在这方面提供了一些非常强大的特性。(可以参考Daniel 的文章获得更多关于格式化字符串的细节)

以字符串「Run 1 out of 3 completed.」为例,我们可以这样构造格式化字符串:

在翻译的时候经常需要对其中的格式化占位符进行顺序调整以符合语法,幸运的是我们可以在字符串文件中轻松地搞定: