|
@@ -0,0 +1,239 @@
|
|
|
+package com.twm.tools.local
|
|
|
+
|
|
|
+import java.io.File
|
|
|
+import java.io.RandomAccessFile
|
|
|
+import java.util.Scanner
|
|
|
+import java.util.concurrent.ExecutorService
|
|
|
+import java.util.concurrent.Executors
|
|
|
+import java.util.concurrent.Future
|
|
|
+import java.util.concurrent.Semaphore
|
|
|
+import java.util.concurrent.TimeUnit
|
|
|
+import javax.xml.parsers.SAXParserFactory
|
|
|
+import javax.xml.transform.OutputKeys
|
|
|
+import javax.xml.transform.sax.SAXTransformerFactory
|
|
|
+import javax.xml.transform.stream.StreamResult
|
|
|
+import kotlin.system.exitProcess
|
|
|
+
|
|
|
+
|
|
|
+object MainClass {
|
|
|
+ private const val TARGET_LIB = "BusinessCommon"
|
|
|
+ private const val STRING_FILE = "strings.xml"
|
|
|
+ private const val LIMITED_QPS = 6
|
|
|
+ private val translateApi = BaiduTranslateService
|
|
|
+ private val semaphore = Semaphore(LIMITED_QPS)
|
|
|
+ private lateinit var taskExecutor: ExecutorService
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 每次新增多语言, 在默认strings.xml处新增完毕后,点此处开始即可同步所有多语言
|
|
|
+ */
|
|
|
+ @JvmStatic
|
|
|
+ fun main(args: Array<String>) {
|
|
|
+ prepareHttpExecutors()
|
|
|
+ syncTranslations(listOf(
|
|
|
+ AndroidLanguage.EN,
|
|
|
+ AndroidLanguage.DE,
|
|
|
+ AndroidLanguage.JP,
|
|
|
+ AndroidLanguage.KO,
|
|
|
+ AndroidLanguage.HK,
|
|
|
+ AndroidLanguage.RU,
|
|
|
+ AndroidLanguage.FR,
|
|
|
+ AndroidLanguage.TH,
|
|
|
+ ))
|
|
|
+ exitProcess(0)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun prepareHttpExecutors() {
|
|
|
+ semaphore.acquire(LIMITED_QPS)
|
|
|
+ val executor = Executors.newScheduledThreadPool(1)
|
|
|
+ val qpsRelease = {
|
|
|
+ semaphore.release(1)
|
|
|
+ }
|
|
|
+ executor.scheduleAtFixedRate(
|
|
|
+ qpsRelease, 0, (1000f / LIMITED_QPS).toLong(), TimeUnit.MILLISECONDS
|
|
|
+ )
|
|
|
+ taskExecutor = Executors.newFixedThreadPool(LIMITED_QPS)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun syncTranslations(languages: List<AndroidLanguage>) {
|
|
|
+ val parentDir = File("${TARGET_LIB}/src/main/res")
|
|
|
+ val templateDir = File(parentDir, "values")
|
|
|
+ if (!templateDir.exists()) {
|
|
|
+ throw IllegalStateException("没有找到${TARGET_LIB}下的values文件夹")
|
|
|
+ }
|
|
|
+ val templateFile = File(templateDir, STRING_FILE)
|
|
|
+ if (!templateFile.exists()) {
|
|
|
+ throw IllegalStateException("模板values中无strings.xml文件")
|
|
|
+ }
|
|
|
+ for (language in languages) {
|
|
|
+ val langDir = File(parentDir, "values-${language.valuesLang}")
|
|
|
+ if (!langDir.exists() && !langDir.mkdirs()) {
|
|
|
+ throw IllegalStateException("创建文件夹失败:${langDir}")
|
|
|
+ }
|
|
|
+ val langFile = File(langDir, STRING_FILE)
|
|
|
+ if (!langFile.exists() && !langFile.createNewFile()) {
|
|
|
+ throw IllegalStateException("创建文件失败:${langFile}")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // parse languages
|
|
|
+ val saxFactory = SAXParserFactory.newInstance()
|
|
|
+ val saxParser = saxFactory.newSAXParser()
|
|
|
+ val saxHandler = ValuesStringSaxHandler()
|
|
|
+ val saxResult = runCatching {
|
|
|
+ saxParser.parse(templateFile, saxHandler)
|
|
|
+ }
|
|
|
+ if (saxResult.isFailure) {
|
|
|
+ println("模板strings文件解析失败:${saxResult.exceptionOrNull()}")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ val templateValues = saxHandler.getCollectValues()
|
|
|
+ val holdingValues = mutableMapOf<AndroidLanguage, Set<String>>()
|
|
|
+ for (language in languages) {
|
|
|
+ val stringFile = getStringValuesDir(language)
|
|
|
+ val valueHandler = ValuesStringSaxHandler()
|
|
|
+ val parseResult = runCatching {
|
|
|
+ saxParser.parse(stringFile, valueHandler)
|
|
|
+ }
|
|
|
+ if (parseResult.isFailure) {
|
|
|
+ println("${language}中的文件内容解析失败或内容为空")
|
|
|
+ }
|
|
|
+ holdingValues[language] = valueHandler
|
|
|
+ .getCollectValues()
|
|
|
+ .map { it.key }
|
|
|
+ .toHashSet()
|
|
|
+ }
|
|
|
+ val pendingValues = mutableMapOf<AndroidLanguage, MutableList<Translation>>()
|
|
|
+ for (value in templateValues) {
|
|
|
+ for (entry in holdingValues.entries) {
|
|
|
+ if (entry.value.contains(value.key)) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ val valueList = pendingValues.getOrPut(entry.key) {
|
|
|
+ mutableListOf()
|
|
|
+ }
|
|
|
+ valueList.add(Translation(ValuesData(value.key), value.value))
|
|
|
+ println("${entry.key.valuesLang} 待翻译key => ${value.key}")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (pendingValues.isEmpty()) {
|
|
|
+ println("所有翻译均与默认strings.xml同步, 无新增翻译")
|
|
|
+ return
|
|
|
+ }
|
|
|
+ println("<==========================================>")
|
|
|
+ for (entry in pendingValues.entries) {
|
|
|
+ if (entry.value.isEmpty()) {
|
|
|
+ println("${entry.key.valuesLang} 无新增翻译内容")
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ inflateTranslations(entry.key, entry.value, taskExecutor)
|
|
|
+ writeResultToFile(entry.key, entry.value)
|
|
|
+ println("写入文件完成:${entry.key.valuesLang}, 翻译数量:${entry.value.size}")
|
|
|
+ println("<==========================================>")
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun inflateTranslations(
|
|
|
+ language: AndroidLanguage,
|
|
|
+ translations: List<Translation>,
|
|
|
+ executor: ExecutorService
|
|
|
+ ) {
|
|
|
+ val records = mutableMapOf<ValuesData, Future<String?>>()
|
|
|
+ for (translation in translations) {
|
|
|
+ val callable = TranslateCallable(
|
|
|
+ translateApi, translation.template, language.baiduLang
|
|
|
+ )
|
|
|
+ val qpsTask = QPSTaskWrapper(semaphore, callable)
|
|
|
+ records[translation.valueData] = executor.submit(qpsTask)
|
|
|
+ }
|
|
|
+ for (record in records) {
|
|
|
+ try {
|
|
|
+ val translation = record.value.get()
|
|
|
+ if (translation == null) {
|
|
|
+ println("翻译失败 => ${language.valuesLang} => ${record.key.key}")
|
|
|
+ } else {
|
|
|
+ record.key.value = processForTranslation(language, translation)
|
|
|
+ println("翻译完成 => ${language.valuesLang} => ${record.key}")
|
|
|
+ }
|
|
|
+ } catch (ex: Exception) {
|
|
|
+ ex.printStackTrace()
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun writeResultToFile(
|
|
|
+ language: AndroidLanguage,
|
|
|
+ translations: List<Translation>
|
|
|
+ ) {
|
|
|
+ val targetFile = getStringValuesDir(language)
|
|
|
+ ensureValidXmlFile(targetFile)
|
|
|
+ val accessFile = RandomAccessFile(targetFile, "rw")
|
|
|
+ val fileLength = accessFile.length()
|
|
|
+ val endTag = "</resources>"
|
|
|
+ if (fileLength <= endTag.length * 2) {
|
|
|
+ throw RuntimeException("strings.xml is invalid!")
|
|
|
+ }
|
|
|
+ var seekPosition = endTag.length
|
|
|
+ var resourceEnd = -1
|
|
|
+ while (seekPosition <= fileLength) {
|
|
|
+ accessFile.seek(fileLength - seekPosition)
|
|
|
+ val currentLine = accessFile.readLine()
|
|
|
+ if (currentLine.contains("</resources>")) {
|
|
|
+ resourceEnd = seekPosition
|
|
|
+ break
|
|
|
+ }
|
|
|
+ seekPosition++
|
|
|
+ }
|
|
|
+ val current = resourceEnd
|
|
|
+ if (current < 0) {
|
|
|
+ throw IllegalStateException("strings.xml文件格式有误")
|
|
|
+ }
|
|
|
+ accessFile.seek(fileLength - current)
|
|
|
+ for (translation in translations) {
|
|
|
+ val insertString = " <string name=\"${translation.valueData.key}\">${translation.valueData.value}</string>"
|
|
|
+ accessFile.write(insertString.toByteArray())
|
|
|
+ accessFile.writeBytes("\n")
|
|
|
+ }
|
|
|
+ accessFile.writeBytes(endTag)
|
|
|
+ accessFile.close()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun ensureValidXmlFile(file: File) {
|
|
|
+ var validFile = false
|
|
|
+ val scanner = Scanner(file)
|
|
|
+ while (scanner.hasNextLine()) {
|
|
|
+ val nextLine = scanner.nextLine()
|
|
|
+ if (nextLine.contains("<resources>")) {
|
|
|
+ validFile = true
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (validFile) {
|
|
|
+ return
|
|
|
+ }
|
|
|
+ val target = SAXTransformerFactory.newInstance() as SAXTransformerFactory
|
|
|
+ val handler = target.newTransformerHandler()
|
|
|
+ handler.transformer.apply {
|
|
|
+ setOutputProperty(OutputKeys.ENCODING, "UTF-8")
|
|
|
+ setOutputProperty(OutputKeys.INDENT, "yes")
|
|
|
+ }
|
|
|
+ val result = StreamResult(file)
|
|
|
+ handler.setResult(result)
|
|
|
+ handler.startDocument()
|
|
|
+ val indent = "\n".toCharArray()
|
|
|
+ handler.characters(indent, 0, indent.size)
|
|
|
+ handler.startElement("", "", "resources", null)
|
|
|
+ handler.characters(indent, 0, indent.size)
|
|
|
+ handler.endElement("", "", "resources")
|
|
|
+ handler.endDocument()
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun processForTranslation(language: AndroidLanguage, source: String): String {
|
|
|
+ if (language != AndroidLanguage.FR) {
|
|
|
+ return source
|
|
|
+ }
|
|
|
+ return source.replace("'", "\\'")
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun getStringValuesDir(language: AndroidLanguage): File {
|
|
|
+ return File("${TARGET_LIB}/src/main/res/values-${language.valuesLang}/${STRING_FILE}")
|
|
|
+ }
|
|
|
+}
|