Flutter 应用里的国际化

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

由于大多数应用程序都是以这种方式编写的,因此该页面主要介绍了使用 MaterialAppCupertinoApp 对 Flutter 应用程序进行本地化所需的概念和工作流程。但是,使用较低级别的 WidgetsApp 类编写的应用程序也可以使用相同的类和逻辑进行国际化。

Flutter 应用本地化介绍

#

本节主要介绍如何对 Flutter 应用进行国际化,以及针对目标平台需要设置的其他内容。

你可以在 gen_l10n_example 仓库找到源代码。

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

#

默认情况下,Flutter 只提供美式英语的本地化。如果想要添加其他语言,你的应用必须指定额外的 MaterialApp 或者 CupertinoApp 属性并且添加一个名为 flutter_localizations 的 package。截至到 2023 年 12 月份,这个 package 已经支持 115 种语言

若要开始使用,在 Flutter 工程文件夹下执行 flutter create 命令:

flutter create <name_of_flutter_app>

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

flutter pub add flutter_localizations --sdk=flutter
flutter pub add intl:any

最终的 pubspec.yaml 文件中形如:

yaml
dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: any

下一步,先运行 pub get packages,然后引入 flutter_localizations 库,然后为 MaterialApp 指定 localizationsDelegatessupportedLocales

dart
import 'package:flutter_localizations/flutter_localizations.dart';
dart
return const MaterialApp(
  title: 'Localizations Sample App',
  localizationsDelegates: [
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
  ],
  supportedLocales: [
    Locale('en'), // English
    Locale('es'), // Spanish
  ],
  home: MyHomePage(),
);

引入 flutter_localizations package 并添加了上面的代码之后, MaterialCupertino 包现在应该被正确地本地化为 115 个受支持的语言环境之一。 widget 应当与本地化信息保持同步,并具有正确的从左到右或从右到左的布局。

你可以尝试将目标平台的语言环境切换为西班牙语 (es),然后应该可以发现信息已经被本地化了。

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

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

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

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

重载语言

#

Localizations.override 提供了一个工厂构造方法,使得你可以从在某一个位置设置与应用不同的语言(非一般情况)。

下面的示例展示了 Localizations.overrideCalendarDatePicker 组合使用的情况:

dart
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text(widget.title),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          // Add the following code
          Localizations.override(
            context: context,
            locale: const Locale('es'),
            // Using a Builder to get the correct BuildContext.
            // Alternatively, you can create a new widget and Localizations.override
            // will pass the updated BuildContext to the new widget.
            child: Builder(
              builder: (context) {
                // A toy example for an internationalized Material widget.
                return CalendarDatePicker(
                  initialDate: DateTime.now(),
                  firstDate: DateTime(1900),
                  lastDate: DateTime(2100),
                  onDateChanged: (value) {},
                );
              },
            ),
          ),
        ],
      ),
    ),
  );
}

应用热重载后,你将能够发现 CalendarDatePicker widget 显示为西班牙语了。

添加你自己的本地化信息

#

