mcpField
I
i18n-rtl-l10n

i18n-rtl-l10n

github.com/zakariaf开发63158 次安装5 周前更新
代码仓库

关于 i18n-rtl-l10n

面向支持多语言或从右向左语言的 Flutter 应用的本地化技能。内容涵盖 ARB 翻译流程、按区域设置显示数据,以及可正确镜像的布局设计。

主要功能

  1. 01保持 ARB 键与占位符一致
  2. 02使用 ICU 复数和选择消息
  3. 03采用方向性布局几何
  4. 04本地化显示数字与日期
  5. 05处理 RTL 和双向文本
  6. 06检查区域设置支持与字体覆盖

适用场景

  1. 01以一致的 ARB 键添加翻译
  2. 02构建适用于阿拉伯语或波斯语的界面
  3. 03格式化本地数字和日历日期
  4. 04处理混合方向界面的技术文本

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/zakariaf/flutter-skills --skill i18n-rtl-l10n

安装后在 Claude Code 中输入/i18n-rtl-l10n确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

examples/
directional_widget.dart
numeral_normalizer.dart
unsupported_locale_delegates.dart
references/
arb-and-icu.md
numerals-and-calendars.md
rtl-and-bidi.md
unsupported-locales.md
scripts/
check_arb_parity.sh
check_i18n_bans.sh

详细介绍

i18n、RTL 与本地化

一项约定:存储规范化数据,在渲染时本地化。 所有转换和格式化都集中在一个 l10n 层;功能组件接收值对象和已本地化的字符串,绝不包含原始格式化逻辑。RTL 是一种布局规范,而不是翻译任务——使用逻辑几何进行编写,使界面能够按设计自动镜像。适用于支持多种语言区域或任何 RTL 语言区域(阿拉伯语、波斯语、希伯来语、索拉尼库尔德语、乌尔都语)的应用。

请根据当前任务阅读相应参考资料:

  • references/arb-and-icu.md — ARB 添加与翻译工作流、l10n.yaml、ICU 复数与选择、占位符类型。
  • references/rtl-and-bidi.md — 方向几何的允许/禁止表、图标镜像、双向文本隔离、固定为 LTR 的 CustomPainter、RTL 图表。
  • references/numerals-and-calendars.md — 数字系统、分隔符陷阱、格式化/解析、日历投影。
  • references/unsupported-locales.md — 为 flutter_localizations 不提供数据的语言区域发布应用:探测方法、随应用打包的 delegate 三件套、delegate 顺序、字体覆盖范围。

提交 PR 前,运行 scripts/check_arb_parity.sh 和 scripts/check_i18n_bans.sh。

