Flutter Localizations实现原理

启动时通过Flutter framework层的ui.window获取到当前系统的local,根据MaterialApp用户配置的locale进行mapping,初始化Localizations,并加载LocalizationDelegateload方法(需要在此方法中读取本地对应的locale的翻译),然后将LocalizationDelegate所代理的具体的Localizations和内置的_LocalizationsScope关联,这样就通过内部的InheritedWidget获取到当前locale对应的全部翻译数据。

关键步骤

  1. Locale: app启动时通过flutter engine 的回调获取到当前系统的locale,同时结合MaterialApp设置supported的locale以及localeCallback回调函数,确定当前优先使用哪一个locale
  2. 然后通过根视图挂在的子Localizations初始化方法对设置的localizationDelegates一次调用,加载他们的load方法,通过实现代理的load方法获取本地翻译文件
  3. 通过自定义XXXLocalizations继承WidgetsLocalizations,并在delegate执行load方法时初始化XXXLocalizations加载本地翻译文件。
  4. 然后就可以通过Localizations内置的_LocalizationsScope获取XXXLocalizations时例子
  5. 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;
    });
    }

小结

  1. app启动时候通过flutter engine的回调事件获取locale,并根据MaterialApp设置的locale信息和回调方法对系统的locale进行加工和过滤处理得到当前app使用的locale

  2. 根视图Localizaitons接受到MaterialApp传递的delegates和_WidgetsApppState通过上面步骤1获取的locale初始化

  3. LocalizaitonsState初始化和变更时进行检测delegates是否更新,并加载delegates,遍历的执行load方法加载本地翻译,同时将 delegate的type和delegate的value以键值对的形式保存在字典中,(这里的value是 delegate通过load方法返回的对象.type是delegate的属性,而这个对象就是我们的XXXLocalizaitons的实例类)

  4. LocalizaitonsStatebuild(BuildContext context)方法中,通过内置的_LocalizationsScope传递LocalizaitonsState和他对应的{delegateType:delegateValue},这样做其实就是为了将数据数据的逻辑封装太Localizations中,方便开发者调用。

管理本地的翻译文件

  • 上面部分主要是localizations的触发条件及调用,下部分则是本地翻译文件的管理
  • 翻译文件都会采用比较清量级的文件保存,这样可以节省空间
  • 为了便于管理和读取,每一种语言单独放一个文件
  • 为了便于代码书写,需要将键值对保存的语言提取出来生成具体的dart类,通常的做法就是运用脚本工具分析后写入到模版文件中
  • 为了便于阅读和查找,需要将翻译文件按照某种规则排序,比如首字母排序
  • 为了便于理解翻译,需要将翻译加上对应的描述
  • 当然还有很多需要考虑的,者取决于项目的复杂程度是否有必要

Intl库管理翻译

它是flutter官方推荐的翻译管理工具,其内部维护了locale翻译文件的mapping表,同时也提供了一些常用的格式化翻译工具,省去了大量的步骤,非常完善的轮子拿来即用。

posted @ 2020-10-09 21:51  阿甘左  阅读(1363)  评论(0编辑  收藏  举报