Flutter 多语言方案调研对比

根据资料选择了大众的两种方式进行调研和对比,主要是 Map方式和intl方式。

Map方式

  1. yaml添加本地化依赖
  flutter_localizations:
    sdk: flutter
  1. 定义资源包:
import 'package:flutter/material.dart';

class DemoLocalizations {
  DemoLocalizations(this.locale);

  final Locale locale;

  static DemoLocalizations of(BuildContext context) {
    return Localizations.of<DemoLocalizations>(context, DemoLocalizations);
  }

  static Map<String, Map<String, String>> _localizedValues = {
    'en': {
      'title': 'Hello World',
    },
    'es': {
      'title': 'Hola Mundo',
    },
    'zh': {
      'title': '你好呀',
    },
  };

  String get title {
    return _localizedValues[locale.languageCode]['title'];
  }
}
  1. 创建代理:
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:international_demo/localization_src.dart';

class DemoLocalizationsDelegate
    extends LocalizationsDelegate<DemoLocalizations> {
  const DemoLocalizationsDelegate();

  @override
  bool isSupported(Locale locale) =>
      ['en', 'es', 'zh'].contains(locale.languageCode);

  @override
  Future<DemoLocalizations> load(Locale locale) {
    return SynchronousFuture<DemoLocalizations>(DemoLocalizations(locale));
  }

  @override
  bool shouldReload(DemoLocalizationsDelegate old) => false;
}
  1. 使用代理:
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      localizationsDelegates: [
        DemoLocalizationsDelegate(),// 这是我们新建的代理
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: [
        const Locale('en', 'US'), // English
        const Locale('he', 'IL'), // Hebrew
        const Locale('zh', ''),   // 新添中文,后面的countryCode暂时不指定
      ],
      home: new MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

  1. 获取字符串:
DemoLocalizations localizations = DemoLocalizations.of(context);
DemoLocalizations.of(context).title

优点:定义简单,使用简单。
缺点:语言包多内存占用较大,翻译协作不是太友好,语言包没有单独隔离。

intl

  1. yaml添加本地化依赖
  flutter_localizations:
    sdk: flutter
  1. 安装Flutter Intl插件
    Flutter Intl
  1. 使用插件初始化项目和添加语言


    初始化
  1. 定义翻译字段以及生成对应的文件
翻译字段

l10n/intl_en.arb intl_pt_BR.arb intl_zh_CN.arb:语言包定义

arb

generated/intl/messages_en.dart,messages_pt_BR.dart,messages_zh_CN.dart:具体的语言包实现类

具体的语言包实现类

generated/intl/messages_all.dart: 负责分发语言包的具体实现类

dart

generated/l10n.dart:生成的语言代理类,负责load资源包和使用字符串

l10n

  1. 使用代理:
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        //...
      localizationsDelegates: [
        S.delegate,// 这是我们的代理
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
      ],
      supportedLocales: S.delegate.supportedLocales,
     //...
    );
  }
}

  1. 获取字符串:
S.of(context).cacheClean
S.current.cacheClean

优点:语言包管理独立清晰,方便协作,内存占用小,使用简介,有插件配合简化使用。
缺点:文件结构稍显复杂。

对比 intl Map
内存
复杂度 稍微难一点,但是有插件协作
协作性 友好 不友好
容错率 因为语言包和调用分开,容错高 因为语言包和调用分开,容错低

出于翻译人员,开发人员协作方面考虑,最终采用intl方式

遇到的问题:

多package设置localizationsDelegates只有第一个生效

在设置localizationsDelegates的时候,只能加载第一个顺序配置的语言代理,后续配置的代理将会失效。这个多模块各自管理各自的语言包是有很大的阻碍的。

问题点深入挖掘:

l10n.dart:

static Future<S> load(Locale locale) {
final name = (locale.countryCode?.isEmpty ?? false) ? locale.languageCode : locale.toString();
final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) {
  Intl.defaultLocale = localeName;
  S.current = S();

  return S.current;
});
}


//messages_all.dart:
/// User programs should call this before using [localeName] for messages.
Future<bool> initializeMessages(String localeName) async {
  var availableLocale = Intl.verifiedLocale(
    localeName,
    (locale) => _deferredLibraries[locale] != null,
    onFailure: (_) => null);
  if (availableLocale == null) {
    return new Future.value(false);
  }
  var lib = _deferredLibraries[availableLocale];
  await (lib == null ? new Future.value(false) : lib());
  initializeInternalMessageLookup(() => new CompositeMessageLookup());
  
  //这里 messageLookup:CompositeMessageLookup
  messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
  return new Future.value(true);
}

message_lookup_by_library.dart:  
/// If we do not already have a locale for [localeName] then
  /// [findLocale] will be called and the result stored as the lookup
  /// mechanism for that locale.
  void addLocale(String localeName, Function findLocale) {
    //
    if (localeExists(localeName)) return;
    var canonical = Intl.canonicalizedLocale(localeName);
    var newLocale = findLocale(canonical);
    if (newLocale != null) {
      availableMessages[localeName] = newLocale;
      availableMessages[canonical] = newLocale;
      // If there was already a failed lookup for [newLocale], null the cache.
      if (_lastLocale == newLocale) {
        _lastLocale = null;
        _lastLookup = null;
      }
    }
  }
  
