阅读 891

【你应该了解的】前端通用国际化解决方案

skFeTeam  本文作者:王芬

国际化背景

前端技术日新月异,技术栈繁多。以前端框架来说有React, Vue, Angular等等,再配以webpack, gulp等等构建工具去满足日常的开发工作。同时在日常的工作当中,不同的项目使用的技术栈也会不一样。当需要对部分项目进行国际化改造时,由于技术栈的差异,这时你需要去寻找和当前项目使用的技术栈相匹配的国际化的插件工具。比如:

  • vue + vue-i18n
  • angular + angular-translate
  • react + react-intl
  • jquery + jquery.i18n.property

等等,同时可能有些页面没有使用框架,或者完全是没有进行工程化的静态前端页面。

为了减少由于不同技术栈所带来的学习相关国际化插件的成本及开发过程中可能遇到的国际化坑,在尝试着分析前端国际化所面临的主要问题及相关的解决方案后,我觉得是可以使用更加通用的技术方案去完成国际化的工作。

本文目的

将页面显示的信息翻译成不同语言,可以根据不同语言开发多个版本,然后根据用户选择的语言显示不同的页面。但是这样需要很高的成本,而且维护起来也很麻烦,一个地方有问题就要修改所有版本。本文的初衷是为了寻找一种比较通用的国际化解决方案。

国际化需要考虑的问题

1.文案翻译

  • 静态文案翻译(前端静态模板文案)

    使用i18n国际化插件匹配字典重新赋值(预估工作量相当大)

  • 动态文案翻译(server端下发的动态数据)