不可妥协的规则

  1. 所有面向用户的字符串都必须通过 gen-l10n。 先将键添加到模板 app_en.arb 中,再通过 AppLocalizations.of(context) 读取。将 l10n.yaml 中的 nullable-getter: false 设为 false,确保 getter 非空,且缺失或拼写错误的键会导致编译错误,而不是静默生成空组件。绝不要在 Text、tooltip:、label:、hint: 或 semanticsLabel: 中硬编码字面文本。绝不要在 Localizations 作用域之外调用 AppLocalizations.of(例如在 runApp 之前的 main() 中)。
  2. 所有区域设置中的键和占位符必须一致。 模板中有某个键但某个区域设置缺少该键时,发布时会静默使用模板语言;重命名占位符会导致该翻译在运行时出错。确保每个键和每个 {placeholder} 名称都完全一致——check_arb_parity.sh 会对此进行检查。
  3. 使用 ICU 构建句子和复数,绝不要拼接。 在 ARB 中使用 {count, plural, ...} 和 {x, select, ...}。不同语言的语序和复数类别各不相同(阿拉伯语有六种 CLDR 形式:zero/one/two/few/many/other)。字符串拼接会产生不符合语法的输出,也无法适应 RTL 重排。
  4. 方向由区域设置决定,绝不要硬编码。 在 MaterialApp 上配置 localizationsDelegates 和 supportedLocales,并通过状态驱动当前区域设置。在根节点使用 Directionality(TextDirection.rtl) 会掩盖物理方向边侧问题,并破坏 LTR 局部区域。RTL 会通过 GlobalWidgetsLocalizations 生效,无需编写任何逐组件代码——但仅适用于 flutter_localizations 实际包含的区域设置(规则 10)。其他区域设置会静默回退到 LTR。应逐个区域设置断言 Directionality.of,而不是想当然。
  5. 从首次提交起就只使用方向性几何属性。 使用 EdgeInsetsDirectional、 AlignmentDirectional、PositionedDirectional、TextAlign.start/end、 MainAxisAlignment.start/end、BorderRadiusDirectional、Icons.adaptive.*。绝不要使用 EdgeInsets.only(left/right)、Alignment.centerLeft/Right、Positioned(left/right:)、 TextAlign.left/right 或 Icons.arrow_back。grep 检查会拒绝此类 PR。
  6. 规范存储:UTC 纪元时间 + ASCII 数字。 持久化 dateTime.toUtc().millisecondsSinceEpoch 和西方/拉丁数字。日历和数字系统只是纯粹的显示转换。按纪元时间/DateTime 排期,绝不要使用 Jalali/Hijri 算术——闰年和月份长度的特殊情况应与日历无关。
  7. 解析或存储前,将所有数字输入规范化为 ASCII——包括数字和分隔符。使用 double.parse(normalizeToAscii(text));绝不要对原始输入调用 int.parse(text)(波斯语/阿拉伯语软键盘会输入 ۱۲۳/١٢٣ 和 1٫5)。
  8. 使用一个 FSI/PDI 辅助函数,仅在视图层隔离强 LTR 技术文本段(账号/订单/参考编号、电话、电子邮件、URL、产品代码)。绝不要让隔离字符(U+2066–U+2069)进入存储、搜索或导出流程——在边界处剥离。
  9. 打包覆盖所需文字系统的字体,并提供回退;运行时不要获取字体。 设置主题的 fontFamily 和 fontFamilyFallback,确保阿拉伯语/波斯语字形不会显示为方框。离线/无遥测应用中避免使用 google_fonts——它会在运行时获取字体。根据字体自身的 cmap 验证覆盖范围,而不是看字样示例:文字系统覆盖范围不等于语言覆盖范围——一种能显示波斯语的字体可能缺少 Sorani 使用的七个额外字母,而使用阿拉伯语设置的字样示例也绝不会展示这些字母。
  10. 对于 flutter_localizations 未内置的区域设置,必须提供三个自带的 delegate,并将它们排在最前。 gen-l10n 会本地化你的字符串;Flutter 自身的界面文本和环境中的 TextDirection 来自另一套目录。借用同一文字系统中相近语言的实现,提供 MaterialLocalizations、CupertinoLocalizations 和 WidgetsLocalizations,且每个 delegate 仅声明对应的语言代码;将它们放在 Global* delegate 之前——AppLocalizations.localizationsDelegates 已包含这些 delegate。原因:缺少 Material delegate 会在首个 Tooltip 处触发断言;而 Widgets 回退 delegate 会匹配所有区域设置,并将 TextDirection.ltr 硬编码,因此只修复 Material 部分会让 RTL 应用从右往左的文字读反,且不再崩溃。 请参阅 references/unsupported-locales.md。

ARB 工作流(添加或翻译键)

  1. 将键及其 @key 元数据(类型化的 placeholders、供翻译人员参考的 description)添加到 模板 app_en.arb。
  2. 将同一个键同步到所有其他语言环境文件,并提供真实的译文;占位符名称和 ICU 结构必须完全一致(分支正文可以不同,但分支形状必须相同)。
  3. 运行 scripts/check_arb_parity.sh——任何缺失键、多余键或占位符不匹配都会导致检查失败。
  4. 重新生成(flutter gen-l10n)并运行 flutter analyze——缺失的键现在会导致编译错误。
