前端实现网页多语言展示

2019-02-27

超简!前端实现网页多语言展示全攻略(含代码)

前情提要

在我最近重构的老项目中,有一个需求:根据不同地区展示不同语言且语言包的切换和展示由前端控制。我首先想到的是去网上查了一下有没有什么好用的插件,毕竟能拿来就用比自己造轮子方便多了。经过我一番并不太辛苦地搜索,发现了一个叫做 i18next 的东西。文档很友好,写得认真详细,可以很方便地查看源码;而且这个项目很大很多人参与,本身就又衍生出许多的 plugins 和 utils。

但是吧。(看我前面说那么多,肯定是要有转折的,不然这篇文章又为啥存在呢?😀

但是吧,这个有些太繁琐了,就我当前重构的这个项目而言,有些杀鸡用牛刀。实现这个语言包切换的功能真的需要这么复杂么?(毕竟查看这个文档以及找到我需要的utils就花了我差不多两天时间 Orz。当然期间还有别的需求穿插进来,并不是全部精力搞这个。

但这不是我把所有代码删除转换思路决定自己写代码实现的最终原因。我不是半途而废的人,既然选了这条路就不能只因为怕麻烦就放弃。致使我放弃的原因是:这条路走不通…😭 i18next 只允许异步加载语言包。虽然异步加载静态资源是前端非常非常非常常规的操作,但是总有例外。我当前重构的项目是选用了 Handlebars 动态模板加载html元素, 在 Handlebars 中插入的文字应该也是多语言的,顺着这个思路写了一个 Helper 进行转换。而这个 Helper 需要用到的转换函数的内置语言包居然因为异步加载还没有load完成?那么可想而知,转换的结果是不成功的。语言包必然要在转换函数用到之前加载完成。

(我也考虑过回调函数,但是就当前这个项目的架构来说,回调函数的写法不符合现有的规范。说实在的,这是我经验不足造成的。因为多语言的需求是我在完成了几乎其他所有的代码后,才意识到需要满足的需求,所以在写法上,为了尽量小改已经写好的代码,就选择了折中的 同步加载语言包 的方式了。)

我又想到,既然是这么大的项目,肯定有人会写相关的插件,所以我更仔细的阅读了官方文档,感觉光明就在前方,只差临门一jio。皇天不负有心人,我在文档中成功发现了一个叫做 i18next-sync-fs-backend 的包。我点到代码仓库一看,嗯?不在 i18next 官方仓库之下?算了,既然是上了官方文档的应该不至于太不靠谱。我按照 readme 进行了安装和配置。嗯?编译报错了?ojbk,被磨到没耐心。我又认真的在脑子里过了一遍这个需求实现的逻辑,发现其实并不难,完全可以自己写一个,就当练手了。

好,终于叭叭完了,接下来上代码。

Talk is cheap. Show me the code.

配置工作环境

想要详细代码戳我戳我(●’◡’●)

webpack.js

const path = require('path');

module.exports = {
    // webpack入口文件,同目录下的index.js
    entry: path.resolve(__dirname, 'index.js'),
    // 输出路径为dist,输出的文件为index.js
    output: {
        publicPath: '/',
        filename: 'dist/index.js'
    },
    // 开发工具,方便调试
    devtool: 'source-map',
    // 本地服务器,运行服务时,将监听8899端口并自动在浏览器打开
    devServer: {
        contentBase: './',
        inline: true,
        port: 8899,
        open: 'http://localhost:8899/',
        host: '0.0.0.0'
    },
    // loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务
    // 插件的范围包括:打包优化、资源管理和注入环境变量
    // loader是针对某个文件,而plugin是针对整个打包流程
    module: {
        // 将es6语法的js文件转换为浏览器可以理解的语法
        loaders: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [{
                    loader: 'babel-loader',
                    options: {
                        presets: ['es2015']
                    }
                }]
            }
        ]
    }
}

package.json

{
  //...
  // 在命令行中输入 npm start 启动本地服务
  "scripts": {
    "start": "webpack-dev-server --color --progress --config webpack.js"
  },
  //...
}

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>multi language</title>
</head>
<body>
    <h2>多语言展示</h2>

    <!-- p#wrap用于展示多语言的变换 -->
    <p id="wrap"></p>
    <script src="/dist/index.js"></script>
