vue多语言支持

717 阅读5分钟

后台管理项目经常会遇到需要支持多语言的情况,主要实现思路是element-ui的多语言+界面多语言

技术栈

  1. element-ui多语言包(element-ui中自带)
  2. vue-i18n组合语言包
  3. 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.jsApp.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>

总结

  1. vue2vue3整体的实现思路都相似,只是vue3中使用了hooks对多语言进一步封装,已经有不少内容属于ts类型的定义
  2. 多语言需要考虑语言包的管理和存储,不建议直接存储在前端,后续包体积会持续增大,同时可能存在前端和后端环境都需要存储一份语言包文件
  3. 切换不同语言后,需要考虑页面布局是否会出现内容溢出或换行等情况,尽可能保证不同语言下都能正常显示

参考内容

  1. vue-element-admin
  2. vue-element-plus-admin