translate perf stats

pull/6012/head
Thibault Duplessis 2020-02-10 11:45:19 -06:00
parent 998cc3b9ae
commit 6885860a7c
8 changed files with 241 additions and 174 deletions

View File

@ -2,13 +2,13 @@ package lila.app
package templating
import java.util.Locale
import play.api.i18n.Lang
import scala.collection.mutable.AnyRefMap
import org.joda.time.format._
import org.joda.time.format.ISODateTimeFormat
import org.joda.time.{ DateTime, DateTimeZone, DurationFieldType, Period, PeriodType }
import lila.api.Context
import lila.app.ui.ScalatagsTemplate._
trait DateHelper { self: I18nHelper =>
@ -29,45 +29,45 @@ trait DateHelper { self: I18nHelper =>
private val englishDateFormatter = DateTimeFormat forStyle dateStyle
private def dateTimeFormatter(ctx: Context): DateTimeFormatter =
private def dateTimeFormatter(implicit lang: Lang): DateTimeFormatter =
dateTimeFormatters.getOrElseUpdate(
ctx.lang.code,
DateTimeFormat forStyle dateTimeStyle withLocale ctx.lang.toLocale
lang.code,
DateTimeFormat forStyle dateTimeStyle withLocale lang.toLocale
)
private def dateFormatter(ctx: Context): DateTimeFormatter =
private def dateFormatter(implicit lang: Lang): DateTimeFormatter =
dateFormatters.getOrElseUpdate(
ctx.lang.code,
DateTimeFormat forStyle dateStyle withLocale ctx.lang.toLocale
lang.code,
DateTimeFormat forStyle dateStyle withLocale lang.toLocale
)
private def periodFormatter(ctx: Context): PeriodFormatter =
private def periodFormatter(implicit lang: Lang): PeriodFormatter =
periodFormatters.getOrElseUpdate(
ctx.lang.code, {
lang.code, {
Locale setDefault Locale.ENGLISH
PeriodFormat wordBased ctx.lang.toLocale
PeriodFormat wordBased lang.toLocale
}
)
def showDateTimeZone(date: DateTime, zone: DateTimeZone)(implicit ctx: Context): String =
dateTimeFormatter(ctx) print date.toDateTime(zone)
def showDateTimeZone(date: DateTime, zone: DateTimeZone)(implicit lang: Lang): String =
dateTimeFormatter print date.toDateTime(zone)
def showDateTimeUTC(date: DateTime)(implicit ctx: Context): String =
def showDateTimeUTC(date: DateTime)(implicit lang: Lang): String =
showDateTimeZone(date, DateTimeZone.UTC)
def showDate(date: DateTime)(implicit ctx: Context): String =
dateFormatter(ctx) print date
def showDate(date: DateTime)(implicit lang: Lang): String =
dateFormatter print date
def showEnglishDate(date: DateTime): String =
englishDateFormatter print date
def semanticDate(date: DateTime)(implicit ctx: Context): Frag =
def semanticDate(date: DateTime)(implicit lang: Lang): Frag =
timeTag(datetimeAttr := isoDate(date))(showDate(date))
def showPeriod(period: Period)(implicit ctx: Context): String =
periodFormatter(ctx) print period.normalizedStandard(periodType)
def showPeriod(period: Period)(implicit lang: Lang): String =
periodFormatter print period.normalizedStandard(periodType)
def showMinutes(minutes: Int)(implicit ctx: Context): String =
def showMinutes(minutes: Int)(implicit lang: Lang): String =
showPeriod(new Period(minutes * 60 * 1000L))
def isoDate(date: DateTime): String = isoFormatter print date

View File

@ -13,6 +13,8 @@ import controllers.routes
object perfStat {
import trans.perfStat._
def apply(
u: User,
rankMap: lila.rating.UserRankMap,
@ -22,7 +24,7 @@ object perfStat {
ratingChart: Option[String]
)(implicit ctx: Context) =
views.html.base.layout(
title = s"${u.username} ${perfType.name} stats",
title = s"${u.username} ${perfStats.txt(perfType.name)} stats",
robots = false,
moreJs = frag(
jsAt("compiled/user.js"),
@ -41,14 +43,14 @@ object perfStat {
div(cls := "box__top")(
h1(
a(href := routes.User.show(u.username))(u.username),
span(perfType.name, " stats")
span(perfStats(perfType.name))
),
div(cls := "box__top__actions")(
u.perfs(perfType).nb > 0 option a(
cls := "button button-empty text",
dataIcon := perfType.iconChar,
href := s"${routes.User.games(u.username, "search")}?perf=${perfType.id}"
)("View the games"),
)(viewTheGames()),
bits.perfTrophies(u, rankMap.view.filterKeys(perfType ==).toMap)
)
),
@ -68,45 +70,41 @@ object perfStat {
private def decimal(v: Double) = lila.common.Maths.roundAt(v, 2)
private def glicko(perfType: PerfType, perf: Perf, percentile: Option[Double]): Frag =
private def glicko(perfType: PerfType, perf: Perf, percentile: Option[Double])(implicit lang: Lang): Frag =
st.section(cls := "glicko")(
h2(
"Rating: ",
strong(title := "Yes, ratings have decimal accuracy.")(decimal(perf.glicko.rating).toString),
trans.perfRatingX(strong(decimal(perf.glicko.rating).toString)),
perf.glicko.provisional option frag(
" ",
span(
title := "Not enough rated games have been played to establish a reliable rating.",
title := notEnoughRatedGames.txt(),
cls := "details"
)("(provisional)")
)("(", provisional(), ")")
),
". ",
percentile.filter(_ != 0.0 && !perf.glicko.provisional).map { percentile =>
span(cls := "details")(
"Better than ",
a(href := routes.Stat.ratingDistribution(perfType.key))(
strong(percentile, "%"),
" of ",
perfType.name,
" players"
),
"."
trans.youAreBetterThanPercentOfPerfTypePlayers(
a(href := routes.Stat.ratingDistribution(perfType.key))(strong(percentile, "%")),
a(href := routes.Stat.ratingDistribution(perfType.key))(perfType.name)
)
)
}
),
p(
"Progression over the last twelve games: ",
progressOverLastXGames(12),
" ",
span(cls := "progress")(
if (perf.progress > 0) tag("green")(dataIcon := "N")(perf.progress)
else if (perf.progress < 0) tag("red")(dataIcon := "M")(-perf.progress)
else frag("none")
else "-"
),
". ",
"Rating deviation: ",
strong(
title := "Lower value means the rating is more stable. Above 110, the rating is considered provisional."
)(decimal(perf.glicko.deviation).toString),
"."
ratingDeviation(
strong(
title := ratingDeviationHelp.txt(lila.rating.Glicko.provisionalDeviation)
)(decimal(perf.glicko.deviation).toString)
)
)
)
@ -114,130 +112,130 @@ object perfStat {
(denom != 0) ?? s"${Math.round(num * 100.0 / denom)}%"
}
private def counter(count: lila.perfStat.Count): Frag = st.section(cls := "counter split")(
div(
table(
tbody(
tr(
th("Total games"),
td(count.all),
td
),
tr(cls := "full")(
th("Rated games"),
td(count.rated),
td(pct(count.rated, count.all))
),
tr(cls := "full")(
th("Tournament games"),
td(count.tour),
td(pct(count.tour, count.all))
),
tr(cls := "full")(
th("Berserked games"),
td(count.berserk),
td(pct(count.berserk, count.tour))
),
count.seconds > 0 option tr(cls := "full")(
th("Time spent playing"),
td(colspan := "2") {
val hours = count.seconds / (60 * 60)
val minutes = (count.seconds % (60 * 60)) / 60
s"${hours}h, ${minutes}m"
}
private def counter(count: lila.perfStat.Count)(implicit lang: Lang): Frag =
st.section(cls := "counter split")(
div(
table(
tbody(
tr(
th(totalGames()),
td(count.all),
td
),
tr(cls := "full")(
th(ratedGames()),
td(count.rated),
td(pct(count.rated, count.all))
),
tr(cls := "full")(
th(tournamentGames()),
td(count.tour),
td(pct(count.tour, count.all))
),
tr(cls := "full")(
th(berserkedGames()),
td(count.berserk),
td(pct(count.berserk, count.tour))
),
count.seconds > 0 option tr(cls := "full")(
th(timeSpentPlaying()),
td(colspan := "2")(showPeriod(count.period))
)
)
)
)
),
div(
table(
tbody(
tr(
th("Average opponent"),
td(decimal(count.opAvg.avg).toString),
td
),
tr(cls := "full")(
th("Victories"),
td(tag("green")(count.win)),
td(tag("green")(pct(count.win, count.all)))
),
tr(cls := "full")(
th("Draws"),
td(count.draw),
td(pct(count.draw, count.all))
),
tr(cls := "full")(
th("Defeats"),
td(tag("red")(count.loss)),
td(tag("red")(pct(count.loss, count.all)))
),
tr(cls := "full")(
th("Disconnections"),
td(if (count.disconnects > count.all * 100 / 15) tag("red") else frag())(count.disconnects),
td(pct(count.disconnects, count.all))
),
div(
table(
tbody(
tr(
th(averageOpponent()),
td(decimal(count.opAvg.avg).toString),
td
),
tr(cls := "full")(
th(victories()),
td(tag("green")(count.win)),
td(tag("green")(pct(count.win, count.all)))
),
tr(cls := "full")(
th(trans.draws()),
td(count.draw),
td(pct(count.draw, count.all))
),
tr(cls := "full")(
th(defeats()),
td(tag("red")(count.loss)),
td(tag("red")(pct(count.loss, count.all)))
),
tr(cls := "full")(
th(disconnections()),
td(if (count.disconnects > count.all * 100 / 15) tag("red") else emptyFrag)(count.disconnects),
td(pct(count.disconnects, count.all))
)
)
)
)
)
)
private def highlowSide(title: String, opt: Option[lila.perfStat.RatingAt], color: String): Frag =
private def highlowSide(title: Frag => Frag, opt: Option[lila.perfStat.RatingAt], color: String)(
implicit lang: Lang
): Frag =
opt match {
case Some(r) =>
div(
h2(title, ": ", strong(tag(color)(r.int))),
h2(title(strong(tag(color)(r.int)))),
a(cls := "glpt", href := routes.Round.watcher(r.gameId, "white"))(absClientDateTime(r.at))
)
case None => div(h2(title), " ", span("Not enough games played"))
case None => div(h2(title(emptyFrag)), " ", span(notEnoughGames()))
}
private def highlow(stat: PerfStat): Frag = st.section(cls := "highlow split")(
highlowSide("Highest rating", stat.highest, "green"),
highlowSide("Lowest rating", stat.lowest, "red")
private def highlow(stat: PerfStat)(implicit lang: Lang): Frag = st.section(cls := "highlow split")(
highlowSide(highestRating(_), stat.highest, "green"),
highlowSide(lowestRating(_), stat.lowest, "red")
)
private def fromTo(s: lila.perfStat.Streak): Frag =
private def fromTo(s: lila.perfStat.Streak)(implicit lang: Lang): Frag =
s.from match {
case Some(from) =>
frag(
"from ",
fromXToY(
a(cls := "glpt", href := routes.Round.watcher(from.gameId, "white"))(absClientDateTime(from.at)),
" to ",
s.to match {
case Some(to) =>
a(cls := "glpt", href := routes.Round.watcher(to.gameId, "white"))(absClientDateTime(to.at))
case None => frag("now")
case None => now()
}
)
case None => nbsp
}
private def resultStreakSideStreak(s: lila.perfStat.Streak, title: String, color: String): Frag =
private def resultStreakSideStreak(s: lila.perfStat.Streak, title: Frag => Frag, color: String)(
implicit lang: Lang
): Frag =
div(cls := "streak")(
h3(
title,
": ",
if (s.v == 1) tag(color)(frag(strong(s.v), " game"))
else if (s.v > 0) tag(color)(frag(strong(s.v), " games"))
else frag("none")
title(
if (s.v > 0) tag(color)(trans.nbGames.plural(s.v, strong(s.v)))
else "-"
)
),
fromTo(s)
)
private def resultStreakSide(s: lila.perfStat.Streaks, title: String, color: String): Frag = div(
private def resultStreakSide(s: lila.perfStat.Streaks, title: Frag, color: String)(
implicit lang: Lang
): Frag = div(
h2(title),
resultStreakSideStreak(s.max, "Longest", color),
resultStreakSideStreak(s.cur, "Current", color)
resultStreakSideStreak(s.max, longestStreak(_), color),
resultStreakSideStreak(s.cur, currentStreak(_), color)
)
private def resultStreak(streak: lila.perfStat.ResultStreak): Frag =
private def resultStreak(streak: lila.perfStat.ResultStreak)(implicit lang: Lang): Frag =
st.section(cls := "resultStreak split")(
resultStreakSide(streak.win, "Winning streak", "green"),
resultStreakSide(streak.loss, "Losing streak", "red")
resultStreakSide(streak.win, winningStreak(), "green"),
resultStreakSide(streak.loss, losingStreak(), "red")
)
private def resultTable(results: lila.perfStat.Results, title: String)(implicit lang: Lang): Frag = div(
private def resultTable(results: lila.perfStat.Results, title: Frag)(implicit lang: Lang): Frag = div(
table(
thead(
tr(
@ -256,54 +254,52 @@ object perfStat {
)
private def result(stat: PerfStat)(implicit lang: Lang): Frag = st.section(cls := "result split")(
resultTable(stat.bestWins, "Best rated victories"),
resultTable(stat.worstLosses, "Worst rated defeats")
resultTable(stat.bestWins, bestRated()),
resultTable(stat.worstLosses, worstRated())
)
private def playStreakNbStreak(s: lila.perfStat.Streak, title: String): Frag = div(
div(cls := "streak")(
h3(
title,
": ",
if (s.v == 1) frag(strong(s.v), " game")
else if (s.v > 0) frag(strong(s.v), " games")
else frag("none")
),
fromTo(s)
private def playStreakNbStreak(s: lila.perfStat.Streak, title: Frag => Frag)(implicit lang: Lang): Frag =
div(
div(cls := "streak")(
h3(
title(
if (s.v > 0) trans.nbGames.plural(s.v, strong(s.v))
else "-"
)
),
fromTo(s)
)
)
)
private def playStreakNbStreaks(streaks: lila.perfStat.Streaks): Frag = div(cls := "split")(
playStreakNbStreak(streaks.max, "Longest streak"),
playStreakNbStreak(streaks.cur, "Current streak")
)
private def playStreakNb(playStreak: lila.perfStat.PlayStreak): Frag = st.section(cls := "playStreak")(
h2(span(title := "Less than one hour between games")("Games played in a row")),
playStreakNbStreaks(playStreak.nb)
)
private def playStreakTimeStreak(s: lila.perfStat.Streak, title: String): Frag = div(
div(cls := "streak")(
h3(
title,
": ", {
val hours = s.v / (60 * 60)
val minutes = (s.v % (60 * 60)) / 60
s"${hours} hours, ${minutes} minutes"
}
),
fromTo(s)
private def playStreakNbStreaks(streaks: lila.perfStat.Streaks)(implicit lang: Lang): Frag =
div(cls := "split")(
playStreakNbStreak(streaks.max, longestStreak(_)),
playStreakNbStreak(streaks.cur, currentStreak(_))
)
)
private def playStreakTimeStreaks(streaks: lila.perfStat.Streaks): Frag = div(cls := "split")(
playStreakTimeStreak(streaks.max, "Longest streak"),
playStreakTimeStreak(streaks.cur, "Current streak")
)
private def playStreakNb(playStreak: lila.perfStat.PlayStreak)(implicit lang: Lang): Frag =
st.section(cls := "playStreak")(
h2(span(title := lessThanOneHour.txt())(gamesInARow())),
playStreakNbStreaks(playStreak.nb)
)
private def playStreakTime(playStreak: lila.perfStat.PlayStreak): Frag = st.section(cls := "playStreak")(
h2(span(title := "Less than one hour between games")("Max time spent playing")),
playStreakTimeStreaks(playStreak.time)
)
private def playStreakTimeStreak(s: lila.perfStat.Streak, title: Frag => Frag)(implicit lang: Lang): Frag =
div(
div(cls := "streak")(
h3(title(showPeriod(s.period))),
fromTo(s)
)
)
private def playStreakTimeStreaks(streaks: lila.perfStat.Streaks)(implicit lang: Lang): Frag =
div(cls := "split")(
playStreakTimeStreak(streaks.max, longestStreak(_)),
playStreakTimeStreak(streaks.cur, currentStreak(_))
)
private def playStreakTime(playStreak: lila.perfStat.PlayStreak)(implicit lang: Lang): Frag =
st.section(cls := "playStreak")(
h2(span(title := lessThanOneHour.txt())(maxTimePlaying())),
playStreakTimeStreaks(playStreak.time)
)
}

View File

@ -2,7 +2,7 @@ const fs = require('fs-extra');
const parseString = require('xml2js').parseString;
const baseDir = 'translation/source';
const dbs = 'site arena emails learn activity coordinates study clas contact patron coach broadcast streamer tfa settings preferences team'.split(' ');
const dbs = 'site arena emails learn activity coordinates study clas contact patron coach broadcast streamer tfa settings preferences team perfStat'.split(' ');
function ucfirst(s) {
return s.charAt(0).toUpperCase() + s.slice(1);

View File

@ -76,7 +76,7 @@ lazy val i18n = module("i18n",
MessageCompiler(
sourceDir = new File("translation/source"),
destDir = new File("translation/dest"),
dbs = "site arena emails learn activity coordinates study class contact patron coach broadcast streamer tfa settings preferences team".split(' ').toList,
dbs = "site arena emails learn activity coordinates study class contact patron coach broadcast streamer tfa settings preferences team perfStat".split(' ').toList,
compileTo = (sourceManaged in Compile).value / "messages"
)
}.taskValue,

View File

@ -22,6 +22,7 @@ object I18nDb {
case object Settings extends Ref
case object Preferences extends Ref
case object Team extends Ref
case object PerfStat extends Ref
val site: Messages = lila.i18n.db.site.Registry.load
val arena: Messages = lila.i18n.db.arena.Registry.load
@ -40,6 +41,7 @@ object I18nDb {
val settings: Messages = lila.i18n.db.settings.Registry.load
val preferences: Messages = lila.i18n.db.preferences.Registry.load
val team: Messages = lila.i18n.db.team.Registry.load
val perfStat: Messages = lila.i18n.db.perfStat.Registry.load
def apply(ref: Ref): Messages = ref match {
case Site => site
@ -59,6 +61,7 @@ object I18nDb {
case Settings => settings
case Preferences => preferences
case Team => team
case PerfStat => perfStat
}
val langs: Set[Lang] = site.keys.toSet

View File

@ -1,7 +1,7 @@
// Generated with bin/trans-dump.js
package lila.i18n
import I18nDb.{ Activity, Arena, Broadcast, Clas, Coach, Contact, Coordinates, Emails, Learn, Patron, Preferences, Settings, Site, Streamer, Study, Team, Tfa }
import I18nDb.{ Activity, Arena, Broadcast, Clas, Coach, Contact, Coordinates, Emails, Learn, Patron, PerfStat, Preferences, Settings, Site, Streamer, Study, Team, Tfa }
// format: OFF
object I18nKeys {
@ -1526,4 +1526,37 @@ val `nbMembers` = new Translated("nbMembers", Team)
val `xJoinRequests` = new Translated("xJoinRequests", Team)
}
object perfStat {
val `perfStats` = new Translated("perfStats", PerfStat)
val `viewTheGames` = new Translated("viewTheGames", PerfStat)
val `provisional` = new Translated("provisional", PerfStat)
val `notEnoughRatedGames` = new Translated("notEnoughRatedGames", PerfStat)
val `progressOverLastXGames` = new Translated("progressOverLastXGames", PerfStat)
val `ratingDeviation` = new Translated("ratingDeviation", PerfStat)
val `ratingDeviationHelp` = new Translated("ratingDeviationHelp", PerfStat)
val `totalGames` = new Translated("totalGames", PerfStat)
val `ratedGames` = new Translated("ratedGames", PerfStat)
val `tournamentGames` = new Translated("tournamentGames", PerfStat)
val `berserkedGames` = new Translated("berserkedGames", PerfStat)
val `timeSpentPlaying` = new Translated("timeSpentPlaying", PerfStat)
val `averageOpponent` = new Translated("averageOpponent", PerfStat)
val `victories` = new Translated("victories", PerfStat)
val `defeats` = new Translated("defeats", PerfStat)
val `disconnections` = new Translated("disconnections", PerfStat)
val `notEnoughGames` = new Translated("notEnoughGames", PerfStat)
val `highestRating` = new Translated("highestRating", PerfStat)
val `lowestRating` = new Translated("lowestRating", PerfStat)
val `fromXToY` = new Translated("fromXToY", PerfStat)
val `winningStreak` = new Translated("winningStreak", PerfStat)
val `losingStreak` = new Translated("losingStreak", PerfStat)
val `longestStreak` = new Translated("longestStreak", PerfStat)
val `currentStreak` = new Translated("currentStreak", PerfStat)
val `bestRated` = new Translated("bestRated", PerfStat)
val `worstRated` = new Translated("worstRated", PerfStat)
val `gamesInARow` = new Translated("gamesInARow", PerfStat)
val `lessThanOneHour` = new Translated("lessThanOneHour", PerfStat)
val `maxTimePlaying` = new Translated("maxTimePlaying", PerfStat)
val `now` = new Translated("now", PerfStat)
}
}

View File

@ -3,7 +3,7 @@ package lila.perfStat
import lila.game.Pov
import lila.rating.PerfType
import org.joda.time.DateTime
import org.joda.time.{ DateTime, Period }
case class PerfStat(
_id: String, // userId/perfId
@ -103,6 +103,7 @@ case class Streak(v: Int, from: Option[RatingAt], to: Option[RatingAt]) {
from = from orElse pov.player.rating.map { RatingAt(_, pov.game.createdAt, pov.gameId) },
to = pov.player.ratingAfter.map { RatingAt(_, pov.game.movedAt, pov.gameId) }
)
def period = new Period(v * 1000L)
}
object Streak {
val init = Streak(0, none, none)
@ -139,6 +140,7 @@ case class Count(
if (~pov.loss && pov.game.status == chess.Status.Timeout) 1 else 0
}
)
def period = new Period(seconds * 1000L)
}
object Count {
val init = Count(

View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="perfStats">%s stats</string>
<string name="viewTheGames">View the games</string>
<string name="provisional">provisional</string>
<string name="notEnoughRatedGames">Not enough rated games have been played to establish a reliable rating.</string>
<string name="progressOverLastXGames">Progression over the last %s games:</string>
<string name="ratingDeviation">Rating deviation: %s.</string>
<string name="ratingDeviationHelp">Lower value means the rating is more stable. Above %s, the rating is considered provisional.</string>
<string name="totalGames">Total games</string>
<string name="ratedGames">Rated games</string>
<string name="tournamentGames">Tournament games</string>
<string name="berserkedGames">Berserked games</string>
<string name="timeSpentPlaying">Time spent playing</string>
<string name="averageOpponent">Average opponent</string>
<string name="victories">Victories</string>
<string name="defeats">Defeats</string>
<string name="disconnections">Disconnections</string>
<string name="notEnoughGames">Not enough games played</string>
<string name="highestRating">Highest rating: %s</string>
<string name="lowestRating">Lowest rating: %s</string>
<string name="fromXToY">from %1$s to %2$s</string>
<string name="winningStreak">Winning streak</string>
<string name="losingStreak">Losing streak</string>
<string name="longestStreak">Longest streak: %s</string>
<string name="currentStreak">Current streak: %s</string>
<string name="bestRated">Best rated victories</string>
<string name="worstRated">Worst rated defeats</string>
<string name="gamesInARow">Games played in a row</string>
<string name="lessThanOneHour">Less than one hour between games</string>
<string name="maxTimePlaying">Max time spent playing</string>
<string name="now">now</string>
</resources>