Vue2 element-ui 框架中集成国际化 vue-i18n 并封装成可切换语言的组件

在 Vue 2 中配置国际化,您可以使用 Vue I18n 插件。Vue I18n 是 Vue.js 官方推荐的国际化插件,它可以帮助您轻松地实现多语言支持。

安装 vue-i18n

项目根目录下打开终端或命令行工具,运行以下命令来安装相关依赖包:

npm install vue-i18n@8.27.1 --save

使用

1. 在 src/components 目录中新增一个名为 i18n 的目录,并添加以下3个文件:

src/components/i18n/locales/en/index.js 英语语言包:

export default {
  'Language': 'English'
}

src/components/i18n/locales/zh-CN/index.js 中文语言包:

export default {
  'Language': '中文'
}

src/components/i18n/index.js 语言包入口文件:

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

Vue.use(VueI18n)

// 各个国家的key
const localeKeys = ['en', 'zh-CN']

// 各个国家语言包
const messages = {}
for (const key of localeKeys) {
  messages[key] = require(`./locales/${key}/index.js`).default
}

export default new VueI18n({
  locale: 'en',
  messages,
  silentTranslationWarn: true // 忽略翻译警告
})
2. 打开 src/main.js 文件,挂载到Vue实例:
import i18n from './components/i18n'

new Vue({
  i18n
})

修改后的代码如下:

import Vue from 'vue'
import Element from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

import App from './App.vue'
import router from './router'
import store from './store'
import i18n from './components/i18n'
import '@css/index.less'

// 禁用生产环境提示
Vue.config.productionTip = false

// Element挂载到Vue
Vue.$message = Element.Message
Vue.use(Element)

new Vue({
  router,
  store,
  i18n,
  render: (h) => h(App)
}).$mount('#app')
3. 在页面、js代码中使用:

一旦将 VueI18n 实例挂载到 Vue 实例上,在 Vue 组件中直接使用 $t 方法,可以通过指定键(key)来获取对应语言的翻译文本。这个键可以是简单的字符串,也可以是一个对象,用于支持更复杂的翻译需求。

以下是使用 $t 方法获取翻译文本的示例:

<template>
  <div>
    <p>{{ $t('Language') }}</p>
  </div>
</template>

<script>
export default {
  mounted() {
    console.log(this.$t('Language'))
  }
}
</script>

Element 语言包

完成上述步骤后,我们在使用 Element 组件时可能会遇到一个问题:虽然我们成功切换了自定义的语言,在应用中自定义的文本已经被正确翻译,但是 Element UI 组件的显示内容并没有随之切换。

这是因为 Element UI 组件库本身并不直接集成 vue-i18n 插件,因此它并不会自动根据我们设置的语言环境来翻译组件的显示文本。

解决这个问题的办法是,我们需要手动为 Element UI 组件进行国际化配置和翻译。

以下是解决方案的基本步骤:

1. 创建 Element 语言包文件

src/components/i18n 目录下添加一个名为 element 的目录,并在目录中添加以下两个文件:

en.js

