NSLocalizedString
方法将相应语言的字符串加载进来即可。但最近公司项目的新需求增加英文版本,并支持应用内无死角切换~,这才跳过各种坑实现了应用内切换语言,并记录至此。
环境
系统环境: iOS7 - iOS9
开发环境: Swift2 & Xcode7
DEMO: LocalDemo
这个Demo的功能主要是切换语言后相应的界面文字&图片以及搜索引擎都会随语言变化。我们会围绕这个DEMO进行讲解,读者可以先下载这个Demo运行看下效果再往下
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
处于可展开状态,子级有着相应语言的副本。我们把相应语言的文本放在副本里面就行了
开启Use Base Internationalization是有关联的,只有开启了全局Use Base Internationalization此处才会显示。那为什么这里没有勾选Base?Base做为一个基础模板,作用于Strings文件是没有太大意义的,另外去掉Base意义着在Base.lproj中少了一个strings文件,APP大小也所有下降,这点对于图片的Base更是如此
"key" = "value";
的格式,注意有;
号
我们在代码中这样写就行了
<code class="hljs scss has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-function" style="box-sizing: border-box;">NSLocalizedString(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"首页"</span>,comment: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">""</span>)</span><span class="hljs-function" style="box-sizing: border-box;">NSLocalizedString(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"好友"</span>,comment: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">""</span>)</span>
<span class="hljs-function" style="box-sizing: border-box;">NSLocalizedString(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"我"</span>,comment: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">""</span>)</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li></ul>
另外中文strings【Localizable.strings(Simplified)】可以不要的(可以理解为中文为APP的默认语言),因为key就是value,当找不到相应的语言strings或value时会直接返回key。nice!这样一来我们做文本的国际化就只要维护一个英文副本strings就O了
图片的国际化
二种方案,通过原生支持与自定义命名
Localization),Xcode5以前是支持的
- 方案一:自定义文本命名
• 利用文本国际化的方式,在代码中调用<code class="hljs scss has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-function" style="box-sizing: border-box;">UIImage(named: <span class="hljs-function" style="box-sizing: border-box;">NSLocalizedString(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"search_logo"</span>,comment: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">""</span>)</span>)</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>
- 不推荐,一是因为做法太low了,工作量明显加大。二是不能在Storyboard或XIB中使用
方案二:原生支持
同上,Base副本去掉。另外需要注意的是,使用这种方式,在XIB或Storyboard中引用图片时如果只使用名称是实时显示不了的,一定要加上后缀名。如avater.png
使用方式不变,iOS会自动找相应语言(xxx.lproj)下的图片
<code class="hljs http has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-attribute" style="box-sizing: border-box;">UIImage(named</span>: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"avater")</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>
需要国际化的图片放在自定义Group里面,不需要国际化的图片放在Images.xcassets
Storyboard&XIB的国际化
前面的两种资源国际化比较简单,但Storyboard国际化就稍微麻烦了点。同样它也有二种方案
- 方案一:每种语言定制一套Storyboard
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也不能同步改动
ibtool
工具来生成Storyboard的strings文件。
<code class="hljs lasso has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">ibtool Main<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">.</span>storyboard <span class="hljs-subst" style="color: rgb(0, 0, 0); box-sizing: border-box;">--</span>generate<span class="hljs-attribute" style="box-sizing: border-box;">-strings</span><span class="hljs-attribute" style="box-sizing: border-box;">-file</span> <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">.</span>/NewTemp<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">.</span><span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">string</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>
但是ibtool生成的strings文件是BaseStoryboard的strings(默认语言的strings),且会把我们原来的strings替换掉。所以我们要做的就是把新生成的strings与旧的strings进行冲突处理(新的附加上,删除掉的注释掉),这一切可以用这个pythoy脚本来实现,见AutoGenStrings.py。然后我们将借助Xcode 中 Run Script
来运行这段脚本。这样每次Build时都会保证语言strings与Base Storyboard保持一致
应用内切换语言
AppleLanguages
的内容,该key返回一个String数组,存储着APP支持的语言列表,数组的第一项为APP当前默认的语言。
在安装后第一次打开APP时,会自动初始化该key为当前系统的语言编码,如简体中文就是zh-Hans。
<code class="hljs javascript has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//获取APP当前语言</span>(NSUserDefaults.standardUserDefaults().valueForKey(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"AppleLanguages"</span>) as! <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">Array</span><<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">String</span>>)[<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>]</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li></ul>
AppleLanguages
的值即可,但是这里有一个坑,因为苹果没提供给我们直接修改APP默认语言的API,我们只能通过NSUserDefaults手动去操作,且AppleLanguages
的值改变后APP得重新启动后才会生效(才会读取相应语言的lproj中的资源,意义着就算你改了,资源还是加载的APP启动时lproj中的资源),猜测应该是框架层在第一次加载时对AppleLanguages
的值进行了内存缓冲
<code class="hljs scala has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//设置APP当前语言</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">var</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">def</span> = NSUserDefaults.standardUserDefaults()
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">def</span>.setValue([“zh-Hans”], forKey:<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"AppleLanguages"</span>)
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">def</span>.synchronize()</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li></ul>
AppleLanguages
的值就加载相应语言的lproj资源?NSBundle.mainBundle()
上操作的,那么我们只要在语言切换后把NSBundle.mainBundle()
替换成当前语言的bundle就行了,这样系统通过NSBundle.mainBundle()
去加载资源时实则是加载的当前语言bundle中的资源
lproj目录可以用一个NSBundle表示
<code class="hljs java has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">import</span> Foundation<span class="hljs-javadoc" style="color: rgb(136, 0, 0); box-sizing: border-box;">/**
* 当调用onLanguage后替换掉mainBundle为当前语言的bundle
*/</span>
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> let _bundle:UnsafePointer<Void> = unsafeBitCast(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>,UnsafePointer<Void>.self)
class BundleEx: NSBundle {
override func localizedStringForKey(key: String, value: String?, table tableName: String?) -> String {
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">if</span> let bundle = languageBundle() {
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> bundle.localizedStringForKey(key, value: value, table: tableName)
}<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">else</span>{
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">super</span>.localizedStringForKey(key, value: value, table: tableName)
}
}
}
extension NSBundle{
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">private</span> struct Static {
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> var onceToken : dispatch_once_t = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>
}
func onLanguage(){
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//替换NSBundle.mainBundle()为自定义的BundleEx</span>
dispatch_once(&Static.onceToken) {
object_setClass(NSBundle.mainBundle(), BundleEx.self)
}
}
<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//当前语言的bundle</span>
func languageBundle()->NSBundle?{
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> Languager.standardLanguager().currentLanguageBundle
}
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li></ul>
Languager
是 iOS-i18n 开源库的一部分,我把项目中国际化部分封装了下,有兴趣的童鞋可以去看看
其他
- 设置运行语言环境
有时我们第一次安装APP时不想默认跟随系统,那么可以通过Xcode的scheme来指定特定语言 - Storyboard实时预览
直接上图~ - IB中UIImageView国际化无效
UIImageView
- 扩展一个方法,然后通过IB中的
User Defined Runtime Attributes
• 把imageName传进去<code class="hljs lasso has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">extension UIImageView{ <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">var</span> locale:<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">String</span>{
get{
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">""</span>
}
<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">set</span>(newlocale){
<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">self</span><span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">.</span>image <span class="hljs-subst" style="color: rgb(0, 0, 0); box-sizing: border-box;">=</span> localizedImage(newlocale)
}
}
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul>
• IB中UITextView国际化无效
解决办法和UIImageView类似,扩展一个方法,然后把self.text做为key去strings文件中拿相应语言的value<code class="hljs livecodeserver has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">extension UITextView{ var locale:Bool{
<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">get</span>{
<span class="hljs-constant" style="box-sizing: border-box;">return</span> <span class="hljs-constant" style="box-sizing: border-box;">true</span>
}
<span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">set</span>(newlocale){
self.<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">text</span> = localized(self.<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">text</span>)
}
}
}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul>
- LaunchScreen.xib的国际化
很遗憾,到目前为止,还不支持LaunchScreen.xib的国际化,我们只能通过自定义一个LaunchViewController来完成此需求,但也有些不足,就是应用启动时会黑屏一段时间,所以建议启动页面不要弄国际化
参考:
- iOS国际化——通过脚本使storyboard翻译自增
- Working with Localization
- How to force NSLocalizedString to use a specific language