From 45b5f0cfbbf6c045ad774bba1b781b7a688e0612 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Mon, 14 Oct 2024 14:39:18 -0500 Subject: [PATCH 1/9] use javascript assets for client i18n --- .gitignore | 2 +- app/controllers/Dasher.scala | 13 +- app/views/analyse/replay.scala | 2 +- app/views/base/embed.scala | 2 +- app/views/base/page.scala | 2 +- app/views/board.scala | 1 - app/views/insight.scala | 1 - app/views/lobby/home.scala | 46 - app/views/msg.scala | 23 +- app/views/puzzle/ui.scala | 2 +- app/views/relay.scala | 1 - app/views/round/bits.scala | 3 +- app/views/round/player.scala | 1 - app/views/round/watcher.scala | 1 - app/views/study.scala | 9 +- app/views/tv.scala | 2 +- app/views/ui.scala | 4 - app/views/user/show/page.scala | 2 +- bin/schlawg-dev | 3 +- modules/analyse/src/main/ui/AnalyseI18n.scala | 349 -- modules/analyse/src/main/ui/AnalyseUi.scala | 2 + modules/challenge/src/main/JsonView.scala | 15 +- modules/chat/src/main/ChatUi.scala | 11 - .../coordinate/src/main/CoordinateUi.scala | 32 +- modules/notify/src/main/JsonHandlers.scala | 27 +- modules/perfStat/src/main/PerfStatUi.scala | 5 +- modules/practice/src/main/PracticeUi.scala | 5 +- modules/puzzle/src/main/ui/PuzzleBits.scala | 62 +- modules/puzzle/src/main/ui/PuzzleUi.scala | 4 +- modules/racer/src/main/ui/RacerUi.scala | 31 +- modules/relay/src/main/ui/RelayUi.scala | 16 +- modules/round/src/main/ui/RoundI18n.scala | 110 - modules/simul/src/main/ui/SimulShow.scala | 1 - modules/simul/src/main/ui/SimulUi.scala | 18 - modules/storm/src/main/ui/StormUi.scala | 37 +- modules/study/src/main/ui/StudyBits.scala | 131 - modules/swiss/src/main/ui/SwissBitsUi.scala | 32 - modules/swiss/src/main/ui/SwissShow.scala | 8 +- .../src/main/ui/TournamentList.scala | 2 +- .../src/main/ui/TournamentShow.scala | 4 +- .../tournament/src/main/ui/TournamentUi.scala | 53 - modules/ui/src/main/Page.scala | 11 +- modules/user/src/main/ui/UserShow.scala | 7 - modules/web/src/main/ui/BoardEditorUi.scala | 25 +- modules/web/src/main/ui/LearnUi.scala | 178 - modules/web/src/main/ui/layout.scala | 37 +- pnpm-lock.yaml | 602 +- translation/dest/site/de-CH.xml | 18 - translation/dest/site/pa-PK.xml | 1 - ui/.build/package.json | 1 + ui/.build/readme | 4 +- ui/.build/src/build.ts | 7 +- ui/.build/src/clean.ts | 4 + ui/.build/src/copies.ts | 3 +- ui/.build/src/i18n.ts | 214 + ui/.build/src/main.ts | 26 +- ui/.build/src/manifest.ts | 29 +- ui/@types/lichess/i18n.d.ts | 5439 +++++++++++++++++ ui/@types/lichess/index.d.ts | 24 +- ui/analyse/src/ctrl.ts | 2 - ui/analyse/src/explorer/explorerConfig.ts | 30 +- ui/analyse/src/explorer/explorerView.ts | 75 +- ui/analyse/src/explorer/tablebaseView.ts | 27 +- ui/analyse/src/forecast/forecastView.ts | 19 +- ui/analyse/src/interfaces.ts | 2 - ui/analyse/src/plugins/analyse.nvui.ts | 14 +- ui/analyse/src/practice/practiceView.ts | 51 +- ui/analyse/src/retrospect/retroCtrl.ts | 4 - ui/analyse/src/retrospect/retroView.ts | 55 +- ui/analyse/src/serverSideUnderboard.ts | 12 +- ui/analyse/src/start.ts | 2 - ui/analyse/src/study/chapterEditForm.ts | 53 +- ui/analyse/src/study/chapterNewForm.ts | 56 +- .../src/study/gamebook/gamebookButtons.ts | 6 +- .../src/study/gamebook/gamebookPlayCtrl.ts | 1 - .../src/study/gamebook/gamebookPlayView.ts | 25 +- ui/analyse/src/study/inviteForm.ts | 13 +- ui/analyse/src/study/multiBoard.ts | 17 +- ui/analyse/src/study/nextChapter.ts | 2 +- .../src/study/practice/studyPracticeView.ts | 1 - ui/analyse/src/study/relay/relayTourView.ts | 7 +- ui/analyse/src/study/serverEval.ts | 13 +- ui/analyse/src/study/studyChapters.ts | 4 +- ui/analyse/src/study/studyCtrl.ts | 8 +- ui/analyse/src/study/studyForm.ts | 63 +- ui/analyse/src/study/studyMembers.ts | 23 +- ui/analyse/src/study/studyShare.ts | 37 +- ui/analyse/src/study/studyTags.ts | 7 +- ui/analyse/src/study/studyView.ts | 28 +- ui/analyse/src/study/topics.ts | 7 +- ui/analyse/src/treeView/columnView.ts | 2 +- ui/analyse/src/treeView/common.ts | 2 +- ui/analyse/src/treeView/contextMenu.ts | 17 +- ui/analyse/src/treeView/inlineView.ts | 2 +- ui/analyse/src/view/actionMenu.ts | 37 +- ui/analyse/src/view/components.ts | 19 +- ui/analyse/src/view/main.ts | 2 +- ui/analyse/src/view/roundTraining.ts | 20 +- ui/bits/src/bits.user.ts | 9 +- ui/board/src/menu.ts | 10 +- ui/ceval/src/types.ts | 1 - ui/ceval/src/view/main.ts | 40 +- ui/ceval/src/view/settings.ts | 5 +- ui/challenge/src/ctrl.ts | 3 - ui/challenge/src/interfaces.ts | 3 - ui/challenge/src/view.ts | 14 +- ui/chart/src/acpl.ts | 9 +- ui/chart/src/division.ts | 10 +- ui/chart/src/interface.ts | 9 +- ui/chart/src/movetime.ts | 5 +- ui/chat/src/ctrl.ts | 5 - ui/chat/src/discussion.ts | 6 +- ui/chat/src/interfaces.ts | 4 - ui/chat/src/note.ts | 3 +- ui/chat/src/view.ts | 4 +- ui/common/src/controls.ts | 6 +- ui/common/src/i18n.ts | 9 +- ui/common/src/linkPopup.ts | 14 +- ui/common/src/socket.ts | 4 +- ui/coordinateTrainer/src/ctrl.ts | 2 - ui/coordinateTrainer/src/interfaces.ts | 1 - ui/coordinateTrainer/src/side.ts | 60 +- ui/coordinateTrainer/src/view.ts | 31 +- ui/dasher/src/background.ts | 10 +- ui/dasher/src/board.ts | 16 +- ui/dasher/src/ctrl.ts | 3 - ui/dasher/src/interfaces.ts | 4 - ui/dasher/src/langs.ts | 2 +- ui/dasher/src/links.ts | 28 +- ui/dasher/src/piece.ts | 2 +- ui/dasher/src/ping.ts | 4 +- ui/dasher/src/sound.ts | 2 +- ui/editor/src/ctrl.ts | 4 - ui/editor/src/interfaces.ts | 1 - ui/editor/src/view.ts | 43 +- ui/game/src/interfaces.ts | 6 - ui/game/src/view/status.ts | 53 +- ui/insight/src/ctrl.ts | 1 - ui/learn/src/congrats.ts | 27 - ui/learn/src/ctrl.ts | 4 +- ui/learn/src/learn.ts | 5 +- ui/learn/src/mapSideView.ts | 22 +- ui/learn/src/promotionView.ts | 12 +- ui/learn/src/run/congrats.ts | 22 +- ui/learn/src/run/runCtrl.ts | 5 - ui/learn/src/run/runView.ts | 21 +- ui/learn/src/run/stageComplete.ts | 15 +- ui/learn/src/run/stageStarting.ts | 6 +- ui/learn/src/sideCtrl.ts | 2 - ui/learn/src/stage/bishop.ts | 20 +- ui/learn/src/stage/capture.ts | 18 +- ui/learn/src/stage/castling.ts | 26 +- ui/learn/src/stage/check1.ts | 22 +- ui/learn/src/stage/check2.ts | 22 +- ui/learn/src/stage/checkmate1.ts | 22 +- ui/learn/src/stage/combat.ts | 18 +- ui/learn/src/stage/enpassant.ts | 16 +- ui/learn/src/stage/king.ts | 14 +- ui/learn/src/stage/knight.ts | 20 +- ui/learn/src/stage/list.ts | 8 +- ui/learn/src/stage/outOfCheck.ts | 22 +- ui/learn/src/stage/pawn.ts | 24 +- ui/learn/src/stage/protection.ts | 24 +- ui/learn/src/stage/queen.ts | 18 +- ui/learn/src/stage/rook.ts | 20 +- ui/learn/src/stage/setup.ts | 22 +- ui/learn/src/stage/stalemate.ts | 10 +- ui/learn/src/stage/value.ts | 18 +- ui/learn/src/view.ts | 55 +- ui/lobby/src/ctrl.ts | 2 - ui/lobby/src/interfaces.ts | 2 - ui/lobby/src/lobby.ts | 2 - ui/lobby/src/options.ts | 22 +- ui/lobby/src/setupCtrl.ts | 2 +- ui/lobby/src/view/correspondence.ts | 18 +- ui/lobby/src/view/playing.ts | 4 +- ui/lobby/src/view/pools.ts | 2 +- ui/lobby/src/view/realTime/chart.ts | 6 +- ui/lobby/src/view/realTime/filter.ts | 2 +- ui/lobby/src/view/realTime/list.ts | 19 +- .../src/view/setup/components/colorButtons.ts | 6 +- .../src/view/setup/components/fenInput.ts | 6 +- .../view/setup/components/gameModeButtons.ts | 4 +- .../src/view/setup/components/levelButtons.ts | 8 +- .../components/ratingDifferenceSliders.ts | 4 +- .../src/view/setup/components/ratingView.ts | 3 +- .../setup/components/timePickerAndSliders.ts | 22 +- .../view/setup/components/variantPicker.ts | 4 +- ui/lobby/src/view/setup/modal.ts | 6 +- ui/lobby/src/view/table.ts | 18 +- ui/lobby/src/view/tabs.ts | 12 +- ui/msg/src/ctrl.ts | 1 - ui/msg/src/interfaces.ts | 1 - ui/msg/src/msg.ts | 3 +- ui/msg/src/view/actions.ts | 12 +- ui/msg/src/view/msgs.ts | 58 +- ui/msg/src/view/search.ts | 8 +- ui/notify/src/interfaces.ts | 1 - ui/notify/src/renderers.ts | 52 +- ui/notify/src/view.ts | 12 +- ui/puz/src/interfaces.ts | 1 - ui/puz/src/run.ts | 6 +- ui/puz/src/view/history.ts | 9 +- ui/puzzle/src/ctrl.ts | 16 +- ui/puzzle/src/interfaces.ts | 9 +- ui/puzzle/src/plugins/puzzle.nvui.ts | 22 +- ui/puzzle/src/view/after.ts | 16 +- ui/puzzle/src/view/boardMenu.ts | 4 +- ui/puzzle/src/view/feedback.ts | 27 +- ui/puzzle/src/view/main.ts | 2 +- ui/puzzle/src/view/side.ts | 60 +- ui/puzzle/src/view/theme.ts | 30 +- ui/puzzle/src/view/tree.ts | 19 +- ui/racer/src/ctrl.ts | 3 - ui/racer/src/interfaces.ts | 1 - ui/racer/src/view/main.ts | 53 +- ui/round/src/corresClock/corresClockView.ts | 9 +- ui/round/src/ctrl.ts | 28 +- ui/round/src/interfaces.ts | 1 - ui/round/src/plugins/round.nvui.ts | 11 +- ui/round/src/round.ts | 2 +- ui/round/src/socket.ts | 6 +- ui/round/src/title.ts | 6 +- ui/round/src/view/boardMenu.ts | 4 +- ui/round/src/view/button.ts | 74 +- ui/round/src/view/expiration.ts | 2 +- ui/round/src/view/replay.ts | 8 +- ui/round/src/view/table.ts | 31 +- ui/round/src/view/user.ts | 13 +- ui/simul/src/ctrl.ts | 3 - ui/simul/src/interfaces.ts | 1 - ui/simul/src/view/created.ts | 12 +- ui/simul/src/view/main.ts | 5 +- ui/simul/src/view/results.ts | 12 +- ui/site/src/boot.ts | 4 +- ui/site/src/friends.ts | 3 +- ui/site/src/renderTimeAgo.ts | 6 +- ui/site/src/site.ts | 3 +- ui/storm/src/ctrl.ts | 3 - ui/storm/src/interfaces.ts | 1 - ui/storm/src/view/end.ts | 29 +- ui/storm/src/view/main.ts | 24 +- ui/swiss/src/ctrl.ts | 5 +- ui/swiss/src/interfaces.ts | 1 - ui/swiss/src/view/header.ts | 9 +- ui/swiss/src/view/main.ts | 39 +- ui/swiss/src/view/playerInfo.ts | 11 +- ui/swiss/src/view/podium.ts | 6 +- ui/tournament/src/ctrl.ts | 5 +- ui/tournament/src/interfaces.ts | 2 - ui/tournament/src/tournament.calendar.ts | 2 +- ui/tournament/src/tournament.schedule.ts | 5 +- ui/tournament/src/view/arena.ts | 13 +- ui/tournament/src/view/battle.ts | 8 +- ui/tournament/src/view/button.ts | 6 +- ui/tournament/src/view/finished.ts | 24 +- ui/tournament/src/view/header.ts | 2 +- ui/tournament/src/view/playerInfo.ts | 11 +- ui/tournament/src/view/scheduleView.ts | 6 +- ui/tournament/src/view/started.ts | 12 +- ui/tournament/src/view/table.ts | 2 +- ui/tournament/src/view/teamInfo.ts | 13 +- 262 files changed, 7343 insertions(+), 3216 deletions(-) delete mode 100644 modules/analyse/src/main/ui/AnalyseI18n.scala delete mode 100644 modules/round/src/main/ui/RoundI18n.scala create mode 100644 ui/.build/src/i18n.ts create mode 100644 ui/@types/lichess/i18n.d.ts delete mode 100644 ui/learn/src/congrats.ts diff --git a/.gitignore b/.gitignore index fda385d593cc1..54c794900ba59 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ project/.bloop/ conf/application.conf conf/version.conf conf/manifest.* - +translation/js logs project/metals.sbt project/project diff --git a/app/controllers/Dasher.scala b/app/controllers/Dasher.scala index aa60bac906e38..1619dca90365f 100644 --- a/app/controllers/Dasher.scala +++ b/app/controllers/Dasher.scala @@ -11,16 +11,6 @@ import lila.pref.ui.DasherJson final class Dasher(env: Env)(using ws: StandaloneWSClient) extends LilaController(env): - private def translations(using ctx: Context) = - val langLang = - if LangPicker.allFromRequestHeaders(ctx.req).has(ctx.lang) then ctx.lang - else LangPicker.bestFromRequestHeaders(ctx.req) | defaultLang - lila.i18n.JsDump.keysToObject( - if ctx.isAnon then DasherJson.i18n.anon else DasherJson.i18n.auth - ) ++ - // the language settings should never be in a totally foreign language - lila.i18n.JsDump.keysToObject(List(trans.site.language))(using ctx.translate.copy(lang = langLang)) - private lazy val galleryJson = env.memo.cacheApi.unit[Option[JsValue]]: _.refreshAfterWrite(10.minutes).buildAsyncFuture: _ => ws.url(s"${env.net.assetBaseUrlInternal}/assets/lifat/background/gallery.json") @@ -43,6 +33,5 @@ final class Dasher(env: Env)(using ws: StandaloneWSClient) extends LilaControlle "accepted" -> LangPicker.allFromRequestHeaders(ctx.req).map(_.code), "list" -> LangList.allChoices ), - "streamer" -> isStreamer, - "i18n" -> translations + "streamer" -> isStreamer ) ++ DasherJson(ctx.pref, gallery) diff --git a/app/views/analyse/replay.scala b/app/views/analyse/replay.scala index 58ced200d80a9..38b1f3d42d567 100644 --- a/app/views/analyse/replay.scala +++ b/app/views/analyse/replay.scala @@ -78,6 +78,7 @@ def replay( .css((pov.game.variant == Crazyhouse).option("analyse.zh")) .css(ctx.blind.option("round.nvui")) .css(ctx.pref.hasKeyboardMove.option("keyboardMove")) + .i18n("puzzle", "study") .js(analyseNvuiTag) .js( bits.analyseModule( @@ -85,7 +86,6 @@ def replay( Json .obj( "data" -> data, - "i18n" -> views.analysisI18n(), "userId" -> ctx.userId, "chat" -> chatJson ) diff --git a/app/views/base/embed.scala b/app/views/base/embed.scala index 4b6f58f9a0c4b..0a6eacb401314 100644 --- a/app/views/base/embed.scala +++ b/app/views/base/embed.scala @@ -65,7 +65,7 @@ object embed: page.ui.pieceSprite(ctx.pieceSet.name), cssTag("common.theme.embed"), // includes both light & dark colors cssKeys.map(cssTag), - page.ui.sitePreload(allModules, isInquiry = false), + page.ui.sitePreload(Nil, allModules, isInquiry = false), page.ui.lichessFontFaceCss ), st.body(bodyModifiers)( diff --git a/app/views/base/page.scala b/app/views/base/page.scala index 27c95a227b23c..a5588f7ed2e65 100644 --- a/app/views/base/page.scala +++ b/app/views/base/page.scala @@ -88,7 +88,7 @@ object page: boardPreload, manifests, p.withHrefLangs.map(hrefLangs), - sitePreload(allModules, isInquiry = ctx.data.inquiry.isDefined), + sitePreload(p.i18nModules, allModules, isInquiry = ctx.data.inquiry.isDefined), lichessFontFaceCss, (ctx.pref.bg === lila.pref.Pref.Bg.SYSTEM).so(systemThemeScript(ctx.nonce)) ), diff --git a/app/views/board.scala b/app/views/board.scala index bc66085606b4e..8c6328eda0245 100644 --- a/app/views/board.scala +++ b/app/views/board.scala @@ -21,7 +21,6 @@ def userAnalysis( Json .obj( "data" -> data, - "i18n" -> views.userAnalysisI18n(withForecast = withForecast), "wiki" -> pov.game.variant.standard ) .add("inlinePgn", inlinePgn) ++ diff --git a/app/views/insight.scala b/app/views/insight.scala index f75c6ce37f28b..5fd633c0a2a01 100644 --- a/app/views/insight.scala +++ b/app/views/insight.scala @@ -20,7 +20,6 @@ def index( Json.obj( "ui" -> ui, "initialQuestion" -> question, - "i18n" -> Json.obj(), "myUserId" -> ctx.userId, "user" -> (lila.common.Json.lightUser.write(u.light) ++ Json.obj( "nbGames" -> insightUser.count, diff --git a/app/views/lobby/home.scala b/app/views/lobby/home.scala index 8c9c6bc412975..08c0f900553e8 100644 --- a/app/views/lobby/home.scala +++ b/app/views/lobby/home.scala @@ -18,7 +18,6 @@ object home: Json .obj( "data" -> data, - "i18n" -> i18nJsObject(i18nKeys), "showRatings" -> ctx.pref.showRatings, "hasUnreadLichessMessage" -> hasUnreadLichessMessage ) @@ -165,48 +164,3 @@ object home: views.bits.connectLinks ) ) - - private val i18nKeys = List( - trans.site.realTime, - trans.site.correspondence, - trans.site.unlimited, - trans.site.timeControl, - trans.site.incrementInSeconds, - trans.site.minutesPerSide, - trans.site.daysPerTurn, - trans.site.ratingRange, - trans.site.nbPlayers, - trans.site.nbGamesInPlay, - trans.site.player, - trans.site.time, - trans.site.joinTheGame, - trans.site.cancel, - trans.site.casual, - trans.site.rated, - trans.site.perfRatingX, - trans.site.variant, - trans.site.mode, - trans.site.list, - trans.site.graph, - trans.site.filterGames, - trans.site.youNeedAnAccountToDoThat, - trans.site.oneDay, - trans.site.nbDays, - trans.site.aiNameLevelAiLevel, - trans.site.yourTurn, - trans.site.rating, - trans.site.createAGame, - trans.site.playWithAFriend, - trans.site.playWithTheMachine, - trans.site.strength, - trans.site.pasteTheFenStringHere, - trans.site.quickPairing, - trans.site.lobby, - trans.site.custom, - trans.site.anonymous, - trans.site.side, - trans.site.white, - trans.site.randomColor, - trans.site.black, - trans.site.boardEditor - ) diff --git a/app/views/msg.scala b/app/views/msg.scala index 8c9685981a91b..6a5c6e9d99dc9 100644 --- a/app/views/msg.scala +++ b/app/views/msg.scala @@ -7,26 +7,7 @@ import lila.app.UiEnv.{ *, given } def home(json: JsObject)(using Context) = Page(trans.site.inbox.txt()) .css("msg") - .js(PageModule("msg", Json.obj("data" -> json, "i18n" -> i18nJsObject(i18nKeys)))) + .i18n("challenge") + .js(PageModule("msg", Json.obj("data" -> json))) .csp(_.withInlineIconFont): main(cls := "box msg-app") - -private val i18nKeys = List( - trans.site.inbox, - trans.challenge.challengeToPlay, - trans.site.block, - trans.site.unblock, - trans.site.blocked, - trans.site.delete, - trans.site.reportXToModerators, - trans.site.searchOrStartNewDiscussion, - trans.site.players, - trans.site.friends, - trans.site.discussions, - trans.site.today, - trans.site.yesterday, - trans.site.youAreLeavingLichess, - trans.site.neverTypeYourPassword, - trans.site.cancel, - trans.site.proceedToX -) diff --git a/app/views/puzzle/ui.scala b/app/views/puzzle/ui.scala index 4d61ac61bb098..0649567f8314f 100644 --- a/app/views/puzzle/ui.scala +++ b/app/views/puzzle/ui.scala @@ -2,7 +2,7 @@ package views.puzzle import lila.app.UiEnv.{ *, given } import lila.puzzle.DailyPuzzle -lazy val bits = lila.puzzle.ui.PuzzleBits(helpers)(views.userAnalysisI18n.cevalTranslations) +lazy val bits = lila.puzzle.ui.PuzzleBits(helpers) lazy val ui = lila.puzzle.ui.PuzzleUi(helpers, bits)(views.analyse.ui.csp, externalEngineEndpoint) def embed(daily: DailyPuzzle.WithHtml)(using config: EmbedContext) = diff --git a/app/views/relay.scala b/app/views/relay.scala index a76a063dde317..7ed93376b2d96 100644 --- a/app/views/relay.scala +++ b/app/views/relay.scala @@ -6,7 +6,6 @@ import lila.core.socket.SocketVersion val ui = lila.relay.ui.RelayUi(helpers)( picfitUrl, - views.study.jsI18n, views.study.socketUrl, views.board.explorerAndCevalConfig ) diff --git a/app/views/round/bits.scala b/app/views/round/bits.scala index 2bebbd9c57c4a..f1463afd77e05 100644 --- a/app/views/round/bits.scala +++ b/app/views/round/bits.scala @@ -3,8 +3,7 @@ package views.round import lila.app.UiEnv.{ *, given } import lila.game.GameExt.playerBlurPercent -lazy val ui = lila.round.ui.RoundUi(helpers, views.game.ui) -lazy val jsI18n = lila.round.ui.RoundI18n(helpers) +lazy val ui = lila.round.ui.RoundUi(helpers, views.game.ui) def crosstable(cross: Option[lila.game.Crosstable.WithMatchup], game: Game)(using ctx: Context) = cross.map: c => diff --git a/app/views/round/player.scala b/app/views/round/player.scala index 4453d576012b4..0f609a63adcf8 100644 --- a/app/views/round/player.scala +++ b/app/views/round/player.scala @@ -50,7 +50,6 @@ def player( Json .obj( "data" -> data, - "i18n" -> jsI18n(pov.game), "userId" -> ctx.userId, "chat" -> chatJson ) diff --git a/app/views/round/watcher.scala b/app/views/round/watcher.scala index 34b03d960ff5a..b3d0866005e84 100644 --- a/app/views/round/watcher.scala +++ b/app/views/round/watcher.scala @@ -35,7 +35,6 @@ def watcher( "round", Json.obj( "data" -> data, - "i18n" -> jsI18n(pov.game), "chat" -> chatJson ) ) diff --git a/app/views/study.scala b/app/views/study.scala index 6edb20d2dffd4..ada2c0e1dd1aa 100644 --- a/app/views/study.scala +++ b/app/views/study.scala @@ -23,13 +23,6 @@ def staffPicks(p: lila.cms.CmsPage.Render)(using Context) = def streamers(streamers: List[UserId])(using Translate) = views.streamer.bits.contextual(streamers).map(_(cls := "none")) -def jsI18n()(using Translate) = - views.userAnalysisI18n(withAdvantageChart = true) ++ - i18nJsObject(bits.i18nKeys ++ bits.gamebookPlayKeys) - -def embedJsI18n(chapter: lila.study.Chapter)(using Translate) = - views.userAnalysisI18n() ++ chapter.isGamebook.so(i18nJsObject(bits.gamebookPlayKeys)) - def clone(s: lila.study.Study)(using Context) = views.site.message(title = s"Clone ${s.name}", icon = Icon.StudyBoard.some)(ui.clone(s)) @@ -57,6 +50,7 @@ def show( Page(s.name.value) .css("analyse.study") .css(ctx.pref.hasKeyboardMove.option("keyboardMove")) + .i18n("study") .js(analyseNvuiTag) .js( PageModule( @@ -66,7 +60,6 @@ def show( .add("admin", isGranted(_.StudyAdmin)) .add("showRatings", ctx.pref.showRatings), "data" -> data.analysis, - "i18n" -> jsI18n(), "tagTypes" -> lila.study.PgnTags.typesToString, "userId" -> ctx.userId, "chat" -> chatOption.map: c => diff --git a/app/views/tv.scala b/app/views/tv.scala index 9879ab0e40f1a..93087c56555bb 100644 --- a/app/views/tv.scala +++ b/app/views/tv.scala @@ -18,7 +18,7 @@ def index( pov.game.variant, s"${channel.name} TV: ${playerText(pov.player)} vs ${playerText(pov.opponent)}" ) - .js(PageModule("round", Json.obj("data" -> data, "i18n" -> views.round.jsI18n(pov.game)))) + .js(PageModule("round", Json.obj("data" -> data))) .css("bits.tv.single") .graph( title = s"Watch the best ${channel.name.toLowerCase} games of lichess.org", diff --git a/app/views/ui.scala b/app/views/ui.scala index b81695dd71a19..f48128f590935 100644 --- a/app/views/ui.scala +++ b/app/views/ui.scala @@ -9,9 +9,6 @@ val chat = lila.chat.ChatUi(helpers) val boardEditor = lila.web.ui.BoardEditorUi(helpers) -val userAnalysisI18n = lila.analyse.ui.AnalyseI18n(helpers) -val analysisI18n = lila.analyse.ui.GameAnalyseI18n(helpers, userAnalysisI18n) - val setup = lila.setup.ui.SetupUi(helpers) val gathering = lila.gathering.ui.GatheringUi(helpers)(env.web.settings.prizeTournamentMakers.get) @@ -55,7 +52,6 @@ object account: val practice = lila.practice.ui.PracticeUi(helpers)( csp = analyse.ui.csp, - translations = userAnalysisI18n.vector() ++ views.study.bits.gamebookPlayKeys, board.explorerAndCevalConfig, modMenu = mod.ui.menu("practice") ) diff --git a/app/views/user/show/page.scala b/app/views/user/show/page.scala index 93e85c059120f..adaff0637b5f0 100644 --- a/app/views/user/show/page.scala +++ b/app/views/user/show/page.scala @@ -75,7 +75,7 @@ object page: private def esModules(withSearch: Boolean = false)(using Context): EsmList = import play.api.libs.json.Json infiniteScrollEsmInit - ++ esmInitObj("bits.user", "i18n" -> i18nJsObject(ui.i18nKeys)) + ++ esmInit("bits.user") ++ withSearch.so(Esm("bits.gameSearch")) ++ isGranted(_.UserModView).so(Esm("mod.user")) diff --git a/bin/schlawg-dev b/bin/schlawg-dev index 17cb412770461..d7c98e7f96a4c 100755 --- a/bin/schlawg-dev +++ b/bin/schlawg-dev @@ -2,6 +2,7 @@ cd "$(dirname "${BASH_SOURCE:-$0}")/.." trap 'tmux kill-session -t muxlog1234 2>/dev/null' EXIT +# l=/ui-build tmux new-session -d -s muxlog1234 \ - 'journalctl --user -fu lila -o cat | grep -E -v fishnet & ui/build -cdrl=/ui-build & wait; exec bash' \; \ + 'journalctl --user -fu lila -o cat | grep -E -v fishnet & ui/build -cdw & wait; exec bash' \; \ attach-session -d -t muxlog1234 \; \ No newline at end of file diff --git a/modules/analyse/src/main/ui/AnalyseI18n.scala b/modules/analyse/src/main/ui/AnalyseI18n.scala deleted file mode 100644 index 96d6e481cff15..0000000000000 --- a/modules/analyse/src/main/ui/AnalyseI18n.scala +++ /dev/null @@ -1,349 +0,0 @@ -package lila.analyse -package ui - -import lila.core.i18n.{ I18nKey, Translate } -import lila.ui.* - -final class AnalyseI18n(helpers: Helpers): - import helpers.* - - def apply( - withCeval: Boolean = true, - withExplorer: Boolean = true, - withForecast: Boolean = false, - withAdvantageChart: Boolean = false - )(using Translate) = - i18nJsObject(vector(withCeval, withExplorer, withForecast, withAdvantageChart)) - - def vector( - withCeval: Boolean = true, - withExplorer: Boolean = true, - withForecast: Boolean = false, - withAdvantageChart: Boolean = false - ): Vector[I18nKey] = - baseTranslations ++ { - withCeval.so(cevalTranslations) - } ++ { - withExplorer.so(explorerTranslations) - } ++ { - withForecast.so(forecastTranslations) - } ++ { - withAdvantageChart.so(advantageTranslations) - } - - import lila.core.i18n.I18nKey.{ site, puzzle, study, preferences } - - private val baseTranslations = Vector( - site.analysis, - site.flipBoard, - site.backToGame, - site.gameAborted, - site.checkmate, - site.whiteResigned, - site.blackResigned, - site.whiteDidntMove, - site.blackDidntMove, - site.stalemate, - site.whiteLeftTheGame, - site.blackLeftTheGame, - site.draw, - site.whiteTimeOut, - site.blackTimeOut, - site.playingRightNow, - site.whiteIsVictorious, - site.blackIsVictorious, - site.cheatDetected, - site.kingInTheCenter, - site.threeChecks, - site.variantEnding, - site.drawByMutualAgreement, - site.fiftyMovesWithoutProgress, - site.insufficientMaterial, - site.whitePlays, - site.blackPlays, - site.gameOver, - site.importPgn, - site.requestAComputerAnalysis, - site.computerAnalysis, - site.learnFromYourMistakes, - site.averageCentipawnLoss, - site.accuracy, - site.viewTheSolution, - // action menu - site.menu, - site.boardEditor, - site.continueFromHere, - site.toStudy, - site.playWithTheMachine, - site.playWithAFriend, - site.openStudy, - preferences.preferences, - site.inlineNotation, - site.makeAStudy, - site.clearSavedMoves, - site.replayMode, - site.slow, - site.fast, - site.realtimeReplay, - site.byCPL, - // context menu - site.promoteVariation, - site.makeMainLine, - site.deleteFromHere, - site.collapseVariations, - site.expandVariations, - site.forceVariation, - site.copyVariationPgn, - // practice (also uses checkmate, draw) - site.practiceWithComputer, - puzzle.goodMove, - site.inaccuracy, - site.mistake, - site.blunder, - site.threefoldRepetition, - site.anotherWasX, - site.bestWasX, - site.youBrowsedAway, - site.resumePractice, - site.whiteWinsGame, - site.blackWinsGame, - site.theGameIsADraw, - site.yourTurn, - site.computerThinking, - site.seeBestMove, - site.hideBestMove, - site.getAHint, - site.evaluatingYourMove, - // gamebook - puzzle.findTheBestMoveForWhite, - puzzle.findTheBestMoveForBlack - ) - - val cevalWidget = Vector( - site.gameOver, - site.depthX, - site.usingServerAnalysis, - site.loadingEngine, - site.calculatingMoves, - site.engineFailed, - site.cloudAnalysis, - site.goDeeper, - site.showThreat, - site.inLocalBrowser, - site.toggleLocalEvaluation, - site.computerAnalysisDisabled - ) - - val cevalTranslations: Vector[I18nKey] = cevalWidget ++ Vector( - // ceval menu - site.computerAnalysis, - site.enable, - site.bestMoveArrow, - site.showVariationArrows, - site.evaluationGauge, - site.infiniteAnalysis, - site.removesTheDepthLimit, - site.multipleLines, - site.cpus, - site.memory, - site.engineManager - ) - - val explorerTranslations = Vector( - // also uses gameOver, checkmate, stalemate, draw, variantEnding - site.openingExplorerAndTablebase, - site.openingExplorer, - site.xOpeningExplorer, - site.move, - site.games, - site.variantLoss, - site.variantWin, - site.insufficientMaterial, - site.capture, - site.pawnMove, - site.close, - site.winning, - site.unknown, - site.losing, - site.drawn, - site.timeControl, - site.averageElo, - site.database, - site.recentGames, - site.topGames, - site.whiteDrawBlack, - site.averageRatingX, - site.masterDbExplanation, - site.mateInXHalfMoves, - site.dtzWithRounding, - site.winOr50MovesByPriorMistake, - site.lossOr50MovesByPriorMistake, - site.unknownDueToRounding, - site.noGameFound, - site.maxDepthReached, - site.maybeIncludeMoreGamesFromThePreferencesMenu, - site.winPreventedBy50MoveRule, - site.lossSavedBy50MoveRule, - site.allSet, - study.searchByUsername, - site.mode, - site.rated, - site.casual, - site.since, - site.until, - site.switchSides, - site.lichessDbExplanation, - site.player, - site.asWhite, - site.asBlack - ) - - private val forecastTranslations = Vector( - site.conditionalPremoves, - site.addCurrentVariation, - site.playVariationToCreateConditionalPremoves, - site.noConditionalPremoves, - site.playX, - site.andSaveNbPremoveLines - ) - - val advantageChartTranslations = Vector( - site.advantage, - site.nbSeconds, - site.opening, - site.middlegame, - site.endgame - ) - - private val advantageTranslations = - advantageChartTranslations ++ - Vector( - site.nbInaccuracies, - site.nbMistakes, - site.nbBlunders - ) - -final class GameAnalyseI18n(helpers: Helpers, board: AnalyseI18n): - import helpers.* - - def apply()(using Translate) = i18nJsObject(i18nKeys) - - import lila.core.i18n.I18nKey.{ site, puzzle } - - private val i18nKeys: Vector[I18nKey] = { - board.cevalWidget ++ - board.advantageChartTranslations ++ - board.explorerTranslations ++ - Vector( - site.flipBoard, - site.gameAborted, - site.gameOver, - site.checkmate, - site.whiteResigned, - site.blackResigned, - site.whiteDidntMove, - site.blackDidntMove, - site.stalemate, - site.whiteLeftTheGame, - site.blackLeftTheGame, - site.draw, - site.whiteTimeOut, - site.blackTimeOut, - site.playingRightNow, - site.whiteIsVictorious, - site.blackIsVictorious, - site.cheatDetected, - site.kingInTheCenter, - site.threeChecks, - site.variantEnding, - site.drawByMutualAgreement, - site.fiftyMovesWithoutProgress, - site.insufficientMaterial, - site.analysis, - site.boardEditor, - site.continueFromHere, - site.playWithTheMachine, - site.playWithAFriend, - site.openingExplorer, - site.nbInaccuracies, - site.nbMistakes, - site.nbBlunders, - site.averageCentipawnLoss, - site.accuracy, - site.viewTheSolution, - site.youNeedAnAccountToDoThat, - site.aiNameLevelAiLevel, - // action menu - site.menu, - site.toStudy, - site.inlineNotation, - site.makeAStudy, - site.clearSavedMoves, - site.computerAnalysis, - site.enable, - site.bestMoveArrow, - site.showVariationArrows, - site.evaluationGauge, - site.infiniteAnalysis, - site.removesTheDepthLimit, - site.multipleLines, - site.cpus, - site.memory, - site.engineManager, - site.replayMode, - site.slow, - site.fast, - site.realtimeReplay, - site.byCPL, - // context menu - site.promoteVariation, - site.makeMainLine, - site.deleteFromHere, - site.collapseVariations, - site.expandVariations, - site.forceVariation, - site.copyVariationPgn, - // practice (also uses checkmate, draw) - site.practiceWithComputer, - puzzle.goodMove, - site.inaccuracy, - site.mistake, - site.blunder, - site.threefoldRepetition, - site.anotherWasX, - site.bestWasX, - site.youBrowsedAway, - site.resumePractice, - site.whiteWinsGame, - site.blackWinsGame, - site.drawByFiftyMoves, - site.theGameIsADraw, - site.yourTurn, - site.computerThinking, - site.seeBestMove, - site.hideBestMove, - site.getAHint, - site.evaluatingYourMove, - // retrospect (also uses youBrowsedAway, bestWasX, evaluatingYourMove) - site.learnFromYourMistakes, - site.learnFromThisMistake, - site.skipThisMove, - site.next, - site.xWasPlayed, - site.findBetterMoveForWhite, - site.findBetterMoveForBlack, - site.resumeLearning, - site.youCanDoBetter, - site.tryAnotherMoveForWhite, - site.tryAnotherMoveForBlack, - site.solution, - site.waitingForAnalysis, - site.noMistakesFoundForWhite, - site.noMistakesFoundForBlack, - site.doneReviewingWhiteMistakes, - site.doneReviewingBlackMistakes, - site.doItAgain, - site.reviewWhiteMistakes, - site.reviewBlackMistakes - ) - }.distinct diff --git a/modules/analyse/src/main/ui/AnalyseUi.scala b/modules/analyse/src/main/ui/AnalyseUi.scala index e71f98480e479..642fb1c1a081b 100644 --- a/modules/analyse/src/main/ui/AnalyseUi.scala +++ b/modules/analyse/src/main/ui/AnalyseUi.scala @@ -23,6 +23,8 @@ final class AnalyseUi(helpers: Helpers)(externalEngineEndpoint: String): .css(ctx.blind.option("round.nvui")) .css(ctx.pref.hasKeyboardMove.option("keyboardMove")) .csp(csp.compose(_.withExternalAnalysisApis)) + .i18n("puzzle") + .i18n("study") .graph( title = "Chess analysis board", url = s"$netBaseUrl${routes.UserAnalysis.index.url}", diff --git a/modules/challenge/src/main/JsonView.scala b/modules/challenge/src/main/JsonView.scala index f6e45c4fa1698..9268f97353584 100644 --- a/modules/challenge/src/main/JsonView.scala +++ b/modules/challenge/src/main/JsonView.scala @@ -36,9 +36,8 @@ final class JsonView( def apply(a: AllChallenges)(using Translate): JsObject = Json.obj( - "in" -> a.in.map(apply(Direction.In.some)), - "out" -> a.out.map(apply(Direction.Out.some)), - "i18n" -> jsDump.keysToObject(i18nKeys), + "in" -> a.in.map(apply(Direction.In.some)), + "out" -> a.out.map(apply(Direction.Out.some)), "reasons" -> JsObject(Challenge.DeclineReason.allExceptBot.map: r => r.key -> JsString(r.trans.txt())) ) @@ -110,13 +109,3 @@ final class JsonView( if c.variant == chess.variant.FromPosition then Icon.Feather else c.perfType.icon - - private val i18nKeys = List( - trans.site.rated, - trans.site.casual, - trans.site.waiting, - trans.site.accept, - trans.site.decline, - trans.site.viewInFullSize, - trans.site.cancel - ) diff --git a/modules/chat/src/main/ChatUi.scala b/modules/chat/src/main/ChatUi.scala index d6bd9cb4dcc29..6cf37405da9f9 100644 --- a/modules/chat/src/main/ChatUi.scala +++ b/modules/chat/src/main/ChatUi.scala @@ -78,7 +78,6 @@ final class ChatUi(helpers: Helpers): .add("loginRequired" -> chat.loginRequired) .add("restricted" -> restricted) .add("palantir" -> (palantir && ctx.isAuth)), - "i18n" -> chatI18nObject(withNote = withNoteAge.isDefined), "writeable" -> writeable, "public" -> public, "permissions" -> Json @@ -96,13 +95,3 @@ final class ChatUi(helpers: Helpers): "timeoutReasons" -> (!localMod && (Granter.opt(_.ChatTimeout) || Granter.opt(_.BroadcastTimeout))) .option(JsonView.timeoutReasons) ) - - private def chatI18nObject(withNote: Boolean)(using Context) = - i18nOptionJsObject( - trans.site.talkInChat.some, - trans.site.toggleTheChat.some, - trans.site.loginToChat.some, - trans.site.youHaveBeenTimedOut.some, - Option.when(withNote)(trans.site.notes), - Option.when(withNote)(trans.site.typePrivateNotesHere) - ) diff --git a/modules/coordinate/src/main/CoordinateUi.scala b/modules/coordinate/src/main/CoordinateUi.scala index d2a893044dee2..99dec8eeb5382 100644 --- a/modules/coordinate/src/main/CoordinateUi.scala +++ b/modules/coordinate/src/main/CoordinateUi.scala @@ -15,6 +15,9 @@ final class CoordinateUi(helpers: Helpers): Page(trans.coordinates.coordinateTraining.txt()) .css("coordinateTrainer") .css("voice") + .i18n("coordinates") + .i18n("storm") + .i18n("study") .js(pageModule(scoreOption)) .csp(_.withPeer.withWebAssembly) .graph( @@ -41,7 +44,6 @@ final class CoordinateUi(helpers: Helpers): PageModule( "coordinateTrainer", Json.obj( - "i18n" -> i18nJsObject(i18nKeys), "resizePref" -> ctx.pref.resizeHandle, "is3d" -> ctx.pref.is3d, "scores" -> Json.obj( @@ -56,31 +58,3 @@ final class CoordinateUi(helpers: Helpers): ) ) ).some - - private val i18nKeys: List[I18nKey] = List( - trans.coordinates.aSquareIsHighlightedExplanation, - trans.coordinates.aCoordinateAppears, - trans.coordinates.youHaveThirtySeconds, - trans.coordinates.goAsLongAsYouWant, - trans.coordinates.averageScoreAsBlackX, - trans.coordinates.averageScoreAsWhiteX, - trans.coordinates.coordinates, - trans.coordinates.knowingTheChessBoard, - trans.coordinates.mostChessCourses, - trans.coordinates.startTraining, - trans.coordinates.talkToYourChessFriends, - trans.coordinates.youCanAnalyseAGameMoreEffectively, - trans.coordinates.findSquare, - trans.coordinates.nameSquare, - trans.coordinates.showCoordinates, - trans.coordinates.showCoordsOnAllSquares, - trans.coordinates.showPieces, - trans.storm.score, - trans.study.back, - trans.site.time, - trans.site.asWhite, - trans.site.asBlack, - trans.site.randomColor, - trans.site.youPlayTheWhitePieces, - trans.site.youPlayTheBlackPieces - ) diff --git a/modules/notify/src/main/JsonHandlers.scala b/modules/notify/src/main/JsonHandlers.scala index 9aa0df9b0d6f3..a6483ecd645dc 100644 --- a/modules/notify/src/main/JsonHandlers.scala +++ b/modules/notify/src/main/JsonHandlers.scala @@ -97,29 +97,4 @@ final class JSONHandlers(getLightUser: LightUser.GetterSync, jsDump: JsDump): given OWrites[Notification.AndUnread] = Json.writes - private val i18nKeys: List[I18nKey] = List( - trans.mentionedYouInX, - trans.xMentionedYouInY, - trans.startedStreaming, - trans.xStartedStreaming, - trans.invitedYouToX, - trans.xInvitedYouToY, - trans.youAreNowPartOfTeam, - trans.youHaveJoinedTeamX, - trans.thankYou, - trans.someoneYouReportedWasBanned, - trans.victory, - trans.defeat, - trans.draw, - trans.congratsYouWon, - trans.gameVsX, - trans.resVsX, - trans.lostAgainstTOSViolator, - trans.refundXpointsTimeControlY, - trans.timeAlmostUp - ) - - def apply(notify: Notification.AndUnread)(using Translate) = - Json.toJsObject(notify) ++ Json.obj( - "i18n" -> jsDump.keysToObject(i18nKeys) - ) + def apply(notify: Notification.AndUnread)(using Translate) = Json.toJsObject(notify) diff --git a/modules/perfStat/src/main/PerfStatUi.scala b/modules/perfStat/src/main/PerfStatUi.scala index 6ba74b813f000..4c9f46c3725ea 100644 --- a/modules/perfStat/src/main/PerfStatUi.scala +++ b/modules/perfStat/src/main/PerfStatUi.scala @@ -382,10 +382,7 @@ final class PerfStatUi(helpers: Helpers)(communityMenu: Context ?=> Frag): "freq" -> data, "myRating" -> myVisiblePerfs.map(_(perfType).intRating), "otherRating" -> otherUser.ifTrue(ctx.pref.showRatings).map(_.perfs(perfType).intRating), - "otherPlayer" -> otherUser.map(_.username), - "i18n" -> i18nJsObject( - List(trans.site.players, trans.site.yourRating, trans.site.cumulative, trans.site.glicko2Rating) - ) + "otherPlayer" -> otherUser.map(_.username) ) ) ): diff --git a/modules/practice/src/main/PracticeUi.scala b/modules/practice/src/main/PracticeUi.scala index 2ea3c9a3d2cb7..479c0edef9cb2 100644 --- a/modules/practice/src/main/PracticeUi.scala +++ b/modules/practice/src/main/PracticeUi.scala @@ -11,7 +11,6 @@ import ScalatagsTemplate.{ *, given } final class PracticeUi(helpers: Helpers)( csp: Update[ContentSecurityPolicy], - translations: Vector[I18nKey], explorerAndCevalConfig: Context ?=> JsObject, modMenu: Context ?=> Frag ): @@ -20,6 +19,7 @@ final class PracticeUi(helpers: Helpers)( def show(us: UserStudy, data: JsonView.JsData)(using Context) = Page(us.practiceStudy.name.value) .css("analyse.practice") + .i18n("study") .js(analyseNvuiTag) .js( PageModule( @@ -27,8 +27,7 @@ final class PracticeUi(helpers: Helpers)( Json.obj( "practice" -> data.practice, "study" -> data.study, - "data" -> data.analysis, - "i18n" -> i18nJsObject(translations) + "data" -> data.analysis ) ++ explorerAndCevalConfig ) ) diff --git a/modules/puzzle/src/main/ui/PuzzleBits.scala b/modules/puzzle/src/main/ui/PuzzleBits.scala index 891f7144a1a12..8b857160d4ffb 100644 --- a/modules/puzzle/src/main/ui/PuzzleBits.scala +++ b/modules/puzzle/src/main/ui/PuzzleBits.scala @@ -9,16 +9,12 @@ import lila.ui.* import ScalatagsTemplate.{ *, given } -final class PuzzleBits(helpers: Helpers)(cevalTranslations: Seq[I18nKey]): +final class PuzzleBits(helpers: Helpers): import helpers.{ *, given } def daily(p: lila.puzzle.Puzzle, fen: BoardFen, lastMove: Uci) = chessgroundMini(fen, p.color, lastMove.some)(span) - def jsI18n(streak: Boolean)(using Translate) = - if streak then i18nJsObject(streakI18nKeys) - else i18nJsObject(trainingI18nKeys) - lazy val jsonThemes = PuzzleTheme.visible .collect { case t if t != PuzzleTheme.mix => t.key } .partition(PuzzleTheme.staticThemes.contains) match @@ -150,59 +146,3 @@ final class PuzzleBits(helpers: Helpers)(cevalTranslations: Seq[I18nKey]): iconTag(if results.canReplay then Icon.PlayTriangle else Icon.Checkmark) ) ) - - private val themeI18nKeys: List[I18nKey] = - PuzzleTheme.visible.map(_.name) ::: PuzzleTheme.visible.map(_.description) - - private val baseI18nKeys: List[I18nKey] = List( - trans.puzzle.bestMove, - trans.puzzle.keepGoing, - trans.puzzle.notTheMove, - trans.puzzle.trySomethingElse, - trans.site.yourTurn, - trans.puzzle.findTheBestMoveForBlack, - trans.puzzle.findTheBestMoveForWhite, - trans.site.viewTheSolution, - trans.puzzle.puzzleSuccess, - trans.puzzle.puzzleComplete, - trans.puzzle.hidden, - trans.puzzle.jumpToNextPuzzleImmediately, - trans.puzzle.fromGameLink, - trans.puzzle.puzzleId, - trans.puzzle.ratingX, - trans.puzzle.playedXTimes, - trans.puzzle.continueTraining, - trans.puzzle.didYouLikeThisPuzzle, - trans.puzzle.voteToLoadNextOne, - trans.site.analysis, - trans.site.playWithTheMachine, - trans.preferences.zenMode, - trans.site.asWhite, - trans.site.asBlack, - trans.site.randomColor, - trans.site.flipBoard - ) ::: cevalTranslations.toList - - private val trainingI18nKeys: List[I18nKey] = baseI18nKeys ::: List[I18nKey]( - trans.puzzle.example, - trans.puzzle.dailyPuzzle, - trans.puzzle.addAnotherTheme, - trans.puzzle.difficultyLevel, - trans.site.rated, - trans.puzzle.yourPuzzleRatingWillNotChange, - trans.site.signUp, - trans.puzzle.toGetPersonalizedPuzzles, - trans.puzzle.nbPointsBelowYourPuzzleRating, - trans.puzzle.nbPointsAboveYourPuzzleRating - ) ::: - themeI18nKeys ::: - PuzzleDifficulty.all.map(_.name) - - private val streakI18nKeys: List[I18nKey] = baseI18nKeys ::: List[I18nKey]( - trans.storm.skip, - trans.puzzle.streakDescription, - trans.puzzle.yourStreakX, - trans.puzzle.streakSkipExplanation, - trans.puzzle.continueTheStreak, - trans.puzzle.newStreak - ) ::: themeI18nKeys diff --git a/modules/puzzle/src/main/ui/PuzzleUi.scala b/modules/puzzle/src/main/ui/PuzzleUi.scala index 9f78fbfb94895..21818848be5ec 100644 --- a/modules/puzzle/src/main/ui/PuzzleUi.scala +++ b/modules/puzzle/src/main/ui/PuzzleUi.scala @@ -30,6 +30,9 @@ final class PuzzleUi(helpers: Helpers, val bits: PuzzleBits)( .css(ctx.pref.hasKeyboardMove.option("keyboardMove")) .css(ctx.pref.hasVoice.option("voice")) .css(ctx.blind.option("round.nvui")) + .i18n("puzzle") + .i18n("puzzleTheme") + .i18n("storm") .js(ctx.blind.option(Esm("puzzle.nvui"))) .js( PageModule( @@ -38,7 +41,6 @@ final class PuzzleUi(helpers: Helpers, val bits: PuzzleBits)( .obj( "data" -> data, "pref" -> pref, - "i18n" -> bits.jsI18n(streak = isStreak), "showRatings" -> ctx.pref.showRatings, "settings" -> Json.obj("difficulty" -> settings.difficulty.key).add("color" -> settings.color), "externalEngineEndpoint" -> externalEngineEndpoint diff --git a/modules/racer/src/main/ui/RacerUi.scala b/modules/racer/src/main/ui/RacerUi.scala index eed3b6c79c441..5bfa96986a25a 100644 --- a/modules/racer/src/main/ui/RacerUi.scala +++ b/modules/racer/src/main/ui/RacerUi.scala @@ -34,7 +34,8 @@ final class RacerUi(helpers: Helpers): def show(data: JsObject)(using Context) = Page("Puzzle Racer") .css("racer") - .js(PageModule("racer", data ++ Json.obj("i18n" -> i18nJsObject(i18nKeys)))) + .i18n("storm") + .js(PageModule("racer", data)) .zoom .zen: main( @@ -43,31 +44,3 @@ final class RacerUi(helpers: Helpers): div(cls := "racer__side") ) ) - - private val i18nKeys: List[lila.core.i18n.I18nKey] = List( - s.score, - s.combo, - s.youPlayTheWhitePiecesInAllPuzzles, - s.youPlayTheBlackPiecesInAllPuzzles, - s.getReady, - s.waitingForMorePlayers, - s.raceComplete, - s.spectating, - s.joinTheRace, - s.startTheRace, - s.yourRankX, - s.waitForRematch, - s.nextRace, - s.joinRematch, - s.waitingToStart, - s.createNewGame, - trans.site.toInviteSomeoneToPlayGiveThisUrl, - s.skip, - s.skipHelp, - s.skipExplanation, - s.puzzlesPlayed, - s.failedPuzzles, - s.slowPuzzles, - s.skippedPuzzle, - trans.site.flipBoard - ) diff --git a/modules/relay/src/main/ui/RelayUi.scala b/modules/relay/src/main/ui/RelayUi.scala index 83a1e6610a236..978fb5093d091 100644 --- a/modules/relay/src/main/ui/RelayUi.scala +++ b/modules/relay/src/main/ui/RelayUi.scala @@ -13,7 +13,6 @@ import ScalatagsTemplate.{ *, given } final class RelayUi(helpers: Helpers)( picfitUrl: lila.core.misc.PicfitUrl, - studyJsI18n: () => helpers.Translate ?=> JsObject, socketUrl: StudyId => String, explorerAndCevalConfig: Context ?=> JsObject ): @@ -29,6 +28,8 @@ final class RelayUi(helpers: Helpers)( )(using ctx: Context) = Page(rt.fullName) .css("analyse.relay") + .i18n("study") + .i18n("broadcast") .js(analyseNvuiTag) .js(pageModule(rt, data, chatOption, socketVersion)) .zoom @@ -56,7 +57,6 @@ final class RelayUi(helpers: Helpers)( "relay" -> data.relay, "study" -> data.study.add("admin" -> Granter.opt(_.StudyAdmin)), "data" -> data.analysis, - "i18n" -> jsI18n, "tagTypes" -> lila.study.PgnTags.typesToString, "userId" -> ctx.userId, "chat" -> chatOption.map(_._1), @@ -126,15 +126,3 @@ final class RelayUi(helpers: Helpers)( a(dataIcon := Icon.InfoCircle, cls := "text", href := routes.RelayTour.help)( trans.broadcast.howToUseLichessBroadcasts() ) - - def jsI18n(using Translate) = - studyJsI18n() ++ i18nJsObject(i18nKeys) - - val i18nKeys = - import trans.broadcast as trb - List( - trb.addRound, - trb.currentGameUrl, - trb.downloadAllRounds, - trb.editRoundStudy - ) diff --git a/modules/round/src/main/ui/RoundI18n.scala b/modules/round/src/main/ui/RoundI18n.scala deleted file mode 100644 index a517c7b4f2eba..0000000000000 --- a/modules/round/src/main/ui/RoundI18n.scala +++ /dev/null @@ -1,110 +0,0 @@ -package lila.round -package ui - -import lila.core.i18n.{ I18nKey, Translate } -import lila.ui.* - -final class RoundI18n(helpers: Helpers): - import helpers.* - - def apply(g: Game)(using t: Translate) = - i18nJsObject: - baseTranslations ++ { - if g.isCorrespondence then correspondenceTranslations - else realtimeTranslations - } ++ { - g.variant.exotic.so(variantTranslations) - } ++ { - g.isTournament.so(tournamentTranslations) - } ++ { - g.isSwiss.so(swissTranslations) - } - - private val correspondenceTranslations = Vector( - trans.site.oneDay, - trans.site.nbDays, - trans.site.nbHours - ) - - private val realtimeTranslations = Vector(trans.site.nbSecondsToPlayTheFirstMove) - - private val variantTranslations = Vector( - trans.site.kingInTheCenter, - trans.site.threeChecks, - trans.site.variantEnding - ) - - private val tournamentTranslations = Vector( - trans.site.backToTournament, - trans.site.viewTournament, - trans.site.standing - ) - - private val swissTranslations = Vector( - trans.site.backToTournament, - trans.site.viewTournament, - trans.site.noDrawBeforeSwissLimit - ) - - private val baseTranslations = Vector( - trans.site.anonymous, - trans.site.flipBoard, - trans.site.aiNameLevelAiLevel, - trans.site.yourTurn, - trans.site.abortGame, - trans.site.proposeATakeback, - trans.site.offerDraw, - trans.site.resign, - trans.site.opponentLeftCounter, - trans.site.opponentLeftChoices, - trans.site.forceResignation, - trans.site.forceDraw, - trans.site.threefoldRepetition, - trans.site.claimADraw, - trans.site.drawOfferSent, - trans.site.cancel, - trans.site.yourOpponentOffersADraw, - trans.site.accept, - trans.site.decline, - trans.site.takebackPropositionSent, - trans.site.yourOpponentProposesATakeback, - trans.site.thisAccountViolatedTos, - trans.site.gameAborted, - trans.site.checkmate, - trans.site.cheatDetected, - trans.site.whiteResigned, - trans.site.blackResigned, - trans.site.whiteDidntMove, - trans.site.blackDidntMove, - trans.site.stalemate, - trans.site.whiteLeftTheGame, - trans.site.blackLeftTheGame, - trans.site.draw, - trans.site.whiteTimeOut, - trans.site.blackTimeOut, - trans.site.whiteIsVictorious, - trans.site.blackIsVictorious, - trans.site.drawByMutualAgreement, - trans.site.fiftyMovesWithoutProgress, - trans.site.insufficientMaterial, - trans.site.pause, - trans.site.withdraw, - trans.site.rematch, - trans.site.rematchOfferSent, - trans.site.rematchOfferAccepted, - trans.site.waitingForOpponent, - trans.site.cancelRematchOffer, - trans.site.newOpponent, - trans.site.confirmMove, - trans.site.viewRematch, - trans.site.whitePlays, - trans.site.blackPlays, - trans.site.giveNbSeconds, - trans.preferences.giveMoreTime, - trans.site.gameOver, - trans.site.analysis, - trans.site.yourOpponentWantsToPlayANewGameWithYou, - trans.site.youPlayTheWhitePieces, - trans.site.youPlayTheBlackPieces, - trans.site.itsYourTurn - ) diff --git a/modules/simul/src/main/ui/SimulShow.scala b/modules/simul/src/main/ui/SimulShow.scala index 40dae5a8d162b..ac467c35f0ae3 100644 --- a/modules/simul/src/main/ui/SimulShow.scala +++ b/modules/simul/src/main/ui/SimulShow.scala @@ -29,7 +29,6 @@ final class SimulShow(helpers: Helpers, ui: SimulUi, gathering: GatheringUi): "simul", Json.obj( "data" -> data, - "i18n" -> ui.jsI18n, "socketVersion" -> socketVersion, "userId" -> ctx.userId, "chat" -> chatOption.map(_._1), diff --git a/modules/simul/src/main/ui/SimulUi.scala b/modules/simul/src/main/ui/SimulUi.scala index 00290784b4834..98749e0f51c40 100644 --- a/modules/simul/src/main/ui/SimulUi.scala +++ b/modules/simul/src/main/ui/SimulUi.scala @@ -11,8 +11,6 @@ final class SimulUi(helpers: Helpers): def link(simulId: SimulId): Frag = a(href := routes.Simul.show(simulId))("Simultaneous exhibition") - def jsI18n(using Translate) = i18nJsObject(baseTranslations) - def notFound(using Context) = Page(trans.site.noSimulFound.txt()): main(cls := "page-small box box-pad")( @@ -67,19 +65,3 @@ final class SimulUi(helpers: Helpers): s.ongoing, " ongoing" ) - - import lila.core.i18n.I18nKey - private val baseTranslations: Vector[I18nKey] = Vector( - trans.site.finished, - trans.site.withdraw, - trans.site.join, - trans.site.cancel, - trans.site.joinTheGame, - trans.site.nbPlaying, - trans.site.nbWins, - trans.site.nbDraws, - trans.site.nbLosses, - trans.site.by, - trans.site.signIn, - trans.site.mustBeInTeam - ) diff --git a/modules/storm/src/main/ui/StormUi.scala b/modules/storm/src/main/ui/StormUi.scala index cd7fb85d09c9e..7dd883e6410e9 100644 --- a/modules/storm/src/main/ui/StormUi.scala +++ b/modules/storm/src/main/ui/StormUi.scala @@ -15,7 +15,8 @@ final class StormUi(helpers: Helpers): def home(data: JsObject, high: Option[StormHigh])(using Context) = Page("Puzzle Storm") .css("storm") - .js(PageModule("storm", data ++ Json.obj("i18n" -> i18nJsObject(i18nKeys)))) + .i18n("storm") + .js(PageModule("storm", data)) .zoom .zen .hrefLangs(lila.ui.LangPath(routes.Storm.home)): @@ -122,37 +123,3 @@ final class StormUi(helpers: Helpers): ) ) ) - - private val i18nKeys = - import trans.{ storm as s } - List( - s.moveToStart, - s.puzzlesSolved, - s.newDailyHighscore, - s.newWeeklyHighscore, - s.newMonthlyHighscore, - s.newAllTimeHighscore, - s.previousHighscoreWasX, - s.playAgain, - s.score, - s.moves, - s.accuracy, - s.combo, - s.time, - s.timePerMove, - s.highestSolved, - s.puzzlesPlayed, - s.newRun, - s.endRun, - s.youPlayTheWhitePiecesInAllPuzzles, - s.youPlayTheBlackPiecesInAllPuzzles, - s.failedPuzzles, - s.slowPuzzles, - s.thisWeek, - s.thisMonth, - s.allTime, - s.clickToReload, - s.thisRunHasExpired, - s.thisRunWasOpenedInAnotherTab, - trans.site.flipBoard - ) diff --git a/modules/study/src/main/ui/StudyBits.scala b/modules/study/src/main/ui/StudyBits.scala index 5b0c04f85570a..4de1b544a41e3 100644 --- a/modules/study/src/main/ui/StudyBits.scala +++ b/modules/study/src/main/ui/StudyBits.scala @@ -92,134 +92,3 @@ final class StudyBits(helpers: Helpers): ) ) ) - - val i18nKeys = - import trans.{ site, study as trs } - List( - site.name, - site.white, - site.black, - site.variant, - site.clearBoard, - site.startPosition, - site.cancel, - site.chat, - trs.addNewChapter, - trs.importFromChapterX, - trs.addMembers, - trs.inviteToTheStudy, - trs.pleaseOnlyInvitePeopleYouKnow, - trs.searchByUsername, - trs.spectator, - trs.contributor, - trs.kick, - trs.leaveTheStudy, - trs.youAreNowAContributor, - trs.youAreNowASpectator, - trs.pgnTags, - trs.like, - trs.unlike, - trs.topics, - trs.manageTopics, - trs.newTag, - trs.commentThisPosition, - trs.commentThisMove, - trs.annotateWithGlyphs, - trs.theChapterIsTooShortToBeAnalysed, - trs.onlyContributorsCanRequestAnalysis, - trs.getAFullComputerAnalysis, - trs.makeSureTheChapterIsComplete, - trs.allSyncMembersRemainOnTheSamePosition, - trs.shareChanges, - trs.playing, - trs.showEvalBar, - trs.first, - trs.previous, - trs.next, - trs.last, - trs.nextChapter, - trs.shareAndExport, - trs.cloneStudy, - trs.studyPgn, - trs.downloadAllGames, - trs.chapterPgn, - trs.copyChapterPgn, - trs.downloadGame, - trs.studyUrl, - trs.currentChapterUrl, - trs.youCanPasteThisInTheForumToEmbed, - trs.startAtInitialPosition, - trs.startAtX, - trs.embedInYourWebsite, - trs.readMoreAboutEmbedding, - trs.onlyPublicStudiesCanBeEmbedded, - trs.open, - trs.xBroughtToYouByY, - trs.studyNotFound, - trs.editChapter, - trs.newChapter, - trs.orientation, - trs.analysisMode, - trs.pinnedChapterComment, - trs.saveChapter, - trs.clearAnnotations, - trs.clearVariations, - trs.deleteChapter, - trs.deleteThisChapter, - trs.clearAllCommentsInThisChapter, - trs.rightUnderTheBoard, - trs.noPinnedComment, - trs.normalAnalysis, - trs.hideNextMoves, - trs.interactiveLesson, - trs.chapterX, - trs.empty, - trs.startFromInitialPosition, - trs.editor, - trs.startFromCustomPosition, - trs.loadAGameByUrl, - trs.loadAPositionFromFen, - trs.loadAGameFromPgn, - trs.automatic, - trs.urlOfTheGame, - trs.loadAGameFromXOrY, - trs.createChapter, - trs.createStudy, - trs.editStudy, - trs.visibility, - trs.public, - trs.`private`, - trs.unlisted, - trs.inviteOnly, - trs.allowCloning, - trs.nobody, - trs.onlyMe, - trs.contributors, - trs.members, - trs.everyone, - trs.enableSync, - trs.yesKeepEveryoneOnTheSamePosition, - trs.noLetPeopleBrowseFreely, - trs.pinnedStudyComment, - trs.start, - trs.save, - trs.clearChat, - trs.deleteTheStudyChatHistory, - trs.deleteStudy, - trs.confirmDeleteStudy, - trs.whereDoYouWantToStudyThat, - trs.nbChapters, - trs.nbGames, - trs.nbMembers, - trs.pasteYourPgnTextHereUpToNbGames - ) - - val gamebookPlayKeys = - List( - trans.study.back, - trans.study.playAgain, - trans.study.nextChapter, - trans.site.retry, - trans.study.whatWouldYouPlay, - trans.study.youCompletedThisLesson - ) diff --git a/modules/swiss/src/main/ui/SwissBitsUi.scala b/modules/swiss/src/main/ui/SwissBitsUi.scala index 1fbc5cbd53864..6b3355fee0ffe 100644 --- a/modules/swiss/src/main/ui/SwissBitsUi.scala +++ b/modules/swiss/src/main/ui/SwissBitsUi.scala @@ -84,35 +84,3 @@ final class SwissBitsUi(helpers: Helpers, getName: GetSwissName): ) ) ) - - def jsI18n(using Context) = i18nJsObject(i18nKeys) - - private val i18nKeys = List( - trans.site.join, - trans.site.withdraw, - trans.site.youArePlaying, - trans.site.joinTheGame, - trans.site.signIn, - trans.site.averageElo, - trans.site.gamesPlayed, - trans.site.whiteWins, - trans.site.blackWins, - trans.site.drawRate, - trans.swiss.byes, - trans.swiss.absences, - trans.study.downloadAllGames, - trans.site.winRate, - trans.site.points, - trans.swiss.tieBreak, - trans.site.performance, - trans.site.standByX, - trans.site.averageOpponent, - trans.site.tournamentComplete, - trans.site.tournamentEntryCode, - trans.swiss.viewAllXRounds, - trans.swiss.ongoingGames, - trans.swiss.startingIn, - trans.swiss.nextRound, - trans.swiss.nbRounds, - trans.team.joinTeam - ) diff --git a/modules/swiss/src/main/ui/SwissShow.scala b/modules/swiss/src/main/ui/SwissShow.scala index c187b4a826310..5575d0c7b52e7 100644 --- a/modules/swiss/src/main/ui/SwissShow.scala +++ b/modules/swiss/src/main/ui/SwissShow.scala @@ -31,6 +31,11 @@ final class SwissShow(helpers: Helpers, ui: SwissBitsUi, gathering: GatheringUi) val isDirector = ctx.is(s.createdBy) val hasScheduleInput = isDirector && s.settings.manualRounds && s.isNotFinished Page(fullName(s, team)) + .css("swiss.show") + .css(hasScheduleInput.option("bits.flatpickr")) + .i18n("study") + .i18n("swiss") + .i18n("team") .js(hasScheduleInput.option(Esm("bits.flatpickr"))) .js( PageModule( @@ -38,7 +43,6 @@ final class SwissShow(helpers: Helpers, ui: SwissBitsUi, gathering: GatheringUi) Json .obj( "data" -> data, - "i18n" -> ui.jsI18n, "userId" -> ctx.userId, "chat" -> chatOption.map(_._1), "showRatings" -> ctx.pref.showRatings @@ -46,8 +50,6 @@ final class SwissShow(helpers: Helpers, ui: SwissBitsUi, gathering: GatheringUi) .add("schedule" -> hasScheduleInput) ) ) - .css("swiss.show") - .css(hasScheduleInput.option("bits.flatpickr")) .graph( OpenGraph( title = s"${fullName(s, team)}: ${s.variant.name} ${s.clock.show} #${s.id}", diff --git a/modules/tournament/src/main/ui/TournamentList.scala b/modules/tournament/src/main/ui/TournamentList.scala index 135d780593242..a051b01ee47bf 100644 --- a/modules/tournament/src/main/ui/TournamentList.scala +++ b/modules/tournament/src/main/ui/TournamentList.scala @@ -28,7 +28,7 @@ final class TournamentList(helpers: Helpers, ui: TournamentUi)( .js( PageModule( "tournament.schedule", - Json.obj("data" -> json, "i18n" -> ui.scheduleJsI18n) + Json.obj("data" -> json) ) ) .hrefLangs(LangPath(routes.Tournament.home)) diff --git a/modules/tournament/src/main/ui/TournamentShow.scala b/modules/tournament/src/main/ui/TournamentShow.scala index d341e858e53b1..2070259205622 100644 --- a/modules/tournament/src/main/ui/TournamentShow.scala +++ b/modules/tournament/src/main/ui/TournamentShow.scala @@ -27,12 +27,14 @@ final class TournamentShow(helpers: Helpers, ui: TournamentUi, gathering: Gather val extraCls = tour.schedule.so: sched => s" tour-sched tour-sched-${sched.freq.name} tour-speed-${sched.speed.name} tour-variant-${sched.variant.key} tour-id-${tour.id}" Page(s"${tour.name()} #${tour.id}") + .i18n("study", "swiss") + .i18n(tour.isTeamBattle.option("team")) + .i18n(tour.isTeamBattle.option("arena")) .js( PageModule( "tournament", Json.obj( "data" -> data, - "i18n" -> ui.jsI18n(tour), "userId" -> ctx.userId, "chat" -> chat.map(_._2), "showRatings" -> ctx.pref.showRatings diff --git a/modules/tournament/src/main/ui/TournamentUi.scala b/modules/tournament/src/main/ui/TournamentUi.scala index 5b99b8d16a2a3..cb368dc68d132 100644 --- a/modules/tournament/src/main/ui/TournamentUi.scala +++ b/modules/tournament/src/main/ui/TournamentUi.scala @@ -120,56 +120,3 @@ final class TournamentUi(helpers: Helpers)(getTourName: GetTourName): tour.schedule.map(_.freq) match case Some(Schedule.Freq.Marathon | Schedule.Freq.ExperimentalMarathon) => Icon.Globe case _ => tour.spotlight.flatMap(_.iconFont) | tour.perfType.icon - - def scheduleJsI18n(using Context) = i18nJsObject(schedulei18nKeys) - - def jsI18n(tour: Tournament)(using Context) = i18nJsObject( - i18nKeys ++ (tour.isTeamBattle.so(teamBattleI18nKeys)) - ) - - private val i18nKeys = List( - trans.site.standing, - trans.site.starting, - trans.swiss.startingIn, - trans.site.tournamentIsStarting, - trans.site.youArePlaying, - trans.site.standByX, - trans.site.tournamentPairingsAreNowClosed, - trans.site.join, - trans.site.pause, - trans.site.withdraw, - trans.site.joinTheGame, - trans.site.signIn, - trans.site.averageElo, - trans.site.gamesPlayed, - trans.site.nbPlayers, - trans.site.winRate, - trans.site.berserkRate, - trans.study.downloadAllGames, - trans.site.performance, - trans.site.tournamentComplete, - trans.site.movesPlayed, - trans.site.whiteWins, - trans.site.blackWins, - trans.site.drawRate, - trans.site.nextXTournament, - trans.site.averageOpponent, - trans.site.tournamentEntryCode, - trans.site.topGames - ) - - private val teamBattleI18nKeys = List( - trans.arena.viewAllXTeams, - trans.site.players, - trans.arena.averagePerformance, - trans.arena.averageScore, - trans.team.teamPage, - trans.arena.pickYourTeam, - trans.arena.whichTeamWillYouRepresentInThisBattle, - trans.arena.youMustJoinOneOfTheseTeamsToParticipate - ) - - private val schedulei18nKeys = List( - trans.site.ratedTournament, - trans.site.casualTournament - ) diff --git a/modules/ui/src/main/Page.scala b/modules/ui/src/main/Page.scala index d18d75ce3d061..c3acf60bad0c7 100644 --- a/modules/ui/src/main/Page.scala +++ b/modules/ui/src/main/Page.scala @@ -22,6 +22,7 @@ case class Page( fullTitle: Option[String] = None, robots: Boolean = true, cssKeys: List[String] = Nil, + i18nModules: List[String] = List("site", "timeago", "preferences"), modules: EsmList = Nil, jsFrag: Option[WithNonce[Frag]] = None, pageModule: Option[PageModule] = None, @@ -41,10 +42,12 @@ case class Page( def js(f: Option[WithNonce[Frag]]): Page = f.foldLeft(this)(_.js(_)) def js(pm: PageModule): Page = copy(pageModule = pm.some) @scala.annotation.targetName("jsModuleOption") - def js(pm: Option[PageModule]): Page = copy(pageModule = pm) - def iife(iifeFrag: Frag): Page = js(_ => iifeFrag) - def iife(iifeFrag: Option[Frag]): Page = iifeFrag.foldLeft(this)(_.iife(_)) - def graph(og: OpenGraph): Page = copy(openGraph = og.some) + def js(pm: Option[PageModule]): Page = copy(pageModule = pm) + def iife(iifeFrag: Frag): Page = js(_ => iifeFrag) + def iife(iifeFrag: Option[Frag]): Page = iifeFrag.foldLeft(this)(_.iife(_)) + def i18n(cats: String*): Page = copy(i18nModules = i18nModules ::: cats.toList) + def i18n(cat: Option[String]) = copy(i18nModules = i18nModules ++ cat) + def graph(og: OpenGraph): Page = copy(openGraph = og.some) def graph(title: String, description: String, url: String): Page = graph(OpenGraph(title, description, url)) def robots(b: Boolean): Page = copy(robots = b) def css(keys: String*): Page = copy(cssKeys = cssKeys ::: keys.toList) diff --git a/modules/user/src/main/ui/UserShow.scala b/modules/user/src/main/ui/UserShow.scala index 4c3d031fbcf98..8237e971affda 100644 --- a/modules/user/src/main/ui/UserShow.scala +++ b/modules/user/src/main/ui/UserShow.scala @@ -140,11 +140,4 @@ final class UserShow(helpers: Helpers, bits: UserBits): s" Current ${p.key.perfTrans} rating: ${p.perf.intRating}." s"$name played $nbGames games since $createdAt.$currentRating" - val i18nKeys = List( - trans.site.youAreLeavingLichess, - trans.site.neverTypeYourPassword, - trans.site.cancel, - trans.site.proceedToX - ) - val dataUsername = attr("data-username") diff --git a/modules/web/src/main/ui/BoardEditorUi.scala b/modules/web/src/main/ui/BoardEditorUi.scala index 182fe9164f9fb..85357683be139 100644 --- a/modules/web/src/main/ui/BoardEditorUi.scala +++ b/modules/web/src/main/ui/BoardEditorUi.scala @@ -44,29 +44,6 @@ final class BoardEditorUi(helpers: Helpers): .obj( "baseUrl" -> s"$netBaseUrl${routes.Editor.index}", "animation" -> Json.obj("duration" -> ctx.pref.animationMillis), - "is3d" -> ctx.pref.is3d, - "i18n" -> i18nJsObject(i18nKeys) + "is3d" -> ctx.pref.is3d ) .add("fen" -> fen) - - private val i18nKeys = List( - trans.site.setTheBoard, - trans.site.boardEditor, - trans.site.startPosition, - trans.site.clearBoard, - trans.site.flipBoard, - trans.site.loadPosition, - trans.site.popularOpenings, - trans.site.endgamePositions, - trans.site.castling, - trans.site.whiteCastlingKingside, - trans.site.blackCastlingKingside, - trans.site.whitePlays, - trans.site.blackPlays, - trans.site.variant, - trans.site.continueFromHere, - trans.site.playWithTheMachine, - trans.site.playWithAFriend, - trans.site.analysis, - trans.site.toStudy - ) diff --git a/modules/web/src/main/ui/LearnUi.scala b/modules/web/src/main/ui/LearnUi.scala index 45abc5a16a78a..13eede12740ae 100644 --- a/modules/web/src/main/ui/LearnUi.scala +++ b/modules/web/src/main/ui/LearnUi.scala @@ -18,7 +18,6 @@ final class LearnUi(helpers: Helpers): "learn", Json.obj( "data" -> data, - "i18n" -> i18nJsObject(i18nKeys), "pref" -> Json.obj( "coords" -> ctx.pref.coords, "destination" -> ctx.pref.destination @@ -34,180 +33,3 @@ final class LearnUi(helpers: Helpers): .hrefLangs(lila.ui.LangPath(routes.Learn.index)) .zoom: main(id := "learn-app") - - private val i18nKeys = List( - trans.learn.play, - trans.site.yourScore, - trl.learnChess, - trl.byPlaying, - trl.menu, - trl.progressX, - trl.resetMyProgress, - trl.youWillLoseAllYourProgress, - trl.chessPieces, - trl.theRook, - trl.itMovesInStraightLines, - trl.rookIntro, - trl.rookGoal, - trl.grabAllTheStars, - trl.theFewerMoves, - trl.useTwoRooks, - trl.rookComplete, - trl.theBishop, - trl.itMovesDiagonally, - trl.bishopIntro, - trl.youNeedBothBishops, - trl.bishopComplete, - trl.theQueen, - trl.queenCombinesRookAndBishop, - trl.queenIntro, - trl.queenComplete, - trl.theKing, - trl.theMostImportantPiece, - trl.kingIntro, - trl.theKingIsSlow, - trl.lastOne, - trl.kingComplete, - trl.theKnight, - trl.itMovesInAnLShape, - trl.knightIntro, - trl.knightsHaveAFancyWay, - trl.knightsCanJumpOverObstacles, - trl.knightComplete, - trl.thePawn, - trl.itMovesForwardOnly, - trl.pawnIntro, - trl.pawnsMoveOneSquareOnly, - trl.mostOfTheTimePromotingToAQueenIsBest, - trl.pawnsMoveForward, - trl.captureThenPromote, - trl.useAllThePawns, - trl.aPawnOnTheSecondRank, - trl.grabAllTheStarsNoNeedToPromote, - trl.pawnComplete, - trl.pawnPromotion, - trl.yourPawnReachedTheEndOfTheBoard, - trl.itNowPromotesToAStrongerPiece, - trl.selectThePieceYouWant, - trl.fundamentals, - trl.capture, - trl.takeTheEnemyPieces, - trl.captureIntro, - trl.takeTheBlackPieces, - trl.takeTheBlackPiecesAndDontLoseYours, - trl.captureComplete, - trl.protection, - trl.keepYourPiecesSafe, - trl.protectionIntro, - trl.protectionComplete, - trl.escape, - trl.noEscape, - trl.dontLetThemTakeAnyUndefendedPiece, - trl.combat, - trl.captureAndDefendPieces, - trl.combatIntro, - trl.combatComplete, - trl.checkInOne, - trl.attackTheOpponentsKing, - trl.checkInOneIntro, - trl.checkInOneGoal, - trl.checkInOneComplete, - trl.outOfCheck, - trl.defendYourKing, - trl.outOfCheckIntro, - trl.escapeWithTheKing, - trl.theKingCannotEscapeButBlock, - trl.youCanGetOutOfCheckByTaking, - trl.thisKnightIsCheckingThroughYourDefenses, - trl.escapeOrBlock, - trl.outOfCheckComplete, - trl.mateInOne, - trl.defeatTheOpponentsKing, - trl.mateInOneIntro, - trl.attackYourOpponentsKing, - trl.mateInOneComplete, - trl.intermediate, - trl.boardSetup, - trl.howTheGameStarts, - trl.boardSetupIntro, - trl.thisIsTheInitialPosition, - trl.firstPlaceTheRooks, - trl.thenPlaceTheKnights, - trl.placeTheBishops, - trl.placeTheQueen, - trl.placeTheKing, - trl.pawnsFormTheFrontLine, - trl.boardSetupComplete, - trl.castling, - trl.theSpecialKingMove, - trl.castlingIntro, - trl.castleKingSide, - trl.castleQueenSide, - trl.theKnightIsInTheWay, - trl.castleKingSideMovePiecesFirst, - trl.castleQueenSideMovePiecesFirst, - trl.youCannotCastleIfMoved, - trl.youCannotCastleIfAttacked, - trl.findAWayToCastleKingSide, - trl.findAWayToCastleQueenSide, - trl.castlingComplete, - trl.enPassant, - trl.theSpecialPawnMove, - trl.enPassantIntro, - trl.blackJustMovedThePawnByTwoSquares, - trl.enPassantOnlyWorksImmediately, - trl.enPassantOnlyWorksOnFifthRank, - trl.takeAllThePawnsEnPassant, - trl.enPassantComplete, - trl.stalemate, - trl.theGameIsADraw, - trl.stalemateIntro, - trl.stalemateGoal, - trl.stalemateComplete, - trl.advanced, - trl.pieceValue, - trl.evaluatePieceStrength, - trl.pieceValueIntro, - trl.queenOverBishop, - trl.takeThePieceWithTheHighestValue, - trl.pieceValueLegal, - trl.pieceValueExchange, - trl.pieceValueComplete, - trl.checkInTwo, - trl.twoMovesToGiveCheck, - trl.checkInTwoIntro, - trl.checkInTwoGoal, - trl.checkInTwoComplete, - trl.whatNext, - trl.youKnowHowToPlayChess, - trl.register, - trl.getAFreeLichessAccount, - trl.practice, - trl.learnCommonChessPositions, - trl.puzzles, - trl.exerciseYourTacticalSkills, - trl.videos, - trl.watchInstructiveChessVideos, - trl.playPeople, - trl.opponentsFromAroundTheWorld, - trl.playMachine, - trl.testYourSkillsWithTheComputer, - trl.letsGo, - trl.stageX, - trl.awesome, - trl.excellent, - trl.greatJob, - trl.perfect, - trl.outstanding, - trl.wayToGo, - trl.yesYesYes, - trl.youreGoodAtThis, - trl.nailedIt, - trl.rightOn, - trl.stageXComplete, - trl.next, - trl.nextX, - trl.backToMenu, - trl.puzzleFailed, - trl.retry - ) diff --git a/modules/web/src/main/ui/layout.scala b/modules/web/src/main/ui/layout.scala index 99a48084a7d97..d095faf77c2ad 100644 --- a/modules/web/src/main/ui/layout.scala +++ b/modules/web/src/main/ui/layout.scala @@ -142,8 +142,12 @@ final class layout(helpers: Helpers, assetHelper: lila.web.ui.AssetFullHelper)( ) // consolidate script packaging here to dedup chunk dependencies - def sitePreload(modules: EsmList, isInquiry: Boolean)(using ctx: Context) = - scriptsPreload("site" :: (isInquiry.option("mod.inquiry") :: modules.map(_.map(_.key))).flatten) + def sitePreload(i18nDicts: List[String], modules: EsmList, isInquiry: Boolean)(using ctx: Context) = + val langs = if ctx.lang.code == "en-GB" then List("en-GB") else List("en-GB", ctx.lang.code) + val i18nModules = i18nDicts.flatMap(dict => langs.map(lang => s"i18n/$dict.$lang")) + scriptsPreload( + i18nModules ::: "site" :: (isInquiry.option("mod.inquiry") :: modules.map(_.map(_.key))).flatten + ) def scriptsPreload(keys: List[String]) = frag( @@ -334,32 +338,6 @@ final class layout(helpers: Helpers, assetHelper: lila.web.ui.AssetFullHelper)( object inlineJs: def apply(nonce: Nonce)(using Translate): Frag = embedJsUnsafe(jsCode)(nonce.some) - private val i18nKeys = List( - trans.site.pause, - trans.site.resume, - trans.site.nbFriendsOnline, - trans.site.reconnecting, - trans.site.noNetwork, - trans.timeago.justNow, - trans.timeago.inNbSeconds, - trans.timeago.inNbMinutes, - trans.timeago.inNbHours, - trans.timeago.inNbDays, - trans.timeago.inNbWeeks, - trans.timeago.inNbMonths, - trans.timeago.inNbYears, - trans.timeago.rightNow, - trans.timeago.nbMinutesAgo, - trans.timeago.nbHoursAgo, - trans.timeago.nbDaysAgo, - trans.timeago.nbWeeksAgo, - trans.timeago.nbMonthsAgo, - trans.timeago.nbYearsAgo, - trans.timeago.nbMinutesRemaining, - trans.timeago.nbHoursRemaining, - trans.timeago.completed - ) - private val cache = new java.util.concurrent.ConcurrentHashMap[Lang, String] lila.common.Bus.subscribeFun("i18n.load"): case lang: Lang => cache.remove(lang) @@ -370,7 +348,6 @@ final class layout(helpers: Helpers, assetHelper: lila.web.ui.AssetFullHelper)( _ => "if (!window.site) window.site={};" + """window.site.load=new Promise(r=>document.addEventListener("DOMContentLoaded",r));""" + - s"window.site.quantity=${jsQuantity(t.lang)};" + - s"window.site.siteI18n=${safeJsonValue(i18nJsObject(i18nKeys))};" + s"window.site.quantity=${jsQuantity(t.lang)};" ) end inlineJs diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d99ac3f10f9e2..5ecc2de12fd9a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,16 +13,16 @@ importers: version: link:ui/@types/lichess '@types/node': specifier: ^22.7.4 - version: 22.7.4 + version: 22.7.5 '@types/web': specifier: ^0.0.166 version: 0.0.166 '@typescript-eslint/eslint-plugin': specifier: ^8.7.0 - version: 8.7.0(@typescript-eslint/parser@8.7.0(eslint@9.11.1)(typescript@5.6.2))(eslint@9.11.1)(typescript@5.6.2) + version: 8.8.1(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0)(typescript@5.6.3) '@typescript-eslint/parser': specifier: ^8.7.0 - version: 8.7.0(eslint@9.11.1)(typescript@5.6.2) + version: 8.8.1(eslint@9.12.0)(typescript@5.6.3) ab: specifier: github:lichess-org/ab-stub version: https://codeload.github.com/lichess-org/ab-stub/tar.gz/94236bf34dbc9c05daf50f4c9842d859b9142be0 @@ -31,7 +31,7 @@ importers: version: 9.1.1 eslint: specifier: ^9.11.1 - version: 9.11.1 + version: 9.12.0 lint-staged: specifier: ^15.2.10 version: 15.2.10 @@ -43,7 +43,7 @@ importers: version: 3.3.3 typescript: specifier: ^5.6.2 - version: 5.6.2 + version: 5.6.3 bin: dependencies: @@ -58,7 +58,7 @@ importers: version: 25.0.1 vitest: specifier: ^2.1.1 - version: 2.1.1(@types/node@22.7.4)(jsdom@25.0.1) + version: 2.1.2(@types/node@22.7.5)(jsdom@25.0.1) ui/@types/lichess: {} @@ -480,7 +480,7 @@ importers: version: 4.17.9(prop-types@15.8.1) apexcharts: specifier: ^3.48.0 - version: 3.53.0 + version: 3.54.0 common: specifier: workspace:* version: link:../common @@ -974,8 +974,8 @@ packages: resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.11.1': - resolution: {integrity: sha512-/qu+TWz8WwPWc7/HcIJKi+c+MOm46GdVaSlTTQcaqaL53+GsoA6MxWp5PtTx48qbSP7ylM1Kn7nhvkugfJvRSA==} + '@eslint/js@9.12.0': + resolution: {integrity: sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.4': @@ -998,12 +998,20 @@ packages: '@fnando/sparkline@0.3.10': resolution: {integrity: sha512-Rwz2swatdSU5F4sCOvYG8EOWdjtLgq5d8nmnqlZ3PXdWJI9Zq9BRUvJ/9ygjajJG8qOyNpMFX3GEVFjZIuB1Jg==} + '@humanfs/core@0.19.0': + resolution: {integrity: sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.5': + resolution: {integrity: sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==} + engines: {node: '>=18.18.0'} + '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.3.0': - resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} '@jridgewell/sourcemap-codec@1.5.0': @@ -1031,83 +1039,83 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@rollup/rollup-android-arm-eabi@4.22.4': - resolution: {integrity: sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==} + '@rollup/rollup-android-arm-eabi@4.24.0': + resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.22.4': - resolution: {integrity: sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==} + '@rollup/rollup-android-arm64@4.24.0': + resolution: {integrity: sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.22.4': - resolution: {integrity: sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==} + '@rollup/rollup-darwin-arm64@4.24.0': + resolution: {integrity: sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.22.4': - resolution: {integrity: sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==} + '@rollup/rollup-darwin-x64@4.24.0': + resolution: {integrity: sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==} cpu: [x64] os: [darwin] - '@rollup/rollup-linux-arm-gnueabihf@4.22.4': - resolution: {integrity: sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==} + '@rollup/rollup-linux-arm-gnueabihf@4.24.0': + resolution: {integrity: sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.22.4': - resolution: {integrity: sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==} + '@rollup/rollup-linux-arm-musleabihf@4.24.0': + resolution: {integrity: sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.22.4': - resolution: {integrity: sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==} + '@rollup/rollup-linux-arm64-gnu@4.24.0': + resolution: {integrity: sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.22.4': - resolution: {integrity: sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==} + '@rollup/rollup-linux-arm64-musl@4.24.0': + resolution: {integrity: sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.22.4': - resolution: {integrity: sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==} + '@rollup/rollup-linux-powerpc64le-gnu@4.24.0': + resolution: {integrity: sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.22.4': - resolution: {integrity: sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==} + '@rollup/rollup-linux-riscv64-gnu@4.24.0': + resolution: {integrity: sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.22.4': - resolution: {integrity: sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==} + '@rollup/rollup-linux-s390x-gnu@4.24.0': + resolution: {integrity: sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.22.4': - resolution: {integrity: sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==} + '@rollup/rollup-linux-x64-gnu@4.24.0': + resolution: {integrity: sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.22.4': - resolution: {integrity: sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==} + '@rollup/rollup-linux-x64-musl@4.24.0': + resolution: {integrity: sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.22.4': - resolution: {integrity: sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==} + '@rollup/rollup-win32-arm64-msvc@4.24.0': + resolution: {integrity: sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.22.4': - resolution: {integrity: sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==} + '@rollup/rollup-win32-ia32-msvc@4.24.0': + resolution: {integrity: sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.22.4': - resolution: {integrity: sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==} + '@rollup/rollup-win32-x64-msvc@4.24.0': + resolution: {integrity: sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==} cpu: [x64] os: [win32] @@ -1134,9 +1142,6 @@ packages: '@types/dragscroll@0.0.3': resolution: {integrity: sha512-Nti+uvpcVv2VAkqF1+XJivEdE1rxdvWmE4mtMydm9kYBaT0/QsvQSjzqA2G0SE62RoTjVmhC48ap8yfApl8YJw==} - '@types/estree@1.0.5': - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} - '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -1146,14 +1151,11 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@20.16.9': - resolution: {integrity: sha512-rkvIVJxsOfBejxK7I0FO5sa2WxFmJCzoDwcd88+fq/CUfynNywTo/1/T6hyFz22CyztsnLS9nVlHOnTI36RH5w==} + '@types/node@20.16.11': + resolution: {integrity: sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==} - '@types/node@22.7.3': - resolution: {integrity: sha512-qXKfhXXqGTyBskvWEzJZPUxSslAiLaB6JGP1ic/XTH9ctGgzdgYguuLP1C601aRTSDNlLb0jbKqXjZ48GNraSA==} - - '@types/node@22.7.4': - resolution: {integrity: sha512-y+NPi1rFzDs1NdQHHToqeiX2TIS79SWEAw9GYhkkx8bD0ChpfqC+n2j5OXOCpzfojBEBt6DnEnnG9MY0zk1XLg==} + '@types/node@22.7.5': + resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} '@types/prop-types@15.7.13': resolution: {integrity: sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==} @@ -1161,8 +1163,8 @@ packages: '@types/qrcode@1.5.5': resolution: {integrity: sha512-CdfBi/e3Qk+3Z/fXYShipBT13OJ2fDO2Q2w5CIP5anLTLIndQG9z6P1cnm+8zCWSpm5dnxMFd/uREtb0EXuQzg==} - '@types/react@18.3.9': - resolution: {integrity: sha512-+BpAVyTpJkNWWSSnaLBk6ePpHLOGJKnEQNbINNovPWzvEUyAe3e+/d494QdEh71RekM/qV7lw6jzf1HGrJyAtQ==} + '@types/react@18.3.11': + resolution: {integrity: sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==} '@types/serviceworker@0.0.96': resolution: {integrity: sha512-AthVWRCY0KS863vJzBo5TDQh43BMii6DYKO9qMXbCPTmL1IBgMBVhrBHKXD9g/RkI1h2YVZT1TVDRKrVuAFc9g==} @@ -1182,8 +1184,8 @@ packages: '@types/zxcvbn@4.4.5': resolution: {integrity: sha512-FZJgC5Bxuqg7Rhsm/bx6gAruHHhDQ55r+s0JhDh8CQ16fD7NsJJ+p8YMMQDhSQoIrSmjpqqYWA96oQVMNkjRyA==} - '@typescript-eslint/eslint-plugin@8.7.0': - resolution: {integrity: sha512-RIHOoznhA3CCfSTFiB6kBGLQtB/sox+pJ6jeFu6FxJvqL8qRxq/FfGO/UhsGgQM9oGdXkV4xUgli+dt26biB6A==} + '@typescript-eslint/eslint-plugin@8.8.1': + resolution: {integrity: sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 @@ -1193,8 +1195,8 @@ packages: typescript: optional: true - '@typescript-eslint/parser@8.7.0': - resolution: {integrity: sha512-lN0btVpj2unxHlNYLI//BQ7nzbMJYBVQX5+pbNXvGYazdlgYonMn4AhhHifQ+J4fGRYA/m1DjaQjx+fDetqBOQ==} + '@typescript-eslint/parser@8.8.1': + resolution: {integrity: sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1203,12 +1205,12 @@ packages: typescript: optional: true - '@typescript-eslint/scope-manager@8.7.0': - resolution: {integrity: sha512-87rC0k3ZlDOuz82zzXRtQ7Akv3GKhHs0ti4YcbAJtaomllXoSO8hi7Ix3ccEvCd824dy9aIX+j3d2UMAfCtVpg==} + '@typescript-eslint/scope-manager@8.8.1': + resolution: {integrity: sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.7.0': - resolution: {integrity: sha512-tl0N0Mj3hMSkEYhLkjREp54OSb/FI6qyCzfiiclvJvOqre6hsZTGSnHtmFLDU8TIM62G7ygEa1bI08lcuRwEnQ==} + '@typescript-eslint/type-utils@8.8.1': + resolution: {integrity: sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -1216,12 +1218,12 @@ packages: typescript: optional: true - '@typescript-eslint/types@8.7.0': - resolution: {integrity: sha512-LLt4BLHFwSfASHSF2K29SZ+ZCsbQOM+LuarPjRUuHm+Qd09hSe3GCeaQbcCr+Mik+0QFRmep/FyZBO6fJ64U3w==} + '@typescript-eslint/types@8.8.1': + resolution: {integrity: sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.7.0': - resolution: {integrity: sha512-MC8nmcGHsmfAKxwnluTQpNqceniT8SteVwd2voYlmiSWGOtjvGXdPl17dYu2797GVscK30Z04WRM28CrKS9WOg==} + '@typescript-eslint/typescript-estree@8.8.1': + resolution: {integrity: sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '*' @@ -1229,23 +1231,23 @@ packages: typescript: optional: true - '@typescript-eslint/utils@8.7.0': - resolution: {integrity: sha512-ZbdUdwsl2X/s3CiyAu3gOlfQzpbuG3nTWKPoIvAu1pu5r8viiJvv2NPN2AqArL35NCYtw/lrPPfM4gxrMLNLPw==} + '@typescript-eslint/utils@8.8.1': + resolution: {integrity: sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - '@typescript-eslint/visitor-keys@8.7.0': - resolution: {integrity: sha512-b1tx0orFCCh/THWPQa2ZwWzvOeyzzp36vkJYOpVg0u8UVOIsfVrnuC9FqAw9gRKn+rG2VmWQ/zDJZzkxUnj/XQ==} + '@typescript-eslint/visitor-keys@8.8.1': + resolution: {integrity: sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/expect@2.1.1': - resolution: {integrity: sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==} + '@vitest/expect@2.1.2': + resolution: {integrity: sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg==} - '@vitest/mocker@2.1.1': - resolution: {integrity: sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==} + '@vitest/mocker@2.1.2': + resolution: {integrity: sha512-ExElkCGMS13JAJy+812fw1aCv2QO/LBK6CyO4WOPAzLTmve50gydOlWhgdBJPx2ztbADUq3JVI0C5U+bShaeEA==} peerDependencies: - '@vitest/spy': 2.1.1 + '@vitest/spy': 2.1.2 msw: ^2.3.5 vite: ^5.0.0 peerDependenciesMeta: @@ -1254,20 +1256,20 @@ packages: vite: optional: true - '@vitest/pretty-format@2.1.1': - resolution: {integrity: sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==} + '@vitest/pretty-format@2.1.2': + resolution: {integrity: sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA==} - '@vitest/runner@2.1.1': - resolution: {integrity: sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==} + '@vitest/runner@2.1.2': + resolution: {integrity: sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw==} - '@vitest/snapshot@2.1.1': - resolution: {integrity: sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==} + '@vitest/snapshot@2.1.2': + resolution: {integrity: sha512-xtAeNsZ++aRIYIUsek7VHzry/9AcxeULlegBvsdLncLmNCR6tR8SRjn8BbDP4naxtccvzTqZ+L1ltZlRCfBZFA==} - '@vitest/spy@2.1.1': - resolution: {integrity: sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==} + '@vitest/spy@2.1.2': + resolution: {integrity: sha512-GSUi5zoy+abNRJwmFhBDC0yRuVUn8WMlQscvnbbXdKLXX9dE59YbfwXxuJ/mth6eeqIzofU8BB5XDo/Ns/qK2A==} - '@vitest/utils@2.1.1': - resolution: {integrity: sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==} + '@vitest/utils@2.1.2': + resolution: {integrity: sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==} '@yaireo/tagify@4.17.9': resolution: {integrity: sha512-x9aZy22hzte7BNmMrFcYNrZH71ombgH5PnzcOVXqPevRV/m/ItSnWIvY5fOHYzpC9Uxy0+h/1P5v62fIvwq2MA==} @@ -1323,8 +1325,8 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} - apexcharts@3.53.0: - resolution: {integrity: sha512-QESZHZY3w9LPQ64PGh1gEdfjYjJ5Jp+Dfy0D/CLjsLOPTpXzdxwlNMqRj+vPbTcP0nAHgjWv1maDqcEq6u5olw==} + apexcharts@3.54.0: + resolution: {integrity: sha512-ZgI/seScffjLpwNRX/gAhIkAhpCNWiTNsdICv7qxnF0xisI23XSsaENUKIcMlyP1rbe8ECgvybDnp7plZld89A==} arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -1550,20 +1552,20 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-scope@8.0.2: - resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} + eslint-scope@8.1.0: + resolution: {integrity: sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.0.0: - resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + eslint-visitor-keys@4.1.0: + resolution: {integrity: sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.11.1: - resolution: {integrity: sha512-MobhYKIoAO1s1e4VUrgx1l1Sk2JBR/Gqjjgw8+mfgoLE2xwsHur4gdfTxyTgShrhvdVFTaJSgMiQBl1jv/AWxg==} + eslint@9.12.0: + resolution: {integrity: sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -1572,8 +1574,8 @@ packages: jiti: optional: true - espree@10.1.0: - resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} + espree@10.2.0: + resolution: {integrity: sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esquery@1.6.0: @@ -1651,8 +1653,8 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - form-data@4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + form-data@4.0.1: + resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==} engines: {node: '>= 6'} fsevents@2.3.3: @@ -1668,9 +1670,6 @@ packages: resolution: {integrity: sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==} engines: {node: '>=18'} - get-func-name@2.0.2: - resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} - get-stream@8.0.1: resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} engines: {node: '>=16'} @@ -1761,10 +1760,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} @@ -1822,8 +1817,8 @@ packages: engines: {node: '>=18.12.0'} hasBin: true - listr2@8.2.4: - resolution: {integrity: sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==} + listr2@8.2.5: + resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} engines: {node: '>=18.0.0'} locate-path@5.0.0: @@ -1845,11 +1840,11 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true - loupe@3.1.1: - resolution: {integrity: sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==} + loupe@3.1.2: + resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} - magic-string@0.30.11: - resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} + magic-string@0.30.12: + resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==} merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -1907,8 +1902,8 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - nwsapi@2.2.12: - resolution: {integrity: sha512-qXDmcVlZV4XRtKFzddidpfVP4oMSGhga+xdMc25mv8kaLUHtgzCDhUxkrN8exkGdTlLNaXj7CV3GtON7zuGZ+w==} + nwsapi@2.2.13: + resolution: {integrity: sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==} object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} @@ -2019,8 +2014,8 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} - prosemirror-commands@1.6.0: - resolution: {integrity: sha512-xn1U/g36OqXn2tn5nGmvnnimAj/g1pUx2ypJJIe8WkVX83WyJVC5LTARaxZa2AtQRwntu9Jc5zXs9gL9svp/mg==} + prosemirror-commands@1.6.1: + resolution: {integrity: sha512-tNy4uaGWzvuUYXDke7B28krndIrdQJhSh0OLpubtwtEwFbjItOj/eoAfPvstBJyyV0S2+b5t4G+4XPXdxar6pg==} prosemirror-history@1.4.1: resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==} @@ -2031,14 +2026,14 @@ packages: prosemirror-keymap@1.2.2: resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==} - prosemirror-model@1.22.3: - resolution: {integrity: sha512-V4XCysitErI+i0rKFILGt/xClnFJaohe/wrrlT2NSZ+zk8ggQfDH4x2wNK7Gm0Hp4CIoWizvXFP7L9KMaCuI0Q==} + prosemirror-model@1.23.0: + resolution: {integrity: sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==} prosemirror-state@1.4.3: resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} - prosemirror-transform@1.10.0: - resolution: {integrity: sha512-9UOgFSgN6Gj2ekQH5CTDJ8Rp/fnKR2IkYfGdzzp5zQMFsS4zDllLVx/+jGcX86YlACpG7UR5fwAXiWzxqWtBTg==} + prosemirror-transform@1.10.2: + resolution: {integrity: sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==} prosemirror-view@1.34.3: resolution: {integrity: sha512-mKZ54PrX19sSaQye+sef+YjBbNu2voNwLS1ivb6aD2IRmxRGW64HU9B644+7OfJStGLyxvOreKqEgfvXa91WIA==} @@ -2084,8 +2079,8 @@ packages: rfdc@1.4.1: resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} - rollup@4.22.4: - resolution: {integrity: sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==} + rollup@4.24.0: + resolution: {integrity: sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -2272,11 +2267,11 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} - tldts-core@6.1.47: - resolution: {integrity: sha512-6SWyFMnlst1fEt7GQVAAu16EGgFK0cLouH/2Mk6Ftlwhv3Ol40L0dlpGMcnnNiiOMyD2EV/aF3S+U2nKvvLvrA==} + tldts-core@6.1.50: + resolution: {integrity: sha512-na2EcZqmdA2iV9zHV7OHQDxxdciEpxrjbkp+aHmZgnZKHzoElLajP59np5/4+sare9fQBfixgvXKx8ev1d7ytw==} - tldts@6.1.47: - resolution: {integrity: sha512-R/K2tZ5MiY+mVrnSkNJkwqYT2vUv1lcT6wJvd2emGaMJ7PHUGRY4e3tUsdFCXgqxi2QgbHjL3yJgXCo40v9Hxw==} + tldts@6.1.50: + resolution: {integrity: sha512-q9GOap6q3KCsLMdOjXhWU5jVZ8/1dIib898JBRLsN+tBhENpBDcAVQbE0epADOjw11FhQQy9AcbqKGBQPUfTQA==} hasBin: true to-regex-range@5.0.1: @@ -2305,8 +2300,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript@5.6.2: - resolution: {integrity: sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==} + typescript@5.6.3: + resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} hasBin: true @@ -2323,8 +2318,8 @@ packages: resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} hasBin: true - vite-node@2.1.1: - resolution: {integrity: sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==} + vite-node@2.1.2: + resolution: {integrity: sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true @@ -2359,15 +2354,15 @@ packages: terser: optional: true - vitest@2.1.1: - resolution: {integrity: sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==} + vitest@2.1.2: + resolution: {integrity: sha512-veNjLizOMkRrJ6xxb+pvxN6/QAWg95mzcRjtmkepXdN87FNfxAss9RKe2far/G9cQpipfgP2taqg0KiWsquj8A==} engines: {node: ^18.0.0 || >=20.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/node': ^18.0.0 || >=20.0.0 - '@vitest/browser': 2.1.1 - '@vitest/ui': 2.1.1 + '@vitest/browser': 2.1.2 + '@vitest/ui': 2.1.2 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -2558,9 +2553,9 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true - '@eslint-community/eslint-utils@4.4.0(eslint@9.11.1)': + '@eslint-community/eslint-utils@4.4.0(eslint@9.12.0)': dependencies: - eslint: 9.11.1 + eslint: 9.12.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.11.1': {} @@ -2579,7 +2574,7 @@ snapshots: dependencies: ajv: 6.12.6 debug: 4.3.7 - espree: 10.1.0 + espree: 10.2.0 globals: 14.0.0 ignore: 5.3.2 import-fresh: 3.3.0 @@ -2589,7 +2584,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.11.1': {} + '@eslint/js@9.12.0': {} '@eslint/object-schema@2.1.4': {} @@ -2610,9 +2605,16 @@ snapshots: '@fnando/sparkline@0.3.10': {} + '@humanfs/core@0.19.0': {} + + '@humanfs/node@0.16.5': + dependencies: + '@humanfs/core': 0.19.0 + '@humanwhocodes/retry': 0.3.1 + '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.3.0': {} + '@humanwhocodes/retry@0.3.1': {} '@jridgewell/sourcemap-codec@1.5.0': {} @@ -2634,52 +2636,52 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 - '@rollup/rollup-android-arm-eabi@4.22.4': + '@rollup/rollup-android-arm-eabi@4.24.0': optional: true - '@rollup/rollup-android-arm64@4.22.4': + '@rollup/rollup-android-arm64@4.24.0': optional: true - '@rollup/rollup-darwin-arm64@4.22.4': + '@rollup/rollup-darwin-arm64@4.24.0': optional: true - '@rollup/rollup-darwin-x64@4.22.4': + '@rollup/rollup-darwin-x64@4.24.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.22.4': + '@rollup/rollup-linux-arm-gnueabihf@4.24.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.22.4': + '@rollup/rollup-linux-arm-musleabihf@4.24.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.22.4': + '@rollup/rollup-linux-arm64-gnu@4.24.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.22.4': + '@rollup/rollup-linux-arm64-musl@4.24.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.22.4': + '@rollup/rollup-linux-powerpc64le-gnu@4.24.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.22.4': + '@rollup/rollup-linux-riscv64-gnu@4.24.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.22.4': + '@rollup/rollup-linux-s390x-gnu@4.24.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.22.4': + '@rollup/rollup-linux-x64-gnu@4.24.0': optional: true - '@rollup/rollup-linux-x64-musl@4.22.4': + '@rollup/rollup-linux-x64-musl@4.24.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.22.4': + '@rollup/rollup-win32-arm64-msvc@4.24.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.22.4': + '@rollup/rollup-win32-ia32-msvc@4.24.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.22.4': + '@rollup/rollup-win32-x64-msvc@4.24.0': optional: true '@textcomplete/core@0.1.13': @@ -2698,11 +2700,11 @@ snapshots: '@toast-ui/editor@3.2.2': dependencies: dompurify: 2.5.7 - prosemirror-commands: 1.6.0 + prosemirror-commands: 1.6.1 prosemirror-history: 1.4.1 prosemirror-inputrules: 1.4.0 prosemirror-keymap: 1.2.2 - prosemirror-model: 1.22.3 + prosemirror-model: 1.23.0 prosemirror-state: 1.4.3 prosemirror-view: 1.34.3 @@ -2712,23 +2714,17 @@ snapshots: '@types/dragscroll@0.0.3': {} - '@types/estree@1.0.5': {} - '@types/estree@1.0.6': {} '@types/fnando__sparkline@0.3.7': {} '@types/json-schema@7.0.15': {} - '@types/node@20.16.9': + '@types/node@20.16.11': dependencies: undici-types: 6.19.8 - '@types/node@22.7.3': - dependencies: - undici-types: 6.19.8 - - '@types/node@22.7.4': + '@types/node@22.7.5': dependencies: undici-types: 6.19.8 @@ -2736,9 +2732,9 @@ snapshots: '@types/qrcode@1.5.5': dependencies: - '@types/node': 22.7.3 + '@types/node': 22.7.5 - '@types/react@18.3.9': + '@types/react@18.3.11': dependencies: '@types/prop-types': 15.7.13 csstype: 3.1.3 @@ -2753,129 +2749,129 @@ snapshots: '@types/yaireo__tagify@4.17.5': dependencies: - '@types/react': 18.3.9 + '@types/react': 18.3.11 '@types/zxcvbn@4.4.5': {} - '@typescript-eslint/eslint-plugin@8.7.0(@typescript-eslint/parser@8.7.0(eslint@9.11.1)(typescript@5.6.2))(eslint@9.11.1)(typescript@5.6.2)': + '@typescript-eslint/eslint-plugin@8.8.1(@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3))(eslint@9.12.0)(typescript@5.6.3)': dependencies: '@eslint-community/regexpp': 4.11.1 - '@typescript-eslint/parser': 8.7.0(eslint@9.11.1)(typescript@5.6.2) - '@typescript-eslint/scope-manager': 8.7.0 - '@typescript-eslint/type-utils': 8.7.0(eslint@9.11.1)(typescript@5.6.2) - '@typescript-eslint/utils': 8.7.0(eslint@9.11.1)(typescript@5.6.2) - '@typescript-eslint/visitor-keys': 8.7.0 - eslint: 9.11.1 + '@typescript-eslint/parser': 8.8.1(eslint@9.12.0)(typescript@5.6.3) + '@typescript-eslint/scope-manager': 8.8.1 + '@typescript-eslint/type-utils': 8.8.1(eslint@9.12.0)(typescript@5.6.3) + '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.8.1 + eslint: 9.12.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.6.2) + ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.7.0(eslint@9.11.1)(typescript@5.6.2)': + '@typescript-eslint/parser@8.8.1(eslint@9.12.0)(typescript@5.6.3)': dependencies: - '@typescript-eslint/scope-manager': 8.7.0 - '@typescript-eslint/types': 8.7.0 - '@typescript-eslint/typescript-estree': 8.7.0(typescript@5.6.2) - '@typescript-eslint/visitor-keys': 8.7.0 + '@typescript-eslint/scope-manager': 8.8.1 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.3) + '@typescript-eslint/visitor-keys': 8.8.1 debug: 4.3.7 - eslint: 9.11.1 + eslint: 9.12.0 optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.7.0': + '@typescript-eslint/scope-manager@8.8.1': dependencies: - '@typescript-eslint/types': 8.7.0 - '@typescript-eslint/visitor-keys': 8.7.0 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/visitor-keys': 8.8.1 - '@typescript-eslint/type-utils@8.7.0(eslint@9.11.1)(typescript@5.6.2)': + '@typescript-eslint/type-utils@8.8.1(eslint@9.12.0)(typescript@5.6.3)': dependencies: - '@typescript-eslint/typescript-estree': 8.7.0(typescript@5.6.2) - '@typescript-eslint/utils': 8.7.0(eslint@9.11.1)(typescript@5.6.2) + '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.3) + '@typescript-eslint/utils': 8.8.1(eslint@9.12.0)(typescript@5.6.3) debug: 4.3.7 - ts-api-utils: 1.3.0(typescript@5.6.2) + ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - eslint - supports-color - '@typescript-eslint/types@8.7.0': {} + '@typescript-eslint/types@8.8.1': {} - '@typescript-eslint/typescript-estree@8.7.0(typescript@5.6.2)': + '@typescript-eslint/typescript-estree@8.8.1(typescript@5.6.3)': dependencies: - '@typescript-eslint/types': 8.7.0 - '@typescript-eslint/visitor-keys': 8.7.0 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/visitor-keys': 8.8.1 debug: 4.3.7 fast-glob: 3.3.2 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.6.2) + ts-api-utils: 1.3.0(typescript@5.6.3) optionalDependencies: - typescript: 5.6.2 + typescript: 5.6.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.7.0(eslint@9.11.1)(typescript@5.6.2)': + '@typescript-eslint/utils@8.8.1(eslint@9.12.0)(typescript@5.6.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.11.1) - '@typescript-eslint/scope-manager': 8.7.0 - '@typescript-eslint/types': 8.7.0 - '@typescript-eslint/typescript-estree': 8.7.0(typescript@5.6.2) - eslint: 9.11.1 + '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0) + '@typescript-eslint/scope-manager': 8.8.1 + '@typescript-eslint/types': 8.8.1 + '@typescript-eslint/typescript-estree': 8.8.1(typescript@5.6.3) + eslint: 9.12.0 transitivePeerDependencies: - supports-color - typescript - '@typescript-eslint/visitor-keys@8.7.0': + '@typescript-eslint/visitor-keys@8.8.1': dependencies: - '@typescript-eslint/types': 8.7.0 + '@typescript-eslint/types': 8.8.1 eslint-visitor-keys: 3.4.3 - '@vitest/expect@2.1.1': + '@vitest/expect@2.1.2': dependencies: - '@vitest/spy': 2.1.1 - '@vitest/utils': 2.1.1 + '@vitest/spy': 2.1.2 + '@vitest/utils': 2.1.2 chai: 5.1.1 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.1(@vitest/spy@2.1.1)(vite@5.4.8(@types/node@22.7.4))': + '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.5))': dependencies: - '@vitest/spy': 2.1.1 + '@vitest/spy': 2.1.2 estree-walker: 3.0.3 - magic-string: 0.30.11 + magic-string: 0.30.12 optionalDependencies: - vite: 5.4.8(@types/node@22.7.4) + vite: 5.4.8(@types/node@22.7.5) - '@vitest/pretty-format@2.1.1': + '@vitest/pretty-format@2.1.2': dependencies: tinyrainbow: 1.2.0 - '@vitest/runner@2.1.1': + '@vitest/runner@2.1.2': dependencies: - '@vitest/utils': 2.1.1 + '@vitest/utils': 2.1.2 pathe: 1.1.2 - '@vitest/snapshot@2.1.1': + '@vitest/snapshot@2.1.2': dependencies: - '@vitest/pretty-format': 2.1.1 - magic-string: 0.30.11 + '@vitest/pretty-format': 2.1.2 + magic-string: 0.30.12 pathe: 1.1.2 - '@vitest/spy@2.1.1': + '@vitest/spy@2.1.2': dependencies: tinyspy: 3.0.2 - '@vitest/utils@2.1.1': + '@vitest/utils@2.1.2': dependencies: - '@vitest/pretty-format': 2.1.1 - loupe: 3.1.1 + '@vitest/pretty-format': 2.1.2 + loupe: 3.1.2 tinyrainbow: 1.2.0 '@yaireo/tagify@4.17.9(prop-types@15.8.1)': @@ -2924,7 +2920,7 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 - apexcharts@3.53.0: + apexcharts@3.54.0: dependencies: '@yr/monotone-cubic-spline': 1.0.3 svg.draggable.js: 2.2.2 @@ -2972,7 +2968,7 @@ snapshots: assertion-error: 2.0.1 check-error: 2.1.1 deep-eql: 5.0.2 - loupe: 3.1.1 + loupe: 3.1.2 pathval: 2.0.0 chalk@4.1.2: @@ -3142,27 +3138,27 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-scope@8.0.2: + eslint-scope@8.1.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.0.0: {} + eslint-visitor-keys@4.1.0: {} - eslint@9.11.1: + eslint@9.12.0: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.11.1) + '@eslint-community/eslint-utils': 4.4.0(eslint@9.12.0) '@eslint-community/regexpp': 4.11.1 '@eslint/config-array': 0.18.0 '@eslint/core': 0.6.0 '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.11.1 + '@eslint/js': 9.12.0 '@eslint/plugin-kit': 0.2.0 + '@humanfs/node': 0.16.5 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.3.0 - '@nodelib/fs.walk': 1.2.8 + '@humanwhocodes/retry': 0.3.1 '@types/estree': 1.0.6 '@types/json-schema': 7.0.15 ajv: 6.12.6 @@ -3170,9 +3166,9 @@ snapshots: cross-spawn: 7.0.3 debug: 4.3.7 escape-string-regexp: 4.0.0 - eslint-scope: 8.0.2 - eslint-visitor-keys: 4.0.0 - espree: 10.1.0 + eslint-scope: 8.1.0 + eslint-visitor-keys: 4.1.0 + espree: 10.2.0 esquery: 1.6.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -3182,22 +3178,20 @@ snapshots: ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 text-table: 0.2.0 transitivePeerDependencies: - supports-color - espree@10.1.0: + espree@10.2.0: dependencies: acorn: 8.12.1 acorn-jsx: 5.3.2(acorn@8.12.1) - eslint-visitor-keys: 4.0.0 + eslint-visitor-keys: 4.1.0 esquery@1.6.0: dependencies: @@ -3280,7 +3274,7 @@ snapshots: flatted@3.3.1: {} - form-data@4.0.0: + form-data@4.0.1: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -3293,8 +3287,6 @@ snapshots: get-east-asian-width@1.2.0: {} - get-func-name@2.0.2: {} - get-stream@8.0.1: {} glob-parent@5.1.2: @@ -3368,8 +3360,6 @@ snapshots: is-number@7.0.0: {} - is-path-inside@3.0.3: {} - is-potential-custom-element-name@1.0.1: {} is-stream@3.0.0: {} @@ -3387,12 +3377,12 @@ snapshots: cssstyle: 4.1.0 data-urls: 5.0.0 decimal.js: 10.4.3 - form-data: 4.0.0 + form-data: 4.0.1 html-encoding-sniffer: 4.0.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.5 is-potential-custom-element-name: 1.0.1 - nwsapi: 2.2.12 + nwsapi: 2.2.13 parse5: 7.1.2 rrweb-cssom: 0.7.1 saxes: 6.0.0 @@ -3427,7 +3417,7 @@ snapshots: lichess-pgn-viewer@2.1.3: dependencies: - '@types/node': 20.16.9 + '@types/node': 20.16.11 chessground: 9.1.1 chessops: 0.12.7 snabbdom: 3.6.2 @@ -3443,7 +3433,7 @@ snapshots: debug: 4.3.7 execa: 8.0.1 lilconfig: 3.1.2 - listr2: 8.2.4 + listr2: 8.2.5 micromatch: 4.0.8 pidtree: 0.6.0 string-argv: 0.3.2 @@ -3451,7 +3441,7 @@ snapshots: transitivePeerDependencies: - supports-color - listr2@8.2.4: + listr2@8.2.5: dependencies: cli-truncate: 4.0.0 colorette: 2.0.20 @@ -3482,11 +3472,9 @@ snapshots: dependencies: js-tokens: 4.0.0 - loupe@3.1.1: - dependencies: - get-func-name: 2.0.2 + loupe@3.1.2: {} - magic-string@0.30.11: + magic-string@0.30.12: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 @@ -3531,7 +3519,7 @@ snapshots: dependencies: path-key: 4.0.0 - nwsapi@2.2.12: {} + nwsapi@2.2.13: {} object-assign@4.1.1: {} @@ -3633,48 +3621,48 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 - prosemirror-commands@1.6.0: + prosemirror-commands@1.6.1: dependencies: - prosemirror-model: 1.22.3 + prosemirror-model: 1.23.0 prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.0 + prosemirror-transform: 1.10.2 prosemirror-history@1.4.1: dependencies: prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.0 + prosemirror-transform: 1.10.2 prosemirror-view: 1.34.3 rope-sequence: 1.3.4 prosemirror-inputrules@1.4.0: dependencies: prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.0 + prosemirror-transform: 1.10.2 prosemirror-keymap@1.2.2: dependencies: prosemirror-state: 1.4.3 w3c-keyname: 2.2.8 - prosemirror-model@1.22.3: + prosemirror-model@1.23.0: dependencies: orderedmap: 2.1.1 prosemirror-state@1.4.3: dependencies: - prosemirror-model: 1.22.3 - prosemirror-transform: 1.10.0 + prosemirror-model: 1.23.0 + prosemirror-transform: 1.10.2 prosemirror-view: 1.34.3 - prosemirror-transform@1.10.0: + prosemirror-transform@1.10.2: dependencies: - prosemirror-model: 1.22.3 + prosemirror-model: 1.23.0 prosemirror-view@1.34.3: dependencies: - prosemirror-model: 1.22.3 + prosemirror-model: 1.23.0 prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.0 + prosemirror-transform: 1.10.2 punycode@2.3.1: {} @@ -3707,26 +3695,26 @@ snapshots: rfdc@1.4.1: {} - rollup@4.22.4: + rollup@4.24.0: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.22.4 - '@rollup/rollup-android-arm64': 4.22.4 - '@rollup/rollup-darwin-arm64': 4.22.4 - '@rollup/rollup-darwin-x64': 4.22.4 - '@rollup/rollup-linux-arm-gnueabihf': 4.22.4 - '@rollup/rollup-linux-arm-musleabihf': 4.22.4 - '@rollup/rollup-linux-arm64-gnu': 4.22.4 - '@rollup/rollup-linux-arm64-musl': 4.22.4 - '@rollup/rollup-linux-powerpc64le-gnu': 4.22.4 - '@rollup/rollup-linux-riscv64-gnu': 4.22.4 - '@rollup/rollup-linux-s390x-gnu': 4.22.4 - '@rollup/rollup-linux-x64-gnu': 4.22.4 - '@rollup/rollup-linux-x64-musl': 4.22.4 - '@rollup/rollup-win32-arm64-msvc': 4.22.4 - '@rollup/rollup-win32-ia32-msvc': 4.22.4 - '@rollup/rollup-win32-x64-msvc': 4.22.4 + '@rollup/rollup-android-arm-eabi': 4.24.0 + '@rollup/rollup-android-arm64': 4.24.0 + '@rollup/rollup-darwin-arm64': 4.24.0 + '@rollup/rollup-darwin-x64': 4.24.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.24.0 + '@rollup/rollup-linux-arm-musleabihf': 4.24.0 + '@rollup/rollup-linux-arm64-gnu': 4.24.0 + '@rollup/rollup-linux-arm64-musl': 4.24.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.24.0 + '@rollup/rollup-linux-riscv64-gnu': 4.24.0 + '@rollup/rollup-linux-s390x-gnu': 4.24.0 + '@rollup/rollup-linux-x64-gnu': 4.24.0 + '@rollup/rollup-linux-x64-musl': 4.24.0 + '@rollup/rollup-win32-arm64-msvc': 4.24.0 + '@rollup/rollup-win32-ia32-msvc': 4.24.0 + '@rollup/rollup-win32-x64-msvc': 4.24.0 fsevents: 2.3.3 rope-sequence@1.3.4: {} @@ -3875,11 +3863,11 @@ snapshots: tinyspy@3.0.2: {} - tldts-core@6.1.47: {} + tldts-core@6.1.50: {} - tldts@6.1.47: + tldts@6.1.50: dependencies: - tldts-core: 6.1.47 + tldts-core: 6.1.50 to-regex-range@5.0.1: dependencies: @@ -3887,7 +3875,7 @@ snapshots: tough-cookie@5.0.0: dependencies: - tldts: 6.1.47 + tldts: 6.1.50 tr46@5.0.0: dependencies: @@ -3895,15 +3883,15 @@ snapshots: tree-kill@1.2.2: {} - ts-api-utils@1.3.0(typescript@5.6.2): + ts-api-utils@1.3.0(typescript@5.6.3): dependencies: - typescript: 5.6.2 + typescript: 5.6.3 type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript@5.6.2: {} + typescript@5.6.3: {} undate@0.3.0: {} @@ -3915,12 +3903,12 @@ snapshots: uuid@9.0.0: {} - vite-node@2.1.1(@types/node@22.7.4): + vite-node@2.1.2(@types/node@22.7.5): dependencies: cac: 6.7.14 debug: 4.3.7 pathe: 1.1.2 - vite: 5.4.8(@types/node@22.7.4) + vite: 5.4.8(@types/node@22.7.5) transitivePeerDependencies: - '@types/node' - less @@ -3932,38 +3920,38 @@ snapshots: - supports-color - terser - vite@5.4.8(@types/node@22.7.4): + vite@5.4.8(@types/node@22.7.5): dependencies: esbuild: 0.21.5 postcss: 8.4.47 - rollup: 4.22.4 + rollup: 4.24.0 optionalDependencies: - '@types/node': 22.7.4 + '@types/node': 22.7.5 fsevents: 2.3.3 - vitest@2.1.1(@types/node@22.7.4)(jsdom@25.0.1): + vitest@2.1.2(@types/node@22.7.5)(jsdom@25.0.1): dependencies: - '@vitest/expect': 2.1.1 - '@vitest/mocker': 2.1.1(@vitest/spy@2.1.1)(vite@5.4.8(@types/node@22.7.4)) - '@vitest/pretty-format': 2.1.1 - '@vitest/runner': 2.1.1 - '@vitest/snapshot': 2.1.1 - '@vitest/spy': 2.1.1 - '@vitest/utils': 2.1.1 + '@vitest/expect': 2.1.2 + '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.5)) + '@vitest/pretty-format': 2.1.2 + '@vitest/runner': 2.1.2 + '@vitest/snapshot': 2.1.2 + '@vitest/spy': 2.1.2 + '@vitest/utils': 2.1.2 chai: 5.1.1 debug: 4.3.7 - magic-string: 0.30.11 + magic-string: 0.30.12 pathe: 1.1.2 std-env: 3.7.0 tinybench: 2.9.0 tinyexec: 0.3.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.4.8(@types/node@22.7.4) - vite-node: 2.1.1(@types/node@22.7.4) + vite: 5.4.8(@types/node@22.7.5) + vite-node: 2.1.2(@types/node@22.7.5) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.7.4 + '@types/node': 22.7.5 jsdom: 25.0.1 transitivePeerDependencies: - less diff --git a/translation/dest/site/de-CH.xml b/translation/dest/site/de-CH.xml index a567405a4cf75..cd512fae6d359 100644 --- a/translation/dest/site/de-CH.xml +++ b/translation/dest/site/de-CH.xml @@ -233,7 +233,6 @@ Au vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multiko Partii isch am laufä Schpillt jetzt Beändet - ändet in %s Partii abbrächä Partii abbrochä Standard @@ -416,9 +415,6 @@ Au vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multiko Wähl än möglichscht sichäre Namä fürs Turniär. Alles - au liecht Unagmässes - cha zur Schlüssig vu dim Konto fühere. Frei laa zum s\'Turnier nach eme namhafte Schachspiller z\'benänne. - Mir empfehled, das nöd z\'ändere. - Wänn du Teilnahmebedingige verlangsch, wird dis Turnier weniger Schpiller ha. - Zeig die erwiterete Iischtellige Mach das Turnier privat und beschränk de Zuegang mit Passwort Mach mit Usstiige @@ -457,10 +453,7 @@ Au vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multiko Nur falls vorhandä Profil Profil bearbeite - Vornamä - Nachname Biografii - Land oder Fahne Ich danke dir! Social Media Links Nur 1 Web Adrässe (URL) pro Zile. @@ -477,7 +470,6 @@ Au vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multiko Nach em Zug automatisch zur nächschte Partie Automatische Wächsel Ufgabe - Turnier Sieger Namä Beschriibig Privati Beschriibig @@ -500,11 +492,8 @@ Au vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multiko Grund Was isch s\'Problem? Bschiss - Beleidigung Troll - Manipulation vu de Wertig Suschtigs - Füeg de Link dere/dene Partie bii und erchlär, wieso sich de Benutzer falsch benah hät. Säg nöd nur eifach \"er bschiist\", schrib eus wie du da druf chunnsch. (in änglisch gschribeni Mäldige, werded schnäller behandlet). Bitte gib mindeschtens 1 Link zume Schpiel aa, wo bschisse worde isch. vu %s Importiert vu %s @@ -710,9 +699,6 @@ Au vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multiko Dunkel Durchsichtig Hindergrund-Bild-URL: - Brättgrössi und 3D - Brättdesign - Brättgrössi Figureart I dini Website integriere De Benutzername isch bereits vergeh, bitte probier en andere. @@ -784,7 +770,6 @@ Au vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multiko Äxgüsi :( Mir händ dich für es Wiili müesse schperre. - Die Schperrig ändet i %s. Wieso? Mir wänd allne e möglichscht gueti Schach-Erfahrig büte. Zum dem Zwäck müend mir sicher sii, dass sich all Schpiller korräkt verhalted. @@ -841,7 +826,6 @@ Au vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multiko D\'Ziit isch fascht abgloffe! [Klick, zum die E-Mail-Adrässe aazeige] Abälade - Willkomme! Lichess isch e Wohltätigkeitsorganisation und e völlig choschtelosi/freii Open-Source-Software. Alli Betriebs-Choschte, d\'Entwicklig und d\'Inhält werded usschliesslich dur Benutzerschpände finanziert. Trainer Verwalter @@ -856,8 +840,6 @@ und beiiflussed dini Wertig Minimum gwerteti Partie Minimali Wertig Maximali wüchentlichi Wertig - Nur für Schpiller mit Titel - Für das Turnier muesch en offizielle Titel haa E gültigi FEN startet jedes Schpiil ab ere beschtimmte Schtellig. Das funktioniert nur für Standardschpiil und nöd für Variante! Chasch mit %s so e FEN-Schtelliig generiere und sie dänn im Fäld diff --git a/translation/dest/site/pa-PK.xml b/translation/dest/site/pa-PK.xml index 7338813e4fc1b..c1df8af6d6f61 100644 --- a/translation/dest/site/pa-PK.xml +++ b/translation/dest/site/pa-PK.xml @@ -75,7 +75,6 @@ ਹੁਣੇ ਖੇਡ ਰਿਹਾ ਹੈ ਹੁਣੇ ਖੇਡ ਰਿਹਾ ਹੈ ਸਮਾਪਤ ਹੋਇਆ - ਮੁਕੰਮਲ %s ਖੇਡੋ ਭੇਜੋ ਮੁਫਤ ਦਾ ਸ਼ਤਰੰਜ diff --git a/ui/.build/package.json b/ui/.build/package.json index f3cd442eaa6a1..86909ea0b6177 100644 --- a/ui/.build/package.json +++ b/ui/.build/package.json @@ -10,6 +10,7 @@ "@types/tinycolor2": "^1.4.6", "esbuild": "^0.24.0", "fast-glob": "^3.3.2", + "fast-xml-parser": "^4.5.0", "tinycolor2": "^1.6.0", "typescript": "^5.6.2" }, diff --git a/ui/.build/readme b/ui/.build/readme index 2666be72f01a7..4159e1aeff02c 100644 --- a/ui/.build/readme +++ b/ui/.build/readme @@ -7,14 +7,14 @@ ui/build was lovingly crafted from a single block of wood as a personal gift to Options: -h, --help show this help and exit -w, --watch build and watch for changes - -c, --clean-build clean build artifacts then build + -c, --clean-build build fresh artifacts -p, --prod build minified assets (prod builds) -n, --no-install don't run pnpm install -d, --debug build assets with site.debug = true -l, --log= monkey patch console log functions in javascript manifest to POST log messages to or localhost:8666 (default). if used with --watch, the ui/build process will listen for http on 8666 and display received json as 'web' in build logs - --clean clean all build artifacts and exit + --clean clean all build artifacts, including translation/js, and exit --update update ui/build's node_modules --no-color don't use color in logs --no-time don't log the time diff --git a/ui/.build/src/build.ts b/ui/.build/src/build.ts index bdec6e05b081e..b44515e65e696 100644 --- a/ui/.build/src/build.ts +++ b/ui/.build/src/build.ts @@ -10,6 +10,7 @@ import { monitor, stopMonitor } from './monitor.ts'; import { writeManifest } from './manifest.ts'; import { clean } from './clean.ts'; import { type Package, env, errorMark, colors as c } from './main.ts'; +import { i18n, stopI18n } from './i18n.ts'; export async function build(pkgs: string[]): Promise { await stop(); @@ -38,7 +39,8 @@ export async function build(pkgs: string[]): Promise { ]); monitor(pkgs); - await Promise.all([sass(), copies(), esbuild(tsc())]); + await Promise.all([sass(), copies(), i18n()]); + await esbuild(tsc()); } export async function stop(): Promise { @@ -46,6 +48,7 @@ export async function stop(): Promise { stopSass(); stopTsc(); stopCopies(); + stopI18n(); await stopEsbuild(); } @@ -68,6 +71,8 @@ export function prePackage(pkg: Package | undefined): void { }); } +export const quantize = (n?: number, factor = 10000) => Math.floor((n ?? 0) / factor) * factor; + function depsOne(pkgName: string): Package[] { const collect = (dep: string): string[] => [...(env.deps.get(dep) || []).flatMap(d => collect(d)), dep]; return unique(collect(pkgName).map(name => env.packages.get(name))); diff --git a/ui/.build/src/clean.ts b/ui/.build/src/clean.ts index 084e6ae21e5c5..70bf0331686fa 100644 --- a/ui/.build/src/clean.ts +++ b/ui/.build/src/clean.ts @@ -32,3 +32,7 @@ export async function clean(globs?: string[]): Promise { } } } + +export async function deepClean(): Promise { + return clean(['ui/@types/lichess/site.d.ts', 'translation/js', ...allGlobs]); +} diff --git a/ui/.build/src/copies.ts b/ui/.build/src/copies.ts index 7e93aacf5a370..19883033f054a 100644 --- a/ui/.build/src/copies.ts +++ b/ui/.build/src/copies.ts @@ -3,6 +3,7 @@ import path from 'node:path'; import { globArray, globArrays } from './parse.ts'; import { hashedManifest, writeManifest } from './manifest.ts'; import { type Sync, env, errorMark, colors as c } from './main.ts'; +import { quantize } from './build.ts'; const syncWatch: fs.FSWatcher[] = []; let watchTimeout: NodeJS.Timeout | undefined; @@ -105,5 +106,3 @@ async function syncOne(absSrc: string, absDest: string, pkgName: string) { env.log(`[${c.grey(pkgName)}] - ${errorMark} - failed sync '${c.cyan(absSrc)}' to '${c.cyan(absDest)}'`); } } - -const quantize = (n?: number, factor = 10000) => Math.floor((n ?? 0) / factor) * factor; diff --git a/ui/.build/src/i18n.ts b/ui/.build/src/i18n.ts new file mode 100644 index 0000000000000..dab92961007e5 --- /dev/null +++ b/ui/.build/src/i18n.ts @@ -0,0 +1,214 @@ +import path from 'node:path'; +import fs from 'node:fs'; +import { XMLParser } from 'fast-xml-parser'; +import { env, colors as c } from './main.ts'; +import { globArray } from './parse.ts'; +import { i18nManifest } from './manifest.ts'; +import { quantize } from './build.ts'; +import { transform } from 'esbuild'; + +type Plural = { [key in 'zero' | 'one' | 'two' | 'few' | 'many' | 'other']?: string }; +type Dict = Map; +type DictMap = Map>; + +let locales: string[], dicts: string[]; +let watchTimeout: NodeJS.Timeout | undefined; +const i18nWatch: fs.FSWatcher[] = []; +const isFormat = /%(?:[\d]\$)?s/; + +const tsPrelude = `// Generated +interface I18nFormat { + (...args: (string | number)[]): string; + asArray: (...args: T[]) => (T | string)[], // vdom +} +interface I18nPlural { + (quantity: number, ...args: (string | number)[]): string, // pluralSame + raw: (quantity: number, ...args: (string | number)[]) => string, // plural + asArray: (quantity: number, ...args: T[]) => (T | string)[], // vdomPlural +} +interface I18n { + /** Global noarg key lookup (only if absolutely necessary). */ + (key: string): string;\n\n`; + +const jsPrelude = + '"use strict";(()=>{' + + ( + await transform( + // s(...) is the standard format function, p(...) is the plural format function. + // both have an asArray method for vdom. + `function p(t) { + let r = (n, ...e) => l(o(t, n), n, ...e).join(''); + return (r.asArray = (n, ...e) => l(o(t, n), ...e)), r; + } + function s(t) { + let r = (...n) => l(t, ...n).join(''); + return (r.asArray = (...n) => l(t, ...n)), r; + } + function o(t, n) { + return t[site.quantity(n)] || t.other || t.one || ''; + } + function l(t, ...r) { + let n = t.split(/(%(?:\\d\\$)?s)/); + if (r.length) { + let e = n.indexOf('%s'); + if (e !== -1) n[e] = r[0]; + else + for (let i = 0; i < r.length; i++) { + let s = n.indexOf('%' + (i + 1) + '$s'); + s !== -1 && (n[s] = r[i]); + } + } + return n; + }`, + { minify: true, loader: 'js' }, + ) + ).code; + +export function stopI18n(): void { + clearTimeout(watchTimeout); + watchTimeout = undefined; + for (const watcher of i18nWatch) watcher.close(); + i18nWatch.length = 0; +} + +export async function i18n(): Promise { + if (!env.i18n) return; + + [locales, dicts] = ( + await Promise.all([ + globArray('*.xml', { cwd: path.join(env.i18nDestDir, 'site'), absolute: false }), + globArray('*.xml', { cwd: env.i18nSrcDir, absolute: false }), + ]) + ).map(list => list.map(x => x.split('.')[0])); + + await compileTypings(); + compileJavascripts(); // no await + + if (!env.watch) return; + + const onChange = () => { + clearTimeout(watchTimeout); + watchTimeout = setTimeout(() => compileTypings().then(() => compileJavascripts(false)), 2000); + }; + i18nWatch.push(fs.watch(env.i18nSrcDir, onChange)); + for (const d of dicts) { + i18nWatch.push(fs.watch(path.join(env.i18nDestDir, d), onChange)); + } +} + +async function compileTypings(): Promise { + const [tstat] = await Promise.all([ + fs.promises.stat(path.join(env.typesDir, 'lichess', `i18n.d.ts`)).catch(() => undefined), + fs.promises.mkdir(env.i18nJsDir).catch(() => {}), + ]); + const dictStats = await Promise.all(dicts.map(d => updated(d))); + if (!tstat || dictStats.some(x => x && x.mtimeMs > tstat.mtimeMs)) { + env.log(`Building ${c.grey('i18n')}`); + const dictMap = new Map(); + await Promise.all( + dicts.map(async d => + dictMap.set(d, parseXml(await fs.promises.readFile(path.join(env.i18nSrcDir, `${d}.xml`), 'utf8'))), + ), + ); + await writeTypescript(dictMap); + } +} + +async function compileJavascripts(dirty: boolean = true): Promise { + for (const dict of dicts) { + await Promise.all( + [undefined, ...locales].map(locale => + updated(dict, locale).then(async xstat => { + if (!xstat) return; + if (!dirty) env.log(`Building ${c.grey('i18n')}`); + dirty = true; + return writeJavascript(dict, locale, xstat); + }), + ), + ); + } + if (dirty) i18nManifest(); +} + +async function updated(dict: string, locale?: string): Promise { + const xmlPath = locale + ? path.join(env.i18nDestDir, dict, `${locale}.xml`) + : path.join(env.i18nSrcDir, `${dict}.xml`); + const jsPath = path.join(env.i18nJsDir, `${dict}.${locale ?? 'en-GB'}.js`); + const [xml, js] = await Promise.allSettled([fs.promises.stat(xmlPath), fs.promises.stat(jsPath)]); + return xml.status === 'rejected' || + (js.status !== 'rejected' && quantize(xml.value.mtimeMs, 2000) === quantize(js.value.mtimeMs, 2000)) + ? false + : xml.value; +} + +async function writeJavascript(dict: string, locale?: string, xstat: fs.Stats | false = false) { + const translations = parseXml( + await fs.promises.readFile( + locale ? path.join(env.i18nDestDir, dict, `${locale}.xml`) : path.join(env.i18nSrcDir, `${dict}.xml`), + 'utf-8', + ), + ); + const jsInit = + dict === 'site' && !locale + ? 'window.i18n=function(k){for(let v of Object.values(window.i18n))if(v[k])return v[k];return k};' + : ''; + const code = + jsPrelude + + jsInit + + `if(!window.i18n.${dict})window.i18n.${dict}={};` + + `let i=window.i18n.${dict};` + + [...translations] + .map( + ([k, v]) => + `i['${k}']=` + + (typeof v !== 'string' + ? `p(${JSON.stringify(v)})` + : isFormat.test(v) + ? `s(${JSON.stringify(v)})` + : JSON.stringify(v)), + ) + .join(';') + + '})()'; + const filename = path.join(env.i18nJsDir, `${dict}.${locale ?? 'en-GB'}.js`); + await fs.promises.writeFile(filename, code); + if (!xstat) return; + return fs.promises.utimes(filename, xstat.mtime, xstat.mtime); +} + +async function writeTypescript(dictMap: DictMap) { + const code = + tsPrelude + + [...dictMap] + .map( + ([dict, trans]) => + ` ${dict}: {\n` + + [...trans.entries()] + .map(([k, v]) => { + const tpe = typeof v !== 'string' ? 'I18nPlural' : isFormat.test(v) ? 'I18nFormat' : 'string'; + const comment = typeof v === 'string' ? v.split('\n')[0] : v['other']?.split('\n')[0]; + return ` /** ${comment} */\n '${k}': ${tpe};`; + }) + .join('\n') + + '\n };\n', + ) + .join('') + + '}\n'; + return fs.promises.writeFile(path.join(env.typesDir, 'lichess', `i18n.d.ts`), code); +} + +function parseXml(xmlData: string): Map { + const i18nMap: Map = new Map(); + const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '' }); + const { string: strings, plurals } = parser.parse(xmlData).resources; + for (const item of strings ? (Array.isArray(strings) ? strings : [strings]) : []) + i18nMap.set(item.name, item['#text'].replaceAll('\\"', '"').replaceAll("\\'", "'")); + for (const plural of plurals ? (Array.isArray(plurals) ? plurals : [plurals]) : []) { + const group: Record = {}; + for (const item of Array.isArray(plural.item) ? plural.item : [plural.item]) { + group[item.quantity] = item['#text'].replaceAll('\\"', '"').replaceAll("\\'", "'"); + } + i18nMap.set(plural.name, group); + } + return new Map([...i18nMap.entries()].sort(([a], [b]) => a.localeCompare(b))); +} diff --git a/ui/.build/src/main.ts b/ui/.build/src/main.ts index 279e31a7df011..9089eb806e423 100644 --- a/ui/.build/src/main.ts +++ b/ui/.build/src/main.ts @@ -1,7 +1,7 @@ import ps from 'node:process'; import path from 'node:path'; import fs from 'node:fs'; -import { clean } from './clean.ts'; +import { deepClean } from './clean.ts'; import { build, postBuild } from './build.ts'; import { startConsole } from './console.ts'; @@ -11,11 +11,11 @@ const args: Record = { '--sass': '', '--esbuild': '', '--copies': '', + '--i18n': '', '--no-color': '', '--no-time': '', '--no-context': '', '--help': 'h', - '--rebuild': 'r', '--watch': 'w', '--prod': 'p', '--debug': 'd', @@ -48,22 +48,19 @@ export function main(): void { .filter(x => x.startsWith('--') && !Object.keys(args).includes(x.split('=')[0])) .forEach(arg => env.exit(`Unknown argument '${arg}'`)); - if (['--tsc', '--sass', '--esbuild', '--copies'].filter(x => argv.includes(x)).length) { + if (['--tsc', '--sass', '--esbuild', '--copies', '--i18n'].filter(x => argv.includes(x)).length) { // including one or more of these disables the others if (!argv.includes('--sass')) env.exitCode.set('sass', false); if (!argv.includes('--tsc')) env.exitCode.set('tsc', false); if (!argv.includes('--esbuild')) env.exitCode.set('esbuild', false); + env.i18n = argv.includes('--i18n'); env.copies = argv.includes('--copies'); } if (argv.includes('--no-color')) env.color = undefined; if (argv.includes('--no-time')) env.logTime = false; if (argv.includes('--no-context')) env.logContext = false; - env.watch = - argv.includes('--rebuild') || - oneDashArgs.includes('r') || - argv.includes('--watch') || - oneDashArgs.includes('w'); + env.watch = argv.includes('--watch') || oneDashArgs.includes('w'); env.prod = argv.includes('--prod') || oneDashArgs.includes('p'); env.debug = argv.includes('--debug') || oneDashArgs.includes('d'); env.remoteLog = stringArg('--log'); @@ -75,8 +72,7 @@ export function main(): void { if (argv.length === 1 && (argv[0] === '--help' || argv[0] === '-h')) { console.log(fs.readFileSync(path.resolve(env.buildDir, 'readme'), 'utf8')); } else if (argv.includes('--clean')) { - env.log('Cleaning then exiting. Use --clean-build or -c to clean then build'); - clean(); + deepClean(); } else { startConsole(); build(argv.filter(x => !x.startsWith('-'))); @@ -139,6 +135,7 @@ class Env { rgb = false; install = true; copies = true; + i18n = true; exitCode: Map = new Map(); startTime: number | undefined = Date.now(); logTime = true; @@ -201,6 +198,15 @@ class Env { get typesDir(): string { return path.join(this.uiDir, '@types'); } + get i18nSrcDir(): string { + return path.join(this.rootDir, 'translation', 'source'); + } + get i18nDestDir(): string { + return path.join(this.rootDir, 'translation', 'dest'); + } + get i18nJsDir(): string { + return path.join(this.rootDir, 'translation', 'js'); + } get manifestFile(): string { return path.join(this.jsOutDir, `manifest.${this.prod ? 'prod' : 'dev'}.json`); } diff --git a/ui/.build/src/manifest.ts b/ui/.build/src/manifest.ts index 3d3d2852fce67..f7879044dfdac 100644 --- a/ui/.build/src/manifest.ts +++ b/ui/.build/src/manifest.ts @@ -9,9 +9,10 @@ import { isUnmanagedAsset } from './copies.ts'; import { allSources } from './sass.ts'; import { jsLogger } from './console.ts'; -type Manifest = { [key: string]: { hash?: string; imports?: string[]; mtime?: number } }; +export type Manifest = { [key: string]: { hash?: string; imports?: string[]; mtime?: number } }; -const current: { js: Manifest; css: Manifest; hashed: Manifest; dirty: boolean } = { +const current: { js: Manifest; i18n: Manifest; css: Manifest; hashed: Manifest; dirty: boolean } = { + i18n: {}, js: {}, css: {}, hashed: {}, @@ -92,6 +93,23 @@ export async function hashedManifest(): Promise { writeManifest(); } +export async function i18nManifest(): Promise { + const i18nManifest: Manifest = {}; + fs.mkdirSync(path.join(env.jsOutDir, 'i18n'), { recursive: true }); + const scripts = await globArray('*.js', { cwd: env.i18nJsDir }); + for (const file of scripts) { + const name = `i18n/${path.basename(file, '.js')}`; + const content = await fs.promises.readFile(file, 'utf-8'); + const hash = crypto.createHash('md5').update(content).digest('hex').slice(0, 12); + const destPath = path.join(env.jsOutDir, `${name}.${hash}.js`); + i18nManifest[name] = { hash }; + if (!fs.existsSync(destPath)) await fs.promises.writeFile(destPath, content); + } + current.i18n = shallowSort(i18nManifest); + current.dirty = true; + writeManifest(); +} + async function write() { if (!env.manifestOk || !(await isComplete())) return; const commitMessage = cps @@ -127,7 +145,7 @@ async function write() { new Date(new Date().toUTCString()).toISOString().split('.')[0] + '+00:00' }';\n`; const serverManifest = { - js: { manifest: { hash }, ...current.js }, + js: { manifest: { hash }, ...current.js, ...current.i18n }, css: { ...current.css }, hashed: { ...current.hashed }, }; @@ -140,7 +158,8 @@ async function write() { ), ]); current.dirty = false; - env.log(`Manifest hash ${c.green(hash)}`); + env.log(`Client manifest '${c.cyan(`public/compiled/manifest.${hash}.js`)}'`); + env.log(`Server manifest '${c.cyan(`public/compiled/manifest.${env.prod ? 'prod' : 'dev'}.json`)}'`); } async function hashMoveCss(src: string) { @@ -180,7 +199,7 @@ async function isComplete() { return false; } } - return true; + return Object.keys(current.i18n).length; } function shallowSort(obj: { [key: string]: any }): { [key: string]: any } { diff --git a/ui/@types/lichess/i18n.d.ts b/ui/@types/lichess/i18n.d.ts new file mode 100644 index 0000000000000..8f1b7eb54da0d --- /dev/null +++ b/ui/@types/lichess/i18n.d.ts @@ -0,0 +1,5439 @@ +// Generated +interface I18nFormat { + (...args: (string | number)[]): string; + asArray: (...args: T[]) => (T | string)[]; // vdom +} +interface I18nPlural { + (quantity: number, ...args: (string | number)[]): string; // pluralSame + raw: (quantity: number, ...args: (string | number)[]) => string; // plural + asArray: (quantity: number, ...args: T[]) => (T | string)[]; // vdomPlural +} +interface I18n { + /** Global noarg key lookup */ + (key: string): string; + + activity: { + /** Activity */ + activity: string; + /** Competed in %s Swiss tournaments */ + competedInNbSwissTournaments: I18nPlural; + /** Competed in %s Arena tournaments */ + competedInNbTournaments: I18nPlural; + /** Completed %s correspondence games */ + completedNbGames: I18nPlural; + /** Completed %1$s %2$s correspondence games */ + completedNbVariantGames: I18nPlural; + /** Created %s new studies */ + createdNbStudies: I18nPlural; + /** Started following %s players */ + followedNbPlayers: I18nPlural; + /** Gained %s new followers */ + gainedNbFollowers: I18nPlural; + /** Hosted a live stream */ + hostedALiveStream: string; + /** Hosted %s simultaneous exhibitions */ + hostedNbSimuls: I18nPlural; + /** in %1$s correspondence games */ + inNbCorrespondenceGames: I18nPlural; + /** Participated in %s simultaneous exhibitions */ + joinedNbSimuls: I18nPlural; + /** Joined %s teams */ + joinedNbTeams: I18nPlural; + /** Played %1$s %2$s games */ + playedNbGames: I18nPlural; + /** Played %1$s moves */ + playedNbMoves: I18nPlural; + /** Posted %1$s messages in %2$s */ + postedNbMessages: I18nPlural; + /** Practised %1$s positions on %2$s */ + practicedNbPositions: I18nPlural; + /** Ranked #%1$s in %2$s */ + rankedInSwissTournament: I18nFormat; + /** Ranked #%1$s (top %2$s%%) with %3$s games in %4$s */ + rankedInTournament: I18nPlural; + /** Signed up to lichess.org */ + signedUp: string; + /** Solved %s training puzzles */ + solvedNbPuzzles: I18nPlural; + /** Supported lichess.org for %1$s months as a %2$s */ + supportedNbMonths: I18nPlural; + }; + appeal: { + /** Your account is muted. */ + accountMuted: string; + /** Read our %s. Failure to follow the communication guidelines can result in accounts being muted. */ + accountMutedInfo: I18nFormat; + /** Your account is banned from joining arenas. */ + arenaBanned: string; + /** blog rules */ + blogRules: string; + /** Your account is marked for rating manipulation. */ + boosterMarked: string; + /** We define this as deliberately manipulating rating by losing games on purpose or by playing against another account that is deliberately losing games. */ + boosterMarkedInfo: string; + /** Your account is not marked or restricted. You're all good! */ + cleanAllGood: string; + /** Your account was closed by moderators. */ + closedByModerators: string; + /** communication guidelines */ + communicationGuidelines: string; + /** Your account is marked for external assistance in games. */ + engineMarked: string; + /** We define this as using any external help to reinforce your knowledge and/or calculation skills in order to gain an unfair advantage over your opponent. See the %s page for more details. */ + engineMarkedInfo: I18nFormat; + /** Your account has been excluded from leaderboards. */ + excludedFromLeaderboards: string; + /** We define this as using any unfair way to get on the leaderboard. */ + excludedFromLeaderboardsInfo: string; + /** Fair Play */ + fairPlay: string; + /** Your blogs have been hidden by moderators. */ + hiddenBlog: string; + /** Make sure to read again our %s. */ + hiddenBlogInfo: I18nFormat; + /** You have a play timeout. */ + playTimeout: string; + /** Your account is banned from tournaments with real prizes. */ + prizeBanned: string; + }; + arena: { + /** All averages on this page are %s. */ + allAveragesAreX: I18nFormat; + /** Allow Berserk */ + allowBerserk: string; + /** Let players halve their clock time to gain an extra point */ + allowBerserkHelp: string; + /** Let players discuss in a chat room */ + allowChatHelp: string; + /** Arena */ + arena: string; + /** Arena streaks */ + arenaStreaks: string; + /** After 2 wins, consecutive wins grant 4 points instead of 2. */ + arenaStreaksHelp: string; + /** Arena tournaments */ + arenaTournaments: string; + /** Average performance */ + averagePerformance: string; + /** Average score */ + averageScore: string; + /** Arena Berserk */ + berserk: string; + /** When a player clicks the Berserk button at the beginning of the game, they lose half of their clock time, but the win is worth one extra tournament point. */ + berserkAnswer: string; + /** Best results */ + bestResults: string; + /** Created */ + created: string; + /** Custom start date */ + customStartDate: string; + /** In your own local timezone. This overrides the "Time before tournament starts" setting */ + customStartDateHelp: string; + /** Defender */ + defender: string; + /** Drawing the game within the first %s moves will earn neither player any points. */ + drawingWithinNbMoves: I18nPlural; + /** Draw streaks: When a player has consecutive draws in an arena, only the first draw will result in a point or draws lasting more than %s moves in standard games. The draw streak can only be broken by a win, not a loss or a draw. */ + drawStreakStandard: I18nFormat; + /** The minimum game length for drawn games to award points differs by variant. The table below lists the threshold for each variant. */ + drawStreakVariants: string; + /** Edit team battle */ + editTeamBattle: string; + /** Edit tournament */ + editTournament: string; + /** Arena History */ + history: string; + /** How are scores calculated? */ + howAreScoresCalculated: string; + /** A win has a base score of 2 points, a draw 1 point, and a loss is worth no points. */ + howAreScoresCalculatedAnswer: string; + /** How does it end? */ + howDoesItEnd: string; + /** The tournament has a countdown clock. When it reaches zero, the tournament rankings are frozen, and the winner is announced. Games in progress must be finished, however, they don't count for the tournament. */ + howDoesItEndAnswer: string; + /** How does the pairing work? */ + howDoesPairingWork: string; + /** At the beginning of the tournament, players are paired based on their rating. */ + howDoesPairingWorkAnswer: string; + /** How is the winner decided? */ + howIsTheWinnerDecided: string; + /** The player(s) with the most points after the tournament's set time limit will be announced the winner(s). */ + howIsTheWinnerDecidedAnswer: string; + /** Is it rated? */ + isItRated: string; + /** This tournament is *not* rated and will *not* affect your rating. */ + isNotRated: string; + /** This tournament is rated and will affect your rating. */ + isRated: string; + /** medians */ + medians: string; + /** Minimum game length */ + minimumGameLength: string; + /** My tournaments */ + myTournaments: string; + /** New Team Battle */ + newTeamBattle: string; + /** No Arena streaks */ + noArenaStreaks: string; + /** No Berserk allowed */ + noBerserkAllowed: string; + /** Only titled players */ + onlyTitled: string; + /** Require an official title to join the tournament */ + onlyTitledHelp: string; + /** Other important rules */ + otherRules: string; + /** Pick your team */ + pickYourTeam: string; + /** Points average */ + pointsAvg: string; + /** Points sum */ + pointsSum: string; + /** Rank average */ + rankAvg: string; + /** The rank average is a percentage of your ranking. Lower is better. */ + rankAvgHelp: string; + /** Recently played */ + recentlyPlayed: string; + /** Share this URL to let people join: %s */ + shareUrl: I18nFormat; + /** Some tournaments are rated and will affect your rating. */ + someRated: string; + /** Stats */ + stats: string; + /** There is a countdown for your first move. Failing to make a move within this time will forfeit the game to your opponent. */ + thereIsACountdown: string; + /** This is a private tournament */ + thisIsPrivate: string; + /** Total */ + total: string; + /** Tournament shields */ + tournamentShields: string; + /** Tournament stats */ + tournamentStats: string; + /** Tournament winners */ + tournamentWinners: string; + /** Variant */ + variant: string; + /** View all %s teams */ + viewAllXTeams: I18nPlural; + /** Which team will you represent in this battle? */ + whichTeamWillYouRepresentInThisBattle: string; + /** You will be notified when the tournament starts, so it is safe to play in another tab while waiting. */ + willBeNotified: string; + /** You must join one of these teams to participate! */ + youMustJoinOneOfTheseTeamsToParticipate: string; + }; + broadcast: { + /** About broadcasts */ + aboutBroadcasts: string; + /** Add a round */ + addRound: string; + /** Age this year */ + ageThisYear: string; + /** Broadcast calendar */ + broadcastCalendar: string; + /** Broadcasts */ + broadcasts: string; + /** Completed */ + completed: string; + /** Lichess detects round completion, but can get it wrong. Use this to set it manually. */ + completedHelp: string; + /** Credit the source */ + credits: string; + /** Current game URL */ + currentGameUrl: string; + /** Definitively delete the round and all its games. */ + definitivelyDeleteRound: string; + /** Definitively delete the entire tournament, all its rounds and all its games. */ + definitivelyDeleteTournament: string; + /** Delete all games of this round. The source will need to be active in order to re-create them. */ + deleteAllGamesOfThisRound: string; + /** Delete this round */ + deleteRound: string; + /** Delete this tournament */ + deleteTournament: string; + /** Download all rounds */ + downloadAllRounds: string; + /** Edit round study */ + editRoundStudy: string; + /** Federation */ + federation: string; + /** FIDE federations */ + fideFederations: string; + /** FIDE player not found */ + fidePlayerNotFound: string; + /** FIDE players */ + fidePlayers: string; + /** FIDE profile */ + fideProfile: string; + /** Full tournament description */ + fullDescription: string; + /** Optional long description of the tournament. %1$s is available. Length must be less than %2$s characters. */ + fullDescriptionHelp: I18nFormat; + /** How to use Lichess Broadcasts. */ + howToUseLichessBroadcasts: string; + /** Live tournament broadcasts */ + liveBroadcasts: string; + /** My broadcasts */ + myBroadcasts: string; + /** %s broadcasts */ + nbBroadcasts: I18nPlural; + /** New live broadcast */ + newBroadcast: string; + /** Ongoing */ + ongoing: string; + /** Period in seconds */ + periodInSeconds: string; + /** Optional, how long to wait between requests. Min 2s, max 60s. Defaults to automatic based on the number of viewers. */ + periodInSecondsHelp: string; + /** Recent tournaments */ + recentTournaments: string; + /** Optional: replace player names, ratings and titles */ + replacePlayerTags: string; + /** Reset this round */ + resetRound: string; + /** Round name */ + roundName: string; + /** Round number */ + roundNumber: string; + /** Show players scores based on game results */ + showScores: string; + /** Up to 64 Lichess game IDs, separated by spaces. */ + sourceGameIds: string; + /** PGN Source URL */ + sourceSingleUrl: string; + /** URL that Lichess will check to get PGN updates. It must be publicly accessible from the Internet. */ + sourceUrlHelp: string; + /** Optional, if you know when the event starts */ + startDateHelp: string; + /** Start date in the tournament local timezone: %s */ + startDateTimeZone: I18nFormat; + /** Subscribed broadcasts */ + subscribedBroadcasts: string; + /** The new round will have the same members and contributors as the previous one. */ + theNewRoundHelp: string; + /** Top 10 rating */ + top10Rating: string; + /** Short tournament description */ + tournamentDescription: string; + /** Tournament name */ + tournamentName: string; + /** Unrated */ + unrated: string; + /** Upcoming */ + upcoming: string; + }; + challenge: { + /** Cannot challenge due to provisional %s rating. */ + cannotChallengeDueToProvisionalXRating: I18nFormat; + /** Challenge accepted! */ + challengeAccepted: string; + /** Challenge cancelled. */ + challengeCanceled: string; + /** Challenge declined. */ + challengeDeclined: string; + /** Challenges: %1$s */ + challengesX: I18nFormat; + /** Challenge to a game */ + challengeToPlay: string; + /** Please send me a casual challenge instead. */ + declineCasual: string; + /** I'm not accepting challenges at the moment. */ + declineGeneric: string; + /** This is not the right time for me, please ask again later. */ + declineLater: string; + /** I'm not accepting challenges from bots. */ + declineNoBot: string; + /** I'm only accepting challenges from bots. */ + declineOnlyBot: string; + /** Please send me a rated challenge instead. */ + declineRated: string; + /** I'm not accepting variant challenges right now. */ + declineStandard: string; + /** I'm not accepting challenges with this time control. */ + declineTimeControl: string; + /** This time control is too fast for me, please challenge again with a slower game. */ + declineTooFast: string; + /** This time control is too slow for me, please challenge again with a faster game. */ + declineTooSlow: string; + /** I'm not willing to play this variant right now. */ + declineVariant: string; + /** Or invite a Lichess user: */ + inviteLichessUser: string; + /** Please register to send challenges to this user. */ + registerToSendChallenges: string; + /** %s does not accept challenges. */ + xDoesNotAcceptChallenges: I18nFormat; + /** %s only accepts challenges from friends. */ + xOnlyAcceptsChallengesFromFriends: I18nFormat; + /** You cannot challenge %s. */ + youCannotChallengeX: I18nFormat; + /** Your %1$s rating is too far from %2$s. */ + yourXRatingIsTooFarFromY: I18nFormat; + }; + class: { + /** Add Lichess usernames to invite them as teachers. One per line. */ + addLichessUsernames: string; + /** Add student */ + addStudent: string; + /** A link to the class will be automatically added at the end of the message, so you don't need to include it yourself. */ + aLinkToTheClassWillBeAdded: string; + /** An invitation has been sent to %s */ + anInvitationHasBeenSentToX: I18nFormat; + /** Apply to be a Lichess Teacher */ + applyToBeLichessTeacher: string; + /** Class description */ + classDescription: string; + /** Class name */ + className: string; + /** Class news */ + classNews: string; + /** Click the link to view the invitation: */ + clickToViewInvitation: string; + /** Close class */ + closeClass: string; + /** The student will never be able to use this account again. Closing is final. Make sure the student understands and agrees. */ + closeDesc1: string; + /** You may want to give the student control over the account instead so that they can continue using it. */ + closeDesc2: string; + /** Close account */ + closeStudent: string; + /** Close the student account permanently. */ + closeTheAccount: string; + /** Create a new Lichess account */ + createANewLichessAccount: string; + /** If the student doesn't have a Lichess account yet, you can create one for them here. */ + createDesc1: string; + /** No email address is required. A password will be generated, and you will have to transmit it to the student so that they can log in. */ + createDesc2: string; + /** Important: a student must not have multiple accounts. */ + createDesc3: string; + /** If they already have one, use the invite form instead. */ + createDesc4: string; + /** create more classes */ + createMoreClasses: string; + /** Create multiple Lichess accounts at once */ + createMultipleAccounts: string; + /** Only create accounts for real students. Do not use this to make multiple accounts for yourself. You would get banned. */ + createStudentWarning: string; + /** Edit news */ + editNews: string; + /** Features */ + features: string; + /** 100% free for all, forever, with no ads or trackers */ + freeForAllForever: string; + /** Generate a new password for the student */ + generateANewPassword: string; + /** Generate a new username */ + generateANewUsername: string; + /** You are invited to join the class "%s" as a student. */ + invitationToClass: I18nFormat; + /** Invite */ + invite: string; + /** Invite a Lichess account */ + inviteALichessAccount: string; + /** If the student already has a Lichess account, you can invite them to the class. */ + inviteDesc1: string; + /** They will receive a message on Lichess with a link to join the class. */ + inviteDesc2: string; + /** Important: only invite students you know, and who actively want to join the class. */ + inviteDesc3: string; + /** Never send unsolicited invites to arbitrary players. */ + inviteDesc4: string; + /** Invited to %1$s by %2$s */ + invitedToXByY: I18nFormat; + /** Invite the student back */ + inviteTheStudentBack: string; + /** Active */ + lastActiveDate: string; + /** Classes */ + lichessClasses: string; + /** Lichess profile %1$s created for %2$s. */ + lichessProfileXCreatedForY: I18nFormat; + /** Lichess username */ + lichessUsername: string; + /** Make sure to copy or write down the password now. You won’t ever be able to see it again! */ + makeSureToCopy: string; + /** Managed */ + managed: string; + /** Note that a class can have up to %1$s students. To manage more students, %2$s. */ + maxStudentsNote: I18nFormat; + /** Message all students about new class material */ + messageAllStudents: string; + /** You can also %s to create multiple Lichess accounts from a list of student names. */ + multipleAccsFormDescription: I18nFormat; + /** N/A */ + na: string; + /** %s pending invitations */ + nbPendingInvitations: I18nPlural; + /** %s students */ + nbStudents: I18nPlural; + /** %s teachers */ + nbTeachers: I18nPlural; + /** New class */ + newClass: string; + /** News */ + news: string; + /** All class news in a single field. */ + newsEdit1: string; + /** Add the recent news at the top. Don't delete previous news. */ + newsEdit2: string; + /** Separate news with --- */ + newsEdit3: string; + /** No classes yet. */ + noClassesYet: string; + /** No removed students. */ + noRemovedStudents: string; + /** No students in the class, yet. */ + noStudents: string; + /** Nothing here, yet. */ + nothingHere: string; + /** Notify all students */ + notifyAllStudents: string; + /** Only visible to the class teachers */ + onlyVisibleToTeachers: string; + /** or */ + orSeparator: string; + /** Over days */ + overDays: string; + /** Overview */ + overview: string; + /** Password: %s */ + passwordX: I18nFormat; + /** Private. Will never be shown outside the class. Helps you remember who the student is. */ + privateWillNeverBeShown: string; + /** Progress */ + progress: string; + /** Quickly generate safe usernames and passwords for students */ + quicklyGenerateSafeUsernames: string; + /** Real name */ + realName: string; + /** Real, unique email address of the student. We will send a confirmation email to it, with a link to graduate the account. */ + realUniqueEmail: string; + /** Graduate */ + release: string; + /** A graduated account cannot be managed again. The student will be able to toggle kid mode and reset password themselves. */ + releaseDesc1: string; + /** The student will remain in the class after their account is graduated. */ + releaseDesc2: string; + /** Graduate the account so the student can manage it autonomously. */ + releaseTheAccount: string; + /** Removed by %s */ + removedByX: I18nFormat; + /** Removed */ + removedStudents: string; + /** Remove student */ + removeStudent: string; + /** Reopen */ + reopen: string; + /** Reset password */ + resetPassword: string; + /** Send a message to all students. */ + sendAMessage: string; + /** Student: %1$s */ + studentCredentials: I18nFormat; + /** Students */ + students: string; + /** Students' real names, one per line */ + studentsRealNamesOnePerLine: string; + /** Teach classes of chess students with the Lichess Classes tool suite. */ + teachClassesOfChessStudents: string; + /** Teachers */ + teachers: string; + /** Teachers of the class */ + teachersOfTheClass: string; + /** Teachers: %s */ + teachersX: I18nFormat; + /** This student account is managed */ + thisStudentAccountIsManaged: string; + /** Time playing */ + timePlaying: string; + /** Track student progress in games and puzzles */ + trackStudentProgress: string; + /** Upgrade from managed to autonomous */ + upgradeFromManaged: string; + /** use this form */ + useThisForm: string; + /** %1$s over last %2$s */ + variantXOverLastY: I18nFormat; + /** Visible by both teachers and students of the class */ + visibleByBothStudentsAndTeachers: string; + /** Welcome to your class: %s. */ + welcomeToClass: I18nFormat; + /** Win rate */ + winrate: string; + /** %s already has a pending invitation */ + xAlreadyHasAPendingInvitation: I18nFormat; + /** %1$s is a kid account and can't receive your message. You must give them the invitation URL manually: %2$s */ + xIsAKidAccountWarning: I18nFormat; + /** %s is now a student of the class */ + xisNowAStudentOfTheClass: I18nFormat; + /** You accepted this invitation. */ + youAcceptedThisInvitation: string; + /** You declined this invitation. */ + youDeclinedThisInvitation: string; + /** You have been invited by %s. */ + youHaveBeenInvitedByX: I18nFormat; + }; + coach: { + /** About me */ + aboutMe: string; + /** Accepting students */ + accepting: string; + /** Are you a great chess coach with a %s? */ + areYouCoach: I18nFormat; + /** Availability */ + availability: string; + /** Best skills */ + bestSkills: string; + /** Confirm your title here and we will review your application. */ + confirmTitle: string; + /** Hourly rate */ + hourlyRate: string; + /** Languages */ + languages: string; + /** Lichess coach */ + lichessCoach: string; + /** Lichess coaches */ + lichessCoaches: string; + /** Location */ + location: string; + /** NM or FIDE title */ + nmOrFideTitle: string; + /** Not accepting students at the moment */ + notAccepting: string; + /** Other experiences */ + otherExperiences: string; + /** Playing experience */ + playingExperience: string; + /** Public studies */ + publicStudies: string; + /** Rating */ + rating: string; + /** Send us an email at %s and we will review your application. */ + sendApplication: I18nFormat; + /** Send a private message */ + sendPM: string; + /** Teaching experience */ + teachingExperience: string; + /** Teaching methodology */ + teachingMethod: string; + /** View %s Lichess profile */ + viewXProfile: I18nFormat; + /** %s coaches chess students */ + xCoachesStudents: I18nFormat; + /** YouTube videos */ + youtubeVideos: string; + }; + contact: { + /** However if you indeed used engine assistance, even just once, then your account is unfortunately lost. */ + accountLost: string; + /** I need account support */ + accountSupport: string; + /** Authorisation to use Lichess */ + authorizationToUse: string; + /** Appeal for a ban or IP restriction */ + banAppeal: string; + /** In certain circumstances when playing against a bot account, a rated game may not award points if it is determined that the player is abusing the bot for rating points. */ + botRatingAbuse: string; + /** Buying Lichess */ + buyingLichess: string; + /** It is called "en passant" and is one of the rules of chess. */ + calledEnPassant: string; + /** We can't change more than the case. For technical reasons, it's downright impossible. */ + cantChangeMore: string; + /** It's not possible to clear your game history, puzzle history, or ratings. */ + cantClearHistory: string; + /** If you imported the game, or started it from a position, make sure you correctly set the castling rights. */ + castlingImported: string; + /** Castling is only prevented if the king goes through a controlled square. */ + castlingPrevented: string; + /** Make sure you understand the castling rules */ + castlingRules: string; + /** Visit this page to change the case of your username */ + changeUsernameCase: string; + /** You can close your account on this page */ + closeYourAccount: string; + /** Collaboration, legal, commercial */ + collaboration: string; + /** Contact */ + contact: string; + /** Contact Lichess */ + contactLichess: string; + /** Credit is appreciated but not required. */ + creditAppreciated: string; + /** Do not ask us by email to close an account, we won't do it. */ + doNotAskByEmail: string; + /** Do not ask us by email to reopen an account, we won't do it. */ + doNotAskByEmailToReopen: string; + /** Do not deny having cheated. If you want to be allowed to create a new account, just admit to what you did, and show that you understood that it was a mistake. */ + doNotDeny: string; + /** Please do not send direct messages to moderators. */ + doNotMessageModerators: string; + /** Do not report players in the forum. */ + doNotReportInForum: string; + /** Do not send us report emails. */ + doNotSendReportEmails: string; + /** Complete a password reset to remove your second factor */ + doPasswordReset: string; + /** Engine or cheat mark */ + engineAppeal: string; + /** Error page */ + errorPage: string; + /** Please explain your request clearly and thoroughly. State your Lichess username, and any information that could help us help you. */ + explainYourRequest: string; + /** False positives do happen sometimes, and we're sorry about that. */ + falsePositives: string; + /** According to the FIDE Laws of Chess §6.9, if a checkmate is possible with any legal sequence of moves, then the game is not a draw */ + fideMate: string; + /** I forgot my password */ + forgotPassword: string; + /** I forgot my username */ + forgotUsername: string; + /** Please describe what the bug looks like, what you expected to happen instead, and the steps to reproduce the bug. */ + howToReportBug: string; + /** I can't log in */ + iCantLogIn: string; + /** If your appeal is legitimate, we will lift the ban ASAP. */ + ifLegit: string; + /** Illegal or impossible castling */ + illegalCastling: string; + /** Illegal pawn capture */ + illegalPawnCapture: string; + /** Insufficient mating material */ + insufficientMaterial: string; + /** It is possible to checkmate with only a knight or a bishop, if the opponent has more than a king on the board. */ + knightMate: string; + /** Learn how to make your own broadcasts on Lichess */ + learnHowToMakeBroadcasts: string; + /** I lost access to my two-factor authentication codes */ + lost2FA: string; + /** Monetising Lichess */ + monetizing: string; + /** I didn't receive my confirmation email */ + noConfirmationEmail: string; + /** None of the above */ + noneOfTheAbove: string; + /** No rating points were awarded */ + noRatingPoints: string; + /** Only reporting players through the report form is effective. */ + onlyReports: string; + /** However, you can close your current account, and create a new one. */ + orCloseAccount: string; + /** Other restriction */ + otherRestriction: string; + /** Make sure you played a rated game. Casual games do not affect the players ratings. */ + ratedGame: string; + /** You can reopen your account on this page. It only works once. */ + reopenOnThisPage: string; + /** In the Lichess Discord server */ + reportBugInDiscord: string; + /** In the Lichess Feedback section of the forum */ + reportBugInForum: string; + /** If you faced an error page, you may report it: */ + reportErrorPage: string; + /** As a Lichess mobile app issue on GitHub */ + reportMobileIssue: string; + /** As a Lichess website issue on GitHub */ + reportWebsiteIssue: string; + /** You may send an appeal to %s. */ + sendAppealTo: I18nFormat; + /** Send us an email at %s. */ + sendEmailAt: I18nFormat; + /** To report a player, use the report form */ + toReportAPlayerUseForm: string; + /** Try this little interactive game to practice castling in chess */ + tryCastling: string; + /** Try this little interactive game to learn more about "en passant". */ + tryEnPassant: string; + /** You can show it in your videos, and you can print screenshots of Lichess in your books. */ + videosAndBooks: string; + /** Visit this page to solve the issue */ + visitThisPage: string; + /** To show your title on your Lichess profile, and participate in Titled Arenas, visit the title confirmation page */ + visitTitleConfirmation: string; + /** I want to change my username */ + wantChangeUsername: string; + /** I want to clear my history or rating */ + wantClearHistory: string; + /** I want to close my account */ + wantCloseAccount: string; + /** I want to reopen my account */ + wantReopen: string; + /** I want to report a player */ + wantReport: string; + /** I want to report a bug */ + wantReportBug: string; + /** I want my title displayed on Lichess */ + wantTitle: string; + /** You are welcome to use Lichess for your activity, even commercial. */ + welcomeToUse: string; + /** What can we help you with? */ + whatCanWeHelpYouWith: string; + /** You can also reach that page by clicking the %s report button on a profile page. */ + youCanAlsoReachReportPage: I18nFormat; + /** You can login with the email address you signed up with */ + youCanLoginWithEmail: string; + }; + coordinates: { + /** A coordinate appears on the board and you must click on the corresponding square. */ + aCoordinateAppears: string; + /** A square is highlighted on the board and you must enter its coordinate (e.g. "e4"). */ + aSquareIsHighlightedExplanation: string; + /** Average score as black: %s */ + averageScoreAsBlackX: I18nFormat; + /** Average score as white: %s */ + averageScoreAsWhiteX: I18nFormat; + /** Coordinates */ + coordinates: string; + /** Coordinate training */ + coordinateTraining: string; + /** Find square */ + findSquare: string; + /** Go as long as you want, there is no time limit! */ + goAsLongAsYouWant: string; + /** Knowing the chessboard coordinates is a very important skill for several reasons: */ + knowingTheChessBoard: string; + /** Most chess courses and exercises use the algebraic notation extensively. */ + mostChessCourses: string; + /** Name square */ + nameSquare: string; + /** Show coordinates */ + showCoordinates: string; + /** Coordinates on every square */ + showCoordsOnAllSquares: string; + /** Show pieces */ + showPieces: string; + /** Start training */ + startTraining: string; + /** It makes it easier to talk to your chess friends, since you both understand the 'language of chess'. */ + talkToYourChessFriends: string; + /** You can analyse a game more effectively if you can quickly recognise coordinates. */ + youCanAnalyseAGameMoreEffectively: string; + /** You have 30 seconds to correctly map as many squares as possible! */ + youHaveThirtySeconds: string; + }; + dgt: { + /** Announce All Moves */ + announceAllMoves: string; + /** Announce Move Format */ + announceMoveFormat: string; + /** As a last resort, setup the board identically as Lichess, then %s */ + asALastResort: I18nFormat; + /** The board will auto connect to any game that is already on course or any new game that starts. Ability to choose which game to play is coming soon. */ + boardWillAutoConnect: string; + /** Check that you have made your opponent's move on the DGT board first. Revert your move. Play again. */ + checkYouHaveMadeOpponentsMove: string; + /** Click to generate one */ + clickToGenerateOne: string; + /** Configuration Section */ + configurationSection: string; + /** Configure */ + configure: string; + /** Configure voice narration of the played moves, so you can keep your eyes on the board. */ + configureVoiceNarration: string; + /** Debug */ + debug: string; + /** DGT board */ + dgtBoard: string; + /** DGT board connectivity */ + dgtBoardConnectivity: string; + /** DGT Board Limitations */ + dgtBoardLimitations: string; + /** DGT Board Requirements */ + dgtBoardRequirements: string; + /** DGT - Configure */ + dgtConfigure: string; + /** A %s entry was added to your PLAY menu at the top. */ + dgtPlayMenuEntryAdded: I18nFormat; + /** You can download the software here: %s. */ + downloadHere: I18nFormat; + /** Enable Speech Synthesis */ + enableSpeechSynthesis: string; + /** If %1$s is running on a different machine or different port, you will need to set the IP address and port here in the %2$s. */ + ifLiveChessRunningElsewhere: I18nFormat; + /** If %1$s is running on this computer, you can check your connection to it by %2$s. */ + ifLiveChessRunningOnThisComputer: I18nFormat; + /** If a move is not detected */ + ifMoveNotDetected: string; + /** The play page needs to remain open on your browser. It does not need to be visible, you can minimize it or set it side to side with the Lichess game page, but don't close it or the board will stop working. */ + keepPlayPageOpen: string; + /** Keywords are in JSON format. They are used to translate moves and results into your language. Default is English, but feel free to change it. */ + keywordFormatDescription: string; + /** Keywords */ + keywords: string; + /** Lichess & DGT */ + lichessAndDgt: string; + /** Lichess connectivity */ + lichessConnectivity: string; + /** SAN is the standard on Lichess like "Nf6". UCI is common on engines like "g8f6". */ + moveFormatDescription: string; + /** No suitable OAuth token has been created. */ + noSuitableOauthToken: string; + /** opening this link */ + openingThisLink: string; + /** Play with a DGT board */ + playWithDgtBoard: string; + /** Reload this page */ + reloadThisPage: string; + /** Select YES to announce both your moves and your opponent's moves. Select NO to announce only your opponent's moves. */ + selectAnnouncePreference: string; + /** Speech synthesis voice */ + speechSynthesisVoice: string; + /** Text to speech */ + textToSpeech: string; + /** This page allows you to connect your DGT board to Lichess, and to use it for playing games. */ + thisPageAllowsConnectingDgtBoard: string; + /** Time controls for casual games: Classical, Correspondence and Rapid only. */ + timeControlsForCasualGames: string; + /** Time controls for rated games: Classical, Correspondence and some Rapids including 15+10 and 20+0 */ + timeControlsForRatedGames: string; + /** To connect to the DGT Electronic Board you will need to install %s. */ + toConnectTheDgtBoard: I18nFormat; + /** To see console message press Command + Option + C (Mac) or Control + Shift + C (Windows, Linux, Chrome OS) */ + toSeeConsoleMessage: string; + /** Use \"%1$s\" unless %2$s is running on a different machine or different port. */ + useWebSocketUrl: I18nFormat; + /** You have an OAuth token suitable for DGT play. */ + validDgtOauthToken: string; + /** Verbose logging */ + verboseLogging: string; + /** %s WebSocket URL */ + webSocketUrl: I18nFormat; + /** When ready, setup your board and then click %s. */ + whenReadySetupBoard: I18nFormat; + }; + emails: { + /** To contact us, please use %s. */ + common_contact: I18nFormat; + /** This is a service email related to your use of %s. */ + common_note: I18nFormat; + /** (Clicking not working? Try pasting it into your browser!) */ + common_orPaste: string; + /** To confirm you have access to this email, please click the link below: */ + emailChange_click: string; + /** You have requested to change your email address. */ + emailChange_intro: string; + /** Confirm new email address, %s */ + emailChange_subject: I18nFormat; + /** Click the link to enable your Lichess account: */ + emailConfirm_click: string; + /** If you did not register with Lichess you can safely ignore this message. */ + emailConfirm_ignore: string; + /** Confirm your lichess.org account, %s */ + emailConfirm_subject: I18nFormat; + /** Log in to lichess.org, %s */ + logInToLichess: I18nFormat; + /** If you made this request, click the link below. If not, you can ignore this email. */ + passwordReset_clickOrIgnore: string; + /** We received a request to reset the password for your account. */ + passwordReset_intro: string; + /** Reset your lichess.org password, %s */ + passwordReset_subject: I18nFormat; + /** Welcome to lichess.org, %s */ + welcome_subject: I18nFormat; + /** You have successfully created your account on https://lichess.org. */ + welcome_text: I18nFormat; + }; + faq: { + /** Accounts */ + accounts: string; + /** The centipawn is the unit of measure used in chess as representation of the advantage. A centipawn is equal to 1/100th of a pawn. Therefore 100 centipawns = 1 pawn. These values play no formal role in the game but are useful to players, and essential in computer chess, for evaluating positions. */ + acplExplanation: string; + /** We regularly receive messages from users asking us for help to stop them from playing too much. */ + adviceOnMitigatingAddiction: I18nFormat; + /** an hourly Bullet tournament */ + aHourlyBulletTournament: string; + /** Are there websites based on Lichess? */ + areThereWebsitesBasedOnLichess: string; + /** many national master titles */ + asWellAsManyNMtitles: string; + /** Lichess time controls are based on estimated game duration = %1$s. */ + basedOnGameDuration: I18nFormat; + /** being a patron */ + beingAPatron: string; + /** be in the top 10 in this rating. */ + beInTopTen: string; + /** breakdown of our costs */ + breakdownOfOurCosts: string; + /** Can I get the Lichess Master (LM) title? */ + canIbecomeLM: string; + /** Can I change my username? */ + canIChangeMyUsername: string; + /** configure */ + configure: string; + /** I lost a game due to lag/disconnection. Can I get my rating points back? */ + connexionLostCanIGetMyRatingBack: string; + /** desktop */ + desktop: string; + /** Why can a pawn capture another pawn when it is already passed? (en passant) */ + discoveringEnPassant: string; + /** display preferences */ + displayPreferences: string; + /** (clock initial time in seconds) + 40 × (clock increment) */ + durationFormula: string; + /** 8 chess variants */ + eightVariants: string; + /** Most browsers can prevent sound from playing on a freshly loaded page to protect users. Imagine if every website could immediately bombard you with audio ads. */ + enableAutoplayForSoundsA: string; + /** 1. Go to lichess.org */ + enableAutoplayForSoundsChrome: string; + /** 1. Go to lichess.org */ + enableAutoplayForSoundsFirefox: string; + /** 1. Click the three dots in the top right corner */ + enableAutoplayForSoundsMicrosoftEdge: string; + /** Enable autoplay for sounds? */ + enableAutoplayForSoundsQ: string; + /** 1. Go to lichess.org */ + enableAutoplayForSoundsSafari: string; + /** Enable or disable notification popups? */ + enableDisableNotificationPopUps: string; + /** Enable Zen-mode in the %1$s, or by pressing %2$s during a game. */ + enableZenMode: I18nFormat; + /** This is a legal move known as "en passant". The Wikipedia article gives a %1$s. */ + explainingEnPassant: I18nFormat; + /** Fair Play */ + fairPlay: string; + /** fair play page */ + fairPlayPage: string; + /** FAQ */ + faqAbbreviation: string; + /** fewer lobby pools */ + fewerLobbyPools: string; + /** FIDE handbook */ + fideHandbook: string; + /** FIDE handbook %s */ + fideHandbookX: I18nFormat; + /** You can find out more about %1$s (including a %2$s). If you want to help Lichess by volunteering your time and skills, there are many %3$s. */ + findMoreAndSeeHowHelp: I18nFormat; + /** Frequently Asked Questions */ + frequentlyAskedQuestions: string; + /** Gameplay */ + gameplay: string; + /** ZugAddict was streaming and for the last 2 hours he had been trying to defeat A.I. level 8 in a 1+0 game, without success. Thibault told him that if he successfully did it on stream, he'd get a unique trophy. One hour later, he smashed Stockfish, and the promise was honoured. */ + goldenZeeExplanation: string; + /** good introduction */ + goodIntroduction: string; + /** guidelines */ + guidelines: string; + /** have played a rated game within the last week for this rating, */ + havePlayedARatedGameAtLeastOneWeekAgo: string; + /** have played at least 30 rated games in a given rating, */ + havePlayedMoreThanThirtyGamesInThatRating: string; + /** Hear it pronounced by a specialist. */ + hearItPronouncedBySpecialist: string; + /** How are Bullet, Blitz and other time controls decided? */ + howBulletBlitzEtcDecided: string; + /** How can I become a moderator? */ + howCanIBecomeModerator: string; + /** How can I contribute to Lichess? */ + howCanIContributeToLichess: string; + /** How do ranks and leaderboards work? */ + howDoLeaderoardsWork: string; + /** How to hide ratings while playing? */ + howToHideRatingWhilePlaying: string; + /** How to... */ + howToThreeDots: string; + /** ≤ %1$ss = %2$s */ + inferiorThanXsEqualYtimeControl: I18nFormat; + /** In order to get on the %1$s you must: */ + inOrderToAppearsYouMust: I18nFormat; + /** Losing on time, drawing and insufficient material */ + insufficientMaterial: string; + /** Is correspondence different from normal chess? */ + isCorrespondenceDifferent: string; + /** What keyboard shortcuts are there? */ + keyboardShortcuts: string; + /** Some Lichess pages have keyboard shortcuts you can use. Try pressing the '?' key on a study, analysis, puzzle, or game page to list available keyboard shortcuts. */ + keyboardShortcutsExplanation: string; + /** If your opponent frequently aborts/leaves games, they get "play banned", which means they're temporarily banned from playing games. This is not publicly indicated on their profile. If this behaviour continues, the length of the playban increases - and prolonged behaviour of this nature may lead to account closure. */ + leavingGameWithoutResigningExplanation: string; + /** lee-chess */ + leechess: string; + /** Lichess can optionally send popup notifications, for example when it is your turn or you received a private message. */ + lichessCanOptionnalySendPopUps: string; + /** Lichess is a combination of live/light/libre and chess. It is pronounced %1$s. */ + lichessCombinationLiveLightLibrePronounced: I18nFormat; + /** In the event of one player running out of time, that player will usually lose the game. However, the game is drawn if the position is such that the opponent cannot checkmate the player's king by any possible series of legal moves (%1$s). */ + lichessFollowFIDErules: I18nFormat; + /** Lichess is powered by donations from patrons and the efforts of a team of volunteers. */ + lichessPoweredByDonationsAndVolunteers: string; + /** Lichess ratings */ + lichessRatings: string; + /** Lichess recognises all FIDE titles gained from OTB (over the board) play, as well as %1$s. Here is a list of FIDE titles: */ + lichessRecognizeAllOTBtitles: I18nFormat; + /** Lichess supports standard chess and %1$s. */ + lichessSupportChessAnd: I18nFormat; + /** Lichess training */ + lichessTraining: string; + /** Lichess userstyles */ + lichessUserstyles: string; + /** This honorific title is unofficial and only exists on Lichess. */ + lMtitleComesToYouDoNotRequestIt: string; + /** stand-alone mental health condition */ + mentalHealthCondition: string; + /** The player has not yet finished enough rated games against %1$s in the rating category. */ + notPlayedEnoughRatedGamesAgainstX: I18nFormat; + /** The player hasn't played enough recent games. Depending on the number of games you've played, it might take around a year of inactivity for your rating to become provisional again. */ + notPlayedRecently: string; + /** We did not repeat moves. Why was the game still drawn by repetition? */ + notRepeatedMoves: string; + /** No. */ + noUpperCaseDot: string; + /** other ways to help */ + otherWaysToHelp: string; + /** That trophy is unique in the history of Lichess, nobody other than %1$s will ever have it. */ + ownerUniqueTrophies: I18nFormat; + /** For more information, please read our %s */ + pleaseReadFairPlayPage: I18nFormat; + /** positions */ + positions: string; + /** What is done about players leaving games without resigning? */ + preventLeavingGameWithoutResigning: string; + /** The question mark means the rating is provisional. Reasons include: */ + provisionalRatingExplanation: string; + /** have a rating deviation lower than %1$s, in standard chess, and lower than %2$s in variants, */ + ratingDeviationLowerThanXinChessYinVariants: I18nFormat; + /** Concretely, it means that the Glicko-2 deviation is greater than 110. The deviation is the level of confidence the system has in the rating. The lower the deviation, the more stable is a rating. */ + ratingDeviationMorethanOneHundredTen: string; + /** rating leaderboard */ + ratingLeaderboards: string; + /** One minute after a player is marked, their 40 latest rated games in the last 5 days are taken. If you were their opponent in one of those games, you lost rating (because of a loss or a draw), and your rating was not provisional, you get a rating refund. The refund is capped based on your peak rating and your rating progress after the game. (For example, if your rating greatly increased after that game, you might get no refund or only a partial refund.) A refund will never exceed 150 points. */ + ratingRefundExplanation: string; + /** Ratings are calculated using the Glicko-2 rating method developed by Mark Glickman. This is a very popular rating method, and is used by a significant number of chess organisations (FIDE being a notable counter-example, as they still use the dated Elo rating system). */ + ratingSystemUsedByLichess: string; + /** Threefold repetition is about repeated %1$s, not moves. Repetition does not have to occur consecutively. */ + repeatedPositionsThatMatters: I18nFormat; + /** The 2nd requirement is so that players who no longer use their accounts stop populating leaderboards. */ + secondRequirementToStopOldPlayersTrustingLeaderboards: string; + /** If you have an OTB title, you can apply to have this displayed on your account by completing the %1$s, including a clear image of an identifying document/card and a selfie of you holding the document/card. */ + showYourTitle: I18nFormat; + /** opponents of similar strength */ + similarOpponents: string; + /** Stop myself from playing? */ + stopMyselfFromPlaying: string; + /** ≥ %1$ss = %2$s */ + superiorThanXsEqualYtimeControl: I18nFormat; + /** Repetition needs to be claimed by one of the players. You can do so by pressing the button that is shown, or by offering a draw before your final repeating move, it won't matter if your opponent rejects the draw offer, the threefold repetition draw will be claimed anyway. You can also %1$s Lichess to automatically claim repetitions for you. Additionally, fivefold repetition always immediately ends the game. */ + threeFoldHasToBeClaimed: I18nFormat; + /** Threefold repetition */ + threefoldRepetition: string; + /** If a position occurs three times, players can claim a draw by %1$s. Lichess implements the official FIDE rules, as described in Article 9.2 of the %2$s. */ + threefoldRepetitionExplanation: I18nFormat; + /** threefold repetition */ + threefoldRepetitionLowerCase: string; + /** What titles are there on Lichess? */ + titlesAvailableOnLichess: string; + /** Unique trophies */ + uniqueTrophies: string; + /** No, usernames cannot be changed for technical and practical reasons. Usernames are materialized in too many places: databases, exports, logs, and people's minds. You can adjust the capitalization once. */ + usernamesCannotBeChanged: string; + /** In general, usernames should not be: offensive, impersonating someone else, or advertising. You can read more about the %1$s. */ + usernamesNotOffensive: I18nFormat; + /** verification form */ + verificationForm: string; + /** View site information popup */ + viewSiteInformationPopUp: string; + /** Watch International Master Eric Rosen checkmate %s. */ + watchIMRosenCheckmate: I18nFormat; + /** To get it, hiimgosu challenged himself to berserk and win all games of %s. */ + wayOfBerserkExplanation: I18nFormat; + /** Unfortunately, we cannot give back rating points for games lost due to lag or disconnection, regardless of whether the problem was at your end or our end. The latter is very rare though. Also note that when Lichess restarts and you lose on time because of that, we abort the game to prevent an unfair loss. */ + weCannotDoThatEvenIfItIsServerSideButThatsRare: string; + /** We repeated a position three times. Why was the game not drawn? */ + weRepeatedthreeTimesPosButNoDraw: string; + /** What is the average centipawn loss (ACPL)? */ + whatIsACPL: string; + /** Why is there a question mark (?) next to a rating? */ + whatIsProvisionalRating: string; + /** What can my username be? */ + whatUsernameCanIchoose: string; + /** What variants can I play on Lichess? */ + whatVariantsCanIplay: string; + /** When am I eligible for the automatic rating refund from cheaters? */ + whenAmIEligibleRatinRefund: string; + /** What rating system does Lichess use? */ + whichRatingSystemUsedByLichess: string; + /** Why are ratings higher compared to other sites and organisations such as FIDE, USCF and the ICC? */ + whyAreRatingHigher: string; + /** It is best not to think of ratings as absolute numbers, or compare them against other organisations. Different organisations have different levels of players, different rating systems (Elo, Glicko, Glicko-2, or a modified version of the aforementioned). These factors can drastically affect the absolute numbers (ratings). */ + whyAreRatingHigherExplanation: string; + /** Why is Lichess called Lichess? */ + whyIsLichessCalledLichess: string; + /** Similarly, the source code for Lichess, %1$s, stands for li[chess in sca]la, seeing as the bulk of Lichess is written in %2$s, an intuitive programming language. */ + whyIsLilaCalledLila: I18nFormat; + /** Live, because games are played and watched in real-time 24/7; light and libre for the fact that Lichess is open-source and unencumbered by proprietary junk that plagues other websites. */ + whyLiveLightLibre: string; + /** Yes. Lichess has indeed inspired other open-source sites that use our %1$s, %2$s, or %3$s. */ + yesLichessInspiredOtherOpenSourceWebsites: I18nFormat; + /** It’s not possible to apply to become a moderator. If we see someone who we think would be good as a moderator, we will contact them directly. */ + youCannotApply: string; + /** On Lichess, the main difference in rules for correspondence chess is that an opening book is allowed. The use of engines is still prohibited and will result in being flagged for engine assistance. Although ICCF allows engine use in correspondence, Lichess does not. */ + youCanUseOpeningBookNoEngine: string; + }; + features: { + /** All chess basics lessons */ + allChessBasicsLessons: string; + /** All features are free for everybody, forever! */ + allFeaturesAreFreeForEverybody: string; + /** All features to come, forever! */ + allFeaturesToCome: string; + /** Board editor and analysis board with %s */ + boardEditorAndAnalysisBoardWithEngine: I18nFormat; + /** Chess insights (detailed analysis of your play) */ + chessInsights: string; + /** Cloud engine analysis */ + cloudEngineAnalysis: string; + /** Contribute to Lichess and get a cool looking Patron icon */ + contributeToLichessAndGetIcon: string; + /** Correspondence chess with conditional premoves */ + correspondenceWithConditionalPremoves: string; + /** Deep %s server analysis */ + deepXServerAnalysis: I18nFormat; + /** Download/Upload any game as PGN */ + downloadOrUploadAnyGameAsPgn: string; + /** 7-piece endgame tablebase */ + endgameTablebase: string; + /** Yes, both accounts have the same features! */ + everybodyGetsAllFeaturesForFree: string; + /** %s games per day */ + gamesPerDay: I18nPlural; + /** Global opening explorer (%s games!) */ + globalOpeningExplorerInNbGames: I18nFormat; + /** If you love Lichess, */ + ifYouLoveLichess: string; + /** iPhone & Android phones and tablets, landscape support */ + landscapeSupportOnApp: string; + /** Light/Dark theme, custom boards, pieces and background */ + lightOrDarkThemeCustomBoardsPiecesAndBackground: string; + /** Personal opening explorer */ + personalOpeningExplorer: string; + /** %1$s (also works on %2$s) */ + personalOpeningExplorerX: I18nFormat; + /** Standard chess and %s */ + standardChessAndX: I18nFormat; + /** Studies (shareable and persistent analysis) */ + studies: string; + /** Support Lichess */ + supportLichess: string; + /** Support us with a Patron account! */ + supportUsWithAPatronAccount: string; + /** Tactical puzzles from user games */ + tacticalPuzzlesFromUserGames: string; + /** Blog, forum, teams, TV, messaging, friends, challenges */ + tvForumBlogTeamsMessagingFriendsChallenges: string; + /** UltraBullet, Bullet, Blitz, Rapid, Classical, Correspondence Chess */ + ultraBulletBulletBlitzRapidClassicalAndCorrespondenceChess: string; + /** We believe every chess player deserves the best, and so: */ + weBelieveEveryChessPlayerDeservesTheBest: string; + /** Zero advertisement, no tracking */ + zeroAdsAndNoTracking: string; + }; + insight: { + /** Sorry, you cannot see %s's chess insights. */ + cantSeeInsights: I18nFormat; + /** Now crunching data just for you! */ + crunchingData: string; + /** Generate %s's chess insights. */ + generateInsights: I18nFormat; + /** %s's chess insights are protected */ + insightsAreProtected: I18nFormat; + /** insights settings */ + insightsSettings: string; + /** Maybe ask them to change their %s? */ + maybeAskThemToChangeTheir: I18nFormat; + /** %s's chess insights */ + xChessInsights: I18nFormat; + /** %s has no chess insights yet! */ + xHasNoChessInsights: I18nFormat; + }; + keyboardMove: { + /** Both the letter "o" and the digit zero "0" can be used when castling */ + bothTheLetterOAndTheDigitZero: string; + /** Capitalization only matters in ambiguous situations involving a bishop and the b-pawn */ + capitalizationOnlyMattersInAmbiguousSituations: string; + /** Drop a rook at b4 (Crazyhouse variant only) */ + dropARookAtB4: string; + /** If it is legal to castle both ways, use enter to kingside castle */ + ifItIsLegalToCastleBothWays: string; + /** If the above move notation is unfamiliar, learn more here: */ + ifTheAboveMoveNotationIsUnfamiliar: string; + /** Including an "x" to indicate a capture is optional */ + includingAXToIndicateACapture: string; + /** Keyboard input commands */ + keyboardInputCommands: string; + /** Kingside castle */ + kingsideCastle: string; + /** Move knight to c3 */ + moveKnightToC3: string; + /** Move piece from e2 to e4 */ + movePieceFromE2ToE4: string; + /** Offer or accept draw */ + offerOrAcceptDraw: string; + /** Other commands */ + otherCommands: string; + /** Perform a move */ + performAMove: string; + /** Promote c8 to queen */ + promoteC8ToQueen: string; + /** Queenside castle */ + queensideCastle: string; + /** Read out clocks */ + readOutClocks: string; + /** Read out opponent's name */ + readOutOpponentName: string; + /** Tips */ + tips: string; + /** To premove, simply type the desired premove before it is your turn */ + toPremoveSimplyTypeTheDesiredPremove: string; + }; + lag: { + /** And now, the long answer! Game lag is composed of two unrelated values (lower is better): */ + andNowTheLongAnswerLagComposedOfTwoValues: string; + /** Is Lichess lagging? */ + isLichessLagging: string; + /** Lag compensation */ + lagCompensation: string; + /** Lichess compensates network lag. This includes sustained lag and occasional lag spikes. There are limits and heuristics based on time control and the compensated lag so far, so that the result should feel reasonable for both players. As a result, having a higher network lag than your opponent is not a handicap! */ + lagCompensationExplanation: string; + /** Lichess server latency */ + lichessServerLatency: string; + /** The time it takes to process a move on the server. It's the same for everybody, and only depends on the servers load. The more players, the higher it gets, but Lichess developers do their best to keep it low. It rarely exceeds 10ms. */ + lichessServerLatencyExplanation: string; + /** Measurements in progress... */ + measurementInProgressThreeDot: string; + /** Network between Lichess and you */ + networkBetweenLichessAndYou: string; + /** The time it takes to send a move from your computer to Lichess server, and get the response back. It's specific to your distance to Lichess (France), and to the quality of your Internet connection. Lichess developers cannot fix your wifi or make light go faster. */ + networkBetweenLichessAndYouExplanation: string; + /** No. And your network is bad. */ + noAndYourNetworkIsBad: string; + /** No. And your network is good. */ + noAndYourNetworkIsGood: string; + /** Yes. It will be fixed soon! */ + yesItWillBeFixedSoon: string; + /** You can find both these values at any time, by clicking your username in the top bar. */ + youCanFindTheseValuesAtAnyTimeByClickingOnYourUsername: string; + }; + learn: { + /** Advanced */ + advanced: string; + /** A pawn on the second rank can move 2 squares at once! */ + aPawnOnTheSecondRank: string; + /** Attack the opponent's king */ + attackTheOpponentsKing: string; + /** Attack your opponent's king */ + attackYourOpponentsKing: string; + /** Awesome! */ + awesome: string; + /** Back to menu */ + backToMenu: string; + /** Congratulations! You can command a bishop. */ + bishopComplete: string; + /** Next we will learn how to manoeuvre a bishop! */ + bishopIntro: string; + /** Black just moved the pawn */ + blackJustMovedThePawnByTwoSquares: string; + /** Board setup */ + boardSetup: string; + /** Congratulations! You know how to set up the chess board. */ + boardSetupComplete: string; + /** The two armies face each other, ready for the battle. */ + boardSetupIntro: string; + /** by playing! */ + byPlaying: string; + /** Capture */ + capture: string; + /** Capture and defend pieces */ + captureAndDefendPieces: string; + /** Congratulations! You know how to fight with chess pieces! */ + captureComplete: string; + /** Identify the opponent's undefended pieces, and capture them! */ + captureIntro: string; + /** Capture, then promote! */ + captureThenPromote: string; + /** Move your king two squares */ + castleKingSide: string; + /** Castle king-side! */ + castleKingSideMovePiecesFirst: string; + /** Move your king two squares */ + castleQueenSide: string; + /** Castle queen-side! */ + castleQueenSideMovePiecesFirst: string; + /** Castling */ + castling: string; + /** Congratulations! You should almost always castle in a game. */ + castlingComplete: string; + /** Bring your king to safety, and deploy your rook for attack! */ + castlingIntro: string; + /** Check in one */ + checkInOne: string; + /** Congratulations! You checked your opponent, forcing them to defend their king! */ + checkInOneComplete: string; + /** Aim at the opponent's king */ + checkInOneGoal: string; + /** To check your opponent, attack their king. They must defend it! */ + checkInOneIntro: string; + /** Check in two */ + checkInTwo: string; + /** Congratulations! You checked your opponent, forcing them to defend their king! */ + checkInTwoComplete: string; + /** Threaten the opponent's king */ + checkInTwoGoal: string; + /** Find the right combination of two moves that checks the opponent's king! */ + checkInTwoIntro: string; + /** Chess pieces */ + chessPieces: string; + /** Combat */ + combat: string; + /** Congratulations! You know how to fight with chess pieces! */ + combatComplete: string; + /** A good warrior knows both attack and defence! */ + combatIntro: string; + /** Defeat the opponent's king */ + defeatTheOpponentsKing: string; + /** Defend your king */ + defendYourKing: string; + /** Don't let them take */ + dontLetThemTakeAnyUndefendedPiece: string; + /** En passant */ + enPassant: string; + /** Congratulations! You can now take en passant. */ + enPassantComplete: string; + /** When the opponent pawn moved by two squares, you can take it like if it moved by one square. */ + enPassantIntro: string; + /** En passant only works */ + enPassantOnlyWorksImmediately: string; + /** En passant only works */ + enPassantOnlyWorksOnFifthRank: string; + /** You're under attack! */ + escape: string; + /** Escape with the king */ + escapeOrBlock: string; + /** Escape with the king! */ + escapeWithTheKing: string; + /** Evaluate piece strength */ + evaluatePieceStrength: string; + /** Excellent! */ + excellent: string; + /** Exercise your tactical skills */ + exerciseYourTacticalSkills: string; + /** Find a way to */ + findAWayToCastleKingSide: string; + /** Find a way to */ + findAWayToCastleQueenSide: string; + /** First place the rooks! */ + firstPlaceTheRooks: string; + /** Fundamentals */ + fundamentals: string; + /** Get a free Lichess account */ + getAFreeLichessAccount: string; + /** Grab all the stars! */ + grabAllTheStars: string; + /** Grab all the stars! */ + grabAllTheStarsNoNeedToPromote: string; + /** Great job! */ + greatJob: string; + /** How the game starts */ + howTheGameStarts: string; + /** Intermediate */ + intermediate: string; + /** It moves diagonally */ + itMovesDiagonally: string; + /** It moves forward only */ + itMovesForwardOnly: string; + /** It moves in an L shape */ + itMovesInAnLShape: string; + /** It moves in straight lines */ + itMovesInStraightLines: string; + /** It now promotes to a stronger piece. */ + itNowPromotesToAStrongerPiece: string; + /** Keep your pieces safe */ + keepYourPiecesSafe: string; + /** You can now command the commander! */ + kingComplete: string; + /** You are the king. If you fall in battle, the game is lost. */ + kingIntro: string; + /** Congratulations! You have mastered the knight. */ + knightComplete: string; + /** Here's a challenge for you. The knight is... a tricky piece. */ + knightIntro: string; + /** Knights can jump over obstacles! */ + knightsCanJumpOverObstacles: string; + /** Knights have a fancy way */ + knightsHaveAFancyWay: string; + /** Last one! */ + lastOne: string; + /** Learn chess */ + learnChess: string; + /** Learn common chess positions */ + learnCommonChessPositions: string; + /** Let's go! */ + letsGo: string; + /** Mate in one */ + mateInOne: string; + /** Congratulations! That is how you win chess games! */ + mateInOneComplete: string; + /** You win when your opponent cannot defend against a check. */ + mateInOneIntro: string; + /** Menu */ + menu: string; + /** Most of the time promoting to a queen is the best. */ + mostOfTheTimePromotingToAQueenIsBest: string; + /** Nailed it. */ + nailedIt: string; + /** Next */ + next: string; + /** Next: %s */ + nextX: I18nFormat; + /** There is no escape, */ + noEscape: string; + /** Opponents from around the world */ + opponentsFromAroundTheWorld: string; + /** Out of check */ + outOfCheck: string; + /** Congratulations! Your king can never be taken, make sure you can defend against a check! */ + outOfCheckComplete: string; + /** You are in check! You must escape or block the attack. */ + outOfCheckIntro: string; + /** Outstanding! */ + outstanding: string; + /** Congratulations! Pawns have no secrets for you. */ + pawnComplete: string; + /** Pawns are weak, but they pack a lot of potential. */ + pawnIntro: string; + /** Pawn promotion */ + pawnPromotion: string; + /** Pawns form the front line. */ + pawnsFormTheFrontLine: string; + /** Pawns move forward, */ + pawnsMoveForward: string; + /** Pawns move one square only. */ + pawnsMoveOneSquareOnly: string; + /** Perfect! */ + perfect: string; + /** Piece value */ + pieceValue: string; + /** Congratulations! You know the value of material! */ + pieceValueComplete: string; + /** Take the piece with the highest value! */ + pieceValueExchange: string; + /** Pieces with high mobility have a higher value! */ + pieceValueIntro: string; + /** Take the piece */ + pieceValueLegal: string; + /** Place the bishops! */ + placeTheBishops: string; + /** Place the king! */ + placeTheKing: string; + /** Place the queen! */ + placeTheQueen: string; + /** play! */ + play: string; + /** Play machine */ + playMachine: string; + /** Play people */ + playPeople: string; + /** Practise */ + practice: string; + /** Progress: %s */ + progressX: I18nFormat; + /** Protection */ + protection: string; + /** Congratulations! A piece you don't lose is a piece you win! */ + protectionComplete: string; + /** Identify the pieces your opponent attacks, and defend them! */ + protectionIntro: string; + /** Puzzle failed! */ + puzzleFailed: string; + /** Puzzles */ + puzzles: string; + /** Queen = rook + bishop */ + queenCombinesRookAndBishop: string; + /** Congratulations! Queens have no secrets for you. */ + queenComplete: string; + /** The most powerful chess piece enters. Her majesty the queen! */ + queenIntro: string; + /** Take the piece */ + queenOverBishop: string; + /** Register */ + register: string; + /** Reset my progress */ + resetMyProgress: string; + /** Retry */ + retry: string; + /** Right on! */ + rightOn: string; + /** Congratulations! You have successfully mastered the rook. */ + rookComplete: string; + /** Click on the rook */ + rookGoal: string; + /** The rook is a powerful piece. Are you ready to command it? */ + rookIntro: string; + /** Select the piece you want! */ + selectThePieceYouWant: string; + /** Stage %s */ + stageX: I18nFormat; + /** Stage %s complete */ + stageXComplete: I18nFormat; + /** Stalemate */ + stalemate: string; + /** Congratulations! Better be stalemated than checkmated! */ + stalemateComplete: string; + /** To stalemate black: */ + stalemateGoal: string; + /** When a player is not in check and does not have a legal move, it's a stalemate. The game is drawn: no one wins, no one loses. */ + stalemateIntro: string; + /** Take all the pawns en passant! */ + takeAllThePawnsEnPassant: string; + /** Take the black pieces! */ + takeTheBlackPieces: string; + /** Take the black pieces! */ + takeTheBlackPiecesAndDontLoseYours: string; + /** Take the enemy pieces */ + takeTheEnemyPieces: string; + /** Take the piece */ + takeThePieceWithTheHighestValue: string; + /** Test your skills with the computer */ + testYourSkillsWithTheComputer: string; + /** The bishop */ + theBishop: string; + /** The fewer moves you make, */ + theFewerMoves: string; + /** The game is a draw */ + theGameIsADraw: string; + /** The king */ + theKing: string; + /** The king cannot escape, */ + theKingCannotEscapeButBlock: string; + /** The king is slow. */ + theKingIsSlow: string; + /** The knight */ + theKnight: string; + /** The knight is in the way! */ + theKnightIsInTheWay: string; + /** The most important piece */ + theMostImportantPiece: string; + /** Then place the knights! */ + thenPlaceTheKnights: string; + /** The pawn */ + thePawn: string; + /** The queen */ + theQueen: string; + /** The rook */ + theRook: string; + /** The special king move */ + theSpecialKingMove: string; + /** The special pawn move */ + theSpecialPawnMove: string; + /** This is the initial position */ + thisIsTheInitialPosition: string; + /** This knight is checking */ + thisKnightIsCheckingThroughYourDefenses: string; + /** Two moves to give a check */ + twoMovesToGiveCheck: string; + /** Use all the pawns! */ + useAllThePawns: string; + /** Use two rooks */ + useTwoRooks: string; + /** Videos */ + videos: string; + /** Watch instructive chess videos */ + watchInstructiveChessVideos: string; + /** Way to go! */ + wayToGo: string; + /** What next? */ + whatNext: string; + /** Yes, yes, yes! */ + yesYesYes: string; + /** You can get out of check */ + youCanGetOutOfCheckByTaking: string; + /** You cannot castle if */ + youCannotCastleIfAttacked: string; + /** You cannot castle if */ + youCannotCastleIfMoved: string; + /** You know how to play chess, congratulations! Do you want to become a stronger player? */ + youKnowHowToPlayChess: string; + /** One light-squared bishop, */ + youNeedBothBishops: string; + /** You're good at this! */ + youreGoodAtThis: string; + /** Your pawn reached the end of the board! */ + yourPawnReachedTheEndOfTheBoard: string; + /** You will lose all your progress! */ + youWillLoseAllYourProgress: string; + }; + oauthScope: { + /** You already have played games! */ + alreadyHavePlayedGames: string; + /** API access tokens */ + apiAccessTokens: string; + /** API documentation */ + apiDocumentation: string; + /** Here's a %1$s and the %2$s. */ + apiDocumentationLinks: I18nFormat; + /** Note for the attention of developers only: */ + attentionOfDevelopers: string; + /** authorization code flow */ + authorizationCodeFlow: string; + /** Play games with board API */ + boardPlay: string; + /** Play games with the bot API */ + botPlay: string; + /** You can make OAuth requests without going through the %s. */ + canMakeOauthRequests: I18nFormat; + /** Carefully select what it is allowed to do on your behalf. */ + carefullySelect: string; + /** Create many games at once for other players */ + challengeBulk: string; + /** Read incoming challenges */ + challengeRead: string; + /** Send, accept and reject challenges */ + challengeWrite: string; + /** Make sure to copy your new personal access token now. You won’t be able to see it again! */ + copyTokenNow: string; + /** Created %s */ + created: I18nFormat; + /** The token will grant access to your account. Do NOT share it with anyone! */ + doNotShareIt: string; + /** Read email address */ + emailRead: string; + /** View and use your external engines */ + engineRead: string; + /** Create and update external engines */ + engineWrite: string; + /** Read followed players */ + followRead: string; + /** Follow and unfollow other players */ + followWrite: string; + /** For example: %s */ + forExample: I18nFormat; + /** generate a personal access token */ + generatePersonalToken: string; + /** Giving these pre-filled URLs to your users will help them get the right token scopes. */ + givingPrefilledUrls: string; + /** Guard these tokens carefully! They are like passwords. The advantage to using tokens over putting your password into a script is that tokens can be revoked, and you can generate lots of them. */ + guardTokensCarefully: string; + /** Instead, %s that you can directly use in API requests. */ + insteadGenerateToken: I18nFormat; + /** Last used %s */ + lastUsed: I18nFormat; + /** Send private messages to other players */ + msgWrite: string; + /** New personal API access token */ + newAccessToken: string; + /** New access token */ + newToken: string; + /** Personal API access tokens */ + personalAccessTokens: string; + /** personal token app example */ + personalTokenAppExample: string; + /** It is possible to pre-fill this form by tweaking the query parameters of the URL. */ + possibleToPrefill: string; + /** Read preferences */ + preferenceRead: string; + /** Write preference */ + preferenceWrite: string; + /** Read puzzle activity */ + puzzleRead: string; + /** Create and join puzzle races */ + racerWrite: string; + /** So you remember what this token is for */ + rememberTokenUse: string; + /** The scope codes can be found in the HTML code of the form. */ + scopesCanBeFound: string; + /** Read private studies and broadcasts */ + studyRead: string; + /** Create, update, delete studies and broadcasts */ + studyWrite: string; + /** Manage teams you lead: send PMs, kick members */ + teamLead: string; + /** Read private team information */ + teamRead: string; + /** Join and leave teams */ + teamWrite: string; + /** ticks the %1$s and %2$s scopes, and sets the token description. */ + ticksTheScopes: I18nFormat; + /** Token description */ + tokenDescription: string; + /** A token grants other people permission to use your account. */ + tokenGrantsPermission: string; + /** Create, update, and join tournaments */ + tournamentWrite: string; + /** Create authenticated website sessions (grants full access!) */ + webLogin: string; + /** Use moderator tools (within bounds of your permission) */ + webMod: string; + /** What the token can do on your behalf: */ + whatTheTokenCanDo: string; + }; + onboarding: { + /** Configure Lichess to your liking. */ + configureLichess: string; + /** Will a child use this account? You might want to enable %s. */ + enabledKidModeSuggestion: I18nFormat; + /** Explore the site and have fun :) */ + exploreTheSiteAndHaveFun: string; + /** Follow your friends on Lichess. */ + followYourFriendsOnLichess: string; + /** Improve with chess tactics puzzles. */ + improveWithChessTacticsPuzzles: string; + /** Learn chess rules */ + learnChessRules: string; + /** Learn from %1$s and %2$s. */ + learnFromXAndY: I18nFormat; + /** Play in tournaments. */ + playInTournaments: string; + /** Play opponents from around the world. */ + playOpponentsFromAroundTheWorld: string; + /** Play the Artificial Intelligence. */ + playTheArtificialIntelligence: string; + /** This is your profile page. */ + thisIsYourProfilePage: string; + /** Welcome! */ + welcome: string; + /** Welcome to lichess.org! */ + welcomeToLichess: string; + /** What now? Here are a few suggestions: */ + whatNowSuggestions: string; + }; + patron: { + /** Yes, here's the act of creation (in French) */ + actOfCreation: string; + /** Amount */ + amount: string; + /** We also accept bank transfers */ + bankTransfers: string; + /** Become a Lichess Patron */ + becomePatron: string; + /** Cancel your support */ + cancelSupport: string; + /** The celebrated Patrons who make Lichess possible */ + celebratedPatrons: string; + /** Change currency */ + changeCurrency: string; + /** Change the monthly amount (%s) */ + changeMonthlyAmount: I18nFormat; + /** Can I change/cancel my monthly support? */ + changeMonthlySupport: string; + /** Yes, at any time, from this page. */ + changeOrContact: I18nFormat; + /** Check out your profile page! */ + checkOutProfile: string; + /** contact Lichess support */ + contactSupport: string; + /** See the detailed cost breakdown */ + costBreakdown: string; + /** Current status */ + currentStatus: string; + /** Date */ + date: string; + /** Decide what Lichess is worth to you: */ + decideHowMuch: string; + /** Donate */ + donate: string; + /** Donate as %s */ + donateAsX: I18nFormat; + /** In one month, you will NOT be charged again, and your Lichess account will revert to a regular account. */ + downgradeNextMonth: string; + /** See the detailed feature comparison */ + featuresComparison: string; + /** Free account */ + freeAccount: string; + /** Free chess for everyone, forever! */ + freeChess: string; + /** Gift Patron wings to a player */ + giftPatronWings: string; + /** Gift Patron wings */ + giftPatronWingsShort: string; + /** If not renewed, your account will then revert to a regular account. */ + ifNotRenewedThenAccountWillRevert: string; + /** Lichess is registered with %s. */ + lichessIsRegisteredWith: I18nFormat; + /** Lichess Patron */ + lichessPatron: string; + /** Lifetime */ + lifetime: string; + /** Lifetime Lichess Patron */ + lifetimePatron: string; + /** Log in to donate */ + logInToDonate: string; + /** Make an additional donation */ + makeAdditionalDonation: string; + /** Monthly */ + monthly: string; + /** New Patrons */ + newPatrons: string; + /** Next payment */ + nextPayment: string; + /** No ads, no subscriptions; but open-source and passion. */ + noAdsNoSubs: string; + /** No longer support Lichess */ + noLongerSupport: string; + /** No, because Lichess is entirely free, forever, and for everyone. That's a promise. */ + noPatronFeatures: string; + /** You are now a lifetime Lichess Patron! */ + nowLifetime: string; + /** You are now a Lichess Patron for one month! */ + nowOneMonth: string; + /** Is Lichess an official non-profit? */ + officialNonProfit: string; + /** One-time */ + onetime: string; + /** Please note that only the donation form above will grant the Patron status. */ + onlyDonationFromAbove: string; + /** Other */ + otherAmount: string; + /** Other methods of donation? */ + otherMethods: string; + /** Are some features reserved to Patrons? */ + patronFeatures: string; + /** Lichess Patron for %s months */ + patronForMonths: I18nPlural; + /** You have a Patron account until %s. */ + patronUntil: I18nFormat; + /** Pay %s once. Be a Lichess Patron forever! */ + payLifetimeOnce: I18nFormat; + /** Payment details */ + paymentDetails: string; + /** You now have a permanent Patron account. */ + permanentPatron: string; + /** Please enter an amount in %s */ + pleaseEnterAmountInX: I18nFormat; + /** Recurring billing, renewing your Patron wings every month. */ + recurringBilling: string; + /** First of all, powerful servers. */ + serversAndDeveloper: I18nFormat; + /** A single donation that grants you the Patron wings for one month. */ + singleDonation: string; + /** Withdraw your credit card and stop payments: */ + stopPayments: string; + /** Cancel PayPal subscription and stop payments: */ + stopPaymentsPayPal: string; + /** Manage your subscription and download your invoices and receipts */ + stripeManageSub: string; + /** Thank you for your donation! */ + thankYou: string; + /** Your transaction has been completed, and a receipt for your donation has been emailed to you. */ + transactionCompleted: string; + /** Thank you very much for your help. You rock! */ + tyvm: string; + /** Update */ + update: string; + /** Update payment method */ + updatePaymentMethod: string; + /** View other Lichess Patrons */ + viewOthers: string; + /** We are a non‑profit association because we believe everyone should have access to a free, world-class chess platform. */ + weAreNonProfit: string; + /** We are a small team, so your support makes a huge difference! */ + weAreSmallTeam: string; + /** We rely on support from people like you to make it possible. If you enjoy using Lichess, please consider supporting us by donating and becoming a Patron! */ + weRelyOnSupport: string; + /** Where does the money go? */ + whereMoneyGoes: string; + /** Credit Card */ + withCreditCard: string; + /** %s became a Lichess Patron */ + xBecamePatron: I18nFormat; + /** %1$s is a Lichess Patron for %2$s months */ + xIsPatronForNbMonths: I18nPlural; + /** %1$s or %2$s */ + xOrY: I18nFormat; + /** You have a Lifetime Patron account. That's pretty awesome! */ + youHaveLifetime: string; + /** You support lichess.org with %s per month. */ + youSupportWith: I18nFormat; + /** You will be charged %1$s on %2$s. */ + youWillBeChargedXOnY: I18nFormat; + }; + perfStat: { + /** Average opponent */ + averageOpponent: string; + /** Berserked games */ + berserkedGames: string; + /** Best rated victories */ + bestRated: string; + /** Current streak: %s */ + currentStreak: I18nFormat; + /** Defeats */ + defeats: string; + /** Disconnections */ + disconnections: string; + /** from %1$s to %2$s */ + fromXToY: I18nFormat; + /** Games played in a row */ + gamesInARow: string; + /** Highest rating: %s */ + highestRating: I18nFormat; + /** Less than one hour between games */ + lessThanOneHour: string; + /** Longest streak: %s */ + longestStreak: I18nFormat; + /** Losing streak */ + losingStreak: string; + /** Lowest rating: %s */ + lowestRating: I18nFormat; + /** Max time spent playing */ + maxTimePlaying: string; + /** Not enough games played */ + notEnoughGames: string; + /** Not enough rated games have been played to establish a reliable rating. */ + notEnoughRatedGames: string; + /** now */ + now: string; + /** %s stats */ + perfStats: I18nFormat; + /** Progression over the last %s games: */ + progressOverLastXGames: I18nFormat; + /** provisional */ + provisional: string; + /** Rated games */ + ratedGames: string; + /** Rating deviation: %s. */ + ratingDeviation: I18nFormat; + /** Lower value means the rating is more stable. Above %1$s, the rating is considered provisional. To be included in the rankings, this value should be below %2$s (standard chess) or %3$s (variants). */ + ratingDeviationTooltip: I18nFormat; + /** Time spent playing */ + timeSpentPlaying: string; + /** Total games */ + totalGames: string; + /** Tournament games */ + tournamentGames: string; + /** Victories */ + victories: string; + /** View the games */ + viewTheGames: string; + /** Winning streak */ + winningStreak: string; + }; + preferences: { + /** Bell notification sound */ + bellNotificationSound: string; + /** Board coordinates (A-H, 1-8) */ + boardCoordinates: string; + /** Board highlights (last move and check) */ + boardHighlights: string; + /** Either */ + bothClicksAndDrag: string; + /** Move king onto rook */ + castleByMovingOntoTheRook: string; + /** Castling method */ + castleByMovingTheKingTwoSquaresOrOntoTheRook: string; + /** Move king two squares */ + castleByMovingTwoSquares: string; + /** Chess clock */ + chessClock: string; + /** Chess piece symbol */ + chessPieceSymbol: string; + /** Claim draw on threefold repetition automatically */ + claimDrawOnThreefoldRepetitionAutomatically: string; + /** Click two squares */ + clickTwoSquares: string; + /** Confirm resignation and draw offers */ + confirmResignationAndDrawOffers: string; + /** Correspondence and unlimited */ + correspondenceAndUnlimited: string; + /** Daily email listing your correspondence games */ + correspondenceEmailNotification: string; + /** Display */ + display: string; + /** Show board resize handle */ + displayBoardResizeHandle: string; + /** Drag a piece */ + dragPiece: string; + /** Can be disabled during a game with the board menu */ + explainCanThenBeTemporarilyDisabled: string; + /** Hold the key while promoting to temporarily disable auto-promotion */ + explainPromoteToQueenAutomatically: string; + /** This hides all ratings from Lichess, to help focus on the chess. Rated games still impact your rating, this is only about what you get to see. */ + explainShowPlayerRatings: string; + /** Game behaviour */ + gameBehavior: string; + /** Give more time */ + giveMoreTime: string; + /** Horizontal green progress bars */ + horizontalGreenProgressBars: string; + /** How do you move pieces? */ + howDoYouMovePieces: string; + /** In casual games only */ + inCasualGamesOnly: string; + /** Correspondence games */ + inCorrespondenceGames: string; + /** In-game only */ + inGameOnly: string; + /** Input moves with the keyboard */ + inputMovesWithTheKeyboard: string; + /** Input moves with your voice */ + inputMovesWithVoice: string; + /** Material difference */ + materialDifference: string; + /** Move confirmation */ + moveConfirmation: string; + /** Move list while playing */ + moveListWhilePlaying: string; + /** Notifications */ + notifications: string; + /** Bell notification within Lichess */ + notifyBell: string; + /** Challenges */ + notifyChallenge: string; + /** Device */ + notifyDevice: string; + /** Forum comment mentions you */ + notifyForumMention: string; + /** Correspondence game updates */ + notifyGameEvent: string; + /** New inbox message */ + notifyInboxMsg: string; + /** Study invite */ + notifyInvitedStudy: string; + /** Device notification when you're not on Lichess */ + notifyPush: string; + /** Streamer goes live */ + notifyStreamStart: string; + /** Correspondence clock running out */ + notifyTimeAlarm: string; + /** Tournament starting soon */ + notifyTournamentSoon: string; + /** Browser */ + notifyWeb: string; + /** Only on initial position */ + onlyOnInitialPosition: string; + /** Letter (K, Q, R, B, N) */ + pgnLetter: string; + /** Move notation */ + pgnPieceNotation: string; + /** Piece animation */ + pieceAnimation: string; + /** Piece destinations (valid moves and premoves) */ + pieceDestinations: string; + /** Preferences */ + preferences: string; + /** Premoves (playing during opponent turn) */ + premovesPlayingDuringOpponentTurn: string; + /** Privacy */ + privacy: string; + /** Promote to Queen automatically */ + promoteToQueenAutomatically: string; + /** Say "Good game, well played" upon defeat or draw */ + sayGgWpAfterLosingOrDrawing: string; + /** Scroll on the board to replay moves */ + scrollOnTheBoardToReplayMoves: string; + /** Show player flairs */ + showFlairs: string; + /** Show player ratings */ + showPlayerRatings: string; + /** Snap arrows to valid moves */ + snapArrowsToValidMoves: string; + /** Sound when time gets critical */ + soundWhenTimeGetsCritical: string; + /** Takebacks (with opponent approval) */ + takebacksWithOpponentApproval: string; + /** Tenths of seconds */ + tenthsOfSeconds: string; + /** When premoving */ + whenPremoving: string; + /** When time remaining < 10 seconds */ + whenTimeRemainingLessThanTenSeconds: string; + /** When time remaining < 30 seconds */ + whenTimeRemainingLessThanThirtySeconds: string; + /** Your preferences have been saved. */ + yourPreferencesHaveBeenSaved: string; + /** Zen mode */ + zenMode: string; + }; + puzzle: { + /** Add another theme */ + addAnotherTheme: string; + /** Advanced */ + advanced: string; + /** Best move! */ + bestMove: string; + /** By openings */ + byOpenings: string; + /** Click to solve */ + clickToSolve: string; + /** Continue the streak */ + continueTheStreak: string; + /** Continue training */ + continueTraining: string; + /** Daily Puzzle */ + dailyPuzzle: string; + /** Did you like this puzzle? */ + didYouLikeThisPuzzle: string; + /** Difficulty level */ + difficultyLevel: string; + /** Down vote puzzle */ + downVote: string; + /** Easier */ + easier: string; + /** Easiest */ + easiest: string; + /** Example */ + example: string; + /** incorrect */ + failed: string; + /** Find the best move for black. */ + findTheBestMoveForBlack: string; + /** Find the best move for white. */ + findTheBestMoveForWhite: string; + /** From game %s */ + fromGameLink: I18nFormat; + /** From my games */ + fromMyGames: string; + /** You have no puzzles in the database, but Lichess still loves you very much. */ + fromMyGamesNone: string; + /** Puzzles from %s' games */ + fromXGames: I18nFormat; + /** %1$s puzzles found in %2$s games */ + fromXGamesFound: I18nFormat; + /** Goals */ + goals: string; + /** Good move */ + goodMove: string; + /** Harder */ + harder: string; + /** Hardest */ + hardest: string; + /** hidden */ + hidden: string; + /** Puzzle history */ + history: string; + /** Improvement areas */ + improvementAreas: string; + /** Train these to optimize your progress! */ + improvementAreasDescription: string; + /** Jump to next puzzle immediately */ + jumpToNextPuzzleImmediately: string; + /** Keep going… */ + keepGoing: string; + /** Lengths */ + lengths: string; + /** Lookup puzzles from a player's games */ + lookupOfPlayer: string; + /** Mates */ + mates: string; + /** Motifs */ + motifs: string; + /** %s played */ + nbPlayed: I18nPlural; + /** %s points above your puzzle rating */ + nbPointsAboveYourPuzzleRating: I18nPlural; + /** %s points below your puzzle rating */ + nbPointsBelowYourPuzzleRating: I18nPlural; + /** %s to replay */ + nbToReplay: I18nPlural; + /** New streak */ + newStreak: string; + /** Next puzzle */ + nextPuzzle: string; + /** Nothing to show, go play some puzzles first! */ + noPuzzlesToShow: string; + /** Normal */ + normal: string; + /** That's not the move! */ + notTheMove: string; + /** Openings you played the most in rated games */ + openingsYouPlayedTheMost: string; + /** Origin */ + origin: string; + /** %s solved */ + percentSolved: I18nFormat; + /** Phases */ + phases: string; + /** Played %s times */ + playedXTimes: I18nPlural; + /** Puzzle complete! */ + puzzleComplete: string; + /** Puzzle Dashboard */ + puzzleDashboard: string; + /** Train, analyse, improve */ + puzzleDashboardDescription: string; + /** Puzzle %s */ + puzzleId: I18nFormat; + /** Puzzle of the day */ + puzzleOfTheDay: string; + /** Puzzles */ + puzzles: string; + /** Puzzles by openings */ + puzzlesByOpenings: string; + /** Success! */ + puzzleSuccess: string; + /** Puzzle Themes */ + puzzleThemes: string; + /** Rating: %s */ + ratingX: I18nFormat; + /** Recommended */ + recommended: string; + /** Search puzzles */ + searchPuzzles: string; + /** solved */ + solved: string; + /** Special moves */ + specialMoves: string; + /** Solve progressively harder puzzles and build a win streak. There is no clock, so take your time. One wrong move, and it's game over! But you can skip one move per session. */ + streakDescription: string; + /** Skip this move to preserve your streak! Only works once per run. */ + streakSkipExplanation: string; + /** You perform the best in these themes */ + strengthDescription: string; + /** Strengths */ + strengths: string; + /** To get personalized puzzles: */ + toGetPersonalizedPuzzles: string; + /** Try something else. */ + trySomethingElse: string; + /** Up vote puzzle */ + upVote: string; + /** Use Ctrl+f to find your favourite opening! */ + useCtrlF: string; + /** Use "Find in page" in the browser menu to find your favourite opening! */ + useFindInPage: string; + /** Vote to load the next one! */ + voteToLoadNextOne: string; + /** Your puzzle rating will not change. Note that puzzles are not a competition. Your rating helps selecting the best puzzles for your current skill. */ + yourPuzzleRatingWillNotChange: string; + /** Your streak: %s */ + yourStreakX: I18nFormat; + }; + puzzleTheme: { + /** Advanced pawn */ + advancedPawn: string; + /** One of your pawns is deep into the opponent position, maybe threatening to promote. */ + advancedPawnDescription: string; + /** Advantage */ + advantage: string; + /** Seize your chance to get a decisive advantage. (200cp ≤ eval ≤ 600cp) */ + advantageDescription: string; + /** Anastasia's mate */ + anastasiaMate: string; + /** A knight and rook or queen team up to trap the opposing king between the side of the board and a friendly piece. */ + anastasiaMateDescription: string; + /** Arabian mate */ + arabianMate: string; + /** A knight and a rook team up to trap the opposing king on a corner of the board. */ + arabianMateDescription: string; + /** Attacking f2 or f7 */ + attackingF2F7: string; + /** An attack focusing on the f2 or f7 pawn, such as in the fried liver opening. */ + attackingF2F7Description: string; + /** Attraction */ + attraction: string; + /** An exchange or sacrifice encouraging or forcing an opponent piece to a square that allows a follow-up tactic. */ + attractionDescription: string; + /** Back rank mate */ + backRankMate: string; + /** Checkmate the king on the home rank, when it is trapped there by its own pieces. */ + backRankMateDescription: string; + /** Bishop endgame */ + bishopEndgame: string; + /** An endgame with only bishops and pawns. */ + bishopEndgameDescription: string; + /** Boden's mate */ + bodenMate: string; + /** Two attacking bishops on criss-crossing diagonals deliver mate to a king obstructed by friendly pieces. */ + bodenMateDescription: string; + /** Capture the defender */ + capturingDefender: string; + /** Removing a piece that is critical to defence of another piece, allowing the now undefended piece to be captured on a following move. */ + capturingDefenderDescription: string; + /** Castling */ + castling: string; + /** Bring the king to safety, and deploy the rook for attack. */ + castlingDescription: string; + /** Clearance */ + clearance: string; + /** A move, often with tempo, that clears a square, file or diagonal for a follow-up tactical idea. */ + clearanceDescription: string; + /** Crushing */ + crushing: string; + /** Spot the opponent blunder to obtain a crushing advantage. (eval ≥ 600cp) */ + crushingDescription: string; + /** Defensive move */ + defensiveMove: string; + /** A precise move or sequence of moves that is needed to avoid losing material or another advantage. */ + defensiveMoveDescription: string; + /** Deflection */ + deflection: string; + /** A move that distracts an opponent piece from another duty that it performs, such as guarding a key square. Sometimes also called "overloading". */ + deflectionDescription: string; + /** Discovered attack */ + discoveredAttack: string; + /** Moving a piece (such as a knight), that previously blocked an attack by a long range piece (such as a rook), out of the way of that piece. */ + discoveredAttackDescription: string; + /** Double bishop mate */ + doubleBishopMate: string; + /** Two attacking bishops on adjacent diagonals deliver mate to a king obstructed by friendly pieces. */ + doubleBishopMateDescription: string; + /** Double check */ + doubleCheck: string; + /** Checking with two pieces at once, as a result of a discovered attack where both the moving piece and the unveiled piece attack the opponent's king. */ + doubleCheckDescription: string; + /** Dovetail mate */ + dovetailMate: string; + /** A queen delivers mate to an adjacent king, whose only two escape squares are obstructed by friendly pieces. */ + dovetailMateDescription: string; + /** Endgame */ + endgame: string; + /** A tactic during the last phase of the game. */ + endgameDescription: string; + /** A tactic involving the en passant rule, where a pawn can capture an opponent pawn that has bypassed it using its initial two-square move. */ + enPassantDescription: string; + /** Equality */ + equality: string; + /** Come back from a losing position, and secure a draw or a balanced position. (eval ≤ 200cp) */ + equalityDescription: string; + /** Exposed king */ + exposedKing: string; + /** A tactic involving a king with few defenders around it, often leading to checkmate. */ + exposedKingDescription: string; + /** Fork */ + fork: string; + /** A move where the moved piece attacks two opponent pieces at once. */ + forkDescription: string; + /** Hanging piece */ + hangingPiece: string; + /** A tactic involving an opponent piece being undefended or insufficiently defended and free to capture. */ + hangingPieceDescription: string; + /** Healthy mix */ + healthyMix: string; + /** A bit of everything. You don't know what to expect, so you remain ready for anything! Just like in real games. */ + healthyMixDescription: string; + /** Hook mate */ + hookMate: string; + /** Checkmate with a rook, knight, and pawn along with one enemy pawn to limit the enemy king's escape. */ + hookMateDescription: string; + /** Interference */ + interference: string; + /** Moving a piece between two opponent pieces to leave one or both opponent pieces undefended, such as a knight on a defended square between two rooks. */ + interferenceDescription: string; + /** Intermezzo */ + intermezzo: string; + /** Instead of playing the expected move, first interpose another move posing an immediate threat that the opponent must answer. Also known as "Zwischenzug" or "In between". */ + intermezzoDescription: string; + /** Kingside attack */ + kingsideAttack: string; + /** An attack of the opponent's king, after they castled on the king side. */ + kingsideAttackDescription: string; + /** Knight endgame */ + knightEndgame: string; + /** An endgame with only knights and pawns. */ + knightEndgameDescription: string; + /** Long puzzle */ + long: string; + /** Three moves to win. */ + longDescription: string; + /** Master games */ + master: string; + /** Puzzles from games played by titled players. */ + masterDescription: string; + /** Master vs Master games */ + masterVsMaster: string; + /** Puzzles from games between two titled players. */ + masterVsMasterDescription: string; + /** Checkmate */ + mate: string; + /** Win the game with style. */ + mateDescription: string; + /** Mate in 1 */ + mateIn1: string; + /** Deliver checkmate in one move. */ + mateIn1Description: string; + /** Mate in 2 */ + mateIn2: string; + /** Deliver checkmate in two moves. */ + mateIn2Description: string; + /** Mate in 3 */ + mateIn3: string; + /** Deliver checkmate in three moves. */ + mateIn3Description: string; + /** Mate in 4 */ + mateIn4: string; + /** Deliver checkmate in four moves. */ + mateIn4Description: string; + /** Mate in 5 or more */ + mateIn5: string; + /** Figure out a long mating sequence. */ + mateIn5Description: string; + /** Middlegame */ + middlegame: string; + /** A tactic during the second phase of the game. */ + middlegameDescription: string; + /** One-move puzzle */ + oneMove: string; + /** A puzzle that is only one move long. */ + oneMoveDescription: string; + /** Opening */ + opening: string; + /** A tactic during the first phase of the game. */ + openingDescription: string; + /** Pawn endgame */ + pawnEndgame: string; + /** An endgame with only pawns. */ + pawnEndgameDescription: string; + /** Pin */ + pin: string; + /** A tactic involving pins, where a piece is unable to move without revealing an attack on a higher value piece. */ + pinDescription: string; + /** Player games */ + playerGames: string; + /** Lookup puzzles generated from your games, or from another player's games. */ + playerGamesDescription: string; + /** Promotion */ + promotion: string; + /** Promote one of your pawn to a queen or minor piece. */ + promotionDescription: string; + /** These puzzles are in the public domain, and can be downloaded from %s. */ + puzzleDownloadInformation: I18nFormat; + /** Queen endgame */ + queenEndgame: string; + /** An endgame with only queens and pawns. */ + queenEndgameDescription: string; + /** Queen and Rook */ + queenRookEndgame: string; + /** An endgame with only queens, rooks and pawns. */ + queenRookEndgameDescription: string; + /** Queenside attack */ + queensideAttack: string; + /** An attack of the opponent's king, after they castled on the queen side. */ + queensideAttackDescription: string; + /** Quiet move */ + quietMove: string; + /** A move that does neither make a check or capture, nor an immediate threat to capture, but does prepare a more hidden unavoidable threat for a later move. */ + quietMoveDescription: string; + /** Rook endgame */ + rookEndgame: string; + /** An endgame with only rooks and pawns. */ + rookEndgameDescription: string; + /** Sacrifice */ + sacrifice: string; + /** A tactic involving giving up material in the short-term, to gain an advantage again after a forced sequence of moves. */ + sacrificeDescription: string; + /** Short puzzle */ + short: string; + /** Two moves to win. */ + shortDescription: string; + /** Skewer */ + skewer: string; + /** A motif involving a high value piece being attacked, moving out the way, and allowing a lower value piece behind it to be captured or attacked, the inverse of a pin. */ + skewerDescription: string; + /** Smothered mate */ + smotheredMate: string; + /** A checkmate delivered by a knight in which the mated king is unable to move because it is surrounded (or smothered) by its own pieces. */ + smotheredMateDescription: string; + /** Super GM games */ + superGM: string; + /** Puzzles from games played by the best players in the world. */ + superGMDescription: string; + /** Trapped piece */ + trappedPiece: string; + /** A piece is unable to escape capture as it has limited moves. */ + trappedPieceDescription: string; + /** Underpromotion */ + underPromotion: string; + /** Promotion to a knight, bishop, or rook. */ + underPromotionDescription: string; + /** Very long puzzle */ + veryLong: string; + /** Four moves or more to win. */ + veryLongDescription: string; + /** X-Ray attack */ + xRayAttack: string; + /** A piece attacks or defends a square, through an enemy piece. */ + xRayAttackDescription: string; + /** Zugzwang */ + zugzwang: string; + /** The opponent is limited in the moves they can make, and all moves worsen their position. */ + zugzwangDescription: string; + }; + search: { + /** Advanced search */ + advancedSearch: string; + /** A.I. level */ + aiLevel: string; + /** Analysis */ + analysis: string; + /** Ascending */ + ascending: string; + /** Colour */ + color: string; + /** Date */ + date: string; + /** Descending */ + descending: string; + /** Evaluation */ + evaluation: string; + /** From */ + from: string; + /** %s games found */ + gamesFound: I18nPlural; + /** Whether the player's opponent was human or a computer */ + humanOrComputer: string; + /** Include */ + include: string; + /** Loser */ + loser: string; + /** Maximum number */ + maxNumber: string; + /** The maximum number of games to return */ + maxNumberExplanation: string; + /** Number of turns */ + nbTurns: string; + /** Only games where a computer analysis is available */ + onlyAnalysed: string; + /** Opponent name */ + opponentName: string; + /** The average rating of both players */ + ratingExplanation: string; + /** Result */ + result: string; + /** Search */ + search: string; + /** Search in %s chess games */ + searchInXGames: I18nPlural; + /** Sort by */ + sortBy: string; + /** Source */ + source: string; + /** To */ + to: string; + /** Winner colour */ + winnerColor: string; + /** %s games found */ + xGamesFound: I18nPlural; + }; + settings: { + /** You will not be allowed to open a new account with the same name, even if the case is different. */ + cantOpenSimilarAccount: string; + /** I changed my mind, don't close my account */ + changedMindDoNotCloseAccount: string; + /** Close account */ + closeAccount: string; + /** Are you sure you want to close your account? Closing your account is a permanent decision. You will NEVER be able to log in EVER AGAIN. */ + closeAccountExplanation: string; + /** Closing is definitive. There is no going back. Are you sure? */ + closingIsDefinitive: string; + /** Your account is managed, and cannot be closed. */ + managedAccountCannotBeClosed: string; + /** Settings */ + settings: string; + /** This account is closed. */ + thisAccountIsClosed: string; + }; + storm: { + /** Accuracy */ + accuracy: string; + /** All-time */ + allTime: string; + /** Best run of day */ + bestRunOfDay: string; + /** Click to reload */ + clickToReload: string; + /** Combo */ + combo: string; + /** Create a new game */ + createNewGame: string; + /** End run (hotkey: Enter) */ + endRun: string; + /** Failed puzzles */ + failedPuzzles: string; + /** Get ready! */ + getReady: string; + /** Highest solved */ + highestSolved: string; + /** Highscores */ + highscores: string; + /** Highscore: %s */ + highscoreX: I18nFormat; + /** Join a public race */ + joinPublicRace: string; + /** Join rematch */ + joinRematch: string; + /** Join the race! */ + joinTheRace: string; + /** Moves */ + moves: string; + /** Move to start */ + moveToStart: string; + /** New all-time highscore! */ + newAllTimeHighscore: string; + /** New daily highscore! */ + newDailyHighscore: string; + /** New monthly highscore! */ + newMonthlyHighscore: string; + /** New run (hotkey: Space) */ + newRun: string; + /** New weekly highscore! */ + newWeeklyHighscore: string; + /** Next race */ + nextRace: string; + /** Play again */ + playAgain: string; + /** Played %1$s runs of %2$s */ + playedNbRunsOfPuzzleStorm: I18nPlural; + /** Previous highscore was %s */ + previousHighscoreWasX: I18nFormat; + /** Puzzles played */ + puzzlesPlayed: string; + /** puzzles solved */ + puzzlesSolved: string; + /** Race complete! */ + raceComplete: string; + /** Race your friends */ + raceYourFriends: string; + /** Runs */ + runs: string; + /** Score */ + score: string; + /** skip */ + skip: string; + /** Skip this move to preserve your combo! Only works once per race. */ + skipExplanation: string; + /** You can skip one move per race: */ + skipHelp: string; + /** Skipped puzzle */ + skippedPuzzle: string; + /** Slow puzzles */ + slowPuzzles: string; + /** Spectating */ + spectating: string; + /** Start the race */ + startTheRace: string; + /** This month */ + thisMonth: string; + /** This run has expired! */ + thisRunHasExpired: string; + /** This run was opened in another tab! */ + thisRunWasOpenedInAnotherTab: string; + /** This week */ + thisWeek: string; + /** Time */ + time: string; + /** Time per move */ + timePerMove: string; + /** View best runs */ + viewBestRuns: string; + /** Wait for rematch */ + waitForRematch: string; + /** Waiting for more players to join... */ + waitingForMorePlayers: string; + /** Waiting to start */ + waitingToStart: string; + /** %s runs */ + xRuns: I18nPlural; + /** You play the black pieces in all puzzles */ + youPlayTheBlackPiecesInAllPuzzles: string; + /** You play the white pieces in all puzzles */ + youPlayTheWhitePiecesInAllPuzzles: string; + /** Your rank: %s */ + yourRankX: I18nFormat; + }; + streamer: { + /** All streamers */ + allStreamers: string; + /** Your stream is approved. */ + approved: string; + /** Become a Lichess streamer */ + becomeStreamer: string; + /** Change/delete your picture */ + changePicture: string; + /** Currently streaming: %s */ + currentlyStreaming: I18nFormat; + /** Download streamer kit */ + downloadKit: string; + /** Do you have a Twitch or YouTube channel? */ + doYouHaveStream: string; + /** Edit streamer page */ + editPage: string; + /** Headline */ + headline: string; + /** Here we go! */ + hereWeGo: string; + /** Keep it short: %s characters max */ + keepItShort: I18nPlural; + /** Last stream %s */ + lastStream: I18nFormat; + /** Lichess streamer */ + lichessStreamer: string; + /** Lichess streamers */ + lichessStreamers: string; + /** LIVE! */ + live: string; + /** Long description */ + longDescription: string; + /** Max size: %s */ + maxSize: I18nFormat; + /** OFFLINE */ + offline: string; + /** Optional. Leave empty if none */ + optionalOrEmpty: string; + /** Your stream is being reviewed by moderators. */ + pendingReview: string; + /** Get a flaming streamer icon on your Lichess profile. */ + perk1: string; + /** Get bumped up to the top of the streamers list. */ + perk2: string; + /** Notify your Lichess followers. */ + perk3: string; + /** Show your stream in your games, tournaments and studies. */ + perk4: string; + /** Benefits of streaming with the keyword */ + perks: string; + /** Please fill in your streamer information, and upload a picture. */ + pleaseFillIn: string; + /** request a moderator review */ + requestReview: string; + /** Include the keyword \"lichess.org\" in your stream title and use the category \"Chess\" when you stream on Lichess. */ + rule1: string; + /** Remove the keyword when you stream non-Lichess stuff. */ + rule2: string; + /** Lichess will detect your stream automatically and enable the following perks: */ + rule3: string; + /** Read our %s to ensure fair play for everyone during your stream. */ + rule4: I18nFormat; + /** Streaming rules */ + rules: string; + /** The Lichess streamer page targets your audience with the language provided by your streaming platform. Set the correct default language for your chess streams in the app or service you use to broadcast. */ + streamerLanguageSettings: string; + /** Your streamer name on Lichess */ + streamerName: string; + /** streaming Fairplay FAQ */ + streamingFairplayFAQ: string; + /** Tell us about your stream in one sentence */ + tellUsAboutTheStream: string; + /** Your Twitch username or URL */ + twitchUsername: string; + /** Upload a picture */ + uploadPicture: string; + /** Visible on the streamers page */ + visibility: string; + /** When approved by moderators */ + whenApproved: string; + /** When you are ready to be listed as a Lichess streamer, %s */ + whenReady: I18nFormat; + /** %s is streaming */ + xIsStreaming: I18nFormat; + /** %s streamer picture */ + xStreamerPicture: I18nFormat; + /** Your streamer page */ + yourPage: string; + /** Your YouTube channel ID */ + youTubeChannelId: string; + }; + site: { + /** Abort game */ + abortGame: string; + /** Abort the game */ + abortTheGame: string; + /** About */ + about: string; + /** Simuls involve a single player facing several players at once. */ + aboutSimul: string; + /** Out of 50 opponents, Fischer won 47 games, drew 2 and lost 1. */ + aboutSimulImage: string; + /** The concept is taken from real world events. In real life, this involves the simul host moving from table to table to play a single move. */ + aboutSimulRealLife: string; + /** When the simul starts, every player starts a game with the host. The simul ends when all games are complete. */ + aboutSimulRules: string; + /** Simuls are always casual. Rematches, takebacks and adding time are disabled. */ + aboutSimulSettings: string; + /** About %s */ + aboutX: I18nFormat; + /** Accept */ + accept: string; + /** You can login right now as %s. */ + accountCanLogin: I18nFormat; + /** The account %s is closed. */ + accountClosed: I18nFormat; + /** You do not need a confirmation email. */ + accountConfirmationEmailNotNeeded: string; + /** The user %s is successfully confirmed. */ + accountConfirmed: I18nFormat; + /** The account %s was registered without an email. */ + accountRegisteredWithoutEmail: I18nFormat; + /** Accuracy */ + accuracy: string; + /** Active players */ + activePlayers: string; + /** Add current variation */ + addCurrentVariation: string; + /** Advanced settings */ + advancedSettings: string; + /** Advantage */ + advantage: string; + /** I agree that I will at no time receive assistance during my games (from a chess computer, book, database or another person). */ + agreementAssistance: string; + /** I agree that I will not create multiple accounts (except for the reasons stated in the %s). */ + agreementMultipleAccounts: I18nFormat; + /** I agree that I will always be respectful to other players. */ + agreementNice: string; + /** I agree that I will follow all Lichess policies. */ + agreementPolicy: string; + /** %1$s level %2$s */ + aiNameLevelAiLevel: I18nFormat; + /** All information is public and optional. */ + allInformationIsPublicAndOptional: string; + /** All set! */ + allSet: string; + /** All squares of the board */ + allSquaresOfTheBoard: string; + /** Always */ + always: string; + /** Analysis board */ + analysis: string; + /** Analysis options */ + analysisOptions: string; + /** Press shift+click or right-click to draw circles and arrows on the board. */ + analysisShapesHowTo: string; + /** and save %s premove lines */ + andSaveNbPremoveLines: I18nPlural; + /** Anonymous */ + anonymous: string; + /** Another was %s */ + anotherWasX: I18nFormat; + /** Submit */ + apply: string; + /** as black */ + asBlack: string; + /** As free as Lichess */ + asFreeAsLichess: string; + /** Your account is managed. Ask your chess teacher about lifting kid mode. */ + askYourChessTeacherAboutLiftingKidMode: string; + /** as white */ + asWhite: string; + /** Automatically proceed to next game after moving */ + automaticallyProceedToNextGameAfterMoving: string; + /** Auto switch */ + autoSwitch: string; + /** Available in %s languages! */ + availableInNbLanguages: I18nPlural; + /** Average centipawn loss */ + averageCentipawnLoss: string; + /** Average rating */ + averageElo: string; + /** Average opponent */ + averageOpponent: string; + /** Average rating: %s */ + averageRatingX: I18nFormat; + /** Background */ + background: string; + /** Background image URL: */ + backgroundImageUrl: string; + /** Back to game */ + backToGame: string; + /** Back to tournament */ + backToTournament: string; + /** Berserk rate */ + berserkRate: string; + /** Best move arrow */ + bestMoveArrow: string; + /** Best was %s */ + bestWasX: I18nFormat; + /** Better than %1$s of %2$s players */ + betterThanPercentPlayers: I18nFormat; + /** Beware, the game is rated but has no clock! */ + bewareTheGameIsRatedButHasNoClock: string; + /** Biography */ + biography: string; + /** Talk about yourself, your interests, what you like in chess, your favourite openings, players, ... */ + biographyDescription: string; + /** Black */ + black: string; + /** Black O-O */ + blackCastlingKingside: string; + /** Black to checkmate in one move */ + blackCheckmatesInOneMove: string; + /** Black declines draw */ + blackDeclinesDraw: string; + /** Black didn't move */ + blackDidntMove: string; + /** Black is victorious */ + blackIsVictorious: string; + /** Black left the game */ + blackLeftTheGame: string; + /** Black offers draw */ + blackOffersDraw: string; + /** Black to play */ + blackPlays: string; + /** Black resigned */ + blackResigned: string; + /** Black time out */ + blackTimeOut: string; + /** Black wins */ + blackWins: string; + /** Black wins */ + blackWinsGame: string; + /** You have used the same password on another site, and that site has been compromised. To ensure the safety of your Lichess account, we need you to set a new password. Thank you for your understanding. */ + blankedPassword: string; + /** Blitz */ + blitz: string; + /** Fast games: 3 to 8 minutes */ + blitzDesc: string; + /** Block */ + block: string; + /** Blocked */ + blocked: string; + /** %s blocks */ + blocks: I18nPlural; + /** Blog */ + blog: string; + /** Blunder */ + blunder: string; + /** Board */ + board: string; + /** Board editor */ + boardEditor: string; + /** Reset colours to default */ + boardReset: string; + /** Bookmark this game */ + bookmarkThisGame: string; + /** Brightness */ + brightness: string; + /** Built for the love of chess, not money */ + builtForTheLoveOfChessNotMoney: string; + /** Bullet */ + bullet: string; + /** Bullet, blitz, classical */ + bulletBlitzClassical: string; + /** Very fast games: less than 3 minutes */ + bulletDesc: string; + /** by %s */ + by: I18nFormat; + /** By CPL */ + byCPL: string; + /** By registering, you agree to the %s. */ + byRegisteringYouAgreeToBeBoundByOur: I18nFormat; + /** Calculating moves... */ + calculatingMoves: string; + /** Cancel */ + cancel: string; + /** Cancel rematch offer */ + cancelRematchOffer: string; + /** Cancel the simul */ + cancelSimul: string; + /** Cancel the tournament */ + cancelTournament: string; + /** If you close your account a second time, there will be no way of recovering it. */ + cantDoThisTwice: string; + /** Please solve the chess captcha. */ + 'captcha.fail': string; + /** Capture */ + capture: string; + /** Castling */ + castling: string; + /** Casual */ + casual: string; + /** Casual */ + casualTournament: string; + /** Change email */ + changeEmail: string; + /** Change password */ + changePassword: string; + /** Change username */ + changeUsername: string; + /** Change your username. This can only be done once and you are only allowed to change the case of the letters in your username. */ + changeUsernameDescription: string; + /** Only the case of the letters can change. For example "johndoe" to "JohnDoe". */ + changeUsernameNotSame: string; + /** Chat */ + chat: string; + /** Chat room */ + chatRoom: string; + /** Cheat */ + cheat: string; + /** Cheat Detected */ + cheatDetected: string; + /** Checkmate */ + checkmate: string; + /** Also check your spam folder, it might end up there. If so, mark it as not spam. */ + checkSpamFolder: string; + /** Check your Email */ + checkYourEmail: string; + /** Chess960 start position: %s */ + chess960StartPosition: I18nFormat; + /** Chess basics */ + chessBasics: string; + /** Claim a draw */ + claimADraw: string; + /** Classical */ + classical: string; + /** Classical games: 25 minutes and more */ + classicalDesc: string; + /** Clear board */ + clearBoard: string; + /** Clear moves */ + clearSavedMoves: string; + /** Click here to read it */ + clickHereToReadIt: string; + /** Click on the board to make your move, and prove you are human. */ + clickOnTheBoardToMakeYourMove: string; + /** [Click to reveal email address] */ + clickToRevealEmailAddress: string; + /** Clock */ + clock: string; + /** Clock increment */ + clockIncrement: string; + /** Clock initial time */ + clockInitialTime: string; + /** Close */ + close: string; + /** If you closed your account, but have since changed your mind, you get one chance of getting your account back. */ + closedAccountChangedMind: string; + /** Closing your account will withdraw your appeal */ + closingAccountWithdrawAppeal: string; + /** Cloud analysis */ + cloudAnalysis: string; + /** Coaches */ + coaches: string; + /** Coach manager */ + coachManager: string; + /** Collapse variations */ + collapseVariations: string; + /** Community */ + community: string; + /** Compose message */ + composeMessage: string; + /** Computer */ + computer: string; + /** Computer analysis */ + computerAnalysis: string; + /** Computer analysis available */ + computerAnalysisAvailable: string; + /** Computer analysis disabled */ + computerAnalysisDisabled: string; + /** Computers and computer-assisted players are not allowed to play. Please do not get assistance from chess engines, databases, or from other players while playing. Also note that making multiple accounts is strongly discouraged and excessive multi-accounting will lead to being banned. */ + computersAreNotAllowedToPlay: string; + /** Computer thinking ... */ + computerThinking: string; + /** Conditional premoves */ + conditionalPremoves: string; + /** Entry requirements: */ + conditionOfEntry: string; + /** Confirm move */ + confirmMove: string; + /** Congratulations, you won! */ + congratsYouWon: string; + /** Continue from here */ + continueFromHere: string; + /** Contribute */ + contribute: string; + /** Copy and paste the above text and send it to %s */ + copyTextToEmail: I18nFormat; + /** Copy variation PGN */ + copyVariationPgn: string; + /** Correspondence */ + correspondence: string; + /** Correspondence chess */ + correspondenceChess: string; + /** Correspondence games: one or several days per move */ + correspondenceDesc: string; + /** Country or region */ + countryRegion: string; + /** CPUs */ + cpus: string; + /** Create */ + create: string; + /** Create a game */ + createAGame: string; + /** Create a new topic */ + createANewTopic: string; + /** Create a new tournament */ + createANewTournament: string; + /** Created by */ + createdBy: string; + /** Newly created simuls */ + createdSimuls: string; + /** Create the topic */ + createTheTopic: string; + /** Crosstable */ + crosstable: string; + /** Cumulative */ + cumulative: string; + /** Current games */ + currentGames: string; + /** Current match score */ + currentMatchScore: string; + /** Current password */ + currentPassword: string; + /** Custom */ + custom: string; + /** Custom position */ + customPosition: string; + /** Cycle previous/next variation */ + cyclePreviousOrNextVariation: string; + /** Dark */ + dark: string; + /** Database */ + database: string; + /** Days per turn */ + daysPerTurn: string; + /** Decline */ + decline: string; + /** Defeat */ + defeat: string; + /** %1$s vs %2$s in %3$s */ + defeatVsYInZ: I18nFormat; + /** Delete */ + delete: string; + /** Delete from here */ + deleteFromHere: string; + /** Delete this imported game? */ + deleteThisImportedGame: string; + /** Depth %s */ + depthX: I18nFormat; + /** Private description */ + descPrivate: string; + /** Text that only the team members will see. If set, replaces the public description for team members. */ + descPrivateHelp: string; + /** Description */ + description: string; + /** Device theme */ + deviceTheme: string; + /** Disable Kid mode */ + disableKidMode: string; + /** Conversations */ + discussions: string; + /** Do it again */ + doItAgain: string; + /** Done reviewing black mistakes */ + doneReviewingBlackMistakes: string; + /** Done reviewing white mistakes */ + doneReviewingWhiteMistakes: string; + /** Download */ + download: string; + /** Download annotated */ + downloadAnnotated: string; + /** Download imported */ + downloadImported: string; + /** Download raw */ + downloadRaw: string; + /** Draw */ + draw: string; + /** The game has been drawn by the fifty move rule. */ + drawByFiftyMoves: string; + /** Draw by mutual agreement */ + drawByMutualAgreement: string; + /** Drawn */ + drawn: string; + /** Draw offer accepted */ + drawOfferAccepted: string; + /** Draw offer cancelled */ + drawOfferCanceled: string; + /** Draw offer sent */ + drawOfferSent: string; + /** Draw rate */ + drawRate: string; + /** Draws */ + draws: string; + /** %1$s vs %2$s in %3$s */ + drawVsYInZ: I18nFormat; + /** DTZ50'' with rounding, based on number of half-moves until next capture or pawn move */ + dtzWithRounding: string; + /** Duration */ + duration: string; + /** Edit */ + edit: string; + /** Edit profile */ + editProfile: string; + /** Email */ + email: string; + /** Email address associated to the account */ + emailAssociatedToaccount: string; + /** It can take some time to arrive. */ + emailCanTakeSomeTime: string; + /** Help with email confirmation */ + emailConfirmHelp: string; + /** Didn't receive your confirmation email after signing up? */ + emailConfirmNotReceived: string; + /** If everything else fails, then send us this email: */ + emailForSignupHelp: string; + /** Email me a link */ + emailMeALink: string; + /** We have sent an email to %s. */ + emailSent: I18nFormat; + /** Do not set an email address suggested by someone else. They will use it to steal your account. */ + emailSuggestion: string; + /** Embed in your website */ + embedInYourWebsite: string; + /** Paste a game URL or a study chapter URL to embed it. */ + embedsAvailable: string; + /** Leave empty to name the tournament after a notable chess player. */ + emptyTournamentName: string; + /** Enable */ + enable: string; + /** Enable Kid mode */ + enableKidMode: string; + /** Endgame */ + endgame: string; + /** Endgame positions */ + endgamePositions: string; + /** Error loading engine */ + engineFailed: string; + /** Engine manager */ + engineManager: string; + /** This email address is invalid */ + 'error.email': string; + /** This email address is not acceptable. Please double-check it, and try again. */ + 'error.email_acceptable': string; + /** This is already your email address */ + 'error.email_different': string; + /** Email address invalid or already taken */ + 'error.email_unique': string; + /** Must be at most %s */ + 'error.max': I18nFormat; + /** Must be at most %s characters long */ + 'error.maxLength': I18nFormat; + /** Must be at least %s */ + 'error.min': I18nFormat; + /** Must be at least %s characters long */ + 'error.minLength': I18nFormat; + /** Please don't use your username as your password. */ + 'error.namePassword': string; + /** Please provide at least one link to a cheated game. */ + 'error.provideOneCheatedGameLink': string; + /** This field is required */ + 'error.required': string; + /** Invalid value */ + 'error.unknown': string; + /** This password is extremely common, and too easy to guess. */ + 'error.weakPassword': string; + /** Estimated start time */ + estimatedStart: string; + /** Evaluating your move ... */ + evaluatingYourMove: string; + /** Evaluation gauge */ + evaluationGauge: string; + /** Playing now */ + eventInProgress: string; + /** Everybody gets all features for free */ + everybodyGetsAllFeaturesForFree: string; + /** Expand variations */ + expandVariations: string; + /** Export games */ + exportGames: string; + /** Fast */ + fast: string; + /** Favourite opponents */ + favoriteOpponents: string; + /** Fifty moves without progress */ + fiftyMovesWithoutProgress: string; + /** Filter games */ + filterGames: string; + /** Find a better move for black */ + findBetterMoveForBlack: string; + /** Find a better move for white */ + findBetterMoveForWhite: string; + /** Finished */ + finished: string; + /** Flair */ + flair: string; + /** Flip board */ + flipBoard: string; + /** Focus chat */ + focusChat: string; + /** Follow */ + follow: string; + /** Follow and challenge friends */ + followAndChallengeFriends: string; + /** Following */ + following: string; + /** Follows you */ + followsYou: string; + /** Follow %s */ + followX: I18nFormat; + /** Call draw */ + forceDraw: string; + /** Claim victory */ + forceResignation: string; + /** Force variation */ + forceVariation: string; + /** Forgot password? */ + forgotPassword: string; + /** Forum */ + forum: string; + /** Free Online Chess */ + freeOnlineChess: string; + /** Friends */ + friends: string; + /** From position */ + fromPosition: string; + /** Full featured */ + fullFeatured: string; + /** Game aborted */ + gameAborted: string; + /** Game analysis */ + gameAnalysis: string; + /** Game as GIF */ + gameAsGIF: string; + /** You have a game in progress with %s. */ + gameInProgress: I18nFormat; + /** Game Over */ + gameOver: string; + /** Games */ + games: string; + /** Games played */ + gamesPlayed: string; + /** Game vs %1$s */ + gameVsX: I18nFormat; + /** Get a hint */ + getAHint: string; + /** Give %s seconds */ + giveNbSeconds: I18nPlural; + /** Glicko-2 rating */ + glicko2Rating: string; + /** Go deeper */ + goDeeper: string; + /** To that effect, we must ensure that all players follow good practice. */ + goodPractice: string; + /** Graph */ + graph: string; + /** Hang on! */ + hangOn: string; + /** Help: */ + help: string; + /** Hide best move */ + hideBestMove: string; + /** Host */ + host: string; + /** Host a new simul */ + hostANewSimul: string; + /** Host colour: %s */ + hostColorX: I18nFormat; + /** How to avoid this? */ + howToAvoidThis: string; + /** Hue */ + hue: string; + /** Human */ + human: string; + /** If none, leave empty */ + ifNoneLeaveEmpty: string; + /** If rating is ± %s */ + ifRatingIsPlusMinusX: I18nFormat; + /** If registered */ + ifRegistered: string; + /** If you don't see the email, check other places it might be, like your junk, spam, social, or other folders. */ + ifYouDoNotSeeTheEmailCheckOtherPlaces: string; + /** Important */ + important: string; + /** Imported by %s */ + importedByX: I18nFormat; + /** Import game */ + importGame: string; + /** Variations will be erased. To keep them, import the PGN via a study. */ + importGameCaveat: string; + /** This PGN can be accessed by the public. To import a game privately, use a study. */ + importGameDataPrivacyWarning: string; + /** Paste a game PGN to get a browsable replay, computer analysis, game chat and public shareable URL. */ + importGameExplanation: string; + /** Import PGN */ + importPgn: string; + /** Inaccuracy */ + inaccuracy: string; + /** Anything even slightly inappropriate could get your account closed. */ + inappropriateNameWarning: string; + /** Inbox */ + inbox: string; + /** Incorrect password */ + incorrectPassword: string; + /** Increment */ + increment: string; + /** Increment in seconds */ + incrementInSeconds: string; + /** Infinite analysis */ + infiniteAnalysis: string; + /** In kid mode, the Lichess logo gets a %s icon, so you know your kids are safe. */ + inKidModeTheLichessLogoGetsIconX: I18nFormat; + /** Inline notation */ + inlineNotation: string; + /** in local browser */ + inLocalBrowser: string; + /** Inside the board */ + insideTheBoard: string; + /** Instructions */ + instructions: string; + /** Insufficient material */ + insufficientMaterial: string; + /** in the FAQ */ + inTheFAQ: string; + /** Invalid authentication code */ + invalidAuthenticationCode: string; + /** Invalid FEN */ + invalidFen: string; + /** Invalid PGN */ + invalidPgn: string; + /** Invalid username or password */ + invalidUsernameOrPassword: string; + /** invited you to "%1$s". */ + invitedYouToX: I18nFormat; + /** In your own local timezone */ + inYourLocalTimezone: string; + /** Private */ + isPrivate: string; + /** It's your turn! */ + itsYourTurn: string; + /** Join */ + join: string; + /** Join the game */ + joinTheGame: string; + /** Join the %1$s, to post in this forum */ + joinTheTeamXToPost: I18nFormat; + /** Keyboard shortcuts */ + keyboardShortcuts: string; + /** Cycle selected variation */ + keyCycleSelectedVariation: string; + /** enter/exit variation */ + keyEnterOrExitVariation: string; + /** go to start/end */ + keyGoToStartOrEnd: string; + /** move backward/forward */ + keyMoveBackwardOrForward: string; + /** Next blunder */ + keyNextBlunder: string; + /** Next branch */ + keyNextBranch: string; + /** Next inaccuracy */ + keyNextInaccuracy: string; + /** Next (Learn from your mistakes) */ + keyNextLearnFromYourMistakes: string; + /** Next mistake */ + keyNextMistake: string; + /** Previous branch */ + keyPreviousBranch: string; + /** Request computer analysis, Learn from your mistakes */ + keyRequestComputerAnalysis: string; + /** show/hide comments */ + keyShowOrHideComments: string; + /** Kid mode */ + kidMode: string; + /** This is about safety. In kid mode, all site communications are disabled. Enable this for your children and school students, to protect them from other internet users. */ + kidModeExplanation: string; + /** Kid mode is enabled. */ + kidModeIsEnabled: string; + /** King in the centre */ + kingInTheCenter: string; + /** Language */ + language: string; + /** Last post */ + lastPost: string; + /** Active %s */ + lastSeenActive: I18nFormat; + /** Latest forum posts */ + latestForumPosts: string; + /** Leaderboard */ + leaderboard: string; + /** Learn from this mistake */ + learnFromThisMistake: string; + /** Learn from your mistakes */ + learnFromYourMistakes: string; + /** Learn */ + learnMenu: string; + /** Less than %s minutes */ + lessThanNbMinutes: I18nPlural; + /** Let other players challenge you */ + letOtherPlayersChallengeYou: string; + /** Let other players follow you */ + letOtherPlayersFollowYou: string; + /** Let other players invite you to study */ + letOtherPlayersInviteYouToStudy: string; + /** Let other players message you */ + letOtherPlayersMessageYou: string; + /** Level */ + level: string; + /** Rated games played on Lichess */ + lichessDbExplanation: string; + /** Lichess is a charity and entirely free/libre open source software. */ + lichessPatronInfo: string; + /** Lichess tournaments */ + lichessTournaments: string; + /** Lifetime score */ + lifetimeScore: string; + /** Light */ + light: string; + /** List */ + list: string; + /** List players you have blocked */ + listBlockedPlayers: string; + /** Loading engine... */ + loadingEngine: string; + /** Load position */ + loadPosition: string; + /** Lobby */ + lobby: string; + /** Location */ + location: string; + /** Sign in to chat */ + loginToChat: string; + /** Sign out */ + logOut: string; + /** Losing */ + losing: string; + /** Losses */ + losses: string; + /** Loss or 50 moves by prior mistake */ + lossOr50MovesByPriorMistake: string; + /** Loss prevented by 50-move rule */ + lossSavedBy50MoveRule: string; + /** You lost rating points to someone who violated the Lichess TOS */ + lostAgainstTOSViolator: string; + /** For safekeeping and sharing, consider making a study. */ + makeAStudy: string; + /** Make mainline */ + makeMainLine: string; + /** Make the tournament private, and restrict access with a password */ + makePrivateTournament: string; + /** Make sure to read %1$s */ + makeSureToRead: I18nFormat; + /** %s is available for more advanced syntax. */ + markdownAvailable: I18nFormat; + /** OTB games of %1$s+ FIDE-rated players from %2$s to %3$s */ + masterDbExplanation: I18nFormat; + /** Mate in %s half-moves */ + mateInXHalfMoves: I18nPlural; + /** Max depth reached! */ + maxDepthReached: string; + /** Maximum: %s characters. */ + maximumNbCharacters: I18nPlural; + /** Maximum weekly rating */ + maximumWeeklyRating: string; + /** Maybe include more games from the preferences menu? */ + maybeIncludeMoreGamesFromThePreferencesMenu: string; + /** Member since */ + memberSince: string; + /** Memory */ + memory: string; + /** mentioned you in "%1$s". */ + mentionedYouInX: I18nFormat; + /** Menu */ + menu: string; + /** Message */ + message: string; + /** Middlegame */ + middlegame: string; + /** Minimum rated games */ + minimumRatedGames: string; + /** Minimum rating */ + minimumRating: string; + /** Minutes per side */ + minutesPerSide: string; + /** Mistake */ + mistake: string; + /** Mobile */ + mobile: string; + /** Mobile App */ + mobileApp: string; + /** Mode */ + mode: string; + /** More */ + more: string; + /** ≥ %1$s %2$s rated games */ + moreThanNbPerfRatedGames: I18nPlural; + /** ≥ %s rated games */ + moreThanNbRatedGames: I18nPlural; + /** Mouse tricks */ + mouseTricks: string; + /** Move */ + move: string; + /** Moves played */ + movesPlayed: string; + /** Move times */ + moveTimes: string; + /** Multiple lines */ + multipleLines: string; + /** Must be in team %s */ + mustBeInTeam: I18nFormat; + /** Name */ + name: string; + /** Navigate the move tree */ + navigateMoveTree: string; + /** %s blunders */ + nbBlunders: I18nPlural; + /** %s bookmarks */ + nbBookmarks: I18nPlural; + /** %s days */ + nbDays: I18nPlural; + /** %s draws */ + nbDraws: I18nPlural; + /** %s followers */ + nbFollowers: I18nPlural; + /** %s following */ + nbFollowing: I18nPlural; + /** %s forum posts */ + nbForumPosts: I18nPlural; + /** %s friends online */ + nbFriendsOnline: I18nPlural; + /** %s games */ + nbGames: I18nPlural; + /** %s games in play */ + nbGamesInPlay: I18nPlural; + /** %s games with you */ + nbGamesWithYou: I18nPlural; + /** %s hours */ + nbHours: I18nPlural; + /** %s imported games */ + nbImportedGames: I18nPlural; + /** %s inaccuracies */ + nbInaccuracies: I18nPlural; + /** %s losses */ + nbLosses: I18nPlural; + /** %s minutes */ + nbMinutes: I18nPlural; + /** %s mistakes */ + nbMistakes: I18nPlural; + /** %1$s %2$s players this week. */ + nbPerfTypePlayersThisWeek: I18nPlural; + /** %s players */ + nbPlayers: I18nPlural; + /** %s playing */ + nbPlaying: I18nPlural; + /** %s puzzles */ + nbPuzzles: I18nPlural; + /** %s rated */ + nbRated: I18nPlural; + /** %s seconds */ + nbSeconds: I18nPlural; + /** %s seconds to play the first move */ + nbSecondsToPlayTheFirstMove: I18nPlural; + /** %s simuls */ + nbSimuls: I18nPlural; + /** %s studies */ + nbStudies: I18nPlural; + /** %s tournament points */ + nbTournamentPoints: I18nPlural; + /** %s wins */ + nbWins: I18nPlural; + /** You need to play %s more rated games */ + needNbMoreGames: I18nPlural; + /** You need to play %1$s more %2$s rated games */ + needNbMorePerfGames: I18nPlural; + /** Network lag between you and Lichess */ + networkLagBetweenYouAndLichess: string; + /** Never */ + never: string; + /** Never type your Lichess password on another site! */ + neverTypeYourPassword: string; + /** New opponent */ + newOpponent: string; + /** New password */ + newPassword: string; + /** New password (again) */ + newPasswordAgain: string; + /** The new passwords don't match */ + newPasswordsDontMatch: string; + /** Password strength */ + newPasswordStrength: string; + /** New tournament */ + newTournament: string; + /** Next */ + next: string; + /** Next %s tournament: */ + nextXTournament: I18nFormat; + /** No */ + no: string; + /** No chat */ + noChat: string; + /** No conditional premoves */ + noConditionalPremoves: string; + /** You cannot draw before 30 moves are played in a Swiss tournament. */ + noDrawBeforeSwissLimit: string; + /** No game found */ + noGameFound: string; + /** No mistakes found for black */ + noMistakesFoundForBlack: string; + /** No mistakes found for white */ + noMistakesFoundForWhite: string; + /** None */ + none: string; + /** Offline */ + noNetwork: string; + /** No note yet */ + noNoteYet: string; + /** No restriction */ + noRestriction: string; + /** Normal */ + normal: string; + /** This simultaneous exhibition does not exist. */ + noSimulExplanation: string; + /** Simul not found */ + noSimulFound: string; + /** Not a checkmate */ + notACheckmate: string; + /** Notes */ + notes: string; + /** Nothing to see here at the moment. */ + nothingToSeeHere: string; + /** Notifications */ + notifications: string; + /** Notifications: %1$s */ + notificationsX: I18nFormat; + /** Offer draw */ + offerDraw: string; + /** One day */ + oneDay: string; + /** One URL per line. */ + oneUrlPerLine: string; + /** Online and offline play */ + onlineAndOfflinePlay: string; + /** Online bots */ + onlineBots: string; + /** Online players */ + onlinePlayers: string; + /** Only existing conversations */ + onlyExistingConversations: string; + /** Only friends */ + onlyFriends: string; + /** Only members of team */ + onlyMembersOfTeam: string; + /** Only team leaders */ + onlyTeamLeaders: string; + /** Only team members */ + onlyTeamMembers: string; + /** This will only work once. */ + onlyWorksOnce: string; + /** On slow games */ + onSlowGames: string; + /** Opacity */ + opacity: string; + /** Opening */ + opening: string; + /** Opening/endgame explorer */ + openingEndgameExplorer: string; + /** Opening explorer */ + openingExplorer: string; + /** Opening explorer & tablebase */ + openingExplorerAndTablebase: string; + /** Openings */ + openings: string; + /** Open study */ + openStudy: string; + /** Open tournaments */ + openTournaments: string; + /** Opponent */ + opponent: string; + /** Your opponent left the game. You can claim victory, call the game a draw, or wait. */ + opponentLeftChoices: string; + /** Your opponent left the game. You can claim victory in %s seconds. */ + opponentLeftCounter: I18nPlural; + /** Or let your opponent scan this QR code */ + orLetYourOpponentScanQrCode: string; + /** Or upload a PGN file */ + orUploadPgnFile: string; + /** Other */ + other: string; + /** other players */ + otherPlayers: string; + /** Our tips for organising events */ + ourEventTips: string; + /** Outside the board */ + outsideTheBoard: string; + /** Password */ + password: string; + /** Password reset */ + passwordReset: string; + /** Do not set a password suggested by someone else. They will use it to steal your account. */ + passwordSuggestion: string; + /** Paste the FEN text here */ + pasteTheFenStringHere: string; + /** Paste the PGN text here */ + pasteThePgnStringHere: string; + /** Pause */ + pause: string; + /** Pawn move */ + pawnMove: string; + /** Performance */ + performance: string; + /** Rating: %s */ + perfRatingX: I18nFormat; + /** Phone and tablet */ + phoneAndTablet: string; + /** Piece set */ + pieceSet: string; + /** Play */ + play: string; + /** Play chess everywhere */ + playChessEverywhere: string; + /** Play chess in style */ + playChessInStyle: string; + /** Play best computer move */ + playComputerMove: string; + /** Player */ + player: string; + /** Players */ + players: string; + /** Play every game you start. */ + playEveryGame: string; + /** Play first opening/endgame-explorer move */ + playFirstOpeningEndgameExplorerMove: string; + /** Playing right now */ + playingRightNow: string; + /** play selected move */ + playSelectedMove: string; + /** Play a variation to create conditional premoves */ + playVariationToCreateConditionalPremoves: string; + /** Play with a friend */ + playWithAFriend: string; + /** Play with the computer */ + playWithTheMachine: string; + /** Play %s */ + playX: I18nFormat; + /** We aim to provide a pleasant chess experience for everyone. */ + pleasantChessExperience: string; + /** Points */ + points: string; + /** Popular openings */ + popularOpenings: string; + /** Paste a valid FEN to start every game from a given position. */ + positionInputHelp: I18nFormat; + /** Posts */ + posts: string; + /** When a potential problem is detected, we display this message. */ + potentialProblem: string; + /** Practice */ + practice: string; + /** Practice with computer */ + practiceWithComputer: string; + /** Previously on Lichess TV */ + previouslyOnLichessTV: string; + /** Privacy */ + privacy: string; + /** Privacy policy */ + privacyPolicy: string; + /** Proceed to %s */ + proceedToX: I18nFormat; + /** Profile */ + profile: string; + /** Profile completion: %s */ + profileCompletion: I18nFormat; + /** Promote variation */ + promoteVariation: string; + /** Propose a takeback */ + proposeATakeback: string; + /** Chess tactics trainer */ + puzzleDesc: string; + /** Puzzles */ + puzzles: string; + /** Quick pairing */ + quickPairing: string; + /** Race finished */ + raceFinished: string; + /** Random side */ + randomColor: string; + /** Rank */ + rank: string; + /** Rank is updated every %s minutes */ + rankIsUpdatedEveryNbMinutes: I18nPlural; + /** Rank: %s */ + rankX: I18nFormat; + /** Rapid */ + rapid: string; + /** Rapid games: 8 to 25 minutes */ + rapidDesc: string; + /** Rated */ + rated: string; + /** Games are rated and impact players ratings */ + ratedFormHelp: string; + /** Rated ≤ %1$s in %2$s for the last week */ + ratedLessThanInPerf: I18nFormat; + /** Rated ≥ %1$s in %2$s */ + ratedMoreThanInPerf: I18nFormat; + /** Rated */ + ratedTournament: string; + /** Rating */ + rating: string; + /** Rating range */ + ratingRange: string; + /** Rating stats */ + ratingStats: string; + /** %1$s rating over %2$s games */ + ratingXOverYGames: I18nPlural; + /** Read about our %s. */ + readAboutOur: I18nFormat; + /** really */ + really: string; + /** Real name */ + realName: string; + /** Real time */ + realTime: string; + /** Realtime */ + realtimeReplay: string; + /** Reason */ + reason: string; + /** Receive notifications when mentioned in the forum */ + receiveForumNotifications: string; + /** Recent games */ + recentGames: string; + /** Reconnecting */ + reconnecting: string; + /** Wait 5 minutes and refresh your email inbox. */ + refreshInboxAfterFiveMinutes: string; + /** Refund: %1$s %2$s rating points. */ + refundXpointsTimeControlY: I18nFormat; + /** Rematch */ + rematch: string; + /** Rematch offer accepted */ + rematchOfferAccepted: string; + /** Rematch offer cancelled */ + rematchOfferCanceled: string; + /** Rematch offer declined */ + rematchOfferDeclined: string; + /** Rematch offer sent */ + rematchOfferSent: string; + /** Keep me logged in */ + rememberMe: string; + /** Removes the depth limit, and keeps your computer warm */ + removesTheDepthLimit: string; + /** Reopen your account */ + reopenYourAccount: string; + /** Replay mode */ + replayMode: string; + /** Replies */ + replies: string; + /** Reply */ + reply: string; + /** Reply to this topic */ + replyToThisTopic: string; + /** Report a user */ + reportAUser: string; + /** Paste the link to the game(s) and explain what is wrong about this user's behaviour. Don't just say "they cheat", but tell us how you came to this conclusion. */ + reportCheatBoostHelp: string; + /** Your report will be processed faster if written in English. */ + reportProcessedFasterInEnglish: string; + /** Explain what about this username is offensive. Don't just say "it's offensive/inappropriate", but tell us how you came to this conclusion, especially if the insult is obfuscated, not in english, is in slang, or is a historical/cultural reference. */ + reportUsernameHelp: string; + /** Report %s to moderators */ + reportXToModerators: I18nFormat; + /** Request a computer analysis */ + requestAComputerAnalysis: string; + /** Required. */ + required: string; + /** Reset */ + reset: string; + /** Resign */ + resign: string; + /** Resign lost games (don't let the clock run down). */ + resignLostGames: string; + /** Resign the game */ + resignTheGame: string; + /** Resume */ + resume: string; + /** Resume learning */ + resumeLearning: string; + /** Resume practice */ + resumePractice: string; + /** %1$s vs %2$s */ + resVsX: I18nFormat; + /** Retry */ + retry: string; + /** Return to simul homepage */ + returnToSimulHomepage: string; + /** Return to tournaments homepage */ + returnToTournamentsHomepage: string; + /** Review black mistakes */ + reviewBlackMistakes: string; + /** Review white mistakes */ + reviewWhiteMistakes: string; + /** revoke all sessions */ + revokeAllSessions: string; + /** Pick a very safe name for the tournament. */ + safeTournamentName: string; + /** Save */ + save: string; + /** Screenshot current position */ + screenshotCurrentPosition: string; + /** Scroll over computer variations to preview them. */ + scrollOverComputerVariationsToPreviewThem: string; + /** Search or start new conversation */ + searchOrStartNewDiscussion: string; + /** Security */ + security: string; + /** See best move */ + seeBestMove: string; + /** Send */ + send: string; + /** We've sent you an email with a link. */ + sentEmailWithLink: string; + /** Sessions */ + sessions: string; + /** Set your flair */ + setFlair: string; + /** Set the board */ + setTheBoard: string; + /** Share your chess insights data */ + shareYourInsightsData: string; + /** Show this help dialog */ + showHelpDialog: string; + /** Show me everything */ + showMeEverything: string; + /** Show threat */ + showThreat: string; + /** You have received a private message from Lichess. */ + showUnreadLichessMessage: string; + /** Show variation arrows */ + showVariationArrows: string; + /** Side */ + side: string; + /** Sign in */ + signIn: string; + /** Register */ + signUp: string; + /** We will only use it for password reset. */ + signupEmailHint: string; + /** Sign up to host or join a simul */ + signUpToHostOrJoinASimul: string; + /** Make sure to choose a family-friendly username. You cannot change it later and any accounts with inappropriate usernames will get closed! */ + signupUsernameHint: string; + /** You may add extra initial time to your clock to help you cope with the simul. */ + simulAddExtraTime: string; + /** Add initial time to your clock for each player joining the simul. */ + simulAddExtraTimePerPlayer: string; + /** Fischer Clock setup. The more players you take on, the more time you may need. */ + simulClockHint: string; + /** Simul description */ + simulDescription: string; + /** Anything you want to tell the participants? */ + simulDescriptionHelp: string; + /** Feature on %s */ + simulFeatured: I18nFormat; + /** Show your simul to everyone on %s. Disable for private simuls. */ + simulFeaturedHelp: I18nFormat; + /** Host colour for each game */ + simulHostcolor: string; + /** Host extra initial clock time */ + simulHostExtraTime: string; + /** Host extra clock time per player */ + simulHostExtraTimePerPlayer: string; + /** Simultaneous exhibitions */ + simultaneousExhibitions: string; + /** If you select several variants, each player gets to choose which one to play. */ + simulVariantsHint: string; + /** Since */ + since: string; + /** Free online chess server. Play chess in a clean interface. No registration, no ads, no plugin required. Play chess with the computer, friends or random opponents. */ + siteDescription: string; + /** Size */ + size: string; + /** Skip this move */ + skipThisMove: string; + /** Slow */ + slow: string; + /** Social media links */ + socialMediaLinks: string; + /** Solution */ + solution: string; + /** Someone you reported was banned */ + someoneYouReportedWasBanned: string; + /** Sorry :( */ + sorry: string; + /** Sound */ + sound: string; + /** Source Code */ + sourceCode: string; + /** Spectator room */ + spectatorRoom: string; + /** Stalemate */ + stalemate: string; + /** Standard */ + standard: string; + /** Stand by %s, pairing players, get ready! */ + standByX: I18nFormat; + /** Standing */ + standing: string; + /** started streaming */ + startedStreaming: string; + /** Starting: */ + starting: string; + /** Starting position */ + startPosition: string; + /** Streamer manager */ + streamerManager: string; + /** Streamers */ + streamersMenu: string; + /** Strength */ + strength: string; + /** Study */ + studyMenu: string; + /** Subject */ + subject: string; + /** Subscribe */ + subscribe: string; + /** Success */ + success: string; + /** Switch sides */ + switchSides: string; + /** Takeback */ + takeback: string; + /** Takeback accepted */ + takebackPropositionAccepted: string; + /** Takeback cancelled */ + takebackPropositionCanceled: string; + /** Takeback declined */ + takebackPropositionDeclined: string; + /** Takeback sent */ + takebackPropositionSent: string; + /** Please be nice in the chat! */ + talkInChat: string; + /** %1$s team */ + teamNamedX: I18nFormat; + /** We apologise for the temporary inconvenience, */ + temporaryInconvenience: string; + /** Terms of Service */ + termsOfService: string; + /** Thank you! */ + thankYou: string; + /** Thank you for reading! */ + thankYouForReading: string; + /** The first person to come to this URL will play with you. */ + theFirstPersonToComeOnThisUrlWillPlayWithYou: string; + /** the forum etiquette */ + theForumEtiquette: string; + /** The game is a draw. */ + theGameIsADraw: string; + /** Thematic */ + thematic: string; + /** This account violated the Lichess Terms of Service */ + thisAccountViolatedTos: string; + /** This game is rated */ + thisGameIsRated: string; + /** This is a chess CAPTCHA. */ + thisIsAChessCaptcha: string; + /** This topic has been archived and can no longer be replied to. */ + thisTopicIsArchived: string; + /** This topic is now closed. */ + thisTopicIsNowClosed: string; + /** Three checks */ + threeChecks: string; + /** Threefold repetition */ + threefoldRepetition: string; + /** Time */ + time: string; + /** Time is almost up! */ + timeAlmostUp: string; + /** Time before tournament starts */ + timeBeforeTournamentStarts: string; + /** Time control */ + timeControl: string; + /** Timeline */ + timeline: string; + /** Time to process a move on Lichess's server */ + timeToProcessAMoveOnLichessServer: string; + /** Today */ + today: string; + /** Toggle all computer analysis */ + toggleAllAnalysis: string; + /** Toggle move annotations */ + toggleGlyphAnnotations: string; + /** Toggle local computer analysis */ + toggleLocalAnalysis: string; + /** Toggle local evaluation */ + toggleLocalEvaluation: string; + /** Toggle position annotations */ + togglePositionAnnotations: string; + /** Toggle the chat */ + toggleTheChat: string; + /** Toggle variation arrows */ + toggleVariationArrows: string; + /** To invite someone to play, give this URL */ + toInviteSomeoneToPlayGiveThisUrl: string; + /** Tools */ + tools: string; + /** Top games */ + topGames: string; + /** Topics */ + topics: string; + /** To report a user for cheating or bad behaviour, %1$s */ + toReportSomeoneForCheatingOrBadBehavior: I18nFormat; + /** To request support, %1$s */ + toRequestSupport: I18nFormat; + /** Study */ + toStudy: string; + /** Tournament */ + tournament: string; + /** Tournament calendar */ + tournamentCalendar: string; + /** Tournament complete */ + tournamentComplete: string; + /** This tournament does not exist. */ + tournamentDoesNotExist: string; + /** Tournament entry code */ + tournamentEntryCode: string; + /** Arena tournament FAQ */ + tournamentFAQ: string; + /** Play fast-paced chess tournaments! Join an official scheduled tournament, or create your own. Bullet, Blitz, Classical, Chess960, King of the Hill, Threecheck, and more options available for endless chess fun. */ + tournamentHomeDescription: string; + /** Chess tournaments featuring various time controls and variants */ + tournamentHomeTitle: string; + /** The tournament is starting */ + tournamentIsStarting: string; + /** The tournament may have been cancelled if all players left before it started. */ + tournamentMayHaveBeenCanceled: string; + /** Tournament not found */ + tournamentNotFound: string; + /** The tournament pairings are now closed. */ + tournamentPairingsAreNowClosed: string; + /** Tournament points */ + tournamentPoints: string; + /** Tournaments */ + tournaments: string; + /** Tournament chat */ + tournChat: string; + /** Tournament description */ + tournDescription: string; + /** Anything special you want to tell the participants? Try to keep it short. Markdown links are available: [name](https://url) */ + tournDescriptionHelp: string; + /** Time featured on TV: %s */ + tpTimeSpentOnTV: I18nFormat; + /** Time spent playing: %s */ + tpTimeSpentPlaying: I18nFormat; + /** Transparent */ + transparent: string; + /** Troll */ + troll: string; + /** Try another move for black */ + tryAnotherMoveForBlack: string; + /** Try another move for white */ + tryAnotherMoveForWhite: string; + /** try the contact page */ + tryTheContactPage: string; + /** Try to win (or at least draw) every game you play. */ + tryToWin: string; + /** Type private notes here */ + typePrivateNotesHere: string; + /** Insanely fast games: less than 30 seconds */ + ultraBulletDesc: string; + /** Unblock */ + unblock: string; + /** Unfollow */ + unfollow: string; + /** Unfollow %s */ + unfollowX: I18nFormat; + /** Unknown */ + unknown: string; + /** Win/loss only guaranteed if recommended tablebase line has been followed since the last capture or pawn move, due to possible rounding of DTZ values in Syzygy tablebases. */ + unknownDueToRounding: string; + /** Unlimited */ + unlimited: string; + /** Unsubscribe */ + unsubscribe: string; + /** Until */ + until: string; + /** User */ + user: string; + /** %1$s is better than %2$s of %3$s players. */ + userIsBetterThanPercentOfPerfTypePlayers: I18nFormat; + /** User name */ + username: string; + /** This username is already in use, please try another one. */ + usernameAlreadyUsed: string; + /** You can use this username to create a new account */ + usernameCanBeUsedForNewAccount: string; + /** The username must only contain letters, numbers, underscores, and hyphens. Consecutive underscores and hyphens are not allowed. */ + usernameCharsInvalid: string; + /** We couldn't find any user by this name: %s. */ + usernameNotFound: I18nFormat; + /** User name or email */ + usernameOrEmail: string; + /** The username must start with a letter. */ + usernamePrefixInvalid: string; + /** The username must end with a letter or a number. */ + usernameSuffixInvalid: string; + /** This username is not acceptable. */ + usernameUnacceptable: string; + /** use the report form */ + useTheReportForm: string; + /** Using server analysis */ + usingServerAnalysis: string; + /** Variant */ + variant: string; + /** Variant ending */ + variantEnding: string; + /** Variant loss */ + variantLoss: string; + /** Variants */ + variants: string; + /** Variant win */ + variantWin: string; + /** Variation arrows let you navigate without using the move list. */ + variationArrowsInfo: string; + /** Victory */ + victory: string; + /** %1$s vs %2$s in %3$s */ + victoryVsYInZ: I18nFormat; + /** Video library */ + videoLibrary: string; + /** View in full size */ + viewInFullSize: string; + /** View rematch */ + viewRematch: string; + /** Views */ + views: string; + /** View the solution */ + viewTheSolution: string; + /** View tournament */ + viewTournament: string; + /** We will come back to you shortly to help you complete your signup. */ + waitForSignupHelp: string; + /** Waiting */ + waiting: string; + /** Waiting for analysis */ + waitingForAnalysis: string; + /** Waiting for opponent */ + waitingForOpponent: string; + /** Watch */ + watch: string; + /** Watch games */ + watchGames: string; + /** Webmasters */ + webmasters: string; + /** Website */ + website: string; + /** Weekly %s rating distribution */ + weeklyPerfTypeRatingDistribution: I18nFormat; + /** We had to time you out for a while. */ + weHadToTimeYouOutForAWhile: string; + /** We've sent you an email. Click the link in the email to activate your account. */ + weHaveSentYouAnEmailClickTheLink: string; + /** We've sent an email to %s. Click the link in the email to reset your password. */ + weHaveSentYouAnEmailTo: I18nFormat; + /** What's the matter? */ + whatIsIheMatter: string; + /** What username did you use to sign up? */ + whatSignupUsername: string; + /** When you create a Simul, you get to play several players at once. */ + whenCreateSimul: string; + /** White */ + white: string; + /** White O-O */ + whiteCastlingKingside: string; + /** White to checkmate in one move */ + whiteCheckmatesInOneMove: string; + /** White declines draw */ + whiteDeclinesDraw: string; + /** White didn't move */ + whiteDidntMove: string; + /** White / Draw / Black */ + whiteDrawBlack: string; + /** White is victorious */ + whiteIsVictorious: string; + /** White left the game */ + whiteLeftTheGame: string; + /** White offers draw */ + whiteOffersDraw: string; + /** White to play */ + whitePlays: string; + /** White resigned */ + whiteResigned: string; + /** White time out */ + whiteTimeOut: string; + /** White wins */ + whiteWins: string; + /** White wins */ + whiteWinsGame: string; + /** Why? */ + why: string; + /** Winner */ + winner: string; + /** Winning */ + winning: string; + /** Win or 50 moves by prior mistake */ + winOr50MovesByPriorMistake: string; + /** Win prevented by 50-move rule */ + winPreventedBy50MoveRule: string; + /** Win rate */ + winRate: string; + /** Wins */ + wins: string; + /** and wish you great games on lichess.org. */ + wishYouGreatGames: string; + /** Withdraw */ + withdraw: string; + /** With everybody */ + withEverybody: string; + /** With friends */ + withFriends: string; + /** With nobody */ + withNobody: string; + /** Write a private note about this user */ + writeAPrivateNoteAboutThisUser: string; + /** %1$s competes in %2$s */ + xCompetesInY: I18nFormat; + /** %1$s created team %2$s */ + xCreatedTeamY: I18nFormat; + /** %1$s hosts %2$s */ + xHostsY: I18nFormat; + /** %1$s invited you to "%2$s". */ + xInvitedYouToY: I18nFormat; + /** %1$s is a free (%2$s), libre, no-ads, open source chess server. */ + xIsAFreeYLibreOpenSourceChessServer: I18nFormat; + /** %1$s joined team %2$s */ + xJoinedTeamY: I18nFormat; + /** %1$s joins %2$s */ + xJoinsY: I18nFormat; + /** %1$s likes %2$s */ + xLikesY: I18nFormat; + /** %1$s mentioned you in "%2$s". */ + xMentionedYouInY: I18nFormat; + /** %s opening explorer */ + xOpeningExplorer: I18nFormat; + /** %1$s posted in topic %2$s */ + xPostedInForumY: I18nFormat; + /** %s rating */ + xRating: I18nFormat; + /** %1$s started following %2$s */ + xStartedFollowingY: I18nFormat; + /** %s started streaming */ + xStartedStreaming: I18nFormat; + /** %s was played */ + xWasPlayed: I18nFormat; + /** Yes */ + yes: string; + /** Yesterday */ + yesterday: string; + /** You are better than %1$s of %2$s players. */ + youAreBetterThanPercentOfPerfTypePlayers: I18nFormat; + /** You are leaving Lichess */ + youAreLeavingLichess: string; + /** You are not in the team %s */ + youAreNotInTeam: I18nFormat; + /** You are now part of the team. */ + youAreNowPartOfTeam: string; + /** You are playing! */ + youArePlaying: string; + /** You browsed away */ + youBrowsedAway: string; + /** Scroll over the board to move in the game. */ + youCanAlsoScrollOverTheBoardToMoveInTheGame: string; + /** You can do better */ + youCanDoBetter: string; + /** There is a setting to hide all user flairs across the entire site. */ + youCanHideFlair: string; + /** You can't post in the forums yet. Play some games! */ + youCannotPostYetPlaySomeGames: string; + /** You can't start a new game until this one is finished. */ + youCantStartNewGame: string; + /** You do not have an established %s rating. */ + youDoNotHaveAnEstablishedPerfTypeRating: I18nFormat; + /** You have been timed out. */ + youHaveBeenTimedOut: string; + /** You have joined "%1$s". */ + youHaveJoinedTeamX: I18nFormat; + /** You need an account to do that */ + youNeedAnAccountToDoThat: string; + /** You play the black pieces */ + youPlayTheBlackPieces: string; + /** You play the white pieces */ + youPlayTheWhitePieces: string; + /** Your opponent offers a draw */ + yourOpponentOffersADraw: string; + /** Your opponent proposes a takeback */ + yourOpponentProposesATakeback: string; + /** Your opponent wants to play a new game with you */ + yourOpponentWantsToPlayANewGameWithYou: string; + /** Your pending simuls */ + yourPendingSimuls: string; + /** Your %s rating is provisional */ + yourPerfRatingIsProvisional: I18nFormat; + /** Your %1$s rating (%2$s) is too high */ + yourPerfRatingIsTooHigh: I18nFormat; + /** Your %1$s rating (%2$s) is too low */ + yourPerfRatingIsTooLow: I18nFormat; + /** Your %1$s rating is %2$s. */ + yourPerfTypeRatingIsRating: I18nFormat; + /** Your question may already have an answer %1$s */ + yourQuestionMayHaveBeenAnswered: I18nFormat; + /** Your rating */ + yourRating: string; + /** Your score: %s */ + yourScore: I18nFormat; + /** Your top weekly %1$s rating (%2$s) is too high */ + yourTopWeeklyPerfRatingIsTooHigh: I18nFormat; + /** Your turn */ + yourTurn: string; + /** Zero advertisement */ + zeroAdvertisement: string; + }; + study: { + /** Add members */ + addMembers: string; + /** Add a new chapter */ + addNewChapter: string; + /** Allow cloning */ + allowCloning: string; + /** All studies */ + allStudies: string; + /** All SYNC members remain on the same position */ + allSyncMembersRemainOnTheSamePosition: string; + /** Alphabetical */ + alphabetical: string; + /** Analysis mode */ + analysisMode: string; + /** Annotate with glyphs */ + annotateWithGlyphs: string; + /** Attack */ + attack: string; + /** Automatic */ + automatic: string; + /** Back */ + back: string; + /** Black is better */ + blackIsBetter: string; + /** Black is slightly better */ + blackIsSlightlyBetter: string; + /** Black is winning */ + blackIsWinning: string; + /** Blunder */ + blunder: string; + /** Brilliant move */ + brilliantMove: string; + /** Chapter PGN */ + chapterPgn: string; + /** Chapter %s */ + chapterX: I18nFormat; + /** Clear all comments, glyphs and drawn shapes in this chapter */ + clearAllCommentsInThisChapter: string; + /** Clear annotations */ + clearAnnotations: string; + /** Clear chat */ + clearChat: string; + /** Clear variations */ + clearVariations: string; + /** Clone */ + cloneStudy: string; + /** Comment on this move */ + commentThisMove: string; + /** Comment on this position */ + commentThisPosition: string; + /** Delete the entire study? There is no going back! Type the name of the study to confirm: %s */ + confirmDeleteStudy: I18nFormat; + /** Contributor */ + contributor: string; + /** Contributors */ + contributors: string; + /** Copy PGN */ + copyChapterPgn: string; + /** Counterplay */ + counterplay: string; + /** Create chapter */ + createChapter: string; + /** Create study */ + createStudy: string; + /** Current chapter URL */ + currentChapterUrl: string; + /** Date added (newest) */ + dateAddedNewest: string; + /** Date added (oldest) */ + dateAddedOldest: string; + /** Delete chapter */ + deleteChapter: string; + /** Delete study */ + deleteStudy: string; + /** Delete the study chat history? There is no going back! */ + deleteTheStudyChatHistory: string; + /** Delete this chapter. There is no going back! */ + deleteThisChapter: string; + /** Development */ + development: string; + /** Download all games */ + downloadAllGames: string; + /** Download game */ + downloadGame: string; + /** Dubious move */ + dubiousMove: string; + /** Edit chapter */ + editChapter: string; + /** Editor */ + editor: string; + /** Edit study */ + editStudy: string; + /** Embed in your website */ + embedInYourWebsite: string; + /** Empty */ + empty: string; + /** Enable sync */ + enableSync: string; + /** Equal position */ + equalPosition: string; + /** Everyone */ + everyone: string; + /** First */ + first: string; + /** Get a full server-side computer analysis of the mainline. */ + getAFullComputerAnalysis: string; + /** Good move */ + goodMove: string; + /** Hide next moves */ + hideNextMoves: string; + /** Hot */ + hot: string; + /** Import from %s */ + importFromChapterX: I18nFormat; + /** Initiative */ + initiative: string; + /** Interactive lesson */ + interactiveLesson: string; + /** Interesting move */ + interestingMove: string; + /** Invite only */ + inviteOnly: string; + /** Invite to the study */ + inviteToTheStudy: string; + /** Kick */ + kick: string; + /** Last */ + last: string; + /** Leave the study */ + leaveTheStudy: string; + /** Like */ + like: string; + /** Load games by URLs */ + loadAGameByUrl: string; + /** Load games from PGN */ + loadAGameFromPgn: string; + /** Load games from %1$s or %2$s */ + loadAGameFromXOrY: I18nFormat; + /** Load a position from FEN */ + loadAPositionFromFen: string; + /** Make sure the chapter is complete. You can only request analysis once. */ + makeSureTheChapterIsComplete: string; + /** Manage topics */ + manageTopics: string; + /** Members */ + members: string; + /** Mistake */ + mistake: string; + /** Most popular */ + mostPopular: string; + /** My favourite studies */ + myFavoriteStudies: string; + /** My private studies */ + myPrivateStudies: string; + /** My public studies */ + myPublicStudies: string; + /** My studies */ + myStudies: string; + /** My topics */ + myTopics: string; + /** %s Chapters */ + nbChapters: I18nPlural; + /** %s Games */ + nbGames: I18nPlural; + /** %s Members */ + nbMembers: I18nPlural; + /** New chapter */ + newChapter: string; + /** New tag */ + newTag: string; + /** Next */ + next: string; + /** Next chapter */ + nextChapter: string; + /** Nobody */ + nobody: string; + /** No: let people browse freely */ + noLetPeopleBrowseFreely: string; + /** None yet. */ + noneYet: string; + /** None */ + noPinnedComment: string; + /** Normal analysis */ + normalAnalysis: string; + /** Novelty */ + novelty: string; + /** Only the study contributors can request a computer analysis. */ + onlyContributorsCanRequestAnalysis: string; + /** Only me */ + onlyMe: string; + /** Only move */ + onlyMove: string; + /** Only public studies can be embedded! */ + onlyPublicStudiesCanBeEmbedded: string; + /** Open */ + open: string; + /** Orientation */ + orientation: string; + /** Paste your PGN text here, up to %s games */ + pasteYourPgnTextHereUpToNbGames: I18nPlural; + /** PGN tags */ + pgnTags: string; + /** Pinned chapter comment */ + pinnedChapterComment: string; + /** Pinned study comment */ + pinnedStudyComment: string; + /** Play again */ + playAgain: string; + /** Playing */ + playing: string; + /** Please only invite people who know you, and who actively want to join this study. */ + pleaseOnlyInvitePeopleYouKnow: string; + /** Popular topics */ + popularTopics: string; + /** Previous chapter */ + prevChapter: string; + /** Previous */ + previous: string; + /** Private */ + private: string; + /** Public */ + public: string; + /** Read more about embedding */ + readMoreAboutEmbedding: string; + /** Recently updated */ + recentlyUpdated: string; + /** Right under the board */ + rightUnderTheBoard: string; + /** Save */ + save: string; + /** Save chapter */ + saveChapter: string; + /** Search by username */ + searchByUsername: string; + /** Share & export */ + shareAndExport: string; + /** Share changes with spectators and save them on the server */ + shareChanges: string; + /** Evaluation bars */ + showEvalBar: string; + /** Spectator */ + spectator: string; + /** Start */ + start: string; + /** Start at initial position */ + startAtInitialPosition: string; + /** Start at %s */ + startAtX: I18nFormat; + /** Start from custom position */ + startFromCustomPosition: string; + /** Start from initial position */ + startFromInitialPosition: string; + /** Studies created by %s */ + studiesCreatedByX: I18nFormat; + /** Studies I contribute to */ + studiesIContributeTo: string; + /** Study actions */ + studyActions: string; + /** Study not found */ + studyNotFound: string; + /** Study PGN */ + studyPgn: string; + /** Study URL */ + studyUrl: string; + /** The chapter is too short to be analysed. */ + theChapterIsTooShortToBeAnalysed: string; + /** Time trouble */ + timeTrouble: string; + /** Topics */ + topics: string; + /** Unclear position */ + unclearPosition: string; + /** Unlike */ + unlike: string; + /** Unlisted */ + unlisted: string; + /** URL of the games, one per line */ + urlOfTheGame: string; + /** Visibility */ + visibility: string; + /** What are studies? */ + whatAreStudies: string; + /** What would you play in this position? */ + whatWouldYouPlay: string; + /** Where do you want to study that? */ + whereDoYouWantToStudyThat: string; + /** White is better */ + whiteIsBetter: string; + /** White is slightly better */ + whiteIsSlightlyBetter: string; + /** White is winning */ + whiteIsWinning: string; + /** With compensation */ + withCompensation: string; + /** With the idea */ + withTheIdea: string; + /** %1$s, brought to you by %2$s */ + xBroughtToYouByY: I18nFormat; + /** Yes: keep everyone on the same position */ + yesKeepEveryoneOnTheSamePosition: string; + /** You are now a contributor */ + youAreNowAContributor: string; + /** You are now a spectator */ + youAreNowASpectator: string; + /** You can paste this in the forum or your Lichess blog to embed */ + youCanPasteThisInTheForumToEmbed: string; + /** Congratulations! You completed this lesson. */ + youCompletedThisLesson: string; + /** Zugzwang */ + zugzwang: string; + }; + swiss: { + /** Absences */ + absences: string; + /** Byes */ + byes: string; + /** Comparison */ + comparison: string; + /** Predefined max rounds, but duration unknown */ + durationUnknown: string; + /** Dutch system */ + dutchSystem: string; + /** In Swiss games, players cannot draw before 30 moves are played. While this measure cannot prevent pre-arranged draws, it at least makes it harder to agree to a draw on the fly. */ + earlyDrawsAnswer: string; + /** What happens with early draws? */ + earlyDrawsQ: string; + /** FIDE handbook */ + FIDEHandbook: string; + /** If this list is non-empty, then users absent from this list will be forbidden to join. One username per line. */ + forbiddedUsers: string; + /** Forbidden pairings */ + forbiddenPairings: string; + /** Usernames of players that must not play together (Siblings, for instance). Two usernames per line, separated by a space. */ + forbiddenPairingsHelp: string; + /** Forbidden */ + identicalForbidden: string; + /** Identical pairing */ + identicalPairing: string; + /** Join or create a team */ + joinOrCreateTeam: string; + /** Late join */ + lateJoin: string; + /** Yes, until more than half the rounds have started; for example in a 11-rounds Swiss, players can join before round 6 starts and in a 12-rounds before round 7 starts. */ + lateJoinA: string; + /** Can players late-join? */ + lateJoinQ: string; + /** Yes until more than half the rounds have started */ + lateJoinUntil: string; + /** Manual pairings in next round */ + manualPairings: string; + /** Specify all pairings of the next round manually. One player pair per line. Example: */ + manualPairingsHelp: string; + /** When all possible pairings have been played, the tournament will be ended and a winner declared. */ + moreRoundsThanPlayersA: string; + /** What happens if the tournament has more rounds than players? */ + moreRoundsThanPlayersQ: string; + /** Must have played their last swiss game */ + mustHavePlayedTheirLastSwissGame: string; + /** Only let players join if they have played their last swiss game. If they failed to show up in a recent swiss event, they won't be able to enter yours. This results in a better swiss experience for the players who actually show up. */ + mustHavePlayedTheirLastSwissGameHelp: string; + /** %s rounds */ + nbRounds: I18nPlural; + /** New Swiss tournament */ + newSwiss: string; + /** Next round */ + nextRound: string; + /** Now playing */ + nowPlaying: string; + /** A player gets a bye of one point every time the pairing system can't find a pairing for them. */ + numberOfByesA: string; + /** How many byes can a player get? */ + numberOfByesQ: string; + /** Number of games */ + numberOfGames: string; + /** As many as can be played in the allotted duration */ + numberOfGamesAsManyAsPossible: string; + /** Decided in advance, same for all players */ + numberOfGamesPreDefined: string; + /** Number of rounds */ + numberOfRounds: string; + /** An odd number of rounds allows optimal colour balance. */ + numberOfRoundsHelp: string; + /** One round every %s days */ + oneRoundEveryXDays: I18nPlural; + /** Ongoing games */ + ongoingGames: I18nPlural; + /** We don't plan to add more tournament systems to Lichess at the moment. */ + otherSystemsA: string; + /** What about other tournament systems? */ + otherSystemsQ: string; + /** With the %1$s, implemented by %2$s, in accordance with the %3$s. */ + pairingsA: I18nFormat; + /** How are pairings decided? */ + pairingsQ: string; + /** Pairing system */ + pairingSystem: string; + /** Any available opponent with similar ranking */ + pairingSystemArena: string; + /** Best pairing based on points and tie breaks */ + pairingSystemSwiss: string; + /** Pairing wait time */ + pairingWaitTime: string; + /** Fast: doesn't wait for all players */ + pairingWaitTimeArena: string; + /** Slow: waits for all players */ + pairingWaitTimeSwiss: string; + /** Pause */ + pause: string; + /** Yes but might reduce the number of rounds */ + pauseSwiss: string; + /** Play your games */ + playYourGames: string; + /** A win is worth one point, a draw is a half point, and a loss is zero points. */ + pointsCalculationA: string; + /** How are points calculated? */ + pointsCalculationQ: string; + /** Possible, but not consecutive */ + possibleButNotConsecutive: string; + /** Predefined duration in minutes */ + predefinedDuration: string; + /** Only allow pre-defined users to join */ + predefinedUsers: string; + /** Players who sign up for Swiss events but don't play their games can be problematic. */ + protectionAgainstNoShowA: string; + /** What is done regarding no-shows? */ + protectionAgainstNoShowQ: string; + /** Swiss tournaments were not designed for online chess. They demand punctuality, dedication and patience from players. */ + restrictedToTeamsA: string; + /** Why is it restricted to teams? */ + restrictedToTeamsQ: string; + /** Interval between rounds */ + roundInterval: string; + /** We'd like to add it, but unfortunately Round Robin doesn't work online. */ + roundRobinA: string; + /** What about Round Robin? */ + roundRobinQ: string; + /** Rounds are started manually */ + roundsAreStartedManually: string; + /** Similar to OTB tournaments */ + similarToOTB: string; + /** Sonneborn–Berger score */ + sonnebornBergerScore: string; + /** Starting in */ + startingIn: string; + /** Starting soon */ + startingSoon: string; + /** Streaks and Berserk */ + streaksAndBerserk: string; + /** Swiss */ + swiss: string; + /** In a Swiss tournament %1$s, each competitor does not necessarily play all other entrants. Competitors meet one-on-one in each round and are paired using a set of rules designed to ensure that each competitor plays opponents with a similar running score, but not the same opponent more than once. The winner is the competitor with the highest aggregate points earned in all rounds. All competitors play in each round unless there is an odd number of players. */ + swissDescription: I18nFormat; + /** Swiss tournaments */ + swissTournaments: string; + /** In a Swiss tournament, all participants play the same number of games, and can only play each other once. */ + swissVsArenaA: string; + /** When to use Swiss tournaments instead of arenas? */ + swissVsArenaQ: string; + /** Swiss tournaments can only be created by team leaders, and can only be played by team members. */ + teamOnly: I18nFormat; + /** Tie Break */ + tieBreak: string; + /** With the %s. */ + tiebreaksCalculationA: I18nFormat; + /** How are tie breaks calculated? */ + tiebreaksCalculationQ: string; + /** Duration of the tournament */ + tournDuration: string; + /** Tournament start date */ + tournStartDate: string; + /** Unlimited and free */ + unlimitedAndFree: string; + /** View all %s rounds */ + viewAllXRounds: I18nPlural; + /** Their clock will tick, they will flag, and lose the game. */ + whatIfOneDoesntPlayA: string; + /** What happens if a player doesn't play a game? */ + whatIfOneDoesntPlayQ: string; + /** No. They're complementary features. */ + willSwissReplaceArenasA: string; + /** Will Swiss replace arena tournaments? */ + willSwissReplaceArenasQ: string; + /** %s minutes between rounds */ + xMinutesBetweenRounds: I18nPlural; + /** %s rounds Swiss */ + xRoundsSwiss: I18nPlural; + /** %s seconds between rounds */ + xSecondsBetweenRounds: I18nPlural; + }; + team: { + /** All teams */ + allTeams: string; + /** Battle of %s teams */ + battleOfNbTeams: I18nPlural; + /** Your join request is being reviewed by a team leader. */ + beingReviewed: string; + /** Close team */ + closeTeam: string; + /** Closes the team forever. */ + closeTeamDescription: string; + /** Completed tournaments */ + completedTourns: string; + /** Declined Requests */ + declinedRequests: string; + /** Team entry code */ + entryCode: string; + /** (Optional) An entry code that new members must know to join this team. */ + entryCodeDescriptionForLeader: string; + /** Incorrect entry code. */ + incorrectEntryCode: string; + /** Inner team */ + innerTeam: string; + /** Join the official %s team for news and events */ + joinLichessVariantTeam: I18nFormat; + /** Join team */ + joinTeam: string; + /** Kick someone out of the team */ + kickSomeone: string; + /** Leaders chat */ + leadersChat: string; + /** Leader teams */ + leaderTeams: string; + /** List the teams that will compete in this battle. */ + listTheTeamsThatWillCompete: string; + /** Manually review admission requests */ + manuallyReviewAdmissionRequests: string; + /** If checked, players will need to write a request to join the team, which you can decline or accept. */ + manuallyReviewAdmissionRequestsHelp: string; + /** Message all members */ + messageAllMembers: string; + /** Send a private message to ALL members of the team. */ + messageAllMembersLongDescription: string; + /** Send a private message to every member of the team */ + messageAllMembersOverview: string; + /** My teams */ + myTeams: string; + /** %s leaders per team */ + nbLeadersPerTeam: I18nPlural; + /** %s members */ + nbMembers: I18nPlural; + /** New team */ + newTeam: string; + /** No team found */ + noTeamFound: string; + /** Number of leaders per team. The sum of their score is the score of the team. */ + numberOfLeadsPerTeam: string; + /** You really shouldn't change this value after the tournament has started! */ + numberOfLeadsPerTeamHelp: string; + /** One team per line. Use the auto-completion. */ + oneTeamPerLine: string; + /** You can copy-paste this list from a tournament to another! */ + oneTeamPerLineHelp: string; + /** Please add a new team leader before leaving, or close the team. */ + onlyLeaderLeavesTeam: string; + /** Leave team */ + quitTeam: string; + /** Your join request was declined by a team leader. */ + requestDeclined: string; + /** Subscribe to team messages */ + subToTeamMessages: string; + /** A Swiss tournament that only members of your team can join */ + swissTournamentOverview: string; + /** Team */ + team: string; + /** This team already exists. */ + teamAlreadyExists: string; + /** Team Battle */ + teamBattle: string; + /** A battle of multiple teams, each player scores points for their team */ + teamBattleOverview: string; + /** Team leaders */ + teamLeaders: I18nPlural; + /** Team page */ + teamPage: string; + /** Recent members */ + teamRecentMembers: string; + /** Teams */ + teams: string; + /** Teams I lead */ + teamsIlead: string; + /** Team tournament */ + teamTournament: string; + /** An Arena tournament that only members of your team can join */ + teamTournamentOverview: string; + /** This tournament is over, and the teams can no longer be updated. */ + thisTeamBattleIsOver: string; + /** Upcoming tournaments */ + upcomingTournaments: string; + /** Who do you want to kick out of the team? */ + whoToKick: string; + /** Your join request will be reviewed by a team leader. */ + willBeReviewed: string; + /** %s join requests */ + xJoinRequests: I18nPlural; + /** You may want to link one of these upcoming tournaments? */ + youWayWantToLinkOneOfTheseTournaments: string; + }; + tfa: { + /** Authentication code */ + authenticationCode: string; + /** Disable two-factor authentication */ + disableTwoFactor: string; + /** Enable two-factor authentication */ + enableTwoFactor: string; + /** Enter your password and the authentication code generated by the app to complete the setup. You will need an authentication code every time you log in. */ + enterPassword: string; + /** If you cannot scan the code, enter the secret key %s into your app. */ + ifYouCannotScanEnterX: I18nFormat; + /** Note: If you lose access to your two-factor authentication codes, you can do a %s via email. */ + ifYouLoseAccessTwoFactor: I18nFormat; + /** Open the two-factor authentication app on your device to view your authentication code and verify your identity. */ + openTwoFactorApp: string; + /** Scan the QR code with the app. */ + scanTheCode: string; + /** Please enable two-factor authentication to secure your account at https://lichess.org/account/twofactor. */ + setupReminder: string; + /** Get an app for two-factor authentication. We recommend the following apps: */ + twoFactorAppRecommend: string; + /** Two-factor authentication */ + twoFactorAuth: string; + /** Two-factor authentication enabled */ + twoFactorEnabled: string; + /** Two-factor authentication adds another layer of security to your account. */ + twoFactorHelp: string; + /** You need your password and an authentication code from your authenticator app to disable two-factor authentication. */ + twoFactorToDisable: string; + }; + timeago: { + /** completed */ + completed: string; + /** in %s days */ + inNbDays: I18nPlural; + /** in %s hours */ + inNbHours: I18nPlural; + /** in %s minutes */ + inNbMinutes: I18nPlural; + /** in %s months */ + inNbMonths: I18nPlural; + /** in %s seconds */ + inNbSeconds: I18nPlural; + /** in %s weeks */ + inNbWeeks: I18nPlural; + /** in %s years */ + inNbYears: I18nPlural; + /** just now */ + justNow: string; + /** %s days ago */ + nbDaysAgo: I18nPlural; + /** %s hours ago */ + nbHoursAgo: I18nPlural; + /** %s hours remaining */ + nbHoursRemaining: I18nPlural; + /** %s minutes ago */ + nbMinutesAgo: I18nPlural; + /** %s minutes remaining */ + nbMinutesRemaining: I18nPlural; + /** %s months ago */ + nbMonthsAgo: I18nPlural; + /** %s weeks ago */ + nbWeeksAgo: I18nPlural; + /** %s years ago */ + nbYearsAgo: I18nPlural; + /** right now */ + rightNow: string; + }; + tourname: { + /** Classical Shield */ + classicalShield: string; + /** Classical Shield Arena */ + classicalShieldArena: string; + /** Daily Classical */ + dailyClassical: string; + /** Daily Classical Arena */ + dailyClassicalArena: string; + /** Daily Rapid */ + dailyRapid: string; + /** Daily Rapid Arena */ + dailyRapidArena: string; + /** Daily %s */ + dailyX: I18nFormat; + /** Daily %s Arena */ + dailyXArena: I18nFormat; + /** Eastern Classical */ + easternClassical: string; + /** Eastern Classical Arena */ + easternClassicalArena: string; + /** Eastern Rapid */ + easternRapid: string; + /** Eastern Rapid Arena */ + easternRapidArena: string; + /** Eastern %s */ + easternX: I18nFormat; + /** Eastern %s Arena */ + easternXArena: I18nFormat; + /** Elite %s */ + eliteX: I18nFormat; + /** Elite %s Arena */ + eliteXArena: I18nFormat; + /** Hourly Rapid */ + hourlyRapid: string; + /** Hourly Rapid Arena */ + hourlyRapidArena: string; + /** Hourly %s */ + hourlyX: I18nFormat; + /** Hourly %s Arena */ + hourlyXArena: I18nFormat; + /** Monthly Classical */ + monthlyClassical: string; + /** Monthly Classical Arena */ + monthlyClassicalArena: string; + /** Monthly Rapid */ + monthlyRapid: string; + /** Monthly Rapid Arena */ + monthlyRapidArena: string; + /** Monthly %s */ + monthlyX: I18nFormat; + /** Monthly %s Arena */ + monthlyXArena: I18nFormat; + /** Rapid Shield */ + rapidShield: string; + /** Rapid Shield Arena */ + rapidShieldArena: string; + /** Weekly Classical */ + weeklyClassical: string; + /** Weekly Classical Arena */ + weeklyClassicalArena: string; + /** Weekly Rapid */ + weeklyRapid: string; + /** Weekly Rapid Arena */ + weeklyRapidArena: string; + /** Weekly %s */ + weeklyX: I18nFormat; + /** Weekly %s Arena */ + weeklyXArena: I18nFormat; + /** %s Arena */ + xArena: I18nFormat; + /** %s Shield */ + xShield: I18nFormat; + /** %s Shield Arena */ + xShieldArena: I18nFormat; + /** %s Team Battle */ + xTeamBattle: I18nFormat; + /** Yearly Classical */ + yearlyClassical: string; + /** Yearly Classical Arena */ + yearlyClassicalArena: string; + /** Yearly Rapid */ + yearlyRapid: string; + /** Yearly Rapid Arena */ + yearlyRapidArena: string; + /** Yearly %s */ + yearlyX: I18nFormat; + /** Yearly %s Arena */ + yearlyXArena: I18nFormat; + }; + ublog: { + /** Our simple tips to write great blog posts */ + blogTips: string; + /** Blog topics */ + blogTopics: string; + /** Community blogs */ + communityBlogs: string; + /** Continue reading this post */ + continueReadingPost: string; + /** Enable comments */ + createBlogDiscussion: string; + /** A forum topic will be created for people to comment on your post */ + createBlogDiscussionHelp: string; + /** Delete this blog post definitively */ + deleteBlog: string; + /** Discuss this blog post in the forum */ + discussThisBlogPostInTheForum: string; + /** Drafts */ + drafts: string; + /** Edit your blog post */ + editYourBlogPost: string; + /** Friends blogs */ + friendBlogs: string; + /** Image alternative text */ + imageAlt: string; + /** Image credit */ + imageCredit: string; + /** Anything inappropriate could get your account closed. */ + inappropriateContentAccountClosed: string; + /** Latest blog posts */ + latestBlogPosts: string; + /** Lichess blog posts in %s */ + lichessBlogPostsFromXYear: I18nFormat; + /** Lichess Official Blog */ + lichessOfficialBlog: string; + /** Liked blog posts */ + likedBlogs: string; + /** More blog posts by %s */ + moreBlogPostsBy: I18nFormat; + /** %s views */ + nbViews: I18nPlural; + /** New post */ + newPost: string; + /** No drafts to show. */ + noDrafts: string; + /** No posts in this blog, yet. */ + noPostsInThisBlogYet: string; + /** Post body */ + postBody: string; + /** Post intro */ + postIntro: string; + /** Post title */ + postTitle: string; + /** Previous blog posts */ + previousBlogPosts: string; + /** Published */ + published: string; + /** Published %s blog posts */ + publishedNbBlogPosts: I18nPlural; + /** If checked, the post will be listed on your blog. If not, it will be private, in your draft posts */ + publishHelp: string; + /** Publish on your blog */ + publishOnYourBlog: string; + /** Please only post safe and respectful content. Do not copy someone else's content. */ + safeAndRespectfulContent: string; + /** It is safe to use images from the following websites: */ + safeToUseImages: string; + /** Save draft */ + saveDraft: string; + /** Select the topics your post is about */ + selectPostTopics: string; + /** This is a draft */ + thisIsADraft: string; + /** This post is published */ + thisPostIsPublished: string; + /** Upload an image for your post */ + uploadAnImageForYourPost: string; + /** You can also use images that you made yourself, pictures you took, screenshots of Lichess... anything that is not copyrighted by someone else. */ + useImagesYouMadeYourself: string; + /** View all %s posts */ + viewAllNbPosts: I18nPlural; + /** %s's Blog */ + xBlog: I18nFormat; + /** %1$s published %2$s */ + xPublishedY: I18nFormat; + /** You are blocked by the blog author. */ + youBlockedByBlogAuthor: string; + }; + voiceCommands: { + /** Cancel timer or deny a request */ + cancelTimerOrDenyARequest: string; + /** Castle (either side) */ + castle: string; + /** Use the %1$s button to toggle voice recognition, the %2$s button to open this help dialog, and the %3$s menu to change speech settings. */ + instructions1: I18nFormat; + /** We show arrows for multiple moves when we are not sure. Speak the colour or number of a move arrow to select it. */ + instructions2: string; + /** If an arrow shows a sweeping radar, that move will be played when the circle is complete. During this time, you may only say %1$s to play the move immediately, %2$s to cancel, or speak the colour/number of a different arrow. This timer can be adjusted or turned off in settings. */ + instructions3: I18nFormat; + /** Enable %s in noisy surroundings. Hold shift while speaking commands when this is on. */ + instructions4: I18nFormat; + /** Use the phonetic alphabet to improve recognition of chessboard files. */ + instructions5: string; + /** %s explains the voice move settings in detail. */ + instructions6: I18nFormat; + /** Move to e4 or select e4 piece */ + moveToE4OrSelectE4Piece: string; + /** Phonetic alphabet is best */ + phoneticAlphabetIsBest: string; + /** Play preferred move or confirm something */ + playPreferredMoveOrConfirmSomething: string; + /** Select or capture a bishop */ + selectOrCaptureABishop: string; + /** Show puzzle solution */ + showPuzzleSolution: string; + /** Sleep (if wake word enabled) */ + sleep: string; + /** Take rook with queen */ + takeRookWithQueen: string; + /** This blog post */ + thisBlogPost: string; + /** Turn off voice recognition */ + turnOffVoiceRecognition: string; + /** Voice commands */ + voiceCommands: string; + /** Watch the video tutorial */ + watchTheVideoTutorial: string; + }; +} diff --git a/ui/@types/lichess/index.d.ts b/ui/@types/lichess/index.d.ts index f7a0798705c73..7fd6721f6b387 100644 --- a/ui/@types/lichess/index.d.ts +++ b/ui/@types/lichess/index.d.ts @@ -1,6 +1,7 @@ /// /// /// +/// // file://./../../site/src/site.ts interface Site { @@ -30,15 +31,13 @@ interface Site { redirect(o: RedirectTo, beep?: boolean): void; reload(err?: any): void; announce(d: LichessAnnouncement): void; - trans: Trans; // file://./../../common/src/i18n.ts sound: SoundI; // file://./../../site/src/sound.ts displayLocale: string; // file://./../../common/src/i18n.ts blindMode: boolean; // the following are not set in site.ts load: Promise; // DOMContentLoaded promise - quantity(n: number): 'zero' | 'one' | 'few' | 'many' | 'other'; - siteI18n: I18nDict; + quantity(n: number): 'zero' | 'one' | 'two' | 'few' | 'many' | 'other'; socket: SocketI; quietMode?: boolean; analysis?: any; // expose the analysis ctrl @@ -52,9 +51,6 @@ interface EsmModuleOpts extends AssetUrlOpts { type PairOf = [T, T]; -type I18nDict = { [key: string]: string }; -type I18nKey = string; - type Flair = string; type RedirectTo = string | { url: string; cookie: Cookie }; @@ -81,7 +77,7 @@ interface QuestionChoice { // file://./../../round/src/ctrl.ts action: () => void; icon?: string; - key?: I18nKey; + text?: string; } interface QuestionOpts { @@ -151,18 +147,6 @@ type Timeout = ReturnType; declare type SocketSend = (type: string, data?: any, opts?: any, noRetry?: boolean) => void; -type TransNoArg = (key: string) => string; - -interface Trans { - // file://./../../common/src/i18n.ts - (key: string, ...args: Array): string; - noarg: TransNoArg; - plural(key: string, count: number, ...args: Array): string; - pluralSame(key: string, count: number, ...args: Array): string; - vdom(key: string, ...args: T[]): Array; - vdomPlural(key: string, count: number, countArg: T, ...args: T[]): Array; -} - interface LichessAnnouncement { msg?: string; date?: string; @@ -180,6 +164,7 @@ interface Fipr { interface Window { site: Site; fipr: Fipr; + i18n: I18n; $as(cash: Cash): T; readonly chrome?: unknown; readonly moment: any; @@ -319,4 +304,5 @@ type SocketHandlers = Dictionary<(d: any) => void>; declare const site: Site; declare const fipr: Fipr; +declare const i18n: I18n; declare module 'tablesort'; diff --git a/ui/analyse/src/ctrl.ts b/ui/analyse/src/ctrl.ts index 73011dc422d66..1b23f0ec1b881 100644 --- a/ui/analyse/src/ctrl.ts +++ b/ui/analyse/src/ctrl.ts @@ -61,7 +61,6 @@ export default class AnalyseCtrl { tree: TreeWrapper; socket: Socket; chessground: ChessgroundApi; - trans: Trans; ceval: CevalCtrl; evalCache: EvalCache; persistence?: Persistence; @@ -137,7 +136,6 @@ export default class AnalyseCtrl { ) { this.data = opts.data; this.element = opts.element; - this.trans = opts.trans; this.isEmbed = !!opts.embed; this.treeView = new TreeView('column'); this.promotion = new PromotionCtrl( diff --git a/ui/analyse/src/explorer/explorerConfig.ts b/ui/analyse/src/explorer/explorerConfig.ts index 845cdf50ea594..ed655d2ddbcd5 100644 --- a/ui/analyse/src/explorer/explorerConfig.ts +++ b/ui/analyse/src/explorer/explorerConfig.ts @@ -152,7 +152,7 @@ export function view(ctrl: ExplorerConfigCtrl): VNode[] { h( 'button.button.button-green.text', { attrs: dataIcon(licon.Checkmark), hook: bind('click', ctrl.toggleOpen) }, - ctrl.root.trans.noarg('allSet'), + i18n.site.allSet, ), ), ]; @@ -165,7 +165,7 @@ const playerDb = (ctrl: ExplorerConfigCtrl) => { return h('div.player-db', [ ctrl.data.playerName.open() ? playerModal(ctrl) : undefined, h('section.name', [ - h('label', ctrl.root.trans('player')), + h('label', i18n.site.player), h('div', [ h( 'div.choices', @@ -184,7 +184,7 @@ const playerDb = (ctrl: ExplorerConfigCtrl) => { attrs: dataIcon(licon.ChasingArrows), hook: bind('click', ctrl.toggleColor, ctrl.root.redraw), }, - ' ' + ctrl.root.trans(ctrl.data.color() == 'white' ? 'asWhite' : 'asBlack'), + ` ${i18n.site[ctrl.data.color() == 'white' ? 'asWhite' : 'asBlack']}`, ), ]), ]), @@ -197,12 +197,9 @@ const playerDb = (ctrl: ExplorerConfigCtrl) => { const masterDb = (ctrl: ExplorerConfigCtrl) => h('div', [ h('section.date', [ + h('label', [i18n.site.since, yearInput(ctrl.data.byDb().since, () => '', ctrl.root.redraw)]), h('label', [ - ctrl.root.trans.noarg('since'), - yearInput(ctrl.data.byDb().since, () => '', ctrl.root.redraw), - ]), - h('label', [ - ctrl.root.trans.noarg('until'), + i18n.site.until, yearInput(ctrl.data.byDb().until, ctrl.data.byDb().since, ctrl.root.redraw), ]), ]), @@ -217,14 +214,14 @@ const radioButton = attrs: { 'aria-pressed': `${storage().includes(v)}`, title: render ? ucfirst('' + v) : '' }, hook: bind('click', _ => ctrl.toggleMany(storage)(v), ctrl.root.redraw), }, - render ? render(v) : ctrl.root.trans.noarg('' + v), + render ? render(v) : i18n(v as string), ); const lichessDb = (ctrl: ExplorerConfigCtrl) => h('div', [ speedSection(ctrl), h('section.rating', [ - h('label', ctrl.root.trans.noarg('averageElo')), + h('label', i18n.site.averageElo), h('div.choices', allRatings.map(radioButton(ctrl, ctrl.data.rating))), ]), monthSection(ctrl), @@ -232,13 +229,13 @@ const lichessDb = (ctrl: ExplorerConfigCtrl) => const speedSection = (ctrl: ExplorerConfigCtrl) => h('section.speed', [ - h('label', ctrl.root.trans.noarg('timeControl')), + h('label', i18n.site.timeControl), h('div.choices', allSpeeds.map(radioButton(ctrl, ctrl.data.speed, s => iconTag(perfIcons[s])))), ]); const modeSection = (ctrl: ExplorerConfigCtrl) => h('section.mode', [ - h('label', ctrl.root.trans.noarg('mode')), + h('label', i18n.site.mode), h('div.choices', allModes.map(radioButton(ctrl, ctrl.data.mode))), ]); @@ -310,12 +307,9 @@ const yearInput = (prop: StoredProp, after: () => Month, redraw: Redraw) const monthSection = (ctrl: ExplorerConfigCtrl) => h('section.date', [ + h('label', [i18n.site.since, monthInput(ctrl.data.byDb().since, () => '', ctrl.root.redraw)]), h('label', [ - ctrl.root.trans.noarg('since'), - monthInput(ctrl.data.byDb().since, () => '', ctrl.root.redraw), - ]), - h('label', [ - ctrl.root.trans.noarg('until'), + i18n.site.until, monthInput(ctrl.data.byDb().until, ctrl.data.byDb().since, ctrl.root.redraw), ]), ]); @@ -346,7 +340,7 @@ const playerModal = (ctrl: ExplorerConfigCtrl) => { h('div.input-wrapper', [ h('input', { attrs: { - placeholder: ctrl.root.trans.noarg('searchByUsername'), + placeholder: i18n.study.searchByUsername, spellcheck: 'false', }, hook: onInsert(input => diff --git a/ui/analyse/src/explorer/explorerView.ts b/ui/analyse/src/explorer/explorerView.ts index 2ac3815a1c956..b976a91c8165e 100644 --- a/ui/analyse/src/explorer/explorerView.ts +++ b/ui/analyse/src/explorer/explorerView.ts @@ -34,7 +34,6 @@ function resultBar(move: OpeningMoveStats): VNode { function showMoveTable(ctrl: AnalyseCtrl, data: OpeningData): VNode | null { if (!data.moves.length) return null; - const trans = ctrl.trans.noarg; const sumTotal = data.white + data.black + data.draws; const movesWithCurrent = data.moves.length > 1 @@ -53,9 +52,9 @@ function showMoveTable(ctrl: AnalyseCtrl, data: OpeningData): VNode | null { return h('table.moves', [ h('thead', [ h('tr', [ - h('th', trans('move')), - h('th', { attrs: { colspan: 2 } }, trans('games')), - h('th', trans('whiteDrawBlack')), + h('th', i18n.site.move), + h('th', { attrs: { colspan: 2 } }, i18n.site.games), + h('th', i18n.site.whiteDrawBlack), ]), ]), h( @@ -84,7 +83,7 @@ function moveTooltip(ctrl: AnalyseCtrl, move: OpeningMoveStats): string { : `${g.white.name} ${result} ${g.black.name}`; } if (ctrl.explorer.opts.showRatings) { - if (move.averageRating) return ctrl.trans('averageRatingX', move.averageRating); + if (move.averageRating) return i18n.site.averageRatingX(move.averageRating); if (move.averageOpponentRating) return `Performance rating: ${move.performance}, average opponent: ${move.averageOpponentRating}`; } @@ -197,7 +196,7 @@ const closeButton = (ctrl: AnalyseCtrl): VNode => h( 'button.button.button-empty.text', { attrs: dataIcon(licon.X), hook: bind('click', ctrl.toggleExplorer, ctrl.redraw) }, - ctrl.trans.noarg('close'), + i18n.site.close, ); const showEmpty = (ctrl: AnalyseCtrl, data?: OpeningData): VNode => @@ -207,23 +206,19 @@ const showEmpty = (ctrl: AnalyseCtrl, data?: OpeningData): VNode => h('div.message', [ h( 'strong', - ctrl.trans.noarg(ctrl.explorer.root.node.ply >= MAX_DEPTH ? 'maxDepthReached' : 'noGameFound'), + ctrl.explorer.root.node.ply >= MAX_DEPTH ? i18n.site.maxDepthReached : i18n.site.noGameFound, ), data?.queuePosition ? h('p.explanation', `Indexing ${data.queuePosition} other players first ...`) : !ctrl.explorer.config.fullHouse() && - h('p.explanation', ctrl.trans.noarg('maybeIncludeMoreGamesFromThePreferencesMenu')), + h('p.explanation', i18n.site.maybeIncludeMoreGamesFromThePreferencesMenu), ]), ]); const showGameEnd = (ctrl: AnalyseCtrl, title: string): VNode => h('div.data.empty', [ - h('div.title', ctrl.trans.noarg('gameOver')), - h('div.message', [ - h('i', { attrs: dataIcon(licon.InfoCircle) }), - h('h3', ctrl.trans.noarg(title)), - closeButton(ctrl), - ]), + h('div.title', i18n.site.gameOver), + h('div.message', [h('i', { attrs: dataIcon(licon.InfoCircle) }), h('h3', title), closeButton(ctrl)]), ]); const openingTitle = (ctrl: AnalyseCtrl, data?: OpeningData) => { @@ -234,7 +229,7 @@ const openingTitle = (ctrl: AnalyseCtrl, data?: OpeningData) => { { attrs: opening ? { title } : {} }, opening ? [h('a', { attrs: { href: `/opening/${opening.name}`, target: '_blank' } }, title)] - : [showTitle(ctrl, ctrl.data.game.variant)], + : [showTitle(ctrl.data.game.variant)], ); }; @@ -244,12 +239,11 @@ export const clearLastShow = () => { }; function show(ctrl: AnalyseCtrl): MaybeVNode { - const trans = ctrl.trans.noarg, - data = ctrl.explorer.current(); + const data = ctrl.explorer.current(); if (data && isOpening(data)) { const moveTable = showMoveTable(ctrl, data), - recentTable = showGameTable(ctrl, data.fen, trans('recentGames'), data.recentGames || []), - topTable = showGameTable(ctrl, data.fen, trans('topGames'), data.topGames || []); + recentTable = showGameTable(ctrl, data.fen, i18n.site.recentGames, data.recentGames || []), + topTable = showGameTable(ctrl, data.fen, i18n.site.topGames, data.topGames || []); if (moveTable || recentTable || topTable) lastShow = h('div.data', [ explorerTitle(ctrl.explorer), @@ -264,23 +258,23 @@ function show(ctrl: AnalyseCtrl): MaybeVNode { showTablebase( ctrl, data.fen, - trans(title), - tooltip && trans(tooltip), + title, + tooltip, data.moves.filter(m => m.category == category), ); if (data.moves.length) lastShow = h('div.data', [ - ...row('loss', 'winning'), - ...row('unknown', 'unknown'), - ...row('maybe-loss', 'winOr50MovesByPriorMistake', 'unknownDueToRounding'), - ...row('blessed-loss', 'winPreventedBy50MoveRule'), - ...row('draw', 'drawn'), - ...row('cursed-win', 'lossSavedBy50MoveRule'), - ...row('maybe-win', 'lossOr50MovesByPriorMistake', 'unknownDueToRounding'), - ...row('win', 'losing'), + ...row('loss', i18n.site.winning), + ...row('unknown', i18n.site.unknown), + ...row('maybe-loss', i18n.site.winOr50MovesByPriorMistake, i18n.site.unknownDueToRounding), + ...row('blessed-loss', i18n.site.winPreventedBy50MoveRule), + ...row('draw', i18n.site.drawn), + ...row('cursed-win', i18n.site.lossSavedBy50MoveRule), + ...row('maybe-win', i18n.site.lossOr50MovesByPriorMistake, i18n.site.unknownDueToRounding), + ...row('win', i18n.site.losing), ]); - else if (data.checkmate) lastShow = showGameEnd(ctrl, 'checkmate'); - else if (data.stalemate) lastShow = showGameEnd(ctrl, 'stalemate'); + else if (data.checkmate) lastShow = showGameEnd(ctrl, i18n.site.checkmate); + else if (data.stalemate) lastShow = showGameEnd(ctrl, i18n.site.stalemate); else if (data.variant_win || data.variant_loss) lastShow = showGameEnd(ctrl, 'variantEnding'); else lastShow = showEmpty(ctrl); } @@ -316,7 +310,7 @@ const explorerTitle = (explorer: ExplorerCtrl) => { explorer.reload, ), }, - explorer.root.trans('player'), + i18n.site.player, ); const active = (nodes: LooseVNodes, title: string) => h( @@ -328,8 +322,8 @@ const explorerTitle = (explorer: ExplorerCtrl) => { nodes, ); const playerName = explorer.config.data.playerName.value(); - const masterDbExplanation = explorer.root.trans('masterDbExplanation', 2200, '1952', '2024-08'), - lichessDbExplanation = explorer.root.trans('lichessDbExplanation'); + const masterDbExplanation = i18n.site.masterDbExplanation(2200, '1952', '2024-08'), + lichessDbExplanation = i18n.site.lichessDbExplanation; const data = explorer.current(); const queuePosition = data && isOpening(data) && data.queuePosition; return h('div.explorer-title', [ @@ -344,7 +338,7 @@ const explorerTitle = (explorer: ExplorerCtrl) => { ? active( [ h(`strong${playerName.length > 14 ? '.long' : ''}`, playerName), - ' ' + explorer.root.trans(explorer.config.data.color() == 'white' ? 'asWhite' : 'asBlack'), + ` ${i18n.site[explorer.config.data.color() == 'white' ? 'asWhite' : 'asBlack']}`, explorer.isIndexing() && !explorer.config.data.open() && h('i.ddloader', { @@ -355,17 +349,16 @@ const explorerTitle = (explorer: ExplorerCtrl) => { }, }), ], - explorer.root.trans('switchSides'), + i18n.site.switchSides, ) : active([h('strong', 'Player'), ' database'], '') : playerLink(), ]); }; -function showTitle(ctrl: AnalyseCtrl, variant: Variant) { - if (variant.key === 'standard' || variant.key === 'fromPosition') - return ctrl.trans.noarg('openingExplorer'); - return ctrl.trans('xOpeningExplorer', variant.name); +function showTitle(variant: Variant) { + if (variant.key === 'standard' || variant.key === 'fromPosition') return i18n.site.openingExplorer; + return i18n.site.xOpeningExplorer(variant.name); } function showConfig(ctrl: AnalyseCtrl): VNode { @@ -374,7 +367,7 @@ function showConfig(ctrl: AnalyseCtrl): VNode { function showFailing(ctrl: AnalyseCtrl) { return h('div.data.empty', [ - h('div.title', showTitle(ctrl, ctrl.data.game.variant)), + h('div.title', showTitle(ctrl.data.game.variant)), h('div.failing.message', [ h('h3', 'Oops, sorry!'), h('p.explanation', ctrl.explorer.failing()?.toString()), diff --git a/ui/analyse/src/explorer/tablebaseView.ts b/ui/analyse/src/explorer/tablebaseView.ts index c0533e180edd1..5c33a9b1ea882 100644 --- a/ui/analyse/src/explorer/tablebaseView.ts +++ b/ui/analyse/src/explorer/tablebaseView.ts @@ -21,7 +21,7 @@ export function showTablebase( moves.map(move => h('tr', { key: move.uci, attrs: { 'data-uci': move.uci } }, [ h('td', move.san), - h('td', [showDtz(ctrl, fen, move), showDtm(ctrl, fen, move), showDtw(fen, move)]), + h('td', [showDtz(fen, move), showDtm(fen, move), showDtw(fen, move)]), ]), ), ), @@ -29,12 +29,12 @@ export function showTablebase( ]; } -function showDtm(ctrl: AnalyseCtrl, fen: FEN, move: TablebaseMoveStats) { +function showDtm(fen: FEN, move: TablebaseMoveStats) { if (move.dtm) return h( 'result.' + winnerOf(fen, move), { - attrs: { title: ctrl.trans.pluralSame('mateInXHalfMoves', Math.abs(move.dtm)) + ' (Depth To Mate)' }, + attrs: { title: i18n.site.mateInXHalfMoves(Math.abs(move.dtm)) + ' (Depth To Mate)' }, }, 'DTM ' + Math.abs(move.dtm), ); @@ -51,24 +51,23 @@ function showDtw(fen: FEN, move: TablebaseMoveStats) { return undefined; } -function showDtz(ctrl: AnalyseCtrl, fen: FEN, move: TablebaseMoveStats): VNode | null { - const trans = ctrl.trans.noarg; - if (move.checkmate) return h('result.' + winnerOf(fen, move), trans('checkmate')); - else if (move.variant_win) return h('result.' + winnerOf(fen, move), trans('variantLoss')); - else if (move.variant_loss) return h('result.' + winnerOf(fen, move), trans('variantWin')); - else if (move.stalemate) return h('result.draws', trans('stalemate')); - else if (move.insufficient_material) return h('result.draws', trans('insufficientMaterial')); +function showDtz(fen: FEN, move: TablebaseMoveStats): VNode | null { + if (move.checkmate) return h('result.' + winnerOf(fen, move), i18n.site.checkmate); + else if (move.variant_win) return h('result.' + winnerOf(fen, move), i18n.site.variantLoss); + else if (move.variant_loss) return h('result.' + winnerOf(fen, move), i18n.site.variantWin); + else if (move.stalemate) return h('result.draws', i18n.site.stalemate); + else if (move.insufficient_material) return h('result.draws', i18n.site.insufficientMaterial); else if (move.dtz === null) return null; - else if (move.dtz === 0) return h('result.draws', trans('draw')); + else if (move.dtz === 0) return h('result.draws', i18n.site.draw); else if (move.zeroing) return move.san.includes('x') - ? h('result.' + winnerOf(fen, move), trans('capture')) - : h('result.' + winnerOf(fen, move), trans('pawnMove')); + ? h('result.' + winnerOf(fen, move), i18n.site.capture) + : h('result.' + winnerOf(fen, move), i18n.site.pawnMove); return h( 'result.' + winnerOf(fen, move), { attrs: { - title: trans('dtzWithRounding') + ' (Distance To Zeroing)', + title: i18n.site.dtzWithRounding + ' (Distance To Zeroing)', }, }, 'DTZ ' + Math.abs(move.dtz), diff --git a/ui/analyse/src/forecast/forecastView.ts b/ui/analyse/src/forecast/forecastView.ts index 8057979219b1e..3b63d6aea955d 100644 --- a/ui/analyse/src/forecast/forecastView.ts +++ b/ui/analyse/src/forecast/forecastView.ts @@ -9,7 +9,7 @@ import { fixCrazySan } from 'chess'; import { findCurrentPath } from '../treeView/common'; import ForecastCtrl from './forecastCtrl'; -function onMyTurn(ctrl: AnalyseCtrl, fctrl: ForecastCtrl, cNodes: ForecastStep[]): VNode | undefined { +function onMyTurn(fctrl: ForecastCtrl, cNodes: ForecastStep[]): VNode | undefined { const firstNode = cNodes[0]; if (!firstNode) return; const fcs = fctrl.findStartingWithNode(firstNode); @@ -25,10 +25,10 @@ function onMyTurn(ctrl: AnalyseCtrl, fctrl: ForecastCtrl, cNodes: ForecastStep[] }, [ h('span', [ - h('strong', ctrl.trans('playX', fixCrazySan(cNodes[0].san))), + h('strong', i18n.site.playX(fixCrazySan(cNodes[0].san))), lines.length - ? h('span', ctrl.trans.pluralSame('andSaveNbPremoveLines', lines.length)) - : h('span', ctrl.trans.noarg('noConditionalPremoves')), + ? h('span', i18n.site.andSaveNbPremoveLines(lines.length)) + : h('span', i18n.site.noConditionalPremoves), ]), ], ); @@ -53,7 +53,7 @@ export default function (ctrl: AnalyseCtrl, fctrl: ForecastCtrl): VNode { return h('div.forecast', { class: { loading: fctrl.loading() } }, [ fctrl.loading() ? h('div.overlay', spinner()) : null, h('div.box', [ - h('div.top', ctrl.trans.noarg('conditionalPremoves')), + h('div.top', i18n.site.conditionalPremoves), h( 'div.list', fctrl.forecasts().map((nodes, i) => @@ -89,14 +89,11 @@ export default function (ctrl: AnalyseCtrl, fctrl: ForecastCtrl): VNode { }, [ isCandidate - ? h('span', [ - h('span', ctrl.trans.noarg('addCurrentVariation')), - h('sans', renderNodesHtml(cNodes)), - ]) - : h('span', ctrl.trans.noarg('playVariationToCreateConditionalPremoves')), + ? h('span', [h('span', i18n.site.addCurrentVariation), h('sans', renderNodesHtml(cNodes))]) + : h('span', i18n.site.playVariationToCreateConditionalPremoves), ], ), ]), - fctrl.onMyTurn() ? onMyTurn(ctrl, fctrl, cNodes) : null, + fctrl.onMyTurn() ? onMyTurn(fctrl, cNodes) : null, ]); } diff --git a/ui/analyse/src/interfaces.ts b/ui/analyse/src/interfaces.ts index 51d973f576465..2102bf461c096 100644 --- a/ui/analyse/src/interfaces.ts +++ b/ui/analyse/src/interfaces.ts @@ -147,14 +147,12 @@ export interface AnalyseOpts { hunter: boolean; explorer: ExplorerOpts; socketSend: AnalyseSocketSend; - trans: Trans; study?: StudyDataFromServer; tagTypes?: string; practice?: StudyPracticeData; relay?: RelayData; $side?: Cash; $underboard?: Cash; - i18n: I18nDict; chat: { enhance: EnhanceOpts; instance?: ChatCtrl; diff --git a/ui/analyse/src/plugins/analyse.nvui.ts b/ui/analyse/src/plugins/analyse.nvui.ts index eff6d23b6eddc..6d0b027030eda 100644 --- a/ui/analyse/src/plugins/analyse.nvui.ts +++ b/ui/analyse/src/plugins/analyse.nvui.ts @@ -98,7 +98,7 @@ export function initModule(ctrl: AnalyseController) { attrs: { 'aria-pressed': `${ctrl.explorer.enabled()}` }, hook: bind('click', _ => ctrl.explorer.toggle(), ctrl.redraw), }, - ctrl.trans.noarg('openingExplorerAndTablebase'), + i18n.site.openingExplorerAndTablebase, ), explorerView(ctrl), ] @@ -273,13 +273,13 @@ function renderEvalAndDepth(ctrl: AnalyseController): string { let evalStr: string, depthStr: string; if (ctrl.threatMode()) { evalStr = evalInfo(ctrl.node.threat); - depthStr = depthInfo(ctrl, ctrl.node.threat, false); - return `${evalInfo(ctrl.node.threat)} ${depthInfo(ctrl, ctrl.node.threat, false)}`; + depthStr = depthInfo(ctrl.node.threat, false); + return `${evalInfo(ctrl.node.threat)} ${depthInfo(ctrl.node.threat, false)}`; } else { const evs = ctrl.currentEvals(), bestEv = cevalView.getBestEval(evs); evalStr = evalInfo(bestEv); - depthStr = depthInfo(ctrl, evs.client, !!evs.client?.cloud); + depthStr = depthInfo(evs.client, !!evs.client?.cloud); } if (!evalStr) { if (!ctrl.ceval.allowed()) return NOT_ALLOWED; @@ -299,10 +299,10 @@ function evalInfo(bestEv: EvalScore | undefined): string { return ''; } -function depthInfo(ctrl: AnalyseController, clientEv: Tree.ClientEval | undefined, isCloud: boolean): string { +function depthInfo(clientEv: Tree.ClientEval | undefined, isCloud: boolean): string { if (!clientEv) return ''; const depth = clientEv.depth || 0; - return ctrl.trans('depthX', depth) + isCloud ? ' Cloud' : ''; + return i18n.site.depthX(depth) + isCloud ? ' Cloud' : ''; } function renderBestMove(ctrl: AnalyseController, style: Style): string { @@ -507,7 +507,7 @@ function renderCurrentNode(ctrl: AnalyseController, style: Style): string { } function renderPlayer(ctrl: AnalyseController, player: Player) { - return player.ai ? ctrl.trans('aiNameLevelAiLevel', 'Stockfish', player.ai) : userHtml(ctrl, player); + return player.ai ? i18n.site.aiNameLevelAiLevel('Stockfish', player.ai) : userHtml(ctrl, player); } function userHtml(ctrl: AnalyseController, player: Player) { diff --git a/ui/analyse/src/practice/practiceView.ts b/ui/analyse/src/practice/practiceView.ts index bbbbcdeef93dc..769dcc74e42e6 100644 --- a/ui/analyse/src/practice/practiceView.ts +++ b/ui/analyse/src/practice/practiceView.ts @@ -5,10 +5,9 @@ import { PracticeCtrl, Comment } from './practiceCtrl'; import AnalyseCtrl from '../ctrl'; import { renderNextChapter } from '../study/nextChapter'; -function commentBest(c: Comment, root: AnalyseCtrl, ctrl: PracticeCtrl): MaybeVNodes { +function commentBest(c: Comment, ctrl: PracticeCtrl): MaybeVNodes { return c.best - ? root.trans.vdom( - c.verdict === 'goodMove' ? 'anotherWasX' : 'bestWasX', + ? i18n.site[c.verdict === 'goodMove' ? 'anotherWasX' : 'bestWasX'].asArray( h( 'move', { @@ -28,14 +27,12 @@ function commentBest(c: Comment, root: AnalyseCtrl, ctrl: PracticeCtrl): MaybeVN : []; } -function renderOffTrack(root: AnalyseCtrl, ctrl: PracticeCtrl): VNode { +function renderOffTrack(ctrl: PracticeCtrl): VNode { return h('div.player.off', [ h('div.icon.off', '!'), h('div.instruction', [ - h('strong', root.trans.noarg('youBrowsedAway')), - h('div.choices', [ - h('a', { hook: bind('click', ctrl.resume, ctrl.redraw) }, root.trans.noarg('resumePractice')), - ]), + h('strong', i18n.site.youBrowsedAway), + h('div.choices', [h('a', { hook: bind('click', ctrl.resume, ctrl.redraw) }, i18n.site.resumePractice)]), ]), ]); } @@ -46,12 +43,12 @@ function renderEnd(root: AnalyseCtrl, end: Outcome): VNode { return h('div.player', [ color ? h('div.no-square', h('piece.king.' + color)) : h('div.icon.off', '!'), h('div.instruction', [ - h('strong', root.trans.noarg(end.winner ? 'checkmate' : 'draw')), + h('strong', end.winner ? i18n.site.checkmate : i18n.site.draw), end.winner - ? h('em', h('color', root.trans.noarg(end.winner === 'white' ? 'whiteWinsGame' : 'blackWinsGame'))) + ? h('em', h('color', i18n.site[end.winner === 'white' ? 'whiteWinsGame' : 'blackWinsGame'])) : isFiftyMoves - ? root.trans.noarg('drawByFiftyMoves') - : h('em', root.trans.noarg('theGameIsADraw')), + ? i18n.site.drawByFiftyMoves + : h('em', i18n.site.theGameIsADraw), ]), ]); } @@ -78,9 +75,9 @@ function renderRunning(root: AnalyseCtrl, ctrl: PracticeCtrl): VNode { h( 'div.instruction', (ctrl.isMyTurn() - ? [h('strong', root.trans.noarg('yourTurn'))] + ? [h('strong', i18n.site.yourTurn)] : [ - h('strong', root.trans.noarg('computerThinking')), + h('strong', i18n.site.computerThinking), renderEvalProgress(ctrl.currentNode(), ctrl.playableDepth()), ] ).concat( @@ -89,9 +86,11 @@ function renderRunning(root: AnalyseCtrl, ctrl: PracticeCtrl): VNode { ? h( 'a', { hook: bind('click', () => root.practice!.hint(), ctrl.redraw) }, - root.trans.noarg( - hint ? (hint.mode === 'piece' ? 'seeBestMove' : 'hideBestMove') : 'getAHint', - ), + hint + ? hint.mode === 'piece' + ? i18n.site.seeBestMove + : i18n.site.hideBestMove + : i18n.site.getAHint, ) : '', ]), @@ -108,20 +107,26 @@ export default function (root: AnalyseCtrl): VNode | undefined { const running: boolean = ctrl.running(); const end = ctrl.currentNode().threefold || isFiftyMoves ? { winner: undefined } : root.outcome(); return h('div.practice-box.training-box.sub-box.' + (comment ? comment.verdict : 'no-verdict'), [ - h('div.title', root.trans.noarg('practiceWithComputer')), + h('div.title', i18n.site.practiceWithComputer), h( 'div.feedback', - !running ? renderOffTrack(root, ctrl) : end ? renderEnd(root, end) : renderRunning(root, ctrl), + !running ? renderOffTrack(ctrl) : end ? renderEnd(root, end) : renderRunning(root, ctrl), ), running ? h( 'div.comment', (end && !root.study?.practice ? renderNextChapter(root) : null) || (comment - ? ([h('span.verdict', root.trans.noarg(comment.verdict)), ' '] as MaybeVNodes).concat( - commentBest(comment, root, ctrl), - ) - : [ctrl.isMyTurn() || end ? '' : h('span.wait', root.trans.noarg('evaluatingYourMove'))]), + ? ( + [ + h( + 'span.verdict', + comment.verdict === 'goodMove' ? i18n.study.goodMove : i18n.site[comment!.verdict], + ), + ' ', + ] as MaybeVNodes + ).concat(commentBest(comment, ctrl)) + : [ctrl.isMyTurn() || end ? '' : h('span.wait', i18n.site.evaluatingYourMove)]), ) : null, ]); diff --git a/ui/analyse/src/retrospect/retroCtrl.ts b/ui/analyse/src/retrospect/retroCtrl.ts index 787fc9a23cdf4..6679a6e8e1388 100644 --- a/ui/analyse/src/retrospect/retroCtrl.ts +++ b/ui/analyse/src/retrospect/retroCtrl.ts @@ -9,8 +9,6 @@ import { Redraw } from '../interfaces'; export interface RetroCtrl { isSolving(): boolean; - trans: Trans; - noarg: TransNoArg; current: Prop; feedback: Prop; color: Color; @@ -254,8 +252,6 @@ export function make(root: AnalyseCtrl, color: Color): RetroCtrl { return isSolving() && !!cur && root.path == cur.prev.path; }, close: root.toggleRetro, - trans: root.trans, - noarg: root.trans.noarg, node: () => root.node, redraw, }; diff --git a/ui/analyse/src/retrospect/retroView.ts b/ui/analyse/src/retrospect/retroView.ts index a3c238a720fc5..438c5de8807be 100644 --- a/ui/analyse/src/retrospect/retroView.ts +++ b/ui/analyse/src/retrospect/retroView.ts @@ -8,15 +8,15 @@ import { h, VNode } from 'snabbdom'; function skipOrViewSolution(ctrl: RetroCtrl) { return h('div.choices', [ - h('a', { hook: bind('click', ctrl.viewSolution, ctrl.redraw) }, ctrl.noarg('viewTheSolution')), - h('a', { hook: bind('click', ctrl.skip) }, ctrl.noarg('skipThisMove')), + h('a', { hook: bind('click', ctrl.viewSolution, ctrl.redraw) }, i18n.site.viewTheSolution), + h('a', { hook: bind('click', ctrl.skip) }, i18n.site.skipThisMove), ]); } function jumpToNext(ctrl: RetroCtrl) { return h('a.half.continue', { hook: bind('click', ctrl.jumpToNext) }, [ h('i', { attrs: dataIcon(licon.PlayTriangle) }), - ctrl.noarg('next'), + i18n.site.next, ]); } @@ -44,8 +44,7 @@ const feedback = { h('div.instruction', [ h( 'strong', - ctrl.trans.vdom( - 'xWasPlayed', + i18n.site.xWasPlayed.asArray( h( 'move', renderIndexAndMove( @@ -55,7 +54,7 @@ const feedback = { ), ), ), - h('em', ctrl.noarg(ctrl.color === 'white' ? 'findBetterMoveForWhite' : 'findBetterMoveForBlack')), + h('em', i18n.site[ctrl.color === 'white' ? 'findBetterMoveForWhite' : 'findBetterMoveForBlack']), skipOrViewSolution(ctrl), ]), ]), @@ -67,10 +66,8 @@ const feedback = { h('div.player', [ h('div.icon.off', '!'), h('div.instruction', [ - h('strong', ctrl.noarg('youBrowsedAway')), - h('div.choices.off', [ - h('a', { hook: bind('click', ctrl.jumpToNext) }, ctrl.noarg('resumeLearning')), - ]), + h('strong', i18n.site.youBrowsedAway), + h('div.choices.off', [h('a', { hook: bind('click', ctrl.jumpToNext) }, i18n.site.resumeLearning)]), ]), ]), ]; @@ -80,8 +77,8 @@ const feedback = { h('div.player', [ h('div.icon', '✗'), h('div.instruction', [ - h('strong', ctrl.noarg('youCanDoBetter')), - h('em', ctrl.noarg(ctrl.color === 'white' ? 'tryAnotherMoveForWhite' : 'tryAnotherMoveForBlack')), + h('strong', i18n.site.youCanDoBetter), + h('em', i18n.site[ctrl.color === 'white' ? 'tryAnotherMoveForWhite' : 'tryAnotherMoveForBlack']), skipOrViewSolution(ctrl), ]), ]), @@ -91,7 +88,7 @@ const feedback = { return [ h( 'div.half.top', - h('div.player', [h('div.icon', '✓'), h('div.instruction', h('strong', ctrl.noarg('goodMove')))]), + h('div.player', [h('div.icon', '✓'), h('div.instruction', h('strong', i18n.study.goodMove))]), ), jumpToNext(ctrl), ]; @@ -103,11 +100,10 @@ const feedback = { h('div.player', [ h('div.icon', '✓'), h('div.instruction', [ - h('strong', ctrl.noarg('solution')), + h('strong', i18n.site.solution), h( 'em', - ctrl.trans.vdom( - 'bestWasX', + i18n.site.bestWasX.asArray( h( 'strong', renderIndexAndMove({ withDots: true, showEval: false }, ctrl.current()!.solution.node), @@ -125,10 +121,7 @@ const feedback = { h( 'div.half.top', h('div.player.center', [ - h('div.instruction', [ - h('strong', ctrl.noarg('evaluatingYourMove')), - renderEvalProgress(ctrl.node()), - ]), + h('div.instruction', [h('strong', i18n.site.evaluatingYourMove), renderEvalProgress(ctrl.node())]), ]), ), ]; @@ -138,7 +131,7 @@ const feedback = { return [ h( 'div.half.top', - h('div.player', [h('div.icon', spinner()), h('div.instruction', ctrl.noarg('waitingForAnalysis'))]), + h('div.player', [h('div.icon', spinner()), h('div.instruction', i18n.site.waitingForAnalysis)]), ), ]; const nothing = !ctrl.completion()[1]; @@ -148,11 +141,15 @@ const feedback = { h('div.instruction', [ h( 'em', - nothing - ? ctrl.noarg(ctrl.color === 'white' ? 'noMistakesFoundForWhite' : 'noMistakesFoundForBlack') - : ctrl.noarg( - ctrl.color === 'white' ? 'doneReviewingWhiteMistakes' : 'doneReviewingBlackMistakes', - ), + i18n.site[ + nothing + ? ctrl.color === 'white' + ? 'noMistakesFoundForWhite' + : 'noMistakesFoundForBlack' + : ctrl.color === 'white' + ? 'doneReviewingWhiteMistakes' + : 'doneReviewingBlackMistakes' + ], ), h('div.choices.end', [ nothing @@ -163,7 +160,7 @@ const feedback = { key: 'reset', hook: bind('click', ctrl.reset), }, - ctrl.noarg('doItAgain'), + i18n.site.doItAgain, ), h( 'a', @@ -171,7 +168,7 @@ const feedback = { key: 'flip', hook: bind('click', ctrl.flip), }, - ctrl.noarg(ctrl.color === 'white' ? 'reviewBlackMistakes' : 'reviewWhiteMistakes'), + i18n.site[ctrl.color === 'white' ? 'reviewBlackMistakes' : 'reviewWhiteMistakes'], ), ]), ]), @@ -195,7 +192,7 @@ export default function (root: AnalyseCtrl): VNode | undefined { completion = ctrl.completion(); return h('div.retro-box.training-box.sub-box', [ h('div.title', [ - h('span', ctrl.noarg('learnFromYourMistakes')), + h('span', i18n.site.learnFromYourMistakes), h('span', `${Math.min(completion[0] + 1, completion[1])} / ${completion[1]}`), h('button.fbt', { hook: bind('click', root.toggleRetro, root.redraw), diff --git a/ui/analyse/src/serverSideUnderboard.ts b/ui/analyse/src/serverSideUnderboard.ts index c224508fca1af..83e7029de700a 100644 --- a/ui/analyse/src/serverSideUnderboard.ts +++ b/ui/analyse/src/serverSideUnderboard.ts @@ -78,11 +78,9 @@ export default function (element: HTMLElement, ctrl: AnalyseCtrl) { ); else if (loading && !$('#acpl-chart-container-loader').length) $panel.append(chartLoader()); site.asset.loadEsm('chart.game').then(m => { - m.acpl($('#acpl-chart')[0] as HTMLCanvasElement, data, ctrl.serverMainline(), ctrl.trans).then( - chart => { - advChart = chart; - }, - ); + m.acpl($('#acpl-chart')[0] as HTMLCanvasElement, data, ctrl.serverMainline()).then(chart => { + advChart = chart; + }); }); } @@ -97,7 +95,7 @@ export default function (element: HTMLElement, ctrl: AnalyseCtrl) { if ((panel == 'move-times' || ctrl.opts.hunter) && !timeChartLoaded) site.asset.loadEsm('chart.game').then(m => { timeChartLoaded = true; - m.movetime($('#movetimes-chart')[0] as HTMLCanvasElement, data, ctrl.trans, ctrl.opts.hunter); + m.movetime($('#movetimes-chart')[0] as HTMLCanvasElement, data, ctrl.opts.hunter); }); if ((panel == 'computer-analysis' || ctrl.opts.hunter) && $('#acpl-chart-container').length) setTimeout(startAdvantageChart, 200); @@ -122,7 +120,7 @@ export default function (element: HTMLElement, ctrl: AnalyseCtrl) { if (!data.analysis) { $panels.find('form.future-game-analysis').on('submit', function (this: HTMLFormElement) { if ($(this).hasClass('must-login')) { - if (confirm(ctrl.trans('youNeedAnAccountToDoThat'))) + if (confirm(i18n.site.youNeedAnAccountToDoThat)) location.href = '/login?referrer=' + window.location.pathname; return false; } diff --git a/ui/analyse/src/start.ts b/ui/analyse/src/start.ts index 715ede0bfcf8e..5e2f06a09b11c 100644 --- a/ui/analyse/src/start.ts +++ b/ui/analyse/src/start.ts @@ -4,7 +4,6 @@ import makeView from './view/main'; import { AnalyseApi, AnalyseOpts } from './interfaces'; import { VNode } from 'snabbdom'; import type * as studyDeps from './study/studyDeps'; -import { trans } from 'common/i18n'; export default function ( patch: (oldVnode: VNode | Element | DocumentFragment, vnode: VNode) => VNode, @@ -12,7 +11,6 @@ export default function ( ) { return function (opts: AnalyseOpts): AnalyseApi { opts.element = document.querySelector('main.analyse') as HTMLElement; - opts.trans = trans(opts.i18n); const ctrl = (site.analysis = new makeCtrl(opts, redraw, deps?.StudyCtrl)); const view = makeView(deps); diff --git a/ui/analyse/src/study/chapterEditForm.ts b/ui/analyse/src/study/chapterEditForm.ts index c4297172f9158..46e116995d7ce 100644 --- a/ui/analyse/src/study/chapterEditForm.ts +++ b/ui/analyse/src/study/chapterEditForm.ts @@ -15,7 +15,6 @@ export class StudyChapterEditForm { constructor( private readonly send: StudySocketSend, private readonly chapterConfig: (id: string) => Promise, - readonly trans: Trans, readonly redraw: Redraw, ) {} @@ -55,8 +54,7 @@ export class StudyChapterEditForm { } export function view(ctrl: StudyChapterEditForm): VNode | undefined { - const data = ctrl.current(), - noarg = ctrl.trans.noarg; + const data = ctrl.current(); return data ? snabDialog({ class: 'edit-' + data.id, // full redraw when changing chapter @@ -65,7 +63,7 @@ export function view(ctrl: StudyChapterEditForm): VNode | undefined { ctrl.redraw(); }, vnodes: [ - h('h2', noarg('editChapter')), + h('h2', i18n.study.editChapter), h( 'form.form3', { @@ -80,7 +78,7 @@ export function view(ctrl: StudyChapterEditForm): VNode | undefined { }, [ h('div.form-group', [ - h('label.form-label', { attrs: { for: 'chapter-name' } }, noarg('name')), + h('label.form-label', { attrs: { for: 'chapter-name' } }, i18n.site.name), h('input#chapter-name.form-control', { attrs: { minlength: 2, maxlength: 80 }, hook: onInsert(el => { @@ -105,37 +103,36 @@ const isLoaded = (data: ChapterPreview | StudyChapterConfig): data is StudyChapt function viewLoaded(ctrl: StudyChapterEditForm, data: StudyChapterConfig): VNode[] { const mode = data.practice - ? 'practice' - : defined(data.conceal) - ? 'conceal' - : data.gamebook - ? 'gamebook' - : 'normal', - noarg = ctrl.trans.noarg; + ? 'practice' + : defined(data.conceal) + ? 'conceal' + : data.gamebook + ? 'gamebook' + : 'normal'; return [ h('div.form-split', [ h('div.form-group.form-half', [ - h('label.form-label', { attrs: { for: 'chapter-orientation' } }, noarg('orientation')), + h('label.form-label', { attrs: { for: 'chapter-orientation' } }, i18n.study.orientation), h( 'select#chapter-orientation.form-control', - ['white', 'black'].map(color => option(color, data.orientation, noarg(color))), + (['white', 'black'] as const).map(color => option(color, data.orientation, i18n.site[color])), ), ]), h('div.form-group.form-half', [ - h('label.form-label', { attrs: { for: 'chapter-mode' } }, noarg('analysisMode')), + h('label.form-label', { attrs: { for: 'chapter-mode' } }, i18n.study.analysisMode), h( 'select#chapter-mode.form-control', - chapterForm.modeChoices.map(c => option(c[0], mode, noarg(c[1]))), + chapterForm.modeChoices.map(c => option(c[0], mode, c[1])), ), ]), ]), h('div.form-group', [ - h('label.form-label', { attrs: { for: 'chapter-description' } }, noarg('pinnedChapterComment')), + h('label.form-label', { attrs: { for: 'chapter-description' } }, i18n.study.pinnedChapterComment), h( 'select#chapter-description.form-control', [ - ['', noarg('noPinnedComment')], - ['1', noarg('rightUnderTheBoard')], + ['', i18n.study.noPinnedComment], + ['1', i18n.study.rightUnderTheBoard], ].map(v => option(v[0], data.description ? '1' : '', v[1])), ), ]), @@ -146,13 +143,13 @@ function viewLoaded(ctrl: StudyChapterEditForm, data: StudyChapterConfig): VNode hook: bind( 'click', () => { - if (confirm(noarg('clearAllCommentsInThisChapter'))) ctrl.clearAnnotations(data.id); + if (confirm(i18n.study.clearAllCommentsInThisChapter)) ctrl.clearAnnotations(data.id); }, ctrl.redraw, ), - attrs: { type: 'button', title: noarg('clearAllCommentsInThisChapter') }, + attrs: { type: 'button', title: i18n.study.clearAllCommentsInThisChapter }, }, - noarg('clearAnnotations'), + i18n.study.clearAnnotations, ), h( emptyRedButton, @@ -160,13 +157,13 @@ function viewLoaded(ctrl: StudyChapterEditForm, data: StudyChapterConfig): VNode hook: bind( 'click', () => { - if (confirm(noarg('clearVariations'))) ctrl.clearVariations(data.id); + if (confirm(i18n.study.clearVariations)) ctrl.clearVariations(data.id); }, ctrl.redraw, ), attrs: { type: 'button' }, }, - noarg('clearVariations'), + i18n.study.clearVariations, ), ]), h('div.form-actions', [ @@ -176,15 +173,15 @@ function viewLoaded(ctrl: StudyChapterEditForm, data: StudyChapterConfig): VNode hook: bind( 'click', () => { - if (confirm(noarg('deleteThisChapter'))) ctrl.delete(data.id); + if (confirm(i18n.study.deleteThisChapter)) ctrl.delete(data.id); }, ctrl.redraw, ), - attrs: { type: 'button', title: noarg('deleteThisChapter') }, + attrs: { type: 'button', title: i18n.study.deleteThisChapter }, }, - noarg('deleteChapter'), + i18n.study.deleteChapter, ), - h('button.button', { attrs: { type: 'submit' } }, noarg('saveChapter')), + h('button.button', { attrs: { type: 'submit' } }, i18n.study.saveChapter), ]), ]; } diff --git a/ui/analyse/src/study/chapterNewForm.ts b/ui/analyse/src/study/chapterNewForm.ts index 5583e285eaacb..580c90f3fd52b 100644 --- a/ui/analyse/src/study/chapterNewForm.ts +++ b/ui/analyse/src/study/chapterNewForm.ts @@ -18,10 +18,10 @@ import type { LichessEditor } from 'editor'; import { pubsub } from 'common/pubsub'; export const modeChoices = [ - ['normal', 'normalAnalysis'], - ['practice', 'practiceWithComputer'], - ['conceal', 'hideNextMoves'], - ['gamebook', 'interactiveLesson'], + ['normal', i18n.study.normalAnalysis], + ['practice', i18n.site.practiceWithComputer], + ['conceal', i18n.study.hideNextMoves], + ['gamebook', i18n.study.interactiveLesson], ]; export const fieldValue = (e: Event, id: string) => @@ -95,8 +95,7 @@ export class StudyChapterNewForm { } export function view(ctrl: StudyChapterNewForm): VNode { - const trans = ctrl.root.trans, - study = ctrl.root.study!; + const study = ctrl.root.study!; const activeTab = ctrl.tab(); const makeTab = (key: ChapterTab, name: string, title: string) => h( @@ -127,7 +126,6 @@ export function view(ctrl: StudyChapterNewForm): VNode { : currentChapter.gamebook ? 'gamebook' : 'normal'; - const noarg = trans.noarg; return snabDialog({ class: 'chapter-new', @@ -140,7 +138,7 @@ export function view(ctrl: StudyChapterNewForm): VNode { vnodes: [ activeTab !== 'edit' && h('h2', [ - noarg('newChapter'), + i18n.study.newChapter, h('i.help', { attrs: { 'data-icon': licon.InfoCircle }, hook: bind('click', ctrl.startTour) }), ]), h( @@ -163,12 +161,12 @@ export function view(ctrl: StudyChapterNewForm): VNode { }, [ h('div.form-group', [ - h('label.form-label', { attrs: { for: 'chapter-name' } }, noarg('name')), + h('label.form-label', { attrs: { for: 'chapter-name' } }, i18n.site.name), h('input#chapter-name.form-control', { attrs: { minlength: 2, maxlength: 80 }, hook: onInsert(el => { if (!el.value) { - el.value = trans('chapterX', ctrl.initial() ? 1 : ctrl.chapters.size() + 1); + el.value = i18n.study.chapterX(ctrl.initial() ? 1 : ctrl.chapters.size() + 1); el.onchange = () => ctrl.isDefaultName(false); el.select(); } @@ -179,11 +177,11 @@ export function view(ctrl: StudyChapterNewForm): VNode { }), ]), h('div.tabs-horiz', { attrs: { role: 'tablist' } }, [ - makeTab('init', noarg('empty'), noarg('startFromInitialPosition')), - makeTab('edit', noarg('editor'), noarg('startFromCustomPosition')), - makeTab('game', 'URL', noarg('loadAGameByUrl')), - makeTab('fen', 'FEN', noarg('loadAPositionFromFen')), - makeTab('pgn', 'PGN', noarg('loadAGameFromPgn')), + makeTab('init', i18n.study.empty, i18n.study.startFromInitialPosition), + makeTab('edit', i18n.study.editor, i18n.study.startFromCustomPosition), + makeTab('game', 'URL', i18n.study.loadAGameByUrl), + makeTab('fen', 'FEN', i18n.study.loadAPositionFromFen), + makeTab('pgn', 'PGN', i18n.study.loadAGameFromPgn), ]), activeTab === 'edit' && h( @@ -215,10 +213,10 @@ export function view(ctrl: StudyChapterNewForm): VNode { h( 'label.form-label', { attrs: { for: 'chapter-game' } }, - trans('loadAGameFromXOrY', 'lichess.org', 'chessgames.com'), + i18n.study.loadAGameFromXOrY('lichess.org', 'chessgames.com'), ), h('textarea#chapter-game.form-control', { - attrs: { placeholder: noarg('urlOfTheGame') }, + attrs: { placeholder: i18n.study.urlOfTheGame }, hook: onInsert((el: HTMLTextAreaElement) => { el.addEventListener('change', () => el.reportValidity()); el.addEventListener('input', () => { @@ -244,7 +242,7 @@ export function view(ctrl: StudyChapterNewForm): VNode { h('input#chapter-fen.form-control', { attrs: { value: ctrl.root.node.fen, - placeholder: noarg('loadAPositionFromFen'), + placeholder: i18n.study.loadAPositionFromFen, spellcheck: 'false', }, hook: onInsert((el: HTMLInputElement) => { @@ -262,14 +260,14 @@ export function view(ctrl: StudyChapterNewForm): VNode { { hook: bind('click', () => ctrl.tab('edit'), ctrl.root.redraw), }, - [h('i.text', { attrs: dataIcon(licon.Eye) }), noarg('editor')], + [h('i.text', { attrs: dataIcon(licon.Eye) }), i18n.study.editor], ), ]), activeTab === 'pgn' && h('div.form-group', [ h('textarea#chapter-pgn.form-control', { attrs: { - placeholder: trans.pluralSame('pasteYourPgnTextHereUpToNbGames', ctrl.multiPgnMax), + placeholder: i18n.study.pasteYourPgnTextHereUpToNbGames(ctrl.multiPgnMax), }, }), h( @@ -288,7 +286,7 @@ export function view(ctrl: StudyChapterNewForm): VNode { false, ), }, - trans('importFromChapterX', study.currentChapter().name), + i18n.study.importFromChapterX(study.currentChapter().name), ), window.FileReader && h('input#chapter-pgn-file.form-control', { @@ -307,17 +305,17 @@ export function view(ctrl: StudyChapterNewForm): VNode { ]), h('div.form-split', [ h('div.form-group.form-half', [ - h('label.form-label', { attrs: { for: 'chapter-variant' } }, noarg('Variant')), + h('label.form-label', { attrs: { for: 'chapter-variant' } }, i18n.site.variant), h( 'select#chapter-variant.form-control', { attrs: { disabled: gameOrPgn } }, gameOrPgn - ? [h('option', { attrs: { value: 'standard' } }, noarg('automatic'))] + ? [h('option', { attrs: { value: 'standard' } }, i18n.study.automatic)] : ctrl.variants.map(v => option(v.key, currentChapter.setup.variant.key, v.name)), ), ]), h('div.form-group.form-half', [ - h('label.form-label', { attrs: { for: 'chapter-orientation' } }, noarg('orientation')), + h('label.form-label', { attrs: { for: 'chapter-orientation' } }, i18n.study.orientation), h( 'select#chapter-orientation.form-control', { @@ -325,22 +323,22 @@ export function view(ctrl: StudyChapterNewForm): VNode { ctrl.editor?.setOrientation((e.target as HTMLInputElement).value as Color), ), }, - [...(activeTab === 'pgn' ? ['automatic'] : []), 'white', 'black'].map(c => - option(c, currentChapter.setup.orientation, noarg(c)), + [activeTab === 'pgn' && i18n.study.automatic, i18n.site.white, i18n.site.black].map( + c => c && option(c, currentChapter.setup.orientation, c), ), ), ]), ]), h('div.form-group', [ - h('label.form-label', { attrs: { for: 'chapter-mode' } }, noarg('analysisMode')), + h('label.form-label', { attrs: { for: 'chapter-mode' } }, i18n.study.analysisMode), h( 'select#chapter-mode.form-control', - modeChoices.map(c => option(c[0], mode, noarg(c[1]))), + modeChoices.map(c => option(c[0], mode, c[1])), ), ]), h( 'div.form-actions.single', - h('button.button', { attrs: { type: 'submit' } }, noarg('createChapter')), + h('button.button', { attrs: { type: 'submit' } }, i18n.study.createChapter), ), ], ), diff --git a/ui/analyse/src/study/gamebook/gamebookButtons.ts b/ui/analyse/src/study/gamebook/gamebookButtons.ts index 648abbd9a854e..7f5d2c1edd648 100644 --- a/ui/analyse/src/study/gamebook/gamebookButtons.ts +++ b/ui/analyse/src/study/gamebook/gamebookButtons.ts @@ -19,7 +19,7 @@ export function playButtons(root: AnalyseCtrl): VNode | undefined { attrs: { 'data-icon': licon.LessThan, type: 'button' }, hook: bind('click', () => root.userJump(''), ctrl.redraw), }, - root.trans.noarg('back'), + i18n.study.back, ), myTurn && h( @@ -28,7 +28,7 @@ export function playButtons(root: AnalyseCtrl): VNode | undefined { attrs: { 'data-icon': licon.PlayTriangle, type: 'button' }, hook: bind('click', ctrl.solution, ctrl.redraw), }, - root.trans.noarg('viewTheSolution'), + i18n.site.viewTheSolution, ), overrideButton(study), ]); @@ -66,7 +66,7 @@ export function overrideButton(study: StudyCtrl): VNode | undefined { study.redraw, ), }, - study.trans.noarg('analysis'), + i18n.site.analysis, ); } } diff --git a/ui/analyse/src/study/gamebook/gamebookPlayCtrl.ts b/ui/analyse/src/study/gamebook/gamebookPlayCtrl.ts index 20193409b3244..3d49b88b190d3 100644 --- a/ui/analyse/src/study/gamebook/gamebookPlayCtrl.ts +++ b/ui/analyse/src/study/gamebook/gamebookPlayCtrl.ts @@ -18,7 +18,6 @@ export default class GamebookPlayCtrl { constructor( readonly root: AnalyseCtrl, readonly chapterId: string, - readonly trans: Trans, readonly redraw: () => void, ) { this.makeState(); diff --git a/ui/analyse/src/study/gamebook/gamebookPlayView.ts b/ui/analyse/src/study/gamebook/gamebookPlayView.ts index 884439a1327b8..ff9b60822cef8 100644 --- a/ui/analyse/src/study/gamebook/gamebookPlayView.ts +++ b/ui/analyse/src/study/gamebook/gamebookPlayView.ts @@ -14,8 +14,8 @@ export function render(ctrl: GamebookPlayCtrl): VNode { : h( 'div.content', state.feedback == 'play' - ? ctrl.trans('whatWouldYouPlay') - : state.feedback == 'end' && ctrl.trans('youCompletedThisLesson'), + ? i18n.study.whatWouldYouPlay + : state.feedback == 'end' && i18n.study.youCompletedThisLesson, ), hintZone(ctrl), ]), @@ -32,7 +32,7 @@ function hintZone(ctrl: GamebookPlayCtrl) { const state = ctrl.state, buttonData = () => ({ attrs: { type: 'button' }, hook: bind('click', ctrl.hint, ctrl.redraw) }); if (state.showHint) return h('button', buttonData(), [h('div.hint', { hook: richHTML(state.hint!) })]); - if (state.hint) return h('button.hint', buttonData(), ctrl.trans.noarg('getAHint')); + if (state.hint) return h('button.hint', buttonData(), i18n.site.getAHint); return undefined; } @@ -43,11 +43,11 @@ function renderFeedback(ctrl: GamebookPlayCtrl, state: State) { return h( 'button.feedback.act.bad' + (state.comment ? '.com' : ''), { attrs: { type: 'button' }, hook: bind('click', ctrl.retry) }, - [iconTag(licon.Reload), h('span', ctrl.trans.noarg('retry'))], + [iconTag(licon.Reload), h('span', i18n.site.retry)], ); if (fb === 'good' && state.comment) return h('button.feedback.act.good.com', { attrs: { type: 'button' }, hook: bind('click', ctrl.next) }, [ - h('span.text', { attrs: dataIcon(licon.PlayTriangle) }, ctrl.trans.noarg('next')), + h('span.text', { attrs: dataIcon(licon.PlayTriangle) }, i18n.study.next), h('kbd', ''), ]); if (fb === 'end') return renderEnd(ctrl); @@ -59,14 +59,11 @@ function renderFeedback(ctrl: GamebookPlayCtrl, state: State) { ? [ h('div.no-square', h('piece.king.' + color)), h('div.instruction', [ - h('strong', ctrl.trans.noarg('yourTurn')), - h( - 'em', - ctrl.trans.noarg(color === 'white' ? 'findTheBestMoveForWhite' : 'findTheBestMoveForBlack'), - ), + h('strong', i18n.site.yourTurn), + h('em', i18n.puzzle[color === 'white' ? 'findTheBestMoveForWhite' : 'findTheBestMoveForBlack']), ]), ] - : ctrl.trans.noarg('goodMove'), + : i18n.study.goodMove, ), ); } @@ -81,7 +78,7 @@ function renderEnd(ctrl: GamebookPlayCtrl) { attrs: { 'data-icon': licon.PlayTriangle, type: 'button' }, hook: bind('click', study.goToNextChapter), }, - study.trans.noarg('nextChapter'), + i18n.study.nextChapter, ), h( 'button.retry', @@ -89,7 +86,7 @@ function renderEnd(ctrl: GamebookPlayCtrl) { attrs: { 'data-icon': licon.Reload, type: 'button' }, hook: bind('click', () => ctrl.root.userJump(''), ctrl.redraw), }, - study.trans.noarg('playAgain'), + i18n.study.playAgain, ), h( 'button.analyse', @@ -97,7 +94,7 @@ function renderEnd(ctrl: GamebookPlayCtrl) { attrs: { 'data-icon': licon.Microscope, type: 'button' }, hook: bind('click', () => study.setGamebookOverride('analyse'), ctrl.redraw), }, - study.trans.noarg('analysis'), + i18n.site.analysis, ), ]); } diff --git a/ui/analyse/src/study/inviteForm.ts b/ui/analyse/src/study/inviteForm.ts index 69c022d6ab1cd..562820b6cf26a 100644 --- a/ui/analyse/src/study/inviteForm.ts +++ b/ui/analyse/src/study/inviteForm.ts @@ -17,7 +17,6 @@ export interface StudyInviteFormCtrl { toggle(): void; invite(titleName: string): void; redraw(): void; - trans: Trans; previouslyInvited: StoredSet; } @@ -26,7 +25,6 @@ export function makeCtrl( members: Prop, setTab: () => void, redraw: () => void, - trans: Trans, ): StudyInviteFormCtrl { const open = prop(false), spectators = prop([]); @@ -52,7 +50,6 @@ export function makeCtrl( setTab(); }, redraw, - trans, previouslyInvited, }; } @@ -69,16 +66,12 @@ export function view(ctrl: ReturnType): VNode { }, noScrollable: true, vnodes: [ - h('h2', ctrl.trans.noarg('inviteToTheStudy')), - h( - 'p.info', - { attrs: { 'data-icon': licon.InfoCircle } }, - ctrl.trans.noarg('pleaseOnlyInvitePeopleYouKnow'), - ), + h('h2', i18n.study.inviteToTheStudy), + h('p.info', { attrs: { 'data-icon': licon.InfoCircle } }, i18n.study.pleaseOnlyInvitePeopleYouKnow), h('div.input-wrapper', [ // because typeahead messes up with snabbdom h('input', { - attrs: { placeholder: ctrl.trans.noarg('searchByUsername'), spellcheck: 'false' }, + attrs: { placeholder: i18n.study.searchByUsername, spellcheck: 'false' }, hook: onInsert(input => userComplete({ input, diff --git a/ui/analyse/src/study/multiBoard.ts b/ui/analyse/src/study/multiBoard.ts index 83d0e4c720791..d39e46e7babed 100644 --- a/ui/analyse/src/study/multiBoard.ts +++ b/ui/analyse/src/study/multiBoard.ts @@ -26,7 +26,6 @@ export class MultiBoardCtrl { readonly multiCloudEval: MultiCloudEval | undefined, private readonly initialTeamSelect: ChapterId | undefined, readonly redraw: () => void, - readonly trans: Trans, ) { this.playing = toggle(false, this.redraw); if (this.initialTeamSelect) this.onChapterChange(this.initialTeamSelect); @@ -99,7 +98,7 @@ export function view(ctrl: MultiBoardCtrl, study: StudyCtrl): MaybeVNode { renderPagerNav(pager, ctrl), h('div.study__multiboard__options', [ ctrl.multiCloudEval && - h('label.eval', [renderEvalToggle(ctrl.multiCloudEval), ctrl.trans.noarg('showEvalBar')]), + h('label.eval', [renderEvalToggle(ctrl.multiCloudEval), i18n.study.showEvalBar]), renderPlayingToggle(ctrl), ]), ]), @@ -121,11 +120,11 @@ function renderPagerNav(pager: Paginator, ctrl: MultiBoardCtrl): to = Math.min(pager.nbResults, page * pager.maxPerPage), max = ctrl.maxPerPage(); return h('div.study__multiboard__pager', [ - pagerButton('first', licon.JumpFirst, () => ctrl.setPage(1), page > 1, ctrl), - pagerButton('previous', licon.JumpPrev, ctrl.prevPage, page > 1, ctrl), + pagerButton(i18n.study.first, licon.JumpFirst, () => ctrl.setPage(1), page > 1, ctrl), + pagerButton(i18n.study.previous, licon.JumpPrev, ctrl.prevPage, page > 1, ctrl), h('span.page', `${from}-${to} / ${pager.nbResults}`), - pagerButton('next', licon.JumpNext, ctrl.nextPage, page < pager.nbPages, ctrl), - pagerButton('last', licon.JumpLast, ctrl.lastPage, page < pager.nbPages, ctrl), + pagerButton(i18n.study.next, licon.JumpNext, ctrl.nextPage, page < pager.nbPages, ctrl), + pagerButton(i18n.study.last, licon.JumpLast, ctrl.lastPage, page < pager.nbPages, ctrl), teamSelector(ctrl), h( 'select.study__multiboard__pager__max-per-page', @@ -154,14 +153,14 @@ const teamSelector = (ctrl: MultiBoardCtrl) => { }; function pagerButton( - transKey: string, + text: string, icon: string, click: () => void, enable: boolean, ctrl: MultiBoardCtrl, ): VNode { return h('button.fbt', { - attrs: { 'data-icon': icon, disabled: !enable, title: ctrl.trans.noarg(transKey) }, + attrs: { 'data-icon': icon, disabled: !enable, title: text }, hook: bind('mousedown', click, ctrl.redraw), }); } @@ -172,7 +171,7 @@ const renderPlayingToggle = (ctrl: MultiBoardCtrl): MaybeVNode => attrs: { type: 'checkbox', checked: ctrl.playing() }, hook: bind('change', e => ctrl.playing((e.target as HTMLInputElement).checked)), }), - ctrl.trans.noarg('playing'), + i18n.study.playing, ]); const previewToCgConfig = (cp: ChapterPreview): CgConfig => ({ diff --git a/ui/analyse/src/study/nextChapter.ts b/ui/analyse/src/study/nextChapter.ts index 8979f85f042a2..186dd69a68539 100644 --- a/ui/analyse/src/study/nextChapter.ts +++ b/ui/analyse/src/study/nextChapter.ts @@ -13,6 +13,6 @@ export const renderNextChapter = (ctrl: AnalyseCtrl) => hook: bind('click', ctrl.study.goToNextChapter), class: { highlighted: !!ctrl.outcome() || ctrl.node == treeOps.last(ctrl.mainline) }, }, - ctrl.trans.noarg('nextChapter'), + i18n.study.nextChapter, ) : null; diff --git a/ui/analyse/src/study/practice/studyPracticeView.ts b/ui/analyse/src/study/practice/studyPracticeView.ts index e1f1a5b81d996..bed9e04922f48 100644 --- a/ui/analyse/src/study/practice/studyPracticeView.ts +++ b/ui/analyse/src/study/practice/studyPracticeView.ts @@ -87,7 +87,6 @@ export function underboard(ctrl: StudyCtrl): MaybeVNodes { checked: p.autoNext(), change: p.autoNext, }, - ctrl.trans, ctrl.redraw, ), ]; diff --git a/ui/analyse/src/study/relay/relayTourView.ts b/ui/analyse/src/study/relay/relayTourView.ts index 7598a352ec8b1..44cc2d3e882e5 100644 --- a/ui/analyse/src/study/relay/relayTourView.ts +++ b/ui/analyse/src/study/relay/relayTourView.ts @@ -202,9 +202,9 @@ const share = (ctx: RelayViewContext) => { ], ['Embed this broadcast in your website', iframe(ctx.relay.tourPath()), iframeHelp], [`Embed ${roundName} in your website`, iframe(ctx.relay.roundPath()), iframeHelp], - ].map(([i18n, path, help]: [string, string, VNode]) => + ].map(([text, path, help]: [string, string, VNode]) => h('div.form-group', [ - h('label.form-label', ctx.ctrl.trans.noarg(i18n)), + h('label.form-label', text), copyMeInput(path.startsWith('/') ? `${baseUrl()}${path}` : path), help, ]), @@ -375,7 +375,7 @@ const subscribe = (relay: RelayCtrl, ctrl: AnalyseCtrl) => ? [ toggle( { - name: 'Subscribe', + name: i18n.site.subscribe, id: 'tour-subscribe', title: 'Subscribe to be notified when each round starts. You can toggle bell or push ' + @@ -388,7 +388,6 @@ const subscribe = (relay: RelayCtrl, ctrl: AnalyseCtrl) => ctrl.redraw(); }, }, - ctrl.trans, ctrl.redraw, ), ] diff --git a/ui/analyse/src/study/serverEval.ts b/ui/analyse/src/study/serverEval.ts index 622fa9af00bd8..13936529c30dd 100644 --- a/ui/analyse/src/study/serverEval.ts +++ b/ui/analyse/src/study/serverEval.ts @@ -44,7 +44,7 @@ export function view(ctrl: ServerEval): VNode { hook: onInsert(el => { requestIdleCallback(async () => { (await site.asset.loadEsm('chart.game')) - .acpl(el as HTMLCanvasElement, ctrl.root.data, mainline, ctrl.root.trans) + .acpl(el as HTMLCanvasElement, ctrl.root.data, mainline) .then(chart => (ctrl.chart = chart)); }, 800); }), @@ -60,23 +60,22 @@ const disabled = () => h('div.study__server-eval.disabled.padded', 'You disabled const requested = () => h('div.study__server-eval.requested.padded', spinnerVdom()); function requestButton(ctrl: ServerEval) { - const root = ctrl.root, - noarg = root.trans.noarg; + const root = ctrl.root; return h( 'div.study__message', root.mainline.length < 5 - ? h('p', noarg('theChapterIsTooShortToBeAnalysed')) + ? h('p', i18n.study.theChapterIsTooShortToBeAnalysed) : !root.study!.members.canContribute() - ? [noarg('onlyContributorsCanRequestAnalysis')] + ? [i18n.study.onlyContributorsCanRequestAnalysis] : [ - h('p', [noarg('getAFullComputerAnalysis'), h('br'), noarg('makeSureTheChapterIsComplete')]), + h('p', [i18n.study.getAFullComputerAnalysis, h('br'), i18n.study.makeSureTheChapterIsComplete]), h( 'a.button.text', { attrs: { 'data-icon': licon.BarChart, disabled: root.mainline.length < 5 }, hook: bind('click', ctrl.request, root.redraw), }, - noarg('requestAComputerAnalysis'), + i18n.site.requestAComputerAnalysis, ), ], ); diff --git a/ui/analyse/src/study/studyChapters.ts b/ui/analyse/src/study/studyChapters.ts index bdb292fd0bfdb..4e2f3abc61cd2 100644 --- a/ui/analyse/src/study/studyChapters.ts +++ b/ui/analyse/src/study/studyChapters.ts @@ -63,7 +63,7 @@ export default class StudyChaptersCtrl { this.list = new StudyChapters(this.store); this.loadFromServer(initChapters); this.newForm = new StudyChapterNewForm(send, this.list, setTab, root); - this.editForm = new StudyChapterEditForm(send, chapterConfig, root.trans, root.redraw); + this.editForm = new StudyChapterEditForm(send, chapterConfig, root.redraw); } sort = (ids: string[]) => this.send('sortChapters', ids); @@ -241,7 +241,7 @@ export function view(ctrl: StudyCtrl): VNode { ? [ h('button.add', { hook: bind('click', ctrl.chapters.toggleNewForm, ctrl.redraw) }, [ h('span', iconTag(licon.PlusButton)), - h('h3', ctrl.trans.noarg('addNewChapter')), + h('h3', i18n.study.addNewChapter), ]), ] : [], diff --git a/ui/analyse/src/study/studyCtrl.ts b/ui/analyse/src/study/studyCtrl.ts index beea1d2a33103..d791c700cb024 100644 --- a/ui/analyse/src/study/studyCtrl.ts +++ b/ui/analyse/src/study/studyCtrl.ts @@ -146,7 +146,6 @@ export default class StudyCtrl { onBecomingContributor: () => (this.vm.mode.write = !relayData || this.relayRecProp()), admin: data.admin, redraw: ctrl.redraw, - trans: ctrl.trans, }); this.chapters = new StudyChaptersCtrl( data.chapters!, @@ -178,7 +177,6 @@ export default class StudyCtrl { this.multiCloudEval, this.relay?.tourShow() ? undefined : this.data.chapter.id, this.redraw, - this.ctrl.trans, ); this.form = new StudyForm( (d, isNew) => { @@ -193,7 +191,6 @@ export default class StudyCtrl { this.chapters.newForm.openInitial(); }, () => data, - ctrl.trans, this.redraw, this.relay, ); @@ -229,7 +226,6 @@ export default class StudyCtrl { this.topics = new TopicsCtrl( topics => this.send('setTopics', topics), () => data.topics || [], - ctrl.trans, this.redraw, ); @@ -241,7 +237,6 @@ export default class StudyCtrl { this.bottomColor, this.relay, this.redraw, - ctrl.trans, ); this.practice = practiceData && new StudyPracticeCtrl(ctrl, data, practiceData); @@ -412,7 +407,7 @@ export default class StudyCtrl { if (n.shapes) n.gamebook.shapes = n.shapes.slice(0); }); if (this.gamebookPlay?.chapterId === this.vm.chapterId) return; - this.gamebookPlay = new GamebookPlayCtrl(this.ctrl, this.vm.chapterId, this.ctrl.trans, this.redraw); + this.gamebookPlay = new GamebookPlayCtrl(this.ctrl, this.vm.chapterId, this.redraw); this.vm.mode.sticky = false; return undefined; }; @@ -603,7 +598,6 @@ export default class StudyCtrl { this.redraw(); this.updateAddressBar(); }; - trans = this.ctrl.trans; socketHandler = (t: string, d: any) => { const handler = (this.socketHandlers as any as SocketHandlers)[t]; if (handler) { diff --git a/ui/analyse/src/study/studyForm.ts b/ui/analyse/src/study/studyForm.ts index b887c711f2036..1e493690690d4 100644 --- a/ui/analyse/src/study/studyForm.ts +++ b/ui/analyse/src/study/studyForm.ts @@ -37,7 +37,6 @@ export class StudyForm { constructor( private readonly doSave: (data: FormData, isNew: boolean) => void, readonly getData: () => StudyData, - readonly trans: Trans, readonly redraw: Redraw, readonly relay?: RelayCtrl, ) {} @@ -77,11 +76,11 @@ export function view(ctrl: StudyForm): VNode { } }; const userSelectionChoices: Choice[] = [ - ['nobody', ctrl.trans.noarg('nobody')], - ['owner', ctrl.trans.noarg('onlyMe')], - ['contributor', ctrl.trans.noarg('contributors')], - ['member', ctrl.trans.noarg('members')], - ['everyone', ctrl.trans.noarg('everyone')], + ['nobody', i18n.study.nobody], + ['owner', i18n.study.onlyMe], + ['contributor', i18n.study.contributors], + ['member', i18n.study.members], + ['everyone', i18n.study.everyone], ]; return snabDialog({ class: 'study-edit', @@ -91,7 +90,10 @@ export function view(ctrl: StudyForm): VNode { }, noClickAway: true, vnodes: [ - h('h2', ctrl.trans.noarg(ctrl.relay ? 'editRoundStudy' : isNew ? 'createStudy' : 'editStudy')), + h( + 'h2', + ctrl.relay ? i18n.broadcast.editRoundStudy : isNew ? i18n.study.createStudy : i18n.study.editStudy, + ), h( 'form.form3', { @@ -146,7 +148,7 @@ export function view(ctrl: StudyForm): VNode { data.flair && h(removeEmojiButton, 'Delete'), ]), h('div.form-group', [ - h('label.form-label', { attrs: { for: 'study-name' } }, ctrl.trans.noarg('name')), + h('label.form-label', { attrs: { for: 'study-name' } }, i18n.site.name), h('input#study-name.form-control', { attrs: { minlength: 3, maxlength: 100 }, hook: { @@ -165,17 +167,17 @@ export function view(ctrl: StudyForm): VNode { h('div.form-split', [ select({ key: 'visibility', - name: ctrl.trans.noarg('visibility'), + name: i18n.study.visibility, choices: [ - ['public', ctrl.trans.noarg('public')], - ['unlisted', ctrl.trans.noarg('unlisted')], - ['private', ctrl.trans.noarg('inviteOnly')], + ['public', i18n.study.public], + ['unlisted', i18n.study.unlisted], + ['private', i18n.study.inviteOnly], ], selected: data.visibility, }), select({ key: 'chat', - name: ctrl.trans.noarg('chat'), + name: i18n.site.chat, choices: userSelectionChoices, selected: data.settings.chat, }), @@ -183,13 +185,13 @@ export function view(ctrl: StudyForm): VNode { h('div.form-split', [ select({ key: 'computer', - name: ctrl.trans.noarg('computerAnalysis'), - choices: userSelectionChoices.map(c => [c[0], ctrl.trans.noarg(c[1])]), + name: i18n.site.computerAnalysis, + choices: userSelectionChoices, selected: data.settings.computer, }), select({ key: 'explorer', - name: ctrl.trans.noarg('openingExplorerAndTablebase'), + name: i18n.site.openingExplorerAndTablebase, choices: userSelectionChoices, selected: data.settings.explorer, }), @@ -197,13 +199,13 @@ export function view(ctrl: StudyForm): VNode { h('div.form-split', [ select({ key: 'cloneable', - name: ctrl.trans.noarg('allowCloning'), + name: i18n.study.allowCloning, choices: userSelectionChoices, selected: data.settings.cloneable, }), select({ key: 'shareable', - name: ctrl.trans.noarg('shareAndExport'), + name: i18n.study.shareAndExport, choices: userSelectionChoices, selected: data.settings.shareable, }), @@ -211,19 +213,19 @@ export function view(ctrl: StudyForm): VNode { h('div.form-split', [ select({ key: 'sticky', - name: ctrl.trans.noarg('enableSync'), + name: i18n.study.enableSync, choices: [ - ['true', ctrl.trans.noarg('yesKeepEveryoneOnTheSamePosition')], - ['false', ctrl.trans.noarg('noLetPeopleBrowseFreely')], + ['true', i18n.study.yesKeepEveryoneOnTheSamePosition], + ['false', i18n.study.noLetPeopleBrowseFreely], ], selected: '' + data.settings.sticky, }), select({ key: 'description', - name: ctrl.trans.noarg('pinnedStudyComment'), + name: i18n.study.pinnedStudyComment, choices: [ - ['false', ctrl.trans.noarg('noPinnedComment')], - ['true', ctrl.trans.noarg('rightUnderTheBoard')], + ['false', i18n.study.noPinnedComment], + ['true', i18n.study.rightUnderTheBoard], ], selected: '' + data.settings.description, }), @@ -255,25 +257,22 @@ export function view(ctrl: StudyForm): VNode { hook: bindNonPassive( 'submit', _ => - isNew || - prompt(ctrl.trans('confirmDeleteStudy', data.name))?.trim() === data.name.trim(), + isNew || prompt(i18n.study.confirmDeleteStudy(data.name))?.trim() === data.name.trim(), ), }, - [h(emptyRedButton, ctrl.trans.noarg(isNew ? 'cancel' : 'deleteStudy'))], + [h(emptyRedButton, isNew ? i18n.site.cancel : i18n.study.deleteStudy)], ), !isNew && h( 'form', { attrs: { action: '/study/' + data.id + '/clear-chat', method: 'post' }, - hook: bindNonPassive('submit', _ => - confirm(ctrl.trans.noarg('deleteTheStudyChatHistory')), - ), + hook: bindNonPassive('submit', _ => confirm(i18n.study.deleteTheStudyChatHistory)), }, - [h(emptyRedButton, ctrl.trans.noarg('clearChat'))], + [h(emptyRedButton, i18n.study.clearChat)], ), ]), - h('button.button', { attrs: { type: 'submit' } }, ctrl.trans.noarg(isNew ? 'start' : 'save')), + h('button.button', { attrs: { type: 'submit' } }, isNew ? i18n.study.start : i18n.study.save), ]), ], ), diff --git a/ui/analyse/src/study/studyMembers.ts b/ui/analyse/src/study/studyMembers.ts index eab1cc4e290fc..da81585e803c2 100644 --- a/ui/analyse/src/study/studyMembers.ts +++ b/ui/analyse/src/study/studyMembers.ts @@ -24,7 +24,6 @@ interface Opts { onBecomingContributor(): void; admin: boolean; redraw(): void; - trans: Trans; } function memberActivity(onIdle: () => void) { @@ -48,13 +47,7 @@ export class StudyMemberCtrl { constructor(readonly opts: Opts) { this.dict = prop(opts.initDict); - this.inviteForm = inviteFormCtrl( - opts.send, - this.dict, - () => opts.tab('members'), - opts.redraw, - opts.trans, - ); + this.inviteForm = inviteFormCtrl(opts.send, this.dict, () => opts.tab('members'), opts.redraw); pubsub.on('socket.in.crowd', d => { const names: string[] = d.users || []; this.inviteForm.spectators(names); @@ -104,12 +97,12 @@ export class StudyMemberCtrl { if (once('study-tour')) this.opts.startTour(); this.opts.onBecomingContributor(); this.opts.notif.set({ - text: this.opts.trans.noarg('youAreNowAContributor'), + text: i18n.study.youAreNowAContributor, duration: 3000, }); } else if (wasContrib && !this.canContribute()) this.opts.notif.set({ - text: this.opts.trans.noarg('youAreNowASpectator'), + text: i18n.study.youAreNowASpectator, duration: 3000, }); this.updateOnline(); @@ -153,7 +146,7 @@ export function view(ctrl: StudyCtrl): VNode { active: members.active.has(member.user.id), online: members.isOnline(member.user.id), }, - attrs: { title: ctrl.trans.noarg(contrib ? 'contributor' : 'spectator') }, + attrs: { title: i18n.study[contrib ? 'contributor' : 'spectator'] }, }, [iconTag(contrib ? licon.User : licon.Eye)], ); @@ -171,7 +164,7 @@ export function view(ctrl: StudyCtrl): VNode { }); if (!isOwner && member.user.id === members.opts.myId) return h('i.act.leave', { - attrs: { 'data-icon': licon.InternalArrow, title: ctrl.trans.noarg('leaveTheStudy') }, + attrs: { 'data-icon': licon.InternalArrow, title: i18n.study.leaveTheStudy }, hook: bind('click', members.leave, ctrl.redraw), }); return undefined; @@ -198,14 +191,14 @@ export function view(ctrl: StudyCtrl): VNode { }), h('label', { attrs: { for: roleId } }), ]), - h('label', { attrs: { for: roleId } }, ctrl.trans.noarg('contributor')), + h('label', { attrs: { for: roleId } }, i18n.study.contributor), ]), h( 'div.kick', h( 'a.button.button-red.button-empty.text', { attrs: dataIcon(licon.X), hook: bind('click', _ => members.kick(member.user.id), ctrl.redraw) }, - ctrl.trans.noarg('kick'), + i18n.study.kick, ), ), ], @@ -232,7 +225,7 @@ export function view(ctrl: StudyCtrl): VNode { h('button.add', { key: 'add', hook: bind('click', members.inviteForm.toggle) }, [ h('div.left', [ h('span.status', iconTag(licon.PlusButton)), - h('div.user-link', ctrl.trans.noarg('addMembers')), + h('div.user-link', i18n.study.addMembers), ]), ]), !members.canContribute() && diff --git a/ui/analyse/src/study/studyShare.ts b/ui/analyse/src/study/studyShare.ts index 27bf036164c58..10ba9df9b1f63 100644 --- a/ui/analyse/src/study/studyShare.ts +++ b/ui/analyse/src/study/studyShare.ts @@ -20,8 +20,8 @@ function fromPly(ctrl: StudyShare): VNode { hook: bind('change', e => ctrl.withPly((e.target as HTMLInputElement).checked), ctrl.redraw), }), ...(renderedMove - ? ctrl.trans.vdom('startAtX', h('strong', renderedMove)) - : [ctrl.trans.noarg('startAtInitialPosition')]), + ? i18n.study.startAtX.asArray(h('strong', renderedMove)) + : [i18n.study.startAtInitialPosition]), ]), ); } @@ -37,7 +37,6 @@ export class StudyShare { readonly bottomColor: () => Color, readonly relay: RelayCtrl | undefined, readonly redraw: () => void, - readonly trans: Trans, ) {} studyId = this.data.id; @@ -71,11 +70,7 @@ export function view(ctrl: StudyShare): VNode { const addPly = (path: string) => ctrl.onMainline() ? (ctrl.withPly() ? `${path}#${ctrl.currentNode().ply}` : path) : `${path}#last`; const youCanPasteThis = () => - h( - 'p.form-help.text', - { attrs: dataIcon(licon.InfoCircle) }, - ctrl.trans.noarg('youCanPasteThisInTheForumToEmbed'), - ); + h('p.form-help.text', { attrs: dataIcon(licon.InfoCircle) }, i18n.study.youCanPasteThisInTheForumToEmbed); return h( 'div.study__share', ctrl.shareable() @@ -85,7 +80,7 @@ export function view(ctrl: StudyShare): VNode { h( 'a.button.text', { attrs: { ...dataIcon(licon.StudyBoard), href: `/study/${studyId}/clone` } }, - ctrl.trans.noarg('cloneStudy'), + i18n.study.cloneStudy, ), ctrl.relay && h( @@ -97,7 +92,7 @@ export function view(ctrl: StudyShare): VNode { download: true, }, }, - ctrl.trans.noarg('downloadAllRounds'), + i18n.broadcast.downloadAllRounds, ), h( 'a.button.text', @@ -108,7 +103,7 @@ export function view(ctrl: StudyShare): VNode { download: true, }, }, - ctrl.trans.noarg(ctrl.relay ? 'downloadAllGames' : 'studyPgn'), + ctrl.relay ? i18n.study.downloadAllGames : i18n.study.studyPgn, ), h( 'a.button.text', @@ -119,7 +114,7 @@ export function view(ctrl: StudyShare): VNode { download: true, }, }, - ctrl.trans.noarg(ctrl.relay ? 'downloadGame' : 'chapterPgn'), + ctrl.relay ? i18n.study.downloadGame : i18n.study.chapterPgn, ), h( 'a.button.text', @@ -148,7 +143,7 @@ export function view(ctrl: StudyShare): VNode { ); }), }, - ctrl.trans.noarg('copyChapterPgn'), + i18n.study.copyChapterPgn, ), h( 'a.button.text', @@ -188,15 +183,15 @@ export function view(ctrl: StudyShare): VNode { ? [ [ctrl.relay.data.tour.name, ctrl.relay.tourPath()], [ctrl.data.name, ctrl.relay.roundPath()], - ['currentGameUrl', addPly(`${ctrl.relay.roundPath()}/${chapter.id}`), true], + [i18n.broadcast.currentGameUrl, addPly(`${ctrl.relay.roundPath()}/${chapter.id}`), true], ] : [ - ['studyUrl', `/study/${studyId}`], - ['currentChapterUrl', addPly(`/study/${studyId}/${chapter.id}`), true], + [i18n.study.studyUrl, `/study/${studyId}`], + [i18n.study.currentChapterUrl, addPly(`/study/${studyId}/${chapter.id}`), true], ] - ).map(([i18n, path, pastable]: [string, string, boolean]) => + ).map(([text, path, pastable]: [string, string, boolean]) => h('div.form-group', [ - h('label.form-label', ctrl.trans.noarg(i18n)), + h('label.form-label', text), copyMeInput(`${baseUrl()}${path}`), pastable && fromPly(ctrl), pastable && isPrivate && youCanPasteThis(), @@ -206,7 +201,7 @@ export function view(ctrl: StudyShare): VNode { ? [] : [ h('div.form-group', [ - h('label.form-label', ctrl.trans.noarg('embedInYourWebsite')), + h('label.form-label', i18n.study.embedInYourWebsite), copyMeInput( !isPrivate ? `` - : ctrl.trans.noarg('onlyPublicStudiesCanBeEmbedded'), + : i18n.study.onlyPublicStudiesCanBeEmbedded, { disabled: isPrivate }, ), fromPly(ctrl), @@ -228,7 +223,7 @@ export function view(ctrl: StudyShare): VNode { ...dataIcon(licon.InfoCircle), }, }, - ctrl.trans.noarg('readMoreAboutEmbedding'), + i18n.study.readMoreAboutEmbedding, ), ]), ]), diff --git a/ui/analyse/src/study/studyTags.ts b/ui/analyse/src/study/studyTags.ts index 280b1ecc54fd0..5ae6693bf4bcc 100644 --- a/ui/analyse/src/study/studyTags.ts +++ b/ui/analyse/src/study/studyTags.ts @@ -35,8 +35,7 @@ export function view(root: StudyCtrl): VNode { return thunk('div.' + chapter.id, doRender, [root, key]); } -const doRender = (root: StudyCtrl): VNode => - h('div', renderPgnTags(root.tags, root.trans, root.data.showRatings)); +const doRender = (root: StudyCtrl): VNode => h('div', renderPgnTags(root.tags, root.data.showRatings)); const editable = (value: string, submit: (v: string, el: HTMLInputElement) => void): VNode => h('input', { @@ -57,7 +56,7 @@ const fixed = ([key, value]: [string, string]) => const fixedValue = (value: string) => h('span', value); -function renderPgnTags(tags: TagsForm, trans: Trans, showRatings: boolean): VNode { +function renderPgnTags(tags: TagsForm, showRatings: boolean): VNode { let rows: TagRow[] = []; const chapter = tags.getChapter(); if (chapter.setup.variant.key !== 'standard') @@ -94,7 +93,7 @@ function renderPgnTags(tags: TagsForm, trans: Trans, showRatings: boolean): VNod }, }, [ - h('option', trans.noarg('newTag')), + h('option', i18n.study.newTag), ...tags.types.map(t => (!existingTypes.includes(t) ? option(t, '', t) : undefined)), ], ), diff --git a/ui/analyse/src/study/studyView.ts b/ui/analyse/src/study/studyView.ts index ab391e236b94b..308a1db10e419 100644 --- a/ui/analyse/src/study/studyView.ts +++ b/ui/analyse/src/study/studyView.ts @@ -56,7 +56,6 @@ function buttons(root: AnalyseCtrl): VNode { const ctrl: StudyCtrl = root.study!, canContribute = ctrl.members.canContribute(), showSticky = ctrl.data.features.sticky && (canContribute || (ctrl.vm.behind && ctrl.isUpdatedRecently())), - noarg = root.trans.noarg, gbButton = gbOverrideButton(ctrl); return h('div.study__buttons', [ h('div.left-buttons.tabs-horiz', { attrs: { role: 'tablist' } }, [ @@ -65,7 +64,7 @@ function buttons(root: AnalyseCtrl): VNode { h( 'a.mode.sync', { - attrs: { title: noarg('allSyncMembersRemainOnTheSamePosition') }, + attrs: { title: i18n.study.allSyncMembersRemainOnTheSamePosition }, class: { on: ctrl.vm.mode.sticky }, hook: bind('click', ctrl.toggleSticky), }, @@ -75,18 +74,18 @@ function buttons(root: AnalyseCtrl): VNode { h( 'a.mode.write', { - attrs: { title: noarg('shareChanges') }, + attrs: { title: i18n.study.shareChanges }, class: { on: ctrl.vm.mode.write }, hook: bind('click', ctrl.toggleWrite), }, [h('i.is'), 'REC'], ), - toolButton({ ctrl, tab: 'tags', hint: noarg('pgnTags'), icon: iconTag(licon.Tag) }), + toolButton({ ctrl, tab: 'tags', hint: i18n.study.pgnTags, icon: iconTag(licon.Tag) }), canContribute && toolButton({ ctrl, tab: 'comments', - hint: noarg('commentThisPosition'), + hint: i18n.study.commentThisPosition, icon: iconTag(licon.BubbleSpeech), onClick() { ctrl.commentForm.start(ctrl.vm.chapterId, root.path, root.node); @@ -97,7 +96,7 @@ function buttons(root: AnalyseCtrl): VNode { toolButton({ ctrl, tab: 'glyphs', - hint: noarg('annotateWithGlyphs'), + hint: i18n.study.annotateWithGlyphs, icon: h('i.glyph-icon'), count: (root.node.glyphs || []).length, }), @@ -105,12 +104,12 @@ function buttons(root: AnalyseCtrl): VNode { toolButton({ ctrl, tab: 'serverEval', - hint: noarg('computerAnalysis'), + hint: i18n.site.computerAnalysis, icon: iconTag(licon.BarChart), count: root.data.analysis && '✓', }), toolButton({ ctrl, tab: 'multiBoard', hint: 'Multiboard', icon: iconTag(licon.Multiboard) }), - toolButton({ ctrl, tab: 'share', hint: noarg('shareAndExport'), icon: iconTag(licon.NodeBranching) }), + toolButton({ ctrl, tab: 'share', hint: i18n.study.shareAndExport, icon: iconTag(licon.NodeBranching) }), !ctrl.relay && !ctrl.data.chapter.gamebook && h('span.help', { @@ -137,7 +136,7 @@ function metadata(ctrl: StudyCtrl): VNode { class: { liked: d.liked }, attrs: { ...dataIcon(d.liked ? licon.Heart : licon.HeartOutline), - title: ctrl.trans.noarg(d.liked ? 'unlike' : 'like'), + title: d.liked ? i18n.study.unlike : i18n.study.like, }, hook: bind('click', ctrl.toggleLike), }, @@ -165,14 +164,11 @@ export function side(ctrl: StudyCtrl, withSearch: boolean): VNode { const chaptersTab = (ctrl.chapters.list.looksNew() && !ctrl.members.canContribute()) || - makeTab( - 'chapters', - ctrl.trans.pluralSame(ctrl.relay ? 'nbGames' : 'nbChapters', ctrl.chapters.list.size()), - ); + makeTab('chapters', i18n.study[ctrl.relay ? 'nbGames' : 'nbChapters'](ctrl.chapters.list.size())); const tabs = h('div.tabs-horiz', { attrs: { role: 'tablist' } }, [ chaptersTab, - ctrl.members.size() > 0 && makeTab('members', ctrl.trans.pluralSame('nbMembers', ctrl.members.size())), + ctrl.members.size() > 0 && makeTab('members', i18n.study.nbMembers(ctrl.members.size())), withSearch && h('span.search.narrow', { attrs: { ...dataIcon(licon.Search), title: 'Search' }, @@ -202,7 +198,7 @@ export function contextMenu(ctrl: StudyCtrl, path: Tree.Path, node: Tree.Node): ctrl.commentForm.start(ctrl.currentChapter().id, path, node); }), }, - ctrl.trans.noarg('commentThisMove'), + i18n.study.commentThisMove, ), h( 'a.glyph-icon', @@ -212,7 +208,7 @@ export function contextMenu(ctrl: StudyCtrl, path: Tree.Path, node: Tree.Node): ctrl.ctrl.userJump(path); }), }, - ctrl.trans.noarg('annotateWithGlyphs'), + i18n.study.annotateWithGlyphs, ), ] : []; diff --git a/ui/analyse/src/study/topics.ts b/ui/analyse/src/study/topics.ts index edbe47dbb7dbc..774b1b355fd49 100644 --- a/ui/analyse/src/study/topics.ts +++ b/ui/analyse/src/study/topics.ts @@ -13,7 +13,6 @@ export default class TopicsCtrl { constructor( readonly save: (data: string[]) => void, readonly getTopics: () => Topic[], - readonly trans: Trans, readonly redraw: Redraw, ) {} } @@ -29,7 +28,7 @@ export const view = (ctrl: StudyCtrl): VNode => ? h( 'a.manage', { hook: bind('click', () => ctrl.topics.open(true), ctrl.redraw) }, - ctrl.trans.noarg('manageTopics'), + i18n.study.manageTopics, ) : null, ]); @@ -44,7 +43,7 @@ export const formView = (ctrl: TopicsCtrl, userId?: string): VNode => ctrl.redraw(); }, vnodes: [ - h('h2', ctrl.trans.noarg('topics')), + h('h2', i18n.study.topics), h( 'form', { @@ -62,7 +61,7 @@ export const formView = (ctrl: TopicsCtrl, userId?: string): VNode => { hook: onInsert(elm => setupTagify(elm as HTMLTextAreaElement, userId)) }, ctrl.getTopics().join(', ').replace(/[<>]/g, ''), ), - h('button.button', { type: 'submit' }, ctrl.trans.noarg('save')), + h('button.button', { type: 'submit' }, i18n.study.save), ], ), ], diff --git a/ui/analyse/src/treeView/columnView.ts b/ui/analyse/src/treeView/columnView.ts index 1c8a65e8d38ea..1f3f2fa58fd9d 100644 --- a/ui/analyse/src/treeView/columnView.ts +++ b/ui/analyse/src/treeView/columnView.ts @@ -121,7 +121,7 @@ function renderLines(ctx: Ctx, parentNode: Tree.Node, nodes: Tree.Node[], opts: ? h('line', { class: { expand: true } }, [ h('branch'), h('a', { - attrs: { 'data-icon': licon.PlusButton, title: ctx.ctrl.trans.noarg('expandVariations') }, + attrs: { 'data-icon': licon.PlusButton, title: i18n.site.expandVariations }, on: { click: () => ctx.ctrl.setCollapsed(opts.parentPath, false) }, }), ]) diff --git a/ui/analyse/src/treeView/common.ts b/ui/analyse/src/treeView/common.ts index 7ab55eb7bbb8d..e3808643f4ba4 100644 --- a/ui/analyse/src/treeView/common.ts +++ b/ui/analyse/src/treeView/common.ts @@ -140,7 +140,7 @@ export function truncateComment(text: string, len: number, ctx: Ctx) { export function retroLine(ctx: Ctx, node: Tree.Node): VNode | undefined { return node.comp && ctx.ctrl.retro && ctx.ctrl.retro.hideComputerLine(node) - ? h('line', ctx.ctrl.trans.noarg('learnFromThisMistake')) + ? h('line', i18n.site.learnFromThisMistake) : undefined; } diff --git a/ui/analyse/src/treeView/contextMenu.ts b/ui/analyse/src/treeView/contextMenu.ts index edcadf8ef1c28..4712d11dc3393 100644 --- a/ui/analyse/src/treeView/contextMenu.ts +++ b/ui/analyse/src/treeView/contextMenu.ts @@ -59,8 +59,7 @@ function action(icon: string, text: string, handler: () => void): VNode { function view(opts: Opts, coords: Coords): VNode { const ctrl = opts.root, node = ctrl.tree.nodeAtPath(opts.path), - onMainline = ctrl.tree.pathIsMainline(opts.path) && !ctrl.tree.pathIsForcedVariation(opts.path), - trans = ctrl.trans.noarg; + onMainline = ctrl.tree.pathIsMainline(opts.path) && !ctrl.tree.pathIsForcedVariation(opts.path); return h( 'div#' + elementId + '.visible', { @@ -76,22 +75,22 @@ function view(opts: Opts, coords: Coords): VNode { h('p.title', nodeFullName(node)), !onMainline && - action(licon.UpTriangle, trans('promoteVariation'), () => ctrl.promote(opts.path, false)), + action(licon.UpTriangle, i18n.site.promoteVariation, () => ctrl.promote(opts.path, false)), - !onMainline && action(licon.Checkmark, trans('makeMainLine'), () => ctrl.promote(opts.path, true)), + !onMainline && action(licon.Checkmark, i18n.site.makeMainLine, () => ctrl.promote(opts.path, true)), - action(licon.Trash, trans('deleteFromHere'), () => ctrl.deleteNode(opts.path)), + action(licon.Trash, i18n.site.deleteFromHere, () => ctrl.deleteNode(opts.path)), - action(licon.PlusButton, trans('expandVariations'), () => ctrl.setAllCollapsed(opts.path, false)), + action(licon.PlusButton, i18n.site.expandVariations, () => ctrl.setAllCollapsed(opts.path, false)), - action(licon.MinusButton, trans('collapseVariations'), () => ctrl.setAllCollapsed(opts.path, true)), + action(licon.MinusButton, i18n.site.collapseVariations, () => ctrl.setAllCollapsed(opts.path, true)), ...(ctrl.study ? studyView.contextMenu(ctrl.study, opts.path, node) : []), onMainline && - action(licon.InternalArrow, trans('forceVariation'), () => ctrl.forceVariation(opts.path, true)), + action(licon.InternalArrow, i18n.site.forceVariation, () => ctrl.forceVariation(opts.path, true)), - action(licon.Clipboard, trans('copyVariationPgn'), () => + action(licon.Clipboard, i18n.site.copyVariationPgn, () => navigator.clipboard.writeText(renderVariationPgn(opts.root.tree.getNodeList(opts.path))), ), ], diff --git a/ui/analyse/src/treeView/inlineView.ts b/ui/analyse/src/treeView/inlineView.ts index 13cdf38f62fe9..dbba4ce3efa09 100644 --- a/ui/analyse/src/treeView/inlineView.ts +++ b/ui/analyse/src/treeView/inlineView.ts @@ -78,7 +78,7 @@ function renderLines(ctx: Ctx, parentNode: Tree.Node, nodes: Tree.Node[], opts: ? h('line', { class: { expand: true } }, [ h('branch'), h('a', { - attrs: { 'data-icon': licon.PlusButton, title: ctx.ctrl.trans.noarg('expandVariations') }, + attrs: { 'data-icon': licon.PlusButton, title: i18n.site.expandVariations }, on: { click: () => ctx.ctrl.setCollapsed(opts.parentPath, false) }, }), ]) diff --git a/ui/analyse/src/view/actionMenu.ts b/ui/analyse/src/view/actionMenu.ts index d3d7300dfec0f..3c1a787432c44 100644 --- a/ui/analyse/src/view/actionMenu.ts +++ b/ui/analyse/src/view/actionMenu.ts @@ -11,7 +11,7 @@ import { cont as contRoute } from 'game/router'; import * as pgnExport from '../pgnExport'; interface AutoplaySpeed { - name: string; + name: keyof I18n['site']; delay: AutoplayDelay; } @@ -30,7 +30,7 @@ const cplSpeed: AutoplaySpeed = { delay: 'cpl', }; -const ctrlToggle = (t: ToggleSettings, ctrl: AnalyseCtrl) => toggle(t, ctrl.trans, ctrl.redraw); +const ctrlToggle = (t: ToggleSettings, ctrl: AnalyseCtrl) => toggle(t, ctrl.redraw); function autoplayButtons(ctrl: AnalyseCtrl): VNode { const d = ctrl.data; @@ -49,7 +49,7 @@ function autoplayButtons(ctrl: AnalyseCtrl): VNode { class: { active, 'button-empty': !active }, hook: bind('click', () => ctrl.togglePlay(speed.delay), ctrl.redraw), }, - ctrl.trans.noarg(speed.name), + String(i18n.site[speed.name]), ); }), ); @@ -69,7 +69,7 @@ function studyButton(ctrl: AnalyseCtrl) { 'data-icon': licon.StudyBoard, }, }, - ctrl.trans.noarg('openStudy'), + i18n.site.openStudy, ); if (ctrl.study || ctrl.ongoing) return; return h( @@ -89,14 +89,13 @@ function studyButton(ctrl: AnalyseCtrl) { hiddenInput('orientation', ctrl.bottomColor()), hiddenInput('variant', ctrl.data.game.variant.key), hiddenInput('fen', ctrl.tree.root.fen), - h('button', { attrs: { type: 'submit', 'data-icon': licon.StudyBoard } }, ctrl.trans.noarg('toStudy')), + h('button', { attrs: { type: 'submit', 'data-icon': licon.StudyBoard } }, i18n.site.toStudy), ], ); } export function view(ctrl: AnalyseCtrl): VNode { const d = ctrl.data, - noarg = ctrl.trans.noarg, canContinue = !ctrl.ongoing && d.game.variant.key === 'standard', ceval = ctrl.getCeval(), mandatoryCeval = ctrl.mandatoryCeval(), @@ -107,7 +106,7 @@ export function view(ctrl: AnalyseCtrl): VNode { h( 'a', { hook: bind('click', ctrl.flip), attrs: { 'data-icon': licon.ChasingArrows, title: 'Hotkey: f' } }, - noarg('flipBoard'), + i18n.site.flipBoard, ), !ctrl.ongoing && h( @@ -126,7 +125,7 @@ export function view(ctrl: AnalyseCtrl): VNode { ...linkAttrs, }, }, - noarg('boardEditor'), + i18n.site.boardEditor, ), canContinue && h( @@ -135,7 +134,7 @@ export function view(ctrl: AnalyseCtrl): VNode { hook: bind('click', () => domDialog({ cash: $('.continue-with.g_' + d.game.id), show: 'modal' })), attrs: dataIcon(licon.Swords), }, - noarg('continueFromHere'), + i18n.site.continueFromHere, ), studyButton(ctrl), ctrl.persistence?.isDirty && @@ -143,12 +142,12 @@ export function view(ctrl: AnalyseCtrl): VNode { 'a', { attrs: { - title: noarg('clearSavedMoves'), + title: i18n.site.clearSavedMoves, 'data-icon': licon.Trash, }, hook: bind('click', ctrl.persistence.clear), }, - noarg('clearSavedMoves'), + i18n.site.clearSavedMoves, ), ]), ]; @@ -156,10 +155,10 @@ export function view(ctrl: AnalyseCtrl): VNode { const cevalConfig: MaybeVNodes = ceval?.possible && ceval.allowed() ? [ - h('h2', noarg('computerAnalysis')), + h('h2', i18n.site.computerAnalysis), ctrlToggle( { - name: 'enable', + name: i18n.site.enable, title: (mandatoryCeval ? 'Required by practice mode' : 'Stockfish') + ' (Hotkey: z)', id: 'all', checked: ctrl.showComputer(), @@ -172,7 +171,7 @@ export function view(ctrl: AnalyseCtrl): VNode { ? [ ctrlToggle( { - name: 'bestMoveArrow', + name: i18n.site.bestMoveArrow, title: 'Hotkey: a', id: 'shapes', checked: ctrl.showAutoShapes(), @@ -182,7 +181,7 @@ export function view(ctrl: AnalyseCtrl): VNode { ), ctrlToggle( { - name: 'evaluationGauge', + name: i18n.site.evaluationGauge, id: 'gauge', checked: ctrl.showGauge(), change: ctrl.toggleGauge, @@ -198,7 +197,7 @@ export function view(ctrl: AnalyseCtrl): VNode { h('h2', 'Display'), ctrlToggle( { - name: noarg('inlineNotation'), + name: i18n.site.inlineNotation, title: 'Shift+I', id: 'inline', checked: ctrl.treeView.inline(), @@ -237,7 +236,7 @@ export function view(ctrl: AnalyseCtrl): VNode { ...tools, ...displayConfig, ...cevalConfig, - ...(ctrl.mainline.length > 4 ? [h('h2', noarg('replayMode')), autoplayButtons(ctrl)] : []), + ...(ctrl.mainline.length > 4 ? [h('h2', i18n.site.replayMode), autoplayButtons(ctrl)] : []), canContinue && h('div.continue-with.none.g_' + d.game.id, [ h( @@ -250,7 +249,7 @@ export function view(ctrl: AnalyseCtrl): VNode { ...linkAttrs, }, }, - noarg('playWithTheMachine'), + i18n.site.playWithTheMachine, ), h( 'a.button', @@ -262,7 +261,7 @@ export function view(ctrl: AnalyseCtrl): VNode { ...linkAttrs, }, }, - noarg('playWithAFriend'), + i18n.site.playWithAFriend, ), ]), ]); diff --git a/ui/analyse/src/view/components.ts b/ui/analyse/src/view/components.ts index b5829ed8ffb28..9807973a0bba1 100644 --- a/ui/analyse/src/view/components.ts +++ b/ui/analyse/src/view/components.ts @@ -257,7 +257,7 @@ export function renderInputs(ctrl: AnalyseCtrl): VNode | undefined { if (pgn !== pgnExport.renderFullTxt(ctrl)) ctrl.changePgn(pgn, true); }), }, - ctrl.trans.noarg('importPgn'), + i18n.site.importPgn, ), h( 'div.bottom-item.bottom-error', @@ -272,8 +272,7 @@ export function renderInputs(ctrl: AnalyseCtrl): VNode | undefined { export function renderControls(ctrl: AnalyseCtrl) { const canJumpPrev = ctrl.path !== '', canJumpNext = !!ctrl.node.children[0], - menuIsOpen = ctrl.actionMenu(), - noarg = ctrl.trans.noarg; + menuIsOpen = ctrl.actionMenu(); return h( 'div.analyse__controls.analyse-controls', { @@ -297,13 +296,13 @@ export function renderControls(ctrl: AnalyseCtrl) { ctrl.studyPractice ? [ h('button.fbt', { - attrs: { title: noarg('analysis'), 'data-act': 'analysis', 'data-icon': licon.Microscope }, + attrs: { title: i18n.site.analysis, 'data-act': 'analysis', 'data-icon': licon.Microscope }, }), ] : [ h('button.fbt', { attrs: { - title: noarg('openingExplorerAndTablebase'), + title: i18n.site.openingExplorerAndTablebase, 'data-act': 'explorer', 'data-icon': licon.Book, }, @@ -318,7 +317,7 @@ export function renderControls(ctrl: AnalyseCtrl) { !ctrl.isEmbed && h('button.fbt', { attrs: { - title: noarg('practiceWithComputer'), + title: i18n.site.practiceWithComputer, 'data-act': 'practice', 'data-icon': licon.Bullseye, }, @@ -336,7 +335,7 @@ export function renderControls(ctrl: AnalyseCtrl) { ? h('div.noop') : h('button.fbt', { class: { active: menuIsOpen }, - attrs: { title: noarg('menu'), 'data-act': 'menu', 'data-icon': licon.Hamburger }, + attrs: { title: i18n.site.menu, 'data-act': 'menu', 'data-icon': licon.Hamburger }, }), ], ); @@ -361,9 +360,9 @@ function renderMoveList(ctrl: AnalyseCtrl, deps?: typeof studyDeps, concealOf?: } else if (ctrl.study) { const result = deps?.findTag(ctrl.study.data.chapter.tags, 'result'); if (!result || result === '*') return []; - if (result === '1-0') return render(result, [ctrl.trans.noarg('whiteIsVictorious')]); - if (result === '0-1') return render(result, [ctrl.trans.noarg('blackIsVictorious')]); - return render('½-½', [ctrl.trans.noarg('draw')]); + if (result === '1-0') return render(result, [i18n.site.whiteIsVictorious]); + if (result === '0-1') return render(result, [i18n.site.blackIsVictorious]); + return render('½-½', [i18n.site.draw]); } return []; } diff --git a/ui/analyse/src/view/main.ts b/ui/analyse/src/view/main.ts index a183ddb9d2c98..64d4b2c6a0397 100644 --- a/ui/analyse/src/view/main.ts +++ b/ui/analyse/src/view/main.ts @@ -77,7 +77,7 @@ function analyseView(ctrl: AnalyseCtrl, deps?: typeof studyDeps): VNode { 'data-icon': licon.Back, }, }, - ctrl.trans.noarg('backToGame'), + i18n.site.backToGame, ), ), ], diff --git a/ui/analyse/src/view/roundTraining.ts b/ui/analyse/src/view/roundTraining.ts index 283f52b642381..1a4f73b8dd59c 100644 --- a/ui/analyse/src/view/roundTraining.ts +++ b/ui/analyse/src/view/roundTraining.ts @@ -10,7 +10,7 @@ type AdviceKind = 'inaccuracy' | 'mistake' | 'blunder'; interface Advice { kind: AdviceKind; - i18n: I18nKey; + i18n: I18nPlural; symbol: string; } @@ -32,9 +32,9 @@ const renderPlayer = (ctrl: AnalyseCtrl, color: Color): VNode => { }; const advices: Advice[] = [ - { kind: 'inaccuracy', i18n: 'nbInaccuracies', symbol: '?!' }, - { kind: 'mistake', i18n: 'nbMistakes', symbol: '?' }, - { kind: 'blunder', i18n: 'nbBlunders', symbol: '??' }, + { kind: 'inaccuracy', i18n: i18n.site.nbInaccuracies, symbol: '?!' }, + { kind: 'mistake', i18n: i18n.site.nbMistakes, symbol: '?' }, + { kind: 'blunder', i18n: i18n.site.nbBlunders, symbol: '??' }, ]; function playerTable(ctrl: AnalyseCtrl, color: Color): VNode { @@ -43,15 +43,15 @@ function playerTable(ctrl: AnalyseCtrl, color: Color): VNode { return h('div.advice-summary__side', [ h('div.advice-summary__player', [h(`i.is.color-icon.${color}`), renderPlayer(ctrl, color)]), - ...advices.map(a => error(ctrl, d.analysis![color][a.kind], color, a)), + ...advices.map(a => error(d.analysis![color][a.kind], color, a)), h('div.advice-summary__acpl', [ h('strong', sideData.acpl), - h('span', ` ${ctrl.trans.noarg('averageCentipawnLoss')}`), + h('span', ` ${i18n.site.averageCentipawnLoss}`), ]), h('div.advice-summary__accuracy', [ h('strong', [sideData.accuracy, '%']), h('span', [ - ctrl.trans.noarg('accuracy'), + i18n.site.accuracy, ' ', h('a', { attrs: { 'data-icon': licon.InfoCircle, href: '/page/accuracy', target: '_blank' }, @@ -61,11 +61,11 @@ function playerTable(ctrl: AnalyseCtrl, color: Color): VNode { ]); } -const error = (ctrl: AnalyseCtrl, nb: number, color: Color, advice: Advice) => +const error = (nb: number, color: Color, advice: Advice) => h( 'div.advice-summary__error' + (nb ? `.symbol.${advice.kind}` : ''), { attrs: nb ? { 'data-color': color, 'data-symbol': advice.symbol } : {} }, - ctrl.trans.vdomPlural(advice.i18n, nb, h('strong', nb)), + advice.i18n.asArray(nb, h('strong', nb)), ); const doRender = (ctrl: AnalyseCtrl): VNode => { @@ -91,7 +91,7 @@ const doRender = (ctrl: AnalyseCtrl): VNode => { attrs: dataIcon(licon.PlayTriangle), hook: bind('click', ctrl.toggleRetro, ctrl.redraw), }, - ctrl.trans.noarg('learnFromYourMistakes'), + i18n.site.learnFromYourMistakes, ), playerTable(ctrl, 'black'), ], diff --git a/ui/bits/src/bits.user.ts b/ui/bits/src/bits.user.ts index 103bd472d4589..d4e0612f91f7c 100644 --- a/ui/bits/src/bits.user.ts +++ b/ui/bits/src/bits.user.ts @@ -1,13 +1,10 @@ import * as xhr from 'common/xhr'; import { makeLinkPopups } from 'common/linkPopup'; -import { trans as translation } from 'common/i18n'; import { pubsub } from 'common/pubsub'; -export function initModule(opts: { i18n: I18nDict }): void { - const trans = translation(opts.i18n); - - makeLinkPopups($('.social_links'), trans); - makeLinkPopups($('.user-infos .bio'), trans); +export function initModule(): void { + makeLinkPopups($('.social_links')); + makeLinkPopups($('.user-infos .bio')); const loadNoteZone = () => { const $zone = $('.user-show .note-zone'); diff --git a/ui/board/src/menu.ts b/ui/board/src/menu.ts index 7af0d06a07b59..92fba2e755526 100644 --- a/ui/board/src/menu.ts +++ b/ui/board/src/menu.ts @@ -15,7 +15,6 @@ export const toggleButton = (toggle: Toggle, title: string): VNode => }); export const menu = ( - trans: Trans, redraw: Redraw, toggle: Toggle, content: (menu: BoardMenu) => MaybeVNodes, @@ -24,17 +23,14 @@ export const menu = ( ? h( 'div.board-menu', { hook: onInsert(onClickAway(() => toggle(false))) }, - content(new BoardMenu(trans, redraw)), + content(new BoardMenu(redraw)), ) : undefined; export class BoardMenu { anonymous: boolean = document.querySelector('body[data-user]') === null; - constructor( - readonly trans: Trans, - readonly redraw: Redraw, - ) {} + constructor(readonly redraw: Redraw) {} flip = (name: string, active: boolean, onChange: () => void): VNode => h( @@ -94,5 +90,5 @@ export class BoardMenu { disabled: !enabled, }); - private cmnToggle = (t: controls.ToggleSettings) => controls.toggle(t, this.trans, this.redraw); + private cmnToggle = (t: controls.ToggleSettings) => controls.toggle(t, this.redraw); } diff --git a/ui/ceval/src/types.ts b/ui/ceval/src/types.ts index 2cf6e92096f0f..afeb9670f717a 100644 --- a/ui/ceval/src/types.ts +++ b/ui/ceval/src/types.ts @@ -134,7 +134,6 @@ export interface ParentCtrl { restartCeval: () => void; redraw?: () => void; externalEngines?: () => ExternalEngineInfo[] | undefined; - trans: Trans; } export interface NodeEvals { diff --git a/ui/ceval/src/view/main.ts b/ui/ceval/src/view/main.ts index 0da0cfe742067..14325fc30ea7a 100644 --- a/ui/ceval/src/view/main.ts +++ b/ui/ceval/src/view/main.ts @@ -26,20 +26,19 @@ const gaugeTicks: VNode[] = [...Array(8).keys()].map(i => function localEvalNodes(ctrl: ParentCtrl, evs: NodeEvals): Array { const ceval = ctrl.getCeval(), - state = ceval.state, - trans = ctrl.trans; + state = ceval.state; if (!evs.client) { if (!ceval.analysable) return ['Engine cannot analyze this position']; - if (state == CevalState.Failed) return [trans.noarg('engineFailed')]; - const localEvalText = state == CevalState.Loading ? loadingText(ctrl) : trans.noarg('calculatingMoves'); - return [evs.server && ctrl.nextNodeBest() ? trans.noarg('usingServerAnalysis') : localEvalText]; + if (state == CevalState.Failed) return [i18n.site.engineFailed]; + const localEvalText = state == CevalState.Loading ? loadingText(ctrl) : i18n.site.calculatingMoves; + return [evs.server && ctrl.nextNodeBest() ? i18n.site.usingServerAnalysis : localEvalText]; } const t: Array = []; if (ceval.canGoDeeper) t.push( h('a.deeper', { - attrs: { title: trans.noarg('goDeeper'), 'data-icon': licon.PlusButton }, + attrs: { title: i18n.site.goDeeper, 'data-icon': licon.PlusButton }, hook: bind('click', ceval.goDeeper), }), ); @@ -47,8 +46,8 @@ function localEvalNodes(ctrl: ParentCtrl, evs: NodeEvals): Array t.push(depthText); if (evs.client.cloud && !ceval.isComputing) - t.push(h('span.cloud', { attrs: { title: trans.noarg('cloudAnalysis') } }, 'Cloud')); - if (ceval.isInfinite) t.push(h('span.infinite', { attrs: { title: trans('infiniteAnalysis') } }, '∞')); + t.push(h('span.cloud', { attrs: { title: i18n.site.cloudAnalysis } }, 'Cloud')); + if (ceval.isInfinite) t.push(h('span.infinite', { attrs: { title: i18n.site.infiniteAnalysis } }, '∞')); if (npsText) t.push(' · ' + npsText); return t; } @@ -62,13 +61,13 @@ function localInfo(ctrl: ParentCtrl, ev?: Tree.ClientEval | false): EvalInfo { const info = { npsText: '', knps: 0, - depthText: ctrl.trans.noarg('calculatingMoves'), + depthText: i18n.site.calculatingMoves, }; if (!ev) return info; const ceval = ctrl.getCeval(); - info.depthText = ctrl.trans('depthX', ev.depth || 0) + (ceval.isDeeper() || ceval.isInfinite ? '/99' : ''); + info.depthText = i18n.site.depthX(ev.depth || 0) + (ceval.isDeeper() || ceval.isInfinite ? '/99' : ''); if (!ceval.isComputing) return info; @@ -87,7 +86,7 @@ function threatButton(ctrl: ParentCtrl): VNode | null { if (ctrl.getCeval().download || (ctrl.disableThreatMode && ctrl.disableThreatMode())) return null; return h('button.show-threat', { class: { active: ctrl.threatMode(), hidden: !!ctrl.getNode().check }, - attrs: { 'data-icon': licon.Target, title: ctrl.trans.noarg('showThreat') + ' (x)' }, + attrs: { 'data-icon': licon.Target, title: i18n.site.showThreat + ' (x)' }, hook: bind('click', ctrl.toggleThreatMode), }); } @@ -155,8 +154,7 @@ export function renderGauge(ctrl: ParentCtrl): VNode | undefined { } export function renderCeval(ctrl: ParentCtrl): LooseVNodes { - const ceval = ctrl.getCeval(), - trans = ctrl.trans; + const ceval = ctrl.getCeval(); if (!ceval.allowed() || !ceval.possible) return []; if (!ctrl.showComputer()) return [analysisDisabled(ctrl)]; const enabled = ceval.enabled(), @@ -219,13 +217,13 @@ export function renderCeval(ctrl: ParentCtrl): LooseVNodes { ? [ h('pearl', [pearl]), h('div.engine', [ - ...(threatMode ? [trans.noarg('showThreat')] : engineName(ceval)), + ...(threatMode ? [i18n.site.showThreat] : engineName(ceval)), h( 'span.info', ctrl.outcome() - ? [trans.noarg('gameOver')] + ? [i18n.site.gameOver] : ctrl.getNode().threefold - ? [trans.noarg('threefoldRepetition')] + ? [i18n.site.threefoldRepetition] : threatMode ? [threatInfo(ctrl, threat)] : localEvalNodes(ctrl, evs), @@ -237,13 +235,13 @@ export function renderCeval(ctrl: ParentCtrl): LooseVNodes { h('help', [ ...engineName(ceval), h('br'), - ceval.analysable ? trans.noarg('inLocalBrowser') : 'Engine cannot analyse this game', + ceval.analysable ? i18n.site.inLocalBrowser : 'Engine cannot analyse this game', ]), ]; const switchButton: VNode | false = !ctrl.mandatoryCeval?.() && - h('div.switch', { attrs: { title: trans.noarg('toggleLocalEvaluation') + ' (L)' } }, [ + h('div.switch', { attrs: { title: i18n.site.toggleLocalEvaluation + ' (L)' } }, [ h('input#analyse-toggle-ceval.cmn-toggle.cmn-toggle--subtle', { attrs: { type: 'checkbox', checked: enabled, disabled: !ceval.analysable }, hook: onInsert((el: HTMLInputElement) => { @@ -492,11 +490,11 @@ function renderPvBoard(ctrl: ParentCtrl): VNode | undefined { const analysisDisabled = (ctrl: ParentCtrl): VNode | undefined => h('div.comp-off__hint', [ - h('span', ctrl.trans.noarg('computerAnalysisDisabled')), + h('span', i18n.site.computerAnalysisDisabled), h( 'button', { hook: bind('click', () => ctrl.toggleComputer?.(), ctrl.redraw), attrs: { type: 'button' } }, - ctrl.trans.noarg('enable'), + i18n.site.enable, ), ]); @@ -504,5 +502,5 @@ function loadingText(ctrl: ParentCtrl): string { const d = ctrl.getCeval().download; if (d && d.total) return `Downloaded ${Math.round((d.bytes * 100) / d.total)}% of ${Math.round(d.total / 1000 / 1000)}MB`; - else return ctrl.trans.noarg('loadingEngine'); + else return i18n.site.loadingEngine; } diff --git a/ui/ceval/src/view/settings.ts b/ui/ceval/src/view/settings.ts index 96f9ad4da7de1..48e2458195a35 100644 --- a/ui/ceval/src/view/settings.ts +++ b/ui/ceval/src/view/settings.ts @@ -25,7 +25,6 @@ const formatHashSize = (v: number): string => (v < 1000 ? v + 'MB' : Math.round( export function renderCevalSettings(ctrl: ParentCtrl): VNode | null { const ceval = ctrl.getCeval(), - noarg = ctrl.trans.noarg, minThreads = ceval.engines.active?.minThreads ?? 1, maxThreads = ceval.maxThreads, engCtrl = ctrl.getCeval().engines, @@ -87,7 +86,7 @@ export function renderCevalSettings(ctrl: ParentCtrl): VNode | null { 'div.setting', { attrs: { title: 'Set number of evaluation lines and move arrows on the board' } }, [ - h('label', { attrs: { for: id } }, noarg('multipleLines')), + h('label', { attrs: { for: id } }, i18n.site.multipleLines), h('input#' + id, { attrs: { type: 'range', min: 0, max, step: 1 }, hook: rangeConfig( @@ -152,7 +151,7 @@ export function renderCevalSettings(ctrl: ParentCtrl): VNode | null { })('analyse-threads'), (id => h('div.setting', { attrs: { title: 'Higher values may improve performance' } }, [ - h('label', { attrs: { for: id } }, noarg('memory')), + h('label', { attrs: { for: id } }, i18n.site.memory), h('input#' + id, { attrs: { type: 'range', diff --git a/ui/challenge/src/ctrl.ts b/ui/challenge/src/ctrl.ts index 0578087b377e9..03dcd9a1c12c5 100644 --- a/ui/challenge/src/ctrl.ts +++ b/ui/challenge/src/ctrl.ts @@ -1,11 +1,9 @@ import * as xhr from 'common/xhr'; -import { trans } from 'common/i18n'; import { once } from 'common/storage'; import { ChallengeOpts, ChallengeData, Reasons } from './interfaces'; export default class ChallengeCtrl { data: ChallengeData; - trans = (key: string) => key; redirecting = false; reasons: Reasons = {}; @@ -21,7 +19,6 @@ export default class ChallengeCtrl { update = (d: ChallengeData) => { this.data = d; - if (d.i18n) this.trans = trans(d.i18n).noarg; if (d.reasons) this.reasons = d.reasons; this.opts.setCount(this.countActiveIn()); this.notifyNew(); diff --git a/ui/challenge/src/interfaces.ts b/ui/challenge/src/interfaces.ts index 2f7d160c3cbdf..a8a963248064b 100644 --- a/ui/challenge/src/interfaces.ts +++ b/ui/challenge/src/interfaces.ts @@ -58,9 +58,6 @@ export type Reasons = { export interface ChallengeData { in: Array; out: Array; - i18n?: { - [key: string]: string; - }; reasons?: Reasons; } diff --git a/ui/challenge/src/view.ts b/ui/challenge/src/view.ts index 5d75cf95e1551..60a0f1153fc9c 100644 --- a/ui/challenge/src/view.ts +++ b/ui/challenge/src/view.ts @@ -51,7 +51,7 @@ function challenge(ctrl: ChallengeCtrl, dir: ChallengeDirection) { h('span.desc', [ h('span.is.color-icon.' + myColor), ' • ', - [ctrl.trans(c.rated ? 'rated' : 'casual'), timeControl(c.timeControl), c.variant.name].join( + [i18n.site[c.rated ? 'rated' : 'casual'], timeControl(c.timeControl), c.variant.name].join( ' • ', ), ]), @@ -79,20 +79,20 @@ function inButtons(ctrl: ChallengeCtrl, c: Challenge): VNode[] { type: 'submit', 'aria-describedby': `challenge-text-${c.id}`, 'data-icon': licon.Checkmark, - title: ctrl.trans('accept'), + title: i18n.site.accept, }, hook: onClick(ctrl.onRedirect), }), ]); const viewElement = () => h('a.view', { - attrs: { 'data-icon': licon.Eye, href: '/' + c.id, title: ctrl.trans('viewInFullSize') }, + attrs: { 'data-icon': licon.Eye, href: '/' + c.id, title: i18n.site.viewInFullSize }, }); return [ viewInsteadOfAccept ? viewElement() : acceptElement(), h('button.button.decline', { - attrs: { type: 'submit', 'data-icon': licon.X, title: ctrl.trans('decline') }, + attrs: { type: 'submit', 'data-icon': licon.X, title: i18n.site.decline }, hook: onClick(() => ctrl.decline(c.id, 'generic')), }), h( @@ -114,13 +114,13 @@ function inButtons(ctrl: ChallengeCtrl, c: Challenge): VNode[] { const outButtons = (ctrl: ChallengeCtrl, c: Challenge) => [ h('div.owner', [ - h('span.waiting', ctrl.trans('waiting')), + h('span.waiting', i18n.site.waiting), h('a.view', { - attrs: { 'data-icon': licon.Eye, href: '/' + c.id, title: ctrl.trans('viewInFullSize') }, + attrs: { 'data-icon': licon.Eye, href: '/' + c.id, title: i18n.site.viewInFullSize }, }), ]), h('button.button.decline', { - attrs: { 'data-icon': licon.X, title: ctrl.trans('cancel') }, + attrs: { 'data-icon': licon.X, title: i18n.site.cancel }, hook: onClick(() => ctrl.cancel(c.id)), }), ]; diff --git a/ui/chart/src/acpl.ts b/ui/chart/src/acpl.ts index 45899f06c9c50..50eb2c29ed469 100644 --- a/ui/chart/src/acpl.ts +++ b/ui/chart/src/acpl.ts @@ -36,14 +36,13 @@ export default async function ( el: HTMLCanvasElement, data: AnalyseData, mainline: Tree.Node[], - trans: Trans, ): Promise { const possibleChart = maybeChart(el); if (possibleChart) return possibleChart as AcplChart; const blurBackgroundColorWhite = 'white'; const blurBackgroundColorBlack = 'black'; const ply = plyLine(0); - const divisionLines = division(trans, data.game.division); + const divisionLines = division(data.game.division); const firstPly = mainline[0].ply; const isPartial = (d: AnalyseData) => !d.analysis || d.analysis.partial; @@ -79,7 +78,7 @@ export default async function ( const { advice, color: glyphColor } = glyphProperties(node); const label = turn + dots + ' ' + node.san; let annotation = ''; - if (advice) annotation = ` [${trans(advice)}]`; + if (advice) annotation = ` [${i18n.site[advice]}]`; const isBlur = blurs[isWhite ? 1 : 0][Math.floor((node.ply - (d.game.startedAtTurn || 0) - 1) / 2)] === '1'; if (isBlur) annotation = ' [blur]'; @@ -93,7 +92,7 @@ export default async function ( }); return { acpl: { - label: trans('advantage'), + label: i18n.site.advantage, data: winChances, borderWidth: 1, fill: { @@ -163,7 +162,7 @@ export default async function ( e = ev.mate; mateSymbol = '#'; } - return trans('advantage') + ': ' + mateSymbol + advantageSign + e; + return i18n.site.advantage + ': ' + mateSymbol + advantageSign + e; }, title: items => (items[0] ? moveLabels[items[0].dataIndex] : ''), }, diff --git a/ui/chart/src/division.ts b/ui/chart/src/division.ts index 8d80cd27ea83a..474e0fec602d7 100644 --- a/ui/chart/src/division.ts +++ b/ui/chart/src/division.ts @@ -2,15 +2,15 @@ import { chartYMax, chartYMin } from './common'; import { Division } from './interface'; import { ChartDataset, Point } from 'chart.js'; -export default function (trans: Trans, div?: Division): ChartDataset<'line'>[] { +export default function (div?: Division): ChartDataset<'line'>[] { const lines: { div: string; loc: number }[] = []; if (div?.middle) { - if (div.middle > 1) lines.push({ div: trans('opening'), loc: 1 }); - lines.push({ div: trans('middlegame'), loc: div.middle }); + if (div.middle > 1) lines.push({ div: i18n.site.opening, loc: 1 }); + lines.push({ div: i18n.site.middlegame, loc: div.middle }); } if (div?.end) { - if (div.end > 1 && !div?.middle) lines.push({ div: trans('middlegame'), loc: 0 }); - lines.push({ div: trans('endgame'), loc: div.end }); + if (div.end > 1 && !div?.middle) lines.push({ div: i18n.site.middlegame, loc: 0 }); + lines.push({ div: i18n.site.endgame, loc: div.end }); } const annotationColor = '#707070'; diff --git a/ui/chart/src/interface.ts b/ui/chart/src/interface.ts index 01600c06b4cca..799f9593ea147 100644 --- a/ui/chart/src/interface.ts +++ b/ui/chart/src/interface.ts @@ -46,13 +46,8 @@ export interface AnalyseData { } export interface ChartGame { - acpl(el: HTMLCanvasElement, data: AnalyseData, mainline: Tree.Node[], trans: Trans): Promise; - movetime( - el: HTMLCanvasElement, - data: AnalyseData, - trans: Trans, - hunter: boolean, - ): Promise; + acpl(el: HTMLCanvasElement, data: AnalyseData, mainline: Tree.Node[]): Promise; + movetime(el: HTMLCanvasElement, data: AnalyseData, hunter: boolean): Promise; } export interface DistributionData { diff --git a/ui/chart/src/movetime.ts b/ui/chart/src/movetime.ts index c8603ecb74760..cfde791aadfb6 100644 --- a/ui/chart/src/movetime.ts +++ b/ui/chart/src/movetime.ts @@ -35,7 +35,6 @@ Chart.register(LineController, LinearScale, PointElement, LineElement, Tooltip, export default async function ( el: HTMLCanvasElement, data: AnalyseData, - trans: Trans, hunter: boolean, ): Promise { const possibleChart = maybeChart(el); @@ -99,7 +98,7 @@ export default async function ( } const seconds = (centis / 100).toFixed(centis >= 200 ? 1 : 2); - label += '\n' + trans('nbSeconds', seconds); + label += '\n' + i18n.site.nbSeconds(Number(seconds)); moveSeries[colorName].push(movePoint); let clock = node ? node.clock : undefined; @@ -164,7 +163,7 @@ export default async function ( datalabels: { display: false }, })) : lineBuilder(moveSeries, true); - const divisionLines = division(trans, data.game.division); + const divisionLines = division(data.game.division); const datasets: ChartDataset[] = [...moveSeriesSet]; if (showTotal) datasets.push(...lineBuilder(totalSeries, false)); datasets.push(plyLine(firstPly), ...divisionLines); diff --git a/ui/chat/src/ctrl.ts b/ui/chat/src/ctrl.ts index 73173f256e5c7..53594b144830b 100644 --- a/ui/chat/src/ctrl.ts +++ b/ui/chat/src/ctrl.ts @@ -15,7 +15,6 @@ import { PresetCtrl, presetCtrl } from './preset'; import { noteCtrl } from './note'; import { moderationCtrl } from './moderation'; import { prop } from 'common'; -import { trans } from 'common/i18n'; import { storage, type LichessStorage } from 'common/storage'; import { pubsub, PubsubEvent, PubsubCallback } from 'common/pubsub'; @@ -32,7 +31,6 @@ export default class ChatCtrl { moderation: ModerationCtrl | undefined; note: NoteCtrl | undefined; preset: PresetCtrl; - trans: Trans; vm: ViewModel; constructor( @@ -47,12 +45,10 @@ export default class ChatCtrl { loaded: false, enabled: prop(!!this.data.palantir), }; - this.trans = trans(this.opts.i18n); const noChat = storage.get('nochat'); this.vm = { tab: this.allTabs.find(tab => tab === this.storedTab) || this.allTabs[0], enabled: opts.alwaysEnabled || !noChat, - placeholderKey: 'talkInChat', loading: false, autofocus: false, timeout: opts.timeout, @@ -64,7 +60,6 @@ export default class ChatCtrl { ? noteCtrl({ id: opts.noteId, text: opts.noteText, - trans: this.trans, redraw: this.redraw, }) : undefined; diff --git a/ui/chat/src/discussion.ts b/ui/chat/src/discussion.ts index 172979958105c..520cdd1741cbf 100644 --- a/ui/chat/src/discussion.ts +++ b/ui/chat/src/discussion.ts @@ -61,12 +61,12 @@ function renderInput(ctrl: ChatCtrl): VNode | undefined { if (!ctrl.vm.writeable) return; if ((ctrl.data.loginRequired && !ctrl.data.userId) || ctrl.data.restricted) return h('input.mchat__say', { - attrs: { placeholder: ctrl.trans('loginToChat'), disabled: true }, + attrs: { placeholder: i18n.site.loginToChat, disabled: true }, }); let placeholder: string; - if (ctrl.vm.timeout) placeholder = ctrl.trans('youHaveBeenTimedOut'); + if (ctrl.vm.timeout) placeholder = i18n.site.youHaveBeenTimedOut; else if (ctrl.opts.blind) placeholder = 'Chat'; - else placeholder = ctrl.trans.noarg(ctrl.vm.placeholderKey); + else placeholder = i18n.site.talkInChat; return h('input.mchat__say', { attrs: { placeholder, diff --git a/ui/chat/src/interfaces.ts b/ui/chat/src/interfaces.ts index 5f440de1e0c7f..90533aba83461 100644 --- a/ui/chat/src/interfaces.ts +++ b/ui/chat/src/interfaces.ts @@ -15,7 +15,6 @@ export interface ChatOpts { public: boolean; permissions: Permissions; timeoutReasons?: ModerationReason[]; - i18n: I18nDict; preset?: string; noteId?: string; noteText?: string; @@ -72,7 +71,6 @@ export interface ChatPalantir { export interface ViewModel { tab: Tab; enabled: boolean; - placeholderKey: string; loading: boolean; autofocus: boolean; timeout: boolean; @@ -83,13 +81,11 @@ export interface ViewModel { export interface NoteOpts { id: string; text?: string; - trans: Trans; redraw: Redraw; } export interface NoteCtrl { id: string; - trans: Trans; text(): string | undefined; fetch(): void; post(text: string): void; diff --git a/ui/chat/src/note.ts b/ui/chat/src/note.ts index 97ed28dfe7c07..430d3532708ca 100644 --- a/ui/chat/src/note.ts +++ b/ui/chat/src/note.ts @@ -10,7 +10,6 @@ export function noteCtrl(opts: NoteOpts): NoteCtrl { }, 1000); return { id: opts.id, - trans: opts.trans, text: () => text, fetch() { xhr.getNote(opts.id).then(t => { @@ -29,7 +28,7 @@ export function noteView(ctrl: NoteCtrl, autofocus: boolean): VNode { const text = ctrl.text(); if (text == undefined) return h('div.loading', { hook: { insert: ctrl.fetch } }); return h('textarea.mchat__note', { - attrs: { placeholder: ctrl.trans('typePrivateNotesHere') }, + attrs: { placeholder: i18n.site.typePrivateNotesHere }, hook: { insert(vnode) { const el = vnode.elm as HTMLTextAreaElement; diff --git a/ui/chat/src/view.ts b/ui/chat/src/view.ts index 075c224c59098..5a856e3ecf95c 100644 --- a/ui/chat/src/view.ts +++ b/ui/chat/src/view.ts @@ -89,13 +89,13 @@ function tabName(ctrl: ChatCtrl, tab: Tab) { h('label', { attrs: { for: id, - title: ctrl.trans.noarg('toggleTheChat'), + title: i18n.site.toggleTheChat, }, }), ]), ]; } - if (tab === 'note') return [h('span', ctrl.trans.noarg('notes'))]; + if (tab === 'note') return [h('span', i18n.site.notes)]; if (ctrl.plugin && tab === ctrl.plugin.tab.key) return [h('span', ctrl.plugin.tab.name)]; return []; } diff --git a/ui/common/src/controls.ts b/ui/common/src/controls.ts index 16afbe2cd38d5..f53e0df1a28a6 100644 --- a/ui/common/src/controls.ts +++ b/ui/common/src/controls.ts @@ -13,11 +13,11 @@ export interface ToggleSettings { change(v: boolean): void; } -export function toggle(t: ToggleSettings, trans: Trans, redraw: () => void): VNode { +export function toggle(t: ToggleSettings, redraw: () => void): VNode { const fullId = 'abset-' + t.id; return h( 'div.setting.' + fullId + (t.cls ? '.' + t.cls : ''), - t.title ? { attrs: { title: trans.noarg(t.title) } } : {}, + t.title ? { attrs: { title: t.title } } : {}, [ h('div.switch', [ h('input#' + fullId + '.cmn-toggle', { @@ -26,7 +26,7 @@ export function toggle(t: ToggleSettings, trans: Trans, redraw: () => void): VNo }), h('label', { attrs: { for: fullId } }), ]), - h('label', { attrs: { for: fullId } }, trans.noarg(t.name)), + h('label', { attrs: { for: fullId } }, t.name), ], ); } diff --git a/ui/common/src/i18n.ts b/ui/common/src/i18n.ts index d5d86c87c0fa7..af5e31118c17f 100644 --- a/ui/common/src/i18n.ts +++ b/ui/common/src/i18n.ts @@ -1,3 +1,7 @@ +type I18nKey = string; +type I18nDict = Record; +type Trans = any; + export const trans = (i18n: I18nDict): Trans => { const trans: Trans = (key: I18nKey, ...args: Array) => { const str = i18n[key]; @@ -58,13 +62,14 @@ export const formatAgo = (seconds: number): string => { const absSeconds = Math.abs(seconds); const strIndex = seconds < 0 ? 1 : 0; const unit = agoUnits.find(unit => absSeconds >= unit[2] * unit[3] && unit[strIndex])!; - return site.trans.pluralSame(unit[strIndex]!, Math.floor(absSeconds / unit[2])); + const fmt = i18n.timeago[unit[strIndex]!]; + return typeof fmt === 'string' ? fmt : fmt(Math.floor(absSeconds / unit[2])); }; type DateLike = Date | number | string; // past, future, divisor, at least -const agoUnits: [string | undefined, string, number, number][] = [ +const agoUnits: [keyof I18n['timeago'] | undefined, keyof I18n['timeago'], number, number][] = [ ['nbYearsAgo', 'inNbYears', 60 * 60 * 24 * 365, 1], ['nbMonthsAgo', 'inNbMonths', (60 * 60 * 24 * 365) / 12, 1], ['nbWeeksAgo', 'inNbWeeks', 60 * 60 * 24 * 7, 1], diff --git a/ui/common/src/linkPopup.ts b/ui/common/src/linkPopup.ts index a8205e18af0a0..47a77ab661d5d 100644 --- a/ui/common/src/linkPopup.ts +++ b/ui/common/src/linkPopup.ts @@ -1,14 +1,14 @@ import { domDialog } from './dialog'; -export const makeLinkPopups = (dom: HTMLElement | Cash, trans: Trans, selector = 'a[href^="http"]'): void => { +export const makeLinkPopups = (dom: HTMLElement | Cash, selector = 'a[href^="http"]'): void => { const $el = $(dom); if (!$el.hasClass('link-popup-ready')) $el.addClass('link-popup-ready').on('click', selector, function (this: HTMLLinkElement) { - return onClick(this, trans); + return onClick(this); }); }; -export const onClick = (a: HTMLLinkElement, trans: Trans): boolean => { +export const onClick = (a: HTMLLinkElement): boolean => { const url = new URL(a.href); if (isPassList(url)) return true; @@ -18,14 +18,14 @@ export const onClick = (a: HTMLLinkElement, trans: Trans): boolean => { htmlText: ` `, }).then(dlg => { diff --git a/ui/common/src/socket.ts b/ui/common/src/socket.ts index 8a48c57cca9f8..0516bd0042a7a 100644 --- a/ui/common/src/socket.ts +++ b/ui/common/src/socket.ts @@ -114,7 +114,7 @@ export default class StrongSocket implements SocketI { if (!isOnline()) { document.body.classList.remove('online'); document.body.classList.add('offline'); - $('#network-status').text(site ? site.trans('noNetwork') : 'Offline'); + $('#network-status').text(i18n?.site?.noNetwork ?? 'Offline'); this.scheduleConnect(4000); return; } @@ -195,7 +195,7 @@ export default class StrongSocket implements SocketI { this.connectSchedule = setTimeout(() => { document.body.classList.add('offline'); document.body.classList.remove('online'); - $('#network-status').text(site.trans ? site.trans('reconnecting') : 'Reconnecting'); + $('#network-status').text(i18n?.site?.reconnecting ?? 'Reconnecting'); this.tryOtherUrl = true; this.connect(); }, delay); diff --git a/ui/coordinateTrainer/src/ctrl.ts b/ui/coordinateTrainer/src/ctrl.ts index e3f808b329b87..688c4210e1145 100644 --- a/ui/coordinateTrainer/src/ctrl.ts +++ b/ui/coordinateTrainer/src/ctrl.ts @@ -1,7 +1,6 @@ import { sparkline } from '@fnando/sparkline'; import * as xhr from 'common/xhr'; import { throttlePromiseDelay } from 'common/timing'; -import { trans } from 'common/i18n'; import { withEffect } from 'common'; import { makeVoice, VoiceCtrl } from 'voice'; import { storedBooleanProp, storedProp } from 'common/storage'; @@ -73,7 +72,6 @@ export default class CoordinateTrainerCtrl { score = 0; timeAtStart: Date; timeLeft = DURATION; - trans: Trans = trans(this.config.i18n); wrong: boolean; wrongTimeout: number; zen: boolean; diff --git a/ui/coordinateTrainer/src/interfaces.ts b/ui/coordinateTrainer/src/interfaces.ts index 0e833fcda889e..ef1db1300f114 100644 --- a/ui/coordinateTrainer/src/interfaces.ts +++ b/ui/coordinateTrainer/src/interfaces.ts @@ -17,7 +17,6 @@ export interface ModeScores { } export interface CoordinateTrainerConfig { - i18n: I18nDict; is3d: boolean; resizePref: number; scores: ModeScores; diff --git a/ui/coordinateTrainer/src/side.ts b/ui/coordinateTrainer/src/side.ts index ea15a8fad8cf2..b629ced2c5ff7 100644 --- a/ui/coordinateTrainer/src/side.ts +++ b/ui/coordinateTrainer/src/side.ts @@ -5,9 +5,9 @@ import { ColorChoice, TimeControl, Mode } from './interfaces'; import { toggle } from 'common/controls'; const colors: [ColorChoice, string][] = [ - ['black', 'asBlack'], - ['random', 'randomColor'], - ['white', 'asWhite'], + ['black', i18n.site.asBlack], + ['random', i18n.site.randomColor], + ['white', i18n.site.asWhite], ]; const timeControls: [TimeControl, string][] = [ @@ -109,12 +109,13 @@ const configurationButtons = (ctrl: CoordinateTrainerCtrl): VNodes => [ { attrs: { for: `coord_mode_${mode}`, - title: ctrl.trans( - mode === 'findSquare' ? 'aCoordinateAppears' : 'aSquareIsHighlightedExplanation', - ), + title: + i18n.coordinates[ + mode === 'findSquare' ? 'aCoordinateAppears' : 'aSquareIsHighlightedExplanation' + ], }, }, - ctrl.trans(mode), + i18n.coordinates[mode], ), ]), ), @@ -146,9 +147,10 @@ const configurationButtons = (ctrl: CoordinateTrainerCtrl): VNodes => [ { attrs: { for: `coord_timeControl_${timeControl}`, - title: ctrl.trans( - timeControl === 'thirtySeconds' ? 'youHaveThirtySeconds' : 'goAsLongAsYouWant', - ), + title: + i18n.coordinates[ + timeControl === 'thirtySeconds' ? 'youHaveThirtySeconds' : 'goAsLongAsYouWant' + ], }, }, timeControlLabel, @@ -160,7 +162,7 @@ const configurationButtons = (ctrl: CoordinateTrainerCtrl): VNodes => [ h('form.color.buttons', [ h( 'group.radio', - colors.map(([key, i18n]) => + colors.map(([key, text]) => h('div', [ h('input', { attrs: { @@ -178,11 +180,7 @@ const configurationButtons = (ctrl: CoordinateTrainerCtrl): VNodes => [ keyup: ctrl.onRadioInputKeyUp, }, }), - h( - `label.color_${key}`, - { attrs: { for: `coord_color_${key}`, title: ctrl.trans.noarg(i18n) } }, - h('i'), - ), + h(`label.color_${key}`, { attrs: { for: `coord_color_${key}`, title: text } }, h('i')), ]), ), ), @@ -196,12 +194,12 @@ const scoreCharts = (ctrl: CoordinateTrainerCtrl): VNode => h( 'div.scores', [ - ['white', 'averageScoreAsWhiteX', ctrl.modeScores[ctrl.mode()].white], - ['black', 'averageScoreAsBlackX', ctrl.modeScores[ctrl.mode()].black], - ].map(([color, transKey, scoreList]: [Color, string, number[]]) => + ['white', i18n.coordinates.averageScoreAsWhiteX, ctrl.modeScores[ctrl.mode()].white], + ['black', i18n.coordinates.averageScoreAsBlackX, ctrl.modeScores[ctrl.mode()].black], + ].map(([color, fmt, scoreList]: [Color, I18nFormat, number[]]) => scoreList.length ? h('div.color-chart', [ - h('p', ctrl.trans.vdom(transKey, h('strong', `${average(scoreList).toFixed(2)}`))), + h('p', fmt.asArray(h('strong', `${average(scoreList).toFixed(2)}`))), h('svg.sparkline', { attrs: { height: '80px', 'stroke-width': '3', id: `${color}-sparkline` }, hook: { insert: vnode => ctrl.updateChart(vnode.elm as SVGSVGElement, color) }, @@ -213,19 +211,19 @@ const scoreCharts = (ctrl: CoordinateTrainerCtrl): VNode => ); const scoreBox = (ctrl: CoordinateTrainerCtrl): VNode => - h('div.box.current-status', [h('h1', ctrl.trans('score')), h('div.score', ctrl.score)]); + h('div.box.current-status', [h('h1', i18n.storm.score), h('div.score', ctrl.score)]); const timeBox = (ctrl: CoordinateTrainerCtrl): VNode => h('div.box.current-status', [ - h('h1', ctrl.trans('time')), + h('h1', i18n.site.time), h('div.timer', { class: { hurry: ctrl.timeLeft <= 10 * 1000 } }, (ctrl.timeLeft / 1000).toFixed(1)), ]); const backButton = (ctrl: CoordinateTrainerCtrl): VNode => - h('div.back', h('a.back-button', { hook: bind('click', ctrl.stop) }, `« ${ctrl.trans('back')}`)); + h('div.back', h('a.back-button', { hook: bind('click', ctrl.stop) }, `« ${i18n.study.back}`)); const settings = (ctrl: CoordinateTrainerCtrl): VNode => { - const { trans, redraw, showCoordinates, showCoordsOnAllSquares, showPieces } = ctrl; + const { redraw, showCoordinates, showCoordsOnAllSquares, showPieces } = ctrl; return h('div.settings', [ ctrl.mode() === 'findSquare' ? toggle( @@ -235,14 +233,12 @@ const settings = (ctrl: CoordinateTrainerCtrl): VNode => { checked: ctrl.selectionEnabled(), change: ctrl.selectionEnabled, }, - trans, redraw, ) : null, ...filesAndRanksSelection(ctrl), toggle( { name: 'showCoordinates', id: 'showCoordinates', checked: showCoordinates(), change: showCoordinates }, - trans, redraw, ), toggle( @@ -253,24 +249,16 @@ const settings = (ctrl: CoordinateTrainerCtrl): VNode => { change: showCoordsOnAllSquares, disabled: !ctrl.showCoordinates(), }, - trans, - redraw, - ), - toggle( - { name: 'showPieces', id: 'showPieces', checked: showPieces(), change: showPieces }, - trans, redraw, ), + toggle({ name: 'showPieces', id: 'showPieces', checked: showPieces(), change: showPieces }, redraw), ]); }; const playingAs = (ctrl: CoordinateTrainerCtrl): VNode => { return h('div.box.current-status.current-status--color', [ h(`label.color_${ctrl.orientation}`, h('i')), - h( - 'em', - ctrl.trans.noarg(ctrl.orientation === 'white' ? 'youPlayTheWhitePieces' : 'youPlayTheBlackPieces'), - ), + h('em', i18n.site[ctrl.orientation === 'white' ? 'youPlayTheWhitePieces' : 'youPlayTheBlackPieces']), ]); }; diff --git a/ui/coordinateTrainer/src/view.ts b/ui/coordinateTrainer/src/view.ts index 1574b8afffd97..a7502176fb84d 100644 --- a/ui/coordinateTrainer/src/view.ts +++ b/ui/coordinateTrainer/src/view.ts @@ -33,18 +33,25 @@ const textOverlay = (ctrl: CoordinateTrainerCtrl): VNode | false => { }; const explanation = (ctrl: CoordinateTrainerCtrl): VNode => { - const { trans } = ctrl; return h('div.explanation.box', [ - h('h1', trans('coordinates')), - h('p', trans('knowingTheChessBoard')), + h('h1', i18n.coordinates.coordinates), + h('p', i18n.coordinates.knowingTheChessBoard), h('ul', [ - h('li', trans('mostChessCourses')), - h('li', trans('talkToYourChessFriends')), - h('li', trans('youCanAnalyseAGameMoreEffectively')), + h('li', i18n.coordinates.mostChessCourses), + h('li', i18n.coordinates.talkToYourChessFriends), + h('li', i18n.coordinates.youCanAnalyseAGameMoreEffectively), ]), - h('strong', trans(ctrl.mode())), - h('p', trans(ctrl.mode() === 'findSquare' ? 'aCoordinateAppears' : 'aSquareIsHighlightedExplanation')), - h('p', trans(ctrl.timeControl() === 'thirtySeconds' ? 'youHaveThirtySeconds' : 'goAsLongAsYouWant')), + h('strong', i18n.coordinates[ctrl.mode()]), + h( + 'p', + i18n.coordinates[ + ctrl.mode() === 'findSquare' ? 'aCoordinateAppears' : 'aSquareIsHighlightedExplanation' + ], + ), + h( + 'p', + i18n.coordinates[ctrl.timeControl() === 'thirtySeconds' ? 'youHaveThirtySeconds' : 'goAsLongAsYouWant'], + ), ]); }; @@ -52,7 +59,11 @@ const table = (ctrl: CoordinateTrainerCtrl): VNode => { return h('div.table', [ !ctrl.hasPlayed && explanation(ctrl), !ctrl.playing && - h('button.start.button.button-fat', { hook: bind('click', ctrl.start) }, ctrl.trans('startTraining')), + h( + 'button.start.button.button-fat', + { hook: bind('click', ctrl.start) }, + i18n.coordinates.startTraining, + ), ]); }; diff --git a/ui/dasher/src/background.ts b/ui/dasher/src/background.ts index e141f9e459ded..be28513a4908c 100644 --- a/ui/dasher/src/background.ts +++ b/ui/dasher/src/background.ts @@ -29,9 +29,9 @@ export class BackgroundCtrl extends PaneCtrl { constructor(root: DasherCtrl) { super(root); this.list = [ - { key: 'system', name: this.trans.noarg('deviceTheme') }, - { key: 'light', name: this.trans.noarg('light') }, - { key: 'dark', name: this.trans.noarg('dark') }, + { key: 'system', name: i18n.site.deviceTheme }, + { key: 'light', name: i18n.site.light }, + { key: 'dark', name: i18n.site.dark }, { key: 'transp', name: 'Picture' }, ]; } @@ -40,7 +40,7 @@ export class BackgroundCtrl extends PaneCtrl { const cur = this.get(); return h('div.sub.background', [ - header(this.trans.noarg('background'), this.close), + header(i18n.site.background, this.close), h( 'div.selector.large', this.list.map(bg => { @@ -112,7 +112,7 @@ export class BackgroundCtrl extends PaneCtrl { private imageInput = () => h('div.image', [ - h('p', this.trans.noarg('backgroundImageUrl')), + h('p', i18n.site.backgroundImageUrl), h('input', { attrs: { type: 'text', placeholder: 'https://', value: this.getImage() }, hook: { diff --git a/ui/dasher/src/board.ts b/ui/dasher/src/board.ts index e6f625e698d83..f6e984796ae3c 100644 --- a/ui/dasher/src/board.ts +++ b/ui/dasher/src/board.ts @@ -26,7 +26,7 @@ export class BoardCtrl extends PaneCtrl { render = (): VNode => h(`div.sub.board.${this.dimension}`, [ - header(this.trans.noarg('board'), this.close), + header(i18n.site.board, this.close), h('div.selector.large', [ h( 'button.text', @@ -55,7 +55,7 @@ export class BoardCtrl extends PaneCtrl { attrs: { 'data-icon': licon.Back, type: 'button' }, hook: bind('click', this.reset), }, - this.trans.noarg('boardReset'), + i18n.site.boardReset, ), h( 'div.list', @@ -159,19 +159,15 @@ export class BoardCtrl extends PaneCtrl { private propSliders = () => { const sliders = []; if (!Number.isNaN(this.getVar('zoom'))) - sliders.push(this.propSlider('zoom', this.trans.noarg('size'), { min: 0, max: 100, step: 1 })); + sliders.push(this.propSlider('zoom', i18n.site.size, { min: 0, max: 100, step: 1 })); if (document.body.dataset.theme === 'transp') - sliders.push( - this.propSlider('board-opacity', this.trans.noarg('opacity'), { min: 0, max: 100, step: 1 }), - ); + sliders.push(this.propSlider('board-opacity', i18n.site.opacity, { min: 0, max: 100, step: 1 })); else - sliders.push( - this.propSlider('board-brightness', this.trans.noarg('brightness'), { min: 20, max: 140, step: 1 }), - ); + sliders.push(this.propSlider('board-brightness', i18n.site.brightness, { min: 20, max: 140, step: 1 })); sliders.push( this.propSlider( 'board-hue', - this.trans.noarg('hue'), + i18n.site.hue, { min: 0, max: 100, step: 1 }, v => `+ ${Math.round(v * 3.6)}°`, ), diff --git a/ui/dasher/src/ctrl.ts b/ui/dasher/src/ctrl.ts index 909606ff2e9b0..7a1db8e585a37 100644 --- a/ui/dasher/src/ctrl.ts +++ b/ui/dasher/src/ctrl.ts @@ -8,7 +8,6 @@ import { LinksCtrl } from './links'; import { MaybeVNode, Redraw } from 'common/snabbdom'; import { DasherData, Mode, PaneCtrl } from './interfaces'; import { Prop, prop } from 'common'; -import { trans } from 'common/i18n'; import { pubsub } from 'common/pubsub'; const defaultMode = 'links'; @@ -16,7 +15,6 @@ const defaultMode = 'links'; type ModeIndexed = { [key in Mode]: PaneCtrl }; export class DasherCtrl implements ModeIndexed { - trans: Trans; ping: PingCtrl; langs: LangsCtrl; sound: SoundCtrl; @@ -33,7 +31,6 @@ export class DasherCtrl implements ModeIndexed { readonly data: DasherData, readonly redraw: Redraw, ) { - this.trans = trans(data.i18n); this.ping = new PingCtrl(this); this.langs = new LangsCtrl(this); this.sound = new SoundCtrl(this); diff --git a/ui/dasher/src/interfaces.ts b/ui/dasher/src/interfaces.ts index 4dca8f3e9c000..3f4c6c30aafef 100644 --- a/ui/dasher/src/interfaces.ts +++ b/ui/dasher/src/interfaces.ts @@ -9,9 +9,6 @@ export { DasherCtrl }; export abstract class PaneCtrl { constructor(readonly root: DasherCtrl) {} - get trans(): Trans { - return this.root.trans; - } get redraw(): Redraw { return this.root.redraw; } @@ -39,7 +36,6 @@ export interface DasherData { piece: PieceData; coach: boolean; streamer: boolean; - i18n: I18nDict; } export type Mode = 'links' | 'langs' | 'sound' | 'background' | 'board' | 'piece'; diff --git a/ui/dasher/src/langs.ts b/ui/dasher/src/langs.ts index 6709b258986d5..6a038eda4902d 100644 --- a/ui/dasher/src/langs.ts +++ b/ui/dasher/src/langs.ts @@ -21,7 +21,7 @@ export class LangsCtrl extends PaneCtrl { render = (): VNode => h('div.sub.langs', [ - header(this.trans.noarg('language'), this.close), + header(i18n.site.language, this.close), h( 'form', { attrs: { method: 'post', action: '/translation/select' } }, diff --git a/ui/dasher/src/links.ts b/ui/dasher/src/links.ts index ea229296f7a4f..1f668e5975bfb 100644 --- a/ui/dasher/src/links.ts +++ b/ui/dasher/src/links.ts @@ -9,16 +9,15 @@ export class LinksCtrl extends PaneCtrl { } render = (): VNode => { - const modeCfg = this.modeCfg, - noarg = this.trans.noarg; + const modeCfg = this.modeCfg; return h('div', [ this.userLinks(), h('div.subs', [ - h('button.sub', modeCfg('langs'), noarg('language')), - h('button.sub', modeCfg('sound'), noarg('sound')), - h('button.sub', modeCfg('background'), noarg('background')), - h('button.sub', modeCfg('board'), noarg('board')), - h('button.sub', modeCfg('piece'), noarg('pieceSet')), + h('button.sub', modeCfg('langs'), i18n.site.language), + h('button.sub', modeCfg('sound'), i18n.site.sound), + h('button.sub', modeCfg('background'), i18n.site.background), + h('button.sub', modeCfg('board'), i18n.site.board), + h('button.sub', modeCfg('piece'), i18n.site.pieceSet), this.root.opts.zenable && h('div.zen.selector', [ h( @@ -27,7 +26,7 @@ export class LinksCtrl extends PaneCtrl { attrs: { 'data-icon': licon.DiscBigOutline, title: 'Keyboard: z', type: 'button' }, hook: bind('click', () => pubsub.emit('zen')), }, - this.trans.noarg('zenMode'), + i18n.preferences.zenMode, ), ]), ]), @@ -41,17 +40,16 @@ export class LinksCtrl extends PaneCtrl { private userLinks(): VNode | null { const d = this.data, - noarg = this.trans.noarg, linkCfg = this.linkCfg; return d.user ? h('div.links', [ h( 'a.user-link.online.text.is-green', linkCfg(`/@/${d.user.name}`, d.user.patron ? licon.Wings : licon.Disc), - noarg('profile'), + i18n.site.profile, ), - h('a.text', linkCfg('/inbox', licon.Envelope), noarg('inbox')), + h('a.text', linkCfg('/inbox', licon.Envelope), i18n.site.inbox), h( 'a.text', @@ -60,15 +58,15 @@ export class LinksCtrl extends PaneCtrl { licon.Gear, this.root.opts.playing ? { target: '_blank', rel: 'noopener' } : undefined, ), - noarg('preferences'), + i18n.preferences.preferences, ), - d.coach && h('a.text', linkCfg('/coach/edit', licon.GraduateCap), noarg('coachManager')), + d.coach && h('a.text', linkCfg('/coach/edit', licon.GraduateCap), i18n.site.coachManager), - d.streamer && h('a.text', linkCfg('/streamer/edit', licon.Mic), noarg('streamerManager')), + d.streamer && h('a.text', linkCfg('/streamer/edit', licon.Mic), i18n.site.streamerManager), h('form.logout', { attrs: { method: 'post', action: '/logout' } }, [ - h('button.text', { attrs: { type: 'submit', 'data-icon': licon.Power } }, noarg('logOut')), + h('button.text', { attrs: { type: 'submit', 'data-icon': licon.Power } }, i18n.site.logOut), ]), ]) : null; diff --git a/ui/dasher/src/piece.ts b/ui/dasher/src/piece.ts index 76dab0bd838c6..b54442ce6b2b3 100644 --- a/ui/dasher/src/piece.ts +++ b/ui/dasher/src/piece.ts @@ -27,7 +27,7 @@ export class PieceCtrl extends PaneCtrl { : `piece/${t}/wN.svg`; return h('div.sub.piece.' + this.dimension, [ - header(this.trans.noarg('pieceSet'), () => this.close()), + header(i18n.site.pieceSet, () => this.close()), h( 'div.list', { attrs: { style: `max-height:${maxHeight}px;` } }, diff --git a/ui/dasher/src/ping.ts b/ui/dasher/src/ping.ts index 6bd8093e3a6c5..3ac97fe1ca05f 100644 --- a/ui/dasher/src/ping.ts +++ b/ui/dasher/src/ping.ts @@ -34,12 +34,12 @@ export class PingCtrl { this.signalBars(), h( 'span.ping', - { attrs: { title: 'PING: ' + this.root.trans.noarg('networkLagBetweenYouAndLichess') } }, + { attrs: { title: 'PING: ' + i18n.site.networkLagBetweenYouAndLichess } }, this.showMillis('PING', this.ping), ), h( 'span.server', - { attrs: { title: 'SERVER: ' + this.root.trans.noarg('timeToProcessAMoveOnLichessServer') } }, + { attrs: { title: 'SERVER: ' + i18n.site.timeToProcessAMoveOnLichessServer } }, this.showMillis('SERVER', this.server), ), ]); diff --git a/ui/dasher/src/sound.ts b/ui/dasher/src/sound.ts index 50081014be20a..d03ebd346ff60 100644 --- a/ui/dasher/src/sound.ts +++ b/ui/dasher/src/sound.ts @@ -36,7 +36,7 @@ export class SoundCtrl extends PaneCtrl { }, }, [ - header(this.trans('sound'), this.close), + header(i18n.site.sound, this.close), h('div.content.force-ltr', [ h('input', { attrs: { diff --git a/ui/editor/src/ctrl.ts b/ui/editor/src/ctrl.ts index c6ac83495c7ab..794947629e443 100644 --- a/ui/editor/src/ctrl.ts +++ b/ui/editor/src/ctrl.ts @@ -17,11 +17,9 @@ import { Castles, defaultPosition, setupPosition } from 'chessops/variant'; import { makeFen, parseFen, parseCastlingFen, INITIAL_FEN, EMPTY_FEN } from 'chessops/fen'; import { lichessVariant, lichessRules } from 'chessops/compat'; import { defined, prop, Prop } from 'common'; -import { trans } from 'common/i18n'; export default class EditorCtrl { options: Options; - trans: Trans; chessground: CgApi | undefined; selected: Prop; @@ -43,8 +41,6 @@ export default class EditorCtrl { ) { this.options = cfg.options || {}; - this.trans = trans(this.cfg.i18n); - this.selected = prop('pointer'); if (cfg.positions) cfg.positions.forEach(p => (p.epd = p.fen.split(' ').splice(0, 4).join(' '))); diff --git a/ui/editor/src/interfaces.ts b/ui/editor/src/interfaces.ts index 46f100c8d7f6f..0be4f4edb6dd4 100644 --- a/ui/editor/src/interfaces.ts +++ b/ui/editor/src/interfaces.ts @@ -36,7 +36,6 @@ export interface Config { embed: boolean; positions?: OpeningPosition[]; endgamePositions?: EndgamePosition[]; - i18n: I18nDict; } export interface Options { diff --git a/ui/editor/src/view.ts b/ui/editor/src/view.ts index f55c0a8d655fc..b7b2eed789c5d 100644 --- a/ui/editor/src/view.ts +++ b/ui/editor/src/view.ts @@ -41,7 +41,7 @@ function studyButton(ctrl: EditorCtrl, state: EditorState): VNode { attrs: { type: 'submit', 'data-icon': licon.StudyBoard, disabled: !state.legalFen }, class: { button: true, 'button-empty': true, text: true, disabled: !state.legalFen }, }, - ctrl.trans.noarg('toStudy'), + i18n.site.toStudy, ), ]); } @@ -50,7 +50,7 @@ function variant2option(key: Rules, name: string, ctrl: EditorCtrl): VNode { return h( 'option', { attrs: { value: key, selected: key == ctrl.rules } }, - `${ctrl.trans.noarg('variant')} | ${name}`, + `${i18n.site.variant} | ${name}`, ); } @@ -74,13 +74,13 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { h( `a.button.button-empty${icon ? '.text' : ''}`, { on: { click: ctrl.startPosition }, attrs: icon ? dataIcon(icon) : {} }, - ctrl.trans.noarg('startPosition'), + i18n.site.startPosition, ); const buttonClear = (icon?: string) => h( `a.button.button-empty${icon ? '.text' : ''}`, { on: { click: ctrl.clearBoard }, attrs: icon ? dataIcon(icon) : {} }, - ctrl.trans.noarg('clearBoard'), + i18n.site.clearBoard, ); return h('div.board-editor__tools', [ @@ -97,25 +97,25 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { }, props: { value: ctrl.turn }, }, - ['whitePlays', 'blackPlays'].map(function (key) { + (['whitePlays', 'blackPlays'] as const).map(function (key) { return h( 'option', { attrs: { value: key[0] === 'w' ? 'white' : 'black', selected: key[0] === ctrl.turn[0] }, }, - ctrl.trans(key), + i18n.site[key], ); }), ), ), h('div.castling', [ - h('strong', ctrl.trans.noarg('castling')), + h('strong', i18n.site.castling), h('div', [ - castleCheckBox(ctrl, 'K', ctrl.trans.noarg('whiteCastlingKingside'), !!ctrl.options.inlineCastling), + castleCheckBox(ctrl, 'K', i18n.site.whiteCastlingKingside, !!ctrl.options.inlineCastling), castleCheckBox(ctrl, 'Q', 'O-O-O', true), ]), h('div', [ - castleCheckBox(ctrl, 'k', ctrl.trans.noarg('blackCastlingKingside'), !!ctrl.options.inlineCastling), + castleCheckBox(ctrl, 'k', i18n.site.blackCastlingKingside, !!ctrl.options.inlineCastling), castleCheckBox(ctrl, 'q', 'O-O-O', true), ]), ]), @@ -181,12 +181,9 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { }, }, [ - h('option', { attrs: { value: '' } }, ctrl.trans.noarg('setTheBoard')), - optgroup(ctrl.trans.noarg('popularOpenings'), ctrl.cfg.positions.map(positionOption)), - optgroup( - ctrl.trans.noarg('endgamePositions'), - ctrl.cfg.endgamePositions.map(endgamePosition2option), - ), + h('option', { attrs: { value: '' } }, i18n.site.setTheBoard), + optgroup(i18n.site.popularOpenings, ctrl.cfg.positions.map(positionOption)), + optgroup(i18n.site.endgamePositions, ctrl.cfg.endgamePositions.map(endgamePosition2option)), ], ); })(), @@ -222,7 +219,7 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { }, }, }, - ctrl.trans.noarg('flipBoard'), + i18n.site.flipBoard, ), h( 'a', @@ -241,7 +238,7 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { disabled: !state.legalFen, }, }, - ctrl.trans.noarg('analysis'), + i18n.site.analysis, ), h( 'button', @@ -253,13 +250,7 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { }, }, }, - [ - h( - 'span.text', - { attrs: { 'data-icon': licon.Swords } }, - ctrl.trans.noarg('continueFromHere'), - ), - ], + [h('span.text', { attrs: { 'data-icon': licon.Swords } }, i18n.site.continueFromHere)], ), studyButton(ctrl, state), ]), @@ -267,12 +258,12 @@ function controls(ctrl: EditorCtrl, state: EditorState): VNode { h( 'a.button', { attrs: { href: '/?fen=' + state.legalFen + '#ai', rel: 'nofollow' } }, - ctrl.trans.noarg('playWithTheMachine'), + i18n.site.playWithTheMachine, ), h( 'a.button', { attrs: { href: '/?fen=' + state.legalFen + '#friend', rel: 'nofollow' } }, - ctrl.trans.noarg('playWithAFriend'), + i18n.site.playWithAFriend, ), ]), ]), diff --git a/ui/game/src/interfaces.ts b/ui/game/src/interfaces.ts index d2e82d2d7ff57..23f634185e793 100644 --- a/ui/game/src/interfaces.ts +++ b/ui/game/src/interfaces.ts @@ -161,7 +161,6 @@ export interface Perf { export interface Ctrl { data: GameData; - trans: Trans; } export interface Blurs { @@ -170,11 +169,6 @@ export interface Blurs { bits?: string; } -export interface Trans { - (key: string): string; - noarg: (key: string) => string; -} - export interface Hold { ply: number; mean: number; diff --git a/ui/game/src/view/status.ts b/ui/game/src/view/status.ts index 9dce33d5e5c0d..4ebd2b06841f1 100644 --- a/ui/game/src/view/status.ts +++ b/ui/game/src/view/status.ts @@ -35,58 +35,59 @@ function insufficientMaterial(variant: VariantKey, fullFen: FEN): boolean { } export default function status(ctrl: Ctrl): string { - const noarg = ctrl.trans.noarg, - d = ctrl.data, - winnerSuffix = d.game.winner ? ' • ' + noarg(d.game.winner + 'IsVictorious') : ''; + const d = ctrl.data, + winnerSuffix = d.game.winner + ? ' • ' + i18n.site[d.game.winner === 'white' ? 'whiteIsVictorious' : 'blackIsVictorious'] + : ''; switch (d.game.status.name) { case 'started': - return noarg('playingRightNow'); + return i18n.site.playingRightNow; case 'aborted': - return noarg('gameAborted') + winnerSuffix; + return i18n.site.gameAborted + winnerSuffix; case 'mate': - return noarg('checkmate') + winnerSuffix; + return i18n.site.checkmate + winnerSuffix; case 'resign': - return noarg(d.game.winner == 'white' ? 'blackResigned' : 'whiteResigned') + winnerSuffix; + return i18n.site[d.game.winner == 'white' ? 'blackResigned' : 'whiteResigned'] + winnerSuffix; case 'stalemate': - return noarg('stalemate') + winnerSuffix; + return i18n.site.stalemate + winnerSuffix; case 'timeout': switch (d.game.winner) { case 'white': - return noarg('blackLeftTheGame') + winnerSuffix; + return i18n.site.blackLeftTheGame + winnerSuffix; case 'black': - return noarg('whiteLeftTheGame') + winnerSuffix; + return i18n.site.whiteLeftTheGame + winnerSuffix; default: - return `${d.game.turns % 2 === 0 ? noarg('whiteLeftTheGame') : noarg('blackLeftTheGame')} • ${noarg( - 'draw', - )}`; + return `${d.game.turns % 2 === 0 ? i18n.site.whiteLeftTheGame : i18n.site.blackLeftTheGame} • ${i18n.site.draw}`; } case 'draw': { if (d.game.fen.split(' ')[4] === '100') - return `${noarg('fiftyMovesWithoutProgress')} • ${noarg('draw')}`; - if (d.game.threefold) return `${noarg('threefoldRepetition')} • ${noarg('draw')}`; + return `${i18n.site.fiftyMovesWithoutProgress} • ${i18n.site.draw}`; + if (d.game.threefold) return `${i18n.site.threefoldRepetition} • ${i18n.site.draw}`; if (insufficientMaterial(d.game.variant.key, d.game.fen)) - return `${noarg('insufficientMaterial')} • ${noarg('draw')}`; - if (d.game.drawOffers?.some(turn => turn >= d.game.turns)) return noarg('drawByMutualAgreement'); - return noarg('draw'); + return `${i18n.site.insufficientMaterial} • ${i18n.site.draw}`; + if (d.game.drawOffers?.some(turn => turn >= d.game.turns)) return i18n.site.drawByMutualAgreement; + return i18n.site.draw; } case 'outoftime': - return `${d.game.turns % 2 === 0 ? noarg('whiteTimeOut') : noarg('blackTimeOut')}${ - winnerSuffix || ` • ${noarg('draw')}` + return `${d.game.turns % 2 === 0 ? i18n.site.whiteTimeOut : i18n.site.blackTimeOut}${ + winnerSuffix || ` • ${i18n.site.draw}` }`; case 'noStart': - return (d.game.winner == 'white' ? noarg('blackDidntMove') : noarg('whiteDidntMove')) + winnerSuffix; + return (d.game.winner == 'white' ? i18n.site.blackDidntMove : i18n.site.whiteDidntMove) + winnerSuffix; case 'cheat': - return noarg('cheatDetected') + winnerSuffix; + return i18n.site.cheatDetected + winnerSuffix; case 'variantEnd': switch (d.game.variant.key) { case 'kingOfTheHill': - return noarg('kingInTheCenter') + winnerSuffix; + return i18n.site.kingInTheCenter + winnerSuffix; case 'threeCheck': - return noarg('threeChecks') + winnerSuffix; + return i18n.site.threeChecks + winnerSuffix; } - return noarg('variantEnding') + winnerSuffix; + return i18n.site.variantEnding + winnerSuffix; case 'unknownFinish': - return d.game.winner ? noarg(d.game.winner + 'IsVictorious') : 'Finished'; + return d.game.winner + ? i18n.site[d.game.winner === 'white' ? 'whiteIsVictorious' : 'blackIsVictorious'] + : i18n.site.finished; default: return d.game.status.name + winnerSuffix; } diff --git a/ui/insight/src/ctrl.ts b/ui/insight/src/ctrl.ts index 32d43a3e03019..d84f4ba7b7eaf 100644 --- a/ui/insight/src/ctrl.ts +++ b/ui/insight/src/ctrl.ts @@ -188,5 +188,4 @@ export default class { this.askQuestion(); } } - // this.trans = site.trans(env.i18n); } diff --git a/ui/learn/src/congrats.ts b/ui/learn/src/congrats.ts deleted file mode 100644 index 1c514ac434ef9..0000000000000 --- a/ui/learn/src/congrats.ts +++ /dev/null @@ -1,27 +0,0 @@ -function shuffle(a: T[]) { - let j, x, i; - for (i = a.length; i; i -= 1) { - j = Math.floor(Math.random() * i); - x = a[i - 1]; - a[i - 1] = a[j]; - a[j] = x; - } -} - -const list = [ - 'awesome', - 'excellent', - 'greatJob', - 'perfect', - 'outstanding', - 'wayToGo', - 'yesYesYes', - 'youreGoodAtThis', - 'nailedIt', - 'rightOn', -]; -shuffle(list); - -let it = 0; - -export default (): string => list[it++ % list.length]; diff --git a/ui/learn/src/ctrl.ts b/ui/learn/src/ctrl.ts index 6e2cbb23e402a..f2f9f9a7e5746 100644 --- a/ui/learn/src/ctrl.ts +++ b/ui/learn/src/ctrl.ts @@ -6,11 +6,9 @@ import { SideCtrl } from './sideCtrl'; import { clearTimeouts } from './timeouts'; import { extractHashParameters } from './hashRouting'; import { RunCtrl } from './run/runCtrl'; -import { trans } from 'common/i18n'; export class LearnCtrl { data: LearnProgress = this.opts.storage.data; - trans: Trans = trans(this.opts.i18n); sideCtrl: SideCtrl; runCtrl: RunCtrl; @@ -24,7 +22,7 @@ export class LearnCtrl { this.setStageLevelFromHash(); this.sideCtrl = new SideCtrl(this, opts); - this.runCtrl = new RunCtrl(this, opts, redraw); + this.runCtrl = new RunCtrl(opts, redraw); window.addEventListener('hashchange', () => { this.setStageLevelFromHash(); diff --git a/ui/learn/src/learn.ts b/ui/learn/src/learn.ts index 4a9b632f36504..5d1a70d4af4a8 100644 --- a/ui/learn/src/learn.ts +++ b/ui/learn/src/learn.ts @@ -25,7 +25,6 @@ export interface StageProgress { } export interface LearnOpts { - i18n: I18nDict; storage: Storage; stageId: number | null; levelId: number | null; @@ -40,14 +39,12 @@ export interface LearnPrefs { interface LearnServerOpts { data?: LearnProgress; - i18n: I18nDict; pref: LearnPrefs; } -export function initModule({ data, i18n, pref }: LearnServerOpts) { +export function initModule({ data, pref }: LearnServerOpts) { const _storage = storage(data); const opts: LearnOpts = { - i18n, storage: _storage, stageId: null, levelId: null, diff --git a/ui/learn/src/mapSideView.ts b/ui/learn/src/mapSideView.ts index 8a396b89ce539..03c751dfb0624 100644 --- a/ui/learn/src/mapSideView.ts +++ b/ui/learn/src/mapSideView.ts @@ -19,10 +19,7 @@ function renderInStage(ctrl: SideCtrl) { { attrs: { href: BASE_LEARN_PATH }, }, - [ - h('img', { attrs: { src: util.assetUrl + 'images/learn/brutal-helm.svg' } }), - ctrl.trans.noarg('menu'), - ], + [h('img', { attrs: { src: util.assetUrl + 'images/learn/brutal-helm.svg' } }), i18n.site.menu], ), ...stages.categs.map((categ, categId) => h( @@ -31,7 +28,7 @@ function renderInStage(ctrl: SideCtrl) { class: { active: categId == ctrl.categId() }, }, [ - h('h2', { hook: bind('click', () => ctrl.categId(categId)) }, ctrl.trans.noarg(categ.name)), + h('h2', { hook: bind('click', () => ctrl.categId(categId)) }, categ.name), h( 'div.categ_stages', categ.stages.map(s => { @@ -42,7 +39,7 @@ function renderInStage(ctrl: SideCtrl) { { attrs: { href: hashHref(s.id) }, }, - [h('img', { attrs: { src: s.image } }), h('span', ctrl.trans.noarg(s.title))], + [h('img', { attrs: { src: s.image } }), h('span', s.title)], ); }), ), @@ -57,10 +54,10 @@ function renderHome(ctrl: SideCtrl) { const progress = ctrl.progress(); return h('div.learn__side-home', [ h('i.fat'), - h('h1', ctrl.trans.noarg('learnChess')), - h('h2', ctrl.trans.noarg('byPlaying')), + h('h1', i18n.learn.learnChess), + h('h2', i18n.learn.byPlaying), h('div.progress', [ - h('div.text', ctrl.trans('progressX', progress + '%')), + h('div.text', i18n.learn.progressX(progress + '%')), h('div.bar', { style: { width: progress + '%', @@ -72,12 +69,9 @@ function renderHome(ctrl: SideCtrl) { ? h( 'a.confirm', { - hook: bind( - 'click', - () => confirm(ctrl.trans.noarg('youWillLoseAllYourProgress')) && ctrl.reset(), - ), + hook: bind('click', () => confirm(i18n.learn.youWillLoseAllYourProgress) && ctrl.reset()), }, - ctrl.trans.noarg('resetMyProgress'), + i18n.learn.resetMyProgress, ) : null, ]), diff --git a/ui/learn/src/promotionView.ts b/ui/learn/src/promotionView.ts index 4fb6364349c6f..010e83034f2f0 100644 --- a/ui/learn/src/promotionView.ts +++ b/ui/learn/src/promotionView.ts @@ -42,16 +42,16 @@ export function promotionView(ctrl: RunCtrl) { h('piece.' + role + '.' + color), ), ), - explain ? renderExplanation(ctrl) : null, + explain ? renderExplanation() : null, ], ); } -function renderExplanation(ctrl: RunCtrl) { +function renderExplanation() { return h('div.explanation', [ - h('h2', ctrl.trans.noarg('pawnPromotion')), - h('p', ctrl.trans.noarg('yourPawnReachedTheEndOfTheBoard')), - h('p', ctrl.trans.noarg('itNowPromotesToAStrongerPiece')), - h('p', ctrl.trans.noarg('selectThePieceYouWant')), + h('h2', i18n.learn.pawnPromotion), + h('p', i18n.learn.yourPawnReachedTheEndOfTheBoard), + h('p', i18n.learn.itNowPromotesToAStrongerPiece), + h('p', i18n.learn.selectThePieceYouWant), ]); } diff --git a/ui/learn/src/run/congrats.ts b/ui/learn/src/run/congrats.ts index 45af3a44938b9..aa0410c45489e 100644 --- a/ui/learn/src/run/congrats.ts +++ b/ui/learn/src/run/congrats.ts @@ -7,17 +7,17 @@ function shuffle(a: T[]) { } } -const list = [ - 'awesome', - 'excellent', - 'greatJob', - 'perfect', - 'outstanding', - 'wayToGo', - 'yesYesYes', - 'youreGoodAtThis', - 'nailedIt', - 'rightOn', +const list: string[] = [ + i18n.learn.awesome, + i18n.learn.excellent, + i18n.learn.greatJob, + i18n.learn.perfect, + i18n.learn.outstanding, + i18n.learn.wayToGo, + i18n.learn.yesYesYes, + i18n.learn.youreGoodAtThis, + i18n.learn.nailedIt, + i18n.learn.rightOn, ]; shuffle(list); diff --git a/ui/learn/src/run/runCtrl.ts b/ui/learn/src/run/runCtrl.ts index 52c18333ac666..ae14f80b8e140 100644 --- a/ui/learn/src/run/runCtrl.ts +++ b/ui/learn/src/run/runCtrl.ts @@ -3,7 +3,6 @@ import * as stages from '../stage/list'; import { Prop, prop } from 'common'; import { LearnProgress, LearnOpts } from '../learn'; import { Stage } from '../stage/list'; -import { LearnCtrl } from '../ctrl'; import { clearTimeouts } from '../timeouts'; import { LevelCtrl } from '../levelCtrl'; import { hashNavigate } from '../hashRouting'; @@ -11,7 +10,6 @@ import { WithGround } from '../util'; export class RunCtrl { data: LearnProgress = this.opts.storage.data; - trans: Trans; chessground: CgApi | undefined; levelCtrl: LevelCtrl; @@ -24,14 +22,11 @@ export class RunCtrl { } constructor( - ctrl: LearnCtrl, readonly opts: LearnOpts, readonly redraw: () => void, ) { clearTimeouts(); - this.trans = ctrl.trans; - this.initializeLevel(); // Helpful for debugging: diff --git a/ui/learn/src/run/runView.ts b/ui/learn/src/run/runView.ts index 3a202b6e23002..0fb6c5de80b3e 100644 --- a/ui/learn/src/run/runView.ts +++ b/ui/learn/src/run/runView.ts @@ -14,11 +14,11 @@ import { promotionView } from '../promotionView'; const renderFailed = (ctrl: RunCtrl): VNode => h('div.result.failed', { hook: bind('click', ctrl.restart) }, [ - h('h2', ctrl.trans.noarg('puzzleFailed')), - h('button', ctrl.trans.noarg('retry')), + h('h2', i18n.learn.puzzleFailed), + h('button', i18n.learn.retry), ]); -const renderCompleted = (ctrl: RunCtrl, level: LevelCtrl): VNode => +const renderCompleted = (level: LevelCtrl): VNode => h( 'div.result.completed', { @@ -26,10 +26,8 @@ const renderCompleted = (ctrl: RunCtrl, level: LevelCtrl): VNode => hook: bind('click', level.onComplete), }, [ - h('h2', ctrl.trans.noarg(congrats())), - level.blueprint.nextButton - ? h('button', ctrl.trans.noarg('next')) - : makeStars(level.blueprint, level.vm.score), + h('h2', congrats()), + level.blueprint.nextButton ? h('button', i18n.learn.next) : makeStars(level.blueprint, level.vm.score), ], ); @@ -58,16 +56,13 @@ export const runView = (ctrl: LearnCtrl) => { h('div.wrap', [ h('div.title', [ h('img', { attrs: { src: stage.image } }), - h('div.text', [ - h('h2', ctrl.trans.noarg(stage.title)), - h('p.subtitle', ctrl.trans.noarg(stage.subtitle)), - ]), + h('div.text', [h('h2', stage.title), h('p.subtitle', stage.subtitle)]), ]), levelCtrl.vm.failed ? renderFailed(runCtrl) : levelCtrl.vm.completed - ? renderCompleted(runCtrl, levelCtrl) - : h('div.goal', util.withLinebreaks(ctrl.trans.noarg(levelCtrl.blueprint.goal))), + ? renderCompleted(levelCtrl) + : h('div.goal', util.withLinebreaks(levelCtrl.blueprint.goal)), progressView(runCtrl), ]), ]), diff --git a/ui/learn/src/run/stageComplete.ts b/ui/learn/src/run/stageComplete.ts index 4ba7601275ef4..718d016178979 100644 --- a/ui/learn/src/run/stageComplete.ts +++ b/ui/learn/src/run/stageComplete.ts @@ -27,11 +27,10 @@ export default function (ctrl: RunCtrl) { }, h('div.learn__screen', [ h('div.stars', makeStars(scoring.getStageRank(stage, score))), - h('h1', ctrl.trans('stageXComplete', stage.id)), + h('h1', i18n.learn.stageXComplete(stage.id)), h( 'span.score', - ctrl.trans.vdom( - 'yourScore', + i18n.site.yourScore.asArray( h( 'span', { @@ -46,19 +45,15 @@ export default function (ctrl: RunCtrl) { ), ), ), - h('p', util.withLinebreaks(ctrl.trans.noarg(stage.complete))), + h('p', util.withLinebreaks(stage.complete)), h('div.buttons', [ next ? h('a.next', { hook: bind('click', () => hashNavigate(next.id)) }, [ - ctrl.trans('nextX', ctrl.trans.noarg(next.title)) + ' ', + i18n.learn.nextX(next.title) + ' ', h('i', { attrs: { 'data-icon': '' } }), ]) : null, - h( - 'a.back.text[data-icon=]', - { hook: bind('click', () => hashNavigate()) }, - ctrl.trans.noarg('backToMenu'), - ), + h('a.back.text[data-icon=]', { hook: bind('click', () => hashNavigate()) }, i18n.learn.backToMenu), ]), ]), ); diff --git a/ui/learn/src/run/stageStarting.ts b/ui/learn/src/run/stageStarting.ts index 9f47021531138..2b38c565898cc 100644 --- a/ui/learn/src/run/stageStarting.ts +++ b/ui/learn/src/run/stageStarting.ts @@ -8,9 +8,9 @@ export default function (ctrl: RunCtrl) { 'div.learn__screen-overlay', { hook: bind('click', ctrl.hideStartingPane) }, h('div.learn__screen', [ - h('h1', ctrl.trans('stageX', ctrl.stage.id) + ': ' + ctrl.trans.noarg(ctrl.stage.title)), + h('h1', i18n.learn.stageX(ctrl.stage.id) + ': ' + ctrl.stage.title), ctrl.stage.illustration, - h('p', util.withLinebreaks(ctrl.trans.noarg(ctrl.stage.intro))), + h('p', util.withLinebreaks(ctrl.stage.intro)), h( 'div.buttons', h( @@ -19,7 +19,7 @@ export default function (ctrl: RunCtrl) { key: ctrl.stage.id, hook: bind('click', ctrl.hideStartingPane), }, - ctrl.trans.noarg('letsGo'), + i18n.learn.letsGo, ), ), ]), diff --git a/ui/learn/src/sideCtrl.ts b/ui/learn/src/sideCtrl.ts index 388368b0b4ae4..2bd6550fe42e8 100644 --- a/ui/learn/src/sideCtrl.ts +++ b/ui/learn/src/sideCtrl.ts @@ -6,14 +6,12 @@ import { LearnCtrl } from './ctrl'; export class SideCtrl { opts: LearnOpts; - trans: Trans; data: LearnProgress; categId: Prop; constructor(ctrl: LearnCtrl, opts: LearnOpts) { this.opts = opts; - this.trans = ctrl.trans; this.data = ctrl.data; this.categId = propWithEffect(this.getCategIdFromStageId() ?? 0, ctrl.redraw); diff --git a/ui/learn/src/stage/bishop.ts b/ui/learn/src/stage/bishop.ts index e2551ced04e06..b6ee581accec5 100644 --- a/ui/learn/src/stage/bishop.ts +++ b/ui/learn/src/stage/bishop.ts @@ -3,50 +3,50 @@ import { StageNoID } from './list'; const stage: StageNoID = { key: 'bishop', - title: 'theBishop', - subtitle: 'itMovesDiagonally', + title: i18n.learn.theBishop, + subtitle: i18n.learn.itMovesDiagonally, image: assetUrl + 'images/learn/pieces/B.svg', - intro: 'bishopIntro', + intro: i18n.learn.bishopIntro, illustration: pieceImg('bishop'), levels: [ { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/8/8/8/8/5B2/8/8 w - -', apples: 'd5 g8', nbMoves: 2, shapes: [arrow('f3d5'), arrow('d5g8')], }, { - goal: 'theFewerMoves', + goal: i18n.learn.theFewerMoves, fen: '8/8/8/8/8/1B6/8/8 w - -', apples: 'a2 b1 b5 d1 d3 e2', nbMoves: 6, }, { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/8/8/8/3B4/8/8/8 w - -', apples: 'a1 b6 c1 e3 g7 h6', nbMoves: 6, }, { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/8/8/8/2B5/8/8/8 w - -', apples: 'a4 b1 b3 c2 d3 e2', nbMoves: 6, }, { - goal: 'youNeedBothBishops', + goal: i18n.learn.youNeedBothBishops, fen: '8/8/8/8/8/8/8/2B2B2 w - -', apples: 'd3 d4 d5 e3 e4 e5', nbMoves: 6, }, { - goal: 'youNeedBothBishops', + goal: i18n.learn.youNeedBothBishops, fen: '8/3B4/8/8/8/2B5/8/8 w - -', apples: 'a3 c2 e7 f5 f6 g8 h4 h7', nbMoves: 11, }, ].map(toLevel), - complete: 'bishopComplete', + complete: i18n.learn.bishopComplete, }; export default stage; diff --git a/ui/learn/src/stage/capture.ts b/ui/learn/src/stage/capture.ts index f190d03cc7fa4..34cbd343397be 100644 --- a/ui/learn/src/stage/capture.ts +++ b/ui/learn/src/stage/capture.ts @@ -5,15 +5,15 @@ import { extinct } from '../assert'; const imgUrl = assetUrl + 'images/learn/bowman.svg'; const stage: StageNoID = { key: 'capture', - title: 'capture', - subtitle: 'takeTheEnemyPieces', + title: i18n.learn.capture, + subtitle: i18n.learn.takeTheEnemyPieces, image: imgUrl, - intro: 'captureIntro', + intro: i18n.learn.captureIntro, illustration: roundSvg(imgUrl), levels: [ { // rook - goal: 'takeTheBlackPieces', + goal: i18n.learn.takeTheBlackPieces, fen: '8/2p2p2/8/8/8/2R5/8/8 w - -', nbMoves: 2, captures: 2, @@ -21,7 +21,7 @@ const stage: StageNoID = { }, { // queen - goal: 'takeTheBlackPiecesAndDontLoseYours', + goal: i18n.learn.takeTheBlackPiecesAndDontLoseYours, fen: '8/2r2p2/8/8/5Q2/8/8/8 w - -', nbMoves: 2, captures: 2, @@ -29,27 +29,27 @@ const stage: StageNoID = { }, { // bishop - goal: 'takeTheBlackPiecesAndDontLoseYours', + goal: i18n.learn.takeTheBlackPiecesAndDontLoseYours, fen: '8/5r2/8/1r3p2/8/3B4/8/8 w - -', nbMoves: 5, captures: 3, }, { // queen - goal: 'takeTheBlackPiecesAndDontLoseYours', + goal: i18n.learn.takeTheBlackPiecesAndDontLoseYours, fen: '8/5b2/5p2/3n2p1/8/6Q1/8/8 w - -', nbMoves: 7, captures: 4, }, { // knight - goal: 'takeTheBlackPiecesAndDontLoseYours', + goal: i18n.learn.takeTheBlackPiecesAndDontLoseYours, fen: '8/3b4/2p2q2/8/3p1N2/8/8/8 w - -', nbMoves: 6, captures: 4, }, ].map((l: LevelPartial, i) => toLevel({ ...l, pointsForCapture: true, success: extinct('black') }, i)), - complete: 'captureComplete', + complete: i18n.learn.captureComplete, }; export default stage; diff --git a/ui/learn/src/stage/castling.ts b/ui/learn/src/stage/castling.ts index f8c7dc1636783..aad73e58ea650 100644 --- a/ui/learn/src/stage/castling.ts +++ b/ui/learn/src/stage/castling.ts @@ -25,14 +25,14 @@ const cantCastleQueenSide9 = and( const stage: StageNoID = { key: 'castling', - title: 'castling', - subtitle: 'theSpecialKingMove', + title: i18n.learn.castling, + subtitle: i18n.learn.theSpecialKingMove, image: imgUrl, - intro: 'castlingIntro', + intro: i18n.learn.castlingIntro, illustration: roundSvg(imgUrl), levels: [ { - goal: 'castleKingSide', + goal: i18n.learn.castleKingSide, fen: 'rnbqkbnr/pppppppp/8/8/2B5/4PN2/PPPP1PPP/RNBQK2R w KQkq -', nbMoves: 1, shapes: [arrow('e1g1')], @@ -40,7 +40,7 @@ const stage: StageNoID = { failure: cantCastleKingSide, }, { - goal: 'castleQueenSide', + goal: i18n.learn.castleQueenSide, fen: 'rnbqkbnr/pppppppp/8/8/4P3/1PN5/PBPPQPPP/R3KBNR w KQkq -', nbMoves: 1, shapes: [arrow('e1c1')], @@ -48,7 +48,7 @@ const stage: StageNoID = { failure: cantCastleQueenSide, }, { - goal: 'theKnightIsInTheWay', + goal: i18n.learn.theKnightIsInTheWay, fen: 'rnbqkbnr/pppppppp/8/8/8/4P3/PPPPBPPP/RNBQK1NR w KQkq -', nbMoves: 2, shapes: [arrow('e1g1'), arrow('g1f3')], @@ -56,7 +56,7 @@ const stage: StageNoID = { failure: cantCastleKingSide, }, { - goal: 'castleKingSideMovePiecesFirst', + goal: i18n.learn.castleKingSideMovePiecesFirst, fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -', nbMoves: 4, shapes: [arrow('e1g1')], @@ -64,7 +64,7 @@ const stage: StageNoID = { failure: cantCastleKingSide, }, { - goal: 'castleQueenSideMovePiecesFirst', + goal: i18n.learn.castleQueenSideMovePiecesFirst, fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -', nbMoves: 6, shapes: [arrow('e1c1')], @@ -72,7 +72,7 @@ const stage: StageNoID = { failure: cantCastleQueenSide, }, { - goal: 'youCannotCastleIfMoved', + goal: i18n.learn.youCannotCastleIfMoved, fen: 'rnbqkbnr/pppppppp/8/8/3P4/1PN1PN2/PBPQBPPP/R3K1R1 w Qkq -', nbMoves: 1, shapes: [arrow('e1g1', 'red'), arrow('e1c1')], @@ -80,7 +80,7 @@ const stage: StageNoID = { failure: cantCastleQueenSide, }, { - goal: 'youCannotCastleIfAttacked', + goal: i18n.learn.youCannotCastleIfAttacked, fen: 'rn1qkbnr/ppp1pppp/3p4/8/2b5/4PN2/PPPP1PPP/RNBQK2R w KQkq -', nbMoves: 2, shapes: [arrow('c4f1', 'red'), circle('e1'), circle('f1'), circle('g1')], @@ -89,7 +89,7 @@ const stage: StageNoID = { detectCapture: false, }, { - goal: 'findAWayToCastleKingSide', + goal: i18n.learn.findAWayToCastleKingSide, fen: 'rnb2rk1/pppppppp/8/8/8/4Nb1n/PPPP1P1P/RNB1KB1R w KQkq -', nbMoves: 2, shapes: [arrow('e1g1')], @@ -98,7 +98,7 @@ const stage: StageNoID = { detectCapture: false, }, { - goal: 'findAWayToCastleQueenSide', + goal: i18n.learn.findAWayToCastleQueenSide, fen: '1r1k2nr/p2ppppp/7b/7b/4P3/2nP4/P1P2P2/RN2K3 w Q -', nbMoves: 4, shapes: [arrow('e1c1')], @@ -107,6 +107,6 @@ const stage: StageNoID = { detectCapture: false, }, ].map((l: LevelPartial, i) => toLevel({ ...l, autoCastle: true }, i)), - complete: 'castlingComplete', + complete: i18n.learn.castlingComplete, }; export default stage; diff --git a/ui/learn/src/stage/check1.ts b/ui/learn/src/stage/check1.ts index 1d6650c9762ed..f99219dfa2ea9 100644 --- a/ui/learn/src/stage/check1.ts +++ b/ui/learn/src/stage/check1.ts @@ -12,42 +12,42 @@ const common = { const stage: StageNoID = { key: 'check1', - title: 'checkInOne', - subtitle: 'attackTheOpponentsKing', + title: i18n.learn.checkInOne, + subtitle: i18n.learn.attackTheOpponentsKing, image: imgUrl, - intro: 'checkInOneIntro', + intro: i18n.learn.checkInOneIntro, illustration: roundSvg(imgUrl), levels: [ { - goal: 'checkInOneGoal', + goal: i18n.learn.checkInOneGoal, fen: '4k3/8/2b5/8/8/8/8/R7 w - -', shapes: [arrow('a1e1')], }, { - goal: 'checkInOneGoal', + goal: i18n.learn.checkInOneGoal, fen: '8/8/4k3/3n4/8/1Q6/8/8 w - -', }, { - goal: 'checkInOneGoal', + goal: i18n.learn.checkInOneGoal, fen: '3qk3/1pp5/3p4/4p3/8/3B4/6r1/8 w - -', }, { - goal: 'checkInOneGoal', + goal: i18n.learn.checkInOneGoal, fen: '2r2q2/2n5/8/4k3/8/2N1P3/3P2B1/8 w - -', }, { - goal: 'checkInOneGoal', + goal: i18n.learn.checkInOneGoal, fen: '8/2b1q2n/1ppk4/2N5/8/8/8/8 w - -', }, { - goal: 'checkInOneGoal', + goal: i18n.learn.checkInOneGoal, fen: '6R1/1k3r2/8/4Q3/8/2n5/8/8 w - -', }, { - goal: 'checkInOneGoal', + goal: i18n.learn.checkInOneGoal, fen: '7r/4k3/8/3n4/4N3/8/2R5/4Q3 w - -', }, ].map((l, i) => toLevel({ ...common, ...l }, i)), - complete: 'checkInOneComplete', + complete: i18n.learn.checkInOneComplete, }; export default stage; diff --git a/ui/learn/src/stage/check2.ts b/ui/learn/src/stage/check2.ts index 21ae43f41cb47..719b9389aa3c1 100644 --- a/ui/learn/src/stage/check2.ts +++ b/ui/learn/src/stage/check2.ts @@ -12,42 +12,42 @@ const common = () => ({ const stage: StageNoID = { key: 'check2', - title: 'checkInTwo', - subtitle: 'twoMovesToGiveCheck', + title: i18n.learn.checkInTwo, + subtitle: i18n.learn.twoMovesToGiveCheck, image: imgUrl, - intro: 'checkInTwoIntro', + intro: i18n.learn.checkInTwoIntro, illustration: roundSvg(imgUrl), levels: [ { - goal: 'checkInTwoGoal', + goal: i18n.learn.checkInTwoGoal, fen: '2k5/2pb4/8/2R5/8/8/8/8 w - -', shapes: [arrow('c5a5'), arrow('a5a8')], }, { - goal: 'checkInTwoGoal', + goal: i18n.learn.checkInTwoGoal, fen: '8/8/5k2/8/8/1N6/5b2/8 w - -', }, { - goal: 'checkInTwoGoal', + goal: i18n.learn.checkInTwoGoal, fen: '6k1/2r3pp/8/1N6/8/8/4B3/8 w - -', }, { - goal: 'checkInTwoGoal', + goal: i18n.learn.checkInTwoGoal, fen: 'r3k3/7b/8/4B3/8/8/4N3/4R3 w - -', }, { - goal: 'checkInTwoGoal', + goal: i18n.learn.checkInTwoGoal, fen: 'r1bqkb1r/pppp1p1p/2n2np1/4p3/2B5/4PN2/PPPP1PPP/RNBQK2R w KQkq -', }, { - goal: 'checkInTwoGoal', + goal: i18n.learn.checkInTwoGoal, fen: '8/8/8/2k5/q7/4N3/3B4/8 w - -', }, { - goal: 'checkInTwoGoal', + goal: i18n.learn.checkInTwoGoal, fen: 'r6r/1Q2nk2/1B3p2/8/8/8/8/8 w - -', }, ].map((l, i) => toLevel({ ...common(), ...l }, i)), - complete: 'checkInTwoComplete', + complete: i18n.learn.checkInTwoComplete, }; export default stage; diff --git a/ui/learn/src/stage/checkmate1.ts b/ui/learn/src/stage/checkmate1.ts index 2f784d12b62e3..4974fe4a05cc4 100644 --- a/ui/learn/src/stage/checkmate1.ts +++ b/ui/learn/src/stage/checkmate1.ts @@ -13,41 +13,41 @@ const common = { const stage: StageNoID = { key: 'checkmate1', - title: 'mateInOne', - subtitle: 'defeatTheOpponentsKing', + title: i18n.learn.mateInOne, + subtitle: i18n.learn.defeatTheOpponentsKing, image: imgUrl, - intro: 'mateInOneIntro', + intro: i18n.learn.mateInOneIntro, illustration: roundSvg(imgUrl), levels: [ { // rook - goal: 'attackYourOpponentsKing', + goal: i18n.learn.attackYourOpponentsKing, fen: '3qk3/3ppp2/8/8/2B5/5Q2/8/8 w - -', shapes: [arrow('f3f7')], }, { // smothered - goal: 'attackYourOpponentsKing', + goal: i18n.learn.attackYourOpponentsKing, fen: '6rk/6pp/7P/6N1/8/8/8/8 w - -', }, { // rook - goal: 'attackYourOpponentsKing', + goal: i18n.learn.attackYourOpponentsKing, fen: 'R7/8/7k/2r5/5n2/8/6Q1/8 w - -', }, { // Q+N - goal: 'attackYourOpponentsKing', + goal: i18n.learn.attackYourOpponentsKing, fen: '2rb4/2k5/5N2/1Q6/8/8/8/8 w - -', }, { // discovered - goal: 'attackYourOpponentsKing', + goal: i18n.learn.attackYourOpponentsKing, fen: '1r2kb2/ppB1p3/2P2p2/2p1N3/B7/8/8/3R4 w - -', }, { // tricky - goal: 'attackYourOpponentsKing', + goal: i18n.learn.attackYourOpponentsKing, fen: '8/pk1N4/n7/b7/6B1/1r3b2/8/1RR5 w - -', scenario: [ { @@ -58,10 +58,10 @@ const stage: StageNoID = { }, { // tricky - goal: 'attackYourOpponentsKing', + goal: i18n.learn.attackYourOpponentsKing, fen: 'r1b5/ppp5/2N2kpN/5q2/8/Q7/8/4B3 w - -', }, ].map((l, i) => toLevel({ ...common, ...l }, i)), - complete: 'mateInOneComplete', + complete: i18n.learn.mateInOneComplete, }; export default stage; diff --git a/ui/learn/src/stage/combat.ts b/ui/learn/src/stage/combat.ts index 6fcb909c523c6..9337ba8990440 100644 --- a/ui/learn/src/stage/combat.ts +++ b/ui/learn/src/stage/combat.ts @@ -6,45 +6,45 @@ const imgUrl = assetUrl + 'images/learn/battle-gear.svg'; const stage: StageNoID = { key: 'combat', - title: 'combat', - subtitle: 'captureAndDefendPieces', + title: i18n.learn.combat, + subtitle: i18n.learn.captureAndDefendPieces, image: imgUrl, - intro: 'combatIntro', + intro: i18n.learn.combatIntro, illustration: roundSvg(imgUrl), levels: [ { // rook - goal: 'takeTheBlackPiecesAndDontLoseYours', + goal: i18n.learn.takeTheBlackPiecesAndDontLoseYours, fen: '8/8/8/8/P2r4/6B1/8/8 w - -', nbMoves: 3, captures: 1, shapes: [arrow('a4a5'), arrow('g3f2'), arrow('f2d4'), arrow('d4a4', 'yellow')], }, { - goal: 'takeTheBlackPiecesAndDontLoseYours', + goal: i18n.learn.takeTheBlackPiecesAndDontLoseYours, fen: '2r5/8/3b4/2P5/8/1P6/2B5/8 w - -', nbMoves: 4, captures: 2, }, { - goal: 'takeTheBlackPiecesAndDontLoseYours', + goal: i18n.learn.takeTheBlackPiecesAndDontLoseYours, fen: '1r6/8/5n2/3P4/4P1P1/1Q6/8/8 w - -', nbMoves: 4, captures: 2, }, { - goal: 'takeTheBlackPiecesAndDontLoseYours', + goal: i18n.learn.takeTheBlackPiecesAndDontLoseYours, fen: '2r5/8/3N4/5b2/8/8/PPP5/8 w - -', nbMoves: 4, captures: 2, }, { - goal: 'takeTheBlackPiecesAndDontLoseYours', + goal: i18n.learn.takeTheBlackPiecesAndDontLoseYours, fen: '8/6q1/8/4P1P1/8/4B3/r2P2N1/8 w - -', nbMoves: 8, captures: 2, }, ].map((l: LevelPartial, i) => toLevel({ ...l, pointsForCapture: true, success: extinct('black') }, i)), - complete: 'combatComplete', + complete: i18n.learn.combatComplete, }; export default stage; diff --git a/ui/learn/src/stage/enpassant.ts b/ui/learn/src/stage/enpassant.ts index b5bd6fdc0a301..dfb5eb2172211 100644 --- a/ui/learn/src/stage/enpassant.ts +++ b/ui/learn/src/stage/enpassant.ts @@ -6,14 +6,14 @@ const imgUrl = assetUrl + 'images/learn/spinning-blades.svg'; const stage: StageNoID = { key: 'enpassant', - title: 'enPassant', - subtitle: 'theSpecialPawnMove', + title: i18n.learn.enPassant, + subtitle: i18n.learn.theSpecialPawnMove, image: imgUrl, - intro: 'enPassantIntro', + intro: i18n.learn.enPassantIntro, illustration: roundSvg(imgUrl), levels: [ { - goal: 'blackJustMovedThePawnByTwoSquares', + goal: i18n.learn.blackJustMovedThePawnByTwoSquares, fen: 'rnbqkbnr/pppppppp/8/2P5/8/8/PP1PPPPP/RNBQKBNR b KQkq -', color: 'white' as const, nbMoves: 1, @@ -30,7 +30,7 @@ const stage: StageNoID = { captures: 1, }, { - goal: 'enPassantOnlyWorksImmediately', + goal: i18n.learn.enPassantOnlyWorksImmediately, fen: 'rnbqkbnr/ppp1pppp/8/2Pp3P/8/8/PP1PPPP1/RNBQKBNR b KQkq -', color: 'white' as const, nbMoves: 1, @@ -47,7 +47,7 @@ const stage: StageNoID = { captures: 1, }, { - goal: 'enPassantOnlyWorksOnFifthRank', + goal: i18n.learn.enPassantOnlyWorksOnFifthRank, fen: 'rnbqkbnr/pppppppp/P7/2P5/8/8/PP1PPPP1/RNBQKBNR b KQkq -', color: 'white' as const, nbMoves: 1, @@ -65,7 +65,7 @@ const stage: StageNoID = { cssClass: 'highlight-5th-rank', }, { - goal: 'takeAllThePawnsEnPassant', + goal: i18n.learn.takeAllThePawnsEnPassant, fen: 'rnbqkbnr/pppppppp/8/2PPP2P/8/8/PP1P1PP1/RNBQKBNR b KQkq -', color: 'white' as const, nbMoves: 4, @@ -76,6 +76,6 @@ const stage: StageNoID = { captures: 4, }, ].map(toLevel), - complete: 'enPassantComplete', + complete: i18n.learn.enPassantComplete, }; export default stage; diff --git a/ui/learn/src/stage/king.ts b/ui/learn/src/stage/king.ts index 6d05c1e18c834..9d7d4ec229d1a 100644 --- a/ui/learn/src/stage/king.ts +++ b/ui/learn/src/stage/king.ts @@ -3,32 +3,32 @@ import { arrow, assetUrl, pieceImg, toLevel } from '../util'; const stage: StageNoID = { key: 'king', - title: 'theKing', - subtitle: 'theMostImportantPiece', + title: i18n.learn.theKing, + subtitle: i18n.learn.theMostImportantPiece, image: assetUrl + 'images/learn/pieces/K.svg', - intro: 'kingIntro', + intro: i18n.learn.kingIntro, illustration: pieceImg('king'), levels: [ { - goal: 'theKingIsSlow', + goal: i18n.learn.theKingIsSlow, fen: '8/8/8/8/8/3K4/8/8 w - -', apples: 'e6', nbMoves: 3, shapes: [arrow('d3d4'), arrow('d4d5'), arrow('d5e6')], }, { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/8/8/8/8/8/8/4K3 w - -', apples: 'c2 d3 e2 e3', nbMoves: 4, }, { - goal: 'lastOne', + goal: i18n.learn.lastOne, fen: '8/8/8/4K3/8/8/8/8 w - -', apples: 'b5 c5 d6 e3 f3 g4', nbMoves: 8, }, ].map((l: LevelPartial, i) => toLevel({ ...l, emptyApples: true }, i)), - complete: 'kingComplete', + complete: i18n.learn.kingComplete, }; export default stage; diff --git a/ui/learn/src/stage/knight.ts b/ui/learn/src/stage/knight.ts index 92146a9b23f7b..9379b77594620 100644 --- a/ui/learn/src/stage/knight.ts +++ b/ui/learn/src/stage/knight.ts @@ -3,50 +3,50 @@ import { StageNoID } from './list'; const stage: StageNoID = { key: 'knight', - title: 'theKnight', - subtitle: 'itMovesInAnLShape', + title: i18n.learn.theKnight, + subtitle: i18n.learn.itMovesInAnLShape, image: assetUrl + 'images/learn/pieces/N.svg', - intro: 'knightIntro', + intro: i18n.learn.knightIntro, illustration: pieceImg('knight'), levels: [ { - goal: 'knightsHaveAFancyWay', + goal: i18n.learn.knightsHaveAFancyWay, fen: '8/8/8/8/4N3/8/8/8 w - -', apples: 'c5 d7', nbMoves: 2, shapes: [arrow('e4c5'), arrow('c5d7')], }, { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/8/8/8/8/8/8/1N6 w - -', apples: 'c3 d4 e2 f3 f7 g5 h8', nbMoves: 8, }, { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/2N5/8/8/8/8/8/8 w - -', apples: 'b6 d5 d7 e6 f4', nbMoves: 5, }, { - goal: 'knightsCanJumpOverObstacles', + goal: i18n.learn.knightsCanJumpOverObstacles, fen: '8/8/8/8/5N2/8/8/8 w - -', apples: 'e3 e4 e5 f3 f5 g3 g4 g5', nbMoves: 9, }, { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/8/8/8/8/3N4/8/8 w - -', apples: 'c3 e2 e4 f2 f4 g6', nbMoves: 6, }, { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/2N5/8/8/8/8/8/8 w - -', apples: 'b4 b5 c6 c8 d4 d5 e3 e7 f5', nbMoves: 9, }, ].map(toLevel), - complete: 'knightComplete', + complete: i18n.learn.knightComplete, }; export default stage; diff --git a/ui/learn/src/stage/list.ts b/ui/learn/src/stage/list.ts index ab3f8e4c2cfc9..e4c57bf70831c 100644 --- a/ui/learn/src/stage/list.ts +++ b/ui/learn/src/stage/list.ts @@ -84,22 +84,22 @@ interface RawCateg { const rawCategs: RawCateg[] = [ { key: 'chess-pieces', - name: 'chessPieces', + name: i18n.learn.chessPieces, stages: [rook, bishop, queen, king, knight, pawn], }, { key: 'fundamentals', - name: 'fundamentals', + name: i18n.learn.fundamentals, stages: [capture, protection, combat, check1, outOfCheck, checkmate1], }, { key: 'intermediate', - name: 'intermediate', + name: i18n.learn.intermediate, stages: [setup, castling, enpassant, stalemate], }, { key: 'advanced', - name: 'advanced', + name: i18n.learn.advanced, stages: [ value, // draw, diff --git a/ui/learn/src/stage/outOfCheck.ts b/ui/learn/src/stage/outOfCheck.ts index f1d2be0b0af3b..4974a80b5ada9 100644 --- a/ui/learn/src/stage/outOfCheck.ts +++ b/ui/learn/src/stage/outOfCheck.ts @@ -11,42 +11,42 @@ const common = { const stage: StageNoID = { key: 'outOfCheck', - title: 'outOfCheck', - subtitle: 'defendYourKing', + title: i18n.learn.outOfCheck, + subtitle: i18n.learn.defendYourKing, image: imgUrl, - intro: 'outOfCheckIntro', + intro: i18n.learn.outOfCheckIntro, illustration: roundSvg(imgUrl), levels: [ { - goal: 'escapeWithTheKing', + goal: i18n.learn.escapeWithTheKing, fen: '8/8/8/4q3/8/8/8/4K3 w - -', shapes: [arrow('e5e1', 'red'), arrow('e1f1')], }, { - goal: 'escapeWithTheKing', + goal: i18n.learn.escapeWithTheKing, fen: '8/2n5/5b2/8/2K5/8/2q5/8 w - -', }, { - goal: 'theKingCannotEscapeButBlock', + goal: i18n.learn.theKingCannotEscapeButBlock, fen: '8/7r/6r1/8/R7/7K/8/8 w - -', }, { - goal: 'youCanGetOutOfCheckByTaking', + goal: i18n.learn.youCanGetOutOfCheckByTaking, fen: '8/8/8/3b4/8/4N3/KBn5/1R6 w - -', }, { - goal: 'thisKnightIsCheckingThroughYourDefenses', + goal: i18n.learn.thisKnightIsCheckingThroughYourDefenses, fen: '4q3/8/8/8/8/5nb1/3PPP2/3QKBNr w - -', }, { - goal: 'escapeOrBlock', + goal: i18n.learn.escapeOrBlock, fen: '8/8/7p/2q5/5n2/1N1KP2r/3R4/8 w - -', }, { - goal: 'escapeOrBlock', + goal: i18n.learn.escapeOrBlock, fen: '8/6b1/8/8/q4P2/2KN4/3P4/8 w - -', }, ].map((l, i) => toLevel({ ...common, ...l }, i)), - complete: 'outOfCheckComplete', + complete: i18n.learn.outOfCheckComplete, }; export default stage; diff --git a/ui/learn/src/stage/pawn.ts b/ui/learn/src/stage/pawn.ts index eb7d032dddd65..dfd086a5cd187 100644 --- a/ui/learn/src/stage/pawn.ts +++ b/ui/learn/src/stage/pawn.ts @@ -4,14 +4,14 @@ import { StageNoID } from './list'; const stage: StageNoID = { key: 'pawn', - title: 'thePawn', - subtitle: 'itMovesForwardOnly', + title: i18n.learn.thePawn, + subtitle: i18n.learn.itMovesForwardOnly, image: assetUrl + 'images/learn/pieces/P.svg', - intro: 'pawnIntro', + intro: i18n.learn.pawnIntro, illustration: pieceImg('pawn'), levels: [ { - goal: 'pawnsMoveOneSquareOnly', + goal: i18n.learn.pawnsMoveOneSquareOnly, fen: '8/8/8/P7/8/8/8/8 w - -', apples: 'f3', nbMoves: 4, @@ -19,13 +19,13 @@ const stage: StageNoID = { explainPromotion: true, }, { - goal: 'mostOfTheTimePromotingToAQueenIsBest', + goal: i18n.learn.mostOfTheTimePromotingToAQueenIsBest, fen: '8/8/8/5P2/8/8/8/8 w - -', apples: 'b6 c4 d7 e5 a8', nbMoves: 8, }, { - goal: 'pawnsMoveForward', + goal: i18n.learn.pawnsMoveForward, fen: '8/8/8/8/8/4P3/8/8 w - -', apples: 'c6 d5 d7', nbMoves: 4, @@ -33,26 +33,26 @@ const stage: StageNoID = { failure: noPieceOn('e3 e4 c6 d5 d7'), }, { - goal: 'captureThenPromote', + goal: i18n.learn.captureThenPromote, fen: '8/8/8/8/8/1P6/8/8 w - -', apples: 'b4 b6 c4 c6 c7 d6', nbMoves: 8, }, { - goal: 'captureThenPromote', + goal: i18n.learn.captureThenPromote, fen: '8/8/8/8/8/3P4/8/8 w - -', apples: 'c4 b5 b6 d5 d7 e6 c8', failure: whitePawnOnAnyOf('b5 d4 d6 c7'), nbMoves: 8, }, { - goal: 'useAllThePawns', + goal: i18n.learn.useAllThePawns, fen: '8/8/8/8/8/P1PP3P/8/8 w - -', apples: 'b5 c5 d4 e5 g4', nbMoves: 7, }, { - goal: 'aPawnOnTheSecondRank', + goal: i18n.learn.aPawnOnTheSecondRank, fen: '8/8/8/8/8/8/4P3/8 w - -', apples: 'd6', nbMoves: 3, @@ -61,12 +61,12 @@ const stage: StageNoID = { cssClass: 'highlight-2nd-rank', }, { - goal: 'grabAllTheStarsNoNeedToPromote', + goal: i18n.learn.grabAllTheStarsNoNeedToPromote, fen: '8/8/8/8/8/8/2PPPP2/8 w - -', apples: 'c5 d5 e5 f5 d3 e4', nbMoves: 9, }, ].map(toLevel), - complete: 'pawnComplete', + complete: i18n.learn.pawnComplete, }; export default stage; diff --git a/ui/learn/src/stage/protection.ts b/ui/learn/src/stage/protection.ts index e540d97031c38..98e7d5b6c9721 100644 --- a/ui/learn/src/stage/protection.ts +++ b/ui/learn/src/stage/protection.ts @@ -5,25 +5,25 @@ const imgUrl = assetUrl + 'images/learn/bolt-shield.svg'; const stage: StageNoID = { key: 'protection', - title: 'protection', - subtitle: 'keepYourPiecesSafe', + title: i18n.learn.protection, + subtitle: i18n.learn.keepYourPiecesSafe, image: imgUrl, - intro: 'protectionIntro', + intro: i18n.learn.protectionIntro, illustration: roundSvg(imgUrl), levels: [ { - goal: 'escape', + goal: i18n.learn.escape, fen: '8/8/8/4bb2/8/8/P2P4/R2K4 w - -', shapes: [arrow('e5a1', 'red'), arrow('a1c1')], }, { // escape - goal: 'escape', + goal: i18n.learn.escape, fen: '8/8/2q2N2/8/8/8/8/8 w - -', }, { // protect - goal: 'noEscape', + goal: i18n.learn.noEscape, fen: '8/N2q4/8/8/8/8/6R1/8 w - -', scenario: [ { @@ -33,27 +33,27 @@ const stage: StageNoID = { ], }, { - goal: 'noEscape', + goal: i18n.learn.noEscape, fen: '8/8/1Bq5/8/2P5/8/8/8 w - -', }, { - goal: 'noEscape', + goal: i18n.learn.noEscape, fen: '1r6/8/5b2/8/8/5N2/P2P4/R1B5 w - -', shapes: [arrow('f6a1', 'red'), arrow('d2d4')], }, { - goal: 'dontLetThemTakeAnyUndefendedPiece', + goal: i18n.learn.dontLetThemTakeAnyUndefendedPiece, fen: '8/1b6/8/8/8/3P2P1/5NRP/r7 w - -', }, { - goal: 'dontLetThemTakeAnyUndefendedPiece', + goal: i18n.learn.dontLetThemTakeAnyUndefendedPiece, fen: 'rr6/3q4/4n3/4P1B1/7P/P7/1B1N1PP1/R5K1 w - -', }, { - goal: 'dontLetThemTakeAnyUndefendedPiece', + goal: i18n.learn.dontLetThemTakeAnyUndefendedPiece, fen: '8/3q4/8/1N3R2/8/2PB4/8/8 w - -', }, ].map((l, i) => toLevel({ nbMoves: 1, ...l }, i)), - complete: 'protectionComplete', + complete: i18n.learn.protectionComplete, }; export default stage; diff --git a/ui/learn/src/stage/queen.ts b/ui/learn/src/stage/queen.ts index 0737daca1a918..54325122df889 100644 --- a/ui/learn/src/stage/queen.ts +++ b/ui/learn/src/stage/queen.ts @@ -3,44 +3,44 @@ import { StageNoID } from './list'; const stage: StageNoID = { key: 'queen', - title: 'theQueen', - subtitle: 'queenCombinesRookAndBishop', + title: i18n.learn.theQueen, + subtitle: i18n.learn.queenCombinesRookAndBishop, image: assetUrl + 'images/learn/pieces/Q.svg', - intro: 'queenIntro', + intro: i18n.learn.queenIntro, illustration: pieceImg('queen'), levels: [ { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/8/8/8/8/8/4Q3/8 w - -', apples: 'e5 b8', nbMoves: 2, shapes: [arrow('e2e5'), arrow('e5b8')], }, { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/8/8/8/3Q4/8/8/8 w - -', apples: 'a3 f2 f8 h3', nbMoves: 4, }, { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/8/8/8/2Q5/8/8/8 w - -', apples: 'a3 d6 f1 f8 g3 h6', nbMoves: 6, }, { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/6Q1/8/8/8/8/8/8 w - -', apples: 'a2 b5 d3 g1 g8 h2 h5', nbMoves: 7, }, { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/8/8/8/8/8/8/4Q3 w - -', apples: 'a6 d1 f2 f6 g6 g8 h1 h4', nbMoves: 9, }, ].map(toLevel), - complete: 'queenComplete', + complete: i18n.learn.queenComplete, }; export default stage; diff --git a/ui/learn/src/stage/rook.ts b/ui/learn/src/stage/rook.ts index 4d99026972f61..6e86b8d76b093 100644 --- a/ui/learn/src/stage/rook.ts +++ b/ui/learn/src/stage/rook.ts @@ -3,51 +3,51 @@ import { StageNoID } from './list'; const stage: StageNoID = { key: 'rook', - title: 'theRook', - subtitle: 'itMovesInStraightLines', + title: i18n.learn.theRook, + subtitle: i18n.learn.itMovesInStraightLines, image: assetUrl + 'images/learn/pieces/R.svg', - intro: 'rookIntro', + intro: i18n.learn.rookIntro, illustration: pieceImg('rook'), levels: [ { - goal: 'rookGoal', + goal: i18n.learn.rookGoal, fen: '8/8/8/8/8/8/4R3/8 w - -', apples: 'e7', nbMoves: 1, shapes: [arrow('e2e7')], }, { - goal: 'grabAllTheStars', + goal: i18n.learn.grabAllTheStars, fen: '8/2R5/8/8/8/8/8/8 w - -', apples: 'c5 g5', nbMoves: 2, shapes: [arrow('c7c5'), arrow('c5g5')], }, { - goal: 'theFewerMoves', + goal: i18n.learn.theFewerMoves, fen: '8/8/8/8/3R4/8/8/8 w - -', apples: 'a4 g3 g4', nbMoves: 3, }, { - goal: 'theFewerMoves', + goal: i18n.learn.theFewerMoves, fen: '7R/8/8/8/8/8/8/8 w - -', apples: 'f8 g1 g7 g8 h7', nbMoves: 5, }, { - goal: 'useTwoRooks', + goal: i18n.learn.useTwoRooks, fen: '8/1R6/8/8/3R4/8/8/8 w - -', apples: 'a4 g3 g7 h4', nbMoves: 4, }, { - goal: 'useTwoRooks', + goal: i18n.learn.useTwoRooks, fen: '8/8/8/8/8/5R2/8/R7 w - -', apples: 'b7 d1 d5 f2 f7 g4 g7', nbMoves: 7, }, ].map(toLevel), - complete: 'rookComplete', + complete: i18n.learn.rookComplete, }; export default stage; diff --git a/ui/learn/src/stage/setup.ts b/ui/learn/src/stage/setup.ts index 1a741babc6e94..04dac1b070c09 100644 --- a/ui/learn/src/stage/setup.ts +++ b/ui/learn/src/stage/setup.ts @@ -6,20 +6,20 @@ const imgUrl = assetUrl + 'images/learn/rally-the-troops.svg'; const stage: StageNoID = { key: 'setup', - title: 'boardSetup', - subtitle: 'howTheGameStarts', + title: i18n.learn.boardSetup, + subtitle: i18n.learn.howTheGameStarts, image: imgUrl, - intro: 'boardSetupIntro', + intro: i18n.learn.boardSetupIntro, illustration: roundSvg(imgUrl), levels: [ { // rook - goal: 'thisIsTheInitialPosition', + goal: i18n.learn.thisIsTheInitialPosition, fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - -', nbMoves: 1, }, { - goal: 'firstPlaceTheRooks', + goal: i18n.learn.firstPlaceTheRooks, fen: 'r6r/pppppppp/8/8/8/8/8/2RR4 w - -', apples: 'a1 h1', nbMoves: 2, @@ -27,41 +27,41 @@ const stage: StageNoID = { success: and(pieceOn('R', 'a1'), pieceOn('R', 'h1')), }, { - goal: 'thenPlaceTheKnights', + goal: i18n.learn.thenPlaceTheKnights, fen: 'rn4nr/pppppppp/8/8/8/8/2NN4/R6R w - -', apples: 'b1 g1', nbMoves: 4, success: and(pieceOn('N', 'b1'), pieceOn('N', 'g1')), }, { - goal: 'placeTheBishops', + goal: i18n.learn.placeTheBishops, fen: 'rnb2bnr/pppppppp/8/8/4BB2/8/8/RN4NR w - -', apples: 'c1 f1', nbMoves: 4, success: and(pieceOn('B', 'c1'), pieceOn('B', 'f1')), }, { - goal: 'placeTheQueen', + goal: i18n.learn.placeTheQueen, fen: 'rnbq1bnr/pppppppp/8/8/5Q2/8/8/RNB2BNR w - -', apples: 'd1', nbMoves: 2, success: pieceOn('Q', 'd1'), }, { - goal: 'placeTheKing', + goal: i18n.learn.placeTheKing, fen: 'rnbqkbnr/pppppppp/8/8/5K2/8/8/RNBQ1BNR w - -', apples: 'e1', nbMoves: 3, success: pieceOn('K', 'e1'), }, { - goal: 'pawnsFormTheFrontLine', + goal: i18n.learn.pawnsFormTheFrontLine, fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - -', nbMoves: 1, cssClass: 'highlight-2nd-rank highlight-7th-rank', }, ].map(toLevel), - complete: 'boardSetupComplete', + complete: i18n.learn.boardSetupComplete, cssClass: 'no-go-home', }; export default stage; diff --git a/ui/learn/src/stage/stalemate.ts b/ui/learn/src/stage/stalemate.ts index dd603e59fb506..6662f2266c4c9 100644 --- a/ui/learn/src/stage/stalemate.ts +++ b/ui/learn/src/stage/stalemate.ts @@ -5,7 +5,7 @@ import { StageNoID } from './list'; const imgUrl = assetUrl + 'images/learn/scales.svg'; const common = { - goal: 'stalemateGoal', + goal: i18n.learn.stalemateGoal, detectCapture: false, nbMoves: 1, nextButton: true, @@ -16,10 +16,10 @@ const common = { const stage: StageNoID = { key: 'stalemate', - title: 'stalemate', - subtitle: 'theGameIsADraw', + title: i18n.learn.stalemate, + subtitle: i18n.learn.theGameIsADraw, image: imgUrl, - intro: 'stalemateIntro', + intro: i18n.learn.stalemateIntro, illustration: roundSvg(imgUrl), levels: [ { @@ -96,6 +96,6 @@ const stage: StageNoID = { ], }, ].map((l, i) => toLevel({ ...common, ...l }, i)), - complete: 'stalemateComplete', + complete: i18n.learn.stalemateComplete, }; export default stage; diff --git a/ui/learn/src/stage/value.ts b/ui/learn/src/stage/value.ts index 7264d1be7db92..67594ec064cf3 100644 --- a/ui/learn/src/stage/value.ts +++ b/ui/learn/src/stage/value.ts @@ -13,15 +13,15 @@ const common = { const stage: StageNoID = { key: 'value', - title: 'pieceValue', - subtitle: 'evaluatePieceStrength', + title: i18n.learn.pieceValue, + subtitle: i18n.learn.evaluatePieceStrength, image: imgUrl, - intro: 'pieceValueIntro', + intro: i18n.learn.pieceValueIntro, illustration: roundSvg(imgUrl), levels: [ { // rook - goal: 'queenOverBishop', + goal: i18n.learn.queenOverBishop, fen: '8/8/2qrbnp1/3P4/8/8/8/8 w - -', scenario: ['d5c6'], shapes: [arrow('d5c6')], @@ -30,7 +30,7 @@ const stage: StageNoID = { detectCapture: false, }, { - goal: 'pieceValueExchange', + goal: i18n.learn.pieceValueExchange, fen: '8/8/4b3/1p6/6r1/8/4Q3/8 w - -', scenario: ['e2e6'], success: scenarioComplete, @@ -38,7 +38,7 @@ const stage: StageNoID = { detectCapture: true, }, { - goal: 'pieceValueLegal', + goal: i18n.learn.pieceValueLegal, fen: '5b2/8/6N1/2q5/3Kn3/2rp4/3B4/8 w - -', scenario: ['d4e4'], offerIllegalMove: true, @@ -46,7 +46,7 @@ const stage: StageNoID = { failure: scenarioFailed, }, { - goal: 'takeThePieceWithTheHighestValue', + goal: i18n.learn.takeThePieceWithTheHighestValue, fen: '1k4q1/pp6/8/3B4/2P5/1P1p2P1/P3Kr1P/3n4 w - -', scenario: ['e2d1'], offerIllegalMove: true, @@ -55,7 +55,7 @@ const stage: StageNoID = { detectCapture: false, }, { - goal: 'takeThePieceWithTheHighestValue', + goal: i18n.learn.takeThePieceWithTheHighestValue, fen: '7k/3bqp1p/7r/5N2/6K1/6n1/PPP5/R1B5 w - -', scenario: ['c1h6'], offerIllegalMove: true, @@ -63,6 +63,6 @@ const stage: StageNoID = { failure: scenarioFailed, }, ].map((l, i) => toLevel({ ...common, ...l }, i)), - complete: 'pieceValueComplete', + complete: i18n.learn.pieceValueComplete, }; export default stage; diff --git a/ui/learn/src/view.ts b/ui/learn/src/view.ts index db58bdf85bb5f..0df82d406f8ea 100644 --- a/ui/learn/src/view.ts +++ b/ui/learn/src/view.ts @@ -20,7 +20,7 @@ const mapView = (ctrl: LearnCtrl) => h('div.learn__main.learn-stages', [ ...stages.categs.map(categ => h('div.categ', [ - h('h2', ctrl.trans.noarg(categ.name)), + h('h2', categ.name), h( 'div.categ_stages', categ.stages.map(stage => { @@ -28,14 +28,14 @@ const mapView = (ctrl: LearnCtrl) => const complete = ctrl.isStageIdComplete(stage.id); const prevComplete = ctrl.isStageIdComplete(stage.id - 1); const status: Status = complete ? 'done' : prevComplete || stageProgress ? 'ongoing' : 'future'; - const title = ctrl.trans.noarg(stage.title); + const title = stage.title; return h( `a.stage.${status}.${titleVerbosityClass(title)}`, { attrs: { href: hashHref(stage.id) } }, [ status != 'future' ? ribbon(ctrl, stage, status, stageProgress) : undefined, h('img', { attrs: { src: stage.image } }), - h('div.text', [h('h3', title), h('p.subtitle', ctrl.trans.noarg(stage.subtitle))]), + h('div.text', [h('h3', title), h('p.subtitle', stage.subtitle)]), ], ); }), @@ -53,7 +53,7 @@ const makeStars = (rank: scoring.Rank): VNode[] => const ongoingStr = (ctrl: LearnCtrl, s: Stage): string => { const progress = ctrl.stageProgress(s); - return progress[0] ? progress.join(' / ') : ctrl.trans.noarg('play'); + return progress[0] ? progress.join(' / ') : i18n.learn.play; }; const ribbon = (ctrl: LearnCtrl, s: Stage, status: Exclude, stageProgress: StageProgress) => @@ -67,32 +67,37 @@ const ribbon = (ctrl: LearnCtrl, s: Stage, status: Exclude, st function whatNext(ctrl: LearnCtrl) { const makeStage = (href: string, img: string, title: string, subtitle: string, done?: boolean) => { - const transTitle = ctrl.trans.noarg(title); - return h( - `a.stage.done.${titleVerbosityClass(transTitle)}`, - { - attrs: { href: href }, - }, - [ - done ? h('span.ribbon-wrapper', h('span.ribbon.done', makeStars(1))) : null, - h('img', { attrs: { src: util.assetUrl + 'images/learn/' + img + '.svg' } }), - h('div.text', [h('h3', transTitle), h('p.subtitle', ctrl.trans.noarg(subtitle))]), - ], - ); + const transTitle = title; + return h(`a.stage.done.${titleVerbosityClass(transTitle)}`, { attrs: { href: href } }, [ + done ? h('span.ribbon-wrapper', h('span.ribbon.done', makeStars(1))) : null, + h('img', { attrs: { src: util.assetUrl + 'images/learn/' + img + '.svg' } }), + h('div.text', [h('h3', transTitle), h('p.subtitle', subtitle)]), + ]); }; const userId = ctrl.data._id; return h('div.categ.what_next', [ - h('h2', ctrl.trans.noarg('whatNext')), - h('p', ctrl.trans.noarg('youKnowHowToPlayChess')), + h('h2', i18n.learn.whatNext), + h('p', i18n.learn.youKnowHowToPlayChess), h('div.categ_stages', [ userId - ? makeStage('/@/' + userId, 'beams-aura', 'register', 'getAFreeLichessAccount', true) - : makeStage('/signup', 'beams-aura', 'register', 'getAFreeLichessAccount'), - makeStage('/practice', 'robot-golem', 'practice', 'learnCommonChessPositions'), - makeStage('/training', 'bullseye', 'puzzles', 'exerciseYourTacticalSkills'), - makeStage('/video?tags=beginner', 'tied-scroll', 'videos', 'watchInstructiveChessVideos'), - makeStage('/#hook', 'sword-clash', 'playPeople', 'opponentsFromAroundTheWorld'), - makeStage('/#ai', 'vintage-robot', 'playMachine', 'testYourSkillsWithTheComputer'), + ? makeStage( + '/@/' + userId, + 'beams-aura', + i18n.learn.register, + i18n.learn.getAFreeLichessAccount, + true, + ) + : makeStage('/signup', 'beams-aura', i18n.learn.register, i18n.learn.getAFreeLichessAccount), + makeStage('/practice', 'robot-golem', i18n.learn.practice, i18n.learn.learnCommonChessPositions), + makeStage('/training', 'bullseye', i18n.learn.puzzles, i18n.learn.exerciseYourTacticalSkills), + makeStage( + '/video?tags=beginner', + 'tied-scroll', + i18n.learn.videos, + i18n.learn.watchInstructiveChessVideos, + ), + makeStage('/#hook', 'sword-clash', i18n.learn.playPeople, i18n.learn.opponentsFromAroundTheWorld), + makeStage('/#ai', 'vintage-robot', i18n.learn.playMachine, i18n.learn.testYourSkillsWithTheComputer), ]), ]); } diff --git a/ui/lobby/src/ctrl.ts b/ui/lobby/src/ctrl.ts index 4eb209cd89fe2..1dbe52eac93c5 100644 --- a/ui/lobby/src/ctrl.ts +++ b/ui/lobby/src/ctrl.ts @@ -38,7 +38,6 @@ export default class LobbyController { stepping = false; redirecting = false; poolMember?: PoolMember; - trans: Trans; pools: Pool[]; filter: Filter; setupCtrl: SetupController; @@ -70,7 +69,6 @@ export default class LobbyController { this.tab = this.me?.isBot ? 'now_playing' : this.stores.tab.get(); this.mode = this.stores.mode.get(); this.sort = this.stores.sort.get(); - this.trans = opts.trans; const locationHash = location.hash.replace('#', ''); if (['ai', 'friend', 'hook'].includes(locationHash)) { diff --git a/ui/lobby/src/interfaces.ts b/ui/lobby/src/interfaces.ts index df884e24612df..f174a4a71dd20 100644 --- a/ui/lobby/src/interfaces.ts +++ b/ui/lobby/src/interfaces.ts @@ -66,8 +66,6 @@ export interface LobbyOpts { playban: boolean; showRatings: boolean; data: LobbyData; - i18n: I18nDict; - trans: Trans; } export interface LobbyMe { diff --git a/ui/lobby/src/lobby.ts b/ui/lobby/src/lobby.ts index 8a6e7b9bfe4c4..2f064ae7d2adb 100644 --- a/ui/lobby/src/lobby.ts +++ b/ui/lobby/src/lobby.ts @@ -2,7 +2,6 @@ import * as xhr from 'common/xhr'; import main from './main'; import { LobbyOpts } from './interfaces'; import StrongSocket from 'common/socket'; -import { trans } from 'common/i18n'; import { pubsub } from 'common/pubsub'; export function initModule(opts: LobbyOpts) { @@ -22,7 +21,6 @@ export function initModule(opts: LobbyOpts) { { id: '30+0', lim: 30, inc: 0, perf: 'Classical' }, { id: '30+20', lim: 30, inc: 20, perf: 'Classical' }, ]; - opts.trans = trans(opts.i18n); site.socket = new StrongSocket('/lobby/socket/v5', false, { receive: (t: string, d: any) => lobbyCtrl.socket.receive(t, d), diff --git a/ui/lobby/src/options.ts b/ui/lobby/src/options.ts index b347456fea7a9..ac757ebea91ef 100644 --- a/ui/lobby/src/options.ts +++ b/ui/lobby/src/options.ts @@ -49,10 +49,10 @@ export const speeds: { key: Speed; name: string; icon: string }[] = [ { icon: licon.PaperAirplane, key: 'correspondence', name: 'Correspondence' }, ]; -export const timeModes = (trans: Trans): { id: number; key: TimeMode; name: string }[] => [ - { id: 1, key: 'realTime', name: trans('realTime') }, - { id: 2, key: 'correspondence', name: trans('correspondence') }, - { id: 0, key: 'unlimited', name: trans('unlimited') }, +export const timeModes: { id: number; key: TimeMode; name: string }[] = [ + { id: 1, key: 'realTime', name: i18n.site.realTime }, + { id: 2, key: 'correspondence', name: i18n.site.correspondence }, + { id: 0, key: 'unlimited', name: i18n.site.unlimited }, ]; export const keyToId = (key: string, items: { id: number; key: string }[]): number => @@ -156,13 +156,13 @@ export const sliderInitVal = ( return undefined; }; -export const gameModes = (trans: Trans): { key: GameMode; name: string }[] => [ - { key: 'casual', name: trans('casual') }, - { key: 'rated', name: trans('rated') }, +export const gameModes: { key: GameMode; name: string }[] = [ + { key: 'casual', name: i18n.site.casual }, + { key: 'rated', name: i18n.site.rated }, ]; -export const colors = (trans: Trans): { key: Color | 'random'; name: string }[] => [ - { key: 'black', name: trans('black') }, - { key: 'random', name: trans('randomColor') }, - { key: 'white', name: trans('white') }, +export const colors: { key: Color | 'random'; name: string }[] = [ + { key: 'black', name: i18n.site.black }, + { key: 'random', name: i18n.site.randomColor }, + { key: 'white', name: i18n.site.white }, ]; diff --git a/ui/lobby/src/setupCtrl.ts b/ui/lobby/src/setupCtrl.ts index 2f646e19c3dda..350fc115a3e0f 100644 --- a/ui/lobby/src/setupCtrl.ts +++ b/ui/lobby/src/setupCtrl.ts @@ -263,7 +263,7 @@ export default class SetupController { xhr.form({ variant: keyToId(this.variant(), variants).toString(), fen: this.fen(), - timeMode: keyToId(this.timeMode(), timeModes(this.root.trans)).toString(), + timeMode: keyToId(this.timeMode(), timeModes).toString(), time: this.time().toString(), time_range: this.timeV().toString(), increment: this.increment().toString(), diff --git a/ui/lobby/src/view/correspondence.ts b/ui/lobby/src/view/correspondence.ts index c0274d26ac5be..f35fb7b762858 100644 --- a/ui/lobby/src/view/correspondence.ts +++ b/ui/lobby/src/view/correspondence.ts @@ -6,8 +6,7 @@ import { Seek } from '../interfaces'; import perfIcons from 'common/perfIcons'; function renderSeek(ctrl: LobbyController, seek: Seek): VNode { - const klass = seek.action === 'joinSeek' ? 'join' : 'cancel', - noarg = ctrl.trans.noarg; + const klass = seek.action === 'joinSeek' ? 'join' : 'cancel'; return h( 'tr.seek.' + klass, { @@ -16,8 +15,8 @@ function renderSeek(ctrl: LobbyController, seek: Seek): VNode { role: 'button', title: seek.action === 'joinSeek' - ? noarg('joinTheGame') + ' - ' + perfNames[seek.perf.key] - : noarg('cancel'), + ? i18n.site.joinTheGame + ' - ' + perfNames[seek.perf.key] + : i18n.site.cancel, 'data-id': seek.id, }, }, @@ -26,10 +25,10 @@ function renderSeek(ctrl: LobbyController, seek: Seek): VNode { ? h('span.ulpt', { attrs: { 'data-href': '/@/' + seek.username } }, seek.username) : 'Anonymous', seek.rating && ctrl.opts.showRatings ? seek.rating + (seek.provisional ? '?' : '') : '', - seek.days ? ctrl.trans.pluralSame('nbDays', seek.days) : '∞', + seek.days ? i18n.site.nbDays(seek.days) : '∞', h('span', [ h('span.varicon', { attrs: { 'data-icon': perfIcons[seek.perf.key] } }), - noarg(seek.mode === 1 ? 'rated' : 'casual'), + seek.mode === 1 ? i18n.site.rated : i18n.site.casual, ]), ]), ); @@ -47,7 +46,7 @@ function createSeek(ctrl: LobbyController): VNode | undefined { ctrl.redraw, ), }, - ctrl.trans('createAGame'), + i18n.site.createAGame, ), ]); return; @@ -60,9 +59,10 @@ export default function (ctrl: LobbyController): MaybeVNodes { 'thead', h( 'tr', - ['player', 'rating', 'time', 'mode'].map(header => h('th', ctrl.trans(header))), + (['player', 'rating', 'time', 'mode'] as const).map(k => h('th', i18n.site[k])), ), ), + h( 'tbody', { @@ -72,7 +72,7 @@ export default function (ctrl: LobbyController): MaybeVNodes { el = el.parentNode as HTMLElement; if (el.nodeName === 'TR') { if (!ctrl.me) { - if (confirm(ctrl.trans('youNeedAnAccountToDoThat'))) location.href = '/signup'; + if (confirm(i18n.site.youNeedAnAccountToDoThat)) location.href = '/signup'; return; } return ctrl.clickSeek(el.dataset['id']!); diff --git a/ui/lobby/src/view/playing.ts b/ui/lobby/src/view/playing.ts index 3ca8f55e78e51..c82c7b46fe80b 100644 --- a/ui/lobby/src/view/playing.ts +++ b/ui/lobby/src/view/playing.ts @@ -30,14 +30,14 @@ export default function (ctrl: LobbyController) { }), h('span.meta', [ pov.opponent.ai - ? ctrl.trans('aiNameLevelAiLevel', 'Stockfish', pov.opponent.ai) + ? i18n.site.aiNameLevelAiLevel('Stockfish', pov.opponent.ai) : pov.opponent.username, h( 'span.indicator', pov.isMyTurn ? pov.secondsLeft && pov.hasMoved ? timer(pov) - : [ctrl.trans.noarg('yourTurn')] + : [i18n.site.yourTurn] : h('span', '\xa0'), ), //   ]), diff --git a/ui/lobby/src/view/pools.ts b/ui/lobby/src/view/pools.ts index 67cd9de24b33d..583a74aa90070 100644 --- a/ui/lobby/src/view/pools.ts +++ b/ui/lobby/src/view/pools.ts @@ -42,7 +42,7 @@ export function render(ctrl: LobbyController) { h( 'div.custom', { class: { transp: !!member }, attrs: { role: 'button', 'data-id': 'custom' } }, - ctrl.trans.noarg('custom'), + i18n.site.custom, ), ); } diff --git a/ui/lobby/src/view/realTime/chart.ts b/ui/lobby/src/view/realTime/chart.ts index 3f07777fe542c..75dd3c6097c3d 100644 --- a/ui/lobby/src/view/realTime/chart.ts +++ b/ui/lobby/src/view/realTime/chart.ts @@ -72,11 +72,11 @@ function renderHook(ctrl: LobbyController, hook: Hook): string { if (ctrl.opts.showRatings) html += ' (' + hook.rating + (hook.prov ? '?' : '') + ')'; html += ''; } else { - html += '' + ctrl.trans('anonymous') + ''; + html += '' + i18n.site.anonymous + ''; } html += '
'; html += `
${hook.clock}
`; - html += ' ' + ctrl.trans(hook.ra ? 'rated' : 'casual') + ''; + html += ' ' + i18n.site[hook.ra ? 'rated' : 'casual'] + ''; html += '
'; return html; } @@ -108,7 +108,7 @@ function renderYAxis() { export function toggle(ctrl: LobbyController) { return h('i.toggle', { key: 'set-mode-list', - attrs: { title: ctrl.trans.noarg('list'), 'data-icon': licon.List }, + attrs: { title: i18n.site.list, 'data-icon': licon.List }, hook: bind('mousedown', _ => ctrl.setMode('list'), ctrl.redraw), }); } diff --git a/ui/lobby/src/view/realTime/filter.ts b/ui/lobby/src/view/realTime/filter.ts index a0e0f36271cf6..2562fb1680aa9 100644 --- a/ui/lobby/src/view/realTime/filter.ts +++ b/ui/lobby/src/view/realTime/filter.ts @@ -64,7 +64,7 @@ export function toggle(ctrl: LobbyController, nbFiltered: number) { return h('i.toggle.toggle-filter', { class: { gamesFiltered: nbFiltered > 0, active: filter.open }, hook: bind('mousedown', filter.toggle, ctrl.redraw), - attrs: { 'data-icon': filter.open ? licon.X : licon.Gear, title: ctrl.trans.noarg('filterGames') }, + attrs: { 'data-icon': filter.open ? licon.X : licon.Gear, title: i18n.site.filterGames }, }); } diff --git a/ui/lobby/src/view/realTime/list.ts b/ui/lobby/src/view/realTime/list.ts index 0185062660fab..78157b6a81fa1 100644 --- a/ui/lobby/src/view/realTime/list.ts +++ b/ui/lobby/src/view/realTime/list.ts @@ -8,7 +8,6 @@ import * as hookRepo from '../../hookRepo'; import { Hook } from '../../interfaces'; function renderHook(ctrl: LobbyController, hook: Hook) { - const noarg = ctrl.trans.noarg; return h( 'tr.hook.' + hook.action, { @@ -19,18 +18,18 @@ function renderHook(ctrl: LobbyController, hook: Hook) { title: hook.disabled ? '' : hook.action === 'join' - ? noarg('joinTheGame') + ' | ' + perfNames[hook.perf] - : noarg('cancel'), + ? i18n.site.joinTheGame + ' | ' + perfNames[hook.perf] + : i18n.site.cancel, 'data-id': hook.id, }, }, tds([ hook.rating ? h('span.ulink.ulpt', { attrs: { 'data-href': '/@/' + hook.u } }, hook.u) - : noarg('anonymous'), + : i18n.site.anonymous, hook.rating && ctrl.opts.showRatings ? hook.rating + (hook.prov ? '?' : '') : '', hook.clock, - h('span', { attrs: { 'data-icon': perfIcons[hook.perf] } }, noarg(hook.ra ? 'rated' : 'casual')), + h('span', { attrs: { 'data-icon': perfIcons[hook.perf] } }, i18n.site[hook.ra ? 'rated' : 'casual']), ]), ); } @@ -44,7 +43,7 @@ const isNotMine = (hook: Hook) => !isMine(hook); export const toggle = (ctrl: LobbyController) => h('i.toggle', { key: 'set-mode-chart', - attrs: { title: ctrl.trans.noarg('graph'), 'data-icon': licon.LineGraph }, + attrs: { title: i18n.site.graph, 'data-icon': licon.LineGraph }, hook: bind('mousedown', _ => ctrl.setMode('chart'), ctrl.redraw), }); @@ -64,7 +63,7 @@ export const render = (ctrl: LobbyController, allHooks: Hook[]) => { ...standards.map(render), variants.length ? h('tr.variants', { key: 'variants' }, [ - h('td', { attrs: { colspan: 5 } }, '— ' + ctrl.trans('variant') + ' —'), + h('td', { attrs: { colspan: 5 } }, '— ' + i18n.site.variant + ' —'), ]) : null, ...variants.map(render), @@ -81,7 +80,7 @@ export const render = (ctrl: LobbyController, allHooks: Hook[]) => { class: { sortable: true, sort: ctrl.sort === 'rating' }, hook: bind('click', _ => ctrl.setSort('rating'), ctrl.redraw), }, - [h('i.is'), ctrl.trans('rating')], + [h('i.is'), i18n.site.rating], ), h( 'th', @@ -89,9 +88,9 @@ export const render = (ctrl: LobbyController, allHooks: Hook[]) => { class: { sortable: true, sort: ctrl.sort === 'time' }, hook: bind('click', _ => ctrl.setSort('time'), ctrl.redraw), }, - [h('i.is'), ctrl.trans('time')], + [h('i.is'), i18n.site.time], ), - h('th', ctrl.trans('mode')), + h('th', i18n.site.mode), ]), ), h( diff --git a/ui/lobby/src/view/setup/components/colorButtons.ts b/ui/lobby/src/view/setup/components/colorButtons.ts index da7b39e2fc130..607ba6643a9cd 100644 --- a/ui/lobby/src/view/setup/components/colorButtons.ts +++ b/ui/lobby/src/view/setup/components/colorButtons.ts @@ -8,7 +8,7 @@ const renderBlindModeColorPicker = (ctrl: LobbyController) => [ ...(ctrl.setupCtrl.gameType === 'hook' ? [] : [ - h('label', { attrs: { for: 'sf_color' } }, ctrl.trans('side')), + h('label', { attrs: { for: 'sf_color' } }, i18n.site.side), h( 'select#sf_color', { @@ -17,7 +17,7 @@ const renderBlindModeColorPicker = (ctrl: LobbyController) => [ ctrl.setupCtrl.blindModeColor((e.target as HTMLSelectElement).value as Color | 'random'), }, }, - colors(ctrl.trans).map(color => option(color, ctrl.setupCtrl.blindModeColor())), + colors.map(color => option(color, ctrl.setupCtrl.blindModeColor())), ), ]), h( @@ -48,7 +48,7 @@ export const createButtons = (ctrl: LobbyController) => { ? renderBlindModeColorPicker(ctrl) : setupCtrl.loading ? spinnerVdom() - : colors(ctrl.trans).map(({ key, name }) => + : colors.map(({ key, name }) => h( `button.button.button-metal.color-submits__button.${key}`, { diff --git a/ui/lobby/src/view/setup/components/fenInput.ts b/ui/lobby/src/view/setup/components/fenInput.ts index 5081c9a142d31..b65e277b5545f 100644 --- a/ui/lobby/src/view/setup/components/fenInput.ts +++ b/ui/lobby/src/view/setup/components/fenInput.ts @@ -4,13 +4,13 @@ import LobbyController from '../../../ctrl'; import { initMiniBoard } from 'common/miniBoard'; export const fenInput = (ctrl: LobbyController) => { - const { trans, setupCtrl } = ctrl; + const { setupCtrl } = ctrl; if (setupCtrl.variant() !== 'fromPosition') return null; const fen = setupCtrl.fen(); return h('div.fen.optional-config', [ h('div.fen__form', [ h('input#fen-input', { - attrs: { placeholder: trans('pasteTheFenStringHere'), value: fen }, + attrs: { placeholder: i18n.site.pasteTheFenStringHere, value: fen }, on: { input: (e: InputEvent) => { setupCtrl.fen((e.target as HTMLInputElement).value.trim()); @@ -23,7 +23,7 @@ export const fenInput = (ctrl: LobbyController) => { h('a.button.button-empty', { attrs: { 'data-icon': licon.Pencil, - title: trans('boardEditor'), + title: i18n.site.boardEditor, href: '/editor' + (fen && !setupCtrl.fenError ? `/${fen.replace(' ', '_')}` : ''), }, }), diff --git a/ui/lobby/src/view/setup/components/gameModeButtons.ts b/ui/lobby/src/view/setup/components/gameModeButtons.ts index a82802df75cfb..592c75e502c52 100644 --- a/ui/lobby/src/view/setup/components/gameModeButtons.ts +++ b/ui/lobby/src/view/setup/components/gameModeButtons.ts @@ -7,12 +7,12 @@ import { gameModes } from '../../../options'; export const gameModeButtons = (ctrl: LobbyController): MaybeVNode => { if (!ctrl.me) return null; - const { trans, setupCtrl } = ctrl; + const { setupCtrl } = ctrl; return h( 'div.mode-choice.buttons', h( 'group.radio', - gameModes(trans).map(({ key, name }) => { + gameModes.map(({ key, name }) => { const disabled = key === 'rated' && setupCtrl.ratedModeDisabled(); return h('div', [ h(`input#sf_mode_${key}.checked_${key === setupCtrl.gameMode()}`, { diff --git a/ui/lobby/src/view/setup/components/levelButtons.ts b/ui/lobby/src/view/setup/components/levelButtons.ts index fb991f1742ec0..95144b7079bb8 100644 --- a/ui/lobby/src/view/setup/components/levelButtons.ts +++ b/ui/lobby/src/view/setup/components/levelButtons.ts @@ -3,10 +3,10 @@ import LobbyController from '../../../ctrl'; import { option } from './option'; export const levelButtons = (ctrl: LobbyController) => { - const { trans, setupCtrl } = ctrl; + const { setupCtrl } = ctrl; return site.blindMode ? [ - h('label', { attrs: { for: 'sf_level' } }, trans('strength')), + h('label', { attrs: { for: 'sf_level' } }, i18n.site.strength), h( 'select#sf_level', { @@ -17,7 +17,7 @@ export const levelButtons = (ctrl: LobbyController) => { ] : [ h('br'), - trans('strength'), + i18n.site.strength, h('div.level.buttons', [ h( 'div.config_level', @@ -45,7 +45,7 @@ export const levelButtons = (ctrl: LobbyController) => { 'div.ai_info', h( `div.sf_level_${setupCtrl.aiLevel()}`, - trans('aiNameLevelAiLevel', 'Fairy-Stockfish 14', setupCtrl.aiLevel()), + i18n.site.aiNameLevelAiLevel('Fairy-Stockfish 14', setupCtrl.aiLevel()), ), ), ]), diff --git a/ui/lobby/src/view/setup/components/ratingDifferenceSliders.ts b/ui/lobby/src/view/setup/components/ratingDifferenceSliders.ts index d695184ee3146..215a6d90d2005 100644 --- a/ui/lobby/src/view/setup/components/ratingDifferenceSliders.ts +++ b/ui/lobby/src/view/setup/components/ratingDifferenceSliders.ts @@ -4,7 +4,7 @@ import LobbyController from '../../../ctrl'; export const ratingDifferenceSliders = (ctrl: LobbyController) => { if (!ctrl.me || site.blindMode || !ctrl.data.ratingMap) return null; - const { trans, setupCtrl } = ctrl; + const { setupCtrl } = ctrl; const selectedPerf = ctrl.setupCtrl.selectedPerf(); const isProvisional = !!ctrl.data.ratingMap[selectedPerf].prov; const disabled = isProvisional ? '.disabled' : ''; @@ -21,7 +21,7 @@ export const ratingDifferenceSliders = (ctrl: LobbyController) => { : undefined, }, [ - trans('ratingRange'), + i18n.site.ratingRange, h('div.rating-range', [ h('input.range.rating-range__min', { attrs: { diff --git a/ui/lobby/src/view/setup/components/ratingView.ts b/ui/lobby/src/view/setup/components/ratingView.ts index 77a2e10788d53..1c4c454170acf 100644 --- a/ui/lobby/src/view/setup/components/ratingView.ts +++ b/ui/lobby/src/view/setup/components/ratingView.ts @@ -19,8 +19,7 @@ export const ratingView = (ctrl: LobbyController): MaybeVNode => { !opts.showRatings ? [h('i', perfIconAttrs), perfOrSpeed.name] : [ - ...ctrl.trans.vdom( - 'perfRatingX', + ...i18n.site.perfRatingX.asArray( h( 'strong', perfIconAttrs, diff --git a/ui/lobby/src/view/setup/components/timePickerAndSliders.ts b/ui/lobby/src/view/setup/components/timePickerAndSliders.ts index 673445aebb5e5..fd7e0213a94ae 100644 --- a/ui/lobby/src/view/setup/components/timePickerAndSliders.ts +++ b/ui/lobby/src/view/setup/components/timePickerAndSliders.ts @@ -13,12 +13,12 @@ const showTime = (v: number) => { }; const renderBlindModeTimePickers = (ctrl: LobbyController, allowAnonymous: boolean) => { - const { trans, setupCtrl } = ctrl; + const { setupCtrl } = ctrl; return [ renderTimeModePicker(ctrl, allowAnonymous), setupCtrl.timeMode() === 'realTime' && h('div.time-choice', [ - h('label', { attrs: { for: 'sf_time' } }, trans('minutesPerSide')), + h('label', { attrs: { for: 'sf_time' } }, i18n.site.minutesPerSide), h( 'select#sf_time', { @@ -31,7 +31,7 @@ const renderBlindModeTimePickers = (ctrl: LobbyController, allowAnonymous: boole ]), setupCtrl.timeMode() === 'realTime' && h('div.increment-choice', [ - h('label', { attrs: { for: 'sf_increment' } }, trans('incrementInSeconds')), + h('label', { attrs: { for: 'sf_increment' } }, i18n.site.incrementInSeconds), h( 'select#sf_increment', { @@ -50,7 +50,7 @@ const renderBlindModeTimePickers = (ctrl: LobbyController, allowAnonymous: boole ]), setupCtrl.timeMode() === 'correspondence' && h('div.days-choice', [ - h('label', { attrs: { for: 'sf_days' } }, trans('daysPerTurn')), + h('label', { attrs: { for: 'sf_days' } }, i18n.site.daysPerTurn), h( 'select#sf_days', { @@ -69,11 +69,11 @@ const renderBlindModeTimePickers = (ctrl: LobbyController, allowAnonymous: boole }; const renderTimeModePicker = (ctrl: LobbyController, allowAnonymous = false) => { - const { trans, setupCtrl } = ctrl; + const { setupCtrl } = ctrl; return ( (ctrl.me || allowAnonymous) && h('div.label-select', [ - h('label', { attrs: { for: 'sf_timeMode' } }, trans('timeControl')), + h('label', { attrs: { for: 'sf_timeMode' } }, i18n.site.timeControl), h( 'select#sf_timeMode', { @@ -81,7 +81,7 @@ const renderTimeModePicker = (ctrl: LobbyController, allowAnonymous = false) => change: (e: Event) => setupCtrl.timeMode((e.target as HTMLSelectElement).value as TimeMode), }, }, - timeModes(trans).map(timeMode => option(timeMode, setupCtrl.timeMode())), + timeModes.map(timeMode => option(timeMode, setupCtrl.timeMode())), ), ]) ); @@ -95,7 +95,7 @@ const inputRange = (min: number, max: number, prop: Prop, classes?: }); export const timePickerAndSliders = (ctrl: LobbyController, allowAnonymous = false) => { - const { trans, setupCtrl } = ctrl; + const { setupCtrl } = ctrl; return h( 'div.time-mode-config.optional-config', site.blindMode @@ -104,7 +104,7 @@ export const timePickerAndSliders = (ctrl: LobbyController, allowAnonymous = fal renderTimeModePicker(ctrl, allowAnonymous), setupCtrl.timeMode() === 'realTime' && h('div.time-choice.range', [ - `${trans('minutesPerSide')}: `, + `${i18n.site.minutesPerSide}: `, h('span', showTime(setupCtrl.time())), inputRange(0, 38, setupCtrl.timeV, { failure: !setupCtrl.validTime() || !setupCtrl.validAiTime(), @@ -112,7 +112,7 @@ export const timePickerAndSliders = (ctrl: LobbyController, allowAnonymous = fal ]), setupCtrl.timeMode() === 'realTime' ? h('div.increment-choice.range', [ - `${trans('incrementInSeconds')}: `, + `${i18n.site.incrementInSeconds}: `, h('span', `${setupCtrl.increment()}`), inputRange(0, 30, setupCtrl.incrementV, { failure: !setupCtrl.validTime() }), ]) @@ -120,7 +120,7 @@ export const timePickerAndSliders = (ctrl: LobbyController, allowAnonymous = fal h( 'div.correspondence', h('div.days-choice.range', [ - `${trans('daysPerTurn')}: `, + `${i18n.site.daysPerTurn}: `, h('span', `${setupCtrl.days()}`), inputRange(1, 7, setupCtrl.daysV), ]), diff --git a/ui/lobby/src/view/setup/components/variantPicker.ts b/ui/lobby/src/view/setup/components/variantPicker.ts index 3d98cf2cc6d5f..a26438eb52d81 100644 --- a/ui/lobby/src/view/setup/components/variantPicker.ts +++ b/ui/lobby/src/view/setup/components/variantPicker.ts @@ -5,10 +5,10 @@ import { variantsBlindMode, variants, variantsForGameType } from '../../../optio import { option } from './option'; export const variantPicker = (ctrl: LobbyController) => { - const { trans, setupCtrl } = ctrl; + const { setupCtrl } = ctrl; const baseVariants = site.blindMode ? variantsBlindMode : variants; return h('div.variant.label-select', [ - h('label', { attrs: { for: 'sf_variant' } }, trans('variant')), + h('label', { attrs: { for: 'sf_variant' } }, i18n.site.variant), h( 'select#sf_variant', { diff --git a/ui/lobby/src/view/setup/modal.ts b/ui/lobby/src/view/setup/modal.ts index c3ecb0b0b53ed..7f040b3d9810e 100644 --- a/ui/lobby/src/view/setup/modal.ts +++ b/ui/lobby/src/view/setup/modal.ts @@ -25,7 +25,7 @@ export default function setupModal(ctrl: LobbyController): MaybeVNode { const views = { hook: (ctrl: LobbyController): MaybeVNodes => [ - h('h2', ctrl.trans('createAGame')), + h('h2', i18n.site.createAGame), h('div.setup-content', [ variantPicker(ctrl), timePickerAndSliders(ctrl), @@ -35,7 +35,7 @@ const views = { ]), ], friend: (ctrl: LobbyController): MaybeVNodes => [ - h('h2', ctrl.trans('playWithAFriend')), + h('h2', i18n.site.playWithAFriend), h('div.setup-content', [ ctrl.setupCtrl.friendUser ? userLink({ name: ctrl.setupCtrl.friendUser, line: false }) : null, variantPicker(ctrl), @@ -46,7 +46,7 @@ const views = { ]), ], ai: (ctrl: LobbyController): MaybeVNodes => [ - h('h2', ctrl.trans('playWithTheMachine')), + h('h2', i18n.site.playWithTheMachine), h('div.setup-content', [ variantPicker(ctrl), fenInput(ctrl), diff --git a/ui/lobby/src/view/table.ts b/ui/lobby/src/view/table.ts index 3e1830106dd96..2a7bfacdef7f7 100644 --- a/ui/lobby/src/view/table.ts +++ b/ui/lobby/src/view/table.ts @@ -6,7 +6,7 @@ import renderSetupModal from './setup/modal'; import { numberFormat } from 'common/number'; export default function table(ctrl: LobbyController) { - const { data, trans, opts } = ctrl; + const { data, opts } = ctrl; const hasOngoingRealTimeGame = ctrl.hasOngoingRealTimeGame(); const hookDisabled = opts.playban || opts.hasUnreadLichessMessage || ctrl.me?.isBot || hasOngoingRealTimeGame; @@ -16,10 +16,10 @@ export default function table(ctrl: LobbyController) { 'div.lobby__start', (site.blindMode ? [h('h2', 'Play')] : []).concat( [ - ['hook', 'createAGame', hookDisabled], - ['friend', 'playWithAFriend', hasOngoingRealTimeGame], - ['ai', 'playWithTheMachine', hasOngoingRealTimeGame], - ].map(([gameType, transKey, disabled]: [Exclude, string, boolean]) => + ['hook', i18n.site.createAGame, hookDisabled], + ['friend', i18n.site.playWithAFriend, hasOngoingRealTimeGame], + ['ai', i18n.site.playWithTheMachine, hasOngoingRealTimeGame], + ].map(([gameType, text, disabled]: [Exclude, string, boolean]) => h( `button.button.button-metal.config_${gameType}`, { @@ -27,7 +27,7 @@ export default function table(ctrl: LobbyController) { attrs: { type: 'button' }, hook: disabled ? {} : bind('click', () => ctrl.setupCtrl.openModal(gameType), ctrl.redraw), }, - trans(transKey), + text, ), ), ), @@ -42,8 +42,7 @@ export default function table(ctrl: LobbyController) { h( 'a', { attrs: site.blindMode ? {} : { href: '/player' } }, - trans.vdomPlural( - 'nbPlayers', + i18n.site.nbPlayers.asArray( members, h( 'strong', @@ -60,8 +59,7 @@ export default function table(ctrl: LobbyController) { h( 'a', site.blindMode ? {} : { attrs: { href: '/games' } }, - trans.vdomPlural( - 'nbGamesInPlay', + i18n.site.nbGamesInPlay.asArray( rounds, h( 'strong', diff --git a/ui/lobby/src/view/tabs.ts b/ui/lobby/src/view/tabs.ts index 17a79517df30f..97885153ee0f4 100644 --- a/ui/lobby/src/view/tabs.ts +++ b/ui/lobby/src/view/tabs.ts @@ -21,16 +21,12 @@ export default function (ctrl: LobbyController) { active = ctrl.tab, isBot = ctrl.me?.isBot; return [ - isBot ? undefined : tab(ctrl, 'pools', active, [ctrl.trans.noarg('quickPairing')]), - isBot ? undefined : tab(ctrl, 'real_time', active, [ctrl.trans.noarg('lobby')]), - isBot ? undefined : tab(ctrl, 'seeks', active, [ctrl.trans.noarg('correspondence')]), + isBot ? undefined : tab(ctrl, 'pools', active, [i18n.site.quickPairing]), + isBot ? undefined : tab(ctrl, 'real_time', active, [i18n.site.lobby]), + isBot ? undefined : tab(ctrl, 'seeks', active, [i18n.site.correspondence]), active === 'now_playing' || nbPlaying || isBot ? tab(ctrl, 'now_playing', active, [ - ...ctrl.trans.vdomPlural( - 'nbGamesInPlay', - nbPlaying, - nbPlaying >= 100 ? '100+' : nbPlaying.toString(), - ), + ...i18n.site.nbGamesInPlay.asArray(nbPlaying, nbPlaying >= 100 ? '100+' : nbPlaying.toString()), myTurnPovsNb > 0 ? h('i.unread', myTurnPovsNb >= 9 ? '9+' : myTurnPovsNb) : null, ]) : null, diff --git a/ui/msg/src/ctrl.ts b/ui/msg/src/ctrl.ts index 484fd9285bdc9..5e7d8540bda2a 100644 --- a/ui/msg/src/ctrl.ts +++ b/ui/msg/src/ctrl.ts @@ -31,7 +31,6 @@ export default class MsgCtrl { constructor( data: MsgData, - readonly trans: Trans, readonly redraw: Redraw, ) { this.data = data; diff --git a/ui/msg/src/interfaces.ts b/ui/msg/src/interfaces.ts index f64fa9d1e93ad..8425feb702d05 100644 --- a/ui/msg/src/interfaces.ts +++ b/ui/msg/src/interfaces.ts @@ -1,6 +1,5 @@ export interface MsgOpts { data: MsgData; - i18n: I18nDict; } export interface MsgData { me: Me; diff --git a/ui/msg/src/msg.ts b/ui/msg/src/msg.ts index e40e3bb740ef0..0e9fad3d772fc 100644 --- a/ui/msg/src/msg.ts +++ b/ui/msg/src/msg.ts @@ -5,7 +5,6 @@ import { init, classModule, attributesModule } from 'snabbdom'; import { MsgOpts } from './interfaces'; import { upgradeData } from './network'; import MsgCtrl from './ctrl'; -import { trans } from 'common/i18n'; export function initModule(opts: MsgOpts) { const element = document.querySelector('.msg-app') as HTMLElement, @@ -13,7 +12,7 @@ export function initModule(opts: MsgOpts) { appHeight = () => document.body.style.setProperty('---app-height', `${window.innerHeight}px`); window.addEventListener('resize', appHeight); appHeight(); - const ctrl = new MsgCtrl(upgradeData(opts.data), trans(opts.i18n), redraw); + const ctrl = new MsgCtrl(upgradeData(opts.data), redraw); const blueprint = view(ctrl); element.innerHTML = ''; diff --git a/ui/msg/src/view/actions.ts b/ui/msg/src/view/actions.ts index 1eb769b88b528..afe6107a8d3b9 100644 --- a/ui/msg/src/view/actions.ts +++ b/ui/msg/src/view/actions.ts @@ -15,7 +15,7 @@ export default function renderActions(ctrl: MsgCtrl, convo: Convo): VNode[] { attrs: { 'data-icon': licon.Swords, href: `/?user=${convo.user.name}#friend`, - title: ctrl.trans.noarg('challengeToPlay'), + title: i18n.challenge.challengeToPlay, }, }), ); @@ -26,9 +26,9 @@ export default function renderActions(ctrl: MsgCtrl, convo: Convo): VNode[] { key: 'unblock', attrs: { 'data-icon': licon.NotAllowed, - title: ctrl.trans.noarg('blocked'), + title: i18n.site.blocked, type: 'button', - 'data-hover-text': ctrl.trans.noarg('unblock'), + 'data-hover-text': i18n.site.unblock, }, hook: bind('click', ctrl.unblock), }), @@ -40,7 +40,7 @@ export default function renderActions(ctrl: MsgCtrl, convo: Convo): VNode[] { attrs: { 'data-icon': licon.NotAllowed, type: 'button', - title: ctrl.trans.noarg('block'), + title: i18n.site.block, }, hook: bind('click', withConfirm(ctrl.block)), }), @@ -48,7 +48,7 @@ export default function renderActions(ctrl: MsgCtrl, convo: Convo): VNode[] { nodes.push( h(`button.${cls}.bad`, { key: 'delete', - attrs: { 'data-icon': licon.Trash, type: 'button', title: ctrl.trans.noarg('delete') }, + attrs: { 'data-icon': licon.Trash, type: 'button', title: i18n.site.delete }, hook: bind('click', withConfirm(ctrl.delete)), }), ); @@ -58,7 +58,7 @@ export default function renderActions(ctrl: MsgCtrl, convo: Convo): VNode[] { attrs: { href: '/report/inbox/' + convo.user.name, 'data-icon': licon.CautionTriangle, - title: ctrl.trans('reportXToModerators', convo.user.name), + title: i18n.site.reportXToModerators(convo.user.name), }, }), ); diff --git a/ui/msg/src/view/msgs.ts b/ui/msg/src/view/msgs.ts index 3b7db5d9a7228..15fe6d7c9660a 100644 --- a/ui/msg/src/view/msgs.ts +++ b/ui/msg/src/view/msgs.ts @@ -8,31 +8,27 @@ import { scroller } from './scroller'; import MsgCtrl from '../ctrl'; export default function renderMsgs(ctrl: MsgCtrl, convo: Convo): VNode { - return h( - 'div.msg-app__convo__msgs', - { hook: { insert: setupMsgs(ctrl, true), postpatch: setupMsgs(ctrl, false) } }, - [ - h('div.msg-app__convo__msgs__init'), - h('div.msg-app__convo__msgs__content', [ - ctrl.canGetMoreSince - ? h( - 'button.msg-app__convo__msgs__more.button.button-empty', - { - key: 'more', - attrs: { type: 'button' }, - hook: bind('click', _ => { - scroller.setMarker(); - ctrl.getMore(); - }), - }, - 'Load more', - ) - : null, - ...contentMsgs(ctrl, convo.msgs), - h('div.msg-app__convo__msgs__typing', ctrl.typing ? `${convo.user.name} is typing...` : null), - ]), - ], - ); + return h('div.msg-app__convo__msgs', { hook: { insert: setupMsgs(true), postpatch: setupMsgs(false) } }, [ + h('div.msg-app__convo__msgs__init'), + h('div.msg-app__convo__msgs__content', [ + ctrl.canGetMoreSince + ? h( + 'button.msg-app__convo__msgs__more.button.button-empty', + { + key: 'more', + attrs: { type: 'button' }, + hook: bind('click', _ => { + scroller.setMarker(); + ctrl.getMore(); + }), + }, + 'Load more', + ) + : null, + ...contentMsgs(ctrl, convo.msgs), + h('div.msg-app__convo__msgs__typing', ctrl.typing ? `${convo.user.name} is typing...` : null), + ]), + ]); } function contentMsgs(ctrl: MsgCtrl, msgs: Msg[]): VNode[] { @@ -44,7 +40,7 @@ function contentMsgs(ctrl: MsgCtrl, msgs: Msg[]): VNode[] { function renderDaily(ctrl: MsgCtrl, daily: Daily): VNode[] { return [ - h('day', renderDate(daily.date, ctrl.trans)), + h('day', renderDate(daily.date)), ...daily.msgs.map(group => h( 'group', @@ -78,9 +74,9 @@ const today = new Date(); const yesterday = new Date(); yesterday.setDate(yesterday.getDate() - 1); -function renderDate(date: Date, trans: Trans) { - if (sameDay(date, today)) return trans.noarg('today').toUpperCase(); - if (sameDay(date, yesterday)) return trans.noarg('yesterday').toUpperCase(); +function renderDate(date: Date) { + if (sameDay(date, today)) return i18n.site.today.toUpperCase(); + if (sameDay(date, yesterday)) return i18n.site.yesterday.toUpperCase(); return renderFullDate(date); } @@ -116,11 +112,11 @@ const renderText = (msg: Msg) => }) : h('t', msg.text); -const setupMsgs = (ctrl: MsgCtrl, insert: boolean) => (vnode: VNode) => { +const setupMsgs = (insert: boolean) => (vnode: VNode) => { const el = vnode.elm as HTMLElement; if (insert) scroller.init(el); enhance.expandLpvs(el); - makeLinkPopups(el, ctrl.trans, 'their a[href^="http"]'); + makeLinkPopups(el, 'their a[href^="http"]'); scroller.toMarker() || scroller.auto(); }; diff --git a/ui/msg/src/view/search.ts b/ui/msg/src/view/search.ts index dde912f34fe99..78698d41e028d 100644 --- a/ui/msg/src/view/search.ts +++ b/ui/msg/src/view/search.ts @@ -9,7 +9,7 @@ import { hookMobileMousedown } from 'common/device'; export const renderInput = (ctrl: MsgCtrl): VNode => h('div.msg-app__side__search', [ h('input', { - attrs: { value: '', placeholder: ctrl.trans.noarg('searchOrStartNewDiscussion') }, + attrs: { value: '', placeholder: i18n.site.searchOrStartNewDiscussion }, hook: { insert(vnode) { const input = vnode.elm as HTMLInputElement; @@ -32,7 +32,7 @@ export function renderResults(ctrl: MsgCtrl, res: SearchResult): VNode { return h('div.msg-app__search.msg-app__side__content', [ res.contacts[0] && h('section', [ - h('h2', ctrl.trans.noarg('discussions')), + h('h2', i18n.site.discussions), h( 'div.msg-app__search__contacts', res.contacts.map(t => renderContacts(ctrl, t)), @@ -40,7 +40,7 @@ export function renderResults(ctrl: MsgCtrl, res: SearchResult): VNode { ]), res.friends[0] && h('section', [ - h('h2', ctrl.trans.noarg('friends')), + h('h2', i18n.site.friends), h( 'div.msg-app__search__users', res.friends.map(u => renderUser(ctrl, u)), @@ -48,7 +48,7 @@ export function renderResults(ctrl: MsgCtrl, res: SearchResult): VNode { ]), res.users[0] && h('section', [ - h('h2', ctrl.trans.noarg('players')), + h('h2', i18n.site.players), h( 'div.msg-app__search__users', res.users.map(u => renderUser(ctrl, u)), diff --git a/ui/notify/src/interfaces.ts b/ui/notify/src/interfaces.ts index fda145ca56881..780fdaa562b9d 100644 --- a/ui/notify/src/interfaces.ts +++ b/ui/notify/src/interfaces.ts @@ -14,7 +14,6 @@ export interface NotifyOpts { export interface NotifyData { pager: Paginator; unread: number; - i18n: I18nDict; } export interface BumpUnread {} diff --git a/ui/notify/src/renderers.ts b/ui/notify/src/renderers.ts index 1efdaa8b6ea0a..9068359e79f30 100644 --- a/ui/notify/src/renderers.ts +++ b/ui/notify/src/renderers.ts @@ -3,15 +3,15 @@ import * as licon from 'common/licon'; import { Notification, Renderer, Renderers } from './interfaces'; import { timeago } from 'common/i18n'; -export default function makeRenderers(trans: Trans): Renderers { +export default function makeRenderers(): Renderers { return { streamStart: { html: n => generic(n, `/streamer/${n.content.sid}/redirect`, licon.Mic, [ h('span', [h('strong', n.content.name), drawTime(n)]), - h('span', trans('startedStreaming')), + h('span', i18n.site.startedStreaming), ]), - text: n => trans('xStartedStreaming', n.content.streamerName), + text: n => i18n.site.xStartedStreaming(n.content.streamerName), }, genericLink: { html: n => @@ -33,17 +33,17 @@ export default function makeRenderers(trans: Trans): Renderers { html: n => generic(n, `/forum/redirect/post/${n.content.postId}`, licon.BubbleConvo, [ h('span', [h('strong', userFullName(n.content.mentionedBy)), drawTime(n)]), - h('span', trans('mentionedYouInX', n.content.topic)), + h('span', i18n.site.mentionedYouInX(n.content.topic)), ]), - text: n => trans('xMentionedYouInY', userFullName(n.content.mentionedBy), n.content.topic), + text: n => i18n.site.xMentionedYouInY(userFullName(n.content.mentionedBy), n.content.topic), }, invitedStudy: { html: n => generic(n, '/study/' + n.content.studyId, licon.StudyBoard, [ h('span', [h('strong', userFullName(n.content.invitedBy)), drawTime(n)]), - h('span', trans('invitedYouToX', n.content.studyName)), + h('span', i18n.site.invitedYouToX(n.content.studyName)), ]), - text: n => trans('xInvitedYouToY', userFullName(n.content.invitedBy), n.content.studyName), + text: n => i18n.site.xInvitedYouToY(userFullName(n.content.invitedBy), n.content.studyName), }, privateMessage: { html: n => @@ -57,9 +57,9 @@ export default function makeRenderers(trans: Trans): Renderers { html: n => generic(n, '/team/' + n.content.id, licon.Group, [ h('span', [h('strong', n.content.name), drawTime(n)]), - h('span', trans.noarg('youAreNowPartOfTeam')), + h('span', i18n.site.youAreNowPartOfTeam), ]), - text: n => trans('youHaveJoinedTeamX', n.content.name), + text: n => i18n.site.youHaveJoinedTeamX(n.content.name), }, titledTourney: { html: n => @@ -72,26 +72,26 @@ export default function makeRenderers(trans: Trans): Renderers { reportedBanned: { html: n => generic(n, undefined, licon.InfoCircle, [ - h('span', [h('strong', trans.noarg('someoneYouReportedWasBanned'))]), - h('span', trans.noarg('thankYou')), + h('span', [h('strong', i18n.site.someoneYouReportedWasBanned)]), + h('span', i18n.site.thankYou), ]), - text: _ => trans.noarg('someoneYouReportedWasBanned'), + text: _ => i18n.site.someoneYouReportedWasBanned, }, gameEnd: { html: n => { let result; switch (n.content.win) { case true: - result = trans.noarg('congratsYouWon'); + result = i18n.site.congratsYouWon; break; case false: - result = trans.noarg('defeat'); + result = i18n.site.defeat; break; default: - result = trans.noarg('draw'); + result = i18n.site.draw; } return generic(n, '/' + n.content.id, licon.PaperAirplane, [ - h('span', [h('strong', trans('gameVsX', userFullName(n.content.opponent))), drawTime(n)]), + h('span', [h('strong', i18n.site.gameVsX(userFullName(n.content.opponent))), drawTime(n)]), h('span', result), ]); }, @@ -99,15 +99,15 @@ export default function makeRenderers(trans: Trans): Renderers { let result; switch (n.content.win) { case true: - result = trans.noarg('victory'); + result = i18n.site.victory; break; case false: - result = trans.noarg('defeat'); + result = i18n.site.defeat; break; default: - result = trans.noarg('draw'); + result = i18n.site.draw; } - return trans('resVsX', result, userFullName(n.content.opponent)); + return i18n.site.resVsX(result, userFullName(n.content.opponent)); }, }, planStart: { @@ -125,19 +125,19 @@ export default function makeRenderers(trans: Trans): Renderers { ratingRefund: { html: n => generic(n, '/faq#rating-refund', licon.InfoCircle, [ - h('span', [h('strong', trans.noarg('lostAgainstTOSViolator')), drawTime(n)]), - h('span', trans('refundXpointsTimeControlY', n.content.points, n.content.perf)), + h('span', [h('strong', i18n.site.lostAgainstTOSViolator), drawTime(n)]), + h('span', i18n.site.refundXpointsTimeControlY(n.content.points, n.content.perf)), ]), - text: n => trans('refundXpointsTimeControlY', n.content.points, n.content.perf), + text: n => i18n.site.refundXpointsTimeControlY(n.content.points, n.content.perf), }, corresAlarm: { html: n => generic(n, '/' + n.content.id, licon.PaperAirplane, [ - h('span', [h('strong', trans.noarg('timeAlmostUp')), drawTime(n)]), + h('span', [h('strong', i18n.site.timeAlmostUp), drawTime(n)]), // not a `LightUser`, could be a game against Stockfish - h('span', trans('gameVsX', n.content.op)), + h('span', i18n.site.gameVsX(n.content.op)), ]), - text: _ => trans.noarg('timeAlmostUp'), + text: _ => i18n.site.timeAlmostUp, }, irwinDone: jobDone('Irwin'), kaladinDone: jobDone('Kaladin'), diff --git a/ui/notify/src/view.ts b/ui/notify/src/view.ts index db6d58fd1f808..88c39348cef22 100644 --- a/ui/notify/src/view.ts +++ b/ui/notify/src/view.ts @@ -3,9 +3,10 @@ import { h, VNode } from 'snabbdom'; import * as licon from 'common/licon'; import { spinnerVdom as spinner } from 'common/spinner'; import makeRenderers from './renderers'; -import { trans } from 'common/i18n'; import { pubsub } from 'common/pubsub'; +const renderers = makeRenderers(); + export default function view(ctrl: Ctrl): VNode { const d = ctrl.data(); return h( @@ -49,8 +50,7 @@ function renderContent(ctrl: Ctrl, d: NotifyData): VNode[] { return nodes; } -export function asText(n: Notification, trans: Trans): string | undefined { - const renderers = makeRenderers(trans); +export function asText(n: Notification): string | undefined { return renderers[n.type] ? renderers[n.type].text(n) : undefined; } @@ -62,8 +62,7 @@ function notificationDenied(): VNode { ); } -function asHtml(n: Notification, trans: Trans): VNode | undefined { - const renderers = makeRenderers(trans); +function asHtml(n: Notification): VNode | undefined { return renderers[n.type] ? renderers[n.type].html(n) : undefined; } @@ -78,14 +77,13 @@ function clickHook(f: () => void) { const contentLoaded = (vnode: VNode) => pubsub.emit('content-loaded', vnode.elm); function recentNotifications(d: NotifyData, scrolling: boolean): VNode { - const translations = trans(d.i18n); return h( 'div', { class: { notifications: true, scrolling }, hook: { insert: contentLoaded, postpatch: contentLoaded }, }, - d.pager.currentPageResults.map(n => asHtml(n, translations)) as VNode[], + d.pager.currentPageResults.map(n => asHtml(n)) as VNode[], ); } diff --git a/ui/puz/src/interfaces.ts b/ui/puz/src/interfaces.ts index cf8b9cd9f0679..5bc94174e6f21 100644 --- a/ui/puz/src/interfaces.ts +++ b/ui/puz/src/interfaces.ts @@ -7,7 +7,6 @@ import * as Prefs from 'common/prefs'; export interface PuzCtrl { run: Run; filters: PuzFilters; - trans: Trans; pref: PuzPrefs; } diff --git a/ui/puz/src/run.ts b/ui/puz/src/run.ts index 088e5be8dc195..72fd7efa2cd08 100644 --- a/ui/puz/src/run.ts +++ b/ui/puz/src/run.ts @@ -23,5 +23,7 @@ export const makeCgOpts = (run: Run, canMove: boolean, flipped: boolean): CgConf }; }; -export const povMessage = (run: Run) => - `youPlayThe${run.pov == 'white' ? 'White' : 'Black'}PiecesInAllPuzzles`; +export const povMessage = (run: Run): string => + run.pov === 'white' + ? i18n.storm.youPlayTheWhitePiecesInAllPuzzles + : i18n.storm.youPlayTheBlackPiecesInAllPuzzles; diff --git a/ui/puz/src/view/history.ts b/ui/puz/src/view/history.ts index 7ce471bea28cb..ba5f14455a896 100644 --- a/ui/puz/src/view/history.ts +++ b/ui/puz/src/view/history.ts @@ -27,14 +27,13 @@ const toggleButton = (prop: Toggle, title: string): VNode => export default (ctrl: PuzCtrl): VNode => { const slowIds = slowPuzzleIds(ctrl), filters = ctrl.filters, - noarg = ctrl.trans.noarg, buttons: VNode[] = [ - toggleButton(filters.fail, noarg('failedPuzzles')), - toggleButton(filters.slow, noarg('slowPuzzles')), + toggleButton(filters.fail, i18n.storm.failedPuzzles), + toggleButton(filters.slow, i18n.storm.slowPuzzles), ]; - if (filters.skip) buttons.push(toggleButton(filters.skip, noarg('skippedPuzzle'))); + if (filters.skip) buttons.push(toggleButton(filters.skip, i18n.storm.skippedPuzzle)); return h('div.puz-history.box.box-pad', [ - h('div.box__top', [h('h2', ctrl.trans('puzzlesPlayed')), h('div.box__top__actions', buttons)]), + h('div.box__top', [h('h2', i18n.storm.puzzlesPlayed), h('div.box__top__actions', buttons)]), h( 'div.puz-history__rounds', ctrl.run.history diff --git a/ui/puzzle/src/ctrl.ts b/ui/puzzle/src/ctrl.ts index bedbb45d39f93..ef05b4b5470d0 100755 --- a/ui/puzzle/src/ctrl.ts +++ b/ui/puzzle/src/ctrl.ts @@ -6,7 +6,16 @@ import moveTest from './moveTest'; import PuzzleSession from './session'; import PuzzleStreak from './streak'; import { throttle } from 'common/timing'; -import { PuzzleOpts, PuzzleData, MoveTest, ThemeKey, ReplayEnd, NvuiPlugin, PuzzleRound } from './interfaces'; +import { + PuzzleOpts, + PuzzleData, + MoveTest, + ThemeKey, + ReplayEnd, + NvuiPlugin, + PuzzleRound, + RoundThemes, +} from './interfaces'; import { Api as CgApi } from 'chessground/api'; import { build as treeBuild, ops as treeOps, path as treePath, TreeWrapper } from 'tree'; import { Chess, normalizeMove } from 'chessops/chess'; @@ -29,13 +38,11 @@ import { last } from 'tree/dist/ops'; import { uciToMove } from 'chessground/util'; import { Redraw } from 'common/snabbdom'; import { ParentCtrl } from 'ceval/src/types'; -import { trans } from 'common/i18n'; import { pubsub } from 'common/pubsub'; export default class PuzzleCtrl implements ParentCtrl { data: PuzzleData; next: Deferred = defer(); - trans: Trans; tree: TreeWrapper; ceval: CevalCtrl; autoNext: StoredProp; @@ -76,7 +83,6 @@ export default class PuzzleCtrl implements ParentCtrl { readonly redraw: Redraw, readonly nvui?: NvuiPlugin, ) { - this.trans = trans(opts.i18n); this.rated = storedBooleanPropWithEffect('puzzle.rated', true, this.redraw); this.autoNext = storedBooleanProp( `puzzle.autoNext${opts.data.streak ? '.streak' : ''}`, @@ -621,7 +627,7 @@ export default class PuzzleCtrl implements ParentCtrl { voteTheme = (theme: ThemeKey, v: boolean) => { if (this.round) { - this.round.themes = this.round.themes || {}; + this.round.themes = this.round.themes || ({} as RoundThemes); if (v === this.round.themes[theme]) { delete this.round.themes[theme]; xhr.voteTheme(this.data.puzzle.id, theme, undefined); diff --git a/ui/puzzle/src/interfaces.ts b/ui/puzzle/src/interfaces.ts index 006c591e44fcc..d2a9e8d51ed36 100644 --- a/ui/puzzle/src/interfaces.ts +++ b/ui/puzzle/src/interfaces.ts @@ -7,8 +7,8 @@ import { FEN } from 'chessground/types'; import { ExternalEngineInfo } from 'ceval'; export type PuzzleId = string; +export type ThemeKey = keyof I18n['puzzleTheme']; -export type ThemeKey = string; export interface AllThemes { dynamic: ThemeKey[]; static: Set; @@ -30,7 +30,6 @@ export interface PuzzleSettings { export interface PuzzleOpts { pref: PuzzlePrefs; data: PuzzleData; - i18n: I18nDict; settings: PuzzleSettings; themes?: { dynamic: string; @@ -124,9 +123,9 @@ export interface PuzzleResult { replayComplete?: boolean; } -export interface RoundThemes { - [key: string]: boolean; -} +export type RoundThemes = { + [key in ThemeKey]: boolean; +}; export interface PuzzleRound { win: boolean; diff --git a/ui/puzzle/src/plugins/puzzle.nvui.ts b/ui/puzzle/src/plugins/puzzle.nvui.ts index 776be0c8d4fc3..c994f4ec0ff26 100644 --- a/ui/puzzle/src/plugins/puzzle.nvui.ts +++ b/ui/puzzle/src/plugins/puzzle.nvui.ts @@ -258,13 +258,13 @@ function onSubmit( ctrl.playUci(uci); switch (ctrl.lastFeedback) { case 'fail': - notify(ctrl.trans.noarg('notTheMove')); + notify(i18n.puzzle.notTheMove); break; case 'good': - notify(ctrl.trans.noarg('bestMove')); + notify(i18n.puzzle.bestMove); break; case 'win': - notify(ctrl.trans.noarg('puzzleSuccess')); + notify(i18n.puzzle.puzzleSuccess); } } else { notify([`Invalid move: ${input}`, ...browseHint(ctrl)].join('. ')); @@ -332,7 +332,7 @@ function nextNode(node?: Tree.Node): Tree.Node | undefined { function renderStreak(ctrl: PuzzleCtrl): VNode[] { if (!ctrl.streak) return []; - return [h('h2', 'Puzzle streak'), h('p', ctrl.streak.data.index || ctrl.trans.noarg('streakDescription'))]; + return [h('h2', 'Puzzle streak'), h('p', ctrl.streak.data.index || i18n.puzzle.streakDescription)]; } function renderStatus(ctrl: PuzzleCtrl): string { @@ -346,17 +346,13 @@ function renderReplay(ctrl: PuzzleCtrl): string { const replay = ctrl.data.replay; if (!replay) return ''; const i = replay.i + (ctrl.mode === 'play' ? 0 : 1); - return `Replaying ${ctrl.trans.noarg(ctrl.data.angle.key)} puzzles: ${i} of ${replay.of}`; + const text = i18n.puzzleTheme[ctrl.data.angle.key]; + return `Replaying ${text} puzzles: ${i} of ${replay.of}`; } function playActions(ctrl: PuzzleCtrl): VNode { if (ctrl.streak) - return button( - ctrl.trans.noarg('skip'), - ctrl.skip, - ctrl.trans.noarg('streakSkipExplanation'), - !ctrl.streak.data.skip, - ); + return button(i18n.storm.skip, ctrl.skip, i18n.puzzle.streakSkipExplanation, !ctrl.streak.data.skip); else return h('div.actions_play', button('View the solution', ctrl.viewSolution)); } @@ -365,14 +361,14 @@ function afterActions(ctrl: PuzzleCtrl): VNode { return h( 'div.actions_after', ctrl.streak && !win - ? anchor(ctrl.trans.noarg('newStreak'), '/streak') + ? anchor(i18n.puzzle.newStreak, '/streak') : [...renderVote(ctrl), button('Continue training', ctrl.nextPuzzle)], ); } const renderVoteTutorial = (ctrl: PuzzleCtrl): VNode[] => ctrl.session.isNew() && ctrl.data.user?.provisional - ? [h('p', ctrl.trans.noarg('didYouLikeThisPuzzle')), h('p', ctrl.trans.noarg('voteToLoadNextOne'))] + ? [h('p', i18n.puzzle.didYouLikeThisPuzzle), h('p', i18n.puzzle.voteToLoadNextOne)] : []; function renderVote(ctrl: PuzzleCtrl): VNode[] { diff --git a/ui/puzzle/src/view/after.ts b/ui/puzzle/src/view/after.ts index 85be6244750aa..ebbadaba5594e 100644 --- a/ui/puzzle/src/view/after.ts +++ b/ui/puzzle/src/view/after.ts @@ -12,8 +12,8 @@ const renderVote = (ctrl: PuzzleCtrl): VNode => ctrl.session.isNew() && ctrl.data.user?.provisional && h('div.puzzle__vote__help', [ - h('p', ctrl.trans.noarg('didYouLikeThisPuzzle')), - h('p', ctrl.trans.noarg('voteToLoadNextOne')), + h('p', i18n.puzzle.didYouLikeThisPuzzle), + h('p', i18n.puzzle.voteToLoadNextOne), ]), h('div.puzzle__vote__buttons', { class: { enabled: !ctrl.voteDisabled } }, [ h('div.vote.vote-up', { hook: bind('click', () => ctrl.vote(true)) }), @@ -25,17 +25,17 @@ const renderVote = (ctrl: PuzzleCtrl): VNode => const renderContinue = (ctrl: PuzzleCtrl) => h('a.continue', { hook: bind('click', ctrl.nextPuzzle) }, [ h('i', { attrs: dataIcon(licon.PlayTriangle) }), - ctrl.trans.noarg('continueTraining'), + i18n.puzzle.continueTraining, ]); const renderStreak = (ctrl: PuzzleCtrl): MaybeVNodes => [ h('div.complete', [ h('span.game-over', 'GAME OVER'), - h('span', ctrl.trans.vdom('yourStreakX', h('strong', `${ctrl.streak?.data.index ?? 0}`))), + h('span', i18n.puzzle.yourStreakX.asArray(h('strong', `${ctrl.streak?.data.index ?? 0}`))), ]), h('a.continue', { attrs: { href: router.withLang('/streak') } }, [ h('i', { attrs: dataIcon(licon.PlayTriangle) }), - ctrl.trans('newStreak'), + i18n.puzzle.newStreak, ]), ]; @@ -47,14 +47,14 @@ export default function (ctrl: PuzzleCtrl): VNode { ctrl.streak && !win ? renderStreak(ctrl) : [ - h('div.complete', ctrl.trans.noarg(win ? 'puzzleSuccess' : 'puzzleComplete')), + h('div.complete', i18n.puzzle[win ? 'puzzleSuccess' : 'puzzleComplete']), data.user ? renderVote(ctrl) : renderContinue(ctrl), h('div.puzzle__more', [ h('a', { attrs: { 'data-icon': licon.Bullseye, href: `/analysis/${ctrl.node.fen.replace(/ /g, '_')}?color=${ctrl.pov}#practice`, - title: ctrl.trans.noarg('playWithTheMachine'), + title: i18n.site.playWithTheMachine, target: '_blank', rel: 'noopener', }, @@ -64,7 +64,7 @@ export default function (ctrl: PuzzleCtrl): VNode { h( 'a', { hook: bind('click', ctrl.nextPuzzle) }, - ctrl.trans.noarg(ctrl.streak ? 'continueTheStreak' : 'continueTraining'), + i18n.puzzle[ctrl.streak ? 'continueTheStreak' : 'continueTraining'], ), ]), ], diff --git a/ui/puzzle/src/view/boardMenu.ts b/ui/puzzle/src/view/boardMenu.ts index 25d0df2ea1496..ebdb06992856b 100644 --- a/ui/puzzle/src/view/boardMenu.ts +++ b/ui/puzzle/src/view/boardMenu.ts @@ -5,8 +5,8 @@ import { boolPrefXhrToggle } from 'common/controls'; import PuzzleCtrl from '../ctrl'; export default function (ctrl: PuzzleCtrl) { - return menuDropdown(ctrl.trans, ctrl.redraw, ctrl.menu, menu => [ - h('section', [menu.flip(ctrl.trans.noarg('flipBoard'), ctrl.flipped(), ctrl.flip)]), + return menuDropdown(ctrl.redraw, ctrl.menu, menu => [ + h('section', [menu.flip(i18n.site.flipBoard, ctrl.flipped(), ctrl.flip)]), h('section', [ menu.zenMode(true), menu.blindfold( diff --git a/ui/puzzle/src/view/feedback.ts b/ui/puzzle/src/view/feedback.ts index ef16b5b368af5..4ef4e67ee8ad9 100644 --- a/ui/puzzle/src/view/feedback.ts +++ b/ui/puzzle/src/view/feedback.ts @@ -8,16 +8,12 @@ const viewSolution = (ctrl: PuzzleCtrl): VNode => ? h('div.view_solution.skip', { class: { show: !!ctrl.streak?.data.skip } }, [ h( 'a.button.button-empty', - { hook: bind('click', ctrl.skip), attrs: { title: ctrl.trans.noarg('streakSkipExplanation') } }, - ctrl.trans.noarg('skip'), + { hook: bind('click', ctrl.skip), attrs: { title: i18n.puzzle.streakSkipExplanation } }, + i18n.storm.skip, ), ]) : h('div.view_solution', { class: { show: ctrl.canViewSolution() } }, [ - h( - 'a.button.button-empty', - { hook: bind('click', ctrl.viewSolution) }, - ctrl.trans.noarg('viewTheSolution'), - ), + h('a.button.button-empty', { hook: bind('click', ctrl.viewSolution) }, i18n.site.viewTheSolution), ]); const initial = (ctrl: PuzzleCtrl): VNode => @@ -25,11 +21,8 @@ const initial = (ctrl: PuzzleCtrl): VNode => h('div.player', [ h('div.no-square', h('piece.king.' + ctrl.pov)), h('div.instruction', [ - h('strong', ctrl.trans.noarg('yourTurn')), - h( - 'em', - ctrl.trans.noarg(ctrl.pov === 'white' ? 'findTheBestMoveForWhite' : 'findTheBestMoveForBlack'), - ), + h('strong', i18n.site.yourTurn), + h('em', i18n.puzzle[ctrl.pov === 'white' ? 'findTheBestMoveForWhite' : 'findTheBestMoveForBlack']), ]), ]), viewSolution(ctrl), @@ -39,10 +32,7 @@ const good = (ctrl: PuzzleCtrl): VNode => h('div.puzzle__feedback.good', [ h('div.player', [ h('div.icon', '✓'), - h('div.instruction', [ - h('strong', ctrl.trans.noarg('bestMove')), - h('em', ctrl.trans.noarg('keepGoing')), - ]), + h('div.instruction', [h('strong', i18n.puzzle.bestMove), h('em', i18n.puzzle.keepGoing)]), ]), viewSolution(ctrl), ]); @@ -51,10 +41,7 @@ const fail = (ctrl: PuzzleCtrl): VNode => h('div.puzzle__feedback.fail', [ h('div.player', [ h('div.icon', '✗'), - h('div.instruction', [ - h('strong', ctrl.trans.noarg('notTheMove')), - h('em', ctrl.trans.noarg('trySomethingElse')), - ]), + h('div.instruction', [h('strong', i18n.puzzle.notTheMove), h('em', i18n.puzzle.trySomethingElse)]), ]), viewSolution(ctrl), ]); diff --git a/ui/puzzle/src/view/main.ts b/ui/puzzle/src/view/main.ts index 0ecd647cd4fbf..fa70aea05f9fe 100644 --- a/ui/puzzle/src/view/main.ts +++ b/ui/puzzle/src/view/main.ts @@ -54,7 +54,7 @@ function controls(ctrl: PuzzleCtrl): VNode { jumpButton(licon.JumpPrev, 'prev', !node.ply), jumpButton(licon.JumpNext, 'next', !nextNode), jumpButton(licon.JumpLast, 'last', !nextNode, notOnLastMove), - boardMenuToggleButton(ctrl.menu, ctrl.trans.noarg('menu')), + boardMenuToggleButton(ctrl.menu, i18n.site.menu), ], ), boardMenu(ctrl), diff --git a/ui/puzzle/src/view/side.ts b/ui/puzzle/src/view/side.ts index 89ed2182b6e5c..1097e7f3072a3 100644 --- a/ui/puzzle/src/view/side.ts +++ b/ui/puzzle/src/view/side.ts @@ -30,10 +30,9 @@ const puzzleInfos = (ctrl: PuzzleCtrl, puzzle: Puzzle): VNode => h('div', [ h( 'p', - ctrl.trans.vdom( - 'puzzleId', + i18n.puzzle.puzzleId.asArray( ctrl.streak && ctrl.mode === 'play' - ? h('span.hidden', ctrl.trans.noarg('hidden')) + ? h('span.hidden', i18n.puzzle.hidden) : h( 'a', { @@ -49,14 +48,13 @@ const puzzleInfos = (ctrl: PuzzleCtrl, puzzle: Puzzle): VNode => ctrl.opts.showRatings && h( 'p', - ctrl.trans.vdom( - 'ratingX', + i18n.puzzle.ratingX.asArray( !ctrl.streak && ctrl.mode === 'play' - ? h('span.hidden', ctrl.trans.noarg('hidden')) + ? h('span.hidden', i18n.puzzle.hidden) : h('strong', `${puzzle.rating}`), ), ), - h('p', ctrl.trans.vdomPlural('playedXTimes', puzzle.plays, h('strong', numberFormat(puzzle.plays)))), + h('p', i18n.puzzle.playedXTimes.asArray(puzzle.plays, h('strong', numberFormat(puzzle.plays)))), ]), ]); @@ -66,8 +64,7 @@ function gameInfos(ctrl: PuzzleCtrl, game: PuzzleGame, puzzle: Puzzle): VNode { h('div', [ h( 'p', - ctrl.trans.vdom( - 'fromGameLink', + i18n.puzzle.fromGameLink.asArray( ctrl.mode == 'play' ? h('span', gameName) : h('a', { attrs: { href: `/${game.id}/${ctrl.pov}#${puzzle.initialPly}` } }, gameName), @@ -84,13 +81,13 @@ function gameInfos(ctrl: PuzzleCtrl, game: PuzzleGame, puzzle: Puzzle): VNode { ]); } -const renderStreak = (streak: PuzzleStreak, noarg: TransNoArg) => +const renderStreak = (streak: PuzzleStreak) => h( 'div.puzzle__side__streak', streak.data.index == 0 ? h('div.puzzle__side__streak__info', [ h('h1.text', { attrs: dataIcon(licon.ArrowThruApple) }, 'Puzzle Streak'), - h('p', noarg('streakDescription')), + h('p', i18n.puzzle.streakDescription), ]) : h( 'div.puzzle__side__streak__score.text', @@ -100,12 +97,11 @@ const renderStreak = (streak: PuzzleStreak, noarg: TransNoArg) => ); export const userBox = (ctrl: PuzzleCtrl): VNode => { - const data = ctrl.data, - noarg = ctrl.trans.noarg; + const data = ctrl.data; if (!data.user) return h('div.puzzle__side__user', [ - h('p', noarg('toGetPersonalizedPuzzles')), - h('a.button', { attrs: { href: router.withLang('/signup') } }, noarg('signUp')), + h('p', i18n.puzzle.toGetPersonalizedPuzzles), + h('a.button', { attrs: { href: router.withLang('/signup') } }, i18n.site.signUp), ]); const diff = ctrl.round?.ratingDiff, ratedId = 'puzzle-toggle-rated'; @@ -123,7 +119,7 @@ export const userBox = (ctrl: PuzzleCtrl): VNode => { }), h('label', { attrs: { for: ratedId } }), ]), - h('label', { attrs: { for: ratedId } }, noarg('rated')), + h('label', { attrs: { for: ratedId } }, i18n.site.rated), ]), h( 'div.puzzle__side__user__rating', @@ -134,13 +130,12 @@ export const userBox = (ctrl: PuzzleCtrl): VNode => { ...(diff && diff > 0 ? [' ', h('good.rp', '+' + diff)] : []), ...(diff && diff < 0 ? [' ', h('bad.rp', '−' + -diff)] : []), ]) - : h('p.puzzle__side__user__rating__casual', noarg('yourPuzzleRatingWillNotChange')), + : h('p.puzzle__side__user__rating__casual', i18n.puzzle.yourPuzzleRatingWillNotChange), ), ]); }; -export const streakBox = (ctrl: PuzzleCtrl) => - h('div.puzzle__side__user', renderStreak(ctrl.streak!, ctrl.trans.noarg)); +export const streakBox = (ctrl: PuzzleCtrl) => h('div.puzzle__side__user', renderStreak(ctrl.streak!)); const difficulties: [PuzzleDifficulty, number][] = [ ['easiest', -600], @@ -153,17 +148,15 @@ const colors = [ ['black', 'asBlack'], ['random', 'randomColor'], ['white', 'asWhite'], -]; +] as const; export function replay(ctrl: PuzzleCtrl): MaybeVNode { const replay = ctrl.data.replay; if (!replay) return; const i = replay.i + (ctrl.mode == 'play' ? 0 : 1); + const text = i18n.puzzleTheme[ctrl.data.angle.key]; return h('div.puzzle__side__replay', [ - h('a', { attrs: { href: `/training/dashboard/${replay.days}` } }, [ - '« ', - `Replaying ${ctrl.trans.noarg(ctrl.data.angle.key)} puzzles`, - ]), + h('a', { attrs: { href: `/training/dashboard/${replay.days}` } }, ['« ', `Replaying ${text} puzzles`]), h('div.puzzle__side__replay__bar', { attrs: { style: `---p:${replay.of ? Math.round((100 * i) / replay.of) : 1}%`, @@ -175,7 +168,6 @@ export function replay(ctrl: PuzzleCtrl): MaybeVNode { export function config(ctrl: PuzzleCtrl): MaybeVNode { const autoNextId = 'puzzle-toggle-autonext', - noarg = ctrl.trans.noarg, data = ctrl.data; return h('div.puzzle__side__config', [ h('div.puzzle__side__config__toggle', [ @@ -194,7 +186,7 @@ export function config(ctrl: PuzzleCtrl): MaybeVNode { }), h('label', { attrs: { for: autoNextId } }), ]), - h('label', { attrs: { for: autoNextId } }, noarg('jumpToNextPuzzleImmediately')), + h('label', { attrs: { for: autoNextId } }, i18n.puzzle.jumpToNextPuzzleImmediately), ]), !data.user || data.replay || ctrl.streak ? null : renderDifficultyForm(ctrl), ]); @@ -205,7 +197,7 @@ export const renderDifficultyForm = (ctrl: PuzzleCtrl): VNode => 'form.puzzle__side__config__difficulty', { attrs: { action: `/training/difficulty/${ctrl.data.angle.key}`, method: 'post' } }, [ - h('label', { attrs: { for: 'puzzle-difficulty' } }, ctrl.trans.noarg('difficultyLevel')), + h('label', { attrs: { for: 'puzzle-difficulty' } }, i18n.puzzle.difficultyLevel), h( 'select#puzzle-difficulty.puzzle__difficulty__selector', { @@ -222,14 +214,12 @@ export const renderDifficultyForm = (ctrl: PuzzleCtrl): VNode => value: key, selected: key == ctrl.opts.settings.difficulty, title: - !!delta && - ctrl.trans.pluralSame( - delta < 0 ? 'nbPointsBelowYourPuzzleRating' : 'nbPointsAboveYourPuzzleRating', - Math.abs(delta), - ), + !!delta && delta < 0 + ? i18n.puzzle.nbPointsBelowYourPuzzleRating(Math.abs(delta)) + : i18n.puzzle.nbPointsAboveYourPuzzleRating(Math.abs(delta)), }, }, - [ctrl.trans.noarg(key), delta ? ` (${delta > 0 ? '+' : ''}${delta})` : ''], + [i18n.puzzle[key], delta ? ` (${delta > 0 ? '+' : ''}${delta})` : ''], ), ), ), @@ -241,12 +231,12 @@ export const renderColorForm = (ctrl: PuzzleCtrl): VNode => 'div.puzzle__side__config__color', h( 'group.radio', - colors.map(([key, i18n]) => + colors.map(([key, i18nKey]) => h('div', [ h( `a.label.color-${key}${key === (ctrl.opts.settings.color || 'random') ? '.active' : ''}`, { - attrs: { href: `/training/${ctrl.data.angle.key}/${key}`, title: ctrl.trans.noarg(i18n) }, + attrs: { href: `/training/${ctrl.data.angle.key}/${key}`, title: i18n.site[i18nKey] }, }, h('i'), ), diff --git a/ui/puzzle/src/view/theme.ts b/ui/puzzle/src/view/theme.ts index b0b1e4d5e14bf..7d81f27ba870a 100644 --- a/ui/puzzle/src/view/theme.ts +++ b/ui/puzzle/src/view/theme.ts @@ -2,6 +2,7 @@ import * as licon from 'common/licon'; import * as router from 'common/router'; import { MaybeVNode, bind, dataIcon, looseH as h } from 'common/snabbdom'; import { VNode } from 'snabbdom'; +import { ThemeKey, RoundThemes } from '../interfaces'; import { renderColorForm } from './side'; import PuzzleCtrl from '../ctrl'; @@ -17,10 +18,7 @@ export default function theme(ctrl: PuzzleCtrl): MaybeVNode { return ctrl.streak ? null : ctrl.isDaily - ? h( - 'div.puzzle__side__theme.puzzle__side__theme--daily', - puzzleMenu(h('h2', ctrl.trans.noarg('dailyPuzzle'))), - ) + ? h('div.puzzle__side__theme.puzzle__side__theme--daily', puzzleMenu(h('h2', i18n.puzzle.dailyPuzzle))) : h('div.puzzle__side__theme', [ puzzleMenu(h('h2', { class: { long: angle.name.length > 20 } }, ['« ', angle.name])), angle.opening @@ -34,7 +32,7 @@ export default function theme(ctrl: PuzzleCtrl): MaybeVNode { h( 'a.puzzle__side__theme__chapter.text', { attrs: { href: `${studyUrl}/${angle.chapter}`, target: '_blank', rel: 'noopener' } }, - [' ', ctrl.trans.noarg('example')], + [' ', i18n.puzzle.example], ), ]), showEditor @@ -50,14 +48,16 @@ const invisibleThemes = new Set(['master', 'masterVsMaster', 'superGM']); const editor = (ctrl: PuzzleCtrl): VNode[] => { const data = ctrl.data, - trans = ctrl.trans.noarg, - votedThemes = ctrl.round?.themes || {}; - const visibleThemes: string[] = data.puzzle.themes - .filter(t => !invisibleThemes.has(t)) - .concat(Object.keys(votedThemes).filter(t => votedThemes[t] && !data.puzzle.themes.includes(t))) - .sort(); + votedThemes = ctrl.round?.themes || ({} as RoundThemes); + const trans = i18n.puzzleTheme as any; + const visibleThemes: ThemeKey[] = [ + ...data.puzzle.themes.filter(t => !invisibleThemes.has(t)), + ...Object.keys(votedThemes).filter( + (t: ThemeKey): t is ThemeKey => votedThemes[t] && !data.puzzle.themes.includes(t), + ), + ].sort(); const allThemes = location.pathname == '/training/daily' ? null : ctrl.allThemes; - const availableThemes = allThemes ? allThemes.dynamic.filter(t => !votedThemes[t]) : null; + const availableThemes = allThemes ? allThemes.dynamic.filter((t: ThemeKey) => !votedThemes[t]) : null; if (availableThemes) availableThemes.sort((a, b) => (trans(a) < trans(b) ? -1 : 1)); return [ h( @@ -65,7 +65,7 @@ const editor = (ctrl: PuzzleCtrl): VNode[] => { { hook: bind('click', e => { const target = e.target as HTMLElement; - const theme = target.getAttribute('data-theme'); + const theme = target.getAttribute('data-theme') as ThemeKey; if (theme) ctrl.voteTheme(theme, target.classList.contains('vote-up')); }), }, @@ -98,7 +98,7 @@ const editor = (ctrl: PuzzleCtrl): VNode[] => { { hook: { ...bind('change', e => { - const theme = (e.target as HTMLInputElement).value; + const theme = (e.target as HTMLInputElement).value as ThemeKey; if (theme) ctrl.voteTheme(theme, true); }), postpatch(_, vnode) { @@ -107,7 +107,7 @@ const editor = (ctrl: PuzzleCtrl): VNode[] => { }, }, [ - h('option', { attrs: { value: '', selected: true } }, trans('addAnotherTheme')), + h('option', { attrs: { value: '', selected: true } }, i18n.puzzle.addAnotherTheme), ...availableThemes.map(theme => h('option', { attrs: { value: theme, title: trans(`${theme}Description`) } }, trans(theme)), ), diff --git a/ui/puzzle/src/view/tree.ts b/ui/puzzle/src/view/tree.ts index 8bb781e0c26dd..ee1c2d1b1fdb9 100644 --- a/ui/puzzle/src/view/tree.ts +++ b/ui/puzzle/src/view/tree.ts @@ -97,31 +97,34 @@ function renderMainlineMoveOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): VNod hist: node.ply < ctx.ctrl.initialNode.ply, }; if (node.puzzle) classes[node.puzzle] = true; - return h('move', { attrs: { p: path }, class: classes }, renderMove(ctx, node)); + return h('move', { attrs: { p: path }, class: classes }, renderMove(node)); } const renderGlyph = (glyph: Glyph): VNode => h('glyph', { attrs: { title: glyph.name } }, glyph.symbol); -function puzzleGlyph(ctx: Ctx, node: Tree.Node): MaybeVNode { +function puzzleGlyph(node: Tree.Node): MaybeVNode { switch (node.puzzle) { case 'good': case 'win': - return renderGlyph({ name: ctx.ctrl.trans.noarg('bestMove'), symbol: '✓' }); + return renderGlyph({ name: i18n.puzzle.bestMove, symbol: '✓' }); case 'fail': - return renderGlyph({ name: ctx.ctrl.trans.noarg('puzzleFailed'), symbol: '✗' }); + return renderGlyph({ + name: 'Puzzle failed', //puzzleFailed key never worked, it's in learn/*.xml + symbol: '✗', + }); case 'retry': - return renderGlyph({ name: ctx.ctrl.trans.noarg('goodMove'), symbol: '?!' }); + return renderGlyph({ name: i18n.puzzle.goodMove, symbol: '?!' }); default: return; } } -function renderMove(ctx: Ctx, node: Tree.Node): LooseVNodes { +function renderMove(node: Tree.Node): LooseVNodes { const ev = node.eval || node.ceval; return [ node.san, ev && (defined(ev.cp) ? renderEval(normalizeEval(ev.cp)) : defined(ev.mate) && renderEval('#' + ev.mate)), - puzzleGlyph(ctx, node), + puzzleGlyph(node), ]; } @@ -134,7 +137,7 @@ function renderVariationMoveOf(ctx: Ctx, node: Tree.Node, opts: RenderOpts): VNo return h('move', { attrs: { p: path }, class: classes }, [ withIndex && renderIndex(node.ply, true), node.san, - puzzleGlyph(ctx, node), + puzzleGlyph(node), ]); } diff --git a/ui/racer/src/ctrl.ts b/ui/racer/src/ctrl.ts index 47d909ad30cae..cff3f5f9830dc 100644 --- a/ui/racer/src/ctrl.ts +++ b/ui/racer/src/ctrl.ts @@ -27,7 +27,6 @@ import { Role } from 'chessground/types'; import { storedBooleanProp } from 'common/storage'; import { PromotionCtrl } from 'chess/promotion'; import StrongSocket from 'common/socket'; -import { trans } from 'common/i18n'; import { pubsub } from 'common/pubsub'; export default class RacerCtrl implements PuzCtrl { @@ -39,7 +38,6 @@ export default class RacerCtrl implements PuzCtrl { run: Run; vm: RacerVm; filters: PuzFilters; - trans: Trans; promotion: PromotionCtrl; countdown: Countdown; boost: Boost = new Boost(); @@ -57,7 +55,6 @@ export default class RacerCtrl implements PuzCtrl { this.race = this.data.race; this.pref = opts.pref; this.filters = new PuzFilters(redraw, true); - this.trans = trans(opts.i18n); this.run = { pov: puzzlePov(this.data.puzzles[0]), moves: 0, diff --git a/ui/racer/src/interfaces.ts b/ui/racer/src/interfaces.ts index 8af4f4094e4eb..c38971785e187 100644 --- a/ui/racer/src/interfaces.ts +++ b/ui/racer/src/interfaces.ts @@ -10,7 +10,6 @@ export type PlayerId = string; export interface RacerOpts { data: RacerData; pref: RacerPrefs; - i18n: I18nDict; } export interface RacerPrefs extends PuzPrefs {} diff --git a/ui/racer/src/view/main.ts b/ui/racer/src/view/main.ts index 54c9e97e9ed09..3747dd27e383f 100644 --- a/ui/racer/src/view/main.ts +++ b/ui/racer/src/view/main.ts @@ -25,20 +25,19 @@ export default function (ctrl: RacerCtrl): VNode { } const selectScreen = (ctrl: RacerCtrl): MaybeVNodes => { - const noarg = ctrl.trans.noarg; switch (ctrl.status()) { case 'pre': { - const povMsg = h('p.racer__pre__message__pov', ctrl.trans(povMessage(ctrl.run))); + const povMsg = h('p.racer__pre__message__pov', povMessage(ctrl.run)); return ctrl.race.lobby ? [ - waitingToStart(noarg), + waitingToStart(), h('div.racer__pre__message.racer__pre__message--with-skip', [ h('div.racer__pre__message__text', [ h( 'p', ctrl.knowsSkip() - ? noarg(ctrl.vm.startsAt ? 'getReady' : 'waitingForMorePlayers') - : skipHelp(noarg), + ? i18n.storm[ctrl.vm.startsAt ? 'getReady' : 'waitingForMorePlayers'] + : skipHelp(), ), povMsg, ]), @@ -47,7 +46,7 @@ const selectScreen = (ctrl: RacerCtrl): MaybeVNodes => { comboZone(ctrl), ] : [ - waitingToStart(noarg), + waitingToStart(), h('div.racer__pre__message', [ ...(ctrl.raceFull() ? ctrl.isPlayer() @@ -66,20 +65,20 @@ const selectScreen = (ctrl: RacerCtrl): MaybeVNodes => { return ctrl.isPlayer() ? [playerScore(ctrl), h('div.puz-clock', [clock, renderSkip(ctrl)]), comboZone(ctrl)] : [ - spectating(noarg), + spectating(), h('div.racer__spectating', [ h('div.puz-clock', clock), - ctrl.race.lobby ? lobbyNext(ctrl) : waitForRematch(noarg), + ctrl.race.lobby ? lobbyNext(ctrl) : waitForRematch(), ]), comboZone(ctrl), ]; } case 'post': { const nextRace = ctrl.race.lobby ? lobbyNext(ctrl) : friendNext(ctrl); - const raceComplete = h('h2', noarg('raceComplete')); + const raceComplete = h('h2', i18n.storm.raceComplete); return ctrl.isPlayer() ? [playerScore(ctrl), h('div.racer__post', [raceComplete, yourRank(ctrl), nextRace]), comboZone(ctrl)] - : [spectating(noarg), h('div.racer__post', [raceComplete, nextRace]), comboZone(ctrl)]; + : [spectating(), h('div.racer__post', [raceComplete, nextRace]), comboZone(ctrl)]; } } }; @@ -89,26 +88,26 @@ const renderSkip = (ctrl: RacerCtrl) => 'button.racer__skip.button.button-red', { class: { disabled: !ctrl.canSkip() }, - attrs: { title: ctrl.trans.noarg('skipExplanation') }, + attrs: { title: i18n.storm.skipExplanation }, hook: bind('click', ctrl.skip), }, - ctrl.trans.noarg('skip'), + i18n.storm.skip, ); -const skipHelp = (noarg: TransNoArg) => h('p', noarg('skipHelp')); +const skipHelp = () => h('p', i18n.storm.skipHelp); const puzzleRacer = () => h('strong', 'Puzzle Racer'); -const waitingToStart = (noarg: TransNoArg) => +const waitingToStart = () => h( 'div.puz-side__top.puz-side__start', - h('div.puz-side__start__text', [puzzleRacer(), h('span', noarg('waitingToStart'))]), + h('div.puz-side__start__text', [puzzleRacer(), h('span', i18n.storm.waitingToStart)]), ); -const spectating = (noarg: TransNoArg) => +const spectating = () => h( 'div.puz-side__top.puz-side__start', - h('div.puz-side__start__text', [puzzleRacer(), h('span', noarg('spectating'))]), + h('div.puz-side__start__text', [puzzleRacer(), h('span', i18n.storm.spectating)]), ); const renderBonus = (bonus: number) => `+${bonus}`; @@ -118,7 +117,7 @@ const renderControls = (ctrl: RacerCtrl): VNode => 'div.puz-side__control', h('a.puz-side__control__flip.button', { class: { active: ctrl.flipped, 'button-empty': !ctrl.flipped }, - attrs: { 'data-icon': licon.ChasingArrows, title: ctrl.trans.noarg('flipBoard') + ' (Keyboard: f)' }, + attrs: { 'data-icon': licon.ChasingArrows, title: i18n.site.flipBoard + ' (Keyboard: f)' }, hook: bind('click', ctrl.flip), }), ); @@ -131,7 +130,7 @@ const playerScore = (ctrl: RacerCtrl): VNode => const renderLink = (ctrl: RacerCtrl) => h('div.puz-side__link', [ - h('p', ctrl.trans.noarg('toInviteSomeoneToPlayGiveThisUrl')), + h('p', i18n.site.toInviteSomeoneToPlayGiveThisUrl), copyMeInput(`${window.location.protocol}//${window.location.host}/racer/${ctrl.race.id}`), ]); @@ -147,14 +146,14 @@ const renderStart = (ctrl: RacerCtrl) => hook: bind('click', ctrl.start), attrs: { disabled: ctrl.players().length < 2 }, }, - ctrl.trans.noarg('startTheRace'), + i18n.storm.startTheRace, ), ); const renderJoin = (ctrl: RacerCtrl) => h( 'div.puz-side__join', - h('button.button.button-fat', { hook: bind('click', ctrl.join) }, ctrl.trans.noarg('joinTheRace')), + h('button.button.button-fat', { hook: bind('click', ctrl.join) }, i18n.storm.joinTheRace), ); const yourRank = (ctrl: RacerCtrl) => { @@ -162,21 +161,21 @@ const yourRank = (ctrl: RacerCtrl) => { if (!score) return; const players = ctrl.players(); const rank = players.filter(p => p.score > score).length + 1; - return h('strong.race__post__rank', ctrl.trans('yourRankX', `${rank}/${players.length}`)); + return h('strong.race__post__rank', i18n.storm.yourRankX(`${rank}/${players.length}`)); }; -const waitForRematch = (noarg: TransNoArg) => +const waitForRematch = () => h( `a.racer__new-race.button.button-fat.button-navaway.disabled`, { attrs: { disabled: true } }, - noarg('waitForRematch'), + i18n.storm.waitForRematch, ); const lobbyNext = (ctrl: RacerCtrl) => h('form', { attrs: { action: '/racer/lobby', method: 'post' } }, [ h( `button.racer__new-race.button.button-navaway${ctrl.race.lobby ? '.button-fat' : '.button-empty'}`, - ctrl.trans.noarg('nextRace'), + i18n.storm.nextRace, ), ]); @@ -185,7 +184,7 @@ const friendNext = (ctrl: RacerCtrl) => h( `a.racer__rematch.button.button-fat.button-navaway`, { attrs: { href: `/racer/${ctrl.race.id}/rematch` } }, - ctrl.trans.noarg('joinRematch'), + i18n.storm.joinRematch, ), h( 'form.racer__post__next__new', @@ -193,7 +192,7 @@ const friendNext = (ctrl: RacerCtrl) => h( 'button.racer__post__next__button.button.button-empty', { attrs: { type: 'submit' } }, - ctrl.trans.noarg('createNewGame'), + i18n.storm.createNewGame, ), ), ]); diff --git a/ui/round/src/corresClock/corresClockView.ts b/ui/round/src/corresClock/corresClockView.ts index 0ce96d3fa0c7a..8c393921361ea 100644 --- a/ui/round/src/corresClock/corresClockView.ts +++ b/ui/round/src/corresClock/corresClockView.ts @@ -8,7 +8,7 @@ const prefixInteger = (num: number, length: number): string => const bold = (x: string) => `${x}`; -function formatClockTime(trans: Trans, time: Millis) { +function formatClockTime(time: Millis) { const date = new Date(time), minutes = prefixInteger(date.getUTCMinutes(), 2), seconds = prefixInteger(date.getSeconds(), 2); @@ -18,8 +18,8 @@ function formatClockTime(trans: Trans, time: Millis) { // days : hours const days = date.getUTCDate() - 1; hours = date.getUTCHours(); - str += (days === 1 ? trans('oneDay') : trans.pluralSame('nbDays', days)) + ' '; - if (hours !== 0) str += trans.pluralSame('nbHours', hours); + str += (days === 1 ? i18n.site.oneDay : i18n.site.nbDays(days)) + ' '; + if (hours !== 0) str += i18n.site.nbHours(hours); } else if (time >= 3600 * 1000) { // hours : minutes hours = date.getUTCHours(); @@ -33,14 +33,13 @@ function formatClockTime(trans: Trans, time: Millis) { export default function ( ctrl: CorresClockController, - trans: Trans, color: Color, position: Position, runningColor: Color, ): VNode { const millis = ctrl.millisOf(color), update = (el: HTMLElement) => { - el.innerHTML = formatClockTime(trans, millis); + el.innerHTML = formatClockTime(millis); }, isPlayer = ctrl.root.data.player.color === color, direction = document.dir == 'rtl' && millis < 86400 * 1000 ? 'ltr' : undefined; diff --git a/ui/round/src/ctrl.ts b/ui/round/src/ctrl.ts index d4487843f988f..788c952ffe4fe 100644 --- a/ui/round/src/ctrl.ts +++ b/ui/round/src/ctrl.ts @@ -49,7 +49,6 @@ import { import { defined, Toggle, toggle, requestIdleCallback } from 'common'; import { Redraw } from 'common/snabbdom'; import { storage, once, type LichessBooleanStorage } from 'common/storage'; -import { trans } from 'common/i18n'; import { pubsub } from 'common/pubsub'; interface GoneBerserk { @@ -65,8 +64,6 @@ export default class RoundController implements MoveRootCtrl { chessground: CgApi; clock?: ClockController; corresClock?: CorresClockController; - trans: Trans; - noarg: TransNoArg; keyboardMove?: KeyboardMove; voiceMove?: VoiceMove; moveOn: MoveOn; @@ -133,9 +130,6 @@ export default class RoundController implements MoveRootCtrl { this.menu = toggle(false, redraw); - this.trans = trans(opts.i18n); - this.noarg = this.trans.noarg; - setTimeout(this.delayedInit, 200); setTimeout(this.showExpiration, 350); @@ -378,11 +372,11 @@ export default class RoundController implements MoveRootCtrl { showYourMoveNotification = (): void => { if (this.opts.local) return; const d = this.data; - const opponent = $('body').hasClass('zen') ? 'Your opponent' : renderUser.userTxt(this, d.opponent); + const opponent = $('body').hasClass('zen') ? 'Your opponent' : renderUser.userTxt(d.opponent); const joined = `${opponent}\njoined the game.`; if (game.isPlayerTurn(d)) notify(() => { - let txt = this.noarg('yourTurn'); + let txt = i18n.site.yourTurn; if (this.ply < 1) txt = `${joined}\n${txt}`; else { let move = d.steps[d.steps.length - 1].san; @@ -635,26 +629,26 @@ export default class RoundController implements MoveRootCtrl { if (this.moveToSubmit || this.dropToSubmit) { setTimeout(() => this.voiceMove?.listenForResponse('submitMove', this.submitMove)); return { - prompt: this.noarg('confirmMove'), + prompt: i18n.site.confirmMove, yes: { action: () => this.submitMove(true) }, - no: { action: () => this.submitMove(false), key: 'cancel' }, + no: { action: () => this.submitMove(false), text: i18n.site.cancel }, }; } else if (this.data.player.proposingTakeback) { this.voiceMove?.listenForResponse('cancelTakeback', this.cancelTakebackPreventDraws); return { - prompt: this.noarg('takebackPropositionSent'), - no: { action: this.cancelTakebackPreventDraws, key: 'cancel' }, + prompt: i18n.site.takebackPropositionSent, + no: { action: this.cancelTakebackPreventDraws, text: i18n.site.cancel }, }; - } else if (this.data.player.offeringDraw) return { prompt: this.noarg('drawOfferSent') }; + } else if (this.data.player.offeringDraw) return { prompt: i18n.site.drawOfferSent }; else if (this.data.opponent.offeringDraw) return { - prompt: this.noarg('yourOpponentOffersADraw'), + prompt: i18n.site.yourOpponentOffersADraw, yes: { action: () => this.socket.send('draw-yes'), icon: licon.OneHalf }, no: { action: () => this.socket.send('draw-no') }, }; else if (this.data.opponent.proposingTakeback) return { - prompt: this.noarg('yourOpponentProposesATakeback'), + prompt: i18n.site.yourOpponentProposesATakeback, yes: { action: this.takebackYes, icon: licon.Back }, no: { action: () => this.socket.send('takeback-no') }, }; @@ -662,11 +656,11 @@ export default class RoundController implements MoveRootCtrl { else return false; }; - opponentRequest(req: string, i18nKey: string): void { + opponentRequest(req: string, text: string): void { this.voiceMove?.listenForResponse(req, (v: boolean) => this.socket.sendLoading(`${req}-${v ? 'yes' : 'no'}`), ); - notify(this.noarg(i18nKey)); + notify(text); } takebackYes = (): void => { diff --git a/ui/round/src/interfaces.ts b/ui/round/src/interfaces.ts index 2ffee7304ff3e..7643f4abe03da 100644 --- a/ui/round/src/interfaces.ts +++ b/ui/round/src/interfaces.ts @@ -99,7 +99,6 @@ export interface RoundOpts { onChange(d: RoundData): void; element?: HTMLElement; crosstableEl?: HTMLElement; - i18n: I18nDict; chat?: ChatOpts; local?: RoundProxy; } diff --git a/ui/round/src/plugins/round.nvui.ts b/ui/round/src/plugins/round.nvui.ts index 6be59fef40017..3ccf7186448c3 100644 --- a/ui/round/src/plugins/round.nvui.ts +++ b/ui/round/src/plugins/round.nvui.ts @@ -346,8 +346,7 @@ function anyClock(ctrl: RoundController, position: Position) { player = ctrl.playerAt(position); return ( (ctrl.clock && renderClock(ctrl, player, position)) || - (d.correspondence && - renderCorresClock(ctrl.corresClock!, ctrl.trans, player.color, position, d.game.player)) || + (d.correspondence && renderCorresClock(ctrl.corresClock!, player.color, position, d.game.player)) || undefined ); } @@ -364,12 +363,8 @@ function renderMoves(steps: Step[], style: Style) { return res; } -function renderAi(ctrl: RoundController, level: number): string { - return ctrl.trans('aiNameLevelAiLevel', 'Stockfish', level); -} - function playerHtml(ctrl: RoundController, player: game.Player) { - if (player.ai) return renderAi(ctrl, player.ai); + if (player.ai) return i18n.site.aiNameLevelAiLevel('Stockfish', player.ai); const d = ctrl.data, user = player.user, perf = user ? user.perfs[d.game.perf] : null, @@ -390,7 +385,7 @@ function playerHtml(ctrl: RoundController, player: game.Player) { } function playerText(ctrl: RoundController, player: game.Player) { - if (player.ai) return renderAi(ctrl, player.ai); + if (player.ai) return i18n.site.aiNameLevelAiLevel('Stockfish', player.ai); const d = ctrl.data, user = player.user, perf = user ? user.perfs[d.game.perf] : null, diff --git a/ui/round/src/round.ts b/ui/round/src/round.ts index 83a49fe1aa823..6ac24885fc693 100644 --- a/ui/round/src/round.ts +++ b/ui/round/src/round.ts @@ -107,7 +107,7 @@ async function boot( const chatOpts = opts.chat; if (chatOpts) { if (data.tournament?.top) { - chatOpts.plugin = tourStandingCtrl(data.tournament.top, data.tournament.team, opts.i18n.standing); + chatOpts.plugin = tourStandingCtrl(data.tournament.top, data.tournament.team, i18n.site.standing); chatOpts.alwaysEnabled = true; } else if (!data.simul && !data.swiss) { chatOpts.preset = getPresetGroup(data); diff --git a/ui/round/src/socket.ts b/ui/round/src/socket.ts index c38443af3197b..517dec6feacb9 100644 --- a/ui/round/src/socket.ts +++ b/ui/round/src/socket.ts @@ -70,7 +70,7 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket { takebackOffers(o: { white?: boolean; black?: boolean }) { ctrl.data.player.proposingTakeback = o[ctrl.data.player.color]; const fromOp = (ctrl.data.opponent.proposingTakeback = o[ctrl.data.opponent.color]); - if (fromOp) ctrl.opponentRequest('takeback', 'yourOpponentProposesATakeback'); + if (fromOp) ctrl.opponentRequest('takeback', i18n.site.yourOpponentProposesATakeback); ctrl.redraw(); }, move: ctrl.apiMove, @@ -101,7 +101,7 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket { rematchOffer(by: Color) { ctrl.data.player.offeringRematch = by === ctrl.data.player.color; if ((ctrl.data.opponent.offeringRematch = by === ctrl.data.opponent.color)) - ctrl.opponentRequest('rematch', 'yourOpponentWantsToPlayANewGameWithYou'); + ctrl.opponentRequest('rematch', i18n.site.yourOpponentWantsToPlayANewGameWithYou); ctrl.redraw(); }, rematchTaken(nextId: string) { @@ -113,7 +113,7 @@ export function make(send: SocketSend, ctrl: RoundController): RoundSocket { if (ctrl.isPlaying()) { ctrl.data.player.offeringDraw = by === ctrl.data.player.color; const fromOp = (ctrl.data.opponent.offeringDraw = by === ctrl.data.opponent.color); - if (fromOp) ctrl.opponentRequest('draw', 'yourOpponentOffersADraw'); + if (fromOp) ctrl.opponentRequest('draw', i18n.site.yourOpponentOffersADraw); } if (by) { let ply = ctrl.lastPly(); diff --git a/ui/round/src/title.ts b/ui/round/src/title.ts index 10912efae8575..74c34aa5a7c01 100644 --- a/ui/round/src/title.ts +++ b/ui/round/src/title.ts @@ -38,12 +38,12 @@ export function set(ctrl: RoundController, text?: string): void { if (ctrl.data.player.spectator) return; if (!text) { if (aborted(ctrl.data) || finished(ctrl.data)) { - text = ctrl.noarg('gameOver'); + text = i18n.site.gameOver; } else if (isPlayerTurn(ctrl.data)) { - text = ctrl.noarg('yourTurn'); + text = i18n.site.yourTurn; if (!document.hasFocus()) startTicker(); } else { - text = ctrl.noarg('waitingForOpponent'); + text = i18n.site.waitingForOpponent; resetTicker(); } } diff --git a/ui/round/src/view/boardMenu.ts b/ui/round/src/view/boardMenu.ts index b7d5f207ab02e..33a76f8b37239 100644 --- a/ui/round/src/view/boardMenu.ts +++ b/ui/round/src/view/boardMenu.ts @@ -6,11 +6,11 @@ import { toggle } from 'common'; import { boolPrefXhrToggle } from 'common/controls'; export default function (ctrl: RoundController): LooseVNode { - return menuDropdown(ctrl.trans, ctrl.redraw, ctrl.menu, menu => { + return menuDropdown(ctrl.redraw, ctrl.menu, menu => { const d = ctrl.data, spectator = d.player.spectator; return [ - h('section', [menu.flip(ctrl.noarg('flipBoard'), ctrl.flip, ctrl.flipNow)]), + h('section', [menu.flip(i18n.site.flipBoard, ctrl.flip, ctrl.flipNow)]), h('section', [ menu.zenMode(true), menu.blindfold( diff --git a/ui/round/src/view/button.ts b/ui/round/src/view/button.ts index 2f9ee530f499d..c6ce16d638bdd 100644 --- a/ui/round/src/view/button.ts +++ b/ui/round/src/view/button.ts @@ -38,7 +38,7 @@ function analysisButton(ctrl: RoundController): VNode | false { if (location.pathname === url.split('#')[0]) location.reload(); }), }, - ctrl.noarg('analysis'), + i18n.site.analysis, ) ); } @@ -47,25 +47,28 @@ function rematchButtons(ctrl: RoundController): LooseVNodes { const d = ctrl.data, me = !!d.player.offeringRematch, disabled = !me && !d.opponent.onGame && (!!d.clock || !d.player.user || !d.opponent.user), - them = !!d.opponent.offeringRematch && !disabled, - noarg = ctrl.noarg; + them = !!d.opponent.offeringRematch && !disabled; if (!game.rematchable(d)) return []; return [ them && h( 'button.rematch-decline', { - attrs: { 'data-icon': licon.X, title: noarg('decline') }, + attrs: { 'data-icon': licon.X, title: i18n.site.decline }, hook: util.bind('click', () => ctrl.socket.send('rematch-no')), }, - ctrl.nvui ? noarg('decline') : '', + ctrl.nvui ? i18n.site.decline : '', ), h( 'button.fbt.rematch.white', { class: { me, glowing: them, disabled }, attrs: { - title: them ? noarg('yourOpponentWantsToPlayANewGameWithYou') : me ? noarg('rematchOfferSent') : '', + title: them + ? i18n.site.yourOpponentWantsToPlayANewGameWithYou + : me + ? i18n.site.rematchOfferSent + : '', }, hook: util.bind( 'click', @@ -84,7 +87,7 @@ function rematchButtons(ctrl: RoundController): LooseVNodes { ctrl.redraw, ), }, - [me ? spinner() : h('span', noarg('rematch'))], + [me ? spinner() : h('span', i18n.site.rematch)], ), ]; } @@ -103,12 +106,12 @@ export function standard( return h( 'button.fbt.' + socketMsg, { - attrs: { disabled: !enabled(), title: ctrl.noarg(hintFn()) }, + attrs: { disabled: !enabled(), title: hintFn() }, hook: util.bind('click', () => { if (enabled()) onclick ? onclick() : ctrl.socket.sendLoading(socketMsg); }), }, - [h('span', ctrl.nvui ? [ctrl.noarg(hintFn())] : util.justIcon(icon))], + [h('span', ctrl.nvui ? [hintFn()] : util.justIcon(icon))], ); } @@ -117,47 +120,44 @@ export function opponentGone(ctrl: RoundController): LooseVNode { if (ctrl.data.game.rules?.includes('noClaimWin')) return null; return gone === true ? h('div.suggestion', [ - h('p', { hook: onSuggestionHook }, ctrl.noarg('opponentLeftChoices')), + h('p', { hook: onSuggestionHook }, i18n.site.opponentLeftChoices), h( 'button.button', { hook: util.bind('click', () => ctrl.socket.sendLoading('resign-force')) }, - ctrl.noarg('forceResignation'), + i18n.site.forceResignation, ), h( 'button.button', { hook: util.bind('click', () => ctrl.socket.sendLoading('draw-force')) }, - ctrl.noarg('forceDraw'), + i18n.site.forceDraw, ), ]) : gone !== false && - h( - 'div.suggestion', - h('p', ctrl.trans.vdomPlural('opponentLeftCounter', gone, h('strong', '' + gone))), - ); + h('div.suggestion', h('p', i18n.site.opponentLeftCounter.asArray(gone, h('strong', '' + gone)))); } -const fbtCancel = (ctrl: RoundController, f: (v: boolean) => void) => +const fbtCancel = (f: (v: boolean) => void) => h('button.fbt.no', { - attrs: { title: ctrl.noarg('cancel'), 'data-icon': licon.X }, + attrs: { title: i18n.site.cancel, 'data-icon': licon.X }, hook: util.bind('click', () => f(false)), }); export const resignConfirm = (ctrl: RoundController): VNode => h('div.act-confirm', [ h('button.fbt.yes', { - attrs: { title: ctrl.noarg('resign'), 'data-icon': licon.FlagOutline }, + attrs: { title: i18n.site.resign, 'data-icon': licon.FlagOutline }, hook: util.bind('click', () => ctrl.resign(true)), }), - fbtCancel(ctrl, ctrl.resign), + fbtCancel(ctrl.resign), ]); export const drawConfirm = (ctrl: RoundController): VNode => h('div.act-confirm', [ h('button.fbt.yes.draw-yes', { - attrs: { title: ctrl.noarg('offerDraw'), 'data-icon': licon.OneHalf }, + attrs: { title: i18n.site.offerDraw, 'data-icon': licon.OneHalf }, hook: util.bind('click', () => ctrl.offerDraw(true)), }), - fbtCancel(ctrl, ctrl.offerDraw), + fbtCancel(ctrl.offerDraw), ]); export const claimThreefold = (ctrl: RoundController, condition: (d: RoundData) => ButtonState): VNode => @@ -168,7 +168,7 @@ export const claimThreefold = (ctrl: RoundController, condition: (d: RoundData) condition(ctrl.data).enabled ? ctrl.socket.sendLoading('draw-claim') : undefined, ), attrs: { - title: ctrl.noarg(condition(ctrl.data)?.overrideHint || 'claimADraw'), + title: condition(ctrl.data)?.overrideHint || i18n.site.claimADraw, disabled: !condition(ctrl.data).enabled, }, class: { disabled: !condition(ctrl.data).enabled }, @@ -179,7 +179,7 @@ export const claimThreefold = (ctrl: RoundController, condition: (d: RoundData) export function threefoldSuggestion(ctrl: RoundController): LooseVNode { return ( ctrl.data.game.threefold && - h('div.suggestion', [h('p', { hook: onSuggestionHook }, ctrl.noarg('threefoldRepetition'))]) + h('div.suggestion', [h('p', { hook: onSuggestionHook }, i18n.site.threefoldRepetition)]) ); } @@ -194,10 +194,10 @@ export function backToTournament(ctrl: RoundController): LooseVNode { attrs: { 'data-icon': licon.PlayTriangle, href: '/tournament/' + d.tournament.id }, hook: util.bind('click', ctrl.setRedirecting), }, - ctrl.noarg('backToTournament'), + i18n.site.backToTournament, ), h('form', { attrs: { method: 'post', action: '/tournament/' + d.tournament.id + '/withdraw' } }, [ - h('button.text.fbt.weak', util.justIcon(licon.Pause), ctrl.noarg('pause')), + h('button.text.fbt.weak', util.justIcon(licon.Pause), i18n.site.pause), ]), analysisButton(ctrl), ]) @@ -215,7 +215,7 @@ export function backToSwiss(ctrl: RoundController): LooseVNode { attrs: { 'data-icon': licon.PlayTriangle, href: '/swiss/' + d.swiss.id }, hook: util.bind('click', ctrl.setRedirecting), }, - ctrl.noarg('backToTournament'), + i18n.site.backToTournament, ), analysisButton(ctrl), ]) @@ -228,8 +228,8 @@ export function moretime(ctrl: RoundController): LooseVNode { h('a.moretime', { attrs: { title: ctrl.data.clock - ? ctrl.trans('giveNbSeconds', ctrl.data.clock.moretime) - : ctrl.noarg('giveMoreTime'), + ? i18n.site.giveNbSeconds(ctrl.data.clock.moretime) + : i18n.preferences.giveMoreTime, 'data-icon': licon.PlusButton, }, hook: util.bind('click', ctrl.socket.moreTime), @@ -252,8 +252,8 @@ export function followUp(ctrl: RoundController): VNode { return h('div.follow-up', [ ...rematchZone, d.tournament && - h('a.fbt', { attrs: { href: '/tournament/' + d.tournament.id } }, ctrl.noarg('viewTournament')), - d.swiss && h('a.fbt', { attrs: { href: '/swiss/' + d.swiss.id } }, ctrl.noarg('viewTournament')), + h('a.fbt', { attrs: { href: '/tournament/' + d.tournament.id } }, i18n.site.viewTournament), + d.swiss && h('a.fbt', { attrs: { href: '/swiss/' + d.swiss.id } }, i18n.site.viewTournament), newable && h( 'a.fbt', @@ -262,7 +262,7 @@ export function followUp(ctrl: RoundController): VNode { href: d.game.source === 'pool' ? poolUrl(d.clock!, d.opponent.user) : '/?hook_like=' + d.game.id, }, }, - ctrl.noarg('newOpponent'), + i18n.site.newOpponent, ), analysisButton(ctrl), ]); @@ -272,15 +272,11 @@ export function watcherFollowUp(ctrl: RoundController): LooseVNode { const d = ctrl.data, content = [ d.game.rematch && - h( - 'a.fbt.text', - { attrs: { href: `/${d.game.rematch}/${d.opponent.color}` } }, - ctrl.noarg('viewRematch'), - ), + h('a.fbt.text', { attrs: { href: `/${d.game.rematch}/${d.opponent.color}` } }, i18n.site.viewRematch), d.tournament && - h('a.fbt', { attrs: { href: '/tournament/' + d.tournament.id } }, ctrl.noarg('viewTournament')), + h('a.fbt', { attrs: { href: '/tournament/' + d.tournament.id } }, i18n.site.viewTournament), - d.swiss && h('a.fbt', { attrs: { href: '/swiss/' + d.swiss.id } }, ctrl.noarg('viewTournament')), + d.swiss && h('a.fbt', { attrs: { href: '/swiss/' + d.swiss.id } }, i18n.site.viewTournament), analysisButton(ctrl), ]; return content.find(x => !!x) && h('div.follow-up', content); diff --git a/ui/round/src/view/expiration.ts b/ui/round/src/view/expiration.ts index 341efdded3d1b..1c79a3acbd223 100644 --- a/ui/round/src/view/expiration.ts +++ b/ui/round/src/view/expiration.ts @@ -20,6 +20,6 @@ export default function (ctrl: RoundController): MaybeVNode { return h( 'div.expiration.expiration-' + side, { class: { emerg, 'bar-glider': myTurn } }, - ctrl.trans.vdomPlural('nbSecondsToPlayTheFirstMove', secondsLeft, h('strong', '' + secondsLeft)), + i18n.site.nbSecondsToPlayTheFirstMove.asArray(secondsLeft, h('strong', '' + secondsLeft)), ); } diff --git a/ui/round/src/view/replay.ts b/ui/round/src/view/replay.ts index 8374b8584de1e..ae27cd415bbb8 100644 --- a/ui/round/src/view/replay.ts +++ b/ui/round/src/view/replay.ts @@ -120,7 +120,7 @@ export function analysisButton(ctrl: RoundController): LooseVNode { { class: { text: !!forecastCount }, attrs: { - title: ctrl.noarg('analysis'), + title: i18n.site.analysis, href: gameRoute(ctrl.data, ctrl.data.player.color) + '/analysis#' + ctrl.ply, 'data-icon': licon.Microscope, }, @@ -162,7 +162,7 @@ function renderButtons(ctrl: RoundController) { attrs: { disabled: !enabled, 'data-icon': b[0], 'data-ply': enabled ? b[1] : '-' }, }); }), - boardMenuToggleButton(ctrl.menu, ctrl.noarg('menu')), + boardMenuToggleButton(ctrl.menu, i18n.site.menu), ], ); } @@ -176,8 +176,8 @@ function initMessage(ctrl: RoundController) { !d.player.spectator && h('div.message', util.justIcon(licon.InfoCircle), [ h('div', [ - ctrl.trans(d.player.color === 'white' ? 'youPlayTheWhitePieces' : 'youPlayTheBlackPieces'), - ...(d.player.color === 'white' ? [h('br'), h('strong', ctrl.trans('itsYourTurn'))] : []), + i18n.site[d.player.color === 'white' ? 'youPlayTheWhitePieces' : 'youPlayTheBlackPieces'], + ...(d.player.color === 'white' ? [h('br'), h('strong', i18n.site.itsYourTurn)] : []), ]), ]) ); diff --git a/ui/round/src/view/table.ts b/ui/round/src/view/table.ts index 94ab0c018aa37..d411faf8583da 100644 --- a/ui/round/src/view/table.ts +++ b/ui/round/src/view/table.ts @@ -20,7 +20,7 @@ function renderPlayer(ctrl: RoundController, position: Position) { : player.ai ? h('div.user-link.online.ruser.ruser-' + position, [ h('i.line'), - h('name', renderUser.aiName(ctrl, player.ai)), + h('name', i18n.site.aiNameLevelAiLevel('Stockfish', player.ai)), ]) : (ctrl.opts.local?.userVNode(player, position) ?? renderUser.userHtml(ctrl, player, position)); } @@ -50,13 +50,14 @@ const prompt = (ctrl: RoundController) => { const o = ctrl.question(); if (!o) return {}; - const btn = (tpe: 'yes' | 'no', icon: string, i18nKey: I18nKey, action: () => void) => + const btn = (tpe: 'yes' | 'no', icon: string, text: string, action: () => void) => ctrl.nvui - ? h('button', { hook: bind('click', action) }, ctrl.noarg(i18nKey)) + ? h('button', { hook: bind('click', action) }, text) : h(`a.${tpe}`, { attrs: { 'data-icon': icon }, hook: bind('click', action) }); - const noBtn = o.no && btn('no', o.no.icon || licon.X, o.no.key || 'decline', o.no.action); - const yesBtn = o.yes && btn('yes', o.yes.icon || licon.Checkmark, o.yes.key || 'accept', o.yes.action); + const noBtn = o.no && btn('no', o.no.icon || licon.X, o.no.text || i18n.site.decline, o.no.action); + const yesBtn = + o.yes && btn('yes', o.yes.icon || licon.Checkmark, o.yes.text || i18n.site.accept, o.yes.action); return { promptVNode: h('div.question', { key: o.prompt }, [noBtn, h('p', o.prompt), yesBtn]), @@ -73,12 +74,12 @@ export const renderTablePlay = (ctrl: RoundController): LooseVNodes => { ? [] : [ game.abortable(d) - ? button.standard(ctrl, undefined, licon.X, 'abortGame', 'abort') + ? button.standard(ctrl, undefined, licon.X, i18n.site.abortGame, 'abort') : button.standard( ctrl, d => ({ enabled: game.takebackable(d) }), licon.Back, - 'proposeATakeback', + i18n.site.proposeATakeback, 'takeback-yes', ctrl.takebackYes, ), @@ -89,17 +90,17 @@ export const renderTablePlay = (ctrl: RoundController): LooseVNodes => { const threefoldable = game.drawableSwiss(d); return { enabled: threefoldable, - overrideHint: threefoldable ? undefined : 'noDrawBeforeSwissLimit', + overrideHint: threefoldable ? undefined : i18n.site.noDrawBeforeSwissLimit, }; }) : button.standard( ctrl, d => ({ enabled: ctrl.canOfferDraw(), - overrideHint: game.drawableSwiss(d) ? undefined : 'noDrawBeforeSwissLimit', + overrideHint: game.drawableSwiss(d) ? undefined : i18n.site.noDrawBeforeSwissLimit, }), licon.OneHalf, - 'offerDraw', + i18n.site.offerDraw, 'draw-yes', () => ctrl.offerDraw(true), ), @@ -109,12 +110,12 @@ export const renderTablePlay = (ctrl: RoundController): LooseVNodes => { ctrl, d => ({ enabled: game.resignable(d) }), licon.FlagOutline, - 'resign', + i18n.site.resign, 'resign', () => ctrl.resign(true), ), replay.analysisButton(ctrl), - boardMenuToggleButton(ctrl.menu, ctrl.noarg('menu')), + boardMenuToggleButton(ctrl.menu, i18n.site.menu), ], buttons = loading ? [loader()] @@ -137,8 +138,8 @@ function whosTurn(ctrl: RoundController, color: Color, position: Position) { h( 'div.rclock-turn__text', d.player.spectator - ? ctrl.trans(d.game.player + 'Plays') - : ctrl.trans(d.game.player === d.player.color ? 'yourTurn' : 'waitingForOpponent'), + ? i18n.site[d.game.player === 'white' ? 'whitePlays' : 'blackPlays'] + : i18n.site[d.game.player === d.player.color ? 'yourTurn' : 'waitingForOpponent'], ), ); } @@ -147,7 +148,7 @@ function anyClock(ctrl: RoundController, position: Position) { const player = ctrl.playerAt(position); if (ctrl.clock) return renderClock(ctrl, player, position); else if (ctrl.data.correspondence && ctrl.data.game.turns > 1) - return renderCorresClock(ctrl.corresClock!, ctrl.trans, player.color, position, ctrl.data.game.player); + return renderCorresClock(ctrl.corresClock!, player.color, position, ctrl.data.game.player); else return whosTurn(ctrl, player.color, position); } diff --git a/ui/round/src/view/user.ts b/ui/round/src/view/user.ts index 774ec751b5a3d..a390f86ef95e9 100644 --- a/ui/round/src/view/user.ts +++ b/ui/round/src/view/user.ts @@ -5,9 +5,6 @@ import { Position } from '../interfaces'; import RoundController from '../ctrl'; import { ratingDiff, userLink } from 'common/userLink'; -export const aiName = (ctrl: RoundController, level: number): string => - ctrl.trans('aiNameLevelAiLevel', 'Stockfish', level); - export function userHtml(ctrl: RoundController, player: Player, position: Position): VNode { const d = ctrl.data, user = player.user, @@ -49,7 +46,7 @@ export function userHtml(ctrl: RoundController, player: Player, position: Positi !!rating && ratingDiff(player), player.engine && h('span', { - attrs: { 'data-icon': licon.CautionCircle, title: ctrl.noarg('thisAccountViolatedTos') }, + attrs: { 'data-icon': licon.CautionCircle, title: i18n.site.thisAccountViolatedTos }, }), ], ); @@ -64,7 +61,7 @@ export function userHtml(ctrl: RoundController, player: Player, position: Positi title: connecting ? 'Connecting to the game' : player.onGame ? 'Joined the game' : 'Left the game', }, }), - h('name', player.name || ctrl.noarg('anonymous')), + h('name', player.name || i18n.site.anonymous), ], ); } @@ -75,9 +72,9 @@ const signalBars = (signal: number) => { return h('signal.q' + signal, bars); }; -export const userTxt = (ctrl: RoundController, player: Player): string => +export const userTxt = (player: Player): string => player.user ? (player.user.title ? player.user.title + ' ' : '') + player.user.username : player.ai - ? aiName(ctrl, player.ai) - : ctrl.noarg('anonymous'); + ? i18n.site.aiNameLevelAiLevel('Stockfish', player.ai) + : i18n.site.anonymous; diff --git a/ui/simul/src/ctrl.ts b/ui/simul/src/ctrl.ts index a2f80d787becc..9365f0200bf3b 100644 --- a/ui/simul/src/ctrl.ts +++ b/ui/simul/src/ctrl.ts @@ -2,12 +2,10 @@ import { makeSocket, SimulSocket } from './socket'; import xhr from './xhr'; import { SimulData, SimulOpts } from './interfaces'; import { storage } from 'common/storage'; -import { trans } from 'common/i18n'; import { idleTimer } from 'common/timing'; export default class SimulCtrl { data: SimulData; - trans: Trans; socket: SimulSocket; constructor( @@ -15,7 +13,6 @@ export default class SimulCtrl { readonly redraw: () => void, ) { this.data = opts.data; - this.trans = trans(opts.i18n); this.socket = makeSocket(opts.socketSend, this); if (this.createdByMe() && this.data.isCreated) this.setupCreatedHost(); } diff --git a/ui/simul/src/interfaces.ts b/ui/simul/src/interfaces.ts index 62094d0c4b2ef..b64cfe1db6c20 100644 --- a/ui/simul/src/interfaces.ts +++ b/ui/simul/src/interfaces.ts @@ -5,7 +5,6 @@ export interface SimulOpts { $side: Cash; socketVersion: number; chat: any; - i18n: I18nDict; showRatings: boolean; socketSend: SocketSend; } diff --git a/ui/simul/src/view/created.ts b/ui/simul/src/view/created.ts index 3628cd0f026f0..6d7bd335cfffa 100644 --- a/ui/simul/src/view/created.ts +++ b/ui/simul/src/view/created.ts @@ -26,11 +26,7 @@ export default function (showText: (ctrl: SimulCtrl) => VNode | false) { ? isHost ? [startOrCancel(ctrl, accepted), randomButton(ctrl)] : ctrl.containsMe() - ? h( - 'a.button', - { hook: bind('click', () => xhr.withdraw(ctrl.data.id)) }, - ctrl.trans('withdraw'), - ) + ? h('a.button', { hook: bind('click', () => xhr.withdraw(ctrl.data.id)) }, i18n.site.withdraw) : h( 'a.button.text' + (canJoin ? '' : '.disabled'), { @@ -52,7 +48,7 @@ export default function (showText: (ctrl: SimulCtrl) => VNode | false) { }) : {}, }, - ctrl.trans('join'), + i18n.site.join, ) : h( 'a.button.text', @@ -62,7 +58,7 @@ export default function (showText: (ctrl: SimulCtrl) => VNode | false) { href: '/login?referrer=' + window.location.pathname, }, }, - ctrl.trans('signIn'), + i18n.site.signIn, ), ), ]), @@ -198,5 +194,5 @@ const startOrCancel = (ctrl: SimulCtrl, accepted: Applicant[]) => if (confirm('Delete this simul?')) xhr.abort(ctrl.data.id); }), }, - ctrl.trans('cancel'), + i18n.site.cancel, ); diff --git a/ui/simul/src/view/main.ts b/ui/simul/src/view/main.ts index ab2b8ce67da99..b3fd01a1ad512 100644 --- a/ui/simul/src/view/main.ts +++ b/ui/simul/src/view/main.ts @@ -38,10 +38,7 @@ const started = (ctrl: SimulCtrl) => [ ]; const finished = (ctrl: SimulCtrl) => [ - h('div.box__top', [ - util.title(ctrl), - h('div.box__top__actions', h('div.finished', ctrl.trans('finished'))), - ]), + h('div.box__top', [util.title(ctrl), h('div.box__top__actions', h('div.finished', i18n.site.finished))]), showText(ctrl), results(ctrl), pairings(ctrl), diff --git a/ui/simul/src/view/results.ts b/ui/simul/src/view/results.ts index 470b8cd419924..41ff707335d1e 100644 --- a/ui/simul/src/view/results.ts +++ b/ui/simul/src/view/results.ts @@ -8,19 +8,19 @@ export default function (ctrl: SimulCtrl) { return h('div.results', [ h( 'div', - trans(ctrl, 'nbPlaying', p => p.game.status < status.ids.aborted), + trans(ctrl, i18n.site.nbPlaying, p => p.game.status < status.ids.aborted), ), h( 'div', - trans(ctrl, 'nbWins', p => p.game.winner === p.hostColor), + trans(ctrl, i18n.site.nbWins, p => p.game.winner === p.hostColor), ), h( 'div', - trans(ctrl, 'nbDraws', p => p.game.status >= status.ids.mate && !p.game.winner), + trans(ctrl, i18n.site.nbDraws, p => p.game.status >= status.ids.mate && !p.game.winner), ), h( 'div', - trans(ctrl, 'nbLosses', p => p.game.winner === opposite(p.hostColor)), + trans(ctrl, i18n.site.nbLosses, p => p.game.winner === opposite(p.hostColor)), ), ]); } @@ -35,5 +35,5 @@ const splitNumber = (s: string) => { return h('div.text', s); }; -const trans = (ctrl: SimulCtrl, key: string, cond: (pairing: Pairing) => boolean) => - splitNumber(ctrl.trans.pluralSame(key, ctrl.data.pairings.filter(cond).length)); +const trans = (ctrl: SimulCtrl, plural: I18nPlural, cond: (pairing: Pairing) => boolean) => + splitNumber(plural(ctrl.data.pairings.filter(cond).length)); diff --git a/ui/site/src/boot.ts b/ui/site/src/boot.ts index 4489244d9ff43..4da6801e97114 100644 --- a/ui/site/src/boot.ts +++ b/ui/site/src/boot.ts @@ -157,7 +157,7 @@ export function boot() { .append( $(``) .attr('href', url + '/withdraw') - .text(site.trans('pause')) + .text(i18n.site.pause) .on('click', function (this: HTMLAnchorElement) { xhr.text(this.href, { method: 'post' }); $('#announce').remove(); @@ -167,7 +167,7 @@ export function boot() { .append( $(``) .attr('href', url) - .text(site.trans('resume')), + .text(i18n.site.resume), ), ), ); diff --git a/ui/site/src/friends.ts b/ui/site/src/friends.ts index 8f0cfcdeb6738..1ae0408054ef8 100644 --- a/ui/site/src/friends.ts +++ b/ui/site/src/friends.ts @@ -50,8 +50,7 @@ export default class OnlineFriends { if (this.loaded) requestAnimationFrame(() => { const ids = Array.from(this.users.keys()).sort(); - this.titleEl.innerHTML = site.trans.pluralSame( - 'nbFriendsOnline', + this.titleEl.innerHTML = i18n.site.nbFriendsOnline( ids.length, this.loaded ? `${ids.length}` : '-', ); diff --git a/ui/site/src/renderTimeAgo.ts b/ui/site/src/renderTimeAgo.ts index e622854421a97..4fa5c48a73334 100644 --- a/ui/site/src/renderTimeAgo.ts +++ b/ui/site/src/renderTimeAgo.ts @@ -40,7 +40,7 @@ export const updateTimeAgo = (interval: number): void => { // format the diff second to *** time remaining const formatRemaining = (seconds: number): string => seconds < 1 - ? site.trans.noarg('completed') + ? i18n.timeago.completed : seconds < 3600 - ? site.trans.pluralSame('nbMinutesRemaining', Math.floor(seconds / 60)) - : site.trans.pluralSame('nbHoursRemaining', Math.floor(seconds / 3600)); + ? i18n.timeago.nbMinutesRemaining(Math.floor(seconds / 60)) + : i18n.timeago.nbHoursRemaining(Math.floor(seconds / 3600)); diff --git a/ui/site/src/site.ts b/ui/site/src/site.ts index 9329e94077cae..7bd5074742a14 100644 --- a/ui/site/src/site.ts +++ b/ui/site/src/site.ts @@ -5,7 +5,7 @@ import powertip from './powertip'; import * as assets from './asset'; import { unload, redirect, reload } from './reload'; import announce from './announce'; -import { trans, displayLocale } from 'common/i18n'; +import { displayLocale } from 'common/i18n'; import sound from './sound'; import { pubsub } from 'common/pubsub'; @@ -25,6 +25,5 @@ site.unload = unload; site.redirect = redirect; site.reload = reload; site.announce = announce; -site.trans = trans(site.siteI18n); site.sound = sound; site.load.then(boot); diff --git a/ui/storm/src/ctrl.ts b/ui/storm/src/ctrl.ts index bccc55c49a934..73559c50b0d40 100644 --- a/ui/storm/src/ctrl.ts +++ b/ui/storm/src/ctrl.ts @@ -15,7 +15,6 @@ import { PuzFilters } from 'puz/filters'; import { Role } from 'chessground/types'; import { StormOpts, StormVm, StormRecap, StormPrefs, StormData } from './interfaces'; import { storage } from 'common/storage'; -import { trans } from 'common/i18n'; import { pubsub } from 'common/pubsub'; export default class StormCtrl implements PuzCtrl { @@ -25,7 +24,6 @@ export default class StormCtrl implements PuzCtrl { run: Run; vm: StormVm; filters: PuzFilters; - trans: Trans; promotion: PromotionCtrl; ground: Prop = prop(false); flipped = false; @@ -35,7 +33,6 @@ export default class StormCtrl implements PuzCtrl { this.pref = opts.pref; this.redraw = () => redraw(this.data); this.filters = new PuzFilters(this.redraw, false); - this.trans = trans(opts.i18n); this.run = { pov: puzzlePov(this.data.puzzles[0]), moves: 0, diff --git a/ui/storm/src/interfaces.ts b/ui/storm/src/interfaces.ts index ad4cce51cbbc8..e9992a277ab16 100644 --- a/ui/storm/src/interfaces.ts +++ b/ui/storm/src/interfaces.ts @@ -5,7 +5,6 @@ export interface StormOpts { puzzles: Puzzle[]; key?: string; pref: StormPrefs; - i18n: I18nDict; } export interface StormPrefs extends PuzPrefs {} diff --git a/ui/storm/src/view/end.ts b/ui/storm/src/view/end.ts index 9992c5e7a75d1..99a2cd57023d2 100644 --- a/ui/storm/src/view/end.ts +++ b/ui/storm/src/view/end.ts @@ -7,17 +7,16 @@ import { onInsert, LooseVNodes, looseH as h } from 'common/snabbdom'; const renderEnd = (ctrl: StormCtrl): LooseVNodes => [...renderSummary(ctrl), renderHistory(ctrl)]; const newHighI18n = { - day: 'newDailyHighscore', - week: 'newWeeklyHighscore', - month: 'newMonthlyHighscore', - allTime: 'newAllTimeHighscore', + day: i18n.storm.newDailyHighscore, + week: i18n.storm.newWeeklyHighscore, + month: i18n.storm.newMonthlyHighscore, + allTime: i18n.storm.newAllTimeHighscore, }; const renderSummary = (ctrl: StormCtrl): LooseVNodes => { const run = ctrl.runStats(); const high = ctrl.vm.response?.newHigh; const accuracy = (100 * (run.moves - run.errors)) / run.moves; - const noarg = ctrl.trans.noarg; const scoreSteps = Math.min(run.score, 50); return [ high && @@ -25,8 +24,8 @@ const renderSummary = (ctrl: StormCtrl): LooseVNodes => { 'div.storm--end__high.storm--end__high-daily.bar-glider', h('div.storm--end__high__content', [ h('div.storm--end__high__text', [ - h('strong', noarg(newHighI18n[high.key])), - high.prev ? h('span', ctrl.trans('previousHighscoreWasX', high.prev)) : null, + h('strong', newHighI18n[high.key]), + high.prev ? h('span', i18n.storm.previousHighscoreWasX(high.prev)) : null, ]), ]), ), @@ -36,27 +35,27 @@ const renderSummary = (ctrl: StormCtrl): LooseVNodes => { { hook: onInsert(el => numberSpread(el, scoreSteps, Math.round(scoreSteps * 50), 0)(run.score)) }, '0', ), - h('p', noarg('puzzlesSolved')), + h('p', i18n.storm.puzzlesSolved), ]), h('div.storm--end__stats.box.box-pad', [ h('table.slist', [ h('tbody', [ - h('tr', [h('th', noarg('moves')), h('td', h('number', `${run.moves}`))]), - h('tr', [h('th', noarg('accuracy')), h('td', [h('number', Number(accuracy).toFixed(1)), '%'])]), - h('tr', [h('th', noarg('combo')), h('td', h('number', `${ctrl.run.combo.best}`))]), - h('tr', [h('th', noarg('time')), h('td', [h('number', `${Math.round(run.time)}`), 's'])]), + h('tr', [h('th', i18n.storm.moves), h('td', h('number', `${run.moves}`))]), + h('tr', [h('th', i18n.storm.accuracy), h('td', [h('number', Number(accuracy).toFixed(1)), '%'])]), + h('tr', [h('th', i18n.storm.combo), h('td', h('number', `${ctrl.run.combo.best}`))]), + h('tr', [h('th', i18n.storm.time), h('td', [h('number', `${Math.round(run.time)}`), 's'])]), h('tr', [ - h('th', noarg('timePerMove')), + h('th', i18n.storm.timePerMove), h('td', [h('number', Number(run.time / run.moves).toFixed(2)), 's']), ]), - h('tr', [h('th', noarg('highestSolved')), h('td', h('number', `${run.highest}`))]), + h('tr', [h('th', i18n.storm.highestSolved), h('td', h('number', `${run.highest}`))]), ]), ]), ]), h( 'a.storm-play-again.button', { attrs: ctrl.run.endAt! < getNow() - 900 ? { href: '/storm' } : {} }, - noarg('playAgain'), + i18n.storm.playAgain, ), ]; }; diff --git a/ui/storm/src/view/main.ts b/ui/storm/src/view/main.ts index 309902607e742..e927d25e3eaff 100644 --- a/ui/storm/src/view/main.ts +++ b/ui/storm/src/view/main.ts @@ -13,8 +13,8 @@ import { Chessground as makeChessground } from 'chessground'; import { pubsub } from 'common/pubsub'; export default function (ctrl: StormCtrl): VNode { - if (ctrl.vm.dupTab) return renderReload(ctrl, 'thisRunWasOpenedInAnotherTab'); - if (ctrl.vm.lateStart) return renderReload(ctrl, 'thisRunHasExpired'); + if (ctrl.vm.dupTab) return renderReload(i18n.storm.thisRunWasOpenedInAnotherTab); + if (ctrl.vm.lateStart) return renderReload(i18n.storm.thisRunHasExpired); if (!ctrl.run.endAt) return h('div.storm.storm-app.storm--play', { class: playModifiers(ctrl.run) }, renderPlay(ctrl)); return h('main.storm.storm--end', renderEnd(ctrl)); @@ -50,12 +50,12 @@ const renderPlay = (ctrl: StormCtrl): VNode[] => { return [ h('div.puz-board.main-board', [chessground(ctrl), ctrl.promotion.view()]), h('div.puz-side', [ - run.clock.startAt ? renderSolved(ctrl) : renderStart(ctrl), + run.clock.startAt ? renderSolved(ctrl) : renderStart(), h('div.puz-clock', [ renderClock(run, ctrl.endNow, true), !!malus && malus.at > now - 900 && h('div.puz-clock__malus', '-' + malus.seconds), !!bonus && bonus.at > now - 900 && h('div.puz-clock__bonus', '+' + bonus.seconds), - ...(run.clock.started() ? [] : [h('span.puz-clock__pov', ctrl.trans.noarg(povMessage(run)))]), + ...(run.clock.started() ? [] : [h('span.puz-clock__pov', povMessage(run))]), ]), h('div.puz-side__table', [renderControls(ctrl), renderCombo(config, renderBonus)(run)]), ]), @@ -69,26 +69,26 @@ const renderControls = (ctrl: StormCtrl): VNode => h('div.puz-side__control', [ h('a.puz-side__control__flip.button', { class: { active: ctrl.flipped, 'button-empty': !ctrl.flipped }, - attrs: { 'data-icon': licon.ChasingArrows, title: ctrl.trans.noarg('flipBoard') + ' (Keyboard: f)' }, + attrs: { 'data-icon': licon.ChasingArrows, title: i18n.site.flipBoard + ' (Keyboard: f)' }, hook: onInsert(el => el.addEventListener('click', ctrl.flip)), }), h('a.puz-side__control__reload.button.button-empty', { - attrs: { href: '/storm', 'data-icon': licon.Trash, title: ctrl.trans('newRun') }, + attrs: { href: '/storm', 'data-icon': licon.Trash, title: i18n.storm.newRun }, }), h('a.puz-side__control__end.button.button-empty', { - attrs: { 'data-icon': licon.FlagOutline, title: ctrl.trans('endRun') }, + attrs: { 'data-icon': licon.FlagOutline, title: i18n.storm.endRun }, hook: onInsert(el => el.addEventListener('click', ctrl.endNow)), }), ]); -const renderStart = (ctrl: StormCtrl) => +const renderStart = () => h('div.puz-side__top.puz-side__start', [ - h('div.puz-side__start__text', [h('strong', 'Puzzle Storm'), h('span', ctrl.trans('moveToStart'))]), + h('div.puz-side__start__text', [h('strong', 'Puzzle Storm'), h('span', i18n.storm.moveToStart)]), ]); -const renderReload = (ctrl: StormCtrl, msgKey: string) => +const renderReload = (text: string) => h('div.storm.storm--reload.box.box-pad', [ h('i', { attrs: { 'data-icon': licon.Storm } }), - h('p', ctrl.trans.noarg(msgKey)), - h('a.storm--dup__reload.button', { attrs: { href: '/storm' } }, ctrl.trans.noarg('clickToReload')), + h('p', text), + h('a.storm--dup__reload.button', { attrs: { href: '/storm' } }, i18n.storm.clickToReload), ]); diff --git a/ui/swiss/src/ctrl.ts b/ui/swiss/src/ctrl.ts index 80e39fce5ac03..805c749ccf680 100644 --- a/ui/swiss/src/ctrl.ts +++ b/ui/swiss/src/ctrl.ts @@ -4,11 +4,9 @@ import { throttlePromiseDelay } from 'common/timing'; import { maxPerPage, myPage, players } from './pagination'; import { SwissData, SwissOpts, Pages, Standing, Player } from './interfaces'; import { storage } from 'common/storage'; -import { trans } from 'common/i18n'; export default class SwissCtrl { data: SwissData; - trans: Trans; socket: SwissSocket; page: number; pages: Pages = {}; @@ -26,7 +24,6 @@ export default class SwissCtrl { readonly redraw: () => void, ) { this.data = this.readData(opts.data); - this.trans = trans(opts.i18n); this.socket = makeSocket(opts.socketSend, this); this.page = this.data.standing.page; this.focusOnMe = this.isIn(); @@ -159,7 +156,7 @@ export default class SwissCtrl { private redrawNbRounds = () => $('.swiss__meta__round').text( - this.trans.plural('nbRounds', this.data.nbRounds, `${this.data.round}/${this.data.nbRounds}`), + i18n.swiss.nbRounds.asArray(this.data.nbRounds, `${this.data.round}/${this.data.nbRounds}`).join(''), ); private readData = (data: SwissData) => ({ diff --git a/ui/swiss/src/interfaces.ts b/ui/swiss/src/interfaces.ts index 6eed87a19cab2..80d8babc0ad0f 100644 --- a/ui/swiss/src/interfaces.ts +++ b/ui/swiss/src/interfaces.ts @@ -6,7 +6,6 @@ export interface SwissOpts { $side: Cash; socketSend: SocketSend; chat: any; - i18n: I18nDict; classes: string | null; showRatings: boolean; } diff --git a/ui/swiss/src/view/header.ts b/ui/swiss/src/view/header.ts index bb8b8c4169fe2..451c4a9913815 100644 --- a/ui/swiss/src/view/header.ts +++ b/ui/swiss/src/view/header.ts @@ -25,19 +25,14 @@ function clock(ctrl: SwissCtrl): VNode | undefined { }), ]); return h(`div.clock.clock-created.time-cache-${next.at}`, [ - h( - 'span.shy', - ctrl.data.status == 'created' ? ctrl.trans.noarg('startingIn') : ctrl.trans.noarg('nextRound'), - ), + h('span.shy', ctrl.data.status == 'created' ? i18n.swiss.startingIn : i18n.swiss.nextRound), h('span.time.text', { hook: startClock(next.in + 1) }), ]); } function ongoing(ctrl: SwissCtrl): VNode | undefined { const nb = ctrl.data.nbOngoing; - return nb - ? h('div.ongoing', [h('span.nb', [nb]), h('span.shy', ctrl.trans.pluralSame('ongoingGames', nb))]) - : undefined; + return nb ? h('div.ongoing', [h('span.nb', [nb]), h('span.shy', i18n.swiss.ongoingGames(nb))]) : undefined; } export default function (ctrl: SwissCtrl): VNode { diff --git a/ui/swiss/src/view/main.ts b/ui/swiss/src/view/main.ts index ee3b0c68e16aa..3bf8d9fc5d413 100644 --- a/ui/swiss/src/view/main.ts +++ b/ui/swiss/src/view/main.ts @@ -57,7 +57,7 @@ const notice = (ctrl: SwissCtrl) => { !d.me.absent && d.status == 'started' && d.nextRound && - h('div.swiss__notice.bar-glider', ctrl.trans('standByX', d.me.name)) + h('div.swiss__notice.bar-glider', i18n.site.standByX(d.me.name)) ); }; @@ -122,14 +122,14 @@ function joinButton(ctrl: SwissCtrl): VNode | undefined { return h( 'a.fbt.text.highlight', { attrs: { href: '/login?referrer=' + window.location.pathname, 'data-icon': licon.PlayTriangle } }, - ctrl.trans('signIn'), + i18n.site.signIn, ); if (d.joinTeam) return h( 'a.fbt.text.highlight', { attrs: { href: `/team/${d.joinTeam}`, 'data-icon': licon.Group } }, - ctrl.trans.noarg('joinTeam'), + i18n.team.joinTeam, ); if (d.canJoin) @@ -143,14 +143,14 @@ function joinButton(ctrl: SwissCtrl): VNode | undefined { 'click', _ => { if (d.password) { - const p = prompt(ctrl.trans.noarg('tournamentEntryCode')); + const p = prompt(i18n.site.tournamentEntryCode); if (p !== null) ctrl.join(p); } else ctrl.join(); }, ctrl.redraw, ), }, - ctrl.trans.noarg('join'), + i18n.site.join, ); if (d.me && d.status != 'finished') @@ -160,14 +160,14 @@ function joinButton(ctrl: SwissCtrl): VNode | undefined { : h( 'button.fbt.text.highlight', { attrs: dataIcon(licon.PlayTriangle), hook: bind('click', _ => ctrl.join(), ctrl.redraw) }, - ctrl.trans.noarg('join'), + i18n.site.join, ) : ctrl.joinSpinner ? spinner() : h( 'button.fbt.text', { attrs: dataIcon(licon.FlagOutline), hook: bind('click', ctrl.withdraw, ctrl.redraw) }, - ctrl.trans.noarg('withdraw'), + i18n.site.withdraw, ); return; @@ -178,9 +178,9 @@ function joinTheGame(ctrl: SwissCtrl) { return ( gameId && h('a.swiss__ur-playing.button.is.is-after', { attrs: { href: '/' + gameId } }, [ - ctrl.trans('youArePlaying'), + i18n.site.youArePlaying, h('br'), - ctrl.trans('joinTheGame'), + i18n.site.joinTheGame, ]) ); } @@ -200,25 +200,24 @@ function confetti(data: SwissData) { function stats(ctrl: SwissCtrl) { const s = ctrl.data.stats, - noarg = ctrl.trans.noarg, slots = ctrl.data.round * ctrl.data.nbPlayers; if (!s) return undefined; return h('div.swiss__stats', [ - h('h2', noarg('tournamentComplete')), + h('h2', i18n.site.tournamentComplete), h('table', [ - ctrl.opts.showRatings ? numberRow(noarg('averageElo'), s.averageRating, 'raw') : null, - numberRow(noarg('gamesPlayed'), s.games), - numberRow(noarg('whiteWins'), [s.whiteWins, slots], 'percent'), - numberRow(noarg('blackWins'), [s.blackWins, slots], 'percent'), - numberRow(noarg('drawRate'), [s.draws, slots], 'percent'), - numberRow(noarg('byes'), [s.byes, slots], 'percent'), - numberRow(noarg('absences'), [s.absences, slots], 'percent'), + ctrl.opts.showRatings ? numberRow(i18n.site.averageElo, s.averageRating, 'raw') : null, + numberRow(i18n.site.gamesPlayed, s.games), + numberRow(i18n.site.whiteWins, [s.whiteWins, slots], 'percent'), + numberRow(i18n.site.blackWins, [s.blackWins, slots], 'percent'), + numberRow(i18n.site.drawRate, [s.draws, slots], 'percent'), + numberRow(i18n.swiss.byes, [s.byes, slots], 'percent'), + numberRow(i18n.swiss.absences, [s.absences, slots], 'percent'), ]), h('div.swiss__stats__links', [ h( 'a', { attrs: { href: `/swiss/${ctrl.data.id}/round/1` } }, - ctrl.trans('viewAllXRounds', ctrl.data.round), + i18n.swiss.viewAllXRounds(ctrl.data.round), ), h('br'), h( @@ -229,7 +228,7 @@ function stats(ctrl: SwissCtrl) { h( 'a.text', { attrs: { 'data-icon': licon.Download, href: `/api/swiss/${ctrl.data.id}/games`, download: true } }, - noarg('downloadAllGames'), + i18n.study.downloadAllGames, ), h( 'a.text', diff --git a/ui/swiss/src/view/playerInfo.ts b/ui/swiss/src/view/playerInfo.ts index e42aa4cfdaae6..8e1d5f1899d46 100644 --- a/ui/swiss/src/view/playerInfo.ts +++ b/ui/swiss/src/view/playerInfo.ts @@ -11,7 +11,6 @@ import { fullName } from 'common/userLink'; export default function (ctrl: SwissCtrl): VNode | undefined { if (!ctrl.playerInfoId) return; const data = ctrl.data.playerInfo; - const noarg = ctrl.trans.noarg; const tag = 'div.swiss__player-info.swiss__table'; if (data?.user.id !== ctrl.playerInfoId) return h(tag, [h('div.stats', [h('h2', ctrl.playerInfoId), spinner()])]); @@ -28,15 +27,15 @@ export default function (ctrl: SwissCtrl): VNode | undefined { h('div.stats', [ h('h2', [h('span.rank', data.rank + '. '), renderPlayer(data, true, false)]), h('table', [ - numberRow(noarg('points'), data.points, 'raw'), - numberRow(noarg('tieBreak'), data.tieBreak, 'raw'), + numberRow(i18n.site.points, data.points, 'raw'), + numberRow(i18n.swiss.tieBreak, data.tieBreak, 'raw'), ...(games ? [ data.performance && ctrl.opts.showRatings && - numberRow(noarg('performance'), data.performance + (games < 3 ? '?' : ''), 'raw'), - numberRow(noarg('winRate'), [wins, games], 'percent'), - ctrl.opts.showRatings && numberRow(noarg('averageOpponent'), avgOp, 'raw'), + numberRow(i18n.site.performance, data.performance + (games < 3 ? '?' : ''), 'raw'), + numberRow(i18n.site.winRate, [wins, games], 'percent'), + ctrl.opts.showRatings && numberRow(i18n.site.averageOpponent, avgOp, 'raw'), ] : []), ]), diff --git a/ui/swiss/src/view/podium.ts b/ui/swiss/src/view/podium.ts index ffb22c62ca9e3..bb17cb8bfd717 100644 --- a/ui/swiss/src/view/podium.ts +++ b/ui/swiss/src/view/podium.ts @@ -5,10 +5,10 @@ import { userLink } from 'common/userLink'; const podiumStats = (p: PodiumPlayer, ctrl: SwissCtrl): VNode => h('table.stats', [ - h('tr', [h('th', ctrl.trans.noarg('points')), h('td', '' + p.points)]), - h('tr', [h('th', ctrl.trans.noarg('tieBreak')), h('td', '' + p.tieBreak)]), + h('tr', [h('th', i18n.site.points), h('td', '' + p.points)]), + h('tr', [h('th', i18n.swiss.tieBreak), h('td', '' + p.tieBreak)]), p.performance && ctrl.opts.showRatings - ? h('tr', [h('th', ctrl.trans.noarg('performance')), h('td', '' + p.performance)]) + ? h('tr', [h('th', i18n.site.performance), h('td', '' + p.performance)]) : null, ]); diff --git a/ui/tournament/src/ctrl.ts b/ui/tournament/src/ctrl.ts index 6c6fde37b838d..882915bb3bb0b 100644 --- a/ui/tournament/src/ctrl.ts +++ b/ui/tournament/src/ctrl.ts @@ -4,7 +4,6 @@ import { maxPerPage, myPage, players } from './pagination'; import * as sound from './sound'; import { TournamentData, TournamentOpts, Pages, PlayerInfo, TeamInfo, Standing, Player } from './interfaces'; import { storage } from 'common/storage'; -import { trans } from 'common/i18n'; import { pubsub } from 'common/pubsub'; interface CtrlTeamInfo { @@ -15,7 +14,6 @@ interface CtrlTeamInfo { export default class TournamentController { opts: TournamentOpts; data: TournamentData; - trans: Trans; socket: TournamentSocket; page: number; pages: Pages = {}; @@ -36,7 +34,6 @@ export default class TournamentController { this.opts = opts; this.data = opts.data; this.redraw = redraw; - this.trans = trans(opts.i18n); this.socket = makeSocket(opts.socketSend, this); this.page = this.data.standing.page; this.focusOnMe = this.isIn(); @@ -154,7 +151,7 @@ export default class TournamentController { } else { let password; if (this.data.private && !this.data.me) { - password = prompt(this.trans.noarg('tournamentEntryCode')); + password = prompt(i18n.site.tournamentEntryCode); if (password === null) { return; } diff --git a/ui/tournament/src/interfaces.ts b/ui/tournament/src/interfaces.ts index 996b69f575856..00af5bd37bf9b 100644 --- a/ui/tournament/src/interfaces.ts +++ b/ui/tournament/src/interfaces.ts @@ -23,8 +23,6 @@ export interface TournamentOpts { element: HTMLElement; socketSend: SocketSend; data: TournamentData; - i18n: I18nDict; - trans: Trans; classes: string | null; $side: Cash; $faq: Cash; diff --git a/ui/tournament/src/tournament.calendar.ts b/ui/tournament/src/tournament.calendar.ts index 81729b5b3d7a1..06a7b5a6bc7f0 100644 --- a/ui/tournament/src/tournament.calendar.ts +++ b/ui/tournament/src/tournament.calendar.ts @@ -17,7 +17,7 @@ export interface Ctrl { data: Data; } -export function initModule(opts: { data: Data; i18n: I18nDict }) { +export function initModule(opts: { data: Data }) { const element = document.getElementById('tournament-calendar'); // enrich tournaments opts.data.tournaments.forEach(t => { diff --git a/ui/tournament/src/tournament.schedule.ts b/ui/tournament/src/tournament.schedule.ts index a3fb75aa7dd9e..828003285f1cb 100644 --- a/ui/tournament/src/tournament.schedule.ts +++ b/ui/tournament/src/tournament.schedule.ts @@ -3,7 +3,6 @@ import view from './view/scheduleView'; import { init, VNode, classModule, attributesModule } from 'snabbdom'; import { Tournament } from './interfaces'; import StrongSocket from 'common/socket'; -import { trans } from 'common/i18n'; import { pubsub } from 'common/pubsub'; const patch = init([classModule, attributesModule]); @@ -17,17 +16,15 @@ export interface Data { } export interface Ctrl { data(): Data; - trans: Trans; } -export function initModule(opts: { data: Data; i18n: I18nDict }) { +export function initModule(opts: { data: Data }) { site.socket = new StrongSocket('/socket/v5', false, { params: { flag: 'tournament' } }); const element = document.querySelector('.tour-chart') as HTMLElement; const ctrl = { data: () => opts.data, - trans: trans(opts.i18n), }; let vnode: VNode; diff --git a/ui/tournament/src/view/arena.ts b/ui/tournament/src/view/arena.ts index 4728cff6ea4df..26a7ec725f21c 100644 --- a/ui/tournament/src/view/arena.ts +++ b/ui/tournament/src/view/arena.ts @@ -48,7 +48,7 @@ function playerTr(ctrl: TournamentController, player: StandingPlayer) { h( 'td.rank', player.withdraw - ? h('i', { attrs: { 'data-icon': licon.Pause, title: ctrl.trans.noarg('pause') } }) + ? h('i', { attrs: { 'data-icon': licon.Pause, title: i18n.site.pause } }) : player.rank, ), h('td.player', [ @@ -66,18 +66,17 @@ function playerTr(ctrl: TournamentController, player: StandingPlayer) { } function podiumStats(p: PodiumPlayer, berserkable: boolean, ctrl: TournamentController): VNode { - const noarg = ctrl.trans.noarg, - nb = p.nb; + const nb = p.nb; return h('table.stats', [ p.performance && ctrl.opts.showRatings - ? h('tr', [h('th', noarg('performance')), h('td', p.performance)]) + ? h('tr', [h('th', i18n.site.performance), h('td', p.performance)]) : null, - h('tr', [h('th', noarg('gamesPlayed')), h('td', nb.game)]), + h('tr', [h('th', i18n.site.gamesPlayed), h('td', nb.game)]), ...(nb.game ? [ - h('tr', [h('th', noarg('winRate')), h('td', ratio2percent(nb.win / nb.game))]), + h('tr', [h('th', i18n.site.winRate), h('td', ratio2percent(nb.win / nb.game))]), berserkable - ? h('tr', [h('th', noarg('berserkRate')), h('td', ratio2percent(nb.berserk / nb.game))]) + ? h('tr', [h('th', i18n.site.berserkRate), h('td', ratio2percent(nb.berserk / nb.game))]) : null, ] : []), diff --git a/ui/tournament/src/view/battle.ts b/ui/tournament/src/view/battle.ts index 53dfa550bff7b..04e12dd304d86 100644 --- a/ui/tournament/src/view/battle.ts +++ b/ui/tournament/src/view/battle.ts @@ -23,11 +23,11 @@ export function joinWithTeamSelector(ctrl: TournamentController) { onClose, vnodes: [ h('div.team-picker', [ - h('h2', ctrl.trans.noarg('pickYourTeam')), + h('h2', i18n.arena.pickYourTeam), h('br'), ...(tb.joinWith.length ? [ - h('p', ctrl.trans.noarg('whichTeamWillYouRepresentInThisBattle')), + h('p', i18n.arena.whichTeamWillYouRepresentInThisBattle), ...tb.joinWith.map(id => h( 'button.button.team-picker__team', @@ -37,7 +37,7 @@ export function joinWithTeamSelector(ctrl: TournamentController) { ), ] : [ - h('p', ctrl.trans.noarg('youMustJoinOneOfTheseTeamsToParticipate')), + h('p', i18n.arena.youMustJoinOneOfTheseTeamsToParticipate), h( 'ul', shuffleArray(Object.keys(tb.teams)).map((id: string) => @@ -75,7 +75,7 @@ function extraTeams(ctrl: TournamentController): VNode { h( 'a', { attrs: { href: `/tournament/${ctrl.data.id}/teams` } }, - ctrl.trans('viewAllXTeams', Object.keys(ctrl.data.teamBattle!.teams).length), + i18n.arena.viewAllXTeams(Object.keys(ctrl.data.teamBattle!.teams).length), ), ), ); diff --git a/ui/tournament/src/view/button.ts b/ui/tournament/src/view/button.ts index c4a091be90d73..12209e6a08b1c 100644 --- a/ui/tournament/src/view/button.ts +++ b/ui/tournament/src/view/button.ts @@ -17,7 +17,7 @@ export function withdraw(ctrl: TournamentController): VNode { attrs: dataIcon(pause ? licon.Pause : licon.FlagOutline), hook: bind('click', ctrl.withdraw, ctrl.redraw), }, - ctrl.trans.noarg(pause ? 'pause' : 'withdraw'), + i18n.site[pause ? 'pause' : 'withdraw'], ); }); } @@ -32,7 +32,7 @@ export function join(ctrl: TournamentController): VNode { attrs: { disabled: !joinable, 'data-icon': licon.PlayTriangle }, hook: bind('click', _ => ctrl.join(), ctrl.redraw), }, - ctrl.trans.noarg('join'), + i18n.site.join, ); return delay ? h('div.delay-wrap', { attrs: { title: 'Waiting to be able to re-join the tournament' } }, [ @@ -64,7 +64,7 @@ export function joinWithdraw(ctrl: TournamentController): VNode | undefined { return h( 'a.fbt.text.highlight', { attrs: { href: '/login?referrer=' + window.location.pathname, 'data-icon': licon.PlayTriangle } }, - ctrl.trans('signIn'), + i18n.site.signIn, ); if (!ctrl.data.isFinished) return ctrl.isIn() ? withdraw(ctrl) : join(ctrl); return undefined; diff --git a/ui/tournament/src/view/finished.ts b/ui/tournament/src/view/finished.ts index fa17a881d3d11..9128f548aff72 100644 --- a/ui/tournament/src/view/finished.ts +++ b/ui/tournament/src/view/finished.ts @@ -21,25 +21,23 @@ function confetti(data: TournamentData): VNode | undefined { } function stats(ctrl: TournamentController): VNode | undefined { - const data = ctrl.data, - trans = ctrl.trans, - noarg = trans.noarg; + const data = ctrl.data; if (!data.stats) return undefined; const tableData = [ - ctrl.opts.showRatings ? numberRow(noarg('averageElo'), data.stats.averageRating, 'raw') : null, - numberRow(noarg('gamesPlayed'), data.stats.games), - numberRow(noarg('movesPlayed'), data.stats.moves), - numberRow(noarg('whiteWins'), [data.stats.whiteWins, data.stats.games], 'percent'), - numberRow(noarg('blackWins'), [data.stats.blackWins, data.stats.games], 'percent'), - numberRow(noarg('drawRate'), [data.stats.draws, data.stats.games], 'percent'), + ctrl.opts.showRatings ? numberRow(i18n.site.averageElo, data.stats.averageRating, 'raw') : null, + numberRow(i18n.site.gamesPlayed, data.stats.games), + numberRow(i18n.site.movesPlayed, data.stats.moves), + numberRow(i18n.site.whiteWins, [data.stats.whiteWins, data.stats.games], 'percent'), + numberRow(i18n.site.blackWins, [data.stats.blackWins, data.stats.games], 'percent'), + numberRow(i18n.site.drawRate, [data.stats.draws, data.stats.games], 'percent'), ]; if (data.berserkable) { - tableData.push(numberRow(noarg('berserkRate'), [data.stats.berserks / 2, data.stats.games], 'percent')); + tableData.push(numberRow(i18n.site.berserkRate, [data.stats.berserks / 2, data.stats.games], 'percent')); } return h('div.tour__stats', [ - h('h2', noarg('tournamentComplete')), + h('h2', i18n.site.tournamentComplete), h('table', tableData), h('div.tour__stats__links.force-ltr', [ ...(data.teamBattle @@ -47,7 +45,7 @@ function stats(ctrl: TournamentController): VNode | undefined { h( 'a', { attrs: { href: `/tournament/${data.id}/teams` } }, - trans('viewAllXTeams', Object.keys(data.teamBattle.teams).length), + i18n.arena.viewAllXTeams(Object.keys(data.teamBattle.teams).length), ), h('br'), ] @@ -55,7 +53,7 @@ function stats(ctrl: TournamentController): VNode | undefined { h( 'a.text', { attrs: { 'data-icon': licon.Download, href: `/api/tournament/${data.id}/games`, download: true } }, - noarg('downloadAllGames'), + i18n.study.downloadAllGames, ), data.me && h( diff --git a/ui/tournament/src/view/header.ts b/ui/tournament/src/view/header.ts index 36f926145d25f..301369efc4dee 100644 --- a/ui/tournament/src/view/header.ts +++ b/ui/tournament/src/view/header.ts @@ -37,7 +37,7 @@ function clock(ctrl: TournamentController): VNode | undefined { }), ]); return h('div.clock.clock-created', [ - h('span.shy', ctrl.trans.noarg('startingIn')), + h('span.shy', i18n.swiss.startingIn), h('span.time.text', { hook: startClock(d.secondsToStart) }), ]); } diff --git a/ui/tournament/src/view/playerInfo.ts b/ui/tournament/src/view/playerInfo.ts index 8324488b5603e..ec87c0a6c9b0d 100644 --- a/ui/tournament/src/view/playerInfo.ts +++ b/ui/tournament/src/view/playerInfo.ts @@ -32,7 +32,6 @@ function setup(vnode: VNode) { export default function (ctrl: TournamentController): VNode { const data = ctrl.playerInfo.data; - const noarg = ctrl.trans.noarg; const tag = 'div.tour__player-info.tour__actor-info'; if (!data || data.player.id !== ctrl.playerInfo.id) return h(tag, [h('div.stats', [playerTitle(ctrl.playerInfo.player!), spinner()])]); @@ -55,13 +54,13 @@ export default function (ctrl: TournamentController): VNode { h('table', [ ctrl.opts.showRatings && data.player.performance && - numberRow(noarg('performance'), data.player.performance + (nb.game < 3 ? '?' : ''), 'raw'), - numberRow(noarg('gamesPlayed'), nb.game), + numberRow(i18n.site.performance, data.player.performance + (nb.game < 3 ? '?' : ''), 'raw'), + numberRow(i18n.site.gamesPlayed, nb.game), ...(nb.game ? [ - numberRow(noarg('winRate'), [nb.win, nb.game], 'percent'), - numberRow(noarg('berserkRate'), [nb.berserk, nb.game], 'percent'), - ctrl.opts.showRatings && numberRow(noarg('averageOpponent'), avgOp, 'raw'), + numberRow(i18n.site.winRate, [nb.win, nb.game], 'percent'), + numberRow(i18n.site.berserkRate, [nb.berserk, nb.game], 'percent'), + ctrl.opts.showRatings && numberRow(i18n.site.averageOpponent, avgOp, 'raw'), ] : []), ]), diff --git a/ui/tournament/src/view/scheduleView.ts b/ui/tournament/src/view/scheduleView.ts index c86f48a7b0e25..2671478b5bbd5 100644 --- a/ui/tournament/src/view/scheduleView.ts +++ b/ui/tournament/src/view/scheduleView.ts @@ -132,7 +132,7 @@ const iconOf = (tour: Tournament) => let mousedownAt: number[] | undefined; -function renderTournament(ctrl: Ctrl, tour: Tournament) { +function renderTournament(tour: Tournament) { let width = tour.minutes * scale; const left = leftPos(tour.startsAt); // moves content into viewport, for long tourneys and marathons @@ -178,7 +178,7 @@ function renderTournament(ctrl: Ctrl, tour: Tournament) { displayClock(tour.clock) + ' ', tour.variant.key === 'standard' ? null : tour.variant.name + ' ', tour.position ? 'Thematic ' : null, - tour.rated ? ctrl.trans('ratedTournament') : ctrl.trans('casualTournament'), + i18n.site[tour.rated ? 'ratedTournament' : 'casualTournament'], ]), tour.nbPlayers ? h('span.nb-players', { attrs: { 'data-icon': licon.User } }, tour.nbPlayers) @@ -284,7 +284,7 @@ export default function (ctrl: Ctrl) { ...tourLanes.map(lane => { return h( 'div.tournamentline', - lane.map(tour => renderTournament(ctrl, tour)), + lane.map(tour => renderTournament(tour)), ); }), ], diff --git a/ui/tournament/src/view/started.ts b/ui/tournament/src/view/started.ts index 9ebf72195d9b2..ded49e0215218 100644 --- a/ui/tournament/src/view/started.ts +++ b/ui/tournament/src/view/started.ts @@ -9,18 +9,18 @@ import * as pagination from '../pagination'; import TournamentController from '../ctrl'; import { MaybeVNodes } from 'common/snabbdom'; -function joinTheGame(ctrl: TournamentController, gameId: string) { +function joinTheGame(gameId: string) { return h('a.tour__ur-playing.button.is.is-after', { attrs: { href: '/' + gameId } }, [ - ctrl.trans('youArePlaying'), + i18n.site.youArePlaying, h('br'), - ctrl.trans('joinTheGame'), + i18n.site.joinTheGame, ]); } function notice(ctrl: TournamentController): VNode { return ctrl.willBePaired() - ? h('div.tour__notice.bar-glider', ctrl.trans('standByX', ctrl.data.myUsername!)) - : h('div.tour__notice.closed', ctrl.trans('tournamentPairingsAreNowClosed')); + ? h('div.tour__notice.bar-glider', i18n.site.standByX(ctrl.data.myUsername!)) + : h('div.tour__notice.closed', i18n.site.tournamentPairingsAreNowClosed); } export const name = 'started'; @@ -30,7 +30,7 @@ export function main(ctrl: TournamentController): MaybeVNodes { pag = pagination.players(ctrl); return [ header(ctrl), - gameId ? joinTheGame(ctrl, gameId) : ctrl.isIn() ? notice(ctrl) : null, + gameId ? joinTheGame(gameId) : ctrl.isIn() ? notice(ctrl) : null, teamStanding(ctrl, 'started'), controls(ctrl, pag), standing(ctrl, pag, 'started'), diff --git a/ui/tournament/src/view/table.ts b/ui/tournament/src/view/table.ts index 667d3b95bf583..855cb57f6ee23 100644 --- a/ui/tournament/src/view/table.ts +++ b/ui/tournament/src/view/table.ts @@ -70,7 +70,7 @@ export default function (ctrl: TournamentController): VNode { h( 'section.tour__duels', { hook: bind('click', _ => !ctrl.disableClicks) }, - [h('h2', ctrl.trans.noarg('topGames'))].concat(ctrl.data.duels.map(renderDuel(ctrl))), + [h('h2', i18n.site.topGames)].concat(ctrl.data.duels.map(renderDuel(ctrl))), ), ]); } diff --git a/ui/tournament/src/view/teamInfo.ts b/ui/tournament/src/view/teamInfo.ts index 82083eee72c00..6b9d8c3d1b292 100644 --- a/ui/tournament/src/view/teamInfo.ts +++ b/ui/tournament/src/view/teamInfo.ts @@ -8,8 +8,7 @@ import { teamName } from './battle'; export default function (ctrl: TournamentController): VNode | undefined { const battle = ctrl.data.teamBattle, - data = ctrl.teamInfo.loaded, - noarg = ctrl.trans.noarg; + data = ctrl.teamInfo.loaded; if (!battle) return undefined; const teamTag = ctrl.teamInfo.requested ? teamName(battle, ctrl.teamInfo.requested) : null; const tag = 'div.tour__team-info.tour__actor-info'; @@ -28,19 +27,19 @@ export default function (ctrl: TournamentController): VNode | undefined { h('div.stats', [ h('h2', [teamTag]), h('table', [ - numberRow(noarg('players'), data.nbPlayers), + numberRow(i18n.site.players, data.nbPlayers), ...(data.rating ? [ - ctrl.opts.showRatings ? numberRow(noarg('averageElo'), data.rating, 'raw') : null, + ctrl.opts.showRatings ? numberRow(i18n.site.averageElo, data.rating, 'raw') : null, ...(data.perf ? [ - ctrl.opts.showRatings ? numberRow(noarg('averagePerformance'), data.perf, 'raw') : null, - numberRow(noarg('averageScore'), data.score, 'raw'), + ctrl.opts.showRatings ? numberRow(i18n.arena.averagePerformance, data.perf, 'raw') : null, + numberRow(i18n.arena.averageScore, data.score, 'raw'), ] : []), ] : []), - h('tr', h('th', h('a', { attrs: { href: '/team/' + data.id } }, noarg('teamPage')))), + h('tr', h('th', h('a', { attrs: { href: '/team/' + data.id } }, i18n.team.teamPage))), ]), ]), h('div', [ From c236f3a6cd771013c19eda06b2510158f3b595a1 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Mon, 14 Oct 2024 15:50:02 -0500 Subject: [PATCH 2/9] fold other locales into en-GB on write --- modules/web/src/main/ui/layout.scala | 4 +-- ui/.build/src/build.ts | 9 +++++++ ui/.build/src/i18n.ts | 38 +++++++++++++++++----------- 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/modules/web/src/main/ui/layout.scala b/modules/web/src/main/ui/layout.scala index d095faf77c2ad..bf43bf4cd4e5d 100644 --- a/modules/web/src/main/ui/layout.scala +++ b/modules/web/src/main/ui/layout.scala @@ -141,10 +141,8 @@ final class layout(helpers: Helpers, assetHelper: lila.web.ui.AssetFullHelper)( ) ) - // consolidate script packaging here to dedup chunk dependencies def sitePreload(i18nDicts: List[String], modules: EsmList, isInquiry: Boolean)(using ctx: Context) = - val langs = if ctx.lang.code == "en-GB" then List("en-GB") else List("en-GB", ctx.lang.code) - val i18nModules = i18nDicts.flatMap(dict => langs.map(lang => s"i18n/$dict.$lang")) + val i18nModules = i18nDicts.map(dict => s"i18n/$dict.${ctx.lang.code}") scriptsPreload( i18nModules ::: "site" :: (isInquiry.option("mod.inquiry") :: modules.map(_.map(_.key))).flatten ) diff --git a/ui/.build/src/build.ts b/ui/.build/src/build.ts index b44515e65e696..53da9d38711aa 100644 --- a/ui/.build/src/build.ts +++ b/ui/.build/src/build.ts @@ -73,6 +73,15 @@ export function prePackage(pkg: Package | undefined): void { export const quantize = (n?: number, factor = 10000) => Math.floor((n ?? 0) / factor) * factor; +export function zip(arr1: T[], arr2: U[]): [T, U][] { + const length = Math.min(arr1.length, arr2.length); + const result: [T, U][] = []; + for (let i = 0; i < length; i++) { + result.push([arr1[i], arr2[i]]); + } + return result; +} + function depsOne(pkgName: string): Package[] { const collect = (dep: string): string[] => [...(env.deps.get(dep) || []).flatMap(d => collect(d)), dep]; return unique(collect(pkgName).map(name => env.packages.get(name))); diff --git a/ui/.build/src/i18n.ts b/ui/.build/src/i18n.ts index dab92961007e5..8bf672b55b34d 100644 --- a/ui/.build/src/i18n.ts +++ b/ui/.build/src/i18n.ts @@ -4,13 +4,14 @@ import { XMLParser } from 'fast-xml-parser'; import { env, colors as c } from './main.ts'; import { globArray } from './parse.ts'; import { i18nManifest } from './manifest.ts'; -import { quantize } from './build.ts'; +import { quantize, zip } from './build.ts'; import { transform } from 'esbuild'; type Plural = { [key in 'zero' | 'one' | 'two' | 'few' | 'many' | 'other']?: string }; type Dict = Map; -type DictMap = Map>; +type DictMap = Map>; +let baseDicts: DictMap = new Map(); let locales: string[], dicts: string[]; let watchTimeout: NodeJS.Timeout | undefined; const i18nWatch: fs.FSWatcher[] = []; @@ -104,13 +105,10 @@ async function compileTypings(): Promise { const dictStats = await Promise.all(dicts.map(d => updated(d))); if (!tstat || dictStats.some(x => x && x.mtimeMs > tstat.mtimeMs)) { env.log(`Building ${c.grey('i18n')}`); - const dictMap = new Map(); - await Promise.all( - dicts.map(async d => - dictMap.set(d, parseXml(await fs.promises.readFile(path.join(env.i18nSrcDir, `${d}.xml`), 'utf8'))), - ), + dicts.forEach(d => + baseDicts.set(d, fs.promises.readFile(path.join(env.i18nSrcDir, `${d}.xml`), 'utf8').then(parseXml)), ); - await writeTypescript(dictMap); + await writeTypescript(new Map(zip(dicts, await Promise.all(baseDicts.values())))); } } @@ -143,14 +141,24 @@ async function updated(dict: string, locale?: string): Promise } async function writeJavascript(dict: string, locale?: string, xstat: fs.Stats | false = false) { - const translations = parseXml( - await fs.promises.readFile( - locale ? path.join(env.i18nDestDir, dict, `${locale}.xml`) : path.join(env.i18nSrcDir, `${dict}.xml`), - 'utf-8', + if (!locale || !baseDicts.has(dict)) + baseDicts.set( + dict, + fs.promises.readFile(path.join(env.i18nSrcDir, `${dict}.xml`), 'utf8').then(parseXml), + ); + + const translations = new Map([ + ...(await baseDicts.get(dict)!), + ...parseXml( + await fs.promises.readFile( + locale ? path.join(env.i18nDestDir, dict, `${locale}.xml`) : path.join(env.i18nSrcDir, `${dict}.xml`), + 'utf-8', + ), ), - ); + ]); + const jsInit = - dict === 'site' && !locale + dict === 'site' ? 'window.i18n=function(k){for(let v of Object.values(window.i18n))if(v[k])return v[k];return k};' : ''; const code = @@ -176,7 +184,7 @@ async function writeJavascript(dict: string, locale?: string, xstat: fs.Stats | return fs.promises.utimes(filename, xstat.mtime, xstat.mtime); } -async function writeTypescript(dictMap: DictMap) { +async function writeTypescript(dictMap: Map): Promise { const code = tsPrelude + [...dictMap] From 1d684b6ff79a195f4f92cc3c0c48d0db9e24ebd1 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Mon, 14 Oct 2024 21:09:35 -0500 Subject: [PATCH 3/9] source xml edits must now rebuild all locales in --watch mode --- ui/.build/src/build.ts | 9 - ui/.build/src/clean.ts | 2 +- ui/.build/src/i18n.ts | 152 ++++++++------ ui/@types/lichess/i18n.d.ts | 404 ++++++++++++++++++------------------ 4 files changed, 287 insertions(+), 280 deletions(-) diff --git a/ui/.build/src/build.ts b/ui/.build/src/build.ts index 53da9d38711aa..b44515e65e696 100644 --- a/ui/.build/src/build.ts +++ b/ui/.build/src/build.ts @@ -73,15 +73,6 @@ export function prePackage(pkg: Package | undefined): void { export const quantize = (n?: number, factor = 10000) => Math.floor((n ?? 0) / factor) * factor; -export function zip(arr1: T[], arr2: U[]): [T, U][] { - const length = Math.min(arr1.length, arr2.length); - const result: [T, U][] = []; - for (let i = 0; i < length; i++) { - result.push([arr1[i], arr2[i]]); - } - return result; -} - function depsOne(pkgName: string): Package[] { const collect = (dep: string): string[] => [...(env.deps.get(dep) || []).flatMap(d => collect(d)), dep]; return unique(collect(pkgName).map(name => env.packages.get(name))); diff --git a/ui/.build/src/clean.ts b/ui/.build/src/clean.ts index 70bf0331686fa..28ffe1b7123eb 100644 --- a/ui/.build/src/clean.ts +++ b/ui/.build/src/clean.ts @@ -34,5 +34,5 @@ export async function clean(globs?: string[]): Promise { } export async function deepClean(): Promise { - return clean(['ui/@types/lichess/site.d.ts', 'translation/js', ...allGlobs]); + return clean(['ui/@types/lichess/i18n.d.ts', 'translation/js', ...allGlobs]); } diff --git a/ui/.build/src/i18n.ts b/ui/.build/src/i18n.ts index 8bf672b55b34d..3ba984054745a 100644 --- a/ui/.build/src/i18n.ts +++ b/ui/.build/src/i18n.ts @@ -4,15 +4,14 @@ import { XMLParser } from 'fast-xml-parser'; import { env, colors as c } from './main.ts'; import { globArray } from './parse.ts'; import { i18nManifest } from './manifest.ts'; -import { quantize, zip } from './build.ts'; +import { quantize } from './build.ts'; import { transform } from 'esbuild'; type Plural = { [key in 'zero' | 'one' | 'two' | 'few' | 'many' | 'other']?: string }; type Dict = Map; -type DictMap = Map>; -let baseDicts: DictMap = new Map(); -let locales: string[], dicts: string[]; +let dicts: Map = new Map(); +let locales: string[], cats: string[]; let watchTimeout: NodeJS.Timeout | undefined; const i18nWatch: fs.FSWatcher[] = []; const isFormat = /%(?:[\d]\$)?s/; @@ -72,10 +71,10 @@ export function stopI18n(): void { i18nWatch.length = 0; } -export async function i18n(): Promise { +export async function i18n(isBoot = true): Promise { if (!env.i18n) return; - [locales, dicts] = ( + [locales, cats] = ( await Promise.all([ globArray('*.xml', { cwd: path.join(env.i18nDestDir, 'site'), absolute: false }), globArray('*.xml', { cwd: env.i18nSrcDir, absolute: false }), @@ -83,16 +82,17 @@ export async function i18n(): Promise { ).map(list => list.map(x => x.split('.')[0])); await compileTypings(); - compileJavascripts(); // no await + compileJavascripts(isBoot); // no await - if (!env.watch) return; + if (!isBoot || !env.watch) return; const onChange = () => { clearTimeout(watchTimeout); - watchTimeout = setTimeout(() => compileTypings().then(() => compileJavascripts(false)), 2000); + watchTimeout = setTimeout(() => i18n(false), 2000); }; i18nWatch.push(fs.watch(env.i18nSrcDir, onChange)); - for (const d of dicts) { + for (const d of cats) { + await fs.promises.mkdir(path.join(env.i18nDestDir, d)).catch(() => {}); i18nWatch.push(fs.watch(path.join(env.i18nDestDir, d), onChange)); } } @@ -102,25 +102,50 @@ async function compileTypings(): Promise { fs.promises.stat(path.join(env.typesDir, 'lichess', `i18n.d.ts`)).catch(() => undefined), fs.promises.mkdir(env.i18nJsDir).catch(() => {}), ]); - const dictStats = await Promise.all(dicts.map(d => updated(d))); - if (!tstat || dictStats.some(x => x && x.mtimeMs > tstat.mtimeMs)) { + const catStats = await Promise.all(cats.map(d => updated(d))); + + if (!tstat || catStats.some(x => x)) { env.log(`Building ${c.grey('i18n')}`); - dicts.forEach(d => - baseDicts.set(d, fs.promises.readFile(path.join(env.i18nSrcDir, `${d}.xml`), 'utf8').then(parseXml)), + dicts = new Map( + zip( + cats, + await Promise.all( + cats.map(d => fs.promises.readFile(path.join(env.i18nSrcDir, `${d}.xml`), 'utf8').then(parseXml)), + ), + ), ); - await writeTypescript(new Map(zip(dicts, await Promise.all(baseDicts.values())))); + const code = + tsPrelude + + [...dicts] + .map( + ([cat, dict]) => + ` ${cat}: {\n` + + [...dict.entries()] + .map(([k, v]) => { + const tpe = typeof v !== 'string' ? 'I18nPlural' : isFormat.test(v) ? 'I18nFormat' : 'string'; + const comment = typeof v === 'string' ? v.split('\n')[0] : v['other']?.split('\n')[0]; + return ` /** ${comment} */\n '${k}': ${tpe};`; + }) + .join('\n') + + '\n };\n', + ) + .join('') + + '}\n'; + return fs.promises.writeFile(path.join(env.typesDir, 'lichess', `i18n.d.ts`), code); } } async function compileJavascripts(dirty: boolean = true): Promise { - for (const dict of dicts) { + for (const cat of cats) { + const u = await updated(cat); + if (u) await writeJavascript(cat, undefined, u); await Promise.all( - [undefined, ...locales].map(locale => - updated(dict, locale).then(async xstat => { - if (!xstat) return; + locales.map(locale => + updated(cat, locale).then(xstat => { + if (!u && !xstat) return; if (!dirty) env.log(`Building ${c.grey('i18n')}`); dirty = true; - return writeJavascript(dict, locale, xstat); + return writeJavascript(cat, locale, xstat); }), ), ); @@ -128,44 +153,31 @@ async function compileJavascripts(dirty: boolean = true): Promise { if (dirty) i18nManifest(); } -async function updated(dict: string, locale?: string): Promise { - const xmlPath = locale - ? path.join(env.i18nDestDir, dict, `${locale}.xml`) - : path.join(env.i18nSrcDir, `${dict}.xml`); - const jsPath = path.join(env.i18nJsDir, `${dict}.${locale ?? 'en-GB'}.js`); - const [xml, js] = await Promise.allSettled([fs.promises.stat(xmlPath), fs.promises.stat(jsPath)]); - return xml.status === 'rejected' || - (js.status !== 'rejected' && quantize(xml.value.mtimeMs, 2000) === quantize(js.value.mtimeMs, 2000)) - ? false - : xml.value; -} - -async function writeJavascript(dict: string, locale?: string, xstat: fs.Stats | false = false) { - if (!locale || !baseDicts.has(dict)) - baseDicts.set( - dict, - fs.promises.readFile(path.join(env.i18nSrcDir, `${dict}.xml`), 'utf8').then(parseXml), +async function writeJavascript(cat: string, locale?: string, xstat: fs.Stats | false = false) { + if (!dicts.has(cat)) + dicts.set( + cat, + await fs.promises.readFile(path.join(env.i18nSrcDir, `${cat}.xml`), 'utf8').then(parseXml), ); const translations = new Map([ - ...(await baseDicts.get(dict)!), - ...parseXml( - await fs.promises.readFile( - locale ? path.join(env.i18nDestDir, dict, `${locale}.xml`) : path.join(env.i18nSrcDir, `${dict}.xml`), - 'utf-8', - ), - ), + ...dicts.get(cat)!, + ...(locale + ? await fs.promises + .readFile(path.join(env.i18nDestDir, cat, `${locale}.xml`), 'utf-8') + .catch(() => '') + .then(parseXml) + : []), ]); - const jsInit = - dict === 'site' + cat === 'site' ? 'window.i18n=function(k){for(let v of Object.values(window.i18n))if(v[k])return v[k];return k};' : ''; const code = jsPrelude + jsInit + - `if(!window.i18n.${dict})window.i18n.${dict}={};` + - `let i=window.i18n.${dict};` + + `if(!window.i18n.${cat})window.i18n.${cat}={};` + + `let i=window.i18n.${cat};` + [...translations] .map( ([k, v]) => @@ -178,35 +190,30 @@ async function writeJavascript(dict: string, locale?: string, xstat: fs.Stats | ) .join(';') + '})()'; - const filename = path.join(env.i18nJsDir, `${dict}.${locale ?? 'en-GB'}.js`); + + const filename = path.join(env.i18nJsDir, `${cat}.${locale ?? 'en-GB'}.js`); await fs.promises.writeFile(filename, code); + if (!xstat) return; return fs.promises.utimes(filename, xstat.mtime, xstat.mtime); } -async function writeTypescript(dictMap: Map): Promise { - const code = - tsPrelude + - [...dictMap] - .map( - ([dict, trans]) => - ` ${dict}: {\n` + - [...trans.entries()] - .map(([k, v]) => { - const tpe = typeof v !== 'string' ? 'I18nPlural' : isFormat.test(v) ? 'I18nFormat' : 'string'; - const comment = typeof v === 'string' ? v.split('\n')[0] : v['other']?.split('\n')[0]; - return ` /** ${comment} */\n '${k}': ${tpe};`; - }) - .join('\n') + - '\n };\n', - ) - .join('') + - '}\n'; - return fs.promises.writeFile(path.join(env.typesDir, 'lichess', `i18n.d.ts`), code); +async function updated(cat: string, locale?: string): Promise { + const xmlPath = locale + ? path.join(env.i18nDestDir, cat, `${locale}.xml`) + : path.join(env.i18nSrcDir, `${cat}.xml`); + const jsPath = path.join(env.i18nJsDir, `${cat}.${locale ?? 'en-GB'}.js`); + const [xml, js] = await Promise.allSettled([fs.promises.stat(xmlPath), fs.promises.stat(jsPath)]); + return xml.status === 'rejected' || + (js.status !== 'rejected' && quantize(xml.value.mtimeMs, 2000) <= quantize(js.value.mtimeMs, 2000)) + ? false + : xml.value; } function parseXml(xmlData: string): Map { const i18nMap: Map = new Map(); + if (!xmlData) return i18nMap; + const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '' }); const { string: strings, plurals } = parser.parse(xmlData).resources; for (const item of strings ? (Array.isArray(strings) ? strings : [strings]) : []) @@ -220,3 +227,12 @@ function parseXml(xmlData: string): Map { } return new Map([...i18nMap.entries()].sort(([a], [b]) => a.localeCompare(b))); } + +function zip(arr1: T[], arr2: U[]): [T, U][] { + const length = Math.min(arr1.length, arr2.length); + const result: [T, U][] = []; + for (let i = 0; i < length; i++) { + result.push([arr1[i], arr2[i]]); + } + return result; +} diff --git a/ui/@types/lichess/i18n.d.ts b/ui/@types/lichess/i18n.d.ts index 8f1b7eb54da0d..376e7f58087d5 100644 --- a/ui/@types/lichess/i18n.d.ts +++ b/ui/@types/lichess/i18n.d.ts @@ -9,7 +9,7 @@ interface I18nPlural { asArray: (quantity: number, ...args: T[]) => (T | string)[]; // vdomPlural } interface I18n { - /** Global noarg key lookup */ + /** Global noarg key lookup (only if absolutely necessary). */ (key: string): string; activity: { @@ -893,7 +893,7 @@ interface I18n { toConnectTheDgtBoard: I18nFormat; /** To see console message press Command + Option + C (Mac) or Control + Shift + C (Windows, Linux, Chrome OS) */ toSeeConsoleMessage: string; - /** Use \"%1$s\" unless %2$s is running on a different machine or different port. */ + /** Use "%1$s" unless %2$s is running on a different machine or different port. */ useWebSocketUrl: I18nFormat; /** You have an OAuth token suitable for DGT play. */ validDgtOauthToken: string; @@ -2622,206 +2622,6 @@ interface I18n { /** This account is closed. */ thisAccountIsClosed: string; }; - storm: { - /** Accuracy */ - accuracy: string; - /** All-time */ - allTime: string; - /** Best run of day */ - bestRunOfDay: string; - /** Click to reload */ - clickToReload: string; - /** Combo */ - combo: string; - /** Create a new game */ - createNewGame: string; - /** End run (hotkey: Enter) */ - endRun: string; - /** Failed puzzles */ - failedPuzzles: string; - /** Get ready! */ - getReady: string; - /** Highest solved */ - highestSolved: string; - /** Highscores */ - highscores: string; - /** Highscore: %s */ - highscoreX: I18nFormat; - /** Join a public race */ - joinPublicRace: string; - /** Join rematch */ - joinRematch: string; - /** Join the race! */ - joinTheRace: string; - /** Moves */ - moves: string; - /** Move to start */ - moveToStart: string; - /** New all-time highscore! */ - newAllTimeHighscore: string; - /** New daily highscore! */ - newDailyHighscore: string; - /** New monthly highscore! */ - newMonthlyHighscore: string; - /** New run (hotkey: Space) */ - newRun: string; - /** New weekly highscore! */ - newWeeklyHighscore: string; - /** Next race */ - nextRace: string; - /** Play again */ - playAgain: string; - /** Played %1$s runs of %2$s */ - playedNbRunsOfPuzzleStorm: I18nPlural; - /** Previous highscore was %s */ - previousHighscoreWasX: I18nFormat; - /** Puzzles played */ - puzzlesPlayed: string; - /** puzzles solved */ - puzzlesSolved: string; - /** Race complete! */ - raceComplete: string; - /** Race your friends */ - raceYourFriends: string; - /** Runs */ - runs: string; - /** Score */ - score: string; - /** skip */ - skip: string; - /** Skip this move to preserve your combo! Only works once per race. */ - skipExplanation: string; - /** You can skip one move per race: */ - skipHelp: string; - /** Skipped puzzle */ - skippedPuzzle: string; - /** Slow puzzles */ - slowPuzzles: string; - /** Spectating */ - spectating: string; - /** Start the race */ - startTheRace: string; - /** This month */ - thisMonth: string; - /** This run has expired! */ - thisRunHasExpired: string; - /** This run was opened in another tab! */ - thisRunWasOpenedInAnotherTab: string; - /** This week */ - thisWeek: string; - /** Time */ - time: string; - /** Time per move */ - timePerMove: string; - /** View best runs */ - viewBestRuns: string; - /** Wait for rematch */ - waitForRematch: string; - /** Waiting for more players to join... */ - waitingForMorePlayers: string; - /** Waiting to start */ - waitingToStart: string; - /** %s runs */ - xRuns: I18nPlural; - /** You play the black pieces in all puzzles */ - youPlayTheBlackPiecesInAllPuzzles: string; - /** You play the white pieces in all puzzles */ - youPlayTheWhitePiecesInAllPuzzles: string; - /** Your rank: %s */ - yourRankX: I18nFormat; - }; - streamer: { - /** All streamers */ - allStreamers: string; - /** Your stream is approved. */ - approved: string; - /** Become a Lichess streamer */ - becomeStreamer: string; - /** Change/delete your picture */ - changePicture: string; - /** Currently streaming: %s */ - currentlyStreaming: I18nFormat; - /** Download streamer kit */ - downloadKit: string; - /** Do you have a Twitch or YouTube channel? */ - doYouHaveStream: string; - /** Edit streamer page */ - editPage: string; - /** Headline */ - headline: string; - /** Here we go! */ - hereWeGo: string; - /** Keep it short: %s characters max */ - keepItShort: I18nPlural; - /** Last stream %s */ - lastStream: I18nFormat; - /** Lichess streamer */ - lichessStreamer: string; - /** Lichess streamers */ - lichessStreamers: string; - /** LIVE! */ - live: string; - /** Long description */ - longDescription: string; - /** Max size: %s */ - maxSize: I18nFormat; - /** OFFLINE */ - offline: string; - /** Optional. Leave empty if none */ - optionalOrEmpty: string; - /** Your stream is being reviewed by moderators. */ - pendingReview: string; - /** Get a flaming streamer icon on your Lichess profile. */ - perk1: string; - /** Get bumped up to the top of the streamers list. */ - perk2: string; - /** Notify your Lichess followers. */ - perk3: string; - /** Show your stream in your games, tournaments and studies. */ - perk4: string; - /** Benefits of streaming with the keyword */ - perks: string; - /** Please fill in your streamer information, and upload a picture. */ - pleaseFillIn: string; - /** request a moderator review */ - requestReview: string; - /** Include the keyword \"lichess.org\" in your stream title and use the category \"Chess\" when you stream on Lichess. */ - rule1: string; - /** Remove the keyword when you stream non-Lichess stuff. */ - rule2: string; - /** Lichess will detect your stream automatically and enable the following perks: */ - rule3: string; - /** Read our %s to ensure fair play for everyone during your stream. */ - rule4: I18nFormat; - /** Streaming rules */ - rules: string; - /** The Lichess streamer page targets your audience with the language provided by your streaming platform. Set the correct default language for your chess streams in the app or service you use to broadcast. */ - streamerLanguageSettings: string; - /** Your streamer name on Lichess */ - streamerName: string; - /** streaming Fairplay FAQ */ - streamingFairplayFAQ: string; - /** Tell us about your stream in one sentence */ - tellUsAboutTheStream: string; - /** Your Twitch username or URL */ - twitchUsername: string; - /** Upload a picture */ - uploadPicture: string; - /** Visible on the streamers page */ - visibility: string; - /** When approved by moderators */ - whenApproved: string; - /** When you are ready to be listed as a Lichess streamer, %s */ - whenReady: I18nFormat; - /** %s is streaming */ - xIsStreaming: I18nFormat; - /** %s streamer picture */ - xStreamerPicture: I18nFormat; - /** Your streamer page */ - yourPage: string; - /** Your YouTube channel ID */ - youTubeChannelId: string; - }; site: { /** Abort game */ abortGame: string; @@ -4552,6 +4352,206 @@ interface I18n { /** Zero advertisement */ zeroAdvertisement: string; }; + storm: { + /** Accuracy */ + accuracy: string; + /** All-time */ + allTime: string; + /** Best run of day */ + bestRunOfDay: string; + /** Click to reload */ + clickToReload: string; + /** Combo */ + combo: string; + /** Create a new game */ + createNewGame: string; + /** End run (hotkey: Enter) */ + endRun: string; + /** Failed puzzles */ + failedPuzzles: string; + /** Get ready! */ + getReady: string; + /** Highest solved */ + highestSolved: string; + /** Highscores */ + highscores: string; + /** Highscore: %s */ + highscoreX: I18nFormat; + /** Join a public race */ + joinPublicRace: string; + /** Join rematch */ + joinRematch: string; + /** Join the race! */ + joinTheRace: string; + /** Moves */ + moves: string; + /** Move to start */ + moveToStart: string; + /** New all-time highscore! */ + newAllTimeHighscore: string; + /** New daily highscore! */ + newDailyHighscore: string; + /** New monthly highscore! */ + newMonthlyHighscore: string; + /** New run (hotkey: Space) */ + newRun: string; + /** New weekly highscore! */ + newWeeklyHighscore: string; + /** Next race */ + nextRace: string; + /** Play again */ + playAgain: string; + /** Played %1$s runs of %2$s */ + playedNbRunsOfPuzzleStorm: I18nPlural; + /** Previous highscore was %s */ + previousHighscoreWasX: I18nFormat; + /** Puzzles played */ + puzzlesPlayed: string; + /** puzzles solved */ + puzzlesSolved: string; + /** Race complete! */ + raceComplete: string; + /** Race your friends */ + raceYourFriends: string; + /** Runs */ + runs: string; + /** Score */ + score: string; + /** skip */ + skip: string; + /** Skip this move to preserve your combo! Only works once per race. */ + skipExplanation: string; + /** You can skip one move per race: */ + skipHelp: string; + /** Skipped puzzle */ + skippedPuzzle: string; + /** Slow puzzles */ + slowPuzzles: string; + /** Spectating */ + spectating: string; + /** Start the race */ + startTheRace: string; + /** This month */ + thisMonth: string; + /** This run has expired! */ + thisRunHasExpired: string; + /** This run was opened in another tab! */ + thisRunWasOpenedInAnotherTab: string; + /** This week */ + thisWeek: string; + /** Time */ + time: string; + /** Time per move */ + timePerMove: string; + /** View best runs */ + viewBestRuns: string; + /** Wait for rematch */ + waitForRematch: string; + /** Waiting for more players to join... */ + waitingForMorePlayers: string; + /** Waiting to start */ + waitingToStart: string; + /** %s runs */ + xRuns: I18nPlural; + /** You play the black pieces in all puzzles */ + youPlayTheBlackPiecesInAllPuzzles: string; + /** You play the white pieces in all puzzles */ + youPlayTheWhitePiecesInAllPuzzles: string; + /** Your rank: %s */ + yourRankX: I18nFormat; + }; + streamer: { + /** All streamers */ + allStreamers: string; + /** Your stream is approved. */ + approved: string; + /** Become a Lichess streamer */ + becomeStreamer: string; + /** Change/delete your picture */ + changePicture: string; + /** Currently streaming: %s */ + currentlyStreaming: I18nFormat; + /** Download streamer kit */ + downloadKit: string; + /** Do you have a Twitch or YouTube channel? */ + doYouHaveStream: string; + /** Edit streamer page */ + editPage: string; + /** Headline */ + headline: string; + /** Here we go! */ + hereWeGo: string; + /** Keep it short: %s characters max */ + keepItShort: I18nPlural; + /** Last stream %s */ + lastStream: I18nFormat; + /** Lichess streamer */ + lichessStreamer: string; + /** Lichess streamers */ + lichessStreamers: string; + /** LIVE! */ + live: string; + /** Long description */ + longDescription: string; + /** Max size: %s */ + maxSize: I18nFormat; + /** OFFLINE */ + offline: string; + /** Optional. Leave empty if none */ + optionalOrEmpty: string; + /** Your stream is being reviewed by moderators. */ + pendingReview: string; + /** Get a flaming streamer icon on your Lichess profile. */ + perk1: string; + /** Get bumped up to the top of the streamers list. */ + perk2: string; + /** Notify your Lichess followers. */ + perk3: string; + /** Show your stream in your games, tournaments and studies. */ + perk4: string; + /** Benefits of streaming with the keyword */ + perks: string; + /** Please fill in your streamer information, and upload a picture. */ + pleaseFillIn: string; + /** request a moderator review */ + requestReview: string; + /** Include the keyword "lichess.org" in your stream title and use the category "Chess" when you stream on Lichess. */ + rule1: string; + /** Remove the keyword when you stream non-Lichess stuff. */ + rule2: string; + /** Lichess will detect your stream automatically and enable the following perks: */ + rule3: string; + /** Read our %s to ensure fair play for everyone during your stream. */ + rule4: I18nFormat; + /** Streaming rules */ + rules: string; + /** The Lichess streamer page targets your audience with the language provided by your streaming platform. Set the correct default language for your chess streams in the app or service you use to broadcast. */ + streamerLanguageSettings: string; + /** Your streamer name on Lichess */ + streamerName: string; + /** streaming Fairplay FAQ */ + streamingFairplayFAQ: string; + /** Tell us about your stream in one sentence */ + tellUsAboutTheStream: string; + /** Your Twitch username or URL */ + twitchUsername: string; + /** Upload a picture */ + uploadPicture: string; + /** Visible on the streamers page */ + visibility: string; + /** When approved by moderators */ + whenApproved: string; + /** When you are ready to be listed as a Lichess streamer, %s */ + whenReady: I18nFormat; + /** %s is streaming */ + xIsStreaming: I18nFormat; + /** %s streamer picture */ + xStreamerPicture: I18nFormat; + /** Your streamer page */ + yourPage: string; + /** Your YouTube channel ID */ + youTubeChannelId: string; + }; study: { /** Add members */ addMembers: string; From 1bf63868500d1e179441feba2a891bbf244f26c5 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Mon, 14 Oct 2024 21:33:32 -0500 Subject: [PATCH 4/9] generate i18n.d.ts that prettier likes --- ui/.build/src/build.ts | 2 +- ui/.build/src/i18n.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/.build/src/build.ts b/ui/.build/src/build.ts index b44515e65e696..5d383da46c5d5 100644 --- a/ui/.build/src/build.ts +++ b/ui/.build/src/build.ts @@ -38,9 +38,9 @@ export async function build(pkgs: string[]): Promise { fs.promises.mkdir(env.buildTempDir), ]); - monitor(pkgs); await Promise.all([sass(), copies(), i18n()]); await esbuild(tsc()); + monitor(pkgs); } export async function stop(): Promise { diff --git a/ui/.build/src/i18n.ts b/ui/.build/src/i18n.ts index 3ba984054745a..19b1f5ade9b69 100644 --- a/ui/.build/src/i18n.ts +++ b/ui/.build/src/i18n.ts @@ -124,7 +124,7 @@ async function compileTypings(): Promise { .map(([k, v]) => { const tpe = typeof v !== 'string' ? 'I18nPlural' : isFormat.test(v) ? 'I18nFormat' : 'string'; const comment = typeof v === 'string' ? v.split('\n')[0] : v['other']?.split('\n')[0]; - return ` /** ${comment} */\n '${k}': ${tpe};`; + return ` /** ${comment} */\n ${k}: ${tpe};`; }) .join('\n') + '\n };\n', From 70d2d1e5416a32f0e6a708030db0eda4a4a263f4 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 15 Oct 2024 07:44:44 -0500 Subject: [PATCH 5/9] match prettier on codegen --- ui/.build/src/i18n.ts | 57 +++++++++++++++++++++---------------- ui/@types/lichess/i18n.d.ts | 2 +- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/ui/.build/src/i18n.ts b/ui/.build/src/i18n.ts index 19b1f5ade9b69..ef557da09a4db 100644 --- a/ui/.build/src/i18n.ts +++ b/ui/.build/src/i18n.ts @@ -18,13 +18,13 @@ const isFormat = /%(?:[\d]\$)?s/; const tsPrelude = `// Generated interface I18nFormat { - (...args: (string | number)[]): string; - asArray: (...args: T[]) => (T | string)[], // vdom + (...args: (string | number)[]): string; // formatted + asArray: (...args: T[]) => (T | string)[]; // vdom } interface I18nPlural { - (quantity: number, ...args: (string | number)[]): string, // pluralSame - raw: (quantity: number, ...args: (string | number)[]) => string, // plural - asArray: (quantity: number, ...args: T[]) => (T | string)[], // vdomPlural + (quantity: number, ...args: (string | number)[]): string; // pluralSame + raw: (quantity: number, ...args: (string | number)[]) => string; // plural + asArray: (quantity: number, ...args: T[]) => (T | string)[]; // vdomPlural } interface I18n { /** Global noarg key lookup (only if absolutely necessary). */ @@ -98,8 +98,9 @@ export async function i18n(isBoot = true): Promise { } async function compileTypings(): Promise { + const typingsPathname = path.join(env.typesDir, 'lichess', `i18n.d.ts`); const [tstat] = await Promise.all([ - fs.promises.stat(path.join(env.typesDir, 'lichess', `i18n.d.ts`)).catch(() => undefined), + fs.promises.stat(typingsPathname).catch(() => undefined), fs.promises.mkdir(env.i18nJsDir).catch(() => {}), ]); const catStats = await Promise.all(cats.map(d => updated(d))); @@ -114,24 +115,32 @@ async function compileTypings(): Promise { ), ), ); - const code = + await fs.promises.writeFile( + typingsPathname, tsPrelude + - [...dicts] - .map( - ([cat, dict]) => - ` ${cat}: {\n` + - [...dict.entries()] - .map(([k, v]) => { - const tpe = typeof v !== 'string' ? 'I18nPlural' : isFormat.test(v) ? 'I18nFormat' : 'string'; - const comment = typeof v === 'string' ? v.split('\n')[0] : v['other']?.split('\n')[0]; - return ` /** ${comment} */\n ${k}: ${tpe};`; - }) - .join('\n') + - '\n };\n', - ) - .join('') + - '}\n'; - return fs.promises.writeFile(path.join(env.typesDir, 'lichess', `i18n.d.ts`), code); + [...dicts] + .map( + ([cat, dict]) => + ` ${cat}: {\n` + + [...dict.entries()] + .map(([k, v]) => { + if (!/^[A-Za-z_]\w*$/.test(k)) k = `'${k}'`; + const tpe = + typeof v !== 'string' ? 'I18nPlural' : isFormat.test(v) ? 'I18nFormat' : 'string'; + const comment = typeof v === 'string' ? v.split('\n')[0] : v['other']?.split('\n')[0]; + return ` /** ${comment} */\n ${k}: ${tpe};`; + }) + .join('\n') + + '\n };\n', + ) + .join('') + + '}\n', + ); + const mstat = catStats.reduce( + (a, b) => (a && b && quantize(a.mtimeMs) > quantize(b.mtimeMs) ? a : b), + tstat || false, + ); + if (mstat) await fs.promises.utimes(typingsPathname, mstat.mtime, mstat.mtime); } } @@ -205,7 +214,7 @@ async function updated(cat: string, locale?: string): Promise const jsPath = path.join(env.i18nJsDir, `${cat}.${locale ?? 'en-GB'}.js`); const [xml, js] = await Promise.allSettled([fs.promises.stat(xmlPath), fs.promises.stat(jsPath)]); return xml.status === 'rejected' || - (js.status !== 'rejected' && quantize(xml.value.mtimeMs, 2000) <= quantize(js.value.mtimeMs, 2000)) + (js.status !== 'rejected' && quantize(xml.value.mtimeMs) <= quantize(js.value.mtimeMs)) ? false : xml.value; } diff --git a/ui/@types/lichess/i18n.d.ts b/ui/@types/lichess/i18n.d.ts index 376e7f58087d5..6ac9d4d23994f 100644 --- a/ui/@types/lichess/i18n.d.ts +++ b/ui/@types/lichess/i18n.d.ts @@ -1,6 +1,6 @@ // Generated interface I18nFormat { - (...args: (string | number)[]): string; + (...args: (string | number)[]): string; // formatted asArray: (...args: T[]) => (T | string)[]; // vdom } interface I18nPlural { From 849fbdf3070b6bdeb29ba72c60fc4d3336a049ee Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Tue, 15 Oct 2024 07:51:33 -0500 Subject: [PATCH 6/9] tighter quanitzation for stat.mtime compares --- ui/.build/src/build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/.build/src/build.ts b/ui/.build/src/build.ts index 5d383da46c5d5..62b32e256531c 100644 --- a/ui/.build/src/build.ts +++ b/ui/.build/src/build.ts @@ -71,7 +71,7 @@ export function prePackage(pkg: Package | undefined): void { }); } -export const quantize = (n?: number, factor = 10000) => Math.floor((n ?? 0) / factor) * factor; +export const quantize = (n?: number, factor = 2000) => Math.floor((n ?? 0) / factor) * factor; function depsOne(pkgName: string): Package[] { const collect = (dep: string): string[] => [...(env.deps.get(dep) || []).flatMap(d => collect(d)), dep]; From 9d106f409fdd6bd030d234ebdd6e662b94c534ea Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Thu, 17 Oct 2024 17:06:59 +0200 Subject: [PATCH 7/9] fix package.json multilog --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 13e73e5753b3e..eb67806d694f5 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,6 @@ "serverlog": "pnpm journal & pnpm metals", "piece-css": "pnpx tsx bin/gen/piece-css.ts", "trans-dump": "pnpx tsx bin/trans-dump.ts", - "multilog": "pnpm serverlog & ui/build -r" + "multilog": "pnpm serverlog & ui/build" } } From 6cd70ada15fd0ec76c11466aa88bf362c7c93cf5 Mon Sep 17 00:00:00 2001 From: Thibault Duplessis Date: Thu, 17 Oct 2024 17:07:25 +0200 Subject: [PATCH 8/9] ui.Page.i18nModules type safety with lila.core.i18n.I18nModule --- app/views/analyse/replay.scala | 2 +- app/views/msg.scala | 2 +- app/views/study.scala | 2 +- modules/analyse/src/main/ui/AnalyseUi.scala | 3 +-- modules/coordinate/src/main/CoordinateUi.scala | 4 +--- modules/coreI18n/src/main/modules.scala | 9 +++++++++ modules/practice/src/main/PracticeUi.scala | 2 +- modules/puzzle/src/main/ui/PuzzleUi.scala | 4 +--- modules/racer/src/main/ui/RacerUi.scala | 2 +- modules/relay/src/main/ui/RelayUi.scala | 3 +-- modules/storm/src/main/ui/StormUi.scala | 2 +- modules/swiss/src/main/ui/SwissShow.scala | 4 +--- .../tournament/src/main/ui/TournamentShow.scala | 6 +++--- modules/ui/src/main/Page.scala | 16 +++++++++------- modules/web/src/main/ui/layout.scala | 8 +++++--- 15 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 modules/coreI18n/src/main/modules.scala diff --git a/app/views/analyse/replay.scala b/app/views/analyse/replay.scala index 38b1f3d42d567..bd555564a4648 100644 --- a/app/views/analyse/replay.scala +++ b/app/views/analyse/replay.scala @@ -78,7 +78,7 @@ def replay( .css((pov.game.variant == Crazyhouse).option("analyse.zh")) .css(ctx.blind.option("round.nvui")) .css(ctx.pref.hasKeyboardMove.option("keyboardMove")) - .i18n("puzzle", "study") + .i18n(_.puzzle, _.study) .js(analyseNvuiTag) .js( bits.analyseModule( diff --git a/app/views/msg.scala b/app/views/msg.scala index 6a5c6e9d99dc9..e4ad75585a35f 100644 --- a/app/views/msg.scala +++ b/app/views/msg.scala @@ -7,7 +7,7 @@ import lila.app.UiEnv.{ *, given } def home(json: JsObject)(using Context) = Page(trans.site.inbox.txt()) .css("msg") - .i18n("challenge") + .i18n(_.challenge) .js(PageModule("msg", Json.obj("data" -> json))) .csp(_.withInlineIconFont): main(cls := "box msg-app") diff --git a/app/views/study.scala b/app/views/study.scala index ada2c0e1dd1aa..0396ad0fb6f28 100644 --- a/app/views/study.scala +++ b/app/views/study.scala @@ -50,7 +50,7 @@ def show( Page(s.name.value) .css("analyse.study") .css(ctx.pref.hasKeyboardMove.option("keyboardMove")) - .i18n("study") + .i18n(_.study) .js(analyseNvuiTag) .js( PageModule( diff --git a/modules/analyse/src/main/ui/AnalyseUi.scala b/modules/analyse/src/main/ui/AnalyseUi.scala index 642fb1c1a081b..b89d5adb33a03 100644 --- a/modules/analyse/src/main/ui/AnalyseUi.scala +++ b/modules/analyse/src/main/ui/AnalyseUi.scala @@ -23,8 +23,7 @@ final class AnalyseUi(helpers: Helpers)(externalEngineEndpoint: String): .css(ctx.blind.option("round.nvui")) .css(ctx.pref.hasKeyboardMove.option("keyboardMove")) .csp(csp.compose(_.withExternalAnalysisApis)) - .i18n("puzzle") - .i18n("study") + .i18n(_.puzzle, _.study) .graph( title = "Chess analysis board", url = s"$netBaseUrl${routes.UserAnalysis.index.url}", diff --git a/modules/coordinate/src/main/CoordinateUi.scala b/modules/coordinate/src/main/CoordinateUi.scala index 99dec8eeb5382..16ea8688c5899 100644 --- a/modules/coordinate/src/main/CoordinateUi.scala +++ b/modules/coordinate/src/main/CoordinateUi.scala @@ -15,9 +15,7 @@ final class CoordinateUi(helpers: Helpers): Page(trans.coordinates.coordinateTraining.txt()) .css("coordinateTrainer") .css("voice") - .i18n("coordinates") - .i18n("storm") - .i18n("study") + .i18n(_.coordinates, _.storm, _.study) .js(pageModule(scoreOption)) .csp(_.withPeer.withWebAssembly) .graph( diff --git a/modules/coreI18n/src/main/modules.scala b/modules/coreI18n/src/main/modules.scala new file mode 100644 index 0000000000000..b1fa4c7b7f59f --- /dev/null +++ b/modules/coreI18n/src/main/modules.scala @@ -0,0 +1,9 @@ +package lila.core.i18n + +enum I18nModule: + case site, arena, emails, learn, activity, coordinates, study, `class`, contact, appeal, patron, coach, + broadcast, streamer, tfa, settings, preferences, team, perfStat, search, tourname, faq, lag, swiss, + puzzle, puzzleTheme, challenge, storm, ublog, insight, keyboardMove, timeago, oauthScope, dgt, + voiceCommands, onboarding, features +object I18nModule: + type Selector = I18nModule.type => I18nModule diff --git a/modules/practice/src/main/PracticeUi.scala b/modules/practice/src/main/PracticeUi.scala index 479c0edef9cb2..011c8ab745db9 100644 --- a/modules/practice/src/main/PracticeUi.scala +++ b/modules/practice/src/main/PracticeUi.scala @@ -19,7 +19,7 @@ final class PracticeUi(helpers: Helpers)( def show(us: UserStudy, data: JsonView.JsData)(using Context) = Page(us.practiceStudy.name.value) .css("analyse.practice") - .i18n("study") + .i18n(_.study) .js(analyseNvuiTag) .js( PageModule( diff --git a/modules/puzzle/src/main/ui/PuzzleUi.scala b/modules/puzzle/src/main/ui/PuzzleUi.scala index 21818848be5ec..d4966d998c3a6 100644 --- a/modules/puzzle/src/main/ui/PuzzleUi.scala +++ b/modules/puzzle/src/main/ui/PuzzleUi.scala @@ -30,9 +30,7 @@ final class PuzzleUi(helpers: Helpers, val bits: PuzzleBits)( .css(ctx.pref.hasKeyboardMove.option("keyboardMove")) .css(ctx.pref.hasVoice.option("voice")) .css(ctx.blind.option("round.nvui")) - .i18n("puzzle") - .i18n("puzzleTheme") - .i18n("storm") + .i18n(_.puzzle, _.puzzleTheme, _.storm) .js(ctx.blind.option(Esm("puzzle.nvui"))) .js( PageModule( diff --git a/modules/racer/src/main/ui/RacerUi.scala b/modules/racer/src/main/ui/RacerUi.scala index 5bfa96986a25a..1a5127b753bc0 100644 --- a/modules/racer/src/main/ui/RacerUi.scala +++ b/modules/racer/src/main/ui/RacerUi.scala @@ -34,7 +34,7 @@ final class RacerUi(helpers: Helpers): def show(data: JsObject)(using Context) = Page("Puzzle Racer") .css("racer") - .i18n("storm") + .i18n(_.storm) .js(PageModule("racer", data)) .zoom .zen: diff --git a/modules/relay/src/main/ui/RelayUi.scala b/modules/relay/src/main/ui/RelayUi.scala index 978fb5093d091..8b760b3cafd5d 100644 --- a/modules/relay/src/main/ui/RelayUi.scala +++ b/modules/relay/src/main/ui/RelayUi.scala @@ -28,8 +28,7 @@ final class RelayUi(helpers: Helpers)( )(using ctx: Context) = Page(rt.fullName) .css("analyse.relay") - .i18n("study") - .i18n("broadcast") + .i18n(_.study, _.broadcast) .js(analyseNvuiTag) .js(pageModule(rt, data, chatOption, socketVersion)) .zoom diff --git a/modules/storm/src/main/ui/StormUi.scala b/modules/storm/src/main/ui/StormUi.scala index 7dd883e6410e9..32fb366477dc1 100644 --- a/modules/storm/src/main/ui/StormUi.scala +++ b/modules/storm/src/main/ui/StormUi.scala @@ -15,7 +15,7 @@ final class StormUi(helpers: Helpers): def home(data: JsObject, high: Option[StormHigh])(using Context) = Page("Puzzle Storm") .css("storm") - .i18n("storm") + .i18n(_.storm) .js(PageModule("storm", data)) .zoom .zen diff --git a/modules/swiss/src/main/ui/SwissShow.scala b/modules/swiss/src/main/ui/SwissShow.scala index 5575d0c7b52e7..b7efe5433dd49 100644 --- a/modules/swiss/src/main/ui/SwissShow.scala +++ b/modules/swiss/src/main/ui/SwissShow.scala @@ -33,9 +33,7 @@ final class SwissShow(helpers: Helpers, ui: SwissBitsUi, gathering: GatheringUi) Page(fullName(s, team)) .css("swiss.show") .css(hasScheduleInput.option("bits.flatpickr")) - .i18n("study") - .i18n("swiss") - .i18n("team") + .i18n(_.study, _.swiss, _.team) .js(hasScheduleInput.option(Esm("bits.flatpickr"))) .js( PageModule( diff --git a/modules/tournament/src/main/ui/TournamentShow.scala b/modules/tournament/src/main/ui/TournamentShow.scala index 2070259205622..9278035e132a1 100644 --- a/modules/tournament/src/main/ui/TournamentShow.scala +++ b/modules/tournament/src/main/ui/TournamentShow.scala @@ -27,9 +27,9 @@ final class TournamentShow(helpers: Helpers, ui: TournamentUi, gathering: Gather val extraCls = tour.schedule.so: sched => s" tour-sched tour-sched-${sched.freq.name} tour-speed-${sched.speed.name} tour-variant-${sched.variant.key} tour-id-${tour.id}" Page(s"${tour.name()} #${tour.id}") - .i18n("study", "swiss") - .i18n(tour.isTeamBattle.option("team")) - .i18n(tour.isTeamBattle.option("arena")) + .i18n(_.study, _.swiss) + .i18nOpt(tour.isTeamBattle, _.team) + .i18nOpt(tour.isTeamBattle, _.arena) .js( PageModule( "tournament", diff --git a/modules/ui/src/main/Page.scala b/modules/ui/src/main/Page.scala index c3acf60bad0c7..187989dc7123c 100644 --- a/modules/ui/src/main/Page.scala +++ b/modules/ui/src/main/Page.scala @@ -1,5 +1,6 @@ package lila.ui +import lila.core.i18n.I18nModule import ScalatagsTemplate.* opaque type LangPath = String @@ -22,7 +23,7 @@ case class Page( fullTitle: Option[String] = None, robots: Boolean = true, cssKeys: List[String] = Nil, - i18nModules: List[String] = List("site", "timeago", "preferences"), + i18nModules: List[I18nModule.Selector] = List(_.site, _.timeago, _.preferences), modules: EsmList = Nil, jsFrag: Option[WithNonce[Frag]] = None, pageModule: Option[PageModule] = None, @@ -42,12 +43,13 @@ case class Page( def js(f: Option[WithNonce[Frag]]): Page = f.foldLeft(this)(_.js(_)) def js(pm: PageModule): Page = copy(pageModule = pm.some) @scala.annotation.targetName("jsModuleOption") - def js(pm: Option[PageModule]): Page = copy(pageModule = pm) - def iife(iifeFrag: Frag): Page = js(_ => iifeFrag) - def iife(iifeFrag: Option[Frag]): Page = iifeFrag.foldLeft(this)(_.iife(_)) - def i18n(cats: String*): Page = copy(i18nModules = i18nModules ::: cats.toList) - def i18n(cat: Option[String]) = copy(i18nModules = i18nModules ++ cat) - def graph(og: OpenGraph): Page = copy(openGraph = og.some) + def js(pm: Option[PageModule]): Page = copy(pageModule = pm) + def iife(iifeFrag: Frag): Page = js(_ => iifeFrag) + def iife(iifeFrag: Option[Frag]): Page = iifeFrag.foldLeft(this)(_.iife(_)) + def i18n(mods: I18nModule.Selector*): Page = copy(i18nModules = i18nModules ::: mods.toList) + def i18nOpt(cond: Boolean, mod: => I18nModule.Selector) = + if cond then copy(i18nModules = i18nModules.appended(mod)) else this + def graph(og: OpenGraph): Page = copy(openGraph = og.some) def graph(title: String, description: String, url: String): Page = graph(OpenGraph(title, description, url)) def robots(b: Boolean): Page = copy(robots = b) def css(keys: String*): Page = copy(cssKeys = cssKeys ::: keys.toList) diff --git a/modules/web/src/main/ui/layout.scala b/modules/web/src/main/ui/layout.scala index bf43bf4cd4e5d..7fe3d7e96fa40 100644 --- a/modules/web/src/main/ui/layout.scala +++ b/modules/web/src/main/ui/layout.scala @@ -3,7 +3,7 @@ package ui import play.api.i18n.Lang -import lila.core.i18n.Language +import lila.core.i18n.{ I18nModule, Language } import lila.core.report.ScoreThresholds import lila.ui.* @@ -141,8 +141,10 @@ final class layout(helpers: Helpers, assetHelper: lila.web.ui.AssetFullHelper)( ) ) - def sitePreload(i18nDicts: List[String], modules: EsmList, isInquiry: Boolean)(using ctx: Context) = - val i18nModules = i18nDicts.map(dict => s"i18n/$dict.${ctx.lang.code}") + def sitePreload(i18nMods: List[I18nModule.Selector], modules: EsmList, isInquiry: Boolean)(using + ctx: Context + ) = + val i18nModules = i18nMods.map(mod => s"i18n/${mod(I18nModule)}.${ctx.lang.code}") scriptsPreload( i18nModules ::: "site" :: (isInquiry.option("mod.inquiry") :: modules.map(_.map(_.key))).flatten ) From 26012ee8ee4c9a84322948baf9b6fad4e7fe86e6 Mon Sep 17 00:00:00 2001 From: Jonathan Gamble Date: Thu, 17 Oct 2024 15:32:24 -0500 Subject: [PATCH 9/9] remove unused, unimplemented method from typing --- ui/.build/src/i18n.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ui/.build/src/i18n.ts b/ui/.build/src/i18n.ts index ef557da09a4db..ae70e5a156812 100644 --- a/ui/.build/src/i18n.ts +++ b/ui/.build/src/i18n.ts @@ -23,8 +23,7 @@ interface I18nFormat { } interface I18nPlural { (quantity: number, ...args: (string | number)[]): string; // pluralSame - raw: (quantity: number, ...args: (string | number)[]) => string; // plural - asArray: (quantity: number, ...args: T[]) => (T | string)[]; // vdomPlural + asArray: (quantity: number, ...args: T[]) => (T | string)[]; // vdomPlural / plural } interface I18n { /** Global noarg key lookup (only if absolutely necessary). */