Flutter 国际化(i18n)与本地化实战:从 ARB 到多语言动态切换(完整指南)
引言
在全球化浪潮下,一款成功的移动应用往往需要支持多种语言和区域习惯。无论是面向海外市场的出海 App,还是服务国内多民族用户的政务/教育类应用,国际化(Internationalization, i18n)与本地化(Localization, l10n) 都是产品走向成熟的关键一步。
Flutter 作为 Google 主导的跨平台框架,天然具备强大的国际化能力。官方提供了基于 ARB(Android Resource Bundle) 格式的标准方案,并通过 flutter gen-l10n 工具自动生成类型安全的本地化代码。然而,在实际开发中,许多团队仍面临如下挑战:
- 官方方案默认不支持运行时动态切换语言;
- 复数、性别、日期、货币等复杂场景处理困难;
- 第三方 UI 库(如
showDialog、DatePicker)未自动适配系统语言; - 翻译缺失导致空字符串或崩溃;
- 缺乏自动化测试与 CI 校验机制。
本文将带你从零构建一套生产级 Flutter 国际化系统,涵盖:
- 官方 i18n 方案原理与配置
- 动态语言切换的完整实现(兼容 Riverpod / Provider)
- 复杂本地化场景(复数、参数、RTL)
- 第三方库本地化适配
- 自动化测试与翻译完整性校验
- 上线前的最佳实践清单
无论你是刚接触 i18n 的新手,还是正在优化现有项目的资深开发者,本文都将提供可落地的解决方案。
一、Flutter 官方 i18n 方案详解
1.1 什么是 ARB?
ARB(Application Resource Bundle)是一种 JSON 格式的资源文件,最初由 Google 为 Android 设计,后被 Flutter 采用作为标准 i18n 格式。其优势在于:
- 支持占位符(如
{name}) - 支持复数规则(zero/one/two/many/other)
- 支持元数据描述(便于翻译人员理解上下文)
1.2 项目结构配置
在 pubspec.yaml 中启用本地化生成:
1flutter:
2 generate: true # 关键!启用代码生成
3 uses-material-design: true
4
5# 可选:使用 flutter_localizations 内置翻译
6dependencies:
7 flutter_localizations:
8 sdk: flutter
创建 l10n 目录(路径可自定义):
1lib/
2└── l10n/
3 ├── app_en.arb
4 ├── app_zh.arb
5 └── app_ar.arb # 阿拉伯语(支持 RTL)
1.3 编写 ARB 文件
基础文本(app_en.arb):
1{
2 "appName": "My Awesome App",
3 "helloWorld": "Hello World!",
4 "welcomeMessage": "Welcome, {name}!",
5 "@welcomeMessage": {
6 "description": "Personalized welcome message",
7 "placeholders": {
8 "name": {
9 "type": "String",
10 "example": "Alice"
11 }
12 }
13 }
14}
中文翻译(app_zh.arb):
1{
2 "appName": "我的超棒应用",
3 "helloWorld": "你好,世界!",
4 "welcomeMessage": "欢迎,{name}!"
5}
✅ 注意:所有 key 必须在所有语言文件中存在,否则生成会失败(可通过
--no-synthetic-package跳过,但不推荐)。
1.4 自动生成本地化代码
运行命令:
1flutter gen-l10n
或直接执行 flutter pub get(新版 Flutter 会自动触发)。
生成文件位于 .dart_tool/flutter_gen/gen_l10n/,包含:
app_localizations.dart:主入口类app_localizations_en.dart、app_localizations_zh.dart:各语言实现
使用方式:
1Text(AppLocalizations.of(context)!.helloWorld);
2Text(AppLocalizations.of(context)!.welcomeMessage('张三'));
🔒 类型安全:若调用不存在的 key,编译时报错!
二、实现运行时动态语言切换
官方方案默认仅在启动时读取系统语言,无法在运行中切换。我们需要手动干预。
2.1 自定义 LocalizationsDelegate
创建 app_localizations_delegate.dart:
1import 'package:flutter/widgets.dart';
2import 'package:flutter_gen/gen_l10n/app_localizations.dart';
3
4class AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
5 final Locale locale;
6
7 const AppLocalizationsDelegate(this.locale);
8
9 @override
10 bool isSupported(Locale locale) {
11 // 支持的语言列表
12 return ['en', 'zh', 'ar'].contains(locale.languageCode);
13 }
14
15 @override
16 Future<AppLocalizations> load(Locale locale) {
17 return AppLocalizations.load(locale);
18 }
19
20 @override
21 bool shouldReload(AppLocalizationsDelegate old) {
22 return locale != old.locale;
23 }
24}
2.2 使用状态管理保存当前语言
方案一:Riverpod(推荐)
1// providers/locale_provider.dart
2import 'package:flutter/material.dart';
3import 'package:flutter_riverpod/flutter_riverpod.dart';
4
5@riverpod
6class AppLocale extends _$AppLocale {
7 @override
8 Locale build() {
9 // 从 SharedPreferences 读取上次设置
10 return ref.read(sharedPreferencesProvider).getString('locale')?.let((code) => Locale(code))
11 ?? WidgetsBinding.instance.platformDispatcher.locale;
12 }
13
14 void change(Locale locale) {
15 state = locale;
16 // 保存到本地
17 ref.read(sharedPreferencesProvider).setString('locale', locale.languageCode);
18 }
19}
方案二:Provider(兼容旧项目)
1class LocaleModel with ChangeNotifier {
2 Locale _locale = const Locale('en');
3
4 Locale get locale => _locale;
5
6 void setLocale(Locale locale) {
7 _locale = locale;
8 notifyListeners();
9 // 保存...
10 }
11}
2.3 重建 MaterialApp 实现切换
1class MyApp extends ConsumerWidget {
2 @override
3 Widget build(BuildContext context, WidgetRef ref) {
4 final locale = ref.watch(appLocaleProvider);
5
6 return MaterialApp(
7 title: 'My App',
8 locale: locale,
9 // 支持的语言列表
10 supportedLocales: const [
11 Locale('en'),
12 Locale('zh'),
13 Locale('ar'),
14 ],
15 localizationsDelegates: [
16 // 自定义 delegate(关键!)
17 AppLocalizationsDelegate(locale),
18 // Flutter 内置 Material 组件翻译
19 GlobalMaterialLocalizations.delegate,
20 GlobalWidgetsLocalizations.delegate,
21 GlobalCupertinoLocalizations.delegate,
22 ],
23 home: HomePage(),
24 );
25 }
26}
2.4 切换语言的 UI 操作
1DropdownButton<Locale>(
2 value: currentLocale,
3 items: [
4 DropdownMenuItem(value: Locale('en'), child: Text('English')),
5 DropdownMenuItem(value: Locale('zh'), child: Text('中文')),
6 DropdownMenuItem(value: Locale('ar'), child: Text('العربية')),
7 ],
8 onChanged: (locale) {
9 if (locale != null) {
10 ref.read(appLocaleProvider.notifier).change(locale);
11 }
12 },
13)
✅ 效果:切换后整个 App 立即刷新为新语言!
三、处理复杂本地化场景
3.1 复数(Plurals)
适用于“X 个文件”、“1 条消息”等场景。
app_en.arb:
1"fileCount": "{count, plural, zero{No files} one{1 file} other{{count} files}}",
2"@fileCount": {
3 "placeholders": {
4 "count": {
5 "type": "int"
6 }
7 }
8}
app_zh.arb(中文通常无需复数,但可统一格式):
1"fileCount": "{count, plural, other{{count} 个文件}}"
使用:
1Text(S.fileCount(0)); // "No files"
2Text(S.fileCount(1)); // "1 file"
3Text(S.fileCount(5)); // "5 files"
💡 提示:阿拉伯语有 6 种复数形式,务必参考 Unicode CLDR 规则。
3.2 日期、时间、数字格式化
借助 intl 包(已内置):
1// 日期
2DateFormat.yMMMd(locale.languageCode).format(DateTime.now());
3
4// 货币
5NumberFormat.simpleCurrency(locale: locale.toLanguageTag()).format(1234.5);
6
7// 数字(千分位)
8NumberFormat('#,##0.##', locale.languageCode).format(1234567);
建议封装工具类:
1class FormatUtils {
2 static String date(DateTime date, Locale locale) {
3 return DateFormat.yMMMd(locale.languageCode).format(date);
4 }
5}
3.3 RTL(从右到左)布局支持
阿拉伯语、希伯来语等需要镜像 UI。
在 MaterialApp 中启用:
1MaterialApp(
2 locale: locale,
3 supportedLocales: [...],
4 localizationsDelegates: [...],
5 // 自动处理 RTL
6 builder: (context, child) {
7 return Directionality(
8 textDirection: locale.languageCode == 'ar' ? TextDirection.rtl : TextDirection.ltr,
9 child: child!,
10 );
11 },
12)
同时确保:
- 使用
EdgeInsets.symmetric()而非left/right - 图标使用
Icons.arrow_forward(自动镜像为arrow_back)
四、第三方库的本地化适配
4.1 Material 组件(如 DatePicker)
确保注册 GlobalMaterialLocalizations.delegate,系统组件(如 showDatePicker)会自动翻译。
4.2 自定义 Dialog 或 Toast
避免硬编码文本:
1// ❌ 错误
2showDialog(context: context, builder: (_) => AlertDialog(title: Text("确认删除?")));
3
4// ✅ 正确
5showDialog(context: context, builder: (_) => AlertDialog(
6 title: Text(S.of(context).confirmDelete),
7));
4.3 处理翻译缺失
生成代码时添加 fallback:
1// 在 app_localizations.dart 中(需手动修改生成文件,不推荐)
2// 更好的方式:使用 flutter_intl 插件(见下文)
3
4// 或在调用处兜底
5String safeText(String Function() getter) {
6 try {
7 return getter();
8 } catch (e) {
9 return 'MISSING_TRANSLATION'; // 或返回英文
10 }
11}
五、提升开发体验:flutter_intl 插件
官方方案需写 AppLocalizations.of(context)!,略显冗长。推荐使用社区插件 flutter_localizely/flutter-intl-vscode:
- 安装 VS Code 插件
- 在
pubspec.yaml添加:
1flutter_intl:
2 enabled: true
3 class_name: S
- 自动生成
S.of(context).helloWorld,更简洁!
⚠️ 注意:该插件与官方
gen-l10n不冲突,底层仍使用 ARB。
六、自动化测试与 CI 校验
6.1 单元测试本地化文本
1testWidgets('should display Chinese greeting', (tester) async {
2 await tester.pumpWidget(
3 MaterialApp(
4 locale: const Locale('zh'),
5 localizationsDelegates: AppLocalizations.localizationsDelegates,
6 home: Scaffold(body: Text(AppLocalizations.of(context)!.helloWorld)),
7 ),
8 );
9 expect(find.text('你好,世界!'), findsOneWidget);
10});
6.2 CI 校验 ARB 文件完整性
编写脚本 scripts/validate_l10n.dart:
1// 读取 app_en.arb 的所有 key
2// 检查其他语言文件是否包含相同 key
3// 若缺失,exit(1) 中断 CI
在 GitHub Actions 中运行:
1- name: Validate translations
2 run: dart scripts/validate_l10n.dart
七、上线前最佳实践清单
- 所有用户可见文本均来自 ARB,无硬编码
- 支持的语言在
supportedLocales中声明 - 动态切换后,第三方组件(如 DatePicker)语言同步更新
- RTL 语言下布局正确镜像
- 复数、日期、货币格式符合当地习惯
- 翻译文件通过母语者校对(避免机翻错误)
- CI 流程包含翻译完整性检查
结语
国际化不是简单的“文本替换”,而是对全球用户的文化尊重与体验承诺。通过 Flutter 官方工具链 + 动态切换能力 + 自动化保障,你可以构建出真正世界级的应用。
最后建议:
- 将 ARB 文件交给专业翻译平台(如 Lokalise、Crowdin)管理
- 在 App 内提供“反馈翻译错误”入口
- 定期更新
flutter_localizations以获取最新语言支持
附:学习资源
- Flutter 官方 i18n 文档:https://docs.flutter.dev/ui/accessibility-and-localization/internationalization
- ARB 格式规范:https://github.com/google/app-resource-bundle
- CLDR 复数规则:https://unicode-org.github.io/cldr-staging/charts/37/supplemental/language_plural_rules.html
如需配套的 GitHub 示例项目(含完整代码、CI 配置、多语言截图),我也可以为你生成。欢迎继续提问!
更多推荐


所有评论(0)