项目需求要做国际化,结果网上找了好几篇文章,没有一个可以一次性搞定,现在这里总结一下。首先,我们分为两部分处理,一个是前端页面的静态文字,这个由前端vue.json自行处理。第二部分就是后端的错误消息和日志部分,我们由springboot的拦截器来处理。
i18n(其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数)是“国际化”的简称。
安装插件时候,注意必须指定版本号,不然安装会报错。
npm i vue-i18n@8.22.2
文件路径依次是RUOYI-UI/src/utils/i18n。具体的文件结构看图
文件的位置请看上图。
- // I18n
- import VueI18n from 'vue-i18n'
- import Vue from 'vue'
- import locale from 'element-ui/lib/locale'
-
- // 引入 elementui 的多语言
- import enLocale from 'element-ui/lib/locale/lang/en'
- import zhCnLocale from 'element-ui/lib/locale/lang/zh-CN'
- import zhTwLocale from 'element-ui/lib/locale/lang/zh-TW'
- // 如果还有新的语言在下面继续添加
-
- // 引入自己定义的 I18n 文件
- import myI18nEn from './i18n-en-US.json'
- import myI18nZh from './i18n-zh-CN.json'
- import myI18nTw from './i18n-zh-TW.json'
- // 如果还有新的语言在下面继续添加
-
- // 注册 vue-i18n
- Vue.use(VueI18n)
-
- // 默认中文
- const lang = 'zh-CN'
- const i18n = new VueI18n({
- locale: lang,
- messages: {
- // 会把myI18nZh的所有内容拷贝到zhCnLocale文件中
- 'zh-CN': Object.assign(zhCnLocale, myI18nZh),
- 'en-US': Object.assign(enLocale, myI18nEn),
- 'zh-TW': Object.assign(zhTwLocale, myI18nTw),
- // 如果还有新的语言在下面继续添加
- }
- })
-
- locale.i18n((key, value) => i18n.t(key, value))
- export default i18n
-
- import Vue from 'vue'
-
- import Cookies from 'js-cookie'
-
- import Element from 'element-ui'
-
- // i18n js
- import i18n from './utils/i18n/i18n.js'
- // 其余的信息不用修改,就增加上面的i18n js,然后在new Vue中把i18n添加进去
-
- new Vue({
- el: '#app',
- router,
- store,
- i18n,
- render: h => h(App)
- })
- // title
- :title="$t('btnBulkOperations')"
-
- // js
- this.$i18n.t('username')
-
- // 标签,注意冒号
- :label="$t('username')"
-
- // 输入框中的占位符,注意冒号
- :placeholder="this.$t('username')"
-
- // 表格标题
- :label="$t('username')"
-
- // div语法
- {{$t("username")}}
-
- // 多个key拼接
- :placeholder="`${this.$t('userInput')}${this.$t('userPhone')}`"
- :label="`${this.$t('indexTablePrimaryKey')} • ${this.$t('wordName')}`"
大家可以参照一下这个登录页面的实现方法
- "login">
-
- "login-title-image">
- "font-size: 45px;color: #00549e;padding-left: 15px;font-weight: 700;">{{$t("loginLeftWord1")}}
- "font-size: 45px;color: #00549e;padding-left: 80px;font-weight: 700;">{{$t("loginLeftWord2")}}
-
-
"loginForm" :model="loginForm" :rules="loginRules" class="login-form"> - "title">
- "float: left;">
-
"../assets/images/scr_title_logo.png" style="width: 100px;" /> -
- "float: left;float: left;color: #00549e;font-size: 20px;font-weight: 700;height: 50px;padding-top: 8px;">
- {{$t("loginFormTitle")}}
-
-
-
-
"username"> -
- v-model="loginForm.username"
- type="text"
- auto-complete="off"
- :placeholder="this.$t('formAccount')"
- >
-
"prefix" icon-class="user" class="el-input__icon input-icon" /> -
-
-
"password"> -
- v-model="loginForm.password"
- type="password"
- auto-complete="off"
- :placeholder="this.$t('formPassword')"
- @keyup.enter.native="handleLogin"
- >
-
"prefix" icon-class="password" class="el-input__icon input-icon" /> -
-
-
"code" v-if="captchaEnabled"> -
- type="text"
- v-model="loginForm.code"
- auto-complete="off"
- :placeholder="this.$t('formCaptcha')"
- oninput="if(value.length>4)value=value.slice(0,4)"
- style="width: 63%"
- @keyup.enter.native="handleLogin"
- >
-
"prefix" icon-class="validCode" class="el-input__icon input-icon" /> -
- "login-code">
-
"codeUrl" @click="getCode" class="login-code-img"/> -
-
- "width: 100%; height: 30px;color: #00549e;">
-
"click" style="cursor: pointer;"> - "el-dropdown-link">
- "float: left;">
"../assets/images/changeLanguage.png" style="width: 20px;" /> - "float: left;">{{$t("changeLanguage")}}
-
-
"dropdown"> -
- v-for="item in language"
- :disabled="item.key==chooseLanguage?true:false"
- :key="item.key"
- :label="item.value"
- :value="item.key"
- @click.native="handleChangeLanguage(item.key)"
- >{{item.value}}
-
-
-
-
"width:100%;"> -
- :loading="loading"
- size="medium"
- type="primary"
- style="width:100%;"
- @click.native.prevent="handleLogin"
- >
- if="!loading">{{$t("btnLogin")}}
- else>{{$t("btnLogining")}}
-
-
-
-
- "el-login-footer">
- {{$t("loginCopyRight")}}
-
-
-
- import { getCodeImg } from "@/api/login";
- import Cookies from "js-cookie";
- import { encrypt, decrypt } from '@/utils/jsencrypt'
-
- export default {
- name: "Login",
- data() {
- return {
- language:[
- {"key":"zh-CN","value":"简体中文"},
- {"key":"zh-TW","value":"繁體中文"},
- {"key":"en-US","value":"English"}
- ],
- chooseLanguage:"zh-CN",
- codeUrl: "",
- loginForm: {
- username: "",
- password: "",
- rememberMe: false,
- code: "",
- uuid: "",
- language:""
- },
-
- loading: false,
- // 验证码开关
- captchaEnabled: true,
- // 注册开关
- register: false,
- redirect: undefined
- };
- },
- watch: {
- $route: {
- handler: function(route) {
- this.redirect = route.query && route.query.redirect;
- },
- immediate: true
- }
- },
- computed: {
- loginRules() {
- let loginRules = {
- username: [
- { required: true, trigger: "blur", message: this.$i18n.t('formAccountRules') }
- ],
- password: [
- { required: true, trigger: "blur", message: this.$i18n.t('formPasswordRules') }
- ],
- code: [
- { required: true, trigger: "blur", message: this.$i18n.t('formCaptchaRules') }
- ],
- };
- // 清空表单验证信息
- this.$nextTick( () => {
- // 这里不要使用resetFields()方法,否则input无法输入
- this.$refs['loginForm'].clearValidate();
- });
- return loginRules;
- }
- },
- created() {
- this.setWebLanguage();
- this.getCode();
- },
- methods: {
- setWebLanguage(){
- // cookies中是刚才用户选择的语言
- let language = Cookies.get("flowinnIotLanguage");
- if(language != null && language != "" && language != undefined){
- this.chooseLanguage = language;
- }else{
- // 如果cookies中没有,说明用户第一次打开系统,直接获取系统的语言
- let systemLanguage = (navigator.language || navigator.userLanguage).substring(0, 2);
- if(systemLanguage != null && systemLanguage != "" && systemLanguage != undefined){
- // 这里获取到的是zh和es,不是zh-CN需要单独处理
- this.chooseLanguage = systemLanguage;
- }
- }
- if(this.chooseLanguage == "zh" || this.chooseLanguage == "zh-CN"){
- this.chooseLanguage = "zh-CN";
- }else if(this.chooseLanguage == "zh-TW"){
- this.chooseLanguage = "zh-TW";
- }else{
- this.chooseLanguage = "en-US";
- }
- var time = new Date().getTime();
- // 设置一个3年的有效期
- let timeNum = time + (3 * 365 * 24 * 60 * 60);
- Cookies.set("flowinnIotLanguage", this.chooseLanguage,{ expires: timeNum });
- // 保存到全局变量中,后面可以使用
- localStorage.setItem("language", this.chooseLanguage);
- // 页面刷新语言进行显示
- this.$i18n.locale = this.chooseLanguage;
- // 修改页面标题
- document.title = this.$i18n.t('loginFormTitle');
- },
- handleChangeLanguage(languageKey){
- // 用户修改了显示语言,重新设置语言
- this.chooseLanguage = languageKey;
- // 临时保存到cookies中,后面由具体的方法去处理
- Cookies.set("flowinnIotLanguage", this.chooseLanguage,{ expires: 30 });
- this.setWebLanguage();
- },
- getCode() {
- getCodeImg().then(res => {
- this.captchaEnabled = res.captchaEnabled === undefined ? true : res.captchaEnabled;
- if (this.captchaEnabled) {
- this.codeUrl = "data:image/gif;base64," + res.img;
- this.loginForm.uuid = res.uuid;
- }
- });
- },
- handleLogin() {
- this.$refs.loginForm.validate(valid => {
- if (valid) {
- this.loading = true;
- // 登录的时候把language传递给后台
- this.loginForm.language = this.chooseLanguage;
- this.$store.dispatch("Login", this.loginForm).then(() => {
- this.$router.push({ path: this.redirect || "/" }).catch(()=>{});
- }).catch(() => {
- this.loading = false;
- });
- }
- });
- }
- }
- };
-
-
7、修改request.js
这样就可以保证,每一次请求都从cookies中读取出来用户设置的语言,然后传递到后台
- let language = localStorage.getItem("language");
- if(!(language != null && language != "" && language != undefined) ){
- language = 'zh-CN';
- }
- config.headers['language'] = language;

