当前位置: 代码迷 >> 综合 >> Vue+Element 国际化(i18n)页面全覆盖(路由、表单检验、弹出框等)
  详细解决方案

Vue+Element 国际化(i18n)页面全覆盖(路由、表单检验、弹出框等)

热度:68   发布时间:2023-12-25 13:44:21.0

安装 i18n

internationalization 这个单词中,i 和 n 之间有 18 个字母

注意: 当前 i18n 最新版本为 9 版本,9 版本没有 VueI18n,import VueI18n from 'vue-i18n' 解构会报错 Cannot read property 'install' of undefined

  • 这里为了让 Element 兼容,安装的是 8 版本的
npm install vue-i18n@8

基础配置篇

1.页面中使用国际化

* i18n 文件夹下的 index.js 文件

require.context

它允许传入一个目录进行搜索,一个标志指示是否应该搜索子目录,还有一个正则表达式来匹配文件

  • 第一个参数:directory 要搜索的文件夹目录不能是变量,否则在编译阶段无法定位目录
  • 第二个参数:useSubdirectories 是否搜索子目录
  • 第三个参数:regExp 匹配文件的正则表达式
require.context('demo', false, /\.js$/)
/* i18n/index.js */
import Vue from 'vue'
import VueI18n from 'vue-i18n'Vue.use(VueI18n)const langFiles = require.context('./config', false, /\.js$/)
const regExp = /\.\/([^\.\/]+)\.([^\.]+)$/
const messages = {
    }
const lang = {
    zh: zhCNLocale,zhTW: zhTWLocale,en: enLocale
}
// 默认语言
const loadLanguage = 'zh'langFiles.keys().forEach(key => {
    messages[k] = langFiles(key).default
})function getLanguage() {
    // 第一次进入页面或手动清除设置默认语言localStorage.getItem('lang') ? null : localStorage.setItem('lang', loadLanguage)let locale = localStorage.getItem('lang')if (!(locale in messages)) locale = loadLanguagereturn locale
}const i18n = new VueI18n({
    locale: getLanguage(),messages
})
export default i18n
  • messages 对象

main.js 引入

/* main.js */
import Vue from 'vue'
import App from './App.vue'
import i18n from './i18n'new Vue({
    render: h => h(App),i18n
}).$mount('#app')

展示页面

效果图如下:

