iOS国际化(多语言)漫谈

目录

概览
各种资源的国际化

1.文本
2.图片
3.nib
4.其他资源

特定模块/功能的国际化

1.APP图标
2.应用名与权限提示
3.启动图(LaunchScreen)
4.app调系统资源页面的国际化
5.涉及服务端数据内容的国际化

app内更改语言

1.更改语言的方案
2.未做国际化的旧项目迁移


概览

国际化的本质是为每种语言单独提供一份资源(文本,图片,音视频等)。
本文术语
本地化:指单独一种语言
国际化:多种语言的合体

在工程的Localizations中每新增一种语言,xcode会提示我们生成对应的文件,而后也生成了对应的文件夹。


新增本地化语言
新增中文简体后生成的文件夹与文件

两种语言

iOS为这些文件提供了快捷的国际化方案。对于字符串资源文件生成相应语言的字符串文件放在对应的文件夹中,而XIB和StoryBoard则可选整个文件和字符串资源。具体的方案后续讨论。


image.png

如果忘了添加某个资源的具体语言文件,或者后续增加的资源文件,可以通过该资源文件的 文件监察器File Inspector 中的 Localize按钮添加。

image.png

Localizable.stringsInfoPlist.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)

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的两种国际化方式也是无效的。


nib的两种国际化方式

另辟蹊径,利用info.plist的国际化来做LaunchScreen的静态国际化,如下:


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,或者留言。
最后附上:demo地址

参考文章:
Internationalization and Localization Guide
Using the genstrings Tool to Create Strings Files
iOS国际化——通过脚本使storyboard翻译自增
iOS国际化
在iOS App内优雅的动态切换语言

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容