Android国际化翻译多国String导入导出(EXCEL)实现
Posted QIANDXX
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android国际化翻译多国String导入导出(EXCEL)实现相关的知识,希望对你有一定的参考价值。
前言
突然发现自己已经几个月没有更博了,一方面是因为最近换了工作比较忙,另一方面也是因为感觉没什么好分享的(也可能是因为懒。
恰好最近做公司项目涉及到大量文本的国际化翻译问题,手动一个个添加肯定不现实,浪费时间又容易出错。本来部门内部已经有了相关的脚本,可以把项目中的string导出为excel文件,然后发给专业翻译进行翻译,之后再导入到项目中;但是我用了这个脚本后发现问题太多,而且已经无人维护了,不得不痛下杀手完全重写了一个新的,本文就分享出来,简单记录一下。
使用方法
-
clone项目到本地,修改
module_string_script
模块中Config
类的配置 -
运行
ExportStrings2ExcelScript.kt
导出目标项目中的string内容,导出后如下图 -
把导出的excel第一列锁定起来防止修改,然后提供出去翻译
-
运行
ImportFromExcelScript.kt
把翻译完的excel内容导入到项目里面,如果isBaseOnWord
为true,导入时需要至少需要一个参照列(BASE_LANG
或者DEFAULT_LANG
) -
建议导入前先提交代码,这样导入之后就可以清楚看到变动了,方便进行二次确认
实现过程和遇到的问题
导出
-
遍历项目每个模块,使用dom4j库解各国语言的string.xml,存放到一个
LinkedHashMap<语言目录(如values-zh-rCN),<name,word>>
private const val colHead = "name" private const val ROOT_TAG = "resources" private const val TAG_NAME = "string" /** * 解析当前的多语言内容 <语言目录(如values-zh-rCN),<name,word>> */ fun collectRes(res: File): LinkedHashMap<String, LinkedHashMap<String, String>> val hashMap = LinkedHashMap<String, LinkedHashMap<String, String>>() hashMap[colHead] = LinkedHashMap() val saxReader = SAXReader() res.listFiles().forEach langDir -> val stringFile = File(langDir, "strings.xml") if (!stringFile.exists()) return@forEach val data = LinkedHashMap<String, String>() // 收集所有string name,作为excel第一列 val names = hashMap.computeIfAbsent(colHead) LinkedHashMap() val doc = saxReader.read(stringFile) val root = doc.rootElement if (root.name == ROOT_TAG) val iterator = root.elementIterator() while (iterator.hasNext()) val element = iterator.next() if (element.name == TAG_NAME) val name = element.attribute("name").text val word = element.text names[name] = name data[name] = word hashMap[langDir.name] = data return hashMap
-
转换数据结构,把
<语言目录(如values-zh-rCN),<name,word>>
结构转换成<name,<语言目录,word>>
结构,方便后续操作 -
根据策略对解析到的内容做处理,可以把相同内容但是不同name的string排列到一起,或者去重,
Config.isBaseOnWord
表示是否基于string内容去重,BASE_LANG
表示基于哪个语言的内容进行去重/** * 处理可能出现的相同内容但是不同key的string * [source] <name,<语言目录,word>> * @return <name,<语言目录,word>> */ fun processSameWords(source: LinkedHashMap<String, LinkedHashMap<String, String>>): LinkedHashMap<String, LinkedHashMap<String, String>> // 由于可能存在不同key但是相同内容的string,导出时将内容相同的string聚合到一起 val haveCNKey = source.entries.first().value.containsKey(Config.BASE_LANG) val baseLang = if (haveCNKey) Config.BASE_LANG else Config.DEFAULT_LANG // 是否根据中文或者默认语言的内容为基准去重,否则将相同的内容行排序到一起 return if (Config.isBaseOnWord) // 去重 source.entries.distinctBy val baseWord = it.value[baseLang] return@distinctBy if (!baseWord.isNullOrBlank()) baseWord else it .fold(linkedMapOf()) acc, entry -> acc[entry.key] = entry.value acc else // 相同的排到一起 source.entries.sortedBy val baseWord = it.value[baseLang] if (!baseWord.isNullOrEmpty()) return@sortedBy baseWord else return@sortedBy null .fold(linkedMapOf()) acc, entry -> acc[entry.key] = entry.value acc
-
将Map数据写入Excel文件
导入
-
poi读取excel文件内容,存放在一个
Map<表名,<name,<语言目录,值>>>
中```
/**-
获取excel某个表的数据 <表名,<name,<语言目录,值>>>
*/
fun getSheetsData(filePath: String): LinkedHashMap<String, LinkedHashMap<String, LinkedHashMap<String, String>>>
val inputStream = FileInputStream(filePath)
val excelWBook = XSSFWorkbook(inputStream)
val map = linkedMapOf<String, LinkedHashMap<String, LinkedHashMap<String, String>>>()
excelWBook.forEach
val dataMap = LinkedHashMap<String, LinkedHashMap<String, String>>()
val head = ArrayList()
//获取工作簿
val excelWSheet = excelWBook.getSheet(it.sheetName)
excelWSheet.run
// 总行数
val rowCount = lastRowNum - firstRowNum + 1
// 总列数
val colCount = getRow(0).physicalNumberOfCells
// 获取所有语言目录
for (col in 0 until colCount)
head.add(getCellData(excelWBook, sheetName, 0, col))
for (row in 1 until rowCount) // 第一列为string name val name = getCellData(excelWBook, sheetName, row, 0) Log.d(TAG, "第$row行,name = $name") val v = LinkedHashMap<String, String>() for (col in 0 until colCount) val content = getCellData(excelWBook, sheetName, row, col) val text = WordHelper.escapeText(content) v[head[col]] = text Log.d(TAG, "lang = $head[col] ,value = $v[head[col]]") dataMap[name] = v excelWBook.close() inputStream.close() map[it.sheetName] = dataMap
excelWBook.close()
return map
-
-
遍历Map,将每个表中的数据结构转换成
Map<语言目录(如values-zh-rCN),<name,word>>
结构,方便合并```
/**- 还原string map数据结构,以language为行标识方便写入多个string.xml
- [source] <name,<语言目录,word>>
- @return <语言目录(如values-zh-rCN),<name,word>>
*/
fun revertResData(source: LinkedHashMap<String, LinkedHashMap<String, String>>): LinkedHashMap<String, LinkedHashMap<String, String>>
// <语言目录(如values-zh-rCN),<name,word>>
val resData = LinkedHashMap<String, LinkedHashMap<String, String>>()
source.forEach (name, value) ->
value.forEach (langDir, word) ->
val langRes = resData.computeIfAbsent(langDir) LinkedHashMap()
langRes[name] = word
return resData
-
再次读取项目中所有模块的string存到一个
Map<语言目录,<name,word>>
里,和从excel表中读出的Map进行合并```
/**- 将excel中string合并到项目原本string中,不存在则追加,存在则覆盖
- [newData] excel中读出的string <语言目录,<name,word>>
- [resData] 项目中读出的string <语言目录,<name,word>>
/
fun mergeLangNameString(
newData: LinkedHashMap<String, LinkedHashMap<String, String>>,
resData: LinkedHashMap<String, LinkedHashMap<String, String>>
)
if (Config.isBaseOnWord)
/*
* 1. excel中某个string name匹配到了项目中的某个string name
* 2. 找到项目中和该string基准语言的内容相同的其他string
* 3. 将这些string视为相同的string,复制一份添加到newData中
*/
val baseLang = if (newData.containsKey(Config.BASE_LANG)) Config.BASE_LANG else Config.DEFAULT_LANG
val baseLangMap = newData[baseLang]
if (baseLangMap != null)
// 寻找基准值相同的string
val sameWords = baseLangMap.map (name, newWord) ->
val oldBaseWord = resData[baseLang]?.get(name)
return@map name to resData[baseLang]?.filter
if (!oldBaseWord.isNullOrBlank())
return@filter it.value == oldBaseWord
false
?.keys
sameWords.forEach pair ->
if (pair.second?.size ?: 0 > 1)
Log.e(TAG, “newName: p a i r . f i r s t m a p p i n g o l d n a m e s : pair.first mapping old names: pair.firstmappingoldnames:pair.second”)
val newName = pair.first
pair.second?.forEach oldName ->
newData.forEach (lang, map) ->
map[oldName] = map[newName] ?: “”
resData.keys.reversed().forEach
if (!newData.keys.contains(it))
resData.remove(it)
// excel中不存的语言列直接移除掉,不参与合并
Log.e(TAG, “new data have no lang dir:$it, skip”)
// 遍历更新项目string
newData.forEach (lang, map) ->
// 排除第一列
if (lang == colHead)
return@forEach
var hasChanged = false
// 当前项目中一条包含多语种的string map
val nameWordMap = resData.computeIfAbsent(lang) linkedMapOf()
map.forEach (name, newWord) ->
// 项目中存在该语言的字符,遍历每个的值,依次覆盖为excel中的新值
if (name.isNotEmpty() && newWord.isNotBlank())
val oldWord = nameWordMap[name]
if (oldWord != null && oldWord.isNotEmpty())
if (oldWord != newWord)
hasChanged = true
Log.e(
TAG,
“替换string:[name: $name, lang: l a n g , 旧 值 : lang, 旧值: lang,旧值:oldWord, 新值:$newWord]”
)
else
hasChanged = true
Log.e(TAG, “新增string:[name: $name, lang: $lang, 新值: $newWord]”)
nameWordMap[name] = newWord
if (!hasChanged)
// 该语言string内容没有变动,也跳过
resData.remove(lang)
Log.e(TAG, “lang dir $lang have no change, skip”)
-
遍历合并后的string map,使用dom4j修改或者创建原本项目中对应的string.xml文件```
/**- 将excel中string导入到项目
*/
fun importWords(newLangNameMap: LinkedHashMap<String, LinkedHashMap<String, String>>, parentDir: File)
newLangNameMap.forEach (langDir, hashMap) ->
Log.e(TAG, “import lang dir KaTeX parse error: Expected '', got 'EOF' at end of input: …le(parentDir, "langDir/strings.xml”)
if (stringFile.exists())
// 修改原本dom
val saxReader = SAXReader()
val doc = saxReader.read(stringFile)
val root = doc.rootElement
val nodeMap = linkedMapOf<String, Element>()
if (root.name == ROOT_TAG)
val iterator = root.elementIterator()
while (iterator.hasNext())
val element = iterator.next()
if (element.name == TAG_NAME)
val name = element.attribute(“name”).text
nodeMap[name] = element
hashMap.forEach (name, word) ->
val node = nodeMap[name]
if (node == null)
root.addElement(TAG_NAME)
.addAttribute(“name”, name)
.addText(word)
else
if (node.text != word)
node.text = word
outputStringFile(doc, stringFile)
else
// 创建新dom
val langFile = File(parentDir, langDir)
langFile.mkdirs()
stringFile.createNewFile()
val doc = DocumentHelper.createDocument()
val root = doc.addElement(ROOT_TAG)
hashMap.forEach (name, word) ->
val element = root.addElement(TAG_NAME)
element.addAttribute(“name”, name)
.addText(word)
outputStringFile(doc, stringFile)
/**
- 输出string文件
*/
private fun outputStringFile(doc: Document, file: File)
// 遍历所有节点,移除掉原本的换行符节点,否则输出时会因为newlines多出换行符
val root = doc.rootElement
if (root.name == ROOT_TAG)
val iterator = root.nodeIterator()
while (iterator.hasNext())
val element = iterator.next()
if (element.nodeType == org.dom4j.Node.TEXT_NODE)
if (element.text.isBlank())
iterator.remove()
// 输出
val format = OutputFormat()
format.encoding = “utf-8”
format.setIndentSize(4)
format.isNewLineAfterDeclaration = false
format.isNewlines = true
format.lineSeparator = System.getProperty(“line.separator”)
file.outputStream().use os ->
val writer = XMLWriter(os, format)
// 是否将字符转义
writer.isEscapeText = false
writer.write(doc)
writer.flush()
writer.close()
- 将excel中string导入到项目
遇到过的问题
内容相同但是name不同的string如何处理
项目中总会因为这样或者那样的原因包含不少这样的字符,通常也会尽量不去改动,但是导出翻译时多一条重复的文案意味着白白多一份钱,所以最好还是要根据内容做去重。然后导入的时候,一旦匹配到项目中的某个string name,就再去寻找和它的内容相同的其他string,然后将它们全都覆盖为excel中的新值,这样问题也就解决了。
收集项目字符的问题
最开始收集string是使用的单行正则匹配进行的,但是这样明显有问题,一旦string标签不在同一行中就无法匹配,所以改为dom4j xml文档解析
输出文件格式问题
考虑输出时尽量保持源文件最小改动,使用dom4j的OutputFormat
进行xml格式化时,如果是基于原文档修改,并且isNewlines=true
时会多出很多空行,这是因为在dom4j
中换行符也被视为一个Node
,原本每一行后面有一个换行符,设置isNewlines后再次加了一个换行,所以需要先把原本的换行符移除掉
字符转义问题
英文单双引号等字符如果不进行转义就放在string中会被忽略掉,在UI上无法显示,所以在读取excel中字符时可以预先对这类字符做转义处理
以上是关于Android国际化翻译多国String导入导出(EXCEL)实现的主要内容,如果未能解决你的问题,请参考以下文章