啟動時通過Flutter framework層的ui.window獲取到當前系統的local,根據MaterialApp用戶配置的locale進行mapping,初始化
Localizations
,並加載LocalizationDelegate
的load
方法(需要在此方法中讀取本地對應的locale的翻譯),然后將LocalizationDelegate
所代理的具體的Localizations
和內置的_LocalizationsScope
關聯,這樣就通過內部的InheritedWidget
獲取到當前locale
對應的全部翻譯數據。
關鍵步驟
- Locale: app啟動時通過flutter engine 的回調獲取到當前系統的locale,同時結合MaterialApp設置supported的locale以及localeCallback回調函數,確定當前優先使用哪一個locale
- 然后通過根視圖掛在的子Localizations初始化方法對設置的localizationDelegates一次調用,加載他們的load方法,通過實現代理的load方法獲取本地翻譯文件
- 通過自定義
XXXLocalizations
繼承WidgetsLocalizations,並在delegate執行load方法時初始化XXXLocalizations
加載本地翻譯文件。 - 然后就可以通過
Localizations
內置的_LocalizationsScope
獲取XXXLocalizations
時例子 - intl包的使用,管理翻譯文件。
Locale簡介
代表了本地區域和語言,組成格式如下
{languageCode}-{scriptCode/optional}-countryCode
例: zh-Hans-CN, zh-Hants-TW,zh-CN,zh-EN
const Locale( this._languageCode, [
this._countryCode,
])
const Locale.fromSubtags({
String languageCode = 'und',
this.scriptCode,
String countryCode,
})
//拼接語言
String _rawToString(String separator) {
final StringBuffer out = StringBuffer(languageCode);
if (scriptCode != null && scriptCode.isNotEmpty)
out.write('$separator$scriptCode');
if (_countryCode != null && _countryCode.isNotEmpty)
out.write('$separator$countryCode');
return out.toString();
}
Locale配置
在
MaterialApp
配置合適的Locale
//case1: 指定app支持的語言 MaterialApp(
...
supportedLocales: [
const Locale.fromSubtags(languageCode: 'zh'), // generic Chinese 'zh'
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hans'), // generic simplified Chinese 'zh_Hans'
const Locale.fromSubtags(languageCode: 'zh', scriptCode: 'Hant'), // generic traditional Chinese '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'
],
)
//case2: 指定app支持的語言和區域
MaterialApp(
localizationsDelegates: [
// ... app-specific localization delegate[s] here
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [
const Locale('en', ''), // English, no country code
const Locale('he', ''), // Hebrew, no country code
const Locale.fromSubtags(languageCode: 'zh'), // Chinese *See Advanced Locales below*
// ... other locales the app supports
],
// ...
)
獲取系統的Locale
- 在app啟動之后,根視圖創建的時候會通過dart.ui的回調獲取當前的
locale
- 通過在MaterialApp傳入的數據可以對locale進行替換和過濾
_WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver
@override
void initState() {
super.initState();
_updateNavigator();
_locale = _resolveLocales(WidgetsBinding.instance.window.locales, widget.supportedLocales);
WidgetsBinding.instance.addObserver(this);
}
//此處3個屬性就是我們在MaterialApp里面所賦值的,在執行`_resolveLocales`方法時會提供方法對Locale重新處理
if (widget.localeListResolutionCallback != null) {
...
if (widget.localeResolutionCallback != null) {
...
widget.supportedLocales,
創建Localizations提供翻譯數據
創建
Localizations
類,申明翻譯的字符串屬性,可參考系統默認的localizations.dart
定義CupertinoLocalizations (localizations.dart) DefaultCupertinoLocalizations (localizations.dart)
localizaitions的抽象層定義:
定義字符串屬性,方便快速定義
特殊字符串處理,傳參類型, 貨幣,時間,格式化字符串
命名規范可以參照MaterialLocalizations的格式,最后面一般加上控件名字abstract class MaterialLocalizations { //下面為幾種不同類型的字符串定義
String get openAppDrawerTooltip;
String tabLabel({ int tabIndex, int tabCount });
String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat = false
String get showAccountsLabel;
//提供Context獲取方法,get到當前可以提供翻譯的具體實例類
static MaterialLocalizations of(BuildContext context) {
return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations);
}
}localizaitions的具體實現類定義
class DefaultMaterialLocalizations implements MaterialLocalizations { //存儲了部分常用字段
static const List<String> _weekdays = <String>[
//提供了需要格式化的方法,實際使用中根據項目需求定義,常用的有`時間/貨幣/大小寫/數字拼接`
int _getDaysInMonth(int year, int month) {
@override
String formatHour(TimeOfDay timeOfDay, { bool alwaysUse24HourFormat = false }
String get openAppDrawerTooltip => 'Open navigation menu';
//項目里面會有很多翻譯,在實際使用過程中,通常是在系統delegeate調用load時,從本地加載數據
static Future<MaterialLocalizations> load(Locale locale) {
return SynchronousFuture<MaterialLocalizations>(const DefaultMaterialLocalizations());
}
//上面的方法可以加工改成如下
static Future<MaterialLocalizations> load(Locale locale) {
final localizations = DefaultMaterialLocalizations();
return SynchronousFuture<MaterialLocalizations>(localizations.loadLocalizationsJsonFromCache());
}
//為系統提供一個delegate,注冊locale變化事件,通過delegate.load觸發this.load,重新加載翻譯文件
static const LocalizationsDelegate<MaterialLocalizations> delegate = _MaterialLocalizationsDelegate();
}
創建localizationsDelegate
監聽locale變更事件,讀取本地翻譯文件到內存中,具體實現如下,主要提供了Locale的檢測,以及加載提供翻譯數據的實例類.
class _MaterialLocalizationsDelegate extends LocalizationsDelegate<MaterialLocalizations> { const _MaterialLocalizationsDelegate();
@override
bool isSupported(Locale locale) => locale.languageCode == 'en';
@override
Future<MaterialLocalizations> load(Locale locale) => DefaultMaterialLocalizations.load(locale);
@override
bool shouldReload(_MaterialLocalizationsDelegate old) => false;
@override
String toString() => 'DefaultMaterialLocalizations.delegate(en_US)';
}在啟動時,將delegate傳遞給MaterialApp,MaterialApp在locale信息初始化之后會逐個調用delegate的load方法,將對應local的字符串加載到內存中.
class _WidgetsAppState ...
assert(_debugCheckLocalizations(appLocale));
Widget build(BuildContext context) {
...
child: _MediaQueryFromWindow(
child: Localizations(
locale: appLocale,
delegates: _localizationsDelegates.toList(),
child: title,
),
),
),
),
);
}在locale變更時時如何觸發
localizationsDelegates
依次執行load
方法的?class Localizations extends StatefulWidget { Localizations({
Key key,
@required this.locale,
@required this.delegates,
this.child,
})
final List<LocalizationsDelegate<dynamic>> delegates;
final Widget child;
//獲取當前應用設置的locale
static Locale localeOf(BuildContext context, { bool nullOk = false }) {
...
final _LocalizationsScope scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
return scope.localizationsState.locale;
}
//獲取當前系統設置的localizations的delegate
static List<LocalizationsDelegate<dynamic>> _delegatesOf(BuildContext context) {
final _LocalizationsScope scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
return List<LocalizationsDelegate<dynamic>>.from(scope.localizationsState.widget.delegates);
}
//獲取localizations實例子類
static T of<T>(BuildContext context, Type type) {
assert(context != null);
assert(type != null);
final _LocalizationsScope scope = context.dependOnInheritedWidgetOfExactType<_LocalizationsScope>();
return scope?.localizationsState?.resourcesFor<T>(type);
}
@override
_LocalizationsState createState() => _LocalizationsState();
...
}
class _LocalizationsState extends State<Localizations> {
final GlobalKey _localizedResourcesScopeKey = GlobalKey();
Map<Type, dynamic> _typeToResources = <Type, dynamic>{};
Locale get locale => _locale;
@override
void initState() {
super.initState();
//在`LocalizationsState`初始化的時候開始加載delegate的load方法
load(widget.locale);
}
//更新變動的localizationDelegate
bool _anyDelegatesShouldReload(Localizations old) {
...
@override
void didUpdateWidget(Localizations old) {
//接上文的initState,獲取前的所有delegates,通過`_loadAll`異步加載所有的locale信息
void load(Locale locale) {
final Iterable<LocalizationsDelegate<dynamic>> delegates = widget.delegates;
Map<Type, dynamic> typeToResources;
final Future<Map<Type, dynamic>> typeToResourcesFuture = _loadAll(locale, delegates)
.then<Map<Type, dynamic>>((Map<Type, dynamic> value) {
return typeToResources = value;
});
if (typeToResources != null) {
_typeToResources = typeToResources;
_locale = locale;
} else {
//通知flutter engine延遲加載第一幀,上面的異步回調,if同步判定,對`typeToResourcesFuture`重新訂閱,直到翻譯加載完成。(基於這個原理我突然想到了很多優化方案,比如先加載啟動所需要的部分翻譯,在轉圈的時候再加載其余部分翻譯)
RendererBinding.instance.deferFirstFrame();
typeToResourcesFuture.then<void>((Map<Type, dynamic> value) {
if (mounted) {
setState(() {
_typeToResources = value;
_locale = locale;
});
}
RendererBinding.instance.allowFirstFrame();
});
}
}
T resourcesFor<T>(Type type) {
assert(type != null);
final T resources = _typeToResources[type] as T;
return resources;
}
//這里使用了as強轉,所以我們使用的Localizations類需要實現於它,定義文本方向
TextDirection get _textDirection {
final WidgetsLocalizations resources = _typeToResources[WidgetsLocalizations] as WidgetsLocalizations;
assert(resources != null);
return resources.textDirection;
}
@override
Widget build(BuildContext context) {
if (_locale == null)
return Container();
return Semantics(
textDirection: _textDirection,
//_LocalizationsScope為InheritedWidget傳遞當前的state數據和_typeToResources,這樣子類就可以通過inheritedXXXOfExtract() 方法獲取到數據了
child: _LocalizationsScope(
key: _localizedResourcesScopeKey,
locale: _locale,
localizationsState: this,
typeToResources: _typeToResources,
child: Directionality(
textDirection: _textDirection,
child: widget.child,
),
),
);
}
}_typeToResources具體實現
在LocalizationsDelegateType實現 get type => T;這里采用鍵值對的方式將T和delegate實例子保存起來 Future<Map<Type, dynamic>> _loadAll(Locale locale, Iterable<LocalizationsDelegate<dynamic>> allDelegates) { //1. 保存 Set<Type> types, List<LocalizationsDelegate<dynamic>> delegates
final Map<Type, dynamic> output = <Type, dynamic>{};
List<_Pending> pendingList;
final Set<Type> types = <Type>{};
final List<LocalizationsDelegate<dynamic>> delegates = <LocalizationsDelegate<dynamic>>[];
for (final LocalizationsDelegate<dynamic> delegate in allDelegates) {
if (!types.contains(delegate.type) && delegate.isSupported(locale)) {
//在LocalizationsDelegate<T>Type實現 get type => T;
types.add(delegate.type);
delegates.add(delegate);
}
}
//將types和delegate一一對應,並保證delegate的load方法執行完畢
for (final LocalizationsDelegate<dynamic> delegate in delegates) {
final Future<dynamic> inputValue = delegate.load(locale);
dynamic completedValue;
final Future<dynamic> futureValue = inputValue.then<dynamic>((dynamic value) {
return completedValue = value;
});
//加這個主要是擔心執行太快,沒進到pending隊列里,漏掉部分數據
if (completedValue != null) { // inputValue was a SynchronousFuture
final Type type = delegate.type;
assert(!output.containsKey(type));
output[type] = completedValue;
} else {
pendingList ??= <_Pending>[];
pendingList.add(_Pending(delegate, futureValue));
}
}
//當所有delegate load的數據加載完畢后同步返回{DelegateType,DelegateInstance}信息
if (pendingList == null)
return SynchronousFuture<Map<Type, dynamic>>(output);
//同步執行每一個feature對象,一次mapping到output字典中.
return Future.wait<dynamic>(pendingList.map<Future<dynamic>>((_Pending p) => p.futureValue))
.then<Map<Type, dynamic>>((List<dynamic> values) {
assert(values.length == pendingList.length);
for (int i = 0; i < values.length; i += 1) {
final Type type = pendingList[i].delegate.type;
assert(!output.containsKey(type));
output[type] = values[i];
}
return output;
});
}
小結
app啟動時候通過flutter engine的回調事件獲取locale,並根據MaterialApp設置的locale信息和回調方法對系統的locale進行加工和過濾處理得到當前app使用的locale
根視圖Localizaitons接受到MaterialApp傳遞的delegates和
_WidgetsApppState
通過上面步驟1獲取的locale初始化在
LocalizaitonsState
初始化和變更時進行檢測delegates是否更新,並加載delegates,遍歷的執行load方法加載本地翻譯,同時將 delegate的type和delegate的value以鍵值對的形式保存在字典中,(這里的value
是 delegate通過load方法返回的對象.type是delegate的屬性,而這個對象就是我們的XXXLocalizaitons的實例類)在
LocalizaitonsState
的build(BuildContext context)
方法中,通過內置的_LocalizationsScope
傳遞LocalizaitonsState
和他對應的{delegateType:delegateValue}
,這樣做其實就是為了將數據數據的邏輯封裝太Localizations中,方便開發者調用。
管理本地的翻譯文件
- 上面部分主要是localizations的觸發條件及調用,下部分則是本地翻譯文件的管理
- 翻譯文件都會采用比較清量級的文件保存,這樣可以節省空間
- 為了便於管理和讀取,每一種語言單獨放一個文件
- 為了便於代碼書寫,需要將鍵值對保存的語言提取出來生成具體的dart類,通常的做法就是運用腳本工具分析后寫入到模版文件中
- 為了便於閱讀和查找,需要將翻譯文件按照某種規則排序,比如首字母排序
- 為了便於理解翻譯,需要將翻譯加上對應的描述
- 當然還有很多需要考慮的,者取決於項目的復雜程度是否有必要
Intl庫管理翻譯
它是flutter官方推薦的翻譯管理工具,其內部維護了locale
翻譯文件的mapping
表,同時也提供了一些常用的格式化翻譯工具,省去了大量的步驟,非常完善的輪子拿來即用。