// 英语
exports.default = {
  el: {
    colorpicker: {
      confirm: 'OK',
      clear: 'Clear'
    },
    datepicker: {
      now: 'Now',
      today: 'Today',
      cancel: 'Cancel',
      clear: 'Clear',
      confirm: 'OK',
      selectDate: 'Select date',
      selectTime: 'Select time',
      startDate: 'Start Date',
      startTime: 'Start Time',
      endDate: 'End Date',
      endTime: 'End Time',
      prevYear: 'Previous Year',
      nextYear: 'Next Year',
      prevMonth: 'Previous Month',
      nextMonth: 'Next Month',
      year: '',
      month1: 'January',
      month2: 'February',
      month3: 'March',
      month4: 'April',
      month5: 'May',
      month6: 'June',
      month7: 'July',
      month8: 'August',
      month9: 'September',
      month10: 'October',
      month11: 'November',
      month12: 'December',
      week: 'week',
      weeks: {
        sun: 'Sun',
        mon: 'Mon',
        tue: 'Tue',
        wed: 'Wed',
        thu: 'Thu',
        fri: 'Fri',
        sat: 'Sat'
      },
      months: {
        jan: 'Jan',
        feb: 'Feb',
        mar: 'Mar',
        apr: 'Apr',
        may: 'May',
        jun: 'Jun',
        jul: 'Jul',
        aug: 'Aug',
        sep: 'Sep',
        oct: 'Oct',
        nov: 'Nov',
        dec: 'Dec'
      }
    },
    select: {
      loading: 'Loading',
      noMatch: 'No matching data',
      noData: 'No data',
      placeholder: 'Select'
    },
    cascader: {
      noMatch: 'No matching data',
      loading: 'Loading',
      placeholder: 'Select',
      noData: 'No data'
    },
    pagination: {
      goto: 'Go to',
      pagesize: '/page',
      total: 'Total {total}',
      pageClassifier: ''
    },
    messagebox: {
      title: 'Message',
      confirm: 'OK',
      cancel: 'Cancel',
      error: 'Illegal input'
    },
    upload: {
      deleteTip: 'press delete to remove',
      delete: 'Delete',
      preview: 'Preview',
      continue: 'Continue'
    },
    table: {
      emptyText: 'No Data',
      confirmFilter: 'Confirm',
      resetFilter: 'Reset',
      clearFilter: 'All',
      sumText: 'Sum'
    },
    tree: {
      emptyText: 'No Data'
    },
    transfer: {
      noMatch: 'No matching data',
      noData: 'No data',
      titles: ['List 1', 'List 2'], // to be translated
      filterPlaceholder: 'Enter keyword', // to be translated
      noCheckedFormat: '{total} items', // to be translated
      hasCheckedFormat: '{checked}/{total} checked' // to be translated
    },
    image: {
      error: 'FAILED'
    },
    pageHeader: {
      title: 'Back' // to be translated
    },
    popconfirm: {
      confirmButtonText: 'Yes',
      cancelButtonText: 'No'
    },
    empty: {
      description: 'No Data'
    }
  }
}

zh-CN.js

// 中文
exports.default = {
  el: {
    colorpicker: {
      confirm: '确定',
      clear: '清空'
    },
    datepicker: {
      now: '此刻',
      today: '今天',
      cancel: '取消',
      clear: '清空',
      confirm: '确定',
      selectDate: '选择日期',
      selectTime: '选择时间',
      startDate: '开始日期',
      startTime: '开始时间',
      endDate: '结束日期',
      endTime: '结束时间',
      prevYear: '前一年',
      nextYear: '后一年',
      prevMonth: '上个月',
      nextMonth: '下个月',
      year: '年',
      month1: '1 月',
      month2: '2 月',
      month3: '3 月',
      month4: '4 月',
      month5: '5 月',
      month6: '6 月',
      month7: '7 月',
      month8: '8 月',
      month9: '9 月',
      month10: '10 月',
      month11: '11 月',
      month12: '12 月',
      // week: '周次',
      weeks: {
        sun: '日',
        mon: '一',
        tue: '二',
        wed: '三',
        thu: '四',
        fri: '五',
        sat: '六'
      },
      months: {
        jan: '一月',
        feb: '二月',
        mar: '三月',
        apr: '四月',
        may: '五月',
        jun: '六月',
        jul: '七月',
        aug: '八月',
        sep: '九月',
        oct: '十月',
        nov: '十一月',
        dec: '十二月'
      }
    },
    select: {
      loading: '加载中',
      noMatch: '无匹配数据',
      noData: '无数据',
      placeholder: '请选择'
    },
    cascader: {
      noMatch: '无匹配数据',
      loading: '加载中',
      placeholder: '请选择',
      noData: '暂无数据'
    },
    pagination: {
      goto: '前往',
      pagesize: '条/页',
      total: '共 {total} 条',
      pageClassifier: '页'
    },
    messagebox: {
      title: '提示',
      confirm: '确定',
      cancel: '取消',
      error: '输入的数据不合法!'
    },
    upload: {
      deleteTip: '按 delete 键可删除',
      delete: '删除',
      preview: '查看图片',
      continue: '继续上传'
    },
    table: {
      emptyText: '暂无数据',
      confirmFilter: '筛选',
      resetFilter: '重置',
      clearFilter: '全部',
      sumText: '合计'
    },
    tree: {
      emptyText: '暂无数据'
    },
    transfer: {
      noMatch: '无匹配数据',
      noData: '无数据',
      titles: ['列表 1', '列表 2'],
      filterPlaceholder: '请输入搜索内容',
      noCheckedFormat: '共 {total} 项',
      hasCheckedFormat: '已选 {checked}/{total} 项'
    },
    image: {
      error: '加载失败'
    },
    pageHeader: {
      title: '返回'
    },
    popconfirm: {
      confirmButtonText: '确定',
      cancelButtonText: '取消'
    },
    empty: {
      description: '暂无数据'
    }
  }
}

