iOS-项目快速支持多语言国际化

前言场景

已存在的项目(中文)突然要支持多语言切换或者国际化了。前段时间突然遇到这么一个比较急的需求。在经历了半脚本半人工体力活的加班修改之后(眼睛都快花了),匆匆完成,内心对结果并不满意,其中还是有着一些重复的,不必要的翻译,最后再去查漏补缺去重。后面事情告一段落。静下心来,就再研究了下方案的优化,把纯体力活尽量干掉。

整理主要步骤

  1. 获取汉字及相关页面文件
  2. strings文案生成及中文中转映射key
  3. 项目代码中文添加本地化方法
  4. xib/storyBoard的检查处理
  5. 检查验证,查看实际效果

简单模拟项目demo准备

  1. 添加设置切换多语言以便查看效果。
  2. 预先增加两个已国际化的汉字
  3. 不同页面同文案
  4. 添加中文注释、断言、log输出、特殊字符换行等场景
  5. xib内包含中文文案
    运行效果如下图

具体流程

1. 获取汉字及相关页面文件

汉字的获取整理是第一步,这一块我们可以用脚本去处理,但是过程中需要注意的有一些不必要处理的要过滤掉。

  • 注释、log输出、断言等
  • 不需要处理的文件包括特定页面、已处理过的页面等。
  • 已经添加国际化的中文。
  • 去重,仅针对翻译,后续替换时需要替换所有。

这里用的是python脚本,主要作用是获取当前文件夹下特定文件下的中文信息。后面完整项目里有,部分代码如下。
整行的过滤部分如下。str为逐行读取的内容。这里过滤条件可视项目情况调整。

                    # log assert类型 忽略
                    if  str.startswith("//") or str.startswith("DYYLog") or str.startswith("NSLog") or str.startswith("print") or str.startswith("NSAssert") or str.startswith("assert"):
                        continue
                    if str.startswith("/*"):
                        isComment = True
                    if str.endswith("*/"):
                        isComment = False
                    if isComment:
                        continue

具体汉字匹配后,已本地化的也需要过滤掉。可视项目情况调整。

                    # 匹配包含中文
                    matchObjs = re.findall(u'"[^"]*[\u4E00-\u9FA5]+[^"\n]*?"', str, re.M|re.S)
                    if matchObjs and len(matchObjs) > 0:
                        for cnStr in matchObjs:
                            # 已本地化则忽略
                            locali1 = "JJLocalized(" + cnStr
                            locali2 = "JJLocalized(@" + cnStr
                            locali3 = cnStr + ".localizedString"
                            if locali1 in str or locali2 in str or locali3 in str:
                                continue

直接运行,可以看到xib、swift、m文件里的都有过滤出来。

运行效果

同时同文件夹下生成了三个文件。

  • 第一个用于接下来的第二步
  • 第二个文件记录需要处理的xib及storyboard
  • 第三个内容是汉字对应的文件完整路径,后续会用到

第一步完成

2. strings文案生成及中文中转映射key

py_cnStr.txt加入项目中。其内容如下,--*--仅作为分隔符。接下来基于以下内容生成strings内容。

ViewController1.swift--*--"晚上好"
ViewController.m--*--"晚上好"
ViewController1.swift--*--"早安"
SettingVC.swift--*--"切换英文"
SettingVC.swift--*--"切换中文"
ViewController.m--*--"早上好"
ViewController1.swift--*--"“特殊字符”%@%d个"
ViewController.m--*--"中午好"
AppDelegate.m--*--"测试2-(Swift)"
ViewController1.swift--*--"有"
ViewController1.swift--*--"测试\n换行"
AppDelegate.m--*--"测试1-(OC)"
ViewController1.xib--*--"xib标题"
ViewController1.swift--*--"晚安"

本着stringskey尽量不使用中文的原则,我们需要中文key做一次格式化处理(后续新增文案应规范命名)。如下:

