Spring MVC国际化

一、需求描述

项目需要做整体的国际化。通常的解决思路有两种,一种解决方案是重新部署一套专门针对所在语言国家的国际站点,这种方式的典型特点是启用一套新的域名,并且无论是前端还是后台都需要重新独立部署;而另外一种解决方案则是,使多语言的国际化,通过用户自主选择或者主动识别当前用户所在的地区,前端传递不同的请求参数获取不同的语言描述的内容。

二、解决方案

2.1 国际化开发概述

软件的国际化:软件开发时,要使它能同时应对世界不同地区和国家的访问,并针对不同地区和国家的访问,提供相应的、符合来访者阅读习惯的页面或数据。

国际化(internationalization)又称为 i18n(读法为i 18 n,据说是因为internationalization(国际化)这个单词从i到n之间有18个英文字母,i18n的名字由此而来)

本文根据这张图来介绍SpringMVC实现国际化的过程:

  1. 根据浏览器语言进行国际化配置
  2. 根据语言切换进行国际化配置
国际化

2.2 合格的国际化软件

软件实现国际化,需具备以下两个特征:

  1. 对于程序中固定使用的文本元素,例如菜单栏、导航条等中使用的文本元素、或错误提示信息,状态信息等,需要根据来访者的地区和国家,选择不同语言的文本为之服务。
  2. 对于程序动态产生的数据,例如(日期,货币等),软件应能根据当前所在的国家或地区的文化习惯进行显示。

解决方案:
方案一:通过资源文件来实现国际化,页面获得浏览器语言来进行设置。

固定文本元素的国际化
对于软件中的菜单栏、导航条、错误提示信息,状态信息等这些固定不变的文本信息,可以把它们写在一个properties文件中,并根据不同的国家编写不同的properties文件。这一组properties文件称之为一个资源包。

创建资源包和资源文件
一个资源包中的每个资源文件都必须拥有共同的基名。除了基名,每个资源文件的名称中还必须有标识其本地信息的附加部分。例如:一个资源包的基名是“myproperties”,则与中文、英文环境相对应的资源文件名则为: "myproperties_zh.properties" "myproperties_en.properties"
  
每个资源包都应有一个默认资源文件,这个文件不带有标识本地信息的附加部分。若ResourceBundle对象在资源包中找不到与用户匹配的资源文件,它将选择该资源包中与用户最相近的资源文件,如果再找不到,则使用默认资源文件。例如:myproperties.properties

3.2、资源文件的书写格式

资源文件的内容通常采用"关键字=值"的形式,软件根据关键字检索值显示在页面上。一个资源包中的所有资源文件的关键字必须相同,值则为相应国家的文字。

并且资源文件中采用的是properties格式文件,所以文件中的所有字符都必须是ASCII字码,属性(properties)文件是不能保存中文的,对于像中文这样的非ACSII字符,须先进行编码。

例如:
国际化的中文环境的properties文件
国际化的英文环境的properties文件

java提供了一个native2ascII工具用于将中文字符进行编码处理,native2ascII的用法如下所示:

Spring配置文件:

<!--国际化配置-->
    <!--1. 语言包及其解析器配置-->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <!--表示多语言配置文件在根路径下,并且是以'messages'开头的文件-->
        <property name="basenames">
            <list>
                <value>i18n.messages</value>
            </list>
        </property>
        <!-- 如果在国际化资源文件中找不到对应代码的信息,就用这个代码作为名称  -->
        <property name="useCodeAsDefaultMessage" value="true"/>
    </bean>

    <!--2. 存储区域设置信息:SessionLocaleResolver类通过一个预定义会话名将区域化信息存储在会话中。-->
    <bean id="localeResolver" class="org.springframework.web.servlet.i18n.SessionLocaleResolver"/>

    <!--拦截器配置-->
    <mvc:interceptors>
        <mvc:interceptor>
            <!--语言拦截器,支持国际化-->
            <mvc:mapping path="/**"/>
            <bean class="interceptor.LanguageInterceptor">
                <property name="paramName" value="lang"/>
            </bean>
        </mvc:interceptor>
    </mvc:interceptors>

多语言的资源包放置在resources目录下新建的i18n文件夹下,两个中英文的资源包文件名分别为和messages_zh.propertiesmessages_en.properties。二者的内容分别如下:
messages_zh.properties:

