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

2. 解决的痛点
- 减少人工错误:避免手动复制粘贴翻译内容导致的错别字、标签遗漏等问题。
- 提升协作效率:翻译人员只需关注Excel表格,开发人员无需手动维护多语言文件,测试人员也可快速验证翻译一致性。
- 适配车载应用:针对车载系统可能需要支持的多种语言(如英语、日语、韩语、欧洲各语言等),实现一键生成,适配全球化车型需求。
二、插件集成与使用指南
1. 集成方式(Kotlin DSL示例)
(1)使用方式一 - 全局应用
在根目录的build.gradle.kts中应用插件并设定配置项:
dart
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.android.library) apply false
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")
}
(2)使用方式二 - 单模块应用
在模块内build.gradle.kts中应用插件并设定配置项:
csharp
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任务
bash
./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)自动应用与配置继承
kotlin
// 主插件中自动应用到所有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解析
kotlin
@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 ?: continue
val code = cell.split("/").lastOrNull()?.trim()
if (code != null && code.isNotEmpty()) {
languageCodes[col] = code
logger.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) ?: continue
val defaultLangCell = row.getCell(defaultLangCol) ?: continue
val defaultText = defaultLangCell.stringCellValue.trim()
if (defaultText.isEmpty()) {
continue
}
// 找到对应的key
val key = defaultStrings.entries.find { it.value == defaultText } ?.key
if (key == null) {
logger.warn("==> 在默认strings.xml中未找到文本对应的key: $defaultText (行号: ${rowNum + 1})")
continue
}
// 为每种语言生成翻译
languageCodes.forEach { (colIndex, langCode) ->
val translationCell = row.getCell(colIndex) ?: return@forEach
val translationText = translationCell.stringCellValue.trim()
// 跳过默认语言,因为它已经存在
if (langCode == defaultLanguage.get()) return@forEach
// 生成对应语言的strings.xml
generateLanguageFile(resDir, langCode, key, translationText)
}
}
}
}
核心逻辑:
-
解析Excel表头获取语言编码(如
zh-rCN),生成对应values-xx目录。 -
通过DOM操作读取基准语言
strings.xml,匹配Excel中的翻译内容,生成新的翻译节点。 -
自动处理XML特殊字符转义(如
&转&),并清理无效空白节点,保证文件格式规范。
(3)资源生成
scss
/**
* 生成或更新特定语言的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? = null
val existingNodes: NodeList = resources.getElementsByTagName("string")
for (i in 0 until existingNodes.length) {
val node = existingNodes.item(i) as Element
if (node.getAttribute("name") == key) {
stringNode = node
break
}
}
// 如果存在则更新,不存在则创建
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源码地址:github.com/linxu-link/...