本文是投稿文章,作者:龔凱
原文:iOS國際化
在真正將國際化實踐前,只知道通過NSLocalizedString方法將相應語言的字符串加載進來即可。但最近公司項目的新需求增加英文版本,並支持應用內無死角切換~,這才跳過各種坑實現了應用內切換語言,並記錄至此。
環境
系統環境: iOS7 - iOS9
開發環境: Swift2 & Xcode7
DEMO: LocalDemo
這個Demo的功能主要是切換語言後相應的界面文字&圖片以及搜索引擎都會隨語言變化。我們會圍繞這個DEMO進行講解,讀者可以先下載這個Demo運行看下效果再往下
iOS國際化原理分析
國際化其實都大同小異,其核心思想就是為每種語言單獨定義一份資源。
iOS就是通過xxx.lproj目錄來定義每個語言的資源,這裡的資源可以是圖片,文本,Storyboard,Xib等。我們可以看看LocalDemo源代碼的物理目錄結構
Base,暫時無需理會
English
中文
每種語言都有自己的 語言代碼.lproj文件夾,加載資源時只需要加載相應語言文件夾下的資源就OK,這步可以系統為我們完成,也可以手動去做。
項目源代碼中如果有多個不同目錄的國際化資源,則會有產生多個xxx.lproj,但在編譯打包後,會集中放在app的根目錄中的xxx.lproj中,不信你看~
開始國際化
首先點擊項目->PROJECT->Info->Localizations中添加要支持的語言.
此處Use Base Internationalization開啟狀態下,每個國際化資源文件會有個Base選項,主要針對String,Storyboard,Xib作為一個基礎的模板,像後述storyboard國際化中方案二就是基於Base StoryBoard進行改動。
在點擊+ 添加相應語言時會彈出以下對話框,意思是為現有的資源添加語言文件,我們點擊Finish就行了.
文本的國際化
主要針對代碼中的字符串進行國際化,比如說一些消息,UI標題等。
我們通過一個Localizable.strings文件來存儲每個語言的文本,它是iOS默認加載的文件,如果想用自定義名稱命名,在使用NSLocalizedString方法時指定tableName為自定義名稱就好了,但你的應用規模不是很大就不要分模塊搞特殊了。
每個資源文件如果想為一種語言添加支持,通過其屬性面板中的Localization添加相應語言就行了,此時Localizable.strings處於可展開狀態,子級有著相應語言的副本。我們把相應語言的文本放在副本裡面就行了.
此處Base與前面提過到的開啟Use Base Internationalization是有關聯的,只有開啟了全局Use Base Internationalization此處才會顯示。那為什麼這裡沒有勾選Base?Base做為一個基礎模板,作用於Strings文件是沒有太大意義的,另外去掉Base意義著在Base.lproj中少了一個strings文件,APP大小也所有下降,這點對於圖片的Base更是如此.
在上圖可以看到其實就是為每一套語言新建一份strings,其內容采用"key" = "value";的格式,注意有;號
我們在代碼中這樣寫就行了
NSLocalizedString("首頁",comment: "") NSLocalizedString("好友",comment: "") NSLocalizedString("我",comment: "")
另外中文strings【Localizable.strings(Simplified)】可以不要的(可以理解為中文為APP的默認語言),因為key就是value,當找不到相應的語言strings或value時會直接返回key。nice!這樣一來我們做文本的國際化就只要維護一個英文副本strings就O了
圖片的國際化
二種方案,通過原生支持與自定義命名
注意,新版Xcode中Images.xcassets不支持國際化(屬性頁面中沒有Localization),Xcode5以前是支持的
方案一:自定義文本命名
利用文本國際化的方式,在代碼中調用
UIImage(named: NSLocalizedString("search_logo",comment: ""))
不推薦,一是因為做法太low了,工作量明顯加大。二是不能在Storyboard或XIB中使用
方案二:原生支持
同上,Base副本去掉。另外需要注意的是,使用這種方式,在XIB或Storyboard中引用圖片時如果只使用名稱是實時顯示不了的,一定要加上後綴名。如avater.png
使用方式不變,iOS會自動找相應語言(xxx.lproj)下的圖片
UIImage(named: "avater")
對於圖片的放置,正確姿態應該是需要國際化的圖片放在自定義Group裡面,不需要國際化的圖片放在Images.xcassets
Storyboard&XIB的國際化
前面的兩種資源國際化比較簡單,但Storyboard國際化就稍微麻煩了點。同樣它也有二種方案
方案一:每種語言定制一套Storyboard
在上圖我們可以看到,每種語言都可以切換為strings或Storyboard(默認為strings)。如果選用Interface Builder Storyboard方案,那麼每種語言都有一套相應的Storyboard,各個語言Storyboard間的界面改動不關聯.
方案二:基於基礎的Base StoryBoard以及每種語言一套strings
基於一個基礎的Storyboard,可以看作是一個基礎的模板,Storyboard裡面所有的文本類資源(如UILabel的text)都會被放在相應語言的strings裡面。此時我們為Storyboard裡的字符類資源作國際化只需要編輯相應語言的strings就行了
首選方案二。因為采用方案一,意義著你每改動一個界面元素就得去相應語言Storyboard一一改動,那跟為每個語言新起一個項目是一樣的道理。但是采用方案二,我們只需改動Base Storyboard就行了.
注意,方案二中相應語言的strings一旦生成後,Base Storyboard有任何編輯都不會影響到strings,這就意味著如果我們刪除或添加了一個UILabel的text,strings也不能同步改動。
還好,Xcode為我們提供了ibtool工具來生成Storyboard的strings文件。
ibtool Main.storyboard --generate-strings-file ./NewTemp.string
但是ibtool生成的strings文件是BaseStoryboard的strings(默認語言的strings),且會把我們原來的strings替換掉。所以我們要做的就是把新生成的strings與舊的strings進行沖突處理(新的附加上,刪除掉的注釋掉),這一切可以用這個pythoy腳本來實現,見AutoGenStrings.py。然後我們將借助Xcode 中 Run Script來運行這段腳本。這樣每次Build時都會保證語言strings與Base Storyboard保持一致。
應用內切換語言
應用啟動時,首先會讀取NSUserDefaults中的key為AppleLanguages的內容,該key返回一個String數組,存儲著APP支持的語言列表,數組的第一項為APP當前默認的語言。
在安裝後第一次打開APP時,會自動初始化該key為當前系統的語言編碼,如簡體中文就是zh-Hans。
//獲取APP當前語言 (NSUserDefaults.standardUserDefaults().valueForKey("AppleLanguages") as! Array)[0]
那麼我們要實現語言切換改變AppleLanguages的值即可,但是這裡有一個坑,因為蘋果沒提供給我們直接修改APP默認語言的API,我們只能通過NSUserDefaults手動去操作,且AppleLanguages的值改變後APP得重新啟動後才會生效(才會讀取相應語言的lproj中的資源,意義著就算你改了,資源還是加載的APP啟動時lproj中的資源),猜測應該是框架層在第一次加載時對AppleLanguages的值進行了內存緩沖
//設置APP當前語言 var def = NSUserDefaults.standardUserDefaults() def.setValue([“zh-Hans”], forKey:"AppleLanguages") def.synchronize()
那麼問題來了,如何做到改變AppleLanguages的值就加載相應語言的lproj資源?
其實,APP中的Storyboard的加載,圖片與字符串的加載都是在NSBundle.mainBundle()上操作的,那麼我們只要在語言切換後把NSBundle.mainBundle()替換成當前語言的bundle就行了,這樣系統通過NSBundle.mainBundle()去加載資源時實則是加載的當前語言bundle中的資源
lproj目錄可以用一個NSBundle表示:
import Foundation /** * 當調用onLanguage後替換掉mainBundle為當前語言的bundle */ private let _bundle:UnsafePointer= unsafeBitCast(0,UnsafePointer.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) } } //當前語言的bundle func languageBundle()->NSBundle?{ return Languager.standardLanguager().currentLanguageBundle } }
以上Languager是 iOS-i18n 開源庫的一部分,我把項目中國際化部分封裝了下,有興趣的童鞋可以去看看
其他
設置運行語言環境
有時我們第一次安裝APP時不想默認跟隨系統,那麼可以通過Xcode的scheme來指定特定語言
Storyboard實時預覽,直接上圖~
IB中UIImageView國際化無效
解決辦法就是為UIImageView擴展一個方法,然後通過IB中的User Defined Runtime Attributes把imageName傳進去
extension UIImageView{ var locale:String{ get{ return "" } set(newlocale){ self.image = localizedImage(newlocale) } } }
IB中UITextView國際化無效
解決辦法和UIImageView類似,擴展一個方法,然後把self.text做為key去strings文件中拿相應語言的value
extension UITextView{ var locale:Bool{ get{ return true } set(newlocale){ self.text = localized(self.text) } } }
LaunchScreen.xib的國際化
很遺憾,到目前為止,還不支持LaunchScreen.xib的國際化,我們只能通過自定義一個LaunchViewController來完成此需求,但也有些不足,就是應用啟動時會黑屏一段時間,所以建議啟動頁面不要弄國際化.
參考:
iOS國際化——通過腳本使storyboard翻譯自增
Working with Localization
How to force NSLocalizedString to use a specific language