/// 检测并生成本地化文案
- (void)generationLocalizationStr {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"py_cnStr" ofType:@"txt"];
    NSString *str = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
    NSArray *arr = [str componentsSeparatedByString:@"\n"];
    int num2 = 0;
    NSString *docPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, true) objectAtIndex:0];
    NSString *enStringsPath = [docPath stringByAppendingPathComponent:@"waitingTranslation_en.strings"];
    NSString *cnStringsPath = [docPath stringByAppendingPathComponent:@"waitingTranslation_cn.strings"];
    // 英文 strings
    NSMutableArray *enStrArray = [@[] mutableCopy];
    // 中文 strings
    NSMutableArray *cnStrArray = [@[] mutableCopy];
    NSMutableArray *transStrArray = [@[] mutableCopy];
    // 包含中文字符串正则
    NSString *regex = @"[^\"]*[\u4E00-\u9FA5]+[^\"\n]*?";
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", regex];
    NSMutableDictionary *numDic = [NSMutableDictionary new];
    
    NSMutableDictionary * keyValue = [@{} mutableCopy];
    NSMutableSet *set = [NSMutableSet new];
    for (NSString *lineStr in arr) {
        NSArray *array = [lineStr componentsSeparatedByString:@"--*--"];
        NSString *originStr = array.lastObject;
        if (originStr.length < 2) {
            continue;
        }
        NSString *txt = [originStr substringWithRange:NSMakeRange(1, originStr.length-2)];
        // 用于检测是否已经有对应翻译了
        NSString *localizedTxt = [txt stringByReplacingOccurrencesOfString:@"\\n" withString:@"\n"];
        localizedTxt = [localizedTxt stringByReplacingOccurrencesOfString:@"\\\\" withString:@"\\"];
        localizedTxt = [localizedTxt stringByReplacingOccurrencesOfString:@"\\\"" withString:@"\""];
        if ([set containsObject:localizedTxt]) {
            continue;
        }
        NSString *tran = localizedTxt.localizedString;
        BOOL hasCN = [predicate evaluateWithObject:tran];
        NSString *fileName = [array.firstObject stringByDeletingPathExtension];
        NSString *hashCode = [NSString stringWithFormat:@"%lu",fileName.hash];
        hashCode = [hashCode substringToIndex:6];
        NSNumber *num = numDic[fileName];
        if (!num) {
            num = @0;
        }
        int num1 = [num intValue];
        NSString *classPre = fileName;
        classPre = [classPre stringByReplacingOccurrencesOfString:@"ViewController" withString:@"VC"];
        classPre = [classPre stringByReplacingOccurrencesOfString:@"Controller" withString:@"C"];
        classPre = [classPre stringByReplacingOccurrencesOfString:@"View" withString:@"V"];
        NSString *key = [NSString stringWithFormat:@"%@_%@_%d",classPre,hashCode,num1];
        if (hasCN) {
            NSLog(@"-->未多语言化%d:%@",num1,originStr);
            [enStrArray addObject:[NSString stringWithFormat:@"\"%@\" = \"\";//%@\n",key,txt]];
            [cnStrArray addObject:[NSString stringWithFormat:@"\"%@\" = \"%@\";\n",key,txt]];
            num1++;
            numDic[fileName] = @(num1);
            keyValue[key] = txt;
            [set addObject:txt];
        } else {
            NSLog(@"-->已多语言化%d:%@-%@",num2,originStr,tran);
            num2++;
        }
    }
    [enStrArray sortUsingSelector:@selector(compare:)];
    [cnStrArray sortUsingSelector:@selector(compare:)];
    [transStrArray sortUsingSelector:@selector(compare:)];
    NSString *enStr = [enStrArray componentsJoinedByString:@""];
    NSString *cnStr = [cnStrArray componentsJoinedByString:@""];
    [self writeStr:enStr toPath:enStringsPath];
    [self writeStr:cnStr toPath:cnStringsPath];
    NSString *keyValuePath = [docPath stringByAppendingPathComponent:@"keyValue.txt"];
    NSArray *sortArray = [keyValue.allKeys sortedArrayUsingSelector:@selector(compare:)];
    NSMutableString *keyValueStr = [@"" mutableCopy];
    for (NSString *key in sortArray) {
        [keyValueStr appendFormat:@"\"%@\": \"%@\",\n", keyValue[key],key];
// OC
//      [keyValueStr appendFormat:@"@\"%@\": @\"%@\",\n", keyValue[key],key];
    }
    [self writeStr:keyValueStr toPath:keyValuePath];
}

