一、需求描述
項目需要做整體的國際化。通常的解決思路有兩種,一種解決方案是重新部署一套專門針對所在語言國家的國際站點(diǎn),這種方式的典型特點(diǎn)是啟用一套新的域名,并且無論是前端還是后臺都需要重新獨(dú)立部署;而另外一種解決方案則是,使多語言的國際化,通過用戶自主選擇或者主動識別當(dāng)前用戶所在的地區(qū),前端傳遞不同的請求參數(shù)獲取不同的語言描述的內(nèi)容。
二、解決方案
2.1 國際化開發(fā)概述
軟件的國際化:軟件開發(fā)時,要使它能同時應(yīng)對世界不同地區(qū)和國家的訪問,并針對不同地區(qū)和國家的訪問,提供相應(yīng)的、符合來訪者閱讀習(xí)慣的頁面或數(shù)據(jù)。
國際化(internationalization)又稱為 i18n(讀法為i 18 n,據(jù)說是因為internationalization(國際化)這個單詞從i到n之間有18個英文字母,i18n的名字由此而來)
本文根據(jù)這張圖來介紹SpringMVC實現(xiàn)國際化的過程:
- 根據(jù)瀏覽器語言進(jìn)行國際化配置
- 根據(jù)語言切換進(jìn)行國際化配置

2.2 合格的國際化軟件
軟件實現(xiàn)國際化,需具備以下兩個特征:
- 對于程序中固定使用的文本元素,例如菜單欄、導(dǎo)航條等中使用的文本元素、或錯誤提示信息,狀態(tài)信息等,需要根據(jù)來訪者的地區(qū)和國家,選擇不同語言的文本為之服務(wù)。
- 對于程序動態(tài)產(chǎn)生的數(shù)據(jù),例如(日期,貨幣等),軟件應(yīng)能根據(jù)當(dāng)前所在的國家或地區(qū)的文化習(xí)慣進(jìn)行顯示。
解決方案:
方案一:通過資源文件來實現(xiàn)國際化,頁面獲得瀏覽器語言來進(jìn)行設(shè)置。
固定文本元素的國際化
對于軟件中的菜單欄、導(dǎo)航條、錯誤提示信息,狀態(tài)信息等這些固定不變的文本信息,可以把它們寫在一個properties文件中,并根據(jù)不同的國家編寫不同的properties文件。這一組properties文件稱之為一個資源包。
創(chuàng)建資源包和資源文件
一個資源包中的每個資源文件都必須擁有共同的基名。除了基名,每個資源文件的名稱中還必須有標(biāo)識其本地信息的附加部分。例如:一個資源包的基名是“myproperties”,則與中文、英文環(huán)境相對應(yīng)的資源文件名則為: "myproperties_zh.properties" "myproperties_en.properties"
每個資源包都應(yīng)有一個默認(rèn)資源文件,這個文件不帶有標(biāo)識本地信息的附加部分。若ResourceBundle對象在資源包中找不到與用戶匹配的資源文件,它將選擇該資源包中與用戶最相近的資源文件,如果再找不到,則使用默認(rèn)資源文件。例如:myproperties.properties
3.2、資源文件的書寫格式
資源文件的內(nèi)容通常采用"關(guān)鍵字=值"的形式,軟件根據(jù)關(guān)鍵字檢索值顯示在頁面上。一個資源包中的所有資源文件的關(guān)鍵字必須相同,值則為相應(yīng)國家的文字。
并且資源文件中采用的是properties格式文件,所以文件中的所有字符都必須是ASCII字碼,屬性(properties)文件是不能保存中文的,對于像中文這樣的非ACSII字符,須先進(jìn)行編碼。
例如:
國際化的中文環(huán)境的properties文件
國際化的英文環(huán)境的properties文件
java提供了一個native2ascII工具用于將中文字符進(jìn)行編碼處理,native2ascII的用法如下所示:
Spring配置文件:
<!--國際化配置-->
<!--1. 語言包及其解析器配置-->
<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
<!--表示多語言配置文件在根路徑下,并且是以'messages'開頭的文件-->
<property name="basenames">
<list>
<value>i18n.messages</value>
</list>
</property>
<!-- 如果在國際化資源文件中找不到對應(yīng)代碼的信息,就用這個代碼作為名稱 -->
<property name="useCodeAsDefaultMessage" value="true"/>
</bean>
<!--2. 存儲區(qū)域設(shè)置信息:SessionLocaleResolver類通過一個預(yù)定義會話名將區(qū)域化信息存儲在會話中。-->
<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。二者的內(nèi)容分別如下:
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
這里為大家介紹一個小技巧:雖然官方要求中文環(huán)境的value必須寫成unicode編碼的格式,但是unicode編碼后的內(nèi)容可讀性太差,而且每次手動去轉(zhuǎn)換費(fèi)時費(fèi)力,有沒有什么好的解決辦法呢?其實,神器inteilj idea早就幫助我們考慮到這個問題了,我們可以通過簡單的設(shè)置之后,中文包的資源文件的value仍然可以寫成中文,這樣閱讀良好,便于排錯。具體設(shè)置請參考下圖:
依次打開Mac版本Preferences(Windows版本是Setting)->Editor->File Encoding,然后檢查是否跟下圖中的設(shè)置完全一致。

