iOS國(guó)際化

Demo同步更新到Swift2.3
本文地址: http://mokai.me/iOS-i18n.html

在真正將國(guó)際化實(shí)踐前,只知道通過(guò)NSLocalizedString方法將相應(yīng)語(yǔ)言的字符串加載進(jìn)來(lái)即可。但項(xiàng)目的新需求增加英文版本,并支持應(yīng)用內(nèi)無(wú)死角切換~,這才跳過(guò)各種坑實(shí)現(xiàn)了應(yīng)用內(nèi)切換語(yǔ)言,并記錄至此。

環(huán)境

系統(tǒng)環(huán)境: iOS7 or later
開(kāi)發(fā)環(huán)境: Swift2.3 & Xcode7.3.1
DEMO: LocalDemo

這個(gè)Demo的功能主要是切換語(yǔ)言后相應(yīng)的界面文字&圖片以及搜索引擎都會(huì)隨語(yǔ)言變化。我們會(huì)圍繞這個(gè)DEMO進(jìn)行講解,讀者可以先下載這個(gè)Demo運(yùn)行看下效果再往下

iOS國(guó)際化原理分析

國(guó)際化其實(shí)都大同小異,其核心思想就是為每種語(yǔ)言單獨(dú)定義一份資源
iOS就是通過(guò)xxx.lproj目錄來(lái)定義每個(gè)語(yǔ)言的資源,這里的資源可以是圖片,文本,Storyboard,Xib等。我們可以看看LocalDemo源代碼的物理目錄結(jié)構(gòu)

Base,暫時(shí)無(wú)需理會(huì)


English


中文


每種語(yǔ)言都有自己的 語(yǔ)言代碼.lproj文件夾,加載資源時(shí)只需要加載相應(yīng)語(yǔ)言文件夾下的資源就OK,這步可以系統(tǒng)為我們完成,也可以手動(dòng)去做。

項(xiàng)目源代碼中如果有多個(gè)不同目錄的國(guó)際化資源,則會(huì)有產(chǎn)生多個(gè)xxx.lproj,但在編譯打包后,會(huì)集中放在app的根目錄中的xxx.lproj中,不信你看~

開(kāi)始國(guó)際化

首先點(diǎn)擊項(xiàng)目->PROJECT->Info->Localizations中添加要支持的語(yǔ)言

此處Use Base Internationalization開(kāi)啟狀態(tài)下,每個(gè)國(guó)際化資源文件會(huì)有個(gè)Base選項(xiàng),主要針對(duì)String,Storyboard,Xib作為一個(gè)基礎(chǔ)的模板,像后述storyboard國(guó)際化中方案二就是基于Base StoryBoard進(jìn)行改動(dòng)。

在點(diǎn)擊+ 添加相應(yīng)語(yǔ)言時(shí)會(huì)彈出以下對(duì)話框,意思是為現(xiàn)有的資源添加語(yǔ)言文件,我們點(diǎn)擊Finish就行了

文本的國(guó)際化

主要針對(duì)代碼中的字符串進(jìn)行國(guó)際化,比如說(shuō)一些消息,UI標(biāo)題等。

我們通過(guò)一個(gè)Localizable.strings文件來(lái)存儲(chǔ)每個(gè)語(yǔ)言的文本,它是iOS默認(rèn)加載的文件,如果想用自定義名稱命名,在使用NSLocalizedString方法時(shí)指定tableName為自定義名稱就好了,但你的應(yīng)用規(guī)模不是很大就不要分模塊搞特殊了。

每個(gè)資源文件如果想為一種語(yǔ)言添加支持,通過(guò)其屬性面板中的Localization添加相應(yīng)語(yǔ)言就行了,此時(shí)Localizable.strings處于可展開(kāi)狀態(tài),子級(jí)有著相應(yīng)語(yǔ)言的副本。我們把相應(yīng)語(yǔ)言的文本放在副本里面就行了

此處Base與前面提過(guò)到的開(kāi)啟Use Base Internationalization是有關(guān)聯(lián)的,只有開(kāi)啟了全局Use Base Internationalization此處才會(huì)顯示。那為什么這里沒(méi)有勾選Base?Base做為一個(gè)基礎(chǔ)模板,作用于Strings文件是沒(méi)有太大意義的,另外去掉Base意義著在Base.lproj中少了一個(gè)strings文件,APP大小也所有下降,這點(diǎn)對(duì)于圖片的Base更是如此

在上圖可以看到其實(shí)就是為每一套語(yǔ)言新建一份strings,其內(nèi)容采用"key" = "value";的格式,注意有;號(hào)

我們?cè)诖a中這樣寫就行了

NSLocalizedString("首頁(yè)",comment: "")
NSLocalizedString("好友",comment: "")
NSLocalizedString("我",comment: "")

另外中文strings【Localizable.strings(Simplified)】可以不要的(可以理解為中文為APP的默認(rèn)語(yǔ)言),因?yàn)閗ey就是value,當(dāng)找不到相應(yīng)的語(yǔ)言strings或value時(shí)會(huì)直接返回key。nice!這樣一來(lái)我們做文本的國(guó)際化就只要維護(hù)一個(gè)英文副本strings就O了

