Flutter 應用裡的國際化

如果你的 app 會部署給說其他語言的使用者使用,那麼你就需要對它進行國際化。這就意味著你在編寫 app 的時候,需要採用一種容易對它進行本地化的方式進行開發,這種方式讓你能夠為每一種語言或者 app 所支援的語言環境下的文字和佈局等進行本地化。 Flutter 提供了 widgets 和類來幫助開發者進行國際化,當然 Flutter 庫本身就是國際化的。

由於大多數應用程式都是以這種方式編寫的,因此該頁面主要介紹了使用 MaterialAppCupertinoApp 對 Flutter 應用程式進行本地化所需的概念和工作流程。但是,使用較低級別的 WidgetsApp 類編寫的應用程式也可以使用相同的類和邏輯進行國際化。

Flutter 應用本地化介紹

本節主要介紹如何對 Flutter 應用進行國際化,以及針對目標平台需要設定的其他內容。

配置一個國際化的 app:flutter_localizations package

預設情況下,Flutter 只提供美式英語的本地化。如果想要新增其他語言,你的應用必須指定額外的 MaterialApp 或者 CupertinoApp 屬性並且新增一個名為 flutter_localizations 的 package。截至到 2023 年 1 月份,這個 package 已經支援大約 79 種語言。

若要開始使用,在 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 檔案中形如:

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

下一步,先執行 pub get packages,然後引入 flutter_localizations 庫,然後為 MaterialApp 指定 localizationsDelegatessupportedLocales

import 'package:flutter_localizations/flutter_localizations.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 包現在應該被正確地本地化為 78 個受支援的語言環境之一。 widget 應當與本地化資訊保持同步,並具有正確的從左到右或從右到左的佈局。

你可以嘗試將目標平台的語言環境切換為西班牙語(es),然後應該可以發現資訊已經被本地化了。

基於 WidgetsApp 建構的 app 在新增語言環境時,除了 GlobalMaterialLocalizations.delegate 不需要之外,其他的操作是類似的。

雖然 語言環境 (Locale) 預設的建構函式是完全沒有問題的,但是還是建議大家使用 Locale.fromSubtags 的建構函式,因為它支援設定 文字程式碼

localizationDelegates 陣列是用於產生本地化值集合的工廠。 GlobalMaterialLocalizations.delegate 為 Material 元件庫提供本地化的字串和一些其他的值。 GlobalWidgetsLocalizations.delegate 為 widgets 庫定義了預設的文字排列方向,由左到右或者由右到左。

想知道更多關於這些 app 屬性,它們依賴的型別以及那些國際化的 Flutter app 通常是如何組織的,可以繼續閱讀下面內容。

Overriding the Locale

Localizations.override is a factory constructor for the Localizations widget that allows for (the typically rare) situation where a section of your application needs to be localized to a different locale than the locale configured for your device.

To observe this behavior, add a call to Localizations.override and a simple CalendarDatePicker:

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 檔案中後面的部分。

    # The following section is specific to Flutter.
    flutter:
      generate: true # Add this line
  3. 在 Flutter 專案的根目錄中新增一個新的 yaml 檔案,命名為 l10n.yaml,其內容如下:

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

    該檔案用於配置本地化工具;在上面的範例中,指定輸入檔案在 ${FLUTTER_PROJECT}/lib/l10n 中,app_en.arb 檔案提供範本,產生的本地化檔案在 app_localizations.dart 檔案中。

  4. ${FLUTTER_PROJECT}/lib/l10n 中,新增 app_en.arb 範本檔案。如下:

    {
      "helloWorld": "Hello World!",
      "@helloWorld": {
        "description": "The conventional newborn programmer greeting"
      }
    }
  5. 接下來,在同一目錄中新增一個 app_es.arb 檔案,對同一條資訊做西班牙語的翻譯:

    {
        "helloWorld": "¡Hola Mundo!"
    }
  6. 現在,執行 flutter gen-l10n 命令,您將在 ${FLUTTER_PROJECT}/.dart_tool/flutter_gen/gen_l10n 中看到產生的檔案。

    同樣的,你可以在應用沒有執行的時候執行那個命令來產生本地化檔案。

  7. 在呼叫 MaterialApp 的建構函式時候,新增 import 陳述式,匯入 app_localizations.dartAppLocalizations.delegate

    import 'package:flutter_gen/gen_l10n/app_localizations.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 列表,而無需手動提供它們。

    const MaterialApp(
      title: 'Localizations Sample App',
      localizationsDelegates: AppLocalizations.localizationsDelegates,
      supportedLocales: AppLocalizations.supportedLocales,
    );
  8. 現在,你可以在應用的任意地方使用 AppLocalizations 了:

    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:

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

Placeholders, plurals, and selects

You can also include application values in a message with special syntax that uses a placeholder to generate a method instead of a getter. A placeholder, which must be a valid Dart identifier name, becomes a positional parameter in the generated method in the AppLocalizations code. Define a placeholder name by wrapping it in curly braces as follows:

"{placeholderName}"

Define each placeholder in the placeholders object in the app’s .arb file. For example, to define a hello message with a userName parameter, add the following to lib/l10n/app_en.arb:

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

Regenerate the AppLocalizations file. This adds a hello method call to the AppLocalizations.of(context) object, and the method accepts a parameter of type String; the hello method returns a string.

Implement this by replacing the code passed into Builder with the following:

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

You can also use numerical placeholders to specify multiple values. Different languages have different ways to pluralize words. The syntax also supports specifying how a word should be pluralized. A pluralized message must include a num parameter indicating how to pluralize the word in different situations. English, for example, pluralizes “person” to “people”, but that doesn’t go far enough. The message0 plural might be “no people” or “zero people”. The messageFew plural might be “several people”, “some people”, or “a few people”. The messageMany plural might be “most people” or “many people”, or “a crowd”. Only the more general messageOther field is required. The following example shows what options are available:

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

The expression above will be replaced by the message variation (message0, message1, …) corresponding to the value of the countPlaceholder. Only the messageOther field is required.

The following example defines a message that pluralizes the word, “wombat”:

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

Using a plural method is easy enough, just pass it the item count parameter:

// 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)),
  ],
);

Similar to plurals, you can also choose a value based on a String placeholder. This is most often used to support gendered languages. The syntax is:

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

The following example defines a message that selects a pronoun based on gender:

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

To use this feature, pass the gender string as a parameter:

// 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')),
  ],
);

Keep in mind that when using select statements, comparison between the parameter and the actual value is case-sensitive. That is, AppLocalizations.of(context)!.pronoun("Male") will default to the “other” case, and return “they”.

Messages with numbers and currencies

Numbers, including those that represent currency values, are displayed very differently in different locales. The localizations generation tool in flutter_localizations uses the intl package’s NumberFormat class to properly format numbers based on the locale and the desired format.

The int, double, and number types can use any of the following NumberFormat constructors:

Message “format” value Output for 1200000
compact “1.2M”
compactCurrency* “$1.2M”
compactSimpleCurrency* “$1.2M”
compactLong “1.2 million”
currency* “USD1,200,000.00”
decimalPattern “1,200,000”
decimalPercentPattern* “120,000,000%”
percentPattern “120,000,000%”
scientificPattern “1E6”
simpleCurrency* “$1,200,000”

The starred NumberFormat constructors in the table offer optional, named parameters. Those parameters can be specified as the value of the placeholder’s optionalParameters object. For example, to specify the optional decimalDigits parameter for compactCurrency, make the following changes to the lib/l10n/app_en.arg file:

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

Messages with dates

Dates strings are formatted in many different ways depending both the locale and the app’s needs.

Placeholder values with type DateTime are formatted with intl’s DateFormat class. There are 41 format variations, identified by the names of their DateFormat factory constructors. In the following example, the DateTime value that appears in the helloWorldOn message is formatted with DateFormat.yMd:

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

在語言環境為英語(美國)的應用中,以下表達式將會是 7/10/1996,在俄羅斯語言環境中,它將是 10.07.1996。

AppLocalizations.of(context).helloWorldOn(DateTime.utc(1996, 7, 10))

有關本地化工具的更多資訊,例如處理 DateTime 和複數,請參見 國際化使用者指南

iOS 本地化:更新 iOS app bundle

iOS 應用在內置於應用程式套件中的 Info.plist 檔案中定義了關鍵的應用程式元資料,其中包括了受支援的語言環境,要配置您的應用支援的語言環境,請按照以下步驟進行操作:

  1. 開啟專案的 ios/Runner.xcworkspace Xcode 檔案。

  2. Project Navigator 中,開啟 Runner 專案的 Runner 資料夾下的 Info.plist 檔案。

  3. 選擇 Information Property List 項。然後從 Editor 選單中選擇 Add Item,接著從彈出選單中選擇 Localizations

  4. 選擇並展開新建立的 Localizations 項。對於您的應用程式支援的每種語言環境,請新增一個新項,然後從 Value 欄位中的彈出選單中選擇要新增的語言環境。該列表應需要與 supportedLocales 引數中列出的語言一致。

  5. 新增所有受支援的語言環境後,儲存檔案。

客製的進階操作

本節介紹自訂本地 Flutter 應用程式的其他方法。

高階語言環境定義

一些具有著多個變體的語言僅用 languageCode 來區分是不夠充分的。

例如,在多語言應用開發這個話題裡,如果要更好的區分具有多種語言變體的中文,則需要指定其 languageCodescriptCodecountryCode。因為目前有兩種主要的,且存在地區使用差異的中文書寫系統:簡體和繁體。

為了讓 CNTWHK 能夠更充分地表示到每個中文變體,建構應用時,設定支援的語言列表可以參考如下程式碼:

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 當前的語言環境:

Locale myLocale = Localizations.localeOf(context);