8、 前端国际化结束
到此前端的国际化就结束了,大家可以试试看。
springboot2后端国际化
1、保存messages.properties
大家先把messages.properties文件中的内容保存到别的地方。
2、删除messages.properties
然后我们需要删除这个文件,重新生成。
3、新建messages.properties
在ruoyi-admin/src/main/resources/i18n这个文件夹上右键New-Resource Bundle

然后输入messages,个人建议就使用这个固定的messages名字,不然后面的配置中要多一个配置,然后点击右边的加号,添加自己需要的国际化语言,如果要添加中文就输入zh_CN,英文就输入en_US。因为我这里已经存在文件了,所以会有错误提示。
新建好了之后,显示就会变成下面这个样子,如果大家不是这种结构,那就要删除重新搞,必须是这样的结构才可以。经过上面的操作之后,就会自动把对应的文件都生成。

4、编辑messages.properties
大家双击messages.properties文件,然后在右边点击Resource Bundle,然后在右边就可以一个一个编辑了。

5、国际化地区语言码对照表
6、I18nConfig.java
- package com.ruoyi.common.config;
-
- import com.ruoyi.common.filter.MyI18nInterceptor;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
- import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
- import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
-
-
- @Configuration
- @Slf4j
- public class I18nConfig implements WebMvcConfigurer {
-
-
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- // 注册拦截器
- MyI18nInterceptor myHandlerInterceptor = new MyI18nInterceptor();
- InterceptorRegistration loginRegistry = registry.addInterceptor(myHandlerInterceptor);
- // 拦截路径
- loginRegistry.addPathPatterns("/**");
- }
-
- }
-
7、MyI18nInterceptor.java
- package com.ruoyi.common.filter;
-
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.context.i18n.LocaleContextHolder;
- import org.springframework.web.servlet.HandlerInterceptor;
- import org.springframework.web.servlet.ModelAndView;
-
- import javax.servlet.http.HttpServletRequest;
- import javax.servlet.http.HttpServletResponse;
- import java.util.Locale;
-
- @Slf4j
- public class MyI18nInterceptor implements HandlerInterceptor {
-
- @Override
- public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
- final String key = "language";
- String language = request.getHeader(key);
- // 前端传递的language必须是zh-CN格式的,中间的-必须要完整,不能只传递zh或en
- log.info("当前语言={}",language);
- Locale locale = new Locale(language.split("-")[0],language.split("-")[1]);
- // 这样赋值以后,MessageUtils.message方法就不用修改了
- LocaleContextHolder.setLocale(locale);
- return true;
- }
-
- /**
- * 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
- */
- @Override
- public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
- }
-
- /**
- * 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)
- */
- @Override
- public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
- }
-
- }
8、解决乱码
在线ASCII编码汉字互转https://www.ip138.com/ascii/打开网站后,把文件中的文字全部复制粘贴,然后勾选不转换字母和数字,点击转换ASCII就行,然后在复制粘贴回去。

