Simple Pattern
/en/page
/ja/page
Optional Lang Path Variable Pattern
/page
/ja/page
Lang
を生成して view に渡す
def index = Action { implicit request =>
... // request2lang (trait Controller)
}
@()(implicit lang: Lang)
<h1>@Messages("my.message")</h1>
Messages.apply
が implicit lang: Lang
を取る
Lang
を渡さなくてもコンパイルが通る
(object Lang
に implicit lazy val defaultLang: Lang
が存在するため)
e.g. controller側で Lang
を渡し忘れる (implicit req
忘れ)
e.g. view側で Lang
を渡し忘れる ↓
@()(implicit lang: Lang)
<h1>@Messages("hello")</h1>
@partial()
partial.scala.html
@()
<h2>@Messages("world")</h2>
結果
<h1>こんにちは</h1>
<h2>World</h2>
trait MyLangSupport
or object MyImplicits
)
case class MyLang(val lang: Lang)
implicit def request2lang(implicit req: RequestHeader): MyLang = ...
package my.views
object ViewHelpers {
// (ここでrequestなどを使って優先する言語を変更したり等も簡単に出来る)
def i18n(key: String, args: Any*)(implicit myLang: MyLang): String =
messages(key, args: _*)(myLang.lang)
}
play.twirl.sbt.TwirlKeys.templateImports ++= Seq(
"my.views.ViewHelpers"
)
@()(implicit lang: MyLang)
@ViewHelpers.i18n("my.message")
MyLang
渡し忘れは コンパイルエラー
🙅 Messages
を必要とする箇所には別途対応が必要
views.html.helper...
play.api.data.validation.Constraints...
Messages
(not Lang
) を生成してviewに渡す
class Application @Inject() (val messagesApi: MessagesApi) extends Controller with I18nSupport {
def index = Action { implicit request =>
... // request2Messages (I18nSupport)
}
}
@()(implicit messages: Messages)
<h1>@Messages("my.message")</h1>
implicit def request2Messages(implicit request: RequestHeader): Messages =
messagesApi.preferred(request)
controller に implicit request
忘れ
view に implicit messages
忘れ
DefaultMessagesApi
リクエスト側の候補順を基に、サーバ側にマッチするものを探す
application.conf
に書かれたもの全て
langCookieName
に含まれる言語
Accept-Language
ヘッダ (前に書かれたものが優先)
application.conf
に書かれた最初のもの
Lang.defaultLang
java.util.Locale.getDefault
def setLang(result: Result, lang: Lang): Result
DefaultMessagesApi
def setLang(result: Result, lang: Lang) =
result.withCookies(Cookie(langCookieName, lang.code, path = Session.path, domain = Session.domain, secure = langCookieSecure, httpOnly = langCookieHttpOnly))
独自の MessageApi
をInjectさせると、先ほどの ViewHelper
の代替も可能
DefaultMessageApi
を継承して、override
させるのが簡単
Lang
を決定する事も可能
package my.components
@Singleton
class MyMessagesApi @Inject() (
environment: Environment,
configuration: Configuration,
langs: Langs
) extends DefaultMessagesApi(environment, configuration, langs)
with MyRequestToUserSupport {
override def preferred(request: RequestHeader) =
getUser(request).fold(super.preferred(request))(user => new Messages(user.lang, this))
)
(注: I18nSupport
を独自のものに変えた方が楽な事もある)
package modules
class MyI18nModule extends Module {
def bindings(environment: Environment, configuration: Configuration) = {
Seq(
bind[Langs].to[DefaultLangs],
bind[MessagesApi].to[MyMessagesApi] // instead of DefaultMyMessagesApi
)
}
}
play.modules.enabled += "modules.MyI18nModule"
play.modules.disabled += "play.api.i18n.I18nModule"
DefaultMessagesApi
が誤って呼ばれる事は無い, コントローラ側の変更が無い
class Application @Inject() (val messagesApi: MyMessagesApi) extends Controller with I18nSupport ...
🙅 MesagesApi
と書いたらDefaultMessagesApi
が呼ばれる
e.g. jodatime
を利用する際に、MessageFormat
の date
を利用する
java.text.MessageFormat
)
my.time=Time is {0,date}
package my.components
@Singleton
class MyMessagesApi ... {
override def preferred(request: RequestHeader) = ...
override def apply(key: String, args: Any*)(implicit lang: Lang): String = {
super.apply(key, args.map {
case localdate: org.joda.time.LocalDate => localdate.toDate
case arg => arg
}: _*)(lang)
}
)
/en/page
/ja/page
Lang
を利用
GET /$lang<[a-zA-Z]{2,3}>/page controllers.NoI18nSupportController.page(lang: Lang)
Langs
を Inject して langs.availables
のみ通す)
package components
object MyImplicits {
implicit def langPathBindable = new PathBindable[Lang] {
override def bind(key: String, value: String) = Lang.get(value) match {
case Some(lang) => Right(lang)
case None => Left("Invalid lang path value.")
}
override def unbind(key: String, value: Lang) = value.language
}
}
routesImport ++= Seq("components.MyImplicits._", "play.api.i18n.Lang")
/page
⇔ /ja/page
GET /page controllers.NoI18nSupportController.page(lang: Option[Lang] = None)
GET /$lang<[a-zA-Z]{2,3}>/page controllers.NoI18nSupportController.page(lang: Option[Lang])
package components
object MyImplicits {
implicit def langPathBindable = new PathBindable[Lang] {...}
// =====>> New!! <<=====
implicit def optionPathBindable[A: PathBindable] = new PathBindable[Option[A]] {
def bind(key: String, value: String): Either[String, Option[A]] =
implicitly[PathBindable[A]].bind(key, value).fold(
left => Left(left),
right => Right(Some(right))
)
def unbind(key: String, value: Option[A]): String =
value.map(_.toString).getOrElse("")
}
...
}
/contents/ja/...
⇔ /contents/en/...
# GET /contents/:lang/ controllers...
-> /contents routers.MyContentsRouter
package routers
class MyContentsRouter @Inject()(controller: ContentsController) extends SimpleRouter {
override def routes = Function.unlift {
case p"/$lang<[a-zA-Z]{2,3}>" =>
Lang.get(lang).map(controller.index)
case _ => None
}
}
ReverseRouter???
🙌 こころをこめて 🙌
package routers
// call like `@routers.ContentsReverseRouter.index` in views and so on
object ContentsReverseRouter {
private def instance: ReverseRouter = play.api.Play.current.injector.instanceOf[ReverseRouter]
def index(maybeLang: Option[Lang]) = instance.index(maybeLang)
}
@Singleton
class ReverseRouter @Inject() (routerProvider: Provider[Router]) {
private def router: Router = routerProvider.get
def index(maybeLang: Option[Lang]) = {
maybeLang.fold("/contents/")(lang => s"/contents/${lang.code}")
}
}
🙇 redundant definitions 🙇
GET / controllers.MyController.index(lang: Option[Lang] = None)
GET /$lang<[a-zA-Z]{2,3}>/ controllers.MyController.index(lang: Option[Lang])
GET /profile controllers.MyController.profile(lang: Option[Lang] = None)
GET /$lang<[a-zA-Z]{2,3}>/profile controllers.MyController.profile(lang: Option[Lang])
GET /access controllers.MyController.access(lang: Option[Lang] = None)
GET /$lang<[a-zA-Z]{2,3}>/access controllers.MyController.access(lang: Option[Lang])
package routers
class MyContentsRouter ... {
override def routes = Function.unlift {
case p"/$lang<[a-zA-Z]{2,3}>" => controller.index
case p"/$lang<[a-zA-Z]{2,3}>/profile" => controller.profile
case p"/$lang<[a-zA-Z]{2,3}>/access" => controller.access
case _ => None
}
}
object ContentsReverseRouter {
def index(maybeLang: Option[Lang]) = instance.index(maybeLang)
def profile(maybeLang: Option[Lang]) = instance.profile(maybeLang)
def access(maybeLang: Option[Lang]) = instance.access(maybeLang)
}
class ReverseRouter ... {
def index(maybeLang: Option[Lang]) = ...
def profile(maybeLang: Option[Lang]) = ...
def access(maybeLang: Option[Lang]) = ...
}
🙏 Let’s generate Router/routes by sbt 🙏
sbt
を用いて、routes
関連のコードの重複を無くす
routes
)を用意 (or something else)
Router
or routes
を作成
Play 2.4 で routes
をパースする方法
play.routes.compiler.RoutesFileParser.parse
routes
の場所: baseDirectory / "conf" / "routes"
(or another solution?)
Play 2.3 で routes
をパースする方法
private[router] play.router.RouteFileParser.parse
play.router
パッケージに置いて誤魔化す or 自作パーサー
routes
の場所: confDirectory / "routes"
/contents/
, /contents/ja/
, /contents/profile
, /contents/ja/profile
, /contents/access
, /contents/ja/access
, …
-> /contents routers.MyGeneratedRouter
GET / controllers.ContentsController.index(lang: Option[play.api.i18n.Lang])
GET /profile controllers.ContentsController.profile(lang: Option[play.api.i18n.Lang])
GET /access controllers.ContentsController.access(lang: Option[play.api.i18n.Lang])
sourceGenerators in Compile <+= (baseDirectory, sourceManaged) map { case (base, source) =>
val countries = Seq("en", "ja") // it should be taken from play.i18n.langs in application.conf
val langRoutesFile = base / "conf" / "routes.i18n"
MyI18NContentsRouterGenerator.generate(langRoutesFile, countries, source).toSeq
}
import play.routes.compiler.{RoutesFileParser, Route, Rule}
import sbt.File
import sbt.Path._
object MyI18NContentsRouterGenerator {
val LANG_ROUTER_CLASS = "MyGeneratedRouter"
val LANG_ROUTER_FILE = LANG_ROUTER_CLASS + ".scala"
def generate(langRoutes: File, countries: Seq[String], sourceManaged: File): Option[File] = {
RoutesFileParser.parse(langRoutes) match {
case Right(rules: List[Rule]) =>
val outputFile = sourceManaged / LANG_ROUTER_FILE
sbt.IO.write(outputFile, _generate(rules))
Some(outputFile)
case Left(_) => None
}
}
private def _generate(rules: List[Rule]): String = {
val routesValues = StringBuilder.newBuilder
val reverseRouterObjValues = StringBuilder.newBuilder
val reverseRouterValues = StringBuilder.newBuilder
rules.foreach {
case route: Route =>
routesValues.append(generateRoutesValue(route))
reverseRouterObjValues.append(generateReverseRouterObjValue(route))
reverseRouterValues.append(generateReverseRouterValue(route))
case _ => // ignore
}
s"""
|package routers
|
|import javax.inject.{Singleton, Provider, Inject}
|
|import controllers.ContentsController
|import play.api.i18n.Lang
|import play.api.routing.{Router, SimpleRouter}
|import play.api.routing.sird._
|
|class ${LANG_ROUTER_CLASS} @Inject()(controller: ContentsController) extends SimpleRouter {
| val DEFAULT_LANG = Lang("en")
| override def routes = Function.unlift {
| ${routesValues}
| case _ => None
| }
|}
|
|object LangReverseRouter {
| private def instance: ReverseRouter = play.api.Play.current.injector.instanceOf[ReverseRouter]
| ${reverseRouterObjValues}
|}
|
|@Singleton
|class ReverseRouter @Inject() (routerProvider: Provider[Router]) {
| private def router: Router = routerProvider.get
| ${reverseRouterValues}
|}
""".stripMargin
}
private def generateRoutesValue(route: Route): String = {
s"""
| case p"/${route.call.method}" =>
| Some(controller.${route.call.method}(DEFAULT_LANG))
| case p"/$$lang<[a-zA-Z]{2,3}>/${route.call.method}" =>
| Lang.get(lang).map(controller.${route.call.method})
""".stripMargin
}
private def generateReverseRouterObjValue(route: Route): String =
s"""
| def ${route.call.method}(maybeLang: Option[Lang]) = instance.${route.call.method}(maybeLang)
""".stripMargin
private def generateReverseRouterValue(route: Route): String =
s"""
| def ${route.call.method}(maybeLang: Option[Lang]) =
| maybeLang.fold("/${route.call.method}")(lang => "/" + lang.code + "/${route.call.method}")
""".stripMargin
}
routes.i18n
をチェックして変更のあった時のみファイルを生成する
Simple Pattern
/en/page
/ja/page
Optional Lang Path Variable Pattern
/page
/ja/page