Flutter国际化与主题

What — 是什么

Flutter 国际化(i18n)与主题系统是应用的两个核心基础设施。国际化让应用支持多语言和地区适配,主题系统统一管理颜色、字体、组件样式,两者配合实现多语言 + 多主题的企业级应用。

国际化核心概念:

  • Locale:语言和地区标识(如 zh_CN、en_US)
  • ARB 文件:Application Resource Bundle,标准翻译文件格式
  • flutter_localizations:官方本地化支持包
  • intl 包:消息格式化、日期/数字本地化

主题核心概念:

  • ThemeData:Material 主题数据对象,包含颜色、字体、组件样式
  • ColorScheme:Material 3 颜色方案,从种子色生成完整色板
  • TextTheme:文本样式体系
  • InheritedWidget:主题通过 InheritedWidget 向下传递

Why — 为什么

适用场景:

  • 应用需要支持多语言(中文/英文/日文等)
  • 需要暗黑模式切换
  • 多品牌/多主题白标应用
  • 日期/数字/货币需要地区适配

与 Web 端对比:

维度Flutter i18nWeb i18n (i18next)
翻译格式ARB (JSON)JSON
代码生成intl_utils / 自动手动
切换语言setState + Localei18next.changeLanguage
日期格式intl 包Intl.DateTimeFormat
暗黑模式ThemeData.dark()CSS 变量 / prefers-color-scheme
主题系统ThemeData + ColorSchemeCSS 变量 / CSS-in-JS

How — 怎么用

1. 国际化基础配置

# pubspec.yaml
dependencies:
  flutter_localizations:
    sdk: flutter
  intl: ^0.19.0
// ===== MaterialApp 配置 =====
MaterialApp(
  localizationsDelegates: const [
    AppLocalizations.delegate,           // 应用翻译
    GlobalMaterialLocalizations.delegate, // Material 组件翻译
    GlobalWidgetsLocalizations.delegate,  // Widget 翻译
    GlobalCupertinoLocalizations.delegate,// Cupertino 组件翻译
  ],
  supportedLocales: const [
    Locale('zh', 'CN'),
    Locale('en', 'US'),
    Locale('ja', 'JP'),
  ],
  locale: _currentLocale,               // 当前语言
  // 或根据系统语言自动选择
  // localeListResolutionCallback: (locales, supported) {
  //   for (var locale in locales) {
  //     if (supported.contains(locale)) return locale;
  //   }
  //   return const Locale('en', 'US');
  // },
  home: const HomePage(),
);

2. ARB 文件与 intl_utils

# 安装 intl_utils
dart pub global activate intl_utils

# 初始化
intl_utils init

# 生成代码(从 ARB 文件生成 Dart 类)
intl_utils generate

ARB 文件示例:

// l10n/app_zh.arb
{
  "@@locale": "zh",
  "appTitle": "我的应用",
  "greeting": "你好,{name}!",
  "@greeting": {
    "placeholders": {
      "name": { "type": "String" }
    }
  },
  "itemCount": "{count} 个项目",
  "@itemCount": {
    "placeholders": {
      "count": { "type": "int" }
    }
  },
  "homeTab": "首页",
  "profileTab": "我的",
  "settingsTitle": "设置"
}

// l10n/app_en.arb
{
  "@@locale": "en",
  "appTitle": "My App",
  "greeting": "Hello, {name}!",
  "itemCount": "{count} items",
  "homeTab": "Home",
  "profileTab": "Profile",
  "settingsTitle": "Settings"
}

生成的 Dart 类使用:

// 获取翻译文本
Text(AppLocalizations.of(context)!.appTitle)
Text(AppLocalizations.of(context)!.greeting('Alice'))
Text(AppLocalizations.of(context)!.itemCount(5))

3. Flutter 3.10+ 新版国际化(推荐)

# l10n.yaml(项目根目录)
arb-dir: lib/l10n
template-arb-file: app_zh.arb
output-localization-file: app_localizations.dart
// MaterialApp 配置
MaterialApp(
  onGenerateTitle: (context) => AppLocalizations.of(context)!.appTitle,
  localizationsDelegates: AppLocalizations.localizationsDelegates,
  supportedLocales: AppLocalizations.supportedLocales,
  locale: _locale,
);
# 生成
flutter gen-l10n

4. 运行时切换语言

class LocaleProvider extends ChangeNotifier {
  Locale _locale = const Locale('zh', 'CN');
  final SharedPreferences _prefs;

  LocaleProvider(this._prefs) {
    final saved = _prefs.getString('locale');
    if (saved != null) {
      final parts = saved.split('_');
      _locale = Locale(parts[0], parts.length > 1 ? parts[1] : null);
    }
  }

  Locale get locale => _locale;

