ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Flutter+OpenHarmony国际化实战:电子合同多语言切换的坑与解

Flutter+OpenHarmony国际化实战:电子合同多语言切换的坑与解

上个月,我把一款跑在OpenHarmony设备上的电子合同签署App从单语言改成了支持中英文切换。改完之后最大的感受是:国际化这件事,翻译只占三成,剩下的七成全藏在"合同"两个字里。如果你只是把按钮文案翻译一遍,那叫多语言皮肤,不叫国际化支持。

这篇博文不打算讲Flutter的基础知识,也不科普OpenHarmony怎么装环境,我默认你已经有一台能跑的OpenHarmony设备、有一个Flutter for OpenHarmony的工程。我会从电子合同这个具体业务出发,把Flutter的国际化链路(flutter_localizations + intl + ARB模板 + 代码生成)在OpenHarmony上的落地过程、踩过的坑、以及合同业务必须考虑的特殊点完整讲一遍。内容适合正在做或者准备做多语言版本Flutter应用的开发者,尤其是金融、法务、政务类应用的同行,因为这类App的国际化跟普通工具类App完全是两个量级的问题。

1. 为什么说电子合同App的国际化,根本不是"翻译一遍"的事

1.1 合同业务对国际化的要求,天然比其他App高一档

普通App的国际化,核心是字符串替换:把代码里的中文字面量挪进资源文件,再翻译成目标语言,基本就算完成。到了上线前,顶多再检查一下有没有漏网之鱼。但电子合同App不能这么干,因为合同正文不是普通文案,它是证据。界面上的"发送"按钮翻译错了,用户最多吐槽一句;合同正文里一个有歧义的金额、一个被机器翻译污染的条款,直接影响的是这份合同的法律效力。所以我们项目定了第一条铁律:合同正文的多语言不是UI翻译,是法律翻译,必须有人工复核,并且每一版翻译都要留版本号。

第二个差异点是时效性和一致性。用户在周三签署合同,当时界面显示的是中文;三个月后他打开归档记录,手机系统语言早就切成了英文,如果合同内容跟着界面语言走,签过的合同就"变色"了。这种事在普通App里无伤大雅,在合同App里就是一个事故级别的bug。因此,合同记录必须在签署那一刻就把当时使用的语言固化下来,之后任何时候打开归档,看到的都应该是签署时的那份原文。这一点我在第4章会详细展开。

第三个差异是数字和格式的敏感性。日期、金额、签署时间戳,这些数据在国际化时不能简单套用通用的格式化函数。欧洲有些地区写1.500,00,美国写1,500.00,合同里如果出现这种歧义,就是灰色地带。合同正文里的金额必须强调"无歧义",宁可牺牲一点本地化习惯,也要用国际通用的明确格式。这是法务同事在评审时反复强调的。

1.2 Flutter + OpenHarmony 这套组合的特殊性

Flutter for OpenHarmony并不是简单的"给Flutter加一个平台",它是由OpenHarmony社区维护的Flutter分支,Dart语法、Flutter框架API大体与上游保持一致,但插件生态比Android/iOS年轻好几个量级。做国际化的时候,这个差异格外明显。

先说好消息:国际化依赖的核心包,intl和flutter_localizations,基本都是纯Dart实现,不依赖具体平台的native能力,所以在OpenHarmony上可以正常使用。真正出问题的是那些需要平台通道配合的插件,比如获取系统语言、持久化用户选择、监听系统设置变化,这些功能在不同设备上的实现并不完全一致。我在项目里遇到的一个典型场景是:某台设备上系统语言上报不完整,导致中文资源始终无法命中,这个问题一度排查了整整一天。另外,OpenHarmony各厂商的系统实现也存在差异,对语言标签(locale tag)的处理方式并不统一,有些返回"zh-Hans-CN",有些返回"zh_CN",有些就直接返回"en_US"。这些坑我会在第5章专门说。

2. 环境准备:在OpenHarmony设备上把Flutter工程跑起来

2.1 工程初始化的几个关键步骤

整个环境搭建过程我不展开成安装手册,只列出和国际化相关的关键点。前提是你已经装好了OpenHarmony SDK和Flutter for OpenHarmony的SDK,并且配好了PATH环境变量。剩下的步骤是:

  1. 创建工程时指定OpenHarmony平台:flutter create --platforms=ohos project_name;如果已经有Flutter工程,就在工程根目录执行flutter create --platforms=ohos .来补上ohos平台目录。
  2. 用开发工具打开ohos目录,配置好签名信息,才能往真机安装。
  3. 在pubspec.yaml里添加国际化相关依赖,然后flutter pub get。
  4. 先跑一个空壳App确认能编译、能安装、能打开,再开始写业务代码。

这里有一个很实在的建议:开发阶段尽量用真机。我们在某款模拟器上遇到过系统语言接口返回不完整的情况,系统设置明明切到了简体中文,Flutter侧拿到的语言标签却始终是英文,导致中文资源一直命中不了。这种问题在真机上很少出现,但在模拟器上很常见。如果一开始就依赖模拟器调试,很容易被这种环境差异误导,以为是自己的代码写错了。

2.2 工程第一天就改好的三件配置

国际化最忌讳的事情是"以后再加"。电子合同App页面多、流程长,如果先写一堆硬编码的中文再回来改,你要么写脚本扫描字符串,要么人肉排查,都会漏。我见过有项目因为遗漏硬编码字符串,在签名确认按钮上出现中英混合,用户截了图投诉——在合同类产品里,这种低级错误格外扎眼。所以第一天就要把下面三件配置落地。

第一件,pubspec.yaml。添加flutter_localizations、intl,并开启generate: true:

dependencies: flutter: sdk: flutter flutter_localizations: sdk: flutter intl: any shared_preferences: ^2.2.0 flutter: generate: true

第二件,l10n.yaml。这是Flutter官方生成多语言代码的配置文件,放在工程根目录:

arb-dir: lib/l10n template-arb-file: app_en.arb output-localization-file: app_localizations.dart output-class: AppLocalizations synthetic-package: false nullable-getter: false

我特意把synthetic-package设成false,这样生成的代码会直接落在lib目录里,方便审查生成结果,也方便调试。如果你保持默认的synthetic package,生成的类在虚拟包路径里,查问题的时候很不直观。

第三件,先建好初始ARB文件。在lib/l10n目录下创建app_en.arb和app_zh.arb,各自至少包含App名称等基础文案。不要求一次配全,但文件要先建起来,因为生成代码的机制是"有模板文件才开始生成"。

提示:如果你们的项目已经写了一半才开始补国际化,我建议先花一天把页面里的所有可见字符串扫描出来,建立一份清单,再统一迁移到ARB里。这一步省不了,漏掉的硬编码字符串会在后面不停冒出来,而且总是在你最不想看到的地方出现。

3. 多语言链路核心:ARB模板、代码生成与动态切换

3.1 MaterialApp 的本地化接线

国际化第一步是把MaterialApp接上本地化配置。代码本身很简单:

MaterialApp( title: context.l10n.appTitle, locale: _locale, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const HomePage(), );

这里需要解释一个很多人搞不清楚的点:AppLocalizations.localizationsDelegates是生成代码自动展开的,它里面除了你自己生成的AppLocalizationsDelegate之外,还包含了GlobalMaterialLocalizations.delegate、GlobalWidgetsLocalizations.delegate、GlobalCupertinoLocalizations.delegate这三件套。这三件套是Material组件自带的文案翻译,比如返回按钮的tooltip、日期选择器的月份、对话框的默认按钮文字。最常见的坑是:自己只添加了AppLocalizations.delegate,把这三件套漏了,结果自己写的字符串都翻译了,日历选择器还是英文。记住,千万不要手动精简这个delegate列表,否则你会被各种"漏翻译"的组件折腾到怀疑人生。

另外要注意,supportedLocales也是生成的代码,它只会包含你提供了ARB文件的那些locale。比如你有app_en.arb和app_zh.arb,它就会生成[Locale('en'), Locale('zh')]。如果后面新增了阿拉伯语,记得补上对应的ARB文件,否则即使你把系统语言切到阿拉伯语,Flutter也不会去用它。

3.2 ARB文件编写规范:占位符与参数类型

ARB(Application Resource Bundle)是Flutter官方的本地化资源格式,本质是JSON,但带了一套元数据规范。最核心的一点是占位符:文案里如果包含动态值,不要用字符串拼接,要写成占位符,让翻译人员可以整体调整语序。

举个例子,签署确认弹窗:

app_en.arb:

