美文网首页Flutter随笔
Flutter localizations备忘

Flutter localizations备忘

作者: 嘛尼嘛哄 | 来源:发表于2020-09-17 08:32 被阅读0次

    Flutter Localizations

    语言国际化,主要包括以下几个关键因子

    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
    • ege. 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 Usage1

    //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

    CupertinoLocalizations (localizations.dart)
        DefaultCupertinoLocalizations (localizations.dart)
    
    • localizaitons的抽象层定义:
      定义字符串属性,方便快速定义
      特殊字符串处理,传参类型, 货币,时间,格式化字符串
      命名规范可以参照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);
      }
    }
    
    • localizaitons的具体实现类定义
    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;
      }
      //获取当前系统设置的localizaitions的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具体实现
      在LocalizationsDelegate<T>Type实现 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官方推荐的翻译管理工具,这是它的描述

    This package provides internationalization and localization facilities, including message translation, plurals and genders, date/number formatting and parsing, and bidirectional text.
    

    相关文章

      网友评论

        本文标题:Flutter localizations备忘

        本文链接:https://www.haomeiwen.com/subject/dfekyktx.html