  void setLocale(Locale locale) {
    if (_locale == locale) return;
    _locale = locale;
    _prefs.setString('locale', locale.toString());
    notifyListeners();
  }
}

// 在顶层提供
ChangeNotifierProvider(
  create: (context) => LocaleProvider(prefs),
  child: const MyApp(),
);

// 使用
class LanguageSwitcher extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final localeProv = context.watch<LocaleProvider>();
    return DropdownButton<Locale>(
      value: localeProv.locale,
      items: const [
        DropdownMenuItem(value: Locale('zh', 'CN'), child: Text('中文')),
        DropdownMenuItem(value: Locale('en', 'US'), child: Text('English')),
        DropdownMenuItem(value: Locale('ja', 'JP'), child: Text('日本語')),
      ],
      onChanged: (locale) => localeProv.setLocale(locale!),
    );
  }
}

5. RTL/LTR 文本方向

// 自动适配(根据 Locale)
Directionality(
  textDirection: TextDirection.rtl,  // 阿拉伯语、希伯来语
  child: Text('مرحبا'),
);

// 检测当前方向
bool isRTL = Directionality.of(context) == TextDirection.rtl;

// EdgeInsets 方向适配
final padding = EdgeInsetsDirectional.fromSTEB(16, 8, 16, 8);
// start=左(LTR)/右(RTL), end=右(LTR)/左(RTL)

6. 地区特定格式

import 'package:intl/intl.dart';

// ===== 日期格式 =====
var now = DateTime.now();
var dateFormat = DateFormat.yMMMMd('zh_CN');  // 2026年5月11日
var shortDate = DateFormat.yMd('en_US');      // 5/11/2026
var fullDate = DateFormat.EEEE('ja_JP');      // 月曜日

// 自定义格式
var custom = DateFormat('yyyy-MM-dd HH:mm');
print(custom.format(now));  // 2026-05-11 14:30

// ===== 数字格式 =====
var numFormat = NumberFormat.decimalPattern('zh_CN');
print(numFormat.format(1234567));  // 1,234,567

var currencyFormat = NumberFormat.currency(locale: 'zh_CN', symbol: '¥');
print(currencyFormat.format(99.9));  // ¥99.90

var percentFormat = NumberFormat.percentPattern('en_US');
print(percentFormat.format(0.85));  // 85%

7. 主题系统

// ===== ThemeData 基础 =====
ThemeData({
  ColorScheme? colorScheme,       // 颜色方案(Material 3 核心)
  TextTheme? textTheme,           // 文本样式
  AppBarTheme? appBarTheme,       // AppBar 样式
  CardTheme? cardTheme,           // Card 样式
  ElevatedButtonThemeData? elevatedButtonTheme,  // 按钮样式
  InputDecorationTheme? inputDecorationTheme,    // 输入框样式
  // ... 几十种组件主题
})

// ===== Material 3 主题(推荐)=====
ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.blue,        // 种子色,自动生成完整色板
    brightness: Brightness.light,
  ),
);

// 暗黑模式
ThemeData(
  useMaterial3: true,
  colorScheme: ColorScheme.fromSeed(
    seedColor: Colors.blue,
    brightness: Brightness.dark,
  ),
);

// ===== 动态颜色(Android 12+ Dynamic Color)=====
// flutter pub add dynamic_color
DynamicColorBuilder(
  builder: (ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
    ColorScheme lightColorScheme;
    ColorScheme darkColorScheme;

    if (lightDynamic != null) {
      lightColorScheme = lightDynamic;
      darkColorScheme = darkDynamic!;
    } else {
      lightColorScheme = ColorScheme.fromSeed(seedColor: Colors.blue);
      darkColorScheme = ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark);
    }

    return MaterialApp(
      theme: ThemeData(colorScheme: lightColorScheme, useMaterial3: true),
      darkTheme: ThemeData(colorScheme: darkColorScheme, useMaterial3: true),
      themeMode: ThemeMode.system,
      home: const HomePage(),
    );
  },
);

8. 暗黑模式

// ===== 三种模式 =====
ThemeMode.system    // 跟随系统
ThemeMode.light     // 强制浅色
ThemeMode.dark      // 强制深色

// ===== 切换实现 =====
class ThemeProvider extends ChangeNotifier {
  ThemeMode _mode = ThemeMode.system;
  final SharedPreferences _prefs;

  ThemeProvider(this._prefs) {
    final saved = _prefs.getString('themeMode');
    if (saved != null) {
      _mode = ThemeMode.values.firstWhere((m) => m.name == saved, orElse: () => ThemeMode.system);
    }
  }

  ThemeMode get mode => _mode;