test.info1=\u4ec0\u4e48\u007b\u0030\u007d\u4ec0\u4e48\u4e8b\u60c5\u007b\u0031\u007d
test.info2=\u8FD9\u91CC\u662F\u5C55\u73B0\u7528\u6237\u4FE1\u606F

messages_en.properties:

test.info1=what {0} what thing {1}
test.info2=this is display user information

这里为大家介绍一个小技巧:虽然官方要求中文环境的value必须写成unicode编码的格式,但是unicode编码后的内容可读性太差,而且每次手动去转换费时费力,有没有什么好的解决办法呢?其实,神器inteilj idea早就帮助我们考虑到这个问题了,我们可以通过简单的设置之后,中文包的资源文件的value仍然可以写成中文,这样阅读良好,便于排错。具体设置请参考下图:

依次打开Mac版本Preferences(Windows版本是Setting)->Editor->File Encoding,然后检查是否跟下图中的设置完全一致。

idea自动转码设置

语言拦截器:

package interceptor;

import controller.BaseController;
import http.ExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

public class LanguageInterceptor extends HandlerInterceptorAdapter {

    private static final Logger logger = LoggerFactory.getLogger(LanguageInterceptor.class);

    private static String LANG_HERDER = "X-163-AcceptLanguage";

    /**
     * Default name of the locale specification parameter: "locale".
     */
    public static final String DEFAULT_PARAM_NAME = "locale";
    private String paramName = DEFAULT_PARAM_NAME;

    public void setParamName(String paramName) {
        this.paramName = paramName;
    }

    public String getParamName() {
        return this.paramName;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 拦截方式一:拦截请求参数
//        Locale newLocale = getLocale(request.getParameter(getParamName()));

        // 拦截方式二:拦截请求头部参数
        String header = request.getHeader(LANG_HERDER);
        logger.info(LANG_HERDER + ":" + header);
        Locale newLocale = getLocale(request.getHeader(LANG_HERDER));

        LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver(request);

        if (localeResolver == null) {
            throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
        }

        localeResolver.setLocale(request, response, newLocale);

        ExecutionContext context = new ExecutionContext(request, response);
        BaseController.CONTEXT.set(context);

        return true;
    }

    //根据language 获取Locale
    public static Locale getLocale(String language) {
        Locale locale = new Locale("zh", "CN");
        if (language != null && language.equals("en")) {
            locale = new Locale("en", "US");
        }

        return locale;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        BaseController.CONTEXT.remove();
    }
}

一个session内共享的类ExecutionContext

package http;

import org.springframework.web.servlet.support.RequestContextUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

public class ExecutionContext {

    private HttpServletRequest request;

    private HttpServletResponse response;

    private Locale locale;

    public ExecutionContext(HttpServletRequest request, HttpServletResponse response) {
        this.request = request;
        this.response = response;
    }

    public HttpServletRequest getRequest() {
        return request;
    }

    public void setRequest(HttpServletRequest request) {
        this.request = request;
    }

    public HttpServletResponse getResponse() {
        return response;
    }

    public void setResponse(HttpServletResponse response) {
        this.response = response;
    }

    public Locale getLocale() {
        if (locale == null && request != null) {
            return RequestContextUtils.getLocale(request);
        }
        return locale;
    }

    public void setLocale(Locale locale) {
        this.locale = locale;
    }
}

BaseController的实现:

package controller;

import http.ExecutionContext;

public class BaseController {

    // 获取执行环境的上下文信息,保存执行相关的参数
    public static final ThreadLocal<ExecutionContext> CONTEXT = new ThreadLocal<>();

    protected ExecutionContext getExecutionContext() {
        return CONTEXT.get();
    }
}

补充介绍下MessageSource
Spring定义了访问国际化信息的MessageSource接口,并提供了几个易用的实现类。首先来了解一下该接口的几个重要方法:
1)String getMessage(String code, Object[] args, String defaultMessage, Locale locale) code
表示国际化资源中的属性名;args用于传递格式化串占位符所用的运行期参数;当在资源找不到对应属性名时,返回defaultMessage参数所指定的默认信息;locale表示本地化对象;
2)String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException
与上面的方法类似,只不过在找不到资源中对应的属性名时,直接抛出NoSuchMessageException异常;
3)String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException
MessageSourceResolvable 将属性名、参数数组以及默认信息封装起来,它的功能和第一个接口方法相同。

测试Controller的实现:

package controller;

