flatter i18n DB implementation

JS translations broken atm
i18nv2
Thibault Duplessis 2020-02-12 10:46:59 -06:00
parent ed7ae6a48c
commit 4654058dcb
28 changed files with 2036 additions and 1842 deletions

View File

@ -23,19 +23,19 @@ final class Dasher(env: Env) extends LilaController(env) {
trans.boardSize,
trans.pieceSet,
trans.preferences.zenMode
)
).map(_.key)
private val translationsAnon = List(
trans.signIn,
trans.signUp
) ::: translationsBase
).map(_.key) ::: translationsBase
private val translationsAuth = List(
trans.profile,
trans.inbox,
trans.preferences.preferences,
trans.logOut
) ::: translationsBase
).map(_.key) ::: translationsBase
private def translations(implicit ctx: Context) =
lila.i18n.JsDump.keysToObject(
@ -43,7 +43,7 @@ final class Dasher(env: Env) extends LilaController(env) {
ctx.lang
) ++ lila.i18n.JsDump.keysToObject(
// the language settings should never be in a totally foreign language
List(trans.language),
List(trans.language.key),
if (I18nLangPicker.allFromRequestHeaders(ctx.req).has(ctx.lang)) ctx.lang
else I18nLangPicker.bestFromRequestHeaders(ctx.req) | enLang
)

View File

@ -597,7 +597,7 @@ abstract private[controllers] class LilaController(val env: Env)
.mapValues { errors =>
JsArray {
errors.map { e =>
JsString(lila.i18n.Translator.txt.literal(e.message, lila.i18n.I18nDb.Site, e.args, lang))
JsString(lila.i18n.Translator.txt.literal(e.message, e.args, lang))
}
}
}

View File

@ -5,7 +5,6 @@ import play.api.data._
import lila.api.Context
import lila.app.ui.ScalatagsTemplate._
import lila.i18n.I18nDb
trait FormHelper { self: I18nHelper =>
@ -14,7 +13,7 @@ trait FormHelper { self: I18nHelper =>
def errMsg(form: Form[_])(implicit ctx: Context): Frag = errMsg(form.errors)
def errMsg(error: FormError)(implicit ctx: Context): Frag =
p(cls := "error")(transKey(error.message, I18nDb.Site, error.args))
p(cls := "error")(transKey(error.message, error.args))
def errMsg(errors: Seq[FormError])(implicit ctx: Context): Frag =
errors map errMsg
@ -39,7 +38,7 @@ trait FormHelper { self: I18nHelper =>
private def errors(errs: Seq[FormError])(implicit ctx: Context): Frag = errs map error
private def errors(field: Field)(implicit ctx: Context): Frag = errors(field.errors)
private def error(err: FormError)(implicit ctx: Context): Frag =
p(cls := "error")(transKey(err.message, I18nDb.Site, err.args))
p(cls := "error")(transKey(err.message, err.args))
private def validationModifiers(field: Field): Seq[Modifier] = field.constraints collect {
/* Can't use constraint.required, because it applies to optional fields

View File

@ -5,22 +5,19 @@ import play.api.libs.json.JsObject
import play.api.i18n.Lang
import lila.app.ui.ScalatagsTemplate._
import lila.i18n.{ I18nDb, I18nKey, JsDump, LangList, TimeagoLocales, Translator }
import lila.i18n.{ I18nKey, JsDump, LangList, MessageKey, TimeagoLocales, Translator }
import lila.user.UserContext
trait I18nHelper extends HasEnv with UserContext.ToLang {
def transKey(key: String, db: I18nDb.Ref, args: Seq[Any] = Nil)(implicit lang: Lang): Frag =
Translator.frag.literal(key, db, args, lang)
def transKey(key: MessageKey, args: Seq[Any] = Nil)(implicit lang: Lang): Frag =
Translator.frag.literal(key, args, lang)
def i18nJsObject(keys: Seq[I18nKey])(implicit lang: Lang): JsObject =
def i18nJsObject(keys: Seq[MessageKey])(implicit lang: Lang): JsObject =
JsDump.keysToObject(keys, lang)
def i18nOptionJsObject(keys: Option[I18nKey]*)(implicit lang: Lang): JsObject =
JsDump.keysToObject(keys.flatten, lang)
def i18nFullDbJsObject(db: I18nDb.Ref)(implicit lang: Lang): JsObject =
JsDump.dbToObject(db, lang)
JsDump.keysToObject(keys.collect { case Some(k) => k.key }, lang)
def timeagoLocaleScript(implicit ctx: lila.api.Context): String = {
TimeagoLocales.js.get(ctx.lang.code) orElse

View File

@ -1,14 +1,15 @@
package views.html.analyse
import lila.api.Context
import play.api.i18n.Lang
import lila.app.templating.Environment._
import lila.i18n.{ I18nKeys => trans }
private object jsI18n {
def apply()(implicit ctx: Context) = i18nJsObject(translations)
def apply()(implicit lang: Lang) = i18nJsObject(i18nKeys)
private val translations = List(
private val i18nKeys = List(
trans.flipBoard,
trans.gameAborted,
trans.checkmate,
@ -151,5 +152,5 @@ private object jsI18n {
trans.opening,
trans.middlegame,
trans.endgame
)
).map(_.key)
}

View File

@ -235,7 +235,7 @@ object layout {
"playing" -> ctx.onlineFriends.playing,
"patrons" -> ctx.onlineFriends.patrons,
"studying" -> ctx.onlineFriends.studying,
"i18n" -> i18nJsObject(List(trans.nbFriendsOnline))
"i18n" -> i18nJsObject(i18nKeys)
)
)
)(
@ -329,4 +329,6 @@ object layout {
)
)
}
private val i18nKeys = List(trans.nbFriendsOnline.key)
}

View File

@ -29,10 +29,10 @@ object bits {
"duration" -> ctx.pref.animationFactor * animationDuration.toMillis
),
"is3d" -> ctx.pref.is3d,
"i18n" -> i18nJsObject(translations)(ctxLang(ctx))
"i18n" -> i18nJsObject(i18nKeyes)
)
private val translations = List(
private val i18nKeyes = List(
trans.setTheBoard,
trans.boardEditor,
trans.startPosition,
@ -51,5 +51,5 @@ object bits {
trans.playWithAFriend,
trans.analysis,
trans.toStudy
)
).map(_.key)
}

View File

@ -3,7 +3,7 @@ package views.html.board
import play.api.i18n.Lang
import lila.app.templating.Environment._
import lila.i18n.{ I18nKey, I18nKeys => trans }
import lila.i18n.{ MessageKey, I18nKeys => trans }
object userAnalysisI18n {
@ -24,7 +24,7 @@ object userAnalysisI18n {
}
)
private val baseTranslations: Vector[I18nKey] = Vector(
private val baseTranslations: Vector[MessageKey] = Vector(
trans.analysis,
trans.flipBoard,
trans.backToGame,
@ -100,9 +100,9 @@ object userAnalysisI18n {
// gamebook
trans.findTheBestMoveForWhite,
trans.findTheBestMoveForBlack
)
).map(_.key)
private val cevalTranslations = Vector(
private val cevalTranslations: Vector[MessageKey] = Vector(
// also uses gameOver
trans.depthX,
trans.usingServerAnalysis,
@ -122,9 +122,9 @@ object userAnalysisI18n {
trans.multipleLines,
trans.cpus,
trans.memory
)
).map(_.key)
private val explorerTranslations = Vector(
private val explorerTranslations: Vector[MessageKey] = Vector(
// also uses gameOver, checkmate, stalemate, draw, variantEnding
trans.openingExplorerAndTablebase,
trans.openingExplorer,
@ -156,21 +156,21 @@ object userAnalysisI18n {
trans.winPreventedBy50MoveRule,
trans.lossSavedBy50MoveRule,
trans.allSet
)
).map(_.key)
private val forecastTranslations = Vector(
private val forecastTranslations: Vector[MessageKey] = Vector(
trans.conditionalPremoves,
trans.addCurrentVariation,
trans.playVariationToCreateConditionalPremoves,
trans.noConditionalPremoves,
trans.playX,
trans.andSaveNbPremoveLines
)
).map(_.key)
private val advantageChartTranslations = Vector(
private val advantageChartTranslations: Vector[MessageKey] = Vector(
trans.advantage,
trans.opening,
trans.middlegame,
trans.endgame
)
).map(_.key)
}

View File

@ -11,16 +11,18 @@ import controllers.routes
object index {
import trans.learn.{ play => _, _ }
def apply(data: Option[play.api.libs.json.JsValue])(implicit ctx: Context) =
views.html.base.layout(
title = s"${trans.learn.learnChess.txt()} - ${trans.learn.byPlaying.txt()}",
title = s"${learnChess.txt()} - ${byPlaying.txt()}",
moreJs = frag(
jsAt(s"compiled/lichess.learn${isProd ?? (".min")}.js"),
embedJsUnsafe(s"""$$(function() {
LichessLearn(document.getElementById('learn-app'), ${safeJsonValue(
Json.obj(
"data" -> data,
"i18n" -> i18nFullDbJsObject(lila.i18n.I18nDb.Learn)
"i18n" -> i18nJsObject(i18nKeys)
)
)})})""")
),
@ -28,8 +30,7 @@ LichessLearn(document.getElementById('learn-app'), ${safeJsonValue(
openGraph = lila.app.ui
.OpenGraph(
title = "Learn chess by playing",
description =
"You don't know anything about chess? Excellent! Let's have fun and learn to play chess!",
description = "You don't know much about chess? Excellent! Let's have fun and learn to play chess!",
url = s"$netBaseUrl${routes.Learn.index}"
)
.some,
@ -37,4 +38,177 @@ LichessLearn(document.getElementById('learn-app'), ${safeJsonValue(
) {
main(id := "learn-app")
}
private val i18nKeys: List[lila.i18n.MessageKey] =
List(
menu,
progressX,
resetMyProgress,
youWillLoseAllYourProgress,
trans.learn.play,
chessPieces,
theRook,
itMovesInStraightLines,
rookIntro,
rookGoal,
grabAllTheStars,
theFewerMoves,
useTwoRooks,
rookComplete,
theBishop,
itMovesDiagonally,
bishopIntro,
youNeedBothBishops,
bishopComplete,
theQueen,
queenCombinesRookAndBishop,
queenIntro,
queenComplete,
theKing,
theMostImportantPiece,
kingIntro,
theKingIsSlow,
lastOne,
kingComplete,
theKnight,
itMovesInAnLShape,
knightIntro,
knightsHaveAFancyWay,
knightsCanJumpOverObstacles,
knightComplete,
thePawn,
itMovesForwardOnly,
pawnIntro,
pawnsMoveOneSquareOnly,
mostOfTheTimePromotingToAQueenIsBest,
pawnsMoveForward,
captureThenPromote,
useAllThePawns,
aPawnOnTheSecondRank,
grabAllTheStarsNoNeedToPromote,
pawnComplete,
pawnPromotion,
yourPawnReachedTheEndOfTheBoard,
itNowPromotesToAStrongerPiece,
selectThePieceYouWant,
fundamentals,
capture,
takeTheEnemyPieces,
captureIntro,
takeTheBlackPieces,
takeTheBlackPiecesAndDontLoseYours,
captureComplete,
protection,
keepYourPiecesSafe,
protectionIntro,
protectionComplete,
escape,
noEscape,
dontLetThemTakeAnyUndefendedPiece,
combat,
captureAndDefendPieces,
combatIntro,
combatComplete,
checkInOne,
attackTheOpponentsKing,
checkInOneIntro,
checkInOneGoal,
checkInOneComplete,
outOfCheck,
defendYourKing,
outOfCheckIntro,
escapeWithTheKing,
theKingCannotEscapeButBlock,
youCanGetOutOfCheckByTaking,
thisKnightIsCheckingThroughYourDefenses,
escapeOrBlock,
outOfCheckComplete,
mateInOne,
defeatTheOpponentsKing,
mateInOneIntro,
attackYourOpponentsKing,
mateInOneComplete,
intermediate,
boardSetup,
howTheGameStarts,
boardSetupIntro,
thisIsTheInitialPosition,
firstPlaceTheRooks,
thenPlaceTheKnights,
placeTheBishops,
placeTheQueen,
placeTheKing,
pawnsFormTheFrontLine,
boardSetupComplete,
castling,
theSpecialKingMove,
castlingIntro,
castleKingSide,
castleQueenSide,
theKnightIsInTheWay,
castleKingSideMovePiecesFirst,
castleQueenSideMovePiecesFirst,
youCannotCastleIfMoved,
youCannotCastleIfAttacked,
findAWayToCastleKingSide,
findAWayToCastleQueenSide,
castlingComplete,
enPassant,
theSpecialPawnMove,
enPassantIntro,
blackJustMovedThePawnByTwoSquares,
enPassantOnlyWorksImmediately,
enPassantOnlyWorksOnFifthRank,
takeAllThePawnsEnPassant,
enPassantComplete,
stalemate,
theGameIsADraw,
stalemateIntro,
stalemateGoal,
stalemateComplete,
advanced,
pieceValue,
evaluatePieceStrength,
pieceValueIntro,
queenOverBishop,
takeThePieceWithTheHighestValue,
pieceValueComplete,
checkInTwo,
twoMovesToGiveCheck,
checkInTwoIntro,
checkInTwoGoal,
checkInTwoComplete,
whatNext,
youKnowHowToPlayChess,
register,
getAFreeLichessAccount,
practice,
learnCommonChessPositions,
puzzles,
exerciseYourTacticalSkills,
videos,
watchInstructiveChessVideos,
playPeople,
opponentsFromAroundTheWorld,
playMachine,
testYourSkillsWithTheComputer,
letsGo,
stageX,
awesome,
excellent,
greatJob,
perfect,
outstanding,
wayToGo,
yesYesYes,
youreGoodAtThis,
nailedIt,
rightOn,
stageXComplete,
yourScore,
next,
backToMenu,
puzzleFailed,
retry
).map(_.key)
}

View File

@ -32,7 +32,7 @@ object home {
"remainingSeconds" -> (pb.remainingSeconds + 3)
)
},
"i18n" -> i18nJsObject(translations)
"i18n" -> i18nJsObject(i18nKeys)
)
)}"""
)
@ -186,7 +186,7 @@ object home {
}
}
private val translations = List(
private val i18nKeys = List(
trans.realTime,
trans.correspondence,
trans.nbGamesInPlay,
@ -212,7 +212,7 @@ object home {
trans.lobby,
trans.custom,
trans.anonymous
)
).map(_.key)
private val nbPlaceholder = strong("--,---")
}

View File

@ -28,9 +28,9 @@ object msg {
main(cls := "box msg-app")
}
def jsI18n(implicit ctx: Context) = i18nJsObject(translations)
def jsI18n(implicit ctx: Context) = i18nJsObject(i18nKeys)
private val translations = List(
private val i18nKeys = List(
trans.inbox,
trans.challengeToPlay,
trans.block,
@ -44,5 +44,5 @@ object msg {
trans.discussions,
trans.today,
trans.yesterday
)
).map(_.key)
}

View File

@ -1,7 +1,8 @@
package views
package html.puzzle
import lila.api.Context
import play.api.i18n.Lang
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
@ -20,9 +21,9 @@ object bits {
dataLastmove := lastMove
)(cgWrapContent)
def jsI18n()(implicit ctx: Context) = i18nJsObject(translations)
def jsI18n()(implicit lang: Lang) = i18nJsObject(i18nKeys)
private val translations = List(
private val i18nKeys = List(
trans.yourPuzzleRatingX,
trans.goodMove,
trans.butYouCanDoBetter,
@ -65,5 +66,5 @@ object bits {
trans.gameOver,
trans.inLocalBrowser,
trans.toggleLocalEvaluation
)
).map(_.key)
}

View File

@ -28,13 +28,10 @@ object show {
analyseNvuiTag,
embedJsUnsafe(s"""lichess=window.lichess||{};lichess.relay=${safeJsonValue(
Json.obj(
"relay" -> data.relay,
"study" -> data.study,
"data" -> data.analysis,
"i18n" -> {
board.userAnalysisI18n(withAdvantageChart = true) ++
i18nFullDbJsObject(lila.i18n.I18nDb.Study)
},
"relay" -> data.relay,
"study" -> data.study,
"data" -> data.analysis,
"i18n" -> views.html.study.jsI18n(),
"tagTypes" -> lila.study.PgnTags.typesToString,
"userId" -> ctx.userId,
"chat" -> chatOption.map(c =>

View File

@ -1,12 +1,13 @@
package views.html.round
import lila.api.Context
import play.api.i18n.Lang
import lila.app.templating.Environment._
import lila.i18n.{ I18nKeys => trans }
object jsI18n {
def apply(g: lila.game.Game)(implicit ctx: Context) = i18nJsObject {
def apply(g: lila.game.Game)(implicit lang: Lang) = i18nJsObject {
baseTranslations ++ {
if (g.isCorrespondence) correspondenceTranslations
else realtimeTranslations
@ -21,21 +22,21 @@ object jsI18n {
trans.oneDay,
trans.nbDays,
trans.nbHours
)
).map(_.key)
private val realtimeTranslations = Vector(trans.nbSecondsToPlayTheFirstMove)
private val realtimeTranslations = Vector(trans.nbSecondsToPlayTheFirstMove).map(_.key)
private val variantTranslations = Vector(
trans.kingInTheCenter,
trans.threeChecks,
trans.variantEnding
)
).map(_.key)
private val tournamentTranslations = Vector(
trans.backToTournament,
trans.viewTournament,
trans.standing
)
).map(_.key)
private val baseTranslations = Vector(
trans.flipBoard,
@ -89,5 +90,5 @@ object jsI18n {
trans.youPlayTheWhitePieces,
trans.youPlayTheBlackPieces,
trans.itsYourTurn
)
).map(_.key)
}

View File

@ -1,5 +1,7 @@
package views.html.simul
import play.api.i18n.Lang
import lila.api.Context
import lila.app.templating.Environment._
import lila.app.ui.ScalatagsTemplate._
@ -11,7 +13,7 @@ object bits {
def link(simulId: lila.simul.Simul.ID): Frag =
a(href := routes.Simul.show(simulId))("Simultaneous exhibition")
def jsI18n()(implicit ctx: Context) = i18nJsObject(baseTranslations)
def jsI18n()(implicit lang: Lang) = i18nJsObject(baseTranslations)
def notFound()(implicit ctx: Context) =
views.html.base.layout(
@ -69,5 +71,5 @@ object bits {
trans.by,
trans.signIn,
trans.mustBeInTeam
)
).map(_.key)
}

View File

@ -22,18 +22,9 @@ object ratingDistribution {
jsTag("chart/ratingDistribution.js"),
embedJsUnsafe(s"""lichess.ratingDistributionChart(${safeJsonValue(
Json.obj(
"freq" -> data,
"myRating" -> ctx.me.map { me =>
me.perfs(perfType).intRating
},
"i18n" -> i18nJsObject(
List(
trans.players,
trans.yourRating,
trans.cumulative,
trans.glicko2Rating
)
)
"freq" -> data,
"myRating" -> ctx.me.map(_.perfs(perfType).intRating),
"i18n" -> i18nJsObject(i18nKeys)
)
)})""")
)
@ -84,4 +75,11 @@ object ratingDistribution {
)
}
private val i18nKeys =
List(
trans.players,
trans.yourRating,
trans.cumulative,
trans.glicko2Rating
).map(_.key)
}

View File

@ -9,17 +9,117 @@ object jsI18n {
def apply()(implicit lang: Lang) =
views.html.board.userAnalysisI18n(withAdvantageChart = true) ++
i18nFullDbJsObject(lila.i18n.I18nDb.Study) ++
i18nJsObject(translations)
i18nJsObject(i18nKeys)
private val translations = Vector(
trans.name,
trans.white,
trans.black,
trans.variant,
trans.clearBoard,
trans.startPosition,
trans.cancel,
trans.chat
)
val i18nKeys: List[lila.i18n.MessageKey] = {
import trans.study._
List(
trans.name,
trans.white,
trans.black,
trans.variant,
trans.clearBoard,
trans.startPosition,
trans.cancel,
trans.chat,
addNewChapter,
addMembers,
inviteToTheStudy,
pleaseOnlyInvitePeopleYouKnow,
searchByUsername,
spectator,
contributor,
kick,
leaveTheStudy,
youAreNowAContributor,
youAreNowASpectator,
pgnTags,
like,
newTag,
commentThisPosition,
commentThisMove,
annotateWithGlyphs,
theChapterIsTooShortToBeAnalysed,
onlyContributorsCanRequestAnalysis,
getAFullComputerAnalysis,
makeSureTheChapterIsComplete,
allSyncMembersRemainOnTheSamePosition,
shareChanges,
playing,
first,
previous,
next,
last,
shareAndExport,
cloneStudy,
studyPgn,
chapterPgn,
studyUrl,
currentChapterUrl,
youCanPasteThisInTheForumToEmbedTheChapter,
startAtInitialPosition,
startAtX,
embedThisChapter,
readMoreAboutEmbeddingAStudyChapter,
open,
xBroughtToYouByY,
studyNotFound,
editChapter,
newChapter,
orientation,
analysisMode,
pinnedChapterComment,
saveChapter,
clearAnnotations,
deleteChapter,
deleteThisChapter,
clearAllCommentsInThisChapter,
rightUnderTheBoard,
noPinnedComment,
normalAnalysis,
hideNextMoves,
interactiveLesson,
chapterX,
empty,
startFromInitialPosition,
editor,
startFromCustomPosition,
loadAGameByUrl,
loadAPositionFromFen,
loadAGameFromPgn,
automatic,
urlOfTheGame,
loadAGameFromXOrY,
createChapter,
configureLiveBroadcast,
createStudy,
editStudy,
visibility,
public,
`private`,
unlisted,
inviteOnly,
allowCloning,
nobody,
onlyMe,
contributors,
members,
everyone,
enableSync,
yesKeepEveryoneOnTheSamePosition,
noLetPeopleBrowseFreely,
pinnedStudyComment,
start,
save,
clearChat,
deleteTheStudyChatHistory,
deleteStudy,
deleteTheEntireStudy,
whereDoYouWantToStudyThat,
nbChapters,
nbGames,
nbMembers,
pasteYourPgnTextHereUpToNbGames
).map(_.key)
}
}

View File

@ -41,9 +41,9 @@ object bits {
}
)
def jsI18n(implicit ctx: Context) = i18nJsObject(translations)
def jsI18n(implicit ctx: Context) = i18nJsObject(i18nKeys)
private val translations = List(
private val i18nKeys = List(
trans.standing,
trans.starting,
trans.tournamentIsStarting,
@ -70,5 +70,5 @@ object bits {
trans.averageOpponent,
trans.ratedTournament,
trans.casualTournament
)
).map(_.key)
}

View File

@ -20,7 +20,7 @@ function keyListFrom(name) {
const keys = strings.concat(plurals);
resolve({
name: name,
code: keys.map(k => 'val `' + k + '` = new I18nKey("' + k + '", ' + ucfirst(name) + ')').join('\n') + '\n',
code: keys.map(k => 'val `' + k + '` = new I18nKey("' + (name == 'site' ? '' : xmlName(name) + ':') + k + '")').join('\n') + '\n',
});
}));
});
@ -35,8 +35,6 @@ Promise.all(dbs.map(keyListFrom)).then(objs => {
const code = `// Generated with bin/trans-dump.js
package lila.i18n
import I18nDb.{ ${dbs.map(ucfirst).sort().join(', ')} }
// format: OFF
object I18nKeys {

View File

@ -77,7 +77,7 @@ lazy val i18n = module("i18n",
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 perfStat search".split(' ').toList,
compileTo = (sourceManaged in Compile).value / "messages"
compileTo = (sourceManaged in Compile).value
)
}.taskValue,
scalacOptions += "-P:silencer:pathFilters=modules/i18n/target"

View File

@ -33,7 +33,7 @@ final class JsonView(
def apply(a: AllChallenges, lang: Lang): JsObject = Json.obj(
"in" -> a.in.map(apply(Direction.In.some)),
"out" -> a.out.map(apply(Direction.Out.some)),
"i18n" -> translations(lang)
"i18n" -> lila.i18n.JsDump.keysToObject(i18nKeys, lang)
)
def show(challenge: Challenge, socketVersion: SocketVersion, direction: Option[Direction]) = Json.obj(
@ -79,17 +79,13 @@ final class JsonView(
if (c.variant == chess.variant.FromPosition) '*'
else c.perfType.iconChar
private def translations(lang: Lang) =
lila.i18n.JsDump.keysToObject(
List(
trans.rated,
trans.casual,
trans.waiting,
trans.accept,
trans.decline,
trans.viewInFullSize,
trans.cancel
),
lang
)
private val i18nKeys = List(
trans.rated,
trans.casual,
trans.waiting,
trans.accept,
trans.decline,
trans.viewInFullSize,
trans.cancel
).map(_.key)
}

View File

@ -1,71 +0,0 @@
package lila.i18n
import play.api.i18n.Lang
object I18nDb {
sealed trait Ref
case object Site extends Ref
case object Arena extends Ref
case object Emails extends Ref
case object Learn extends Ref
case object Activity extends Ref
case object Coordinates extends Ref
case object Study extends Ref
case object Clas extends Ref
case object Contact extends Ref
case object Patron extends Ref
case object Coach extends Ref
case object Broadcast extends Ref
case object Streamer extends Ref
case object Tfa extends Ref
case object Settings extends Ref
case object Preferences extends Ref
case object Team extends Ref
case object PerfStat extends Ref
case object Search extends Ref
val site: Messages = lila.i18n.db.site.Registry.load
val arena: Messages = lila.i18n.db.arena.Registry.load
val emails: Messages = lila.i18n.db.emails.Registry.load
val learn: Messages = lila.i18n.db.learn.Registry.load
val activity: Messages = lila.i18n.db.activity.Registry.load
val coordinates: Messages = lila.i18n.db.coordinates.Registry.load
val study: Messages = lila.i18n.db.study.Registry.load
val clas: Messages = lila.i18n.db.clas.Registry.load
val contact: Messages = lila.i18n.db.contact.Registry.load
val patron: Messages = lila.i18n.db.patron.Registry.load
val coach: Messages = lila.i18n.db.coach.Registry.load
val broadcast: Messages = lila.i18n.db.broadcast.Registry.load
val streamer: Messages = lila.i18n.db.streamer.Registry.load
val tfa: Messages = lila.i18n.db.tfa.Registry.load
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
val search: Messages = lila.i18n.db.search.Registry.load
def apply(ref: Ref): Messages = ref match {
case Site => site
case Arena => arena
case Emails => emails
case Learn => learn
case Activity => activity
case Coordinates => coordinates
case Study => study
case Clas => clas
case Contact => contact
case Patron => patron
case Coach => coach
case Broadcast => broadcast
case Streamer => streamer
case Tfa => tfa
case Settings => settings
case Preferences => preferences
case Team => team
case PerfStat => perfStat
case Search => search
}
val langs: Set[Lang] = site.keys.toSet
}

View File

@ -3,21 +3,21 @@ package lila.i18n
import play.api.i18n.Lang
import scalatags.Text.RawFrag
final class I18nKey(val key: String, val db: I18nDb.Ref) {
final class I18nKey(val key: String) {
def apply(args: Any*)(implicit lang: Lang): RawFrag =
Translator.frag.literal(key, db, args, lang)
Translator.frag.literal(key, args, lang)
def plural(count: Count, args: Any*)(implicit lang: Lang): RawFrag =
Translator.frag.plural(key, db, count, args, lang)
Translator.frag.plural(key, count, args, lang)
def pluralSame(count: Int)(implicit lang: Lang): RawFrag = plural(count, count)
def txt(args: Any*)(implicit lang: Lang): String =
Translator.txt.literal(key, db, args, lang)
Translator.txt.literal(key, args, lang)
def pluralTxt(count: Count, args: Any*)(implicit lang: Lang): String =
Translator.txt.plural(key, db, count, args, lang)
Translator.txt.plural(key, count, args, lang)
def pluralSameTxt(count: Int)(implicit lang: Lang): String = pluralTxt(count, count)
}

File diff suppressed because it is too large Load Diff

View File

@ -29,12 +29,12 @@ object I18nLangPicker {
Lang get str flatMap findCloser
private val defaultByLanguage: Map[String, Lang] =
I18nDb.langs.foldLeft(Map.empty[String, Lang]) {
Registry.langs.foldLeft(Map.empty[String, Lang]) {
case (acc, lang) => acc + (lang.language -> lang)
}
def findCloser(to: Lang): Option[Lang] =
if (I18nDb.langs contains to) Some(to)
if (Registry.langs contains to) Some(to)
else
defaultByLanguage.get(to.language) orElse
lichessCodes.get(to.language)

View File

@ -2,7 +2,7 @@ package lila.i18n
import java.io._
import scala.concurrent.Future
import scala.jdk.CollectionConverters._
// import scala.jdk.CollectionConverters._
import play.api.libs.json.{ JsObject, JsString }
import play.api.i18n.Lang
@ -20,7 +20,7 @@ final private[i18n] class JsDump(path: String)(implicit ec: scala.concurrent.Exe
private def dumpFromKey(keys: Set[String], lang: Lang): String =
keys
.map { key =>
""""%s":"%s"""".format(key, escape(Translator.txt.literal(key, I18nDb.Site, Nil, lang)))
""""%s":"%s"""".format(key, escape(Translator.txt.literal(key, Nil, lang)))
}
.mkString("{", ",", "}")
@ -34,9 +34,10 @@ final private[i18n] class JsDump(path: String)(implicit ec: scala.concurrent.Exe
.mkString("[", ",", "]")
)
private def writeFullJson() = I18nDb.langs foreach { lang =>
val code = dumpFromKey(I18nDb.site(defaultLang).keySet.asScala.toSet, lang)
writeFile(new File("%s/%s.all.json".format(pathFile.getCanonicalPath, lang.code)), code)
private def writeFullJson() = Registry.langs foreach { lang =>
???
// val code = dumpFromKey(I18nDb.site(defaultLang).keySet.asScala.toSet, lang)
// writeFile(new File("%s/%s.all.json".format(pathFile.getCanonicalPath, lang.code)), code)
}
private def writeFile(file: File, content: String) = {
@ -64,7 +65,7 @@ object JsDump {
private type JsTrans = Iterable[(String, JsString)]
private def translatedJs(k: String, t: Translation): JsTrans = t match {
private def translatedJs(k: MessageKey, t: Translation): JsTrans = t match {
case literal: Simple => List(k -> JsString(literal.message))
case literal: Escaped => List(k -> JsString(literal.message))
case plurals: Plurals =>
@ -73,21 +74,9 @@ object JsDump {
}
}
def keysToObject(keys: Seq[I18nKey], lang: Lang): JsObject = JsObject {
def keysToObject(keys: Seq[MessageKey], lang: Lang): JsObject = JsObject {
keys.flatMap { k =>
Translator.findTranslation(k.key, k.db, lang).fold[JsTrans](Nil) { translatedJs(k.key, _) }
Translator.findTranslation(k, lang).fold[JsTrans](Nil) { translatedJs(k, _) }
}
}
val emptyMessages: MessageMap = new java.util.HashMap()
def dbToObject(ref: I18nDb.Ref, lang: Lang): JsObject =
I18nDb(ref).get(defaultLang) ?? { defaultMsgs =>
JsObject {
val msgs = I18nDb(ref).get(lang) | emptyMessages
defaultMsgs.asScala.flatMap {
case (k, v) => translatedJs(k, msgs.getOrDefault(k, v))
}
}
}
}

View File

@ -8,20 +8,19 @@ import lila.common.String.html.escapeHtml
object Translator {
object frag {
def literal(key: MessageKey, db: I18nDb.Ref, args: Seq[Any], lang: Lang): RawFrag =
translate(key, db, lang, I18nQuantity.Other /* grmbl */, args)
def literal(key: MessageKey, args: Seq[Any], lang: Lang): RawFrag =
translate(key, lang, I18nQuantity.Other /* grmbl */, args)
def plural(key: MessageKey, db: I18nDb.Ref, count: Count, args: Seq[Any], lang: Lang): RawFrag =
translate(key, db, lang, I18nQuantity(lang, count), args)
def plural(key: MessageKey, count: Count, args: Seq[Any], lang: Lang): RawFrag =
translate(key, lang, I18nQuantity(lang, count), args)
private def translate(
key: MessageKey,
db: I18nDb.Ref,
lang: Lang,
quantity: I18nQuantity,
args: Seq[Any]
): RawFrag =
findTranslation(key, db, lang) flatMap { translation =>
findTranslation(key, lang) flatMap { translation =>
val htmlArgs = escapeArgs(args)
try {
translation match {
@ -31,7 +30,7 @@ object Translator {
}
} catch {
case e: Exception =>
logger.warn(s"Failed to format html $db/$lang/$key -> $translation (${args.toList})", e)
logger.warn(s"Failed to format html $lang/$key -> $translation (${args.toList})", e)
Some(RawFrag(key))
}
} getOrElse {
@ -49,20 +48,19 @@ object Translator {
object txt {
def literal(key: MessageKey, db: I18nDb.Ref, args: Seq[Any], lang: Lang): String =
translate(key, db, lang, I18nQuantity.Other /* grmbl */, args)
def literal(key: MessageKey, args: Seq[Any], lang: Lang): String =
translate(key, lang, I18nQuantity.Other /* grmbl */, args)
def plural(key: MessageKey, db: I18nDb.Ref, count: Count, args: Seq[Any], lang: Lang): String =
translate(key, db, lang, I18nQuantity(lang, count), 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,
db: I18nDb.Ref,
lang: Lang,
quantity: I18nQuantity,
args: Seq[Any]
): String =
findTranslation(key, db, lang) flatMap { translation =>
findTranslation(key, lang) flatMap { translation =>
try {
translation match {
case literal: Simple => Some(literal.formatTxt(args))
@ -71,16 +69,16 @@ object Translator {
}
} catch {
case e: Exception =>
logger.warn(s"Failed to format txt $db/$lang/$key -> $translation (${args.toList})", e)
logger.warn(s"Failed to format txt $lang/$key -> $translation (${args.toList})", e)
Some(key)
}
} getOrElse {
logger.info(s"No translation found for $quantity $db/$lang/$key in $lang")
logger.info(s"No translation found for $quantity $lang/$key in $lang")
key
}
}
private[i18n] def findTranslation(key: MessageKey, db: I18nDb.Ref, lang: Lang): Option[Translation] =
I18nDb(db).get(lang).flatMap(t => Option(t get key)) orElse
I18nDb(db).get(defaultLang).flatMap(t => Option(t get key))
private[i18n] def findTranslation(key: MessageKey, lang: Lang): Option[Translation] =
Registry.all.get(lang).flatMap(t => Option(t get key)) orElse
Option(Registry.default.get(key))
}

View File

@ -5,107 +5,77 @@ import scala.xml.XML
object MessageCompiler {
def apply(sourceDir: File, destDir: File, dbs: List[String], compileTo: File): Seq[File] =
dbs.flatMap { db =>
doFile(
db = db,
sourceFile = sourceDir / s"$db.xml",
destDir = destDir / db,
compileTo = compileTo / db
)
}
private def doFile(db: String, sourceFile: File, destDir: File, compileTo: File): Seq[File] = {
destDir.mkdirs()
val registry = ("en-GB" -> sourceFile) :: destDir.list.toList
.map { f =>
f.takeWhile('.' !=) -> (destDir / f)
}
.sortBy(_._1)
def apply(sourceDir: File, destDir: File, dbs: List[String], compileTo: File): Seq[File] = {
compileTo.mkdirs()
var translatedLocales = Set.empty[String]
val res = for {
entry <- registry
compilable <- {
val (locale, file) = entry
val compileToFile = compileTo / s"$locale.scala"
if (!isFileEmpty(file)) {
translatedLocales = translatedLocales + locale
if (file.lastModified > compileToFile.lastModified) {
printToFile(compileToFile)(render(db, locale, file))
}
Some(compileToFile)
} else None
val locales: List[String] = "en-GB" ::
(destDir / "site").list.toList.map { _.takeWhile('.' !=) }.sorted
val localeFiles = locales
.map { locale =>
locale -> writeLocale(locale, sourceDir, destDir, compileTo, dbs)
}
} yield compilable
writeRegistry(db, compileTo, translatedLocales) :: res
.filter(_._2.exists)
writeRegistry(compileTo, localeFiles.map(_._1)) :: localeFiles.map(_._2)
}
private def isFileEmpty(f: File) = {
Source.fromFile(f, "UTF-8").getLines.drop(2).next == "<resources></resources>"
}
// dbs.flatMap { db =>
// doFile(
// db = db,
// sourceFile = sourceDir / s"$db.xml",
// destDir = destDir / db,
// compileTo = compileTo / db
// )
// }
private def packageName(db: String) = if (db == "class") "clas" else db
private def writeRegistry(db: String, compileTo: File, locales: Iterable[String]) = {
val file = compileTo / "Registry.scala"
printToFile(file) {
val content = locales.map { locale =>
s"""Lang("${locale.replace("-", "\",\"")}")->`$locale`.load"""
} mkString ",\n"
s"""package lila.i18n
package db.${packageName(db)}
import play.api.i18n.Lang
// format: OFF
private[i18n] object Registry {
def load = Map[Lang, java.util.HashMap[MessageKey, Translation]]($content)
}
"""
}
file
}
private def ucfirst(str: String) = str(0).toUpper + str.drop(1)
private def toKey(e: scala.xml.Node) = s""""${e.\("@name")}""""
private def escape(str: String) = {
// is someone trying to inject scala code?
if (str contains "\"\"\"") sys error s"Skipped translation: $str"
// crowdin escapes ' and " with \, and encodes &. We'll do it at runtime instead.
else str.replace("\\'", "'").replace("\\\"", "\"")
}
private def render(db: String, locale: String, file: File): String = {
val xml =
try {
XML.loadFile(file)
} catch {
case e: Exception => println(file); throw e;
}
def quote(msg: String) = s"""""\"$msg""\""""
val content = xml.child.collect {
case e if e.label == "string" =>
val safe = escape(e.text)
val translation = escapeHtmlOption(safe) match {
case None => s"""new Simple(\"\"\"$safe\"\"\")"""
case Some(escaped) => s"""new Escaped(\"\"\"$safe\"\"\",\"\"\"$escaped\"\"\")"""
private def writeLocale(
locale: String,
sourceDir: File,
destDir: File,
compileTo: File,
dbs: List[String]
): File = {
val scalaFile = compileTo / s"$locale.scala"
val xmlFiles =
if (locale == "en-GB") dbs.map { db =>
db -> (sourceDir / s"$db.xml")
} else
dbs.map { db =>
db -> (destDir / db / s"$locale.xml")
}
s"""m.put(${toKey(e)},$translation)"""
case e if e.label == "plurals" =>
val items: Map[String, String] = e.child
.filter(_.label == "item")
.map { i =>
ucfirst(i.\("@quantity").toString) -> s"""\"\"\"${escape(i.text)}\"\"\""""
}
.toMap
s"""m.put(${toKey(e)},new Plurals(${pluralMap(items)}))"""
val isNew = xmlFiles.exists {
case (_, file) => !isFileEmpty(file) && file.lastModified > scalaFile.lastModified
}
s"""package lila.i18n
package db.${packageName(db)}
if (!isNew) scalaFile
else
printToFile(scalaFile) {
val puts = xmlFiles flatMap {
case (db, file) =>
val xml =
try {
XML.loadFile(file)
} catch {
case e: Exception => println(file); throw e;
}
xml.child.collect {
case e if e.label == "string" =>
val safe = escape(e.text)
val translation = escapeHtmlOption(safe) match {
case None => s"""new Simple(\"\"\"$safe\"\"\")"""
case Some(escaped) => s"""new Escaped(\"\"\"$safe\"\"\",\"\"\"$escaped\"\"\")"""
}
s"""m.put(${toKey(e, db)},$translation)"""
case e if e.label == "plurals" =>
val items: Map[String, String] = e.child
.filter(_.label == "item")
.map { i =>
ucfirst(i.\("@quantity").toString) -> s"""\"\"\"${escape(i.text)}\"\"\""""
}
.toMap
s"""m.put(${toKey(e, db)},new Plurals(${pluralMap(items)}))"""
}
}
s"""package lila.i18n
import I18nQuantity._
@ -113,12 +83,55 @@ import I18nQuantity._
private object `$locale` {
def load: java.util.HashMap[MessageKey, Translation] = {
val m = new java.util.HashMap[MessageKey, Translation](${content.size + 1})
${content mkString "\n"}
val m = new java.util.HashMap[MessageKey, Translation](${puts.size + 1})
${puts mkString "\n"}
m
}
}
"""
}
}
private def isFileEmpty(file: File) = {
!file.exists() || Source.fromFile(file, "UTF-8").getLines.drop(2).next == "<resources></resources>"
}
private def packageName(db: String) = if (db == "class") "clas" else db
private def writeRegistry(destDir: File, locales: Iterable[String]) =
printToFile(destDir / "Registry.scala") {
val content = locales.map { locale =>
s"""Lang("${locale.replace("-", "\",\"")}")->`$locale`.load"""
} mkString ",\n"
s"""package lila.i18n
import play.api.i18n.Lang
// format: OFF
private object Registry {
val all = Map[Lang, java.util.HashMap[MessageKey, Translation]](\n$content)
val default: java.util.HashMap[MessageKey, Translation] = all(defaultLang)
val langs: Set[Lang] = all.keys.toSet
}
"""
}
private def ucfirst(str: String) = str(0).toUpper + str.drop(1)
private def toKey(e: scala.xml.Node, db: String) =
if (db == "site") s""""${e.\("@name")}""""
else s""""$db:${e.\("@name")}""""
private def quote(msg: String) = s"""""\"$msg""\""""
private def escape(str: String) = {
// is someone trying to inject scala code?
if (str contains "\"\"\"") sys error s"Skipped translation: $str"
// crowdin escapes ' and " with \, and encodes &. We'll do it at runtime instead.
else str.replace("\\'", "'").replace("\\\"", "\"")
}
private def pluralMap(items: Map[String, String]): String =
@ -146,12 +159,13 @@ ${content mkString "\n"}
sb.toString
} else None
private def printToFile(f: File)(content: String): Unit = {
val p = new java.io.PrintWriter(f, "UTF-8")
private def printToFile(file: File)(content: String): File = {
val p = new java.io.PrintWriter(file, "UTF-8")
try {
content.foreach(p.print)
} finally {
p.close()
}
file
}
}