【车载Android】使用自定义插件实现多语言自动化适配
2024年中国成为世界第一汽车出口大国,车载Android应用的全球化适配需求也日益迫切。在实际开发中,多语言适配往往是一项繁琐且容易出错的工作,博主曾对Jira上百个翻译错误的Bug单不停地叹气,无论是翻译人员、测试工程师还是开发人员,都需要在无聊的重复劳动中耗费大量时间和精力。
为解决这一痛点,博主基于实践经验开发了MultilingualPlugin多语言自动化插件,目前已开源,希望能帮助更多车载Android开发者提升开发时的效率。
MultilingualPlugin源码地址:https://github.com/linxu-link/MultilingualPlugin
一、插件核心功能与优势
1. 核心功能
- Excel驱动翻译:通过Excel文件统一管理多语言文本,只需维护一份表格即可生成所有语言的资源文件。
- 自动匹配与生成:插件会自动读取基准语言(如中文)的
strings.xml,并根据Excel中的翻译内容生成其他语言的values-xx目录及对应文件。 - 全项目适配:支持多模块工程(如车载应用常见的主应用+子模块结构),只需在根目录配置一次,即可自动应用到所有
app和lib模块,也支持仅配置单一模块的场景。 - 增量更新:新增或修改翻译时,插件会智能更新已有文件,避免重复生成导致的冲突。

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:基准语言的目录,默认为values。MultilingualPlugin会以基准语言目录下的string.xml为蓝本,获取生成其他语言需要的string name,所以**
baselineDir下的string.xml**必须是完整的。 - defaultLanguage:基准语言在Excel内的编码,默认为zh-rCN。
2. Excel文件格式规范
表头:定义语言类型,格式为**语言名称/语言编码**(如Chinese/zh-rCN、English/en)。
语言名称可以自行定义,插件不会进行解析,/后的语言编码必须是符合Android多语言规范的编码,插件会根据语言编码生成对应的values文件夹。示例如下:
| Chinese/zh-rCN | English/en-rUS | Japanese/ja-rJP | Korean/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-en、values-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特殊字符转义(如
&转&),并清理无效空白节点,保证文件格式规范。
(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
