一、需求描述
项目需要做整体的国际化。通常的解决思路有两种,一种解决方案是重新部署一套专门针对所在语言国家的国际站点,这种方式的典型特点是启用一套新的域名,并且无论是前端还是后台都需要重新独立部署;而另外一种解决方案则是,使多语言的国际化,通过用户自主选择或者主动识别当前用户所在的地区,前端传递不同的请求参数获取不同的语言描述的内容。
二、解决方案
2.1 国际化开发概述
软件的国际化:软件开发时,要使它能同时应对世界不同地区和国家的访问,并针对不同地区和国家的访问,提供相应的、符合来访者阅读习惯的页面或数据。
国际化(internationalization)又称为 i18n(读法为i 18 n,据说是因为internationalization(国际化)这个单词从i到n之间有18个英文字母,i18n的名字由此而来)
本文根据这张图来介绍SpringMVC实现国际化的过程:
- 根据浏览器语言进行国际化配置
- 根据语言切换进行国际化配置
2.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.properties
和messages_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,然后检查是否跟下图中的设置完全一致。
语言拦截器:
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 参考源码
上文中使用的源代码可以参考:
示例源代码