如何更优雅的实现微信小程序国际化

3,742 阅读4分钟

常见的国际化方式

官方方案

官方链接,其实官方的解决方案最大的问题就是麻烦,主要体现在以下几个方面

强依赖目录结构

由于gulp.js里是按照此目录结构进行处理的,如果要维持自定义目录需要修改glup文件,如下图

image.png

特别好笑的一点官方示例里居然不是这个目录结构,不过依然是强依赖目录结构,因为gulp中路径是写死的

文档简陋

通过官方文档快速入门居然无法搭建起项目,暂时只用这种方式搭建起来了 官方github demo,通过对比发现好多必要代码都没有在文档中说明。

比如需要在app.js中开始便需要执行getI18nInstance(),否则全局都无法正常国际化。这么重要的信息居然在快速入门中没有说明

调试麻烦

每次修改代码都要重新执行npm run build,注意是每次

由于国际化必须通过npm run build来实现,而每次npm run build过后dist文件就会被覆盖,所以每次只能修改src,而小程序预览的却是dist文件,这也就导致必须频繁的执行build命令。下面演示一下增加一个表头的操作步骤

2021-05-22 07-57-21.2021-05-22 08_03_21.gif

说下优点

代码简洁。解释一下,从上图可以看到,他的书写方式和其他主流框架的国际化书写方式很类似(vue,react)。view层都是类似t(key,参数),对js侵入也很小,下面是上图页面对应的js部分。

import { I18nPage } from '@miniprogram-i18n/core'

I18nPage({
  onLoad() {
    this.onLocaleChange((locale) => {
      console.log('current locale:', this.getLocale(), locale)
    })

    this.setLocale('zh-CN')
  },

  toggleLocale() {
    this.setLocale(
      this.getLocale() === 'zh-CN' ? 'en-US' : 'zh-CN'
    )
  },

  nativate() {
    wx.navigateTo({
      url: '/pages/logs/logs'
    })
  }
})

可以说除了I18nPage以外没有别的侵入,剩下那些代码都是用于切换语言所需,如果只是最简单国际化,只需要I18nPage({})即可

聊一下为什每次都需build

其实咱们看下dist/i18n/locales.wxs文件即可

var fallbackLocale = "zh-CN";
var translations = {
  "en-US": {
    test: ["test messages"],
    test2: ["test message 2, ", ["label"], ", ", ["label2"]],
    nested: ["nested message: ", ["test"]],
    toggle: ["Toggle locale"],
    navigate: ["Navigate to Log"],
    "window.title": ["I18n test"],
    "index.test": ["Test fallback"],
    navigate2: ["Navigation 2nd"],
  },
  "zh-CN": {
    test: ["测试消息"],
    test2: ["测试消息二, ", ["label"], ", ", ["label2"]],
    nested: ["嵌套消息: ", ["test"]],
    toggle: ["切换语言"],
    navigate: ["跳转"],
    "window.title": ["国际化测试"],
    "index.test": ["备选"],
    navigate2: ["导航2"],
  },
};
var Interpreter = (function (r) {
  var i = "";
  function f(r, n) {
    return r
      ? "string" == typeof r
        ? r
        : r
            .reduce(function (r, t) {
              return r.concat([
                (function (n, e) {
                  if (((e = e || {}), "string" == typeof n)) return n;
                  if (n[2] && "object" == typeof n[2]) {
                    var r = Object.keys(n[2]).reduce(function (r, t) {
                        return (r[t] = f(n[2][t], e)), r;
                      }, {}),
                      t = r[e[0]],
                      u = e[n[0]];
                    return void 0 !== u
                      ? r[u.toString()] || r.other || i
                      : t || r.other || i;
                  }
                  if ("object" == typeof n && 0 < n.length) {
                    return (function r(t, n, e) {
                      void 0 === e && (e = 0);
                      if (!n || !t || t.length <= 0) return "";
                      var n = n[t[e]];
                      if ("string" == typeof n) return n;
                      if ("number" == typeof n) return n.toString();
                      if (!n) return "{" + t.join(".") + "}";
                      return r(t, n, ++e);
                    })(n[0].split("."), e, 0);
                  }
                  return "";
                })(t, n),
              ]);
            }, [])
            .join("")
      : i;
  }
  function c(r, t, n) {
    t = r[t];
    if (!t) return n;
    t = t[n];
    return t || n;
  }
  return (
    (r.getMessageInterpreter = function (i, o) {
      function e(r, t, n) {
        var e, u;
        return f(
          ((e = r),
          (u = o),
          ((n = (r = i)[(n = n)]) && (n = n[e])) || c(r, u, e)),
          t
        );
      }
      return function (r, t, n) {
        return 2 === arguments.length
          ? e(r, null, t)
          : 3 !== arguments.length
          ? ""
          : e(r, t, n);
      };
    }),
    r
  );
})({});

module.exports.t = Interpreter.getMessageInterpreter(
  translations,
  fallbackLocale
);

其实搞这么麻烦构建方式主要是为了生成这个wxs文件,我们之所以能在vue中看到{{t('key')}}的方式进行国际化,是因为wxml层本身支持函数调用且函数可以调用外部资源(国际化文件),而小程序只允许通过wxs(官方文档)的方式在页面使用函数,出于性能考虑又不允许wxs引用任何外部资源(除了其他的wxs),所以这个build最核心的诉求是把 国际化文件copy一份到wxs文件中。下面是wxs无法引用外部资源的官方说明

image.png

优化