// app_en.arb (template) — declare the key + @metadata here first.
{
  "@@locale": "en",
  "itemsRemaining": "{count, plural, =0{No items left} =1{1 item left} other{{count} items left}}",
  "@itemsRemaining": {
    "description": "Count shown on the order summary card.",
    "placeholders": { "count": { "type": "int" } }
  }
}
// app_ar.arb — same key; Arabic's SIX CLDR categories are a translation contract.
{
  "@@locale": "ar",
  "itemsRemaining": "{count, plural, zero{لا عناصر متبقية} one{عنصر واحد متبقٍ} two{عنصران متبقيان} few{{count} عناصر متبقية} many{{count} عنصرًا متبقيًا} other{{count} عنصر متبقٍ}}"
}

方向性几何——通过构造实现镜像

// Reads correctly under LTR and RTL with zero conditionals.
Padding(
  padding: const EdgeInsetsDirectional.only(start: 16, end: 8), // never left/right
  child: Row(
    children: [
      Expanded(child: Text(task.title, textAlign: TextAlign.start)), // never .left
      IconButton(
        icon: Icon(Icons.adaptive.arrow_forward), // auto-mirrors in RTL
        onPressed: onNext,
      ),
    ],
  ),
)

对表示方向的字形(返回/下一步、尖括号、进度箭头)使用 Icons.adaptive.* 进行镜像。 切勿镜像含义固定的字形(时钟、对勾、媒体播放/暂停、徽标)。完整的允许/禁止表和手动翻转方法,请参阅 references/rtl-and-bidi.md。

数字——在边界处格式化,统一使用一个格式化器

使用每种语言环境各自的 NumberFormat:intl 会根据语言环境的符号数据输出本地数字,而非 根据 -u-nu- 扩展(回退时会丢弃 unicode -u 扩展)。对于 intl 没有数字符号的语言环境(例如 ckb), 会静默回退为拉丁数字;因此应将其固定到共享同一数字系统且已覆盖的基础语言环境 (ckb→fa),并在测试中断言输出的数字区块。界面元素和任何画布绘制器都使用同一个格式化器。

// One formatter per locale. Persian and Arabic digits are DISTINCT Unicode blocks
// (۴۵۶ U+06Fx vs ٤٥٦ U+066x) — not interchangeable. Format with a locale intl ships
// symbols for; a `-u-nu-` extension is dropped, not honored.
NumberFormat numberFormatFor(Locale locale) => switch (locale.languageCode) {
      'fa' => NumberFormat.decimalPattern('fa'),  // ۰۱۲۳۴۵۶۷۸۹ (arabext)
      // ckb has no intl number symbols — borrow fa (same arabext block + separators).
      'ckb' => NumberFormat.decimalPattern('fa'),
      'ar' => NumberFormat.decimalPattern('ar'),  // ٠١٢٣٤٥٦٧٨٩ (arab)
      _ => NumberFormat.decimalPattern('en'),      // Western (en, de, fr)
    };

解析前先规范化——转换两种数字范围以及波斯语/阿拉伯语分隔符:

String normalizeToAscii(String input) {
  final sb = StringBuffer();
  for (final r in input.runes) {
    if (r >= 0x0660 && r <= 0x0669) {        // Eastern-Arabic ٠-٩
      sb.writeCharCode(0x30 + (r - 0x0660));
    } else if (r >= 0x06F0 && r <= 0x06F9) { // Persian ۰-۹
      sb.writeCharCode(0x30 + (r - 0x06F0));
    } else if (r == 0x066B) {                // ٫ decimal separator -> '.'
      sb.write('.');
    } else if (r == 0x066C) {                // ٬ grouping separator -> drop
      continue;
    } else {
      sb.writeCharCode(r);
    }
  }
  return sb.toString();
}
// double.parse(normalizeToAscii(text)) — never int.parse on raw input.

1٫5 表示 1.5,而不是 15:只规范化数字、不规范化分隔符,会悄无声息地破坏输入的金额。 完整的数字/分隔符/历法表:references/numerals-and-calendars.md。

双向文本隔离——混合文字脚本片段