以上两个文件来源于 Element官方。在当前项目中,将Element语言包放到本地的目的是为了便于后期对语言包进行修改拓展。

如果不想在本地创建这两个文件,也可以通过引入的方式直接获取到对应的文件,例如:import elementLang from 'element-ui/src/locale/lang/en'

2. 在语言包入口文件引入 Element 语言包

src/components/i18n/index.js 文件中找到以下代码块:

// 各个国家语言包
const messages = {}
for (const key of localeKeys) {
  messages[key] = require(`./locales/${key}/index.js`).default
}

将其修改为:

// 各个国家语言包
const messages = {}
for (const key of localeKeys) {
  const langObj = require(`./locales/${key}/index.js`).default
  const langElement = require(`./element/${key}`)
  messages[key] = {
    ...langObj,
    ...langElement ? langElement.default : {}
  }
}

修改后的代码如下:

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

Vue.use(VueI18n)

// 各个国家的key
const localeKeys = ['en', 'zh-CN']

// 各个国家语言包
const messages = {}
for (const key of localeKeys) {
  const langObj = require(`./locales/${key}/index.js`).default
  const langElement = require(`./element/${key}`)
  messages[key] = {
    ...langObj,
    ...langElement ? langElement.default : {}
  }
}

export default new VueI18n({
  locale: 'en',
  messages,
  silentTranslationWarn: true // 忽略翻译警告
})

通过这种方式,Element语言包与我们自定义的语言包便合并在一起了。

3. 挂载 Element 到 Vue 实例中时将语言包注入

src/main.js 中找到以下代码块

Vue.use(Element)

将其修改为:

Vue.use(Element, {
  i18n: (key, value) => i18n.t(key, value)
})

修改后的代码如下:

import Vue from 'vue'
import Element from 'element-ui'
import 'element-ui/lib/theme-chalk/index.css'

import App from './App.vue'
import router from './router'
import store from './store'
import i18n from './components/i18n'
import '@css/index.less'

// 禁用生产环境提示
Vue.config.productionTip = false

// Element挂载到Vue
Vue.$message = Element.Message
Vue.use(Element, {
  i18n: (key, value) => i18n.t(key, value)
})

new Vue({
  router,
  store,
  i18n,
  render: (h) => h(App)
}).$mount('#app')

完成后 Element 相关组件就能正常的显示翻译后的内容了。

切换组件的封装

在我们的UI组件中,我们使用了 Element 中的下拉菜单组件。如果想了解更多关于下拉菜单组件的用法,请访问 Dropdown下拉菜单文档地址 查看详细文档内容。

新建组件文件

src/components/i18n目录中,新建一个名为change-language.vue 的文件,并添加以下内容:

<template>
  <el-dropdown @command="handle">
    <span class="el-dropdown-link">
      {{$t('Language')}}<i class="el-icon-caret-bottom el-icon--right"></i>
    </span>
    <el-dropdown-menu slot="dropdown">
      <el-dropdown-item v-for="(item, index) of list" :key="index" :command="item.key">{{item.name}}</el-dropdown-item>
    </el-dropdown-menu>
  </el-dropdown>
</template>

<script>
export default {
  name: 'change-language',
  data() {
    return {
      list: [
        { key: 'en', name: 'English' }, // 英语
        { key: 'zh-CN', name: '中文' } // 中文
      ]
    }
  },
  methods: {
    handle(value) {
    }
  }
}
</script>
<style scoped lang="less">
</style>

然后,我们可以找一个Vue文件,在其中使用这个组件进行观察。使用方式如下(以Vue单文件组件为例):

  • 引入组件
import ChangeLanguage from '@/components/i18n/change-language'
  • 注册组件
components: {
  ChangeLanguage
}
  • 使用
<ChangeLanguage />

这样一个基本的语言包下拉单组件样式就完成了,之后还需要对组件进行功能上的开发。

切换语言

在组件handle函数代码中添加代码,如下:

handle(value) {
  this.$i18n.locale = value
}

通过将value赋给this.$i18n.locale,我们可以动态地切换当前语言为下拉菜单中所选中的语言。

现在虽然基本的切换功能已经完成,但是当我们在网页中切换语言包并刷新页面后,之前选择的语言包不会被保留,而是重新加载页面时返回到默认的语言包。这是因为在刷新页面时,浏览器会重新加载整个应用程序,并重置Vue实例的状态,包括设置的语言包。这时候我们就需要做切换数据的持久化处理,来保证我们切换后的内容显示不会出错。

数据持久化

数据持久化的方式有很多种,这里我们采用了浏览器的本地存储 LocalStorage 来实现。

在组件handle函数代码中添加代码,如下:

handle(value) {
  this.$i18n.locale = value
  localStorage.setItem('change-language', value)
}

找到 src/components/i18n/index.js 文件中以下代码块:

export default new VueI18n({
  locale: 'en',
  messages,
  silentTranslationWarn: true // 忽略翻译警告
})

将其中 locale 写死的值 'en' 改为 localStorage 获取的值,如下:

export default new VueI18n({
  locale: localStorage.getItem('change-language') || 'zh-CN',
  messages,
  silentTranslationWarn: true // 忽略翻译警告
})

上述代码中,语言包的key值会先从本地存储中获取,如果获取不到则设置为默认值'zh-CN'

这样,我们切换语言包再刷新页面后,仍然可以正确地显示之前选择的语言包。

后端语言包

在一般情况下,仅仅使用前端的语言包是无法涵盖整个系统的翻译需求的。例如,后端接口返回的菜单、下拉列表的数据等。针对这种情况,我们可以采取一种方案,即在切换语言后调用location.reload()方法刷新页面以重新获取后端的翻译数据。

使用这种方案时,我们需要与后端约定好国际化的翻译key的格式,并在语言切换时通过前端与后端接口进行数据交互。前端可以将当前选择的语言作为参数传递给后端,后端将返回相应语言的翻译数据。然后在前端刷新页面时,通过重新加载页面来获取更新后的翻译内容。

请注意,使用location.reload()方法会导致整个页面重新加载,这可能会对用户的体验产生一定影响,特别是在数据量较大或网络较慢的情况下。因此,在实施这种方案之前,请确保综合考虑用户体验和性能方面的因素。

总结来说,为了在整个系统中实现翻译需求,我们可以通过与后端接口协作,传递语言选择,并在需要时重新加载页面来获取最新的翻译内容。这样就能够在前后端协同工作下,正确显示经过翻译的内容。

在组件handle函数中添加 location.reload()

