安装

vue2版需要安装8.x版本的,9.x的是vue3版本

使用上大同小异。

vue2安装:

yarn add vue-i18n@8

vue3安装:

yarn add vue-i18n

封装

官方虽然支持很不错的用法,但是自定义处理是难免的。

vue3

文件目录结构

├─ src
│  ├─ language
│  │  ├─ lang
│  │  │  ├─ en.json
│  │  │  └─ zh.json
│  │  ├─ core
│  │  │  ├─ i18n.ts
│  │  │  ├─ customization.ts
│  │  │  └─ language.ts
│  │  ├─ index.ts
│  │  └─ types.ts
│  ├─ main.ts

lang目录中存放所有翻译的文件,语言文件建议就取前两位的小写,因为通过浏览器navigator.language取得的语言文件会有很多变种:en-US、en-AU...;这并不利于我们判断,所以我的做法就是取其前两位的小写。

core为核心文件:

  • language.ts 封装的自定义语言处理类,比如首次加载语言,异步加载语言,切换语言
  • customization.ts 自定义多语言处理,对$t的二次封装
  • i18n.ts 实例化处理,频繁的修改在此文件

其他:

  • index.ts 入口文件,包含i18n注册,导出一些自定义语言处理函数
  • types.ts 一些类型声明

本来我是想用用9版本的i18n的Composition API写法的,但是考虑到需要适配两个vue的版本,所以统一采用低版本的写法,有兴趣可以查看官方的这种写法:《Composition API》

下面是核心代码了!

language.ts

/*
 * @Author: mulingyuer
 * @Date: 2022-05-30 13:59:14
 * @LastEditTime: 2022-05-30 16:38:59
 * @LastEditors: mulingyuer
 * @Description:
 * @FilePath: \casino-xian\src\language\core\language.ts
 * 怎么可能会有bug!!!
 */
import { I18n } from "vue-i18n";
import { getItem, setItem } from "@/utils";
import { nextTick } from "vue";
import { LanguageOptions } from "../types";

class Language {
  private i18n: I18n; //i18n实例
  private defaultLocale = "en"; //默认语言
  private SUPPORT_LOCALES: Array<string> = []; //支持的语言数组
  private localKey = "language"; //本地持久化key
  private loadLocale: Array<string> = []; //已加载的语言

  constructor(i18n: I18n, options: LanguageOptions) {
    this.i18n = i18n;

    if (options) {
      this.initOptions(options);
    }
  }

  private initOptions(options: LanguageOptions): void {
    const { defaultLocale, supportLocales, localKey, loadLocale } = options;
    if (typeof defaultLocale === "string" && defaultLocale.trim() !== "") {
      this.defaultLocale = defaultLocale;
    }

    if (Array.isArray(supportLocales)) {
      this.SUPPORT_LOCALES = supportLocales;
    }

    if (this.SUPPORT_LOCALES.length === 0) {
      this.SUPPORT_LOCALES.push(this.defaultLocale);
    }

    if (typeof localKey === "string" && localKey.trim() !== "") {
      this.localKey = localKey;
    }

    if (Array.isArray(loadLocale)) {
      this.loadLocale = loadLocale;
    }
  }

  /**
   * @description: 设置语言
   * @param {string} locale 语言
   * @Date: 2022-05-16 16:48:21
   * @Author: mulingyuer
   */
  private setI18nLanguage(locale: string) {
    if (this.i18n.mode === "legacy") {
      this.i18n.global.locale = locale;
    } else {
      (this.i18n.global.locale as any).value = locale;
    }

    document.querySelector("html")?.setAttribute("lang", locale);

    setItem(this.localKey, locale);
  }

  /**
   * @description: 异步加载语言并使用
   * @param {string} locale 语言
   * @Date: 2022-05-16 15:25:55
   * @Author: mulingyuer
   */
  public async loadLocaleMessages(locale: string): Promise<void> {
    try {
      //是否已经加载了
      if (this.loadLocale.includes(locale)) {
        this.setI18nLanguage(locale);
        return nextTick();
      }

      //不合法的语言
      if (!this.SUPPORT_LOCALES.includes(locale)) {
        //使用默认的语言
        return this.loadLocaleMessages(this.defaultLocale);
      }

      //加载语言
      const messages = await import(/* webpackChunkName: "locale-[request]" */ `../lang/${locale}.json`);

      //设置i18n的messages
      this.i18n.global.setLocaleMessage(locale, messages.default);
      this.loadLocale.push(locale);
      this.setI18nLanguage(locale);

      return nextTick();
    } catch (error) {
      console.error(`获取语言文件失败:${locale}`, error);

      return nextTick();
    }
  }

