国际化处理方案
# 需求
我们有一个变量
msg
,但是这个msg
有且只能有两个值:
- hello world
- 你好世界
要求:根据需要切换
msg
的值
# 代码实现
<script>
// 1. 定义 msg 值的数据源
const messages = {
en: {
msg: 'hello world'
},
zh: {
msg: '你好世界'
}
}
// 2. 定义切换变量
let locale = 'en'
// 3. 定义赋值函数
function t(key) {
return messages[locale][key]
}
// 4. 为 msg 赋值
let msg = t('msg')
console.log(msg);
// 修改 locale, 重新执行 t 方法,获取不同语言环境下的值
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 总结
- 通过一个变量来 控制 语言环境
- 所有语言环境下的数据源要 预先 定义好
- 通过一个方法来获取 当前语言 下 指定属性 的值
- 该值即为国际化下展示值
# 基于 vue-i18n V9 实现方案
注意
vue3下需要使用
V 9.x的
i18n
vue-i18n (opens new window) 使用可以分为四个部分:
- 创建
messages
数据源 - 创建
locale
语言变量 - 初始化
i18n
实例 - 注册
i18n
实例
# 代码实现
安装
vue-i18n
npm install vue-i18n@next
1创建
i18n/index.js
文件创建
messages
数据源const messages = { en: { msg: { test: 'hello world' } }, zh: { msg: { test: '你好世界' } } }
1
2
3
4
5
6
7
8
9
10
11
12创建
locale
语言变量const locale = 'en'
1初始化
i18n
实例import { createI18n } from 'vue-i18n' const i18n = createI18n({ // 使用 Composition API 模式,则需要将其设置为false legacy: false, // 全局注入 $t 函数 globalInjection: true, locale, messages })
1
2
3
4
5
6
7
8
9
10把
i18n
注册到vue
实例export default i18n
1在
main.js
中导入// i18n (PS:导入放到 APP.vue 导入之前,因为后面我们会在 app.vue 中使用国际化内容) import i18n from '@/i18n' ... app.use(i18n)
1
2
3
4在
layout/components/Sidebar/index.vue
中使用i18n
<h1 class="logo-title" v-if="$store.getters.sidebarOpened"> {{ $t('msg.test') }} </h1>
1
2
3修改
locale
的值,即可改变展示的内容
项目中完成国际化分成以下几步进行:
- 封装
langSelect
组件用于修改locale
- 导入
el-locale
语言包 - 创建自定义语言包
# 方案落地:封装 langSelect 组件
定义
store/app.js
import { LANG } from '@/constant' import { getItem, setItem } from '@/utils/storage' export default { namespaced: true, state: () => ({ ... language: getItem(LANG) || 'zh' }), mutations: { ... /** * 设置国际化 */ setLanguage(state, lang) { setItem(LANG, lang) state.language = lang } }, actions: {} }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20在
constant
中定义常量// 国际化 export const LANG = 'language'
1
2在
store/getters.js
中定义language: state => state.app.language
1创建
components/LangSelect/index
<template> <el-dropdown trigger="click" class="international" @command="handleSetLanguage" > <div> <el-tooltip content="国际化" :effect="effect"> <svg-icon icon="language" /> </el-tooltip> </div> <template #dropdown> <el-dropdown-menu> <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> </template> </el-dropdown> </template> <script setup> import { useI18n } from 'vue-i18n' import { defineProps, computed } from 'vue' import { useStore } from 'vuex' import { ElMessage } from 'element-plus' defineProps({ effect: { type: String, default: 'dark', validator: function(value) { // 这个值必须匹配下列字符串中的一个 return ['dark', 'light'].indexOf(value) !== -1 } } }) const store = useStore() const language = computed(() => store.getters.language) // 切换语言的方法 const i18n = useI18n() const handleSetLanguage = lang => { i18n.locale.value = lang store.commit('app/setLanguage', lang) ElMessage.success('更新成功') } </script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52在
navbar
中导入LangSelect
<template> <div class="navbar"> ... <div class="right-menu"> <lang-select class="right-menu-item hover-effect" /> <!-- 头像 --> ... </div> </div> </template> <script setup> import LangSelect from '@/components/LangSelect' ... </script> <style lang="scss" scoped> .navbar { ... .right-menu { ... ::v-deep .right-menu-item { display: inline-block; padding: 0 18px 0 0; font-size: 24px; color: #5a5e66; vertical-align: text-bottom; &.hover-effect { cursor: pointer; } } ... } </style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 方案落地:element-plus 国际化处理
对于语言包来说,整个项目会分成两部分:
element-plus
语言包:用来处理element
组件的国际化功能- 自定义语言包:用来处理 非
element
组件的国际化功能
**按照正常的逻辑,我们是可以通过 element-ui
配合 vue-i18n
来实现国际化功能的,但是目前的 element-plus
尚未提供配合 vue-i18n
实现国际化的方式! **
所以说,我们暂时只能先去做临时处理,等到
element-plus
支持vue-i18n
功能之后,我们再进行对接实现
升级
element-plus
到最新版本npm i element-plus
1注意
目前课程中使用的最新版本为:
^1.1.0-beta.15
升级版本之后,左侧
menu
菜单无法正常显示,这是因为element-plus
修改了el-submenu
的组件名称到
layout/components/Sidebar/SidebarItem
中,修改el-submenu
为el-sub-menu
接下来实现国际化
在
plugins/index
中导入element
的中文、英文语言包:
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/lib/locale/lang/en'
2
注册
element
时,根据当前语言选择使用哪种语言包import store from '@/store' export default app => { app.use(ElementPlus, { locale: store.getters.language === 'en' ? en : zhCn }) }
1
2
3
4
5
6
7
# 自定义语言包国际化处理
自定义语言包我们使用了 commonJS
导出了一个对象,这个对象就是所有的 自定义语言对象
在
lang/index
中,导入语言包import mZhLocale from './lang/zh' import mEnLocale from './lang/en'
1
2在
messages
中注册到语言包const messages = { en: { msg: { ...mEnLocale } }, zh: { msg: { ...mZhLocale } } }
1
2
3
4
5
6
7
8
9
10
11
12
# 处理项目国际化内容
对于我们目前的项目而言,需要进行国际化处理的地方主要分为:
- 登录页面
navbar
区域sidebar
区域- 面包屑区域
登录页面:
login/index
<template>
<div class="login-container">
...
<div class="title-container">
<h3 class="title">{{ $t('msg.login.title') }}</h3>
<lang-select class="lang-select" effect="light"></lang-select>
</div>
...
<el-button
type="primary"
style="width: 100%; margin-bottom: 30px"
:loading="loading"
@click="handleLogin"
>{{ $t('msg.login.loginBtn') }}</el-button
>
<div class="tips" v-html="$t('msg.login.desc')"></div>
</el-form>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
...
// 验证规则
const i18n = useI18n()
const loginRules = ref({
username: [
{
...
message: i18n.t('msg.login.usernameRule')
}
],
...
})
...
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
login/rules
import i18n from '@/i18n'
export const validatePassword = () => {
return (rule, value, callback) => {
if (value.length < 6) {
callback(new Error(i18n.global.t('msg.login.passwordRule')))
} else {
callback()
}
}
}
2
3
4
5
6
7
8
9
10
navbar
区域
layout/components/navbar
<template>
<div class="navbar">
...
<template #dropdown>
<el-dropdown-menu class="user-dropdown">
<router-link to="/">
<el-dropdown-item> {{ $t('msg.navBar.home') }} </el-dropdown-item>
</router-link>
<a target="_blank" href="">
<el-dropdown-item>{{ $t('msg.navBar.course') }}</el-dropdown-item>
</a>
<el-dropdown-item divided @click="logout">
{{ $t('msg.navBar.logout') }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
components/LangSelect/index
<el-tooltip :content="$t('msg.navBar.lang')" :effect="effect">
...
const handleSetLanguage = lang => {
...
ElMessage.success(i18n.t('msg.toast.switchLangSuccess'))
}
2
3
4
5
6
# sidebar 与 面包屑 区域的国际化处理
sidebar 区域
目前对于 sidebar
而言,显示的文本是我们在定义路由表时的 title
<span>{{ title }}</span>
我们可以 把 title
作为语言包内容的 key
进行处理
创建 utils/i18n
工具模块,用于 将 title
转化为国际化内容
import i18n from '@/i18n'
export function generateTitle(title) {
return i18n.global.t('msg.route.' + title)
}
2
3
4
在 layout/components/Sidebar/MenuItem.vue
中导入该方法:
<template>
...
<span>{{ generateTitle(title) }}</span>
</template>
<script setup>
import { generateTitle } from '@/utils/i18n'
...
</script>
2
3
4
5
6
7
8
9
最后修改下 sidebarHeader
的内容
<h1 class="logo-title" v-if="$store.getters.sidebarOpened">
imooc-admin
</h1>
2
3
面包屑区域:
在 components/Breadcrumb/index
<template>
...
<!-- 不可点击项 -->
<span v-if="index === breadcrumbData.length - 1" class="no-redirect">{{
generateTitle(item.meta.title)
}}</span>
<!-- 可点击项 -->
<a v-else class="redirect" @click.prevent="onLinkClick(item)">{{
generateTitle(item.meta.title)
}}</a>
...
</template>
<script setup>
import { generateTitle } from '@/utils/i18n'
...
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 国际化缓存处理
我们希望在 刷新页面后,当前的国际化选择可以被保留,所以想要实现这个功能,那么就需要进行 国际化的缓存处理
此处的缓存,我们依然通过两个方面进行:
vuex
缓存LocalStorage
缓存
只不过这里的缓存,我们已经在处理 langSelect
组件时 处理完成了,所以此时我们只需要使用缓存下来的数据即可。
在 i18n/index
中,创建 getLanguage
方法:
import store from '@/store'
/**
* 返回当前 lang
*/
function getLanguage() {
return store && store.getters && store.getters.language
}
2
3
4
5
6
7
修改 createI18n
的 locale
为 getLanguage()
const i18n = createI18n({
...
locale: getLanguage()
})
2
3
4
# 更新:关于 element-plus 国际化问题更新
现在 element-plus
已经提供了 国际化的处理方案 (opens new window),我们可以直接通过 el-config-provider
组件中的 locale
属性来指定当前国际化环境。
具体代码如下:
- 在
src/plugins/element
中 - 在
src/App.vue
中
# 更新:关于登录页面表单校验提示无法自动国际化的问题
此问题来源于一个反馈:
https://coding.imooc.com/learn/questiondetail/254087.html
复现方式:
- 在一种语言环境下,展示表单校验提示信息
- 切换语言环境
- 预期:提示信息进行国际化转换
- 现实:提示信息未进行国际化转换
分析原因:
表单校验提示信息的内容取决于 loginRules
中具体选项的 message
属性,我们对该 message
属性进行了初始赋值:
初始赋值时, i18n
会根据当前语言环境获取到对应的国际化内容。
但是当语言环境改变时,message
属性的 value
未重新获取。(即:依然为 初始赋值 内容)。
因此会出现以上 bug
解决方案:
那么想要解决这个问题,就需要从问题的原因入手。
方案具体分为两步:
message
属性的value
应该是动态获取的:针对
username
,它应该为一个计算属性:const loginRules = ref({ username: [ { ... - message: i18n.t('msg.login.usernameRule') + message: computed(() => { + return i18n.t('msg.login.usernameRule') + }) } ], password: [...] })
1
2
3
4
5
6
7
8
9
10
11
12针对
password
,因为它的验证为validator
,本身就会重新计算,所以无需改动
监听语言的变化,主动触发校验,以便
message
属性的value
进行重新获取import { watchSwitchLang } from '@/utils/i18n' watchSwitchLang(() => { loginFromRef.value.validate() })
1
2
3
4
5
至此,该问题得到处理。
# 国际化方案总结
国际化是前端项目中的一个非常常见的功能,那么在前端项目中实现国际化主要依靠的就是 vue-i18n
这个第三方的包。
关于国际化的实现原理大家可以参照 国际化实现原理 这一小节,这里我们就不再赘述了。
而 i18n
的使用,整体来说就分为这么四步:
- 创建
messages
数据源 - 创建
locale
语言变量 - 初始化
i18n
实例 - 注册
i18n
实例
核心的内容其实就是 数据源的部分,但是大家需要注意,如果你的项目中使用了 第三方组件库 ,那么不要忘记 第三方组件库的数据源 需要 单独 进行处理!
# 接口国际化:处理接口国际化问题
项目介绍 中,数据存在两种:
- 本地写死的国际化数据
- 接口获取到的数据
那么针对于第一种数据是可以直接完成国际化展示的。
但是第二种数据因为是从服务端获取到的,所以说,服务端返回什么内容,那么前端就会展示什么内容。
所以说如果我们想要完成接口的国际化,那么就需要让服务端返回对应国际化的数据。
在接口请求的 headers
中增加 Accept-Language
表明当前我们所需要的语言类型,在 支持国际化 的接口服务中,可以直接获取到国际化数据
在
utils/request.js
的请求拦截器中增加headers
配置// 请求拦截器 service.interceptors.request.use( config => { // 在这个位置需要统一的去注入token if (store.getters.token) { ... } // 配置接口国际化 config.headers['Accept-Language'] = store.getters.language return config // 必须返回配置 }, error => { return Promise.reject(error) } )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
在我们切换了语言之后,刷新 页面即可获取到 国际化返回数据
但是每次都刷新页面的操作未免不太友好,那么有没有什么办法可以跳过刷新这个步骤呢?
我们之前定义过的 watchSwitchLang
方法,该方法可以 监听到语言的变化,然后指定操作。依赖这个方法得出以下代码:
在
views/profile/index
中import { watchSwitchLang } from '@/utils/i18n' // 监听语言切换 watchSwitchLang(getFeatureData)
1
2
3
剩下的就是 用户信息 数据的国际化实现,在 app.js
中监听语言变化,重新制定获取用户信息的动作
在
src/App.vue
中import { watchSwitchLang } from '@/utils/i18n' /** * 监听 语言变化,重新获取个人信息 */ watchSwitchLang(() => { if (store.getters.token) { store.dispatch('user/getUserInfo') } })
1
2
3
4
5
6
7
8
9
10