9、自定义properties文件名
如果你的名字不叫messages.properties,而是i18nMessage.properties或者其余的名字,那么配置文件中需要修改。注意以下的配置没有经过验证。
- @Configuration
- public class I18nConfig implements WebMvcConfigurer {
-
-
-
- /**
- * 配置 MessageSource 其实这个可以不配置如果不配置请注意 message 多语言文件的位置
- *
- * @return
- */
- @Bean
- public ResourceBundleMessageSource messageSource() {
- Locale.setDefault(Locale.CHINESE);
- ResourceBundleMessageSource source = new ResourceBundleMessageSource();
- // 这里设置自己的文件名
- source.setBasenames("i18nMessage");
- source.setUseCodeAsDefaultMessage(true);
- source.setDefaultEncoding("UTF-8");
- return source;
- }
- }
10、后端国际化结束
数据库菜单国际化
1、菜单国际化翻译
把数据库菜单的id和名字都翻译一遍,然后messages.properties文件中的key可以自定义,我这里的规则是menu+id。

2、菜单国际化转换
- /**
- * 根据父节点的ID获取所有子节点
- *
- * @param list 分类表
- * @param parentId 传入的父节点ID
- * @return String
- */
- public List
getChildPerms(List list, int parentId) - {
- List
returnList = new ArrayList(); - for (Iterator
iterator = list.iterator(); iterator.hasNext();) - {
- // 国际化转换
- SysMenu t = (SysMenu) iterator.next();
- t.setMenuName(MessageUtils.message("menu"+t.getMenuId()));
- // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点
- if (t.getParentId() == parentId)
- {
- recursionFn(list, t);
- returnList.add(t);
- }
- }
- return returnList;
- }

