昨天在写后台管理系统时,遇到一个需求就是需要实现国际化功能。
antd
和element-ui
这两个框架其实都是有国际化的。
具体展示形式就是如下:
点击右上角头部的语言,切换语言,然后整个系统的文字都改变成对应的语言展示。
切换成英文的效果如下:
下面对整个系统的国际化进行介绍:
1.安装i18n
插件,如果是使用的vue-admin
的框架,则已经安装过了
具体i18n
插件是否安装过了,可以在package,json
中进行查看。
npm install vue-i18n --save
2.在utils
文件夹中添加i18n.js
文件——路由的国际化需要单独处理,其他的国际化是可以用公用国际化文件的
文件内容如下:
import Vue from 'vue'
import VueI18n from 'vue-i18n'
import routesI18n from '@/router/i18n'
import {getI18nKey} from '@/utils/routerUtil'
import CommonI18n from '@/locales/common.i18n';
/**
* 创建 i18n 配置
* @param locale 本地化语言
* @param fallback 回退语言
* @returns {VueI18n}
*/
function initI18n(locale, fallback) {
Vue.use(VueI18n)
let i18nOptions = {
locale,
fallbackLocale: fallback,
silentFallbackWarn: true,
silentTranslationWarn: true,
...CommonI18n,
}
return new VueI18n(i18nOptions)
}
/**
* 根据 router options 配置生成 国际化语言
* @param lang
* @param routes
* @param valueKey
* @returns {*}
*/
function generateI18n(lang, routes, valueKey) {
routes.forEach(route => {
let keys = getI18nKey(route.fullPath).split('.')
let value = valueKey === 'path' ? route[valueKey].split('/').filter(item => !item.startsWith(':') && item != '').join('.') : route[valueKey]
lang.assignProps(keys, value)
if (route.children) {
generateI18n(lang, route.children, valueKey)
}
})
return lang
}
/**
* 格式化 router.options.routes,生成 fullPath
* @param routes
* @param parentPath
*/
function formatFullPath(routes, parentPath = '') {
routes.forEach(route => {
let isFullPath = route.path.substring(0, 1) === '/'
route.fullPath = isFullPath ? route.path : (parentPath === '/' ? parentPath + route.path : parentPath + '/' + route.path)
if (route.children) {
formatFullPath(route.children, route.fullPath)
}
})
}
/**
* 从路由提取国际化数据
* @param i18n
* @param routes
*/
function mergeI18nFromRoutes(i18n, routes) {
formatFullPath(routes)
const CN = generateI18n(new Object(), routes, 'name')
const US = generateI18n(new Object(), routes, 'path')
i18n.mergeLocaleMessage('CN', CN)
i18n.mergeLocaleMessage('US', US)
const messages = routesI18n.messages
Object.keys(messages).forEach(lang => {
i18n.mergeLocaleMessage(lang, messages[lang])
})
}
export {
initI18n,
mergeI18nFromRoutes,
formatFullPath
}
3.router
中添加i18n.js
文件——路由的国际化需要单独处理,其他的国际化是可以用公用国际化文件的
文件内容如下:
注意:这个文件中的格式要跟路由配置文件中的格式要保持一致。比如user下面的children子页面有userCenter和changePassword两个,则需要像下面的对象一样做嵌套。
messages中的对象,多种语言就需要写多个对象,对象的key命名最好跟语言中的key保持一致。
module.exports = {
messages: {
CN: {
home: { name: '首页' },
demo: {
name: '演示页',
},
user: {
name: '个人中心',
userCenter: { name: '个人信息' },
changePassword: { name: '修改账户密码' },
},
},
US: {
home: { name: 'home' },
demo: {
name: 'Demo Page',
},
user: {
name: 'user',
userCenter: { name: 'userCenter' },
changePassword: { name: 'changePassword' },
},
},
HK: {
home: { name: '首頁' },
demo: {
name: '演示頁',
},
user: {
name: '個人中心',
userCenter: { name: '個人信息' },
changePassword: { name: '修改賬戶密碼' },
},
},
},
};
4.utils
中添加routerUtil.js
文件
文件内容如下:
import routerMap from '@/router/async/router.map'
import {mergeI18nFromRoutes} from '@/utils/i18n'
import Router from 'vue-router'
import deepMerge from 'deepmerge'
import basicOptions from '@/router/async/config.async'
//应用配置
let appOptions = {
router: undefined,
i18n: undefined,
store: undefined
}
/**
* 设置应用配置
* @param options
*/
function setAppOptions(options) {
const {router, store, i18n} = options
appOptions.router = router
appOptions.store = store
appOptions.i18n = i18n
}
/**
* 根据 路由配置 和 路由组件注册 解析路由
* @param routesConfig 路由配置
* @param routerMap 本地路由组件注册配置
*/
function parseRoutes(routesConfig, routerMap) {
let routes = []
routesConfig.forEach(item => {
// 获取注册在 routerMap 中的 router,初始化 routeCfg
let router = undefined, routeCfg = {}
if (typeof item === 'string') {
router = routerMap[item]
routeCfg = {path: router.path || item, router: item}
} else if (typeof item === 'object') {
router = routerMap[item.router]
routeCfg = item
}
if (!router) {
console.warn(`can't find register for router ${routeCfg.router}, please register it in advance.`)
router = typeof item === 'string' ? {path: item, name: item} : item
}
// 从 router 和 routeCfg 解析路由
const route = {
path: routeCfg.path || router.path || routeCfg.router,
name: routeCfg.name || router.name,
component: router.component,
redirect: routeCfg.redirect || router.redirect,
meta: {
authority: routeCfg.authority || router.authority || routeCfg.meta?.authority || router.meta?.authority || '*',
icon: routeCfg.icon || router.icon || routeCfg.meta?.icon || router.meta?.icon,
page: routeCfg.page || router.page || routeCfg.meta?.page || router.meta?.page,
link: routeCfg.link || router.link || routeCfg.meta?.link || router.meta?.link
}
}
if (routeCfg.invisible || router.invisible) {
route.meta.invisible = true
}
if (routeCfg.children && routeCfg.children.length > 0) {
route.children = parseRoutes(routeCfg.children, routerMap)
}
routes.push(route)
})
return routes
}
/**
* 加载路由
* @param routesConfig {RouteConfig[]} 路由配置
*/
function loadRoutes(routesConfig) {
//兼容 0.6.1 以下版本
/*************** 兼容 version < v0.6.1 *****************/
if (arguments.length > 0) {
const arg0 = arguments[0]
if (arg0.router || arg0.i18n || arg0.store) {
routesConfig = arguments[1]
console.error('the usage of signature loadRoutes({router, store, i18n}, routesConfig) is out of date, please use the new signature: loadRoutes(routesConfig).')
console.error('方法签名 loadRoutes({router, store, i18n}, routesConfig) 的用法已过时, 请使用新的方法签名 loadRoutes(routesConfig)。')
}
}
/*************** 兼容 version < v0.6.1 *****************/
// 应用配置
const {router, store, i18n} = appOptions
// 如果 routesConfig 有值,则更新到本地,否则从本地获取
if (routesConfig) {
store.commit('account/setRoutesConfig', routesConfig)
} else {
routesConfig = store.getters['account/routesConfig']
}
// 如果开启了异步路由,则加载异步路由配置
const asyncRoutes = store.state.setting.asyncRoutes
if (asyncRoutes) {
if (routesConfig && routesConfig.length > 0) {
const routes = parseRoutes(routesConfig, routerMap)
const finalRoutes = mergeRoutes(basicOptions.routes, routes)
formatRoutes(finalRoutes)
router.options = {...router.options, routes: finalRoutes}
router.matcher = new Router({...router.options, routes:[]}).matcher
router.addRoutes(finalRoutes)
}
}
// 提取路由国际化数据
mergeI18nFromRoutes(i18n, router.options.routes)
// 初始化Admin后台菜单数据
const rootRoute = router.options.routes.find(item => item.path === '/')
const menuRoutes = rootRoute && rootRoute.children
if (menuRoutes) {
store.commit('setting/setMenuData', menuRoutes)
}
}
/**
* 合并路由
* @param target {Route[]}
* @param source {Route[]}
* @returns {Route[]}
*/
function mergeRoutes(target, source) {
const routesMap = {}
target.forEach(item => routesMap[item.path] = item)
source.forEach(item => routesMap[item.path] = item)
return Object.values(routesMap)
}
/**
* 深度合并路由
* @param target {Route[]}
* @param source {Route[]}
* @returns {Route[]}
*/
function deepMergeRoutes(target, source) {
// 映射路由数组
const mapRoutes = routes => {
const routesMap = {}
routes.forEach(item => {
routesMap[item.path] = {
...item,
children: item.children ? mapRoutes(item.children) : undefined
}
})
return routesMap
}
const tarMap = mapRoutes(target)
const srcMap = mapRoutes(source)
// 合并路由
const merge = deepMerge(tarMap, srcMap)
// 转换为 routes 数组
const parseRoutesMap = routesMap => {
return Object.values(routesMap).map(item => {
if (item.children) {
item.children = parseRoutesMap(item.children)
} else {
delete item.children
}
return item
})
}
return parseRoutesMap(merge)
}
/**
* 格式化路由
* @param routes 路由配置
*/
function formatRoutes(routes) {
routes.forEach(route => {
const {path} = route
if (!path.startsWith('/') && path !== '*') {
route.path = '/' + path
}
})
formatAuthority(routes)
}
/**
* 格式化路由的权限配置
* @param routes 路由
* @param pAuthorities 父级路由权限配置集合
*/
function formatAuthority(routes, pAuthorities = []) {
routes.forEach(route => {
const meta = route.meta
const defaultAuthority = pAuthorities[pAuthorities.length - 1] || {permission: '*'}
if (meta) {
let authority = {}
if (!meta.authority) {
authority = defaultAuthority
}else if (typeof meta.authority === 'string') {
authority.permission = meta.authority
} else if (typeof meta.authority === 'object') {
authority = meta.authority
const {role} = authority
if (typeof role === 'string') {
authority.role = [role]
}
if (!authority.permission && !authority.role) {
authority = defaultAuthority
}
}
meta.authority = authority
} else {
const authority = defaultAuthority
route.meta = {authority}
}
route.meta.pAuthorities = pAuthorities
if (route.children) {
formatAuthority(route.children, [...pAuthorities, route.meta.authority])
}
})
}
/**
* 从路由 path 解析 i18n key
* @param path
* @returns {*}
*/
function getI18nKey(path) {
const keys = path.split('/').filter(item => !item.startsWith(':') && item != '')
keys.push('name')
return keys.join('.')
}
/**
* 加载导航守卫
* @param guards
* @param options
*/
function loadGuards(guards, options) {
const {beforeEach, afterEach} = guards
const {router} = options
beforeEach.forEach(guard => {
if (guard && typeof guard === 'function') {
router.beforeEach((to, from, next) => guard(to, from, next, options))
}
})
afterEach.forEach(guard => {
if (guard && typeof guard === 'function') {
router.afterEach((to, from) => guard(to, from, options))
}
})
}
export {parseRoutes, loadRoutes, formatAuthority, getI18nKey, loadGuards, deepMergeRoutes, formatRoutes, setAppOptions}
5.router
中async
文件夹中添加router.map
文件
里面的内容比较多,有需要的可以留个邮箱给我,我打包发给你。
6.重点是commonI18n
文件,在locales
文件夹中
common.i18n.js
文件中的内容如下:
import CN from './CN';
import US from './US';
import HK from './HK';
//多种语言,则需要有多个文件用于区分
// 全局公共的国际化定义
export default {
messages: {
CN,
US,
HK,
},
};
以CN.js
为例:
// 全局公共的国际化定义 - CN
export default {
user:'用户',
creator:'创建人',
orderNo: '订单编号',
search:'搜索',
cancel:'取消',
CancelEditing:'取消编辑',
edit:'编辑',
submit:'提交',
reset:'重置',
....
}
对应的US.js
文件内容如下:
// 全局公共的国际化定义 - US
export default {
user:'User',
creator:'Creator',
orderNo: 'Order No',
search: 'Search',
cancel:'Cancel',
edit:'Edit',
CancelEditing:'Cancel Editing',
submit:'Submit',
reset:'Reset',
...
}
这个算是国际化的公共文件,国际化是就近原则,如果是单个页面有单独的i18n
文件,则会从单独的i18n
文件中查找对应的字段,没有找不到,则会从公共的i18n
文件中去查找。
7.以单个文件国际化为例:
页面中使用国际化字段的方式$t(xxx)
8.路由的国际化文件需要跟路由配置文件进行匹配,其他页面的国际化要跟公共国际化文件的格式保持一致即可。
9.通过以上的步骤,菜单+页面中静态的文字都可以实现国际化了,但是接口返回的数据国际化,则需要接口返回不同的文字了。
此时可以在axios
请求时,则请求头上添加当前的语言类型。
9.1 在axios
拦截器文件中的请求部分添加如下的代码
我需要在拦截器.js文件中获取vuex中存储的lang字段的值,此时是拿不到vuex中的数据的,因为this是undefined
因此需要在main.js
文件中添加如下的内容:
将vue挂载到window上
,则其他页面都可以通过window.vm
获取到vue了
...
window.vm = new Vue({
router,
store,
i18n,
render: (h) => h(App),
}).$mount('#app');
拦截器中的写法:
const reqCommon = {
/**
* 发送请求之前做些什么
* @param config axios config
* @param options 应用配置 包含: {router, i18n, store, message}
* @returns {*}
*/
onFulfilled(config, options) {
const { message } = options;
const { url, xsrfCookieName, headers } = config;
// if (url.indexOf('login') === -1 && xsrfCookieName && !Cookie.get(xsrfCookieName)) {
// message.warning('认证 token 已过期,请重新登录')
// }
if (
headers.Authorization &&
xsrfCookieName &&
!Cookie.get(xsrfCookieName)
) {
message.warning('认证 token 已过期,请重新登录');
}
config.headers['Authorization'] = Cookie.get(xsrfHeaderName);
window.vm.$store.commit(
'setting/setLang',
localStorage.getItem('language') || 'CN'
);
console.log('语言', window.vm.$store.state.setting.lang);
config.headers['language'] = window.vm.$store.state.setting.lang;
return config;
},
.......
上面的代码最重要的就是:
window.vm.$store.commit(
'setting/setLang',
localStorage.getItem('language') || 'CN'
);
console.log('语言', window.vm.$store.state.setting.lang);
config.headers['language'] = window.vm.$store.state.setting.lang;
为什么要存储到localStorage
中?因为,在切换语言时,接口也需要重新请求,则也就是说整个页面全部刷新,此时最简单的方法就是window.vm.$router.go(0)
实现页面的刷新。
页面刷新时,vuex中的setting/lang的默认值是CN简体中文
,为了能够存储上次切换的语言类型,可以存储到本地localStorage,这样浏览器不关闭的时候,这个缓存还是有的。
vuex中的setting文件中的setLang方法也需要改变
setLang(state, lang) {
state.lang = lang;
if (localStorage.getItem('language') != lang) {
window.vm.$router.go(0);
}
localStorage.setItem('language', lang);
console.log('setLang', window.vm.$route);
},
完成!!!