導語
本文說說java web的多語言國際化實現和主題(Theme)的實現,具體到框架是Spring MVC+Freemarker+jQuery/JS的多語言國際化實現和主題(Theme)的實現。如果一個系統會被多個國家使用,則多語言國際化/本地化是必須的。其實多語言只是國際化(i18n)的一部分,國際化(i18n)/本地化(l10n)(Wiki:Internationalization and localization)還包括貨幣、時區、符號、格式等其它內容。每種語言基本都支持國際化,比如.NET, php都支持,當然java也一樣。Java是一種基於Unicode的編程語言,提供了資源綁定(ResourceBundle)、地區(Locale)、時區(Timezone)等支持國際化。本文只說多語言(Multi-language)和主題(Theme)的實現。由於多語言散布在html、jsp、freemarker/*.frl、controller/Spring MVC、和javascript/js中,所以我們需要實現的是一個整體解決方案。本文內容:
- Java的資源國際化
- ResourceBundle類
- Servlet和Spring訪問資源文件
- Servlet和多語言
- Spring MVC+Freemarker+jQuery/js的多語言實現
- jFreeReport的多語言國際化
- jqGrid的多語言/國際化/i18n實現
- jQuery.UI.DatePicker的多語言/國際化/i18n實現
- Spring mvc的主題Theme實現
Java的資源國際化
Java默認的資源文件為*.properties(例如:messages_en_US.properties),資源文件要放到相應的classPath下面,和java一起編譯。資源文件里面的內容是key/value的格式,比如:hello = hello, world. 資源文件的編碼是UTF-8,也就是說里面可能不是直接要顯示的文字,而是UTF-8編碼以后的內容。properties里面的資源必須經過編碼,不允許里面出現法文、德文、中文、日文等Unicode字符,而必須是ASCII字符。Unicode轉ASCII可以使用JSK自帶的native2ascii.exe工具,位於JDK安裝目錄的bin目錄下,雙擊運行,輸入中午或日文或法文,格式:native2ascii -[options] [inputfile [outputfile]],例如:native2ascii -encoding UTF-8 c:\message_de_DE.txt c:\message_de_DE.properties,回車開始轉換。
ResourceBundle類
JSP和JSTL標簽底層都是通過Java的ResourceBundle類來獲取資源並設置參數的。如果弄java不知道這個ResourceBundle類就菜鳥了,比如搞個配置文件還寫個FileReader在那搞,折騰半天,最后還不能把配置文件和jar包打在一起發布....例子,讀取message_en.properties,里面內容myKey = hello, world.
2 static {
3 try {
4 ResourceBundle bundle = ResourceBundle
5 .getBundle("messages", Locale.ENGLISH);
6 myName = bundle.getString("myKey").trim();
7 }
8 catch(Exception ex) {
9 System.err.println( "[Property]:Can't Load property.properties");
10 myName = "default name";
11
12 System.out.println( "myName will use the default value: " + myName);
13
14 }
15 }
有關ResourceBundle類的更多用法,參考這個網頁。
Servlet和Spring訪問資源文件
JSP和JSTL標簽底層都是通過Java的ResourceBundle類來獲取資源並設置參數的,而對於java web程序中Servlet訪問資源文件可以通過ServletContext類中getResourceAsStream方法,它是通過Servlet容器來獲得資源文件的,它使得Servlet程序可以訪問web應用程序內部的任意位置的文件。(非Servlet中用classLoader,jdk中ClassLoader類專門提供了getResource等方法去裝載資源文件,他們使用與查找Java類文件同樣的方式去查找原文件,即在類 裝載器所搜索的目錄中查找。為了防止外部使用瀏覽器訪問到資源文件,web應用程序中的資源文件通常應放到Web-INF目錄或其子目錄中。由於web應 用程序的類裝載器會搜索web-inf/classes目錄,所有ClassLoader.getResourceAsStream方法也可以訪問該目錄中的資源文件,但是,該方法不能訪問web應用程序內的其他目錄中的資源。)
2
3 java.util.Properties properties= new java.util.Properties();
4 properties.load(in); // 得到的是map集合
5
6 String url=properties.getProperty("url");
7 String username=properties.getProperty("username");
8 String password=properties.getProperty("password");
Servlet訪問資源文件要注意相對路徑、絕對路徑,以及權限問題,具體查看這個頁面。
而Spring中的org.springframework.core.io.Resource接口代表着物理存在的任何資源,其繼承於org.springframework.core.io.InputStreamSource;其子類有如下幾 種:ByteArrayResource, ClassPathResource, DescriptiveResource, FileSystemResource, InputStreamResource, PortletContextResource, ServletContextResource, UrlResource 。常見的有下面四種:
- ClassPathResource:通過 ClassPathResource 以類路徑的方式進行訪問;
- FileSystemResource:通過 FileSystemResource 以文件系統絕對路徑的方式進行訪問;
- ServletContextResource:通過 ServletContextResource以相對於Web應用根目錄的方式進行訪問。例如:Resource resource = new ServletContextResource(servletContext, "/path/to/file"); File resourceFile = resource.getFile();
- UrlResource :通過java.net.URL來訪問資源,當然它也支持File格式,如“file:”
Servlet和多語言
HttpServletRequest支持一些接口來獲得請求客戶端瀏覽器的地區、語言和國家(IE-Internet Options設置-General-Language可以設置你的瀏覽器語言),然后HttpServletResponse通過設置http頭的"Content-Language"來動態設置客戶端語言。
2 import javax.servlet.*;
3 import javax.servlet.http.*;
4 import java.util.Locale;
5
6 public class DisplaySpanish extends HttpServlet{
7
8 public void doGet(HttpServletRequest request,
9 HttpServletResponse response)
10 throws ServletException, IOException
11 {
12 // Get the client's Locale
13 Locale locale = request.getLocale();
14 String language = locale.getLanguage();
15 String country = locale.getCountry();
16
17 // Set response content type
18 response.setContentType("text/html");
19 PrintWriter out = response.getWriter();
20 // Set spanish language code.
21 response.setHeader("Content-Language", "es");
22
23 String title = "En Español";
24 String docType =
25 "<!doctype html public \"-//w3c//dtd html 4.0 " +
26 "transitional//en\">\n";
27 out.println(docType +
28 "<html>\n" +
29 "<head><title>" + title + "</title></head>\n" +
30 "<body bgcolor=\"#f0f0f0\">\n" +
31 "<h1>" + "En Español:" + "</h1>\n" +
32 "<h1>" + "¡Hola Mundo!" + "</h1>\n" +
33 "</body></html>");
34 }
35 }
Spring MVC+Freemarker+jQuery/js的多語言實現
上面說了那么多廢話,現在進入正題Spring MVC+Freemarker+jQuery/js的多語言實現,最終我們要實現的是:
- 根據客戶端瀏覽器語言顯示相應的語言
- 用戶可以動態切換語言,保存在Cookie中,下次自動使用該語言
- 靜態html、jsp頁面多語言、controller里面能獲得多語言、freemarker多語言、jquery/js的多語言
首先說一下Spring MVC對多語言的支持可以看Spring官方網頁,但我們用了Freemarker,這些有些麻煩。另外,Spring MVC提供了下面幾種方式來支持多語言:
- 用param的方式:url?lang=de,就是URL的方式,然后用攔截器LocaleChangeInterceptor: 來實現動態多語言。 本文沒有采用這種方式,不喜歡改變URL,一切應該是在背后默默進行的。
- 用Session方式來存儲用戶動態選擇的語言:session過期就沒有了,所以本文沒有采用這種方式。
- 用Cookie的方式來存儲用戶動態選擇的語言:本文采用這種方式,session過期了下次登錄還是能記住,但同一機器多個用戶需要區分。
具體實現:
1. 在Spring-servlet.xml(具體看自己項目中的命名)加入:
2 < property name ="basenames" >
3 < list >
4 < value >resources/messages </ value >
5 </ list >
6 </ property >
7 </bean>
2. 添加資源文件messages.properties, messages_en_US.properties, messages_zh_CN.properties,注意路徑和上面配置的一致,在classpath的resources目錄下:
3. 針對Freemarker的,首先Spring jar包反解出Spring.ftl,然后拷貝到你的ftl目錄。這樣比較變態,但這樣就能在ftl文件中使用宏來獲得需要的message了。
4. 在Spring-servlet.xml(具體看自己項目中的命名)加入設置:Freemarker自動導入Spring.ftl宏。不用在每個ftl里面定義這個宏。
2 class ="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer" >
3 < property name ="freemarkerSettings" >
4 < props >
5 < prop key ="auto_import" >localization/spring.ftl as spring </ prop >
6 </ props >
7 </ property >
8 </bean>
如下圖:
5. 在freemarker中使用:<@spring.message "label.menu"/>
6. 如果不能自動獲取宏,需要ftl中加入: <#import "/WEB-INF/views/localization/spring.ftl" as spring/>
7. 在Spring-servlet.xml(具體看自己項目中的命名)加入設置:
2 < property name ="cookieName" value ="clientlanguage" />
3 < property name ="cookieMaxAge" value ="94608000" />
4 < property name ="defaultLocale" value ="en" />
5 </bean>
8. 界面上加個按鈕動態切換語言:
|
<span><a href="javascript:void(0)" onclick="changeLanguage('fr')">FR</a></span>
9. 對於的動態切換語言的js:
2 {
3 $.ajax({
4 type: "POST",
5 url: base + "ajax/changelanguage.do",
6 data: "new_lang="+language,
7 dataType:"json",
8 async: true,
9 error: function(data, error) {alert("change lang error!");},
10 success: function(data)
11 {
12 window.location.reload();
13 }
14 });
15 }
10. Spring MVC后台對於的controller函數實現動態切換語言:(Spring會自動保存到上面配置的cookie。)
2 public ModelAndView changeLanguage(@RequestParam String new_lang, HttpServletResponse response)
3 {
4 String msg = "";
5
6 try
7 {
8 LocaleResolver localeResolver = RequestContextUtils.getLocaleResolver( this.getRequest());
9 if (localeResolver == null) {
10 throw new IllegalStateException("No LocaleResolver found: not in a DispatcherServlet request?");
11 }
12
13 LocaleEditor localeEditor = new LocaleEditor();
14 localeEditor.setAsText(new_lang);
15 localeResolver.setLocale(getRequest(), response, (Locale)localeEditor.getValue());
16
17 msg = "Change Language Success!";
18 }
19 catch(Exception ex)
20 {
21 msg = "error";
22 }
23 return new ModelAndView("jsonView", "json", msg);
24 }
11. 在其他controller里面如何獲取當前語言並根據語言獲取相應的文字? 首先在基類 baseController中加入:
protected MessageSource messageSource;
然后在繼承的controller里面加入參數:Locale locale ,如下圖:
然后就可以用messageSource 自動根據當前語言獲取文字:messageSource.getMessage("myKey", null, locale)
12. 這里說一下,freemarker可能會報錯:Template Spring.ftl not found! 但事實上你的Spring.ftl是存在的,為何freemarer會找不到。這是因為freemarker尋找模版是根據配置的位置,templateLoaderPath,所以你配置的Spring.ftl路徑必須是相對templateLoaderPath的相對路徑。比如:
13. Javascript/jQuery/js的多語言問題:界面輸入的validation往往會用js實現,而獨立的js文件里面輸出的validation message自然也要多語言。這里js的多語言問題有兩種實現方法:1. 從后台讀取資源,然后存入js數組,而且后台讀取是異步的, 這個會有性能問題。有興趣的朋友可以去實現以下,有一些插件可以實現js讀取后台i18n的java資源:參考1(jQuery i18n plugin),參考2(i18next),參考3(jawr). 2. 在js validation message放入單獨的多個js文件,比如:validation_message.js和validation_message_fr_FR.js。本文采用的第二種方法。這種方法性能好,清晰,簡單,因為validation messages可以拆開,而FCKEditor也是用這個方法。
首先增加validation.strings.js 和 validation.strings.fr.js(一個英語,一個發育),內容:
my_key1: "hello",
my_key2: "world"
};
在js中可以這樣用:messageStrings.my_key1.
然后是關鍵一步:根據cookie的語言,動態加載相應的js文件:
為了性能考慮,在頁面最后加載,就是</body> 的前面,加入以下的js:
var base = "${base}/"; // freemarker use
var lang = getCookie("clientlanguage");
var script=document.createElement('script');
script.setAttribute("type","text/javascript");
if(lang == null){
script.setAttribute("src", base+"/resources/js/validation.strings.js");
} else{
script.setAttribute("src", base+"/resources/js/validation.strings."+lang+".js");
}
function getCookie(name){
var arr = document.cookie.match( new RegExp("(^| )"+name+"=([^;]*)(;|$)"));
if(arr != null) { return unescape(arr[2]);} else{ return null; }
}
</script> (注:上面少了一行:document.body.appendChild(script);))
如果語言有參數例如:mykey = please input {0},可以寫個js函數來設置:
* var str0 = "{0} must smaller than {1}"
* var str1 = str0.fillArgs("apple", "watermelon");
* srt1 equals to "apple must smaller than watermelon"
*/
String.prototype.fillArgs = function()
{
var formated = this;
for ( var i=0;i<arguments.length;i++)
{
var param = "\{"+i+"\}";
formated = formated.replace(param,arguments[i])
}
return formated;
}
jFreeReport的多語言國際化
< property name ="org.jfree.report.modules.gui.csv.Enable" >false </ property >
< property name ="org.jfree.report.modules.gui.html.Enable" >false </ property >
< property name ="org.jfree.report.modules.gui.xls.Enable" >false </ property >
< property name ="org.jfree.report.modules.gui.rtf.Enable" >false </ property >
< property name ="org.jfree.report.modules.gui.plaintext.Enable" >false </ property >
< property name ="org.jfree.report.ResourceBundle" >temp.messages </ property >
</ configuration >
ResourceBundle類有一點注意,給的key注意路徑:比如你的文件clasPath路徑是/temp/messages_cn.properties ,那么這個key應該是“temp.messages”。
然后在其它的各個report的xml里面進行如下配置獲取鍵值:
在reportconfig.xml里面配置了ResourceBundle固然很好,但是如何把用戶當前的語言傳給jFreeReport呢?這個就需要自定義ResourceBundleFactory。先寫一個ResourceBundleFactory類 :
{
Locale locale;
public bundleFactory(Locale locale)
{
this.locale=locale;
}
@Override
public ResourceBundle getResourceBundle(String key) {
return ResourceBundle.getBundle(key, locale);
}
@Override
public Locale getLocale() {
return locale;
}
}
這個ResourceBundleFactory類可以根據傳進去的Locale來獲取語言資源,locale從httpRequest可以獲取。最后,在jFreeReport中調用這個ResourceBundleFactory:
report.setResourceBundleFactory( new bundleFactory(locale /* get from http_request */));
report.setProperty("titleString2", messageSource.getMessage("Reports.History.Pdf.title2", null, locale));
上面那個設置屬性 titleString是因為reportConfig.xml 里面有用到:
總之,jFreeReport的國際化/本地化/多語言實現還是比較繁瑣的,但總體還是支持的不錯。
jqGrid的多語言/國際化/i18n實現
有時候會顯示"No records to view",或者"Page 1 of 5",這些文字是需要國際化的。還好jqGrid支持他們。只要動態引用jqGrid/js/i18n/grid.locale-en.js,或者jqGrid/js/i18n/grid.locale-fr.js。具體動態引用的js:
var base = "/";
function getCookie(name){
var arr = document.cookie.match( new RegExp("(^| )"+name+"=([^;]*)(;|$)"));
if(arr != null) { return unescape(arr[2]);} else{ return null; }
}
var curlang = getCookie("clientlanguage");
var script=document.createElement('script');
script.setAttribute("type","text/javascript");
if(curlang == null || curlang == "en"){
script.setAttribute("src", base+"resources/js/jqGrid/js/i18n/grid.locale-en.js");
} else{
script.setAttribute("src", base+"resources/js/jqGrid/js/i18n/grid.locale-"+curlang+".js");
}
document.getElementsByTagName('head')[0].appendChild(script);
</script>
把這段js放到html的head部分,就是動態根據當前的cookie設置的語言來加載響應的js,會在</head>前面動態引用js,注意是document.head.appendChild (document.getElementsByTagName('head')[0])哦,不是document.body.appendChild。 效果:
注意:動態加載和動態切換jqGrid的語言用上面的代碼會出現各個瀏覽器的兼容性問題,原因是jqGrid/js/i18n/grid.locale-xx.js必須在引用jgGrid之前引用,而動態引用js的執行在各個瀏覽器(IE、Chrome、Firefox、Opera、Safari)里面順序不一樣,有的是立即同步執行,有的是異步執行,導致jqGrid使用的時候報異常。當然你可以用下面的帶OnLoadComple callback的方法來強制同步動態加載引用外部js,保證它們是順序加載引用和執行的:
var script = document.createElement("script")
script.type = "text/javascript";
if (script.readyState){ // IE
script.onreadystatechange = function(){
if (script.readyState == "loaded" ||
script.readyState == "complete"){
script.onreadystatechange = null;
callback();
}
};
} else { // Others
script.onload = function(){
callback();
};
}
script.src = url;
if(head) document.getElementsByTagName("head")[0].appendChild(script);
else document.body.appendChild(script);
}
但這樣也會出現其他的問題,比如你在局部的ftl/html里面有內嵌的js:$("#abc").jqGrid.....這些是立即執行的,而上面的動態加載引用的js可能還沒有加載完畢....這樣的問題在各個瀏覽器兼容上面有問題。最終實現jqGrid多語言/國際化/本地化/i18n並能實時動態切換語言的解決方案是擴展jqgrid,具體看這個老外的帖子,demo在這兒,需要改造grid.locale-XX.js,注意不是引用的官方的jqGrid。
jQuery.UI.DatePicker的多語言/國際化/i18n實現
jQuery.UI有一個DatePicker控件,也是需要多語言和國際化的。因為上面的文字比如月份:
首先去官網下載語言對應的datePicker文件,例如:jquery.ui.datepicker-fr.js就是法語的。然后在html的body后面動態加載響應的js即可,把下面的js調用一下,放在</body>前面就行了,會在</body>前面動態引用js。
function handleDatepickerI18n(){
var base="/";
var lang = getCookie("clientlanguage");
if(lang == null || lang == "en") return;
var script=document.createElement('script');
script.setAttribute("type","text/javascript");
script.setAttribute("src", base+"resources/js/UI/i18n/jquery.ui.datepicker-"+lang+".js");
document.body.appendChild(script);
}
</script>
Spring mvc的主題Theme實現
Spring MVC對Theme主題的支持可以參考這個Spring官方網頁,同上面的多語言類似,Spring MVC也提供了多種方式,比如url的param和攔截器、session和cookie等,這里我們還是用cookie的方式來實現。
1. 在Spring-servlet.xml(具體看自己項目中的命名)加入設置:
2 < bean id ="themeSource"
3 class ="org.springframework.ui.context.support.ResourceBundleThemeSource" >
4 < property name ="basenamePrefix" value ="resources/theme-" />
5 </ bean >
6
7 < bean id ="themeResolver" class ="org.springframework.web.servlet.theme.CookieThemeResolver" >
8 < property name ="cookieName" value ="clienttheme" />
9 < property name ="cookieMaxAge" value ="94608000" />
10 < property name ="defaultThemeName" value ="default" />
11 </bean>
2. 在src/resources 添加兩個文件: theme-default.properties, theme-blue.properties
theme-blue.properties內容: css=themes/blue.css
3. 添加themes目錄,添加兩個文件:default.css, blue.css
default.css內容:
4. 界面上加一個按鈕動態切換主題:
|
<span><a href="javascript:void(0)" onclick="changeTheme('blue')">blue</a></span>
5. 按鈕對應的js:
{
$.ajax({
type: "POST",
url: base + "ajax/changetheme.do",
data: "new_theme="+theme,
dataType:"json",
async: true,
error: function(data, error) {alert("change theme error!");},
success: function(data)
{
window.location.reload();
}
});
}
6. Spring MVC對應的后台controller函數實現動態切換主題:
public ModelAndView umChangeTheme(@RequestParam String new_theme, HttpServletResponse response)
{
String msg = "";
try
{ ThemeResolver themeResolver = RequestContextUtils.getThemeResolver( this.getRequest());
if (themeResolver == null) {
throw new IllegalStateException("No themeResolver found: not in a DispatcherServlet request?");
}
themeResolver.setThemeName(getRequest(), response, new_theme);
msg = "change Theme Success!";
}
catch(Exception ex)
{
msg = "error";
}
return new ModelAndView("jsonView", "json", msg);
}
7. 在頁面中根據cookie動態加載主題相應的css(注意放在html開頭來動態加載css) :
var base = "${base}/"; // freemarker use
loadTheme();
function loadTheme(){
var theme = getCookie("clienttheme");
var head = document.getElementsByTagName('HEAD').item(0);
var style=document.createElement('link');
style.rel = 'stylesheet';
style.type = 'text/css';
if(theme == null){
style.href = base+"/themes/default.css";
} else{
style.href = base+"/themes/"+theme+".css";
}
head.appendChild(style);
}
function getCookie(name){
var arr = document.cookie.match( new RegExp("(^| )"+name+"=([^;]*)(;|$)"));
if(arr != null) { return unescape(arr[2]);} else{ return null; }
}
</script>
8. 如果你項目中用到其它jQuery插件,比如jQuery UI,比如jqGrid,要注意配合jqGrid的主題來動態切換。首先從http://jqueryui.com/themeroller/下載需要的jqGrid皮膚(你也可以創建自己的Theme這個jqGrid也支持的,事實上jqGrid用的是jQuery UI的ThemeRoller),然后在html中飲用相應的css即可(參考)。或者用js實現動態加載css(參加文中的代碼實現動態加載css)。
9. 除了動態加載css,還可以在html或ftl中寫動態css,更加方便。注意一點,在Freemarker中不能用<spring:theme code="abc"這樣的語法,要這樣用:
< @spring .theme "stylesheet_main" />
</ #assign >
< link href ="${base}/${maincss}?v=31" rel ="stylesheet" type ="text/css" />
注意這個stylesheet_main是在theme_xx.properties定義的:stylesheet_main=resources/css/theme/default/main.css
如果你運行上面的代碼報錯:Expression spring is undefined on .....,那么就是因為你沒有引用...spring.ftl,參考本文上面的多語言的部分如何import這個spring.ftl吧。
總結
本文講述了java web的多語言國際化實現和主題(Theme)的實現,具體來說是Spring MVC+Freemarker+Javascript的多語言(國際化i18n/本地化)和主題(Theme)實現。總結一下,不比純粹的Java Servlet國際化,由於我們用到多個框架(Spring MVC, Freemarker, jQuery, jqGrid....),所以實現多語言起來難度加大,具體來說就是要整合Spring MVC的國際化和Freemarker,這個會遇到很多問題。而且多語言散布在html、jsp、freemarker/*.frl、controller/Spring MVC、和javascript/js中,所以我們需要的是一個整體解決方案,本文供有用到的朋友參考。如果使用上有問題,可以直接在下面留言。