( ꒪⌓꒪) ゆるよろ日記

( ゚∀゚)o彡°オパーイ!オパーイ! ( ;゚皿゚)ノシΣ フィンギィィーーッ!!!

scalaのWebフレームワーク liftで遊ぶ(11) - 国際化のお話

目次はこちら。
scalaのWebフレームワーク liftで遊ぶ 目次 - ゆろよろ日記


を見ながら。
今回は国際化のお話。

Lift has full support for internationalization and localization of text in code and templates.

Based on the current locale lift can selects the appropriate template. You can also localize a subsection of a default template or in inline code.

Liftでは、コード中のテキストとテンプレートでの国際化とローカライズを完全にサポートします。
Liftはロケールに合わせて適切なテンプレートを選択します。 また、テンプレートかインラインコードにおけるデフォルトの一部分をローカライズすることができます。

てなことで、さっそく実践。

テンプレートの国際化

htmlテンプレートの一部をロケールに合わせて変換するには、<lift:loc>タグで囲むようにする。
このタグ内が、プロパティファイルの内容に応じて国際化される。

<lift:loc locid="speak">のように、locidを指定した場合は、locidをキーにしてプロパティファイルから検索される。
locidを指定しない場合は、タグのtextがキーになる。


まずは、国際化するhtmlをsrc/main/webapp/hello.htmlとして作成する。


src/main/webapp/hello.html

<lift:surround with="default" at="content">
	<h1><lift:loc>Hello</lift:loc></h1>
	<lift:loc locid="speak">I speak English</lift:loc>
</lift:surround>

作成したページはSiteMapに登録しておく。


src/main/scala/bootstrap/liftweb/Boot.scalaのbootメソッドで、/pages/helloへのリンクを追加した。

    val entries = Menu(Loc("Home", "/", "Home")) :: 
      Menu(Loc("Test", "/test", "Test Page"))  :: 
      Menu(Loc("Hello", "/pages/hello", "Hello page.")) ::
      User.sitemap

これをやらないと、http://localhost:8080/pages/helloにアクセスしても404だった。


次に、実際に出力される文字列をロケール毎にプロパティファイルとして作成する。
公式ではフランス語でやっていたが、せっかくなので日本語でやることにした。

src/main/resources以下に、jaとenの2つのプロパティファイルを作成する。
当然、native2asciiすべし。


src/main/resources/lift_ja.properties

Hello=こんにちは
speak=日本語でおk


こっちはnative2ascii後。

Hello=\u3053\u3093\u306b\u3061\u306f
speak=\u65e5\u672c\u8a9e\u3067\u304ak


src/main/resources/lift_en.properties

Hello=Hello
speak=I speak English


プロパティファイルを作ったら、クラスパス上に配置する必要がある。
eclipse上で作成して、src/main/resourcecの出力フォルダーを設定しても、なぜかtarget/classesにコピーされなかった。
以下のmvnコマンドで、プロパティファイルをクラスパス上に配置する。

mvn resources:resources


これで準備完了。jettyを起動して、http://localhost:8080/pages/helloにアクセスすると、ちゃんと日本語が出る。

scalaコード内の国際化

scalsaコード内の文字列を国際化したい場合は、S.?("locid")を使用すればよいようだ。
前回作ったFriendsクラスのshow_dateメソッドを修正して動作確認する。


Friends.scala

class Friends {
  def show = <span>Scala</span>
  
  def show_date  = {
    val now = new Date
    <span>{getDateInstance(LONG, Locale.JAPANESE) format now} {S.?("speak")}</span>
  }
}


http://localhost:8080/testで、プロパティファイルの内容で出力されていることが確認できる。

ロケールの判定

公式にドキュメントがなかったので、liftのソースを追ってみたよ。scalaわからんけど。


