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
開發(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中,不信你看~

開始國(guó)際化

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

此處Use Base Internationalization開啟狀態(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處于可展開狀態(tài),子級(jí)有著相應(yīng)語(yǔ)言的副本。我們把相應(yīng)語(yǔ)言的文本放在副本里面就行了

此處Base與前面提過(guò)到的開啟Use Base Internationalization是有關(guān)聯(lián)的,只有開啟了全局Use Base Internationalization此處才會(huì)顯示。那為什么這里沒有勾選Base?Base做為一個(gè)基礎(chǔ)模板,作用于Strings文件是沒有太大意義的,另外去掉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è)面中沒有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)椴捎梅桨敢?,意義著你每改動(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),見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ǔ)言。

在安裝后第一次打開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果沒提供給我們直接修改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()

那么問題來(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)目開發(fā),如果你有需求或者有資源請(qǐng)速與我聯(lián)系吧,QQ865425695

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容