后台管理项目经常会遇到需要支持多语言的情况,主要实现思路是element-ui的多语言+界面多语言
技术栈
element-ui多语言包(element-ui中自带)vue-i18n组合语言包vuex存储记录当前多语言标识
vue2环境下实现
1. 安装对应插件
# vue2环境下使用<=8版本
npm i vue-i18n@7.3.2 -s
# 或者其它ui框架
npm i element-ui -s
2. 引入对应语言包
// lang/index.js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import Cookies from 'js-cookie'
import elementEnLocale from 'element-ui/lib/locale/lang/en' // element-ui lang
import elementZhLocale from 'element-ui/lib/locale/lang/zh-CN'// element-ui lang
import elementEsLocale from 'element-ui/lib/locale/lang/es'// element-ui lang
import elementJaLocale from 'element-ui/lib/locale/lang/ja'// element-ui lang
import enLocale from './en'
import zhLocale from './zh'
import esLocale from './es'
import jaLocale from './ja'
Vue.use(VueI18n)
const messages = {
en: {
...enLocale,
...elementEnLocale
},
zh: {
...zhLocale,
...elementZhLocale
},
es: {
...esLocale,
...elementEsLocale
},
ja: {
...jaLocale,
...elementJaLocale
}
}
export function getLanguage() {
const chooseLanguage = Cookies.get('language')
if (chooseLanguage) return chooseLanguage
// if has not choose language
const language = (navigator.language || navigator.browserLanguage).toLowerCase()
const locales = Object.keys(messages)
for (const locale of locales) {
if (language.indexOf(locale) > -1) {
return locale
}
}
return 'en'
}
const i18n = new VueI18n({
// set locale
// options: en | zh | es
locale: getLanguage(),
// set locale messages
messages
})
export default i18n
3. 挂载到Vue下
// main.js
import i18n from './lang' // internationalization
new Vue({
el: '#app',
// ...
i18n,
render: h => h(App)
})
4. 使用vuex+cookie存储当前语言标识
// store/modules/app.js
import Cookies from 'js-cookie'
import {getLanguage} from '@/lang'
const state = {
language: getLanguage(),
}
const mutations = {
SET_LANGUAGE: (state, language) => {
state.language = language
Cookies.set('language', language)
},
}
const actions = {
setLanguage({commit}, language) {
commit('SET_LANGUAGE', language)
},
}
export default {
namespaced: true,
state,
mutations,
actions
}
5. 多语言切换和使用
<template>
<div>
<el-card class="box-card" style="margin-top:40px;">
<div slot="header" class="clearfix">
<svg-icon icon-class="international" />
<span style="margin-left:10px;">{{ $t('i18nView.title') }}</span>
</div>
<div>
<el-radio-group v-model="lang" size="small">
<el-radio label="zh" border>
简体中文
</el-radio>
<el-radio label="en" border>
English
</el-radio>
<el-radio label="es" border>
Español
</el-radio>
<el-radio label="ja" border>
日本語
</el-radio>
</el-radio-group>
<el-tag style="margin-top:15px;display:block;" type="info">
{{ $t('i18nView.note') }}
</el-tag>
</div>
</el-card>
<el-row :gutter="20" style="margin:100px 15px 50px;">
<el-col :span="12" :xs="24">
<div class="block">
<el-date-picker v-model="date" :placeholder="$t('i18nView.datePlaceholder')" type="date" />
</div>
<div class="block">
<el-select v-model="value" :placeholder="$t('i18nView.selectPlaceholder')">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</div>
<div class="block">
<el-button class="item-btn" size="small">
{{ $t('i18nView.default') }}
</el-button>
<el-button class="item-btn" size="small" type="primary">
{{ $t('i18nView.primary') }}
</el-button>
<el-button class="item-btn" size="small" type="success">
{{ $t('i18nView.success') }}
</el-button>
<el-button class="item-btn" size="small" type="info">
{{ $t('i18nView.info') }}
</el-button>
<el-button class="item-btn" size="small" type="warning">
{{ $t('i18nView.warning') }}
</el-button>
<el-button class="item-btn" size="small" type="danger">
{{ $t('i18nView.danger') }}
</el-button>
</div>
</el-col>
<el-col :span="12" :xs="24">
<el-table :data="tableData" fit highlight-current-row border style="width: 100%">
<el-table-column :label="$t('i18nView.tableName')" prop="name" width="100" align="center" />
<el-table-column :label="$t('i18nView.tableDate')" prop="date" width="120" align="center" />
<el-table-column :label="$t('i18nView.tableAddress')" prop="address" />
</el-table>
</el-col>
</el-row>
</div>
</template>
<script>
import local from './local'
const viewName = 'i18nView'
export default {
name: 'I18n',
data() {
return {
date: '',
tableData: [{
date: '2016-05-03',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles'
},
{
date: '2016-05-02',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles'
},
{
date: '2016-05-04',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles'
},
{
date: '2016-05-01',
name: 'Tom',
address: 'No. 189, Grove St, Los Angeles'
}],
options: [],
value: ''
}
},
computed: {
lang: {
get() {
return this.$store.state.app.language
},
set(lang) {
this.$i18n.locale = lang
this.$store.dispatch('app/setLanguage', lang)
}
}
},
watch: {
lang() {
this.setOptions()
}
},
created() {
if (!this.$i18n.getLocaleMessage('en')[viewName]) {
this.$i18n.mergeLocaleMessage('en', local.en)
this.$i18n.mergeLocaleMessage('zh', local.zh)
this.$i18n.mergeLocaleMessage('es', local.es)
this.$i18n.mergeLocaleMessage('ja', local.ja)
}
this.setOptions() // set default select options
},
methods: {
setOptions() {
this.options = [
{
value: '1',
label: this.$t('i18nView.one')
},
{
value: '2',
label: this.$t('i18nView.two')
},
{
value: '3',
label: this.$t('i18nView.three')
}
]
}
}
}
</script>
<style scoped>
.box-card {
width: 600px;
max-width: 100%;
margin: 20px auto;
}
.item-btn{
margin-bottom: 15px;
margin-left: 0px;
}
.block {
padding: 25px;
}
</style>
6. 动态加载语言包
如果语言包需要存储在服务端或在cdn资源,需要在
lang/index.js和App.vue进行修改。 需要注意的是,切换完成语言后,需要调用location.reload重新加载页面,确保先加载语言包,再渲染页面。
// lang/index.js
// 注释调资源包的引入,这里只注册element-ui对应的语言包
import elementEnLocale from 'jylink-web-ui/lib/locale/lang/en' // element-ui lang
import elementZhLocale from 'jylink-web-ui/lib/locale/lang/zh-CN'// element-ui lang
import elementFrLocale from 'jylink-web-ui/lib/locale/lang/fr'// element-ui lang
import elementSrLocale from 'jylink-web-ui/lib/locale/lang/sr'// element-ui lang
// import zhLocale from './zh'
Vue.use(VueI18n)
const messages = {
en: {
...elementEnLocale
},
'zh-CN': {
// ...zhLocale,
...elementZhLocale
},
fr: {
...elementFrLocale
},
sr: {
...elementSrLocale
},
}
App.Vue
<template>
<div id="app" v-if="flag">
<router-view></router-view>
</div>
</template>
<script>
import store from "./store/index";
import {getLang} from "@/api/index";
export default {
name: "App",
data() {
return {
flag: false
}
},
computed: {
language() {
return store.state.app.language
},
},
created() {
this.getLanguage()
},
mounted() {
},
methods: {
/**
* @method 获取多语言配置
* @returns {Promise<void>}
* @description 使用flag控制,保证先获取语言包信息,再进行加载页面,解决页面多语言未解析问题
*/
async getLanguage() {
// token存在时,才获取语言包信息
const token = store.getters.access_token
if (!token) {
this.flag = true
return
}
// 获取当前语言下的语言包
let res = await getLangInfo({code: this.language})
let {data, code} = res.data
// 处理token过期或请求失败
if (code != 0) {
this.flag = true
return
}
// 处理json解析异常问题
try {
if (data.config) {
let jsonData = JSON.parse(data.config)
// 使用vue-i18n的mergeLocaleMessage合并语言包
this.$i18n.mergeLocaleMessage(this.language, jsonData)
this.flag = true
}
} catch (e) {
console.error('JSON 解析错误:', e);
this.flag = true
}
},
},
};
</script>
vue3环境语言包支持
vue3和vue2整理的配置都相差不大,这里只记录下差异点
1. 安装vue-i18n
# vue3环境下安装9版本以上
npm i vue-i18n@9.2.2 -s
2. 配置vue-i18n插件
// plugins/vue|18n/.index.ts
import type { App } from 'vue'
import { createI18n } from 'vue-i18n'
import { useLocaleStoreWithOut } from '@/store/modules/locale'
import type { I18n, I18nOptions } from 'vue-i18n'
export let i18n: ReturnType<typeof createI18n>
export const setHtmlPageLang = (locale: LocaleType) => {
document.querySelector('html')?.setAttribute('lang', locale)
}
const createI18nOptions = async (): Promise<I18nOptions> => {
const localeStore = useLocaleStoreWithOut()
const locale = localeStore.getCurrentLocale
const localeMap = localeStore.getLocaleMap
const defaultLocal = await import(`../../locales/${locale.lang}.ts`)
const message = defaultLocal.default ?? {}
setHtmlPageLang(locale.lang)
localeStore.setCurrentLocale({
lang: locale.lang
// elLocale: elLocal
})
return {
legacy: false,
locale: locale.lang,
fallbackLocale: locale.lang,
messages: {
[locale.lang]: message
},
availableLocales: localeMap.map((v) => v.lang),
sync: true,
silentTranslationWarn: true,
missingWarn: false,
silentFallbackWarn: true
}
}
export const setupI18n = async (app: App<Element>) => {
const options = await createI18nOptions()
i18n = createI18n(options) as I18n
app.use(i18n)
}
3. 配置pina管理语言配置
// @store/modules/locala.ts
import { defineStore } from 'pinia'
import { store } from '../index'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'
import { useCache } from '@/hooks/web/useCache'
import { LocaleDropdownType } from '@/types/localeDropdown'
const { wsCache } = useCache()
const elLocaleMap = {
'zh-CN': zhCn,
en: en
}
interface LocaleState {
currentLocale: LocaleDropdownType
localeMap: LocaleDropdownType[]
}
export const useLocaleStore = defineStore('locales', {
state: (): LocaleState => {
return {
currentLocale: {
lang: wsCache.get('lang') || 'zh-CN',
elLocale: elLocaleMap[wsCache.get('lang') || 'zh-CN']
},
// 多语言
localeMap: [
{
lang: 'zh-CN',
name: '简体中文'
},
{
lang: 'en',
name: 'English'
}
]
}
},
getters: {
getCurrentLocale(): LocaleDropdownType {
return this.currentLocale
},
getLocaleMap(): LocaleDropdownType[] {
return this.localeMap
}
},
actions: {
setCurrentLocale(localeMap: LocaleDropdownType) {
// this.locale = Object.assign(this.locale, localeMap)
this.currentLocale.lang = localeMap?.lang
this.currentLocale.elLocale = elLocaleMap[localeMap?.lang]
wsCache.set('lang', localeMap?.lang)
}
}
})
export const useLocaleStoreWithOut = () => {
return useLocaleStore(store)
}
4. 使用vue3 hooks方式,封装多语言调用
// hooks/web/use|18n.ts
import { i18n } from '@/plugins/vueI18n'
type I18nGlobalTranslation = {
(key: string): string
(key: string, locale: string): string
(key: string, locale: string, list: unknown[]): string
(key: string, locale: string, named: Record<string, unknown>): string
(key: string, list: unknown[]): string
(key: string, named: Record<string, unknown>): string
}
type I18nTranslationRestParameters = [string, any]
const getKey = (namespace: string | undefined, key: string) => {
if (!namespace) {
return key
}
if (key.startsWith(namespace)) {
return key
}
return `${namespace}.${key}`
}
export const useI18n = (
namespace?: string
): {
t: I18nGlobalTranslation
} => {
const normalFn = {
t: (key: string) => {
return getKey(namespace, key)
}
}
if (!i18n) {
return normalFn
}
const { t, ...methods } = i18n.global
const tFn: I18nGlobalTranslation = (key: string, ...arg: any[]) => {
if (!key) return ''
if (!key.includes('.') && !namespace) return key
return (t as any)(getKey(namespace, key), ...(arg as I18nTranslationRestParameters))
}
return {
...methods,
t: tFn
}
}
export const t = (key: string) => key
5. 页面中使用多语言
<script setup lang="ts">
import { ContentWrap } from '@/components/ContentWrap'
import { useI18n } from '@/hooks/web/useI18n'
import { Infotip } from '@/components/Infotip'
const { t } = useI18n()
const keyClick = (key: string) => {
if (key === t('iconDemo.accessAddress')) {
window.open('https://iconify.design/')
}
}
</script>
<template>
<ContentWrap :title="t('infotipDemo.infotip')" :message="t('infotipDemo.infotipDes')">
<Infotip
:show-index="false"
:title="`${t('iconDemo.recommendedUse')}${t('iconDemo.iconify')}`"
:schema="[
{
label: t('iconDemo.recommendeDes'),
keys: ['Iconify']
},
{
label: t('iconDemo.accessAddress'),
keys: [t('iconDemo.accessAddress')]
}
]"
@click="keyClick"
/>
</ContentWrap>
</template>
总结
vue2和vue3整体的实现思路都相似,只是vue3中使用了hooks对多语言进一步封装,已经有不少内容属于ts类型的定义- 多语言需要考虑语言包的管理和存储,不建议直接存储在前端,后续包体积会持续增大,同时可能存在前端和后端环境都需要存储一份语言包文件
- 切换不同语言后,需要考虑页面布局是否会出现内容溢出或换行等情况,尽可能保证不同语言下都能正常显示