Flutter 应用里的国际化

你将学习到

  • 如何去获取设备的语言环境(用户首选的语言)。

  • 如何去管理特定语言环境下的 app 值。

  • 如何去定义 app 支持的语言环境。

如果你的 app 会部署给说其他语言的用户使用,那么你就需要对它进行国际化。这就意味着你在编写 app的时候,需要采用一种容易对它进行本地化的方式进行开发,这种方式让你能够为每一种语言或者 app 所支持的语言环境下的文本和布局等进行本地化。Flutter 提供了 widgets 和类来帮助开发者进行国际化,当然 Flutter 库本身就是国际化的。

和大多数应用一样,下面的教程主要都是使用 Flutter MaterialApp 类编写。那些使用更底层的 WidgetsApp 类编写的应用也能通过使用相同的类和逻辑来进行国际化。

国际化的 app 示例

如果你想通过阅读已经国际化的 Flutter app 代码来开始的话,这里有两个小例子。第一个例子是一个尽可能简单的实现。第二个例子使用了 intl package 提供的 API 和工具。如果你还不熟悉 Dart 的 intl 包,请查看 使用 Dart intl 工具

配置一个国际化的 app:flutter_localizations package

默认情况下,Flutter 只提供美式英语的本地化。如果想要添加其他语言,你的应用必须指定额外的 MaterialApp属性并且添加一个单独的 package,叫做 flutter_localizations。截至到 2019 年 4 月份,这个 package 已经支持大约 52 种语言。如果你希望在 iOS 上顺利运行,你需要额外加入 flutter_cupertino_localizations 这个 package。

想要使用 flutter_localizations 的话,你需要在 pubspec.yaml 文件中添加它作为依赖:

  1. dependencies:
  2. flutter:
  3. sdk: flutter
  4. flutter_localizations:
  5. sdk: flutter
  6. flutter_cupertino_localizations: ^1.0.1

下一步,引入 flutter_localizations 库,然后为 MaterialApp 指定 localizationsDelegatessupportedLocales

  1. import 'package:flutter_localizations/flutter_localizations.dart';
  2. import 'package:flutter_cupertino_localizations/flutter_cupertino_localizations.dart';
  3.  
  4. MaterialApp(
  5. localizationsDelegates: [
  6. // ... app-specific localization delegate[s] here
  7. GlobalMaterialLocalizations.delegate,
  8. GlobalWidgetsLocalizations.delegate,
  9. GlobalCupertinoLocalizations.delegate,
  10. ],
  11. supportedLocales: [
  12. const Locale('en'), // English
  13. const Locale('he'), // Hebrew
  14. const Locale.fromSubtags(languageCode: 'zh'), // Chinese *See Advanced Locales below*
  15. // ... other locales the app supports
  16. ],
  17. // ...
  18. )

基于 WidgetsApp 构建的 app 在添加语言环境时,除了 GlobalMaterialLocalizations.delegate 不需要之外,其他的操作是类似的。

虽然 语言环境 默认的构造函数是完全没有问题的,但是还是建议大家使用 Locale.fromSubtags 的构造函数,因为它支持设置文字代码。

localizationDelegates 数组是用于生成本地化值集合的工厂。GlobalMaterialLocalizations.delegate 为 Material 组件库提供本地化的字符串和一些其他的值。GlobalWidgetsLocalizations.delegate 为 widgets 库定义了默认的文本排列方向,由左到右或者由右到左。

想知道更多关于这些 app 属性,它们依赖的类型以及那些国际化的 Flutter app 通常是如何组织的,可以继续阅读下面内容。

高级语言环境定义

一些具有着多个变种的语言仅仅用语言代码是不能合适地区分的。

例如,要能完全区分具有多个变种的中文需要指定语言代码、文字代码和国家代码。这是因为存在着简体和繁体的文字系统,而且同时使用相同文字系统写的字符又有地域性的差别。

为了让 CNTWHK 三个不同的国家/地区代码能够完整地表达每个变种的中文,你应该包括以下支持的语言环境:

  1. // Full Chinese support for CN, TW, and HK
  2. supportedLocales: [
  3. const Locale.fromSubtags(languageCode: 'zh'), // generic Chinese 'zh'
  4. const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'), // generic simplified Chinese 'zh_Hans'
  5. const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), // generic traditional Chinese 'zh_Hant'
  6. const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'), // 'zh_Hans_CN'
  7. const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'TW'), // 'zh_Hant_TW'
  8. const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'HK'), // 'zh_Hant_HK'
  9. ],

这种明确完整的定义可以确保你的 app 能够区分以及提供完全地道的本地内容给这些国家/地区代码的所有组合的用户。如果用户没有指定首选的语言环境,那么我们就会使用最近的匹配,这很可能与用户的期望会有差异。Flutter 只会解析定义在 supportedLocales里面的语言环境。对于那些常用语言,Flutter 为本地化内容提供了文字代码级别的区分。查看 Localizations了解 Flutter 是如何解析支持的语言环境和首选的语言环境的。

