NSLocalizedString
NSLocalizedString 这个宏是字符串本地化的核心工具。它还有三个鲜为人知的变体:NSLocalizedStringFromTable、NSLocalizedStringFromTableInBundle 和 NSLocalizedStringWithDefaultValue。这些宏最终都调用 NSBundle 的 localizedStringForKey:value:table: 方法来完成任务
使用这些宏有两个好处:一方面相比直接调用 localizedStringForKey:value:table: 方法,使用宏让代码简单易懂;另一方面,类似 genstrings 这样的工具能够监测到这些宏,从而生成供你翻译使用的字符串文件。这些工具会解析 .c 和 .m 后缀的文件,然后为其中每一个需要进行本地化的字符串都生成对应条目,并写入到生成的 .strings 文件中。
如果想让 genstrings 检测自己项目中所有的 .m 后缀文件,可以执行如下命令:
find . -name *.m | xargs genstrings -o en.lproj
-o 选项指定了生成字符串文件的存放目录,默认情况下文件名是 Localizable.strings。需要注意的是,genstrings 默认会覆盖已存在的同名字符串文件。-a 选项可以让 genstrings 将生成的条目追加到已存在同名文件的末尾,而不会覆盖原文件。
不过一般情况下你也许想将生成文件放到另一个目录中,然后使用你喜欢的合并工具将它们与已有文件合并以保留已翻译好的条目。
字符串文件的格式非常简单,都是键值对的形式:
/* Insert new contact button */
"contact-editor.insert-new-contact-button" = "Insert contact";
/* Delete contact button */
"contact-editor.delete-contact-button" = "Delete contact";
字符串文件现在可以保存成 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」为例,作为名词表示「跑步」,作为动词表示「奔跑」,在翻译的时候要加以区别。而且根据上下文的不同,每种具体的译法在文字上可能还会有细微变化。
一个健身应用在不同的地方用到这个单词的不同意思是很正常的,但是如果你使用下面的方法来进行本地化
NSLocalizedString(@"Run", nil)
无论第二个参数指定了注释内容还是留空,你在字符串文件中都只有一个「run」的条目。而在德语中,「run」作名词时应该译为「Lauf」,作动词时则应该译为「laufen」,或者在特定情况下译为完全不同的形式比如「loslaufen」和「Los geht’s」。
好的键值应该满足两个条件:首先键值必须在每个具体的上下文中保持唯一性,其次如果我们没有翻译特定的那个上下文,那么它们不会被其他情况覆盖到而被翻译。
本文推荐使用如下的命名空间方法:
NSLocalizedString(@"activity-profile.title.the-run", nil)
NSLocalizedString(@"home.button.start-run", nil)
这样的键值可以区分应用中不同地方出现的单词,同时提供具体的上下文,比如是标题中的或者按钮中的。上面的例子里我们为了简便忽略了第二个参数,实际使用中如果键值本身没有提供清晰的上下文说明,你可以将进一步的说明作为第二个参数传入。同时请确保键值中只含有 ASCII 字符。
分割字符串文件
正如我们一开始提到的,NSLocalizedString 有一些变体能够提供更多字符串本地化的操作方式。NSLocalizedStringFromTable 接收 key、table 和 comment 这三个参数,其中 table 参数表示该字符串对应的一个表格,genstrings 会为表中的每一个条目生成一个以条目名称(假设为 table-item)命名的独立字符串文件 table-item.strings。
这样你就可以把字符串文件分割成几个小一些的文件。在一个庞大的项目或者团队中工作时,这一点显得尤为重要。同时这也让合并原有的和重新生成的字符串文件变得容易一些。
相比在每个地方调用下面的语句:
NSLocalizedStringFromTable(@"home.button.start-run", @"ActivityTracker", @"some comment..")
你可以自定义一个用于字符串本地化的函数来让工作变得轻松一些
static NSString * LocalizedActivityTrackerString(NSString *key, NSString *comment) {
return [[NSBundle mainBundle] localizedStringForKey:key value:key table:@"ActivityTracker"];
}
为了给所有调用此函数的地方生成字符串文件,你可以在执行 genstrings 的时候加上 -s 选项:
find . -name *.m | xargs genstrings -o en.lproj -s LocalizedActivityTrackerString
-s 这个选项指定了本地化函数的共同前缀名称,如果你还定义了 LocalizedActivityTrackerStringFromTable,LocalizedActivityTrackerStringFromTableInBundle, LocalizedActivityTrackerStringWithDefaultValue 等函数,以上命令也会调用它们。
运用格式化字符串
我们经常需要对一些在运行时才能最终确定下来的字符串进行本地化,格式化字符串可以完成这项工作。Foundation 在这方面提供了一些非常强大的特性。(可以参考Daniel 的文章获得更多关于格式化字符串的细节
以字符串「Run 1 out of 3 completed.」为例,我们可以这样构造格式化字符串:
NSString *localizedString = NSLocalizedString(@"activity-profile.label.run %lu out of %lu completed", nil);
self.label.text = [NSString localizedStringWithFormat:localizedString, completedRuns, totalRuns];
在翻译的时候经常需要对其中的格式化占位符进行顺序调整以符合语法,幸运的是我们可以在字符串文件中轻松地搞定:
"activity-profile.label.run %lu out of %lu completed" = "Von %2$lu Läufen hast du %$1lu absolver";
上面的德文翻译得不是非常好,只是单纯用来说明调换占位符顺序的功能而已。
如果你需要对简单的整数或者浮点数进行本地化,你可以使用 localizedStringWithFormat: 这个变体。数字本地化的更高级用法涉及 NSNumberFormatter,会在本文后面讲到
单复数与阴阳性
在 OS X 10.9 和 iOS 7 中,本地化字符串的时候可以使用比替换格式化字符串中的占位符更酷的特性:苹果官方想处理不同语言中对于名词复数和不同性别采取的不同变化。
让我们再看一下之前的例子:@”%lu out of %lu runs completed.” 这个翻译在「跑多次」的时候才是对的(译者注:即第二个 %lu 代表的数字大于 1),所以我们不得不定义两个不同的字符串来处理单次和多次的情况:
@"%lu out of one run completed"
@"%lu out of %lu runs completed"
这种做法在英语中是对的,但是在其他很多语言中会出错。比如希伯来语中名词有三种形式:第一种是单数和十的倍数,第二种是 2,第三种是其他的复数。克罗地亚语中,个位数为 1 的数字有单独的表示方法:「31 od 32 staze završene」,与之相对的是「5 od 8 staza završene」(注意其中「staze」和「staza」的差别)。很多语言针对非整型数也有不同的表达方式。
想全面了解这个问题可以参见基于 Unicode 的语言复数规则。其中涵盖的变化之博大精深令人叹为观止。
为了在 10.9 和 iOS 7 平台上正确处理这个问题,我们需要如下构造可本地化字符串:
[NSString localizedStringWithFormat:NSLocalizedString(@"activity-profile.label.%lu out of %lu runs completed"), completedRuns, totalRuns];
然后我们在 .strings 后缀文件所处目录中创建一个同名的 .stringsdict 后缀的文件,如果前者名为 Localizable.strings,则后者为 Localizable.stringsdict。保留 .strings 后缀的字符串文件是必须的,即使它里面什么内容也没有。这个 .stringsdict 后缀的字符串字典文件是一个属性列表(plist)文件,比字符串文件复杂得多,换来的是正确处理所有语言的名词复数问题,而不需要将处理逻辑写在代码中。