鍍金池/ 教程/ iOS/ 字符串本地化
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫(kù)支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語(yǔ)言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

字符串本地化

一個(gè)應(yīng)用在進(jìn)行多語(yǔ)言本地化的時(shí)候涉及到大量的工作。因?yàn)檫@一期的主題是字符串,所以本文主要探討字符串的本地化。字符串本地化有兩種方法:修改代碼或修改 nib 文件和 storyboard。本文將專注于通過代碼實(shí)現(xiàn)字符串的本地化。

NSLocalizedString

NSLocalizedString 這個(gè)宏是字符串本地化的核心工具。它還有三個(gè)鮮為人知的變體:NSLocalizedStringFromTable、NSLocalizedStringFromTableInBundleNSLocalizedStringWithDefaultValue。這些宏最終都調(diào)用 NSBundlelocalizedStringForKey:value:table: 方法來完成任務(wù)。

使用這些宏有兩個(gè)好處:一方面相比直接調(diào)用 localizedStringForKey:value:table: 方法,使用宏讓代碼簡(jiǎn)單易懂;另一方面,類似 genstrings 這樣的工具能夠監(jiān)測(cè)到這些宏,從而生成供你翻譯使用的字符串文件。這些工具會(huì)解析 .c 和 .m 后綴的文件,然后為其中每一個(gè)需要進(jìn)行本地化的字符串都生成對(duì)應(yīng)條目,并寫入到生成的 .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ì)覆蓋原文件。

不過一般情況下你也許想將生成文件放到另一個(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";

更復(fù)雜的操作比如在需要本地化的字符串中插入格式化占位符等,我們將在稍后談到。

另外,字符串文件現(xiàn)在可以保存成 UTF-8 格式了,因?yàn)?Xcode 在構(gòu)建過程中能夠?qū)⑺鼈冝D(zhuǎn)換成所需的 UTF-16 格式。

應(yīng)用中哪些字符串需要本地化?

一般而言,所有你想以某種形式展現(xiàn)在用戶眼前的字符串都需要本地化,包括標(biāo)簽和按鈕上的文本,或者在運(yùn)行時(shí)通過格式化字符串和數(shù)據(jù)動(dòng)態(tài)生成的字符串。

在本地化字符串時(shí),根據(jù)語(yǔ)法規(guī)則為每一種類型的語(yǔ)句定義一個(gè)可本地化的字符串是非常重要的。假設(shè)你在應(yīng)用中需要顯示「Paul invited you」和「You invited Paul」,那么只本地化格式化字符串「%@ invited %@」看起來是個(gè)不錯(cuò)的選擇,這樣在合適的時(shí)候把「you」本地化之后插入進(jìn)去就可以完成任務(wù)。

在英語(yǔ)中這種做法沒什么問題,但是請(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ǔ)中看起來極為相像,也要?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)是無法滿足的,因?yàn)橐粋€(gè)單詞翻譯為其他語(yǔ)言時(shí)經(jīng)常會(huì)有多種意思,需要對(duì)應(yīng)到多種文字表示。

以英文單詞「run」為例,作為名詞表示「跑步」,作為動(dòng)詞表示「奔跑」,在翻譯的時(shí)候要加以區(qū)別。而且根據(jù)上下文的不同,每種具體的譯法在文字上可能還會(huì)有細(xì)微變化。

一個(gè)健身應(yīng)用在不同的地方用到這個(gè)單詞的不同意思是很正常的,但是如果你使用下面的方法來進(jìn)行本地化:

NSLocalizedString(@"Run", nil)

無論第二個(gè)參數(shù)指定了注釋內(nèi)容還是留空,你在字符串文件中都只有一個(gè)「run」的條目。而在德語(yǔ)中,「run」作名詞時(shí)應(yīng)該譯為「Lauf」,作動(dòng)詞時(shí)則應(yīng)該譯為「laufen」,或者在特定情況下譯為完全不同的形式比如「loslaufen」和「Los geht’s」。

