
前两年我把一个基于 Flutter 的老项目往 OpenHarmony 上迁移时心里最没底的不是容器适配也不是 PlatformView 能不能用反而是看起来最不起眼的多语言国际化。原因很简单OpenHarmony 的 locale 解析、字体回退、资源产物和 Android/iOS 都不一样照着老经验写出来的国际化代码在鸿蒙设备上经常出现“英文正常、中文变方块”“系统切语言 App 不响应”“日期格式多出一个时区偏差”这类诡异问题。这篇文章以我手上的“万能游戏库 App”为例完整梳理一遍在 Flutter for OpenHarmony 上做多语言国际化的落地过程从 ARB 文件怎么写、gen-l10n 怎么配到 MaterialApp 里怎么动态切语言、怎么和原生侧通信再到实际调试中踩过的一堆坑。不管你手头是游戏库、工具类还是内容社区类的 App只要你的 Flutter 项目需要跑到鸿蒙设备上这条链路基本都是通用的。文章不会只贴结论我会把每一步为什么这么做讲清楚方便你根据自己项目的实际情况调整。1. 为什么 OpenHarmony 上的 Flutter 国际化不能直接照搬 Android/iOS 的经验先说一个很多人容易忽略的事实Flutter 的国际化机制本身是跨平台统一的但“读取系统语言”“加载字体”“打包 emoji/日期符号数据”这些能力每一层都依赖底层平台的具体实现。OpenHarmony 对 Flutter 的适配层flutter_ohos在几个关键点上和 Android 有差异如果你只是把以前 Android/iOS 项目的国际化代码原样搬过来大概率会在鸿蒙设备上翻车。1.1 locale 解析顺序和语言标签差异Flutter 在启动时会通过PlatformDispatcher.instance.locales拿到系统当前的语言列表然后交给MaterialApp的localeResolutionCallback去做匹配。在 Android 上系统返回的语言标签通常是zh-CN、en-US这种 BCP 47 格式但在 OpenHarmony 上不同厂商定制系统返回的标签格式并不完全统一我见过zh-Hans-CN、zh-CN、zh-Hans混着来的情况。这里有个隐蔽的坑如果你在代码里直接比较字符串比如locale.toString() zh_CN那在标签格式不一致时就会匹配失败。正确的做法是永远基于languageCode和scriptCode做判断而不是比字符串。比如判断是否中文应该看locale.languageCode zh简体/繁体的区分再看locale.scriptCode或locale.countryCode。另一个差异是 locale 的解析时机。在 Android 上如果系统语言中途改变Flutter 的didChangeLocales回调会及时触发但在部分 OpenHarmony 设备上这个回调触发时机偏晚甚至需要重启 App 才能生效。这个问题我在后面的动态切换章节会专门讲应对方案。1.2 资源打包裁剪带来的“隐形缺数据”OpenHarmony 的 HAP 打包机制会把资源做压缩和裁剪这和 Android 的 AAB/APK 资源合并逻辑不一样。Flutter 的flutter_localizations和intl依赖了一份完整的 locale 数据包括日期符号、数字分隔符、货币格式等这些数据在 Android 上通常会被完整打包但在鸿蒙的 HAP 产物里有概率被裁剪掉部分语言数据。我实际遇到的情况是App 里切到法语、阿拉伯语时日期格式化直接抛LocaleDataException报错信息大概意思是“找不到该 locale 的日期符号数据”。排查后发现不是intl依赖没加而是 HAP 打包时把用不到的语言数据过滤了。针对这个问题比较稳妥的做法是在pubspec.yaml里显式声明你需要的语言资源并且不依赖intl的隐式加载所有日期/数字格式化都自己传入 locale 参数。1.3 字体回退链完全不同OpenHarmony 系统默认字体是 HarmonyOS Sans中英文混排时它有自己的回退优先级。但 Flutter 层如果给某个Text组件显式指定了 fontFamily比如只指定了某个西文字体那中文字符在鸿蒙上可能直接渲染成豆腐块因为 Flutter 的字体回退机制不会像系统原生那样自动去系统字体里找中文字形。这个问题的排查难度在于同样的代码在 Android 上完全正常因为 Android 的字体回退链覆盖广换到鸿蒙上就变成“某些页面中文全没了”。我在第五章会给出具体的 fontFamilyFallback 配置方案这里先提醒一句在 OpenHarmony 上做国际化字体策略一定要单独测不能依赖“Android 上没问题”的经验。1.4 为什么选 gen-l10n 而不是第三方方案现在 Flutter 社区里有不少国际化方案比如easy_localization、i18n_extension还有各种自己手写Localizations类的做法。我的结论很明确新项目或者准备长期维护的项目直接用官方gen-l10n就好。官方方案的类型安全做得最彻底。ARB 文件里定义的每一个 key生成代码后都会有对应的强类型方法写错 key 名编译期就报错而不是运行时显示一个缺失文案的 key 字符串。更关键的是gen-l10n生成的AppLocalizations和flutter_localizations是官方同一个技术栈两者在 locale 匹配、日期符号加载上的协作最顺畅。第三方方案在鸿蒙适配时一旦出问题你基本找不到人能帮你排查。2. 工程初始化与 l10n 配置细节确定了用官方 gen-l10n 之后接下来就是把工程底子打好。这个阶段配置错了后面写再多 ARB 文件都是白费所以我建议你把这个章节当成“照着抄就行”的 checklist 来看。2.1 环境准备Flutter SDK 与 OpenHarmony SDK 的搭配先在开发机上装好支持 OpenHarmony 的 Flutter SDK。目前社区主流的做法是从 OpenHarmony 官方 Gitee 仓库拉 flutter_flutter 的 OpenHarmony 分支然后配合 DevEco Studio 一起使用。需要注意版本匹配不同的 OpenHarmony API 版本对应不同的 Flutter 分支装错版本会导致编译阶段直接报错。日常开发和调试我推荐在 DevEco Studio 里启动鸿蒙模拟器跑 Flutter 项目。模拟器版本选择上优先选 API 9 以上的系统镜像因为太老的镜像对 Flutter engine 的支持不完整容易出现页面白屏或渲染异常容易被误判成国际化代码的问题。2.2 依赖声明与 pubspec 配置国际化相关的依赖其实只有两个别多装。在pubspec.yaml里加上dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter intl: any flutter: generate: truegenerate: true这个配置是关键。打开它之后每次flutter run或flutter build都会自动触发代码生成你不需要手动跑 gen-l10n 命令。但有一点要注意如果你在 IDE 里开启了热重载改完 ARB 文件后通常需要手动执行一次flutter gen-l10n才能看到效果这个我在常见问题章节会细说。intl的版本这里写了any官方推荐是intl: ^0.19.0或更高版本但我建议不要锁死小版本因为flutter_localizations内部对intl有版本约束锁得太死容易在pub get时产生依赖冲突。2.3 l10n.yaml 配置文件逐项讲解在项目根目录新建l10n.yaml这是 gen-l10n 的核心配置。我用的配置长这样arb-dir: lib/l10n template-arb-file: app_zh.arb output-localization-file: app_localizations.dart output-class: AppLocalizations output-dir: lib/generated nullable-getter: false synthetic-package: false untranslated-messages-file: untranslated.json每一项的作用我拆开讲arb-dir存放 ARB 文件的目录建议固定在lib/l10n方便统一管理。template-arb-file模板文件也就是你所有文案的“主语言”。我用中文app_zh.arb作为模板因为我的主力用户是中文用户主语言文案最全其他语言文件以它为基准做翻译。output-class和output-localization-file生成出来的类名和文件名这里定义了AppLocalizations后面代码里到处要用到它。nullable-getter: false生成AppLocalizations.of(context)时返回非空类型。默认如果没找到匹配的 locale 会返回 null你在业务代码里就得到处做空判断改成 false 后找不到 locale 时会自动 fallback 到模板语言代码会干净很多。前提是你必须把supportedLocales配置好。synthetic-package: false把生成代码输出到真实目录而不是虚拟包。这样做的意义是生成代码可以被 IDE 索引跳转定义时能看到具体实现排查问题方便得多。untranslated-messages-file导出未翻译的文案清单方便你在 CI 流程里检查遗漏。2.4 ARB 文件目录结构与最小示例在lib/l10n目录下我会放置这样几个文件lib/l10n/ app_zh.arb app_en.arbapp_zh.arb最小内容示例{ locale: zh, appTitle: 游戏库, tabHome: 首页, tabMine: 我的, downloadCount: {count} 次下载, downloadCount: { placeholders: { count: { type: int } } } }这里locale是必须的gen-l10n 靠它识别语言文件名里的zh会和它做校验不一致会报错。downloadCount这种带占位符的字符串必须在对应的 metadata 里声明placeholders的类型否则生成代码时没法确定参数类型是 int 还是 String。3. ARB 文件编写与代码生成实战配置好工程后真正花时间的其实是 ARB 文件本身的编写。很多人觉得翻译文案很简单写起来才发现“同一句话在不同语言里语序不一样”“单复数形式完全不同”“日期格式一换语言就乱掉”这些才是国际化工作的真正难点。3.1 占位符与复数最容易翻车的两件事先说占位符。中文里“5 次下载”和英文里 “5 downloads” 结构差不多但换成“该游戏支持 2 人联机”中文是“数字 单位 动作”某些语言里可能是“动作 数字 单位”。所以千万不要在代码里写$count downloads这种字符串拼接而是把整句话放到 ARB 文件里让翻译人员决定语序。这个原则叫“完整句子优先”是国际化里最基础也最重要的一条。复数问题则更隐蔽。英文有单数/复数两套形式中文有“零、一、二、多”多套量词逻辑俄语还有更复杂的复数规则。gen-l10n 通过 ICU MessageFormat 语法来处理一个典型的例子playCount: {count, plural, 0{暂无下载} 1{下载 1 次} other{下载 {count} 次}}生成代码后你会发现playCount方法的签名里直接接收一个count参数会自动根据传入的数字做匹配。在中文文案里0、1、other三段可能看起来是重复的但还是建议全部写出来因为英文版本真的需要单复数之分。3.2 日期和时间格式化必须显式传 localeARB 文件里不建议直接写“2024年1月5日”这种硬编码的日期文案因为不同语言环境下格式完全不一样。正确做法是在生成代码里用DateFormat配合AppLocalizations的 locale 参数做格式化。我通常的做法是给日期字符串定义成带占位符的模板然后传入格式化好的日期字符串String getDateText(String formattedDate) { return l10n.dateLabel(formattedDate); }而formattedDate本身在业务层用DateFormat.yMMMd().format(DateTime.now())生成这个DateFormat会从Localizations.localeOf(context)自动读取当前语言。这样换语言时日期显示格式会自动跟随变化而不需要为每种语言手工维护一套“几月几号”的翻译。3.3 生成代码的产物与使用方式在lib/l10n目录下准备好app_zh.arb和app_en.arb后执行flutter gen-l10n生成的代码默认在lib/generated/下核心文件是app_localizations.dart它导出了AppLocalizations类和AppLocalizations.delegate。调用文案的方式有两种一种是传统写法AppLocalizations.of(context)!.appTitle另一种是 gen-l10n 附带生成的扩展属性context.l10n.appTitle。我推荐后者写法更简洁也不容易忘记空判断。有一点要特别提醒生成目录里的代码是自动生成的不要手工改动。哪怕你只是想临时改一个单词也应该回到 ARB 文件里改重新执行生成命令。否则下次生成时你的手改会被直接覆盖造成“改了但没生效”的困惑。3.4 用 part 组织生成的代码时的注意事项如果你的项目已经在用part指令做模块化组织比如想把AppLocalizations的扩展方法拆到其他文件里这部分要格外小心。gen-l10n 默认生成的app_localizations.dart是独立库它不会主动参与你的 part 体系。如果你确实需要把相关代码纳入 part 结构我的建议是不要试图修改生成文件的头部来适配part of而是单独写一个扩展文件通过extension AppLocalizationsX on BuildContext的方式做二次封装这样既能保持生成代码纯净又能让业务代码统一走你的封装入口。将来升级 Flutter SDK 导致生成代码结构变化时你的封装层不受影响。4. MaterialApp 加载语言与动态切换方案ARB 文件写好了生成代码也出来了接下来就是把它接到MaterialApp上并实现运行时的语言切换。这个章节是整个国际化方案里最容易出各种“奇奇怪怪问题”的地方尤其是动态切换时页面状态丢失、原生控件语言不跟随这类我会一个个讲。4.1 注册四个 delegates一个都不能少在MaterialApp里配置localizationsDelegates新手经常会漏掉。标准配置如下MaterialApp( locale: _locale, supportedLocales: const [ Locale(zh), Locale(en), ], localizationsDelegates: const [ AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], )这四个 delegate 各有分工AppLocalizations.delegate加载你自己定义的业务文案GlobalMaterialLocalizations.delegate负责 Material 组件内置文案比如日期选择器、对话框按钮的“确定/取消”GlobalWidgetsLocalizations.delegate负责 widget 层的语义文案最后一个GlobalCupertinoLocalizations.delegate经常被忽略但如果你用了Cupertino系列组件或者某些 Material 组件内部依赖了 Cupertino 的本地化文案漏掉它就会在运行时收到 “Localizations not found” 的报错。4.2 首帧语言匹配本地偏好优先系统语言兜底很多 App 要求“用户手动切换语言后App 始终记住用户选择而不是跟随系统语言变化”。这个逻辑要在localeResolutionCallback里实现而不是简单地把locale设成系统语言。我的做法是启动时先读本地缓存的语言偏好如果有就返回用户偏好没有就把系统语言作为默认。这段逻辑写成代码大致是localeResolutionCallback: (locale, supportedLocales) { final savedLang _prefs.getString(app_language); if (savedLang ! null) { return Locale(savedLang); } for (final supported in supportedLocales) { if (supported.languageCode locale?.languageCode) { return supported; } } return const Locale(zh); }这里有一个细节supportedLocales列表里我写的是Locale(zh)和Locale(en)没有写具体的国家地区。这样zh能同时匹配zh_CN、zh_TW、zh_HKen能匹配所有英语地区。如果你在列表里写了Locale(zh, CN)那繁体中文地区的用户就不会匹配到中文会直接掉到 fallback 英语这个坑很容易踩。4.3 动态切换语言用全局状态驱动 MaterialApp 重建实现语言热切换的关键思路是让MaterialApp的locale参数变成响应式的切换语言时更新全局状态从而触发整棵 widget 树重建语言相关的配置。我用的是一个简单的ChangeNotifier不引入重量级状态管理库也有很好效果class LocaleProvider extends ChangeNotifier { Locale _locale const Locale(zh); Locale get locale _locale; Futurevoid setLocale(Locale locale) async { _locale locale; notifyListeners(); await _saveToPrefs(locale.languageCode); } }然后在main.dart里把MaterialApp包进ListenableBuilderListenableBuilder( listenable: _localeProvider, builder: (context, _) { return MaterialApp( locale: _localeProvider.locale, ... ); }, )这样的好处是切换语言只触发MaterialApp层级的 rebuild底层已构建好的路由栈不会销毁。也就是说用户在“游戏详情页”切完语言页面只是刷新文案点击返回能回到原来的列表位置不会跳回首页。如果你用的是Cubit或Bloc这类状态管理库思路完全一样只需要把ChangeNotifier换成语境对应的状态类关键是保证locale变化能驱动MaterialApp的locale参数更新。4.4 语言切换后与原生侧通信EventChannel 的正确用法游戏库 App 里难免有原生控件场景比如登录页嵌了鸿蒙原生的验证码组件、分享面板调用的是系统服务。这些原生界面里如果写死了中文或英文就会和 Flutter 层切换语言后文案不一致。我的做法是切换语言后通过EventChannel主动通知鸿蒙原生侧。具体来说在 Flutter 端const _languageChannel EventChannel(com.example.app/language); _languageChannel.receiveBroadcastStream().listen((event) { // 接收原生侧发来的语言状态 });而在切换语言的方法里const _methodChannel MethodChannel(com.example.app/language); await _methodChannel.invokeMethod(setLanguage, {lang: _locale.languageCode});原生侧收到setLanguage后同步更新所有原生页面的本地化文案。这里要注意的是 EventChannel 和 MethodChannel 的分工Flutter 主动通知原生用 MethodChannel 更直接原生主动推送状态变化给 Flutter 才用 EventChannel。别搞反了否则调试时很难定位是通信时机问题还是参数传递问题。4.5 切换语言后的页面状态保持语言切换后常见的两个状态问题我实测踩过并解决了第一个是TabBar索引跳回第一页。原因是很多 App 的TabBarView在语言切换时因为MaterialApprebuild 导致整个 tab 页面树重建。解决办法是让 tab 索引状态不要依赖DefaultTabController而是显式用一个ValueNotifierint保存索引传给TabBar和TabBarView这样 rebuild 时索引不会丢。第二个是用户已滚动到很长的列表位置丢失。比如游戏列表页切语言后回到顶部体验很差。这个问题本质上不是国际化的问题而是你切换语言时把整个列表页的ScrollController状态也一起重建了。解决办法是不要把列表页的滚动位置状态放在build方法里创建的局部变量中应该提升到页面 State 的成员变量或者用PageStorageKey保存滚动位置。5. 针对 OpenHarmony 的落地细节与调试技巧前几章的内容在 Android 和 iOS 上也基本适用但这一章我要专门讲只有在鸿蒙设备上才会遇到的问题。如果你手上暂时没有 OpenHarmony 设备建议先把这章收藏等真机调试时回来看。5.1 DevEco 模拟器上的系统语言设置差异我在鸿蒙模拟器上做首轮测试时发现系统设置里的语言列表不一定包含你 App 声明的所有语言。比如某些厂商定制系统语言列表里只有简体中文、英文、繁体中文等少数几个选项。如果你的 supportedLocales 里声明了日语、韩语但系统设置里根本没有日语选项那么即使用户是日语使用者他也无法在系统层面切到日语你的 App 在首帧时会直接落到 fallback。针对这种情况有两个应对策略一是把用户手动切换语言的功能放在 App 内部不依赖系统设置二是在localeResolutionCallback里打印调试日志确认设备实际回传的 locale 到底是什么。调试时可别只看模拟器界面的语言选择要在代码里debugPrint(PlatformDispatcher.instance.locales)看到真值才靠谱。5.2 字体回退配置HarmonyOS Sans 与 fontFamilyFallback前面提到了鸿蒙字体回退的问题这里给具体方案。如果你在 App 里使用了自定义字体尤其是一套只有拉丁字符的英文字体必须给它配置中文回退。在TextStyle里有一种写法TextStyle( fontFamily: MyLatinFont, fontFamilyFallback: [HarmonyOS Sans, sans-serif], )这样在显示中文时Flutter 会优先走fontFamily发现没有对应字形就回退到fontFamilyFallback列表里的字体。在鸿蒙上把HarmonyOS Sans放第一位通常是对的即使你的设备上没有显式注册该字体系统也会在回退过程里处理。还有一点容易被忽略全局设置ThemeData里的fontFamily会影响所有文本如果你的全局字体只设置了英文字体那所有中文都会出问题。排查思路是先看单个 Text 是否显式指定了字体再看全局 Theme 是否指定了不合适的字体最后才是系统层面的字体缺失。5.3 Impeller 渲染引擎与文本测量问题Flutter 在 OpenHarmony 上的渲染后端目前还是以 Skia 为主但官方也一直在推进 Impeller 的适配工作。我实测下来在鸿蒙设备上开启 Impeller 后某些中英文混排的长文本换行位置会和 Skia 后端不同具体的表现是同一个字符串同一屏宽度下换行位置变了可能导致部分布局出现轻微遮挡。这个问题在国际化场景下容易放大因为欧美语言的文本普遍比中文长。如果你的布局是按中文长度设计的切到英文后文本溢出再叠加渲染后端差异排查起来会非常头疼。我的建议是在鸿蒙设备上做语言切换测试时至少要跑一遍所有长文案场景英文、德文这种长单词语言特别容易暴露溢出问题。不要因为是“同一个 Flutter 版本”就忽略渲染后端的差异。5.4 包体控制语言数据按需加载与裁剪flutter_localizations会引入大量语言的日期、数字符号数据如果全量打包对鸿蒙 HAP 的包体影响不小。游戏库这类中大型 App 对包体敏感所以我会做两个裁剪操作第一在l10n.yaml里通过supportedLocales或生成配置只保留目标语言。比如只做中英文就不要让 gen-l10n 为所有语言生成 getter。第二在pubspec.yaml里如果某些依赖库允许挑选 locale 子集用--dart-defineFLUTTER_LOCALIZATION_LOCALESzh,en这类参数在构建时缩减数据。这个参数不一定在所有版本可用但方向是对的凡是能按需加载的语言数据都不要贪多。顺带提醒做这些裁剪操作后一定要在鸿蒙真机上重新验证“切阿拉伯语”“切泰语”这类极端场景因为裁剪过头会直接导致缺失语言数据而崩溃而模拟器上因为数据缓存原因有时候反而测不出来。6. 常见问题与排查技巧实录这一章是从多个项目里整理出来的高频问题清单。大部分问题我在前文已经点到过这里集中做一个速查配上我排查时的思路。问题现象根本原因排查办法改完 ARB 文件热重载不生效gen-l10n 的代码生成不会随热重载自动触发手动执行flutter gen-l10n或重启flutter run系统语言切换后 App 不响应鸿蒙部分设备didChangeLocales时机不稳在 App 内提供手动切换入口不依赖系统事件中文显示成方块字体回退链断掉指定字体无中文字形配置fontFamilyFallback检查全局 Theme 字体日期格式化抛 LocaleDataExceptionHAP 打包裁剪了 intl 语言数据显式声明所需语言资源验证 build 后资源完整性切换语言后 TabBar 跳回第一页页面树重建导致 tab 状态丢失用ValueNotifierint显式保存 tab 索引PlatformView 内原生控件文案不跟随原生侧不知道语言切换事件通过 MethodChannel 主动通知原生刷新英文文本溢出遮挡布局按中文长度设计翻译后文本变长所有动态文本容器预留边距测试长语言首帧语言匹配成英文supportedLocales 列表不完整或 fallback 语义不对检查 locale 匹配逻辑打印平台实际 locale 列表6.1 改完 ARB 文件热重载没反应这个问题几乎每个用 gen-l10n 的人都会遇到。原因是代码生成发生在编译之前热重载并不会感知 ARB 文件的修改。解决办法很简单回到终端执行flutter gen-l10n让它重新生成 Dart 代码然后再热重载。如果你用的是 Android Studio 或 DevEco Studio 里的 run 模式可以把它理解成“改完资源后要先编译一次资源再加载 Dart 代码”。6.2 切到某些语言后 App 直接崩溃这类崩溃大概率是 locale 数据找不到。常见场景是supportedLocales里声明了Locale(fr)但intl的日期符号数据里没有fr运行时 build 日期格式就炸了。排查时看崩溃堆栈里有没有LocaleDataException有的话就回到 5.4 节的“语言数据按需加载”部分检查你的裁剪配置。6.3 PlatformView 嵌入原生控件文案语言不统一游戏库 App 里如果嵌了原生广告、原生地图或系统相册选择器这些组件走的不是 Flutter 的 Localizations。我的经验是所有需要和原生打交道的文案都走一遍我们项目里统一的“语言同步通道”。简单说就是切换语言时除了更新 Flutter 内部状态同时调用 MethodChannel 把当前语言告诉原生层让原生层自己更新界面。如果你发现某个原生控件语言没变先检查原生侧有没有监听对应通道再检查事件是不是在setState之前发的——顺序错了也会丢消息。6.4 首帧语言标签匹配错误在鸿蒙设备上系统设置里选择“简体中文”后PlatformDispatcher可能返回Locale(zh, Hans, CN)或Locale(zh, CN)两种格式。如果你的supportedLocales里写了Locale(zh, CN)那么遇到Locale(zh, Hans, CN)就可能匹配不上。我之前给过一个办法语言匹配永远只用languageCode判断除非你要明确区分简繁。这条规则在国际化项目里应该作为铁律写进团队规范。6.5 字符串拼接导致翻译不自然最后这条不算 bug但影响品质。很多游戏库 App 会把“查看全部”和“下载量”分开写再用$text1 $text2拼起来。这种拼接在中文里看着正常翻译到英文、日文就可能变成“View All 5 Downloads”这种别扭形式。我建议所有需要组合的文案都整句进 ARB哪怕要传三四个参数。翻译文本有整句上下文语言质量会高很多也方便后续接入专业翻译团队。7. 多语言文件的团队协作与长期维护国际化不是一个一次性的编码任务。尤其在游戏库这种快速迭代的项目里每次发版都有新游戏文案要加、新活动页要加语言。如果团队里多人同时改 ARB 文件很容易产生冲突而且翻译内容本身也需要审核流程。最后这一章聊聊我在协作和维护层面沉淀下来的经验。7.1 ARB 文件的命名与版本管理约定我给团队定的规范是ARB 文件按语言分文件文件名统一app_langCode.arb。中文模板app_zh.arb永远作为主文件新增 key 先加在中文模板里然后其他语言的翻译文件在它的基础上补充。多人协作时ARB 文件的冲突比较常见。因为 JSON 结构简单Git 合并冲突通常能自动解决但为了减少冲突面我要求每次提交只改自己负责的那几个 key不要在同一个提交里大范围重排字段顺序。另外建议配置 CI 检查如果某个语言文件缺少模板文件中的 key构建直接失败。这样能避免“中文有、英文没有”发布上线后才被发现。7.2 生成代码不要手工改但可以加封装层前面说过生成目录里的文件不要手改。但业务代码里直接到处写context.l10n.xxx将来如果生成代码出现破坏性变更改起来会想哭。我习惯在业务代码和生成代码之间加一个薄薄的封装比如抽取AppStrings类统一暴露所有文案方法。这样生成代码的内部实现变了业务层基本不用动。这个封装层在 Flutter SDK 升级时特别值钱。7.3 翻译文案需要“语境注释”ARB 文件里的keymetadata 里除了placeholders官方还预留了description字段。我强烈建议把文案出现的场景写在里面比如“用于游戏详情页下载按钮下方展示累计下载次数”这样翻译人员不会把语境弄错。尤其是“Play”这种词名词动词不分没有语境注释很容易翻错。我的做法是把 description 作为必填项写不出来的业务人员说明这个 key 本身定义得有问题。7.4 多语言自测清单我每次发版前必跑一遍发版前面临的多语言问题多数不是代码逻辑问题而是“某些页面漏了翻译”“某些语言下布局崩了”。我整理了一份自测清单每次覆盖多语言发布都会跑一遍所有一级页面首页、分类、我的在每种语言下截屏对比。游戏详情页的动态字段模拟数字超过 10000、文本超过 200 字符的场景。切换语言后回到首页tab 索引和滚动位置保持不变。系统切换语言后杀掉 App 冷启动验证首帧语言偏好逻辑。在真机上验证日期、数字、货币格式是否按当前语言显示。检查所有 PlatformView 原生组件文案是否同步切换。检查推送通知里的文案是否跟随 App 内语言选择。这份清单看着繁琐但大多数国际化事故都能在上面几个环节提前暴露。省下来的线上投诉远比测试成本值。8. 几个容易被忽略的小细节个人经验向最后再分享几个我在多个项目里验证过的小经验不构成完整章节但每一个都能帮你少踩点坑。第一关于语言偏好存储。很多人喜欢用shared_preferences存语言代码这没问题但注意存储的 key 要独立于其他配置项并且写入时要做校验。曾经遇到过一个线上问题用户设备上app_language被第三方清理工具清空了导致每次冷启动语言都变回系统语言用户以为自己的设置丢了投诉了好几次。后来改成写入同时校验值是否在 supportedLocales 里不在就丢弃问题才解决。第二关于文本溢出检测。在游戏列表页和详情页建议在 debug 模式下开启Text控件的溢出检测或者干脆在开发期用一个脚本跑所有页面截图把每张截图里的溢出标记全部标红。英文文本比中文文本长 30% 到 50% 是常态游戏名称、活动标题这种不知道多长的字段是最容易溢出的地方。第三关于 OpenHarmony 设备上的测试覆盖。不同厂商的鸿蒙定制系统语言标签格式和字体回退表现都会有差异。有条件的话至少找一台纯 OpenHarmony 设备、一台主流厂商设备分别测一遍。我自己遇到过“某品牌手机上中文显示正常、英文换行位置异常”的案例最后定位到是厂商在系统字体层面做了特殊处理这个问题只靠模拟器根本看不出来。第四关于国际化和项目架构的先后顺序。真的越早把语言机制设计进去越好。如果项目已经跑了两三年几百个页面都直接写死中文再来补国际化那工作量是推倒重来的级别。我见过太多团队在需求爆发的阶段把“先写死中文后面再说”当口头禅结果后面永远没空补。从第一天就接上 gen-l10n哪怕只做中文后期加语言的成本也不会太高。