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 檔案中形如:

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 套件現在應該被正確地本地化為 115 個受支援的語言環境之一。 widget 應當與本地化訊息保持同步,並具有正確的從左到右或從右到左的佈局。

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

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

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

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

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

過載語言

Localizations.override 提供了一個工廠構造方法,使得你可以從在某一個位置設定與應用不同的語言(非一般情況)。

下面的範例展示了 Localizations.overrideCalendarDatePicker 組合使用的情況:

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

    該檔案用於設定本地化工具。它為你的專案設定瞭如下內容:

    • 應用資源套件 (.arb) 的輸入路徑指定為 ${FLUTTER_PROJECT}/lib/l10n

      .arb 檔案提供了應用的本地化資源。

    • 將英文的語言樣板設定為 app_en.arb

    • 指定 Flutter 生成本地化內容到 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 run 指令,你將在 ${FLUTTER_PROJECT}/.dart_tool/flutter_gen/gen_l10n 中看到生成的檔案。同樣的,你可以在應用沒有執行的時候執行 flutter 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. Material 應用啟動後,你就可以在應用的任意地方使用 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,

佔位符、複數和選項

你還可以使用特殊語法在訊息中包含應用程式的值,該語法使用 佔位符 生成方法(並非 getter)。佔位符必須是有效的 Dart 識別符號名稱,它將成為 AppLocalizations 程式碼中生成方法的位置引數。用大括號定義佔位符名稱,如下所示:

"{placeholderName}"

在應用程式 .arb 檔案內的 placeholders 物件中定義每個佔位符。例如,需要定義帶有 userName 引數的 hello 訊息,請在 lib/l10n/app_en.arb 中新增以下內容:

"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 中的程式碼替換為以下程式碼:

// 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 欄位是必填的。下面的範例顯示了可用的選項:

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

前面的表示式由 countPlaceholder 值相對應的訊息變數(message0message1、…)所替代。只有 messageOther 欄位是必填的。

下面的範例定義了「袋熊」複數化的訊息:

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

透過傳遞 count 引數來使用複數方法:

// 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 佔位符選擇一個值。這通常用於性別。語法如下:

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

下面的範例定義了一條訊息,該訊息根據性別選擇代詞:

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

將性別字串作為引數傳遞,即可使用該功能:

// 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

use-escaping: true

啟用後,解析器會忽略使用一對單引號包括的文字,如果在文字中又想使用單個單引號,需要使用成對的單引號進行轉義。例如,下面的文字會直接轉為 Dart 的 String

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

結果如下:

"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 檔案進行以下更改:

"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 進行的格式化:

"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。

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

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);

指定應用程式 supported­Locales 引數

儘管 flutter_localizations 函式庫目前支援 115 種語言和語言變體,但預設情況下僅提供英語譯文。具體支援哪些語言由開發人員決定。

MaterialAppsupportedLocales 引數限制了本地化設定的更改。當用戶更改裝置上的語言設定時,只有在 supportedLocales 引數清單中包含了使用者更改的本地化語言設定的情況下,應用程式的 Localizations widget 才會生效。如果找不到與裝置本地化完全對應的語言,則會使用與 languageCode 對應的第一個受支援的語言。如果仍然找不到,則使用 supportedLocales 清單中的第一個元素。

如果應用程式希望使用不同的「本地化解析」方法,可以提供 localeResolutionCallback。例如,應用程式可以無條件接受使用者選擇的任何語言:

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 指定後,將不會進行警告。

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,

為 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/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 實例。

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

更多訊息

如果你希望透過程式碼進行學習,你可以檢視以下的範例。

  • 最簡範例
    最簡展示了如果以最簡單的方式實現國際化。

  • intl 範例
    利用 intl package 實現國際化的範例。

如果你還未使用過 intl package,你可以閱讀 如何使用 Dart 的 intl 工具