(圖說:地方料理使用了各種當地風味的香料,影響了世代人的味覺與口感。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:
AppLocalizationsclass: 裡頭的 get 系列,在下一步驟將用來抽取出訊息字串,產出 ARB (Application Resource Bundle) 檔案。AppLocalizations 可以自己視專案需求自由創造。AppLocalizationsDelegateclass: 延伸自LocalizationsDelegatewidget,是給整個 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.dartmessage 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
