Play Frameworkの国際化手法

1. プロローグ
2. Play Frameworkの国際化(Messages)
3. String.format的機能
4. Java Script(JS)にも対応
5. SBTにTSVからmessagesファイルを自動生成するスクリプトを組み込む

1. プロローグ

エンジニアの大宮です。
早いものでScalaで開発を始めて1年が経ちました(ブログを書くのも久しぶりになってしまいました)。

最近はサーバーサイドよりもクライアントサイド(JS, CSS)のメンテが多くなってきて、Scalaに触れる機会が減っていて寂しいです。
また、さすがに1年書いてるとちょっとやそっとの事では「おお、Scalaスゲー!!」となる機会は減ってきてしまいました。

そんな日々の中、久しぶりに感動した事があったので、今回はその事を書いてみます。ScalaというよりもPlay Frameworkですが、、、。

それはすなわち、アプリケーションの国際化です。

ENishiでも(海外経験豊富な社員がジョインした事で)海外を視野に入れた動きが「ドーン!!」と進んできており、「国際化といえば、アプリケーションの英語版をPrepareしないとね!!」という間にあれやこれやとENishi英語化の運びとなりました。

元々構想はあったのですが、まさかこんなに早く実装する事になろうとは、、、焦りを隠せない大宮。

2. Play Frameworkの国際化(Messages)

しかしそこは安心のPlay Framework。デフォルトでメッセージ機能がついています。英語と日本語でHTMLを分けるなんて事はせず、テンプレートは統一で、中のメッセージだけを変える仕組みです。

  • conf/messages
  • conf/messages.ja
  • conf/messages.en

のようなファイルを用意します。拡張子「ja」には日本語、「en」には英語、ない場合はデフォルトの文言が入ります。

ファイルの中身はこんな感じ。

toppage.title=ENishi
toppage.welcome=ENishiにようこそ!!<br/>ENishiは国内外の士業を対象としたSNSです。

左辺がKEY, 右辺がVALUEです。
Scala Templateから参照する時には下記のように書きます。

@import play.i18n._
@Messages.get(Lang.forCode("ja"), "toppage.welcome")

Lang.forCode()内の引数で、言語情報を指定します。
第一引数を省略した場合は、Playが自動でユーザーのHTTPリクエストヘッダーから言語情報を抜き出して、その言語情報を格納します。

便利じゃん!!と思われるかもしれませんが、Internet Explorerと一部のスマホ端末ではリクエストヘッダーに言語情報が入ってなかったりして、そういうブラウザでは常にデフォルトのメッセージ(conf/messagesに定義した値)が表示されてしまいます。

また、ENishiではユーザーが表示言語を選べるようにしようとしているので、下記のように、DBからとってきた言語値を突っ込むようにしています。

@(loginUser:User)
@import play.i18n._
@Messages.get(Lang.forCode(loginUser.language), "toppage.welcome")

あと、これだけではHTMLタグが適用されず、プレーンテキストとして表示されてしまうのでよろしくない。
それに、いちいち@Messages.get(Lang.forCode(loginUser.language), “toppage.welcome”)と書いていたら、テンプレートファイルが非常に見辛くなってしまう。

なのでテンプレートの冒頭で関数定義をしてしまいます。

@(loginUser:User)
@import play.i18n._
@msg(key:String)=@{Html(Messages.get(Lang.forCode(loginUser.language), key))}

これでテンプレ―ト内部では@msg(“toppage.welcome”)で参照できるようになりました。

3. String.format的機能

国際化のシーンでは、String.format機能が必要なケースが多く出てきます。
例を挙げるまでもないかもしれませんが、例。

日本:Userさんにリクエストメッセージを送りましょう
英語:Let's send a request message to Mr.User.

これ、テンプレートで無理やり表記するとこんなアホな感じになりそうです。

@if(loginUser.language=="en"){
@(msg("user.request")+loginUser.name)
}else{
@(loginUser.name+msg("user.request"))
}

全然スマートじゃないですね。。。

そこは痒い所に手が届くPlay Framework。String.format的な機能もバッチリ用意されています。

conf/messages.jauser.request={0}さんにリクエストメッセージを送りましょう
conf/messages.enuser.request=Let's send a request message to {0}.
views/index.scala.html@(loginUser:User)
@import play.i18n._
@Messages.get(Lang.forCode(loginUser.language), "user.request", List(user.name)))

これで、{0}の所にuser.nameの値が挿入されるようになります。
代入値は複数挿入する必要があるシーンもありますが、そういう時は、第三引数のListの値を増やし、messageファイル側の表記は{0}, {1}, {2}…としておけばOKです。

