处理i18n国际电话区号的代码实践
http://www.cocoachina.com/ios/20150821/13115.html
作者:里脊串 授权本站转载。
前言
上周在忙产品的国际化(i18n)的问题,其中一个很重要的地方就是电话号码的国际化(我们以电话号码为主账号)。电话号码有个很重要的部分就是区号。
上图是我们产品的登录界面,除了常规的电话号码之外,前面还有一个区号,代表这个电话号码所属的是哪个国家和地区。关于区号的概念,可以看一下维基百科。
看到这里 可能有人奇怪 这有什么难的? 不就是按照列表来展示吗? 这样有几个问题
-
由于是支持多语言 那么不同的语言环境的系统 显示出来的国家名称是不一样的 比如“中国” 简体中文是“中国” 英文是“China” 韩文是“???????” 其在各个语言中的显示排序都是不一样的
-
如果根据不同国家和语言来维护一张这样的表 工作量太大 一般的公司估计做不来
所以这个工作我们就会放到本地来做 不过iOS已经帮我们做了一部分工作了 我们可以根据国家代码来获取某个国家或在当前区域中的本地化名称
1
2
3
4
5
6
7
8
9
|
//获取当前locale NSLocale *locale = [NSLocale currentLocale]; //获取所有国家的代码 NSArray *countryArray = [NSLocale ISOCountryCodes]; for (NSString *countryCode in countryArray) { //根据当前locale和国家短码 获取指定国家的本地化名称 NSString *localName = [locale displayNameForKey:NSLocaleCountryCode value:countryCode]; } |
我们简单测试一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
NSArray *countryArray = [NSLocale ISOCountryCodes]; NSArray *languageArray = @[@ "zh_CN" ,@ "en_US" ,@ "ja_JP" ]; for ( NSString *languege in languageArray) { NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:languege]; for ( int i = 0 ; i < 5 ; ++i ) { NSString *countryCode = countryArray[i]; NSString *displayName = [locale displayNameForKey:NSLocaleCountryCode value:countryCode]; NSLog(@ "%@\t%@\t%@" ,languege,countryCode,displayName); } } |
结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
h_CN AD 安道尔 zh_CN AE 阿拉伯联合酋长国 zh_CN AF 阿富汗 zh_CN AG 安提瓜和巴布达 zh_CN AI 安圭拉 en_US AD Andorra en_US AE United Arab Emirates en_US AF Afghanistan en_US AG Antigua and Barbuda en_US AI Anguilla ja_JP AD アンドラ ja_JP AE アラブ首長国連邦 ja_JP AF アフガニスタン ja_JP AG アンティグア?バーブーダ ja_JP AI アンギラ |
已经介绍完iOS帮我们做的一部分工作了 那么另一部分就得我们自己来了
我们需要有一张 地区->区号 的列表 不过这个也简单 网上一抓一大把 我也是网上找的 文件内容如下(diallingcode.json)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
[ { "name" : "Afghanistan" , "dial_code" : "+93" , "code" : "AF" }, { "name" : "Albania" , "dial_code" : "+355" , "code" : "AL" }, ... ... //中间省略 ... ... { "name" : "Virgin Islands, British" , "dial_code" : "+1 284" , "code" : "VG" }, { "name" : "Virgin Islands, U.S." , "dial_code" : "+1 340" , "code" : "VI" } ] |
维护这样一张表就很简单了我们可以存在本地 也可以放在服务器(“name”字段其实是不必须的 只是为了好看)
研究
我们暂时先把代码放一放 来看一看其他产品是怎么做的
这个是微信的
微信的问题还是挺多的
-
左边是中文环境 按拼音分组是分对了 但是文字排序却粗错了 “阿”开头的国家并没有排列在一起
-
右边是法语环境 这些衍生拉丁字母 并没有正确的归类
这个是Twitter的
Twitter在中文环境下还是挺奇怪的 但是却没有犯微信第二个错误
Facebook的呢? 人家的工程师比较聪明(懒) 压根就不支持索引
接下来我们会解决出现的这几个问题
代码
先简历一个Modal 用来表示国家相关的信息
1
2
3
4
5
6
|
@interface MMCountry : NSObject @property (nonatomic, strong) NSString *name; //国家名(本地化后的版本) @property (nonatomic, strong) NSString *code; //国家代号 @property (nonatomic, strong) NSString *latin; //国家名的拉丁文(只包含基本拉丁字母) @property (nonatomic, strong) NSString *dial_code; //区号 @end |
然后我们要把区号从配置文件中读取出来 并以区号为key 建立索引
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
NSData *data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@ "diallingcode" ofType:@ "json" ]]; NSError *error = nil; NSArray *arrayCode = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if ( error ) { return ; } //读取文件 NSMutableDictionary *dicCode = [@{} mutableCopy]; for ( NSDictionary *item in arrayCode ) { MMCountry *c = [MMCountry new ]; c.code = item[@ "code" ]; c.dial_code = item[@ "dial_code" ]; [dicCode setObject:c forKey:c.code]; } |
接着获取这些国家的本地话名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
NSLocale *locale = [NSLocale currentLocale]; NSArray *countryArray = [NSLocale ISOCountryCodes]; NSMutableDictionary *dicCountry = [@{} mutableCopy]; for (NSString *countryCode in countryArray) { if ( dicCode[countryCode] ) { MMCountry *c = dicCode[countryCode]; //这里 你懂的 c.name = [locale displayNameForKey:NSLocaleCountryCode value:countryCode]; if ( [c.name isEqualToString:@ "台湾" ] ) { c.name = @ "中国台湾" ; } //把名称拉丁字母化 c.latin = [self latinize:c.name]; [dicCountry setObject:c forKey:c.code]; } else { //找不到则说明配置文件不全 可以补全 NSLog(@ "missed %@ %@" ,[locale displayNameForKey:NSLocaleCountryCode value:countryCode],countryCode); } } |
这里要注意的是 把字母拉丁文化 解决了微信的第二个问题 使非基本拉丁字母也可以按照基本拉丁字母来排序 其函数如下
1
2
3
4
5
6
7
8
9
10
11
12
13
|
- (NSString*)latinize:(NSString*)str { NSMutableString *source = [str mutableCopy]; CFStringTransform((__bridge CFMutableStringRef)source, NULL, kCFStringTransformToLatin, NO); //微信是这样做的 //CFStringTransform((__bridge CFMutableStringRef)source, NULL, kCFStringTransformMandarinLatin, NO); CFStringTransform((__bridge CFMutableStringRef)source, NULL, kCFStringTransformStripDiacritics, NO); return source; } |
这里有两步
-
先将文字 转成拉丁字母(kCFStringTransformToLatin)
-
再将拉丁字母去掉变音符(kCFStringTransformStripDiacritics)
这里是微信犯的第一个错误 也就是没有正确归类的错误 因为微信在第一步的时候只针对汉字进行了处理 其他字符则没有处理 导致第二步没有得到正确的基本拉丁字符(kCFStringTransformMandarinLatin 参见注释掉的代码)
我们来测试一下这两步会造成得到效果 还是之前的例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
NSArray *countryArray = [NSLocale ISOCountryCodes]; NSArray *languageArray = @[@ "zh_CN" ,@ "en_US" ,@ "ja_JP" ]; for ( NSString *languege in languageArray) { NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:languege]; for ( int i = 0 ; i < 5 ; ++i ) { NSString *countryCode = countryArray[i]; NSString *displayName = [locale displayNameForKey:NSLocaleCountryCode value:countryCode]; NSLog(@ "%@\t%@\t%@\t@" ,languege,countryCode,displayName,[self latinize:displayName]); } } |
结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
zh_CN AD 安道尔 | an dao er zh_CN AE 阿拉伯联合酋长国 | a la bo lian he qiu zhang guo zh_CN AF 阿富汗 | a fu han zh_CN AG 安提瓜和巴布达 | an ti gua he ba bu da zh_CN AI 安圭拉 | an gui la en_US AD Andorra | Andorra en_US AE United Arab Emirates | United Arab Emirates en_US AF Afghanistan | Afghanistan en_US AG Antigua & Barbuda | Antigua & Barbuda en_US AI Anguilla | Anguilla ja_JP AD アンドラ | andora ja_JP AE アラブ首長国連邦 | arabu shou zhang guo lian ban ja_JP AF アフガニスタン | afuganisutan ja_JP AG アンティグア?バーブーダ | antigua?babuda ja_JP AI アンギラ | angira |
可以到看 系统会根据不同国家和不同语言的特点 将同一个国家的不同表达形式转化成不同的拉丁字母
接下来 我们把获取过的数据根据’A’-‘Z’进行归类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
NSMutableDictionary *dicSort = [@{} mutableCopy]; for ( MMCountry *c in dicCountry.allValues ) { NSString *indexKey = @ "" ; if ( c.latin.length > 0 ) { indexKey = [[c.latin substringToIndex:1] uppercaseString]; char c = [indexKey characterAtIndex:0]; if ( ( c < 'A' ) || ( c > 'Z' ) ) { continue ; } } else { continue ; } NSMutableArray *array = dicSort[indexKey]; if ( !array ) { array = [NSMutableArray array]; dicSort[indexKey] = array; } [array addObject:c]; } |
最后 将每个归类下面的数据 排序重新整理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
for ( NSString *key in dicSort.allKeys ) { NSArray *array = dicSort[key]; array = [array sortedArrayUsingComparator:^NSComparisonResult(MMCountry *obj1, MMCountry *obj2) { return [obj1.name localizedStandardCompare:obj2.name]; }]; // array = [array sortedArrayUsingComparator:^NSComparisonResult(CSCountry *obj1, CSCountry *obj2) { // // return obj1.latin > obj2.latin; // }]; dicSort[key] = array; } |
这样dicSort就是我们最终得到的结果集
这里是微信犯的第二个错误 微信的排序是按照latin来排序的(见注释掉的代码) 所以导致了相同汉字的国家排不到一起的情况 正确的方式是用localizedStandardCompare来排序 这也是iOS已为我们提供好了的本地化比较函数
看看之前的图中 挑三个国家出来 比如:阿尔巴尼亚 爱尔兰 阿鲁巴 他们的拼音是 aerbabiya aierlan aluba 如果按照拼音排序的话 这样的排序就是正确的
我们来看看最终的效果
是不是比微信的更好?
讨论
虽然代码是写完了 但是问题并没有结果 有一个关键的问题就是 为什么我们要按照’A’-‘Z’来索引排序呢? 比如Twitter在日文和韩文环境下是这样的
其实按照不同国家的语言特点来进行对应的索引 应该才是最优的解决办法(PS:看到Twitter在中文环境下的糟糕结果 我也不确定其在日文和韩文下的结果是否是正确的(ˉ﹃ˉ)
当然 如果真要这样做 其实改动量也不大 只要在索引的那块稍微修改一下就行了
小结
文中的demo可以在这里找到
正如讨论中说的一样 本文所讨论的方案 并不是最终的解决方案 如果需要更好的体验的话 还要深入研究各国的文化才行 所以 国际化并不单纯是个技术问题 更是个社会工程啊~~~~