目录
概览
各种资源的国际化
1.文本
2.图片
3.nib
4.其他资源
特定模块/功能的国际化
1.APP图标
2.应用名与权限提示
3.启动图(LaunchScreen)
4.app调系统资源页面的国际化
5.涉及服务端数据内容的国际化
app内更改语言
1.更改语言的方案
2.未做国际化的旧项目迁移
概览
国际化的本质是为每种语言单独提供一份资源(文本,图片,音视频等)。
本文术语
本地化:指单独一种语言
国际化:多种语言的合体
在工程的Localizations中每新增一种语言,xcode会提示我们生成对应的文件,而后也生成了对应的文件夹。
iOS为这些文件提供了快捷的国际化方案。对于字符串资源文件生成相应语言的字符串文件放在对应的文件夹中,而XIB和StoryBoard则可选整个文件和字符串资源。具体的方案后续讨论。
如果忘了添加某个资源的具体语言文件,或者后续增加的资源文件,可以通过该资源文件的 文件监察器File Inspector
中的 Localize按钮添加。
Localizable.strings
和InfoPlist.strings
在国际化方案中是常见的。
- Localizable.strings
这个是读取多语言字符串方法NSLocalizedString
默认会加载的文件,如果自定了这个文件名字,则使用NSLocalizedStringFromTable
指定table即可 - InfoPlist.strings
info.plist
的字符串国际化文件,系统默认读取,名字固定
各种资源的国际化
1. 文本
添加了多语言的字符串资源文件处于可展开状态,子级有着相应语言的副本。我们把相应语言的文本放在副本里面就行了。
字符串文件中具体的格式是"key" = "value";
笔者发现写成key = "value";
也是不会有问题的(但是不加双引号不能有空格,会识别不了),比如应用名称的本地化:
看截图,最终的应用名称是后面那一个,说明有效且 被覆盖了。
使用NSLocalizedString(key, comment)
来读取字符串。第二个参数comment可以是nil,可以是一段为空的字符串,也可以是对key的注释。
看一下这个方法的实现
#define NSLocalizedString(key, comment) \
[NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:nil]
...
/* Method for retrieving localized strings. */
- (NSString *)localizedStringForKey:(NSString *)key value:(nullable NSString *)value table:(nullable NSString *)tableName NS_FORMAT_ARGUMENT(1);
localizedStringForKey:value:table:
是NSBundle的对象方法,由此可见,可以加载不同的包名和字符串资源表的字符串。也提供了相关宏
#define NSLocalizedStringFromTable(key, tbl, comment) \
[NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:(tbl)]
#define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \
[bundle localizedStringForKey:(key) value:@"" table:(tbl)]
重要:当找不到相应的语言strings或value时会直接返回key,如果你用英文的内容作为Key,甚至都可以不用维护英文本地化。
2. 图片
Xcode5之后图片资产(Assets.xcassets)不再支持国际化了,单张图片资源的方式仍然可用,使用方式同字符串。将需要国际化的图片拖入工程,选择文件监察器,点击Localize并选择多个语言后即可生成如如字符串资源一样的可展开状态了。要配置不同语言的图片前往该语言目录替换即可。
这个系统提供的方案支持Interface Builder
版的nib资源国际化(当然Localizable Strings 方式很显然只是字符串而已),也支持+ imageNamed:
加载方式的国际化。
还有个只适合用于纯代码,不支持nib的方式。就是把图片的名称做字符串国际化,然后再使用+ imageNamed:
加载。
另外,针对+imageWithContentsOfFile:
的加载方式,可以通过分类的方式,根据语言修改相应的加载路径。
PS:图片的国际化带来的是多份的副本,如果国际化中需要做的具体本地化语言较多,必然造成包的急剧增大。所以建议能避免就避免。
3. nib文件(XIB和StoryBoard)
nib文件的国际化方式上面提到有两种方式:
- 只做字符串资源(Localizable Strings)
- 整个nib文件(Interface Builder CocoaTouch XIB/StoryBoard)
nib文件有一个大坑,画重点
各个本地化的nib修改不会同步,nib的修改也不会同步至字符串资源
也就是说第一种方案每增加一种语言就得再画一个页面,本地化语言多的话,额外工作量惊人。
第二种方案,也得自己将新增的字符串拷贝出来。
如果要把更新同步的过程做成自动化,当然也是字符串方便一点。用整个nib文件做国际化比字符串资源方式强的地方也就是以下两点了
- 图片
- 不同本地化不同布局
一般也不会去根据不同本地化语言去修改布局,阿拉伯国家也就改个文本方向,这个特性视图自带,全局修改即可。
[UIView appearance].semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
nib中的图片资源用代码也可轻松解决,综上笔者建议针对nib文件只做字符串资源(Localizable Strings)。
所以,上面的图片方案选了支持nib的方案,然后现在不用了 🤣
意不意外?惊不惊喜?
不说这个了,来说说怎么做nib新增的需要国际化的字符串同步吧。
Xcode为我们提供了ibtool
工具来生成nib的strings文件
ibtool FirstViewController.xib --generate-strings-file lanuchScreen.strings
ibtool Main.storyboard --generate-strings-file storyBoard.strings
但是ibtool生成的strings文件是BaseStoryboard的strings(默认语言的strings),且会把我们原来的(甚至是翻译好的)strings替换掉。还是自己用脚本来做这个工作靠谱点,再借助Xcode 中 Run Script 来运行这段脚本,更新的时候build一下就行了。
具体的脚本代码在最后的Demo中,Run Script的添加方法:
Target->Build Phases->New Run Script Phase,在shell里面写入下面指令
python ${SRCROOT}/${TARGET_NAME}/RunScript/AutoGenStrings.py ${SRCROOT}/${TARGET_NAME}
4. 其他资源
其他资源(json、音视频、压缩包等等)的国际化方式与图片资源相同,读的时候读 主Bundle 即可,不同语言环境下iOS自动切。
剧透:后续的应用内切换语言会利用主Bundle的这一特性。
5. 涉及服务端数据内容的国际化
这部分内容的国际化,可考虑以下两种方案:
- 服务端不关心当前用户的本地化语言,返回所有适配的本地化内容,由客户端自己控制显示
- 服务端获取当前用户的本地化信息,返回相应的本地化内容
第一种适合适配本地化语言较少的情况,比如只适配中英文;而第二种,对配置信息的依赖比较高,服务端需要修改的内容也是比较多的。
如果使用第一种方案,一些常用的报错信息或者其他业务成功等信息可以整理成特定的code
由客户端直接做解析,减少信息传输量(虽然相比单个本地化还是会大很多)。
如果使用第二种方案,可以在请求头中带入当前用户的本地化信息,服务端根据这个判断,可以简便得多。
特定模块/功能的国际化
1. APP图标
除了动态图标的方法, 暂时也查不到什么动态修改图标的方法了。这个方法多用于 APP的节日活动。
事实上也没人去做这个的国际化,顺带提一下。
2. 应用名与权限提示
应用名与权限提示的国际化就是依赖info.plist的国际化。不同本地化文件放不同的键值对即可。
3. 启动图(LaunchScreen)
Xcode Overview 的 Adding Assets章节中有关于启动图的描述
Because the launch screen is shown before your app is running, you can only use a single root view of type UIView or UIViewController.
也就是说启动界面的展示是发生在main函数入口之前,也就决定了我们无法动态地修改启动图。另外,以下nib的两种国际化方式也是无效的。
另辟蹊径,利用info.plist的国际化来做LaunchScreen的静态国际化,如下:
这里要吐槽一下,即使做了静态国际化,以下两种状况是不会切换的
- 系统切换语言的时候
- 重启系统
只有重新安装app的时候才会切换 所以做启动图的国际化意义有限。
也看到有人说自己做一个LaunchScreenController作为启动页,但是这个状况下,app启动会黑屏一段时间,这不是想要的效果啊。
4. app调系统页面的国际化
关于调用系统资源,相机,相册,通讯录之类,APP内修改语言暂没找到刷新的方法。如果你有方法,麻烦告知楼主,非常感谢。
只有在app重启时,main函数中应用程序代理(AppDelegate)返回之前去设置偏好设置的 AppleLanguages
才是有效的。
所以,目前的解决方案就是这些页面全部自己实现。另外,导航按钮的国际化文本也不是在 main bundle 中加载的,一般app也会自定义这个按钮,这个不是痛点。
如有其它,欢迎补充。
app内更改语言
1.更改语言的方案
app的语言过年聚系统语言设置变化是最基本的国际化需求,更多的时候我们希望能够做到app内部热切换。
APP中的资源加载(Storyboard、图片、字符串)基本是在NSBundle.mainBundle()上操作的(自建私有库或者是三方库可能会自己做国际化,把国际化字符串资源放在自己的bundle中,如MJRefresh
),那么我们只要在语言切换后把相应资源加载的bundle替换成当前语言的bundle就行了。如下替换为 字符串资源加载的主要代码:
id value = language ? [NSBundle bundleWithPath:[[NSBundle mainBundle] pathForResource:@"zh-Hans" ofType:@"lproj"]] : nil;
objc_setAssociatedObject([NSBundle mainBundle], &kBundleKey, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
……
- (NSString *)localizedStringForKey:(NSString *)key value:(NSString *)value table:(NSString *)tableName {
NSBundle *bundle = objc_getAssociatedObject(self, &kBundleKey);
if (bundle) {
return [bundle localizedStringForKey:key value:value table:tableName];
} else {
return [super localizedStringForKey:key value:value table:tableName];
}
}
相应的nib文件中的图片加载和UITextView
中的文本也需要
如果,项目是纯代码的,也就是说国际化涉及不到nib文件,那就不需要这个实现方式了。自己存个当前语言标记,切换时换一个,做好判断即可。
如果想要在第一次安装的时候跟随系统的语言,应用启动后从NSUserDefault中可以读到语言数组,其中数组的第一个元素即是主要语言(Primary Language),系统的当前语言。
这些语言字符串当中的最后一部分是地区,会根据地区变化,后续根据这个列表判断的时候需要注意。
NSArray *array = [[NSUserDefaults standardUserDefaults] arrayForKey:@"AppleLanguages"];
不论哪种方式,切换语言都是需要刷新视图的,这个无法避免。
那么,如何优雅地刷新UI
- 微博的思路是,在切换语言时,发送通知NSNotification,所有的UI控件监听通知,然后在适当的时候刷新UI。
那么其实这么写,需要做的东西很多,或是通过Base类来实现,或是通过runtime实现,总之Button、Label、TextField等等都需要有一套统一的更新机制,可能不是一个最简单的办法。 - 而微信切换的方案是,刷新keyWindow的rootViewController,然后跳转到设置页。
这个思路有篇文章说的比较详细,直接看:在iOS App内优雅的动态切换语言
2.未做国际化的旧项目迁移
老项目的国际化迁移也都会牵涉到以上提到的各种问题。但是以上的问题都不是主要的,主要的是那些散落在代码中的各种需要国际化的文本。一个一个去抠出来肯定不现实。
Xcode为我们提供了一个工具genstrings
,这个工具与ibtool
类似,也是导出字符串资源文件的。只不过ibtool
适用于nib文件,而genstrings
适用于源代码文件。支持C,Objective-C,swift(官方未明确指出,笔者尝试通过),java等语言文件,如下官方描述:
The
genstrings
tool can parse C, Objective-C, and Java code files with the.c
,.m
, or.java
filename extensions.
然而,还是有很多工作要做,看一下官方的来那个外一个描述:
If you wrote your code using the Core Foundation and Foundation macros, the simplest way to create your strings files is using the
genstrings
command-line tool. You can use this tool to generate a new set of strings files or update a set of existing files based on your source code.
也就是说,这个脚本生效的前提是必须要使用NSLocalizedString
系列宏,一个一个去替换字符串为这个宏的读取的这个工作还是得自己做的。不过,想想也是符合逻辑的,毕竟哪个字符串要国际化还是得开发者自己确认。通过Find navigator
自己做吧。
如何使用
//指定到en.lproj目录下的Localizable.strings文件,直接覆盖
genstrings -o en.lproj *.swift
//指定到en.lproj目录下的Localizable.strings文件,追加内容
genstrings -a -o en.lproj *.swift
其他参数可以使用man genstrings
命令查看,不再赘述。
这个命令行工具同样有ibtool
的诟病,全量输出。所以,在使用的时候千万小心别覆盖了已经翻译的内容。
看下效果:
另外,这个命令一次只能解析一个文件,简单写了一个递归脚本:
#!/bin/bash
function getdir(){
for element in `ls $1`
do
dir_or_file=$1"/"$element
if [ -d $dir_or_file ]
then
getdir $dir_or_file
else
echo $dir_or_file
suffix="${dir_or_file##*.}"
if [ "$suffix"x = "swift"x ]||[ "$suffix"x = "m"x ]||[ "$suffix"x = "mm"x ];
then
genstrings -a -o en.lproj $dir_or_file
fi
fi
done
}
root_dir="./"
getdir $root_dir
iOS国际化至此结束,如有哪里不清楚,欢迎查看demo,或者留言。