查找 Flutter 应用 ARB 文件中缺失的键

更新于 · LocalePilot

在英文中新增、却在德语中忘了翻译的键,只在一种语言里被改名的占位符:这些错误都能顺利编译,最后却出现在用户面前。下面介绍如何在发布前发现它们。

赶时间?直接在浏览器中检查你的 ARB 文件:免费,无需注册,不上传任何内容。

回顾:ARB 文件的组织方式

使用 flutter gen-l10n 时,每种语言都有自己的文件(app_en.arb、app_fr.arb 等)。由 l10n.yaml 中 template-arb-file 指定的模板文件是参照:它包含每个键及其 @key 元数据(描述、占位符)。

{
  "cartCount": "{count, plural, =0{Empty cart} =1{1 item} other{{count} items}}",
  "@cartCount": {
    "placeholders": { "count": { "type": "int" } }
  }
}

1. 让 Flutter 列出未翻译的消息

在 l10n.yaml 中添加 untranslated-messages-file 选项:

arb-dir: lib/l10n
template-arb-file: app_en.arb
untranslated-messages-file: untranslated.json

每次生成时,Flutter 都会把每种语言缺少的模板键写入该文件。这是最容易设置的检查;发布前请确认该文件为空(或只包含 {})。

2. 用一个小 Dart 脚本比较键

如果想在 CI 中做一个失败时信息清晰的检查,一个脚本就够了:

import 'dart:convert';
import 'dart:io';

void main() {
  final dir = Directory('lib/l10n');
  final template = _keys(File('lib/l10n/app_en.arb'));
  var failed = false;
  for (final file in dir.listSync().whereType<File>()) {
    if (!file.path.endsWith('.arb')) continue;
    final missing = template.difference(_keys(file));
    if (missing.isNotEmpty) {
      failed = true;
      print('${file.path}: ${missing.join(', ')}');
    }
  }
  if (failed) exit(1);
}

Set<String> _keys(File f) => (jsonDecode(f.readAsStringSync()) as Map<String, dynamic>)
    .keys
    .where((k) => !k.startsWith('@'))
    .toSet();

以 @ 开头的键是元数据,因此不参与比较。

3. 检查占位符

键在所有语言中都存在,也可能出问题:如果法语译文使用 {nombre},而模板声明的是 {count},gen-l10n 会在每个语言中把 nombre 添加为额外参数,现有的调用处就无法再编译。反过来,译文直接丢掉占位符时,则不会有任何警告(详见 ICU 复数与占位符指南)。对每个键,把花括号中的名称与 @key.placeholders 中声明的名称比较。注意 ICU 消息(plural、select):other 之类的关键字不是占位符。

4. 同时留意空值

有些翻译流程会先用空值创建键,等翻译后再填写。它不会显示为“缺失”,所以要再加一项检查,在除模板以外的每种语言中标记空字符串。

发布前检查清单

  • 未翻译消息文件为空。
  • 每种语言的键都与模板相同。
  • 占位符逐键一致。
  • 模板以外没有空值。
  • 已在文字最长的语言(例如德语)中实际查看过长消息的显示效果。

常见问题

Flutter 会报告缺失的翻译吗?

会,前提是在 l10n.yaml 中添加 untranslated-messages-file:flutter gen-l10n 会在该文件中列出每种语言缺少的模板键。如果不加,你只会得到一条汇总警告。

需要把 @key 元数据复制到每种语言中吗?

不需要。元数据(描述、占位符)从模板文件中读取;其他文件只需包含译文。

如何避免占位符名称写错?

对每个键,把译文中花括号里的名称与模板中声明的名称比较,不一致时让 CI 失败。