vue-i18n国际化翻译及原理

1,861 阅读3分钟

官网:kazupon.github.io/vue-i18n/zh…

开始

npm install vue-i18n

import Vue from 'vue'
import VueI18n from 'vue-i18n'

Vue.use(VueI18n)

0、项目结构

// index为i18n的声明文件,en/zh分别是各个语言的文件夹,内部再根据业务模块划分翻译模块,再import到语言文件下的index中,可对象嵌对象地引用。
├── i18n
│   ├── en
│   │   ├── otherModule.ts
│   │   └── index.ts
│   ├── index.ts
│   └── zh
│       ├── otherModule.ts
│       └── index.ts

1、弹窗组件中使用

// minxin注入i18n
Vue.use({
    install(Vue, options) {
        Vue.mixin({
            beforeCreate() {
                // @Popup 根元素
                // @ts-ignore
                this._i18n ??= i18n;
            }
        });
    }
});

2、支持在js/ts文件中使用

// i18n/index文件中 将i18n对象包一层new Vue,导出
const i18n = new VueI18n({
    locale: initLang ?? 'zh',
    messages: {zh: zhI18n, en: enI18n}
});
function useI18n() {
    return {
        i18n: new Vue({i18n}),
        current: i18n.locale,
        setLanguage,
        switchLanguage() {
            if (i18n.locale === 'en') {
                setLanguage('zh');
            } else {
                setLanguage('en');
            }
        }
    };
}

export {useI18n};


// js文件中使用, 结构出i18n对象,即可使用i18n.$t()
import {useI18n} from '../i18n/index';
const {i18n} = useI18n(); 
console.log(i18n.$t('xxx'))

常用语法

1、template中使用

// 插值
<div>{{ $t('xx.x') }}<div/>
// 绑定属性
<div :title="$t('xxx.xx')"></div>

2、vue-js中使用

this.$t('xxx') ;

3、js中使用

import i18n from '../i18n/index'
i18n.$t('xxx.xx')

4、传递参数

// define
xxx: `传递{attrName}参数啊`
// use
this.$t('xxx', {attrName: yourValue});

5、传递参数并做判断

// 场景: 英文单复数有加s和不加s的区别,first,second, xth的区别
// define
value: ctx => `${ctx.named('n') > 1 ?  '' : ''}`
this.$t('value', {n: yourValue});

6、$tc

单复数时使用
一个变量可对应多个值

常见问题

0、npm包内如何使用i18n?

// 场景:业务使用到的包内部没有翻译
.dist目录下:
├── i18n
│   ├── en.js
│   ├── index.js
│   └── zh.js
├── index.js
// i18n/index.js
import Lang from '@ncfe/nc.lang';
import Vue from '@ncfe/nc.vue';
import VueI18n from '@ncfe/nc.vue-i18n';
Vue.use(VueI18n);

import en from './en';
import zh from './zh';

const i18n = new VueI18n({
    locale: Lang.get() || 'zh',
    messages: {
        zh,
        en
    }
});
const MyI18N = new Vue({i18n});
export default MyI18N
// 在需要使用的地方,import i18n,使用i18n.$t即可
// export i18 给业务层切换语言

1、npm包内使用了i18n如何动态切换?

//将npm包内的i18n对象导出,import到你的i18n的index文件内,再将其写在你的i18n的切换语言函数内执行。
import npmI18n from 'yourNpm';
setLanguage(lang: string, cache: boolean) {
   npmI18n.$i18n.locale = lang;
}

// 方案2: 切换语言时,使用$emit(),$on()事件发布

2、npm包内部使用的i18n翻译不符合业务需求如何替换?

/**
 * 暴露给业务用与更新 i18n 的方法
 * @param {string} locale 语言
 * @param {<{key: string, value: string}>} data 需要被变更的 i18n 对象
 */
export function updateI18nValue(locale, data) {
    if (typeof data !== 'object') return;

    if (locale === 'en' || locale === 'zh') {
        const messages = locale === 'en' ? en : zh;

        Object.entries(data).forEach(([key, value]) => {
            if (key === 'string' && value === 'string') {
                messages[key] = value;
            }
        });

        i18n.setLocaleMessage(locale, {...messages, ...data});
    }
}

3、引用javaScript等代码写的组件如何动态切换语言?

// 场景:富文本编辑器的语言动态切换
//强刷组件,在外层组件强刷
<cpm :key="freshKey"><cpm/>
@Watch('language')
switchLanguage() {
    this.freshKey = Symbal();
}

4、后端接口返回的msg翻译(设置cookie)?

