当前位置: 首页 > news >正文

【车载Android】使用自定义插件实现多语言自动化适配

2024年中国成为世界第一汽车出口大国,车载Android应用的全球化适配需求也日益迫切。在实际开发中,多语言适配往往是一项繁琐且容易出错的工作,博主曾对Jira上百个翻译错误的Bug单不停地叹气,无论是翻译人员、测试工程师还是开发人员,都需要在无聊的重复劳动中耗费大量时间和精力。

为解决这一痛点,博主基于实践经验开发了MultilingualPlugin多语言自动化插件,目前已开源,希望能帮助更多车载Android开发者提升开发时的效率。

MultilingualPlugin源码地址:https://github.com/linxu-link/MultilingualPlugin

一、插件核心功能与优势

1. 核心功能

  • Excel驱动翻译:通过Excel文件统一管理多语言文本,只需维护一份表格即可生成所有语言的资源文件。
  • 自动匹配与生成:插件会自动读取基准语言(如中文)的strings.xml,并根据Excel中的翻译内容生成其他语言的values-xx目录及对应文件。
  • 全项目适配:支持多模块工程(如车载应用常见的主应用+子模块结构),只需在根目录配置一次,即可自动应用到所有applib模块,也支持仅配置单一模块的场景。
  • 增量更新:新增或修改翻译时,插件会智能更新已有文件,避免重复生成导致的冲突。

2. 解决的痛点

  • 减少人工错误:避免手动复制粘贴翻译内容导致的错别字、标签遗漏等问题。
  • 提升协作效率:翻译人员只需关注Excel表格,开发人员无需手动维护多语言文件,测试人员也可快速验证翻译一致性。
  • 适配车载应用:针对车载系统可能需要支持的多种语言(如英语、日语、韩语、欧洲各语言等),实现一键生成,适配全球化车型需求。

二、插件集成与使用指南

1. 集成方式(Kotlin DSL示例)

(1)使用方式一 - 全局应用

根目录build.gradle.kts中应用插件并设定配置项:

plugins {
alias(libs.plugins.android.application) apply falsealias(libs.plugins.kotlin.android) apply falsealias(libs.plugins.android.library) apply falseid("io.github.linxu-link.multilingual") version "0.2.0"
}multilingual {
// 启用多语言适配,默认关闭enable.set(true)// 使用project.rootDir获取项目根目录,再拼接相对路径excelFilePath.set(file("${project.rootDir}/language/多语言V1.0.xlsx").absolutePath)// 基准语言目录,必须与代码中资源文件目录一致baselineDir.set("values")// 基准语言编码,必须与Excel文件中的语言编码一致defaultLanguage.set("zh-rCN")
} 

(2)使用方式二 - 单模块应用

模块build.gradle.kts中应用插件并设定配置项:

plugins {
alias(libs.plugins.android.application)alias(libs.plugins.kotlin.android)id("io.github.linxu-link.multilingual") version "0.2.0"
}multilingual {
// 启用多语言适配,默认关闭enable.set(true)// 使用project.rootDir获取项目根目录,再拼接相对路径excelFilePath.set(file("${project.rootDir}/language/多语言V1.0.xlsx").absolutePath)// 基准语言目录,必须与代码中资源文件目录一致baselineDir.set("values")// 基准语言编码,必须与Excel文件中的语言编码一致defaultLanguage.set("zh-rCN")
} 

全局应用和单模块应用,两种应用方式是互斥的,根据你的需要只在一个build.gradle中配置即可。

MultilingualPlugin有四个配置项

  • enable:是否启用插件,默认为false。在生成多语言字符串资源后,应该将插件关闭,防止拖慢正常的编译流程。
  • excelFilePath:Excel翻译文件的路径。
  • baselineDir:基准语言的目录,默认为valuesMultilingualPlugin会以基准语言目录下的string.xml为蓝本,获取生成其他语言需要的string name,所以**baselineDir下的string.xml**必须是完整的。
  • defaultLanguage:基准语言在Excel内的编码,默认为zh-rCN

2. Excel文件格式规范

表头:定义语言类型,格式为**语言名称/语言编码**(如Chinese/zh-rCNEnglish/en)。

语言名称可以自行定义,插件不会进行解析,/后的语言编码必须是符合Android多语言规范的编码,插件会根据语言编码生成对应的values文件夹。示例如下:

Chinese/zh-rCNEnglish/en-rUSJapanese/ja-rJPKorean/ko-rKR
我的应用My Applicationマイアプリ내 앱
你好,世界!Hello World!こんにちは、世界!안녕, 세계!
欢迎使用本应用。Welcome to the app.アプリへようこそ。앱에 오신 것을 환영합니다.
设置Settings設定설정
登录Loginログイン로그인
退出登录Logoutログアウト로그아웃
用户名Usernameユーザー名사용자 이름
密码Passwordパスワード비밀번호

3. 生成多语言文件