handle(value) {
  this.$i18n.locale = value
  localStorage.setItem('change-language', value)
  location.reload()
}
小小的优化一下

上述步骤完成,我们的组件整体功能就已经开发完毕了,但是代码中有一部部分内容可以复用简化。

组件代码中的以下部分:

list: [
  { key: 'en', name: 'English' }, // 英语
  { key: 'zh-CN', name: '中文' } // 中文
]

src/components/i18n/index.js 代码中的以下部分:

// 各个国家的key
const localeKeys = ['en', 'zh-CN']

以上两部分代码对比,就能够发现他们有两个相同点:都是数组、内容中都有一样的值。

这样我们就能将组件代码中的内容移动到 src/components/i18n/index.js 文件中,避免后期维护需要新增国家时,还要同时维护两个文件的列表了。

根据以下步骤进行优化:

  1. 找到 src/components/i18n/index.js 文件中以下内容:
// 各个国家的key
const localeKeys = ['en', 'zh-CN']

// 各个国家语言包
const messages = {}
for (const key of localeKeys) {
  const langObj = require(`./locales/${key}/index.js`).default
  const langElement = require(`./element/${key}`)
  messages[key] = {
    ...langObj,
    ...langElement ? langElement.default : {}
  }
}
  1. 将其修改为:
// 各个国家的key
export const localeKeys = [
  { key: 'en', name: 'English' }, // 英语
  { key: 'zh-CN', name: '中文' } // 中文
]

// 各个国家语言包
const messages = {}
for (const item of localeKeys) {
  const key = item.key
  const langObj = require(`./locales/${key}/index.js`).default
  const langElement = require(`./element/${key}`)
  messages[key] = {
    ...langObj,
    ...langElement ? langElement.default : {}
  }
}
  1. 在组件中引入 src/components/i18n/index.js 文件中的 localeKeys
import { localeKeys } from './index'
  1. localeKeys赋值给 list
return {
  list: localeKeys
}

这样,这个小小的优化就完成了。

完整代码

src/components/i18n/change-language.vue 组件代码:

<!--
@Descripttion 国际化语言切换
@version 1.0.0
@Author Bell
@ 使用
  引入组件
    import ChangeLanguage from '@/components/i18n/change-language'
  注册组件
    components: {
      ChangeLanguage
    }
  使用
    <ChangeLanguage />
 -->
<template>
  <el-dropdown @command="handle">
    <span class="el-dropdown-link">
      {{$t('Language')}}<i class="el-icon-caret-bottom el-icon--right"></i>
    </span>
    <el-dropdown-menu slot="dropdown">
      <el-dropdown-item v-for="(item, index) of list" :key="index" :command="item.key">{{item.name}}</el-dropdown-item>
    </el-dropdown-menu>
  </el-dropdown>
</template>

<script>
import { localeKeys } from './index'

export default {
  name: 'change-language',
  data() {
    return {
      list: localeKeys
    }
  },
  methods: {
    handle(value) {
      this.$i18n.locale = value
      localStorage.setItem('change-language', value)
      location.reload()
    }
  }
}
</script>
<style scoped lang="less">
</style>

src/components/i18n/index.js 国际化入口文件:

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

Vue.use(VueI18n)

// 各个国家的key
export const localeKeys = [
  { key: 'en', name: 'English' }, // 英语
  { key: 'zh-CN', name: '中文' } // 中文
]

// 各个国家语言包
const messages = {}
for (const item of localeKeys) {
  const key = item.key
  const langObj = require(`./locales/${key}/index.js`).default
  const langElement = require(`./element/${key}`)
  messages[key] = {
    ...langObj,
    ...langElement ? langElement.default : {}
  }
}

export default new VueI18n({
  locale: localStorage.getItem('change-language') || 'zh-CN',
  messages,
  silentTranslationWarn: true // 忽略翻译警告
})



框架搭建整体流程

点击下载步骤 1-7 配置完成的完整 Demo



本框架更多功能 Demo 下载

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容