NSLocalizedString
NSLocalizedString 這個(gè)宏是字符串本地化的核心工具。它還有三個(gè)鮮為人知的變體:NSLocalizedStringFromTable、NSLocalizedStringFromTableInBundle 和 NSLocalizedStringWithDefaultValue。這些宏最終都調(diào)用 NSBundle 的 localizedStringForKey:value:table: 方法來(lái)完成任務(wù)
使用這些宏有兩個(gè)好處:一方面相比直接調(diào)用 localizedStringForKey:value:table: 方法,使用宏讓代碼簡(jiǎn)單易懂磁携;另一方面,類(lèi)似 genstrings 這樣的工具能夠監(jiān)測(cè)到這些宏,從而生成供你翻譯使用的字符串文件括授。這些工具會(huì)解析 .c 和 .m 后綴的文件,然后為其中每一個(gè)需要進(jìn)行本地化的字符串都生成對(duì)應(yīng)條目,并寫(xiě)入到生成的 .strings 文件中荚虚。
如果想讓 genstrings 檢測(cè)自己項(xiàng)目中所有的 .m 后綴文件薛夜,可以執(zhí)行如下命令:
find . -name *.m | xargs genstrings -o en.lproj
-o 選項(xiàng)指定了生成字符串文件的存放目錄,默認(rèn)情況下文件名是 Localizable.strings版述。需要注意的是梯澜,genstrings 默認(rèn)會(huì)覆蓋已存在的同名字符串文件。-a 選項(xiàng)可以讓 genstrings 將生成的條目追加到已存在同名文件的末尾渴析,而不會(huì)覆蓋原文件晚伙。
不過(guò)一般情況下你也許想將生成文件放到另一個(gè)目錄中,然后使用你喜歡的合并工具將它們與已有文件合并以保留已翻譯好的條目俭茧。
字符串文件的格式非常簡(jiǎn)單咆疗,都是鍵值對(duì)的形式:
/* Insert new contact button */
"contact-editor.insert-new-contact-button" = "Insert contact";
/* Delete contact button */
"contact-editor.delete-contact-button" = "Delete contact";
字符串文件現(xiàn)在可以保存成 UTF-8 格式了,因?yàn)?Xcode 在構(gòu)建過(guò)程中能夠?qū)⑺鼈冝D(zhuǎn)換成所需的 UTF-16 格式母债。
應(yīng)用中哪些字符串需要本地化民傻?
一般而言,所有你想以某種形式展現(xiàn)在用戶(hù)眼前的字符串都需要本地化场斑,包括標(biāo)簽和按鈕上的文本漓踢,或者在運(yùn)行時(shí)通過(guò)格式化字符串和數(shù)據(jù)動(dòng)態(tài)生成的字符串。
在本地化字符串時(shí)漏隐,根據(jù)語(yǔ)法規(guī)則為每一種類(lèi)型的語(yǔ)句定義一個(gè)可本地化的字符串是非常重要的喧半。假設(shè)你在應(yīng)用中需要顯示「Paul invited you」和「You invited Paul」,那么只本地化格式化字符串「%@ invited %@」看起來(lái)是個(gè)不錯(cuò)的選擇青责,這樣在合適的時(shí)候把「you」本地化之后插入進(jìn)去就可以完成任務(wù)挺据。
在英語(yǔ)中這種做法沒(méi)什么問(wèn)題,但是請(qǐng)謹(jǐn)記脖隶,當(dāng)把這種小伎倆應(yīng)用到其他語(yǔ)言中時(shí)基本都會(huì)以失敗而告終扁耐。以德語(yǔ)為例,「Paul invited you」譯為「Paul hat dich eingeladen」产阱,而「You invited Paul」則譯為「Du hast Paul eingeladen」婉称。
正確的做法是定義兩個(gè)可本地化字符串「%@ invited you」和「You invited %@」,只有這樣翻譯器才能正確處理其他語(yǔ)言的特殊語(yǔ)法規(guī)則。
永遠(yuǎn)不要將句子分解為幾個(gè)部分,而要將它們作為一個(gè)完整的可本地化字符串鳖枕。如果一個(gè)句子與另一個(gè)句子的語(yǔ)法規(guī)則并不完全一致,那么即使它們?cè)谀愕哪刚Z(yǔ)中看起來(lái)極為相像俗壹,也要?jiǎng)?chuàng)建兩個(gè)可本地化字符串。
字符串鍵值最佳實(shí)踐
使用 NSLocalizedString 宏的時(shí)候藻烤,第一個(gè)參數(shù)就是為每個(gè)特殊字符串指定的鍵值(key)绷雏。程序員經(jīng)常使用母語(yǔ)中的單詞作為鍵值头滔,這樣乍一看是個(gè)便利的方案,但是實(shí)際上相當(dāng)糟糕涎显,會(huì)引發(fā)非常嚴(yán)重的錯(cuò)誤拙毫。
在一個(gè)字符串文件中,鍵值需要具有唯一性棺禾,因此任何母語(yǔ)中字面上具有唯一性的單詞在翻譯為其他語(yǔ)言的時(shí)候也必須具有唯一性缀蹄。這一點(diǎn)是無(wú)法滿(mǎn)足的,因?yàn)橐粋€(gè)單詞翻譯為其他語(yǔ)言時(shí)經(jīng)常會(huì)有多種意思膘婶,需要對(duì)應(yīng)到多種文字表示缺前。
以英文單詞「run」為例,作為名詞表示「跑步」悬襟,作為動(dòng)詞表示「奔跑」衅码,在翻譯的時(shí)候要加以區(qū)別。而且根據(jù)上下文的不同脊岳,每種具體的譯法在文字上可能還會(huì)有細(xì)微變化逝段。
一個(gè)健身應(yīng)用在不同的地方用到這個(gè)單詞的不同意思是很正常的,但是如果你使用下面的方法來(lái)進(jìn)行本地化
NSLocalizedString(@"Run", nil)
無(wú)論第二個(gè)參數(shù)指定了注釋內(nèi)容還是留空割捅,你在字符串文件中都只有一個(gè)「run」的條目奶躯。而在德語(yǔ)中,「run」作名詞時(shí)應(yīng)該譯為「Lauf」亿驾,作動(dòng)詞時(shí)則應(yīng)該譯為「laufen」嘹黔,或者在特定情況下譯為完全不同的形式比如「loslaufen」和「Los geht’s」。
好的鍵值應(yīng)該滿(mǎn)足兩個(gè)條件:首先鍵值必須在每個(gè)具體的上下文中保持唯一性莫瞬,其次如果我們沒(méi)有翻譯特定的那個(gè)上下文儡蔓,那么它們不會(huì)被其他情況覆蓋到而被翻譯。
本文推薦使用如下的命名空間方法:
NSLocalizedString(@"activity-profile.title.the-run", nil)
NSLocalizedString(@"home.button.start-run", nil)
這樣的鍵值可以區(qū)分應(yīng)用中不同地方出現(xiàn)的單詞疼邀,同時(shí)提供具體的上下文喂江,比如是標(biāo)題中的或者按鈕中的。上面的例子里我們?yōu)榱撕?jiǎn)便忽略了第二個(gè)參數(shù)旁振,實(shí)際使用中如果鍵值本身沒(méi)有提供清晰的上下文說(shuō)明获询,你可以將進(jìn)一步的說(shuō)明作為第二個(gè)參數(shù)傳入。同時(shí)請(qǐng)確保鍵值中只含有 ASCII 字符规求。
分割字符串文件
正如我們一開(kāi)始提到的筐付,NSLocalizedString 有一些變體能夠提供更多字符串本地化的操作方式卵惦。NSLocalizedStringFromTable 接收 key阻肿、table 和 comment 這三個(gè)參數(shù),其中 table 參數(shù)表示該字符串對(duì)應(yīng)的一個(gè)表格沮尿,genstrings 會(huì)為表中的每一個(gè)條目生成一個(gè)以條目名稱(chēng)(假設(shè)為 table-item)命名的獨(dú)立字符串文件 table-item.strings丛塌。
這樣你就可以把字符串文件分割成幾個(gè)小一些的文件较解。在一個(gè)龐大的項(xiàng)目或者團(tuán)隊(duì)中工作時(shí),這一點(diǎn)顯得尤為重要赴邻。同時(shí)這也讓合并原有的和重新生成的字符串文件變得容易一些印衔。
相比在每個(gè)地方調(diào)用下面的語(yǔ)句:
NSLocalizedStringFromTable(@"home.button.start-run", @"ActivityTracker", @"some comment..")
你可以自定義一個(gè)用于字符串本地化的函數(shù)來(lái)讓工作變得輕松一些
static NSString * LocalizedActivityTrackerString(NSString *key, NSString *comment) {
return [[NSBundle mainBundle] localizedStringForKey:key value:key table:@"ActivityTracker"];
}
為了給所有調(diào)用此函數(shù)的地方生成字符串文件,你可以在執(zhí)行 genstrings 的時(shí)候加上 -s 選項(xiàng):
find . -name *.m | xargs genstrings -o en.lproj -s LocalizedActivityTrackerString
-s 這個(gè)選項(xiàng)指定了本地化函數(shù)的共同前綴名稱(chēng)姥敛,如果你還定義了 LocalizedActivityTrackerStringFromTable奸焙,LocalizedActivityTrackerStringFromTableInBundle, LocalizedActivityTrackerStringWithDefaultValue 等函數(shù)彤敛,以上命令也會(huì)調(diào)用它們与帆。
運(yùn)用格式化字符串
我們經(jīng)常需要對(duì)一些在運(yùn)行時(shí)才能最終確定下來(lái)的字符串進(jìn)行本地化,格式化字符串可以完成這項(xiàng)工作墨榄。Foundation 在這方面提供了一些非常強(qiáng)大的特性玄糟。(可以參考Daniel 的文章獲得更多關(guān)于格式化字符串的細(xì)節(jié)
以字符串「Run 1 out of 3 completed.」為例,我們可以這樣構(gòu)造格式化字符串:
NSString *localizedString = NSLocalizedString(@"activity-profile.label.run %lu out of %lu completed", nil);
self.label.text = [NSString localizedStringWithFormat:localizedString, completedRuns, totalRuns];
在翻譯的時(shí)候經(jīng)常需要對(duì)其中的格式化占位符進(jìn)行順序調(diào)整以符合語(yǔ)法袄秩,幸運(yùn)的是我們可以在字符串文件中輕松地搞定:
"activity-profile.label.run %lu out of %lu completed" = "Von %2$lu L?ufen hast du %$1lu absolver";
上面的德文翻譯得不是非常好阵翎,只是單純用來(lái)說(shuō)明調(diào)換占位符順序的功能而已。
如果你需要對(duì)簡(jiǎn)單的整數(shù)或者浮點(diǎn)數(shù)進(jìn)行本地化之剧,你可以使用 localizedStringWithFormat: 這個(gè)變體郭卫。數(shù)字本地化的更高級(jí)用法涉及 NSNumberFormatter,會(huì)在本文后面講到
單復(fù)數(shù)與陰陽(yáng)性
在 OS X 10.9 和 iOS 7 中背稼,本地化字符串的時(shí)候可以使用比替換格式化字符串中的占位符更酷的特性:蘋(píng)果官方想處理不同語(yǔ)言中對(duì)于名詞復(fù)數(shù)和不同性別采取的不同變化箱沦。
讓我們?cè)倏匆幌轮暗睦樱篅”%lu out of %lu runs completed.” 這個(gè)翻譯在「跑多次」的時(shí)候才是對(duì)的(譯者注:即第二個(gè) %lu 代表的數(shù)字大于 1),所以我們不得不定義兩個(gè)不同的字符串來(lái)處理單次和多次的情況:
@"%lu out of one run completed"
@"%lu out of %lu runs completed"
這種做法在英語(yǔ)中是對(duì)的雇庙,但是在其他很多語(yǔ)言中會(huì)出錯(cuò)谓形。比如希伯來(lái)語(yǔ)中名詞有三種形式:第一種是單數(shù)和十的倍數(shù),第二種是 2疆前,第三種是其他的復(fù)數(shù)寒跳。克羅地亞語(yǔ)中竹椒,個(gè)位數(shù)為 1 的數(shù)字有單獨(dú)的表示方法:「31 od 32 staze zavr?ene」童太,與之相對(duì)的是「5 od 8 staza zavr?ene」(注意其中「staze」和「staza」的差別)。很多語(yǔ)言針對(duì)非整型數(shù)也有不同的表達(dá)方式胸完。
想全面了解這個(gè)問(wèn)題可以參見(jiàn)基于 Unicode 的語(yǔ)言復(fù)數(shù)規(guī)則书释。其中涵蓋的變化之博大精深令人嘆為觀(guān)止。
為了在 10.9 和 iOS 7 平臺(tái)上正確處理這個(gè)問(wèn)題赊窥,我們需要如下構(gòu)造可本地化字符串:
[NSString localizedStringWithFormat:NSLocalizedString(@"activity-profile.label.%lu out of %lu runs completed"), completedRuns, totalRuns];
然后我們?cè)?.strings 后綴文件所處目錄中創(chuàng)建一個(gè)同名的 .stringsdict 后綴的文件爆惧,如果前者名為 Localizable.strings,則后者為 Localizable.stringsdict锨能。保留 .strings 后綴的字符串文件是必須的扯再,即使它里面什么內(nèi)容也沒(méi)有芍耘。這個(gè) .stringsdict 后綴的字符串字典文件是一個(gè)屬性列表(plist)文件,比字符串文件復(fù)雜得多熄阻,換來(lái)的是正確處理所有語(yǔ)言的名詞復(fù)數(shù)問(wèn)題斋竞,而不需要將處理邏輯寫(xiě)在代碼中。