まず、現在のリクエストがどのロケールなのかは、S.localeメソッドで取得できる。
この中身は以下のようになっており、LiftRules.localeCalculatorに設定されている関数を呼び出すようになっているようだ。

  /**
  * Returns the Locale for this request based on the HTTP request's 
  * Accept-Language header. If that header corresponds to a Locale
  * that's installed on this JVM then return it, otherwise return the
  * default Locale for this JVM.
  */
  def locale: Locale = LiftRules.localeCalculator(request.map(_.request))


次に、LiftRules.localeCalculatorを見てみる。

  /**
  * A function that takes the current HTTP request and returns the current
  */
  var localeCalculator: Can[HttpServletRequest] => Locale = defaultLocaleCalculator _
  
  def defaultLocaleCalculator(request: Can[HttpServletRequest]) = request.flatMap(_.getLocale() match {case null => Empty case l: Locale => Full(l)}).openOr(Locale.getDefault())
  

var localeCalculatorは(Can[HttpServletRequest] => Locale)型の関数オブジェクトで、defaultLocaleCalculatorを初期値で設定している。

このlocaleCalculatorを任意の関数に差し替えることで、ロケールの決定方法をカスタマイズできるようだ。
このほかにも、TimeZoneの決定方法などもカスタマイズできるようだ。


デフォルトで設定されているdefaultLocaleCalculatorは、HttpRequestヘッダの"Accept-Language:"を元にロケールを決定している。
(HttpServletRequest#getLocale()を呼び出している。)
ロケールが決定できない場合は、JVMロケールを返すようになっている。


では、実際にQueryStringsからlocaleを設定するように、LiftRules.localeCalculatorを置き換えてみる。
たとえば、http://localhost:8080/test?locale=enとある場合は、ロケールがenとなる。


LiftRules.localeCalculatorの設定は、Bootクラスで行う。
src/main/scala/bootstrap/liftweb/Boot.scalaのbootメソッドに、以下のように追加した。


src/main/scala/bootstrap/liftweb/Boot.scalaのbootメソッド。

    LiftRules.localeCalculator = queryStringLocaleCalculator _
    LiftRules.localizationLookupFailureNotice =  Can.legacyNullTest(localizationFailureNoticeToConsole )


LiftRules.localeCalculatorにqueryStringLocaleCalculatorという関数を設定している。
また、LiftRules.localizationLookupFailureNoticeは、ローカライズに失敗した場合に呼び出される関数を設定できる。
ここでは、ローカライズに失敗した際に標準出力にメッセージを出す関数を設定している。


これら関数の実体も、Bootクラスに追加しておく。

  def queryStringLocaleCalculator(request: Can[HttpServletRequest]) :Locale= {
    var l = S.param("locale")
    if (!l.isEmpty ) 
      new Locale(l.open_! ) 
    else 
      LiftRules.defaultLocaleCalculator(request)
  }
  
  def localizationFailureNoticeToConsole (locid:String, locale:Locale) :Unit =
    Console.println("Localization Failed locid = " + locid + " locale = " + locale)


もう少しscalaっぽくスマートに書けると思うんだが、俺にはこれが限界です。


では、実際にhttp://localhost:8080/pages/hello?locale=enでアクセスしてみる。
日本語ではなく、lift_en.propertiesに書いてある内容が出力される。


さらに、Friendsクラスもちょっと修正して、ローカライズに失敗するように存在しないlocidでS.?を呼び出すようにしてみる。

  def show_date  = {
    val now = new Date
    <span>{getDateInstance(LONG, Locale.JAPANESE) format now} {S.locale}{S.?("hoge")}</span>
  }


http://localhost:8080/testにアクセスすると、LiftRules.localizationLookupFailureNoticeにより、標準出力に以下のように出力される。

Localization Failed locid = hoge locale = ja


これでカスタマイズ完了!

使ってみて

ロケールの決定方法や失敗したときの処理を任意の関数に決定できるというのは、scalaの関数指向を生かした方法だと思う。

コンセプトはRubygetTextに近いかな。
公式のドキュメントがもう少し整備されてくればなぁ。