美文网首页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