若你的應用程式會發佈給一些使用其他語言的使用者,那麼就會需要支援各種語系。這表示我們需要讓應用程式支援其他語言或本地化呈現的資料。Flutter 當然提供了支援國際化的組件和類別且 Flutter 的函式庫本身就是國際化的。
下面我們會介紹相關概念和實作以讓我們的應用程式實現本地化,這裡主要會使用 MaterialApp
或 CupertinoApp
類別來實現,大部分的應用都是使用上面 2 者來處理多國語系,但底層的 WidgetApp
也可以實作相同的邏輯。
首先,我們須執行 flutter_localizations
相關設定。預設情況 Flutter 僅提供英文語系,為了支援其他語系我們必須在 MaterialApp
或 CupertinoApp
組件加入其他屬性設定,並且使用 flutter_localizations
套件。截止 2023 年底,該套件支援 大約 100 多種語言。
我們先建立一個範例專案:
$ flutter create i18n_demo
接著安裝 flutter_localizations
和 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
上面指令加上 --sdk
是因為這是從 Fluttter 框架安裝而不是從 pub.dev 安裝。而 any
的部分則是任意版本皆可。
然後我們開啟我們的專案,在 main.dart
匯入 flutter_localizations
並且為 MaterialApp
或者 CupertinoApp
加入 localizationsDelegates
和 supportedLocales
@override
Widget build(BuildContext context) {
return const MaterialApp(
title: '多國語系應用',
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
Locale('en'),
Locale('es'),
Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'TW'),
],
home: MyHomePage(),
);
}
在匯入 flutter_localizations
套件並且加入上面程式碼之後,Material
或 Cupertino
套件中的組件現在應可在 100 多種支援的區域設定其中一種本地化除了語系還有排版例如由右到左閱讀的排版。
基於 WidgetApp
的使用也類似,除了不需要 GlobalMaterialLocalizations.delegate
。
另外,雖然上面使用了預設的 Locale
建構子可以滿足多數情況,不過還是建議使用 Locale.fromSubtags
,因為它支持 scriptCode
。
localizationsDelegates
列表包含本地化資源的委派處理器,其概念類似於工廠模式,但專門用於異步載入和管理本地化資源,作用就是產生本地化「值」的集合。GlobalMaterialLocalizations.delegate
提供字串等給 Material 組件,GlobalCupertinoLocalizations.delegate
則是 Cupertino 組件,GlobalWidgetsLocalizations.delegate
負責處理預設文案的方向,右到左或左到右。
Localizations.override
是一個 Localizations
組件的工廠建構子。它用來處理一種不太常見的情況;當我們應用程式中某部分需要和裝置設定不同的語言或地區設定時。
例如:手機設定為中文,但你希望程式中的日曆顯示英文。這個時候就可以使用 Localizations.override
。
要觀察其行為,我們可以呼叫 Localizations.override
搭配 CalendarDatePicker
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Localizations.override(
context: context,
locale: const Locale("en"),
// 使用 Builder 取得當前的 BuildContext
child: Builder(
builder: (context) {
return CalendarDatePicker(
initialDate: DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime(2100),
onDateChanged: (value) {},
);
}
),
),
]
),
),
);
}
如果不使用 Builder
直接讓 CalendarDatePicker
作為子組件可能會導致本地化無法正常,又或者你可以建立一個新組件
class LocalizedCalendar extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 這個 context 現在包含了 Localizations.override 的設定
return CalendarDatePicker(
initialDate: DateTime.now(),
firstDate: DateTime(1900),
lastDate: DateTime(2100),
onDateChanged: (value) {},
);
}
}
概念上就是我們傳入舊的 context
覆寫後需要一種方式取得新的 context
。
在安裝 flutter_localizations
套件之後,你可以設定字典檔:
安裝 intl
套件
開啟 pubspec.yaml
啟動 generate
flutter:
uses-material-design: true
generate: true // <- 新增此行
在專案根目錄新增 l10n.yaml
並設定如下:
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart
這個檔案用來設定本地化工具,在這個例子中,我們完成了以下幾件事:
.arb
檔案設定在 lib/l10n
資料夾底下。app_en.arb
.arb
自動生成 app_localizations.dart
檔案工作流程為:開發者創建 ARB 文件,Flutter 的工具讀取這些文件,自動生成 Dart 代碼,使得這些翻譯可以在應用中使用。
在 lib/l10n
目錄下,加入 app_en.arb
樣板檔案:
{
"helloWorld": "Hello World!",
"@helloWorld": {
"description": "The conventional newborn programmer greeting"
}
}
加入其他語系檔 app_es.arb
、app_zh.arb
{
"helloWorld": "哈囉,世界!"
}
上面帶 @
的鍵是用來加入更多 meta 資訊。
現在執行 flutter pub get
或 flutter run
你應該會在 .dart_tool/flutter_gen/gen_l10n
目錄下產生檔案。又或者可以使用 flutter gen-l10n
在程式中匯入 app_localizations.dart
和 AppLocalizations.delegate
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// ...
return const MaterialApp(
title: 'Localizations Sample App',
localizationsDelegates: [
AppLocalizations.delegate, // 加入此行
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
Locale('en'), // English
Locale('es'), // Spanish
],
home: MyHomePage(),
);
AppLocalizations
類別除了 delegate
之外也自動提供 localizationsDelegates
和 supportedLocales
列表,你可以使用這些列表,而不需要手動提供它們。
const MaterialApp(
title: '多語系',
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
);
最後,我們可以在 MaterialApp
下任何地方使用:
appBar: AppBar(
title: Text(AppLocalizations.of(context)!.helloWorld),
),
MaterialApp
必須實例化且初始化 AppLocalizations
否則會造成 null
例外。
上面程式碼加入 Text
組件,如果目標裝置的語言設置為英語,它會顯示 "Hello World!";如果設置為西班牙語,則顯示 "¡Hola Mundo!"。在 arb
文件中,每個項目的「鍵」被用作 getter 方法。
若你使用 VS Code 可以安裝 arb-editor ,該擴展套件可以支援
.arb
語法高亮、診斷、快速修正等。
你還可以在翻譯訊息中使用特殊語法代入應用程式中動態的「值」,使用佔位符而不只是單純的 getter。不過這個佔位符號必須是個有效的變數,因為它會成為 AppLocalizations
方法中的一個位置參數。簡單來說,佔位符就是在翻譯文本中留下的"空位",讓你可以在實際使用時填入動態的內容。
{
"greeting": "Hello, {name}!"
}
上面範例是在 .arb
檔案中,定義佔位符。語法就是使用 {}
後續在程式碼中,可以如下代入值:
AppLocalizations.of(context)!.greeting('andyyou')
除此之外,還可以使用數字化的佔位符來指定多個值。不同的語言有不同組織「複數」單字的方式。這個語法同時支援設定如何呈現。一個複數單字須包含數字參數來判定如何顯示文字。例如英文中複數的 “person” 為 "people",但不只如此。 message0
複數可能是 “no people” 或 "zero people" 。一些的複數 messageFew
可能是 “several people” 或 “some people”
{
"nCats": "{count, plural, =0{no cats} =1{1 cat} other{{count} cats}} !",
}
Text(AppLocalizations.of(context)!.nCats(0)) // no cats !
Text(AppLocalizations.of(context)!.nCats(1)) // 1 cat !
Text(AppLocalizations.of(context)!.nCats(2)) // 2 cats !
類似複數形式,我們也可以基於 String
佔位符選擇「值」。這常用來支援性別區分的語言例如法文、西班牙文
{
"greeting": "{gender, select, male{歡迎先生} female{歡迎女士} other{歡迎}}"
}
Text(AppLocalizations.of(context)!.greeting('male')) // 歡迎先生
Text(AppLocalizations.of(context)!.greeting('female')) // 歡迎女士
注意,使用 select
語句時,參數和實際值之間是區分大小寫。也就是說,AppLocalizations.of(context)!.greeting("Male")
為「other」情況,並傳回「歡迎」。
有時候,你需要將 {
和 }
這樣的符號當作普通字元來使用 。為了避免符號被錯誤解析,這個時候可以在 l10n.yaml
增加設定:
use-escaping: true
此時解析器會忽略任何被使用單引號包起來的字串。此時要使用一般單引號需要連續兩個 ''
{
"helloWorld": "Hello! '{Isn''t}' this a wonderful day?"
}
輸出:Hello! {Isn't} this a wonderful day?
數字包含表示貨幣的數字,在不同地區中顯示方式也非常不同。flutter_localizations
的生成工具使用 intl
中的 NumberFormat
類別進行基於本地的格式化。
int
double
number
型別可以使用 NumberFormat
任意的建構子函式。這裡以數字 1200000 為例展示不同的輸出結果:
格式方法 | 描述 | 輸出範例 (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" |
上面表格中標註 *
的,表示提供可選或具名參數。這些參數可以設定佔位符的 optionalParameters
物件值。例如, compactCurrency
指定可選的 decimalDigits
參數
"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
中的 DateFormat
來格式化。支援 41 種格式化,由 DateFormat
工廠建構子名稱識別。在下面的例子中,出現在 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 專案的 Info.plist
檔案中應會定義關鍵的應用程式設定資料,包含支援的語系。如果要設定支援的語系須依照下面步驟設定:
ios/Runner.xcworkspace
Runner
下開啟 Info.plist
+
或者有件 「Add Row」supportedLocales
一致一些語系具有多種變體,除了語系代碼之後還需要其他設定來正確區分。
例如:中文就包含多種變體需要額外指定語言代碼、腳本代碼和國家代碼
supportedLocales: [
const Locale.fromSubtags(languageCode: 'zh'), // 通用中文 'zh'
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'), // 通用簡體中文 'zh_Hans'
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), // 通用繁體中文 'zh_Hant'
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans', countryCode: 'CN'), // 中國大陸簡體中文 'zh_Hans_CN'
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'TW'), // 台灣繁體中文 'zh_Hant_TW'
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant', countryCode: 'HK'), // 香港繁體中文 'zh_Hant_HK'
],
這種完整的設定確保我們的應用程式能夠區分所有組合,如果使用者首選的語系未被指定,Flutter 會選擇最接近的,但可能和使用者期望的不同。
除了我們舉例的中文,其他如法語 fr_FR
fr_CA
也應該區分。
Locale
類別可以識別使用者的語系。行動裝置可以為全部應用程式設定語系,通常使用系統設定選單來設定。
支援多個國家的應用程式通常會根據使用者的語言設定,自動顯示對應語言的內容。
Localizations
組件則是為子組件定義了支援的語言設定和翻譯資源。WidgetsApp
組件會建立一個 Localizations
組件,並在系統語言變更時重新建立它。
你可以使用下面程式碼查詢應用程式目前的語言設定:
Locale currentLocale = Localizations.localeOf(context);
儘管 flutter_localizations
庫目前支援 100 多種語言和語言變體,但預設只提供英語翻譯。開發者需要決定具體支援哪些語言。
MaterialApp
的 supportedLocales
參數限制了語系的支援。當使用者變更裝置的語系時,應用程式的 Localizations
組件只有在語言有設定在列表中,才會變更。如果找不到與和裝置設定語言完全匹配的選項,則會使用第一個具有匹配 languageCode
的支援語言。如果這也失敗了,則使用 supportedLocales
列表的第一個元素。
如果應用程式想要使用不同的「語言解析」方法,可以提供一個 localeResolutionCallback
。例如,要讓你的應用無條件接受用戶選擇的任何語言
MaterialApp(
localeResolutionCallback: (locale, supportedLocales) {
return locale;
},
)
l10n.yaml
檔案可以設定 gen-l10n
工具
要取得完整的設定選項,可以執行 flutter gen-l10n --help
選項 | 說明 |
---|---|
arb-dir |
模板和翻譯 arb 檔案所在目錄預設為 lib/l10n |
output-dir |
生成本地化類別的目錄。只有在當你想在專案其他地方產生類別才需要設定。且還需要設定 synthetic-package 為 false。匯入時除了目錄還需要搭配 output-localization-file 的檔名。如果沒有設定預設跟 arb-dir 一樣 |
template-arb-file |
模板 arb 檔案作為產生 Dart 字典檔的基礎。預設為 app_en.arb |
output-localization-file |
輸出 delegate 類別的檔名,預設為 app_localizations.dart |
untranslated-messages-file |
列出尚未翻譯檔案資訊。這個選項會在指定路徑建立 JSON 檔案內容就是未翻譯檔案資訊。如果沒有設定,則在指令 Console 介面輸出未翻譯訊息的摘要。 |
output-class |
設定 Localization 和 Localization Delegate 的 Dart 類別名稱。預設為 AppLocalizations 。 |
preferred-supported-locales |
偏好支援的語系列表。預設,工具會依據字母順序產生語系列表,使用此設定預設語系,例如 en_US 可以在裝置支援的情況下預設為美式英文。 |
header |
預先生成到 Dart 本地化檔案的開頭加入內容。通常會用來加入版權聲明。例如 /// Copyright @ xxxx 所有產生的檔案都會加入。另外還可以使用 header-file 選項來傳入比較長的內容。 |
header-file |
如同上面說明,可以將內容存放在一個檔案。然後指定這個檔案的內容會加入檔頭。 |
[no-]use-deferred-loading |
設定是否支援延遲匯入,支援 Flutter Web 延遲載入。這可以有效減少 web 初始化時載入的 JavaScript 的大小 |
gen-inputs-and-outputs-list |
當設定時,工具會產生一個 JSON 檔案,包含工具的輸入輸出,預設 gen_l10n_inputs_and_outputs.json 。這可以用來追蹤專案中最新產生的檔案。Flutter 工具會使用此檔案來確認 Hot Relaod 期間何時呼叫 gen_l10n 。此選項的值為 JSON 的目錄。當為 null 時,不會生成。 |
synthetic-package |
決定生成檔案時事合成還是在指定路徑 |
project-dir |
設定時,工具使用此選項的值作為根目錄路徑 |
[no-]required-resource-attributes |
全部資源 ID 須包含對應屬性。預設簡單的訊息不會需要額外的資訊,但推薦使用。 |
{
"welcomeMessage": "歡迎使用我們的應用!",
"@welcomeMessage": {
"description": "顯示在應用首頁的歡迎文字"
}
}
選項 | 說明 |
---|---|
[no-]nullable-getter |
設定 Localizations 類別的 getter 是否 nullable。預設是 nullable |
[no-]format |
設定時,生成檔案後要執行 dart format 指令。簡單說就是要不要自動為生成的檔案格式化。 |
use-escaping |
是否支援單引號脱逸語法 |
[no-]suppress-warnings |
是否關閉警告訊息的輸出 |
下面我們會說明 Flutter 支援多語系的運作機制。如果你預計會支援自己自訂的字典檔下面的內容可能對你有幫助。
Localizations
組件是用來載入以及搜尋本地化資源的物件。應用程式通常使用 Localizations.of(context, type)
來取得這些物件。如果裝置變更語系,Localizations
組件會自動載入新的語系資源並重新渲染使用這些值的組件。這是因為 Localizations
的運作方式類似於 InheritedWidget
。當 build
函式引用 InheritedWidget
時,就會建立一個依賴關係。當 InheritedWidget
改變時(例如 Localizations
組件的語言設定變更),所有依賴它的組件都會重新渲染。
本地化資源使用 Localizations
組件的 LocalizationsDelegate
列表載入。每一個 delegate 必須定義非同步的 load()
方法生成本地化資源集合的物件。通常,每一個值都會定義一個方法。
在大型應用程式中,不同模組或套件可能封裝自己的本地化資源。這也是為何 Localizations
組件需要管理物件表,每一個 LocalizationsDelegate
一個表格。
要取得由 LocalizationsDelegate
的 load
方法生成的物件,需要指定 BuildContext
和物件型別
舉例來說 Material 組件的本地化字串,定義在 MaterialLocalizations
類別中。這個類別由 MaterialApp
類別的 LocalizationDelegate
建立。可以使用 Localizations.of()
取得。
Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
由於 Localizations.of()
使用頻繁,因此 MaterialLocalizations
類別提供了方便簡短的語法:
static MaterialLocalizations of(BuildContext context) {
return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
}
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
除了上面通用的方式取得語系物件外,通常支援多語系的應用程式會封裝自己的語系資源。簡單的說下面範例介紹了自訂類似 MaterialLocalizations
的類別。
這個範例使用 intl
套件提供的 API 和工具。下面「本地化資源替代類別」會介紹其他非 intl
套件的方式。
DemoLocalizations
類別包含了應用程式使用的內容翻譯。通過 initializeMessages()
使用 intl
的功能生成。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:intl/intl.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 localName = Intl.cannonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
return DemoLocalizations(localeName);
});
}
static DemoLocalizations of(BuildContext context) {
return Localizations.of<DemoLocalizations>(context, DemoLocalizations)!;
}
String get title {
return Intl.message(
'Hello World',
name: 'title',
desc: 'Title for the Demo application',
locale: localeName,
);
}
}
intl
套件會導入一個自動生成包含所有翻譯文字的檔案,這個目錄提供了 initializeMessage()
和 Intl.message()
。簡單來說,DemoLocalizations
類別定義了全部所需的文字,intl
工具會掃描這個類別,找出全部需要翻譯的文字,進而生成字典檔。當使用 initializeMessages()
時會載入翻譯。
彙整來說:
arb
: 其他語言或框架中的字典檔,各語言的翻譯內容Intl.message
:標記須翻譯的「鍵」,去 arb 查對照翻譯Localizations.of
取得當前語系下對應翻譯的物件Intl.message
或 Localizations.of
Intl 負責提供字符串轉換,Localizations 管理組件結構,處理動態語言切換和相應的 UI 更新。
如果你的應用程式要加入 GlobalMaterialLocalizations
不支援的語系,則需要額外的處理。須為該新語言提供大約 70 個翻譯還有語言的日期格式等。下面示範如何新增支援挪威尼諾斯克語(Norwegian Nynorsk)。
一個新的 GlobalMaterialLocalizations
子類別定義 Material 函式庫需要的翻譯。LocalizationsDelegate
的子類別,該子類作為 GlobalMaterialLocalizations
子類的工廠。
具體實作請參考範例和Material and Cupertino Libraries Localizations。
本節將介紹不同實現多語系的方式。
上面的範例通過 Intl
套件,為了簡化或者可能為了與其他 i18n 框架整合,你可以選擇自己的方法來管理本地化的值。
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']!;
}
}
Dart intl 工具是處理多語言翻譯的強大助手。它能夠從我們的程式碼中識別並提取需要翻譯的文本。
在開發初期,我們可以專注於功能實現,使用單一語言(通常是英文)編寫界面文本。例如,直接使用 Text('Hello')
或 Text('Welcome to my app')
。
當需要支援多語言時,我們需要稍作調整,將普通的文本替換為 Intl 的特殊語法。例如,將 Text('Hello')
改寫為 Text(Intl.message('Hello', name: 'helloText', desc: 'Greeting'))
。
接下來,我們使用 extract_to_arb
命令掃描代碼,提取所有使用 Intl.message()
的文本,並生成一個包含所有待翻譯文本的 ARB 文件。
流程主要包含兩個步驟:
提取需要翻譯的文本,例如為我們的 lib/main.dart
提取:
$ dart run intl_translation:extract_to_arb --output-dir=lib/l10n lib/main.dart
此命令會從 main.dart
中提取所有標記的文本,並在 lib/l10n
目錄下生成 ARB 文件。
為每個語系生成 Dart 文件
$ dart run intl_translation:generate_from_arb \
--output-dir=lib/l10n --no-use-deferred-loading \
lib/main.dart lib/l10n/intl_*.arb
這個步驟會為每個 intl_<locale>.arb
文件生成對應的 messages_<locale>.dart
文件。同時會生成 messages_all.dart
,它導入了所有語言的消息文件。
總結來說 Flutter 多語系支援的核心概念:
flutter_localizations
包來啟用多語系支援。MaterialApp
或 CupertinoApp
中設置 localizationsDelegates
和 supportedLocales
。flutter gen-l10n
指令生成 Dart 程式碼。AppLocalizations.of(context).messageKey
來取得翻譯文本。LocalizationsDelegate
。最後我們介紹了 Dart intl 工具,協助我們更有效率的開發。