若依vue3 实现国际化,使用i18n实现
·
前端国际化
1.安装依赖
yarn add vue-i18n
2.src目录下创建lang目录,存放国际化文件
此处包含三个文件,分别是 index.js zh.js en.js
- index.js
import { createI18n } from 'vue-i18n'
import Cookies from 'js-cookie'
import elementEnLocale from 'element-plus/es/locale/lang/en'
import elementZhLocale from 'element-plus/es/locale/lang/zh-cn'
import enLocale from './en'
import zhLocale from './zh'
const messages = {
en_US: {
...enLocale,
...elementEnLocale
},
zh_CN: {
...zhLocale,
...elementZhLocale
}
}
const i18n = createI18n({
// 设置语言 选项 en_US | zh_CN
locale: Cookies.get('language') || 'zh_CN',
// 设置回退语言
fallbackLocale: 'zh_CN',
// 设置文本内容
messages,
// 如果您想使用 Composition API,可以设置 legacy 为 false
legacy: false
})
export default i18n
- en.js
export default {
login: {
title: "Dispatch Management System",
logIn: "Login in",
LoggingIn: "logging in...",
username: "Username",
password: "Password",
code: "Code",
rememberMe: "Remember Me",
},
logout: {
title: "Log Out",
message: "Are you sure you want to exit the system?",
},
tagsView: {
refresh: "Refresh",
close: "Close",
closeOthers: "Close Others",
closeAll: "Close All",
closeLeft: "Close Left",
closeRight: "Close Right",
},
settings: {
account: "Account",
title: "Page Setting",
themeSettings: "Theme Settings",
theme: "Theme Color",
systemLayoutConfiguration: "System Layout Configuration",
topNav: "Open TopNav",
tagsView: "Open Tags-View",
fixedHeader: "Fixed Header",
sidebarLogo: "Sidebar Logo",
dynamicTitle: "Dynamic Title",
saveMessage: "Saving to your device. Please wait...",
resetMessage: "Clearing settings cache and refreshing. Please wait...",
},
dialog: {
warn: "Alert",
confirm: "Confirm",
cancel: "Cancel",
save: "Save",
reset: "Reset",
},
sizeSelect: {
title: "Layout Size",
large: "large",
default: "default",
small: "small",
message: "Setting up the layout, please wait...",
},
menus: {
'首页': 'Home',
'系统管理': 'System',
'系统监控': 'Monitoring',
'系统工具': 'Tools',
// 系统管理
'用户管理': 'User',
'角色管理': 'Role',
'菜单管理': 'Menu',
'部门管理': 'Department',
'岗位管理': 'Position',
'字典管理': 'Dictionary',
'参数设置': 'Settings',
'通知公告': 'Notifications',
'日志管理': 'Log',
'操作日志': 'Operation Log',
'登录日志': 'Login Log',
//系统监控
'在线用户': 'Online Users',
'定时任务': 'Scheduled Tasks',
'数据监控': 'Data Monitoring',
'服务监控': 'Service Monitoring',
'缓存监控': 'Cache Monitoring',
'缓存列表': 'Caches',
//系统工具
'表单构建': 'Form Builder',
'代码生成': 'Code Generation',
'系统接口': 'System API',
}
}
- zn.js
export default {
login: {
title: "派单管理系统",
logIn: "登录",
LoggingIn: "登 录 中 ...",
username: "账号",
password: "密码",
code: "验证码",
rememberMe: "记住密码",
},
logout: {
title: "退出登录",
message: "确定退出系统吗?",
},
tagsView: {
refresh: "刷新",
close: "关闭",
closeOthers: "关闭其它",
closeAll: "关闭所有",
closeLeft: "关闭左侧",
closeRight: "关闭右侧",
},
settings: {
account: "个人中心",
title: "布局设置",
themeSettings: "主题风格设置",
theme: "主题色",
systemLayoutConfiguration: "系统布局配置",
topNav: "开启 TopNav",
tagsView: "开启 Tags-View",
fixedHeader: "固定 Header",
sidebarLogo: "显示 Logo",
dynamicTitle: "动态标题",
saveMessage: "正在保存到本地,请稍候...",
resetMessage: "正在清除设置缓存并刷新,请稍候...",
},
dialog: {
warn: "提示",
confirm: "确定",
cancel: "取消",
save: "保存",
reset: "重置"
},
sizeSelect:{
title:'布局大小',
large:'较大',
default:'默认',
small:'稍小',
message:'正在设置布局大小,请稍候...',
},
menus: {
'首页': '首页',
'系统管理': '系统管理',
'系统监控': '系统监控',
'系统工具': '系统工具',
// 系统管理
'用户管理': '用户管理',
'角色管理': '角色管理',
'菜单管理': '菜单管理',
'部门管理': '部门管理',
'岗位管理': '岗位管理',
'字典管理': '字典管理',
'参数设置': '参数设置',
'通知公告': '通知公告',
'日志管理': '日志管理',
'操作日志': '操作日志',
'登录日志': '登录日志',
// 系统监控
'在线用户': '在线用户',
'定时任务': '定时任务',
'数据监控': '数据监控',
'服务监控': '服务监控',
'缓存监控': '缓存监控',
'缓存列表': '缓存列表',
// 系统工具
'表单构建': '表单构建',
'代码生成': '代码生成',
'系统接口': '系统接口'
}
}
3、在src/main.js中增量添加i18n
import i18n from './lang'
app.use(i18n)
// 使用element-plus 并且设置全局的大小
app.use(ElementPlus, {
// 设置语言 选项 en_US | zh_CN
locale: Cookies.get('language') || 'zh_CN',
// 通过i18n实例动态切换语言
i18n: (key, value) => i18n.global.t(key, value),
})
4.在src/store/modules/app.js中增量添加i18n
// state 新增语言状态,与i18n配置保持一致
language: Cookies.get('language') || 'zh_CN'
//actions 新增语言相关actions
setLanguage(language) {
this.language = language
Cookies.set('language', language)
}
5.src/components/LangSelect/index.vue中创建汉化组件
<template>
<el-dropdown trigger="click" class="international" @command="handleSetLanguage">
<svg-icon class-name="international-icon" icon-class="language" :aria-hidden="false" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item :disabled="language === 'zh_CN'" command="zh_CN">
中文
</el-dropdown-item>
<el-dropdown-item :disabled="language === 'en_US'" command="en_US">
English
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script setup>
const { proxy } = getCurrentInstance();
import { changeLanguage } from "@/api/login";
import { useI18n } from 'vue-i18n'
import { ElMessage } from 'element-plus'
import { computed } from 'vue'
import useAppStore from "@/store/modules/app";
const appStore = useAppStore();
const language = computed(() => appStore.language);
const { locale } = useI18n()
function handleSetLanguage(value) {
proxy.$modal.loading("正在设置语音,请稍候...");
appStore.setLanguage(value);
locale.value = value
changeLanguage(value).then(() => {
window.location.reload()
})
}
</script>
<style lang="scss" scoped>
.international {
display: inline-flex;
align-items: center;
height: 100%;
vertical-align: middle;
.international-icon {
font-size: 18px;
color: #5a5e66;
vertical-align: middle;
height: 100%;
cursor: pointer;
&:focus {
outline: none;
}
}
}
</style>
6.在login.js新增修改语言方法
// 修改语言
export function changeLanguage(lang){
return request({
url: '/changeLanguage',
method: 'get',
headers: {
isToken: false,
},
params: {
lang: lang
}
})
}
7.登录页面汉化
标题下,增加切换入口
<h3 class="title">{{ $t('login.title') }}</h3>
<div class="language-container">
<lang-select class="set-language" />
</div>
.language-container {
position: absolute;
right: 25px;
top: 26px;
z-index: 10;
height: 20px;
.set-language{
width: 20px;
}
}
//普通文本使用方式:
{{ $t('login.title') }}
//标签内使用方式:
:placeholder="$t('login.password')"
//js内使用方式
this.$t('login.user.password.not.match')
//js 内使用方式
import { useI18n } from 'vue-i18n'
const {t} = useI18n()
function logout() {
ElMessageBox.confirm(t('logout.message'), t('dialog.warn'), {
confirmButtonText: t('dialog.confirm'),
cancelButtonText: t('dialog.cancel'),
type: 'warning'
}).then(() => {
userStore.logOut().then(() => {
// location.href = '/index';
router.push({path:'/login'})
})
}).catch(() => { });
}
8.导航栏增加语言切换
<lang-select class="right-menu-item hover-effect" />
import LangSelect from '@/components/LangSelect'
.right-menu-item {
display: inline-block;
padding: 0 8px;
height: 100%;
font-size: 18px;
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
cursor: pointer;
transition: background 0.3s;
&:hover {
background: rgba(0, 0, 0, 0.025);
}
}
}
9.左侧菜单实现国际化
app.vue
<template>
<el-config-provider :locale="language">
<router-view/>
</el-config-provider>
</template>
<script setup>
import useSettingsStore from '@/store/modules/settings'
import { handleThemeStyle } from '@/utils/theme'
import cn from 'element-plus/es/locale/lang/zh-cn';
import en from 'element-plus/es/locale/lang/en';
import useAppStore from "@/store/modules/app";
const appStore = useAppStore();
const language = computed(() => appStore.language=== 'en_US' ? en : cn);
onMounted(() => {
nextTick(() => {
// 初始化主题样式
handleThemeStyle(useSettingsStore().theme)
})
})
</script>
修改layout/src/components/Sidebar/SidebarItem.vue文件
<template>
<div v-if="!item.hidden">
<template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
<el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
<svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>
<template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ menusTitle(onlyOneChild.meta.title) }}</span></template>
</el-menu-item>
</app-link>
</template>
<el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
<template v-if="item.meta" #title>
<svg-icon :icon-class="item.meta && item.meta.icon" />
<span class="menu-title" :title="hasTitle(item.meta.title)">{{ menusTitle(item.meta.title) }}</span>
</template>
<sidebar-item
v-for="(child, index) in item.children"
:key="child.path + index"
:is-nest="true"
:item="child"
:base-path="resolvePath(child.path)"
class="nest-menu"
/>
</el-sub-menu>
</div>
</template>
<script setup>
import { isExternal } from '@/utils/validate'
import AppLink from './Link'
import { getNormalPath } from '@/utils/ruoyi'
import { useI18n } from 'vue-i18n'
const {t} = useI18n()
function menusTitle(item) {
let itemName = t('menus.' + item)
if(itemName){
itemName =itemName.replace('menus.','')
}
return itemName?itemName:item;
}
const props = defineProps({
// route object
item: {
type: Object,
required: true
},
isNest: {
type: Boolean,
default: false
},
basePath: {
type: String,
default: ''
}
})
const onlyOneChild = ref({});
function hasOneShowingChild(children = [], parent) {
if (!children) {
children = [];
}
const showingChildren = children.filter(item => {
if (item.hidden) {
return false
} else {
// Temp set(will be used if only has one showing child)
onlyOneChild.value = item
return true
}
})
// When there is only one child router, the child router is displayed by default
if (showingChildren.length === 1) {
return true
}
// Show parent if there are no child router to display
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
return true
}
return false
};
function resolvePath(routePath, routeQuery) {
if (isExternal(routePath)) {
return routePath
}
if (isExternal(props.basePath)) {
return props.basePath
}
if (routeQuery) {
let query = JSON.parse(routeQuery);
return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
}
return getNormalPath(props.basePath + '/' + routePath)
}
function hasTitle(title){
if (title.length > 5) {
return title;
} else {
return "";
}
}
</script>
10.面包屑国际化
src/components/Breadcrumb/index.vue 使用语言设定组件
<template>
<el-breadcrumb class="app-breadcrumb" separator="/">
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item,index) in levelList" :key="item.path">
<span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ menusTitle(item.meta.title) }}</span>
<a v-else @click.prevent="handleLink(item)">{{ menusTitle(item.meta.title) }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
const {t} = useI18n()
function menusTitle(item) {
let itemName = t('menus.' + item)
if(itemName){
itemName =itemName.replace('menus.','')
}
return itemName?itemName:item;
}
const route = useRoute();
const router = useRouter();
const levelList = ref([])
function getBreadcrumb() {
// only show routes with meta.title
let matched = route.matched.filter(item => item.meta && item.meta.title);
const first = matched[0]
// 判断是否为首页
if (!isDashboard(first)) {
matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched)
}
levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
}
function isDashboard(route) {
const name = route && route.name
if (!name) {
return false
}
return name.trim() === 'Index'
}
function handleLink(item) {
const { redirect, path } = item
if (redirect) {
router.push(redirect)
return
}
router.push(path)
}
watchEffect(() => {
// if you go to the redirect page, do not update the breadcrumbs
if (route.path.startsWith('/redirect/')) {
return
}
getBreadcrumb()
})
getBreadcrumb();
</script>
<style lang='scss' scoped>
.app-breadcrumb.el-breadcrumb {
display: inline-block;
font-size: 14px;
line-height: 50px;
margin-left: 8px;
.no-redirect {
color: #97a8be;
cursor: text;
}
}
</style>
src/components/TopNav/index.vue 使用语言设定组件
<template>
<el-menu
:default-active="activeMenu"
mode="horizontal"
@select="handleSelect"
:ellipsis="false"
>
<template v-for="(item, index) in topMenus">
<el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"/>
{{ item.meta.title }}
</el-menu-item>
</template>
<!-- 顶部菜单超出数量折叠 -->
<el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
<template #title>更多菜单</template>
<template v-for="(item, index) in topMenus">
<el-menu-item
:index="item.path"
:key="index"
v-if="index >= visibleNumber">
<svg-icon
v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
:icon-class="item.meta.icon"/>
{{ item.meta.title }}
</el-menu-item>
</template>
</el-sub-menu>
</el-menu>
</template>
<script setup>
import { constantRoutes } from "@/router"
import { isHttp } from '@/utils/validate'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
// 顶部栏初始数
const visibleNumber = ref(null);
// 当前激活菜单的 index
const currentIndex = ref(null);
// 隐藏侧边栏路由
const hideList = ['/index', '/user/profile'];
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const route = useRoute();
const router = useRouter();
// 主题颜色
const theme = computed(() => settingsStore.theme);
// 所有的路由信息
const routers = computed(() => permissionStore.topbarRouters);
// 顶部显示菜单
const topMenus = computed(() => {
let topMenus = [];
routers.value.map((menu) => {
if (menu.hidden !== true) {
// 兼容顶部栏一级菜单内部跳转
if (menu.path === "/") {
topMenus.push(menu.children[0]);
} else {
topMenus.push(menu);
}
}
})
return topMenus;
})
// 设置子路由
const childrenMenus = computed(() => {
let childrenMenus = [];
routers.value.map((router) => {
for (let item in router.children) {
if (router.children[item].parentPath === undefined) {
if(router.path === "/") {
router.children[item].path = "/" + router.children[item].path;
} else {
if(!isHttp(router.children[item].path)) {
router.children[item].path = router.path + "/" + router.children[item].path;
}
}
router.children[item].parentPath = router.path;
}
childrenMenus.push(router.children[item]);
}
})
return constantRoutes.concat(childrenMenus);
})
// 默认激活的菜单
const activeMenu = computed(() => {
const path = route.path;
let activePath = path;
if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
const tmpPath = path.substring(1, path.length);
activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
if (!route.meta.link) {
appStore.toggleSideBarHide(false);
}
} else if(!route.children) {
activePath = path;
appStore.toggleSideBarHide(true);
}
activeRoutes(activePath);
return activePath;
})
function setVisibleNumber() {
const width = document.body.getBoundingClientRect().width / 3;
visibleNumber.value = parseInt(width / 85);
}
function handleSelect(key, keyPath) {
currentIndex.value = key;
const route = routers.value.find(item => item.path === key);
if (isHttp(key)) {
// http(s):// 路径新窗口打开
window.open(key, "_blank");
} else if (!route || !route.children) {
// 没有子路由路径内部打开
const routeMenu = childrenMenus.value.find(item => item.path === key);
if (routeMenu && routeMenu.query) {
let query = JSON.parse(routeMenu.query);
router.push({ path: key, query: query });
} else {
router.push({ path: key });
}
appStore.toggleSideBarHide(true);
} else {
// 显示左侧联动菜单
activeRoutes(key);
appStore.toggleSideBarHide(false);
}
}
function activeRoutes(key) {
let routes = [];
if (childrenMenus.value && childrenMenus.value.length > 0) {
childrenMenus.value.map((item) => {
if (key == item.parentPath || (key == "index" && "" == item.path)) {
routes.push(item);
}
});
}
if(routes.length > 0) {
permissionStore.setSidebarRouters(routes);
} else {
appStore.toggleSideBarHide(true);
}
return routes;
}
onMounted(() => {
window.addEventListener('resize', setVisibleNumber)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', setVisibleNumber)
})
onMounted(() => {
setVisibleNumber()
})
</script>
<style lang="scss">
.topmenu-container.el-menu--horizontal > .el-menu-item {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #999093 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
border-bottom: 2px solid #{'var(--theme)'} !important;
color: #303133;
}
/* sub-menu item */
.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
float: left;
height: 50px !important;
line-height: 50px !important;
color: #999093 !important;
padding: 0 5px !important;
margin: 0 10px !important;
}
/* 背景色隐藏 */
.topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
background-color: #ffffff !important;
}
/* 图标右间距 */
.topmenu-container .svg-icon {
margin-right: 4px;
}
/* topmenu more arrow */
.topmenu-container .el-sub-menu .el-sub-menu__icon-arrow {
position: static;
vertical-align: middle;
margin-left: 8px;
margin-top: 0px;
}
</style>
11. 顶部组件
实现效果