{ "@@locale": "en", "appTitle": "Contract Sign", "signConfirmTitle": "Confirm signing", "signConfirmBody": "You are about to sign the contract \"{contractName}\" as {signerRole}.", "@signConfirmBody": { "description": "Signing confirmation dialog body", "placeholders": { "contractName": {"type": "String"}, "signerRole": {"type": "String"} } } }

app_zh.arb:

{ "@@locale": "zh", "appTitle": "合同签署", "signConfirmTitle": "确认签署", "signConfirmBody": "您即将以{signerRole}身份签署合同“{contractName}”。", "@signConfirmBody": { "description": "签署确认弹窗正文", "placeholders": { "contractName": {"type": "String"}, "signerRole": {"type": "String"} } } }

注意中文和英文模板里占位符的顺序是不同的:中文是"以{signerRole}身份签署合同{contractName}",英文是"sign the contract {contractName} as {signerRole}"。这就是占位符的价值——翻译人员不需要为了迁就代码逻辑而硬生生把英文翻成中文语序。使用的时候调用生成的getter方法就行:

context.l10n.signConfirmBody(contract.name, currentUser.roleLabel);

placeholder支持的类型包括String、int、double、DateTime等。还有一类是ICU复数格式,比如"已签署N份协议":

"signedAgreements": "{count, plural, =0{No agreements signed} one{1 agreement signed} other{{count} agreements signed}}"

这个在生成代码时会被正确处理。不过说实话,在合同正文里我们很少用复数句式,因为法律文本讲究精确,条条框框都是完整句子。复数的使用场景更多是界面上的一些提示文案、通知消息。

3.3 动态切换语言与持久化

电子合同App有一个业务需求:用户可以在App内主动切换显示语言,而不是完全跟随系统。原因在第4章会说,这里先讲实现。

我们项目里用一个全局的ValueNotifier<Locale>来管理当前语言:

final ValueNotifier<Locale> localeNotifier = ValueNotifier(_initLocale()); Future<Locale> _initLocale() async { final prefs = await SharedPreferences.getInstance(); final saved = prefs.getString('locale.appLocale'); if (saved != null) { return Locale(saved); } return PlatformDispatcher.instance.locale; } void switchLocale(Locale locale) { localeNotifier.value = locale; SharedPreferences.getInstance().then((prefs) { prefs.setString('locale.appLocale', locale.toLanguageTag()); }); }

然后在MaterialApp外层用ValueListenableBuilder包一层,locale一变,整棵树重建:

ValueListenableBuilder<Locale>( valueListenable: localeNotifier, builder: (context, locale, _) { return MaterialApp( locale: locale, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: const HomePage(), ); }, );

这里有一个容易被忽略的细节:localeResolutionCallback。当系统locale带区域变体时,比如zh_TW、zh_HK,而你的ARB文件只写了"zh",Flutter默认的解析规则不一定能正确回落。我们加了这样一个回调:

localeResolutionCallback: (locale, supportedLocales) { if (locale == null) return supportedLocales.first; if (locale.languageCode == 'zh') return const Locale('zh'); if (supportedLocales.contains(locale)) return locale; return const Locale('en'); },

这样做的意图很明确:只要是中文变体,统一命中简体中文资源;其他语言如果没有对应资源,就兜底到英文,而不是让用户看到一个全部是占位符的界面。

4. 合同签署场景下的本地化实战:日期、金额与法律文案

4.1 签署日期:通用格式与法律格式要分开

界面上的签署日期,交给intl的DateFormat就行了。DateFormat.yMMMMd('zh_CN')输出"2024年12月25日",DateFormat.yMMMMd('en_US')输出"December 25, 2024"。这部分没有太多技术含量,难点在合同正文里的日期。

中文法律文本里有大写日期写法,比如"二〇二四年十二月二十五日",这个intl不提供,得自己写转换函数。我们实现了一个查表法的工具函数,把年月日数字分别映射到"〇一二三四五六七八九",再拼上"年月日"三个字。注意处理"十"的逻辑:十二月不能写成"一十二月",二十日不能写成"二十零日",这些琐碎规则都需要测试用例覆盖。实现不复杂,但边界值多,光"〇"和"零"的区分就够写一页。

英文法律模板的做法又不一样:它不能靠拼接,要整句放进ARB模板里。我们在ARB里是这样定义的:

"signDateClause_zh": "本合同于{signedDate}在{signedPlace}签订。", "signDateClause_en": "This Agreement is entered into on {signedDate} at {signedPlace} on the date set forth below."

