如何在 Flutter 应用中大规模实现多语言翻译并妥善处理 RTL(从右到左)布局?

166 阅读5分钟

本地化(Localization)绝非仅仅是“翻译一些字符串”那么简单。在大规模应用中,它会演变为一个贯穿始终的交叉系统:构建流程、开发人员体验、译者上下文、运行时行为,以及无障碍性(Accessibility)都至关重要。做对了,你的应用在每个市场都会感觉像原生应用;做错了,译者和用户都会感到沮丧。

🎯 即将分享的实战策略

接下来,我将展示一系列经过实践检验的实用策略,涉及:

  • 动态内容(Dynamic content)
  • 复数/性别规则(Plural/gender rules)
  • RTL(从右到左)布局支持
  • 工程模式(确保本地化工作随着应用和团队的壮大而可持续维护)

📝 策略一:使用清晰的约定——Keys、Context 和杜绝字符串拼接

这是小团队经常犯错的地方。

  • 使用语义化的 Key,而非使用原始的英文句子作为 Key。

    • 相比于在代码中散布 "Hello, {name}!" 这样的字符串,使用 home.greeting 更容易管理。
  • 绝不通过拼接翻译片段来构建句子。

    • 这会破坏许多语言的语法结构。请始终使用带有占位符的完整字符串模板。
  • 为译者提供上下文(Context)。

    • 例如,提供屏幕截图或简短的说明。许多翻译平台支持为 Key 添加注释。

📌 示例 ARB(或 JSON)条目:

{
  "inboxCount": "{count, plural, =0{No messages} =1{1 message} other{{count} messages}}",
  "@inboxCount": {
    "description": "Number of messages in the user's inbox",
    "placeholders": { "count": {"type":"int"} }
  }
}

🔑 使用 ICU 复数语法和正确的工具链

🗣️ ICU 复数语法至关重要

This uses ICU plural syntax — crucial for correct plural forms across languages.

这使用了 ICU 复数语法(ICU plural syntax) ,它对于在不同语言中实现正确的复数形式至关重要。

🛠️ 使用正确的工具链(Flutter 友好模式)

Use the right toolchain (Flutter-friendly patterns)

Flutter provides a good starting point out of the box:

使用正确的工具链(采用 Flutter 友好的模式)。Flutter 提供了良好的开箱即用支持:

  • 使用 gen_l10n

    Use Flutter’s gen_l10n to generate typed localization classes from ARB files (keeps code clean and type-safe).

    使用 Flutter 的 gen_l10n 工具,从 ARB 文件中生成类型安全的本地化类(Typed Localization Classes),以保持代码整洁和类型安全。

  • 使用 intl 包:

    Use the intl package for pluralization and formatting when you need runtime message logic.

    当你需要运行时消息逻辑(如处理复数和格式化)时,使用 intl 软件包。

🔌 配置 MaterialApp

Wire MaterialApp with generated delegates and supported locales:

MaterialApp 与生成的代理(delegates)和支持的区域设置(supported locales)连接起来:

MaterialApp(
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  supportedLocales: AppLocalizations.supportedLocales,
  locale: _overrideLocale, // optional: allow runtime override
  home: HomeScreen(),
);

🌎 集中控制区域设置 (Locale)

This centralizes locale behavior and lets you override the app locale (handy for QA, screenshots, or user-controlled language settings).

这使得区域设置(Locale)的行为得以集中控制,并允许你覆盖应用的区域设置(这对于质量保证/QA、截屏生成或用户控制的语言设置非常方便)。


🔄 动态内容:服务器 vs. 客户端职责

动态内容很棘手:应用中会显示用户生成的文本(如评论)、来自服务器的内容(如营销文案)以及 UI 界面元素(如按钮/消息)。你需要决定翻译工作发生在哪里:

1. 服务器本地化内容 (Server-localized content)

优点 (Pros)缺点 (Cons)
翻译集中在一个地方。UI 文本的发布周期较慢。
客户端接收的是本地化后的载荷。服务器端需要承担更重的翻译工作。

使用场景: 当营销内容是动态的,或者你支持许多客户端平台并希望文案保持一致时,可使用此方法。

2. 客户端本地化内容 (Client-localized content)

(推荐用于 UI 界面元素以及复数/性别格式化)

优点 (Pros)缺点 (Cons)
客户端迭代速度快。你需要将翻译文件随应用一起发布,或使用动态资源包。
复数和 ICU 逻辑可以在计数所在的地方(客户端)发生。

使用场景: 当消息依赖于应用状态(如计数、用户名)时,可使用此方法。

3. 实用混合方法 (Practical hybrid approach)

UI 界面元素和格式化消息在客户端处理(使用 Token + ICU 模板),而服务器发送结构化数据(如计数、名称、Token)。服务器可以选择性地提供本地化后的营销文案,以实现跨平台一致性。

示例: 服务器发送 { "messageCount": 3 }\texttt{\{ "messageCount": 3 \}},客户端使用 Intl.plural\texttt{Intl.plural} 方法进行渲染。


🔢 复数和性别 — 使用 ICU,避免启发式方法

语言有不同的复数类别(零数/单数/双数/少数/多数/其他)。请使用内置于 Dart 的 intl 包或生成的 l10n 方法中的 ICU 复数/性别模式

Dart 示例:

String messages(int count) => Intl.plural(
  count,
  zero: 'No messages',
  one: '1 message',
  other: '$count messages',
  name: 'messages',
  args: [count],
  desc: 'Number of messages',
);