  /**
   * @description: 首次加载语言文件
   * @Date: 2022-05-16 17:18:33
   * @Author: mulingyuer
   */
  public async firstLoadI18nLocale(specifyLang?: string) {
    const htmlLang = this.getDocumentLanguage();
    const localLang = getItem(this.localKey);
    const systemLang = this.getStystemLanguage();
    const lang = specifyLang || localLang || systemLang || htmlLang || this.defaultLocale;

    await this.loadLocaleMessages(lang);
    this.firstLoadI18nLocale = () => Promise.resolve();
  }

  /**
   * @description: 获取系统语言
   * @Date: 2022-05-18 19:39:48
   * @Author: mulingyuer
   */
  private getStystemLanguage() {
    const lang = navigator.language;
    return lang ? lang.substring(0, 2).toLocaleLowerCase() : "";
  }

  /**
   * @description: 获取文档语言
   * @Date: 2022-05-19 10:54:51
   * @Author: mulingyuer
   */
  private getDocumentLanguage() {
    const lang = document.documentElement.getAttribute("lang");
    return lang ? lang.substring(0, 2).toLocaleLowerCase() : "";
  }
}

export default Language;

types.ts

/*
 * @Author: mulingyuer
 * @Date: 2022-05-20 14:18:51
 * @LastEditTime: 2022-05-30 15:36:33
 * @LastEditors: mulingyuer
 * @Description: 类型声明
 * @FilePath: \casino-xian\src\language\types.ts
 * 怎么可能会有bug!!!
 */

//api中的info字段
export type Info = string | null | undefined;
//language类的options
export type LanguageOptions = {
  defaultLocale?: string;
  supportLocales: Array<string>;
  localKey?: string;
  loadLocale?: Array<string>;
};

getItem, setItem是对于localStorage的封装,可以看成是:localStorage.getItem,localStorage.setItem

customization.ts

import i18n from "./i18n";

const $t = i18n.global.t;

/**
 * @description: 多语言保底输出 tOriginalOutput
 * @param message 文本
 * @Date: 2022-05-19 19:36:23
 * @Author: mulingyuer
 */
export function $toop(message: string, backMessage: any = "") {
  const langMessage = $t(message);
  //如果没有对应的文本,用最后一个.后面的内容作为翻译文本
  if (langMessage === message) {
    return backMessage;
  }

  return langMessage;
}

i18n.ts

/*
 * @Author: mulingyuer
 * @Date: 2022-05-19 17:21:29
 * @LastEditTime: 2022-05-30 16:34:40
 * @LastEditors: mulingyuer
 * @Description: i18n
 * @FilePath: \casino-xian\src\language\core\i18n.ts
 * 怎么可能会有bug!!!
 */
import { createI18n } from "vue-i18n";
import Language from "./language";

//创建实例
const i18n = createI18n({
  // locale: getItem("language") || defaultLocale, //读取本地存储的语言
  datetimeFormats: {},
  numberFormats: {},
  globalInjection: true, //是否全局注入
  // fallbackLocale: "en", //回退用的语言
  messages: {
    //   en: require("../lang/en.json"),
  },
});

const language = new Language(i18n, {
  supportLocales: ["en", "zh"],
  defaultLocale: "en",
});

export { language };
export default i18n;

index.ts

/*
 * @Author: mulingyuer
 * @Date: 2022-03-04 11:30:23
 * @LastEditTime: 2022-05-30 16:34:59
 * @LastEditors: mulingyuer
 * @Description: 多语言
 * @FilePath: \casino-xian\src\language\index.ts
 * 怎么可能会有bug!!!
 */
import i18n, { language } from "./core/i18n";
import { $toop } from "./core/customization";