Specifying the app’s supported­Locales parameter

Although the flutter_localizations library currently supports 78 languages and language variants, only English language translations are available by default. It’s up to the developer to decide exactly which languages to support.

The MaterialApp supportedLocales parameter limits locale changes. When the user changes the locale setting on their device, the app’s Localizations widget only follows suit if the new locale is a member of this list. If an exact match for the device locale isn’t found, then the first supported locale with a matching languageCode is used. If that fails, then the first element of the supportedLocales list is used.

An app that wants to use a different “locale resolution” method can provide a localeResolutionCallback. For example, to have your app unconditionally accept whatever locale the user selects:

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

Configuring the l10n.yaml file

The l10n.yaml file allows you to configure the gen-l10n tool to specify:

  • where all the input files are located
  • where all the output files should be created
  • what Dart class name to give your localizations delegate

For a full list of options, check out the following table:

Option Description
arb-dir The directory where the template and translated arb files are located. The default is lib/l10n.
output-dir The directory where the generated localization classes will be written. This option is only relevant if you want to generate the localizations code somewhere else in the Flutter project. You will also need to set the synthetic-package flag to false.

The app must import the file specified in the output-localization-file option from this directory. If unspecified, this defaults to the same directory as the input directory specified in arb-dir.
template-arb-file The template arb file that will be used as the basis for generating the Dart localization and messages files. The default is app_en.arb.
output-localization-file The filename for the output localization and localizations delegate classes. The default is app_localizations.dart.
untranslated-messages-file The location of a file that describes the localization messages have not been translated yet. Using this option will create a JSON file at the target location, in the following format:

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

If this option is not specified, a summary of the messages that have not been translated will be printed on the command line.
output-class The Dart class name to use for the output localization and localizations delegate classes. The default is AppLocalizations.
preferred-supported-locales The list of preferred supported locales for the application. By default, the tool will generate the supported locales list in alphabetical order. Use this flag if you would like to default to a different locale.

For example, pass in [ en_US ] if you would like your app to default to American English if a device supports it.
synthetic-package Determines whether or not the generated output files will be generated as a synthetic package or at a specified directory in the Flutter project. This flag is set to true by default. When synthetic-package is set to false, it will generate the localizations files in the directory specified by arb-dir by default. If output-dir is specified, files will be generated there.
header The header to prepend to the generated Dart localizations files. This option takes in a string.

For example, pass in "/// All localized files." if you would like this string prepended to the generated Dart file.

Alternatively, see the header-file option to pass in a text file for longer headers.
header-file The header to prepend to the generated Dart localizations files. The value of this option is the name of the file that contains the header text which will be inserted at the top of each generated Dart file.

Alternatively, see the header option to pass in a string for a simpler header.

This file should be placed in the directory specified in arb-dir.
[no-]use-deferred-loading Whether to generate the Dart localization file with locales imported as deferred, allowing for lazy loading of each locale in Flutter web.

This can reduce a web app’s initial startup time by decreasing the size of the JavaScript bundle. When this flag is set to true, the messages for a particular locale are only downloaded and loaded by the Flutter app as they are needed. For projects with a lot of different locales and many localization strings, it can be a performance improvement to have deferred loading. For projects with a small number of locales, the difference is negligible, and might slow down the start up compared to bundling the localizations with the rest of the application.

Note that this flag does not affect other platforms such as mobile or desktop.

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 方法獲得。

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

因為這個特定的 Localizations.of() 表示式經常使用,所以 MaterialLocalizations 類提供了一個快捷存取:

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,

Defining a class for the app’s localized resources

為 app 的本地化資源定義一個類別

綜合所有這些在一起,一個需要國際化的 app 經常以一個封裝 app 本地化值的類開始的。下面是使用這種類別的典型範例。

此範例 app 的 完整的原始碼

這個範例是基於 intl package 提供的 API 和工具開發的, app 本地化資源的替代方法 裡面講解了一個不依賴於 intl package 的 範例

DemoLocalizations 類包含了 app 語言環境內支援的已經翻譯成了本地化語言的字串(本例子只有一個)。它透過呼叫由 Dart 的 intl package 產生的 initializeMessages() 方法來載入翻譯好的字串,然後使用 Intl.message() 來查閱它們。

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 所定義的,像下面這樣:

@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 返回的是原始的字串,因為有一些時候這些字串會包含一些帶有 $ 字首的變數。透過呼叫帶引數的本地化方法,這些變數會被替換:

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

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

語言對應的日期格式和符號需要一併指定。在原始碼中,它們會以下列形式進行定義:

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

上列內容需要修改以匹配語言的正確日期格式。可惜的是,intl 並不具備數字格式的靈活性,以至於 _NnMaterialLocalizationsDelegate 需要使用現有的語言的格式作為替代方法:

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/intl/blob/master/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 例項。

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 中包括了所有的翻譯。

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,因為不需要進行非同步載入。

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() 來查閱這些本地化的資訊。