后端国际化
1. 在 framework\config\下增加 I18nConfig.java
package com.car.framework.config;
import com.car.common.constant.Constants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
/**
* 资源文件配置加载
*
* @author ruoyi
*/
@Configuration
public class I18nConfig implements WebMvcConfigurer
{
@Bean
public LocaleResolver localeResolver()
{
SessionLocaleResolver slr = new SessionLocaleResolver();
// 默认语言
slr.setDefaultLocale(Constants.DEFAULT_LOCALE);
return slr;
}
@Bean
public LocaleChangeInterceptor localeChangeInterceptor()
{
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
// 参数名
lci.setParamName("lang");
return lci;
}
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(localeChangeInterceptor());
}
}
Constants 中DEFAULT_LOCALE 值为 :
/**
* 系统语言
*/
public static final Locale DEFAULT_LOCALE = Locale.SIMPLIFIED_CHINESE;
2. 修改配置application.yml中的basename国际化文件,默认是i18n路径下messages文件
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages
3. i18n目录文件下定义资源文件
messages_en_US.properties
user.login.username=User name
user.login.password=Password
user.login.code=Security code
user.login.remember=Remember me
user.login.submit=Sign In
messages_zh_CN.properties
user.login.username=用户名
user.login.password=密码
user.login.code=验证码
user.login.remember=记住我
user.login.submit=登录
4.java代码使用MessageUtils获取国际化
MessageUtils.message("user.login.username")
MessageUtils.message("user.login.password")
MessageUtils.message("user.login.code")
MessageUtils.message("user.login.remember")
MessageUtils.message("user.login.submit")
5.在SysLoginController.java新增修改语言方法
@GetMapping("/changeLanguage")
public AjaxResult changeLanguage(String lang)
{
//自定义逻辑`在这里插入代码片`
return AjaxResult.success();
}
6.在SecurityConfig.java允许匿名访问此方法
.antMatchers("/changeLanguage").permitAll()
更多推荐


所有评论(0)