前言上周忙于产品的国际化(i18n)。最重要的事情之一是电话号码的国际化(我们使用电话号码作为主要帐户)。电话号码的一个非常重要的部分是区号。上图是我们的产品除了常规的电话号码外,在登录界面前面还有一个区号,代表这个电话号码属于哪个国家和地区。关于区号的概念,可以看维基百科,看这里。可能有人会疑惑,这有什么难的?你想在列表中显示它吗?这种方式存在一些问题。因为它支持多种语言,所以在不同的语言环境下显示的国名是不一样的。比如“China”在简体中文是“中国”,英文是“China”,韩文是“??”?????”每个语言的显示和排序都不一样,如果按照这样的表维护起来工作量太大到不同的国家和语言,估计一般的公司做不到,所以我们把这个工作放在本地。但是iOS已经帮我们完成了一部分工作,我们可以得到一个国家或者当前地区的本地化名称根据国家代码//获取当前localeNSLocale*locale=[NSLocalecurrentLocale];//获取所有国家代码NSArray*countryArray=[NSLocaleISOCountryCodes];for(NSString*countryCodeincountryArray){//获取指定的本地化名称country根据当前区域设置和国家短代码NSString*localName=[localedisplayNameForKey:NSLocaleCountryCodevalue:countryCode];}让我们简单地测试NSArray*countryArray=[NSLocaleISOCountryCodes];NSArray*languageArray=@[@"zh_CN",@"en_US",@"ja_JP"];for(NSString*languegeinlanguageArray){NSLocale*locale=[[NSLocalealloc]initWithLocaleIdentifier:language];for(inti=0;i<5;++i){NSString*countryCode=countryArray[i];NSString*displayName=[localedisplayNameForKey:NSLocaleCountryCodevalue:countryCode];NSLog(@"%@\t%@\t%@",languege,countryCode,displayName);}}结果zh_CNAD安道尔zh_CNAE阿拉伯联合酋长国zh_CNAF阿富汗zh_CNAG安提瓜和巴布达zh_CNAI安圭拉en_USADAndorraen_USAEUnitedArabEmiratesen_USAFAfghanistanen_USAGAntiguaandBarbudaen_USAIAnguillaja_JPADアンドラja_JPAEアラブ首長国連邦ja_JPAFアフガニスタンja_JPAGアンティグア?バープードja_JPAIアンギラ已经介绍了一部分iOS帮我们做的工作,所以另一部分就得自己做。我们需要一个区域列表->区号,但这也很容易在网上大量获取。我也在网上找到了文件的内容。如下(dialingcode.json)[{"name":"阿富汗","dial_code":"+93","code":"AF"},{"name":"阿尔巴尼亚","dial_code":"+355","code":"AL"},...//中间省略...{"name":"VirginIslands,British","dial_code":"+1284","code":"VG"},{"名称":"VirginIslands,U.S.","dial_code":"+1340","code":"VI"}]维护这样一个表非常简单,我们可以将它存储在本地或服务器上(“name”字段其实没必要,只是为了好看)为了研究,先把代码放一边,看看其他产品是怎么做出来的,这是微信的微信问题,还是有很多的,左边是中文环境,排序不对,“A”开头的国家没有排列在一起,右边是法语环境,这些派生的拉丁字母没有正确分类,这是推特,推特还是挺奇怪的中文环境,但是没让微信秒。Facebook呢?他们的工程师比较聪明(懒),根本不支持索引。接下来解决这些问题。代码先恢复一个Modal来表示国家相关information@interfaceMMCountry:NSObject@property(nonatomic,strong)NSString*name;//国家名称(localizedversion)@property(nonatomic,strong)NSString*code;//国家代码@property(nonatomic,strong)NSString*latin;//国名拉丁文(只包含基本拉丁字母)@property(nonatomic,strong)NSString*dial_code;//区号@end然后我们需要从配置文件中读取区号,并以区号为key创建索引NSData*data=[NSDatadataWithContentsOfFile:[[NSBundlemainBundle]pathForResource:@"dialingcode"ofType:@"json"]];NSError*error=nil;NSArray*arrayCode=[NSJSONSerializationJSONObjectWithData:dataoptions:0error:&error];if(error){return;}//读取抓取文件NSMutableDictionary*dicCode=[@{}mutableCopy];for(NSDictionary*iteminarrayCode){MMCountry*c=[MMCountrynew];c.code=item[@"code"];c.dial_code=item[@"dial_code"];[dicCodesetObject:cforKey:c.code];}然后获取这些国家的当地方言名称NSLocale*locale=[NSLocalecurrentLocale];NSArray*countryArray=[NSLocaleISOCountryCodes];NSMutableDictionary*dicCountry=[@{}mutableCopy];for(NSString*countryCodeincountryArray){if(dicCode[countryCode]){MMCountry*c=dicCode[countryCode];//这里就知道c.name=[localedisplayNameForKey:NSLocaleCountryCodevalue:countryCode];if([c.nameisEqualToString:@"Taiwan"]){c.name=@"Taiwan,China";}//拉丁化名字c.latin=[selflatinize:c.name];[dicCountrysetObject:cforKey:c.code];}else{//没有找到的意思配置文件不完整,可以补全NSLog(@"missed%@%@",[localedisplayNameForKey:NSLocaleCountryCodevalue:countryCode],countryCode);}}这里需要注意的是字母的拉丁文化解决了第二个微信的问题。非基本拉丁字母也可以按照基本拉丁字母排序。函数如下-(NSString*)latinize:(NSString*)str{NSMutableString*source=[strmutableCopy];CFStringTransform((__bridgeCFMutableStringRef)source,NULL,kCFStringTransformToLatin,NO);//微信是这样做的;}这里有两个步骤,首先将文本转换为拉丁字母(kCFStringTransformToLatin),然后从拉丁字母中删除变音符号(kCFStringTransformStripDiacritics)。第一步只处理汉字,不处理其他字符,所以第二步没有得到正确的基本拉丁字符(kCFStringTransformMandarinLatin,看注释掉的代码)。让我们测试一下这两个步骤的结果是否会和前面的例子一样。NSArray*countryArray=[NSLocaleISOCountryCodes];NSArray*languageArray=@[@"zh_CN",@"en_US",@"ja_JP"];for(NSString*languegeinlanguageArray){NSLocale*locale=[[NSLocalealloc]initWithLocaleIdentifier:languege];for(inti=0;i<5;++i){NSString*countryCode=countryArray[i];NSString*displayName=[localedisplayNameForKey:NSLocaleCountryCodevalue:countryCode];NSLog(@"%@\t%@\t%@\t@",语言,国家代码,displayName,[selflatinize:displayName]);}}结果zh_CNAD安道尔|andaoerzh_CNAE阿拉伯联合酋长国|alabolianheqiuzhangguozh_CNAF阿富汗|afuhanzh_CNAG安提瓜和巴布达|antiguahebabudazh_CNAI安圭拉|anguilaen_USADAndorra|Andorraen_USAEUnitedArabEmirates|UnitedArabEmiratesen_USAFAfghanistan|Afghanistanen_USAGAntigua&Barbuda|Antigua&Barbudaen_USAIAnguilla|Anguillaja_JPADアンドラ|andoraja_JPAEアラブChiefStateLianbang|arabushouzhangguolianbanja_JPAFアフガニスタン|afuganisutanja_JPAGアンティグア?バーブーダ|antigua?babudaja_JPAIアンギラ|angiraYoucanseethatthesystemwillconvertdifferentexpressionsofthesamecountryintodifferentLatinalphabetsaccordingtothecharacteristicsofdifferentcountriesanddifferentlanguages接下来,我们将获取到的数据按照'A'-'Z'NSMutableDictionary*dicSort=[@{}mutableCopy];for(MMCountry*cindicCountry.allValues){NSString*indexKey=@"";if(c.latin.length>0){indexKey=[[c.latinsubstringToIndex:1]uppercaseString];charc=[indexKeycharacterAtIndex:0];if((c<'A')||(c>'Z')){继续;}}else{continue;}NSMutableArray*array=dicSort[indexKey];if(!array){array=[NSMutableArrayArray];dicSort[indexKey]=array;}[arrayaddObject:c];}***对每个类别下的数据进行排序和重新排列for(NSString*keyindicSort.allKeys){NSArray*array=dicSort[key];array=[arraysortedArrayUsingComparator:^NSComparisonResult(MMCountry*obj1,MMCountry*obj2){return[obj1.namelocalizedStandardCompare:obj2.name];}];//array=[arraysortedArrayUsingComparator:^NSComparisonResult(CSCountry*obj1,CSCountry*obj2){////returnobj1.latin>obj2.latin;//}];dicSort[key]=array;}这样,dicSort就是我们最终得到的结果集。这是微信犯的第二个错误。微信按照拉丁文排序(看注释掉的代码)所以汉字相同的国家不能排在一起。正确的做法是使用localizedStandardCompare来排序。这也是iOS为我们提供的本地化比较功能。看看上一张图,挑出三个国家。比如:AlbaniaAruba,Ireland,他们的拼音是aerbabiyaaierlanaluba如果按照拼音排序,排序是正确的。看看最后的效果是不是比微信的好?讨论代码虽然写好了,问题并没有解决。有一个关键问题就是我们为什么要按照'A'-'Z'进行索引排序呢?比如推特在日韩环境下就是这样。其实根据不同国家的语言特点来索引应该是最好的。解决方法(PS:看到推特在中文环境下的不好结果,不确定日文和韩文的结果是否正确(ˉ﹃ˉ)当然,如果真要这么干,改量也不是很多,只要在索引的部分稍微修改一下即可。总结文章中的demo可以在这里找到。讨论中提到,本文讨论的解决方案不是最终解决方案。如果您需要一个更好的体验,需要深入研究每个国家的文化。所以国际化这不仅仅是一个技术问题,这是一个社会工程~~~~
