こんばんは。この記事は Spring Framework Advent Calendar 2012の??日目です。参加者少なくて途切れてしまってるみたいますが ^^; 遅ればせながら参戦。
さて、11月にJJUG CCC 2012 Fall 日本Javaユーザーグループっていうイベントがありました。twitter4jというライブラリを作っている山本さんの講演で、ライブラリの配布や使い方のマニュアルなどを載せているWebサイトを、日本語、英語両方で作っているという話がありました。 自作のタグライブラリやjspでちょっとしたCMS(コンテンツマネジメントシステム)を自作しているそうです。
それにからんで、twitterでこんなやりとりをさせていただきました。
マルチリンガルのための自作CMSウケるw。mixer2.org 使っていただくと<span class="en">english</span> <span class="ja">日本語</span>みたいなことも作れまっせ。#jjug_b11
— nabedgeさん (@nabedge) 11月 10, 2012
.@nabedge いいですね!permlinkは変えられますか?
— 山本裕介さん (@yusuke) 11月 10, 2012
@yusuke <a href="foo-ja.html">foo</a> を <a href="foo-en.html">foo</a> に自動置換する、みたいな意味でしょうか?可能ですよ。そのうちブログにでも書こうかな。「mixer2によるお手軽i18nサイト構築」みたいなw
— nabedgeさん (@nabedge) 11月 10, 2012
@nabedge いや、twitter4j.org/en/index.html だと英語を、twitter4j.org/ja/index.html だと日本語を出すっていうことです。あとtwitter4j.orgではtaglibを使ってるんですよねー
— 山本裕介さん (@yusuke) 11月 10, 2012
実際、ドキュメントを日本語と英語の両方で書かなければならない場面は結構あって、mixer2もいちおう英語と日本語の両方で最低限の情報を書いています。
mavenに限らず、Java言語の世界では、hogehoge.propertiesには英語、hogehoge_ja.propertiesには日本語で書いておくと、JavaのLocaleという標準的な多言語対応(i18n)が使えるようになりますよ、みたいな実装が普通です。
ところが、案外これが面倒です。mixer2の場合はmavenのsite機能を使っていますが、たとえば英語のページのソースと日本語のページのソースを、別々のファイルに書かなければなりません。 結果、日本語には書いてあるけど英語のほうで翻訳漏れ!といったことが起きます。やったことある人ならわかると思いますが、翻訳の場合、同じファイルに英語と日本語を交互に書くようにすると結構便利です。
山本さんもそこを目指して、同じjsp(拡張子はhtmlにしてるみたいですが)に日本語と英語の両方を特殊なカスタムタグで書いているのでしょう。(実際そんなスライドが講演資料にありました)
そこで、mixer2とSpringMVCを使って、同じリソースに英語と日本語を交互に書けるが、結果は英語サイトと、日本語サイトのように別々に表示できる簡易CMSを作ってみました。
サンプルの説明に入る前に先にお詫びです。山本さんが言っていた「カスタムタグもまだ使い続けたい」という要望には下記のサンプルでは対応できていません。html全体をmxier2で出力する方式をとったためです。ただ、mixer2は部分マーシャル機能もありますので、画面の一部をmixer2に作らせて、それ以外を普通のjspとカスタムタグで作る、みたいなことで対応可能でしょう。
さて、サンプルのソースはここです。mixer2-sampleというリポジトリにまとめて入っています。
作業環境はmavenが動けばどんなIDEでもかまいませんが、ここでは「 SpringToolSuiteでSAStrutsとSpringMVCのHelloWorldを作るまで」で紹介したSTSを想定しています。
先日の「nabedge blog: mixer2でJSPレスなSpringMVCアプリケーションを作ってみた」と同じ方法でmixer2-multilingual-websiteプロジェクトをIDE上に持ちこんでください。そのままTomcat等で起動してみましょう。
シャーロックホームズの原著と日本語訳を載せるサイトです。適当にクリックしてみるとわかるとおり、en/index.htmlでは英語が、ja/index.htmlでは日本語が表示されます。ところが、index.htmlファイルはソースツリー上に一つしかありません。ここがポイントです。
index.html(トップページのテンプレート)はこんな感じです。
Sherlock Holmes
シャーロック ホームズ
Sherlock Holmes is a fictional detective created by author and physician Sir Arthur Conan Doyle.
シャーロック・ホームズ(Sherlock Holmes)は、アーサー・コナン・ドイルの推理小説 に登場する架空の探偵。
日本語のタグにはlang-jaというclass属性を、英語のタグにはlang-enをあてています。SpringMVCのコントローラクラスがmixer2実行エンジンを通じてこのテンプレートをロードし、URLの先頭が「en」の場合はlang-ja属性を持つタグを全て削除、「ja」の場合はその逆をやって、出力させるという方法です。
全てのhttpリクエストはPageControllerクラスが処理するようにします。 下の @RequestMapping(value = "/{langStr}/**/*.html") によって、この形式に当てはまるurlへのリクエストは全てこのメソッドに集まります。
@RequestMapping(value = "/{langStr}/**/*.html") public ModelAndView show(@PathVariable String langStr, HttpServletRequest request) throws TagTypeUnmatchException, IOException { logger.debug("# request processing..."); ModelAndView modelAndView = new ModelAndView(); // set Lang Lang lang; try { lang = Lang.valueOf(langStr.toUpperCase()); } catch (IllegalArgumentException e) { // set "en" if langStr unmatch Lang enum. modelAndView.setViewName("redirect:/" + defaultLang.toString().toLowerCase() + "/index.html"); return modelAndView; } logger.debug("# lang = " + lang.toString()); // build template file path from URI String path = StringUtils.substringAfter(request.getRequestURI(), request.getContextPath() + "/" + lang.toString().toLowerCase()); logger.debug("# path = " + path); String templatePath = "classpath:m2mockup/m2template" + path; logger.debug("# templatePath = " + templatePath); // load template Html html = mixer2Engine.loadHtmlTemplate(ResourceUtils .getFile(templatePath)); // replace side menu (except for index.html) if (!templatePath.equals(indexTemplatePath)) { Html indexHtml = mixer2Engine.loadHtmlTemplate(ResourceUtils .getFile(indexTemplatePath)); Div sideBarDiv = indexHtml.getById("sidebar", Div.class); html.replaceById("sidebar", sideBarDiv); } // remove other language tags PageHelper.removeOtherLangTags(html, lang); // replace anchor to top page for (A a : html.getDescendants("topPageAnchor", A.class)) { a.setHref(request.getContextPath() + "/"); } // remake lang list at page top PageHelper.remakeLangList(html, lang, path); // replace static file path M2staticHelper.replaceM2staticPath(html); modelAndView.setViewName("mixer2view"); modelAndView.addObject("htmlString", mixer2Engine.saveToString(html)); return modelAndView; }
たとえば、/en/foo/bar.htmlというURLへのリクエストから、まず言語が英語であることを認識します。次にclasspath:m2mockup/m2template/foo/bar.html というテンプレートファイルを探してロードします。 PageHelper.removeOtherLangTags(html, lang); によって、英語以外のタグを全て削除しています。
PageHelperクラスの説明をする前に、Langというenum型クラスについて説明します。
public enum Lang { EN("English") ,JA("日本語") ; private String name; private Lang(String name){ this.setName(name); } public String getName() { return name; } public void setName(String name) { this.name = name; } }
コンストラクタつきのenumクラスです。HashMapよりも使い勝手がいいです。
さて、PageHelperクラスはこんな感じです。
public class PageHelper { public static void removeOtherLangTags(Html html, Lang lang) { for (Lang l : Lang.values()) { if (l.equals(lang)) { continue; } String target = "lang-" + l.toString().toLowerCase(); html.removeDescendants(target); } }
html.removeDescendants(target); がポイントです。要するに、指定されたLang型引数以外の言語がclass属性で指定されているタグを、削除しています。要するに en/foo/bar.html であればLang型変数に英語が指定され、その情報をもとにhtml.removeDescendants("lang-ja") しているわけです。こうすることで、表示したい言語以外の言語で書かれたhtmlタグを全て削除しています。
最後にSpringMVCでのLocaleの使い方の話。このサンプルアプリでは、http://.../[contextPath]/ にアクセスすると、そのブラウザのlocaleにあわせて自動的に /en/index.html または /ja/index.html にリダイレクトされるようになっています。やり方は簡単です。
@RequestMapping(value = "/") public String top(Locale locale) { Lang lang = defaultLang; try { lang = Lang.valueOf(locale.getLanguage().toUpperCase()); } catch (IllegalArgumentException e) { return "redirect:/" + defaultLang.toString().toLowerCase() + "/index.html"; } String redirect = "redirect:/" + lang.toString().toLowerCase() + "/index.html"; logger.debug("# " + redirect); return redirect; }
@RequestMapping(value = "/") というアノテーションがついているメソッドが http://.../[contextPath]/ へのアクセスを処理します。SpringMVCの便利なところは、この処理メソッドの引数にいろいろなものを指定できるところです。 jaav.util.Locale型引数をあてればそこにはブラウザからのリクエストに含まれるロケール情報が自動的にはいります。あとはその言語情報をこっちのLang型変数にあてるだけです。
いかがでしたでしょうか。mixer2はhtml専用のテンプレートエンジンにすぎませんが、他のフレームワークと組み合わせることで結構便利に使えます。それではHappy hacking !