我们国际化的核心诉求就是解决官方国际化问题同时保留他的优点,这里我们列下此次的目标

  • 路径灵活,不强依赖路径减少后期添加国际化的路径改动成本
  • 调试方便,和原始开发调试方式相同
  • 书写简洁,保持和vue一样的书写方式

先来看下效果

2021-05-22 10-27-02.2021-05-22 10_28_39.gif

wxml代码

<wxs src="../wxs/i18n.wxs" module="i18n" />
<!-- 标题国际化 -->
<page-meta>
  <navigation-bar title="{{i18n.t(locales['主页'])}}" />
</page-meta>
<!-- 一般国际化 -->
<view>{{i18n.t(locales['通往爱人家里的路总不会漫长。'])}}</view>
<!-- js国际化 -->
<view>{{jsMsg}}</view>
<!-- 支持变量国际化 -->
<view>{{i18n.t(locales['当前页面:{path}'],[{key:'path',value:'home-page/home-page'}])}}</view>
<!-- 切换中英文按钮 -->
<button bindtap="zhClick" type="default">{{i18n.t(locales['切换中文'])}}</button>
<button bindtap="enClick" type="warn">{{i18n.t(locales['切换英语'])}}</button>

js代码


const i18n = require('../behaviors/i18n');
// home-page/home-page.js
Component({
  behaviors: [i18n],
  /**
   * 组件的初始数据
   */
  data: {
  },
  /**
   * 组件的方法列表
   */
  methods: {
    zhClick() {
      this.switchLanguage('zh_CN')
    },
    enClick() {
      this.switchLanguage('en_US')
    },
  }
})

基本维持和官方相同的写法,尽可能少的代码写入

代码段链接

解决思路

利用behaviors(官方文档)将国际化文案进行引入每个页面。然后将所有国际化数据和key值以参数的形式传递给wxs函数,这样就可以避开wxs外部资源限制实现和vue i18n相同的效果。

  • behaviors负责将国际化方法和文案导入全局,以下是behaviors/i18n.js源码:
// behaviors/i18n.js

const {
  t
} = require('../utils/index')
const i18n = Behavior({
  data: {
    language:{}, // 当前语种
    locales: {}, // 当前语言的全部国际化信息
  },
  pageLifetimes: {
    // 每次页面打开拉取对应语言国际化数据
    show() {
      if (this.data.language === 'en_US') {
        this.setData({
          locales: require('../i18n/en_US')
        })
      } else {
        this.setData({
          locales: require('../i18n/zh_CN')
        })
      }
    }
  },
  methods: {
    // 全局js国际化便捷调用
    $t(key, option) {
      return t(key, option)
    },
    // 由于tab只能通过js修改,所以每次语言切换需要重新更新tab国际化内容
    refreshTab() {
      wx.setTabBarItem({
        index: 0,
        text: this.data.locales['主页']
      })
      wx.setTabBarItem({
        index: 1,
        text: this.data.locales['我的']
      })
    },
    // 切换语种
    switchLanguage(language) {
      this.setData({
        language
      })
      if (language === 'zh_CN') {
        this.setData({
          locales: require('../i18n/zh_CN')
        })
      } else {
        this.setData({
          locales: require('../i18n/en_US')
        })
      }
      // 切换下方tab
      this.refreshTab()
    },
  }
})

module.exports = i18n
  • wxs负责为wxml层提供国际化方法,此处逻辑比较简单,先找到国际化文件中key值对应的语句,然后根据第二参数(arr)将变量进行替换,此处替换逻辑比较粗暴,使用{key}方式代表变量。如:"我的年龄是{age}",age代表变量,参数传递格式为 [{key:'xxx',value:'xxxx'}]
// 国际化.js
{
  "ageText":"my age is {age}",
}
// wxml
<view>{{i18n.t(locales['ageText'],[{key:'age',value:'18'}])}}</view>

wxs源码如下:

var i18n = {
  t: function (str, arr) {
    var result = str;
    if (arr) {
      arr.forEach(function (item) {
        if(result){
          result = result.replace('{'+item.key+'}', item.value)
        }
      })
    }
    return result
  }
}
module.exports = i18n

同时提供一个在js里获取国际化的util方法

// 国际化
const t = (key, option = {}) => {
  const language = wx.getStorageSync('language');
  let locales = null
  if (language === 'en_US') {
    locales = require('../i18n/en_US')
  } else {
    locales = require('../i18n/zh_CN')
  }
  let result = locales[key]
  for (let optionKey in option) {
    result = result.replace(`{${optionKey}}`, option[optionKey])
  }
  return result
}

module.exports = {
  t
}

这样就基本实现了同时在wxml,js中进行国际化的基本需求,同时也解决了官方调试体验不足的缺点。

不足

  • 其实调试体验还不是那么完美,由于只有在show中初始化国际化文件内容,所以当开启“热重载”时对国际化文件进行修改,国际化内容不会自动进行更新

  • 每个page的js文件都需要引入behaviors/i18n.js,wxml文件引入<wxs src="../wxs/i18n.wxs" module="i18n" />有些略显繁琐

  • require('../i18n/xxx')整个项目引入了3遍略显繁琐,可以封装一下

  • i18n.t(locales['key']) 其中locals每次都要写一遍比较繁琐,不过由于wxs的限制也想不到太好的方法

使用建议

由于只是写一个demo很多逻辑并没有写完整,所以如果使用的话需要根据项目进行修改

  1. 如果i18n路径和命名方式不同需要同时修改behaviors/i18n.js,以及utils/index.js中的路径

  2. 现在每次刷新页面国际化都会被重置成中文,建议在behaviors/i18n.js的show方法中从全局获取当前的国际化语言。这样就不会每次都被重置成中文了。