对国际化 i18n 项目的一点思考

9,724 阅读10分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,点击查看活动详情

本来没有建群的打算,因为看到蛮多的群归于平静,但是部分同学想进入进阶群一起学习,既然有需求就整起,可以加 WX:Mr10212021,回复 加群 即可,原先加过的默认进群!

国际化是什么?

国际化 对应的英文单词为 Internationalization,又称 i18n

  • i 为单词的 【第一个】 字母
  • 18 为 【in 之间】 单词的个数
  • n 代表这个单词的 【最后一个】 字母

如果你的项目是 Vue,那么相信你在实现国际化功能时,也必不可少的会使用到 vue-i18n 这个库,接下来本文也是通过这个库搭配 Vue 实现最基本的国际化功能,但关注点并不是如何使用这个库,而是在实现的过程中思考 可优化的点

实现基本国际化功能

这里就不再多余演示 demo 项目的创建过程了,并且文中只演示最基本的 中英文 切换,好了现在直奔核心吧!

05873B02.jpg

集成 vue-i18n

安装依赖

熟悉的命令:npm install vue-i18n -S

配置 vue-i18n

image.png

  • src 目录下创建 language 目录用于保持和语言切换相关的内容
  • language 目录下创建 lang 目录用于保存不同语言的映射关系,如中文对应 zh.js、英文对应 en.js
  • language 目录下创建 index.js 作为默认导出,并在其中创建 i18n 对象
     import { createI18n } from "vue-i18n";
      import zh from './lang/zh';
      import en from './lang/en';
      const i18n = createI18n({
        legacy: false,
        locale: "zh", // 初始化配置语言
        messages: {
          zh,
          en,
        },
      });
    
      export default i18n;
    

main.js 注册 i18n

内容非常简单,直接上代码:

import { createApp } from "vue";
import i18n from "./language";
import store from "./store";
import App from "./App.vue";

createApp(App)
    .use(store)
    .use(i18n)
    .mount("#app");

实际上在通过 use(i18n) 时,会调用 i18n.install() 方法,大概内容如下:

  • 通过 app.provide(app.__VUE_I18N_SYMBOL__, i18n)i18n 对象提供给应用中的所有后代组件可通过 inject 注入
  • 通过 app.config.globalProperties.xxx = xxx 的方式为应用添加 全局属性/方法,实际上是对 Vue2 中 Vue.prototype 使用方式的一种替代
    • 常见全局属性,如 $i18n 通过 app.config.globalProperties.$i18n = i18n 添加到全局
    • 常见全局方法,如:$t, $rt, $d, $n, $tm 通过 Object.defineProperty(app.config.globalProperties, `$${method}`, desc) 添加到全局
  • 通过 aplly(...) 方法注册常用的 全局指令 v-t全局组件 i18n
  • 在根组件卸载时移除/释放 i18n 相关内容
    const unmountApp = app.unmount;
    app.unmount = () => {
        i18n.dispose();
        unmountApp();
    };
    
  • 注册 vue-devtools 的相关插件

根据数据信息填充国际化内容

页面渲染

假设需要渲染如下数据对应的列表,并且要实现国际化:

const data = [
  {
    url: vueImg,
    title: 'Vue',
    describe: '渐进式 JavaScript 框架'
  },
  {
    url: reactImg,
    title: 'React',
    describe: '用于构建用户界面的 JavaScript 库'
  },
  {
    url: angularImg,
    title: 'Angular',
    describe: '现代 Web 开发平台'
  },
  {
    url: nodeImg,
    title: 'Node',
    describe: 'Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时'
  },
  {
    url: webpackImg,
    title: 'Webpack',
    describe: 'webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具'
  },
];

对应 App.vue 组件模板内容如下:

<template>
  <button class="btn" @click="changeLang">{{ $t("中/英") }}</button>
  <List :data="data" />
</template>

对应的页面效果如下:

image.png

填充 lang 目录下映射关系

lang/zh.js 文件中:

export default {
  "渐进式 JavaScript 框架": "渐进式 JavaScript 框架",

  "用于构建用户界面的 JavaScript 库": "用于构建用户界面的 JavaScript 库",

  "现代 Web 开发平台": "现代 Web 开发平台",

  "Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时":
    "Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时",

  "webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具":
    "webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具",

  "中/英": "中/英",
};

lang/zh.js 文件中:

export default {
  "渐进式 JavaScript 框架": "Progressive JavaScript framework",

  "用于构建用户界面的 JavaScript 库":
    "JavaScript library for building user interface",

  "现代 Web 开发平台": "Modern web development platform",

  "Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时":
    "Node.js is a JavaScript runtime based on the chrome V8 engine",

  "webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具":
    "Webpack is a static module packaging tool for modern JavaScript applications",

  "中/英": "Chinese / English",
};

<List /> 组件中进行翻译处理

翻译处理可通过如下方式处理:

  • 使用 $t(...) 方法
  • 使用 v-t 指令
  • 使用 <i18n-t></i18n-t> 组件

这里选择第一种,因为它更灵活,能使用的范围也更广,指令和组件形式限定在了 template 中:

效果演示

1.gif

优化 i18n 配置

基于以上简单的例子,已经能够实现了国际化切换功能,但其中需要考虑的优化点还不少,下面的内容仅属于 抛转引玉,不一定全面。

05F51BEA.jpg

优化翻译文件中的 key

现在很明显的一点,就是 zh.js、en.js 文件中用于映射的 key 太长了,导致整个文件看起来会很多、很乱,因此我们可以将对应的 key 进行精简,如下:

// zh.js
export default {
  "Vue 简介": "渐进式 JavaScript 框架",
  
  "React 简介": "用于构建用户界面的 JavaScript 库",

  "Angular 简介": "现代 Web 开发平台",

  "Node 简介": "Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时",

  "Webpack 简介":
    "webpack 是一个用于现代 JavaScript 应用程序的静态模块打包工具",

  "中/英": "中/英",
};

// en.js
export default {
  "Vue 简介": "Progressive JavaScript framework",

  "React 简介": "JavaScript library for building user interface",

  "Angular 简介": "Modern web development platform",

  "Node 简介": "Node.js is a JavaScript runtime based on the chrome V8 engine",

  "Webpack 简介":
    "Webpack is a static module packaging tool for modern JavaScript applications",

  "中/英": "Chinese / English",
};

那么对应到外部传入需要渲染的数据源 data 就可以简写为:

const data = [
  {
    url: vueImg,
    title: "Vue",
    describe: "Vue 简介",
  },
  {
    url: reactImg,
    title: "React",
    describe: "React 简介",
  },
  {
    url: angularImg,
    title: "Angular",
    describe: "Angular 简介",
  },
  {
    url: nodeImg,
    title: "Node",
    describe: "Node 简介",
  },
  {
    url: webpackImg,
    title: "Webpack",
    describe: "Webpack 简介",
  },
];

另一种精简方式就是将 key 用对应的类似于 变量命名 的方式来定义,但我个人不是很喜欢这种方式,首先在语义上很难直接读出相关信息,而且很难用一两个英文单词去概括文字内容,而且在后期需要排查对应问题并需要定位 tmeplate 时是极其不方便.

转换翻译文件类型

.js 转 .json

上述的翻译文件是 .js 文件,因此,为了能够让其能被其他文件导入,我们不得不在文件中使用 export defualtexport 将对应文件内容向外导出,但其实我们可以将 .js 文件转换为 .json 文件直接使用,如下:

excel 转 .json

在实际项目中翻译的内容通常是由业务专门找对应的翻译人员提供的,而真正到了开发者手中往往就是一个 excel 类型的表格文件,如果我们使用的是前面的纯 json 方式,免不了要自己一个一个从表格中复制对应的内容到我们对应的 .json 文件中,而且是分别填充到 zh.jsonen.json 中,值得注意的是现在才是仅支持两个国家的语言,如果后续支持的国家变多,那么手动复制的方式岂不是要 0694CB5F.jpg

因此,最好的做法是我们根据业务方提供的表格自动转成 json 格式的数据,避免不必要的手动操作,用命令帮我们处理这个内容:

  • 安装依赖 npm install xlsx-to-json

  • 将对应的转换操作封装 excel2json.js 文件中,基于第三方库简单封装即可

    const xlsx2json = require("xlsx-to-json");
    const path = require("path");
    
    xlsx2json(
      {
        input: path.join(__dirname, "./i18n.xlsx"),
        output: path.join(__dirname, "./i18n.json"),
      },
      function (err, result) {
        if (err) {
          console.error(err);
        } else {
          console.log(result);
        }
      }
    );
    
  • 修改 i18n 配置的入口文件 src\language\index.js

    import { createI18n } from "vue-i18n";
    import i18njson from "./i18n.json";
    
    // 动态获取 message 信息
    function getMessage() {
     const messages = {
       zh: {},
       en: {}
     };
     i18njson.forEach(({Short, Chinese, English}) => {
       messages.zh[Short] = Chinese;
       messages.en[Short] = English;
     });
     return messages;
    }
    
    const i18n = createI18n({
     legacy: false,
     locale: "zh", // 初始化配置语言
     messages: getMessage(),
    });
    
    export default i18n;
    
  • 提供 i18n.xlsx 文件作为数据源

  • package.json 问文件中添加对应转换命令

      "scripts": {
          "dev": "vite",
          "build": "vite build",
          "preview": "vite preview",
          "i18n": "node ./src/language/excel2json.js"
        }
    

    具体效果如下:

