Skip to content

前后端分离的情况下国际化方案

GoldSubmarine edited this page Jan 10, 2020 · 1 revision

国际化的过程,分为两步

  1. 确定语言环境
  2. 针对语言环境展示不同语言的文字

确定语言环境

对用户来说:

  1. 页面提供切换功能,用户可以选择语言环境。
  2. 系统自动判断语言环境,自动展示对应的语言文字。自动判断语言环境,可以是前端浏览器获取,也可以是后端服务根据请求头的相关信息(如时区、IP)等信息来判断。

做的好的话,可以做成先自动判断,然后以用户选择为最高优先级。

需要注意:

  1. 无论用哪种方式,确定好语言环境后,需要前后端保持一致。
  2. 前端、后端,最好只有一个地方判断语言环境,判断好之后,通知另一方

优势比较:

  1. 前端实现比较简单
  2. 后端实现可以实现更复杂的判断规则

对于我们的项目来说,前端判断足以满足需求。因此,统一在前端判断语言环境。

针对语言环境展示不同语言的文字

确定了语言环境后,需要针对不同的语言展示不同的文字。

需要国际化的地方有:

  1. 前端的菜单、按钮、静态页面上的文字
  2. 后端返回的数据的国际化

可能的做法有三种:

  1. 完全后端来做

    前后端分离情况下,前端的静态页面不从后端获取,后端无法根据不同的语言环境,展示不同的文字。这个方案是行不通的。

  2. 前后端都做

    前后端分离,那么静态页面,由前端来做国际化。后端返回的信息,由后端来做国际化。

    这个方案:

    1. 前后端都做,职责不单一
    2. 如果要修改,某些返回的提示信息,需要修改后端服务,可能需要重启(或者不重启,采用提示信息存数据库的方案),带来新的复杂度

    因此,放弃这个方案。采用完全由前端做国际化的方案。

    后端不关心当前的语言环境。前端判断语言环境之后,自己用就可以了,不用传给后台。

    有个例外情况,就是对于用户产生数据的国际化。 一般来说,用户产生数据,不需要国际化。但是某些情况下,用户产生的数据,需要展示给不同国家的人(比如,淘宝的商品介绍),那么此时我们会提供给用户功能,针对不同的语言环境,用户产生不同的内容数据。这种情况下,后端需要关心语言环境。

  3. 完全前端来做

    静态页面的国际化,前端做起来很容易。后端返回数据需要特别处理一下。

    后端返回的数据大概分为两类:

    1. 用户产生的内容信息
    2. 用户操作的提示信息(保存成功、参数错误等提示信息)

    第一类信息一般是不需要国际化的(见上一条),第二类信息是需要的。

    后端需要对返回给用户的提示信息,做统一格式的返回

    基本上返回的信息,需要包含一个code,表明正确或错误的类型,前端根据这个code做翻译,显示不同的文字。

    一个完整的后端返回信息如下(见 SpringBoot统一错误处理):

    {
        "status": 404,
        "code": 40483,
        "message": "Oops! It looks like that file does not exist.",
        "developerMessage": "File resource for path /uploads/foobar.txt does not exist.  Please wait 10 minutes until the upload batch completes before checking again.",
        "moreInfo": "http://www.mycompany.com/errors/40483"
    }
    
    

    根据我们的实际情况,做裁剪,最终决定:

    1. 返回的内容分为两类,一类是内容,比如一个User或User List,一类是提示信息
    2. 对于内容信息,直接返回内容
    3. 对于提示信息,无论正确还是错误,统一返回一个Result

    后端返回Result的格式为:

    {
        "code": 2000,
        "msg": "",
        "data": ""
    }
    
    1. code 表明正确或错误的提示种类,前端根据code 去不同的语言环境文件里找 提示字符串
    2. 提示字符串,有可能有占位符,需要数据填充,data 里就有需要填充的数据。data的格式不固定,既可能是一个字符或数字,也可能是数组或对象等等。data 里除了可以放“替代占位符的数据”之外,还可以放其他前端需要的内容,比如返回重置后的密码等内容。
    3. msg的内容是某一种语言的提示文字,我们这里选定了中文。这个文字,对于后端人员来说,可以知道 code代表的msg是什么样的(后端在定义返回结果时,定义一个枚举ResultStatus,有codemsgmsg就是对code的描述)。对于前端来说,如果前端不需要支持国际化,那么可以不用根据code查找国际化文件。直接根据msgdata来提示就可以。

Bean Validation 的国际化

这里,Bean Validation表示的是 JSR 303规范

Bean Validation 通过在字段上加注解的方式来校验数据,并抛出对应的异常。

由于 Bean Validation 自带了国际化,根据枚举类型,或者注解上的message进行错误提示。

我们想把我们的枚举ResultStatus 应用在注解上是不可能的,注解上没法用枚举。其他也没有比较优雅的定义错误码code的方式。因此,我们的错误处理机制在这里是失效的。

另外,考虑一种情况:一个字段,假设是 userName 为空了,后端最多提示出来 “userName不能为空”,无法提示“用户名不能为空”。这里的问题是 userName代表的label是在前端指定的,后端是不知道的。这种情况下,后端无法提示出对用户友好的错误提示。 因此,Bean Validation 自带的国际化方案也不好用。

权衡之后,最终的方案是:

  1. 前后端的 Bean Validation 保持一致,或者前端的校验比后端更严格。
  2. 用户在前台页面输入,正常情况下错误信息会由前端给出。
  3. 如果前端未拦住非法输入,提交给后端了,后端对于Bean Validation类异常,统一返回一个统一的code,msg返回Bean Validation默认语言文件的提示信息。前端收到这个统一的错误code之后,统一提示“参数错误”,忽略msg。msg只给api调用者使用,这里msg的内容是 “userName不能为空”,返回这个错误信息,在这里是合理的。因为api调用者,提交给后台的就是 userName:null,因此返回 userName不能为空 是合理的。