前端页面自定义菜单国际化
1、前端菜单title处理
把自定义菜单中的内容替换为国际化定义的key。


2、菜单国际化代码处理
接着找到src/layout/components/Siderbar/SiderbarItem.vue文件,把菜单显示的title换成国际化的title,请看下图。

然后找到src/layout/components/Navbar.vue,在mounted方法中添加下面的代码,直接设置国际化即可。这样也解决了页面刷新后国际化失效和菜单国际化失效的问题。

上传接口单独处理
由于上传接口不走统一的request,所以需要在上传的时候单独传递header
- <template>
- <div class="app-container">
- <el-row :gutter="10" class="mb8">
- <el-col :span="1.5">
- <el-button
- :style="{display:displayCtrl}"
- type="primary"
- icon="el-icon-plus"
- size="mini"
- @click="handleAdd"
- >{{$t("btnAdd")}}el-button>
- el-col>
- el-row>
-
-
- <el-dialog :title="title"
- :close-on-click-modal="false"
- :visible.sync="open" width="510px" append-to-body>
- <el-form ref="form" :model="form" :rules="rules" label-width="100px" @submit.native.prevent>
- <el-row>
- <el-col :span="24">
- <div style="height: 30px; color: #f56c6c;text-align: center;">{{dialogErrorMessage}}div>
- el-col>
- el-row>
- <el-form-item :label="$t('deviceVersion')" prop="version" >
- <el-input style="width: 360px;" v-model="form.version" :placeholder="`${this.$t('userInput')}${this.$t('deviceVersion')}`" :maxlength="100" auto-complete="off" />
- el-form-item>
- <el-form-item :label="$t('versionDesc')" prop="description">
- <el-input style="width: 360px;" v-model="form.description" :placeholder="`${this.$t('userInput')}${this.$t('versionDesc')}`" :maxlength="100" auto-complete="off" />
- el-form-item>
- <el-form-item :label="$t('versionFile')" prop="file" >
- <div style="width: 360px;height: 290px;">
- <el-upload ref="upload"
- :headers="headers"
- :data="uploadData"
- :file-list="fileList"
- :action="fileAction"
- :auto-upload="false"
- :on-error = "uploadError"
- :on-success="uploadSuccess"
- :on-remove="handleRemoveFile"
- class="upload-demo"
- :on-change="handleChangeFile"
- name="file"
- drag
- :before-upload="fileBeforeUpload">
- <i class="el-icon-upload">i>
- <div class="el-upload__text">{{$t("versionFile1")}}<em>{{$t("versionClickUpload")}}em>div>
- <div class="el-upload__tip" style="line-height: 0;" slot="tip">{{$t("versionLimit")}}div>
- el-upload>
- div>
- el-form-item>
- el-form>
- <div slot="footer" class="dialog-footer">
- <el-button @click="cancel">{{$t("btnCancel")}}el-button>
- <el-button type="primary" @click="submitForm">{{$t("btnConfirm")}}el-button>
- div>
- el-dialog>
-
-
- div>
- template>
-
- <script>
- import { getToken } from "@/utils/auth";
- import { listVersion } from "@/api/version";
-
- export default {
- name: "Version",
- components: {
- },
- data() {
- return {
- displayCtrl:"none",
- fileNum:0,
- uploadData:null,
- headers: {
- language: localStorage.getItem("language"),
- Authorization: "Bearer " + getToken(),
- },
- fileAction: process.env.VUE_APP_BASE_API + "/uploadFile",
- fileList: [],
- dialogErrorMessage:"",
- tableHeight: window.innerHeight - 250,
- // 遮罩层
- loading: true,
- // 选中数组
- ids: [],
- // 非单个禁用
- single: true,
- // 非多个禁用
- multiple: true,
- // 显示搜索条件
- showSearch: true,
- // 总条数
- total: 0,
- // 固件版本表格数据
- versionList: [],
- // 弹出层标题
- title: "",
- // 是否显示弹出层
- open: false,
- // 查询参数
- queryParams: {
- pageNum: 1,
- pageSize: 10,
- version: null,
- name: null
- },
- // 表单参数
- form: {
-
- file: null,
- version: null,
- description: null,
- },
- };
- },
- computed: {
- rules() {
- let validateFile = (rule, value, callback) => {
- if(this.fileNum == 0){
- callback(new Error(this.$i18n.t('userChoose')+this.$i18n.t('versionFile')));
- }else{
- callback();
- }
- };
- let rules = {
- version: [
- { required: true, message: this.$i18n.t('userInput')+this.$i18n.t('deviceVersion'), trigger: 'blur' }
- ],
- file: [
- { required: true, validator: validateFile, trigger: 'change' }
- ],
- };
- // 清空表单验证信息
- this.$forceUpdate( () => {
- // 这里不要使用resetFields()方法,否则input无法输入
- this.$refs['form'].clearValidate();
- });
- return rules;
- }
- },
- created() {
- this.getList();
- },
- methods: {
- uploadSuccess(response, file, fileList){
- if(response.code != 200){
- this.dialogErrorMessage = this.$i18n.t('wordErrorStart')+response.msg;
- }else{
- this.open = false;
- this.$modal.msgSuccess(this.$i18n.t('messageSuccess'));
- this.getList();
- }
- },
- uploadError(response, file, fileList){
- this.dialogErrorMessage = this.$i18n.t('wordErrorStart')+response.msg;
- },
- handleRemoveFile(file, files){
- this.fileNum = 0;
- },
- handleChangeFile(file, fileList){
- // 只保留一个文件
- if (fileList.length > 1) {
- // 这里直接改了引用值 组件内部 uploadFiles
- fileList.splice(0, 1);
- }
- if(fileList.length > 0){
- this.fileNum = 1;
- }
- },
- fileBeforeUpload(file) {
- let isRightSize = file.size / 1024 / 1024 < 2
- if (!isRightSize) {
- this.dialogErrorMessage = this.$i18n.t('wordErrorStart')+this.$i18n.t('versionLimitError');
- return false;
- }
- let fileName = file.name;
- let pos = fileName.lastIndexOf(".");
- let lastName = fileName.substring(pos, fileName.length);
- if (lastName.toLowerCase() !== ".bin") {
- this.dialogErrorMessage = this.$i18n.t('wordErrorStart')+this.$i18n.t('versionLimitType');
- return false;
- }
- this.uploadData = {version:this.form.version, description:this.form.description,};
- let promise = new Promise((resolve) => {
- this.$nextTick(function () {
- resolve(true);
- });
- });
- return promise; //通过返回一个promis对象解决
- },
- /** 查询固件版本列表 */
- getList() {
- this.loading = true;
- listVersion(this.queryParams).then(response => {
- this.versionList = response.rows;
- this.total = response.total;
- this.loading = false;
- });
- },
- // 取消按钮
- cancel() {
- this.open = false;
- this.fileList=[];
- this.reset();
- },
- // 表单重置
- reset() {
- this.form = {
- file: null,
- version: null,
- description: null,
- };
- this.resetForm("form");
- },
- /** 搜索按钮操作 */
- handleQuery() {
- this.queryParams.pageNum = 1;
- this.getList();
- },
- /** 重置按钮操作 */
- resetQuery() {
- this.resetForm("queryForm");
- this.handleQuery();
- },
- /** 新增按钮操作 */
- handleAdd() {
- this.reset();
- this.fileList=[];
- this.dialogErrorMessage = "";
- this.open = true;
- this.title = this.$i18n.t('btnAdd');
- },
- /** 提交按钮 */
- submitForm() {
- this.$refs["form"].validate(valid => {
- if (valid) {
- this.$refs.upload.submit();
- }
- });
- },
-
- }
- };
- script>
-
- <style rel="stylesheet/scss">
-
- style>
-
- <style rel="stylesheet/scss" lang="scss" scoped>
- ::v-deep .el-upload-dragger .el-upload__text {
- color: #c0c4cc;
- }
- ::v-deep .el-upload__tip {
- color: #c0c4cc;
- margin-top: 0px;
- }
- style>
-
相关阅读:
Kubernetes在rancher中自定义应用商店
分布式爬虫管理平台gerapy通过docker部署scrapyd来添加主机(四)
【Docker】- 【入门】- 001 - 创建docker 账户 以及 上传image和部署image
为了学明白中断机制,我努力了
【Leetcode每日一题】 穷举vs暴搜vs深搜vs回溯vs剪枝_全排列 - 子集(难度⭐⭐)(65)
数据结构 栈与队列详解!!
瞬间理解防抖和节流
代码随想录 动态规划 part16
Linux上文本处理三剑客之grep
UTONMOS:数字藏品市场将迎来科技大变局
-
原文地址:https://blog.csdn.net/renkai721/article/details/132873120