Browse Source

提交人:jtm
提交内容:百度翻译插件

江天明 1 năm trước cách đây
mục cha
commit
a0a0012cbb

+ 1 - 0
LocalTools/.gitignore

@@ -0,0 +1 @@
+/build

+ 15 - 0
LocalTools/build.gradle

@@ -0,0 +1,15 @@
+plugins {
+    id 'java-library'
+    id 'org.jetbrains.kotlin.jvm'
+}
+
+java {
+    sourceCompatibility = JavaVersion.VERSION_17
+    targetCompatibility = JavaVersion.VERSION_17
+}
+
+dependencies {
+    implementation files('libs/okhttp-4.9.0.jar')
+    implementation files('libs/okio-2.10.0.jar')
+    implementation files('libs/gson-2.10.1.jar')
+}

BIN
LocalTools/libs/gson-2.10.1.jar


BIN
LocalTools/libs/okhttp-4.9.0.jar


BIN
LocalTools/libs/okio-2.10.0.jar


+ 23 - 0
LocalTools/src/main/java/com/twm/tools/local/AndroidLanguage.kt

@@ -0,0 +1,23 @@
+package com.twm.tools.local
+
+enum class AndroidLanguage(
+    val valuesLang: String,
+    val baiduLang: String
+) {
+    TW("zh-rTW", BaiduLanguage.TW),
+    HK("zh-rHK", BaiduLanguage.TW),
+    EN("en", BaiduLanguage.EN),
+    JP("ja", BaiduLanguage.JP),
+    KO("ko", BaiduLanguage.KO),
+    FR("fr", BaiduLanguage.FR),
+    SP("es", BaiduLanguage.SP),
+    RU("ru", BaiduLanguage.RU),
+    PT("pt", BaiduLanguage.PT),
+    DE("de", BaiduLanguage.DE),
+    IT("it", BaiduLanguage.IT),
+    NL("nl", BaiduLanguage.NL),
+    PL("pl", BaiduLanguage.PL),
+    SW("sw", BaiduLanguage.SW),
+    VI("vi", BaiduLanguage.VI),
+    TH("th", BaiduLanguage.TH),
+}

+ 19 - 0
LocalTools/src/main/java/com/twm/tools/local/BaiduLanguage.kt

@@ -0,0 +1,19 @@
+package com.twm.tools.local
+
+object BaiduLanguage {
+    const val TW = "cht" // 繁中
+    const val EN = "en" // 英语
+    const val JP = "jp" // 日语
+    const val KO = "kor" // 韩语
+    const val FR = "fra" // 法语
+    const val SP = "spa" // 西班牙
+    const val RU = "ru" // 俄语
+    const val PT = "pt" // 葡萄牙语
+    const val DE = "de" // 德语
+    const val IT = "it" // 意大利语
+    const val NL = "nl" // 荷兰语
+    const val PL = "pl" // 波兰语
+    const val SW = "swe" // 瑞典语
+    const val VI = "vie" // 越南语
+    const val TH = "th" // 泰语
+}

+ 12 - 0
LocalTools/src/main/java/com/twm/tools/local/BaiduTranslateResponse.kt

@@ -0,0 +1,12 @@
+package com.twm.tools.local
+
+class BaiduTranslateResponse(
+    val from: String,
+    val to: String,
+    val trans_result: List<BaiduTranslateResult>?
+)
+
+class BaiduTranslateResult(
+    val src: String,
+    val dst: String?
+)

+ 49 - 0
LocalTools/src/main/java/com/twm/tools/local/BaiduTranslateService.kt

