国际化与本地化
把界面文案硬编码在 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)); // 落盘,下次启动恢复
}
}
切换语言不需要重启应用:MaterialApp 的 locale 变化会触发整棵树重建并加载新文案。注意切换后要处理已经缓存在 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/end(AlignmentDirectional、EdgeInsetsDirectional),它们会自动镜像。 - Material 组件大多已支持 RTL,自定义绘制与
Positioned(left:)是主要漏网处。 - 中英混排时数字与英文方向固定,必要时用
Directionality局部包裹,避免整段错位。
文案管理与规范
| 做法 | 原因 |
|---|---|
禁止在 Text 里直接写中文 | 无法被提取,也无法翻译 |
key 用语义化英文(productDetailBuyNow) | 文案可改,key 不改,避免翻译返工 |
每条文案补 description | 翻译人员需要上下文才能翻准 |
| 中文同样走 ARB | 便于统一管理与后续扩展 |
| 与后端的语言协商 | 请求头带 Accept-Language: zh-CN,服务端返回对应语言 |
| 页面截图回归 | 德语、俄语文本更长,容易撑破按钮与卡片 |
常见坑
| 坑 | 现象 | 正确做法 |
|---|---|---|
忘记 flutter gen-l10n | 找不到 AppLocalizations | 在 pubspec.yaml 里开 generate: true |
| 模板 ARB 缺占位符声明 | 生成的代码参数对不上 | 占位符与类型统一写在 app_en.arb |
用 DateFormat() 不传 locale | 中文环境显示英文月份 | 传 l10n.localeName |
| 直接拼接字符串再翻译 | 语序错误、无法复用 | 用占位符整句翻译 |
| 文案里写死空格与标点 | 不同语言排版错乱 | 标点写在 ARB 内,由翻译决定 |
小结:i18n 的落地路径是“pubspec.yaml 开 generate: true + 写 l10n.yaml + 建 app_en.arb 模板 + flutter gen-l10n 生成 AppLocalizations”,页面里统一用 AppLocalizations.of(context)! 取文案;复数与占位符写在 ARB、日期货币用 intl 的 DateFormat/NumberFormat 并按 locale 格式化;语言切换只需改 MaterialApp.locale 并持久化,布局要预留文本变长的空间,从第一天起就不要硬编码中文。