做爲一個 Android 開發者,Flutter 上來就讓我把各種字符串寫在 widget 裏,其實我內心是拒絕的。硬編碼是不可能硬編碼的。國際化又不會,就是隻能去看看文檔,才能學點新姿式這樣子。看了文檔以後,以爲國際化這部分,仍是有點麻煩的,我以爲有必要拎出來單獨寫寫。git
我的但願能把應用的字符串資源獨立出來,以方便管理。至於支持多語言這種,反而是順帶完成的結果。本文以實用優先,由於我認爲這部份內容是每一個應用都須要使用的。github
首先簡單認識一下 Flutter 國際化相關的知識點。json
添加 flutter_localizations
依賴,讓 Flutter 知道咱們須要使用國際化相關的包。Flutter 自帶的 widget 中,也用到了一些字符串資源,好比,showSearch()
方法打開的搜索欄提示。而這個包能夠提供英文以外的,被 Flutter 內部默認使用的國際化字符串資源。app
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
複製代碼
而後在建立 App 時,加入 LocalizationsDelegate
,國際化的內容就由這些類來提供。GlobalMaterialLocalizations.delegate
提供了 Material 組件庫所使用的字符串資源;GlobalWidgetsLocalizations.delegate
則定義了在當前的語言中,文字默認的排列方向。less
以後咱們定義了本身的國際化內容後,也須要加入到這個列表的頭部。ide
還要聲明要支持什麼語言,supportedLocales
這裏添加了英文和中文兩種。若是說用戶的語言不在這個列表內,則會默認使用列表第一項指定的語言。假如你對這個規則不滿意,可使用 localeResolutionCallback
參數來自定義本身想要的規則。函數
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
class ThisApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
localizationsDelegates: [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'),
const Locale('zh', 'CN'),
],
title: 'App Title',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: HomePage(),
);
}
}
複製代碼
如今,咱們將一些自帶的國際化資源加入到了應用中,Flutter 自身已經可以使用它們了。但咱們要怎麼使用它們呢?工具
經過 MaterialLocalizations.of(context)
獲取到 MaterialLocalizations
的實例,而後訪問裏面的字符串。好比上面的 title 一行,能夠替換爲:優化
onGenerateTitle: (context) => MaterialLocalizations.of(context).closeButtonLabel,
複製代碼
注意這裏將 title 替換成 onGenerateTitle 了,由於此時還在初始化 App 中,沒法獲取到 context,更沒法經過 context 獲取字符串了。ui
如今來考慮怎麼將咱們本身的國際化加入到其中。也就是,須要在 localizationsDelegates
中加入本身的 LocalizationsDelegate
。
查看文檔,LocalizationsDelegate
須要一個泛型參數。參考官方的文檔,可知這裏指定的類型就是咱們存放字符串的類。在這裏,有兩種選擇:第一是基於 map 的,很是簡單的實現;第二個則是經過 Dart 語言中專門負責國際化的 intl 包來實現。接下來咱們按次來看看。
class SimpleLocalizations {
SimpleLocalizations(this.locale);
final Locale locale;
static SimpleLocalizations of(BuildContext context) {
return Localizations.of<SimpleLocalizations>(context, SimpleLocalizations);
}
static Map<String, Map<String, String>> _localizedValues = {
'en': {
'app_name': 'App Name',
'hello_world': 'Hello World',
},
'zh': {
'app_name': '應用名',
'hello_world': '你好世界',
},
};
Map<String, String> get _stringMap {
return _localizedValues[locale.languageCode];
}
String get helloWorld {
return _stringMap['hello_world'];
}
String get appName {
return _stringMap['app_name'];
}
}
複製代碼
從上面的代碼能夠看到,這種方法的原理很是簡單,就是將全部字符串放進 map,而後經過應用的 Locale
來取出對應語言的字符串。使用時,就是 SimpleLocalizations.of(context).helloWorld
這樣來引用字符串。
其對應的 LocalizationsDelegate
以下:
class SimpleLocalizationsDelegate extends LocalizationsDelegate<SimpleLocalizations> {
const SimpleLocalizationsDelegate();
@override
bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);
@override
Future<SimpleLocalizations> load(Locale locale) {
return SynchronousFuture<SimpleLocalizations>(SimpleLocalizations(locale));
}
@override
bool shouldReload(SimpleLocalizationsDelegate old) => false;
}
複製代碼
只要將這個 SimpleLocalizationsDelegate
加入到上面的 delegates 列表中,國際化就算完成了。
回看一下整個流程,並不算複雜,須要經手部分的原理也很是簡單,只是一個 map 的使用。使用這個方法能夠將整個應用的字符串都集中到一塊兒管理。可是,維護起來仍是很不方便。
接下來看看基於 intl 包的實現方法是怎麼樣的。
第一步,添加依賴:
dependencies:
intl: ^0.15.7
dev_dependencies:
intl_translation: ^0.17.3
複製代碼
經過查看官方的例子,能夠知道 Intl.message()
方法是咱們管理字符串的關鍵。因而去看相關的文檔,會發現——嗯,沒有卵用(甚至沒解釋每一個參數有什麼做用)。
接下來仍是同樣添加一個跟 SimpleLocalizations
差很少類:
class IntlLocalizations {
static IntlLocalizations of(BuildContext context) {
return Localizations.of<IntlLocalizations>(context, IntlLocalizations);
}
String get appName {
return Intl.message('App Name');
}
String get helloWorld {
return Intl.message('Hello world');
}
}
複製代碼
從命令行中運行 flutter pub pub run intl_translation:extract_to_arb --output-dir=你想要的輸出目錄 IntlLocalizations所在文件
。這一操做將會在指定目錄裏生成一個名爲 intl_messages.arb 的文件,內容大體以下:
{
"@@last_modified": "2019-02-17T15:57:00.554988",
"App Name": "App Name",
"@App Name": {
"type": "text",
"placeholders": {}
},
"Hello world": "Hello world",
"@Hello world": {
"type": "text",
"placeholders": {}
}
}
複製代碼
將這個文件複製一份,命名爲 intl_en.arb,做爲英文版本使用。接着再複製一份,命名爲 intl_zh.arb 做爲中文版本使用。將 intl_zh.arb 的內容修改成對應中文的內容:
{
"@@last_modified": "2019-02-17T15:57:00.554988",
"App Name": "應用名",
"@App Name": {
"type": "text",
"placeholders": {}
},
"Hello world": "你好世界",
"@Hello world": {
"type": "text",
"placeholders": {}
}
}
複製代碼
若是須要其餘語言的版本,請自行添加並修改。
再來輸入一段長長的命令行:flutter pub pub run intl_translation:generate_from_arb --output-dir=輸出目錄 --no-use-deferred-loading IntlLocalizations所在文件 全部arb文件
。這樣會生成幾個 messages_
開頭的 dart 文件。能夠自行查看一下里面的內容,我如今的 Dart 水平還比較菜,就先不分析其中的原理了。
其中名爲 messages_all.dart 的文件裏,生成了 initializeMessages(String localeName)
這個方法,將會在下面的步驟中使用到。
在 IntlLocalizations
中添加以下的方法:
static Future<IntlLocalizations> load(Locale locale) {
final name =
locale.countryCode.isEmpty ? locale.languageCode : locale.toString();
final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName;
return IntlLocalizations();
});
}
複製代碼
IntlLocalizations
就準備完畢了。而後,開始實現 delegate,內容很簡單:
class IntlLocalizationsDelegate extends LocalizationsDelegate<IntlLocalizations> {
const IntlLocalizationsDelegate();
@override
bool isSupported(Locale locale) => ['en', 'zh'].contains(locale.languageCode);
@override
Future<IntlLocalizations> load(Locale locale) {
return IntlLocalizations.load(locale);
}
@override
bool shouldReload(IntlLocalizationsDelegate old) => false;
}
複製代碼
以後就是正常使用流程了,這裏不贅訴。回想整個流程,真正國際化的內容在 arb 文件中,對於集中管理字符串來講,比使用 map 仍是好一點。可是整個流程仍是顯得異常麻煩,尤爲是兩次長得過度的命令行,明顯應該由工具來改進。我相信 Flutter/Dart 團隊應該會在這一點上作出優化。
那麼,有沒有一款工具能夠解救咱們呢?您好,有的。
Android Studio(IDEA)上有一款名爲 flutter_i18n 的插件,能夠幫助簡化這個過程。其原理是經過 arb 文件來自動生成所須要的代碼。
插件的使用很是簡單,安裝後會出現一個新的按鈕。一旦你按下這個按鈕——boom——插件就會根據 res/values
文件夾(Android 開發者以爲很親切)中的 arb 文件,在 lib/generated
中生成 Dart 代碼。
那麼咱們的重心就放在了 arb 文件上。Arb 文件全稱是 Application Resource Bundle,是基於 JSON 的 balabala 接下去的我也不想接着說了,由於並不實用。仍是來看下 Flutter 國際化中切實相關的部分。
雖然咱們知道了 arb 文件是類 JSON 格式,但咱們還並不清楚文件裏具體須要什麼樣的內容。這裏咱們經過 Intl.message()
方法再從新認識一下。
String get appName {
return Intl.message(
'App Name',
desc: 'Name for the application',
name: 'IntlLocalizations_appName',
);
}
String hello(String name) {
return Intl.message(
'Hello $name',
name: 'IntlLocalizations_hello',
desc: 'Say hello to someone',
args: [name],
locale: 'en',
examples: const {'name': 'Someone'},
meaning: 'What is this?',
skip: false,
);
}
複製代碼
這裏有兩個更爲詳細的實現,其中 hello
方法將所有的參數都賦值了,以方便觀察經過 intl_translation
包處理後的 arb 文件會是什麼樣的。
不過這以前簡單介紹一下 Intl.message()
的部分參數。
name
參數必須與函數名一致,或者是類名_方法名
這個形式——建議使用後者避免衝突;args
就是重複一遍參數;name
和 args
能夠省略;desc
參數就是描述這個字符串的字符串,必須是一個字符串字面量;examples
是參數的示例;desc
和 examples
在運行時不會被使用,但會被提取出來做爲額外的信息提供給翻譯人員做爲參考;skip
若是爲 true,那麼這條記錄就不會被提取出來;而後咱們再運行一下那個很長的命令行,將其處理成 arb 文件看看:
{
"@@last_modified": "2019-02-18T21:31:28.750455",
"IntlLocalizations_appName": "App Name",
"@IntlLocalizations_appName": {
"description": "Name for the application",
"type": "text",
"placeholders": {}
},
"IntlLocalizations_hello": "Hello {name}",
"@IntlLocalizations_hello": {
"description": "Say hello to someone",
"type": "text",
"placeholders": {
"name": {
"example": "Someone"
}
}
}
}
複製代碼
首先,meaning
彷佛沒有用處。其核心就是 "IntlLocalizations_appName": "App Name"
這樣的一條一條的記錄。以 @ 開頭的部分,並不會真正在程序中使用,而是給翻譯人員做爲參考使用的。
這麼一來,咱們接下來就能夠在 res/values
文件夾中建立須要的 arb 文件了。這個插件還提供了快捷建立 arb 文件的功能,只須要在 res/values
目錄右鍵選擇 New -> Arb File 就能夠選擇這個 arb 文件的 locale 了。
須要注意的是,在這個插件中,若是字符串內須要包含變量,使用的語法是 $var_name
,而不是上面例子裏使用大括號的形式。
這裏我建立了兩個 arb 文件:
// strings_en.arb
{
"appName": "App Name",
"hello": "Hello $name"
}
// strings_zh_CN.arb
{
"appName": "應用名",
"hello": "你好${name}"
}
複製代碼
使用插件生成代碼後,將 delegate 加入到應用的列表中,使用時也只要直接利用 S
這個類名來引用就好:
MaterialApp(
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
],
supportedLocales: [
const Locale('en', 'US'),
const Locale('zh', 'CN'),
],
onGenerateTitle: (context) => S.of(context).appName,
...
複製代碼
使用了這個插件以後,國際化就算得上方便了。生成的代碼也能夠稍微看一眼,或許有你用獲得的其餘方法。
最後提醒一句,因爲生成代碼是由插件完成的,因此依賴中的 intl_translation
能夠刪掉了。
可能有的人會問,不使用 IDEA 的開發者,有沒有什麼更好的選擇呢?或許有。如今還有一個名爲 rosetta 的庫,致力於解決 Flutter 國際化太過複雜的問題。我嘗試過,但並無跑通正常的流程,沒法更多評價。有興趣的朋友能夠試試看。
到此,這篇指南就結束了,但願能對一些人有幫助。