虽然中文是最主要的一个示例,但是其他语言如法语(FR_fr,FR_ca 等等)也应该为了更细致的本地化而做完全的区分。

获取语言环境:Locale 类和 Localizations Widget

Locale 类用来识别用户的语言。移动设备支持为所有的应用设置语言环境,经常是通过系统设置菜单来进行操作。设置完之后,国际化的 app 就会展示成对应特定语言环境的值。例如,如果用户把设备的语言环境从英语切换到法语,显示 “Hello World” 的文本 widget 会使用 “Bonjour le monde” 进行重建。

Localizations widget 定义了它的子节点的语言环境和依赖的本地化的资源。WidgetsApp 创建了一个本地化的 widget,如果系统的语言环境变化了,它会重建这个 widget。

你可以通过调用 Localizations.localeOf() 方法来查看 app 当前的语言环境。

  1. Locale myLocale = Localizations.localeOf(context);

加载和获取本地化值

我们使用 Localizations widget 来加载和查询那些包含本地化值集合的对象。app 通过调用 Localizations.of(context,type)来引用这些对象。如果设备的语言环境变化了,Localizations widget 会自动地加载新的语言环境的值,然后重建那些使用了语言环境的 widget。这是因为 Localizations 像 继承 widget 一样执行。当一个构建过程涉及到继承 widget,对继承 widget 的隐式依赖就创建了。当一个继承 widget 变化了(即 Localizations widget 的语言环境变化),它的依赖上下文就会被重建。

本地化的值是通过使用 Localizations widget 的 LocalizationsDelegate 加载的。每一个 delegate 必须定义一个异步的 load() 方法。这个方法生成了一个封装本地化值的对象。通常这些对象为每个本地化的值定义了一个方法。

在一个大型的 app 中,不同的模块或者 package 需要和它们对应的本地化资源打包在一起。这就是为什么 Localizations widget 管理着对象的一个对应表,每个 LocationsDelegate 对应一个对象。为了获得由 LocationsDelegate 的 load 方法生成的对象,你需要指定一个构建上下文和对象的类型。

例如,Material 组件 widget 的本地化字符串是由 MaterialLocalizations类定义的。这个类的实例是由 MaterialApp 类提供的一个 LocalizationDelegate 方法创建的。它们可以通过 Localizations.of 方法获得。

  1. Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);

因为这个特定的 Localizations.of() 表达式经常使用,所以 MaterialLocalizations 类提供了一个快捷访问:

  1. static MaterialLocalizations of(BuildContext context) {
  2. return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
  3. }
  4.  
  5. /// References to the localized values defined by MaterialLocalizations
  6. /// are typically written like this:
  7.  
  8. tooltip: MaterialLocalizations.of(context).backButtonTooltip,

使用内置的 LocalizationsDelegates

Flutter package 包括的 MaterialLocalizations 和 WidgetsLocalizations 的接口都只提供美式英语的值,这样使得它尽可能小而简单。这些实现的类被分别称为 DefaultMaterialLocalizations 和 DefaultWidgetsLocalizations。它们会被自动地引入程序,除非你在 localizationsDelegates 参数中,相同的基本类型指定了一个不同的 delegate。

flutter_localizations package 包括了多种语言本地化接口的实现,它们称为 GlobalMaterialLocalizationsGlobalWidgetsLocalizations。国际化 app 必须为这些类的指定本地化 delegate,就如在 配置一个国际化的 app 中描述的那样。

  1. import 'package:flutter_localizations/flutter_localizations.dart';
  2.  
  3. MaterialApp(
  4. localizationsDelegates: [
  5. // ... app-specific localization delegate[s] here
  6. GlobalMaterialLocalizations.delegate,
  7. GlobalWidgetsLocalizations.delegate,
  8. ],
  9. supportedLocales: [
  10. const Locale('en'), // English
  11. const Locale('he'), // Hebrew
  12. const Locale('zh'), // Chinese
  13. // ... other locales the app supports
  14. ],
  15. // ...
  16. )

全球本地化 delegate 构建了对应类在特定语言环境下的实例。例如,GlobalMaterialLocalizations.delegate 就是一个本地化 delegate,它用来产生一个 GlobalMaterialLocalizations 的实例。

截至 2019 年 4 月,这个全球本地化类一共支持 大约 52 种语言

为 app 的本地化资源定义一个类

综合所有这些在一起,一个需要国际化的 app 经常以一个封装 app 本地化值的类开始的。下面是使用这种类的典型示例。

此示例 app 的 完整的源码

这个示例是基于 intl package 提供的 API 和工具开发的。app 本地化资源的替代方法 里面讲解了一个不依赖于 intl package 的 示例

