国际化与本地化

把界面文案硬编码在 Widget 里,是短期内最省事、长期最贵的决定:改一句文案要全局搜索,加一种语言等于重写一遍界面。Flutter 官方的 flutter_localizations + intl + ARB 方案已经把这件事做得很轻:声明式配置一次,之后只需要往 .arb 文件里补文案。本章按“配置 → 写文案 → 用文案 → 切换语言 → 格式化 → 收尾规范”的顺序讲。

基础三件套

# pubspec.yaml
dependencies:
  flutter_localizations:
    sdk: flutter
  intl: any            # 由 flutter_localizations 锁定版本,写 any 可避免冲突

flutter:
  generate: true       # 开启生成,否则 flutter gen-l10n 不会随构建运行
# l10n.yaml(放在项目根目录)
arb-dir: lib/l10n                  # .arb 文件所在目录
template-arb-file: app_en.arb      # 模板文件,其他语言以它为准
output-localization-file: app_localizations.dart
output-class: AppLocalizations     # 生成的类名
nullable-getter: false             # 生成非空 getter,少写一个 !
flutter gen-l10n        # 手动生成本地化代码;开启 generate: true 后构建时也会自动生成

ARB 文件结构

// lib/l10n/app_en.arb(模板:包含占位符与类型声明)
{
  "@@locale": "en",
  "appTitle": "Shop",
  "welcomeUser": "Welcome, {name}",
  "@welcomeUser": { "description": "首页欢迎语", "placeholders": { "name": { "type": "String" } } },
  "itemCount": "{count, plural, =0{No items} =1{1 item} other{{count} items}}",
  "@itemCount": { "placeholders": { "count": { "type": "int" } } }
}
// lib/l10n/app_zh.arb(只写文案,不用重复声明占位符)
{
  "@@locale": "zh",
  "appTitle": "商城",
  "welcomeUser": "欢迎,{name}",
  "itemCount": "{count, plural, =0{暂无商品} other{{count} 件商品}}"
}
语法示例说明
占位符{name}类型在 @ 元数据里声明,模板文件必须写全
复数{count, plural, =0{...} other{...}}英语有 one/other,中文只需 other
选择{gender, select, male{...} other{...}}按枚举值选文案
日期数字在 Dart 侧用 DateFormat/NumberFormat不放在 ARB 里,避免格式碎片化

使用文案

// 页面里取用:nullable-getter: false 时可直接 AppLocalizations.of(context)!.xxx
final l10n = AppLocalizations.of(context)!;

Text(l10n.appTitle),
Text(l10n.welcomeUser(userName)),
Text(l10n.itemCount(items.length)),   // 复数自动按语言规则处理
// MaterialApp 上注册委托与支持的语言
MaterialApp(
  localizationsDelegates: AppLocalizations.localizationsDelegates, // 含 Material/Cupertino/Widgets 三个委托
  supportedLocales: AppLocalizations.supportedLocales,            // 由 arb 文件自动推导
  locale: localeController.locale,                                 // 传 null 表示跟随系统
  home: const HomePage(),
);

运行时切换与持久化

class LocaleController extends ChangeNotifier {
  Locale? _locale;                       // null = 跟随系统
  Locale? get locale => _locale;

  void load(String? code) => _locale = code == null ? null : Locale(code);

  Future<void> setLocale(Locale? locale, SharedPreferences prefs) async {
    _locale = locale;
    notifyListeners();                                    // 切换语言只需重建 MaterialApp
    await (locale == null
        ? prefs.remove('locale')
        : prefs.setString('locale', locale.languageCode)); // 落盘,下次启动恢复
  }
}

切换语言不需要重启应用:MaterialApplocale 变化会触发整棵树重建并加载新文案。注意切换后要处理已经缓存在 State 里的字符串(例如拼接好的日期),把它们改成在 build 里生成。

日期、数字与货币

import 'package:intl/intl.dart';

// 必须传入当前语言的 locale,否则 DateFormat 默认用英文
DateFormat.yMMMd(l10n.localeName).format(DateTime.now());        // 2025年3月8日 / Mar 8, 2025
NumberFormat.decimalPattern(l10n.localeName).format(1234567.8);  // 1,234,567.8
NumberFormat.currency(locale: l10n.localeName, symbol: '¥').format(99.5);
final local = DateTime.parse(utcString).toLocal();               // 服务端给 UTC,展示再转本地

RTL 与双向文本

  • Directionality.of(context) 可读取当前方向;阿拉伯语、希伯来语需要 textDirection: TextDirection.rtl
  • 布局要少用 Row 的硬编码左右对齐,改用 start/endAlignmentDirectionalEdgeInsetsDirectional),它们会自动镜像。
  • Material 组件大多已支持 RTL,自定义绘制与 Positioned(left:) 是主要漏网处。
  • 中英混排时数字与英文方向固定,必要时用 Directionality 局部包裹,避免整段错位。

文案管理与规范

做法原因
禁止在 Text 里直接写中文无法被提取,也无法翻译
key 用语义化英文(productDetailBuyNow文案可改,key 不改,避免翻译返工
每条文案补 description翻译人员需要上下文才能翻准
中文同样走 ARB便于统一管理与后续扩展
与后端的语言协商请求头带 Accept-Language: zh-CN,服务端返回对应语言
页面截图回归德语、俄语文本更长,容易撑破按钮与卡片

常见坑

现象正确做法
忘记 flutter gen-l10n找不到 AppLocalizationspubspec.yaml 里开 generate: true
模板 ARB 缺占位符声明生成的代码参数对不上占位符与类型统一写在 app_en.arb
DateFormat() 不传 locale中文环境显示英文月份l10n.localeName
直接拼接字符串再翻译语序错误、无法复用用占位符整句翻译
文案里写死空格与标点不同语言排版错乱标点写在 ARB 内,由翻译决定

小结:i18n 的落地路径是“pubspec.yamlgenerate: true + 写 l10n.yaml + 建 app_en.arb 模板 + flutter gen-l10n 生成 AppLocalizations”,页面里统一用 AppLocalizations.of(context)! 取文案;复数与占位符写在 ARB、日期货币用 intlDateFormat/NumberFormat 并按 locale 格式化;语言切换只需改 MaterialApp.locale 并持久化,布局要预留文本变长的空间,从第一天起就不要硬编码中文。

笔记加载中…