<template><div><el-dropdowntrigger="click"class="international"@command="handleSetLanguage"><div><i class="el-icon-s-tools" />切换语言</div><el-dropdown-menu slot="dropdown"><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></el-dropdown><el-table :data="tableData" style="width: 100%"><el-table-column prop="date" :label="$t('table.date')" width="180"></el-table-column><el-table-column prop="name" :label="$t('table.name')" width="180"></el-table-column><el-table-column prop="age" :label="$t('table.age')"> </el-table-column></el-table></div>
</template><script> export default {
      data() {
      return {
      tableData: [{
      date: '2016-05-02',name: '王小虎',age: '23'},{
      date: '2016-05-04',name: '李小鸭',age: '14'}],language: localStorage.getItem('lang')}},methods: {
      handleSetLanguage(lang) {
      this.$i18n.locale = langthis.language = langlocalStorage.setItem('lang', lang)// window.location.reload()}} } </script>

语言配置文件

/* en.js */
export default {
    table: {
    date: 'Date',name: 'Name',age: 'Age'}
}/* zh.js */
export default {
    table: {
    date: '日期',name: '姓名',age: '年龄'}
}

英文首字母大写

英文情况下,需要对其进行大小写转换处理(让每个单词首字母大写)

for (const k in langs) {
    const item = langs[k]for (const j in item) {
    item[j] = item[j].toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase())}
}

2.Element 国际化

main.js 引入

  • 参考 兼容 vue-i18n@6.x 的步骤(需要手动处理)
/* main.js */
import Vue from 'vue'
import App from './App.vue'
import i18n from './i18n'
import ElementUI from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'// 8版本以上需要这么使用
Vue.use(ElementUI, {
    i18n: (key, value) => i18n.t(key, value),size: 'mini'
})new Vue({
    render: h => h(App),i18n
}).$mount('#app')

* i18n 文件夹下的 index.js 文件

在 index.js 中使用 Object.assign 合并有一个好处:方便扩展(相对于在语言配置表引入使用扩展运算符)

/* i18n/index.js */
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import enLocale from 'element-ui/lib/locale/lang/en'
import zhCNLocale from 'element-ui/lib/locale/lang/zh-CN'
import zhTWLocale from 'element-ui/lib/locale/lang/zh-TW'Vue.use(VueI18n)const langFiles = require.context('./config', false, /\.js$/)
const regExp = /\.\/([^\.\/]+)\.([^\.]+)$/
const messages = {
    }
const lang = {
    zh: zhCNLocale,zhTW: zhTWLocale,en: enLocale
}
// 默认语言
const loadLanguage = 'zh'langFiles.keys().forEach(key => {
    const k = regExp.exec(key)[1]// 合并Element国际化配置messages[k] = Object.assign(langFiles(key).default, lang[k])
})function getLanguage() {
    // 第一次进入页面或手动清除设置默认语言localStorage.getItem('lang') ? null : localStorage.setItem('lang', loadLanguage)let locale = localStorage.getItem('lang')if (!(locale in messages)) locale = loadLanguagereturn locale
}const i18n = new VueI18n({
    locale: getLanguage(),messages
})export default i18n

到此为止,Element 和 页面国际化基本做完了,不过还有一些国际化难点,接下来说一下

3.与vuex进行关联

参考:ant-design-vue-pro

  • 增加给 HTML 根节点增加 lang 属性
  • 可以在 loadLanguageAsync 力作缓存语言设置,切换语言时懒加载对应语言,以提升性能
  • 增加 i18nRender 方法
/* i18n/index.js */
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import enLocale from 'element-ui/lib/locale/lang/en'
import zhCNLocale from 'element-ui/lib/locale/lang/zh-CN'
import zhTWLocale from 'element-ui/lib/locale/lang/zh-TW'Vue.use(VueI18n)const langFiles = require.context('./config', false, /\.js$/)
const regExp = /\.\/([^\.\/]+)\.([^\.]+)$/
const messages = {
    }
const lang = {
    zh: zhCNLocale,zhTW: zhTWLocale,en: enLocale
}
// 默认语言
const loadLanguage = 'zh'langFiles.keys().forEach(key => {
    const k = regExp.exec(key)[1]// 合并Element国际化配置messages[k] = Object.assign(langFiles(key).default, lang[k])
})function setI18nLanguage(lang) {
    i18n.locale = langlocalStorage.setItem('lang', lang)document.querySelector('html').setAttribute('lang', lang)return lang
}const i18n = new VueI18n({
    silentTranslationWarn: true, // 取消本地化失败时输出的警告locale: getLanguage(), // 语言环境messages
})export function getLanguage() {
    // 第一次进入页面或手动清除设置默认语言localStorage.getItem('lang') ? null : localStorage.setItem('lang', loadLanguage)let locale = localStorage.getItem('lang')if (!(locale in messages)) locale = loadLanguagereturn locale
}// 可以在这里做动态加载国际化语言
export function loadLanguageAsync(lang) {
    return new Promise(resolve => {
    return resolve(setI18nLanguage(lang))})
}export function i18nRender(key) {
    return i18n.t(`${
      key}`)
}export default i18n

在 vuex 里增加 statemutationactions

/* store/modules/app.js */
import {
     loadLanguageAsync } from '@/i18n'const state = {
    lang: 'zh'
}const mutations = {
    SET_LANG: (state, lang) => {
    state.lang = lang}
}const actions = {
    setLang({
      commit }, lang) {
    return new Promise((resolve, reject) => {
    commit('SET_LANG', lang)loadLanguageAsync(lang).then(() => {
    resolve()}).catch(e => {
    reject(e)})})}
}

在 vuex 的 getters 里增加 lang

/* store/getters.js */
const getters = {
    lang: state => state.app.lang
}
export default getters

在创建 Vue 实例时 dispatchstate 有 lang 的状态

/* main.js */
import App from './App'
import store from '@/store'
import i18n from '@/i18n' 
import {
     getLanguage } from '@/i18n'function createdConfig() {
    store.dispatch('app/setLang', getLanguage())
}new Vue({
    el: '#app',store,i18n,created: createdConfig,render: h => h(App)
})

可以将修改语言抽成组件(这里只写 JS 部分)

import {
     mapState } from 'vuex'
export default {
    computed: {
    ...mapState({
    language: state => state.app.lang})},methods: {
    handleSetLanguage(lang) {
    this.$store.dispatch('app/setLang', lang).then(() => {
    window.location.reload()}).catch(() => {
    })}}
}

路由导航篇

这里有一些前端后端都可以处理,如果后端进行国际化处理,前端需要在请求拦截器里加 Accept-Language 请求头

import axios from 'axios'const service = axios.create({
    baseURL: process.env.VUE_APP_BASE_API,withCredentials: true,timeout: 5000
})service.interceptors.request.use(config => {
    const langConfig = {
    zh: 'zh_CN',en: 'en_US',zhTW: 'zh_TW',other: 'other'}config.headers['Accept-Language'] = langConfig[locale]return config},error => {
    return Promise.reject(error)}
)export default service

1.路由菜单国际化

* 使用 $te 判断是否存在语言包内

  • 首先需要处理一下菜单名,如果这个名在语言配置文件中就直接返回,如果不在返回原信息中的菜单。这个可以后端进行国际化处理,如果想修改 UPDATE 数据库一下即可

    $te 这个方法用以判断需要翻译的 key 在你提供的 语言包(messages) 中是否存在

/* utils/get-page-title.js */
export function routeTitle(item) {
    const name = `sidebar.${
      item.path}`if (this.$te(name)) {
    return this.$t(name)}return item.meta.title
}

展示页面

把每一项传到处理菜单的方法中,这样路由菜单国际化基本完成了,如果有多级路由,基本同理

<template v-for="(item, index) in routesSystem"><el-menu-item :index="index.toString()">{
   { routeTitle(item) }}</el-menu-item>
</template><script> import {
       routeTitle } from '@/utils/get-page-title' export default {
      methods: {
      routeTitle // 声明一下} } </script>

2.面包屑导航国际化

面包屑导航使用的是 vue-element-admin 的 Breadcrumb 组件

<template><el-breadcrumb class="app-breadcrumb" separator="/"><transition-group name="breadcrumb"><el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path"><span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{
   { breadTitle(item) }}</span><a v-else>{
   { breadTitle(item) }}</a></el-breadcrumb-item></transition-group></el-breadcrumb>
</template><script> import {
       breadTitle } from '@/utils/get-page-title' export default {
      methods: {
      breadTitle // 声明一下} } </script>

可以通过 / 去判断它是几级路由;也可以在路由的 meta 中设定一个标识,判断这个标识即可

  • 三级路由场景:一、二级路由可以放在 sidebar 对象中,三级路由放在 menu 中(当然也可以都放在一个对象里)
/* utils/get-page-title.js */
export function breadTitle(item) {
    if (window.indexConfig.afterTitle) return item.meta.titleconst n = item.name || item.path.slice(1)let nameif (item.path.match(/\//g).length !== 3) {
    name = `sidebar.${
      n}`} else {
    name = `menu.${
      n}`}if (this.$te(name)) {
    return this.$t(name)}return item.meta.title
}

3.导航守卫国际化

因为导航守卫中拿不到 this,所以可以采用了一种取巧的方式。首先在 router 文件中引入 i18n 文件夹下的 index.js,之后直接取它的 messageslocale 即可拿到对应语言文件

  • 不过这样写会产生一个问题,就是切换语言无法实时更新,需要刷新状态
/* src/permission.js */
import router from './router/index'
import i18n from '@jp/i18n'
import modal from '@jp/utils/modal' // 二次封装的弹出框
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
import {
     getToken, removeCookies, removeToken } from '@/utils/auth'
import {
     isDebugger } from '@/settings' // 也可以维护在store里NProgress.configure({
     showSpinner: false })const locale = i18n.locale
const msg = i18n.messages[locale]
const whiteList = ['/login']router.beforeEach((to, from, next) => {
    NProgress.start()if (getToken()) {
    document.title = getPageTitle(to.meta.title)if (to.path === '/login') {
    next({
     path: '/' })} else {
    // debugger下不获取用户权限if (isDebugger) {
    next()} else {
    // 设置动态路由,拉取user_info...}}} else {
    if (whiteList.includes(to.path)) {
    next()} else {
    removeCookies()modal.msgError(msg.info.tokenExpire) // 弹出框国际化next({
     path: '/login' })NProgress.done()}}
})router.afterEach(() => {
    NProgress.done()
})

4.render 函数展示文字

vue-element-admin 里的 Item 组件,是采用 render 函数渲染的,这个一般

<!-- src/layout/components/Sidebar/Item.vue -->
<script> import {
       childTitle } from '@/utils/get-page-title' export default {
      name: 'MenuItem',functional: true,props: {
      icon: {
      type: String,default: ''},title: {
      type: String,default: ''},path: {
      type: String,default: ''}},render(h, context) {
      const {
       icon, title } = context.propsconst vnodes = []if (icon) {
      if (icon.includes('el-icon')) {
      vnodes.push(<i class={
      [icon, 'sub-el-icon']} />)} else {
      vnodes.push(<svg-icon icon-class={
      icon}/>)}}if (title) {
      // 三级目录文字展示vnodes.push(<span slot="title">{
      childTitle(context)}</span>)}return vnodes} } </script>

这里可以用一个取巧的方式,因为 render 函数里的 context 里面有 parent 父节点,可以使用这个父节点上面的方法($t$te

export function childTitle(item) {
    const p = item.props.pathconst parent = item.parentconst name = `menu.${
      p}`if (parent.$te(name)) {
    return parent.$t(name)}return item.props.title
}

这样路由菜单、面包屑导航及导航守卫国际化基本做完事了

组件篇

1.选择框、输入框组件

<el-form-item :label="$t('viewsC.animal')" prop="animal"><el-input v-model="rowsFormData.animal" :placeholder="$t('viewsC.animalHolder')" />
</el-form-item>

选择框输入框,我将其都放在 viewC 对象中,并按照,如下方式命名:

  • 没有加 Holder 后缀的后面需要加 ,有加 Holder 后缀的前面需要加 请输入/请选择
const ps = '请选择'
const pi = '请输入'
const end = ':'const viewsC = {
    startDate: '起始日期',startDateHolder: ps + '起始日期',stopDate: '截止日期',stopDateHolder: ps + '截止日期'
}for (const item in viewsC) {
    if (!item.includes('Holder')) {
    viewsC[item] = viewsC[item] + end} else {
    if (!viewsC[item].includes('请')) {
    console.log(viewsC[item] + '需要加 ps 或 pi')}}
}

详细例子可以参考 2.选择框、输入框表单校验,表格 Table 跟这个同理 el-table-column : label="$t('tableC.xxx)" 即可

* 2.选择框、输入框表单校验

如果直接把 rules 校验规则写在 data 里面,Element 内部对其 this 进行了处理,所以需要换一种思路

  1. 使用 computed 计算属性

  2. 写在 data 里面,在 return 之前是可以拿到 this 的,message 取到这个变量即可(不过这个切换语言不能实时更新)

    注意:如果使用自定义规则是可以实时更新的,但是出于复用性一般会将其抽到 utils 方法库中,这样就会产生其他问题

    • this 无法处理
    • 简单认证还得写复杂的自定义规则

    所以这个不是一个好方法,推荐使用计算属性

一般出于复用性将方法抽到 utils 方法库中,直接写是不行的(Element 内部对其 this 进行了处理),需要结合 bind/call/apply 来使用

  • pattern: '[^ \x22]+' 是校验不能全部为空格
<el-scrollbar wrap-class="scrollbar-wrapper" style="height: 100%;"><el-form ref="rowsForm" :model="rowsFormData" :rules="rules" label-width="130px"><el-row :gutter="20"><el-col :span="8"><el-form-item :label="$t('viewsC.animal')" prop="animal"><el-input v-model="rowsFormData.animal" :placeholder="$t('viewsC.animalHolder')" /></el-form-item></el-col><el-col :span="8"><el-form-item :label="$t('viewsC.plants')" prop="plants"><el-input v-model="rowsFormData.plants" :placeholder="$t('viewsC.plantsHolder')" /></el-form-item></el-col></el-row></el-form>
</el-scrollbar><script> import {
       ValidAnimal } from '@/utils/viewValidate' export default {
      data() {
      return {
      rowsFormData: {
      animal: '',plants: ''}}},computed: {
      rules() {
      return {
      animal: [{
       required: true, validator: ValidAnimal.bind(this), trigger: 'change', pattern: '[^ \x22]+' }],plants: [{
       required: true, message: this.$t('form.plantsTip'), trigger: 'blur', pattern: '[^ \x22]+' },{
       max: 5, message: this.$t('form.maxLength10'), trigger: 'blur' }]}}}, } </script>

3.弹出框(MessageBox 或 Notification)

弹出框的 title 是可以使用 this 的,不过需要注意:

  • 如果使用 function,拿到的是错误的 this (可能由于代码不规范导致有这个问题)

    要么用一个变量存,要么使用箭头函数

this.$confirm(this.$t('info.notify') , this.$t('info.friendlyTips'), {
    type: 'warning'
}).then(function() {
    const params = {
    }Api(params).then(res => {
    this.$notify({
     title: this.$t('info.friendlyTips'), message: res.data.msg, type: 'success' })this.getList()}).catch(e => console.log(e))})

4.自制弹出框组件国际化(Vue.extend)

Vue.extend 其实相当于创建了一个 Vue 子类,然后对这个子类上扩展一些 options 属性

popup.js 中处理

结合 el-dialog 自制弹出框组件,需要在创建实例时混入 $i18n ,这样弹出框组件才能使用 $t$te

import store from '@/store'
import i18n from '@/i18n' // 引入国际化
import Vue from 'vue'
export default function(el, params) {
    // 初始化一个Promiselet resolvelet rejectconst promise = new Promise((res, rej) => {
    resolve = resreject = rej})// 初始化弹窗const Popups = Vue.extend(el)const vEl = new Popups({
    store,i18n,propsData: {
    params: Object.assign({
    }, params)},data() {
    return {
    visible: true}},methods: {
    resolve(arg) {
    resolve(arg)this.visible = false},reject(arg) {
    reject(arg)this.visible = false},closed() {
    this.visible = falseremovePopup()}}})// 把弹窗挂载到bodyconst shadeEl = vEl.$mount().$eldocument.body.appendChild(shadeEl)// 从body中移除弹窗体function removePopup() {
    document.body.removeChild(shadeEl)}return promise
}

