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

  • 相关阅读:
    1074. Reversing Linked List (25)
    1056. Mice and Rice (25)
    1051. Pop Sequence (25)
    1001. A+B Format (20)
    1048. 数字加密(20)
    1073. Scientific Notation (20)
    1061. Dating (20)
    1009. 说反话 (20)
    MyBatis学习总结(8)——Mybatis3.x与Spring4.x整合
    MyBatis学习总结(8)——Mybatis3.x与Spring4.x整合
  • 原文地址:https://www.cnblogs.com/wwoo/p/flutter-localizations-shi-xian-yuan-li.html
Copyright © 2020-2023  润新知