refactor i18n keys, translations, and translator, to support txt/html

pull/3085/merge
Thibault Duplessis 2017-05-27 21:31:06 +02:00
parent f04652dff1
commit 8610835f0f
5 changed files with 137 additions and 68 deletions

View File

@ -8,51 +8,65 @@ sealed trait I18nKey {
val key: String
def literalTo(lang: Lang, args: Seq[Any]): String
def literalHtmlTo(lang: Lang, args: Seq[Any]): Html
def pluralTo(lang: Lang, count: Count, args: Seq[Any]): String
def pluralHtmlTo(lang: Lang, count: Count, args: Seq[Any]): Html
def literalTxtTo(lang: Lang, args: Seq[Any]): String
def pluralTxtTo(lang: Lang, count: Count, args: Seq[Any]): String
/* Implicit context convenience functions */
def literalStr(args: Any*)(implicit ctx: UserContext): String = literalTo(ctx.lang, args)
def literal(args: Any*)(implicit ctx: UserContext): Html = literalHtmlTo(ctx.lang, args)
def pluralStr(count: Count, args: Any*)(implicit ctx: UserContext): String = pluralTo(ctx.lang, count, args)
def plural(count: Count, args: Any*)(implicit ctx: UserContext): Html = pluralHtmlTo(ctx.lang, count, args)
def literal(args: Any*)(implicit ctx: UserContext): Html = Html(literalTo(ctx.lang, args))
def literalTxt(args: Any*)(implicit ctx: UserContext): String = literalTxtTo(ctx.lang, args)
def plural(count: Count, args: Any*)(implicit ctx: UserContext): Html = Html(pluralTo(ctx.lang, count, args))
def pluralTxt(count: Count, args: Any*)(implicit ctx: UserContext): String = pluralTxtTo(ctx.lang, count, args)
/* Shortcuts */
def apply()(implicit ctx: UserContext): Html = literal()
def str()(implicit ctx: UserContext): String = literalStr()
def txt()(implicit ctx: UserContext): String = literalTxt()
// reuses the count as the single argument
// allows `plural(nb)` instead of `plural(nb, nb)`
def pluralSame(count: Int)(implicit ctx: UserContext): Html = plural(count, count)
def pluralSameStr(count: Int)(implicit ctx: UserContext): String = pluralStr(count, count)
def pluralSameTxt(count: Int)(implicit ctx: UserContext): String = pluralTxt(count, count)
/* English translations */
def literalEn(args: Any*): String = literalTo(enLang, args)
def pluralEn(count: Count, args: Any*): String = pluralTo(enLang, count, args)
// def literalEn(args: Any*): Html = literalHtmlTo(enLang, args)
// def pluralEn(count: Count, args: Any*): Html = pluralHtmlTo(enLang, count, args)
}
final class Translated(val key: String) extends I18nKey {
def literalTo(lang: Lang, args: Seq[Any]): String =
Translator.literal(key, args, lang)
def literalHtmlTo(lang: Lang, args: Seq[Any]): Html =
Translator.html.literal(key, args, lang)
def pluralTo(lang: Lang, count: Count, args: Seq[Any]): String =
Translator.plural(key, count, args, lang)
def pluralHtmlTo(lang: Lang, count: Count, args: Seq[Any]): Html =
Translator.html.plural(key, count, args, lang)
def literalTxtTo(lang: Lang, args: Seq[Any]): String =
Translator.txt.literal(key, args, lang)
def pluralTxtTo(lang: Lang, count: Count, args: Seq[Any]): String =
Translator.txt.plural(key, count, args, lang)
}
final class Untranslated(val key: String) extends I18nKey {
def literalTo(lang: Lang, args: Seq[Any]) = key
def literalHtmlTo(lang: Lang, args: Seq[Any]) = Html(key)
def pluralTo(lang: Lang, count: Count, args: Seq[Any]) = key
def pluralHtmlTo(lang: Lang, count: Count, args: Seq[Any]) = Html(key)
def literalTxtTo(lang: Lang, args: Seq[Any]) = key
def pluralTxtTo(lang: Lang, count: Count, args: Seq[Any]) = key
}
object I18nKey {

View File

@ -10,13 +10,13 @@ private[i18n] final class JsDump(path: String) {
def keysToObject(keys: Seq[I18nKey], lang: Lang) = JsObject {
keys.map { k =>
k.key -> JsString(k.literalTo(lang, Nil))
k.key -> JsString(k.literalTxtTo(lang, Nil))
}
}
def keysToMessageObject(keys: Seq[I18nKey], lang: Lang) = JsObject {
keys.map { k =>
k.literalEn() -> JsString(k.literalTo(lang, Nil))
k.literalTxtTo(enLang, Nil) -> JsString(k.literalTxtTo(lang, Nil))
}
}
@ -30,7 +30,7 @@ private[i18n] final class JsDump(path: String) {
private def dumpFromKey(keys: Iterable[String], lang: Lang): String =
keys.map { key =>
""""%s":"%s"""".format(key, escape(Translator.literal(key, Nil, lang)))
""""%s":"%s"""".format(key, escape(Translator.txt.literal(key, Nil, lang)))
}.mkString("{", ",", "}")
private def writeRefs = writeFile(

View File

@ -0,0 +1,41 @@
package lila.i18n
import play.twirl.api.Html
import lila.common.String.html.{ escape => escapeHtml }
private sealed trait Translation extends Any
private case class Literal(message: String) extends AnyVal with Translation {
private def escaped = escapeHtml(message)
def formatTxt(args: Seq[Any]): String =
if (args.isEmpty) message
else message.format(args: _*)
def formatHtml(args: Seq[Html]): Html =
if (args.isEmpty) escaped
else Html(escaped.body.format(args.map(_.body): _*))
}
private case class Plurals(messages: Map[I18nQuantity, String]) extends AnyVal with Translation {
private def messageFor(quantity: I18nQuantity): Option[String] =
messages.get(quantity)
.orElse(messages.get(I18nQuantity.Other))
.orElse(messages.headOption.map(_._2))
def formatTxt(quantity: I18nQuantity, args: Seq[Any]): Option[String] =
messageFor(quantity).map { message =>
if (args.isEmpty) message
else message.format(args: _*)
}
def formatHtml(quantity: I18nQuantity, args: Seq[Html]): Option[Html] =
messageFor(quantity).map { message =>
val escaped = escapeHtml(message)
if (args.isEmpty) escaped
else Html(escaped.body.format(args.map(_.body): _*))
}
}

View File

@ -1,60 +1,74 @@
package lila.i18n
import play.api.i18n.Lang
import play.api.mvc.RequestHeader
import play.twirl.api.Html
private sealed trait Translation extends Any
private case class Literal(message: String) extends AnyVal with Translation {
def format(args: Seq[Any]): String =
if (args.isEmpty) message
else message.format(args: _*)
}
private case class Plurals(messages: Map[I18nQuantity, String]) extends AnyVal with Translation {
def format(quantity: I18nQuantity, args: Seq[Any]): Option[String] =
messages.get(quantity)
.orElse(messages.get(I18nQuantity.Other))
.orElse(messages.headOption.map(_._2))
.map { message =>
if (args.isEmpty) message
else message.format(args: _*)
}
}
import lila.common.String.html.{ escape => escapeHtml }
object Translator {
def literal(key: MessageKey, args: Seq[Any], lang: Lang): String =
findTranslation(key, lang) flatMap {
formatTranslation(key, _, I18nQuantity.Other /* grmbl */ , args)
} getOrElse {
logger.warn(s"Failed to translate $key to $lang (${args.toList})")
key
}
object html {
def plural(key: MessageKey, count: Count, args: Seq[Any], lang: Lang): String =
findTranslation(key, lang) flatMap {
formatTranslation(key, _, I18nQuantity(lang, count), args)
} getOrElse {
logger.warn(s"Failed to translate $key to $lang (${args.toList})")
key
def literal(key: MessageKey, args: Seq[Any], lang: Lang): Html =
translate(key, lang, I18nQuantity.Other /* grmbl */ , args)
def plural(key: MessageKey, count: Count, args: Seq[Any], lang: Lang): Html =
translate(key, lang, I18nQuantity(lang, count), args)
private def translate(key: MessageKey, lang: Lang, quantity: I18nQuantity, args: Seq[Any]): Html =
findTranslation(key, lang) flatMap { translation =>
val htmlArgs = escapeArgs(args)
try {
translation match {
case literal: Literal => Some(literal.formatHtml(htmlArgs))
case plurals: Plurals => plurals.formatHtml(quantity, htmlArgs)
}
}
catch {
case e: Exception =>
logger.warn(s"Failed to format html $key -> $translation (${args.toList})", e)
Some(Html(key))
}
} getOrElse {
logger.warn(s"No translation found for $quantity $key in $lang")
Html(key)
}
private def escapeArgs(args: Seq[Any]): Seq[Html] = args.map {
case s: String => escapeHtml(s)
case h: Html => h
case a => Html(a.toString)
}
}
object txt {
def literal(key: MessageKey, args: Seq[Any], lang: Lang): String =
translate(key, lang, I18nQuantity.Other /* grmbl */ , args)
def plural(key: MessageKey, count: Count, args: Seq[Any], lang: Lang): String =
translate(key, lang, I18nQuantity(lang, count), args)
private def translate(key: MessageKey, lang: Lang, quantity: I18nQuantity, args: Seq[Any]): String =
findTranslation(key, lang) flatMap { translation =>
try {
translation match {
case literal: Literal => Some(literal.formatTxt(args))
case plurals: Plurals => plurals.formatTxt(quantity, args)
}
}
catch {
case e: Exception =>
logger.warn(s"Failed to format txt $key -> $translation (${args.toList})", e)
Some(key)
}
} getOrElse {
logger.warn(s"No translation found for $quantity $key in $lang")
key
}
}
private def findTranslation(key: MessageKey, lang: Lang): Option[Translation] =
I18nDb.all.get(lang).flatMap(_ get key) orElse
I18nDb.all.get(defaultLang).flatMap(_ get key)
private def formatTranslation(key: MessageKey, translation: Translation, quantity: I18nQuantity, args: Seq[Any]): Option[String] = try {
translation match {
case literal: Literal => Some(literal.format(args))
case plurals: Plurals => plurals.format(quantity, args)
}
}
catch {
case e: Exception =>
logger.warn(s"Failed to translate $key -> $translation (${args.toList})", e)
None
}
}

View File

@ -6,18 +6,18 @@ import actorApi._
import lila.chat.actorApi._
import lila.game.Game
import lila.i18n.I18nKey.{ Select => SelectI18nKey }
import lila.i18n.I18nKeys
import lila.i18n.{ I18nKeys, enLang }
final class Messenger(val chat: ActorSelection) {
def system(game: Game, message: SelectI18nKey, args: Any*) {
val translated = message(I18nKeys).literalEn(args: _*)
val translated = message(I18nKeys).literalTxtTo(enLang, args)
chat ! SystemTalk(watcherId(game.id), translated)
if (game.nonAi) chat ! SystemTalk(game.id, translated)
}
def systemForOwners(gameId: String, message: SelectI18nKey, args: Any*) {
val translated = message(I18nKeys).literalEn(args: _*)
val translated = message(I18nKeys).literalTxtTo(enLang, args)
chat ! SystemTalk(gameId, translated)
}