popup.vue 组件

<template><el-dialog:title="title":width="width":visible.sync="visible"append-to-body:close-on-click-modal="false":close-on-press-escape="false":show-close="showClose":modal="modal":custom-class="customClass"class="popup-wrapper":top="top"@closed="closed"><div class="popup__body"><slot /></div><div v-if="!isFooter" slot="footer" class="dialog-footer"><slot name="footer"><el-button @click="onCancel">{
   { $t('tools.cancel') }}</el-button><el-button type="primary" @click="onSubmit">{
   { $t('tools.confirm') }}</el-button></slot></div></el-dialog>
</template>
<script> export default {
      name: 'UiPopup',props: {
      value: {
      type: [String, Object, Array, Number, Boolean],default: ''},title: {
      type: String,default: ''},width: {
      type: String,default: ''},isFooter: {
      type: String,default: ''},showClose: {
      type: Boolean,default: true},modal: {
      type: Boolean,default: true},customClass: {
      type: String,default: ''},top: {
      type: String,default: ''}},computed: {
      // visible控制器,点X时调用取消visible: {
      get: function() {
      return this.$parent.visible},set: function(v) {
      this.$parent.reject(this.value)}}},methods: {
      onCancel() {
      this.$emit('onCancel', this.value)},onSubmit() {
      this.$emit('onSubmit', this.value)},// 点 X 关闭时调用父组件的注入closed() {
      this.$parent.closed()}} } </script>

