作為一個 Android 開發者,Flutter 上來就讓我把各類字串寫在 widget 裡,其實我心裡是拒絕的。硬編碼是不可能硬編碼的。國際化又不會,就是隻能去看看文件,才能學點新姿勢這樣子。看了文件之後,覺得國際化這部分,還是有點麻煩的,我覺得有必要拎出來單獨寫寫。
個人希望能把應用的字串資源獨立出來,以方便管理。至於支援多語言這種,反而是順帶完成的結果。本文以實用優先,因為我認為這部分內容是每個應用都需要使用的。
切入點
首先簡單認識一下 Flutter 國際化相關的知識點。
新增 flutter_localizations
依賴,讓 Flutter 知道我們需要使用國際化相關的包。Flutter 自帶的 widget 中,也用到了一些字串資源,比如,showSearch()
方法開啟的搜尋欄提示。而這個包可以提供英文之外的,被 Flutter 內部預設使用的國際化字串資源。
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
複製程式碼
然後在建立 App 時,加入 LocalizationsDelegate
,國際化的內容就由這些類來提供。GlobalMaterialLocalizations.delegate
提供了 Material 元件庫所使用的字串資源;GlobalWidgetsLocalizations.delegate
則定義了在當前的語言中,文字預設的排列方向。
之後我們定義了自己的國際化內容後,也需要加入到這個列表的頭部。
還要宣告要支援什麼語言,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 獲取字串了。
自定義的國際化內容
現在來考慮怎麼將我們自己的國際化加入到其中。也就是,需要在 localizationsDelegates
中加入自己的 LocalizationsDelegate
。
檢視文件,LocalizationsDelegate
需要一個泛型引數。參考官方的文件,可知這裡指定的型別就是我們存放字串的類。在這裡,有兩種選擇:第一是基於 map 的,非常簡單的實現;第二個則是通過 Dart 語言中專門負責國際化的 intl 包來實現。接下來我們按次來看看。
基於 Map
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
接下來看看基於 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 團隊應該會在這一點上做出優化。
flutter_i18n
那麼,有沒有一款工具可以解救我們呢?您好,有的。
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 國際化太過複雜的問題。我嘗試過,但並沒有跑通正常的流程,無法更多評價。有興趣的朋友可以試試看。
到此,這篇指南就結束了,希望能對一些人有幫助。