iOS 国际化 和 多语言

最近公司的项目要求适配国际化,在过程中遇到挺多有意思的知识点。

多语言大家都知道,字母文字有显著的形状差别。

但国际化并不仅仅只包含多语言,它还有不同语言的表达方式和使用习惯的差异。

一般大家把多语言叫做Multilingualization,即M17N。
国际化叫做Internationalization,即i18n。

测试环境

Xcode: 10.1

语言的特性和差异

学过英语的,应该都知道中文和英文在文字、语法、特定表达有很大区别。

对于我们不了解的其它语种,也存在许多差异化的地方。

一、语序、语义和语法

拿英文来说,同一个单词可以有2种表达意思。

对于当「天」使用的情况,「5 day」和「5天」的表达习惯是一致的。

但如果是当「第X天」使用的情况,「Day 5」和「天5」就无法匹配了。

所以这里需要考虑用2个 key。英文写成「Day %ld」和 中文写成「第%ld天」。

二、大小写

对于英文,同一个英文单词会有小写、首字母大写、全大写的显示要求。

不需要使用多个key,否则你的语言包不仅大,而且管理有会变混乱。

应该考虑统一使用小写,使用系统提供的 String 库的功能来处理。

[@"test" capitalizedString];
[@"test" uppercaseString];

三、单复数

中文使用者没有这种困扰。但英文使用者有1和其它有单复数差别,其它国家可能单复数有1、2、10的倍数等表达差别。

被总结出来即:zero、one、two、few、many、other,详情看文章末尾的 Language Plural Rules 参考文档。

四、数字

分隔位的不同,标点字符使用的不同,都是数字表达的差异性。例如:

中文:1000.01

英文:1,000.01

德语: 1.000,01

印地语:१,०००.०१

五、日期

日期年月日表达顺序和分隔符,也是一种常见的差异点。例如:

中文:2019年1月1日

英文:January 1, 2019

印地语:१ जनवरी २०१९

六、日历

iOS 系统提供了3种日历。如果 app 内有需要处理日历相关的需求,那么这里需要做一些适配工作。

对于中文使用者:

公历:2019年1月1日

日本语:平成31年1月1日

佛历:佛历2562年1月1日

calendar.png

七、RTL

对于如阿拉伯语,它的阅读方向和大部分国家相反。

我们是 Left-to-Right(从左到右),它们是 Right-to-Left(从右到左),常说的 RTL 适配就是说的这种语言。

做过布局的大家都知道 Masonry 的 left 和 leading、right 和 trailing,区别就在这种语言国家得到体现。对于中国,leading 和 trailing 完全等同于 left 和 right,但对于阿拉伯语的使用者来说,leading = right、trailing = left。类似 UICollectionView 的布局,也与我们想象的不同。

中文使用者9宫格顺序

1 2 3

4 5 6

7 8 9

阿拉伯语使用者9宫格顺序

3 2 1

6 5 4

9 8 7

八、排序

不同国家的排序规则也是不同的,重音符号、某些符号的出现都会加大排序权重。

你将你的手机切换在日文,打开通讯录即能看到明显的区别。

九、键盘

不同语言的键盘的不同,可能会导致你的正则匹配失效等。

键盘上的字符并不是固定每个语言都出现,有些语种可能会缺失你常用的语言下的输入按键。

键盘

十、度量衡

千克和磅、摄氏度和华氏度,这些单位的互转可能会导致精度缺失。这也是实际用到需要考虑的问题。

国际化实践(简略,不详细)

一般步骤是:

  1. 选定项目需要支持的语种

  2. 增加语言映射表 Strings File。

  3. 代码中调用

一、管理项目语种

Target - PROJECT - Localizations 就是管理我们程序支持语种的地方。

language.png

二、填写语言映射文件

建立新建一个名叫 Localizable.strings 的 Strings 文件

后期每在 Localizations 增加的语言,都可以在文件右边 inspector 勾选语言适配。

文件里的内容可以为任意格式,左边为key,右边为显示的文字,用分号结尾

"lg_days" = "%ld days";
"key.test" = "i'm value";
"😂" = "cry";
inspector.png

三、用法

无格式化的
NSString *str = NSLocalizedString(@"key.test", @"我是注释");
// print i'm value

带格式化的
NSString *str2 = [NSString localizedStringWithFormat:NSLocalizedString(@"lg_days", nil), 1];
// print 1 day

四、InfoPlist.strings

我们的项目并没有使用到这个。

它可以定义替换掉项目 info.plist 相关的一些内容。比如 app icon、app name 等。但要求是,名字必须是 InfoPlist.strings

但它有个限制,它是跟随系统语言,即你代码修改程序语言AppleLanguages并不起作用。

注意,即使你在 InfoPlist.strings 填了相应的权限文案,info.plist 里还是需要填一次,否则会法上传 ipa 包。

五、用代码修改 app 语言

NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setValue:@[@"en"] forKey:@"AppleLanguages"];
[userDefaults synchronize];

value的值与你建的语言文件夹名一致。即 .lproj 前的名称。

name.png

这样就可以修改 app 内语言了。但是它只能在下一次启动时生效,所以我们需要 hook 代码,修改它。详情请见掘金 - iOS开发之APP内部切换语言

六、storyboard 和 xib 国际化

apple 支持做国际化,很容易,请自行找文档和blog。

但是 Launch Screen.storyboard 启动屏即是不支持的。官方文档(Human Interface Guidelines - Launch Screen)里说

Avoid including text on your launch screen. Because launch screens are static, any displayed text won’t be localized.
launchScreen.png

如果一定要做:

plan B 就是用 InfoPlist.strings,缺点也如刚刚描述一样,只能跟随系统语言。