圖片的國(guó)際化

二種方案,通過(guò)原生支持與自定義命名

注意,新版Xcode中Images.xcassets不支持國(guó)際化(屬性頁(yè)面中沒(méi)有Localization),Xcode5以前是支持的

  • 方案一:自定義文本命名

    利用文本國(guó)際化的方式,在代碼中調(diào)用

    UIImage(named: NSLocalizedString("search_logo",comment: ""))
    

    不推薦,一是因?yàn)樽龇ㄌ玪ow了,工作量明顯加大。二是不能在Storyboard或XIB中使用

  • 方案二:原生支持


同上,Base副本去掉。另外需要注意的是,使用這種方式,在XIB或Storyboard中引用圖片時(shí)如果只使用名稱是實(shí)時(shí)顯示不了的,一定要加上后綴名。如avater.png

使用方式不變,iOS會(huì)自動(dòng)找相應(yīng)語(yǔ)言(xxx.lproj)下的圖片

```
UIImage(named: "avater")
```

對(duì)于圖片的放置,正確姿態(tài)應(yīng)該是`需要國(guó)際化的圖片放在自定義Group里面,不需要國(guó)際化的圖片放在Images.xcassets`

Storyboard&XIB的國(guó)際化

前面的兩種資源國(guó)際化比較簡(jiǎn)單,但Storyboard國(guó)際化就稍微麻煩了點(diǎn)。同樣它也有二種方案

  • 方案一:每種語(yǔ)言定制一套Storyboard

    在上圖我們可以看到,每種語(yǔ)言都可以切換為strings或Storyboard(默認(rèn)為strings)。如果選用Interface Builder Storyboard方案,那么每種語(yǔ)言都有一套相應(yīng)的Storyboard,各個(gè)語(yǔ)言Storyboard間的界面改動(dòng)不關(guān)聯(lián)

  • 方案二:基于基礎(chǔ)的Base StoryBoard以及每種語(yǔ)言一套strings <a id='storyboard_2'></a>

    基于一個(gè)基礎(chǔ)的Storyboard,可以看作是一個(gè)基礎(chǔ)的模板,Storyboard里面所有的文本類資源(如UILabel的text)都會(huì)被放在相應(yīng)語(yǔ)言的strings里面。此時(shí)我們?yōu)镾toryboard里的字符類資源作國(guó)際化只需要編輯相應(yīng)語(yǔ)言的strings就行了

首選方案二。因?yàn)椴捎梅桨敢唬饬x著你每改動(dòng)一個(gè)界面元素就得去相應(yīng)語(yǔ)言Storyboard一一改動(dòng),那跟為每個(gè)語(yǔ)言新起一個(gè)項(xiàng)目是一樣的道理。但是采用方案二,我們只需改動(dòng)Base Storyboard就行了

注意,方案二中相應(yīng)語(yǔ)言的strings一旦生成后,Base Storyboard有任何編輯都不會(huì)影響到strings,這就意味著如果我們刪除或添加了一個(gè)UILabel的text,strings也不能同步改動(dòng)

還好,Xcode為我們提供了ibtool工具來(lái)生成Storyboard的strings文件。

ibtool Main.storyboard --generate-strings-file ./NewTemp.string

但是ibtool生成的strings文件是BaseStoryboard的strings(默認(rèn)語(yǔ)言的strings),且會(huì)把我們?cè)瓉?lái)的strings替換掉。所以我們要做的就是把新生成的strings與舊的strings進(jìn)行沖突處理(新的附加上,刪除掉的注釋掉),這一切可以用這個(gè)pythoy腳本來(lái)實(shí)現(xiàn),見(jiàn)AutoGenStrings.py。然后我們將借助Xcode 中 Run Script來(lái)運(yùn)行這段腳本。這樣每次Build時(shí)都會(huì)保證語(yǔ)言strings與Base Storyboard保持一致

應(yīng)用內(nèi)切換語(yǔ)言

應(yīng)用啟動(dòng)時(shí),首先會(huì)讀取NSUserDefaults中的key為AppleLanguages的內(nèi)容,該key返回一個(gè)String數(shù)組,存儲(chǔ)著APP支持的語(yǔ)言列表,數(shù)組的第一項(xiàng)為APP當(dāng)前默認(rèn)的語(yǔ)言。

在安裝后第一次打開(kāi)APP時(shí),會(huì)自動(dòng)初始化該key為當(dāng)前系統(tǒng)的語(yǔ)言編碼,如簡(jiǎn)體中文就是zh-Hans。

//獲取APP當(dāng)前語(yǔ)言
(NSUserDefaults.standardUserDefaults().valueForKey("AppleLanguages") as! Array<String>)[0]

