最近公司的项目要求适配国际化,在过程中遇到挺多有意思的知识点。
多语言大家都知道,字母文字有显著的形状差别。
但国际化并不仅仅只包含多语言,它还有不同语言的表达方式和使用习惯的差异。
一般大家把多语言叫做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日
七、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
八、排序
不同国家的排序规则也是不同的,重音符号、某些符号的出现都会加大排序权重。
你将你的手机切换在日文,打开通讯录即能看到明显的区别。
九、键盘
不同语言的键盘的不同,可能会导致你的正则匹配失效等。
键盘上的字符并不是固定每个语言都出现,有些语种可能会缺失你常用的语言下的输入按键。
十、度量衡
千克和磅、摄氏度和华氏度,这些单位的互转可能会导致精度缺失。这也是实际用到需要考虑的问题。
国际化实践(简略,不详细)
一般步骤是:
选定项目需要支持的语种
增加语言映射表 Strings File。
代码中调用
一、管理项目语种
Target - PROJECT - Localizations 就是管理我们程序支持语种的地方。
二、填写语言映射文件
建立新建一个名叫 Localizable.strings
的 Strings 文件
后期每在 Localizations 增加的语言,都可以在文件右边 inspector 勾选语言适配。
文件里的内容可以为任意格式,左边为key,右边为显示的文字,用分号结尾
"lg_days" = "%ld days";
"key.test" = "i'm value";
"😂" = "cry";
三、用法
无格式化的
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 前的名称。
这样就可以修改 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.
如果一定要做:
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 里打开即是这样
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 自己也建议使用单例来管理缓存对象。
二、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种情况下文字也反向。
参考
Apple Human Interface Guidelines - Launch Screen
UNICODE LOCALE DATA MARKUP LANGUAGE(日期国际化)
Apple - Stringsdict File Format