Flutter之旅 -- 多语言(本地化/国际化)

本篇文章主要介绍以下几个内容:

  • Flutter 国际化原理
  • 与 Android/iOS 原生国际化的对比
  • 使用 Riverpod 实现语言切换的代码示例
Flutter之旅

本文实现效果:


Flutter 国际化

1. Flutter 国际化底层原理

1.1 核心组件与数据流

Flutter 国际化的核心由三部分构成,并通过一条明确的数据流完成 “选择语言 → 加载资源 → 分发到树 → 精准重建”。

  1. LocalizationsLocalizationsDelegate

    • Localizations 是一个 InheritedWidget,负责在 Widget 树中提供当前 Locale 的本地化资源实例。
    • LocalizationsDelegate<T> 负责“按需加载 + 构建”类型为 T 的资源(本项目为 S),并交由 Localizations 管理与缓存。
  2. Locale 的解析与选取

    • 入口在 MaterialAppsupportedLocales 定义支持列表;locale 可显式指定当前语言(不指定则跟随系统);
    • 可选回调 localeListResolutionCallback/localeResolutionCallback 用于自定义匹配策略(如兼容 zhzh_CN)。
  3. 资源访问类 S

    • 由 Intl 工具链根据 .arb 生成,用于类型安全地访问文案:S.of(context)S.current
    • 委托 S.delegateAppLocalizationDelegate)在 load(Locale) 时构建并更新当前实例。

Localizations 树装配流程(逐步)

  • 启动时,MaterialApp 创建并插入 Localizations 到根部;
  • 遍历 localizationsDelegates,调用每个 delegate 的 isSupportedload(locale)
  • 对于 S.delegateload(locale) 会设置 Intl.defaultLocale 并创建新的 S 实例,赋值给 S._current
  • 树中通过 Localizations.of<S>(context, S)(即 S.of(context))读取到当前 S 实例。

示例(来自生成代码的关键路径):

// lib/generated/l10n.dart(节选逻辑)
static Future<S> load(Locale locale) {
  final localeName = Intl.canonicalizedLocale(/* ... */);
  return initializeMessages(localeName).then((_) {
    Intl.defaultLocale = localeName;
    final instance = S();
    S._current = instance; // 更新全局当前实例
    return instance;
  });
}

依赖追踪与重建触发

  • 任何使用 S.of(context) 的 Widget,会通过 Localizations.ofLocalizations 建立依赖;
  • locale 或 delegates 改变时,Localizations 会更新并通知依赖者,触发这些节点的“局部重建”;
  • 这保证了语言切换时,只刷新依赖文案的组件,代价最小。

Delegate 缓存与 shouldReload

  • 生成的 AppLocalizationDelegate.shouldReload 返回 false(稳定资源不需要因为 delegate 本身变化而重载);
  • 真正触发重新加载的,是 locale 变化(MaterialApp.locale 或系统语言更改)导致的 Localizations 重新构建。

Locale 解析顺序(默认)

  • 指定了 locale:优先使用该 Locale
  • 未指定 locale:按系统首选语言列表与 supportedLocales 匹配;
  • 匹配失败:回落到 supportedLocales 的第一个条目。

本质上,Flutter 通过 Localizations 在 Widget 树中下发“当前语言资源”的引用;当 locale 变更时,由 Localizations 精准通知依赖节点重建,实现动态更新多语言文案。

1.2 与主题系统的类比

  • 主题切换依赖 InheritedWidgetTheme),国际化同样依赖 InheritedWidgetLocalizations)。
  • 变更 ThemeMode 会导致依赖主题的节点重建;变更 Locale 会导致依赖文案的节点重建。二者机制类似,都是高效的“精准重建”。

2. 跨平台国际化方案对比

2.1 Android 原生(资源限定符)

<!-- res/values/strings.xml -->
<resources>
    <string name="ok">OK</string>
    <string name="cancel">Cancel</string>
    <string name="device_list_count">%1$d devices</string>
}</resources>

<!-- res/values-zh/strings.xml -->
<resources>
    <string name="ok">确定</string>
    <string name="cancel">取消</string>
    <string name="device_list_count">%1$d个设备</string>
</resources>
  • 特点:基于资源目录(values-xx)与 Configuration 的系统级匹配;切换语言常伴随 Activity 重建。
  • 使用:getString(R.string.ok)

2.2 iOS 原生(Localizable.strings)

// Localizable.strings (English)
"ok" = "OK";
"cancel" = "Cancel";

// Localizable.strings (Chinese - Simplified)
"ok" = "确定";
"cancel" = "取消";

// 代码
let title = NSLocalizedString("ok", comment: "OK button")
  • 特点:基于 Bundle 的字符串表;由系统自动按用户首选语言解析。

