flutter 开发笔记(八):本地化

490 阅读5分钟

本地化是 app 常见的需求,本文介绍如何在 flutter 中添加本地化支持,并提供详细的步骤说明以及可供复现的 demo

安装

项目做本地化通常是 intl 和 flutter_localizations 搭配使用。下面是项目的 pubspec.yaml 文件,除了安装前面提到的两个包外,还需要设置 generate: true 以供使用

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations: 
    sdk: flutter 
  intl: ^0.19.0

flutter:
  uses-material-design: true
  generate: true

有一点需要注意的是,flutter_localizations 对使用的 intl 有版本限制,比如目前限定为 0.19.0。如果版本不匹配,你会遇到类似的错误:

Because test_drive depends on flutter_localizations from sdk which depends on intl 0.19.0, intl 0.19.0 is
  required.
So, because test_drive depends on intl ^0.20.0, version solving failed

接着我们创建语言文件,计划支持两种语言,英文和简体中文

app_en.arb

{
  "@@locale": "en",
  "title": "Flutter Demo",
  "message": "Hello World",
  "greeting": "Hello {userName}",
  "@greeting": {
    "placeholders": {
      "userName": {
        "type": "String"
      }
    }
  }
}

app_zh.arb

{
  "@@locale": "zh",
  "title": "Flutter 示例",
  "message": "你好,世界",
  "greeting": "你好,{userName}",
  "@greeting": {
    "placeholders": {
      "userName": {
        "type": "String"
      }
    }
  }
}

这两个文件放在 lib 下的 l10n 目录,树形结构如下

lib
├── l10n
│   ├── app_en.arb
│   └── app_zh.arb

另外,需要注意的是,l10n 的首字母是 L 转小写(有些字体很难看出与 i 转大写后的 I 的区别),很容易与 i18n 混淆,实际上它俩是同样的命名规则,包含首尾以及中间字母的数量,l10n 的全程是 localization

@@locale 是一个特殊的 key,它用于指定 ARB(Application Resource Bundle) 文件中的语言环境,告诉本地化生成工具当前文件代表哪个语言;greeting@greeting 组合在一起,提供了插值写法的示例,本地化生成工具会生成一个 greeting 方法,传入 userName 字符串来生成完整的字符串

接着就是使用 gen-l10n 命令了,它的作用是 Flutter 提供的一个工具,用于简化和自动化本地化(Localization)的过程

通过 command 传参比较麻烦,我们可以建一个 l10n.yaml 放在项目的根目录下

arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
output-class: AppLocalizations

接着使用命令,会遇到这样的警告,其实是可以忽略的

Because l10n.yaml exists, the options defined there will be used instead.
To use the command line arguments, delete the l10n.yaml file in the Flutter project.

一般情况下,你不需要手动执行 flutter gen-l10n 命令,当你启动项目(flutter run)或构建项目(flutter build apkflutter build ios)都会自动执行 gen-l10n 命令

然后你会在项目根目录下的 .dart_tool 看到有文件生成,自此,前期安装的准备工作完成

.dart_tool
├── flutter_gen
│   ├── gen_l10n
│   │   ├── app_localizations.dart
│   │   ├── app_localizations_en.dart
│   │   └── app_localizations_zh.dart
│   └── pubspec.yaml

使用(简单 demo)

import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

void main() {
  runApp(const MyApp());
}

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

  static void setLocale(BuildContext context, Locale newLocale) {
    final MyAppState state = context.findAncestorStateOfType<MyAppState>()!;
    state.setLocale(newLocale);
  }

  @override
  MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  late Locale _locale;

  void setLocale(Locale locale) {
    setState(() {
      _locale = locale;
    });
  }

  @override
  void initState() {
    super.initState();
    _locale = const Locale('en'); // Set default locale
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      locale: _locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(AppLocalizations.of(context)?.title ?? ''),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(AppLocalizations.of(context)?.message ?? ''),
            Text(AppLocalizations.of(context)?.greeting(
                    Localizations.localeOf(context).languageCode == 'en'
                        ? 'User'
                        : '用户') ??
                ''),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // Toggle between English and Chinese
                Locale newLocale =
                    Localizations.localeOf(context).languageCode == 'en'
                        ? const Locale('zh')
                        : const Locale('en');
                MyApp.setLocale(context, newLocale);
              },
              child: const Text('切换语言'),
            ),
          ],
        ),
      ),
    );
  }
}