(1)方案一 - 执行Gradle任务

./gradlew generateTranslations  # 生成所有模块的多语言文件
./gradlew :app:generateTranslations  # 生成指定模块的文件

(2)方案二 - 执行build Task

插件会自动在res目录下生成values-envalues-ja等目录,并创建对应的strings.xml,内容基于Excel翻译生成。

三、插件源码核心逻辑解读

1. 插件架构设计

插件采用Gradle插件标准架构,主要包含三个核心部分:

  • 主插件类( MultilingualPlugin):负责初始化配置、监听项目生命周期,并为符合条件的模块(Android应用/库)注册子插件。
  • 模块插件类( MultilingualModulePlugin):为单个模块添加翻译生成任务,并关联到构建流程。
  • 任务类( MultilingualTask):核心逻辑实现,负责解析Excel、读取基准语言文件、生成翻译资源。

2. 关键功能实现

(1)自动应用与配置继承

// 主插件中自动应用到所有Android模块  
override fun apply(project: Project) {if (project == project.rootProject) {// 根项目创建全局配置扩展  project.extensions.create("multilingual", MultilingualExtension::class.java)// 监听子项目,自动应用模块插件  project.rootProject.subprojects { subproject ->subproject.afterEvaluate {if (it.plugins.hasPlugin("com.android.application") || it.plugins.hasPlugin("com.android.library")) {it.plugins.apply(MultilingualModulePlugin::class.java)}}}}
}

通过subprojects监听所有子模块,自动为Android模块应用插件,避免手动配置每个模块。

(2)Excel解析

@TaskAction
fun generateTranslations() {val excelFile = File(excelFilePath.get())if (!excelFile.exists()) {throw GradleException("==> Excel文件不存在: ${excelFile.absolutePath}")}// 查找Android项目的res目录val resDir = findAndroidResDirectory()// 读取默认语言的string.xml文件val baselineValuesDir = File(resDir, baselineDir.get())if (!baselineValuesDir.exists()) {throw GradleException("==> 基准语言目录不存在: ${baselineValuesDir.absolutePath}")}val defaultStringsFile = File(baselineValuesDir, "strings.xml")if (!defaultStringsFile.exists()) {throw GradleException("==> 默认语言的strings.xml不存在: ${defaultStringsFile.absolutePath}")}// 解析默认strings.xml获取键值对val defaultStrings = parseStringsXml(defaultStringsFile)logger.lifecycle("==> 从${defaultStringsFile.name}读取到${defaultStrings.size}个字符串")// 读取并解析Excel文件WorkbookFactory.create(excelFile.inputStream()).use { workbook ->
val sheet = workbook.getSheetAt(0) ?: throw GradleException("Excel中没有工作表")// 解析第一行获取语言编码信息val headerRow = sheet.getRow(0) ?: throw GradleException("Excel中没有标题行")val languageCodes = mutableMapOf<Int, String>() // 列索引 -> 语言编码for (col in 0 until headerRow.lastCellNum) {val cell = headerRow.getCell(col)?.stringCellValue ?: continueval code = cell.split("/").lastOrNull()?.trim()if (code != null && code.isNotEmpty()) {languageCodes[col] = codelogger.lifecycle("==> 检测到语言: $code (列索引: $col)")}}// 找到默认语言在Excel中的列索引val defaultLangCol = languageCodes.entries.find { it.value == defaultLanguage.get() } ?.key?: throw GradleException("==> Excel中未找到默认语言: ${defaultLanguage.get()}")// 处理excel每一行数据for (rowNum in 1..sheet.lastRowNum) {val row = sheet.getRow(rowNum) ?: continueval defaultLangCell = row.getCell(defaultLangCol) ?: continueval defaultText = defaultLangCell.stringCellValue.trim()if (defaultText.isEmpty()) {continue}// 找到对应的keyval key = defaultStrings.entries.find { it.value == defaultText } ?.keyif (key == null) {logger.warn("==> 在默认strings.xml中未找到文本对应的key: $defaultText (行号: ${rowNum + 1})")continue}// 为每种语言生成翻译languageCodes.forEach { (colIndex, langCode) ->
val translationCell = row.getCell(colIndex) ?: return@forEachval translationText = translationCell.stringCellValue.trim()// 跳过默认语言,因为它已经存在if (langCode == defaultLanguage.get()) return@forEach// 生成对应语言的strings.xmlgenerateLanguageFile(resDir, langCode, key, translationText)}
}}
}

核心逻辑:

  • 解析Excel表头获取语言编码(如zh-rCN),生成对应values-xx目录。

  • 通过DOM操作读取基准语言strings.xml,匹配Excel中的翻译内容,生成新的翻译节点。

  • 自动处理XML特殊字符转义(如&&amp;),并清理无效空白节点,保证文件格式规范。

