refactor i18n keys, translations, and translator, to support txt/html
parent
f04652dff1
commit
8610835f0f
|
@ -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 {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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): _*))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue