如何封装一个flutter的多语言plugin

2,193 阅读3分钟

前言

关于flutter多语言的使用之前已经写过文章介绍过了,有兴趣的可以移步以$t形式使用flutter多语言,关于flutter国际化的具体介绍,大家可以移步国际化Flutter App

本文主要介绍在flutter如何封装一个插件,以减少开发者接人flutter多语言的重复工作量

接入使用

FlutterLocalization

效果

多语言插件.gif

多语言plugin功能

  • flutter 接入Efox多语言平台,支持加载本地和Efox平台切换,语言切换
  • 支持语言自定义,支持中文简繁体区分
  • 开放配置项:
    • 支持的语种
    • 默认语言
    • 语种映射关系
    • 本地语言路径
    • Efox平台语言路径
    • 是否加载本地多语言
  • 可获取方法
    • 获取是否加载本地多语言: AppLocalizations.isLocale
    • 修改是否加载本地多语言: AppLocalizations.changeIsLocale(true|false);
    • 获取当前语种: AppLocalizations.localeLang
    • 获取多语言翻译: AppLocalizations.$t('title_page')

Plugin packages和Dart packages的区别

  • 插件包(Plugin packages)是当你需要暴露Native API给别人的时候使用的形式,内部需要使用Platform Channels并包含Androiod/iOS原生逻辑,并且内部有example目录下的flutter项目可直接运行,进行代码测试
  • Dart包(Dart packages)是当你需要开发一个纯Dart组件(比如一个自定义的Weidget)的时候使用的形式,内部没有Native代码,代码测试可通过自己新建的flutter项目通过本地路径引入进行测试

新建package

多语言插件新建一个Dart包就可以代码撸起

新建package

步骤1:

接入flutter多语言需指定MaterialApp的localizationsDelegates和supportedLocales,而localeResolutionCallback是在应用获取用户设置的语言区域时会被回调

localizationsDelegates: AppLocal.localizationsDelegates,
supportedLocales: AppLocal.supportedLocales,
localeResolutionCallback: AppLocal.localeResolutionCallback,

所以需提供一个AppLocal类实现上述三个实例

class AppLocal {
  static Iterable<LocalizationsDelegate<dynamic>> _localizationsDelegates = [
    GlobalMaterialLocalizations.delegate,
    GlobalWidgetsLocalizations.delegate,
    GlobalCupertinoLocalizations.delegate,
    AppLocalizationsDelegate()
  ];
  // 支持的语言
  static Iterable<Locale> _supportedLocales = ConfigLanguage.supportedLocales;
  // 应用获取语言区域时会被回调
  static Locale _localeResolutionCallback(deviceLocale, supportedLocal) {
    print(
        '当前设备语种 deviceLocale: $deviceLocale, 支持语种 supportedLocale: $supportedLocal}');
    // null [] [en_US] [en_US, zh_CH] Locale('en', 'US') 不同手机对于语言的获取返回参数不同,所以需要根据返回做对应的处理
    var useDeciceLocale;
    if (deviceLocale != null &&
        deviceLocale.runtimeType.toString().contains('List') &&
        deviceLocale.isNotEmpty) {
      useDeciceLocale = deviceLocale[0];
    } else {
      if (deviceLocale.runtimeType.toString() == 'Locale') {
        useDeciceLocale = deviceLocale;
      } else {
        useDeciceLocale = null;
      }
    }
    print(
        '手机获取匹配后的语种:$useDeciceLocale, ${useDeciceLocale.runtimeType.toString()} ${useDeciceLocale.runtimeType.toString() == 'Locale'}');
    Locale _locale;
    bool hasLanguage = false;

    if (useDeciceLocale != null) {
      for (num i = 0; i < supportedLocal.length; i++) {
        if (useDeciceLocale.scriptCode == 'Hant') {
          if (useDeciceLocale.languageCode == supportedLocal[i].languageCode &&
              useDeciceLocale.scriptCode == supportedLocal[i].scriptCode) {
            hasLanguage = true;
            useDeciceLocale = supportedLocal[i];
            print('繁体语言匹配上了: $useDeciceLocale');
            break;
          }
        } else {
          if (useDeciceLocale.languageCode == supportedLocal[i].languageCode) {
            hasLanguage = true;
            useDeciceLocale = supportedLocal[i];
            print('普通语言码匹配上了: $useDeciceLocale');
            break;
          }
        }
      }
      _locale = hasLanguage
          ? useDeciceLocale
          : Locale.fromSubtags(
              languageCode: ConfigLanguage.defaultLanguage['language_code'],
              scriptCode: ConfigLanguage.defaultLanguage['script_code'],
            );
      print(
          '${hasLanguage ? '手机系统语言本app支持,使用系统指定语言: $_locale' : '手机系统语言本app不支持,使用app规定默认语言: $_locale'}');
    } else {
      _locale = Locale.fromSubtags(
        languageCode: ConfigLanguage.defaultLanguage['language_code'],
        scriptCode: ConfigLanguage.defaultLanguage['script_code'],
      );
      print('手机系统语言本app不支持,使用app规定默认语言: $_locale');
    }
    return _locale;
  }
  static get supportedLocales => _supportedLocales;
  static get localizationsDelegates => _localizationsDelegates;
  static get localeResolutionCallback => _localeResolutionCallback;
}