  void setMode(ThemeMode mode) {
    _mode = mode;
    _prefs.setString('themeMode', mode.name);
    notifyListeners();
  }

  void toggle() {
    setMode(_mode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light);
  }
}

// MaterialApp 配置
MaterialApp(
  theme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
    useMaterial3: true,
  ),
  darkTheme: ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue, brightness: Brightness.dark),
    useMaterial3: true,
  ),
  themeMode: context.watch<ThemeProvider>().mode,
);

9. 自定义主题

// ===== 自定义颜色扩展 =====
class AppColors extends ThemeExtension<AppColors> {
  final Color brand;
  final Color success;
  final Color warning;

  const AppColors({
    required this.brand,
    required this.success,
    required this.warning,
  });

  @override
  AppColors copyWith({Color? brand, Color? success, Color? warning}) {
    return AppColors(
      brand: brand ?? this.brand,
      success: success ?? this.success,
      warning: warning ?? this.warning,
    );
  }

  @override
  AppColors lerp(covariant AppColors other, double t) {
    return AppColors(
      brand: Color.lerp(brand, other.brand, t)!,
      success: Color.lerp(success, other.success, t)!,
      warning: Color.lerp(warning, other.warning, t)!,
    );
  }
}

// 注册到 ThemeData
ThemeData(
  extensions: const [
    AppColors(brand: Color(0xFF6750A4), success: Colors.green, warning: Colors.orange),
  ],
);

// 使用
final appColors = Theme.of(context).extension<AppColors>()!;
Container(color: appColors.brand);

// ===== 组件样式覆盖 =====
ThemeData(
  elevatedButtonTheme: ElevatedButtonThemeData(
    style: ElevatedButton.styleFrom(
      minimumSize: const Size(double.infinity, 48),
      shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
    ),
  ),
  inputDecorationTheme: InputDecorationTheme(
    border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
    filled: true,
    fillColor: Colors.grey.shade100,
  ),
  cardTheme: CardTheme(
    elevation: 2,
    shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
  ),
);

10. 多品牌/多主题方案

// ===== 品牌主题定义 =====
class BrandTheme {
  final String name;
  final Color seedColor;
  final String? logoAsset;

  const BrandTheme({required this.name, required this.seedColor, this.logoAsset});
}

const brands = [
  BrandTheme(name: 'Default', seedColor: Colors.blue),
  BrandTheme(name: 'Brand A', seedColor: Colors.red),
  BrandTheme(name: 'Brand B', seedColor: Colors.green),
];

// 切换品牌
void setBrand(BrandTheme brand) {
  _currentBrand = brand;
  _lightTheme = ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: brand.seedColor),
    useMaterial3: true,
  );
  _darkTheme = ThemeData(
    colorScheme: ColorScheme.fromSeed(seedColor: brand.seedColor, brightness: Brightness.dark),
    useMaterial3: true,
  );
  notifyListeners();
}

常见问题与踩坑

问题原因解决方案
翻译不生效ARB 文件未生成代码运行 flutter gen-l10n
切换语言不更新locale 未 setState通过状态管理更新
暗黑模式部分组件不跟随硬编码颜色使用 Theme.of(context).colorScheme
RTL 布局错乱用了 LTR 假设用 EdgeInsetsDirectional / AlignmentDirectional
日期格式异常未指定 localeDateFormat(pattern, localeStr)
ColorScheme 不够用需要自定义颜色ThemeExtension 扩展
字体不生效未在 pubspec.yaml 声明添加 fonts 配置

最佳实践

  • 使用 flutter gen-l10n 管理翻译,ARB 格式与翻译工具兼容
  • 语言设置持久化到 SharedPreferences
  • 主题切换用 ThemeMode + Provider/Riverpod
  • Material 3 用 ColorScheme.fromSeed 自动生成色板
  • 自定义颜色用 ThemeExtension 扩展
  • 避免硬编码颜色,统一用 Theme.of(context)
  • RTL 语言用 Directional 系列属性
  • 日期/数字格式化用 intl 包指定 locale

面试题

Q1: Flutter 国际化的完整流程是什么?ARB 文件是什么?

完整流程:①创建 l10n.yaml 配置文件指定 ARB 目录;②编写 ARB 翻译文件(JSON 格式,每种语言一个);③运行 flutter gen-l10n 生成 Dart 代码;④在 MaterialApp 配置 localizationsDelegates 和 supportedLocales;⑤通过 AppLocalizations.of(context)!.key 访问翻译。ARB(Application Resource Bundle)是标准的翻译文件格式,基于 JSON,支持占位符({name})、复数(plural)、性别(gender)等高级特性,被 Google 翻译工具和 Crowdin/Transifex 等平台支持。

Q2: Flutter 的暗黑模式如何实现?ThemeMode 的三种模式有什么区别?

