项目国际化的一些总结

项目国际化的一些总结

最近项目要做国际化的功能,真真体会了知之非难,行之不易 这句话的博大精深之处。虽然看起来只要装一个翻译插件可以完成的事,但是其实不然,谷歌翻译虽好,但是往往词不达意,样式紊乱等等。所谓术业有专攻,想要做好一个项目,还是需要手动翻译。于是用到了vue-i18n

思路总结

主要是在项目中创建一个中英文的资源包,然后使用$t或者i18n官网提供的函数翻译,

从业务角度出发,国际化分为全局类和非全局类,全局类是指按钮、枚举值、校验提示等多个地方用到的字段,大概国际化分为以下几类:

  1. 系统标题
  2. 菜单
  3. 枚举值
  4. 用户信息/个人中心
  5. 登录页面
  6. 公共组件
  7. 校验提示
  8. ui组件i18的引入
  9. 接口报错提示
  10. 业务模块

系统标题

导航栏随着中英文的变化而变化

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中配置就好):

  1. content:左菜单右内容

  2. blank:全页面,大概是以下几类

    1. 注册页面
    2. 登录页面
    3. 数据大屏
    4. 首页
    5. ......

一般就是用于切换中/英文的组件,全页面中都需要设置一个切换组件

安装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 切换语言

主要做了以下几件事:

  1. 切换语言i18n.locale
  2. 修改localStorage的当前语言
  3. 切换网站标题document.title
  4. 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库中英文引入

主要用到了vuetifyelement-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),
  },
})

elementvuetify字段的使用

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个问题:

  1. 工作重复
  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,
}

菜单格式化

主要有以下两个方法:

  1. 在后端维护:创建一个englishName表字段

    1. 优点:易于维护
    2. 缺点:语言不止两种时,字段增多
  2. 在前端维护:创建一个menu.js

    1. 优点:暂时没有发现,没有用过
    2. 缺点:创建菜单时,需要在前端增加中英文

本人采用的方法二,所以按钮和菜单的名称全放在后端维护,主要步骤如下:

在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...

参考链接

一份不错的前端国际化方案指南 - 掘金 (juejin.cn)

vue-i18n

相关推荐
乐闻x几秒前
Vue.js 性能优化指南:掌握 keep-alive 的使用技巧
前端·vue.js·性能优化
一条晒干的咸魚2 分钟前
【Web前端】创建我的第一个 Web 表单
服务器·前端·javascript·json·对象·表单
花海少爷13 分钟前
第十章 JavaScript的应用课后习题
开发语言·javascript·ecmascript
Amd79418 分钟前
Nuxt.js 应用中的 webpack:compiled 事件钩子
前端·webpack·开发·编译·nuxt.js·事件·钩子
生椰拿铁You26 分钟前
09 —— Webpack搭建开发环境
前端·webpack·node.js
狸克先生37 分钟前
如何用AI写小说(二):Gradio 超简单的网页前端交互
前端·人工智能·chatgpt·交互
sinat_3842410940 分钟前
在有网络连接的机器上打包 electron 及其依赖项,在没有网络连接的机器上安装这些离线包
javascript·arcgis·electron
baiduopenmap1 小时前
百度世界2024精选公开课:基于地图智能体的导航出行AI应用创新实践
前端·人工智能·百度地图
loooseFish1 小时前
小程序webview我爱死你了 小程序webview和H5通讯
前端
小牛itbull1 小时前
ReactPress vs VuePress vs WordPress
开发语言·javascript·reactpress