2.3 Flutter 对比(跨平台一致)

维度 Android 原生 iOS 原生 Flutter
资源组织 res/values-xx Localizable.strings .arb + 代码生成(S 类)
切换机制 配置变更/Activity 重建 系统语言变更触发 Localizations 局部重建
一致性 平台差异明显 平台差异明显 跨平台一致
自定义能力 依赖系统 依赖系统 完全代码可控

3. 实现步骤与代码示例

用 Flutter Intl 实现国际化:https://juejin.cn/post/7410645914585546779

3.1 安装 Flutter Intl 插件

在 IDE 中安装 Flutter Intl 插件,以 Android studio 为例:

Android Studio --->Settings --->Plugins ---> Marketplace ---> 搜索框输入Intl ---> Install

3.2 初始化 Flutter Intl

Tools → Flutter Intl → Initialize for the project;
  • 生成目录:
    • lib/l10n/:存放 .arb 语言资源文件(配置文件);
    • lib/generated/:存放生成的 l10n.dartmessages_*.dart(自动生成)。

3.3 添加国际化语言

Tools ---> Flutter Intl ---> Add Locale,新增如 `en`、`zh_CN` 等国际化语言
  • 在相应 .arb 中新增键值:
{
  "drawer_item_theme": "暗黑模式",
  "drawer_item_language": "语言设置",
  "device_list_count": "{count}个设备"
}

占位符在 .arb 中需声明参数类型:

{
  "device_list_count": "{count}个设备",
  "@device_list_count": {
    "placeholders": { "count": { "type": "int" } }
  },
  // 复合参数
  "storage_usage": "已用{usedGB}GB / 共{totalGB}GB"
}

3.4. 在项目中配置

MaterialApp 中接入:

// lib/main.dart(节选)
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'generated/l10n.dart';

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 如果使用 Riverpod 管理语言,直接 watch 对应的 Locale(详见下一节)
    final locale = ref.watch(languageProvider); // Locale?,null 表示跟随系统

    return MaterialApp(
      localizationsDelegates: const [
        S.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: S.delegate.supportedLocales,
      locale: locale,
      // ... 其他配置
    );
  }
}

3.5 在代码中使用

import 'generated/l10n.dart';

// 推荐:会随着语言变化而自动更新
Text(S.of(context).drawer_item_language);

// 特殊场景(不随 context 重建,如后台任务):
final text = S.current.ok; // 注意:不会触发 UI 自动刷新

3.6 注意事项

命名规范(建议)

  • 采用 “模块--组件--语义” 三级结构:如 home_titledevice_manage_button_add
  • 常用键:okcancelconfirm 等通用词直接命名;
  • 参数化:device_list_countdevice_status_{status}
  • 复用性优先:尽量抽出通用短语,避免页面强绑定;
  • 保持多语言键一致,避免遗漏。

数字/日期/复数与参数
借助 intl 格式化与 plural/gender 支持:

// .arb
{
  "files_count": "{count, plural, =0{没有文件} =1{1个文件} other{{count}个文件}}",
  "@files_count": { "placeholders": { "count": { "type": "int" } } }
}

// Dart
Text(S.of(context).files_count(count));

4. 使用 Riverpod 实现语言切换

4.1 实现持久化存储

如使用 SharedPreferences 保存用户的语言选择:

// lib/common/utils/share_preferences_utils.dart
class SharePreferenceUtils {
  /// 保存语言
  static Future<bool> saveLanguage(String languageCode) async {
    return await saveString(SharePreferenceKey.language, languageCode);
  }

  /// 获取语言
  static Future<String> getLanguage() async {
    return await getString(SharePreferenceKey.language, "");
  }
}

class SharePreferenceKey {
  static const String language = "APP_LANGUAGE";  // 语言
}

// 语言常量
class LanguageConstant {
  static const Locale en = Locale('en', 'US');
  static const Locale zh = Locale('zh', 'CN');
}

4.2 创建主题状态管理器

使用 Riverpod 的 StateNotifier 创建主题状态管理器:

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../generated/l10n.dart';
import '../channel/platform_service.dart';
import '../constant.dart';
import '../utils/share_preferences_utils.dart';

class LanguageNotifier extends StateNotifier<Locale?> {
  LanguageNotifier() : super(null) {
    _initLanguage();
  }

  /// 初始化语言设置
  Future<void> _initLanguage() async {
    final savedLanguage = await SharePreferenceUtils.getLanguage();
    if (savedLanguage.isNotEmpty) {
      state = _localeFromString(savedLanguage);
    } else {
      // 默认语言改为英文
      state = LanguageConstant.en;
    }
  }