调用时传入格式化好的日期字符串。为什么用整句模板?因为法律句子的语序跟普通口语不一样,英文模板里"on the date set forth below"这类法律套语必须由翻译人员整体把控,代码不能擅自组织。合同领域特别忌讳"看起来每个词都对,但整句话不像人话"的翻译。

跟日期相关还有一个细节:归档时同时存UTC时间戳和签约当地时间的可读字符串快照。国际化的场景里,如果归档后重新对时间戳做格式化,会因为时区变化或者localde变化显示不同的日期,这在证据链上是个隐患。我们的做法是:签署完成那一瞬间,把按照当时区域格式化的日期字符串直接固化到合同数据里,之后任何页面展示都用这个快照,不再重新格式化。

4.2 金额格式化与中文大写金额

界面上的金额可以用NumberFormat.currency来做:NumberFormat.currency(locale: 'zh_CN', symbol: '¥')输出"¥1,234.56",英文界面配美元符号输出"$1,234.56"。但这里有一个业务层面的约束:金额单位是合同元数据,跟界面语言无关。合同币种是人民币,即使用户把界面切成了英文,展示的也应该是"CNY 1,234.56"或"¥1,234.56",而不是变成美元符号。

更关键的是合同正文里的金额格式。我们的规定是:合同正文展示金额一律用"三位分组 + 两位小数 + ISO币种代码"的固定格式,不随界面语言切换数字分组符号。为什么这么较真?因为NumberFormat.decimalPattern在德语环境可能输出"1.234,56",在英语环境输出"1,234.56",欧洲用户看前者是1234.56,美国用户看后者也是1234.56,可一旦这两者出现在同一份合同里,就会产生"这个逗号到底是分组还是小数点"的争议。法律文本不能留这种灰色地带,所以宁可牺牲一点本地化习惯,也要用无歧义的格式。

金额内部存储我们坚持用整数分(cents),不用double。这个原则在普通App里是推荐项,在合同App里是强制项——一分钱的浮点误差在UI上可能显示不出来,但在合同条款的自动核对逻辑里就是大问题。

中文大写金额是另一个独立模块,它不是文案,是算法。我们实现了amountToChineseUpper(int cents),查表映射"零壹贰叁肆伍陆柒捌玖分角拾佰仟万亿",测试用例覆盖了0、整数、带角分、中间连续零、末尾零等各种边界。英文合同里的对应物是"in words: One Thousand Two Hundred Thirty-Four and 56/100"这种写法,逻辑类似,按locale选择不同的实现函数。这块代码建议单独放一个文件,不要硬编码在页面里。

4.3 签署流程锁定语言、归档记录语言元数据

这一节是这个项目里让我印象最深的设计决策。当时产品经理提了一个需求:"签署过程中能不能让用户切换语言?"我说不行,理由很简单:用户在看中文合同的过程中,把界面切成英文,然后再点确认签署,这算他签的是中文版还是英文版?将来有纠纷的时候,对方律师问一句"他到底看懂了哪份",这个问题就解释不清。

所以我们定了规则:从进入签署确认页开始,语言切换入口隐藏或禁用,当前语言快照写入签署会话。用户想换语言可以,先退出签署流程,换好再重新进来。规则看起来很死板,但它保证了"看什么签什么"的基本逻辑。

合同数据模型上加了两个字段:contractLocale和contractVersion。签署完成后,合同正文渲染一律使用contractLocale,跟当前UI语言完全无关。这样即使三个月后手机系统切了英文,用户打开旧合同看到的还是签署时的那版中文,落款、附件、时间戳全部一致。UI界面语言可以灵活切换,合同正文语言一旦签署就不可变。

如果产品要支持双语合同,数据模型要升级为"语言对"(language pair),比如"中文为主、英文对照"或"英文为主、中文对照"两种模式。这个最好在需求评审阶段就想清楚,因为数据模型定下来之后再改,涉及合同创建、签署、归档、验真一整条链路的返工,成本非常高。

5. OpenHarmony适配实测:系统语言、字体与插件缺口

5.1 系统语言获取与变更监听

Flutter默认会通过平台层获取系统locale,理论上WidgetsBinding会把这件事处理好。但我们在OpenHarmony目标机上实测发现,某些版本的系统语言上报并不完整:系统设置切到简体中文,Flutter侧拿到的却还是en_US。这导致中文ARB永远不会命中,界面内容看着像"没有翻译"。