好的鍵值應(yīng)該滿足兩個(gè)條件:首先鍵值必須在每個(gè)具體的上下文中保持唯一性,其次如果我們沒有翻譯特定的那個(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í)際使用中如果鍵值本身沒有提供清晰的上下文說明,你可以將進(jìn)一步的說明作為第二個(gè)參數(shù)傳入。同時(shí)請(qǐng)確保鍵值中只含有 ASCII 字符。

分割字符串文件

正如我們一開始提到的,NSLocalizedString 有一些變體能夠提供更多字符串本地化的操作方式。NSLocalizedStringFromTable 接收 key、table 和 comment 這三個(gè)參數(shù),其中 table 參數(shù)表示該字符串對(duì)應(yīng)的一個(gè)表格,genstrings 會(huì)為表中的每一個(gè)條目生成一個(gè)以條目名稱(假設(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ù)來讓工作變得輕松一些

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ù)的共同前綴名稱,如果你還定義了 LocalizedActivityTrackerStringFromTable,LocalizedActivityTrackerStringFromTableInBundle, LocalizedActivityTrackerStringWithDefaultValue 等函數(shù),以上命令也會(huì)調(diào)用它們。

運(yùn)用格式化字符串

我們經(jīng)常需要對(duì)一些在運(yùn)行時(shí)才能最終確定下來的字符串進(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 absolviert";

上面的德文翻譯得不是非常好,只是單純用來說明調(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í)候可以使用比替換格式化字符串中的占位符更酷的特性:蘋果官方想處理不同語(yǔ)言中對(duì)于名詞復(fù)數(shù)和不同性別采取的不同變化。

讓我們?cè)倏匆幌轮暗睦樱篅”%lu out of %lu runs completed.” 這個(gè)翻譯在「跑多次」的時(shí)候才是對(duì)的(譯者注:即第二個(gè) %lu 代表的數(shù)字大于 1),所以我們不得不定義兩個(gè)不同的字符串來處理單次和多次的情況:

@"%lu out of one run completed"
@"%lu out of %lu runs completed"

這種做法在英語(yǔ)中是對(duì)的,但是在其他很多語(yǔ)言中會(huì)出錯(cuò)。比如希伯來語(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è)問題可以參見基于 Unicode 的語(yǔ)言復(fù)數(shù)規(guī)則。其中涵蓋的變化之博大精深令人嘆為觀止。

為了在 10.9 和 iOS 7 平臺(tái)上正確處理這個(gè)問題,我們需要如下構(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)容也沒有。這個(gè) .stringsdict 后綴的字符串字典文件是一個(gè)屬性列表(plist)文件,比字符串文件復(fù)雜得多,換來的是正確處理所有語(yǔ)言的名詞復(fù)數(shù)問題,而不需要將處理邏輯寫在代碼中。

下面是一個(gè)該文件的例子:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>activity-profile.label.%lu out of %lu runs completed</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%lu out of %#@lu_total_runs@ completed</string>
        <key>lu_total_runs</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>lu</string>
            <key>one</key>
            <string>%lu run</string>
            <key>other</key>
            <string>%lu runs</string>
        </dict>
    </dict>
</dict>
</plist>

頂層字典的鍵值即為待翻譯的字符串(即 activity-profile.label.%lu out of %lu runs completed ),在下層字典中又指定了 NSStringLocalizedFormatKey 所需的格式化字符串。為了將不同的占位符替換為不同的數(shù)字,必須擴(kuò)展格式化字符串的語(yǔ)法。所以我們可以定義類似 %#@lu_total_runs@ 的格式化字符串,然后定義一個(gè)字典來解析它。在上面的字典中,我們通過將 NSStringFormatSpecTypeKey 設(shè)置為 NSStringPluralRuleType 表明這是一個(gè)處理名詞復(fù)數(shù)的規(guī)則,指定了值的類型(在本例中是 lu,即無符號(hào)長(zhǎng)整數(shù)),還定義了針對(duì)不同復(fù)數(shù)形式的不同輸出(可以從「zero」、「one」、「few」、「many」和「others」中選擇,上例中僅制定了「one」和「other」)。