plan C 就是自己代码实现启动屏

当然砍掉这个需求是最棒的!

NSLocale

幸运的是在 iOS 系统里,NSLocale 就是专门为语言区域性差异处理而存在的。

时间和日期

=== 初始化
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"zh"];
    
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.dateStyle = NSDateFormatterLongStyle;
fmt.locale = locale;

NSLog(@"%@", [fmt stringFromDate:[NSDate date]]);

=== 当 app 语言变更时,需要更新 locale
fmt.locale = newLanguageLocale;

=== 如果使用的是 dateFormatFromTemplate 初始化
=== 那么语言更新时,要刷新 dateFormat 和 locale
fmt.dateFormat = [NSDateFormatter dateFormatFromTemplate:@"dMMM" options:0 locale:self.locale];
fmt.locale = newLanguageLocale;
// 输出中文1月1日,英文Apr 1

数字

NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"de"];

NSNumberFormatter *fmt = [[NSNumberFormatter alloc] init];
fmt.numberStyle = NSNumberFormatterDecimalStyle;
fmt.locale = locale;

NSLog(@"%@", [fmt stringFromNumber:@(1000.01)]);

单复数处理(Strings File 和 Stringsdict File)

Stringsdict 是专门用来处理单复数的。

比如我在 Strings File 里定义了一个 key

英文
"lg_days" = "%ld days"

中文
"lg_days" = "%ld天"

对于英文使用者,有「1 day」 和「2 days」的差别。

这里有2种处理方式

你可以把 "%ld days" 写成 %ld day(s)

或者使用 Stringsdict

将 Stringsdict 的文件名命名与 Strings 一致。在英文和中文的 Stringsdict 填写如下

能发现,英文需要处理 zero、one、other 的情况,中文只需要处理 zero、other 的情况。

不过,如果和 Android 一起开发共同管理 key,可能需要把 zero 单独拿出来不在此处处理。因为安卓系统将英语使用者的 zero 无效化,不予处理,即安卓只会处理 one 和 other。

英文

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>lg_days</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@VARIABLE@</string>
        <key>VARIABLE</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>ld</string>
            <key>zero</key>
            <string>none</string>
            <key>one</key>
            <string>1 day</string>
            <key>other</key>
            <string>%ld days</string>
        </dict>
    </dict>
</dict>
</plist>

中文

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>lg_days</key>
    <dict>
        <key>NSStringLocalizedFormatKey</key>
        <string>%#@VARIABLE@</string>
        <key>VARIABLE</key>
        <dict>
            <key>NSStringFormatSpecTypeKey</key>
            <string>NSStringPluralRuleType</string>
            <key>NSStringFormatValueTypeKey</key>
            <string>ld</string>
            <key>zero</key>
            <string>当天</string>
            <key>other</key>
            <string>%ld天</string>
        </dict>
    </dict>
</dict>
</plist>

在 Xcode 里打开即是这样

stringsdict.png

1 即为 strings 文件里的 key

2 VARIABLE 可以换名,自定义需要替换的字符,格式为 %#@自定义key@

3 即2的定义的那个key

4 层级和 lg_days 相同,是另外一个 key

stringsdict的一个系统坑

NSString *str1 = [NSString localizedStringWithFormat:NSLocalizedString(@"lg_days", nil), 1];
NSString *str2 = [NSString localizedStringWithFormat:NSLocalizedString(@"lg_days", nil), 2];
NSLog(@"%@, %@", str1, str2);

上面这段,我的目标输出是 1 day 和 2 days。

但这里会遇到一个坑。

用模拟器(英文)测试发现完全没问题,但是真机使用的中文系统测试却得到 1 days。

最后发现原来 stringsdict 是和 NSLocale currentLocale 联动的,并不是AppleLanguages。因为中文只处理zero 和 other,所以对于使用中文系统的人来说 1 走 other 流程。

我们需要对 NSString 使用 locale,使用你 app 内语言相关的 locale。 例如:

NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en"];

NSString *format = NSLocalizedString(@"lg_days", nil);
NSString s = [[NSString alloc] initWithFormat:format locale:locale, 1];

实战优化

一、缓存Formatter

无论 NSDateFormatter 还是 NSNumberFormatter 都是特别损耗性能的,Apple 自己也建议使用单例来管理缓存对象。

cache.png

二、UI 刷新问题

当语言变化时,收到通知的非活跃的 UI 最好做延迟刷新。

一是为了避免语言切换时,大数量的 cpu 暴涨。二是存在刷新先后顺序的冲突。

比如,你的一个页面用到了缓存的 NSDateFormatter,你必须要保证 NSDateFormatter 这个实例是最先刷新,拿到的 string 才是对应语言的。所以非存活页面的 UI 不能抢刷新。

测试工具

Xcode 提供了一些国际化测试的选项。

Double-Length Pseudolanguage 是把你的国际化文字加长到2倍,以测试布局是否还正确。
Right-to-Left Pseudolanguage 就是类似阿拉伯语使用者的左右相反
Accented Pseudolanguage 是带音调的语言,如果固定高度的 label 可能会出现显示不全的问题。
Bounded String Pseudolanguage 是给所有的语言加个中括号
Right-to-Left Pseudolanguage with Right-to-Left String 是在第2种情况下文字也反向。
testXcode.png

参考

ObjC 中国 - 字符串本地化

Apple Human Interface Guidelines - Launch Screen

UNICODE LOCALE DATA MARKUP LANGUAGE(日期国际化)

Language Plural Rules(语言单复数)

Apple - Stringsdict File Format

格式化显示日期/时间的一点总结

Why locale affects plural rules in iOS 9?

掘金 - iOS语言国际化/本地化-实践总结

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

推荐阅读更多精彩内容