Android脚本插件系列(一):安卓国际化多语自动合入脚本
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。
碰到的问题
每次发版都要手动合入22种语言的文案,操作僵硬,效率低下。
不同语言返回的文案很有可能出现未转义的特殊字符(例如法语的
j'aime
),导致编译失败,从而要手动一个一个去加转义符。对于
%
在某些情况下会出现不易察觉的问题,如之前向翻译公司提了一条文案为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%
。
脚本能做的事
针对以上问题编写了一个脚本,主要功能如下
- 自动合入多语,提高效率
- 对返回的文案进行单双引号正确性检测,并自动纠正
- 对返回的文案进行%正确性检测,发现隐患并自动纠正
效果
因为要输入项目地址、多语返回地址等,所以采用界面化的形式。
选择完项目地址、多语返回地址,输入本次合入注释之后,点击开始合入。
实现
自动合入
自动合入最关键的就是映射关系,附上我的映射关系,这里大家可以根据实际情况映射。
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中。文案被脚本自动纠,会输出相应日志(旧文案和纠正后的文案)供使用者确认。正对返回多语文案的文件格式没有要求,只要保证具体的单条多语是
即可。
同时考虑到多语公司返回文案有可能格式不太规范,脚本会使用放宽条件的正则匹配寻找返回多语中的文案条数,然后和合入的文案条数进行对比,不符则输出提示
代码注释比较完善,具体代码如下
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*string\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插件转换的.
首先一个未转义%
的右边肯定不是一个%
, 使用顺序否定环视(?!%)
,表示所在位置右侧不能匹配%
.
接着一个未转义%
的左边肯定是偶数个连续%
,使用逆序肯定环视(?<=(^|[^%])(%%)*)
,表示左边是偶数个连续%
. 是偶数个连续%
是以非%
开始或者是字符串的起点,后面跟着大于大于0个%%
.
注意点
对于阿拉伯语言(RTL语言)%的检查纠正方法和正常语言保持一致,因为在把RTL语言语言合入values-ar的strings.xml中时,AS会自动进行左右变换。如حفظ %s%%
合入之后自动变为%%s% حفظ
, 而حفظ %%s%
合入之后却变为%s%% حفظ
千万不要按照从右往左的顺序对RTL语言进行%
的进行转义,不然合入AS中会出现问题的
最后还是建议在strings.xml中特殊字符还是使用相应的HTML实体符比较稳妥
源码Github地址:android-scripts
更多相关文章
- android 正则表达式
- Android脚本环境
- lua学习笔记 1 android 调用Lua, Lua脚本中启动Intent
- 通过ant脚本,编译打包android工程
- Android build/envsetup.sh 脚本分析(lunch函数)
- 〖Android〗存在多个Android设备时,使用Shell脚本选择一个Android
- Android 正则表达式验证手机和邮箱格式是否正确
- Android Studio中快速替换styles的正则表达式
- 关于脚本PowerShell的设计实例