//方便的获取i18n的方法
const $t = i18n.global.t;

export { language, $toop, $t };

export default {
  install(app: any) {
    //全局注册自定义方法
    app.config.globalProperties.$toop = $toop;
    //注册i18n
    app.use(i18n);
  },
};

基本处理都在这里了,下面是使用!

vue3使用

main.ts中注册

import { createApp } from "vue";
import App from "./App.vue";

import VueI18n from "./language";


createApp(App).use(VueI18n);

路由守卫中使用

因为语言采用异步加载的方式引入,所以需要保证在页面首次进入时加载了一份语言包,如果在i18n.ts初始化i18n实例时默认在messages中require引入了一份语言,其实就可以考虑不用再路由守卫中处理首次加载的问题,因为你一定会有一份语言包的。

import { language } from "@/language";


router.beforeEach(async (to, from, next) => {
    //首次加载语言文件
    await language.firstLoadI18nLocale();

    next();
});

切换语言

import { language } from "@/language";


language.loadLocaleMessages("zh");

基本使用就是这样了,后续可以将支持的语言导出,作为在vue中for循环遍历的数组啥的。

类封装的是通用的逻辑,如果业务变化需要改动,尽量改动i18n.ts中的逻辑。

vue2封装

vue2版大同小异,需要注意的是i18n的使用,以及注册挂载的问题。目录结构也是一样的。

language.ts

/*
 * @Author: mulingyuer
 * @Date: 2022-05-30 13:59:14
 * @LastEditTime: 2022-05-30 16:39:40
 * @LastEditors: mulingyuer
 * @Description:
 * @FilePath: \casino-vue2.0\src\language\core\language.ts
 * 怎么可能会有bug!!!
 */
import { IVueI18n } from "vue-i18n";
import { getItem, setItem } from "@/utils";
import Vue from "vue";
import { LanguageOptions } from "../types";

class Language {
  private i18n: IVueI18n; //i18n实例
  private defaultLocale = "en"; //默认语言
  private SUPPORT_LOCALES: Array<string> = []; //支持的语言数组
  private localKey = "language"; //本地持久化key
  private loadLocale: Array<string> = []; //已加载的语言

  constructor(i18n: IVueI18n, options: LanguageOptions) {
    this.i18n = i18n;

    if (options) {
      this.initOptions(options);
    }
  }

  private initOptions(options: LanguageOptions): void {
    const { defaultLocale, supportLocales, localKey, loadLocale } = options;
    if (typeof defaultLocale === "string" && defaultLocale.trim() !== "") {
      this.defaultLocale = defaultLocale;
    }

    if (Array.isArray(supportLocales)) {
      this.SUPPORT_LOCALES = supportLocales;
    }

    if (this.SUPPORT_LOCALES.length === 0) {
      this.SUPPORT_LOCALES.push(this.defaultLocale);
    }

    if (typeof localKey === "string" && localKey.trim() !== "") {
      this.localKey = localKey;
    }

    if (Array.isArray(loadLocale)) {
      this.loadLocale = loadLocale;
    }
  }

  /**
   * @description: 设置语言
   * @param {string} locale 语言
   * @Date: 2022-05-16 16:48:21
   * @Author: mulingyuer
   */
  private setI18nLanguage(locale: string) {
    this.i18n.locale = locale;

    document.querySelector("html")?.setAttribute("lang", locale);

    setItem(this.localKey, locale);
  }

  /**
   * @description: 异步加载语言并使用
   * @param {string} locale 语言
   * @Date: 2022-05-16 15:25:55
   * @Author: mulingyuer
   */
  public async loadLocaleMessages(locale: string): Promise<void> {
    try {
      //是否已经加载了
      if (this.loadLocale.includes(locale)) {
        this.setI18nLanguage(locale);
        return Vue.nextTick();
      }

      //不合法的语言
      if (!this.SUPPORT_LOCALES.includes(locale)) {
        //使用默认的语言
        return this.loadLocaleMessages(this.defaultLocale);
      }

      //加载语言
      const messages = await import(/* webpackChunkName: "locale-[request]" */ `../lang/${locale}.json`);

      //设置i18n的messages
      this.i18n.setLocaleMessage(locale, messages.default);
      this.loadLocale.push(locale);
      this.setI18nLanguage(locale);

      return Vue.nextTick();
    } catch (error) {
      console.error(`获取语言文件失败:${locale}`, error);

      return Vue.nextTick();
    }
  }