解决办法是自己加一条平台通道,从OpenHarmony侧直接取系统语言。Dart侧代码长这样:

class OhosLocaleService { static const _channel = MethodChannel('com.example.app/locale'); static Future<Locale?> getSystemLocale() async { try { final tag = await _channel.invokeMethod<String>('getSystemLocale'); if (tag == null) return null; return _normalizeLocale(tag); } on PlatformException { return null; } } static Locale _normalizeLocale(String tag) { final parts = tag.replaceAll('-', '_').split('_'); if (parts.length >= 2) return Locale(parts[0], parts[1]); return Locale(parts[0]); } }

OpenHarmony侧的实现要点是:在应用的模块里调用系统提供的语言能力,把类似"zh-Hans-CN"这样的标签归一化成Flutter能识别的形式再回传。不同API版本差异不小,这里不建议我写死代码,你直接以手头SDK文档为准。重点在于:必须做归一化,因为OpenHarmony设备的返回格式真的不统一。

系统语言变更的监听也有讲究。应用在后台时用户切了系统语言,回到前台怎么感知?我们用的方案是onResume生命周期兜底:回到前台后重新拉一次系统语言,跟当前缓存的语言比较,不一致就刷新。最初试过用EventChannel订阅系统语言变化事件,但实测部分设备不触发这个事件,不如onResume兜底稳。这条经验属于"用脚踩出来的"——文档里写得再漂亮,都不如真机行为靠谱。

5.2 字体、RTL与排版差异

阿拉伯语、希伯来语这类从右往左书写的语言,Flutter的Directionality会随locale自动切换,但这个"自动"只对框架层生效。如果你在自定义绘制里写死了Alignment.centerLeft,或者用EdgeInsets.only(left: ...),到了RTL界面就会乱掉。电子合同App里签名画布、骑缝章位置、条款对齐线都是自定义绘制,这些地方必须用Directionality.of(context)来取当前方向,或者用AlignmentDirectional、EdgeInsetsDirectional这些逻辑属性,不能写死物理方向。

字体覆盖是另一个坑。OpenHarmony系统默认字体对中文和拉丁文覆盖没问题,但阿拉伯文、泰文、印地文不一定全。我们的做法是把目标语言的字体文件打进assets,然后在主题里配置fontFamilyFallback:

ThemeData( fontFamily: 'default', textTheme: textTheme.apply( fontFamilyFallback: const ['NotoNaskhArabic', 'NotoSansThai'], ), )

注意这个fallback要同时配置在theme和textTheme两层,否则部分组件不会生效。加字体文件会增大包体积,但合同App本来就不适合做"极致轻量",内容完整可读比体积重要。

还有文本缩放。OpenHarmony系统设置里的字体缩放会通过textScaleFactor影响Flutter布局。合同预览页面内容是按固定字号设计的,字体放大1.3倍就容易溢出。我们没有对合同正文禁用缩放,而是用可滚动布局加弹性行高来容纳,同时把1.3倍缩放设置为验收必测项。因为合同正文必须保证老年用户放大也能读,这是可用性问题,不是bug。

5.3 第三方插件的本地化空白

Flutter社区大量插件自带文字。最典型的例子是某个签名板插件,它的CLEAR / SAVE按钮是硬编码英文,根本不支持传参数。遇到这种情况,我们没有去fork插件,而是在它外面包了一层自定义组件,把按钮替换成自己的本地化文案。原因很简单:fork之后要长期维护,而且上游更新了你还得追;自定义覆盖只需要在UI层拦截,代码量少,风险可控。

还遇到过需要日期选择和时间选择的流程,一开始想用某个第三方picker插件,但它的OpenHarmony适配有问题,本地化文案也对不齐,最后干脆退回Flutter自带的material date picker,配合GlobalMaterialLocalizations之后自动跟着locale走,省心不少。在OpenHarmony上做项目,我的经验是:能上官方组件就上官方组件,第三方插件慎用,尤其是涉及到文字、方向、系统能力调用的插件,每个都是潜在的国际化黑盒。

顺手提一个约定:shared_preferences在OpenHarmony适配版本里存key时是原样字符串。我们给语言key统一加了"locale."前缀,避免跟其他缓存数据冲突。这种看起来不起眼的约定,在多语言、多账号场景下特别有用,否则哪天排查数据串了,你会后悔没早点加前缀。