步骤2:

在localizationsDelegates除了需要指定flutter本身提供的delegate外,我们还需要指定一个本地化代理,如下,作用是在语言加载时判断是否支持加载和重新加载时我们需要的加载逻辑处理以及语言包加载后保存在AppLocalizations类中,AppLocalizations是我们的数据存储和controller类

class AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
  AppLocalizationsDelegate();
  @override
  bool isSupported(Locale locale) {
    return ConfigLanguage.supportedLocales.contains(locale);
  }
  @override
  Future<AppLocalizations> load(Locale locale) async {
    LocalStorage.get('lang').then((lang) async {
      print('本地缓存的语言: $lang');
      AppLocalizations._localeLang = lang;
      if (lang == null) {
        print('将要加载的语言locale: $locale');
        return await AppLocalizations.init(locale);
      } else {
        print('将要加载的语言lang: $lang');
        List<String> langCode = lang.split('-');
        if (langCode.length > 1) {
          return await AppLocalizations.init(Locale.fromSubtags(
              languageCode: langCode[0], scriptCode: langCode[1]));
        } else {
          return await AppLocalizations.init(Locale(langCode[0]));
        }
      }
    });
  }
  @override
  bool shouldReload(LocalizationsDelegate<AppLocalizations> old) {
    // false时,不执行上述重写函数
    return false;
  }
}

步骤3:

AppLocalizations类,我们保存多语言数据以及数据处理、数据初始化的一个类,提供给开发者调用的就是该类下的方法,需要实现的方法:

  • 接收参数配置(支持语种,默认语言,语种映射关系,本地路径,efox平台路径,是否加载本地)的addSupportLanguage方法
  • 获取语言的localeLang方法
  • 在语言切换时需要页面刷新,因此需要保存setState的setProxy方法(设置语言切换代理)
  • 内部init方法以及加载语言包的getLanguageJson方法
  • 需要是否加载本地语言的changeIsLocale方法
  • 修改当前语言的changeLanguage方法
  • 读取的语言的$t方法
class AppLocalizations {
  Locale _locale;
  static AppLocalizations _inst; // AppLocalizations实例
  static Map<String, dynamic> _jsonLanguage = {}; // 语言包
  static Function _setState; // 顶层父节点setState
  static BuildContext _context;
  static String _localePath; // 本地多语言路径
  static String _I18nHost; // Efox平台多语言路径
  static bool _isLocale = true; // 是否加载本地多语言
  static bool _hasConfigLocale = false; // 是否手动配置是否加载本地多语言
  static String _localeLang; // 缓存本地的多语言语种
  static bool get isLocale => _isLocale;
  static String get localeLang {
    return _localeLang ?? Localizations.localeOf(_context).toString();
  }
  AppLocalizations(this._locale);
  // 添加支持语种
  static void addSupportLanguage({
    List<Locale> supportedLocales,
    Map<String, String> defaultLanguage,
    Map<String, String> mapLanguage,
    String localePath,
    String I18nHost,
    bool isLocale,
  }) {
    if (supportedLocales != null) {
      ConfigLanguage.supportedLocales.addAll(supportedLocales);
    }
    if (defaultLanguage != null) {
      ConfigLanguage.defaultLanguage.addAll(defaultLanguage);
    }
    if (mapLanguage != null) {
      ConfigLanguage.mapLanguage.addAll(mapLanguage);
    }
    if (isLocale != null) {
      _hasConfigLocale = true;
      _isLocale = isLocale;
    }
    _localePath = localePath;
    _I18nHost = I18nHost;
  }
  // 设置语言切换代理
  static void setProxy(Function setState, BuildContext context) async {
    _setState = setState;
    _context = context;
  }
  // 初始化 localizations
  static Future<AppLocalizations> init(Locale locale) async {
    _inst = AppLocalizations(locale);
    await getLanguageJson();
    _setState(() {}); // 多语言包更新
    return _inst;
  }
  // 获取语言包
  static Future getLanguageJson() async {
    if (!_hasConfigLocale) {
      _isLocale =
          (await LocalStorage.get('isLocale')) == 'false' ? false : true;
    }
    Locale _tmpLocale = _inst._locale;
    String jsonLang;
    String lang = ConfigLanguage.mapLanguage[_tmpLocale.toString()] ??
        _tmpLocale.toString();
    if (_isLocale) {
      try {
        print('语言包路径: $_localePath/$lang.json');
        jsonLang = await rootBundle.loadString('$_localePath/$lang.json');
        // print('多语言本地加载数据:${json.decode(jsonLang)}');
      } catch (e) {
        print('多语言本地路径: $_localePath/$lang.json');
        print('多语言本地加载路径不存在,加载默认语言数据:$e');
        _inst._locale = Locale.fromSubtags(
            languageCode: ConfigLanguage.defaultLanguage['language_code'],
            scriptCode: ConfigLanguage.defaultLanguage['script_code']);
        _tmpLocale = _inst._locale;
        lang = ConfigLanguage.mapLanguage[_tmpLocale.toString()] ??
            _tmpLocale.toString();
        jsonLang = await rootBundle.loadString('$_localePath/$lang.json');
      }
    } else {
      try {
        print('语言包路径: $_I18nHost/$lang.json');
        jsonLang = (await Http.get(url: '$_I18nHost/$lang.json')).toString();
        // print('多语言平台加载数据:${(json.decode(jsonLang))}');
      } catch (e) {
        print('多语言平台路径: $_I18nHost/$lang.json');
        print('多语言平台加载路径不存在,加载默认语言数据:$e');
        _inst._locale = Locale.fromSubtags(
            languageCode: ConfigLanguage.defaultLanguage['language_code'],
            scriptCode: ConfigLanguage.defaultLanguage['script_code']);
        _tmpLocale = _inst._locale;
        lang = ConfigLanguage.mapLanguage[_tmpLocale.toString()] ??
            _tmpLocale.toString();
        jsonLang = (await Http.get(url: '$_I18nHost/$lang.json')).toString();
      }
    }
    _jsonLanguage['$_tmpLocale'] = json.decode(jsonLang.toString());
    print('是否加载本地语言: $_isLocale');
    print(
        '获取语言包的语种信息: ${_inst._locale}, $lang, $_tmpLocale, ${_tmpLocale.languageCode}, ${_tmpLocale.scriptCode}');
    print("多语言加载的数据: $_jsonLanguage");
  }
  // 修改是否加载本地多语言
  static void changeIsLocale(isLocale) async {
    _isLocale = isLocale;
    LocalStorage.set('isLocale', isLocale.toString());
    init(_inst._locale);
  }
  // 切换语言
  static void changeLanguage([Locale locale]) {
    if (locale == null || locale.languageCode == null) {
      print('修改语言语言码不能为空');
      locale = Locale.fromSubtags(
          languageCode: ConfigLanguage.defaultLanguage['language_code'],
          scriptCode: ConfigLanguage.defaultLanguage['script_code']);
    }
    if (locale.scriptCode != null) {
      _localeLang = '${locale.languageCode}-${locale.scriptCode}';
    } else {
      _localeLang = '${locale.languageCode}';
    }
    LocalStorage.set('lang', _localeLang);
    init(Locale.fromSubtags(
        languageCode: locale.languageCode,
        scriptCode: locale.scriptCode)); // 根据语言获取对应的国际化文件
  }
  static String $t(String key) {
    Locale _tmpLocale = _inst == null
        ? Locale.fromSubtags(
            languageCode: ConfigLanguage.defaultLanguage['language_code'],
            scriptCode: ConfigLanguage.defaultLanguage['script_code'])
        : _inst._locale;
    var _array = key.split('.');
    var _dict = _jsonLanguage['$_tmpLocale'] ?? {};
    var retValue = '';
    try {
      _array.forEach((item) {
        if (!_dict.containsKey(item) || _dict[item].runtimeType == Null) {
          retValue = key;
          return;
        }
        if (_dict[item].runtimeType != String) {
          _dict = _dict[item];
        } else {
          retValue = _dict[item];
        }
      });
      retValue = retValue.isEmpty ? _dict : retValue;
    } catch (e) {
      print('i18n exception');
      print(e);
      retValue = key;
    }
    return retValue ?? '';
  }
}

最后

欢迎更多学习flutter的小伙伴加入QQ群 Flutter UI: 798874340

敬请关注我们github: YYDev

作者