微信小程序国际化

405 阅读3分钟

现状

小程序国际化官方没有支持,也没有现成的插件。

网上有人做了国际化的尝试。但是只能替换静态文本,就是简单的键值匹配。

vue-i18n 由于是基于 htmlvue, 不同于wxml(尤其是不能修改dom),估计进行hack调研可能要花很多时间。

方案

本项目(github 源码)是一个可以直接运行的国际化的小程序 quickstart 项目。 下载后可以直接在 微信开发者工具中运行。

介绍

如果想让翻译的内容渲染在页面中,需要把翻译的数据放在 Page 的 data 中,对于动态渲染带可变参数的数据, 在 setData 的时候加个尾巴(在其后面set 带参数的翻译的 data)。 目前基本方案是自己开发一套翻译工具:

  • 功能: 1. 翻译静态文案 2. 翻译带参数的文案 *缺陷 1.由于只能返回字符串,没有类似vue 通过 v-html 进行标签替换的能力,所以对带标签(样式)的翻译无能为力。

总结: 优先保证中文用户的使用体验,对于不带标签的类型的翻译,中英文没有区别,对于带标签的类型的翻译,将中文翻译直接放在 wxml 中,只是在中文的情况下显示,在非中文时直接隐藏。

基础翻译代码

// i18n.js
module.exports = {
  locale: 'en',
  locales: {},
  registerLocale (locales) {
    this.locales = locales
  },
  setLocale (code) {
    this.locale = code
  },
  /**
   * 返回带(或不带)参数的类型的翻译结果
   * @param {string} key, /util/language/en.js 中的键名,如 "curslide"
   * @param {object} data, 传入的参数,如 {num: 123}
   * @returns {string}
   *
   * @desc 如:"activeno": "当前学生{activeno}位",
   *       activeno 为 key,可以输入data {activeno: 15}
   *       返回:"当前学生15位"
   */
  _ (key, data) {
    let locale = this.locale
    let locales = this.locales
    let hasKey = locale && locales[locale] && locales[locale][key]

    if (hasKey) {
      key = locales[locale][key]

      let res = key.replace(/\{[\s\w]+\}/g, x => {
        x = x.substring(1, x.length-1).trim()
        return data[x];
      })

      return res
    }

    throw new Error(`语言处理错误${key}`)
  },
  /**
   * 返回二选一类型的翻译结果
   * @param {string} key, /util/language/en.js 中的键名,如 "curslide"
   * @param {object} data, 传入的参数,如 {first: true} 选择前面的
   * @returns {string}
   *
   * @desc 如:"sendprob": "Send | Check",
   *       sendprob 为 key,可以输入data {first: true}
   *       返回:"Send"
   */
  _b (key, data) {
    let locale = this.locale
    let locales = this.locales
    let hasKey = locale && locales[locale] && locales[locale][key]

    if (hasKey) {
      key = locales[locale][key]

      let res = key.split('|')[data.first ? 0 : 1].trim()

      return res
    }

    throw new Error(`语言处理错误${key}`)
  }
}

改造setData

主要是基于小程序本身的setData方法。

# /Users/liujunyang/work/xuetang/weapp/util/util.js 在page onload之后调用
/**
 * 代理小程序的 setData,在更新数据后,翻译传参类型的字符串
 *
 * @param {object} langData 当页的翻译模块
 */
function resetSetData (langData) {
  let self = this

  /**
   * 在小程序中,使用子组件的页面, this.setData configurable 和 writable 都是 false
   * 所以不能重置 setData 方法,只能另起一个函数名,这里用了 setComData,
   * 另外,由于在 langData.js 中 setTransData 方法调用的是 setData,
   * 所以,另外给使用子组件的页面,定义了个 setComTransData 方法,去调用 setComData
   */

  let isUsingComponents = !self.__proto__.hasOwnProperty('setData') && self.__proto__.__proto__.hasOwnProperty('setData')
  let _ = self.setData

  if (!isUsingComponents) {
    self.setData = function(data, isSetTrans = false){
      _.call(self, data)
      if (isSetTrans) {/* 阻止翻译循环调用 setData  */return;}
      langData.setTransData && langData.setTransData.call(self)
    }
  } else {
    self.setComData = function(data, isSetTrans = false){
      _.call(self, data)
      if (isSetTrans) {/* 阻止翻译循环调用 setData  */return;}
      langData.setComTransData && langData.setComTransData.call(self)
    }
  }

}
  • 如上所述,定义了一个用于翻译的类,有2中翻译的方法,一种是键值替换,另一种是二选一替换。
  • 每套页面有一个langData.js模块。
  • 在小程序启动过程中异步得到用户语言之后,翻译静态文本挂载到data下的一个语言字段上;
  • 另外就是基于动态数据的模板,改造setData为执行时先执行本身的 setData 修改数据,另外执行修改基于数据文案的方法 setTransData(内部调用了新改造的setData方法去设置数据,同时加变量判断避免了无限循环),更新到data上,从而引起相关视图的变化。
  • 有一个小坑就是小程序使用组件的页面中 setData 方法的 configurable为false,这里做了一些hack,比如另外定义一个方法名 setDataBak,在page中调用这个方法而不是setData。在他的修改组合文案的方法setComTransData中那就相应是调用 setDataBak了(也避免了无限循环)。
# /Users/liujunyang/work/xuetang/weapp/pages/remotecontrol/teacher/hongbao/langData.js
const app = getApp()
const i18n = app.i18n

let langData = {}
let data = setLang()

function setLang () {
  let data = {
    'bonustips': i18n._('bonustips'),
    'bonuslimit': i18n._('bonuslimit'),
    'quantity': i18n._('quantity'),
    'readmore': i18n._('readmore'),
    'back': i18n._('back'),
    'cny': i18n._('cny'),
    'amounteach': i18n._('amounteach'),
    'pcs': i18n._('pcs'),
    'setquantity': i18n._('setquantity'),
    'setamount': i18n._('setamount'),
    'preparebonus': i18n._('preparebonus'),
    'nobonus': i18n._('nobonus'),
    'plsconfpay': i18n._('plsconfpay'),
    'classbonus': i18n._('classbonus'),
    'yktwallet': i18n._('yktwallet'),
    'wxwallet': i18n._('wxwallet'),
    'cfmpay': i18n._('cfmpay'),
    'balance': i18n._('balance'),
    'ykqloading': i18n._('ykqloading'),
    'zhifu': i18n._('zhifu'),
    'paying': i18n._('paying'),
    'paysuccess': i18n._('paysuccess'),
    'payfailed': i18n._('payfailed'),
    'confirm': i18n._('confirm'),
    'reject': i18n._('reject'),
  }
  return data
}

app.loginPromise.then(() => {
  langData.data.T_D = setLang()
})

module.exports  = langData = {
  data: {
  	T_D: data
  },
  setTransData () {
    let self = this
    self.setData({
      'T_D.quantity2': i18n._('quantity2', {num: self.data.stuNumer}),
      'T_D.swfacawryb': i18n._('swfacawryb', {num: self.data.bonusNumber}),
    }, true)
  },
  resetLang(){
    langData.setTransData.call(this)
  },
};