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具體實現
    在LocalizationsDelegate 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官方推薦的翻譯管理工具,其內部維護了locale翻譯文件的mapping表,同時也提供了一些常用的格式化翻譯工具,省去了大量的步驟,非常完善的輪子拿來即用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM