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

字符串本地化

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

NSLocalizedString

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

使用這些宏有兩個好處:一方面相比直接調用 localizedStringForKey:value:table: 方法,使用宏讓代碼簡單易懂;另一方面,類似 genstrings 這樣的工具能夠監(jiān)測到這些宏,從而生成供你翻譯使用的字符串文件。這些工具會解析 .c 和 .m 后綴的文件,然后為其中每一個需要進行本地化的字符串都生成對應條目,并寫入到生成的 .strings 文件中。

如果想讓 genstrings 檢測自己項目中所有的 .m 后綴文件,可以執(zhí)行如下命令:

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";

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

另外,字符串文件現(xiàn)在可以保存成 UTF-8 格式了,因為 Xcode 在構建過程中能夠將它們轉換成所需的 UTF-16 格式。

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

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

在本地化字符串時,根據語法規(guī)則為每一種類型的語句定義一個可本地化的字符串是非常重要的。假設你在應用中需要顯示「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 %@」,只有這樣翻譯器才能正確處理其他語言的特殊語法規(guī)則。

永遠不要將句子分解為幾個部分,而要將它們作為一個完整的可本地化字符串。如果一個句子與另一個句子的語法規(guī)則并不完全一致,那么即使它們在你的母語中看起來極為相像,也要創(chuàng)建兩個可本地化字符串。

字符串鍵值最佳實踐

使用 NSLocalizedString 宏的時候,第一個參數(shù)就是為每個特殊字符串指定的鍵值(key)。程序員經常使用母語中的單詞作為鍵值,這樣乍一看是個便利的方案,但是實際上相當糟糕,會引發(fā)非常嚴重的錯誤。

在一個字符串文件中,鍵值需要具有唯一性,因此任何母語中字面上具有唯一性的單詞在翻譯為其他語言的時候也必須具有唯一性。這一點是無法滿足的,因為一個單詞翻譯為其他語言時經常會有多種意思,需要對應到多種文字表示。

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

一個健身應用在不同的地方用到這個單詞的不同意思是很正常的,但是如果你使用下面的方法來進行本地化:

NSLocalizedString(@"Run", nil)

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

好的鍵值應該滿足兩個條件:首先鍵值必須在每個具體的上下文中保持唯一性,其次如果我們沒有翻譯特定的那個上下文,那么它們不會被其他情況覆蓋到而被翻譯。

本文推薦使用如下的命名空間方法:

NSLocalizedString(@"activity-profile.title.the-run", nil)
NSLocalizedString(@"home.button.start-run", nil)

這樣的鍵值可以區(qū)分應用中不同地方出現(xiàn)的單詞,同時提供具體的上下文,比如是標題中的或者按鈕中的。上面的例子里我們?yōu)榱撕啽愫雎粤说诙€參數(shù),實際使用中如果鍵值本身沒有提供清晰的上下文說明,你可以將進一步的說明作為第二個參數(shù)傳入。同時請確保鍵值中只含有 ASCII 字符。

分割字符串文件

正如我們一開始提到的,NSLocalizedString 有一些變體能夠提供更多字符串本地化的操作方式。NSLocalizedStringFromTable 接收 key、table 和 comment 這三個參數(shù),其中 table 參數(shù)表示該字符串對應的一個表格,genstrings 會為表中的每一個條目生成一個以條目名稱(假設為 table-item)命名的獨立字符串文件 table-item.strings。

這樣你就可以把字符串文件分割成幾個小一些的文件。在一個龐大的項目或者團隊中工作時,這一點顯得尤為重要。同時這也讓合并原有的和重新生成的字符串文件變得容易一些。

相比在每個地方調用下面的語句:

NSLocalizedStringFromTable(@"home.button.start-run", @"ActivityTracker", @"some comment..")

你可以自定義一個用于字符串本地化的函數(shù)來讓工作變得輕松一些

static NSString * LocalizedActivityTrackerString(NSString *key, NSString *comment) {
    return [[NSBundle mainBundle] localizedStringForKey:key value:key table:@"ActivityTracker"];
}

為了給所有調用此函數(shù)的地方生成字符串文件,你可以在執(zhí)行 genstrings 的時候加上 -s 選項:

find . -name *.m | xargs genstrings -o en.lproj -s LocalizedActivityTrackerString

-s 這個選項指定了本地化函數(shù)的共同前綴名稱,如果你還定義了 LocalizedActivityTrackerStringFromTable,LocalizedActivityTrackerStringFromTableInBundle, LocalizedActivityTrackerStringWithDefaultValue 等函數(shù),以上命令也會調用它們。

運用格式化字符串

我們經常需要對一些在運行時才能最終確定下來的字符串進行本地化,格式化字符串可以完成這項工作。Foundation 在這方面提供了一些非常強大的特性。(可以參考Daniel 的文章獲得更多關于格式化字符串的細節(jié))

以字符串「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 absolviert";

上面的德文翻譯得不是非常好,只是單純用來說明調換占位符順序的功能而已。

如果你需要對簡單的整數(shù)或者浮點數(shù)進行本地化,你可以使用 localizedStringWithFormat: 這個變體。數(shù)字本地化的更高級用法涉及 NSNumberFormatter,會在本文后面講到。

單復數(shù)與陰陽性

在 OS X 10.9 和 iOS 7 中,本地化字符串的時候可以使用比替換格式化字符串中的占位符更酷的特性:蘋果官方想處理不同語言中對于名詞復數(shù)和不同性別采取的不同變化。

讓我們再看一下之前的例子:@”%lu out of %lu runs completed.” 這個翻譯在「跑多次」的時候才是對的(譯者注:即第二個 %lu 代表的數(shù)字大于 1),所以我們不得不定義兩個不同的字符串來處理單次和多次的情況:

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