🚻 性别处理 (Gender)

对于涉及性别的文本,请使用 Intl.gender\texttt{Intl.gender} 方法或组合使用 {gender, select, male{...} female{...} other{...}}\texttt{\{gender, select, male\{...\}\ female\{...\}\ other\{...\}\}} 这种 ICU 语法。请务必为译者提供上下文——因为性别处理可能非常敏感


⬅️ RTL 支持 — 不仅仅是镜像文本

RTL(从右到左,如阿拉伯语希伯来语乌尔都语)布局涉及多个层面:

1. 文本方向 (Text direction)

  • 组件必须遵循 Directionality\texttt{Directionality}(即 TextDirection.rtl\texttt{TextDirection.rtl}ltr\texttt{ltr})。Flutter 的 MaterialApp\texttt{MaterialApp} 在许多情况下会根据区域设置(Locale)自动设置。

2. 布局镜像 (Mirrored layouts)

  • 使用方向性 API:如 EdgeInsetsDirectional\texttt{EdgeInsetsDirectional}AlignmentDirectional\texttt{AlignmentDirectional}MainAxisAlignment.start/end\texttt{MainAxisAlignment.start/end}。(它们会自动翻转。)
  • 优先使用带有 TextDirection\texttt{TextDirection} 属性的 Row/Column\texttt{Row/Column},而不是手动设置左/右边距。

3. 图片和图标 (Images and icons)

  • 对于应自动翻转的资源,使用 Image.asset(’...png’, matchTextDirection: true)\texttt{Image.asset('...png', matchTextDirection: true)}
  • 对于图标,优先使用语义化图标(如 Icons.arrow_back\texttt{Icons.arrow\_back}),或者提供 LTR/RTL 两种变体(或在代码中按需翻转)。

4. 字体排版 (Typography & fonts)

  • 使用支持相应脚本的字体——例如,阿拉伯语需要具备塑形能力(shaping-capable)的字体(如 Noto Naskh, Noto Sans Arabic 等)。请使用真实的文本进行测试。

5. UI 镜像陷阱 (UI mirroring pitfalls)

  • 不要假设简单的镜像总是有效(例如,时间轴通常具有方向性的含义)。务必与产品/设计团队讨论 RTL 区域设置的 UX 方向。

6. 文本塑形和双向文本 (Text shaping and bidi)

  • 注意 RTL 句子中嵌入的 LTR 文本(如用户名、URL)。使用 Unicode 方向标记或依赖 Text\texttt{Text} 组件的默认行为;测试像 "Hello محمد"\texttt{"Hello \text{محمد}"} 这样的极端情况。

7. 快速 RTL 测试 (Quick RTL test)

  • 将应用区域设置切换为 RTL 语言,并扫描每个屏幕。使用伪本地化(见下文)来暴露隐藏的问题。

🚀 性能与包体积 — 懒加载语言包

如果你支持 30 个以上的区域设置,附带所有翻译文件可能会增大 APK/IPA 的体积。可选方案:

  • 按需加载区域设置包: 在运行时从你的 CDN 获取翻译包并进行缓存。仅在应用中捆绑一个紧凑的默认集。
  • 延迟导入(高级): 延迟加载特定于区域设置的代码/数据。
  • 优先打包: 捆绑排名靠前的 N 个区域设置,其余的远程获取。

注意: 远程获取翻译文件时,始终验证完整性(签名文件或校验和),并处理离线回退


✅ 质量保证 (QA) 与自动化

尽早且经常进行以下操作:

  • 伪本地化 (Pseudo-localization): 替换字符(如 aaˊ\text{a}\to\text{á}ee¨\text{e}\to\text{ë})并故意拉伸字符串,以暴露布局问题和字符串拼接错误。
  • 自动化检查 (Automated checks): CI 流程应验证所有 ARB 键是否存在、是否缺少占位符以及是否存在未使用的键。
  • Golden/UI 测试: 在包括 RTL 在内的多个区域设置下运行测试,以捕获视觉回归。
  • 译者工作流程 (Translator workflow): 将 ARB/CSV 导出到翻译平台(如 Crowdin, Lokalise),附带上下文/截图,然后导入回应用,并运行 CI 检查。

📝 命名与治理:保持 Key 稳定和重构安全

  • 采用 Key 命名约定(例如:screens.home.title\texttt{screens.home.title})。
  • 对经常变动的文案使用语义化 Token——更改 Token 的值不需要更改代码。
  • 维护 Key 的弃用策略

📋 实用入门清单 (Checklist)

  1. 添加 gen_l10n\texttt{gen\_l10n}intl\texttt{intl};生成类型安全的本地化类。
  2. 保持 UI 文本在客户端本地化;服务器发送结构化数据。
  3. 使用 ICU 复数/性别模式;绝不通过拼接构建字符串。
  4. 使用 Directionality\texttt{Directionality}EdgeInsetsDirectional\texttt{EdgeInsetsDirectional} 测试 RTL。
  5. 使用支持正确脚本的字体。
  6. 实现伪本地化和 CI 检查。
  7. 考虑按需加载语言包。
  8. 记录所有 Key 的翻译上下文。

总结:

本地化既是技术问题,也是流程问题。最佳的系统能够让译者和工程师成为合作伙伴,共同交付正确、自然的体验。从小处着手(英语 + 你的主要市场),尽早测试 RTL 和复数,并逐步扩展,这样能节省无数时间。