引入 flutter_localizations package 后,请按照以下说明将本地化的文本添加到你的应用。

  1. intl package 添加为依赖,使用 any 作为 flutter_localizations 的版本值:

    flutter pub add intl:any
  2. 另外,在 pubspec.yaml 文件中,启用 generate 标志。该设置项添加在 pubspec 中 Flutter 部分,通常处在 pubspec 文件中后面的部分。

    yaml
    # The following section is specific to Flutter.
    flutter:
      generate: true # Add this line
  3. 在 Flutter 项目的根目录中添加一个新的 yaml 文件,命名为 l10n.yaml,其内容如下:

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

    该文件用于配置本地化工具。它为你的项目配置了如下内容:

    • 应用资源包 (.arb) 的输入路径指定为 ${FLUTTER_PROJECT}/lib/l10n

      .arb 文件提供了应用的本地化资源。

    • 将英文的语言模板设定为 app_en.arb

    • 指定 Flutter 生成本地化内容到 app_localizations.dart 文件。

  4. ${FLUTTER_PROJECT}/lib/l10n 中,添加 app_en.arb 模板文件。如下:

    json
    {
      "helloWorld": "Hello World!",
      "@helloWorld": {
        "description": "The conventional newborn programmer greeting"
      }
    }
  5. 接下来,在同一目录中添加一个 app_es.arb 文件,对同一条信息做西班牙语的翻译:

    json
    {
        "helloWorld": "¡Hola Mundo!"
    }
  6. 现在,运行 flutter run 命令,你将在 ${FLUTTER_PROJECT}/.dart_tool/flutter_gen/gen_l10n 中看到生成的文件。同样的,你可以在应用没有运行的时候运行 flutter gen-l10n 来生成本地化文件。

  7. 在调用 MaterialApp 的构造函数时候,添加 import 语句,导入 app_localizations.dartAppLocalizations.delegate

    dart
    import 'package:flutter_gen/gen_l10n/app_localizations.dart';
    dart
    return const MaterialApp(
      title: 'Localizations Sample App',
      localizationsDelegates: [
        AppLocalizations.delegate, // Add this line
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      supportedLocales: [
        Locale('en'), // English
        Locale('es'), // Spanish
      ],
      home: MyHomePage(),
    );

    AppLocalizations 类也可以自动自动生成 localizationsDelegatessupportedLocales 列表,而无需手动提供它们。

    dart
    const MaterialApp(
      title: 'Localizations Sample App',
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
    );
  8. Material 应用启动后,你就可以在应用的任意地方使用 AppLocalizations 了:

    dart
    appBar: AppBar(
      // The [AppBar] title text should update its message
      // according to the system locale of the target platform.
      // Switching between English and Spanish locales should
      // cause this text to update.
      title: Text(AppLocalizations.of(context)!.helloWorld),
    ),

如果目标设备的语言环境设置为英语,此代码生成的 Text widget 会展示「Hello World!」。如果目标设备的语言环境设置为西班牙语,则展示「Hola Mundo!」,在 arb 文件中,每个条目的键值都被用作 getter 的方法名称,而该条目的值则表示本地化的信息。

要查看使用该工具的示例 Flutter 应用,请参阅 gen_l10n_example

如需本地化设备应用描述,你可以将本地化后的字符串传递给 MaterialApp.onGenerateTitle:

dart
return MaterialApp(
  onGenerateTitle: (context) => DemoLocalizations.of(context).title,

占位符、复数和选项

#

你还可以使用特殊语法在信息中包含应用程序的值,该语法使用 占位符 生成方法(并非 getter)。占位符必须是有效的 Dart 标识符名称,它将成为 AppLocalizations 代码中生成方法的位置参数。用大括号定义占位符名称,如下所示:

json
"{placeholderName}"

在应用程序 .arb 文件内的 placeholders 对象中定义每个占位符。例如,需要定义带有 userName 参数的 hello 信息,请在 lib/l10n/app_en.arb 中添加以下内容:

json
"hello": "Hello {userName}",
"@hello": {
  "description": "A message with a single parameter",
  "placeholders": {
    "userName": {
      "type": "String",
      "example": "Bob"
    }
  }
}

此代码段为 AppLocalizations.of(context) 对象添加了一个 hello 方法调用,该方法接收一个 String 类型的参数; hello 方法返回一个字符串。重新生成 AppLocalizations 文件。

Builder 中的代码替换为以下代码:

dart
// Examples of internationalized strings.
return Column(
  children: <Widget>[
    // Returns 'Hello John'
    Text(AppLocalizations.of(context)!.hello('John')),
  ],
);

你还可以使用数字占位符来指定多个值。不同的语言有不同的单词复数化形式。该语法还支持指定单词的复数化形式。一个 复数化 信息必须包含一个 num 参数,指明在不同情况下该单词的复数化形式。例如,英语将「person」复数为「people」,但这还不够。 message0 的复数可能是「no people」或「zero people」。 messageFew 的复数可能是「several people」、「some people」或「a few people」。 messageMany 的复数可能是「most people」、「many people」或「a crowd」。只有更通用的 messageOther 字段是必填的。下面的示例显示了可用的选项:

json
"{countPlaceholder, plural, =0{message0} =1{message1} =2{message2} few{messageFew} many{messageMany} other{messageOther}}"

前面的表达式由 countPlaceholder 值相对应的信息变量(message0message1、...)所替代。只有 messageOther 字段是必填的。

下面的示例定义了「袋熊」复数化的信息:

json
"nWombats": "{count, plural, =0{no wombats} =1{1 wombat} other{{count} wombats}}",
"@nWombats": {
  "description": "A plural message",
  "placeholders": {
    "count": {
      "type": "num",
      "format": "compact"
    }
  }
}

通过传递 count 参数来使用复数方法:

dart
// Examples of internationalized strings.
return Column(
  children: <Widget>[
    ...
    // Returns 'no wombats'
    Text(AppLocalizations.of(context)!.nWombats(0)),
    // Returns '1 wombat'
    Text(AppLocalizations.of(context)!.nWombats(1)),
    // Returns '5 wombats'
    Text(AppLocalizations.of(context)!.nWombats(5)),
  ],
);

与复数类似,你也可以根据 String 占位符选择一个值。这通常用于性别。语法如下:

json
"{selectPlaceholder, select, case{message} ... other{messageOther}}"

下面的示例定义了一条信息,该信息根据性别选择代词:

json
"pronoun": "{gender, select, male{he} female{she} other{they}}",
"@pronoun": {
  "description": "A gendered message",
  "placeholders": {
    "gender": {
      "type": "String"
    }
  }
}

将性别字符串作为参数传递,即可使用该功能:

dart
// Examples of internationalized strings.
return Column(
  children: <Widget>[
    ...
    // Returns 'he'
    Text(AppLocalizations.of(context)!.pronoun('male')),
    // Returns 'she'
    Text(AppLocalizations.of(context)!.pronoun('female')),
    // Returns 'they'
    Text(AppLocalizations.of(context)!.pronoun('other')),
  ],
);

请记住,在使用 select 语句时,参数和实际值之间的比较是区分大小写的。也就是说,AppLocalizations.of(context)!.pronoun("Male") 默认为「other」,并返回「they」。

避免语法解析

#

有时你会使用符号(例如 {})作为普通文本的一部分。如果你想要让它们不被解析为一种语法,可以在 l10n.yaml 中设置 use-escaping

yaml
use-escaping: true

启用后,解析器会忽略使用一对单引号包括的文字,如果在文字中又想使用单个单引号,需要使用成对的单引号进行转义。例如,下面的文字会直接转为 Dart 的 String

json
{
  "helloWorld": "Hello! '{Isn''t}' this a wonderful day?"
}

结果如下:

dart
"Hello! {Isn't} this a wonderful day?"

包含数字和货币的信息

#

数字,包括那些代表货币价值的数字,在不同的本地化环境中显示的方式大相径庭。在 flutter_localizations 中的本地化生成工具使用了 intl package 中的 NumberFormat 类,根据本地化和所需的格式来格式化数字。

intdoublenumber 类型可以使用以下任何一个 NumberFormat 构造函数:

信息「格式」值 输出为 1200000
compact "1.2M"
compactCurrency* "$1.2M"
compactSimpleCurrency* "$1.2M"
compactLong "1.2 million"
currency* "USD1,200,000.00"
decimalPattern "1,200,000"
decimalPatternDigits* "1,200,000"
decimalPercentPattern* "120,000,000%"
percentPattern "120,000,000%"
scientificPattern "1E6"
simpleCurrency* "$1,200,000"

表中带星(*)NumberFormat 构造函数提供了可选的命名参数。这些参数可以指定为 placeholders 中 optionalParameters 对象的值。例如,要为 compactCurrency 指定可选的 decimalDigits 参数,请对 lib/l10n/app_en.arg 文件进行以下更改:

json
"numberOfDataPoints": "Number of data points: {value}",
"@numberOfDataPoints": {
  "description": "A message with a formatted int parameter",
  "placeholders": {
    "value": {
      "type": "int",
      "format": "compactCurrency",
      "optionalParameters": {
        "decimalDigits": 2
      }
    }
  }
}

带日期的信息

#

日期字符串的格式有很多种,取决于地区和应用程序的需求。

DateTime 类型的占位符使用 intl package 中的 DateFormat 格式化。

格式变体共有 41 种,由 DateFormat factory 构造函数的名称标识。在下面的示例种,出现在 helloWorldOn 信息中的 DateTime 值是用 DateFormat.yMd 进行的格式化:

json
"helloWorldOn": "Hello World on {date}",
"@helloWorldOn": {
  "description": "A message with a date parameter",
  "placeholders": {
    "date": {
      "type": "DateTime",
      "format": "yMd"
    }
  }
}

在语言环境为英语(美国)的应用中,以下表达式将会是 7/9/1959,在俄罗斯语言环境中,它将是 9.07.1959。

dart
AppLocalizations.of(context).helloWorldOn(DateTime.utc(1959, 7, 9))

iOS 本地化:更新 iOS app bundle

#

虽然本地化由 Flutter 处理,但你仍需要在 Xcode 项目中添加支持的语言。这将确保你在 App Store 的条目中正确显示支持的语言。

请按照以下说明,来配置应用支持的本地语言:

  1. 打开项目的 ios/Runner.xcodeproj Xcode 文件。

  2. Project Navigator 中,选择 Projects 下的 Runner 项目文件。

  3. 在项目编辑器中选择 Info 选项卡。

  4. Localizations 部分,单击 添加 按钮 (+),将支持的语言和地区添加到项目中。当要求选择文件和参考语言时,只需选择 Finish 即可。

  5. Xcode 会自动创建空的 .strings 文件并更新 ios/Runner.xcodeproj/project.pbxproj 文件。 App Store 会使用这些文件来确定应用支持哪些语言和地区。

定制的进阶操作

#

本节介绍自定义本地 Flutter 应用程序的其他方法。

高级语言环境定义

#

一些具有着多个变体的语言仅用 languageCode 来区分是不够充分的。

例如,在多语言应用开发这个话题里,如果要更好的区分具有多种语言变体的中文,则需要指定其 languageCodescriptCodecountryCode。因为目前有两种主要的,且存在地区使用差异的中文书写系统:简体和繁体。

为了让 CNTWHK 能够更充分地表示到每个中文变体,构建应用时,设定支持的语言列表可以参考如下代码:

dart
supportedLocales: [
  Locale.fromSubtags(languageCode: 'zh'), // generic Chinese 'zh'
  Locale.fromSubtags(
      languageCode: 'zh',
      scriptCode: 'Hans'), // generic simplified Chinese 'zh_Hans'
  Locale.fromSubtags(
      languageCode: 'zh',
      scriptCode: 'Hant'), // generic traditional Chinese 'zh_Hant'
  Locale.fromSubtags(
      languageCode: 'zh',
      scriptCode: 'Hans',
      countryCode: 'CN'), // 'zh_Hans_CN'
  Locale.fromSubtags(
      languageCode: 'zh',
      scriptCode: 'Hant',
      countryCode: 'TW'), // 'zh_Hant_TW'
  Locale.fromSubtags(
      languageCode: 'zh',
      scriptCode: 'Hant',
      countryCode: 'HK'), // 'zh_Hant_HK'
],

代码里这几组明确和完整的定义,可以确保你的应用为各种不同首选语言环境的用户提供更加精细化的本地化内容。如果用户没有指定首选的语言环境,那么我们就会使用最近的匹配,这可能与用户的期望会有差异。 Flutter 只会解析定义在 supportedLocales 里面的语言环境。对于那些常用语言,Flutter 为本地化内容提供了文字代码级别的区分。查看 Localizations 了解 Flutter 是如何解析支持的语言环境和首选的语言环境的。

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

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

#

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

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

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

dart
Locale myLocale = Localizations.localeOf(context);

指定应用程序 supported­Locales 参数

#

尽管 flutter_localizations 库目前支持 115 种语言和语言变体,但默认情况下仅提供英语译文。具体支持哪些语言由开发人员决定。

MaterialAppsupportedLocales 参数限制了本地化设置的更改。当用户更改设备上的语言设置时,只有在 supportedLocales 参数列表中包含了用户更改的本地化语言设置的情况下,应用程序的 Localizations widget 才会生效。如果找不到与设备本地化完全匹配的语言,则会使用与 languageCode 匹配的第一个受支持的语言。如果仍然找不到,则使用 supportedLocales 列表中的第一个元素。

如果应用程序希望使用不同的「本地化解析」方法,可以提供 localeResolutionCallback。例如,应用程序可以无条件接受用户选择的任何语言:

dart
MaterialApp(
  localeResolutionCallback: (
    locale,
    supportedLocales,
  ) {
    return locale;
  },
);

配置 l10n.yaml 文件

#

通过 l10n.yaml 文件,你可以配置 gen-l10n 工具,指定以下内容:

  • 所有输入文件的位置

  • 所有输出文件的创建位置

  • 为本地化委托赋予自定义的 Dart 类名

获取完整的选项列表,可在命令行中运行 flutter gen-l10n --help 或参考下表内容:

可选项 说明
arb-dir 模板和翻译 arb 文件所在的目录。默认为 lib/l10n
output-dir 生成本地化类的目录。只有当你想在 Flutter 项目的其他位置生成本地化代码时,才需要使用此选项。你还需要将 synthetic-package 标志设为 false。

应用程序必须从该目录导入 output-localization-file 选项中指定的文件。如果未指定,则默认与 arb-dir 中指定的输入目录相同。
template-arb-file 用于生成 Dart 本地化和信息文件的 arb 模板文件。默认为 app_en.arb
output-localization-file 输出本地化和本地化委托类的文件名。默认为 app_localizations.dart
untranslated-messages-file 描述尚未翻译的本地化信息的文件位置。使用该选项会在目标位置创建一个 JSON 文件,格式如下:

"locale": ["message_1", "message_2" ... "message_n"]

如果未指定此选项,则会在命令行中打印尚未翻译的信息摘要。
output-class 用于输出本地化和本地化委托类的 Dart 类名。默认为 AppLocalizations
preferred-supported-locales 应用程序首选支持的本地语言列表。默认情况下,工具会按字母顺序生成支持的本地语言列表。使用此标记可默认为不同的本地语言。

例如,设备支持美式英语,则输入 [ en_US ] 默认为美式英语。
header 在生成的 Dart 本地化文件中预置头文件。该选项包含一个字符串。

例如,输入 "/// All localized files.",就会在生成的 Dart 文件中预置这个字符串。

或者,还可以使用 header-file 选项来传递一个文本文件,以获得更长的头文件。
header-file 在生成的 Dart 本地化文件中预置头文件。该选项的值是包含头文件文本的文件名,头文件文本将插入每个生成的 Dart 文件的顶部。

或者,还可以使用 header 选项来传递一个字符串,以获得更简单的头文件。

该文件应放在 arb-dir 中指定的目录下。
[no-]use-deferred-loading 指定是否将生成的 Dart 本地化文件延迟导入,以便在 Flutter web 中对每个本地化进行懒加载。

这可以减少 JavaScript 程序的大小,从而缩短 web 应用的初始启动时间。当此标记设置为 true 时,Flutter 应用程序只会在需要时下载和加载特定语言的信息。对于具有大量不同本地化字符串的项目,延迟加载可以提高性能。对于本地化字符串数量较少的项目,两者之间的差异可以忽略不计,但是将本地化字符串与应用程序的其他部分捆绑在一起相比,可能会降低启动速度。

请注意,此标记不会影响移动或桌面等其他平台。
gen-inputs-and-outputs-list 指定后,工具会生成一个 JSON 文件,其中包含工具的输入和输出的内容,文件名为 gen_l10n_inputs_and_outputs.json

这对于追踪生成最新的本地化时使用了 Flutter 项目中的哪些文件非常有用。例如,Flutter 工具的构建系统会使用此文件来追踪在热重载期间何时调用 gen_l10n。

该选项的值是生成 JSON 文件的目录。如果为空,则不会生成 JSON 文件。
synthetic-package 决定生成的输出文件是作为 synthetic package 还是在 Flutter 项目中指定的目录下生成。该标志默认为 truesynthetic-package 设置为 false 时,默认会在 arb-dir 指定的目录下生成本地化文件。如果指定了 output-dir 目录,则会在该目录下生成文件。
project-dir 指定后,工具将使用此选项中传递的路径作为 Flutter 项目的根目录。

如果为空,则使用当前工作目录的相对路径。
[no-]required-resource-attributes 要求所有资源 ID 包含相应的资源属性。

默认情况下,简单信息不需要元数据,但强烈建议使用元素据,因为它能为读者提供信息含义的上下文。

复数信息仍然需要资源属性。
[no-]nullable-getter 指定本地化类 getter 是否可为空。

默认情况下,该值为 true,这样 Localizations.of(context) 就会返回一个可归零的值,以实现向下兼容。如果该值为 false,则会对 Localizations.of(context) 返回的值进行空值检查,从而无需在用户代码中进行空值检查。
[no-]format 指定后,将在生成本地化文件后运行 dart format 指令。
use-escaping 指定是否启用单引号作为转义语法。
[no-]suppress-warnings 指定后,将不会进行警告。
[no-]relax-syntax 指定后,语法会变得宽松:如果特殊字符“{”后面没有有效的占位符,它将被视为字符串;如果“}”无法配对之前的特殊字符“{”,它也将被视为字符串。
[no-]use-named-parameters 是否为生成的本地化方法使用命名参数。

Flutter 里的国际化是如何工作的

#

本节涵盖了 Flutter 中本地化工作的技术细节,如果你计划使用自定的一套本地化信息,下面的内容会很有帮助。反之则可以跳过本节。

加载和获取本地化值

#

我们使用 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 管理着对象的一个对应表,每个 LocalizationsDelegate 对应一个对象。为了获得由 LocalizationsDelegateload 方法生成的对象,你需要指定一个构建上下文 (BuildContext) 和对象的类型。

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

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

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

dart
static MaterialLocalizations of(BuildContext context) {
  return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
}

/// References to the localized values defined by MaterialLocalizations
/// are typically written like this:

tooltip: MaterialLocalizations.of(context).backButtonTooltip,

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

#

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

此示例 app 的 完整的源码

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

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

dart
class DemoLocalizations {
  DemoLocalizations(this.localeName);

  static Future<DemoLocalizations> load(Locale locale) {
    final String name =
        locale.countryCode == null || locale.countryCode!.isEmpty
            ? locale.languageCode
            : locale.toString();
    final String localeName = Intl.canonicalizedLocale(name);

    return initializeMessages(localeName).then((_) {
      return DemoLocalizations(localeName);
    });
  }

  static DemoLocalizations of(BuildContext context) {
    return Localizations.of<DemoLocalizations>(context, DemoLocalizations)!;
  }

  final String localeName;

  String get title {
    return Intl.message(
      'Hello World',
      name: 'title',
      desc: 'Title for the Demo application',
      locale: localeName,
    );
  }
}

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

添加支持新的语言

#

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

举个例子,我们将给大家展示如何支持挪威尼诺斯克语。

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

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

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

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

dart
@override
String get moreButtonTooltip => r'More';

@override
String get aboutListTileTitleRaw => r'About $applicationName';

@override
String get alertDialogLabel => r'Alert';

当然,这些都是英语翻译。为了完成本地化操作,你需要把每一个 getter 的返回值翻译成合适的新挪威语 (Nynorsk) 字符串。

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

dart
@override
String get pageRowsInfoTitleRaw => r'$firstRow–$lastRow of $rowCount';

@override
String get pageRowsInfoTitleApproximateRaw =>
    r'$firstRow–$lastRow of about $rowCount';

语言对应的日期格式和符号需要一并指定。在源码中,它们会以下列形式进行定义:

dart
const nnLocaleDatePatterns = {
  'd': 'd.',
  'E': 'ccc',
  'EEEE': 'cccc',
  'LLL': 'LLL',
  // ...
}
dart
const nnDateSymbols = {
  'NAME': 'nn',
  'ERAS': <dynamic>[
    'f.Kr.',
    'e.Kr.',
  ],
  // ...
}

上列内容需要修改以匹配语言的正确日期格式。可惜的是,intl 并不具备数字格式的灵活性,以至于 _NnMaterialLocalizationsDelegate 需要使用现有的语言的格式作为替代方法:

dart
class _NnMaterialLocalizationsDelegate
    extends LocalizationsDelegate<MaterialLocalizations> {
  const _NnMaterialLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) => locale.languageCode == 'nn';

  @override
  Future<MaterialLocalizations> load(Locale locale) async {
    final String localeName = intl.Intl.canonicalizedLocale(locale.toString());

    // The locale (in this case `nn`) needs to be initialized into the custom
    // date symbols and patterns setup that Flutter uses.
    date_symbol_data_custom.initializeDateFormattingCustom(
      locale: localeName,
      patterns: nnLocaleDatePatterns,
      symbols: intl.DateSymbols.deserializeFromMap(nnDateSymbols),
    );

    return SynchronousFuture<MaterialLocalizations>(
      NnMaterialLocalizations(
        localeName: localeName,
        // The `intl` library's NumberFormat class is generated from CLDR data
        // (see https://github.com/dart-lang/i18n/blob/main/pkgs/intl/lib/number_symbols_data.dart).
        // Unfortunately, there is no way to use a locale that isn't defined in
        // this map and the only way to work around this is to use a listed
        // locale's NumberFormat symbols. So, here we use the number formats
        // for 'en_US' instead.
        decimalFormat: intl.NumberFormat('#,##0.###', 'en_US'),
        twoDigitZeroPaddedFormat: intl.NumberFormat('00', 'en_US'),
        // DateFormat here will use the symbols and patterns provided in the
        // `date_symbol_data_custom.initializeDateFormattingCustom` call above.
        // However, an alternative is to simply use a supported locale's
        // DateFormat symbols, similar to NumberFormat above.
        fullYearFormat: intl.DateFormat('y', localeName),
        compactDateFormat: intl.DateFormat('yMd', localeName),
        shortDateFormat: intl.DateFormat('yMMMd', localeName),
        mediumDateFormat: intl.DateFormat('EEE, MMM d', localeName),
        longDateFormat: intl.DateFormat('EEEE, MMMM d, y', localeName),
        yearMonthFormat: intl.DateFormat('MMMM y', localeName),
        shortMonthDayFormat: intl.DateFormat('MMM d'),
      ),
    );
  }

  @override
  bool shouldReload(_NnMaterialLocalizationsDelegate old) => false;
}

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

一旦你实现了指定语言的 GlobalMaterialLocalizationsLocalizationsDelegate 的子类,你只需要给你的 app 添加此语言以及一个 delegate 的实例。这里有一些代码展示了如何设置 app 的语言为尼诺斯克语以及如何给 app 的 localizationsDelegates 列表添加 NnMaterialLocalizations delegate 实例。

dart
const MaterialApp(
  localizationsDelegates: [
    GlobalWidgetsLocalizations.delegate,
    GlobalMaterialLocalizations.delegate,
    NnMaterialLocalizations.delegate, // Add the newly created delegate
  ],
  supportedLocales: [
    Locale('en', 'US'),
    Locale('nn'),
  ],
  home: Home(),
),

其他的国际化方法

#

本节主要介绍国际化 Flutter 应用的不同方法。

应用程序本地化资源的替代类

#

之前的示例应用主要根据 Dart intl package 定义,为了简单起见,或者可能想要与不同的 i18n 框架集成,开发者也可以选择自己的方法来管理本地化的值。

点击查看 minimal 应用的完整源代码。

在下面这个样例中,包含应用本地化版本的类 DemoLocalizations 直接在每种语言的 Map 中包括了所有的翻译。

dart
class DemoLocalizations {
  DemoLocalizations(this.locale);

  final Locale locale;

  static DemoLocalizations of(BuildContext context) {
    return Localizations.of<DemoLocalizations>(context, DemoLocalizations)!;
  }

  static const _localizedValues = <String, Map<String, String>>{
    'en': {
      'title': 'Hello World',
    },
    'es': {
      'title': 'Hola Mundo',
    },
  };

  static List<String> languages() => _localizedValues.keys.toList();

  String get title {
    return _localizedValues[locale.languageCode]!['title']!;
  }
}

在 minimal 应用中,DemoLocalizationsDelegate 略有不同,它的 load 方法返回一个 SynchronousFuture,因为不需要进行异步加载。

dart
class DemoLocalizationsDelegate
    extends LocalizationsDelegate<DemoLocalizations> {
  const DemoLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) =>
      DemoLocalizations.languages().contains(locale.languageCode);

  @override
  Future<DemoLocalizations> load(Locale locale) {
    // Returning a SynchronousFuture here because an async "load" operation
    // isn't needed to produce an instance of DemoLocalizations.
    return SynchronousFuture<DemoLocalizations>(DemoLocalizations(locale));
  }

  @override
  bool shouldReload(DemoLocalizationsDelegate old) => false;
}

附录:使用 Dart intl 工具

#

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

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

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

  1. 在 app 的根目录,使用 lib/main.dart 生成 l10n/intl_messages.arb

    dart 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 分别作为英语和西班牙语翻译的模板。这些翻译是由你(开发者)来创建的。

  2. 在 app 的根目录,生成每个 intl_<locale>.arb 文件对应的 intl_messages_<locale>.dart 文件,以及 intl_messages_all.dart 文件,它引入了所有的信息文件。

    dart run intl_translation:generate_from_arb \
        --output-dir=lib/l10n --no-use-deferred-loading \
        lib/main.dart lib/l10n/intl_*.arb

    Windows 系统不支持文件名通配符。列出的 .arb 文件是由 intl_translation:extract_to_arb 命令生成的。

    dart run intl_translation:generate_from_arb \
        --output-dir=lib/l10n --no-use-deferred-loading \
        lib/main.dart \
        lib/l10n/intl_en.arb lib/l10n/intl_fr.arb lib/l10n/intl_messages.arb

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

更多信息

#

如果你希望通过代码进行学习,你可以查看以下的示例。

  • 最简示例
    最简展示了如果以最简单的方式实现国际化。

  • intl 示例
    利用 intl package 实现国际化的示例。

如果你还未使用过 intl package,你可以阅读 如何使用 Dart 的 intl 工具