6. 多语言验收:伪本地化、溢出排查与回归清单

6.1 伪本地化:测试翻译缺失的"放大镜"

伪本地化(pseudo-localization)是国际化的一个成熟测试手段,国内团队用得不多,但它对付"字符串没抽干净""布局容易撑爆"这两类问题效率极高。

做法是在已有ARB基础上建一个伪本地化资源文件,比如app_xx_PSEUDO.arb,把每条文案前面加标记并复制三遍:

{ "@@locale": "xx", "appTitle": "[[ Contract Sign ]] [[ Contract Sign ]] [[ Contract Sign ]]", "signConfirmTitle": "[[ Confirm signing ]] [[ Confirm signing ]] [[ Confirm signing ]]" }

然后临时把这个locale加进supportedLocales,跑一遍全页面。凡是溢出的、被截断的、还是硬编码英文没被替换的字符串,全部会暴露出来。为什么要复制三遍?因为德语、俄语文案通常比中文长30%-50%,阿拉伯语可以到70%以上。三倍长度虽然夸张,但能覆盖绝大多数极端场景。对合同App这种追求稳妥的产品来说,用这个粗暴办法比逐个语言人工翻页检查效率高太多。

跑完之后有一个重要动作:把这个伪资源从正式包里移除。我们曾经因为忘记移除,让用户在语言设置里看到了一个"XX"选项,点进去全是带括号的英文,虽然只影响测试版,但看着非常业余。

6.2 溢出与截断的高发区域

多语言做完之后,布局溢出主要发生在几个固定区域。

一是表格和清单的列头。合同预览页的金额列如果写死了宽度,换成德语马上崩。解决办法是用Expanded分配空间,文本超出用ellipsis加tooltip。二是签名确认弹窗。文案带占位符时,实参(比如合同名称)一旦超长,按钮会被撑到屏幕外。给文本设置maxLines,让长文本滚动而不是硬撑。三是底部按钮组的文案,中文的"确认签署"四个字,翻译成德文可能是十几个字符,如果两个按钮并排,空间一定不够。

有几个原则要记牢。不要滥用FittedBox:合同条款字号被FittedBox缩到看不清,法律上等于没展示。该换行就换行,该滚动就滚动,不能用缩放掩盖布局问题。另外,做RTL适配时,建议在代码审查阶段就用脚本扫一遍工程里有没有Alignment.left、EdgeInsets.only(left:这种物理属性写法,提前拦截,而不是等QA的RTL测试用例跑挂了再说。

6.3 回归验收清单

国际化不是一次性功能,新增页面、新增文案都会引入新问题。我们总结了一份验收清单,每次发版前必须过一遍:

检查项说明通过标准
全页面截图中英文逐页对比无硬编码字符串、无遗漏翻译
系统语言切换切语言后冷启动语言自动跟随,无闪屏
App内语言切换设置页热切换界面即时刷新,状态不丢失
签署流程锁定签署中尝试切语言切换入口隐藏或禁用
归档合同复现换语言后打开旧合同合同正文保持签署时语言
金额格式抽查三种币种3个金额分组符、币种码一致
日期格式抽查界面/合同正文各2种中英文日期格式正确
RTL方向检查阿拉伯语页面对齐方向正确,自定义绘制正常
字体缩放开启1.3倍字体无溢出、无截断
伪本地化遍历xx_PSEUDO资源跑全量无新增溢出

这份清单看起来朴素,但每一行几乎都是真实翻过车之后补进去的。从最初的四五条慢慢扩充到现在的十条,现在回归基本不慌了。

最后说一句个人体会。这个项目做下来,最花时间的不是把字符串挪进ARB文件,而是把"UI文案"和"合同正文文案"彻底分开,并让产品经理、UI设计师、翻译、法务都理解这个边界。技术层面的国际化,Flutter的工具链已经非常成熟,intl和flutter_localizations在OpenHarmony上都能正常用;真正需要你自己拿主意的,是业务规则。合同类应用,宁可少支持一种语言,也要保证已有语言没有任何歧义和遗漏。如果你也在做类似的东西,我建议先从签署页和归档页开始改造,这两个页面是合同证据链的核心,优先保证它们的语言一致性,再去铺其他功能页面。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进