实现方式:在 MaterialApp 中配置 theme(浅色主题)、darkTheme(深色主题)和 themeMode(模式选择)。ThemeMode.system 跟随系统设置自动切换;ThemeMode.light 强制浅色,忽略系统设置;ThemeMode.dark 强制深色。关键点是 theme 和 darkTheme 必须都配置,缺一个就不会切换。使用 ColorScheme.fromSeed 配合 brightness 参数可以自动生成配套的浅色/深色色板。切换时通过状态管理更新 themeMode,持久化用 SharedPreferences。

Q3: Material 3 的 ColorScheme.fromSeed 是如何工作的?种子色生成色板的原理?

ColorScheme.fromSeed 接收一个种子色(seedColor),通过 HCT 色彩空间算法自动生成完整的 ColorScheme(包含 primary、onPrimary、secondary、tertiary、error、surface 等约 30 种颜色)。原理:种子色映射到 HCT 色彩空间(Hue-Chroma-Tone),通过调整 Tone 值生成不同明暗的变体色,确保所有颜色在视觉上和谐统一且满足 WCAG 对比度要求。开发者只需选一个品牌色,无需手动调配色板。配合 brightness 参数,同一种子色可生成浅色和深色两套方案。

Q4: ThemeExtension 是什么?为什么需要它?

ThemeExtension 允许开发者在 ThemeData 中注册自定义主题数据(如品牌色、渐变色、间距系统),与内置的 ColorScheme/TextTheme 并列存在。需要它的原因:ColorScheme 只提供 Material 规范的颜色槽位,无法添加自定义语义色(如 success、warning、brand);ThemeExtension 让自定义颜色也参与主题切换和 lerp 插值动画。实现方式:继承 ThemeExtension,实现 copyWith 和 lerp 方法,注册到 ThemeData.extensions,通过 Theme.of(context).extension() 获取。

Q5: Flutter 中如何处理 RTL(从右到左)语言的布局?

Flutter 内置 RTL 支持,当 Locale 为阿拉伯语(ar)、希伯来语(he)等 RTL 语言时,Directionality 自动设为 rtl。布局组件(Row/Column/Padding)使用 Directional 变体:EdgeInsetsDirectional 替代 EdgeInsets(start=右、end=左);AlignmentDirectional 替代 Alignment;SliverAppBar 自动翻转。开发原则:①用 start/end 替代 left/right;②用 EdgeInsetsDirectional 替代 EdgeInsets.only;③图标和箭头需要镜像时用 Transform.flip;④测试时强制 RTL:Directionality(textDirection: TextDirection.rtl, child: widget)

Q6: intl 包的 DateFormat 和 NumberFormat 如何处理地区差异?

DateFormat 和 NumberFormat 都接受 locale 参数,根据地区自动选择格式规则。日期差异:同一日期,en_US 格式为 “May 11, 2026”,zh_CN 为 “2026年5月11日”,ja_JP 为 “2026年5月11日”,de_DE 为 “11. Mai 2026”。数字差异:1234567.89,en_US 为 “1,234,567.89”,de_DE 为 “1.234.567,89”,fr_FR 为 “1 234 567,89”。货币差异:¥100(中文)、$100(美式)、100 €(法式)。使用时必须传入 locale 字符串,否则使用系统默认。

Q7: 如何实现运行时切换语言?切换后所有页面都更新吗?

运行时切换语言:通过状态管理(Provider/Riverpod)持有当前 Locale,在 MaterialApp 的 locale 属性中监听变化。切换时更新 Locale 对象并 notifyListeners,MaterialApp 重建后所有依赖 AppLocalizations.of(context) 的 Widget 都会更新。关键机制:AppLocalizations.delegate 是 InheritedWidget,Locale 变化时所有 dependOnInheritedWidget 的 Widget 自动 rebuild。注意事项:①需要在 MaterialApp 层级设置 locale,不能在子 Widget 局部切换;②切换后弹出/关闭的 Dialog 也使用新语言;③持久化语言选择避免重启后恢复。

Q8: Flutter Web 端的国际化有什么特殊处理?

Web 端特殊处理:①URL 路径带语言前缀(/en/about、/zh/about),需要 GoRouter 配合 locale 切换;②浏览器语言检测通过 navigator.language 获取,Flutter 自动处理;③HTML lang 属性需要随 Locale 更新(通过 flutter.js 或自定义脚本);④SEO 考虑:每种语言的页面应独立 URL,便于搜索引擎索引;⑤Cookie/LocalStorage 存储语言偏好;⑥刷新页面时需要从 URL 或 Storage 恢复语言设置;⑦Web 端不支持 dynamic_color 插件,需要 fallback 主题。


相关链接: