美文网首页
Flutter 多插件本地化问题

Flutter 多插件本地化问题

作者: 微微笑的蜗牛 | 来源:发表于2021-11-15 08:28 被阅读0次

    大家好,我是微微笑的蜗牛,🐌。

    今天这篇文章主要想讲讲,Flutter 多插件语言本地化遇到的问题,原因以及解决方案。

    本地化

    在 Flutter 开发中,多语言本地化可以使用 Android Studio 提供的 Flutter intl 插件,它能帮开发者自动生成本地化相关的代码。

    不过首先得在 App 的 localizationsDelegates 添加 S.delegate,并添加 supportedLocales 来指定支持的区域。

    在使用文本的地方,只需调用 S.of(context).xx 便可获取 xx 对应的文本。

    在该插件的辅助下,实现多语言非常简单。

    问题

    最近在做 Flutter 插件化,各个插件中有自己的本地化信息,然后会在 App/Submodule 中集成多个插件。

    这样,各个插件的 S.delegate 都需要添加到宿主的 localizationsDelegates 中。

    但此时,多插件的本地化就出现问题了。

    具体表现为:只有第一个插件的本地化生效了。什么意思呢?

    假设当前是中文,只有第一个添加到 localizationsDelegates 的插件显示中文,其他插件都显示英文。

    其实这么说还不太准确,如果再进一步的话,还要看各插件中使用的 message key 值是否一样。

    • 如果使用的 key 值在第一个插件中存在,那么它显示的就是第一个插件中该 key 对应的中文;
    • 如果不存在,显示的则是本插件中 key 对应的英文。

    下面,我们来举个栗子看看。

    栗子

    假设有一个 Flutter App 的工程。

    它内部有三个本地插件,分别是 plugin_a、plugin_b、plugin_c

    每个插件都提供了一个一毛一样的 widget,居中显示文字。只不过背景色有所区别,分别如下:

    • 插件 a:红色
    • 插件 b:蓝色
    • 插件 c:绿色

    另外,这三个插件中都有中英文的本地化信息,即包含 intl_en.arb、intl_zh_CN.arb

    App 的工程结构如下:

    App 工程结构

    然后,App 以本地依赖的方式引入了这三个插件。

    flutter_plugin_a:
        path: './flutter_plugin_a'
    
    flutter_plugin_b:
          path: './flutter_plugin_b'
    
    flutter_plugin_c:
          path: './flutter_plugin_c'
    

    同时,在宿主的 localizationsDelegates 中添加了三个插件的 S.delegate

    localizationsDelegates: const [
          plugin_a_localization.S.delegate,
          plugin_b_localization.S.delegate,
          plugin_c_localization.S.delegate,
          GlobalMaterialLocalizations.delegate,
          GlobalCupertinoLocalizations.delegate,
          GlobalWidgetsLocalizations.delegate
     ],
    

    arb 内容

    1. 「插件 a」 的 arb 内容如下:
    // intl_en.arb
    {
      "text": "plugin_a",
      "edit": "Edit"
    }
    
    // intl_zh_CN.arb
    {
      "text": "插件a",
      "edit": "编辑"
    }
    
    1. 「插件 b」 的 arb 内容如下:
    // intl_en.arb
    {
      "text": "plugin_b",
      "close": "Close"
    }
    
    // intl_zh_CN.arb
    {
      "text": "插件b",
      "close": "关闭"
    }
    
    1. 「插件 c」 的 arb 内容如下:
    // intl_en.arb
    {
      "text": "plugin_c",
      "search": "Search"
    }
    
    // intl_zh_CN.arb
    {
      "text": "插件c",
      "search": "搜索"
    }
    

    💡 温馨提示:它们的 arb 中都有个相同的 key,即 "text"

    接下来,我们将进行实验,看看在使用相同 key 和不同 key 时,文字的显示情况。假设区域是中国。

    相关 demo 代码可查看:https://github.com/silan-liu/flutter_app_localization

    相同 key

    假设三个插件都使用相同 key 值,"text"

    ✅ 正确结果应该为:插件a、插件b、插件c。

    💣 但是呢,显示结果却如下:插件a、插件a、插件a。

    这里就有点奇怪了,o(╥﹏╥)o。

    「插件 b」 和「插件 c」 取到的 text 值竟然都是「插件 a」的!

    不同 key

    假设三个插件分别使用各自的 key 值。

    • 插件 a,使用 text
    • 插件 b,使用 close
    • 插件 c,使用 search

    ✅ 正确结果应该为:插件a、关闭、搜索。

    💣 但是呢,显示结果却如下:插件a、Close、Search。

    我们可以看到,只有「插件 a」正确的显示了中文,其他插件显示了英文。

    这究竟又是为什么?

    追本溯源

    首先,我们得搞清楚 Intl 内部是如何查找文本的?

    通过查看生成的 l10n.dart 代码,发现 Intl 在查找 message 时,会调用到 Intl.message

    String get text {
        return Intl.message(
          'plugin_a',
          name: 'text',
          desc: '',
          args: [],
        );
      }
    

    Intl.message 内部会使用 MessageLookup 对象来寻找对应的文本,也就是下面代码中的 helpers.messageLookup

    static String? _lookupMessage(String? messageText, String? locale,
          String? name, List<Object>? args, String? meaning) {
        return helpers.messageLookup
            .lookupMessage(messageText, locale, name, args, meaning);
      }
    

    在本地化相关代码初始化时,当前 locale 的本地化信息会被添加到该 messageLookup 对象中。

    另外,在添加 locale 对应的信息时,会先判断 locale 是否已存在。如果已经存在,则不会进行添加。

    相关代码在 CompositeMessageLookupaddLocale 里。

    void addLocale(String localeName, Function findLocale) {
        if (localeExists(localeName)) return;
    
        // ...省略
      }
    

    注意看第一句代码,localeExists 用于判断是否已添加 localeName。如果存在,则不往下执行。

    到这里,一切看起来还挺正常的。

    但最致命的问题在于,该对象是一个全局对象。所以在多插件场景下,它们共用的是同一个对象。

    也就是说,如果有一个插件注册了某区域的本地化信息,其他插件就不可能再注册进去了。

    所以,归根结底,还是因为 messageLookup 对象的共用导致。

    栗子解释

    看到这里,也就能解释栗子中出现的两种情形了。

    Q1:为什么使用相同的 key 时,全都显示插件 a 中的文本?

    A:因为只有插件 a 的本地化信息添加进去了。如果此时其他插件也使用相同的 key,那么自然获取到的是插件 a 中的值。

    Q2:为什么使用不同 key 值时,其他插件显示的是英文呢?

    A:这是因为,如果注册的本地化信息中没有这个 key 值,会默认取该 key 对应的英文文本。

    比如下面这段获取 close 对应文本的方法,它的第一个参数就是 Close,也就是对应的英文文本。

    /// `Close`
    String get close {
        return Intl.message(
          'Close',
          name: 'close',
          desc: '',
          args: [],
        );
    }
    

    那为什么要传入英文文本呢?

    想必是有作用的,跟进代码中会发现,它是用于作为默认文本

    相关问题

    经查阅资料,发现有人也遇到了类似的问题。如下:

    文中提到的两种解决方式,大同小异,即:每个插件提供自己的 messageLookup 对象,防止共用。

    下面,我们就参照该思路,一步步解决问题。

    解决方案

    主体思路

    思路已经很清晰了,解决对象的共用问题即可。

    上面我们也已经提到过,Intl.message 内部最终调用到了全局的 messageLookup 对象来进行查找。

    那么,在这一步进行查找的时候,我们可以将其替换为自己的 messageLookup 对象,以此达到目的。

    所以,思路整理下来,就是如下两点:

    • 生成插件自己的 messageLookup 对象。
    • 修改 message 查找方法。

    但现在最重要的一点是,如何让每个插件提供自己的 messageLookup 对象?

    在回答这个问题之前,我们得先看看全局 messageLookup 对象是如何生成的?

    全局 messageLookup 对象的生成

    经过查阅代码,发现 messageLookup 对象是在 messages_all.dart 文件中生成的。

    具体代码在 initializeMessages 中,初始化的类型是其子类 CompositeMessageLookup

    Future<bool> initializeMessages(String localeName) async {
      
      // ...省略
    
      // 这里初始化为 CompositeMessageLookup 的实例
      initializeInternalMessageLookup(() => new CompositeMessageLookup());
    
        // 添加本地化信息
      messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
    
      return new Future.value(true);
    }
    

    那么,参照它的实现,我们可以在每个插件中生成自己 CompositeMessageLookup 实例,在查找文本时用这个内部对象。

    插件 messageLookup 对象的生成

    由于 initializeMessages 内部涉及到一些私有变量和方法的使用,因此,如果我们想生成 messageLookup 对象,需要在 messages_all.dart 中添加代码。

    initializeMessages 的流程差不多,只是在最后一步,返回新生成的对象即可。如下所示:

    Future<MessageLookup?> getMessageLookup(String localeName) async {
        // ... 省略
      
        // 生成 CompositeMessageLookup 对象
        final messageLookup = new CompositeMessageLookup();
        messageLookup.addLocale(availableLocale, _findGeneratedMessagesFor);
      
        return messageLookup;
      }
    

    何时初始化

    不过还有个问题,原有的全局 messageLookup 对象是什么时候进行初始化的?

    看看生成的 l10n.dart 的代码,S 中有个 load 方法,就是在做这个事情。

    S.load 方法的调用,又是什么时候呢?

    通过断点调试,发现 Localizations 在初始化时,会调用到各个 LocalizationsDelegateload 方法,最后调用到 S.load

    由于我们需要将全局 messageLookup 替换成自己生成的对象,那么可考虑在 S.load 方法中进行操作。

    为了便捷性,可将 S.load 方法的实现替换为插件内部自己的 load 实现,以达到替换目的。

    实现替换

    上面提到 message 的查找是通过 Intl.message 方法,那么我们可以模仿它的方式来进行插件内的文本查找。

    说起来,就是在插件内部定义自己的 Intl 类,同样提供 message 方法,只不过在查找时使用插件生成的独立 messageLookup 对象。

    另外,再提供一个 load 方法,用于替换 S.load 实现。

    也就是说,自定义的 Intl 会包含如下部分:

    class Intl {
        // 查找 message
        static String message(xx);
    
        // 自己定义的 messageLookup 对象
        static MessageLookup myMessageLookup;
    
        // load 生成自己的 messageLookup 对象
        static Future<S> load(Locale locale);
    }
    

    而原 S.load 的实现,会被替换成如下方式:

    // Intl 为自定义的类
    static Future<S> load(Locale locale) => Intl.load(locale);
    

    这样最核心的问题就解决了。

    整体流程

    经过上面的分析,我们可以得知,这个解决方案涉及到的文件有:

    • messages_all.dart:用于生成自定义的 messageLookup 对象。
    • l10n.dart:用于替换 S.load 方法。
    • 另外,还有新增的自定义 Intl 类。

    梳理一下,整体的解决流程如下:

    1. 在插件内新定义 Intl 类,实现上述提到的方法。
    2. messages_all.dart 中添加生成内部 messageLookup 的实现。假定方法名为 getMessageLookup
    3. 替换 l10n.dartS.load 方法为 Intl.load,同时屏蔽原 intl.dart 头文件。

    但是 messages_all.dart 等文件是自动生成的,随时可能会发生变化。若以手动的方式修改,不太可取。

    另外,工程中涉及到多个插件,一个个修改肯定不是个事。

    因此,考虑将以上流程以脚本的方式自动化进行。

    脚本自动化

    整体思路也比较简单,如下所示:

    1. 准备一份已经定义好的 intl 文件,也就是内部有自己的 messageLookup
    2. 遍历工程目录,判断是否为插件目录。如果是插件,且存在 l10n.dartmessages_all.dart,则将步骤 1 中的 intl 文件拷贝到插件目录下。
    3. 修改 messages_all.dart 文件,新增 getMessageLookup 方法。若已存在,则进行替换。
    4. 修改 l10n.dart 文件,替换 S.load 实现。

    这样,在 intl 插件生成代码后,运行脚本,则可修正问题。

    完整的 ruby 脚本可查看:flutter_app_localization/intl.rb at main · silan-liu/flutter_app_localization

    不足之处

    由于本地化代码是 IDE 插件自动生成的,那么在开发过程中,很有可能在代码变动后忘记执行脚本,而直接提交了代码。

    这样一来,插件的本地化还是会存在问题。

    因此,后续考虑两种方案:

    • 在 ci 打包时,添加执行脚本这一步骤,让打出的包是正确的。
    • 添加 pre-commit hook,在提交代码之前先执行脚本,保证提交代码的正确性。

    相关链接

    相关文章

      网友评论

          本文标题:Flutter 多插件本地化问题

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