//翻译由后端接口提供,设置cookie标识语言(设置path为需要*)
// 在切换语言的函数中一起执行
setLanguage(lang, cache) {
    document.setCookie(`lang=${lang}`)
}

5、接口不提供翻译的数据怎么翻译?

// 场景: 。。。
// 写一个映射表
export const judgeTitleMap = {
    等待判题: 'Waiting for grades',
    正在编译: 'Compiling',
    正在运行: 'Running'
}
export const judgeMemoMap = {
    'Test the code': '自测运行',
    'Memory overload': '内存超限',
    "The evaluation system hasn't yet evaluated this submitted codes. Please wait.": '评测系统还没有评测到这个提交,请稍候',
    'The evaluation system is compiling. Results will come later.': '评测系统正在编译,稍候会有结果'
}
    // 缓存中文描述
    that.resultZH = JSON.parse(JSON.stringify(oResult.result));
    that.result = oResult.result;
    // 转换英文描述
    if (that.result && that.result.desc && that.result.judgeReplyDesc) {
        if (that.language === 'en' && that.language && that.result && that.result.desc && that.result.judgeReplyDesc) {
            that.result.judgeReplyDesc = judgeTitleMap[that.result.judgeReplyDesc] || that.result.judgeReplyDesc;
            for (let i in judgeMemoMap) {
                if (that.result.desc.indexOf(judgeMemoMap[i]) !== -1) {
                    that.result.desc = that.result.desc.replace(judgeMemoMap[i], i);
                }
                if (that.result.memo.indexOf(judgeMemoMap[i]) !== -1) {
                    that.result.memo = that.result.memo.replace(judgeMemoMap[i], i);
                }
            }
        }
    }

// 中文作为对象key,英文作为value,找不到则返回默认值
 if (that.language === 'en') {
            judgeReplyDesc = judgeTitleMap[item.judgeReplyDesc] || item.judgeReplyDesc;
        }

6、初始化随浏览器语言

// navigator.language API获取浏览器语言,并在init i18n时候使用
function getSystemLang() {
    return navigator.language === 'en' ? 'en' : 'zh';
}

7、常量引用导致无法动态切换?

// 场景1、
// 在constant.ts文件中定义常量,非const也一样
const map = {
    a: i18n.$t('xxx'),
    b: i18n.$t('xxx')
}

// 在其他地方,如vue中import {map}
//map的数据渲染到页面上,切换语言时,map不能动态切换,
解决方案1:
不使用i18n.$t,手动写一份其他语言的map;
const map = {
    a: '中文'
}
const mapEn = {
    a: 'chinese'
}
将两份map同时import进来,通过language来判断使用哪份map;

// 场景2
//某i18n变量在函数内使用,并return一个字符串出来,页面使用该return的值渲染,切换语言时,无法动态切换;
解决方案1:
watch监听 language的切换,并再次调用改函数即可。

8、外部接口/组件数据如何翻译?

// 强制转换
if(language === 'en' && msg === '我爱你') msg = 'I Love You'

9、ui适配

细节提示

1、英文模式使用模版字符串

模版字符串可保留前后空格。拼接的英文句子之间要有一个空格。

2、ts提示类型不对

1this.$t('xxx') as string 类型断言
2`${this.$t('xxx')}` 模版字符串转换

3、被组件断开的拼接句子可以使用或语法

// 其他情况尽量少用,容易出错,切tc写法和t写法不一致容易遗漏
{{ $tc('xxx', 1) }} <div></div> {{ $tc('xxx', 2) }}

4、在无法获取vuex的language状态时,可只有导入i18n,获取locale获取语言

原理解析

1、$t函数

在 Vue原型上注册t函数,参数为key和插值values。并调用i18n.t函数,默认传递i18n对象的locale语言,用来获取message对象中对应的语言对象。核心是调用i18n.t函数,所以也可以newVue(i18n)作为到处对象,去使用用t函数,参数为key和插值values。并调用i18n._t函数,默认传递i18n对象的locale语言,用来获取message对象中对应的语言对象。核心是调用i18n._t函数,所以也可以new Vue({i18n})作为到处对象,去使用用t。

// 在 Vue 的原型中挂载 $t 方法,把 VueI18n 对象实例的方法都注入到 Vue 实例上
Vue.prototype.$t = function (key: Path, ...values: any): TranslateResult {
    const i18n = this.$i18n
    return i18n._t(key, i18n.locale, i18n._getMessages(), this, ...values)
  }

_t函数内部调用_translate函数,并传入messages,locale,和key