  /// 获取系统语言
  Future<Locale> _getSystemLocale() async {
    final systemLocale = WidgetsBinding.instance.platformDispatcher.locale;
    return systemLocale;
  }

  /// 转换字符串为Locale
  Locale _localeFromString(String localeStr) {
    final parts = localeStr.split('_');
    return Locale(parts[0], parts.length > 1 ? parts[1] : '');
  }

  /// 获取支持的语言
  Locale _getSupportedLocale(Locale systemLocale) {
    final supportedLocales = S.delegate.supportedLocales;
    final isSupported = supportedLocales.any((loc) =>
        loc.languageCode == systemLocale.languageCode &&
        (loc.countryCode == null ||
            loc.countryCode == systemLocale.countryCode));

    return isSupported ? systemLocale : LanguageConstant.en; // 默认英语
  }

  /// 切换应用语言
  Future<void> setLanguage(Locale locale) async {
    state = locale;
    await SharePreferenceUtils.saveLanguage(
        '${locale.languageCode}_${locale.countryCode}');

    // 同步语言设置到小组件
    await PlatformService.syncLanguage(locale.languageCode);
  }
}

/// 语言状态提供者
final languageProvider = StateNotifierProvider<LanguageNotifier, Locale?>(
  (ref) => LanguageNotifier(),
);

/// 系统语言提供者
final systemLanguageProvider = Provider<Locale>((ref) {
  return WidgetsBinding.instance.platformDispatcher.locale;
});

/// 判断当前是否为中文的提供者
final isChineseProvider = Provider<bool>((ref) {
  final currentLocale = ref.watch(languageProvider);
  return currentLocale?.languageCode == LanguageConstant.zh.languageCode;
});

/// 字体提供者
final fontFamilyProvider = Provider<String>((ref) {
  final currentLocale = ref.watch(languageProvider);
  return currentLocale == LanguageConstant.zh
      ? FontConstant.notoSansSC
      : FontConstant.inter;
});

4.3 在 MaterialApp 中应用语言

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'language/language_provider.dart';

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final language = ref.watch(languageProvider);
    
    return MaterialApp(
      title: 'Flutter语言切换示例',
      locale: language,
      localizationsDelegates: [
        S.delegate,
        GlobalMaterialLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: S.delegate.supportedLocales,
      home: MyHomePage(),
    );
  }
}

使用示例:

// 切换到中文(简体)
ref.read(languageProvider.notifier).setLanguage(const Locale('zh', 'CN'));

// 切换到英文
ref.read(languageProvider.notifier).setLanguage(const Locale('en'));

4.4 为什么要用 Riverpod 管理语言?

  • 类型安全与单一数据源:Locale? 作为应用“语言真相”的唯一输出,避免多处各自维护;
  • 精准重建:依赖语言的组件通过 ref.watch(languageProvider)S.of(context) 获得更新,尽量减少无关重建;
  • 易于持久化与跨端同步:与 SharePreferenceUtilsPlatformService.syncLanguage 无缝对接;
  • 可测试:StateNotifier 便于注入与单元测试;
  • 与主题/字体联动:项目中 fontFamilyProvider 已根据语言自动切换字体,提升整体一致性。

5. 其他事项

  1. S.of(context) 优先于 S.current

    • S.of(context):对 Localizations 建立依赖,locale 变化时会被精准通知并重建;
    • S.current:读取的是全局当前实例,不会“自己触发”重建。若没有其他状态变化导致该 Widget 重建,使用 S.current 的文本不会自动更新。
  2. locale 交由顶层 MaterialApp 管控

    • 避免在子树多处“覆盖”Localizations.override,防止维护复杂度与重建范围不可控。
  3. 避免在频繁重建的 Widget 中做昂贵的格式化

    • 大量 DateFormat/NumberFormat 可复用或上移到较少重建的层级;或做轻量缓存。
  1. FAQ:为什么上面代码里用 S.current 也能更新?
  • 根因:S.current 指向的是由 S.delegate.load(locale) 更新的全局实例。每当 MaterialApp.locale 变化,Localizations 会触发重新加载,S._current 也会被更新。
  • 现象:语言切换通过 Riverpod 改变了顶层 locale,导致应用根部重建,绝大多数子树也会随之重建。于是即便使用 S.current,因为 Widget 本身经历了重建,文本也跟着更新。
  • 关键区别:S.current 本身不具备“依赖跟踪”能力,它不会促使某个未重建的 Widget 自动刷新。若某组件被“缓存”且未参与这次重建,那么其中基于旧 S.current 的字符串不会变化。为了获得确定性的 UI 更新,应优先使用 S.of(context)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容