Spring MVC國際化

一、需求描述

項目需要做整體的國際化。通常的解決思路有兩種,一種解決方案是重新部署一套專門針對所在語言國家的國際站點(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)國際化的過程:

  1. 根據(jù)瀏覽器語言進(jìn)行國際化配置
  2. 根據(jù)語言切換進(jìn)行國際化配置
國際化

2.2 合格的國際化軟件

軟件實現(xiàn)國際化,需具備以下兩個特征:

  1. 對于程序中固定使用的文本元素,例如菜單欄、導(dǎo)航條等中使用的文本元素、或錯誤提示信息,狀態(tài)信息等,需要根據(jù)來訪者的地區(qū)和國家,選擇不同語言的文本為之服務(wù)。
  2. 對于程序動態(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.propertiesmessages_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è)置完全一致。

idea自動轉(zhuǎn)碼設(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)境下的請求測試

英文語言環(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 參考源碼

上文中使用的源代碼可以參考:
示例源代碼

五、參考資料

  1. Web國際化的三種解決方式
  2. 利用Spring進(jìn)行項目國際化(多語言)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容