/// Return true if we have a message lookup for [localeName].
bool localeExists(localeName) => availableMessages.containsKey(localeName);

最终是调用实现的MessageLookup里面的addLocale函数有判断,如果这个语言已经添加过,那么就不会添加了,就造成了localizationsDelegates配置只有第一个生效的现象。

解决方式

我们只需要自己实现一个MessageLookup维护多模块的语言包映射关系,然后将这个MessageLookup替换掉原来的CompositeMessageLookup即可。具体实现也有人实现了出来,参考:multiple_localization

class MultipleLocalizations {
  static _MultipleLocalizationLookup? _lookup;

  static void _init() {
    assert(intl_private.messageLookup is intl_private.UninitializedLocaleData);
    _lookup = _MultipleLocalizationLookup();
    intl_private.initializeInternalMessageLookup(() => _lookup);
  }

  /// Load messages for localization and create localization instance.
  ///
  /// Use [setDefaultLocale] to set loaded locale as [Intl.defaultLocale].
  static Future<T> load<T>(InitializeMessages initializeMessages, Locale locale,
      FutureOr<T> Function(String locale) builder,
      {bool setDefaultLocale = false}) {
    if (_lookup == null) _init();
    final name = locale.toString();
    final localeName = Intl.canonicalizedLocale(name);

    return initializeMessages(localeName).then((_) {
      if (setDefaultLocale) {
        Intl.defaultLocale = localeName;
      }

      return builder(localeName);
    });
  }
}

class _MultipleLocalizationLookup implements intl_private.MessageLookup {
  final Map<Function, CompositeMessageLookup> _lookups = {};

  @override
  void addLocale(String localeName, Function findLocale) {
    final lookup = _lookups.putIfAbsent(
      findLocale,
      () => CompositeMessageLookup(),
    );
    lookup.addLocale(localeName, findLocale);
  }

  @override
  String? lookupMessage(String? messageStr, String? locale, String? name,
      List<Object>? args, String? meaning,
      {MessageIfAbsent? ifAbsent}) {
    for (final lookup in _lookups.values) {
      var isAbsent = false;
      final res = lookup.lookupMessage(messageStr, locale, name, args, meaning,
          ifAbsent: (s, a) {
        isAbsent = true;
        return '';
      });

      if (!isAbsent) return res;
    }

    return ifAbsent == null ? messageStr : ifAbsent(messageStr, args);
  }
}

将生成的l10n.dart的load函数更改即可:

static Future<S> load(Locale locale) {
    // final name = (locale.countryCode?.isEmpty ?? false) ? locale.languageCode : locale.toString();
    // final localeName = Intl.canonicalizedLocale(name);
    // return initializeMessages(localeName).then((_) {
    //   Intl.defaultLocale = localeName;
    //   S.current = S();
    //
    //   return S.current;
    // });

    return MultipleLocalizations.load(initializeMessages, locale, (String l) {
      S.current = S();
      return S.current;
    }, setDefaultLocale: true);
  } 

和native保持一致的语言

主要使用native维护语言名称,flutter通过MethodChannel获取即可

flutter切换语言

runApp里面添加监听,然后setstate即可。


void main() async{
    //从native获取语言
  String lang = await ApplicationPlugin.currentLocal();
  runApp(FocoApp(init, appStyle, lang));
}

class FocoApp extends StatefulWidget {
  String lang;

  App( String lang) {
    this.lang = lang;
  }

  // This widget is the root of your application.
  @override
  _AppState createState() => _AppState();
}


class _AppState extends State<App> {
     @override
  void initState() {
    super.initState();
    

    EventManager.shared().on(Event.langChangeEvent, this, (params) async {
      /// 更新当前语言环境 (启动时执行),从native层获取语言
      widget.lang = await ApplicationPlugin.currentLocal();
      if (mounted) {
        setState(() {});
      }
    });
  }
  
   @override
  void dispose() {
    super.dispose();
    EventManager.shared().off(Event.langChangeEvent, this);
  }
  
  @override
  Widget build(BuildContext context) {
   List<LocalizationsDelegate> localizationsDelegates = [];
    //package 的delegate
    for (LocalizationsDelegate delegate in _localizationsDelegates) {
      localizationsDelegates.add(delegate);
    }
    localizationsDelegates.add(S.delegate);
    localizationsDelegates.add(GlobalMaterialLocalizations.delegate);
    localizationsDelegates.add(GlobalWidgetsLocalizations.delegate);
    localizationsDelegates.add(GlobalCupertinoLocalizations.delegate);

    List<Locale> supportedLocales = S.delegate.supportedLocales;
    return MaterialApp(
      //..
      locale: Locale(widget.lang),
      localizationsDelegates: localizationsDelegates,
      supportedLocales: supportedLocales,
      home: home,
    );
  }
}

参考链接:
国际化Flutter App
multiple_localization
Localization for Dart package
Flutter 基于intl的国际化多语言
Flutter International 国际化,Localization 本地化, 使用字符串Map

推荐阅读更多精彩内容