前端国际化

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()
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