这里key的规则为文件名(缩减)+文件名hashCode(前6位)+数字组成。同时对其中汉字去重并过滤掉已国际化的场景,生成strings的过程中,先进行了一次排序,内容会更整齐有序。放在AppDelegate中调用,运行项目,控制台输出未/已国际化的信息同时在沙盒生成三个文件,将其中中文strings文件内容拷入对应Localizable.strings。英文strings如图,可以丢给专业人士填充翻译后再拷入对应英文strings,检查下特殊字符"添加转义。keyValue.txt内容拷入LanguageManager.swift中作为中转字典。如图,第二步完成。


3. 项目代码中文添加本地化方法

对应关系都好了,就剩下代码处的调用了。这里也是脚本添加一步到位,唯一需要注意的是整文件查找替换时已添加国际化的中文办法区分,所以先替换,再替换还原多替换的场景。代码不多,如下:

#-*- coding:utf-8-*-
#处理中文字符的情况
import sys
reload(sys)
sys.setdefaultencoding('utf-8')
 
import os
import codecs
 
project_path = os.path.split(os.path.realpath(__file__))[0]

def logYellow(str):
    print("\033[36m%s\033[0m"%(str))

def updateFile(file,old_str):
    logYellow(file)
    with open(file,"r") as f:
        file_data = f.read()
        f.close
    new_str = old_str + ".localizedString"
    # 替换
    new_file_data = file_data.replace(old_str,new_str)
    # 已处理场景需还原
    new_file_data = new_file_data.replace(new_str + ".localizedString",new_str)
    new_file_data = new_file_data.replace("JJLocalized(%s)"%(new_str),"JJLocalized(%s)"%(old_str))
    new_file_data = new_file_data.replace("JJLocalized(@%s)"%(new_str),"JJLocalized(@%s)"%(old_str))
    
    with open(file,"w") as f:
        f.write(new_file_data)
        f.close
    logYellow("已更新" + old_str)

separatorStr = "--*--"
with open(os.path.join(project_path, "py_cn_wholePath.txt"), 'r+') as f:
    lineList = f.readlines()
    f.close()
    for str in lineList:
        str = str.decode()
        str = str.strip()
        path_info = str.split(separatorStr)
        cnStr = path_info[1]
        updateFile(path_info[0],cnStr)

执行脚本,第三步完成。

4. xib/storyBoard的检查处理

如果是纯代码的项目,那这一步可以跳过了。打开前面生成的文件py_xibCnStr.xlsx,这里只能去挨个检查xib了,如果xib中控件标题都已经是在代码中设置过的,可以把其中文文案删掉或者替换成其它值。在需要保持中文以便更好理解布局的情况下,也可以把该xib文件名加入到第一步脚本中的ignoreFileNames中,没有连线控件的手动拉线设置文案(又是体力活,这里只能祈祷这种场景比较少了)。

5. 检查验证,查看实际效果

前面四步完成后,可以重新运行下第一次的脚本看看未处理的中文是不是清空了。自测下就可以交给测试了,大功告成。

总结

总结下来,这方案执行下来简单步骤就是,执行脚本->拷贝+翻译->执行脚本->检查xib/storyBoard->完成。在没有或较少xib/storyBoard文案的情况下,开发这边的工作小半天基本就足够了。

链接

完整Demo项目链接

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

推荐阅读更多精彩内容

  • 最近公司的项目要求适配国际化,在过程中遇到挺多有意思的知识点。 多语言大家都知道,字母文字有显著的形状差别。 但国...
    PengElement阅读 6,528评论 0 6
  • 目录 概览 各种资源的国际化 1.文本2.图片3.nib4.其他资源 特定模块/功能的国际化 1.APP图标2.应...
    十拿九稳啦阅读 3,557评论 0 6
  • 开发一款国际化的iOS App,则必须考虑支持多国家语言,如何实现呢? 第一、国际化——多国家语言;第二、本土化—...
    John_LS阅读 3,575评论 0 7
  • 前言 iOS的国际化,即多语言的实现,主要有两种: 跟随系统语言的自动切换显示的语言 手动设置语言,由用户选择,可...
    Fxxxxxxx阅读 12,236评论 1 16
  • 根据当前设备语言自动切换显示。 几个涉及到多语言本地化设置的: 1.应用名称 2.文字 3.图片、素材 4.Sto...
    齐玉婷阅读 3,480评论 2 3