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项目链接

你可能感兴趣的:(iOS-项目快速支持多语言国际化)