启动时通过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
表,同时也提供了一些常用的格式化翻译工具,省去了大量的步骤,非常完善的轮子拿来即用。