@@ -0,0 +1,49 @@
+package com.twm.tools.local
+
+import com.google.gson.Gson
+import okhttp3.FormBody
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import java.math.BigInteger
+import java.security.MessageDigest
+
+object BaiduTranslateService: TranslateApi {
+    private const val TRANSLATE_URL = "https://fanyi-api.baidu.com/api/trans/vip/translate"
+    private const val BAIDU_APPID = "20230809001774667"
+    private const val BAIDU_SECRET = "VOaDhSgt7OhC2_TO225T"
+    private val client = OkHttpClient.Builder().build()
+
+    override fun translate(source: String, lang: String): String? {
+        val salt = System.nanoTime().toString()
+        val sign = (BAIDU_APPID + source + salt + BAIDU_SECRET).getMd5()
+        val body = FormBody.Builder()
+            .add("q", source)
+            .add("from", "auto")
+            .add("to", lang)
+            .add("appid", BAIDU_APPID)
+            .add("salt", salt)
+            .add("sign", sign)
+            .build()
+        val request = Request.Builder()
+            .post(body)
+            .url(TRANSLATE_URL)
+            .header("Content-Type", "application/x-www-form-urlencoded")
+            .build()
+        val response = runCatching {
+            client.newCall(request).execute()
+        }
+        if (response.isFailure) {
+            return null
+        }
+        val result = response.getOrThrow().body?.string() ?: return null
+        val translate = Gson().fromJson(
+            result, BaiduTranslateResponse::class.java
+        )
+        return translate.trans_result?.firstOrNull()?.dst
+    }
+
+    private fun String.getMd5(): String {
+        val md = MessageDigest.getInstance("MD5")
+        return BigInteger(1, md.digest(this.toByteArray())).toString(16).padStart(32, '0')
+    }
+}

+ 239 - 0
LocalTools/src/main/java/com/twm/tools/local/MainClass.kt

@@ -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}")
+    }
+}

+ 16 - 0
LocalTools/src/main/java/com/twm/tools/local/QPSTaskWrapper.kt

@@ -0,0 +1,16 @@
+package com.twm.tools.local
+
+import java.util.concurrent.Callable
+import java.util.concurrent.Semaphore
+
+class QPSTaskWrapper<V>(
+    private val semaphore: Semaphore,
+    private val delegate: Callable<V>
+): Callable<V> {
+
+    override fun call(): V {
+        semaphore.acquireUninterruptibly(1)
+        return delegate.call()
+    }
+
+}

+ 7 - 0
LocalTools/src/main/java/com/twm/tools/local/TranslateApi.kt

@@ -0,0 +1,7 @@
+package com.twm.tools.local
+
+interface TranslateApi {
+
+    fun translate(source: String, lang: String): String?
+
+}

+ 17 - 0
LocalTools/src/main/java/com/twm/tools/local/TranslateCallable.kt

@@ -0,0 +1,17 @@
+package com.twm.tools.local
+
+import java.util.concurrent.Callable
+
+class TranslateCallable(
+    private val translateApi: TranslateApi,
+    private val source: String,
+    private val language: String
+): Callable<String?> {
+
+    override fun call(): String? {
+        if (source.isEmpty()) {
+            return null
+        }
+        return translateApi.translate(source, language)
+    }
+}

+ 6 - 0
LocalTools/src/main/java/com/twm/tools/local/Translation.kt

@@ -0,0 +1,6 @@
+package com.twm.tools.local
+
+class Translation(
+    val valueData: ValuesData,
+    val template: String
+)

+ 6 - 0
LocalTools/src/main/java/com/twm/tools/local/ValuesData.kt

@@ -0,0 +1,6 @@
+package com.twm.tools.local
+
+data class ValuesData(
+    val key: String,
+    var value: String = ""
+)

+ 42 - 0
LocalTools/src/main/java/com/twm/tools/local/ValuesStringSaxHandler.kt

@@ -0,0 +1,42 @@
+package com.twm.tools.local
+
+import org.xml.sax.Attributes
+import org.xml.sax.helpers.DefaultHandler
+
+class ValuesStringSaxHandler: DefaultHandler() {
+
+    private val values = mutableListOf<ValuesData>()
+    private var pending: ValuesData? = null
+
+    override fun startElement(uri: String?, localName: String?, qName: String?, attrs: Attributes?) {
+        if (qName != "string") {
+            pending = null
+            return
+        }
+        val key = attrs?.getValue("name")
+        if (key == null) {
+            pending = null
+        } else {
+            pending = ValuesData(key)
+        }
+    }
+
+    override fun endElement(uri: String?, localName: String?, qName: String?) {
+        if (qName != "string") {
+            pending = null
+            return
+        }
+        if (pending != null) {
+            values.add(pending!!)
+            pending = null
+        }
+    }
+
+    override fun characters(p0: CharArray, p1: Int, p2: Int) {
+        if (pending != null) {
+            pending?.value = String(p0, p1, p2)
+        }
+    }
+
+    fun getCollectValues() = values as List<ValuesData>
+}

+ 1 - 0
settings.gradle

@@ -30,3 +30,4 @@ include ':BusinessMain'
 include ':BusinessStep'
 include ':BusinessAirFryer'
 include ':skin-support'
+include ':LocalTools'