  /**
   * @description: 首次加载语言文件
   * @Date: 2022-05-16 17:18:33
   * @Author: mulingyuer
   */
  public async firstLoadI18nLocale(specifyLang?: string) {
    const htmlLang = this.getDocumentLanguage();
    const localLang = getItem(this.localKey);
    const systemLang = this.getStystemLanguage();
    const lang = specifyLang || localLang || systemLang || htmlLang || this.defaultLocale;

    await this.loadLocaleMessages(lang);
    this.firstLoadI18nLocale = () => Promise.resolve();
  }

  /**
   * @description: 获取系统语言
   * @Date: 2022-05-18 19:39:48
   * @Author: mulingyuer
   */
  private getStystemLanguage() {
    const lang = navigator.language;
    return lang ? lang.substring(0, 2).toLocaleLowerCase() : "";
  }

  /**
   * @description: 获取文档语言
   * @Date: 2022-05-19 10:54:51
   * @Author: mulingyuer
   */
  private getDocumentLanguage() {
    const lang = document.documentElement.getAttribute("lang");
    return lang ? lang.substring(0, 2).toLocaleLowerCase() : "";
  }
}

export default Language;

types.ts

/*
 * @Author: mulingyuer
 * @Date: 2022-05-20 14:18:51
 * @LastEditTime: 2022-05-30 16:06:38
 * @LastEditors: mulingyuer
 * @Description: 类型声明
 * @FilePath: \casino-vue2.0\src\language\types.ts
 * 怎么可能会有bug!!!
 */

//api中的info字段
export type Info = string | null | undefined;
//language类的options
export type LanguageOptions = {
  defaultLocale?: string;
  supportLocales: Array<string>;
  localKey?: string;
  loadLocale?: Array<string>;
};

customization.ts

/*
 * @Author: mulingyuer
 * @Date: 2022-05-19 17:24:44
 * @LastEditTime: 2022-05-30 16:44:18
 * @LastEditors: mulingyuer
 * @Description: 自定义转换方法
 * @FilePath: \casino-vue2.0\src\language\core\customization.ts
 * 怎么可能会有bug!!!
 */
import i18n from "./i18n";

const $t = i18n.t.bind(i18n);

/**
 * @description: 多语言保底输出 tOriginalOutput
 * @param message 文本
 * @Date: 2022-05-19 19:36:23
 * @Author: mulingyuer
 */
export function $toop(message: string, backMessage: any = "") {
  const langMessage = $t(message);
  //如果没有对应的文本,用最后一个.后面的内容作为翻译文本
  if (langMessage === message) {
    return backMessage;
  }

  return langMessage;
}

i18n.ts

/*
 * @Author: mulingyuer
 * @Date: 2022-05-20 15:55:00
 * @LastEditTime: 2022-05-30 16:44:13
 * @LastEditors: mulingyuer
 * @Description:i18n
 * @FilePath: \casino-vue2.0\src\language\core\i18n.ts
 * 怎么可能会有bug!!!
 */
import Vue from "vue";
import VueI18n from "vue-i18n";
import Language from "./language";
Vue.use(VueI18n);

//创建实例
const i18n = new VueI18n({
  silentTranslationWarn: true,
  // locale: defaultLocale,
  // fallbackLocale: "en", //回退用的语言
  // messages: {
  //   en: require("../lang/en.json"),
  // },
});

const language = new Language(i18n, {
  supportLocales: ["en", "zh"],
  defaultLocale: "en",
});

export { language };
export default i18n;

因为vue3版和vue2的ts类型声明不一样,你会发现在实例化i18n的时候,部分参数不同。

index.ts

/*
 * @Author: mulingyuer
 * @Date: 2022-05-20 15:55:00
 * @LastEditTime: 2022-05-30 16:44:35
 * @LastEditors: mulingyuer
 * @Description: 多语言
 * @FilePath: \casino-vue2.0\src\language\index.ts
 * 怎么可能会有bug!!!
 */
