ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android App自动翻译成多语言:从strings.xml到ML Kit与云端API的完整实践

Android App自动翻译成多语言:从strings.xml到ML Kit与云端API的完整实践 做Android开发这些年隔三差五就会遇到一个需求把App翻译成其他语言。老板说“做个海外版”产品说“要支持多语言”客户说“最好能自动翻译”。但等你真正坐下来动手会发现这个需求至少有两层含义一是把App界面本身做成多语言版本用户切换系统语言时界面跟着变二是让App具备翻译能力比如聊天工具里的消息翻译、新闻阅读器里的内容翻译。很多人一开始会把两者混在一起导致方案越做越复杂。这篇内容不聊框架不画大饼就把我实际做过的“Android App自动翻译成其他语言”这条路完整拆开从最基础的strings.xml多语言方案讲起到运行时动态切换语言再到给App接入实时翻译能力最后再分享一套能把几十种语言批量翻译落地的流水线脚本以及我在真实项目里踩过的一堆坑。不管你是刚入门的小白还是已经在做国际化项目的开发这篇内容应该都能帮上忙。1. 动手之前先想清楚你要“翻界面”还是“翻内容”很多项目一听到“自动翻译”第一反应就是找翻译SDK接入结果接完了发现界面上的按钮还是中文用户骂骂咧咧。原因很简单翻译能力解决的是“输入一段文本输出另一段文本”而界面国际化解决的是“整个App的UI文本要随着语言环境变化”。这是两条完全不同的技术路线。1.1 两个容易混淆的需求先说“翻界面”。它的本质是把代码里写死的界面文字全部抽离出来按语言分别存储。Android系统有一套成熟的本地化机制res/values/strings.xml 放默认语言res/values-en/ 放英文res/values-zh-rTW/ 放繁体中文依此类推。系统会根据用户的语言环境自动加载对应资源。这套方案是Android自带的稳定、轻量、离线可用也是我做国际化项目时的首选方案。再说“翻内容”。它面向的是运行期动态产生的文本比如用户在聊天框里输入的外语消息、新闻App抓取到的海外文章、旅行App里的景点介绍。这些内容不可能提前翻译好必须在用户看到之前实时翻译。这种场景才需要接翻译能力通常是调用云端翻译API或者用设备端的离线翻译模型。当一个需求说“自动翻译成其他语言”你得先当面问清楚对方到底要哪一种。我遇到过一个项目产品经理说要“多语言”结果开发到一半发现他想要的是“用户把App里的所有文章一键翻译成英文阅读”。方案完全不一样提前问清楚能省后面数周返工。1.2 翻译是个流程问题不只是字符串问题不管选哪条路线我都要强调一个观点多语言不是简单地把翻译结果填进项目而是一个从代码规范到资源管理、再到翻译发布的完整流程。写代码时要养成习惯所有用户可见的文本一律走资源引用严禁硬编码。字符串资源要有清晰的命名规范比如settings_title、login_error_network不能随便写string1、string2。翻译要跟着版本走每加一个新功能都要考虑旧语言的翻译何时补齐。语言切换的入口要做成用户可感知的最好在设置页提供语言选项而不要只依赖系统语言。这一点很像装修房子你首先得把水电管线布置好后面刷墙贴砖才顺利。代码规范没做好翻译做得再好也白搭。2. 界面多语言化从字符串抽取到运行时切换如果你的目标真的就是“让App界面变成多语言”那先别急着接SDK把官方那套资源方案吃透就够用了。这一节我把整个流程压缩成一套能直接照做的步骤项目里已经有多语言基础的可以跳过前半段重点看运行时切换那部分。2.1 用Android Studio一键抽取硬编码文本老项目里最容易出现的情况是界面文字直接写在布局文件里比如TextView android:text确定/代码里也到处都是setText(确定)。Android Studio提供了一个好用的功能叫“Extract string resource”选中有问题的文本按AltEnterWindows或OptionEnterMac选择“Extract string resource”IDE会自动把这段文本提取到strings.xml里并生成一个resource引用替换原来的位置。但IDE不是万能的。它只能帮你抽取当前文件里能被静态识别的字符串对于动态拼接文本就无能为力了。比如代码里写共 count 条记录这种硬编码IDE能提取变量拼好的部分但很多团队往往是手动处理漏掉了结果中文文案就悄悄留在代码里。更麻烦的是第三方依赖库内部的字符串你管不到比如一个图片选择器库可能是英文的也可能自带中文资源最终显示什么基本靠运气。所以我的习惯是首先明确“硬编码字符串是bug”的观念code review时就拦截其次遇到第三方库的文案问题能通过库自身的语言设置解决就用不能就考虑自己包一层界面把文案替换成App自己的多语言资源。2.2 创建多语言目录不是建几个文件夹那么简单抽取完成后下一步是创建语言目录。在Android Studio里右键res目录选择New Android Resource Directory资源类型选values然后在Locale列表里添加需要的语言地区。IDE会自动生成类似values-en、values-zh-rTW的目录名并创建对应的strings.xml。这里有几个细节容易被忽略默认的values/strings.xml不要留空。系统在找不到对应语言时会回退到这里如果这里只有中文而用户是法语界面会显示中文如果连默认的都没有可能直接崩溃或者空白。所以默认语言建议用覆盖面最广的英文或者至少是你App用户量最大的语言。不是每种语言都翻译得完美。法语、德语、阿拉伯语这些语言文本通常比英文长很多硬塞到固定高度的按钮里会显示不全。我一般会在翻译完成后做一轮“文本溢出检查”给TextView适当留足padding关键按钮用android:minWidth兜底。右到左语言阿拉伯语、希伯来语不只是翻译布局还要镜面化。Android有supportsRtltrue的属性开启后布局会自动翻转但如果代码里用了很多绝对坐标或者左对齐的习惯性写法RTL下体验会非常怪。这块经常被团队遗漏等上线后中东用户反馈才想起来。2.3 运行时切换语言从传统locale到Android 13新特性很多产品不想跟着系统语言走而是希望用户在App内单独设置语言。这意味着“切换语言”是运行时操作开发要把新的语言设置应用到当前Activity甚至整个App。Android 13之前开发者普遍的做法是通过LocaleManager.setLocale()或者反射改Configuration。我比较推荐兼容性好的方案保存用户选择到SharedPreferences然后在Application.onCreate()里读出来调用Locale.setDefault(newLocale)再通过Configuration.setLocale()更新每个Activity的configuration最后recreate()刷新界面。fun applyLocale(context: Context, languageCode: String) { val locale Locale(languageCode) Locale.setDefault(locale) val configuration context.resources.configuration configuration.setLocale(locale) context.resources.updateConfiguration(configuration, context.resources.displayMetrics) }但这个方法有个经典问题在Android 7.0以上系统已经支持多语言环境LocaleManager的优先级反而比updateConfiguration更高导致你辛苦改完又被系统覆盖。网上流传的很多“运行时切换语言”代码都有这个隐患我在旧项目里也踩过。Android 13API 33推出了官方支持的Per-app language preferences也就是“应用内语言设置”。直接在应用的AndroidManifest.xml里给activity加上android:localeConfigxml/locales_config然后系统设置里就会出现该App的语言选项App内部再用同样的API读取用户选中的语言。它解决了长期以来的兼容性问题。application android:localeConfigxml/locales_config /application?xml version1.0 encodingutf-8? locale-config xmlns:androidhttp://schemas.android.com/apk/res/android locale android:namezh-rCN / locale android:nameen / locale android:nameja / /locale-config运行时读取用户语言切换调用LocaleManager.getApplicationLocales()设置语言后调用LocaleManager.setApplicationLocales(...)系统会替你处理Activity重建。这个方案想法很好但有个现实问题市面上还有大量Android 12以下的设备你要么用兼容方案要么接受旧的updateConfiguration方式。我的项目是同时维护两套逻辑低版本走旧方案高版本走官方新API最后在onCreate里统一应用。3. 应用内自动翻译给App装上“读得懂外语”的能力接下来是另一种场景App本身已经有内容了用户需要“自动翻译”内容。这个需求常见于聊天类App、社区类App、阅读类App。你不可能提前准备所有译文必须在用户点击“翻译”按钮后实时返回结果。3.1 ML Kit TranslationGoogle的端侧离线翻译方案ML Kit是Google提供的移动端机器学习SDK其中的翻译模块支持50多种语言最大的优势是翻译过程可以在设备端离线完成。你把模型下载到本地后翻译时不消耗网络流量也不会有延迟适合对隐私敏感的内容。接入方式非常简单。先引入依赖implementation com.google.mlkit:translate:17.0.2然后创建翻译器并下载模型val translator Translation.getClient( TranslatorOptions.Builder() .setSourceLanguage(TranslateLanguage.ENGLISH) .setTargetLanguage(TranslateLanguage.CHINESE) .build() ) translator.downloadModelIfNeeded() .addOnSuccessListener { translator.translate(Hello, world) .addOnSuccessListener { translatedText - textView.text translatedText } }注意几点模型是按语言对拆分的比如英文到中文是一个模型英文到日文是另一个模型。每个模型几十MB到一百多MB不等首次下载前要跟用户确认最好在Wi-Fi环境下下载。如果目标语言不固定用户可能随便选来源和目标模型数量会爆炸建议只支持固定几种常用语言组合。翻译质量对短句还行遇到长段文本、俚语、专业术语效果比云端API差一截。所以我的定位是“应急翻译”不是“高质量翻译”。我在一个旅行App里试过这套方案用户看到景点介绍是日语一键翻译成中文尽管模型翻译有些生硬但核心意思能看懂满意度还不错。关键是它离线能用流量和隐私问题都解决了。3.2 云端翻译API质量更好但要处理好网络与流量如果对翻译质量要求更高云端API是更稳妥的选择。国内用得比较多的有百度翻译开放平台、有道智云、腾讯云机器翻译每家都有免费额度个人项目试用期够用商业项目按量付费。选型时我建议从三个维度比较免费额度百度和有道有一定免费调用量适合早期验证。语言覆盖看App目标市场如果要做小语种出海语言覆盖率比价格更重要。签名复杂度有些平台签名简单有些要算HMAC接入工作量不一样。云端的通用调用模式基本一致App拿到待翻译文本请求后端接口后端再去调用翻译服务商API然后把结果返回给App。有人问为什么不直接在App里调翻译API原因很简单密钥不能放在客户端否则别人反编译一下你的APK就能把你的翻译额度偷光。所以正确做法是App请求自己的服务器服务器统一管理密钥和限流。3.3 一个可落地的翻译引擎封装思路为了避免每个页面都写一遍网络请求和回调我一般会先定义一个接口屏蔽不同翻译服务商的差异interface TranslateEngine { fun translate( text: String, targetLanguage: String, sourceLanguage: String, callback: (String?, String?) - Unit ) }需要说明的是这里的 callback 只是示意生产环境建议用协程挂起或者Kotlin Flow避免回调地狱。然后分别实现百度引擎、腾讯引擎等再写一个 TranslatorManager 根据配置选择当前引擎。这样将来要换翻译服务商或者同时接入多家做兜底都只改一个类。具体的翻译API调用以百度翻译为例国内常用的开放平台之一签名规则是sign md5(appId query salt secretKey)请求参数通过GET或POST提交。这是一个极简的调用示例val salt System.currentTimeMillis().toString() val sign MD5(appId text salt secretKey) val url https://fanyi-api.baidu.com/api/trans/vip/translate ?q${URLEncoder.encode(text, UTF-8)} fromautoto$targetLanguage appid$appId salt$salt sign$sign返回的JSON里有trans_result数组取dst字段就是译文。实际项目里我更推荐用Retrofit或OkHttp封装统一处理超时、重试、错误码。翻译API不是100%可用网络波动、参数错误、语言方向不支持都会返回错误码要做好用户提示和降级处理。如果企业级项目建议在后端加缓存一样的文本只翻译一次既能省费用又能提高响应速度。4. 批量翻译流水线用脚本让多语言落地不再重复劳动真正让人头疼的不是翻译一两个字符串而是几十种语言的资源文件同步维护。上网搜教程很多人教你手动创建每个values-xx/strings.xml然后一个个粘贴翻译。听起来还行但项目有几百条字符串要维护中英日韩法德西俄八种语言手工人肉翻译一次就想辞职。所以我后来的做法是把“导出未翻译字符串 → 调用翻译API → 写入目标语言资源文件”这条流水线固化成脚本。4.1 整体思路用脚本自动生成多语言资源流水线的核心分三步读取res/values/strings.xml拿到所有string条目的name和text。对每条字符串调用翻译API把默认语言翻译成目标语言。把翻译结果写入res/values-xx/strings.xml注意保持XML格式合法特殊字符要转义。第一步和第三步本质上是XML解析与生成Python的xml.etree.ElementTree足够用。第二步完全复用上一节提到的翻译API把请求地址和签名规则封装成函数即可。翻译时有一个性能优化点有些API支持单次传入多个文本批量翻译务必用批处理一次传几十条速度比一条条调用快得多也能降低触发限流的概率。下面是一个示例脚本核心片段演示读取源语言文件并按条目翻译写入英文资源目录import hashlib import random import json import time import requests import xml.etree.ElementTree as ET APP_ID 你的百度翻译APP_ID SECRET_KEY 你的百度翻译SECRET_KEY def translate_batch(texts, to_lang): query \n.join(texts) salt str(random.randint(32768, 65536)) sign hashlib.md5((APP_ID query salt SECRET_KEY).encode()).hexdigest() resp requests.get(https://fanyi-api.baidu.com/api/trans/vip/translate, params{ q: query, from: zh, to: to_lang, appid: APP_ID, salt: salt, sign: sign, }) result resp.json() return [item[dst] for item in result[trans_result]] def generate_en_resources(): tree ET.parse(res/values/strings.xml) root tree.getroot() texts [item.text for item in root.findall(string)] translated translate_batch(texts, en) new_root ET.Element(resources) for i, item in enumerate(root.findall(string)): new_item ET.SubElement(new_root, string, nameitem.get(name)) new_item.text translated[i] ET.ElementTree(new_root).write(res/values-en/strings.xml, encodingutf-8, xml_declarationTrue) if __name__ __main__: generate_en_resources()这段脚本在单语言简单场景下能跑通但生产环境还有很多细节要处理带占位符的字符串如%1$s会被翻译API当作普通文本处理格式符号可能被翻没带格式化标签的字符串如b、a href需要做特殊保护复数资源plurals的翻译策略跟普通字符串不同。所以脚本只是起步最后一定要人工走查一版不能完全信任机器翻译。4.2 辅助工具资源翻译检查和lint自动化即便有了脚本项目依然可能因为某条字符串漏翻而出现界面一半中文一半英文的尴尬。Android本身提供了lint规则来检查多语言缺失在Android Studio里执行./gradlew lintlint报告里会列出MissingTranslation和ExtraTranslation问题。前者告诉你某条默认字符串在某个语言目录中没有对应翻译后者告诉你某个语言目录里有些字符串在默认语言里已经不存在了大多是删接口时忘了清理翻译。这两个规则看着简单实际帮助非常大。我一般在提交MR前跑一遍凡是新增的英文文案都要保证所有目标语言同步更新。4.3 翻译内容质量机器翻译完一定要有确认环节机器翻译最大的问题不是“能不能翻”而是“翻得对不对”。产品里用户能看到的所有文案都影响品牌调性出现低级翻译错误非常败好感。我踩过一个例子品牌Slogan被直译成一句完全不通的外文团队没人懂那个小语种直到海外用户发截图吐槽才发现。所以我的个人实践是机器翻译的结果仅作为初稿主流语言英、日、韩交给真人翻译或至少让懂语言的同学过一遍小语种用机器翻译后要让人工质检抽检重点检查术语一致性。如果项目有预算优先找专业翻译供应商机器翻译负责高效生成初稿人工负责精修定稿这是目前行业里性价比最高的方式。5. 翻译项目里那些必踩的坑逐个记下来做过一轮完整的多语言/翻译落地下面这些问题的出现率几乎是100%。我把它们整理成“问题-原因-解法”的形式方便你排查时快速对照。问题现象常见原因我的处理办法切换语言后部分界面仍显示旧语言硬编码字符串未走资源引用项目里用lint HardcodedText规则强制代码评审时拦截动态拼句翻译后句子乱序原文包含%s等占位符翻译时保护占位符译文里的%s数量必须一致阿拉伯语布局反了但有些控件位置不对未开启RTL适配布局上用start/end替代left/right检查ViewPager2循环方向日期和时间显示不对代码里直接用硬编码格式用DateFormat和locale一起格式化不要写死MM/dd/yyyy某些语言文本在按钮里显示不全默认英文短翻译后变长德语尤其明显设计稿预留20%-30%空间给TextView设置maxLines和ellipsize首次翻译慢用户等太久大段文本串行调用API批量翻译加缓存服务端把相同文本缓存住前端加loading状态用户切换语言后App崩溃资源目录缺失或locale变更后Activity未重建加try-catch兜底res下所有语言目录保持一致5.1 硬编码字符串多语言最大的隐形杀手这个问题写在表格里但值得单独拎出来多说几句。硬编码字符串意味着翻译工作做得再多总有几个角落里漏掉。实际排查时我不能只看源码还得检查第三方SDK是否自带中文资源、BaseApplication里有没有对系统默认语言做特殊处理、WebView里的网页文案是不是由后端直接下发的。我给团队的代码规范有三条布局和代码里禁止出现中文或英文原文只能引用string/。新建字符串必须同时提交默认语言翻译哪怕是先用占位符也要保证占位一致。CI持续集成脚本里跑一次lintHardcodedText和MissingTranslation视为错误级别直接让流水线失败。规矩定在前面比上线后补窟窿省事一百倍。5.2 格式化字符串和复数机器翻译最容易翻车的区域字符串资源里会有%1$s、%2$d这类占位符比如string nameorder_count共 %1$d 条订单/string翻译成英文时由于语序变化原文的位置指示符可能完全失效机器翻译容易把占位符弄丢或者重复。翻译前我先给翻译人员一份术语表并说明占位符不能动如果走API翻译则用一种变通方法把占位符替换成特殊标记比如__PH1__翻译完成后再替换回来。同样plurals复数是Android提供的多数量语言处理机制英语有1条和一个多于一条的区别中文则没有复数形态。翻译时不能简单复制原文的plurals结构得让翻译者判断目标语言的数量词规则并正确填写所有quantity字段。5.3 文件编码和特殊字符XML资源文件里的暗坑生成的strings.xml如果编码不对轻则乱码重则编译不过。很多翻译API返回的是UTF-8文本但有些平台默认输出GBK或者Windows机器上Python脚本写文件没有指定编码就会把资源文件写成GBK导致中文乱码。写文件时一定要显式encodingutf-8。此外XML里有保留字符文本内容里出现时必须写成amp;出现或时要写成lt;/gt;。脚本生成时只想着拼接字符串很容易漏掉这步到编译时报错才想起来。5.4 切换语言后的Activity重建与缓存清理运行时切换语言后所有已经存在的Activity都要响应语言变化最直接的做法是重建。但重建后会丢失页面状态用户正在填写的表单内容也会丢失体验很糟糕。我的建议是把用户选中的语言持久化在Activity.onCreate()里应用切换语言时对需要保留状态的页面用onSaveInstanceState()保存数据再调用recreate()。同时还要注意图片、字体、SharedPreferences缓存里如果存了根据语言生成的内容也要清理或按语言区分存储否则切完语言后旧缓存图片还会在页面上出现。5.5 动态语言列表和维护成本别一开始做全语言很多团队一上来就想支持30种语言结果后续每次加文案都痛苦得想放弃。我的经验是先小后大首发只支持默认语言、英文、一个目标市场语言上线验证后再逐渐扩展。多语言不只是翻译成本每次版本更新都要同步更新所有语言文件这是持续的维护负担。真上线后优先看各语言活跃度和用户反馈再决定扩展方向不需要为了“齐全”而堆语言数量。最后再分享两句体会这两年做了不少国际化项目最大的感受是多语言/翻译这个需求技术本身并不难难的是把流程理顺、把边界想清楚。你在哪个环节先定义清楚“翻界面还是翻内容”后面就不会走弯路你在代码规范上多花一周功夫后面每个版本都能省一周。别迷信“接个SDK自动翻译就完事了”——合规、质量、维护这三座大山靠的是持续投入。如果非让我给一句建议第一步永远是重新审视项目里所有用户可见的文本把硬编码清理干净。这个基础不打牢其他一切优化都是空中楼阁。
返回列表