這是一個(gè)非常強(qiáng)大的特性,不但可以處理其他語(yǔ)言中多種復(fù)數(shù)形式的問題,還可以為不同的數(shù)字定制不同的字面表示。

我們還可以更進(jìn)一步定義遞歸的規(guī)則。為了讓上面例子的輸出更友好,我們需要覆蓋如下幾種自定義的字符串用例:

Completed runs    Total Runs    Output
------------------------------------------------------------------
0                 0+            No runs completed yet
1                 1             One run completed
1                 2+            One of x runs completed
2+                2+            x of y runs completed

我們可以通過字符串字典后綴文件來處理以上四種情況,而無需修改代碼邏輯,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>scope.%lu out of %lu runs</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%1$#@lu_completed_runs@</string>
        <key>lu_completed_runs</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>lu</string>
            <key>zero</key>
            <string>No runs completed yet</string>
            <key>one</key>
            <string>One %2$#@lu_total_runs@</string>
            <key>other</key>
            <string>%lu %2$#@lu_total_runs@</string>
        </dict>
        <key>lu_total_runs</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>lu</string>
            <key>one</key>
            <string>run completed</string>
            <key>other</key>
            <string>of %lu runs completed</string>
        </dict>
    </dict>
</dict>
</plist>

調(diào)用 localizedStringForKey:value:table: 會(huì)返回根據(jù)字符串字典文件中的鍵值對(duì)進(jìn)行初始化的字符串集合,這些字符串都是包含字符串字典文件中信息的的代理對(duì)象(proxy objects)。這些信息在調(diào)用 copymutableCopy 進(jìn)行字符串拷貝的時(shí)候會(huì)被保留,但是一旦你修改了該字符串,這些額外信息就會(huì)丟失。更多細(xì)節(jié)請(qǐng)參見 OS X 10.9 的 Foundation 發(fā)行說明。

字母大小寫

如果你要修改一個(gè)用戶可見字符串的大小寫,請(qǐng)一定使用包含本地化功能的 NSString 方法變體:lowercaseStringWithLocale:uppercaseStringWithLocale:。

調(diào)用這些方法的時(shí)候你需要傳入?yún)^(qū)域設(shè)置參數(shù) locale ,這樣就可以將大小寫的改變應(yīng)用到本地化之后的其他語(yǔ)言版本中。當(dāng)你使用 NSLocalizedString 及其變體的那些宏時(shí)無須擔(dān)心本地化后的大小寫問題,因?yàn)樵诜椒▋?nèi)部已經(jīng)自動(dòng)做了處理,而且在用戶選擇的語(yǔ)言不可用時(shí)會(huì)使用默認(rèn)語(yǔ)言來代替。

為了用戶界面的一致性,使用區(qū)域設(shè)置(locale)來本地化界面的其他部分是一個(gè)很好的方法,可以參見后面的小節(jié)「選擇正確的區(qū)域設(shè)置」。

文件路徑的本地化

一般而言你應(yīng)該始終用 NSURL 來表現(xiàn)文件路徑,因?yàn)檫@會(huì)讓文件名的本地化變得容易:

NSURL *url = [NSURL fileURLWithPath:@"/Applications/System Preferences.app"];
NSString *name;
[url getResourceValue:&name forKey:NSURLLocalizedTypeDescriptionKey error:NULL];
NSLog(@"localized name: %@", name);

// output: System Preferences.app

以上輸出在英語(yǔ)系統(tǒng)中是正確的,但是假設(shè)我們換到了阿拉伯語(yǔ)系統(tǒng)中,系統(tǒng)設(shè)置被稱為「??????? ??????.app」。

構(gòu)造這樣一個(gè)其他語(yǔ)言的文件名是否包含后綴需要參照用戶 Finder 中的相關(guān)選項(xiàng)。如果你需要獲取文件的類型,也可以這樣調(diào)用 NSURLLocalizedTypeDescriptionKey 來從中獲得。

本地化之后的文件名僅供顯示使用,不能用來訪問實(shí)際的文件資源,可以參考 Daniel 關(guān)于常見字符串模式的文章 以獲取更多關(guān)于路徑的細(xì)節(jié)。

格式器

在不同的語(yǔ)言中,數(shù)字和日期被表現(xiàn)為各種形式。幸好蘋果官方已經(jīng)提供了處理這些問題的方法,所以我們只需要使用 NSNumberFormatter or NSDateFormatter 類來顯示用戶界面中的數(shù)字和日期即可。

請(qǐng)記住數(shù)字和日期的格式器是可變對(duì)象,因此并不線程安全。

格式化數(shù)字

數(shù)字格式器對(duì)象有很多配置選項(xiàng),但大多數(shù)情況下你只要使用一種定義好的數(shù)字格式就好。畢竟使用數(shù)字格式器的原因就是不必再擔(dān)心其他語(yǔ)言中特定的數(shù)字格式。

對(duì)于數(shù)字 2.5,在本文作者的機(jī)器上使用不同的格式器會(huì)得到不同的輸出:

數(shù)字類型                              德語(yǔ)結(jié)果                      阿拉伯語(yǔ)結(jié)果
------------------------------------------------------------------------------------------------------
NSNumberFormatterNoStyle             2                             ?
NSNumberFormatterDecimalStyle        2,5                           ???
NSNumberFormatterCurrencyStyle       2,50 €                        ????? ?.?.
NSNumberFormatterScientificStyle     2,5E0                         ??????
NSNumberFormatterPercentStyle        250 %                         ????
NSNumberFormatterSpellOutStyle       zwei Komma fünf               ????? ???? ????

在上表中數(shù)字格式器的一個(gè)很好的特性無法直觀地表現(xiàn)出來:在貨幣和百分?jǐn)?shù)形式中,貨幣單位和百分號(hào)前面插入的不是一個(gè)普通空格,而是一個(gè)不換行空格,因此實(shí)際顯示的時(shí)候數(shù)字和后面的符號(hào)不會(huì)被顯示在兩行中。(而且這種加空格的顯示不是很酷嗎?)

默認(rèn)情況下格式器會(huì)使用系統(tǒng)設(shè)置中指定的區(qū)域設(shè)置。在「字母大小寫」一節(jié)中我們已經(jīng)說過,根據(jù)特定用戶界面的特定要求為格式器指定正確的區(qū)域設(shè)置是非常重要的,在后面的小節(jié)會(huì)進(jìn)一步討論這一點(diǎn)。

格式化日期

與數(shù)字的格式化一樣,日期的格式化也非常復(fù)雜,因此我們有必要讓 NSDateFormatter 來負(fù)責(zé)這一點(diǎn)。使用日期格式器的時(shí)候你可以選擇蘋果官方提供的適用于所有區(qū)域設(shè)置的不同日期和時(shí)間格式。再?gòu)?qiáng)調(diào)一遍,選擇匹配界面其他元素的正確區(qū)域設(shè)置。

有時(shí)你想用一種 NSDateFormatter 默認(rèn)不支持的格式來顯示日期,這時(shí)不要使用簡(jiǎn)單的格式化字符串(這樣做在應(yīng)用到其他語(yǔ)言中時(shí)幾乎肯定會(huì)出錯(cuò)),而要使用 NSDateFormatter 提供的 dateFormatFromTemplate:options:locale: 方法。

假設(shè)你想只顯示天和月份的縮寫,系統(tǒng)并沒有提供這樣的默認(rèn)風(fēng)格的。所以我們可以自定義格式器:

NSString *format = [NSDateFormatter dateFormatFromTemplate:@"dMMM"
                                                   options:0
                                                    locale:locale];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:format];
NSString *output = [dateFormatter stringFromDate:[NSDate date]];
NSLog(@"Today's day and month: %@", output);

相比使用格式化字符串,調(diào)用這個(gè)方法的一大好處就在于輸出結(jié)果在其他語(yǔ)言中也肯定是正確的。舉例來說,在美國(guó)英語(yǔ)中,我們期望輸出「Feb 2」,而在德語(yǔ)中則應(yīng)該輸出「2. Feb」。dateFormatFromTemplate:options:locale: 方法使用我們指定的模板和區(qū)域設(shè)置來構(gòu)造正確的輸出結(jié)果,在美國(guó)英語(yǔ)中將模板變?yōu)椤窶MM d」,在德語(yǔ)中則變?yōu)椤竏. MMM」。

想要深入了解模板字符串中可以使用的占位符,可以參考Unicode 格式的區(qū)域設(shè)置數(shù)據(jù)標(biāo)記語(yǔ)言文檔.

緩存格式器對(duì)象

因?yàn)閯?chuàng)建格式器對(duì)象是一個(gè)非常消耗資源的操作,所以最好將它緩存起來以供之后使用:

static NSDateFormatter *formatter;

- (NSString *)displayDate:(NSDate *)date
{
    if (!formatter) {
        formatter = [[NSDateFormatter alloc] init];
        formatter.dateStyle = NSDateFormatterShortStyle;
        formatter.timeStyle = NSDateFormatterNoStyle;
    }
    return [formatter stringFromDate:date];
}

這里有一個(gè)小的陷阱需要注意:如果用戶修改了區(qū)域設(shè)置,我們就需要廢棄這個(gè)緩存。因此我們需要使用 NSCurrentLocaleDidChangeNotification 注冊(cè)一個(gè)通知事件:

static NSDateFormatter *formatter;

- (void)setup
{
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter addObserver:self
                           selector:@selector(localeDidChange)
                               name:NSCurrentLocaleDidChangeNotification
                             object:nil];
}

- (NSString *)displayDate:(NSDate *)date
{
    if (!formatter) {
        formatter = [[NSDateFormatter alloc] init];
        formatter.dateStyle = NSDateFormatterShortStyle;
        formatter.timeStyle = NSDateFormatterNoStyle;
    }
    return [formatter stringFromDate:date];
}

- (void)localeDidChange
{
    formatter = nil;
}

- (void)dealloc
{
    NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
    [notificationCenter removeObserver:self
                                  name:NSCurrentLocaleDidChangeNotification
                                object:nil];
}

蘋果官方的數(shù)據(jù)格式化指南中對(duì)此做了注解:

理論上來說你應(yīng)該使用自動(dòng)更新的區(qū)域設(shè)置(autoupdatingCurrentLocale),這樣就可以在用戶做更改時(shí)生成對(duì)應(yīng)的區(qū)域設(shè)置文件,但是這一招對(duì)日期格式器不適用。

所以我們不得不使用為區(qū)域設(shè)置的變更設(shè)置通知機(jī)制。相比格式化日期的那一小段代碼,這一段有點(diǎn)長(zhǎng),但是如果你頻繁使用日期格式器,這樣做是值得的。始終牢記在權(quán)衡利弊之后再進(jìn)行改進(jìn)。

再次強(qiáng)調(diào),格式器不是線程安全的。蘋果官方文檔中寫道,你可以在多線程環(huán)境下使用格式器,但是不能有多個(gè)線程同時(shí)修改格式器。如果你想將用到的所有格式器集中在一個(gè)對(duì)象中,以便在區(qū)域設(shè)置更改時(shí)更方便地廢棄緩存,你必須保證只使用一個(gè)隊(duì)列存放它們從而依次創(chuàng)建和更新。比如你可以使用并發(fā)隊(duì)列(concurrent queue)dispatch_sync 來獲取格式器,在區(qū)域設(shè)置更改時(shí)使用 dispatch_barrier_async 來更新格式器。

解析用戶輸入數(shù)據(jù)

數(shù)字和日期格式器不止可以根據(jù)數(shù)字和日期對(duì)象生成可本地化字符串,還能以其他方式工作。每當(dāng)你需要處理用戶輸入中的數(shù)字或日期時(shí),都應(yīng)該使用合適的格式器類來解析。這是唯一能夠保證用戶輸入能夠按照當(dāng)前區(qū)域設(shè)置正確解析的方法。

解析機(jī)器生成數(shù)據(jù)

雖然格式器在處理用戶輸入時(shí)很好用,在已知格式的情況下處理機(jī)器生成的數(shù)據(jù)有更好的方法,因?yàn)闉樗袇^(qū)域設(shè)置生成正確輸出的數(shù)字和日期格式器有性能上的損失。

舉例來說,如果你從服務(wù)器接收到很多日期字符串,在你將它們轉(zhuǎn)換成日期對(duì)象時(shí),日期格式器并不是最好的選擇。蘋果官方的日期格式化指南中提到對(duì)于這些固定格式且無需進(jìn)行本地化的日期,使用 UNIX 提供的 strptime_l(3) 函數(shù)更高效:

struct tm sometime;
const char *formatString = "%Y-%m-%d %H:%M:%S %z";
(void) strptime_l("2014-02-07 12:00:00 -0700", formatString, &sometime, NULL);
NSLog(@"Issue #9 appeared on %@", [NSDate dateWithTimeIntervalSince1970: mktime(&sometime)]);
// Output: Issue #9 appeared on 2014-02-07 12:00:00 -0700

因?yàn)?strptime_l 函數(shù)也可以感知用戶的區(qū)域設(shè)置,所以確保最后一個(gè)參數(shù)傳入 NULL 以使用標(biāo)準(zhǔn) POSIX 區(qū)域設(shè)置。函數(shù)中可用的占位符請(qǐng)參考 strftime 用戶手冊(cè)。

調(diào)試本地化字符串

應(yīng)用支持的語(yǔ)言版本越多,確保所有元素都正確顯示就越難。但是這里有一些默認(rèn)的用戶選項(xiàng)和工具可以減輕你的負(fù)擔(dān)。

你可以使用 NSDoubleLocalizedStrings、AppleTextDirectionNSForceRightToLeftWritingDirection 選項(xiàng)保證你的布局不會(huì)因?yàn)殚L(zhǎng)字符串或者從右往左讀的語(yǔ)言而混亂。NSShowNonLocalizedStringsNSShowNonLocalizableStrings 則可以幫助你找到?jīng)]有翻譯的字符串和根本沒有制定字符串本地化宏的字符串。(所有這些工具的選項(xiàng)都可以通過程序設(shè)置或者作為 Xcode 的 Scheme 編輯器啟動(dòng)選項(xiàng),如 -NSShowNonLocalizedStrings YES

還有兩個(gè)選項(xiàng)可以控制語(yǔ)言和區(qū)域設(shè)置:AppleLanguagesAppleLocale。你可以配置這兩個(gè)選項(xiàng)讓應(yīng)用以不同于當(dāng)前系統(tǒng)的語(yǔ)言或者區(qū)域設(shè)置啟動(dòng),讓你在測(cè)試時(shí)不用頻繁對(duì)系統(tǒng)設(shè)置進(jìn)行切換。AppleLanguages 選項(xiàng)接收符合 ISO-639 標(biāo)準(zhǔn)的語(yǔ)言代碼列表作為參數(shù),如下所示:

-AppleLanguages (de, fr, en)

AppleLocale 則接收符合Unicode 國(guó)際組件標(biāo)準(zhǔn)(International Components for Unicode) 的區(qū)域設(shè)置標(biāo)識(shí)符作為參數(shù),如下:

-AppleLocale en_US

-AppleLocale en_GR

如果你翻譯的字符串沒有正確顯示,你可以帶上 -lint 選項(xiàng)運(yùn)行 plutil 命令來檢查一下字符串文件是否有語(yǔ)法錯(cuò)誤。例如你在行尾漏寫了分號(hào),plutil 會(huì)輸出如下警告:

$ plutil Localizable.strings
2014-02-04 15:22:40.395 plutil[92263:507] CFPropertyListCreateFromXMLData(): Old-style plist parser: missing semicolon in dictionary on line 6. Parsing will be abandoned. Break on _CFPropertyListMissingSemicolon to debug.
Localizable.strings: Unexpected character / at line 1

當(dāng)我們修正了這個(gè)錯(cuò)誤后,plutil 會(huì)告訴我們一切正常:

$ plutil Localizable.strings
Localizable.strings: OK

對(duì)于支持多種語(yǔ)言的應(yīng)用,還有一個(gè)與調(diào)試無關(guān)的小技巧:你可以在 iOS 上自動(dòng)生成應(yīng)用在多種語(yǔ)言下的屏幕截圖。因?yàn)榭梢允褂?UIAutomation 來控制應(yīng)用,使用 AppleLanguages 在啟動(dòng)時(shí)設(shè)置語(yǔ)言,所以整個(gè)測(cè)試過程可以自動(dòng)化。GitHub 上的這個(gè)項(xiàng)目中可以找到更多細(xì)節(jié)。

