前言场景
已存在的项目(中文)突然要支持多语言切换或者国际化了。前段时间突然遇到这么一个比较急的需求。在经历了半脚本半人工体力活的加班修改之后(眼睛都快花了),匆匆完成,内心对结果并不满意,其中还是有着一些重复的,不必要的翻译,最后再去查漏补缺去重。后面事情告一段落。静下心来,就再研究了下方案的优化,把纯体力活尽量干掉。
整理主要步骤
- 获取汉字及相关页面文件
- strings文案生成及中文中转映射key
- 项目代码中文添加本地化方法
- xib/storyBoard的检查处理
- 检查验证,查看实际效果
简单模拟项目demo准备
- 添加设置切换多语言以便查看效果。
- 预先增加两个已国际化的汉字
- 不同页面同文案
- 添加中文注释、断言、log输出、特殊字符换行等场景
- 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--*--"晚安"
本着strings
内key
尽量不使用中文的原则,我们需要中文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项目链接