Flutter: 国际化与本地化
一个应用走天下——国际化让你的代码说用户的话,用用户的钱。
📋 前置知识:需要先掌握以下内容
- 第16课:主题与样式系统
1. 你将学到
- flutter_localizations + intl 包:ARB 文件工作流与代码生成
- localizationsDelegates 与 supportedLocales 配置
- 动态语言切换:运行时 locale 切换(Riverpod 管理)
- ICU 格式化:数字(千位逗号)、货币(USD/EUR/CNY)、日期、复数
- ShopApp:中/英/日三语言 + USD/CNY/JPY 多币种切换
2. 一个出海混乱的真实故事
(1) 痛点:硬编码让全球用户困惑
Bob 的 ShopApp 只支持英文和 USD。Alice(中国用户)看到 "$9,999.00" 以为是 9999 美元,实际应该是 9999 人民币。Charlie(日本用户)看到 "1,500" 不知道是千五百还是一千五百。更尴尬的是,商品描述全英文,非英语用户跳出率 70%。
(2) ARB + ICU 格式化的解法
Flutter 的 l10n 系统用 ARB 文件管理翻译,ICU 格式化自动处理数字/货币/日期的地区差异。
DART
import 'package:intl/intl.dart';
// ⚙️ **安装依赖**:flutter pub add intl
// Locale-aware formatting
final price = NumberFormat.simpleCurrency(locale: 'zh_CN').format(9999);
// → ¥9,999.00 (CNY)
final price2 = NumberFormat.simpleCurrency(locale: 'en_US').format(1299.99);
// → $1,299.99 (USD)
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
(3) 收益:跳出率从 70% 降到 15%
Bob 实现三语言+三币种后,非英语地区跳出率从 70% 降到 15%,日本市场订单量增长 3x。
3. i18n 数据流
graph TD
ARB[ARB Files] --> |l10n| GEN[Generated Dart]
GEN --> MAT[MaterialApp.localizationsDelegates]
MAT --> L10N[AppLocalizations]
L10N --> EN[English: $1,299.99]
L10N --> ZH[中文: ¥9,999.00]
L10N --> JA[日本語: ¥150,000]
subgraph Users
Alice2[Alice: EN / USD]
Bob2[Bob: ZH / CNY]
Charlie[Charlie: JA / JPY]
end
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
(1) 国际化关键术语
| 术语 | 说明 | 示例 |
|---|---|---|
| i18n | Internationalization(18 个字母省略) | 框架支持 |
| l10n | Localization(10 个字母省略) | 具体翻译 |
| Locale | 语言+区域标识 | en_US, zh_CN, ja_JP |
| ARB | Application Resource Bundle | 翻译文件格式 |
| ICU | International Components for Unicode | 格式化标准 |
4. ARB 文件工作流
(1) 项目配置
YAML
# l10n.yaml (project root)
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: S
nullable-getter: false
YAML
# pubspec.yaml
flutter:
generate: true
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:ARB 翻译文件
JSON
// lib/l10n/app_en.arb (template)
{
"appTitle": "ShopApp",
"productCount": "{count, plural, =0{No products} =1{1 product} other{{count} products}}",
"priceWithCurrency": "{price, select, USD{${price}} CNY{¥{price}} JPY{¥{price}}}",
"welcomeMessage": "Welcome, {name}!",
"lastUpdated": "Last updated: {date}",
"@productCount": {
"placeholders": { "count": { "type": "int" } }
},
"@priceWithCurrency": {
"placeholders": { "price": { "type": "String" }, "currency": { "type": "String" } }
},
"@welcomeMessage": {
"placeholders": { "name": { "type": "String" } }
},
"@lastUpdated": {
"placeholders": { "date": { "type": "DateTime" } }
}
}
JSON
// lib/l10n/app_zh.arb
{
"appTitle": "ShopApp",
"productCount": "{count, plural, =0{没有商品} other{{count} 件商品}}",
"priceWithCurrency": "{price, select, USD{\${price}} CNY{¥{price}} JPY{¥{price}}}",
"welcomeMessage": "欢迎,{name}!",
"lastUpdated": "最后更新:{date}"
}
JSON
// lib/l10n/app_ja.arb
{
"appTitle": "ShopApp",
"productCount": "{count, plural, other{{count}件の商品}}",
"priceWithCurrency": "{price, select, USD{\${price}} CNY{¥{price}} JPY{¥{price}}}",
"welcomeMessage": "ようこそ、{name}さん!",
"lastUpdated": "最終更新: {date}"
}
5. MaterialApp 配置
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:l10n 配置
DART
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// ⚙️ **安装依赖**:flutter pub add flutter_riverpod
// ⚙️ **配置 l10n**:项目根目录创建 l10n.yaml,pubspec.yaml 添加 flutter: generate: true
// 自定义类定义来源:
// - localeProvider: 见本课第7节 LocaleNotifier
class ShopApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale = ref.watch(localeProvider);
return MaterialApp(
locale: locale,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
title: 'ShopApp',
home: const HomePage(),
);
}
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:使用翻译字符串
DART
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// ⚙️ **配置 l10n**:项目根目录创建 l10n.yaml,运行 flutter gen-l10n 生成代码
// In any widget
final s = AppLocalizations.of(context)!;
Text(s.appTitle) // ShopApp
Text(s.productCount(42)) // 42 products / 42 件商品
Text(s.welcomeMessage('Alice')) // Welcome, Alice! / 欢迎,Alice!
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
6. ICU 格式化
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:货币格式化
DART
import 'package:intl/intl.dart';
import 'package:intl/number_symbols_data.dart';
// ⚙️ **安装依赖**:flutter pub add intl
class CurrencyFormatter {
static String format(double amount, {String locale = 'en_US', String? currency}) {
final format = NumberFormat.simpleCurrency(locale: locale, name: currency);
return format.format(amount);
}
}
// Usage
CurrencyFormatter.format(1299.99, locale: 'en_US') // $1,299.99
CurrencyFormatter.format(9999.00, locale: 'zh_CN') // ¥9,999.00
CurrencyFormatter.format(150000, locale: 'ja_JP') // ¥150,000
CurrencyFormatter.format(99.99, locale: 'de_DE', currency: 'EUR') // 99,99 €
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
| Locale | 数字格式 | 货币格式 | 日期格式 |
|---|---|---|---|
| en_US | 1,299.99 | $1,299.99 | 07/13/2026 |
| zh_CN | 1,299.99 | ¥9,999.00 | 2026/07/13 |
| ja_JP | 1,299.99 | ¥150,000 | 2026/07/13 |
| de_DE | 1.299,99 | 1.299,99 € | 13.07.2026 |
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:日期格式化
DART
import 'package:intl/intl.dart';
// ⚙️ **安装依赖**:flutter pub add intl
String formatDate(DateTime date, {String locale = 'en_US'}) {
return DateFormat.yMMMd(locale).format(date);
}
// en_US: Jul 13, 2026
// zh_CN: 2026年7月13日
// ja_JP: 2026年7月13日
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:复数格式化
DART
// In ARB file
"cartItemCount": "{count, plural, =0{Your cart is empty} =1{1 item in cart} other{{count} items in cart}}"
// Usage
Text(s.cartItemCount(0)) // Your cart is empty
Text(s.cartItemCount(1)) // 1 item in cart
Text(s.cartItemCount(5)) // 5 items in cart
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
7. 动态语言切换
▶ 示例
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
:Riverpod Locale 管理
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:intl/intl.dart';
// ⚙️ **安装依赖**:flutter pub add flutter_riverpod riverpod_annotation shared_preferences intl
// ⚙️ **开发依赖**:flutter pub add --dev riverpod_generator build_runner
// 自定义类定义来源:
// - localeNotifierProvider/currencyProvider: 见下方定义
@riverpod
class LocaleNotifier extends _$LocaleNotifier {
@override
Locale build() {
// Load saved preference
_loadSavedLocale();
return const Locale('en');
}
Future<void> _loadSavedLocale() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString('locale');
if (saved != null) {
state = Locale(saved);
}
}
Future<void> setLocale(Locale locale) async {
state = locale;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('locale', locale.languageCode);
}
}
// Settings page
class LanguageSettingsPage extends ConsumerWidget {
static const _locales = [
(Locale('en'), 'English', '🇺🇸'),
(Locale('zh'), '中文', '🇨🇳'),
(Locale('ja'), '日本語', '🇯🇵'),
];
@override
Widget build(BuildContext context, WidgetRef ref) {
final current = ref.watch(localeNotifierProvider);
return Scaffold(
appBar: AppBar(title: const Text('Language')),
body: ListView(children: _locales.map((item) {
final (locale, name, flag) = item;
return ListTile(
leading: Text(flag, style: const TextStyle(fontSize: 24)),
title: Text(name),
trailing: current == locale ? const Icon(Icons.check, color: Colors.green) : null,
onTap: () => ref.read(localeNotifierProvider.notifier).setLocale(locale),
);
}).toList()),
);
}
}
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
8. 完整示例:ShopApp 多语言价格组件
DART
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:intl/intl.dart';
// ⚙️ **安装依赖**:flutter pub add flutter_riverpod riverpod_annotation shared_preferences intl
// ⚙️ **开发依赖**:flutter pub add --dev riverpod_generator build_runner
// 自定义类定义来源:
// - localeNotifierProvider: 见本课第7节 LocaleNotifier
// - currencyProvider: 见下方 CurrencyNotifier
class LocalizedPrice extends ConsumerWidget {
final double amount;
final TextStyle? style;
const LocalizedPrice({super.key, required this.amount, this.style});
@override
Widget build(BuildContext context, WidgetRef ref) {
final locale = ref.watch(localeNotifierProvider);
final currency = ref.watch(currencyProvider);
final formatted = _formatPrice(amount, locale.languageCode, currency);
return Text(formatted, style: style ?? const TextStyle(fontSize: 18, fontWeight: FontWeight.bold));
}
String _formatPrice(double amount, String languageCode, String currencyCode) {
final locale = switch (languageCode) {
'zh' => 'zh_CN',
'ja' => 'ja_JP',
_ => 'en_US',
};
return NumberFormat.simpleCurrency(locale: locale, name: currencyCode).format(amount);
}
}
// Currency provider
@riverpod
class CurrencyNotifier extends _$CurrencyNotifier {
@override
String build() {
_loadSaved();
return 'USD';
}
Future<void> _loadSaved() async {
final prefs = await SharedPreferences.getInstance();
final saved = prefs.getString('currency');
if (saved != null) state = saved;
}
Future<void> setCurrency(String code) async {
state = code;
final prefs = await SharedPreferences.getInstance();
await prefs.setString('currency', code);
}
}
// Usage in product card
LocalizedPrice(amount: product.price) // Auto-formats based on user's locale & currency
TEXT
📖 仅展示
> **输出:** 在本地 Flutter SDK(Flutter 3.x / Dart 3.x)运行。Piston 服务器未安装 Flutter,请在本机 `flutter run` 实操对照。实际 UI/状态会因平台略有差异。
❓ 常见问题
Q ARB 文件修改后怎么生效?
A 运行
flutter gen-l10n 或直接 flutter run(会自动触发生成)。Q 热重载支持语言切换吗?
A 部分支持。ARB 内容变更需要热重启(R),Locale 切换可以热重载(r)。
Q 货币格式化用 intl 还是手动拼接?
A 用 intl 的 NumberFormat.simpleCurrency。手动拼接无法处理千位分隔符和小数点差异。
Q 中文的"万"怎么格式化?
A ICU 标准没有"万"单位,中文数字仍用千位逗号分隔(9,999 而非 0.9999万)。如需"万"显示需自定义格式化。
Q RTL 语言(阿拉伯语)怎么支持?
A Flutter 自动处理 RTL 布局(设置
locale: Locale('ar'))。需确保用 Directionality 和 start/end 替代 left/right。Q 翻译文件由谁维护?
A 开发者维护英文模板 ARB,翻译团队或 AI 翻译其他语言 ARB。推荐用 Lokalise/Crowdin 等翻译管理平台。
📖 小节
- ARB 文件管理翻译,flutter gen-l10n 自动生成类型安全的 Dart 代码
- MaterialApp 配置 localizationsDelegates + supportedLocales
- ICU 格式化自动处理数字/货币/日期/复数的地区差异
- Riverpod + SharedPreferences 管理语言和货币偏好
- NumberFormat.simpleCurrency 实现多币种格式化
📝 作业
- 基础题(难度⭐):配置 l10n.yaml 和 ARB 文件,实现中英双语言切换,至少 10 个翻译字符串。
- 进阶题(难度⭐⭐):添加日文支持,实现 ICU 复数格式化(购物车商品数量),数字千位分隔符自动适配。
- 挑战题(难度⭐⭐⭐):实现完整的 ShopApp 多语言系统:中/英/日三语言 + USD/CNY/JPY 三币种 + 日期格式本地化 + 设置页切换 + SharedPreferences 持久化。