import i18n, { language } from "./core/i18n";
import { $toop } from "./core/customization";

//方便的获取i18n的方法
const $t = i18n.t.bind(i18n);

export { language, $toop, $t };

export const installI18n = {
  install(app: any) {
    //全局注册方法
    app.prototype.$toop = $toop;
  },
};

export default i18n;

细心点你会发现,在vue3版中,我们从i18n实例上获取t是直接拿到的函数,而在vue2版需要进行绑定this指向为i18n实例自身。

应该是vue3版改动较大,支持这种用法,也可能是凑巧,如果以后不行了,可以尝试也进行一次bind。

vue2版使用

main.ts中注册

import  Vue  from  "vue";
import App from "./App.vue";

import i18n, { installI18n } from "@/language";
Vue.use(installI18n);

new Vue({
  i18n,
  render: (h) => h(App),
}).$mount("#app");

路由守卫中使用

因为语言采用异步加载的方式引入,所以需要保证在页面首次进入时加载了一份语言包,如果在i18n.ts初始化i18n实例时默认在messages中require引入了一份语言,其实就可以考虑不用再路由守卫中处理首次加载的问题,因为你一定会有一份语言包的。

import { language } from "@/language";


router.beforeEach(async (to, from, next) => {
    //首次加载语言文件
    await language.firstLoadI18nLocale();

    next();
});

切换语言

import { language } from "@/language";


language.loadLocaleMessages("zh");

基本使用就是这样了,后续可以将支持的语言导出,作为在vue中for循环遍历的数组啥的。

类封装的是通用的逻辑,如果业务变化需要改动,尽量改动i18n.ts中的逻辑。

自定义语言处理

在上面的代码封装中,我们封装了一个$toop的函数,这个函数会挂载到vue的全局。

为此,我们可以直接在template模板语法中使用。

<template>
  <div>
    {{ $toop("xxx.xxx.xx","没有对应的值,采用我这个回退值") }}
  </div>
</template>

这个方法的考虑:

语言包文件为了更好的语义化和维护,肯定会有层级嵌套的,即便他只是一个json文件,他可能会是这种结构:

{ 
  "a": {
    "b": {
      "c" : "value"
    }
  }
}

但是有时候你不能保证这个层级下一定会有你的值,比如这个值可能是一个动态的后端返的值,你的语言包只存在旧的数据,新的数据没有这个,那么$t它会默认将你的key字符串原样返回,显然这并不能很好的回退,所以我自定义了$toop,我会使用第二个参数作为回退的值。

更多的自定义可以自己去尝试。

也正是因为我需要全局注册自定义方法,所以index.ts看上去好像有些复杂。

vue-i18n官网

vue2版:vue-i18n

vue3版:vue-i18n

一些注意事项

  1. vue-i18n的语言包val值只能是字符串,数字会被视为不存在对应的翻译,如:
{
  "name": 1
}

这种会被视为不存在翻译,仔细一想也是,多语言本来就是翻译字符串的,这么做好像也没错,但是我们平时使用还是得注意下。

  1. 关闭警告

在new i18n实例的时候配置一下即可:

//创建实例
const i18n = createI18n({
  // locale: getItem("language") || defaultLocale, //读取本地存储的语言
  datetimeFormats: {},
  numberFormats: {},
  globalInjection: true, //是否全局注入
  // fallbackLocale: "en", //回退用的语言
  messages: {
    //   en: require("../lang/en.json"),
  },
  silentTranslationWarn: true, //是否显示警告
  silentFallbackWarn: true, //是否显示回退警告
});
分类: vue 项目实战 标签: vuevue-i18n多语言

评论

全部评论 2

  1. vue2
    vue2
    Google Chrome Windows 10

    对于vue2来货,这个没有啊, 请问有没有什么解决方法?
    import { getItem, setItem } from "@/utils";

    1. 木灵鱼儿
      木灵鱼儿
      FireFox Windows 10
      @vue2getItem, setItem是对于localStorage的封装,可以看是:localStorage.getItem,localStorage.setItem.............. 我不是说了吗?[疑惑]

目录