Android脚本插件系列(一):安卓国际化多语自动合入脚本

android-scripts 把平时碰到和Android有关的手动操作、人工检查等写成脚本或插件,提高效率和准确度。
Android脚本插件系列(一):安卓国际化多语自动合入脚本
Android脚本插件系列(二):自动打包安装并语音提示脚本


背景

如今越来越多的IT公司都在走国际化,相应的APP也支持国际化。应用国际化首先就是多语言的支持,Android开发支持多语操作比较简洁,在AndroidStudio中分别为不同语言建立相应的values文件夹即可并在其中创建strings.xml(如values、values-ar、values-zh-rCN)。

在这里强调一下values、values-ar这两个文件夹。其中values里的strings.xml是默认使用的文案,在当前手机语言的strings.xml找不到的时候会使用values里的文案,所以一定别忘在values里添加默认的文案,一般添加英文的。values-ar为阿拉伯语言,是一种RTL语言(从右向左)。

一般合入多语的流程是这样的,首先先整理出一份英文的所有的文案,并将该文案写入values文件夹下的strings.xml。然后将该文案交给翻译公司翻译,待翻译的多语返回之后手动把文案写入相应的strings.xml。

碰到的问题

  1. 每次发版都要手动合入22种语言的文案,操作僵硬,效率低下。

  2. 不同语言返回的文案很有可能出现未转义的特殊字符(例如法语的j'aime),导致编译失败,从而要手动一个一个去加转义符。

  3. 对于%在某些情况下会出现不易察觉的问题,如之前向翻译公司提了一条文案为Save %s%%,是”节省百分之几的意思”(Save 50%Save 66%等等)。%s是占位符,%是特殊字符且用反斜杠转义无效,要用%来转义。然而翻译公司在返回多语时,将%%都返回为%(如英文的Save %s%、中文的节省%s%、阿拉伯语的حفظ %s%)。

    手动合入多语之后编译是可以通过的,如果合入者不仔细看的话会忽略该问题,当代码运行到使用该多语的对方会Crash抛出UnknownFromatConversionException异常。合入者没有转义%的情况还是较容易发现的,因为在不同语言下只要代码运行相应对方必现Crash。

    下面详细分析一下合入者在转义了该%从而产生的问题,之前的合入者合入多语后,在lint的帮助下发现了返回的文案中%没有转义。于是手动添加%进行转义,大功告成提交代码,手动测试没有Crash,之后上线进行灰度。一灰的时候发现线上有一个Crash特别高,一看是UnknownFromatConversionException而且报的就是Save %s%%这个文案的地方,emmmm这个文案之前合入的时候已经手动把%转义了,不应该抛这个异常的。

    看来是合入者在转义的时候转错了,排查的时候发现出现Crash的语言都是ar。ar中的文案为حفظ %s%%乍一看好像没什么问题,按照从左往右看是没什么毛病的。不过别忘阿拉伯语是从右往左的,现在在来看看右边两个%%转义之后就是%,占位符的%就没了所以阿拉伯语中使用该文案会crash,正确的转义写法是حفظ %%s%

脚本能做的事

针对以上问题编写了一个脚本,主要功能如下

  1. 自动合入多语,提高效率
  2. 对返回的文案进行单双引号正确性检测,并自动纠正
  3. 对返回的文案进行%正确性检测,发现隐患并自动纠正

效果

因为要输入项目地址、多语返回地址等,所以采用界面化的形式。
选择完项目地址、多语返回地址,输入本次合入注释之后,点击开始合入。

Android脚本插件系列(一):安卓国际化多语自动合入脚本_第1张图片

Android脚本插件系列(一):安卓国际化多语自动合入脚本_第2张图片

Android脚本插件系列(一):安卓国际化多语自动合入脚本_第3张图片


实现

自动合入

自动合入最关键的就是映射关系,附上我的映射关系,这里大家可以根据实际情况映射。

LANGUANGE_MAPPED_1 = {    "en": "values/",    "ar-eg": "values-ar/",    "de-de": "values-de/",    "es-xl": "values-es/",    "es-us": "values-es-rUS/",    ... // 剩略其他语言,保持博客的整洁性}

映射关系确定好了,接着就是递归遍历所有文件,然后提取文件内的多语,经过处理后再写入相应values中的strings.xml中。文案被脚本自动纠,会输出相应日志(旧文案和纠正后的文案)供使用者确认。正对返回多语文案的文件格式没有要求,只要保证具体的单条多语是 xxxxx 即可。

同时考虑到多语公司返回文案有可能格式不太规范,脚本会使用放宽条件的正则匹配寻找返回多语中的文案条数,然后和合入的文案条数进行对比,不符则输出提示

代码注释比较完善,具体代码如下

def mergeSingleStrings(pathIn, pathOut, annotation):    '''    合并单个多语言    pathIn: 返回多语具体语言的地址 如/Users/sunzhaojie/Desktop/多语返回/ar-eg/string.xml    pathOut: 项目中对应strings.xml的地址如/Users/sunzhaojie/WorkSapce/AndroiStudio/baidu/demo/app/src/main/res/values-ar/strings.xml    annotation: 合入多语的注释    '''    # 自动添加开始处的注释    strs = '\n    \n'    f = open(pathIn)    # 读取待合入文案的所有内容    lines = f.readlines()    # 记录合入的文案条数    count = 0    for line in lines:        # 过滤头尾空白字符        line = line.strip()        lineTemp = line        # 通过正则匹配注释,是注释直接加入        if re.match(r'^\s*\s*$', lineTemp):            strs = strs + '    ' + line + '\n'            continue        # 通过正则提取具体的文案(...)        lineTemps = re.split(            r'^\s*<\s*string\s+name\s*=\s*\".+\"\s*>|\s*$', lineTemp)        # 不是文案,继续下面一行        if len(lineTemps) != 3:            continue        # 是文案,提取文案内容        contet = lineTemps[1]        # 对文案内容进行特殊字符格式检查并自动纠正,目前包括%、和单双引号        lineTemp = lineTemp.replace(contet, checkSpecialChars(contet))        # 文案特殊字符使用错误进行自动纠正之后,输出原文和纠正之后的文案,方便用户进行判断        if line != lineTemp:            print '#########################注意 START #########################'            print '#########################返回文案是[%s],格式检查自动纠正为[%s],请确认!' % (line, lineTemp)            print '#########################注意  END  #########################'        # 每行前面添加一个TAB(自动格式化)        strs = strs + '    ' + lineTemp + '\n'        count = count + 1    # 自动添加结尾处的注释    strs = strs + '    \n'    # 最后一行添加    strs = strs + '\n'    f.close()    # 有文案要合入    if count > 0:        # 读取项目中具体语言的strings.xml        f = open(pathOut, 'r+')        all_the_lines = f.readlines()        f.seek(0)        f.truncate()        for line in all_the_lines:            # 将项目中具体语言的strings.xml中的替换为处理后的文案            line = line.replace('', strs)            f.write(line)        f.close()    print '合入文案:%d条' % (count)    # 考虑到多语公司返回文案有可能格式不太规范,这里用放宽条件的正则匹配寻找返回多语中的文案条数,然后和合入的文案条数进行对比,不符则输出提示    originCount = len(re.findall(        r'<\s*string\s*name\s*=\s*\".+\"\s*>', ''.join(lines), re.I))    if(originCount != count):        print '#########################警告 START #########################'        print '#########################返回文案有%d条,只合入了%d条文案,请确认' % (originCount, count)        print '#########################注警告 END  #########################'

单双引号检测纠正

通过正则找到没有转义的单双引号,将其替换为转义后的字符(具体转义有两种做法,直接添加反斜杠或者使用相应的html实体符)。

在这里匹配的正则使用了环视,环视具体见下面的引用

环视只进行子表达式的匹配,不占有字符,匹配到的内容不保存到最终的匹配结果,是零宽度的。环视匹配的最终结果就是一个位置。
环视的作用相当于对所在位置加了一个附加条件,只有满足这个条件,环视子表达式才能匹配成功。
环视按照方向划分有顺序和逆序两种,按照是否匹配有肯定和否定两种,组合起来就有四种环视。顺序环视相当于在当前位置右侧附加一个条件,而逆序环视相当于在当前位置左侧附加一个条件。
表达式说明
(?<=Expression)
逆序肯定环视,表示所在位置左侧能够匹配Expression
(?
逆序否定环视,表示所在位置左侧不能匹配Expression
(?=Expression)
顺序肯定环视,表示所在位置右侧能够匹配Expression
(?!Expression)
顺序否定环视,表示所在位置右侧不能匹配Expression

def checkQuotationMarks(contet):    '''    检查单双引号的正确性并自动纠正    '''    result = contet    # 具体转义有两种做法,直接添加反斜杠或者使用相应的html实体符    result = re.sub(r'(?, '\\\'', result)    # re.sub(r'(?    result = re.sub(r'(?, '\\\"', result)    # re.sub(r'(?    return result

%检测纠正

主要实现分为三大步
第一步: 将文案中的 \% 替换为 %%
第二步:通过正则找到是占位符的 %,并进行分割
第三步:遍历分割之后的文案,找到未转义的 % ,替换为 %%

def checkPercent(contet):    '''    检查并自动纠正%的使用正确性    功能:    1.将\%替换为%%    2.将除占位符之外的未转义的%替换为%%    注意点:    1.对于一个%会优先检查是不是符合占位符,因而在某些情况下会出现歧义.    比如"%%sx"这种情况,第二个%是和后面的s连起来成为占位符第一个%未转义呢,还是%%就是转义的%呢?    因而使用者在具体使用的时候要观察输出日志,如果出现自动纠正%的时候进行判断一下是否符合自己的场景    2.正则查找未转义的%,因为一个未转义的%前面有可能有若干个连续的%%,所以会用到可变长度的逆序环视.    而Python自带的正则解释器re不支持可变长度的逆序环视,如果要使用该正则匹配未转义的%确保安装了regex(安装方法:sudo pip install regex).    也提供了不使用regex替换未转义%的方法, 代码见下面.    3.对于阿拉伯语言(RTL语言)%的检查纠正方法和正常语言保持一致,因为在把RTL语言语言合入values-ar的strings.xml中时,AS会自动进行左右变换。    如“حفظ %s%%”合入之后自动变为”%%s% حفظ”, 而“حفظ %%s%”合入之后却变为”%s%% حفظ”。    '''    result = contet    # 将\%替换为%%    result = result.replace('\%', '%%')    # 使用re.split(r'%(?=(\d+\$)?[sd])',result)正则划分,返回的结果却不是按照找到的%进行划分,会多出几个有规律的子串.    # 目前退而求其次使用ugly的替换在分割方法,待后续解决.    uglyStr = ''    result = re.sub(r'%(?=(\d+\$)?[sd])', uglyStr, result)    # 以占位符%进行分割字符串    strs = result.split(uglyStr)    result = ''    i = 0    # 遍历分割得到的所有字符串,进行检查纠正    for str in strs:        i = i + 1        '''        第一种方法        通过正则找到未转义的%,进行替换        Python自带的正则解释器re不支持可变长度的逆序环视,如果要使用该正则匹配未转义的%确保安装了regex(安装方法:sudo pip install regex).        使用下面正则别忘了import regex        '''        # str = regex.sub(r"(?<=(^|[^%])(%%)*)%(?!%)", '%%', str)        '''        第二种方法        未安装regex使用下面的代码,安装了的可以使用上面的正则        通过ugly的替换在替换方法进行替换为转义的%        '''        str = str.replace('%%', uglyStr)        str = str.replace('%', '%%')        str = str.replace(uglyStr, '%%')        # 添加占位符的%        result = result + str + '%'    if i > 0:        # 去除多余的最后一个%        result = result[:-1]    return result

1. 将文案中的 \% 替换为 `%%

这个比较简单,result = result.replace('\%', '%%') .

2. 通过正则找到是占位符的 %,并进行分割

使用re.split(r'%(?=(\d+\$)?[sd])',result) 正则划分,返回的结果却不是按照找到的%进行划分,会多出几个有规律的子串. 目前退而求其次使用ugly的替换在分割方法,待后续解决.

    uglyStr = ''    result = re.sub(r'%(?=(\d+\$)?[sd])', uglyStr, result)    # 以占位符%进行分割字符串    strs = result.split(uglyStr)

3. 遍历分割之后的文案,找到未转义的 % ,替换为 %%

这里有两种方法,第一种使用正则进行匹配。第二种不使用正则先将%% 替换为一个特殊的字符串,然后将剩下% 替换为%%,最终将特殊的字符串替换回%%.

在这里讲一下第一张方法,使用正则匹配。先给出匹配未转义% 的正则(?<=(^|[^%])(%%)*)%(?!%),推荐大家使用在线工具来进行在线正则匹配,因为该网站在线正则匹配是支持环视的。对了,这里的前提是已经将占位符的%去除了,且已经将\%替换为%%。

我将该正则转换为相应的图形化界面方便大家理解,用的是Atom的regex-railroad-diagram插件转换的.

Android脚本插件系列(一):安卓国际化多语自动合入脚本_第4张图片

首先一个未转义%的右边肯定不是一个%, 使用顺序否定环视(?!%),表示所在位置右侧不能匹配%.
接着一个未转义%的左边肯定是偶数个连续%,使用逆序肯定环视(?<=(^|[^%])(%%)*),表示左边是偶数个连续%. 是偶数个连续%是以非%开始或者是字符串的起点,后面跟着大于大于0个%%.


注意点

对于阿拉伯语言(RTL语言)%的检查纠正方法和正常语言保持一致,因为在把RTL语言语言合入values-ar的strings.xml中时,AS会自动进行左右变换。如حفظ %s%%合入之后自动变为%%s% حفظ, 而حفظ %%s%合入之后却变为%s%% حفظ

千万不要按照从右往左的顺序对RTL语言进行%的进行转义,不然合入AS中会出现问题的

最后还是建议在strings.xml中特殊字符还是使用相应的HTML实体符比较稳妥

源码Github地址:android-scripts

更多相关文章

  1. android 正则表达式
  2. Android脚本环境
  3. lua学习笔记 1 android 调用Lua, Lua脚本中启动Intent
  4. 通过ant脚本,编译打包android工程
  5. Android build/envsetup.sh 脚本分析(lunch函数)
  6. 〖Android〗存在多个Android设备时,使用Shell脚本选择一个Android
  7. Android 正则表达式验证手机和邮箱格式是否正确
  8. Android Studio中快速替换styles的正则表达式
  9. 关于脚本PowerShell的设计实例

随机推荐

  1. 华为P30安卓内核编译android kernel buil
  2. android 隐藏底部虚拟按键
  3. android软件的签名过程
  4. Android Studio3.0开发JNI流程------Java
  5. Android(安卓)悬浮窗弹不出输入法的踏坑
  6. Android小笔记
  7. SetContentView 到底Set去哪里呢?
  8. android微信第三方登录怎么通过code获取o
  9. Android(安卓)中压力测试工具Monkey的用
  10. Android开发指南(34) —— Multimedia an