import entity.User;
import http.ExecutionContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.support.RequestContext;
import org.springframework.web.servlet.support.RequestContextUtils;
import service.UserService;
import util.ResultCode;

import javax.servlet.http.HttpServletRequest;
import java.util.*;

@Controller
public class TestController extends BaseController {

    private static Logger logger = LoggerFactory.getLogger(UserController.class);

    @Autowired
    private MessageSource messageSource;

    @RequestMapping("/test")
    @ResponseBody
    public Object myTest() {
        Object[] args = new Object[]{100, 200};
//        String code = messageSource.getMessage(ResultCode.Test.getName(), args, new Locale("zh", "CN"));
        ExecutionContext context = getExecutionContext();
        Locale locale = context.getLocale();

        logger.info("language=" + locale.getLanguage());
        String code = messageSource.getMessage(ResultCode.TEST_INFO1.getName(), args, locale);
        String info = messageSource.getMessage(ResultCode.TEST_INFO2.getName(), null, locale);

        Map<String, Object> responseMap = new HashMap<>();
        responseMap.put("code", code);
        responseMap.put("info", info);
        responseMap.put("statusCode", 200);
        return responseMap;
    }
}

下面介绍一个用于封装所有key的ResultCode,使用这个类封装所有多语言用到的key的好处在于,如果后面需要修改key,只需要修改该类以及所有的语言包中搜索到需要修改的key即可,无需整个项目区查找和修改,简化了后面项目变更带来的修改成本,提高代码的可扩展性。具体的代码描述如下:

package util;

public enum ResultCode {

    TEST_INFO1("test.info1"),
    TEST_INFO2("test.info2");

    ResultCode(String name) {
        this.name = name;
    }

    private String name;

    public String getName() {
        return this.name;
    }
}

中文语言环境下的请求测试:


中文语言环境下的请求测试

英文语言环境下的请求测试:


英文语言环境下的请求测试

方案二:每个页面进行翻译,在每个控制器里用@RequestHeader获得浏览器语言。

@RequestHeader(“Accept-Language”)获取浏览器设置的优先语言

控制器:

@RequestMapping(value="/displayHeaderInfo")
     public String displayHeaderInfo(@RequestHeader("Accept-Language") String language)  { 
          System.out.println("language:"+language);
          String lang = getlang(language);

          System.out.println("浏览器优先语言:"+getlang(language));

          return "about/"+lang+"_About";
     }

     public static String getlang(String accept_language){
          String[] lang_arr = accept_language.split(",");
          String first_lang = lang_arr[0];
          System.out.println("浏览器优先语言:"+first_lang);
          if(first_lang.equals("zh")||first_lang.equals("zh-CN")){
              return "ZH";
          }if(first_lang.equals("zh-TW")||first_lang.equals("zh-HK")){
              return "HK";
          }else{
              //默认英语
              return "EN";
          }
     }

三、总结

i18n的实现语言就是构造两级的map,第一级map的key是locale变量,选择到对应的语言环境的map,再根据语言包中的key获取到对应的value。需要在程序启动时将所有的多语言环境的数据加载到内存中,因此,需要合理地评估下语言包的数据量是否适合全部加载到内存中,不适合的话就要考虑其他的方案来实现国际化。

此外,上述使用语言包的方式实现国际化仍然有一定的局限性,尤其是面对一个已经存在的完全针对中文语言环境的系统进行国际化改造时,需要在dao层对查询出来的中文信息的字段进行拦截,根据语言包进行变量替换;此外,部分数据库中的中文字段是根据另外几张表中的几个字段拼接而成的,这种情况就比较麻烦,需要在dao层根据语言包获取一下中英文的单参数的配置,再到service层将多个字段根据语言包选择合适的模板,在拼接一次,这样可以最小化代码的改动,以实现国际化的功能。

以上,只是个人在实践项目国际化过程中的一些经验总结,上存在一些不足之处。

四、附录资料

4.1 常用国家语言一览表

语言加代码 语言加国家
zh_CN 中文简体,中国
zh_TW 中文繁体,台湾
zh_HK 中文繁体,香港
en_US 英语,美国
en_GB 英语,英国
es_ES 西班牙
es_US 西班牙语,美国
en_ZA 英语,津巴布韦

3.2 参考源码

上文中使用的源代码可以参考:
示例源代码

五、参考资料

  1. Web国际化的三种解决方式
  2. 利用Spring进行项目国际化(多语言)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容