vue3项目国际化i18n

7,217 阅读3分钟

01:国际化实现原理

先来看一个需求:

我们有一个变量 msg ,但是这个 msg 有且只能有两个值:

  1. hello world
  2. 你好世界

要求:根据需要切换 msg 的值

这样的一个需求就是 国际化 的需求,那么我们可以通过以下代码来实现这个需求

<script>
  // 1. 定义 msg 值的数据源
  const messages = {
    en: {
      msg: 'hello world'
    },
    zh: {
      msg: '你好世界'
    }
  }
  // 2. 定义切换变量
  let locale = 'en'
  // 3. 定义赋值函数
  function t(key) {
    return messages[locale][key]
  }
  // 4. 为 msg 赋值 
  let msg = t('msg')
  console.log(msg);
  // 修改 locale, 重新执行 t 方法,获取不同语言环境下的值

</script>

总结:

  1. 通过一个变量来 控制 语言环境
  2. 所有语言环境下的数据源要 预先 定义好
  3. 通过一个方法来获取 当前语言指定属性 的值
  4. 该值即为国际化下展示值

02:基于 vue-i18n V9 的国际化实现方案分析

vue 的项目中,我们不需要手写这么复杂的一些基础代码,可以直接使用 vue-i18n 进行实现(注意:vue3 下需要使用 V 9.xi18n

vue-i18n 的使用可以分为四个部分:

  1. 创建 messages 数据源
  2. 创建 locale 语言变量
  3. 初始化 i18n 实例
  4. 注册 i18n 实例

那么接下来我们就去实现以下:

  1. 安装 vue-i18n

    npm install vue-i18n@next
    
  2. 创建 src/i18n/index.js 文件

  3. 创建 messages 数据源

    // src/i18n/index.js
       const messages = {
       en: {
         msg: {
           test: "hello world",
         },
       },
       zh: {
         msg: {
           test: "你好世界",
         },
       },
     };
    
  4. 创建 locale 语言变量

    // src/i18n/index.js
    const locale = "en";
    
  5. 初始化 i18n 实例

    // src/i18n/index.js
    import { createI18n } from "vue-i18n";
    
    const i18n = createI18n({
       // 使用 Composition API 模式,则需要将其设置为false
       legacy: false,
       // 全局注入 $t 函数
       globalInjection: true,
       locale,
       messages,
     });
    
    export default i18n
    
  6. i18n 注册到 vue 实例 在 main.js 中导入

    // i18n (PS:导入放到 APP.vue 导入之前,因为后面我们会在 app.vue 中使用国际化内容)
    import i18n from "@/i18n";
    ...
    createApp(App).use(store).use(router).use(i18n).mount("#app");
    
  7. App.vue 中使用 i18n

     <template>
       <h2>{{ $t("msg.test") }}</h2>
     </template>
    
  8. 修改 locale 的值,即可改变展示的内容

    const locale = "zh";
    

截止到现在我们已经实现了 i18n 的最基础用法,那么接下来我们就可以在项目中使用 i18n 完成国际化。

项目中完成国际化分成以下几步进行:

  1. 封装 langSelect 组件用于修改 locale
  2. 导入 el-locale 语言包
  3. 创建自定义语言包

03:方案落地:封装 langSelect 组件

  1. constant 中定义常量

    // 国际化
    export const LANG = "language";
    
  2. 定义 store/index.js

    import { createStore } from "vuex";
    import { LANG } from "@/constant";
    import getters from "./getters.js";
    
    export default createStore({
       state: {
         language: localStorage.getItem(LANG) || "zh",
       },
       mutations: {
         setLanguage(state, lang) {
           // 设置缓存
           localStorage.setItem(LANG, lang);
           // 修改状态
           state.language = lang;
         },
       },
       actions: {},
       modules: {},
       getters,
     });
    

    在getters定义快捷访问

    // store/getters
    const getters = {
       language: (state) => state.language,
     };
     export default getters;
    
  3. 创建 components/LangSelect/index

     // 在此之前npm安装element-plus导入 npm i element-plus
     import "element-plus/dist/index.css";
     import ElementPlus from "element-plus";
     createApp(App).use(store).use(router).use(i18n).use(ElementPlus).mount("#app");
    
     <template>
       <el-dropdown
         trigger="click"
         class="international"
         @command="handleSetLanguage"
       >
         <div>
           <el-tooltip content="国际化" :effect="effect">
             <el-button>切换</el-button>
           </el-tooltip>
         </div>
         <template #dropdown>
           <el-dropdown-menu>
             <el-dropdown-item :disabled="language === 'zh'" command="zh">
               中文
             </el-dropdown-item>
             <el-dropdown-item :disabled="language === 'en'" command="en">
               English
             </el-dropdown-item>
           </el-dropdown-menu>
         </template>
       </el-dropdown>
     </template>
    
     <script setup>
     import { useI18n } from "vue-i18n";
     import { defineProps, computed } from "vue";
     import { useStore } from "vuex";
     import { ElMessage } from "element-plus";
    
     defineProps({
       effect: {
         type: String,
         default: "dark",
         validator: function (value) {
           // 这个值必须匹配下列字符串中的一个
           return ["dark", "light"].includes(value);
         },
       },
     });
    
     const store = useStore();
     const language = computed(() => store.getters.language);
    
     // 切换语言的方法
     const i18n = useI18n();
     const handleSetLanguage = (lang) => {
       i18n.locale.value = lang;
       store.commit("setLanguage", lang);
       ElMessage.success(`语言成功切换为${lang === "zh" ? "中文" : "英文"}`);
     };
     </script>
    
    
  4. 在 任意组件 例如App.vue 中导入 LangSelect使用

     <template>
       <div>
         <lang-select />
       </div>
     </template>
    
     <script setup>
     import LangSelect from "@/components/LangSelect";
     </script>
    
    

04:方案落地:element-plus 国际化处理

截止到目前,我们的国际化内容已经基本功能已经处理完成了。接下来需要处理的就是对应的语言包,有了语言包就可以实现整个项目中的所有国际化处理了。

那么对于语言包来说,我们整个项目中会分成两部分:

  1. element-plus 语言包:用来处理 element 组件的国际化功能
  2. 自定义语言包:用来处理 element 组件的国际化功能

那么首先我们先来处理 element-plus 语言包:

**按照正常的逻辑,我们是可以通过 element-ui 配合 vue-i18n来实现国际化功能的,但是目前的 element-plus 尚未提供配合 vue-i18n 实现国际化的方式! **

所以说,我们暂时只能先去做临时处理,等到 element-plus 支持 vue-i18n 功能之后,我们再进行对接实现

那么临时处理我们怎么去做呢?

  1. 升级 element-plus 到最新版本

    npm i element-plus
    
  2. 接下来实现国际化

  3. plugins/element 中导入 element 的中文、英文语言包:

    import zhCn from 'element-plus/es/locale/lang/zh-cn'
    import en from 'element-plus/lib/locale/lang/en'
    
  4. 注册 element 时,根据当前语言选择使用哪种语言包

    import store from '@/store'
    
    export default app => {
      app.use(ElementPlus, {
        locale: store.getters.language === 'en' ? en : zhCn
      })
    }
    

05:方案落地:自定义语言包国际化处理

处理完 element 的国际化内容之后,接下来我们来处理 自定义语言包

自定义语言包我们导出了一个对象,这个对象就是所有的 自定义语言对象

大家可以在网上寻找一些语言包

展开英文语言包 export default { login: { title: 'User Login', loginBtn: 'Login', usernameRule: 'Username is required', passwordRule: 'Password cannot be less than 6 digits', desc: ` Test authority account:
Provide three kinds of authority accounts:
1. Super administrator account: super-admin
2. Administrator account: admin
3. Test configurable account: test
The uniform password is: 123456

Import user account:
You can log in with the imported username
The password is unified as: 123456
Note: Import user-discriminatory Chinese and English libraries! ! ! ! ` }, route: { profile: 'Profile', user: 'user', excelImport: 'ExcelImport', userManage: 'EmployeeManage', userInfo: 'UserInfo', roleList: 'RoleList', permissionList: 'PermissionList', article: 'article', articleRanking: 'ArticleRanking', articleCreate: 'ArticleCreate', articleDetail: 'ArticleDetail', articleEditor: 'ArticleEditor' }, toast: { switchLangSuccess: 'Switch Language Success' }, tagsView: { refresh: 'Refresh', closeRight: 'Close Rights', closeOther: 'Close Others' }, theme: { themeColorChange: 'Theme Color Change', themeChange: 'Theme Change' }, universal: { confirm: 'confirm', cancel: 'cancel' }, navBar: { themeChange: 'Theme Modification', headerSearch: 'Page Search', screenfull: 'Full Screen Replacement', lang: 'Globalization', guide: 'Function Guide', home: 'Home', course: 'Course homepage', logout: 'Log out' }, guide: { close: 'close', next: 'next', prev: 'previous', guideTitle: 'guidance', guideDesc: 'Turn on the boot function', hamburgerTitle: 'Hamburger button', hamburgerDesc: 'Open and close the left menu', breadcrumbTitle: 'Bread crumbs', breadcrumbDesc: 'Indicates the current page position', searchTitle: 'search', searchDesc: 'Page link search', fullTitle: 'full screen', fullDesc: 'Page display switching', themeTitle: 'theme', themeDesc: 'Change project theme', langTitle: 'globalization', langDesc: 'Language switch', tagTitle: 'Label', tagDesc: 'Opened page tab', sidebarTitle: 'menu', sidebarDesc: 'Project function menu' }, profile: { muted: '"Vue3 rewrite vue-element-admin, realize the back-end front-end integrated solution" project demonstration', introduce: 'Introduce', projectIntroduction: 'Project Introduction', projectFunction: 'Project Function', feature: 'Feature', chapter: 'Chapter', author: 'Author', name: 'Sunday', job: 'A front-end development program', Introduction: 'A senior technical expert, once worked in a domestic first-line Internet company, and has coordinated multiple large-scale projects with more than tens of millions of users. Committed to researching big front-end technology, he has been invited to participate in domestic front-end technology sharing sessions many times, such as: Google China Technology Sharing Session in 2018.' }, userInfo: { print: 'Print', title: 'Employee information', name: 'name', sex: 'gender', nation: 'nationality', mobile: 'phone number', province: 'Place of residence', date: 'Entry Time', remark: 'Remark', address: 'contact address', experience: 'Experience', major: 'Professional', glory: 'Glory', foot: 'Signature:___________Date:___________' }, uploadExcel: { upload: 'Click upload', drop: 'Drag files here' }, excel: { importExcel: 'excel import', exportExcel: 'excel export', exportZip: 'zip export', name: 'Name', mobile: 'contact details', avatar: 'Avatar', role: 'Role', openTime: 'Opening time', action: 'Operate', show: 'Check', showRole: 'Role', defaultRole: 'Staff', remove: 'delete', removeSuccess: 'Deleted successfully', title: 'Export to excel', placeholder: 'excel file name', defaultName: 'Staff Management Form', close: 'Cancel', confirm: 'Export', importSuccess: ' Employee data imported successfully', dialogTitle1: 'Are you sure you want to delete the user ', dialogTitle2: ' Is it?', roleDialogTitle: 'Configure roles' }, role: { buttonTxt: 'New Role', index: 'Serial number', name: 'name', desc: 'describe', action: 'operate', assignPermissions: 'assign permissions', removeRole: 'Delete role', dialogTitle: 'New role', dialogRole: 'Role Name', dialogDesc: 'Role description', updateRoleSuccess: 'User role updated successfully' }, permission: { name: 'Authority name', mark: 'Authority ID', desc: 'Permission description' }, article: { ranking: 'Ranking', title: 'Title', author: 'Author', publicDate: 'release time', desc: 'brief introduction', action: 'operate', dynamicTitle: 'Dynamic display', show: 'check', remove: 'delete', edit: 'editor', dialogTitle1: 'Are you sure you want to delete the article ', dialogTitle2: ' NS?', removeSuccess: 'Article deleted successfully', titlePlaceholder: 'Please enter the title of the article', markdown: 'Markdown', richText: 'Rich Text', commit: 'commit', createSuccess: 'The article was created successfully', editorSuccess: 'Article modified successfully', sortSuccess: 'Article ranking modified successfully' } }
展开中文语言包export default { login: { title: '用户登录', loginBtn: '登录', usernameRule: '用户名为必填项', passwordRule: '密码不能少于6位', desc: ` 测试权限账号:
提供三种权限账号:
1. 超级管理员账号: super-admin
2. 管理员账号:admin
3. 测试可配置账号:test
密码统一为:123456

导入用户账号:
可使用导入的用户名登录
密码统一为:123456
注意:导入用户区分中英文库!!!! ` }, route: { profile: '个人中心', user: '用户', excelImport: 'Excel导入', userManage: '员工管理', userInfo: '员工信息', roleList: '角色列表', permissionList: '权限列表', article: '文章', articleRanking: '文章排名', articleCreate: '创建文章', articleDetail: '文章详情', articleEditor: '文章编辑' }, toast: { switchLangSuccess: '切换语言成功' }, tagsView: { refresh: '刷新', closeRight: '关闭右侧', closeOther: '关闭其他' }, theme: { themeColorChange: '主题色更换', themeChange: '主题更换' }, universal: { confirm: '确定', cancel: '取消' }, navBar: { themeChange: '主题修改', headerSearch: '页面搜索', screenfull: '全屏替换', lang: '国际化', guide: '功能引导', home: '首页', course: '课程主页', logout: '退出登录' }, guide: { close: '关闭', next: '下一个', prev: '上一个', guideTitle: '引导', guideDesc: '打开引导功能', hamburgerTitle: '汉堡按钮', hamburgerDesc: '打开和关闭左侧菜单', breadcrumbTitle: '面包屑', breadcrumbDesc: '指示当前页面位置', searchTitle: '搜索', searchDesc: '页面链接搜索', fullTitle: '全屏', fullDesc: '页面显示切换', themeTitle: '主题', themeDesc: '更换项目主题', langTitle: '国际化', langDesc: '语言切换', tagTitle: '标签', tagDesc: '已打开页面标签', sidebarTitle: '菜单', sidebarDesc: '项目功能菜单' }, profile: { muted: '《vue3 改写 vue-element-admin,实现后台前端综合解决方案》项目演示', introduce: '介绍', projectIntroduction: '项目介绍', projectFunction: '项目功能', feature: '功能', chapter: '章节', author: '作者', name: 'Sunday', job: '一个前端开发程序猿', Introduction: '高级技术专家,曾就职于国内一线互联网公司,统筹过的多个大型项目用户数已过千万级。致力于研究大前端技术,多次受邀参加国内前端技术分享会,如:2018 年 Google 中国技术分享会。' }, userInfo: { print: '打印', title: '员工信息', name: '姓名', sex: '性别', nation: '民族', mobile: '手机号', province: '居住地', date: '入职时间', remark: '备注', address: '联系地址', experience: '经历', major: '专业', glory: '荣耀', foot: '签字:___________日期:___________' }, uploadExcel: { upload: '点击上传', drop: '将文件拖到此处' }, excel: { importExcel: 'excel 导入', exportExcel: 'excel 导出', exportZip: 'zip 导出', name: '姓名', mobile: '联系方式', avatar: '头像', role: '角色', openTime: '开通时间', action: '操作', show: '查看', showRole: '角色', defaultRole: '员工', remove: '删除', removeSuccess: '删除成功', title: '导出为 excel', placeholder: 'excel 文件名称', defaultName: '员工管理表', close: '取 消', confirm: '导 出', importSuccess: ' 条员工数据导入成功', dialogTitle1: '确定要删除用户 ', dialogTitle2: ' 吗?', roleDialogTitle: '配置角色' }, role: { buttonTxt: '新增角色', index: '序号', name: '名称', desc: '描述', action: '操作', assignPermissions: '分配权限', removeRole: '删除角色', dialogTitle: '新增角色', dialogRole: '角色名称', dialogDesc: '角色描述', updateRoleSuccess: '用户角色更新成功' }, permission: { name: '权限名称', mark: '权限标识', desc: '权限描述' }, article: { ranking: '排名', title: '标题', author: '作者', publicDate: '发布时间', desc: '内容简介', action: '操作', dynamicTitle: '动态展示', show: '查看', remove: '删除', edit: '编辑', dialogTitle1: '确定要删除文章 ', dialogTitle2: ' 吗?', removeSuccess: '文章删除成功', titlePlaceholder: '请输入文章标题', markdown: 'markdown', richText: '富文本', commit: '提交', createSuccess: '文章创建成功', editorSuccess: '文章修改成功', sortSuccess: '文章排名修改成功' } }
  1. 复制 lang 文件夹到 i18n文件夹中 中

  2. lang/index.js 中,导入语言包

    import mZhLocale from "./lang/zh";
    import mEnLocale from "./lang/en";
    
  3. messages 中注册到语言包

    const messages = {
      en: {
        msg: {
          ...mEnLocale
        }
      },
      zh: {
        msg: {
          ...mZhLocale
        }
      }
    }
    

06:方案落地:处理项目国际化内容

在处理好了国际化的语言包之后,接下来我们就可以应用国际化功能到我们的项目需要的组件中,但组件中和js文件中使用导入方式有点不同,举个栗子。

登录页面:

login/index

<template>
  <div class="login-container">
    <el-form
      class="login-form"
      :model="loginForm"
      :rules="loginRules"
      ref="loginFromRef"
    >
      <div class="title-container">
        <h3 class="title">{{ $t("msg.login.title") }}</h3>
        <lang-select class="lang-select"></lang-select>
      </div>
      <el-form-item prop="username">
        <el-input
          placeholder="username"
          name="username"
          type="text"
          v-model="loginForm.username"
        />
      </el-form-item>

      <el-form-item prop="password">
        <el-input
          :type="passwordType"
          placeholder="password"
          name="password"
          v-model="loginForm.password"
        />
      </el-form-item>

      <el-button type="primary" class="submit">{{
        $t("msg.login.loginBtn")
      }}</el-button>
      <div class="tips" v-html="$t('msg.login.desc')"></div>
    </el-form>
  </div>
</template>

<script setup>
import { ref } from "vue";
import { useI18n } from "vue-i18n";
import { validatePassword } from "./rules";
import LangSelect from "./components/LangSelect/index.vue";
// 数据源
const loginForm = ref({
  username: "super-admin",
  password: "123456",
});
// 验证规则
const i18n = useI18n();
const loginRules = ref({
  username: [
    {
      required: true,
      trigger: "blur",
      message: i18n.t("msg.login.usernameRule"),
    },
  ],
  password: [
    {
      required: true,
      trigger: "blur",
      validator: validatePassword(),
    },
  ],
});
</script>

login/rules

import i18n from '@/i18n'
export const validatePassword = () => {
  return (rule, value, callback) => {
    if (value.length < 6) {
      callback(new Error(i18n.global.t('msg.login.passwordRule')))
    } else {
      callback()
    }
  }
}

07:方案落地:国际化缓存处理

我们希望在 刷新页面后,当前的国际化选择可以被保留,所以想要实现这个功能,那么就需要进行 国际化的缓存处理

此处的缓存,我们依然通过两个方面进行:

  1. vuex 缓存
  2. LocalStorage 缓存

只不过这里的缓存,我们已经在处理 langSelect 组件时 处理完成了,所以此时我们只需要使用缓存下来的数据即可。

i18n/index 中,创建 getLanguage 方法:

import store from "@/store";

// 返回当前 lang
function getLanguage() {
  return store && store.getters && store.getters.language;
}

修改 createI18nlocalegetLanguage()

const i18n = createI18n({
  // 使用 Composition API 模式,则需要将其设置为false
  legacy: false,
  // 全局注入 $t 函数
  globalInjection: true,
  locale: getLanguage(),
  messages,
});

08:国际化方案总结

国际化是前端项目中的一个非常常见的功能,那么在前端项目中实现国际化主要依靠的就是 vue-i18n 这个第三方的包。

i18n 的使用,整体来说就分为这么四步:

  1. 创建 messages 数据源
  2. 创建 locale 语言变量
  3. 初始化 i18n 实例
  4. 注册 i18n 实例

核心的内容其实就是 数据源的部分,但是大家需要注意,如果你的项目中使用了 第三方组件库 ,那么不要忘记 第三方组件库的数据源 需要 单独 进行处理!