vue后台管理系统——添加i18n国际化功能——技能提升

news/2024/11/15 3:57:21/

昨天在写后台管理系统时,遇到一个需求就是需要实现国际化功能。

antdelement-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.messagesObject.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} = optionsappOptions.router = routerappOptions.store = storeappOptions.i18n = i18n
}/*** 根据 路由配置 和 路由组件注册 解析路由* @param routesConfig 路由配置* @param routerMap 本地路由组件注册配置*/
function parseRoutes(routesConfig, routerMap) {let routes = []routesConfig.forEach(item => {// 获取注册在 routerMap 中的 router,初始化 routeCfglet 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.asyncRoutesif (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:[]}).matcherrouter.addRoutes(finalRoutes)}}// 提取路由国际化数据mergeI18nFromRoutes(i18n, router.options.routes)// 初始化Admin后台菜单数据const rootRoute = router.options.routes.find(item => item.path === '/')const menuRoutes = rootRoute && rootRoute.childrenif (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} = routeif (!path.startsWith('/') && path !== '*') {route.path = '/' + path}})formatAuthority(routes)
}/*** 格式化路由的权限配置* @param routes 路由* @param pAuthorities 父级路由权限配置集合*/
function formatAuthority(routes, pAuthorities = []) {routes.forEach(route => {const meta = route.metaconst 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.authorityconst {role} = authorityif (typeof role === 'string') {authority.role = [role]}if (!authority.permission && !authority.role) {authority = defaultAuthority}}meta.authority = authority} else {const authority = defaultAuthorityroute.meta = {authority}}route.meta.pAuthorities = pAuthoritiesif (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} = guardsconst {router} = optionsbeforeEach.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.routerasync文件夹中添加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);
},

完成!!!


http://www.ppmy.cn/news/33285.html

相关文章

011+limou+C语言深入知识——(3)“字符串函数”和“字符分类函数”和“内存操作函数”以及“部分库函数的模拟实现”

一、字符串库函数 001、求字符串长度strlen size_t strlen ( const char * str );注意size_t是一个无符号类型&#xff0c;没有正负 #include <stdio.h> #include <string.h> int main() {char*str1 "abcdef";strcmpchar*str2 "bbb";if( …

Vue3(递归组件) + 原生Table 实现树结构复杂表格

一、递归组件 什么是递归&#xff0c;Javascript中经常能接触到递归函数。也就是函数自己调用自己。那对于组件来说也是一样的逻辑。平时工作中见得最多应该就是菜单组件&#xff0c;大部分系统里面的都是递归组件。文章中我做了按需引入的配置&#xff0c;所以看不到我引用组…

为什么VMware会给我多创建了两个网络呢?Windows和Linux为什么可以彼此ping的通呢

为什么VMware会给我多创建了两个网络呢&#xff1f;Windows和Linux为什么可以彼此ping的通呢 文章目录为什么VMware会给我多创建了两个网络呢&#xff1f;Windows和Linux为什么可以彼此ping的通呢桥接模式ANT模式&#xff08;VMnet8&#xff09;仅主机模式&#xff08;VMnet1&a…

牛客top100 - 自刷打卡day1 - 链表

top100-打卡day1链表BM1反转链表BM2链表内指定区间反转BM3链表中的节点每k个一组翻转BM4合并两个排序的链表BM5合并k个已排序的链表BM6判断链表中是否有环BM7链表中环的入口结点BM8链表中倒数最后k个结点BM9删除链表的倒数第n个节点BM10两个链表的第一个公共结点BM11链表相加(二…

CSDN周赛第37期题解(Python版)

这期周赛题目和测试集还算完整&#xff0c;没有出现往期的bug。1、题目名称&#xff1a;幼稚班作业幼稚园终于又有新的作业了。 老师安排同学用发给同学的4根木棒拼接成一个三角形。 当然按照正常的逻辑&#xff0c;如果不能拼接成三角形。 必然要折断某个木棍来拼接三角形。 可…

【Linux】进程的程序替换

文章目录1. 程序替换1.创建子进程的目的是什么&#xff1f;2.了解程序是如何进行替换的3. 程序替换的基本原理当创建进程的时候&#xff0c;先有进程数据结构&#xff0c;还是先加载代码和数据&#xff1f;程序替换是整体替换&#xff0c;不是局部替换execl 返回值4. 替换函数1…

vue2+高德地图web端开发使用

创建vue2项目我们创建一个vue2项目&#xff0c;创建vue2项目就不用再多说了吧&#xff0c;使用“vue create 项目名 ”创建即可注册高德地图高德地图官网地址&#xff1a;https://lbs.amap.com/如果是第一次使用&#xff0c;点击注册然后进入我们的控制台注册完之后进入控制台&…

ES6新特性--变量声明

可以使用let关键字来声明变量let a;let b,c;//同时声明多个变量let stu = 张三;let name =李四,age = 12;//声明变量的同时赋值 let关键字使用的注意事项(1).变量在声明的时候不可以重复,这也符合其他语言的变量声明规范 let name = 李四; let name = 张三;//这里开始报错,但…