前言
突然发现自己已经几个月没有更博了,一方面是因为最近换了工作比较忙,另一方面也是因为感觉没什么好分享的(也可能是因为懒。
恰好最近做公司项目涉及到大量文本的国际化翻译问题,手动一个个添加肯定不现实,浪费时间又容易出错。本来部门内部已经有了app小胖子相关的脚本,可以把项数据结构c语言版严蔚敏目中的string导出为excel文件,然后发appreciate给专数据结构c语言版严蔚敏业翻译进行翻译,之后再导入到项目中;但是我用了这个脚本后发现问题太多,而且已经无人维护了,不得不痛下杀手完全重写了一个新的,本文就分享出来,简单记录一下。
使用方法
仓库地址
-
clone项目到本地,elements修改
module_strelement滑板ing_script
模块中Config
类的配置 -
运行
ExportStrings2ExcelScript.kt
导出目标项目中的string内容,导出后如下图 -
把导出的excel第一列锁定起来防止修改,然后提供出去翻译
-
运行
ImportFromEappearxcelScri数据结构课程设计心得体会pt.kt
把翻译完的excel内容导入到项数据结构课程设计心得体会目里面
实现过程和遇到的问题
导出
-
遍历项目elementary每个模块,使用dom4j库解各国语言的strielementsng.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 }
-
转换数element滑板据结构,把
<语言目录elementary翻译(如values-zh-rCN),<name,word>>
结构转换成<name,<语言目录,word>approach>
结构,方便后续操作 -
根据策略对解析到的内容做处理,可以把相同内容但是不同naelementanimationme的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,<语言目录,值>&gappointmentt;>
中/** * 获取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<String>() //获取工作簿 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 }
- 再次读取项目中所有element酒店模块的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 (resData.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:${pair.first} mapping old names:${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: $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,使用doapplicationm4j修改或者创建原本项目中对应的string.xml文件
/** * 将excel中string导入到项目 */ fun importWords(newLangNameMap: LinkedHashMap<String, LinkedHashMap<String, String>>, parentDir: File) { newLangNameMap.forEach { (langDir, hashMap) -> Log.e(TAG, "import lang dir $langDir") if (langDir.startsWith("values")) { val stringFile = File(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() } }
遇到过的问题
内容相同但是name不同的strinappleg如何处理
项目中总会因为这样或者那样的原因包含不少这样的字符,通常也会尽量不去改动,但是导出翻译APP时多一条重复的文案意味着白白多一份钱,所以最好还是要根据内容做去重。然后导入的时候,一旦匹配到项目中的某个string name,就再去寻找和它的数据结构c语言版严蔚敏内容相同的其他strin数据结构c语言版严蔚敏g,然后将它们全都覆盖为excel中的新值,这样问题也就解决了。
收集项目字符的问题
最开始收集string是使用的单行正则匹配进行的,但是这样明显有问题,一旦string标签不在同一行中就无法匹配,所以改为dom4j xml文档解析
输出文件格式问题
考虑输出时尽量保持源文件最小改动,使用dom4j的OutputFormat
进行xml格式化时,如果是基于数据结构与算法原文档修改,并且isNewlines=true
时会多出很多空行,这是因为在dom4j
中换行符也被视为一个Node数据结构c语言版
,原本每一行后面有一个换行符,设置isNeapplicationwlines后再次加了一个换行,所以需要先把原本的换行符移除掉
字符转义问题
英文单双引appstore号等字符如果不进行转义就放在strinelement是什么意思g中会被忽略掉,在UI上无法显示,所以在读取excel中字符时可以预先对这类字符做转义处理