(3)资源生成

 /**
* 生成或更新特定语言的strings.xml文件
*/
private fun generateLanguageFile(resDir: File, langCode: String, key: String, value: String) {val langDir = if (langCode.isEmpty()) {File(resDir, "values")} else {File(resDir, "values-$langCode")}// 确保目录存在if (!langDir.exists()) {langDir.mkdirs()}val stringsFile = File(langDir, "strings.xml")val doc = if (stringsFile.exists()) {// 如果文件存在,读取现有内容DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(stringsFile)} else {// 如果文件不存在,创建新的XML文档val docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()val doc = docBuilder.newDocument()val resources = doc.createElement("resources")doc.appendChild(resources)doc}doc.documentElement.normalize()val resources = doc.documentElement// 检查是否已有该key的翻译var stringNode: Element? = nullval existingNodes: NodeList = resources.getElementsByTagName("string")for (i in 0 until existingNodes.length) {val node = existingNodes.item(i) as Elementif (node.getAttribute("name") == key) {stringNode = nodebreak}}// 如果存在则更新,不存在则创建if (stringNode != null) {stringNode.textContent = escapeXml(value)} else {stringNode = doc.createElement("string")stringNode.setAttribute("name", key)stringNode.textContent = escapeXml(value)resources.appendChild(stringNode)}// 清理可能的空文本节点
cleanEmptyTextNodes(resources)// 保存文件 - 优化XML格式化配置val transformerFactory = TransformerFactory.newInstance()val transformer = transformerFactory.newTransformer()// 关键优化:设置缩进和编码,避免多余空行transformer.setOutputProperty(OutputKeys.INDENT, "yes")transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8")transformer.setOutputProperty(OutputKeys.METHOD, "xml")transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "no")transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4")// 写入文件val result = StreamResult(stringsFile)transformer.transform(DOMSource(doc), result)logger.lifecycle("==> 已更新、翻译: $langCode/$key = $value")
}

插件通过project.rootDir获取根目录Excel文件,确保多模块共享同一份翻译数据;在生成翻译时,会检查已有strings.xml中的节点,存在则更新,不存在则新增,实现增量更新。

总结

由于 MultilingualPlugin 在使用时,会修改已经存在的strings.xml,所以在使用插件之前务必!务必!将工程代码进行备份,防止出现代码丢失等意外情况!

实践下来MultilingualPlugin可以解决90%以上的翻译问题,但是由于不同的工程结构存在差异,而且一些公司车载应用的strings.xml还会进一步定制化,所以如果需要对自动化插件进行定制,请下载MultilingualPlugin源代码,进行修改。

如果之前没有开发Gradle插件的经验,可以继续阅读后续的文章,了解如何开发一个插件以及如何将插件上传到
plugins.gradle.org上。

MultilingualPlugin源码地址:https://github.com/linxu-link/MultilingualPlugin

http://www.dtcms.com/a/556908.html

相关文章:

  • 学习网站建设要什么学历网站颜色表
  • C++ 分治 归并排序解决问题 力扣 315. 计算右侧小于当前元素的个数 题解 每日一题
  • Linux UdpSocket的应用
  • docker compose 创建MySQL8后在容器里备份数据到宿主机(.sql文件)的方式
  • 南昌网站外包几何图形生成网站
  • 《算法通关指南:数据结构和算法篇 --- 顺序表相关算法题》--- 询问学号,寄包柜,合并两个有序数组
  • OS_3 Memory、4 File、5 IO
  • Jenkins vs Tekton vs Arbess,CI/CD工具一文纵评
  • 如何挑选中药饮片供应商才能确保产品质量与安全?
  • 自己制作的网站如何发布素材网站都有哪些
  • 双非大学生自学鸿蒙5.0零基础入门到项目实战 -《基础篇》
  • webrtc代码走读(十四)-QOS-Jitter
  • 计算机网络经典问题透视:当路由器需要同时连接以太网和ATM网络时,需要添加什么硬件?
  • IntelliJ IDEA从安装到使用:零基础完整指南
  • 怎么做局域网asp网站做网站1天转多钱
  • Oracle常用
  • [VT-Refine] Simulation | Fine-Tuning | docker/run.sh
  • 如何修改网站域名制作自己的网站需要什么材料
  • docker快速上手笔记
  • 生成私钥公钥
  • 免费自助建站自助建站平台推广一般收多少钱
  • 《玩转Docker》[应用篇13]:Docker安装部署Emby及使用技巧:家庭媒体服务器
  • switch case语句中return的用法及说明
  • Unity 错误UserSettings\Layouts\CurrentMaximizeLayout.dwlt
  • zsh: corrupt history file /home/tipriest/.zsh_history的解决办法
  • 深入解析提示语言模型校准:从理论算法到任务导向实践
  • 未来之窗昭和仙君(五十)集成电路芯片生产管理出库——东方仙盟筑基期
  • 如何进行电子商务网站推广?无锡市网站
  • C#上位机框架完整案例
  • 建德网站优化公司房管局网上备案查询