messages结构如图所示,同个传入的locale,i18n的locale找不到语言时,则使用兜底语言fallbackLocale从而获取对于语言的翻译。

_translate内部调用_interpolate,返回翻译结果。

若message语言对象内部又拆分了对个对象模块,则传入的key也需要多层调用,知道获取到字符串或返回字符串的函数。

调用parse$1,解析key字符串,成一个数组,再遍历改数组,来一层一层读取message。

function parse$1 (path) {
  var keys = [];
  var index = -1;
  var mode = BEFORE_PATH;
  var subPathDepth = 0;
  var c;
  var key;
  var newChar;
  var type;
  var transition;
  var action;
  var typeMap;
  var actions = [];

  actions[PUSH] = function () {
    if (key !== undefined) {
      keys.push(key);
      key = undefined;
    }
  };

  actions[APPEND] = function () {
    if (key === undefined) {
      key = newChar;
    } else {
      key += newChar;
    }
  };

  actions[INC_SUB_PATH_DEPTH] = function () {
    actions[APPEND]();
    subPathDepth++;
  };

  actions[PUSH_SUB_PATH] = function () {
    if (subPathDepth > 0) {
      subPathDepth--;
      mode = IN_SUB_PATH;
      actions[APPEND]();
    } else {
      subPathDepth = 0;
      if (key === undefined) { return false }
      key = formatSubPath(key);
      if (key === false) {
        return false
      } else {
        actions[PUSH]();
      }
    }
  };

  function maybeUnescapeQuote () {
    var nextChar = path[index + 1];
    if ((mode === IN_SINGLE_QUOTE && nextChar === "'") ||
      (mode === IN_DOUBLE_QUOTE && nextChar === '"')) {
      index++;
      newChar = '\' + nextChar;
      actions[APPEND]();
      return true
    }
  }

  while (mode !== null) {
    index++;
    c = path[index];

    if (c === '\' && maybeUnescapeQuote()) {
      continue
    }

    type = getPathCharType(c);
    typeMap = pathStateMachine[mode];
    transition = typeMap[type] || typeMap['else'] || ERROR;

    if (transition === ERROR) {
      return // parse error
    }

    mode = transition[0];
    action = actions[transition[1]];
    if (action) {
      newChar = transition[2];
      newChar = newChar === undefined
        ? c
        : newChar;
      if (action() === false) {
        return
      }
    }

    if (mode === AFTER_PATH) {
      return keys
    }
  }
}

通过key解析的数组,一层一层获取值

返回结果是对象/数组,则直接返回对象/数组。如果是string/funtion则继续调用_render函数返回解析的翻译结果,否则返回null,控制台warning

如果是字符串,则执行插值函数_interpolate,传递参数为拿到的字符串,和插值values。

// 解析插值语法,判断{},拿到cout,放入tokens数组,类型是named,正常文字类型为text

最后将tokens拼接成最终的翻译文案。

type === named时,执行如下语句获取最终文案,为text则直接push

2、定义的翻译属性,若为函数:

创建一个ctx上下文,作为参数,执行message定义的函数。则调用该函数,并传入一个上下文对象作为参数,最终返回字符串。

总结:在vue原型上注册$t函数,并传入key路径,和values插值,当然内部还会获取i18n的locale语言状态和messages翻译对象。 通过messages[locale]即可获取对于语言的翻译对象。将key路径解析成一个数组,并遍历获取到messagees[locale][key]对应的string或者funtion,如果是string则,解析string内的插值语法,返回最终文案。如果是funtion则调用该funtion,并创建一个上下文为参数传入,最终返回一个string。

2、vue-i18n如何实现无刷新动态切换语言的?

// VueI18n 其实不是一个 Vue 对象,但是它在内部建立了 Vue 的实例对象 vm,所以很多的功能都是跟这个 vm 关联的

 watchI18nData (): Function {
    const self = this
    return this._vm.$watch('$data', () => {
    // 订阅的组件
      const listeners = arrayFrom(this._dataListeners)
      let i = listeners.length
      while(i--) {
        Vue.nextTick(() => {
          listeners[i] && listeners[i].$forceUpdate()
        })
      }
    }, { deep: true })
  }

_initVM 方法中,我们会将翻译相关的数据 data 通过 new Vue 传递给 this._vm 实例。mixin注入berforMounted在vue实例中,若vue实例有i18n对象则收集依赖this,并在beforeDestroy中移除订阅者。当locale、插值对象等发生改变时,调用this.$forceUpdate更新视图。使用vue的watch响应式时,并创建类似vue响应式的发布订阅者模式。