2.样式

  • 不同语言长度不一样造成的样式错乱

    例如:elementui中英文切换时 table表行、表头错位问题

    表行、表头错位问题的解决方法:
     <el-table-column 
    	prop="a" 
    	:label="$t('b.c')"
    	show-overflow-tooltip="true"
     	:width="($t('b.c')).length*18"
      >
     
    其中:
    1. show-overflow-tooltip="true"表示内容超出宽度后省略号表示
    2. a表示绑定的数据
    3. b.c表示国际化zh.js,en.js中对应显示的表头名称
    复制代码
  • h5样式问题,中英文长度不同,布局错乱

    解决方法:

    在最上层html的div上添加一个可以动态切换class的变量,以改变项目的父级的class名称,实现切换语言的同时切换css样式。如下栗子:

    HTML部分
    <template>
      <div :class="langCss">
      </div>
    </template>
    复制代码
    Js部分
    在data中定义保存class的变量
    data () {
        return {
          langCss:window.localStorage.getItem('lang')||'zh',
          //先去取localStorage里保存的语言,如果没有,那么就默认中文
          //这么做的意义是为了用户在刷新页面的时候样式不丢失。
     }
    复制代码

    紧接着在我们的switchlLang函数后添加新的一行,以便在切换语言的同时切换class。

    switchLang(lang)  {
        this.$i18n.locale = lang 
        localStorage.setItem('lang',lang);
        this.langCss=lang;//新添加的,以便切换父级class
    }
    复制代码
    Css部分

    在style标签中中设置你想要的样式,我这边以改变文字颜色为例。

    .en button {
      color: steelblue;
    }
    .zh button {
      color: #666;
    }
    .ja button {
      color: seagreen;
    }
    复制代码

    样式错乱解决方法小结:

    1、写页面时包裹容器尽量不要写死宽高等 2、语言变量写在最外层,里面包裹其他class

    3、还可以给元素设置一个父元素,避免样式互相影响,然后再使用这个语言变量控制class

    4、还有一种方法(就是溢出隐藏): overflow: hidden; white-space: nowrap; text-overflow: ellipsis;

    5、h5最好使用自适应布局

  • 图片的替换

3.时区问题

以泰国举例,相关数据库部署在泰国。如果在中国创建一条数据插入泰国数据库,通过时间控件选择数据的创建时间,这时获取的时间是从浏览器获取 为中国时区时间。需要把这个时间传到后端服务器,存储到数据库,但服务器的时间为泰国时区的时间。中国是东八区 泰国是东七区,相差一个小时。这就会导致两个问题:

a、创建数据:在前端选的8:00,落到数据库中就会变为7:00

b、数据查询时的错觉:在前端选择查询8点到9点创建的数据,到后端经过转换会变为查找7:00到8点的消息,返回的数据又会带有相应的时区,导致,数据的一种假象。以为查的是8点到9点创建的数据,而实际看到的是7:00到8点的消息。
复制代码

问题追踪发现,前端传过来的时间,在经过java时间格式转换后会转换为对应时区的时间。

时区问题解决:

1、这种问题一般数据库的时区设置为UTC时间,前端传的数据增加时区的字段,标识自己是哪个时区,后端将这个时间转为UTC时区传给数据库,同时,数据库返回的时间再转为前端需要的时区传递出去,这样会有效避免时区问题。

2、既然我们的数据库已经是当地的时区,无法改变,那一种有效的做法时,登录页面前做个选择时区的页面,用户选择切换到对应的时区,访问对应的数据源。

3、另外一种简单的做法:所有前端的时间去掉时间格式,作为字符串与后端交互。后端将时间转换为字符串传给前端,前端的时间同样处理为字符串传给后端,后端做时间处理,这样就可避免时区问题,做到前端时间可见即可得(前端展示什么,库中就是什么时间)

4.map语言映射表维护

5.第三方服务

  • SDK

6.本地化

  • 货币单位

    使用阿拉伯数字,货币单位使用฿,要增加货币格式化工具函数处理

  • 时间格式

8.长文案加变量的翻译问题

  • 在翻译这样一句话 我被用户 xxx 拉进一个群聊的时候。英文的翻译是I was dragged into a group chat by user xxx。xxx出现的地方不一致这个时候我们就不能简单的直接设置en.jszh,js进行转换。

  • 我们这里可以把这变量用一个字符替换,然后封装函数匹配变量进行替换 我被用户 %s 拉进一个群聊I was dragged into a group chat by user %s

  • 封装函数代码

    function tplParse(tpl, params) {
        let counter = 0;
        if (params && params.length > 0) {
        let match;
        while(match = tpl.match(/%s/)) {
        tpl = tpl.slice(0, match.index) + params[counter] +tpl.slice(match.index + 2);
        counter ++;
    	}
      }
      return tpl;
    }
    复制代码
  • 第一个参数是需要替换的文案,第二个参数就是替换的变量

9.切换语言的时候遍历的数据无法生效

  • 在切换语言的时候我们会发现,routerindex.js中的语言无法切换。这是因为data是一次性生产的,平常的写法只能是在 data 初始化的时候拿到这些被国际化的值,并不能响应变化。查阅文案给出的方案是需要遍历的数据通过computed重新计算一遍在返回。由于出现的地方较多这种方案实现起来太复杂。暂时的解决方案是切换语言的时候刷新页面。

解决方案

在日常的开发过程当中,遇到的最多的需要国际化的场景是:语言翻译,样式,map表维护打包方案。接下来针对这几块内容并结合日常的开发流程说明国际化的通用解决方案。

首先来看下当前开发环境可能用的技术栈:

  1. 使用了构建工具
    • webpack
    • gulp
    • ...

基于这些构建工具,使用:

  • Vue
  • Angular
  • React
  • ...
  • 未使用任何framework
  1. 未使用构建工具
    • 使用了jqueryzepto等类库
    • 原生js

常见型

常见的前端国际化方法步骤如下:(原理)

  • 定义国际化配置

  • 根据环境读取配置

  • 将配置展现在页面上

展开说:

  1. 定义国际化配置: 定义的方式有多种,多以文件的形式单独保存,如json,js,properties 等, 并且将配置信息以键值对的形式保存备用
  2. 根据环境读取配置: 所谓环境说白了就是用户选择的标志,形式如下: hash型:#cn; #en; #us saerch型:?lan=cn; ?lan=en; ?lan=us url/meta型: 163.com/cn/; 163.com/en 缓存型:缓存形式多为cookie,默认cn,用户重新设定后将缓存更新
  3. 将配置展现在页面上: 使用三方插件或者自己编写插件将配置信息映射到页面上, 可以使用,juery.i18n.js 或 react、angular国际化插件等regular暂无插件 插件的基本原理都是做字典查询键值匹配替换。

以上三步任意组合都可以完成国际化的任务,只是效率各有不同,可根据项目做自由组合

不常见型

不常见的方法步骤如下:(原理)

  • 将国际化配置分散在各个文件中如: <a class='i18n'>登录|登入|Sign in|サインイン|로그인</a>

  • 根据环境确定国际化标记: cn:0, tw:1, en:2, jp:3, kr:4

  • 根据国际化标记显示相应信息 全局搜索class=i18n的元素,保留相应信息

极端型

  • 使用google翻译插件

通用的解决方案[di18n-translate]

综合不同的构建工具,开发框架及类库,针对不同的开发环境似乎是可以找到一个比较通用的国际化的方案的。

这个方案的大致思路就是:通过构建工具去完成样式, 图片替换, class属性等的替换工作,在业务代码中不会出现过多的因国际化而多出的变量名,同时使用一个通用的翻译函数去完成静态文案动态文案的翻译工作,而不用使用不同框架提供的相应的国际化插件。简单点来说就是:

  • 依据你使用的构建工具 + 一个通用的翻译函数去完成前端国际化

首先,这个通用的语言翻译函数: di18n-translate。它所提供的功能就是静态和动态文案的翻译, 不依赖开发框架及构建工具。

  npm install di18n-translate
复制代码

这个时候你只需要将这个通用的翻译函数以适当的方式集成到你的开发框架当中去。

接下来会结合具体的不同场景去说明下相应的解决方案:

1.使用MVVM类的框架

使用了MVVM类的framework时,可以借助framework帮你完成view层的渲染工作, 那么你可以在代码当中轻松的通过代码去控制class的内容, 以及不同语言环境下的图片替换工作.

例如vue, 示例(1):

app.vue文件:
  <template>
    <p class="desc"
      :class="locale"   // locale这个变量去控制class的内容
      :style="{backgroundImage: 'url(' + bgImg + ')'}"  // bgImg去控制背景图片的路径
    ></p>
    <img :src="imgSrc"> // imgSrc去控制图片路径
    <p>{{title}}</p>
  </template>

  <script>
    export default {
      name: 'page',
      data () {
        return {
          locale: LOCALE,
          imgSrc: require(`./images/${LOCALE}/${LOCALE}.png`),,
          bgImg: require(`./images/${LOCALE}/${LOCALE}.png`),,
          title: this.di18n.$t('你好')
        }
      }
    }
  </script>
复制代码

这个时候你再加入翻译函数,就可以满足大部分的国际化的场景了,现在在main.js中添加对翻译函数di18n-translate的引用:

main.js文件:

import Vue from 'vue'

window.LOCALE = 'en'
const DI18n = require('di18n-translate')
const di18n = new DI18n({
    locale: LOCALE,       // 语言环境
    isReplace: false,   // 是否进行替换(适用于没有使用任何构建工具开发流程)
    messages: {         // 语言映射表
      en: {
        你好: 'Hello, {person}'
      },
      zh: {
        你好: '你好, {person}'
      }
    }
  })

Vue.prototype.d18n = di18n

复制代码

使用mvvm framework进行国际化,上述方式应该是较为合适的,主要是借助了framework帮你完成view层的渲染工作, 然后再引入一个翻译函数去完成一些动态文案的翻译工作

这种国际化的方式算是运行时处理,不管是开发还是最终上线都只需要一份代码。

2.偏展示性网页

html文件:

  <div class="wrapper" i18n-class="${locale}">
    <img i18n-img="/images/${locale}/test.png">
    <input i18n-placeholder="你好">
    <p i18n-content="你好"></p>
  </div>
复制代码

运行时:

  <script src="[PATH]/di18-translate/index.js"></script>
  <script>
    const LOCALE = 'en'
    const di18n = new DI18n({
      locale: LOCALE,
      isReplace: true,   // 开启运行时
      messages: {
        en: {
          你好: 'Hello'
        },
        zh: {
          你好: '你好'
        }
      }
    })
  </script>
复制代码

最后html会转化为:

  <div class="wrapper en">
    <img src="/images/en/test.png">
    <input placeholder="Hello">
    <p>Hello</p>
  </div>
复制代码

3.使用构建工具

接下来继续说下借助构建工具进行模块化开发的项目, 这些项目可能最后页面上的DOM都是通过js去动态插入到页面当中的。 html文件:

  <div class="wrapper ${locale}">
    <img src="/images/${locale}/test.png">
    <p>$t('你好')</p>
  </div>
复制代码

js文件:

  let tpl = require('./index.html')
  let wrapper = document.querySelector('.content')
  
  // di18n.$html方法即对你所加载的html字符串进行replace,最后相对应的语言版本
  wrapper.innerHTML = di18n.$html(tpl)
复制代码

最后插入到的页面当中的DOM为:

  <div class="wrapper en">
    <img src="/images/en/test.png">
    <p>Hello</p>
  </div>
复制代码

这个时候动态翻译再借助引入的di18n上的$t方法

  di18n.$t('你好')
复制代码

这种开发方式也属于运行时处理,开发和上线后只需要维护一份代码。

国际化的实现原理

其实原理很简单,这里只讲最基本的原理,不谈框架的特性。

文章最上面列举的框架,有一个共同的特征,就是都有一个类似语言包的东西。

zh.json
en.json
jp.json
...
复制代码

这个也很好理解,把各种语言独立开来,便于管理和维护。

便于测试,我们把请求的过程去掉了,直接写在一个json对象里面,如下

intl.js

var intl = 
{
    "zh": {
        "title": "测试",
        "content": "这是一个测试"
    },
    "cn": {
        "title": "test",
        "content": "this is a test"
    }
}
复制代码

大概会写一些这样的配置语言,然后通过某种手段把对应的字段设置到相应的位置就可以了。

下面是伪代码

<h2 id="title">测试</h2>
<p id="content">这是一个测试</p>
var lang = getGlobalVar('LOCALE')||'zh';//获取语言
var local = intl['lang'];

$title.innerHTML = local['title'];
$content.innerHTML = local['content'];
复制代码

上面是一个简单的实现思路,如果是一个简单的静态页面,大可以用这种方式,也不需要引入一些第三方库,然后啃他的api

小结:

国际化还远不止页面静态文字的简单翻译,还包括本地化服务(时间、货币等等)。希望通过本文,大家对前端国际化有一定的了解。

想了解skFeTeam更多的分享文章,可以点这里,谢谢~