升级为 i18n 系统

上面已经将对应的翻译包从 *.xlsx 转成了 *.json 的形式,这样一定程度上能减少重复劳动力,但在 复用/协作 方面还是不够理想,因此可以从更高的维度将这整个内容升级到 i18n 系统,并提供对应的翻译包上传、自动解析、去重、添加命名空间等功能,再加上对应的列表管理功能,重点是可供多人员、多系统进行 复用/协作,对前端来讲就可以通过 接口 获取对应的翻译包数据,也能减少前端最终构建产物的体积。

处理翻译文件中重复的内容

06A99BDD.gif

什么叫重复的内容呢?其实很简单,比如有个文字内容为 确认 按钮,它在 A 页面需要翻译为 Confirm,它在 B 页面需要翻译为 OK,而它的中文内容就是 确认,意味着它对应的数据内容为:

[
  { "Short": "确认", "Chinese": "确认", "English": "Confirm" },
  { "Short": "确认", "Chinese": "确认", "English": "OK" }
]

但这样其实是不行的,这样最终会被后面的内容覆盖掉,即 A、B 页面最终的翻译内容都为 OK,因为数据中的 Short 其实就是最终的不同语言映射的 key,如下:

const messages = {
    zh: {
     "确认": "确认",
     "确认": "确认"
    },
    en: {
     "确认": "Confirm",
     "确认": "OK"
    }
};

既然这样,那么其实我们只要为不同的翻译内容设置不同的 Short 值即可,如下:

[
  { "Short": "确认1", "Chinese": "确认", "English": "Confirm" },
  { "Short": "确认2", "Chinese": "确认", "English": "OK" }
]

这样变动小,并且也没有丢失掉原本的语义。

考虑不同语言的样式

由于不同语言的表现形式不同,内容长度也不一致,因此在前端进行展示时,就必须要考虑到最终的显式问题,否则一旦切换语言环境那么一定会导致页面的布局展示出现问题,处理方式无非几种:

  • 允许文字内容换行展示,在文字发生换行时,要通过 CSS 设置按完整词换行

  • 不允许换行的,就要控制固定宽度,超出部分打点展示,鼠标移入展示全部内容等

  • 单独为不同语言环境设置样式,具体还是得看展示需求,如需要考虑不同语言环境下文字的对齐方式、文字间距等

  • 针对难以处理的翻译内容,可以通过和业务沟通是否可以替换翻译内容、缩减文字长度等等

    06C8791C.png

考虑后端接口语言环境变更

一个项目的国际化不可能都是前端来实现的,一些接口动态返回的内容也是需要后端去处理的,通常接口的请求头中会存储一个用于标识当前页面语言环境的字段,然后再决定返回给前端页面的具体内容。

06D25789.jpg

基于前面处理的国家化切换功能,本身是会基于 vue 的响应式来切换翻译内容的,即不会刷新页面,因此在切换对应翻译内容后,同样需要修改后续接口请求头中的语言环境。

但这样还是有问题的,已经通过接口返回的数据内容,此时没有办法切换成对应的翻译内容,因为当前的国际化切换是基于页面的变动,但基于接口变动的部分还没重新请求获取新的内容,那怎么处理呢?

  • 前端切换语言环境后,重新刷新页面,让接口也重新获取新的内容
  • 后端在返回数据时,将对应的不同语言环境的翻译内容一起返回,由前端根据语言环境决定如何渲染
  • 将所有的翻译内容全部交由前端管理,一开始就初始化好各个语言环境对应的翻译内容

最后

以上内容是基于国际化功能的一点思考,文中对应的思考点是笔者自己在项目中遇到的点,并不一定适合所有项目,当然也期待评论区给出更多、更好的方案。

06D85B74.gif