那么我們要實(shí)現(xiàn)語(yǔ)言切換改變AppleLanguages的值即可,但是這里有一個(gè)坑,因?yàn)樘O果沒(méi)提供給我們直接修改APP默認(rèn)語(yǔ)言的API,我們只能通過(guò)NSUserDefaults手動(dòng)去操作,且AppleLanguages的值改變后APP得重新啟動(dòng)后才會(huì)生效(才會(huì)讀取相應(yīng)語(yǔ)言的lproj中的資源,意義著就算你改了,資源還是加載的APP啟動(dòng)時(shí)lproj中的資源),猜測(cè)應(yīng)該是框架層在第一次加載時(shí)對(duì)AppleLanguages的值進(jìn)行了內(nèi)存緩沖

//設(shè)置APP當(dāng)前語(yǔ)言
var def = NSUserDefaults.standardUserDefaults()
def.setValue([“zh-Hans”], forKey:"AppleLanguages")
def.synchronize()

那么問(wèn)題來(lái)了,如何做到改變AppleLanguages的值就加載相應(yīng)語(yǔ)言的lproj資源?

其實(shí),APP中的資源加載(Storyboard、圖片、字符串)都是在NSBundle.mainBundle()上操作的,那么我們只要在語(yǔ)言切換后把NSBundle.mainBundle()替換成當(dāng)前語(yǔ)言的bundle就行了,這樣系統(tǒng)通過(guò)NSBundle.mainBundle()去加載資源時(shí)實(shí)則是加載的當(dāng)前語(yǔ)言bundle中的資源

lproj目錄可以用一個(gè)NSBundle表示

import Foundation

/**
*  當(dāng)調(diào)用onLanguage后替換掉mainBundle為當(dāng)前語(yǔ)言的bundle
*/
private let _bundle:UnsafePointer<Void> =  unsafeBitCast(0,UnsafePointer<Void>.self)
class BundleEx: NSBundle {
    override func localizedStringForKey(key: String, value: String?, table tableName: String?) -> String {
        if let bundle = languageBundle() {
            return bundle.localizedStringForKey(key, value: value, table: tableName)
        }else{
            return super.localizedStringForKey(key, value: value, table: tableName)
        }
    }
}

extension NSBundle {
    private struct Static {
        static var onceToken : dispatch_once_t = 0
    }
    func onLanguage(){
        //替換NSBundle.mainBundle()為自定義的BundleEx
        dispatch_once(&Static.onceToken) {
            object_setClass(NSBundle.mainBundle(), BundleEx.self)
        }
    }
    
    //當(dāng)前語(yǔ)言的bundle
    func languageBundle()->NSBundle?{
        return Languager.standardLanguager().currentLanguageBundle
    }
}

其他

  • 設(shè)置運(yùn)行語(yǔ)言環(huán)境
    有時(shí)我們第一次安裝APP時(shí)不想默認(rèn)跟隨系統(tǒng),那么可以通過(guò)Xcode的scheme來(lái)指定特定語(yǔ)言

  • Storyboard實(shí)時(shí)預(yù)覽
    直接上圖~

  • IB中UIImageView國(guó)際化無(wú)效
    解決辦法就是為UIImageView擴(kuò)展一個(gè)方法,然后通過(guò)IB中的User Defined Runtime Attributes把imageName傳進(jìn)去

    extension UIImageView{
        var local: String {
            get{
                return ""
            }
            set(newlocal) {
                self.image = localizedImage(newlocal)
            }
        }
    }
    
  • IB中UITextView國(guó)際化無(wú)效
    解決辦法和UIImageView類似,擴(kuò)展一個(gè)方法,然后把self.text做為key去strings文件中拿相應(yīng)語(yǔ)言的value

    extension UITextView {
        var local: Bool {
            get{
                return true
            }
            set(newlocale) {
                self.text = localized(self.text)
            }
        }
    }
    
  • LaunchScreen.xib的國(guó)際化
    很遺憾,到目前為止,還不支持LaunchScreen.xib的國(guó)際化,我們只能通過(guò)自定義一個(gè)LaunchViewController來(lái)完成此需求,但也有些不足,就是應(yīng)用啟動(dòng)時(shí)會(huì)黑屏一段時(shí)間,所以建議啟動(dòng)頁(yè)面不要弄國(guó)際化

參考

小小廣告

本人目前是一名自由職業(yè)者,接受移動(dòng)兩端的項(xiàng)目開(kāi)發(fā),如果你有需求或者有資源請(qǐng)速與我聯(lián)系吧,QQ865425695

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,825評(píng)論 6 546
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,814評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事。” “怎么了?”我有些...
    開(kāi)封第一講書人閱讀 178,980評(píng)論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 64,064評(píng)論 1 319
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,779評(píng)論 6 414
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 56,109評(píng)論 1 330
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,099評(píng)論 3 450
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書人閱讀 43,287評(píng)論 0 291
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,799評(píng)論 1 338
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,515評(píng)論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,750評(píng)論 1 375
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,221評(píng)論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,933評(píng)論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 35,327評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 36,667評(píng)論 1 296
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,492評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,703評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容