Scala大好きっ子たち、今日も元気にPlayしてますか?Scala初心者のTamaです。Play Framework2.X書きやすいし速いしいいですよね!ただ、レールからちょっと外れようとすると取っ掛かりが見つけにくいのも最近の流行に乗ってるというか…。
今日は、Typetalkの新機能の開発中に必要があって実装したちょっとした工夫を2つほど紹介します。
言語リソースファイルをディレクトリ分けしたい(メッセージリソースの分割)
Playの言語リソースファイルはたとえばデフォルト+日本語の2言語ならconf
ディレクトリ以下に
- messages
- messages_ja
というファイルを用意して、
(implicit messages:Messages)
val msg = Messages("delete.message")
と書けばいいのですよね。でもたくさんのパートを複数人で同時に開発している場合、このmessages
ファイルにどんどん新しい行を追加していくことになり、gitでコンフリクトしたり、別の場所でキーがかぶったりして、
- Aさん
- 「ちょっと、”delete.message”は”削除しました”でもう使ってるんですけど!」
- 僕
- 「いやそれなら”deleted.message”にすべきだと思うな。ここは”削除しますがよろしいですか?”のほうが相応しい(キリッ)」
- Aさん
- 「なにその俺ルール!キモい!ていうかそっちが”delete.confirmation”とかすればいいじゃん!」
- 僕
- 「キモぇ※◎△□%$!!」
- Bさん
- 「まあまあ、間をとって”delete.msg”で」
- 全員
- 「「それはない」」
みたいな不毛な争いが勃発しがちですよね。(しない)
パートごとに決まった名前をキーのプレフィックスにするルールでもいいのですが、キーがやたら長くなるし、とてもDRYじゃなくて嫌な感じです。かといっていちいちパートごとにサブプロジェクトにするのも面倒だし。
複数人の開発者でうまく別々のパートを開発するためには
- conf/featureA/
- conf/featureB/
- :
のように、機能ごとのディレクトリを分けて、それぞれにmessages
やmessages_ja
を配置できれば幸せになれそうだ、そういう仕組が多分あるだろうと思って探したのですが、マニュアルにはそういうのは無かったので、Messages
クラスのコードを調べてこんなのを作りました。
import play.api._
import play.api.i18n._
import play.api.Play.current
import play.utils.Resources
/**
* サブディレクトリからリソースを読み込むMessages
*/
class SubMessages(dirName:String) {
/**
* Messages()と同じ使い方。
*/
def apply(key:String, params:Any*)(implicit messages:Messages):String = {
implicit val lang = messages.lang
messageApi(key, params:_*)
}
/**
* Messages()と同じ使い方。
*/
def apply(keys:Seq[String], params:Any*)(implicit messages:Messages):String = {
implicit val lang = messages.lang
messageApi(keys, params:_*)
}
/**
* MessagesApi実装インスタンス
*/
private lazy val messageApi:MessagesApi = {
val config = current.configuration
val env = new Environment(new java.io.File("."), getClass.getClassLoader, current.mode)
val langs = new DefaultLangs(config)
new DefaultMessagesApi(env, config, langs) {
/**
* リソースファイルをロードするパスをサブディレクトリにするだけ。
*/
override protected def loadMessages(file: String): Map[String, String] = {
import scala.collection.JavaConverters._
env.classLoader.getResources(dirName + "/" + file).asScala.toList
.filterNot(url => Resources.isDirectory(env.classLoader, url)).reverse
.map { messageFile =>
Messages.parse(Messages.UrlMessageSource(messageFile), messageFile.toString).fold(e => throw e, identity)
}.foldLeft(Map.empty[String, String]) { _ ++ _ }
}
}
}
}
いろいろ頑張ってたくさんコード書いた気がするのですが、結局たったこれだけになりました。DefaultMessagesApi
のloadMessages
メソッドをちょっぴりオーバーライドした無名クラスのインスタンスを生成してるだけです。
コンストラクタ引数でconf
ディレクトリからの相対パスを指定して使います。下のようにシングルトンインスタンスに持たせておくのがいいでしょう。
object FeatureA {
val SubMessages = SubMessages("featureA")
:
:
}
使うところではMessages
の代わりにSubMessages
を使います。implicitのMessages
と同じ言語を参照するので特に気をつけることもありません。
(implicit messages:Messages)
val msg = FeatureA.SubMessages("welcome.message")
もちろん、他のディレクトリのmessages
ファイルとキーがかぶっても全く問題ないので、そのパートのスコープ内で好きなキーを使って開発できます。
checkboxを複数表示して複数選択リストを作りたい(Helperでちょっと工夫)
PlayのFormとHelperの組み合わせは、他のフレームワークと比べてちょっと機能が少ない気がします。とりわけ困ったことに、Field
クラスのvalue
変数がString
型のため、そもそも複数の値をHelperに渡せないという問題が。bindFromRequest
メソッドではちゃんとリクエストから複数の値を取り出せてるのに、惜しいなあ。
selectタグの複数選択とかcheckbox複数個使った複数選択が最初から用意されていないことに驚きましたが、まあ無いものは無いので仕方ないです。
Field
クラスで複数の値を持てるようにとか言い出すと大工事になっちゃうので、ある程度妥協して大変になり過ぎないようにサクッと作ります。
Helperを自作するのはTwirテンプレートを作るだけなのでとても簡単です。FieldConstructor
も自作すると結構いろんなことできて便利ですね。views
以下適当なディレクトリで、checkboxes.scala.html
を以下のように作成します。
@import helper._
@(field:Field, options:Seq[(String, Any)], selected:Option[Seq[String]], args: (Symbol,Any)*)(implicit handler: FieldConstructor, messages: play.api.i18n.Messages)
@import play.api.i18n.Messages.Implicits.applicationMessages
@input(field, args:_*) { (id, name, value, htmlArgs) =>
@options.map { v =>
}
}
他の@input()を使うHelperの場合は現在の値をfield.value
で取得できるのですが、複数の値が入るようにFormを宣言している場合はfield.value
はnullになっちゃいます。仕方ないので現在の値をselected
引数でもう一回受け取る仕様にせざるを得ません。
あとはいかにして楽にselected引数を作るかですが、試行錯誤の結果、呼び出し側はこんな感じに落ち着きました。
@checkboxes(form("events"), someSelectionOptions, form.value.map(_.events), '_label -> "Event to Notify")
このぐらい(form.value.map(_.events)
)なら、まあ許容できるんじゃないでしょうか。
以上、ちょっとした工夫でPlayのかゆいところに手が届いてよかったよねというお話でした。
ヌーラボでは始めたばかりの言語でだって無いものは作ってやる!というエンジニアを募集しています。