package play.api.i18n
import javax.inject.{ Inject, Singleton }
import play.api.inject.Module
import play.api.mvc.{ DiscardingCookie, Cookie, Result, RequestHeader, Session }
import play.mvc.Http
import scala.language.postfixOps
import play.api._
import play.utils.{ PlayIO, Resources }
import play.Logger
import scala.util.parsing.input._
import scala.util.parsing.combinator._
import scala.util.control.NonFatal
import java.net.URL
import scala.io.Codec
case class Lang(language: String, country: String = "") {
lazy val toLocale = Option(country).filterNot(_.isEmpty).map(c => new java.util.Locale(language, c)).getOrElse(new java.util.Locale(language))
def satisfies(accept: Lang) = language.equalsIgnoreCase(accept.language) && (accept match {
case Lang(_, "") => true
case Lang(_, c) => country.equalsIgnoreCase(c)
})
lazy val code = language.toLowerCase(java.util.Locale.ENGLISH) + Option(country).filterNot(_.isEmpty).map("-" + _.toUpperCase(java.util.Locale.ENGLISH)).getOrElse("")
override def equals(that: Any) = {
that match {
case lang: Lang => code == lang.code
case _ => false
}
}
override def hashCode: Int = code.hashCode
}
object Lang {
implicit lazy val defaultLang = {
val defaultLocale = java.util.Locale.getDefault
Lang(defaultLocale.getLanguage, defaultLocale.getCountry)
}
private val SimpleLocale = """([a-zA-Z]{2,3})""".r
private val CountryLocale = (SimpleLocale.toString + """-([a-zA-Z]{2}|[0-9]{3})""").r
def apply(code: String): Lang = {
get(code).getOrElse(
sys.error("Unrecognized language: %s".format(code))
)
}
def get(code: String): Option[Lang] = {
code match {
case SimpleLocale(language) => Some(Lang(language, ""))
case CountryLocale(language, country) => Some(Lang(language, country))
case _ => None
}
}
private val langsCache = Application.instanceCache[Langs]
def availables(implicit app: Application): Seq[Lang] = {
langsCache(app).availables
}
def preferred(langs: Seq[Lang])(implicit app: Application): Lang = {
langsCache(app).preferred(langs)
}
}
trait Langs {
def availables: Seq[Lang]
def preferred(candidates: Seq[Lang]): Lang
}
@Singleton
class DefaultLangs @Inject() (configuration: Configuration) extends Langs {
private val config = PlayConfig(configuration)
val availables = {
val langs = configuration.getString("application.langs") map { langsStr =>
Logger.warn("application.langs is deprecated, use play.i18n.langs instead")
langsStr.split(",").map(_.trim).toSeq
} getOrElse {
config.get[Seq[String]]("play.i18n.langs")
}
langs.map { lang =>
try { Lang(lang) } catch {
case NonFatal(e) => throw configuration.reportError("play.i18n.langs",
"Invalid language code [" + lang + "]", Some(e))
}
}
}
def preferred(candidates: Seq[Lang]) = candidates.collectFirst(Function.unlift { lang =>
availables.find(_.satisfies(lang))
}).getOrElse(availables.headOption.getOrElse(Lang.defaultLang))
}
object Messages {
private[play] val messagesApiCache = Application.instanceCache[MessagesApi]
object Implicits {
import scala.language.implicitConversions
implicit def applicationMessagesApi(implicit application: Application): MessagesApi =
messagesApiCache(application)
implicit def applicationMessages(implicit lang: Lang, application: Application): Messages =
new Messages(lang, messagesApiCache(application))
}
def apply(key: String, args: Any*)(implicit messages: Messages): String = {
messages(key, args: _*)
}
def apply(keys: Seq[String], args: Any*)(implicit messages: Messages): String = {
messages(keys, args: _*)
}
def isDefinedAt(key: String)(implicit messages: Messages): Boolean = {
messages.isDefinedAt(key)
}
def parse(messageSource: MessageSource, messageSourceName: String): Either[PlayException.ExceptionSource, Map[String, String]] = {
new Messages.MessagesParser(messageSource, "").parse.right.map { messages =>
messages.map { message => message.key -> message.pattern }.toMap
}
}
trait MessageSource {
def read: String
}
case class UrlMessageSource(url: URL) extends MessageSource {
def read = PlayIO.readUrlAsString(url)(Codec.UTF8)
}
private[i18n] case class Message(key: String, pattern: String, source: MessageSource, sourceName: String) extends Positional
private[i18n] class MessagesParser(messageSource: MessageSource, messageSourceName: String) extends RegexParsers {
case class (: String)
override def skipWhitespace = false
override val whiteSpace = """^[ \t]+""".r
def namedError[A](p: Parser[A], msg: String) = Parser[A] { i =>
p(i) match {
case Failure(_, in) => Failure(msg, in)
case o => o
}
}
val end = """^\s*""".r
val newLine = namedError((("\r"?) ~> "\n"), "End of line expected")
val ignoreWhiteSpace = opt(whiteSpace)
val blankLine = ignoreWhiteSpace <~ newLine ^^ { case _ => Comment("") }
val = """^#.*""".r ^^ { case => Comment(s) }
val messageKey = namedError("""^[a-zA-Z0-9_.-]+""".r, "Message key expected")
val messagePattern = namedError(
rep(
("""\""" ^^ (_ => "")) ~> (
("\r"?) ~> "\n" ^^ (_ => "") |
"n" ^^ (_ => "\n") |
"""\""" |
"^.".r ^^ ("""\""" + _)
) |
"^.".r
) ^^ { case chars => chars.mkString },
"Message pattern expected"
)
val message = ignoreWhiteSpace ~ messageKey ~ (ignoreWhiteSpace ~ "=" ~ ignoreWhiteSpace) ~ messagePattern ^^ {
case (_ ~ k ~ _ ~ v) => Messages.Message(k, v.trim, messageSource, messageSourceName)
}
val sentence = (comment | positioned(message)) <~ newLine
val parser = phrase(((sentence | blankLine).*) <~ end) ^^ {
case messages => messages.collect {
case m @ Messages.Message(_, _, _, _) => m
}
}
def parse: Either[PlayException.ExceptionSource, Seq[Message]] = {
parser(new CharSequenceReader(messageSource.read + "\n")) match {
case Success(messages, _) => Right(messages)
case NoSuccess(message, in) => Left(
new PlayException.ExceptionSource("Configuration error", message) {
def line = in.pos.line
def position = in.pos.column - 1
def input = messageSource.read
def sourceName = messageSourceName
}
)
}
}
}
}
case class Messages(lang: Lang, messages: MessagesApi) {
def apply(key: String, args: Any*): String = messages(key, args: _*)(lang)
def apply(keys: Seq[String], args: Any*): String = messages(keys, args: _*)(lang)
def translate(key: String, args: Seq[Any]): Option[String] = messages.translate(key, args)(lang)
def isDefinedAt(key: String): Boolean = messages.isDefinedAt(key)(lang)
}
trait MessagesApi {
def messages: Map[String, Map[String, String]]
def preferred(candidates: Seq[Lang]): Messages
def preferred(request: RequestHeader): Messages
def preferred(request: play.mvc.Http.RequestHeader): Messages
def setLang(result: Result, lang: Lang): Result
def clearLang(result: Result): Result
def apply(key: String, args: Any*)(implicit lang: Lang): String
def apply(keys: Seq[String], args: Any*)(implicit lang: Lang): String
def translate(key: String, args: Seq[Any])(implicit lang: Lang): Option[String]
def isDefinedAt(key: String)(implicit lang: Lang): Boolean
def langCookieName: String
def langCookieSecure: Boolean
def langCookieHttpOnly: Boolean
}
@Singleton
class DefaultMessagesApi @Inject() (environment: Environment, configuration: Configuration, langs: Langs) extends MessagesApi {
private val config = PlayConfig(configuration)
import java.text._
protected val messagesPrefix =
config.getOptionalDeprecated[String]("play.i18n.path", "messages.path")
val messages: Map[String, Map[String, String]] = loadAllMessages
def preferred(candidates: Seq[Lang]) = Messages(langs.preferred(candidates), this)
def preferred(request: RequestHeader) = {
val maybeLangFromCookie = request.cookies.get(langCookieName)
.flatMap(c => Lang.get(c.value))
val lang = langs.preferred(maybeLangFromCookie.toSeq ++ request.acceptLanguages)
Messages(lang, this)
}
def preferred(request: Http.RequestHeader) = preferred(request._underlyingHeader())
def setLang(result: Result, lang: Lang) = result.withCookies(Cookie(langCookieName, lang.code, path = Session.path, domain = Session.domain,
secure = langCookieSecure, httpOnly = langCookieHttpOnly))
def clearLang(result: Result) = result.discardingCookies(DiscardingCookie(langCookieName, path = Session.path, domain = Session.domain,
secure = langCookieSecure))
def apply(key: String, args: Any*)(implicit lang: Lang): String = {
translate(key, args).getOrElse(noMatch(key, args))
}
def apply(keys: Seq[String], args: Any*)(implicit lang: Lang): String = {
keys.foldLeft[Option[String]](None) {
case (None, key) => translate(key, args)
case (acc, _) => acc
}.getOrElse(noMatch(keys.last, args))
}
private def noMatch(key: String, args: Seq[Any]) = key
def translate(key: String, args: Seq[Any])(implicit lang: Lang): Option[String] = {
val langsToTry: List[Lang] =
List(lang, Lang(lang.language, ""), Lang("default", ""), Lang("default.play", ""))
val pattern: Option[String] =
langsToTry.foldLeft[Option[String]](None)((res, lang) =>
res.orElse(messages.get(lang.code).flatMap(_.get(key))))
pattern.map(pattern =>
new MessageFormat(pattern, lang.toLocale).format(args.map(_.asInstanceOf[java.lang.Object]).toArray))
}
def isDefinedAt(key: String)(implicit lang: Lang): Boolean = {
val langsToTry: List[Lang] = List(lang, Lang(lang.language, ""), Lang("default", ""), Lang("default.play", ""))
langsToTry.foldLeft[Boolean](false)({ (acc, lang) =>
acc || messages.get(lang.code).map(_.isDefinedAt(key)).getOrElse(false)
})
}
private def joinPaths(first: Option[String], second: String) = first match {
case Some(parent) => new java.io.File(parent, second).getPath
case None => second
}
protected def loadMessages(file: String): Map[String, String] = {
import scala.collection.JavaConverters._
environment.classLoader.getResources(joinPaths(messagesPrefix, file)).asScala.toList
.filterNot(url => Resources.isDirectory(environment.classLoader, url)).reverse
.map { messageFile =>
Messages.parse(Messages.UrlMessageSource(messageFile), messageFile.toString).fold(e => throw e, identity)
}.foldLeft(Map.empty[String, String]) { _ ++ _ }
}
protected def loadAllMessages: Map[String, Map[String, String]] = {
langs.availables.map(_.code).map { lang =>
(lang, loadMessages("messages." + lang))
}.toMap
.+("default" -> loadMessages("messages"))
.+("default.play" -> loadMessages("messages.default"))
}
lazy val langCookieName =
config.getDeprecated[String]("play.i18n.langCookieName", "application.lang.cookie")
lazy val langCookieSecure =
config.get[Boolean]("play.i18n.langCookieSecure")
lazy val langCookieHttpOnly =
config.get[Boolean]("play.i18n.langCookieHttpOnly")
}
class I18nModule extends Module {
def bindings(environment: Environment, configuration: Configuration) = {
Seq(
bind[Langs].to[DefaultLangs],
bind[MessagesApi].to[DefaultMessagesApi]
)
}
}
trait I18nComponents {
def environment: Environment
def configuration: Configuration
lazy val messagesApi: MessagesApi = new DefaultMessagesApi(environment, configuration, langs)
lazy val langs: Langs = new DefaultLangs(configuration)
}