• vue国际化教程


     需求背景

    项目需求要做国际化,结果网上找了好几篇文章,没有一个可以一次性搞定,现在这里总结一下。首先,我们分为两部分处理,一个是前端页面的静态文字,这个由前端vue.json自行处理。第二部分就是后端的错误消息和日志部分,我们由springboot的拦截器来处理。

    i18n介绍

    i18n(其来源是英文单词 internationalization的首末字符i和n,18为中间的字符数)是“国际化”的简称。

    vue前端国际化部分

    1、安装i18n插件

    安装插件时候,注意必须指定版本号,不然安装会报错。

    npm i vue-i18n@8.22.2

    2、新建文件夹i18n

    文件路径依次是RUOYI-UI/src/utils/i18n。具体的文件结构看图

    3、新建i18n.js文件

    文件的位置请看上图。

    1. // I18n
    2. import VueI18n from 'vue-i18n'
    3. import Vue from 'vue'
    4. import locale from 'element-ui/lib/locale'
    5. // 引入 elementui 的多语言
    6. import enLocale from 'element-ui/lib/locale/lang/en'
    7. import zhCnLocale from 'element-ui/lib/locale/lang/zh-CN'
    8. import zhTwLocale from 'element-ui/lib/locale/lang/zh-TW'
    9. // 如果还有新的语言在下面继续添加
    10. // 引入自己定义的 I18n 文件
    11. import myI18nEn from './i18n-en-US.json'
    12. import myI18nZh from './i18n-zh-CN.json'
    13. import myI18nTw from './i18n-zh-TW.json'
    14. // 如果还有新的语言在下面继续添加
    15. // 注册 vue-i18n
    16. Vue.use(VueI18n)
    17. // 默认中文
    18. const lang = 'zh-CN'
    19. const i18n = new VueI18n({
    20. locale: lang,
    21. messages: {
    22. // 会把myI18nZh的所有内容拷贝到zhCnLocale文件中
    23. 'zh-CN': Object.assign(zhCnLocale, myI18nZh),
    24. 'en-US': Object.assign(enLocale, myI18nEn),
    25. 'zh-TW': Object.assign(zhTwLocale, myI18nTw),
    26. // 如果还有新的语言在下面继续添加
    27. }
    28. })
    29. locale.i18n((key, value) => i18n.t(key, value))
    30. export default i18n

    4、修改main.js文件

    1. import Vue from 'vue'
    2. import Cookies from 'js-cookie'
    3. import Element from 'element-ui'
    4. // i18n js
    5. import i18n from './utils/i18n/i18n.js'
    6. // 其余的信息不用修改,就增加上面的i18n js,然后在new Vue中把i18n添加进去
    7. new Vue({
    8. el: '#app',
    9. router,
    10. store,
    11. i18n,
    12. render: h => h(App)
    13. })

    5、 页面显示的语法

    1. // title
    2. :title="$t('btnBulkOperations')"
    3. // js
    4. this.$i18n.t('username')
    5. // 标签,注意冒号
    6. :label="$t('username')"
    7. // 输入框中的占位符,注意冒号
    8. :placeholder="this.$t('username')"
    9. // 表格标题
    10. :label="$t('username')"
    11. // div语法
    12. {{$t("username")}}
    13. // 多个key拼接
    14. :placeholder="`${this.$t('userInput')}${this.$t('userPhone')}`"
    15. :label="`${this.$t('indexTablePrimaryKey')} • ${this.$t('wordName')}`"

    6、vue登录页面源码

    大家可以参照一下这个登录页面的实现方法

    7、修改request.js

    这样就可以保证,每一次请求都从cookies中读取出来用户设置的语言,然后传递到后台

    1. let language = localStorage.getItem("language");
    2. if(!(language != null && language != "" && language != undefined) ){
    3. language = 'zh-CN';
    4. }
    5. 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、国际化地区语言码对照表

    在线国际化地区语言码对照表 - UU在线工具在线国际化地区语言码对照表,提供完整的DNN3支持的多语言地区语言码对照表速查,列出了每个国家对应的语言Locale和国家代码对照表。https://uutool.cn/info-lang/

    6、I18nConfig.java

    1. package com.ruoyi.common.config;
    2. import com.ruoyi.common.filter.MyI18nInterceptor;
    3. import lombok.extern.slf4j.Slf4j;
    4. import org.springframework.context.annotation.Configuration;
    5. import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
    6. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    7. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    8. @Configuration
    9. @Slf4j
    10. public class I18nConfig implements WebMvcConfigurer {
    11. @Override
    12. public void addInterceptors(InterceptorRegistry registry) {
    13. // 注册拦截器
    14. MyI18nInterceptor myHandlerInterceptor = new MyI18nInterceptor();
    15. InterceptorRegistration loginRegistry = registry.addInterceptor(myHandlerInterceptor);
    16. // 拦截路径
    17. loginRegistry.addPathPatterns("/**");
    18. }
    19. }

    7、MyI18nInterceptor.java

    1. package com.ruoyi.common.filter;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.springframework.context.i18n.LocaleContextHolder;
    4. import org.springframework.web.servlet.HandlerInterceptor;
    5. import org.springframework.web.servlet.ModelAndView;
    6. import javax.servlet.http.HttpServletRequest;
    7. import javax.servlet.http.HttpServletResponse;
    8. import java.util.Locale;
    9. @Slf4j
    10. public class MyI18nInterceptor implements HandlerInterceptor {
    11. @Override
    12. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    13. final String key = "language";
    14. String language = request.getHeader(key);
    15. // 前端传递的language必须是zh-CN格式的,中间的-必须要完整,不能只传递zh或en
    16. log.info("当前语言={}",language);
    17. Locale locale = new Locale(language.split("-")[0],language.split("-")[1]);
    18. // 这样赋值以后,MessageUtils.message方法就不用修改了
    19. LocaleContextHolder.setLocale(locale);
    20. return true;
    21. }
    22. /**
    23. * 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)
    24. */
    25. @Override
    26. public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {
    27. }
    28. /**
    29. * 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行(主要是用于进行资源清理工作)
    30. */
    31. @Override
    32. public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
    33. }
    34. }

    8、解决乱码

    在线ASCII编码汉字互转https://www.ip138.com/ascii/打开网站后,把文件中的文字全部复制粘贴,然后勾选不转换字母和数字,点击转换ASCII就行,然后在复制粘贴回去。

    9、自定义properties文件名

    如果你的名字不叫messages.properties,而是i18nMessage.properties或者其余的名字,那么配置文件中需要修改。注意以下的配置没有经过验证。

    1. @Configuration
    2. public class I18nConfig implements WebMvcConfigurer {
    3. /**
    4. * 配置 MessageSource 其实这个可以不配置如果不配置请注意 message 多语言文件的位置
    5. *
    6. * @return
    7. */
    8. @Bean
    9. public ResourceBundleMessageSource messageSource() {
    10. Locale.setDefault(Locale.CHINESE);
    11. ResourceBundleMessageSource source = new ResourceBundleMessageSource();
    12. // 这里设置自己的文件名
    13. source.setBasenames("i18nMessage");
    14. source.setUseCodeAsDefaultMessage(true);
    15. source.setDefaultEncoding("UTF-8");
    16. return source;
    17. }
    18. }

    10、后端国际化结束

    数据库菜单国际化

    1、菜单国际化翻译

    把数据库菜单的id和名字都翻译一遍,然后messages.properties文件中的key可以自定义,我这里的规则是menu+id。

     2、菜单国际化转换

    1. /**
    2. * 根据父节点的ID获取所有子节点
    3. *
    4. * @param list 分类表
    5. * @param parentId 传入的父节点ID
    6. * @return String
    7. */
    8. public List getChildPerms(List list, int parentId)
    9. {
    10. List returnList = new ArrayList();
    11. for (Iterator iterator = list.iterator(); iterator.hasNext();)
    12. {
    13. // 国际化转换
    14. SysMenu t = (SysMenu) iterator.next();
    15. t.setMenuName(MessageUtils.message("menu"+t.getMenuId()));
    16. // 一、根据传入的某个父节点ID,遍历该父节点的所有子节点
    17. if (t.getParentId() == parentId)
    18. {
    19. recursionFn(list, t);
    20. returnList.add(t);
    21. }
    22. }
    23. return returnList;
    24. }

    前端页面自定义菜单国际化

    1、前端菜单title处理

    把自定义菜单中的内容替换为国际化定义的key。

    2、菜单国际化代码处理

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

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

     上传接口单独处理

    由于上传接口不走统一的request,所以需要在上传的时候单独传递header

    1. <template>
    2. <div class="app-container">
    3. <el-row :gutter="10" class="mb8">
    4. <el-col :span="1.5">
    5. <el-button
    6. :style="{display:displayCtrl}"
    7. type="primary"
    8. icon="el-icon-plus"
    9. size="mini"
    10. @click="handleAdd"
    11. >{{$t("btnAdd")}}el-button>
    12. el-col>
    13. el-row>
    14. <el-dialog :title="title"
    15. :close-on-click-modal="false"
    16. :visible.sync="open" width="510px" append-to-body>
    17. <el-form ref="form" :model="form" :rules="rules" label-width="100px" @submit.native.prevent>
    18. <el-row>
    19. <el-col :span="24">
    20. <div style="height: 30px; color: #f56c6c;text-align: center;">{{dialogErrorMessage}}div>
    21. el-col>
    22. el-row>
    23. <el-form-item :label="$t('deviceVersion')" prop="version" >
    24. <el-input style="width: 360px;" v-model="form.version" :placeholder="`${this.$t('userInput')}${this.$t('deviceVersion')}`" :maxlength="100" auto-complete="off" />
    25. el-form-item>
    26. <el-form-item :label="$t('versionDesc')" prop="description">
    27. <el-input style="width: 360px;" v-model="form.description" :placeholder="`${this.$t('userInput')}${this.$t('versionDesc')}`" :maxlength="100" auto-complete="off" />
    28. el-form-item>
    29. <el-form-item :label="$t('versionFile')" prop="file" >
    30. <div style="width: 360px;height: 290px;">
    31. <el-upload ref="upload"
    32. :headers="headers"
    33. :data="uploadData"
    34. :file-list="fileList"
    35. :action="fileAction"
    36. :auto-upload="false"
    37. :on-error = "uploadError"
    38. :on-success="uploadSuccess"
    39. :on-remove="handleRemoveFile"
    40. class="upload-demo"
    41. :on-change="handleChangeFile"
    42. name="file"
    43. drag
    44. :before-upload="fileBeforeUpload">
    45. <i class="el-icon-upload">i>
    46. <div class="el-upload__text">{{$t("versionFile1")}}<em>{{$t("versionClickUpload")}}em>div>
    47. <div class="el-upload__tip" style="line-height: 0;" slot="tip">{{$t("versionLimit")}}div>
    48. el-upload>
    49. div>
    50. el-form-item>
    51. el-form>
    52. <div slot="footer" class="dialog-footer">
    53. <el-button @click="cancel">{{$t("btnCancel")}}el-button>
    54. <el-button type="primary" @click="submitForm">{{$t("btnConfirm")}}el-button>
    55. div>
    56. el-dialog>
    57. div>
    58. template>
    59. <script>
    60. import { getToken } from "@/utils/auth";
    61. import { listVersion } from "@/api/version";
    62. export default {
    63. name: "Version",
    64. components: {
    65. },
    66. data() {
    67. return {
    68. displayCtrl:"none",
    69. fileNum:0,
    70. uploadData:null,
    71. headers: {
    72. language: localStorage.getItem("language"),
    73. Authorization: "Bearer " + getToken(),
    74. },
    75. fileAction: process.env.VUE_APP_BASE_API + "/uploadFile",
    76. fileList: [],
    77. dialogErrorMessage:"",
    78. tableHeight: window.innerHeight - 250,
    79. // 遮罩层
    80. loading: true,
    81. // 选中数组
    82. ids: [],
    83. // 非单个禁用
    84. single: true,
    85. // 非多个禁用
    86. multiple: true,
    87. // 显示搜索条件
    88. showSearch: true,
    89. // 总条数
    90. total: 0,
    91. // 固件版本表格数据
    92. versionList: [],
    93. // 弹出层标题
    94. title: "",
    95. // 是否显示弹出层
    96. open: false,
    97. // 查询参数
    98. queryParams: {
    99. pageNum: 1,
    100. pageSize: 10,
    101. version: null,
    102. name: null
    103. },
    104. // 表单参数
    105. form: {
    106. file: null,
    107. version: null,
    108. description: null,
    109. },
    110. };
    111. },
    112. computed: {
    113. rules() {
    114. let validateFile = (rule, value, callback) => {
    115. if(this.fileNum == 0){
    116. callback(new Error(this.$i18n.t('userChoose')+this.$i18n.t('versionFile')));
    117. }else{
    118. callback();
    119. }
    120. };
    121. let rules = {
    122. version: [
    123. { required: true, message: this.$i18n.t('userInput')+this.$i18n.t('deviceVersion'), trigger: 'blur' }
    124. ],
    125. file: [
    126. { required: true, validator: validateFile, trigger: 'change' }
    127. ],
    128. };
    129. // 清空表单验证信息
    130. this.$forceUpdate( () => {
    131. // 这里不要使用resetFields()方法,否则input无法输入
    132. this.$refs['form'].clearValidate();
    133. });
    134. return rules;
    135. }
    136. },
    137. created() {
    138. this.getList();
    139. },
    140. methods: {
    141. uploadSuccess(response, file, fileList){
    142. if(response.code != 200){
    143. this.dialogErrorMessage = this.$i18n.t('wordErrorStart')+response.msg;
    144. }else{
    145. this.open = false;
    146. this.$modal.msgSuccess(this.$i18n.t('messageSuccess'));
    147. this.getList();
    148. }
    149. },
    150. uploadError(response, file, fileList){
    151. this.dialogErrorMessage = this.$i18n.t('wordErrorStart')+response.msg;
    152. },
    153. handleRemoveFile(file, files){
    154. this.fileNum = 0;
    155. },
    156. handleChangeFile(file, fileList){
    157. // 只保留一个文件
    158. if (fileList.length > 1) {
    159. // 这里直接改了引用值 组件内部 uploadFiles
    160. fileList.splice(0, 1);
    161. }
    162. if(fileList.length > 0){
    163. this.fileNum = 1;
    164. }
    165. },
    166. fileBeforeUpload(file) {
    167. let isRightSize = file.size / 1024 / 1024 < 2
    168. if (!isRightSize) {
    169. this.dialogErrorMessage = this.$i18n.t('wordErrorStart')+this.$i18n.t('versionLimitError');
    170. return false;
    171. }
    172. let fileName = file.name;
    173. let pos = fileName.lastIndexOf(".");
    174. let lastName = fileName.substring(pos, fileName.length);
    175. if (lastName.toLowerCase() !== ".bin") {
    176. this.dialogErrorMessage = this.$i18n.t('wordErrorStart')+this.$i18n.t('versionLimitType');
    177. return false;
    178. }
    179. this.uploadData = {version:this.form.version, description:this.form.description,};
    180. let promise = new Promise((resolve) => {
    181. this.$nextTick(function () {
    182. resolve(true);
    183. });
    184. });
    185. return promise; //通过返回一个promis对象解决
    186. },
    187. /** 查询固件版本列表 */
    188. getList() {
    189. this.loading = true;
    190. listVersion(this.queryParams).then(response => {
    191. this.versionList = response.rows;
    192. this.total = response.total;
    193. this.loading = false;
    194. });
    195. },
    196. // 取消按钮
    197. cancel() {
    198. this.open = false;
    199. this.fileList=[];
    200. this.reset();
    201. },
    202. // 表单重置
    203. reset() {
    204. this.form = {
    205. file: null,
    206. version: null,
    207. description: null,
    208. };
    209. this.resetForm("form");
    210. },
    211. /** 搜索按钮操作 */
    212. handleQuery() {
    213. this.queryParams.pageNum = 1;
    214. this.getList();
    215. },
    216. /** 重置按钮操作 */
    217. resetQuery() {
    218. this.resetForm("queryForm");
    219. this.handleQuery();
    220. },
    221. /** 新增按钮操作 */
    222. handleAdd() {
    223. this.reset();
    224. this.fileList=[];
    225. this.dialogErrorMessage = "";
    226. this.open = true;
    227. this.title = this.$i18n.t('btnAdd');
    228. },
    229. /** 提交按钮 */
    230. submitForm() {
    231. this.$refs["form"].validate(valid => {
    232. if (valid) {
    233. this.$refs.upload.submit();
    234. }
    235. });
    236. },
    237. }
    238. };
    239. script>
    240. <style rel="stylesheet/scss">
    241. style>
    242. <style rel="stylesheet/scss" lang="scss" scoped>
    243. ::v-deep .el-upload-dragger .el-upload__text {
    244. color: #c0c4cc;
    245. }
    246. ::v-deep .el-upload__tip {
    247. color: #c0c4cc;
    248. margin-top: 0px;
    249. }
    250. 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