前端Vue3国际化开发 :使用vue-i18n库和Element Plus 组件实现
剖析 Vue3 + Element Plus 国际化(i18n)实现原理
本文将解析基于 Vue3
和 Element Plus
的国际化架构实现,从核心概念到实际应用,带您全面了解多语言支持在现代化前端应用中的实现方式。
国际化(i18n)核心概念
国际化(Internationalization,简称 i18n)是指设计软件应用时,使其能适应多种语言和地区需求的过程。在 Vue 生态中,主要通过 vue-i18n
库实现(Vue I18n 是 Vue.js 的国际化插件),配合 Element Plus 的国际化支持,可构建完整的国际化应用。
国际化实现架构图
实现效果
效果一:Element Plus组件的国际化、使用vue-i8n插件实现页面的国际化
前端国际化开发效果
效果二:国际化语言本地持久化存储
语言本地持久化存储
具体步骤(Vue3 + Element Plus + i18n)
第一步:安装i8n
Vue i18n插件在Vue3
中需要使用v9
版本,这一点需要注意!!
(我当时写项目的时候没有注意到这个一点,直接安装的最新版,目前为止还没发现什么问题,大家自己选择)
npm install vue-i18n@9 # Vue3 专用版本
或者
yarn add vue-i18n@9
或者
pnpm add vue-i18n@9
第二步:创建自定义语言资源文件
1、创建src/locales/en.js
export const language = {
en: {
// 部门
sys_dept_list00001: 'Department Name',
sys_dept_list00002: 'Person in Charge Name',
sys_dept_list00003: 'Department Head',
// 实验室
管理: 'Manage',
人员信息: 'Personnel Information',
序号: 'NO.',
姓名: 'Name',
学工号: 'Student ID',
账号: 'Account',
操作: 'Action',
编辑: 'Edit',
}
}
2、创建src/locales/zh-cn.js
'zh-cn': {
// 部门
sys_dept_list00001: '部门名称',
sys_dept_list00002: '负责人名称',
sys_dept_list00003: '部门负责人',
// 实验室管理
管理: '管理',
人员信息: '人员信息',
序号: '序号',
姓名: '姓名',
学工号: '学工号',
操作: '操作',
编辑: '编辑',`在这里插入代码片`
}
(我一开始用的编号,后来发现代码可读性有点差,不利于后期维护,索性全用中文了!)
(可以分两个对照文档,一份中文,一份英文,我是直接放一起了,大家自行选择!)
第三步:合并 Element Plus 语言包和自定义语言包并在main.ts挂载
import { icons as epIcons } from '@iconify-json/ep'
import { icons as nfIcons } from '@iconify-json/nf'
import { addCollection } from '@iconify/vue'
import { createApp } from 'vue'
import '!/public/style/index.css'
import '@unocss/reset/tailwind-compat.css'
import 'virtual:uno.css'
import '@nofar-core/shared/integrations/element'
import 'animate.css'
import { setupDirectives } from '@nofar-core/directives'
import { setupModules } from '@nofar-core/shared/plugins'
import App from '@nofar-core/shared/views/App.vue'
import 'vue3-draggable-resizable/dist/Vue3DraggableResizable.css'
import ElementPlus, { ElConfigProvider, useLocale } from 'element-plus'
import en from 'element-plus/es/locale/lang/en' // 英文语言包
import zhCn from 'element-plus/es/locale/lang/zh-cn' // 中文语言包
import { createPinia } from 'pinia'
import { createI18n } from 'vue-i18n'
import Vue3DraggableResizable from 'vue3-draggable-resizable'
import { language } from '@nofar-core/shared/locales' // 引入语言包
const pinia = createPinia()
const app = createApp(App)
app.use(pinia)
const appLangStr = sessionStorage.getItem('app-lang')
const appLang = appLangStr ? JSON.parse(appLangStr) : null
const savedLocale = appLang?.currentLang || 'zh-cn' // 默认值
const initialLocale = ref(savedLocale)
// 创建 i18n 实例
const i18n = createI18n({
legacy: false,
// locale: 'en', // 默认语言
locale: initialLocale.value || 'zh-cn', // 初始语言
messages: {
en: {
// welcome: 'Welcome',
...language.en, // 英文语言包
...en // Element Plus 的英文语言包
},
'zh-cn': {
// welcome: '欢迎',
...language['zh-cn'], // 中文语言包
...zhCn // Element Plus 的中文语言包
}
}
})
// 添加语言切换方法
const changeLocale = (locale: 'zh-cn' | 'en') => {
// i18n.global.locale = locale
// // ElConfigProvider.config({
// // locale: locale === 'zh-cn' ? zhCn : en
// // })
// useLocale(ref(locale === 'zh-cn' ? zhCn : en))
i18n.global.locale.value = locale // 操作ref的value属性
useLocale(computed(() => (locale === 'zh-cn' ? zhCn : en)))
}
// 使用 Element Plus 和 i18n
app.use(ElementPlus, {
// locale: i18n.global.locale === 'zh-cn' ? zhCn : en
locale: computed(() => (i18n.global.locale.value === 'zh-cn' ? zhCn : en))
// locale: en
})
app.use(i18n)
// 挂载语言切换方法
app.config.globalProperties.$changeLocale = changeLocale
app.use(Vue3DraggableResizable)
setupModules(app)
setupDirectives(app)
addCollection(epIcons)
addCollection(nfIcons)
app.config.errorHandler = (err) => {
console.error(err)
}
app.mount('#app')
上述代码实现详解
1、语言包导入与管理
// 引入 Element Plus 语言包
import en from 'element-plus/es/locale/lang/en' // 英文语言包
import zhCn from 'element-plus/es/locale/lang/zh-cn' // 中文语言包
//从 vue-i18n中导入用于创建 i18n 实例的方法
import { createI18n } from 'vue-i18n'
// 引入自定义语言包
import { language } from '@nofar-core/shared/locales'
// 创建 i18n 实例
const i18n = createI18n({
legacy: false, // 使用 Vue3 的 Composition API 风格
locale: initialLocale.value || 'zh-cn', // 初始语言
messages: {
en: {
...language.en, // 自定义英文语言包
...en // Element Plus 的英文语言包
},
'zh-cn': {
...language['zh-cn'], // 自定义中文语言包
...zhCn // Element Plus 的中文语言包
}
}
})
关键点解析:
legacy: false
表示使用 Vue3 的 Composition API 风格- 通过对象展开符 … 合并自定义语言包和 Element Plus 语言包
- 语言包结构为键值对形式,例如:
{ welcome: '欢迎', login: '登录' }
2、 语言状态初始化
// 从 sessionStorage 获取保存的语言设置
const appLangStr = sessionStorage.getItem('app-lang')
const appLang = appLangStr ? JSON.parse(appLangStr) : null
// 设置初始语言,默认为中文
const savedLocale = appLang?.currentLang || 'zh-cn'
const initialLocale = ref(savedLocale)
关键点解析:
- 使用
sessionStorage
持久化用户的语言选择 - 初始语言默认为中文(‘zh-cn’)
- 使用 Vue 的
ref
创建响应式语言状态
3、Element Plus 国际化集成
// 使用 Element Plus 并配置国际化
app.use(ElementPlus, {
locale: computed(() =>
i18n.global.locale.value === 'zh-cn' ? zhCn : en
)
})
关键点解析:
- 使用
computed
创建响应式语言配置 - 根据当前选择的语言动态返回对应的 Element Plus 语言包
- 确保 Element Plus 组件(如日期选择器、表单等)显示正确的语言
4、语言切换功能实现
// 添加语言切换方法
const changeLocale = (locale: 'zh-cn' | 'en') => {
// 更新 vue-i18n 的语言设置
i18n.global.locale.value = locale
// 更新 Element Plus 的语言设置
useLocale(computed(() => (locale === 'zh-cn' ? zhCn : en)))
}
// 挂载语言切换方法到全局
app.config.globalProperties.$changeLocale = changeLocale
关键点解析:
i18n.global.locale.value
是响应式属性,更新后会触发界面重新渲染useLocale
是 Element Plus 提供的用于更新组件语言的函数- 将
changeLocale
方法挂载到全局,方便在任何组件中调用
第四步:语言持久化存储
// 在组件中使用语言状态管理
<script setup lang="ts">
import { ElDropdown, ElDropdownItem, ElDropdownMenu, ElMessage } from 'element-plus'
import { getCurrentInstance } from 'vue'
import { useLanguageStore } from '@nofar-core/shared/stores/language'
const {locale } = useI18n()
// 获取Pinia语言状态存储库
const languageStore = useLanguageStore()
// 获取当前Vue实例的代理对象
const { proxy } = getCurrentInstance() as any
// 处理语言切换
const handleCommand = async (command: string) => {
locale.value = command // 更新本地响应式变量
languageStore.setLanguage(command) // 保存到Pinia状态管理
if (command === 'zh-cn') {
proxy.$changeLocale('zh-cn') // 调用全局方法切换中文
ElMessage.success('已切换为中文')
} else if (command === 'en') {
proxy.$changeLocale('en') // 调用全局方法切换英文
ElMessage.success('Switched to English')
}
}
</script>
注意:代码中使用了proxy.$changeLocale
,这是在之前代码中挂载到app.config.globalProperties上的全局方法。
关键点解析:
- 通过调用了
proxy.$changeLocale
,更新i18n的locale
(这样使用i18n的地方会重新渲染),更新Element Plus的locale(这样Element Plus组件会显示对应语言) - 将当前语言保存到
sessionStorage
(键为’app-lang’),实现持久化。 - 当应用重新加载(刷新或重新进入)时,main.ts会从
sessionStorage
中读取'app-lang'
的值,并作为初始语言设置i18n
和Element Plus
。
Pinia状态管理(语言状态存储库定义 (stores/language.ts))
import { defineStore } from 'pinia';
export const useLanguageStore = defineStore('language', {
persist: {
key: 'app-lang',
storage: sessionStorage,
pick: ['currentLang']
},
state: () => ({
currentLang: 'zh-cn' // 默认中文
}),
actions: {
setLanguage(lang: string) {
this.currentLang = lang
},
getApiLangParam() {
return this.currentLang === 'zh-cn' ? 'Ch' : 'En'
}
}
})
第五步:vue页面中使用useI18n中的 $t
函数式组件传递值
代码:
<script setup lang="ts">
import { useI18n } from 'vue-i18n' // 引入 useI18n
const { t } = useI18n()
const querys = reactive([
{
prop: 'personName',
labelKey: '人员名称',
label: ''
},
{
prop: ['startTime', 'endTime'],
labelKey: '出入时间',
label: '',
type: 'date' as const,
datePicker: {
type: 'datetimerange' as const,
valueFormat: 'YYYY-MM-DD HH:mm:ss',
rangeSeparator: '-',
startPlaceholder: t('开始日期'),
endPlaceholder: t('结束日期')
}
},
])
const queryParams = reactive({
userName: undefined,
regionId: undefined,
startTime: undefined,
endTime: undefined
})
const pageParams = reactive({
pageIndex: 1,
pageSize: 10
})
const columns = computed(() => [
{
type: 'selection'
},
{
prop: 'personName',
label: t('人员名称')
},
{
prop: 'cardNo',
label: t('卡号')
},
])
</script>
<template>
<div class="h-full flex flex-col gap-pageGap">
<NfPageHeader v-model="queryParams" :querys @query="resetAndQuery()" @reset="resetAndQuery()" />
<div class="flex flex-1 flex-col gap-pageGap rounded-module bg-white p-4">
<NfTable class="flex-1" height="auto" :columns :data row-key="id">
<template #action="{ row }">
<el-button type="primary" link @click="handleEdit(row)">{{t('查看详情')}}</el-button>
</template>
</NfTable>
<NfPagination v-model="pageParams" :total="total" @change="getList()" />
</div>
</div>
<EditDialog v-model="open" :form :region-list @submit="getList()" />
</template>
我这边用的是封装的组件,正常用form表单可以参考下面的:
<template>
<div class="app-container h-full w-full flex flex-col">
<el-form ref="queryRef" :model="queryParams" :inline="true" class="bg-#fff px-4 pt-6 rounded-lg mb-4">
<el-form-item :label="t('设施名称')" prop="deviceName" label-width="auto">
<el-input v-model="queryParams.deviceName" :placeholder="t('请输入设施名称')" clearable maxlength="30" @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item :label="t('负责人')" prop="userName" label-width="auto">
<el-input v-model="queryParams.userName" :placeholder="t('请输入负责人名称')" clearable maxlength="30" @keyup.enter="handleQuery" />
</el-form-item>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n' // 引入 useI18n
const { t } = useI18n()
const rules = computed(() => {
return {
deviceName: [{ required: true, trigger: 'blur', message: t('设施名称不能为空') }],
userName: [{ required: true, trigger: 'blur', message: t('负责人不能为空') }]
}
})
</script>
效果展示:
第六步:Element Plus的国际化操作
Element Plus 提供了一个 Vue 组件 ConfigProvider
用于全局配置国际化的设置。官方地址https://element-plus.org/zh-CN/guide/i18n.html
只需要在 App.vue
中包一层配置组件即可!
代码:
App.vue
<script setup lang="ts">
import { ElConfigProvider } from 'element-plus'
import en from 'element-plus/es/locale/lang/en' // 英文语言包
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import { useI18n } from 'vue-i18n' // 引入 useI18n
import { RouterView } from 'vue-router'
const { locale } = useI18n() // 获取当前语言
</script>
<template>
<n-config-provider :theme-overrides="themeOverrides" class="h-full">
<el-config-provider :locale="locale === 'zh-cn' ? zhCn : en">
<router-view></router-view>
</el-config-provider>
</n-config-provider>
</template>
效果展示:
第七步:热更新问题
比如:表单验证规则
如果按照reactive
正常写代码,在进行语言切换时会发现表单的验证规则更新不及时!
如下:
const rules = reactive({
name: [{ required: true, message: t('请输入门禁名称'), trigger: 'blur' }],
regionId: [{ required: true, message: t('请选择区域'), trigger: 'change' }],
})
效果:
解决方案:使用computed
const rules = computed(() => {
return {
name: [{ required: true, message: t('请输入门禁名称'), trigger: 'blur' }],
regionId: [{ required: true, message: t('请选择区域'), trigger: 'change' }],
}
})
效果: