Flutter 在 OpenHarmony 上跑起来之后,第一个让我头大的不是渲染性能,也不是插件适配,而是国际化。项目早期只有几十个 Key-Value 字符串,直接写个 Map 随手取用也没觉得有什么问题,等翻译文案涨到几百条,再叠加参数、复数、多语言切换,代码里到处是魔法字符串,改一个 key 要全局搜索半天。这时候我注意到 slang 这个 i18n 代码生成工具,它能在编译期把翻译文案变成类型安全的 Dart 对象,彻底告别手写 key。这篇内容就是我在 Flutter for OpenHarmony 下使用 slang 的完整落地记录,从环境准备、配置生成到鸿蒙工程集成,以及我踩过的坑,希望能帮到要做多端统一国际化的团队。
1. 为什么我放弃手写 Key-Value:slang 的类型安全到底在解决什么
1.1 项目里那些让人崩溃的字符串 Key
早期做 Flutter 国际化,最容易上手的方式就是建一个 Map<String, String> 或者一个静态类,里面写满 static const String homeTitle = 'xxx'。项目小的时候这样确实够用,但一旦翻译条目多起来,问题就很现实:英文、简体中文、繁体中文各一份,还有日语、韩语,每次新增文案要在好几个文件里同步改一遍,漏一个就出现空白页面。
更难受的是 key 本身没有约束。比如原来定义 orderDetail => 'Order Detail',后来产品要把这个字段改成 orderInfo,你需要全局搜一遍所有引用点。如果有拼写错误,框架直接把这个 key 原样输出到 UI 上,用户看到一坨英文标识符,这属于线上事故级别的问题了。
还有参数问题。翻译文案里经常有 Hello, {name} 这种占位符,手写 Map 时没人约束你传几个参数,少传了运行时报错,多传了没人提醒。复数更是重灾区,英文有单复数区分,中文没有,用 Map 自己拼字符串,遇到列表数量变化的文案基本靠感觉写。
1.2 官方 gen-l10n 的边界在哪
Flutter 官方其实提供了 gen-l10n 工具,配合 ARB 文件也能生成 Dart 代码。它的思路是通过 flutter_localizations 那套机制,把 AppLocalizations.of(context) 变成可调用的方法。很多项目用这个方案解决了一部分手工维护的问题,但我自己的体验是它有几个硬伤。
第一,生成的 API 是方法调用,比如 AppLocalizations.of(context)!.helloWorld,虽然 key 有了编译期检查,但方法名、参数做得比较死板,复制变化、复数处理、条件文案这些高级能力写起来非常繁琐。第二,ARB 文件虽然格式统一,但写起来并不轻松,占位符要实现成元数据字典,可读性不高。第三,它对多语言回退、运行时动态改语言的支持不算顺手,想做一个 App 内切换语言,你要自己维护 MaterialApp 的 locale 状态,还得处理 locale 回退链。
不是说 gen-l10n 不能用,而是当项目到了几百个 key、四种语言以上,并且要支持参数和复数时,它的开发体验不够高效。
1.3 类型安全到底在安全什么
slang 的核心卖点是“类型安全”,很多人理解成“编译器帮我检查 key 是否存在”,这只是表面一层。真正有价值的是下面几件事。
一是 IDE 补全和重构。生成的 Dart 类里每个翻译条目都是实际存在的字段,比如 t.home.title,你输入 t.home.,编辑器会列出这个 namespace 下所有可用的翻译项。复制、重命名也完全交给 IDE,换个 key 名称,所有引用点一起变,不会再出现漏改。
二是参数个数和类型检查。如果翻译项声明了 {count} 占位符,生成的翻译方法就一定是 t.cart.itemCount(count),少传一个参数编译直接报错。这个约束看起来简单,实际省了我大量测试时间,因为运行时错误变成编译错误后,回归测试范围小很多。
三是复数类别的显式表达。slang 会根据翻译文件里的复数形式,在 Dart API 中生成对应的条件分支。中文没有复数概念,英文有,你不需要自己在代码里写 count > 1 ? 'items' : 'item',工具帮你处理了 locale 对应的复数规则。
四是平台之间的行为一致性。同一个翻译文件,在 Android 上显示英文、在 OpenHarmony 上显示中文,因为翻译数据源相同,展示行为是一致的。对比一下我早期用 MaterialApp 内置的 locale 加手写 Map 的方式,平台差异导致的问题很少,因为解析逻辑统一放在生成代码里了。
1.4 slang 的关键能力和适用场景
slang 官方支持的翻译文件格式有 JSON、YAML、ARB、CSV,默认我建议用 YAML,层次清晰,注释友好。生成方式也很简单,slg generate 一条命令,支持监听模式 slg watch,文件保存后自动生成。
它最大的好处是生成的 API 完全脱离 BuildContext,大多数翻译方法可以直接在非 Widget 类里调用,不像 gen-l10n 必须在 BuildContext 下面拿对象。这意味着网络层、工具类、服务里的提示文案也能用同一套翻译体系,这个是很多团队忽略的点。
适用场景我总结一下:Flutter 应用要跑在 Android、iOS、OpenHarmony 多端;翻译文件多语言数量超过三种;文案频繁带参数和复数;有