DemoLocalizations 类包含了 app 语言环境内支持的已经翻译成了本地化语言的字符串(本例子只有一个)。它通过调用由 Dart 的 intl package 生成的initializeMessage() 方法来加载翻译好的字符串,然后使用 Intl.message()来查阅它们。

  1. class DemoLocalizations {
  2. DemoLocalizations(this.localeName);
  3.  
  4. static Future<DemoLocalizations> load(Locale locale) {
  5. final String name = locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
  6. final String localeName = Intl.canonicalizedLocale(name);
  7. return initializeMessages(localeName).then((_) {
  8. return DemoLocalizations(localeName);
  9. });
  10. }
  11.  
  12. static DemoLocalizations of(BuildContext context) {
  13. return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
  14. }
  15.  
  16. final String localeName;
  17.  
  18. String get title {
  19. return Intl.message(
  20. 'Hello World',
  21. name: 'title',
  22. desc: 'Title for the Demo application',
  23. locale: localeName,
  24. );
  25. }
  26. }

基于 intl package 的类引入了一个生成好的信息目录,它提供了 initializeMessage() 方法和 Intl.message() 方法的每个语言环境的备份存储。intl 工具 通过分析包含 Intl.message() 调用类的源码生成这个信息目录。在当前情况下,就是 DemoLocalizations 的类(包含了 Intl.message() 调用)。

具体说明 app 支持的语言环境参数

虽然 Flutter 的 flutter_localizations 库能够支持大约 52 种语言,但是默认只支持英语翻译。这是因为应该由开发者决定到底要支持哪一种语言,让工具库默认去支持和 app 不一样的语言环境是完全没有意义的。

MaterialApp 的 supportedLocales 参数限制了语言环境的变化范围。当用户在他们的设备切换语言环境的时候,只有当新语言环境是 supportedLocales 列表项中之一时, app 的 Localizations widget 才会跟着一起变。如果这个设备的语言环境不能被精确匹配,languageCode 相同的第一个支持的语言环境会被使用。如果这个也失败了,那就会使用 supportedLocales 的第一个语言环境。

以上面那个 DemoApp 例子来说,这个 app 仅接受美式英语或者加拿大法语的语言环境。对于其他任何语言环境都是使用美式英语作为替代(因为它是列表当中的第一个)。

如果一个 app 想要使用不同的语言环境解析方案,它可以提供一个 localeResolutionCallback.。例如,让你的 app 无条件的接受用户选择的任何语言环境:

  1. class DemoApp extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return MaterialApp(
  5. localeResolutionCallback(Locale locale, Iterable<Locale> supportedLocales) {
  6. return locale;
  7. }
  8. // ...
  9. );
  10. }
  11. }

app 本地化资源的替代方法

之前的那个 DemoApp 示例是使用 Dart intl package 进行开发的。为了更简便,或者和其他不同的 i18n 框架集成,开发者可以选择他们自己的方法来管理本地化的值。

这个示例 APP 的 完整代码

在这个版本的 DemoApp 中,这个类包含了 app 的 localizations 和 DemoLocalizations,并且直接将它所有的翻译放在每个语言的映射当中。

  1. class DemoLocalizations {
  2. DemoLocalizations(this.locale);
  3.  
  4. final Locale locale;
  5.  
  6. static DemoLocalizations of(BuildContext context) {
  7. return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
  8. }
  9.  
  10. static Map<String, Map<String, String>> _localizedValues = {
  11. 'en': {
  12. 'title': 'Hello World',
  13. },
  14. 'es': {
  15. 'title': 'Hola Mundo',
  16. },
  17. };
  18.  
  19. String get title {
  20. return _localizedValues[locale.languageCode]['title'];
  21. }
  22. }

在这个最小实现的 app 当中,DemoLocalizationDelegate 有一点不一样。它的 load 方法返回了一个SynchronousFuture,因为不需要进行异步的加载。

  1. class DemoLocalizationsDelegate extends LocalizationsDelegate<DemoLocalizations> {
  2. const DemoLocalizationsDelegate();
  3.  
  4. @override
  5. bool isSupported(Locale locale) => ['en', 'es'].contains(locale.languageCode);
  6.  
  7. @override
  8. Future<DemoLocalizations> load(Locale locale) {
  9. return SynchronousFuture<DemoLocalizations>(DemoLocalizations(locale));
  10. }
  11.  
  12. @override
  13. bool shouldReload(DemoLocalizationsDelegate old) => false;
  14. }

添加支持新的语言

如果你要开发一个 app 需要支持的语言不在 GlobalMaterialLocalizations 当中,那就需要做一些额外的工作:它必须提供大概 70 个字和词的翻译(本地化)。

举个例子,我们将给大家展示如何支持白俄罗斯语。

我们需要定义一个新的 GlobalMaterialLocalizations 子类,它定义了 Material 库依赖的 localizations。同时,我们也必须定义一个新的 LocalizationsDelegate 子类,它是给 GlobalMaterialLocalizations 子类作为一个工厂使用的。

这是支持添加一种新语言的一个完整例子的源码,相对实际上要翻译的白俄罗斯语数量,我们只翻译了部分。

这个特定语言环境的 GlobalMaterialLocalizations 子类被称为 BeMaterialLocalizations,LocalizationsDelegate 子类被称为_BeMaterialLocalizationsDelegateBeMaterialLocalizations.delegate 是 delegate 的一个实例,这就是 app 使用这些本地化所需要的全部。

delegate 类包括基本的日期和数字格式的本地化。其他所有的本地化是由 BeMaterialLocalizations 里面的字符串值属性的 getters 所定义的,像下面这样:

  1. @overrideString get backButtonTooltip => r'Back';

  2. @overrideString get cancelButtonLabel => r'CANCEL';

  3. @overrideString get closeButtonLabel => r'CLOSE';

  4. // etc..

当然,这些都是英语翻译。为了完成本地化操作,你需要把每一个 getter 的返回值翻译成合适的白俄罗斯语字符。

r'About $applicationName' 一样,这些带 r 前缀的 getters 返回的是原始的字符串,因为有一些时候这些字符串会包含一些带有 $ 前缀的变量。通过调用带参数的本地化方法,这些变量会被替换:

  1. @overrideString get aboutListTileTitleRaw => r'About $applicationName';

  2. @overrideString aboutListTileTitle(String applicationName) { final String text = aboutListTileTitleRaw; return text.replaceFirst(r'$applicationName', applicationName);}

需要了解更多关于本地化字符串的内容,可以查看 flutter_localizations README

一旦你实现了指定语言的 GlobalMaterialLocalizations 和 LocalizationsDelegate 的子类,你只需要给你的 app 添加此语言以及一个 delegate 的实例。这里有一些代码展示了如何设置 app 的语言为白俄罗斯语以及如何给 app 的 localizationsDelegates 列表添加 BeMaterialLocalizations delegate 实例。

  1. MaterialApp(
  2. localizationsDelegates: [
  3. GlobalWidgetsLocalizations.delegate,
  4. GlobalMaterialLocalizations.delegate,
  5. BeMaterialLocalizations.delegate,
  6. ],
  7. supportedLocales: [
  8. const Locale('be', 'BY')
  9. ],
  10. home: ...
  11. )

附录:使用 Dart intl 工具

在你使用 Dart intl package 进行构建 API 之前,你应该想要了解一下 intl package 的文档。

这个 demo app 依赖于一个生成的源文件,叫做 l10n/messages_all.dart,这个文件定义了 app 使用的所有本地化的字符串。

重建 l10n/messages_all.dart 需要 2 步。

  • 在 app 的根目录,使用 lib/main.dart 生成 l10n/intl_messages.arb
  1. $ flutter pub run intl_translation:extract_to_arb --output-dir=lib/l10n lib/main.dart

intl_messages.arb 是一个 JSON 格式的文件,每一个入口代表定义在 main.dart 里面的 Intl.message() 方法。intl_en.arbintl_es.arb 分别作为英语和西班牙语翻译的模板。这些翻译是由你(开发者)来创建的。

  • 在 app 的根目录,生成每个 intl<locale>.arb 文件对应的 intl_messages<locale>.dart 文件,以及 intl_messages_all.dart 文件,它引入了所有的信息文件。
  1. $ flutter pub run intl_translation:generate_from_arb \
  2. --output-dir=lib/l10n --no-use-deferred-loading \
  3. lib/main.dart lib/l10n/intl_*.arb

DemoLocalizations 类使用生成的 initializeMessages() 方法(该方法定义在 intl_messages_all.dart 文件)来加载本地化的信息,然后使用 Intl.message() 来查阅这些本地化的信息。

附录:更新 iOS app 包

iOS 应用在 Info.plist 文件当中定义了很多关键应用元数据,其中就包括支持的语言环境,而这个文件是会被打包进应用包里面的。为了配置 app 支持的语言环境,你需要编辑这个文件。

首先,打开你项目的 Xcode 工作区文件 ios/Runner.xcworkspace,在项目导航栏中,打开运行项目的对应运行文件夹下的 Info.plist 文件。

下一步,选择 Information Property List 项,从 Editor 菜单中选择 Add Item,然后从弹出菜单中选择 Localizations

选择和展开新创建的 Localizations 项,对于应用需要支持的每个语言环境,你需要添加一个新的项。然后点击 Value 域,从弹出菜单当中选择你想要的语言环境。这个列表应该和 supportedLocales 参数当中的语言列表保持一致。

添加完所有支持的语言环境后,保存这个文件。