(圖說:地方料理使用了各種當地風味的香料,影響了世代人的味覺與口感。Photo by Christian Burri on Unsplash)
打算將手上的 Flutter app 做國際化支援,加入多國語系訊息字串們。
最先參考的是 Flutter 官方文件的 Internationalizing Flutter apps
這份文件,在裡頭打轉了好一陣子(年紀大了),摸索出結果之後趕緊記錄下來,難保下次要用的時候還記不記得 :p
內容大綱
簡介
官方文件提了兩個方法:
簡單法
:維護與管理上相對簡單(但也相對沒有作業流程上的彈性),文件上有提供簡單版的範例程式。大致上將範例程式的lib/main.dart
看過一次就開始開始依樣畫葫蘆對字串進行擴充。使用 intl 套件法
:文件裡頭有著詳盡的描述,且可以將 ARB 語系檔案分離出來,方便分發翻譯作業,較有作業流程上的彈性作業流程上的彈性。本篇記錄採用此方法。
紀錄一下此時開發機上的 Flutter 版本資訊備查:
Flutter 1.12.13+hotfix.9 • channel stable • https://github.com/flutter/flutter.git
Framework • revision f139b11009 (4 weeks ago) • 2020-03-30 13:57:30 -0700
Engine • revision af51afceb8
Tools • Dart 2.7.2
步驟一: flutter_localizations 套件
(突然覺得有點像在寫食譜 XD)
將幾個會用到的相依套件加入 pubspec.yaml
檔案中,記得存擋後跑一下 pub get
。
|
|
會用到的套件分別為:
flutter_localizations
函式庫使得 MaterialApp 可以載入 localizationsDelegates 與 supportedLocales。intl
套件由 Dart team 維護,提供了 internationalization (國際化) 與 localization (在地化) 的基礎。除了協助處理語言訊息字串之外,也包含了日期、數字等格式對應。intl_translation
套件由 Dart team 維護,是個工具套組,可以從 Dart 抽取出訊息(以便分工翻譯) 以及 從翻譯後訊息產出對應的 Dart 程式碼 使其他 Dart 程式可以運用這些翻譯訊息字串。
步驟二: 打造 AppLocalizations class
這個步驟,我們假設將所有在地化相關的檔案集中放在 Flutter 專案的 ./lib/l10n/
裡頭。
接著來編輯一個 ./lib/l10n/app_localizations.dart
檔案,存放兩個 classes:
AppLocalizations
class: 裡頭的 get 系列,在下一步驟將用來抽取出訊息字串,產出 ARB (Application Resource Bundle) 檔案。AppLocalizations 可以自己視專案需求自由創造。AppLocalizationsDelegate
class: 延伸自LocalizationsDelegate
widget,是給整個 Flutter app 存取 AppLocalizations class 的窗口。AppLocalizations 將訊息封裝起來,AppLocalizationsDelegate 提供這些訊息給 Flutter app 使用。
這個 AppLocalizations
的命名原則可以依照專案需求來做調整,例如 MyLocalizations
、ProjectNameLocalizations
、Project1Module2Localizations
。如此一來,未來可以彈性移動至其他專案使用。在本文簡單起見,先使用 AppLocalizations
為名稱。
./lib/l10n/app_localizations.dart:
|
|
此時引入的 messages_all.dart
尚未產生,將於下一步驟說明。
第 27 行的 AppLocalizations
class 有四個主要部分要留意:
load
- 用來載入指定 Locale 的訊息字串。
of
- 方便接下來在 Flutter app 各處叫用訊息字串,例如這樣
AppLocalizations.of(context).workout
。 - 參閱官方文件 Loading and retrieving localized values 段落有提到這個
Localizations.of()
。
- 方便接下來在 Flutter app 各處叫用訊息字串,例如這樣
get
系列- 列出可使用的語系資源(訊息字串)。
- 回傳
Intl.message()
以供Intl
套件工具找到並請initializeMessages()
載入對應的翻譯訊息字串。
initializeMessages
- 在第 6 行載入
messages_all.dart
message catalog 訊息目錄檔案,包含有initializeMessages()
初始化支援各個訊息字串檔案們。訊息目錄檔案由intl
套件工具掃描指定的 class 原始碼中有包含Intl.message()
者而產生。 - 參閱官方文件 Defining a class for the app’s localized resources 段落結尾提到。
- 在第 6 行載入
再來回頭說明,第 8 行的 AppLocalizationsDelegate
class,這裡有三個重點:
load
- 官方文件 Loading and retrieving localized values 段落有提到這個
load
方法,但是範例程式碼段落沒有提到使用 Intl 套件的 LocalizationsDelegate 該如何實作,只有在 An alternative class for the app’s localized resources 段落,提到如果使用簡單版(無使用 Intl 套件)的 LocalizationsDelegate 要改成回傳SynchronousFuture
。 - 所以在此就直接呼叫對應的 Localizations 的 load(),也就是
AppLocalizations.load()
。
- 官方文件 Loading and retrieving localized values 段落有提到這個
isSupported
- 檢查 Flutter app 層想要調用某個語系時,這個 LocalizationsDelegate 是否支援該語系。
- 這裡可以列出有支援的語系 list。
shouldReload
- 一般情況下回傳
false
即可。
- 一般情況下回傳
我選擇將 AppLocalizationsDelegate class 置於 AppLocalizations class 的上方,是考量到 AppLocalizations class 預計會隨著 get
越多而增加長度,但 AppLocalizationsDelegate class 同時也需要顧及列出有支援的語系清單 isSupported()
。
第二步驟快速總結一下,Flutter app 會透過 AppLocalizationsDelegate 叫用 AppLocalizations 裡頭的 get
們。
接下來,來看怎麼處理這些 get
們。
步驟三: 抽取出 ARB 檔案
ARB 全名 Application Resource Bundle,架構上是個 JSON 檔案,內容定義可以參閱這份規格文件。
我們先跑個指令,將 app_localizations.dart
內容(主要是 Intl.message()
)抽取出一個 .arb 檔案,再來細細觀察產出的 .arb 檔案內容。
|
|
- 如果你的目錄結構或檔案名稱不同,記得修改對應。
- 最後那個參數,記得對應到有包含
Intl.message()
的 Localizations class 檔案。不然待會兒下面就不用玩了。 - 這個指令會在
lib/l10n
目錄中產生一個intl_messages.arb
檔案。可以將這個檔案當作英文範本檔案。(建議選英文,你也可以選別的語言作為範本,視你的專案與場景而定,記得跟大家分享後續翻譯的成本有沒有差別。) - 我做了個 Makefile 方便執行指令(年紀大了,參數多的都記不得了),放在 Flutter 專案根目錄即可
make l10n-extract-to-arb
。
此時 l10n 目錄裡頭應該總共會有兩個檔案,且 app_localizations.dart
會出現錯誤說 messages_all.dart
不存在,無法載入。
接著,cp
複製 intl_messages.arb
成本範例的兩個語系檔案 intl_en.arb
和 intl.zh.arb
,你若有其他語系就繼續複製出對應的語系 intl_*.arb
檔案,然後即可送交翻譯(可能是翻譯公司、翻譯系統等等),以下假設完成翻譯後的結果:
./lib/l10n/intl_en.arb:
|
|
./lib/l10n/intl_zh.arb:
|
|
- 這兩個 .arb 檔案,我依照 ARB 規格,多加入了
@@locale
資料,在檔案第 2 行處,也可以使用en_US
,fr_CA
這類標註語言區域碼的形式。 - .arb 檔案中的
"Minute"
與"unitMinutes"
這兩個 key name 是想讓大家對照第二步驟 AppLocalizations 三種get
的不同寫法,會造成的不同結果。我自己的話,Intl.message
的name:
屬性會盡可能使用。
然後即可刪除這次的 intl_messages.arb
檔案。
步驟四: 從 ARB 檔案產出 Dart 檔案
接下來就是期盼已久的 messages_all.dart
檔案終於要生成了。
|
|
- 如果你的目錄結構或檔案名稱不同,記得修改對應。
- 會產生 n+1 個檔案,n 是你的
intl_*.arb
檔案數量。1 是那個失散多時,誒不是,是期盼已久的messages_all.dart
。 messages_all.dart
裡面有我們需要的initializeMessages()
讓 AppLocalizations 得以載入翻譯後的訊息字串們,並讓Intl.message()
完成對應查找。- 我做了個 Makefile 方便執行指令(年紀大了,參數多的都記不得了,是要講幾次 =.=),放在 Flutter 專案根目錄即可
make l10n-generate-from-arb
。
此時 app_localizations.dart
檔案的紅色提醒底線消失了 :)
苦力做完了(第一次的苦力做完了,就先不討論後續維護翻譯的辛苦),剩下讓 Flutter app 認得這個 LocalizationsDelegate。
步驟五: 載入 LocalizationsDelegate
此時回到官方文件的第一個段落 Setting up an internationalized app: the flutter_localizations package (請不要責怪官方文件為什麼如此安排先後順序,寫文件、寫書其中一個挑戰點,即是要講的事情就是如此多,A 先講 B 後講、或是 B 先講 A 後講,都會有人看不懂或抱怨,所以對知識分享來說,先寫下來再來逐步修正比較實在。)
|
|
官方文件開宗明義有先說,這是以 MaterialApp
為主的文件說明,若是以更底層的 WidgetsApp
實作,也可以參考相同類別與邏輯。
這裡兩個重點
localizationsDelegates
- 依照文件,建議載入順序是將我們自製的
AppLocalizationsDelegate()
置於Global*Localizations.delegate
之前。
- 依照文件,建議載入順序是將我們自製的
supportedLocales
- 是 MaterialApp 這一層有支援的 Locale 清單。
完成後,就可以開心地拿 AppLocalizations.of(context).workout
去各個地方使用囉 :)
- 記得
import 'package:my_project/l10n/app_localizations.dart';
結論
喜歡的話,請幫我按讚、分享、開啟小鈴鐺。誒不是 :p
快速總結:
- Flutter app 會透過 AppLocalizationsDelegate 叫用 AppLocalizations 裡頭的
get
們。 flutter pub run intl_translation:extract_to_arb
產生對應語系intl_*.arb
檔案們。flutter pub run intl_translation:generate_from_arb
產生期盼已久的messages_all.dart
。messages_all.dart
裡面有我們需要的initializeMessages()
讓 AppLocalizations 得以載入翻譯後的訊息字串們,並讓Intl.message()
完成對應查找。- 在
MaterialApp
載入 LocalizationsDelegate 。 - 開心使用
AppLocalizations.of(context).workout
,Flutter 多國語系不再是遙不可及的夢想!
結案 :)
最後,喜歡這篇文章的話,歡迎幫我按讚、分享、留言、開啟小鈴鐺 XDD