語言攔截器:
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 {
// 攔截方式一:攔截請求參數(shù)
// Locale newLocale = getLocale(request.getParameter(getParamName()));
// 攔截方式二:攔截請求頭部參數(shù)
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;
}
//根據(jù)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內(nèi)共享的類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的實現(xiàn):
package controller;
import http.ExecutionContext;
public class BaseController {
// 獲取執(zhí)行環(huán)境的上下文信息,保存執(zhí)行相關(guān)的參數(shù)
public static final ThreadLocal<ExecutionContext> CONTEXT = new ThreadLocal<>();
protected ExecutionContext getExecutionContext() {
return CONTEXT.get();
}
}
補(bǔ)充介紹下MessageSource:
Spring定義了訪問國際化信息的MessageSource接口,并提供了幾個易用的實現(xiàn)類。首先來了解一下該接口的幾個重要方法:
1)String getMessage(String code, Object[] args, String defaultMessage, Locale locale) code
表示國際化資源中的屬性名;args用于傳遞格式化串占位符所用的運(yùn)行期參數(shù);當(dāng)在資源找不到對應(yīng)屬性名時,返回defaultMessage參數(shù)所指定的默認(rèn)信息;locale表示本地化對象;
2)String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException
與上面的方法類似,只不過在找不到資源中對應(yīng)的屬性名時,直接拋出NoSuchMessageException異常;
3)String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException
MessageSourceResolvable 將屬性名、參數(shù)數(shù)組以及默認(rèn)信息封裝起來,它的功能和第一個接口方法相同。
測試Controller的實現(xiàn):
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即可,無需整個項目區(qū)查找和修改,簡化了后面項目變更帶來的修改成本,提高代碼的可擴(kuò)展性。具體的代碼描述如下:
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;
}
}
中文語言環(huán)境下的請求測試:

英文語言環(huán)境下的請求測試:

方案二:每個頁面進(jìn)行翻譯,在每個控制器里用@RequestHeader獲得瀏覽器語言。
@RequestHeader(“Accept-Language”)獲取瀏覽器設(shè)置的優(yōu)先語言
控制器:
@RequestMapping(value="/displayHeaderInfo")
public String displayHeaderInfo(@RequestHeader("Accept-Language") String language) {
System.out.println("language:"+language);
String lang = getlang(language);
System.out.println("瀏覽器優(yōu)先語言:"+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("瀏覽器優(yōu)先語言:"+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{
//默認(rèn)英語
return "EN";
}
}
三、總結(jié)
i18n的實現(xiàn)語言就是構(gòu)造兩級的map,第一級map的key是locale變量,選擇到對應(yīng)的語言環(huán)境的map,再根據(jù)語言包中的key獲取到對應(yīng)的value。需要在程序啟動時將所有的多語言環(huán)境的數(shù)據(jù)加載到內(nèi)存中,因此,需要合理地評估下語言包的數(shù)據(jù)量是否適合全部加載到內(nèi)存中,不適合的話就要考慮其他的方案來實現(xiàn)國際化。
此外,上述使用語言包的方式實現(xiàn)國際化仍然有一定的局限性,尤其是面對一個已經(jīng)存在的完全針對中文語言環(huán)境的系統(tǒng)進(jìn)行國際化改造時,需要在dao層對查詢出來的中文信息的字段進(jìn)行攔截,根據(jù)語言包進(jìn)行變量替換;此外,部分?jǐn)?shù)據(jù)庫中的中文字段是根據(jù)另外幾張表中的幾個字段拼接而成的,這種情況就比較麻煩,需要在dao層根據(jù)語言包獲取一下中英文的單參數(shù)的配置,再到service層將多個字段根據(jù)語言包選擇合適的模板,在拼接一次,這樣可以最小化代碼的改動,以實現(xiàn)國際化的功能。
以上,只是個人在實踐項目國際化過程中的一些經(jīng)驗總結(jié),上存在一些不足之處。
四、附錄資料
4.1 常用國家語言一覽表
| 語言加代碼 | 語言加國家 |
|---|---|
| zh_CN | 中文簡體,中國 |
| zh_TW | 中文繁體,臺灣 |
| zh_HK | 中文繁體,香港 |
| en_US | 英語,美國 |
| en_GB | 英語,英國 |
| es_ES | 西班牙 |
| es_US | 西班牙語,美國 |
| en_ZA | 英語,津巴布韋 |
3.2 參考源碼
上文中使用的源代碼可以參考:
示例源代碼