项目国际化的一些总结
最近项目要做国际化的功能,真真体会了知之非难,行之不易
这句话的博大精深之处。虽然看起来只要装一个翻译插件可以完成的事,但是其实不然,谷歌翻译虽好,但是往往词不达意,样式紊乱等等。所谓术业有专攻,想要做好一个项目,还是需要手动翻译。于是用到了vue-i18n
思路总结
主要是在项目中创建一个中英文的资源包,然后使用$t
或者i18n
官网提供的函数翻译,
从业务角度出发,国际化分为全局类和非全局类,全局类是指按钮、枚举值、校验提示等多个地方用到的字段,大概国际化分为以下几类:
- 系统标题
- 菜单
- 枚举值
- 用户信息/个人中心
- 登录页面
- 公共组件
- 校验提示
- ui组件i18的引入
- 接口报错提示
- 业务模块
系统标题
导航栏随着中英文的变化而变化
1. 固定标题
App.vue
js
onMounted(() => {
document.title = vm.$t('projectName')
})
2. 动态标题:简称+菜单名称
router.js
js
router.afterEach(to => {
router.app.$vuetify.theme.dark = !!to.meta.isDark
// 给body添加一个class,目的是为了区分light和dark主题模式
document.getElementsByTagName('body')[0].className = router.app.$vuetify.theme
.dark
? 'theme--dark'
: 'theme--light'
if (to.name) {
document.title = `${themeConfig.app.name}-${generateName(
to.name,
to.meta.enTitle,
)}`
} else {
document.title = i18n.t('projectName')
}
if (getToken() && store.state.global.userIdByAllList.length === 0) {
store.dispatch('global/loadUserList', 'admin')
}
NProgress.done()
})
3. 公共部分
i18n.js
js
// 切换语言
function setI18nLanguage(lang) {
i18n.locale = lang
localStorage.setItem('LOCALE', lang)
const { title, enTitle } = router.currentRoute.meta
// document.title = i18n.t('projectName')
document.title = `${themeConfig.app.name}-${generateName(title, enTitle)}`
window.document.documentElement.setAttribute('lang', lang)
return lang
}
4. 使用3出现的问题
原因:有些静态路由,比如login、404,存在前端,没有做title、enTitle命名
优化:作title为空判断
js
function setI18nLanguage(lang) {
i18n.locale = lang
localStorage.setItem('LOCALE', lang)
const { title, enTitle } = router.currentRoute.meta
if (title) {
document.title = `${themeConfig.app.name}-${generateName(title, enTitle)}`
} else {
document.title = i18n.t('projectName')
}
window.document.documentElement.setAttribute('lang', lang)
return lang
}
系统布局
根据布局而作,layout
布局(在router
中配置就好):
-
content
:左菜单右内容 -
blank
:全页面,大概是以下几类- 注册页面
- 登录页面
- 数据大屏
- 首页
- ......
一般就是用于切换中/英文的组件,全页面中都需要设置一个切换组件
安装i18n
js
yarn add vue-i18n
引入vue-i18n
在系统中单独创立一个i18n
文件夹,locales
文件下存中/英文资源包,大致如下:
i18n文件
文件夹结构
javascript
├─index.js
├─locales
| ├─zh-CN
| | ├─index.js
| | ├─module
| | | ├─account.js
| | | ├─action.js
| | | ├─enums.js
| | | ├─global.js
| | | ├─list.js
| | | ├─login.js
| | | ├─menu.js
| | | ├─organization.js
| | | ├─role.js
| | | ├─screen.js
| | | ├─validation.js
| | | └workbench.js
| ├─en
| | ├─index.js
| | ├─module
| | | ├─account.js
| | | ├─action.js
| | | ├─enums.js
| | | ├─global.js
| | | ├─list.js
| | | ├─login.js
| | | ├─menu.js
| | | ├─organization.js
| | | ├─role.js
| | | ├─screen.js
| | | ├─validation.js
| | | └workbench.js
主要做了以下几件事:
引入并使用vue-i18n
js
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
export const i18n = new VueI18n({...option})
引入中/英文版本
i18n.locale
存在localStorage
中
js
import enMessages from '@/plugins/i18n/locales/en/index'
import messages from '@/plugins/i18n/locales/zh-CN/index'
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
export const i18n = new VueI18n({
locale: localStorage.getItem('LOCALE') || 'zh-CN', // set locale
fallbackLocale: 'zh-CN', // 备选语言环境
silentTranslationWarn: true, // 去掉控制台警告
messages: {
'zh-CN': messages,
en: enMessages,
}, // set locale messages
})
setI18nLanguage
切换语言
主要做了以下几件事:
- 切换语言
i18n.locale
- 修改
localStorage
的当前语言 - 切换网站标题
document.title
- 给
html
中设置lang
标签值(这样子可以使用[::lang()](<https://developer.mozilla.org/zh-CN/docs/Web/HTML/Global_attributes/lang>)
(伪类),如下:
代码
js
function setI18nLanguage(lang) {
i18n.locale = lang
localStorage.setItem('LOCALE', lang)
document.title = i18n.t('projectName')
window.document.documentElement.setAttribute('lang', lang)
return lang
}
loadLanguageAsync:异步语言加载
当loadedLanguages
数组中没有x值时,需要异步加载
js
const loadedLanguages = ['en', 'zh-CN']
export function loadLanguageAsync(lang) {
// If the same language
if (i18n.locale === lang) {
return Promise.resolve(setI18nLanguage(lang))
}
// If the language was already loaded
if (loadedLanguages.includes(lang)) {
return Promise.resolve(setI18nLanguage(lang))
}
// If the language hasn't been loaded yet
/* eslint-disable prefer-template */
return import(
/* webpackChunkName: "lang-[request]" */ '@/plugins/i18n/locales/' +
lang +
'/index.js'
).then(msgs => {
i18n.setLocaleMessage(lang, msgs.default)
loadedLanguages.push(lang)
return setI18nLanguage(lang)
})
/* eslint-enable */
}
全部代码
js
import enMessages from '@/plugins/i18n/locales/en/index'
import messages from '@/plugins/i18n/locales/zh-CN/index'
import Vue from 'vue'
import VueI18n from 'vue-i18n'
Vue.use(VueI18n)
export const i18n = new VueI18n({
locale: localStorage.getItem('LOCALE') || 'zh-CN', // set locale
fallbackLocale: 'zh-CN', // 备选语言环境
silentTranslationWarn: true, // 去掉控制台警告
messages: {
'zh-CN': messages,
en: enMessages,
}, // set locale messages
})
const loadedLanguages = ['en', 'zh-CN'] // our default language that is preloaded
function setI18nLanguage(lang) {
i18n.locale = lang
localStorage.setItem('LOCALE', lang)
document.title = i18n.t('projectName')
window.document.documentElement.setAttribute('lang', lang)
return lang
}
export function loadLanguageAsync(lang) {
// If the same language
if (i18n.locale === lang) {
return Promise.resolve(setI18nLanguage(lang))
}
// If the language was already loaded
if (loadedLanguages.includes(lang)) {
return Promise.resolve(setI18nLanguage(lang))
}
// If the language hasn't been loaded yet
/* eslint-disable prefer-template */
return import(
/* webpackChunkName: "lang-[request]" */ '@/plugins/i18n/locales/' +
lang +
'/index.js'
).then(msgs => {
i18n.setLocaleMessage(lang, msgs.default)
loadedLanguages.push(lang)
return setI18nLanguage(lang)
})
/* eslint-enable */
}
// 菜单
export function generateMenuTitle(record) {
if (i18n.locale === 'en') {
const translatedTitle = record?.enTitle
return translatedTitle
}
return record?.title
}
export function generateName(title, enTitle) {
if (i18n.locale === 'en') {
return enTitle
}
return title
}
Vue.prototype.$generateMenuTitle = generateMenuTitle
Vue.prototype.$generateName = generateName
在main.js中引入i18n
js
import { i18n } from '@/plugins/i18n'
new Vue({
router,
store,
i18n,
vuetify,
render: h => h(App),
}).$mount('#app')
获取当前语言
js
$i18n.locale
ui库中英文引入
主要用到了vuetify
,element-ui
这两个ui库
locales/en/index.js:
js
import elementZhLocale from 'element-ui/lib/locale/lang/zh-CN'
import vuetifyZhHans from 'vuetify/lib/locale/zh-Hans'
locales/zh-CN/index.js:
js
import elementEnLocale from 'element-ui/lib/locale/lang/en'
import vuetifyEn from 'vuetify/lib/locale/en'
在element.js
中使用国际化
js
import eleLocale from 'element-ui/lib/locale'
import { i18n } from './i18n'
eleLocale.i18n((key, value) => i18n.t(key, value))
在vuetify.js
中使用国际化
js
import preset from '@/@core/preset/preset'
import Vue from 'vue'
import Vuetify from 'vuetify/lib/framework'
import { i18n } from './i18n'
Vue.use(Vuetify)
export default new Vuetify({
preset,
icons: {
iconfont: 'mdiSvg',
},
theme: {
options: {
customProperties: true,
variations: false,
},
},
lang: {
t: (key, ...params) => i18n.t(key, params),
},
})
element
和vuetify
字段的使用
js
<div> element国际化:<span>{{ $t('el.colorpicker') }}</span> </div>
<div> vuetify国际化:<span>{{ $t('$vuetify.dataIterator') }}</span> </div>
js文件自动引入
在网上也看到很多人使用json
,貌似都行,个人比较喜欢js
,没有那么复杂的格式校验
en
语言包主要放在locale/en/module
文件夹下,如图
但是所有的js
文件都要放在en/index.js
文件夹中,如果创建一个js
文件就import
一个,主要会有2个问题:
- 工作重复
- 很有可能跟同事发生代码冲突,
故使用path
读取module
文件夹下所有文件信息比较好,具体代码如下:
js
// 读取module文件夹下面所有文件并引入
const path = require('path')
const files = require.context('./module', false, /\.js$/)
const modules = {}
files.keys().forEach(key => {
const name = path.basename(key, '.js')
modules[name] = files(key).default || files(key)
})
en/index.js全部代码
js
import elementEnLocale from 'element-ui/lib/locale/lang/en'
import vuetifyEn from 'vuetify/lib/locale/en'
// 读取module文件夹下面所有文件并引入
const path = require('path')
const files = require.context('./module', false, /\.js$/)
const modules = {}
files.keys().forEach(key => {
const name = path.basename(key, '.js')
modules[name] = files(key).default || files(key)
})
export default {
$vuetify: vuetifyEn,
...elementEnLocale,
...modules,
}
菜单格式化
主要有以下两个方法:
-
在后端维护:创建一个
englishName
表字段- 优点:易于维护
- 缺点:语言不止两种时,字段增多
-
在前端维护:创建一个
menu.js
- 优点:暂时没有发现,没有用过
- 缺点:创建菜单时,需要在前端增加中英文
本人采用的方法二,所以按钮和菜单的名称全放在后端维护,主要步骤如下:
在menu加载时设置enTitle
js
const menus = {
id: item.id,
title: item.name,
code: item.menuCode,
enTitle: item.englishName,
action: item.iconCls,
link: item.url,
iconType: 'svg',
to: item.name,
icon: item.iconCls,
uiType: item.uiType,
}
// 按钮
const routerList = {
name: menu.name,
path: menu.url || '/',
hidden: !menu.isShow,
iconCls: menu.iconCls,
alwaysShow: true,
meta: {
code: menu.menuCode,
layout: menu?.layout || 'content',
title: menu.name,
enTitle: menu.englishName,
buttons: convertToButtons(menu.children),
buttonInfo: convertToButtonInfo(menu.children),
navActiveLink: !menu.isShow && menu.parentName,
},
}
主要使用$generateMenuTitle
切换,主要代码如下:
js
// 菜单
export function generateMenuTitle(record) {
if (i18n.locale === 'en') {
const translatedTitle = record?.enTitle
return translatedTitle
}
return record?.title
}
菜单切换
js
$generateMenuTitle(item)
按钮切换
js
$generateMenuTitle($route.meta.buttonInfo['add'])
request---接口调用国际化
主要是为了与后端通信,告诉后端当前web端是什么语言,主要是设置Accept-Language
,主要代码如下:
js
service.interceptors.request.use(
config => {
if (i18n.locale) {
config.headers['Accept-Language'] = i18n.locale
}
if (config.url.includes('/user/login')) {
return config
}
const token = getToken()
if (token) {
config.headers.Authorization = token
}
return config
},
err => {
Promise.resolve(err)
},
)
枚举值切换
很多地方会用到枚举值,一般调用字典接口,放存入store/modules/enum.js
文件中
在切换语言时,重新调用enum
接口,使用枚举值时,就需要注意判断枚举值的有无,步骤如下:
js
<template v-slot:item.active="{ item }">
<v-icon
v-if="activeEnum[item.active]"
size="1.5rem"
:color="activeEnum[item.active].color"
>
{{ activeEnum[item.active].icon }}
</v-icon>
<span v-else>N/A</span>
</template>
// computed中引入
activeEnum() {
return this.$store.state.enums.ActiveStatus
},
校验国际化
很多时候也会疑惑,校验是否需要写一个公共的文件,后来写多了业务逻辑,就发现真的很有必要,因为好多必填校验,每次都是重复的copy
,完全可以写成一个公共文件,国际化使用主要如下:
js
import { i18n } from '@/plugins/i18n'
// This field is required
export const required = (value, name = i18n.t('validation.defaultName')) =>
!!value || i18n.t('validation.required', [name])
函数使用
vue-i18n
提供了很多函数,如下图:
使用的比较多的是$t
,使用如下:
简单使用
js
// 定义
baseInfo: 'Basic Information', // en
baseInfo: '基础信息', // zh-CN
// 使用
this.$t('baseInfo')
// 复数使用
num: '0 | 1'
this.$tc('num', 1) // 0
this.$tc('num', 2) // 1
动态使用
数组使用
js
// 定义
hint: {
add: '{0}新增成功!', // en
add: '{0} added successfully!', // zh-CN
}
// 使用
this.$t('hint.add', [this.$t('currentTitle')])
对象使用
js
// 定义
drawer: {
addTitle: 'Add {cur}', // en
addTitle: '新增{cur}', // zh-CN
}
// 使用
this.$t('drawer.addTitle', { cur: this.$generateMenuTitle(this.$route.meta) })
根据环境使用
如果是在Vue
环境中,$t
应该是在this
中可以直接调用,如下:
js
this.$t('hint.add', [this.$t('currentTitle')])
如果是纯js
环境或者Vue
尚未实例化,需要先引入i18n
,再调用$t
,如下:
js
import { i18n } from '@/plugins/i18n'
export const min = (value, name = i18n.t('validation.defaultName')) =>
(value && value.length >= 8) || i18n.t('validation.minLen', [name])
踩坑记录
vue-i18n.esm.js:38 [vue-i18n] Value of key '$vuetify.dataTable.ariaLabel.sortNone' is not a string or function !
原因:找不到$vuetify.dataTable.ariaLabel.sortNone
字段
解决办法
vuetify项目国际化时,需要按照以下规则引入:
en.js
js
import elementEnLocale from 'element-ui/lib/locale/lang/en'
import vuetifyEn from 'vuetify/lib/locale/en'
const actions = {
search: 'search',
add: 'add',
edit: 'edit',
del: 'delete',
}
export default {
...elementEnLocale,
$vuetify: vuetifyEn,
actions,
}
zh-CN.js
js
import elementZhLocale from 'element-ui/lib/locale/lang/zh-CN'
import vuetifyZhHans from 'vuetify/lib/locale/zh-Hans'
const actions = {
search: '查询',
add: '新增',
edit: '修改',
del: '删除',
}
export default { ...elementZhLocale, $vuetify: vuetifyZhHans, actions }
vuetify.js
js
import preset from '@/@core/preset/preset'
import Vue from 'vue'
import Vuetify from 'vuetify/lib/framework'
import { i18n } from './i18n'
Vue.use(Vuetify)
export default new Vuetify({
preset,
icons: {
iconfont: 'mdiSvg',
},
theme: {
options: {
customProperties: true,
variations: false,
},
},
// lang: {
// locales: { zhHans },
// current: 'zhHans',
// },
lang: {
t: (key, ...params) => i18n.t(key, params),
},
})
使用
js
vuetify国际化:<span>{{ $t('$vuetify.dataIterator') }}</span>
后端接口国际化
忘记从网上哪里看到的了,但是感觉写的挺好的,侵权立删
实现国际化的第一步是获取到用户的Locale。在Web应用程序中,HTTP规范规定了浏览器会在请求中携带Accept-Language头,用来指示用户浏览器设定的语言顺序,如:
css
Accept-Language: zh-CN,zh;q=0.8,en;q=0.2
上述HTTP请求头表示优先选择简体中文,其次选择中文,最后选择英文。q表示权重,解析后我们可获得一个根据优先级排序的语言列表,把它转换为Java的Locale,即获得了用户的Locale。大多数框架通常只返回权重最高的Locale
主要效果如下:
默认props切换无效
参考链接:stackoverflow.com/questions/5...
有人说以下方法有效,但是亲测无用
js
props: {
// 取消按钮文字
cancelBtnText: {
type: String,
default: function () {
// 不可使用箭头函数
return this.$t('action.cancel')
},
},
},
原因:因为cancelBtnText
是一个String
类型,默认赋的值是取消
,而不是this.$t('action.cancel')
所以还是采用以下办法:
js
// template
{{ cancelBtnText || $t('action.cancel') }}
table headers 使用
参考链接:stackoverflow.com/questions/5...
js
computed: {
// 注意:headers必须放在computed中使用,否则无法实现效果
headers() {
return [
{ text: this.$t('platform.headers.id'), value: 'id', width: 120 },
]
},
},
**注意:**以上headers
使用有限制,headers
只能读,不可写,如果需要可读写,需要使用到动态插槽,要求headers
可读可写
js
// data()中定义headers,如下:
headers: [
{
text: 'ID',
value: 'id',
width: '50px',
sortable: false,
},
{
text: 'headers.type',
value: 'type',
width: '120px',
sortable: false,
},
{
text: '',
value: 'actions',
sortable: false,
width: '110px',
},
],
// 再在表头插槽中配置
<v-data-table
:items-per-page="query.pageSize"
item-key="id"
:height="tableHeight"
hide-default-footer
:headers="headers"
:items="tableData"
show-select
class="thead-light"
:loading="tableDataLoading"
@item-selected="$_tableSelected"
@toggle-select-all="$_tableSelected"
>
<template
v-for="item in headers"
v-slot:[`header.${item.value}`]="{ header }"
>
<div :key="item.value">{{ $t(header.text) }}</div>
</template>
</v-data-table>
注意事项
data()
中不能使用this.$t
,因为data()
是一次性执行的,找了很多方法,无法解决
参考链接:stackoverflow.com/questions/5...
工具: i18n Ally
作用:检测中文,匹配英文
使用方法
踩坑记录
一定要配置标红的代码,表示自定义命名空间匹配,参考链接:github.com/lokalise/i1...
否则,左边的树不会出来,如图中标红的地方:
.vscode\settings.json
js
"i18n-ally.localesPaths": ["src/plugins/i18n/locales"], // 翻译文件路径
"i18n-ally.enabledParsers": ["js"],
// "i18n-ally.dirStructure": "file",
"i18n-ally.keystyle": "nested", // 翻译路径格式,
"i18n-ally.sourceLanguage": "zh-CN", // 翻译源语言
"i18n-ally.displayLanguage": "en", //显示语言, 这里也可以设置显示英文为en
"i18n-ally.extract.keygenStyle": "camelCase", // 翻译字段命名样式采用驼峰
// 以下是标红代码,这里看不出来
"i18n-ally.namespace": true,
"i18n-ally.pathMatcher": "{locale}/module/{namespace}.js"
热更新
每次改变一个字段的中英文,保存之后都会全局加载,页面刷新,其实并不需要,只要局部刷新、加载即可,于是找到了以下方法
i18n/index.js
js
// 热更新
if (module.hot) {
module.hot.accept(
['@/plugins/i18n/locales/en/index', '@/plugins/i18n/locales/zh-CN/index'],
function () {
i18n.setLocaleMessage(
'en',
require('@/plugins/i18n/locales/en/index').default,
)
i18n.setLocaleMessage(
'zh-CN',
require('@/plugins/i18n/locales/zh-CN/index').default,
)
},
)
}
注意:主要参考了webpack的一些方法,监视本地化文件中的更改以及将热更改重新加载到您的应用程序中。
module.hot:webpack.docschina.org/api/hot-mod...
官网地址:kazupon.github.io/vue-i18n/zh...