選擇正確的區(qū)域設(shè)置

在使用日期和數(shù)字格式器或者類似 [NSString lowercaseStringWithLocale:] 的方法調(diào)用時(shí),確保你使用了正確的區(qū)域設(shè)置是很重要的。如果你想使用系統(tǒng)當(dāng)前的區(qū)域設(shè)置,你可以使用 [NSLocale currentLocale] 獲得,但是要注意這不一定與你的應(yīng)用實(shí)際運(yùn)行時(shí)使用的相同。

假設(shè)用戶的系統(tǒng)是中文的,但是你的應(yīng)用只支持英語(yǔ)、德語(yǔ)、西班牙語(yǔ)和法語(yǔ)。這種情況下字符串本地化會(huì)使用默認(rèn)的英語(yǔ)來進(jìn)行,如果你現(xiàn)在使用 [NSLocale currentLocale] 或者使用 [NSNumberFormatter localizedStringFromNumber:numberStyle:] 這種未指定區(qū)域設(shè)置的格式器類,那么這些數(shù)據(jù)會(huì)根據(jù)中文的區(qū)域設(shè)置來進(jìn)行格式化,而界面上的其他字符串則都是英語(yǔ)。

最終需要你來決定特定情況下什么最重要,但是你會(huì)想要應(yīng)用的界面在一些情況下保持一致。為了獲取應(yīng)用實(shí)際使用的而非當(dāng)前系統(tǒng)的區(qū)域設(shè)置,我們必須獲取 mainBundle 中的語(yǔ)言屬性來構(gòu)造區(qū)域設(shè)置:

NSString *localization = [NSBundle mainBundle].preferredLocalizations.firstObject;
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:localization];

在這樣的區(qū)域設(shè)置下,我們可以將日期格式化為與界面其他元素一致的形式:

NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.locale = locale;
formatter.dateStyle = NSDateFormatterShortStyle;
formatter.timeStyle = NSDateFormatterNoStyle;
NSString *localizedDate = [formatter stringFromDate:[NSDate date]];

結(jié)論

任何適用于自己母語(yǔ)的規(guī)律都不一定適用于其他語(yǔ)言,在本地化字符串時(shí)要牢記這一點(diǎn)。眾多框架提供了很多強(qiáng)大的工具將不同語(yǔ)言的復(fù)雜性抽象出來,我們只需要一以貫之地運(yùn)用它們。這會(huì)帶來一些額外的工作,但是會(huì)為你在制作自己應(yīng)用的其他語(yǔ)言版本時(shí)節(jié)約大量的時(shí)間。