ちなみに、今までの例はViewからの呼び出しでしたが、ControllerやModelから呼び出すことも当然可能です。

4. Java Script(JS)にも対応

MVC上で使えるのは分かったよ、でもJSでは使えないんじゃないのか!?

そうなんです。クライアントサイドのJSからはサーバーサイドの関数は呼べません(そりゃそうだ)。と、いうことは、、、JSでメッセージを出す時はこんなコードにしないといけないのか!?

if($("#userLang").text()=="ja"){
alert("この項目は必須項目です。");
}else{
alert("This field is required field.");
}

うわああああああああ!!!!(発狂)

発狂中の大宮を救ってくれたのは、下記のライブラリ(play-jsmessages)でした。
https://github.com/julienrf/play-jsmessages/tree/master/sample-scala

↑お使いのPlayのバージョンにあわせて、Tagを切り替えてご使用ください。

簡単に言えばこのライブラリを使うと、メッセージファイルをJSの連想配列に変換したコードが生成されます。詳しくは上記のURLを参照してほしいのですが、生成されたコードを同一テンプレートにこんな感じで貼って、

<script type="text/javascript" src="@route.Application.JsMessages()"></script>
<script type="text/javascript" src="Assets.at("js/user.js")"></script>

user.js内部ではこんな感じに書けます。

var msg = Messages("ja", "request", option);
alert(msg);

これで通常のテンプレート内と同じように、メッセージの内容が使えます。
第三引数は省略も可能ですが、String.Format的な使い方をする時にはここに文字列を挿入します。

メッセージファイルは結構量が多いから、JSファイルが重くなるの嫌なんだけど、、、必要なメッセージだけ呼ぶ事って出来ないの?
出来ちゃいます。コントローラー内部で生成するメッセージをフィルタリングできます。詳細はリンク先をご覧くださいませ。

5. SBTにTSVからmessagesファイルを自動生成するスクリプトを組み込む

そうは言ってもさー、アプリケーション内のメッセージって大量にあるじゃん。変更管理とか、英語ファイルと日本語ファイルで抜け漏れ出るんじゃないの?

おっしゃる通りでございます。

ENishiでも当然メッセージを外部ファイル化したんですが、KEY-VALUEの数がなんと約1000個!!!
英語ファイルに追加した値を日本語ファイルに追加し忘れるなんて事態が容易に想像されます。
念のためデフォルトのmessageファイルも用意するので、変更管理するファイルは3個!!ちょっとしんどいですね。

TSVかCSVで、こんな感じにしたものから、ファイルを自動生成したいです。

KEY, JA-VALUE, EN-VALUE

CSVかTSVに出来ればExcelのフィルタ機能で抜け漏れも管理できるから、概ね問題は解決されるのではないでしょうか。

一応似たようなツールがないか探してみたんですが、なさそうだったので自作しました。playのコンパイルの際に、メッセージファイルを自動生成するスクリプトを組み込みます。

project/build.scalaobject ApplicationBuild extends Build {
val appName         = "enishi"
val appVersion      = "1.0-SNAPSHOT"
val appDependencies = Seq(
// Add your project dependencies here,
)
val main = play.Project(appName, appVersion, appDependencies).settings(
// コンパイルごとにメッセージ自動生成タスクを実行するよう登録
compile in Compile <<= (compile in Compile).dependsOn(Def.task {
for(ex <- List("", ".ja", ".en")){
val source = Source.fromFile("conf/msgmasta.tsv", "utf-8")
val mode = if(ex == ".en") 2 else 1
val file = new PrintWriter("conf/messages"+ex, "utf8")
for((line, j) <- source.getLines.zipWithIndex){
if(j!=0){generate(line, mode, file)}
}
file.close
}
})
)
// CSVからKEY, VALUEを抽出して書き込みを行う
private def generate(line : String, mode:Int, file:PrintWriter) : Unit = {
val list = line split '\t'
(line!="" && list.size>1 && !line.startsWith("#")) match{
case true => {
val key = dropQuote(list(0))
file.print("%s=%s\n".format(key, dropQuote(list(mode))))
}
case _ => file.print(dropQuote(line)+"\n")
}
}
// 前後のダブルクォーテーションを除去
private def dropQuote(str : String) : String = {
(str.startsWith("\"") && str.endsWith("\"")) match{
case true => (str drop 1 dropRight 1).replace("\"\"","\"")
case _ => str
}
}
}

これで国際化に関しての懸念事項は解消されました。

後は翻訳の仕事をお願いして、大宮は別の開発に移ります。
それにしても、Playは本当に使いやすいですね。

Scalaエンジニアの求人