上面给出了一个简单的 demo,演示如何使用文本和切换语言,需要在 MaterialApp 里面配置,使用 key 在 AppLocalizations.of(context) 里面拿数据,通过更改 MaterialApplocale 参数切换语言

优化

实际上,app 语言逻辑不会像上面演示的那么简单,通常会先判断系统的默认语言,如果系统语言是中文,我们肯定就不能默认英文了,然后如果用户在系统语言设置了中文的情况下,仍然设置 app 内的语言为英文,那我们肯定得设置成英文了,所以优先级应该是 用户设置 > 系统默认 > App 默认;因此,我们需要通过持久化存储把用户的设置给存储起来;我们可以建立一个 manager 来管理本地化需求

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';

class LocaleManager with ChangeNotifier {
  Locale _locale = const Locale('en');
  static const String _languageKey = 'language';

  Locale get locale => _locale;

  LocaleManager() {
    _loadLocale();
  }

  void setLocale(Locale locale) async {
    if (!AppLocalizations.supportedLocales.contains(locale)) return;

    _locale = locale;
    notifyListeners();

    SharedPreferences prefs = await SharedPreferences.getInstance();
    prefs.setString(_languageKey, locale.languageCode);
  }

  void _loadLocale() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    String? languageCode = prefs.getString(_languageKey);

    if (languageCode != null) {
      // 用户设置
      _locale = Locale(languageCode);
    } else {
      final systemLanguageCode =
          WidgetsBinding.instance.platformDispatcher.locale.languageCode;
      final List<String> supportLanguageCodes = AppLocalizations
          .supportedLocales
          .map((locale) => locale.languageCode)
          .toList();
      if (supportLanguageCodes.contains(systemLanguageCode)) {
        // 系统默认
        _locale = Locale(systemLanguageCode);
      } else {
        // App 默认
        _locale = const Locale('en');
      }
    }

    notifyListeners();
  }
}

上述代码在 _loadLocale 中执行了 用户设置 > 系统默认 > App 默认 的优先级处理,为什么使用 languageCode 呢?如果直接使用 Locale,会发现带有一些无关信息,比如 iOS 设置中文返回的是 zh_Hans_CN,很难兼顾处理。而使用 languageCode 就可以拿出 zh,方便我们匹配处理

然后就可以使用了,应用示例如下

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:test_drive/utils/locale_manager.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => LocaleManager(),
      child: const MyApp(),
    ),
  );
}

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

  @override
  MyAppState createState() => MyAppState();
}

class MyAppState extends State<MyApp> {
  @override
  Widget build(BuildContext context) {
    final localeProvider = Provider.of<LocaleManager>(context);

    return MaterialApp(
      locale: localeProvider.locale,
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(AppLocalizations.of(context)?.title ?? ''),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(AppLocalizations.of(context)?.message ?? ''),
            Text(AppLocalizations.of(context)?.greeting(
                    Localizations.localeOf(context).languageCode == 'en'
                        ? 'User'
                        : '用户') ??
                ''),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // Toggle between English and Chinese
                Locale newLocale =
                    Localizations.localeOf(context).languageCode == 'en'
                        ? const Locale('zh')
                        : const Locale('en');
                final provider =
                    Provider.of<LocaleManager>(context, listen: false);
                provider.setLocale(newLocale);
              },
              child: const Text('切换语言'),
            ),
          ],
        ),
      ),
    );
  }
}