</body>
</html>

index.js

import multiLang from './multiLang' //引入多语言实现模块

var lang = 'en-US'; // 假设当前选择的语言是英语,改变此变量的值切换语言包

// 传入参数,调用多语言设置函数
// 参数为:lang, url, isAsync, callback, expired
multiLang(lang, `/locales/${lang}/translation.json`, true, function (data) {
    // 将获取的语言包的值展示到页面上
    document.getElementById('wrap').innerHTML = data.greet;
}, 1000 * 60 * 60);

语言包配置

根据项目中静态文件存放的位置不同,在语言包的前面还可能有另外的路径前缀,我们当前默认是在根目录下。当然,只要保证你的语言包内的文件夹和文件名命名符合规范,就问题不大。此处采用的规范是:

  1. 所有文件都存放在locales文件夹内
  2. 以 国家缩写-语言缩写 命名文件夹,如en-USzh-CN
  3. 语言包文件夹内文件名为translation.json

多语言实现模块

multiLang.js

export default function (lang, url, isAsync, callback, expired) {

  // lang-标识语言的字符串
  // url-请求语言包文件的路径
  // isAsync-是否同步加载语言包
  // callback-语言包返回后执行的函数
  // expired-语言包缓存过期时间

  var needAjax = true; // 是否需要请求语言包
  lang = lang || 'zh-CN'; // 当前的语言,默认为中文

  // 查看本地是否有语言包缓存
  var local_lang = localStorage.getItem(`lang_${lang}`);
  // 若本地有语言包缓存则从本地获取语言包
  if (local_lang !== null) {
    // 查看当前语言包是否超出缓存时间,若超出则删除语言包
    if (new Date().getTime()<=localStorage.getItem(`lang_${lang}_expired`)){
        // 从本地获取语言包,则不需要发送请求获取
        needAjax = false;
        // 是否有需要执行的回调函数,执行或什么都不做
        callback ? callback(JSON.parse(local_lang)) : null;
    } else {
        // 删除本地语言包缓存
        localStorage.removeItem(`lang_${lang}`);
        localStorage.removeItem(`lang_${lang}_expired`);
    }
  }

  // 是否发送请求
  if (needAjax) {
      _ajax(url, isAsync, function (data) {
          // 将获取的语言包缓存到本地并设置过期时间,默认过期时间为7天
          localStorage.setItem(`lang_${lang}`, JSON.stringify(data));
          localStorage.setItem(`lang_${lang}_expired`,
              (new Date().getTime() + expired || 1000 * 60 * 60 * 24 * 7))
          callback ? callback(data) : null;
      });
  }
}

function _ajax(url, isAsync, callback) {
  try {
      // 根据不同浏览器,创建XHR对象
      var xhr;
      if (XMLHttpRequest) {
          xhr = new XMLHttpRequest();
      } else {
          xhr = new ActiveXObject('MSXML2.XMLHTTP.3.0');
      }
      // 是否同步加载语言包
      xhr.open('GET', url, isAsync);

      // 设置HTTP的MIME类型
      if (xhr.overrideMimeType) {
          xhr.overrideMimeType("application/json");
      }

      // 设置回调函数
      xhr.onreadystatechange = function () {
          // 当存在返回数据且有回调函数时,将字符串数据解析为JSON传入回调函数
          xhr.readyState > 3 && callback
            && callback(JSON.parse(xhr.responseText));
      };

      // 发送GET请求
      xhr.send(null);

  } catch (e) {
      // 输出报错
      console && console.log(e);
  }
}

总结一下

i18next是一个很不错的项目,感兴趣的小伙伴可以去了解一下。功能很全,而且可以clone源码自己改吧改吧的用。之所以自己写个模块,主要考虑的是三点:

  1. i18next过于全面使用稍显得复杂,此项目用不上
  2. i18next异步加载语言包,此项目基于现状需要同步加载语言包
  3. 自己想练手写一个模块hhhhhh :P

好久没有写文章了,终于忙里偷闲的把这篇写完了,开心 ~ 😀嘻嘻
特别鸣谢花艺师yyf 送我的头图 ♥谢啦~