const _lri = '⁦'; // LEFT-TO-RIGHT ISOLATE
const _rli = '⁧'; // RIGHT-TO-LEFT ISOLATE
const _fsi = '⁨'; // FIRST STRONG ISOLATE (unknown direction)
const _pdi = '⁩'; // POP DIRECTIONAL ISOLATE

String isolate(String run) => '$_fsi$run$_pdi';    // unknown direction
String isolateLtr(String run) => '$_lri$run$_pdi'; // known LTR (codes, numbers)
String isolateRtl(String run) => '$_rli$run$_pdi'; // known RTL

// Prefer known-direction over FSI: first-strong mis-guesses on leading punctuation.
// The isolated value is an ARB placeholder, never a hard-spliced substring:
Text(l10n.orderRef(isolateLtr(order.reference))); // "{ref} — order" reads correctly in RTL

仅在视图层进行隔离。对于独立字段,应改为在 widget 上设置方向: Text(code, textDirection: TextDirection.ltr, textAlign: TextAlign.end)。切勿使用旧式 LRE/RLE/LRO/RLO 嵌入控制符——应使用隔离控制符(UAX #9)。

历法与日期

仅在显示时,将存储的 UTC 时刻转换为用户所选的历法。切勿持久化特定历法的字段;切勿手动编写 Jalali/Gregorian 转换(请使用 intl、shamsi_date、hijri)。一周的第一天和周末因语言环境而异——在标准日期上 计算分组,并按语言环境显示标签。请参阅 examples/numeral_normalizer.dart 和 references/numerals-and-calendars.md。

使用语言环境无关坐标空间的 CustomPainter

如果绘制器使用固定坐标网格,只将该子树固定为 Directionality(textDirection: TextDirection.ltr, …),以免环境中的 RTL 翻转几何布局; 周围的界面元素仍通过逻辑内边距进行镜像。使用 localPosition,通过绘制器自身的变换映射指针位置—— 切勿在 RTL 下反转 dx。向绘制器传入相同的 numberFormatFor(locale), 确保画布上的数字与界面元素一致。图表界面元素(坐标轴、图例、时间轴)会镜像,但 绘制的数据本身绝不翻转。详见 custom-canvas-and-gestures。

反模式

  • Text('Order details')——硬编码文本,没有 gen-l10n 处理路径。请读取 l10n.orderDetails。 (精确匹配硬编码 UI 字符串的 grep 容易误报,因此此项会在代码审查中发现,而不是由 check_i18n_bans.sh 检查——该检查门禁覆盖几何尺寸、图标、数字拼接、旧式双向文本和字体获取。)
  • 先编写翻译文件,或让缺失的键降级为 null——模板是 en,而 nullable-getter: false 会让缺失键成为编译错误。
  • count == 1 ? 'item' : 'items'——二选一的三元表达式无法涵盖阿拉伯语的 few/many;请使用 ICU plural。
  • Text('$count items') 或 'Total ' + n.toString()——拼接片段并使用原始 ASCII 数字; 在 RTL 中会导致词序和数字区段错误。请使用一条 ICU 消息和 numberFormatFor。
  • 波斯语显示 ٤٥٦(阿拉伯-印度数字),或阿拉伯语显示 ۴۵۶(扩展阿拉伯-印度数字)——它们属于不同的 Unicode 区段;fa/ckb 使用 fa 格式化器,ar 使用 ar 格式化器。
  • EdgeInsets.only(left: 16) / Alignment.centerLeft / Positioned(left:)——使用物理方向的边会绕过镜像处理(grep 会检查出)。
  • 用 Directionality(TextDirection.rtl) 包裹应用根节点来“开启 RTL”——RTL 来自语言区域;硬编码根节点会掩盖物理方向边的问题,并破坏 LTR 区域。
  • [...AppLocalizations.localizationsDelegates, GlobalMaterialLocalizations.delegate, …]——该 展开内容已经包含三个 Global* 委托,因此再次追加会造成重复,并将它们置于你引入的任何委托之前。Localizations._loadAll 会采用第一个声明支持该语言区域的同类型委托;你的委托永远不会被调用。
  • 对不受支持的语言区域,只引入 MaterialLocalizations 委托——崩溃虽然消失,应用却会以错误方向渲染。也要引入 WidgetsLocalizations 委托,或者两个都不引入。
  • 看到字体铸造商页面上写着“支持阿拉伯语”,就认为它适用于 Sorani、乌尔都语或普什图语构建——请根据捆绑字体的 cmap 检查该语言自己的字母。
  • 让隔离字符(U+2066–U+2069)进入存储、搜索或导出——请在边界处剥离。
  • 使用 Ahem 渲染 RTL/数字金样——这样不会测试波斯数字和阿拉伯文字塑形。请加载实际捆绑的字体(参见 widget-golden-and-a11y-testing)。
  • 对原始数字字段使用 int.parse(userText)——请先将其规范化为 ASCII。

完成定义

  • 在模板 app_en.arb 中添加键,并附带 @description 和带类型的占位符;其他每种语言环境都包含相同的键;check_arb_parity.sh 检查通过。
  • 设置 nullable-getter: false;字符串只能通过 AppLocalizations 读取;Text/tooltip/label/hint/semanticsLabel 中不得有字面量。
  • 带计数的字符串使用 ICU plural(提供阿拉伯语时,覆盖全部六种阿拉伯语类别);不得使用三元表达式,也不得拼接 $count。
  • 数字和日期通过 numberFormatFor(locale) / intl DateFormat 获取,并作为占位符注入——绝不手动格式化为 ASCII,也绝不使用错误的数字区块;绘制器使用相同的格式化器。
  • 所有数字输入在解析/存储前都经过 normalizeToAscii;并测试往返转换。
  • 所有几何布局都具有方向性(start/end、Icons.adaptive);grep 不得再找到任何物理侧 API;方向由语言环境决定,不得硬编码根 Directionality。
  • 每段混合文字都通过唯一的 FSI/PDI 辅助函数处理(已知方向时优先使用 FSI);不得直接拼接,也不得使用旧版 LRE/RLE;隔离符不得进入存储。
  • 日期/数字以规范形式存储(UTC 纪元 + ASCII);仅在渲染时转换日历/数字;提醒按纪元时间安排。
  • 字体覆盖所有已提供语言环境的文字脚本,并带有回退字体;不得在运行时获取字体;使用真实字体运行 RTL + 数字黄金测试。根据每个捆绑字体的 cmap 断言覆盖范围——检查语言本身的字母,而不只是文字脚本。
  • 每个已提供的语言环境都包含在 flutter_localizations 中,或在 Global* 委托之前附带全部三个委托;对每种语言环境都断言 Directionality.of 和一个 MaterialLocalizations 字符串;平台清单(CFBundleLocalizations)列出每个标签。

相关技能

  • 有关 Semantics 角色/标签以及从 MediaQuery 读取无障碍状态,请参阅 accessibility-as-code。
  • 有关 RTL/数字黄金测试流程(使用真实字体,而非 Ahem)和 textScaler 溢出矩阵,请参阅 widget-golden-and-a11y-testing。
  • 有关固定为 LTR 的绘制器、共享变换和指针映射,请参阅 custom-canvas-and-gestures。
  • 有关此处渲染的规范整数最小单位存储,请参阅 value-objects-money-and-units。
  • 有关根据规范时刻安排提醒,而非进行日历运算,请参阅 local-notifications-scheduler。
  • 有关这些字符串所在的方向性布局原语,请参阅 widget-composition。
  • 有关通过此 ARB 工作流接入新功能字符串,请参阅 scaffold-feature-module。
  • 有关声明各文字脚本的回退级联位置,以及如何验证捆绑字形覆盖范围,请参阅 design-system-structure(references/typography-and-fonts.md)。
  • 有关在首帧之前恢复持久化语言环境,请参阅 app-startup-and-bootstrap——这是操作系统无法选择的语言环境能够被使用的唯一方式。

参考资料

评论

支持 Markdown