components 下的 index.js

把 popup 通过全局方式注册

import popup from './Popup/popup.vue'
import jsPopup from './Popup/popup.js'
const components = [popup
]
const jsComponents = {
    $popup: jsPopup
}
// 出口函数为组件安装函数
const install = Vue => {
    components.forEach(component => {
    Vue.component(component.name, component)})for (const key in jsComponents) {
    if (!Vue.prototype.hasOwnProperty(key)) {
    Vue.prototype[key] = jsComponents[key]} else {
    console.warn(key + '被占用')}}
}
export default {
    install
}

main.js

这样就可以在全局使用 $popup

import Vue from 'vue'
import globalComponents from '@/components'Vue.use(globalComponents)

打包篇

toB 的项目可能会有这个需求:

  • 因为页面组件和表格很多,如果有一个字段名想改,就需要重新打包,很麻烦,现在想将其提出去(比如提取到 public 文件夹下的 config.js 文件中,这个 config.js 在 index.html 中引入一下),这样以后只需要改一下这个 js 文件无需重新打包

基础配置

首先将各个配置文件复制到 public 下面,并将其用匿名函数包裹,之后在入口 index.html 中,引入如下配置文件

<script>var now = new Date().getTime();document.write('<script src="../config.js?t=' + now + '"><\/script\>')document.write('<script src="../i18n/en.js?t=' + now + '"><\/script\>')document.write('<script src="../i18n/zh.js?t=' + now + '"><\/script\>')document.write('<script src="../i18n/zhTW.js?t=' + now + '"><\/script\>') </script>

config.js 中用 var 声明变量挂载到 window 上,并在 en/zh/zhTW js文件中把配置加到 lang 对象中

var indexConfig = {
    }
indexConfig.lang = {
    }
// zh中文-简体; en-英文; zhTW-中文繁体
indexConfig.langShow = ['zh', 'en', 'zhTW']

最后在各个 lang 文件下用 window.indexConfig.lang 获取即可

打包配置

由于打包时不会携带 i18n 文件夹下面的配置文件,所以需要借助 CopyWebpackPlugin 插件进行复制

const copyWebpackPlugins = ['', '/i18n'].map(item => {
    return new CopyWebpackPlugin([{
    from: `./public${
      item}/*`,to: path.resolve(__dirname, `dist/`),transformPath(targetPath, absolutePath) {
    return targetPath.replace(`public`, '')},toType: 'dir'}])
})module.exports = {
    plugins: [new HtmlWebpackPlugin('src/index.html')].concat(copyWebpackPlugins)
}

这样配置文件就提出去了,不过有安全风险,这个以后再说

推荐参考

前端国际化之Vue-i18n源码分析

vue中如何使用i18n实现国际化

Element国际化

elementUI——locale,国际化方案

  相关解决方案