這種做法在英語中是對的,但是在其他很多語言中會出錯。比如希伯來語中名詞有三種形式:第一種是單數(shù)和十的倍數(shù),第二種是 2,第三種是其他的復數(shù)??肆_地亞語中,個位數(shù)為 1 的數(shù)字有單獨的表示方法:「31 od 32 staze zavr?ene」,與之相對的是「5 od 8 staza zavr?ene」(注意其中「staze」和「staza」的差別)。很多語言針對非整型數(shù)也有不同的表達方式。

想全面了解這個問題可以參見基于 Unicode 的語言復數(shù)規(guī)則。其中涵蓋的變化之博大精深令人嘆為觀止。

為了在 10.9 和 iOS 7 平臺上正確處理這個問題,我們需要如下構造可本地化字符串:

[NSString localizedStringWithFormat:NSLocalizedString(@"activity-profile.label.%lu out of %lu runs completed"), completedRuns, totalRuns];

然后我們在 .strings 后綴文件所處目錄中創(chuàng)建一個同名的 .stringsdict 后綴的文件,如果前者名為 Localizable.strings,則后者為 Localizable.stringsdict。保留 .strings 后綴的字符串文件是必須的,即使它里面什么內容也沒有。這個 .stringsdict 后綴的字符串字典文件是一個屬性列表(plist)文件,比字符串文件復雜得多,換來的是正確處理所有語言的名詞復數(shù)問題,而不需要將處理邏輯寫在代碼中。

下面是一個該文件的例子:

<?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ù)字,必須擴展格式化字符串的語法。所以我們可以定義類似 %#@lu_total_runs@ 的格式化字符串,然后定義一個字典來解析它。在上面的字典中,我們通過將 NSStringFormatSpecTypeKey 設置為 NSStringPluralRuleType 表明這是一個處理名詞復數(shù)的規(guī)則,指定了值的類型(在本例中是 lu,即無符號長整數(shù)),還定義了針對不同復數(shù)形式的不同輸出(可以從「zero」、「one」、「few」、「many」和「others」中選擇,上例中僅制定了「one」和「other」)。

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

我們還可以更進一步定義遞歸的規(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>

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

字母大小寫

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

調用這些方法的時候你需要傳入區(qū)域設置參數(shù) locale ,這樣就可以將大小寫的改變應用到本地化之后的其他語言版本中。當你使用 NSLocalizedString 及其變體的那些宏時無須擔心本地化后的大小寫問題,因為在方法內部已經自動做了處理,而且在用戶選擇的語言不可用時會使用默認語言來代替。

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

文件路徑的本地化

一般而言你應該始終用 NSURL 來表現(xiàn)文件路徑,因為這會讓文件名的本地化變得容易:

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

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

構造這樣一個其他語言的文件名是否包含后綴需要參照用戶 Finder 中的相關選項。如果你需要獲取文件的類型,也可以這樣調用 NSURLLocalizedTypeDescriptionKey 來從中獲得。

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

格式器

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

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

格式化數(shù)字

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

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

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

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

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

格式化日期

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

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

假設你想只顯示天和月份的縮寫,系統(tǒ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);

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

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

緩存格式器對象

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

static NSDateFormatter *formatter;

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

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

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ù)據格式化指南中對此做了注解:

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

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

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

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

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

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

雖然格式器在處理用戶輸入時很好用,在已知格式的情況下處理機器生成的數(shù)據有更好的方法,因為為所有區(qū)域設置生成正確輸出的數(shù)字和日期格式器有性能上的損失。

舉例來說,如果你從服務器接收到很多日期字符串,在你將它們轉換成日期對象時,日期格式器并不是最好的選擇。蘋果官方的日期格式化指南中提到對于這些固定格式且無需進行本地化的日期,使用 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

因為 strptime_l 函數(shù)也可以感知用戶的區(qū)域設置,所以確保最后一個參數(shù)傳入 NULL 以使用標準 POSIX 區(qū)域設置。函數(shù)中可用的占位符請參考 strftime 用戶手冊

調試本地化字符串

應用支持的語言版本越多,確保所有元素都正確顯示就越難。但是這里有一些默認的用戶選項和工具可以減輕你的負擔。

你可以使用 NSDoubleLocalizedStrings、AppleTextDirectionNSForceRightToLeftWritingDirection 選項保證你的布局不會因為長字符串或者從右往左讀的語言而混亂。NSShowNonLocalizedStringsNSShowNonLocalizableStrings 則可以幫助你找到沒有翻譯的字符串和根本沒有制定字符串本地化宏的字符串。(所有這些工具的選項都可以通過程序設置或者作為 Xcode 的 Scheme 編輯器啟動選項,如 -NSShowNonLocalizedStrings YES

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

-AppleLanguages (de, fr, en)

AppleLocale 則接收符合Unicode 國際組件標準(International Components for Unicode) 的區(qū)域設置標識符作為參數(shù),如下:

-AppleLocale en_US

-AppleLocale en_GR

如果你翻譯的字符串沒有正確顯示,你可以帶上 -lint 選項運行 plutil 命令來檢查一下字符串文件是否有語法錯誤。例如你在行尾漏寫了分號,plutil 會輸出如下警告:

$ 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

當我們修正了這個錯誤后,plutil 會告訴我們一切正常:

$ plutil Localizable.strings
Localizable.strings: OK

對于支持多種語言的應用,還有一個與調試無關的小技巧:你可以在 iOS 上自動生成應用在多種語言下的屏幕截圖。因為可以使用 UIAutomation 來控制應用,使用 AppleLanguages 在啟動時設置語言,所以整個測試過程可以自動化。GitHub 上的這個項目中可以找到更多細節(jié)。

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

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

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

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

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

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

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

結論

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