diff --git a/.github/workflows/assets.yml b/.github/workflows/assets.yml
index 6020034c1352e..84a11b474adf6 100644
--- a/.github/workflows/assets.yml
+++ b/.github/workflows/assets.yml
@@ -47,10 +47,9 @@ jobs:
- run: ./ui/build --no-install -p
- run: cd ui && pnpm run test && cd -
- run: mkdir assets && mv public assets/ && cp bin/download-lifat LICENSE COPYING.md README.md assets/ && git log -n 1 --pretty=oneline > assets/commit.txt
- - run: cd assets && tar -cvpJf ../assets.tar.xz . && cd -
- env:
- XZ_OPT: '-0'
- - uses: actions/upload-artifact@v3
+ - run: cd assets && tar --zstd -cvpf ../assets.tar.zst . && cd -
+ - uses: actions/upload-artifact@v4
with:
name: lila-assets
- path: assets.tar.xz
+ path: assets.tar.zst
+ compression-level: 0 # already compressed
diff --git a/.github/workflows/flair.yml b/.github/workflows/flair.yml
new file mode 100644
index 0000000000000..67725139814e1
--- /dev/null
+++ b/.github/workflows/flair.yml
@@ -0,0 +1,16 @@
+name: Validate Flair
+
+on:
+ push:
+ paths:
+ - 'public/flair/**'
+ pull_request:
+ paths:
+ - 'public/flair/**'
+
+jobs:
+ validate-flair:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - run: ./bin/validate-flair public/flair/img/
diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml
index 99cbbecf0f960..473ae8e3ea2ca 100644
--- a/.github/workflows/server.yml
+++ b/.github/workflows/server.yml
@@ -41,10 +41,11 @@ jobs:
- run: TZ=UTC git log -1 --date=iso-strict-local --pretty='format:app.version.commit = "%H"%napp.version.date = "%ad"%napp.version.message = """%s"""%n' | tee conf/version.conf
- run: ./lila -Depoll=true "test;stage"
- run: cp LICENSE COPYING.md README.md target/universal/stage && git log -n 1 --pretty=oneline > target/universal/stage/commit.txt
- - run: cd target/universal/stage && tar -cvpJf ../../../lila-3.0.tar.xz . && cd -
+ - run: cd target/universal/stage && tar --zstd -cvpf ../../../lila-3.0.tar.zst . && cd -
env:
- XZ_OPT: '-0'
- - uses: actions/upload-artifact@v3
+ ZSTD_LEVEL: 1 # most files are already zipped
+ - uses: actions/upload-artifact@v4
with:
name: lila-server
- path: lila-3.0.tar.xz
+ path: lila-3.0.tar.zst
+ compression-level: 0 # already compressed
diff --git a/app/controllers/Api.scala b/app/controllers/Api.scala
index 600ffc7019339..89b7163d93e21 100644
--- a/app/controllers/Api.scala
+++ b/app/controllers/Api.scala
@@ -351,7 +351,7 @@ final class Api(
def mobileGames = Scoped(_.Web.Mobile) { _ ?=> _ ?=>
val ids = get("ids").so(_.split(',').take(50).toList) map GameId.take
ids.nonEmpty.so:
- env.round.roundSocket.getMany(ids).flatMap(env.round.mobile.json).map(JsonOk)
+ env.round.roundSocket.getMany(ids).flatMap(env.round.mobile.online).map(JsonOk)
}
def ApiRequest(js: Context ?=> Fu[ApiResult]) = Anon:
diff --git a/app/controllers/Appeal.scala b/app/controllers/Appeal.scala
index 719c40be2ef4d..e9e56938f6df0 100644
--- a/app/controllers/Appeal.scala
+++ b/app/controllers/Appeal.scala
@@ -31,7 +31,12 @@ final class Appeal(env: Env, reportC: => report.Report, prismicC: => Prismic, us
)(using Context)(using me: Me): Fu[Frag] = env.appeal.api.byId(me) flatMap {
case None =>
renderAsync:
- env.playban.api.currentBan(me).dmap(_.isDefined) map { html.appeal.tree(me, _) }
+ for
+ playban <- env.playban.api.currentBan(me).dmap(_.isDefined)
+ // if no blog, consider it's visible because even if it is not, for now the user
+ // has not been negatively impacted
+ ublogIsVisible <- env.ublog.api.isBlogVisible(me.userId).dmap(_.getOrElse(true))
+ yield html.appeal.tree(me, playban, ublogIsVisible)
case Some(a) => renderPage(html.appeal.discussion(a, me, err | userForm))
}
diff --git a/app/controllers/DailyFeed.scala b/app/controllers/DailyFeed.scala
index 34a01e7135a5c..d9b56cee4de1d 100644
--- a/app/controllers/DailyFeed.scala
+++ b/app/controllers/DailyFeed.scala
@@ -14,13 +14,10 @@ final class DailyFeed(env: Env) extends LilaController(env):
def index = Open:
for
- updates <- api.recent(Max(50))
+ updates <- api.recent
page <- renderPage(html.dailyFeed.index(updates))
yield Ok(page)
- private def get(day: String): Fu[Option[Update]] =
- scala.util.Try(LocalDate.parse(day)).toOption.so(api.get)
-
def createForm = Secure(_.DailyFeed) { _ ?=> _ ?=>
Ok.pageAsync(html.dailyFeed.create(api.form(none)))
}
@@ -31,31 +28,34 @@ final class DailyFeed(env: Env) extends LilaController(env):
.bindFromRequest()
.fold(
err => BadRequest.pageAsync(html.dailyFeed.create(err)),
- up => api.set(up, none) inject Redirect(routes.DailyFeed.edit(up.day)).flashSuccess
+ data =>
+ val up = data toUpdate none
+ api.set(up) inject Redirect(routes.DailyFeed.edit(up.id)).flashSuccess
)
}
- def edit(day: String) = Secure(_.DailyFeed) { _ ?=> _ ?=>
- Found(get(day)): up =>
+ def edit(id: String) = Secure(_.DailyFeed) { _ ?=> _ ?=>
+ Found(api.get(id)): up =>
Ok.pageAsync(html.dailyFeed.edit(api.form(up.some), up))
}
- def update(day: String) = SecureBody(_.DailyFeed) { _ ?=> _ ?=>
- Found(get(day)): from =>
+ def update(id: String) = SecureBody(_.DailyFeed) { _ ?=> _ ?=>
+ Found(api.get(id)): from =>
api
.form(from.some)
.bindFromRequest()
.fold(
err => BadRequest.pageAsync(html.dailyFeed.edit(err, from)),
- up => api.set(up, from.some) inject Redirect(routes.DailyFeed.edit(up.day)).flashSuccess
+ data =>
+ api.set(data toUpdate from.id.some) inject Redirect(routes.DailyFeed.edit(from.id)).flashSuccess
)
}
- def delete(day: String) = Secure(_.DailyFeed) { _ ?=> _ ?=>
- Found(get(day)): up =>
- api.delete(up.day) inject Redirect(routes.DailyFeed.index).flashSuccess
+ def delete(id: String) = Secure(_.DailyFeed) { _ ?=> _ ?=>
+ Found(api.get(id)): up =>
+ api.delete(up.id) inject Redirect(routes.DailyFeed.index).flashSuccess
}
def atom = Anon:
- api.recent(Max(50)) map: ups =>
+ api.recentPublished map: ups =>
Ok(html.dailyFeed.atom(ups)) as XML
diff --git a/app/controllers/Dev.scala b/app/controllers/Dev.scala
index aecb23c38d2eb..2c07c4e968514 100644
--- a/app/controllers/Dev.scala
+++ b/app/controllers/Dev.scala
@@ -33,7 +33,10 @@ final class Dev(env: Env) extends LilaController(env):
env.tutor.nbAnalysisSetting,
env.tutor.parallelismSetting,
env.firefoxOriginTrial,
- env.credentiallessUaRegex
+ env.credentiallessUaRegex,
+ env.relay.proxyDomainRegex,
+ env.relay.proxyHostPort,
+ env.relay.proxyCredentials
)
def settings = Secure(_.Settings) { _ ?=> _ ?=>
diff --git a/app/controllers/Game.scala b/app/controllers/Game.scala
index 7a1e5347db7d9..66d6a6da8400f 100644
--- a/app/controllers/Game.scala
+++ b/app/controllers/Game.scala
@@ -28,10 +28,10 @@ final class Game(env: Env, apiC: => Api) extends LilaController(env):
else Redirect(routes.Round.watcher(game.id, game.naturalOrientation.name))
}
- def exportOne(id: GameAnyId) = Anon:
+ def exportOne(id: GameAnyId) = AnonOrScoped():
exportGame(id.gameId)
- private[controllers] def exportGame(gameId: GameId)(using req: RequestHeader): Fu[Result] =
+ private[controllers] def exportGame(gameId: GameId)(using Context): Fu[Result] =
env.round.proxyRepo.gameIfPresent(gameId) orElse env.game.gameRepo.game(gameId) flatMap {
case None => NotFound
case Some(game) =>
@@ -54,17 +54,17 @@ final class Game(env: Env, apiC: => Api) extends LilaController(env):
private def handleExport(username: UserStr)(using ctx: Context) =
env.user.repo byId username flatMap {
- _.filter(u => u.enabled.yes || ctx.me.exists(_ is u) || isGrantedOpt(_.GamesModView)) so { user =>
+ _.filter(u => u.enabled.yes || ctx.is(u) || isGrantedOpt(_.GamesModView)) so { user =>
val format = GameApiV2.Format byRequest req
import lila.rating.{ Perf, PerfType }
WithVs: vs =>
env.security.ipTrust
- .throttle(MaxPerSecond(ctx.me match
- case Some(m) if m is lila.user.User.explorerId => env.apiExplorerGamesPerSecond.get()
- case Some(m) if m is user.id => 60
- case Some(_) if ctx.isOAuth => 30 // bonus for oauth logged in only (not for CSRF)
- case _ => 25
- ))
+ .throttle(MaxPerSecond:
+ if ctx is lila.user.User.explorerId then env.apiExplorerGamesPerSecond.get()
+ else if ctx is user then 60
+ else if ctx.isOAuth then 30 // bonus for oauth logged in only (not for CSRF)
+ else 25
+ )
.flatMap: perSecond =>
val finished = getBoolOpt("finished") | true
val config = GameApiV2.ByUserConfig(
@@ -87,7 +87,7 @@ final class Game(env: Env, apiC: => Api) extends LilaController(env):
ongoing = getBool("ongoing") || !finished,
finished = finished
)
- if ctx.me.exists(_ is lila.user.User.explorerId) then
+ if ctx.is(lila.user.User.explorerId) then
Ok.chunked(env.api.gameApiV2.exportByUser(config))
.pipe(noProxyBuffer)
.as(gameContentType(config))
diff --git a/app/controllers/GameMod.scala b/app/controllers/GameMod.scala
index 8b6bdcd709cfa..39e809ab04624 100644
--- a/app/controllers/GameMod.scala
+++ b/app/controllers/GameMod.scala
@@ -77,7 +77,7 @@ final class GameMod(env: Env)(using akka.stream.Materializer) extends LilaContro
}.parallel >> env.fishnet.awaiter(games.map(_.id), 2 minutes)
} inject NoContent
- private def downloadPgn(user: lila.user.User, gameIds: Seq[GameId]) =
+ private def downloadPgn(user: lila.user.User, gameIds: Seq[GameId])(using Option[Me]) =
Ok.chunked {
env.api.gameApiV2.exportByIds(
GameApiV2.ByIdsConfig(
diff --git a/app/controllers/LilaController.scala b/app/controllers/LilaController.scala
index 2432a1c904e90..70943d98505cc 100644
--- a/app/controllers/LilaController.scala
+++ b/app/controllers/LilaController.scala
@@ -238,7 +238,7 @@ abstract private[controllers] class LilaController(val env: Env)
def handleScopedFail(accepted: EndpointScopes, e: OAuthServer.AuthError)(using RequestHeader) = e match
case e @ lila.oauth.OAuthServer.MissingScope(available) =>
OAuthServer.responseHeaders(accepted, available):
- Forbidden(jsonError(e.message))
+ forbiddenJson(e.message)
case e =>
OAuthServer.responseHeaders(accepted, TokenScopes(Nil)):
Unauthorized(jsonError(e.message))
@@ -331,6 +331,8 @@ abstract private[controllers] class LilaController(val env: Env)
.flatMap:
f(using _)
+ given (using req: RequestHeader): lila.chat.AllMessages = lila.chat.AllMessages(HTTPRequest.isLitools(req))
+
/* We roll our own action, as we don't want to compose play Actions. */
private def action[A](parser: BodyParser[A])(handler: Request[A] ?=> Fu[Result]): EssentialAction = new:
import play.api.libs.streams.Accumulator
diff --git a/app/controllers/Mod.scala b/app/controllers/Mod.scala
index c13a130ea9c61..8b35beceab7e8 100644
--- a/app/controllers/Mod.scala
+++ b/app/controllers/Mod.scala
@@ -195,13 +195,16 @@ final class Mod(
}
}
- def createNameCloseVote(username: UserStr) = SendToZulip(username, env.irc.api.nameCloseVote)
- def askUsertableCheck(username: UserStr) = SendToZulip(username, env.irc.api.usertableCheck)
+ def createNameCloseVote(username: UserStr) = Secure(_.SendToZulip) { _ ?=> me ?=>
+ env.report.api.inquiries ofModId me.id map {
+ _.filter(_.reason == lila.report.Reason.Username).map(_.bestAtom.simplifiedText)
+ } flatMap: reason =>
+ env.user.repo byId username orNotFound { env.irc.api.nameCloseVote(_, reason) inject NoContent }
- private def SendToZulip(username: UserStr, method: UserModel => Me ?=> Funit) =
- Secure(_.SendToZulip) { _ ?=> _ ?=>
- env.user.repo byId username orNotFound { method(_) inject NoContent }
- }
+ }
+ def askUsertableCheck(username: UserStr) = Secure(_.SendToZulip) { _ ?=> _ ?=>
+ env.user.repo byId username orNotFound { env.irc.api.usertableCheck(_) inject NoContent }
+ }
def table = Secure(_.Admin) { ctx ?=> _ ?=>
Ok.pageAsync:
diff --git a/app/controllers/Push.scala b/app/controllers/Push.scala
index 60322c46590a7..7fc585422061d 100644
--- a/app/controllers/Push.scala
+++ b/app/controllers/Push.scala
@@ -5,11 +5,11 @@ import lila.push.WebSubscription
final class Push(env: Env) extends LilaController(env):
- def mobileRegister(platform: String, deviceId: String) = Auth { ctx ?=> me ?=>
+ def mobileRegister(platform: String, deviceId: String) = AuthOrScoped(_.Web.Mobile) { ctx ?=> me ?=>
env.push.registerDevice(me, platform, deviceId) inject NoContent
}
- def mobileUnregister = Auth { ctx ?=> me ?=>
+ def mobileUnregister = AuthOrScoped(_.Web.Mobile) { ctx ?=> me ?=>
env.push.unregisterDevices(me) inject NoContent
}
diff --git a/app/controllers/RelayRound.scala b/app/controllers/RelayRound.scala
index 521a5080c663c..38ba974a47de3 100644
--- a/app/controllers/RelayRound.scala
+++ b/app/controllers/RelayRound.scala
@@ -9,6 +9,8 @@ import lila.common.HTTPRequest
import lila.relay.{ RelayRound as RoundModel, RelayRoundForm, RelayTour as TourModel }
import chess.format.pgn.PgnStr
import views.*
+import lila.common.config.{ Max, MaxPerSecond }
+import play.api.libs.json.Json
final class RelayRound(
env: Env,
@@ -42,12 +44,11 @@ final class RelayRound(
),
setup =>
rateLimitCreation(whenRateLimited):
- env.relay.api.create(setup, tour) flatMap { round =>
+ env.relay.api.create(setup, tour) flatMap: rt =>
negotiate(
- Redirect(routes.RelayRound.show(tour.slug, round.slug, round.id.value)),
- JsonOk(env.relay.jsonView.withUrl(round withTour tour))
+ Redirect(routes.RelayRound.show(tour.slug, rt.relay.slug, rt.relay.id)),
+ JsonOk(env.relay.jsonView.myRound(rt))
)
- }
)
}
@@ -101,18 +102,28 @@ final class RelayRound(
else env.study.api byIdWithChapter rt.round.studyId
sc orNotFound { doShow(rt, _) }
,
- json = Found(env.relay.api.byIdWithTour(id)): rt =>
- Found(env.study.studyRepo.byId(rt.round.studyId)): study =>
- studyC.CanView(study)(
- env.study.chapterRepo orderedMetadataByStudy rt.round.studyId map { games =>
- JsonOk(env.relay.jsonView.withUrlAndGames(rt, games))
- }
- )(studyC.privateUnauthorizedJson, studyC.privateForbiddenJson)
+ json = doApiShow(id)
)
+ def apiShow(ts: String, rs: String, id: RelayRoundId) = AnonOrScoped(_.Study.Read):
+ doApiShow(id)
+
+ private def doApiShow(id: RelayRoundId)(using Context): Fu[Result] =
+ Found(env.relay.api.byIdWithTour(id)): rt =>
+ Found(env.study.studyRepo.byId(rt.round.studyId)): study =>
+ studyC.CanView(study)(
+ env.study.chapterRepo orderedMetadataByStudy rt.round.studyId map: games =>
+ JsonOk(env.relay.jsonView.withUrlAndGames(rt withStudy study, games))
+ )(studyC.privateUnauthorizedJson, studyC.privateForbiddenJson)
+
def pgn(ts: String, rs: String, id: StudyId) = studyC.pgn(id)
def apiPgn = studyC.apiPgn
+ def apiMyRounds = Scoped(_.Study.Read) { ctx ?=> _ ?=>
+ val source = env.relay.api.myRounds(MaxPerSecond(20), getIntAs[Max]("nb")).map(env.relay.jsonView.myRound)
+ apiC.GlobalConcurrencyLimitPerIP.download(ctx.ip)(source)(apiC.sourceToNdJson)
+ }
+
def stream(id: RelayRoundId) = AnonOrScoped(): ctx ?=>
Found(env.relay.api.byIdWithStudy(id)): rt =>
studyC.CanView(rt.study) {
@@ -127,10 +138,16 @@ final class RelayRound(
def push(id: RelayRoundId) = ScopedBody(parse.tolerantText)(Seq(_.Study.Write)) { ctx ?=> me ?=>
env.relay.api
- .byIdAndContributor(id)
+ .byIdWithStudy(id)
.flatMap:
- case None => notFoundJson()
- case Some(rt) => env.relay.push(rt, PgnStr(ctx.body.body)) inject jsonOkResult
+ case None => notFoundJson()
+ case Some(rt) if !rt.study.canContribute(me) => forbiddenJson()
+ case Some(rt) =>
+ env.relay
+ .push(rt.withTour, PgnStr(ctx.body.body))
+ .map:
+ case Right(moves) => JsonOk(Json.obj("moves" -> moves))
+ case Left(e) => JsonBadRequest(e.message)
}
private def WithRoundAndTour(@nowarn ts: String, @nowarn rs: String, id: RelayRoundId)(
diff --git a/app/controllers/Setup.scala b/app/controllers/Setup.scala
index 9fd72ca8c6c48..71a3a342ddefe 100644
--- a/app/controllers/Setup.scala
+++ b/app/controllers/Setup.scala
@@ -78,7 +78,7 @@ final class Setup(
val message = lila.challenge.ChallengeDenied.translated(denied)
negotiate(
// 403 tells setupCtrl.ts to close the setup modal
- Forbidden(jsonError(message)), // TODO test
+ forbiddenJson(message), // TODO test
BadRequest(jsonError(message))
)
case None =>
diff --git a/app/controllers/Streamer.scala b/app/controllers/Streamer.scala
index 32b898d3c088c..4d6fe9e6a7201 100644
--- a/app/controllers/Streamer.scala
+++ b/app/controllers/Streamer.scala
@@ -24,10 +24,10 @@ final class Streamer(env: Env, apiC: => Api) extends LilaController(env):
page <- renderPage(html.streamer.index(live, pager, requests))
yield Ok(page)
- def featured = Anon:
+ def featured = Anon: ctx ?=>
env.streamer.liveStreamApi.all.map: streams =>
val max = env.streamer.homepageMaxSetting.get()
- val featured = streams.homepage(max, req, none) withTitles env.user.lightUserApi
+ val featured = streams.homepage(max, ctx.acceptLanguages) withTitles env.user.lightUserApi
JsonOk:
featured.live.streams.map: s =>
Json.obj(
diff --git a/app/controllers/Study.scala b/app/controllers/Study.scala
index a354a8096aa03..c5ede8e8fd788 100644
--- a/app/controllers/Study.scala
+++ b/app/controllers/Study.scala
@@ -539,7 +539,7 @@ final class Study(
)
def privateForbiddenText = Forbidden("This study is now private")
- def privateForbiddenJson = Forbidden(jsonError("This study is now private"))
+ def privateForbiddenJson = forbiddenJson("This study is now private")
def privateForbiddenFu(study: StudyModel)(using Context) = negotiate(
Forbidden.page(html.site.message.privateStudy(study)),
privateForbiddenJson
diff --git a/app/controllers/Ublog.scala b/app/controllers/Ublog.scala
index 1745c58eb6714..3ece241718f9d 100644
--- a/app/controllers/Ublog.scala
+++ b/app/controllers/Ublog.scala
@@ -1,11 +1,13 @@
package controllers
import play.api.i18n.Lang
+import play.api.data.Form
+import play.api.data.Forms.*
import views.*
import lila.app.{ given, * }
import lila.common.config
-import lila.i18n.{ I18nLangPicker, LangList }
+import lila.i18n.{ I18nLangPicker, LangList, Language }
import lila.report.Suspect
import lila.ublog.{ UblogBlog, UblogPost }
import lila.user.{ User as UserModel }
@@ -164,9 +166,8 @@ final class Ublog(env: Env) extends LilaController(env):
def like(id: UblogPostId, v: Boolean) = Auth { ctx ?=> _ ?=>
NoBot:
NotForKids:
- env.ublog.rank.like(id, v) map { likes =>
+ env.ublog.rank.like(id, v) map: likes =>
Ok(likes.value)
- }
}
def redirect(id: UblogPostId) = Open:
@@ -189,6 +190,23 @@ final class Ublog(env: Env) extends LilaController(env):
)
}
+ def rankAdjust(postId: String) = SecureBody(_.ModerateBlog) { ctx ?=> me ?=>
+ Found(env.ublog.api.getPost(UblogPostId(postId))): post =>
+ Form:
+ single:
+ "value" -> optional(number)
+ .bindFromRequest()
+ .fold(
+ _ => Redirect(urlOfPost(post)).flashFailure,
+ rankAdjustDays =>
+ for
+ _ <- env.ublog.api.setRankAdjust(post.id, ~rankAdjustDays)
+ _ <- env.mod.logApi.ublogRankAdjust(post.created.by, post.id, ~rankAdjustDays)
+ _ <- env.ublog.rank.recomputePostRank(post)
+ yield Redirect(urlOfPost(post)).flashSuccess
+ )
+ }
+
private val ImageRateLimitPerIp = lila.memo.RateLimit.composite[lila.common.IpAddress](
key = "ublog.image.ip"
)(
@@ -220,9 +238,9 @@ final class Ublog(env: Env) extends LilaController(env):
env.ublog.paginator.liveByFollowed(me, page) map html.ublog.index.friends
}
- def communityLang(language: String, page: Int = 1) = Open:
+ def communityLang(langStr: String, page: Int = 1) = Open:
import I18nLangPicker.ByHref
- I18nLangPicker.byHref(language, ctx.req) match
+ I18nLangPicker.byHref(langStr, ctx.req) match
case ByHref.NotFound => Redirect(routes.Ublog.communityAll(page))
case ByHref.Redir(code) => Redirect(routes.Ublog.communityLang(code, page))
case ByHref.Refused(lang) => communityIndex(lang.some, page)
@@ -233,24 +251,19 @@ final class Ublog(env: Env) extends LilaController(env):
def communityAll(page: Int) = Open:
communityIndex(none, page)
- def communityIndex(l: Option[Lang], page: Int)(using ctx: Context) =
+ private def communityIndex(l: Option[Lang], page: Int)(using ctx: Context) =
NotForKids:
Reasonable(page, config.Max(100)):
pageHit
Ok.pageAsync:
- env.ublog.paginator.liveByCommunity(l, page) map {
- html.ublog.index.community(l, _)
- }
-
- def communityLangBC(code: String) = Anon:
- val l = LangList.popularNoRegion.find(_.code == code)
- Redirect:
- l.fold(routes.Ublog.communityAll())(l => routes.Ublog.communityLang(l.language))
+ val language = l.map(Language.apply)
+ env.ublog.paginator.liveByCommunity(language, page) map:
+ html.ublog.index.community(language, _)
def communityAtom(language: String) = Anon:
val l = LangList.popularNoRegion.find(l => l.language == language || l.code == language)
env.ublog.paginator
- .liveByCommunity(l, page = 1)
+ .liveByCommunity(l.map(Language.apply), page = 1)
.map: posts =>
Ok(html.ublog.atom.community(language, posts.currentPageResults)) as XML
@@ -258,9 +271,8 @@ final class Ublog(env: Env) extends LilaController(env):
NotForKids:
Reasonable(page, config.Max(100)):
Ok.pageAsync:
- env.ublog.paginator.liveByLiked(page) map {
+ env.ublog.paginator.liveByLiked(page) map:
html.ublog.index.liked(_)
- }
}
def topics = Open:
@@ -272,12 +284,10 @@ final class Ublog(env: Env) extends LilaController(env):
def topic(str: String, page: Int, byDate: Boolean) = Open:
NotForKids:
Reasonable(page, config.Max(100)):
- lila.ublog.UblogTopic.fromUrl(str) so { top =>
+ lila.ublog.UblogTopic.fromUrl(str) so: top =>
Ok.pageAsync:
- env.ublog.paginator.liveByTopic(top, page, byDate) map {
+ env.ublog.paginator.liveByTopic(top, page, byDate) map:
html.ublog.index.topic(top, _, byDate)
- }
- }
def userAtom(username: UserStr) = Anon:
env.user.repo
@@ -288,9 +298,8 @@ final class Ublog(env: Env) extends LilaController(env):
env.ublog.api
.getUserBlog(user)
.flatMap: blog =>
- (isBlogVisible(user, blog) so env.ublog.paginator.byUser(user, true, 1)) map { posts =>
+ (isBlogVisible(user, blog) so env.ublog.paginator.byUser(user, true, 1)) map: posts =>
Ok(html.ublog.atom.user(user, posts.currentPageResults)) as XML
- }
private def isBlogVisible(user: UserModel, blog: UblogBlog) = user.enabled.yes && blog.visible
diff --git a/app/controllers/Video.scala b/app/controllers/Video.scala
index b2c3b3d27045c..debf9ec83a5f3 100644
--- a/app/controllers/Video.scala
+++ b/app/controllers/Video.scala
@@ -37,7 +37,7 @@ final class Video(env: Env) extends LilaController(env):
def show(id: String) = Open:
WithUserControl: control =>
- api.video.find(id) flatMap {
+ api.video.find(id) flatMap:
case None => NotFound.page(html.video.bits.notFound(control))
case Some(video) =>
api.video.similar(ctx.me, video, 9) zip
@@ -46,7 +46,6 @@ final class Video(env: Env) extends LilaController(env):
} flatMap { (similar, _) =>
Ok.page(html.video.show(video, similar, control))
}
- }
def author(author: String) = Open:
WithUserControl: control =>
diff --git a/app/http/CtrlErrors.scala b/app/http/CtrlErrors.scala
index 28fb57d720276..2d7312bf1d33e 100644
--- a/app/http/CtrlErrors.scala
+++ b/app/http/CtrlErrors.scala
@@ -15,6 +15,9 @@ trait CtrlErrors extends ControllerHelpers:
def notFoundJson(msg: String = "Not found"): Result = NotFound(jsonError(msg)) as JSON
def notFoundText(msg: String = "Not found"): Result = Results.NotFound(msg)
+ def forbiddenJson(msg: String = "You can't do that"): Result = Forbidden(jsonError(msg)) as JSON
+ def forbiddenText(msg: String = "You can't do that"): Result = Results.Forbidden(msg)
+
private val jsonGlobalErrorRenamer: Reads[JsObject] =
import play.api.libs.json.*
__.json update (
diff --git a/app/http/KeyPages.scala b/app/http/KeyPages.scala
index c6fcabfe36283..d9a206877b09c 100644
--- a/app/http/KeyPages.scala
+++ b/app/http/KeyPages.scala
@@ -28,7 +28,7 @@ final class KeyPages(val env: Env)(using Executor)
.flatMap(env.tournament.featuring.homepage.get)
.recoverDefault,
swiss = env.swiss.feature.onHomepage.getUnit.getIfPresent,
- events = env.event.api.promoteTo(ctx.req).recoverDefault,
+ events = env.event.api.promoteTo(ctx.acceptLanguages).recoverDefault,
simuls = env.simul.allCreatedFeaturable.get {}.recoverDefault,
streamerSpots = env.streamer.homepageMaxSetting.get()
)
diff --git a/app/http/ResponseBuilder.scala b/app/http/ResponseBuilder.scala
index 3de0941095cfb..f89b8bccec85e 100644
--- a/app/http/ResponseBuilder.scala
+++ b/app/http/ResponseBuilder.scala
@@ -56,8 +56,9 @@ trait ResponseBuilder(using Executor)
json = TooManyRequests(jsonError(msg))
)
- val jsonOkBody = Json.obj("ok" -> true)
- val jsonOkResult = JsonOk(jsonOkBody)
+ val jsonOkBody = Json.obj("ok" -> true)
+ val jsonOkResult = JsonOk(jsonOkBody)
+ def jsonOkMsg(msg: String) = JsonOk(Json.obj("ok" -> msg))
def JsonOk(body: JsValue): Result = Ok(body) as JSON
def JsonOk[A: Writes](body: A): Result = Ok(Json toJson body) as JSON
@@ -101,16 +102,14 @@ trait ResponseBuilder(using Executor)
Unauthorized(jsonError("Login required"))
)
- private val forbiddenJsonResult = Forbidden(jsonError("Authorization failed"))
-
def authorizationFailed(using ctx: Context): Fu[Result] =
if HTTPRequest.isSynchronousHttp(ctx.req)
then Forbidden.page(views.html.site.message.authFailed)
else
fuccess:
render:
- case Accepts.Json() => forbiddenJsonResult
- case _ => Results.Forbidden("Authorization failed")
+ case Accepts.Json() => forbiddenJson()
+ case _ => forbiddenText()
def serverError(msg: String)(using ctx: Context): Fu[Result] =
negotiate(
@@ -120,12 +119,12 @@ trait ResponseBuilder(using Executor)
def notForBotAccounts(using Context) = negotiate(
Forbidden.page(views.html.site.message.noBot),
- Forbidden(jsonError("This API endpoint is not for Bot accounts."))
+ forbiddenJson("This API endpoint is not for Bot accounts.")
)
def notForLameAccounts(using Context, Me) = negotiate(
Forbidden.page(views.html.site.message.noLame),
- Forbidden(jsonError("The access to this resource is restricted."))
+ forbiddenJson("The access to this resource is restricted.")
)
def playbanJsonError(ban: lila.playban.TempBan) =
diff --git a/app/mashup/Preload.scala b/app/mashup/Preload.scala
index fb40f3bb9bf3c..e64ec227b59b0 100644
--- a/app/mashup/Preload.scala
+++ b/app/mashup/Preload.scala
@@ -30,7 +30,8 @@ final class Preload(
lightUserApi: LightUserApi,
roundProxy: lila.round.GameProxyRepo,
simulIsFeaturable: SimulIsFeaturable,
- getLastUpdate: lila.blog.DailyFeed.GetLastUpdate,
+ lastPostCache: lila.blog.LastPostCache,
+ getLastUpdates: lila.blog.DailyFeed.GetLastUpdates,
lastPostsCache: AsyncLoadingCache[Unit, List[UblogPost.PreviewPost]],
msgApi: lila.msg.MsgApi,
relayApi: lila.relay.RelayApi,
@@ -74,7 +75,7 @@ final class Preload(
tourWinners.all.dmap(_.top).mon(_.lobby segment "tourWinners") zip
(ctx.noBot so dailyPuzzle()).mon(_.lobby segment "puzzle") zip
(ctx.kid.no so liveStreamApi.all
- .dmap(_.homepage(streamerSpots, ctx.req, ctx.me.flatMap(_.lang)) withTitles lightUserApi)
+ .dmap(_.homepage(streamerSpots, ctx.acceptLanguages) withTitles lightUserApi)
.mon(_.lobby segment "streams")) zip
(ctx.userId so playbanApi.currentBan).mon(_.lobby segment "playban") zip
(ctx.blind so ctx.me so roundProxy.urgentGames) zip
@@ -105,7 +106,8 @@ final class Preload(
currentGame,
simulIsFeaturable,
blindGames,
- getLastUpdate(),
+ lastPostCache.apply.filterNot(_.isOld).filter(_.forKids || ctx.kid.no),
+ getLastUpdates(),
ublogPosts,
withPerfs,
hasUnreadLichessMessage = lichessMsg
@@ -148,7 +150,8 @@ object Preload:
currentGame: Option[Preload.CurrentGame],
isFeaturable: Simul => Boolean,
blindGames: List[Pov],
- lastUpdate: Option[lila.blog.DailyFeed.Update],
+ lastPost: Option[lila.blog.MiniPost],
+ lastUpdates: List[lila.blog.DailyFeed.Update],
ublogPosts: List[UblogPost.PreviewPost],
me: Option[User.WithPerfs],
hasUnreadLichessMessage: Boolean
diff --git a/app/router.scala b/app/router.scala
index d3121b0d85d35..2f4c9d5cd263d 100644
--- a/app/router.scala
+++ b/app/router.scala
@@ -70,6 +70,7 @@ object ReverseRouterConversions:
given Conversion[UserId, UserStr] = _ into UserStr
given Conversion[ForumCategId, String] = _.value
given Conversion[ForumTopicId, String] = _.value
+ given Conversion[lila.i18n.Language, String] = _.value
given challengeIdConv: Conversion[Challenge.Id, String] = _.value
given appealIdConv: Conversion[Appeal.Id, String] = _.value
given reportIdConv: Conversion[Report.Id, String] = _.value
diff --git a/app/templating/DateHelper.scala b/app/templating/DateHelper.scala
index 6fc503a17e66a..86581731e413c 100644
--- a/app/templating/DateHelper.scala
+++ b/app/templating/DateHelper.scala
@@ -35,10 +35,10 @@ trait DateHelper:
_ => DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM).withLocale(lang.toLocale)
)
- def showInstantUTC(instant: Instant)(using Lang): String =
+ def showInstant(instant: Instant)(using Lang): String =
dateTimeFormatter print instant
- def showDate(instant: Instant)(using lang: Lang): String =
+ def showDate(instant: Instant)(using Lang): String =
showDate(instant.date)
def showDate(date: LocalDate)(using lang: Lang): String =
@@ -81,8 +81,8 @@ trait DateHelper:
def secondsFromNow(seconds: Int, alwaysRelative: Boolean = false): Tag =
momentFromNow(nowInstant plusSeconds seconds, alwaysRelative)
- def momentFromNowServer(instant: Instant): Frag =
- timeTag(title := f"${showEnglishInstant(instant)} UTC")(momentFromNowServerText(instant))
+ def momentFromNowServer(instant: Instant)(using Lang): Frag =
+ timeTag(title := f"${showInstant(instant)} UTC")(momentFromNowServerText(instant))
def momentFromNowServerText(instant: Instant, inFuture: Boolean = false): String =
val (dateSec, nowSec) = (instant.toMillis / 1000, nowSeconds)
@@ -101,3 +101,6 @@ trait DateHelper:
else if months == 0 then s"${pluralize("week", weeks)}$preposition"
else if years == 0 then s"${pluralize("month", months)}$preposition"
else s"${pluralize("year", years)}$preposition"
+
+ def timeRemaining(instant: Instant, once: Boolean = false): Tag =
+ timeTag(cls := s"timeago remaining${once so " once"}", datetimeAttr := isoDateTime(instant))(nbsp)
diff --git a/app/templating/Environment.scala b/app/templating/Environment.scala
index 7d997b3581920..d844c210758d8 100644
--- a/app/templating/Environment.scala
+++ b/app/templating/Environment.scala
@@ -20,7 +20,8 @@ object Environment
with TeamHelper
with TournamentHelper
with FlashHelper
- with ChessgroundHelper:
+ with ChessgroundHelper
+ with HtmlHelper:
export lila.Lila.{ id as _, *, given }
export lila.api.Context.{ *, given }
@@ -59,7 +60,3 @@ object Environment
env.report.scoreThresholdsSetting.get().mid,
env.report.scoreThresholdsSetting.get().high
)
-
- val spinner: Frag = raw(
- """
"""
- )
diff --git a/app/templating/FormHelper.scala b/app/templating/FormHelper.scala
index 4545f1a6396dd..7ff4902f12c56 100644
--- a/app/templating/FormHelper.scala
+++ b/app/templating/FormHelper.scala
@@ -270,6 +270,7 @@ trait FormHelper:
details(cls := "form-control emoji-details")(
summary(cls := "button button-metal button-no-upper")(
trans.setFlair(),
+ ":",
nbsp,
view
),
diff --git a/app/templating/HtmlHelper.scala b/app/templating/HtmlHelper.scala
new file mode 100644
index 0000000000000..fc2d12fe0389a
--- /dev/null
+++ b/app/templating/HtmlHelper.scala
@@ -0,0 +1,15 @@
+package lila.app
+package templating
+
+import lila.app.ui.ScalatagsTemplate.*
+
+trait HtmlHelper:
+
+ def renderCache[A](ttl: FiniteDuration)(toFrag: A => Frag): A => Frag =
+ val cache = lila.memo.CacheApi.scaffeineNoScheduler
+ .expireAfterWrite(1 minute)
+ .build[A, String]()
+ from => raw(cache.get(from, from => toFrag(from).render))
+
+ val spinner: Frag = raw:
+ """"""
diff --git a/app/templating/I18nHelper.scala b/app/templating/I18nHelper.scala
index 866091aea5187..4d3477c291003 100644
--- a/app/templating/I18nHelper.scala
+++ b/app/templating/I18nHelper.scala
@@ -33,6 +33,6 @@ trait I18nHelper:
if ctx.isAuth || ctx.lang.language == "en"
then path
else
- val code = lila.i18n.fixJavaLanguageCode(ctx.lang)
+ val code = lila.i18n.fixJavaLanguage(ctx.lang)
if path == "/" then s"/$code"
else s"/$code$path"
diff --git a/app/templating/SetupHelper.scala b/app/templating/SetupHelper.scala
index 15548b570e4ec..7d0f9455fdc6b 100644
--- a/app/templating/SetupHelper.scala
+++ b/app/templating/SetupHelper.scala
@@ -259,7 +259,7 @@ trait SetupHelper:
(Pref.SubmitMove.CORRESPONDENCE, trans.correspondence.txt()),
(Pref.SubmitMove.CLASSICAL, trans.classical.txt()),
(Pref.SubmitMove.RAPID, trans.rapid.txt()),
- (Pref.SubmitMove.BLITZ, "Blitz")
+ (Pref.SubmitMove.BLITZ, trans.blitz.txt())
)
def confirmResignChoices(using Lang) =
diff --git a/app/views/appeal/tree.scala b/app/views/appeal/tree.scala
index eac10831a03ff..943c50fa33810 100644
--- a/app/views/appeal/tree.scala
+++ b/app/views/appeal/tree.scala
@@ -23,6 +23,7 @@ object tree:
val accountMuted = "Your account is muted.";
val excludedFromLeaderboards = "Your account has been excluded from leaderboards.";
val closedByModerators = "Your account was closed by moderators.";
+ val hiddenBlog = "Your blogs have been hidden by moderators."
private def cleanMenu(using PageContext): Branch =
Branch(
@@ -251,6 +252,39 @@ object tree:
)
)
+ private def hiddenBlogMenu(using PageContext): Branch =
+ val accept =
+ "I accept that I have broken the blog rules"
+ val deny =
+ "I deny having broken the blog rules."
+ Branch(
+ "root",
+ hiddenBlog,
+ List(
+ Leaf(
+ "hidden-blog-accept",
+ accept,
+ frag(
+ sendUsAnAppeal,
+ newAppeal(accept)
+ )
+ ),
+ Leaf(
+ "hidden-blog-deny",
+ deny,
+ frag(
+ sendUsAnAppeal,
+ newAppeal(deny)
+ )
+ )
+ ),
+ content = frag(
+ "Make sure to read again our ",
+ a(href := routes.ContentPage.loneBookmark("blog-etiquette"))("blog rules"),
+ "."
+ ).some
+ )
+
private def prizebanMenu(using PageContext): Branch =
val prizebanExpired = "My ban duration has expired, as I was informed by moderators."
val deny = "I reject any allegation of wrongdoing that may have prompted a prizeban."
@@ -338,10 +372,11 @@ object tree:
newAppeal()
)
- def apply(me: User, playban: Boolean)(using ctx: PageContext) =
+ def apply(me: User, playban: Boolean, ublogIsVisible: Boolean)(using ctx: PageContext) =
bits.layout("Appeal a moderation decision") {
- val query = isGranted(_.Appeals) so ctx.req.queryString.toMap
- val isMarked = playban || me.marks.engine || me.marks.boost || me.marks.troll || me.marks.rankban
+ val query = isGranted(_.Appeals) so ctx.req.queryString.toMap.pp
+ val isMarked =
+ playban || me.marks.engine || me.marks.boost || me.marks.troll || me.marks.rankban || me.marks.arenaBan || me.marks.prizeban || !ublogIsVisible
main(cls := "page page-small box box-pad appeal force-ltr")(
h1(cls := "box__top")("Appeal"),
div(cls := s"nav-tree${if isMarked then " marked" else ""}")(
@@ -356,6 +391,7 @@ object tree:
else if me.marks.rankban || query.contains("rankban") then rankBanMenu
else if me.marks.arenaBan || query.contains("arenaban") then arenaBanMenu
else if me.marks.prizeban || query.contains("prizeban") then prizebanMenu
+ else if !ublogIsVisible || query.contains("blog") then hiddenBlogMenu
else cleanMenu
},
none,
diff --git a/app/views/base/layout.scala b/app/views/base/layout.scala
index 19828ee3fd50c..cf9b3e98f8317 100644
--- a/app/views/base/layout.scala
+++ b/app/views/base/layout.scala
@@ -184,14 +184,14 @@ object layout:
ctx.pref.bg == lila.pref.Pref.Bg.SYSTEM option embedJsUnsafe(systemThemePolyfillJs)
)
- private def hrefLang(lang: String, path: String) =
- s""""""
+ private def hrefLang(langStr: String, path: String) =
+ s""""""
private def hrefLangs(path: LangPath) = raw {
val pathEnd = if path.value == "/" then "" else path.value
hrefLang("x-default", path.value) + hrefLang("en", path.value) +
- lila.i18n.LangList.popularAlternateLanguageCodes.map { lang =>
- hrefLang(lang, s"/$lang$pathEnd")
+ lila.i18n.LangList.popularAlternateLanguages.map { l =>
+ hrefLang(l.value, s"/$l$pathEnd")
}.mkString
}
@@ -208,7 +208,7 @@ object layout:
private val dailyNewsAtom = link(
href := routes.DailyFeed.atom,
- st.title := "Daily News",
+ st.title := "Lichess Updates Feed",
tpe := "application/atom+xml",
rel := "alternate"
)
@@ -458,7 +458,10 @@ object layout:
trans.timeago.nbDaysAgo,
trans.timeago.nbWeeksAgo,
trans.timeago.nbMonthsAgo,
- trans.timeago.nbYearsAgo
+ trans.timeago.nbYearsAgo,
+ trans.timeago.nbMinutesRemaining,
+ trans.timeago.nbHoursRemaining,
+ trans.timeago.completed
)
private val cache = new java.util.concurrent.ConcurrentHashMap[Lang, String]
diff --git a/app/views/dailyFeed.scala b/app/views/dailyFeed.scala
index dd693c9d493ca..c15bf284918a8 100644
--- a/app/views/dailyFeed.scala
+++ b/app/views/dailyFeed.scala
@@ -7,6 +7,8 @@ import lila.app.ui.ScalatagsTemplate.{ *, given }
import lila.blog.DailyFeed.Update
import play.api.data.Form
import play.api.i18n.Lang
+import scalatags.text.Builder
+import scalatags.generic.Frag
object dailyFeed:
@@ -19,10 +21,10 @@ object dailyFeed:
)
def index(updates: List[Update])(using PageContext) =
- layout("Daily News"):
+ layout("Updates"):
div(cls := "daily-feed box box-pad")(
boxTop(
- h1("Daily News"),
+ h1("Lichess updates"),
div(cls := "box__top__actions")(
isGranted(_.DailyFeed) option a(
href := routes.DailyFeed.createForm,
@@ -33,32 +35,56 @@ object dailyFeed:
)
),
standardFlash,
- updateList(updates)
+ updateList(updates, editor = isGranted(_.DailyFeed))
)
- private def updateList(ups: List[Update])(using Context) =
+ def updateList(ups: List[Update], editor: Boolean)(using Context) =
div(cls := "daily-feed__updates"):
+ ups.view
+ .filter(_.published || editor)
+ .map: update =>
+ div(cls := "daily-feed__update", id := update.id)(
+ iconTag(licon.StarOutline),
+ div(cls := "daily-feed__update__content")(
+ st.section(cls := "daily-feed__update__day")(
+ h2(a(href := s"#${update.id}")(momentFromNow(update.at))),
+ editor option frag(
+ a(
+ href := routes.DailyFeed.edit(update.id),
+ cls := "button button-green button-empty button-thin text",
+ dataIcon := licon.Pencil
+ ),
+ !update.public option badTag(nbsp, "[Draft]"),
+ update.future option goodTag(nbsp, "[Future]")
+ )
+ ),
+ div(cls := "daily-feed__update__markup")(rawHtml(update.rendered))
+ )
+ )
+ .toList
+
+ val lobbyUpdates = renderCache[List[Update]](1 minute): ups =>
+ div(cls := "daily-feed__updates")(
ups.map: update =>
- div(cls := "daily-feed__update", id := update.dayString)(
+ div(cls := "daily-feed__update")(
iconTag(licon.StarOutline),
- div(cls := "daily-feed__update__content")(
- st.section(cls := "daily-feed__update__day")(
- h2(a(href := s"#${update.dayString}")(semanticDate(update.day))),
- isGranted(_.DailyFeed) option frag(
- a(
- href := routes.DailyFeed.edit(update.day),
- cls := "button button-green button-empty button-thin text",
- dataIcon := licon.Pencil
- ),
- !update.public option badTag("Draft")
- )
- ),
- div(cls := "daily-feed__update__markup")(rawHtml(update.rendered))
+ div(
+ a(cls := "daily-feed__update__day", href := s"/feed#${update.id}"):
+ momentFromNow(update.at)
+ ,
+ rawHtml(update.rendered)
)
- )
+ ),
+ div(cls := "daily-feed__update")(
+ iconTag(licon.StarOutline),
+ div:
+ a(cls := "daily-feed__update__day", href := "/feed"):
+ "All updates »"
+ )
+ )
- def create(form: Form[Update])(using PageContext) =
- layout("Daily News: New", true):
+ def create(form: Form[?])(using PageContext) =
+ layout("Lichess updates: New", true):
main(cls := "daily-feed page-small box box-pad")(
boxTop(
h1(
@@ -71,37 +97,38 @@ object dailyFeed:
inForm(form)
)
- def edit(form: Form[Update], update: Update)(using PageContext) =
- layout(s"Daily News ${update.day}", true):
+ def edit(form: Form[?], update: Update)(using PageContext) =
+ layout(s"Lichess update ${update.id}", true):
main(cls := "daily-feed page-small")(
div(cls := "box box-pad")(
boxTop(
h1(
- a(href := routes.DailyFeed.index)("Daily News"),
+ a(href := routes.DailyFeed.index)("Lichess update"),
" • ",
- semanticDate(update.day)
+ semanticDate(update.at)
)
),
standardFlash,
- postForm(cls := "content_box_content form3", action := routes.DailyFeed.update(update.day)):
+ postForm(cls := "content_box_content form3", action := routes.DailyFeed.update(update.id)):
inForm(form)
),
br,
div(cls := "box box-pad")(
- updateList(List(update)),
- postForm(action := routes.DailyFeed.delete(update.day))(cls := "daily-feed__delete"):
+ updateList(List(update), editor = true),
+ postForm(action := routes.DailyFeed.delete(update.id))(cls := "daily-feed__delete"):
submitButton(cls := "button button-red button-empty confirm")("Delete")
)
)
- private def inForm(form: Form[Update])(using Context) =
+ private def inForm(form: Form[?])(using Context) =
frag(
form3.split(
- form3.group(form("day"), frag("Day"), half = true)(
- form3.flatpickr(_, withTime = false, utc = true, minDate = none, dateFormat = "Y-m-d".some)(
- required
- )
- ),
+ form3.group(
+ form("at"),
+ frag("Date"),
+ help = raw("Set in the future to schedule an update.").some,
+ half = true
+ )(form3.flatpickr(_, minDate = none)(required)),
form3.checkbox(form("public"), raw("Publish"), half = true)
),
form3.group(
@@ -118,16 +145,16 @@ object dailyFeed:
elems = ups,
htmlCall = routes.DailyFeed.index,
atomCall = routes.DailyFeed.atom,
- title = "Lichess Daily News",
- updated = ups.headOption.map(_.instant)
+ title = "Lichess updates feed",
+ updated = ups.headOption.map(_.at)
): up =>
frag(
- tag("id")(up.dayString),
- tag("published")(atomDate(up.instant)),
+ tag("id")(up.id),
+ tag("published")(atomDate(up.at)),
link(
rel := "alternate",
tpe := "text/html",
- href := s"$netBaseUrl${routes.DailyFeed.index}#${up.dayString}"
+ href := s"$netBaseUrl${routes.DailyFeed.index}#${up.id}"
),
tag("title")(up.title),
tag("content")(tpe := "html")(up.rendered)
diff --git a/app/views/event.scala b/app/views/event.scala
index 5f7cfdc453db8..ab2bb641e453a 100644
--- a/app/views/event.scala
+++ b/app/views/event.scala
@@ -108,11 +108,11 @@ object event:
)
),
td(
- showInstantUTC(e.startsAt),
+ showInstant(e.startsAt),
momentFromNow(e.startsAt)
),
td(
- showInstantUTC(e.finishesAt),
+ showInstant(e.finishesAt),
momentFromNow(e.finishesAt)
),
td(a(cls := "text", href := routes.Event.show(e.id), dataIcon := licon.Eye))
@@ -170,20 +170,15 @@ object event:
)
),
form3.split(
- form3.group(form("lang"), raw("Language"), half = true)(
- form3.select(
- _,
- lila.i18n.LangList.popularNoRegion.map { l =>
- l.code -> s"${l.language.toUpperCase} ${LangList name l}"
- }
- )
- ),
+ form3.group(form("lang"), raw("Language"), half = true):
+ form3.select(_, lila.i18n.LangForm.popularLanguages.choices)
+ ,
form3.group(
form("hostedBy"),
raw("Hosted by Lichess user"),
help = raw("Username that must not be featured while the event is ongoing").some,
half = true
- ) { f =>
+ ): f =>
div(cls := "complete-parent")(
input(
cls := "form-control user-autocomplete",
@@ -193,7 +188,6 @@ object event:
dataTag := "span"
)
)
- }
),
form3.split(
form3.checkbox(form("enabled"), raw("Enabled"), help = raw("Display the event").some, half = true),
diff --git a/app/views/game/importGame.scala b/app/views/game/importGame.scala
index 7fa5139972bad..1a18c9e96bcd7 100644
--- a/app/views/game/importGame.scala
+++ b/app/views/game/importGame.scala
@@ -53,6 +53,9 @@ object importGame:
help = Some(analyseHelp),
disabled = ctx.isAnon
),
+ a(cls := "text", dataIcon := licon.InfoCircle, href := routes.Study.allDefault(1)):
+ trans.importGameDataPrivacyWarning()
+ ,
form3.action(form3.submit(trans.importGame(), licon.UploadCloud.some))
)
)
diff --git a/app/views/kaladin.scala b/app/views/kaladin.scala
index 6b06878079455..8c8c042624428 100644
--- a/app/views/kaladin.scala
+++ b/app/views/kaladin.scala
@@ -5,6 +5,7 @@ import lila.app.ui.ScalatagsTemplate.{ *, given }
import controllers.routes
import lila.irwin.KaladinUser
+import play.api.i18n.Lang
object kaladin:
@@ -87,7 +88,7 @@ object kaladin:
)
}
- def report(response: lila.irwin.KaladinUser.Response): Frag =
+ def report(response: lila.irwin.KaladinUser.Response)(using Lang): Frag =
div(cls := "mz-section mz-section--kaladin", dataRel := "kaladin")(
header(
span(cls := "title")(
diff --git a/app/views/lobby/bits.scala b/app/views/lobby/bits.scala
index 671b959e25786..fefa8a4539a78 100644
--- a/app/views/lobby/bits.scala
+++ b/app/views/lobby/bits.scala
@@ -25,9 +25,9 @@ object bits:
h2(cls := "title text", dataIcon := licon.CrownElite)(trans.leaderboard()),
a(cls := "more", href := routes.User.list)(trans.more(), " »")
),
- div(cls := "lobby__box__content")(
- table(
- tbody(
+ div(cls := "lobby__box__content"):
+ table:
+ tbody:
leaderboard.map: l =>
tr(
td(lightUserLink(l.user)),
@@ -36,31 +36,22 @@ object bits:
},
td(ratingProgress(l.progress))
)
- )
- )
- )
),
div(cls := s"lobby__box ${if ctx.pref.showRatings then "lobby__winners" else "lobby__wide-winners"}")(
div(cls := "lobby__box__top")(
h2(cls := "title text", dataIcon := licon.Trophy)(trans.tournamentWinners()),
a(cls := "more", href := routes.Tournament.leaderboard)(trans.more(), " »")
),
- div(cls := "lobby__box__content")(
- table(
- tbody(
- tournamentWinners take 10 map { w =>
+ div(cls := "lobby__box__content"):
+ table:
+ tbody:
+ tournamentWinners take 10 map: w =>
tr(
td(userIdLink(w.userId.some)),
- td(
- a(title := w.tourName, href := routes.Tournament.show(w.tourId))(
+ td:
+ a(title := w.tourName, href := routes.Tournament.show(w.tourId)):
scheduledTournamentNameShortHtml(w.tourName)
- )
- )
)
- }
- )
- )
- )
),
div(cls := "lobby__tournaments-simuls")(
div(cls := "lobby__tournaments lobby__box")(
@@ -68,61 +59,60 @@ object bits:
h2(cls := "title text", dataIcon := licon.Trophy)(trans.openTournaments()),
span(cls := "more")(trans.more(), " »")
),
- div(cls := "enterable_list lobby__box__content")(
+ div(cls := "enterable_list lobby__box__content"):
views.html.tournament.bits.enterable(tours)
- )
),
simuls.nonEmpty option div(cls := "lobby__simuls lobby__box")(
a(cls := "lobby__box__top", href := routes.Simul.home)(
h2(cls := "title text", dataIcon := licon.Group)(trans.simultaneousExhibitions()),
span(cls := "more")(trans.more(), " »")
),
- div(cls := "enterable_list lobby__box__content")(
- views.html.simul.bits.allCreated(simuls)
- )
+ div(cls := "enterable_list lobby__box__content"):
+ views.html.simul.bits.allCreated(simuls, withName = false)
)
)
)
- def lastPosts(update: Option[lila.blog.DailyFeed.Update], uposts: List[lila.ublog.UblogPost.PreviewPost])(
- using ctx: Context
- ): Frag =
+ def lastPosts(
+ lichess: Option[lila.blog.MiniPost],
+ uposts: List[lila.ublog.UblogPost.PreviewPost]
+ )(using ctx: Context): Frag =
div(cls := "lobby__blog ublog-post-cards")(
- update
- .map: up =>
- div(
- cls := List(
- "ublog-post-card daily-feed__update" -> true,
- "daily-feed__update--fresh" -> up.isFresh
- )
- )(
- span(cls := "ublog-post-card__content")(
- h2(cls := "daily-feed__update__day text", dataIcon := licon.Star)(
- a(href := s"${routes.DailyFeed.index}#${up.dayString}")(semanticDate(up.day))
- ),
- div(cls := "daily-feed__update__markup")(rawHtml(up.rendered))
- )
+ lichess.map: post =>
+ val imgSize = UblogPost.thumbnail.Size.Small
+ a(cls := "ublog-post-card ublog-post-card--link", href := routes.Blog.show(post.id, post.slug))(
+ img(
+ src := post.image,
+ cls := "ublog-post-card__image",
+ widthA := imgSize.width,
+ heightA := imgSize.height
),
- ctx.kid.no option uposts.map:
- views.html.ublog.post.card(_, showAuthor = views.html.ublog.post.ShowAt.bottom, showIntro = false)
+ span(cls := "ublog-post-card__content")(
+ h2(cls := "ublog-post-card__title")(post.title),
+ semanticDate(post.date)(using ctx.lang)(cls := "ublog-post-card__over-image")
+ )
+ )
+ ,
+ ctx.kid.no option uposts
+ .take(if lichess.isEmpty then 3 else 2)
+ .map:
+ views.html.ublog.post.card(_, showAuthor = views.html.ublog.post.ShowAt.bottom, showIntro = false)
)
- def showUnreadLichessMessage =
+ def showUnreadLichessMessage(using Context) =
nopeInfo(
cls := "unread-lichess-message",
- p("You have received a private message from Lichess."),
- p(
- a(cls := "button button-big", href := routes.Msg.convo(lila.user.User.lichessId))(
- "Click here to read it"
- )
- )
+ p(trans.showUnreadLichessMessage()),
+ p:
+ a(cls := "button button-big", href := routes.Msg.convo(lila.user.User.lichessId)):
+ trans.clickHereToReadIt()
)
def playbanInfo(ban: lila.playban.TempBan)(using Context) =
nopeInfo(
h1(trans.sorry()),
p(trans.weHadToTimeYouOutForAWhile()),
- p(trans.timeoutExpires(strong(secondsFromNow(ban.remainingSeconds)))),
+ p(strong(timeRemaining(ban.endsAt))),
h2(trans.why()),
p(
trans.pleasantChessExperience(),
@@ -165,9 +155,8 @@ object bits:
br,
br,
postForm(action := routes.Round.resign(current.pov.fullId))(
- button(cls := "text button button-red", dataIcon := licon.X)(
+ button(cls := "text button button-red", dataIcon := licon.X):
if current.pov.game.abortableByUser then trans.abortTheGame() else trans.resignTheGame()
- )
),
br,
p(trans.youCantStartNewGame())
@@ -176,9 +165,8 @@ object bits:
def nopeInfo(content: Modifier*) =
frag(
div(cls := "lobby__app"),
- div(cls := "lobby__nope")(
+ div(cls := "lobby__nope"):
st.section(cls := "lobby__app__content")(content)
- )
)
def spotlight(e: lila.event.Event)(using Context) =
@@ -193,8 +181,7 @@ object bits:
span(cls := "content")(
span(cls := "name")(e.title),
span(cls := "headline")(e.headline),
- span(cls := "more")(
+ span(cls := "more"):
if e.isNow then trans.eventInProgress() else momentFromNow(e.startsAt)
- )
)
)
diff --git a/app/views/lobby/home.scala b/app/views/lobby/home.scala
index 5228ea9f5a5eb..70711313a80c3 100644
--- a/app/views/lobby/home.scala
+++ b/app/views/lobby/home.scala
@@ -75,7 +75,7 @@ object home:
),
div(cls := "lobby__spotlights")(
events.map(bits.spotlight),
- relays.map(views.html.relay.bits.spotlight),
+ views.html.relay.bits.spotlight(relays),
!ctx.isBot option {
val nbManual = events.size + relays.size
val simulBBB = simuls.find(isFeaturable(_) && nbManual < 4)
@@ -116,8 +116,11 @@ object home:
,
puzzle.map: p =>
views.html.puzzle.embed.dailyLink(p)(cls := "lobby__puzzle"),
- bits.lastPosts(lastUpdate, ublogPosts),
+ bits.lastPosts(lastPost, ublogPosts),
ctx.noBot option bits.underboards(tours, simuls, leaderboard, tournamentWinners),
+ div(cls := "lobby__feed"):
+ views.html.dailyFeed.lobbyUpdates(lastUpdates)
+ ,
div(cls := "lobby__support")(
a(href := routes.Plan.index)(
iconTag(patronIconChar),
diff --git a/app/views/puzzle/bits.scala b/app/views/puzzle/bits.scala
index fad6babdd7673..0808110b8092b 100644
--- a/app/views/puzzle/bits.scala
+++ b/app/views/puzzle/bits.scala
@@ -61,6 +61,9 @@ object bits:
)
)
+ private val themeI18nKeys =
+ PuzzleTheme.visible.map(_.name) ::: PuzzleTheme.visible.map(_.description)
+
private val baseI18nKeys = List(
trans.puzzle.bestMove,
trans.puzzle.keepGoing,
@@ -102,8 +105,7 @@ object bits:
trans.puzzle.nbPointsBelowYourPuzzleRating,
trans.puzzle.nbPointsAboveYourPuzzleRating
) :::
- PuzzleTheme.visible.map(_.name) :::
- PuzzleTheme.visible.map(_.description) :::
+ themeI18nKeys :::
PuzzleDifficulty.all.map(_.name)
private val streakI18nKeys = baseI18nKeys ::: List(
@@ -113,4 +115,4 @@ object bits:
trans.puzzle.streakSkipExplanation,
trans.puzzle.continueTheStreak,
trans.puzzle.newStreak
- )
+ ) ::: themeI18nKeys
diff --git a/app/views/relay/bits.scala b/app/views/relay/bits.scala
index 877de440526ca..c618ad442beb5 100644
--- a/app/views/relay/bits.scala
+++ b/app/views/relay/bits.scala
@@ -6,19 +6,26 @@ import lila.app.templating.Environment.{ given, * }
import lila.app.ui.ScalatagsTemplate.{ *, given }
import lila.relay.RelayTour
import play.api.i18n.Lang
+import scalatags.Text.TypedTag
object bits:
def broadcastH1 = h1(dataIcon := licon.RadioTower, cls := "text")
- def spotlight(tr: RelayTour.ActiveWithSomeRounds)(using Lang) =
+ def spotlight(trs: List[RelayTour.ActiveWithSomeRounds])(using ctx: Context): List[Tag] =
+ trs
+ .filter:
+ _.tour.spotlight.map(_.language).exists(ctx.acceptLanguages)
+ .map(spotlight)
+
+ def spotlight(tr: RelayTour.ActiveWithSomeRounds)(using Lang): Tag =
a(
href := tr.path,
cls := s"tour-spotlight event-spotlight relay-spotlight id_${tr.tour.id}"
)(
i(cls := "img", dataIcon := licon.RadioTower),
span(cls := "content")(
- span(cls := "name")(tr.tour.name),
+ span(cls := "name")(tr.tour.spotlight.flatMap(_.title) | tr.tour.name),
span(cls := "more")(
tr.display.caption.fold(tr.display.name.value)(_.value),
" • ",
diff --git a/app/views/relay/roundForm.scala b/app/views/relay/roundForm.scala
index 9275c5f8764b8..8f18cc7f5956b 100644
--- a/app/views/relay/roundForm.scala
+++ b/app/views/relay/roundForm.scala
@@ -62,16 +62,20 @@ object roundForm:
postForm(cls := "form3", action := url)(
div(cls := "form-group")(
bits.howToUse,
- create option p(dataIcon := licon.InfoCircle, cls := "text")(
- theNewRoundHelp()
- )
+ (create && t.createdAt.isBefore(nowInstant minusMinutes 1)).option:
+ p(dataIcon := licon.InfoCircle, cls := "text"):
+ theNewRoundHelp()
),
form3.globalError(form),
form3.split(
form3.group(form("name"), roundName(), half = true)(form3.input(_)(autofocus)),
- t.official option form3.group(form("caption"), "Homepage caption", half = true)(
+ isGranted(_.Relay) option form3.group(
+ form("caption"),
+ "Homepage spotlight custom round name",
+ help = raw("Leave empty to use the round name").some,
+ half = true
+ ):
form3.input(_)
- )
),
form3.group(
form("syncUrl"),
@@ -79,7 +83,9 @@ object roundForm:
help = frag(
sourceUrlHelp(),
br,
- gameIdsHelp()
+ gameIdsHelp(),
+ br,
+ "Or leave empty to push games from another program."
).some
)(form3.input(_)),
form3
diff --git a/app/views/relay/tourForm.scala b/app/views/relay/tourForm.scala
index a7bc4709db86c..828db1263728f 100644
--- a/app/views/relay/tourForm.scala
+++ b/app/views/relay/tourForm.scala
@@ -57,7 +57,7 @@ object tourForm:
moreCss = cssTag("relay.form")
)(menu match
case Some(active) =>
- main(cls := "page-small page-menu")(
+ main(cls := "page page-menu")(
tour.pageMenu(active),
div(cls := "page-menu__content box box-pad")(body)
)
@@ -67,7 +67,15 @@ object tourForm:
private def inner(form: Form[Data])(using Context) = frag(
div(cls := "form-group")(bits.howToUse),
form3.globalError(form),
- form3.group(form("name"), tournamentName())(form3.input(_)(autofocus)),
+ form3.split(
+ form3.group(form("name"), tournamentName(), half = true)(form3.input(_)(autofocus)),
+ isGranted(_.Relay) option form3.group(
+ form("spotlight.title"),
+ "Homepage spotlight custom tournament name",
+ help = raw("Leave empty to use the tournament name").some,
+ half = true
+ )(form3.input(_))
+ ),
form3.group(form("description"), tournamentDescription())(form3.textarea(_)(rows := 2)),
form3.group(
form("markdown"),
@@ -87,18 +95,38 @@ object tourForm:
help = automaticLeaderboardHelp().some,
half = true
),
- if isGranted(_.Relay) then
- form3.group(
- form("tier"),
- raw("Official Lichess broadcast tier"),
- help = raw("Feature on /broadcast - for admins only").some,
- half = true
- )(form3.select(_, RelayTour.Tier.options))
- else form3.hidden(form("tier"))
+ form3.group(
+ form("players"),
+ replace(),
+ help = replaceHelp().some,
+ half = true
+ )(form3.textarea(_)(rows := 3))
),
- form3.group(
- form("players"),
- replace(),
- help = replaceHelp().some
- )(form3.textarea(_)(rows := 3))
+ if isGranted(_.Relay) then
+ frag(
+ form3.split(
+ form3.group(
+ form("tier"),
+ raw("Official Lichess broadcast tier"),
+ help = raw("Feature on /broadcast - for admins only").some,
+ half = true
+ )(form3.select(_, RelayTour.Tier.options))
+ ),
+ form3.split(
+ form3.checkbox(
+ form("spotlight.enabled"),
+ "Show a homepage spotlight",
+ help = raw("As a Big Blue Button - for admins only").some,
+ half = true
+ ),
+ form3.group(
+ form("spotlight.lang"),
+ "Homepage spotlight language",
+ help = raw("Only show to users who speak this language. English is shown to everyone.").some,
+ half = true
+ ):
+ form3.select(_, lila.i18n.LangForm.popularLanguages.choices)
+ )
+ )
+ else form3.hidden(form("tier"))
)
diff --git a/app/views/simul/bits.scala b/app/views/simul/bits.scala
index 8f13615e4e372..24f659ae52269 100644
--- a/app/views/simul/bits.scala
+++ b/app/views/simul/bits.scala
@@ -35,11 +35,11 @@ object bits:
)
)
- def allCreated(simuls: Seq[lila.simul.Simul])(using Lang) =
+ def allCreated(simuls: Seq[lila.simul.Simul], withName: Boolean = true)(using Lang) =
table(cls := "slist"):
simuls.map: simul =>
tr(
- td(cls := "name")(a(href := routes.Simul.show(simul.id))(simul.fullName)),
+ withName option td(cls := "name")(a(href := routes.Simul.show(simul.id))(simul.fullName)),
td(userIdLink(simul.hostId.some)),
td(cls := "text", dataIcon := licon.Clock)(simul.clock.config.show),
td(cls := "text", dataIcon := licon.User)(simul.applicants.size)
diff --git a/app/views/site/faq.scala b/app/views/site/faq.scala
index 16fbabbc0e553..d004bd5f5e50f 100644
--- a/app/views/site/faq.scala
+++ b/app/views/site/faq.scala
@@ -126,7 +126,7 @@ object faq:
),
ul(
li(inferiorThanXsEqualYtimeControl(29, "UltraBullet")),
- li(inferiorThanXsEqualYtimeControl(179, "Bullet")),
+ li(inferiorThanXsEqualYtimeControl(179, trans.bullet())),
li(inferiorThanXsEqualYtimeControl(479, trans.blitz())),
li(inferiorThanXsEqualYtimeControl(1499, trans.rapid())),
li(superiorThanXsEqualYtimeControl(1500, trans.classical()))
diff --git a/app/views/site/page.scala b/app/views/site/page.scala
index dbaf0d4436fe0..68a1f7f6ea497 100644
--- a/app/views/site/page.scala
+++ b/app/views/site/page.scala
@@ -216,7 +216,7 @@ $('#asset-version-message').text(lichess.info.message);"""
main(cls := "page-menu")(
views.html.site.bits.pageMenuSubnav(
a(activeCls("about"), href := "/about")(trans.aboutX("lichess.org")),
- a(activeCls("news"), href := routes.DailyFeed.index)("Daily News"),
+ a(activeCls("news"), href := routes.DailyFeed.index)("Lichess updates"),
a(activeCls("faq"), href := routes.Main.faq)(trans.faq.faqAbbreviation()),
a(activeCls("contact"), href := routes.Main.contact)(trans.contact.contact()),
a(activeCls("tos"), href := routes.ContentPage.tos)(trans.termsOfService()),
diff --git a/app/views/streamer/bits.scala b/app/views/streamer/bits.scala
index 517a115913015..64b5c7b13026c 100644
--- a/app/views/streamer/bits.scala
+++ b/app/views/streamer/bits.scala
@@ -107,7 +107,7 @@ object bits:
span(cls := "streamer-title")(
h1(dataIcon := licon.Mic)(titleTag(s.user.title), s.streamer.name),
s.streamer.lastStreamLang.map: language =>
- span(cls := "streamer-lang")(LangList nameByStr language)
+ span(cls := "streamer-lang")(LangList nameByLanguage language)
)
def subscribeButtonFor(s: lila.streamer.Streamer.WithContext)(using ctx: PageContext): Option[Tag] =
diff --git a/app/views/team/show.scala b/app/views/team/show.scala
index d292bad952cf0..b0f4b73d88d8f 100644
--- a/app/views/team/show.scala
+++ b/app/views/team/show.scala
@@ -74,8 +74,8 @@ object show:
t.publicLeaders.nonEmpty option p(
teamLeaders.pluralSame(t.publicLeaders.size),
": ",
- fragList(t.publicLeaders.toList.map: l =>
- userIdLink(l.some))
+ t.publicLeaders.map: l =>
+ userIdLink(l.some)
),
info.ledByMe option a(
dataIcon := licon.InfoCircle,
diff --git a/app/views/tournament/bits.scala b/app/views/tournament/bits.scala
index ce22d76dfe204..e87a5ef6e7feb 100644
--- a/app/views/tournament/bits.scala
+++ b/app/views/tournament/bits.scala
@@ -29,19 +29,20 @@ object bits:
val visiblePlayers = tour.nbPlayers >= 10 option tour.nbPlayers
tr(
td(cls := "name")(
- a(cls := "text", dataIcon := tournamentIcon(tour), href := routes.Tournament.show(tour.id))(
+ a(cls := "text", dataIcon := tournamentIcon(tour), href := routes.Tournament.show(tour.id)):
tour.name(full = false)
- )
),
- td(momentFromNow(tour.schedule.fold(tour.startsAt)(_.at.instant))),
+ td(
+ if tour.isStarted then timeRemaining(tour.finishesAt)
+ else momentFromNow(tour.schedule.fold(tour.startsAt)(_.at.instant))
+ ),
td(tour.durationString),
tour.conditions.teamMember match
case Some(t) =>
td(dataIcon := licon.Group, cls := "text tour-team-icon", title := t.teamName)(visiblePlayers)
case _ if tour.isTeamBattle =>
- td(dataIcon := licon.Group, cls := "text tour-team-icon", title := trans.team.teamBattle.txt())(
+ td(dataIcon := licon.Group, cls := "text tour-team-icon", title := trans.team.teamBattle.txt()):
visiblePlayers
- )
case None => td(dataIcon := licon.User, cls := "text")(visiblePlayers)
)
)
diff --git a/app/views/tournament/crud.scala b/app/views/tournament/crud.scala
index 0cbe792658bc9..5fbe32bd25a15 100644
--- a/app/views/tournament/crud.scala
+++ b/app/views/tournament/crud.scala
@@ -144,7 +144,7 @@ object crud:
td(tour.variant.name),
td(tour.clock.toString),
td(tour.minutes, "m"),
- td(showInstantUTC(tour.startsAt), " ", momentFromNow(tour.startsAt, alwaysRelative = true)),
+ td(showInstant(tour.startsAt), " ", momentFromNow(tour.startsAt, alwaysRelative = true)),
td(a(href := routes.Tournament.show(tour.id), dataIcon := licon.Eye, title := "View on site"))
)
},
diff --git a/app/views/tournament/homepageSpotlight.scala b/app/views/tournament/homepageSpotlight.scala
index a595f371e30fc..9d07e9f9ae614 100644
--- a/app/views/tournament/homepageSpotlight.scala
+++ b/app/views/tournament/homepageSpotlight.scala
@@ -33,7 +33,7 @@ object homepageSpotlight:
span(cls := "more")(
trans.nbPlayers.plural(tour.nbPlayers, tour.nbPlayers.localize),
" • ",
- if tour.isStarted then trans.finishesX(momentFromNow(tour.finishesAt))
+ if tour.isStarted then timeRemaining(tour.finishesAt)
else momentFromNow(tour.startsAt)
)
)
diff --git a/app/views/ublog/form.scala b/app/views/ublog/form.scala
index 67dcf228ac461..98487822c4ce6 100644
--- a/app/views/ublog/form.scala
+++ b/app/views/ublog/form.scala
@@ -6,7 +6,6 @@ import play.api.data.Form
import lila.app.templating.Environment.{ given, * }
import lila.app.ui.ScalatagsTemplate.{ *, given }
import lila.common.Captcha
-import lila.i18n.LangList
import lila.ublog.UblogForm.UblogPostData
import lila.ublog.{ UblogPost, UblogTopic }
import lila.user.User
@@ -154,14 +153,8 @@ object form:
form3.group(form("topics"), frag(trans.ublog.selectPostTopics()), half = true)(
form3.textarea(_)(dataRel := UblogTopic.all.mkString(","))
),
- form3.group(form("language"), trans.language(), half = true) { field =>
- form3.select(
- field,
- LangList.popularNoRegion.map { l =>
- l.code -> l.toLocale.getDisplayLanguage
- }
- )
- }
+ form3.group(form("language"), trans.language(), half = true):
+ form3.select(_, lila.i18n.LangForm.popularLanguages.choices)
),
form3.split(
form3.checkbox(
diff --git a/app/views/ublog/index.scala b/app/views/ublog/index.scala
index 5c69d0fec25ed..8906f74435d09 100644
--- a/app/views/ublog/index.scala
+++ b/app/views/ublog/index.scala
@@ -1,7 +1,6 @@
package views.html.ublog
import controllers.routes
-import play.api.i18n.Lang
import play.api.mvc.Call
import lila.app.templating.Environment.{ given, * }
@@ -10,6 +9,7 @@ import lila.common.paginator.Paginator
import lila.i18n.LangList
import lila.ublog.{ UblogPost, UblogTopic }
import lila.user.User
+import lila.i18n.Language
object index:
@@ -71,22 +71,22 @@ object index:
)
import views.html.ublog.post.ShowAt
- def community(lang: Option[Lang], posts: Paginator[UblogPost.PreviewPost])(using ctx: PageContext) =
+ def community(language: Option[Language], posts: Paginator[UblogPost.PreviewPost])(using ctx: PageContext) =
views.html.base.layout(
moreCss = cssTag("ublog"),
moreJs = posts.hasNextPage option infiniteScrollTag,
title = "Community blogs",
atomLinkTag = link(
- href := routes.Ublog.communityAtom(lang.fold("all")(_.language)),
+ href := routes.Ublog.communityAtom(language.fold("all")(_.value)),
st.title := "Lichess community blogs"
).some,
withHrefLangs = lila.common.LangPath(langHref(routes.Ublog.communityAll())).some
) {
- val langSelections = ("all", "All languages") :: lila.i18n.I18nLangPicker
- .sortFor(LangList.popularNoRegion, ctx.req)
- .map { l =>
- l.language -> LangList.name(l)
- }
+ val langSelections: List[(String, String)] = ("all", "All languages") ::
+ lila.i18n.I18nLangPicker
+ .sortFor(LangList.popularNoRegion, ctx.req)
+ .map: l =>
+ l.language -> LangList.name(l)
main(cls := "page-menu")(
views.html.blog.bits.menu(none, "community".some),
div(cls := "page-menu__content box box-pad ublog-index")(
@@ -95,19 +95,18 @@ object index:
div(cls := "box__top__actions")(
views.html.base.bits.mselect(
"ublog-lang",
- lang.fold("All languages")(LangList.name),
+ language.fold("All languages")(LangList.nameByLanguage),
langSelections
- .map { case (language, name) =>
+ .map: (languageSel, name) =>
a(
href := {
- if language == "all" then routes.Ublog.communityAll()
- else routes.Ublog.communityLang(language)
+ if languageSel == "all" then routes.Ublog.communityAll()
+ else routes.Ublog.communityLang(languageSel)
},
- cls := (language == lang.fold("all")(_.language)).option("current")
+ cls := (languageSel == language.fold("all")(_.value)).option("current")
)(name)
- }
),
- views.html.site.bits.atomLink(routes.Ublog.communityAtom(lang.fold("all")(_.language)))
+ views.html.site.bits.atomLink(routes.Ublog.communityAtom(language.fold("all")(_.value)))
)
),
if posts.nbResults > 0 then
@@ -116,8 +115,8 @@ object index:
pagerNext(
posts,
p =>
- lang
- .fold(routes.Ublog.communityAll(p))(l => routes.Ublog.communityLang(l.language, p))
+ language
+ .fold(routes.Ublog.communityAll(p))(l => routes.Ublog.communityLang(l, p))
.url
)
)
diff --git a/app/views/ublog/post.scala b/app/views/ublog/post.scala
index 60b4fb6f49282..1abe057ea04c0 100644
--- a/app/views/ublog/post.scala
+++ b/app/views/ublog/post.scala
@@ -100,6 +100,7 @@ object post:
dataIcon := licon.CautionTriangle
)
),
+ !ctx.is(user) && isGranted(_.ModerateBlog) option rankAdjust(blog, post),
div(cls := "ublog-post__topics")(
post.topics.map: topic =>
a(href := routes.Ublog.topic(topic.url, 1))(topic.value)
@@ -123,6 +124,25 @@ object post:
)
)
+ private def rankAdjust(blog: UblogBlog, post: UblogPost)(using PageContext) =
+ env.ublog.rank.computeRank(blog, post) map: rank =>
+ postForm(cls := "ublog-post__meta", action := routes.Ublog.rankAdjust(post.id))(
+ "Rank date:",
+ span(cls := "ublog-post__meta__date")(semanticDate(rank.value)),
+ s"adjust${post.rankAdjustDays.nonEmpty so "ed"} by",
+ span(
+ input(
+ tpe := "number",
+ name := "value",
+ min := -180,
+ max := 180,
+ placeholder := "Days",
+ value := post.rankAdjustDays.so(_.toString)
+ ),
+ form3.submit("Submit")(cls := "button-empty")
+ )
+ )
+
private def editButton(post: UblogPost)(using PageContext) = a(
href := editUrlOfPost(post),
cls := "button button-empty text",
diff --git a/app/views/user/mod.scala b/app/views/user/mod.scala
index f8c03cbb799f7..3af8ec43be93c 100644
--- a/app/views/user/mod.scala
+++ b/app/views/user/mod.scala
@@ -312,7 +312,7 @@ object mod:
" with ",
c.serviceName,
" on ",
- showInstantUTC(c.date),
+ showInstant(c.date),
" UTC"
)
}
@@ -581,7 +581,7 @@ object mod:
private val reportban = iconTag(licon.CautionTriangle)
private val notesText = iconTag(licon.Pencil)
private def markTd(nb: Int, content: => Frag, date: Option[Instant] = None)(using ctx: Context) =
- if nb > 0 then td(cls := "i", dataSort := nb, title := date.map(d => showInstantUTC(d)))(content)
+ if nb > 0 then td(cls := "i", dataSort := nb, title := date.map(showInstant))(content)
else td
def otherUsers(mod: Me, u: User, data: UserLogins.TableData[UserWithModlog], appeals: List[Appeal])(using
@@ -897,7 +897,7 @@ object mod:
submitButton(
title := {
if r.open then "open"
- else s"closed: ${r.done.fold("no data")(done => s"by ${done.by} at ${showInstantUTC(done.at)}")}"
+ else s"closed: ${r.done.fold("no data")(done => s"by ${done.by} at ${showInstant(done.at)}")}"
}
)(reportScore(r.score), " ", strong(r.reason.name))
diff --git a/app/views/video/bits.scala b/app/views/video/bits.scala
index d4ad1c85d56a0..87f9b330d4d7f 100644
--- a/app/views/video/bits.scala
+++ b/app/views/video/bits.scala
@@ -57,15 +57,14 @@ object bits:
def notFound(control: lila.video.UserControl)(using PageContext) =
layout(title = "Video not found", control = control)(
- div(cls := "content_box_top")(
- a(cls := "is4 text", dataIcon := licon.Back, href := routes.Video.index)("Video library")
- ),
- div(cls := "not_found")(
- h1("Video Not Found!"),
- br,
- br,
- a(cls := "big button text", dataIcon := licon.Back, href := routes.Video.index)(
- "Return to the video library"
+ boxTop(
+ h1(
+ a(
+ cls := "is4 text",
+ dataIcon := licon.Back,
+ href := s"${routes.Video.index}"
+ ),
+ "Video Not Found!"
)
)
)
diff --git a/app/views/video/layout.scala b/app/views/video/layout.scala
index 234a88570cc29..d049f09b6fe30 100644
--- a/app/views/video/layout.scala
+++ b/app/views/video/layout.scala
@@ -18,7 +18,7 @@ object layout:
moreJs = infiniteScrollTag,
wrapClass = "full-screen-force",
openGraph = openGraph
- ) {
+ ):
main(cls := "video page-menu force-ltr")(
st.aside(cls := "page-menu__menu")(
views.html.site.bits.subnav(
@@ -44,4 +44,3 @@ object layout:
),
div(cls := "page-menu__content box")(body)
)
- }
diff --git a/bin/deploy b/bin/deploy
index 67be8205b93f8..90752df383cf4 100755
--- a/bin/deploy
+++ b/bin/deploy
@@ -230,8 +230,13 @@ def artifact_url(session, run, name):
for artifact in session.get(run["artifacts_url"]).json()["artifacts"]:
if artifact["name"] == name:
if artifact["expired"]:
- print("Artifact expired.")
- return artifact["archive_download_url"]
+ raise DeployError("Artifact expired.")
+
+ # Will redirect to URL containing a short-lived authorization
+ # token.
+ resolved = session.head(artifact["archive_download_url"], allow_redirects=False)
+ resolved.raise_for_status()
+ return resolved.headers["Location"]
raise DeployError(f"Did not find artifact {name}.")
@@ -248,7 +253,6 @@ def tmux(ssh, script, *, dry_run=False):
def deploy_script(profile, session, run, url):
- auth_header = f"Authorization: {session.headers['Authorization']}"
ua_header = f"User-Agent: {session.headers['User-Agent']}"
deploy_dir = profile["deploy_dir"]
artifact_unzipped = f"{ARTIFACT_DIR}/{profile['artifact_name']}-{run['id']:d}"
@@ -259,12 +263,12 @@ def deploy_script(profile, session, run, url):
"echo \\# Downloading ...",
f"mkdir -p {ARTIFACT_DIR}",
f"mkdir -p {deploy_dir}/logs",
- f"[ -f {artifact_zip} ] || wget --header={shlex.quote(auth_header)} --header={shlex.quote(ua_header)} --no-clobber -O {artifact_zip} {shlex.quote(url)}",
+ f"[ -f {artifact_zip} ] || wget --header={shlex.quote(ua_header)} --no-clobber -O {artifact_zip} {shlex.quote(url)}",
"echo",
"echo \\# Unpacking ...",
f"unzip -q -o {artifact_zip} -d {artifact_unzipped}",
f"mkdir -p {artifact_unzipped}/d",
- f"tar -xf {artifact_unzipped}/*.tar.xz -C {artifact_unzipped}/d",
+ f"tar -xf {artifact_unzipped}/*.tar.zst -C {artifact_unzipped}/d",
f"cat {artifact_unzipped}/d/commit.txt",
"echo",
"echo \\# Preparing lifat ...",
diff --git a/bin/validate-flair b/bin/validate-flair
new file mode 100755
index 0000000000000..318b6d7041af6
--- /dev/null
+++ b/bin/validate-flair
@@ -0,0 +1,50 @@
+#!/bin/bash -e
+
+if [ -z "$1" ]; then
+ echo "Usage: $0 "
+ exit 1
+fi
+
+exit_code=0
+
+for img_file in "$1"/*; do
+ if [[ ! "$img_file" =~ \.webp$ ]]; then
+ echo "Not a WEBP: $img_file"
+ exit_code=1
+ continue
+ fi
+
+ dimensions=$(identify -format "%wx%h" "$img_file")
+
+ width=$(echo "$dimensions" | cut -d 'x' -f 1)
+ height=$(echo "$dimensions" | cut -d 'x' -f 2)
+
+ if [ "$width" -ne "$height" ]; then
+ echo "Not square: $img_file ($dimensions)"
+ exit_code=1
+
+ echo "Converting to square..."
+ max_dimension=$((width > height ? width : height))
+ convert "$img_file" -gravity center -background transparent -extent "${max_dimension}x${max_dimension}" "$img_file"
+ fi
+
+ if [ "$width" -lt 60 ] || [ "$width" -gt 100 ]; then
+ echo "Not between 60-100px: $img_file ($dimensions)"
+ exit_code=1
+
+ echo "Resizing to 100px..."
+ convert "$img_file" -resize 100x100 "$img_file"
+ fi
+
+ # animated WEBPs contain the string "ANMF" in the file
+ if grep -q "ANMF" "$img_file"; then
+ echo "Animated: $img_file"
+ exit_code=1
+ fi
+done
+
+if [ "$exit_code" -eq 0 ]; then
+ echo "✅ Images are OK"
+fi
+
+exit $exit_code
diff --git a/build.sbt b/build.sbt
index aa150ef532f3a..5c60aaf3a4c37 100644
--- a/build.sbt
+++ b/build.sbt
@@ -41,6 +41,11 @@ Compile / scalaSource := baseDirectory.value / "app"
Test / scalaSource := baseDirectory.value / "test"
Universal / sourceDirectory := baseDirectory.value / "dist"
+// cats-parse v1.0.0 is the same as v0.3.1, so this is safe
+ThisBuild / libraryDependencySchemes ++= Seq(
+ "org.typelevel" %% "cats-parse" % VersionScheme.Always
+)
+
// format: off
libraryDependencies ++= akka.bundle ++ playWs.bundle ++ macwire.bundle ++ Seq(
play.json, play.server, play.netty, play.logback,
diff --git a/conf/base.conf b/conf/base.conf
index fafef09cc980f..046db3c92870c 100644
--- a/conf/base.conf
+++ b/conf/base.conf
@@ -266,8 +266,14 @@ push {
url = "http://push.lichess.ovh:9054"
}
firebase {
- url = "https://fcm.googleapis.com/v1/projects/lichess-1366/messages:send"
- json = ""
+ lichobile {
+ url = "https://fcm.googleapis.com/v1/projects/lichess-1366/messages:send"
+ json = ""
+ }
+ mobile {
+ url = "https://fcm.googleapis.com/v1/projects/lichessv2/messages:send"
+ json = ""
+ }
}
}
report {
diff --git a/conf/routes b/conf/routes
index 8859411f67a62..9afe74cd48796 100644
--- a/conf/routes
+++ b/conf/routes
@@ -79,6 +79,7 @@ POST /ublog/$id<\w{8}>/edit controllers.Ublog.update(id)
POST /ublog/$id<\w{8}>/del controllers.Ublog.delete(id)
POST /ublog/$id<\w{8}>/like controllers.Ublog.like(id, v: Boolean)
POST /ublog/:blogId/tier controllers.Ublog.setTier(blogId)
+POST /ublog/$id<\w{8}>/adjust controllers.Ublog.rankAdjust(id)
POST /upload/image/ublog/$id<\w{8}> controllers.Ublog.image(id)
# User
@@ -119,17 +120,16 @@ GET /blog/community controllers.Ublog.communityAll(page: Int
GET /$lang<\w\w\w?>/blog/community controllers.Ublog.communityLang(lang, page: Int ?= 1)
GET /blog/community.atom controllers.Ublog.communityAtom(lang = "all")
GET /blog/community/$lang<[\w-]{2,6}>.atom controllers.Ublog.communityAtom(lang)
-GET /blog/community/$lang<[\w-]{2,6}> controllers.Ublog.communityLangBC(lang)
GET /blog/:id/:slug controllers.Blog.show(id, slug, ref: Option[String] ?= None)
GET /blog/prismic-preview controllers.Blog.preview(token)
GET /feed controllers.DailyFeed.index
GET /feed/new controllers.DailyFeed.createForm
POST /feed/new controllers.DailyFeed.create
-GET /feed/:day/edit controllers.DailyFeed.edit(day)
-GET /feed/:day/edit controllers.DailyFeed.edit(day)
-POST /feed/:day/edit controllers.DailyFeed.update(day)
-POST /feed/:day/delete controllers.DailyFeed.delete(day)
+GET /feed/:id/edit controllers.DailyFeed.edit(id)
+GET /feed/:id/edit controllers.DailyFeed.edit(id)
+POST /feed/:id/edit controllers.DailyFeed.update(id)
+POST /feed/:id/delete controllers.DailyFeed.delete(id)
GET /feed.atom controllers.DailyFeed.atom
GET /opening controllers.Opening.index(q: Option[String] ?= None)
@@ -260,6 +260,7 @@ POST /broadcast/$tourId<\w{8}>/clone controllers.RelayTour.cloneTour(to
GET /broadcast/$tourId<\w{8}>/new controllers.RelayRound.form(tourId)
POST /broadcast/$tourId<\w{8}>/new controllers.RelayRound.create(tourId)
GET /broadcast/:ts/:rs/$roundId<\w{8}> controllers.RelayRound.show(ts, rs, roundId)
+GET /api/broadcast/:ts/:rs/$roundId<\w{8}> controllers.RelayRound.apiShow(ts, rs, roundId)
GET /broadcast/:ts/:rs/$roundId<\w{8}>/$chapterId<\w{8}> controllers.RelayRound.chapter(ts, rs, roundId, chapterId)
GET /broadcast/round/$roundId<\w{8}>/edit controllers.RelayRound.edit(roundId)
POST /broadcast/round/$roundId<\w{8}>/edit controllers.RelayRound.update(roundId)
@@ -268,6 +269,8 @@ POST /broadcast/round/$roundId<\w{8}>/push controllers.RelayRound.push(roundI
GET /broadcast/:ts/:rs/$roundId<\w{8}>.pgn controllers.RelayRound.pgn(ts, rs, roundId)
GET /api/broadcast/round/$roundId<\w{8}>.pgn controllers.RelayRound.apiPgn(roundId)
GET /api/stream/broadcast/round/$roundId<\w{8}>.pgn controllers.RelayRound.stream(roundId)
+GET /api/broadcast controllers.RelayTour.apiIndex
+GET /api/broadcast/my-rounds controllers.RelayRound.apiMyRounds
# Learn
GET /learn controllers.Learn.index
@@ -692,7 +695,6 @@ POST /api/challenge/$id<\w{8}>/cancel controllers.Challenge.apiCancel(id)
POST /api/challenge/$id<\w{8}>/start-clocks controllers.Challenge.apiStartClocks(id)
POST /api/round/$id<\w{8}>/add-time/:seconds controllers.Round.apiAddTime(id, seconds: Int)
GET /api/cloud-eval controllers.Api.cloudEval
-GET /api/broadcast controllers.RelayTour.apiIndex
POST /api/import controllers.Importer.apiSendGame
GET /api/bulk-pairing controllers.BulkPairing.list
POST /api/bulk-pairing controllers.BulkPairing.create
diff --git a/modules/api/src/main/Context.scala b/modules/api/src/main/Context.scala
index 6a4fd2df569d6..c8793d158d6d2 100644
--- a/modules/api/src/main/Context.scala
+++ b/modules/api/src/main/Context.scala
@@ -8,6 +8,7 @@ import lila.pref.Pref
import lila.user.{ Me, MyId, User }
import lila.notify.Notification.UnreadCount
import lila.oauth.{ OAuthScope, TokenScopes }
+import lila.i18n.{ Language, defaultLanguage }
/* Who is logged in, and how */
final class LoginContext(
@@ -52,6 +53,9 @@ class Context(
def flash(name: String): Option[String] = req.flash get name
def withLang(l: Lang) = new Context(req, l, loginContext, pref)
def canPalantir = kid.no && me.exists(!_.marks.troll)
+ lazy val acceptLanguages: Set[Language] =
+ req.acceptLanguages.view.map(Language.apply).toSet + defaultLanguage ++
+ user.flatMap(_.language).toSet
object Context:
export lila.api.{ Context, BodyContext, LoginContext, PageContext, EmbedContext }
diff --git a/modules/api/src/main/GameApiV2.scala b/modules/api/src/main/GameApiV2.scala
index 0dc81ebab8af0..682aee981852a 100644
--- a/modules/api/src/main/GameApiV2.scala
+++ b/modules/api/src/main/GameApiV2.scala
@@ -12,10 +12,10 @@ import lila.common.{ HTTPRequest, LightUser }
import lila.db.dsl.{ *, given }
import lila.game.JsonView.given
import lila.game.PgnDump.WithFlags
-import lila.game.{ Game, Query }
+import lila.game.{ Game, Query, Pov }
import lila.team.GameTeams
import lila.tournament.Tournament
-import lila.user.User
+import lila.user.{ Me, User }
import lila.round.GameProxyRepo
import chess.ByColor
@@ -46,7 +46,7 @@ final class GameApiV2(
(game, initialFen, analysis) <- enrich(config.flags)(game)
formatted <- config.format match
case Format.JSON =>
- toJson(game, initialFen, analysis, config.flags, realPlayers = realPlayers) dmap Json.stringify
+ toJson(game, initialFen, analysis, config, realPlayers = realPlayers) dmap Json.stringify
case Format.PGN =>
PgnStr raw pgnDump(
game,
@@ -102,7 +102,7 @@ final class GameApiV2(
def exportByUser(config: ByUserConfig): Source[String, ?] =
Source.futureSource:
- config.playerFile.so(realPlayerApi.apply) map { realPlayers =>
+ config.playerFile.so(realPlayerApi.apply) map: realPlayers =>
val playerSelect =
if config.finished then
config.vs.fold(Query.user(config.user.id)) { Query.opponents(config.user, _) }
@@ -125,7 +125,6 @@ final class GameApiV2(
.via(upgradeOngoingGame)
.via(preparationFlow(config, realPlayers))
.keepAlive(keepAliveInterval, () => emptyMsgFor(config))
- }
def exportByIds(config: ByIdsConfig): Source[String, ?] =
Source.futureSource:
@@ -179,7 +178,7 @@ final class GameApiV2(
"players" -> Json.obj(color.name -> Json.obj("berserk" -> true))
)
else json
- toJson(game, fen, analysis, config.flags, teams) dmap
+ toJson(game, fen, analysis, config, teams) dmap
addBerserk(chess.White) dmap
addBerserk(chess.Black) dmap { json =>
s"${Json.stringify(json)}\n"
@@ -202,7 +201,7 @@ final class GameApiV2(
config.format match
case Format.PGN => pgnDump.formatter(config.flags)(game, fen, analysis, none, none)
case Format.JSON =>
- toJson(game, fen, analysis, config.flags, None).dmap: json =>
+ toJson(game, fen, analysis, config, None).dmap: json =>
s"${Json.stringify(json)}\n"
def exportUserImportedGames(user: User): Source[PgnStr, ?] =
@@ -222,23 +221,21 @@ final class GameApiV2(
formatterFor(config)(game, fen, analysis, None, realPlayers)
private def enrich(flags: WithFlags)(game: Game) =
- gameRepo initialFen game flatMap { initialFen =>
- (flags.requiresAnalysis so analysisRepo.byGame(game)) dmap {
+ gameRepo initialFen game flatMap: initialFen =>
+ (flags.requiresAnalysis so analysisRepo.byGame(game)) dmap:
(game, initialFen, _)
- }
- }
private def formatterFor(config: Config) =
config.format match
case Format.PGN => pgnDump.formatter(config.flags)
- case Format.JSON => jsonFormatter(config.flags)
+ case Format.JSON => jsonFormatter(config)
private def emptyMsgFor(config: Config) =
config.format match
case Format.PGN => "\n"
case Format.JSON => "{}\n"
- private def jsonFormatter(flags: WithFlags) =
+ private def jsonFormatter(config: Config) =
(
game: Game,
initialFen: Option[Fen.Epd],
@@ -246,25 +243,25 @@ final class GameApiV2(
teams: Option[GameTeams],
realPlayers: Option[RealPlayers]
) =>
- toJson(game, initialFen, analysis, flags, teams, realPlayers).dmap: json =>
+ toJson(game, initialFen, analysis, config, teams, realPlayers).dmap: json =>
s"${Json.stringify(json)}\n"
private def toJson(
g: Game,
initialFen: Option[Fen.Epd],
analysisOption: Option[Analysis],
- withFlags: WithFlags,
+ config: Config,
teams: Option[GameTeams] = None,
realPlayers: Option[RealPlayers] = None
): Fu[JsObject] = for
lightUsers <- gameLightUsers(g)
+ flags = config.flags
pgn <-
- withFlags.pgnInJson soFu pgnDump
- .apply(g, initialFen, analysisOption, withFlags, realPlayers = realPlayers)
+ config.flags.pgnInJson soFu pgnDump
+ .apply(g, initialFen, analysisOption, config.flags, realPlayers = realPlayers)
.dmap(annotator.toPgnString)
- accuracy = analysisOption.ifTrue(withFlags.accuracy).flatMap {
+ accuracy = analysisOption.ifTrue(flags.accuracy) flatMap:
AccuracyPercent.gameAccuracy(g.startedAtPly.turn, _)
- }
yield Json
.obj(
"id" -> g.id,
@@ -278,25 +275,24 @@ final class GameApiV2(
"players" -> JsObject(lightUsers.mapList: (p, user) =>
p.color.name -> gameJsonView
.player(p, user)
- .add(
- "analysis" -> analysisOption.flatMap(
+ .add:
+ "analysis" -> analysisOption.flatMap:
analysisJson.player(g.pov(p.color).sideAndStart)(_, accuracy)
- )
- )
.add("team" -> teams.map(_(p.color))))
)
+ .add("fullId" -> config.by.flatMap(Pov(g, _)).map(_.fullId))
.add("initialFen" -> initialFen)
.add("winner" -> g.winnerColor.map(_.name))
- .add("opening" -> g.opening.ifTrue(withFlags.opening))
- .add("moves" -> withFlags.moves.option {
- withFlags keepDelayIf g.playable applyDelay g.sans mkString " "
+ .add("opening" -> g.opening.ifTrue(flags.opening))
+ .add("moves" -> flags.moves.option {
+ flags keepDelayIf g.playable applyDelay g.sans mkString " "
})
- .add("clocks" -> withFlags.clocks.so(g.bothClockStates).map { clocks =>
- withFlags keepDelayIf g.playable applyDelay clocks
+ .add("clocks" -> flags.clocks.so(g.bothClockStates).map { clocks =>
+ flags keepDelayIf g.playable applyDelay clocks
})
.add("pgn" -> pgn)
.add("daysPerTurn" -> g.daysPerTurn)
- .add("analysis" -> analysisOption.ifTrue(withFlags.evals).map(analysisJson.moves(_, withGlyph = false)))
+ .add("analysis" -> analysisOption.ifTrue(flags.evals).map(analysisJson.moves(_, withGlyph = false)))
.add("tournament" -> g.tournamentId)
.add("swiss" -> g.swissId)
.add("clock" -> g.clock.map: clock =>
@@ -305,7 +301,8 @@ final class GameApiV2(
"increment" -> clock.incrementSeconds,
"totalTime" -> clock.estimateTotalSeconds
))
- .add("lastFen" -> withFlags.lastFen.option(Fen.write(g.chess.situation)))
+ .add("lastFen" -> flags.lastFen.option(Fen.write(g.chess.situation)))
+ .add("lastMove" -> flags.lastFen.option(g.lastMoveKeys))
private def gameLightUsers(game: Game): Future[ByColor[(lila.game.Player, Option[LightUser])]] =
game.players.traverse(_.userId so getLightUser).dmap(game.players.zip(_))
@@ -320,6 +317,7 @@ object GameApiV2:
sealed trait Config:
val format: Format
val flags: WithFlags
+ val by: Option[Me]
enum GameSort(val bson: Bdoc):
case DateAsc extends GameSort(Query.sortChronological)
@@ -330,7 +328,8 @@ object GameApiV2:
imported: Boolean,
flags: WithFlags,
playerFile: Option[String]
- ) extends Config
+ )(using val by: Option[Me])
+ extends Config
case class ByUserConfig(
user: User,
@@ -349,7 +348,8 @@ object GameApiV2:
playerFile: Option[String],
ongoing: Boolean = false,
finished: Boolean = true
- ) extends Config:
+ )(using val by: Option[Me])
+ extends Config:
def postFilter(g: Game) =
rated.forall(g.rated ==) && {
perfType.isEmpty || perfType.contains(g.perfType)
@@ -363,14 +363,16 @@ object GameApiV2:
flags: WithFlags,
perSecond: MaxPerSecond,
playerFile: Option[String] = None
- ) extends Config
+ )(using val by: Option[Me])
+ extends Config
case class ByTournamentConfig(
tour: Tournament,
format: Format,
flags: WithFlags,
perSecond: MaxPerSecond
- ) extends Config
+ )(using val by: Option[Me])
+ extends Config
case class BySwissConfig(
swissId: SwissId,
@@ -378,4 +380,5 @@ object GameApiV2:
flags: WithFlags,
perSecond: MaxPerSecond,
player: Option[UserId]
- ) extends Config
+ )(using val by: Option[Me])
+ extends Config
diff --git a/modules/blog/src/main/BlogPost.scala b/modules/blog/src/main/BlogPost.scala
index ba58c9cf400dc..ff89087353388 100644
--- a/modules/blog/src/main/BlogPost.scala
+++ b/modules/blog/src/main/BlogPost.scala
@@ -20,7 +20,8 @@ case class MiniPost(
date: LocalDate,
image: String,
forKids: Boolean
-)
+):
+ def isOld = date.isBefore(LocalDate.now.minusDays(7))
object MiniPost:
diff --git a/modules/blog/src/main/DailyFeed.scala b/modules/blog/src/main/DailyFeed.scala
index 1cf84659ac413..ec79574ce9fe8 100644
--- a/modules/blog/src/main/DailyFeed.scala
+++ b/modules/blog/src/main/DailyFeed.scala
@@ -1,89 +1,82 @@
package lila.blog
-import java.time.LocalDate
import reactivemongo.api.bson.*
import reactivemongo.api.bson.Macros.Annotations.Key
+import java.time.format.{ DateTimeFormatter, FormatStyle }
import lila.db.dsl.{ *, given }
import lila.memo.CacheApi
import lila.common.config.Max
+import play.api.data.Form
object DailyFeed:
- case class Update(@Key("_id") day: LocalDate, content: Markdown, public: Boolean):
+ type ID = String
- lazy val rendered: Html = renderer(s"dailyFeed:${day}")(content)
+ case class Update(@Key("_id") id: ID, content: Markdown, public: Boolean, at: Instant):
+ lazy val rendered: Html = renderer(s"dailyFeed:${id}")(content)
+ lazy val dateStr = dateFormatter print at
+ lazy val title = "Daily update - " + dateStr
+ def published = public && at.isBeforeNow
+ def future = at.isAfterNow
- lazy val instant: Instant = day.atStartOfDay.instant
+ private val renderer = lila.common.MarkdownRender(autoLink = false, strikeThrough = true)
+ private val dateFormatter = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)
- lazy val dayString: String = day.toString
+ type GetLastUpdates = () => List[Update]
- lazy val title = "Daily update - " + dayString
-
- lazy val isFresh = instant isAfter nowInstant.minusDays(1)
-
- private val renderer = lila.common.MarkdownRender(
- autoLink = false,
- list = true,
- table = true,
- strikeThrough = true,
- header = true
- )
-
- type GetLastUpdate = () => Option[Update]
+ import ornicar.scalalib.ThreadLocalRandom
+ def makeId = ThreadLocalRandom nextString 6
final class DailyFeed(coll: Coll, cacheApi: CacheApi)(using Executor):
- import DailyFeed.Update
+ import DailyFeed.*
+
+ private val max = Max(50)
- private given BSONHandler[LocalDate] = quickHandler[LocalDate](
- { case BSONString(s) => LocalDate.parse(s) },
- d => BSONString(d.toString)
- )
private given BSONDocumentHandler[Update] = Macros.handler
private object cache:
- private var mutableLastUpdate: Option[Update] = None
- val store = cacheApi[Max, List[Update]](4, "dailyFeed.updates"):
- _.expireAfterWrite(1 minute).buildAsyncFuture: nb =>
+ private var mutableLastUpdates: List[Update] = Nil
+ val store = cacheApi.unit[List[Update]]:
+ _.refreshAfterWrite(1 minute).buildAsyncFuture: _ =>
coll
.find($empty)
- .sort($sort.desc("_id"))
+ .sort($sort.desc("at"))
.cursor[Update]()
- .list(nb.value)
+ .list(max.value)
.addEffect: ups =>
- mutableLastUpdate = ups.headOption
- def clear() = store.underlying.synchronous.invalidateAll()
- def lastUpdate: DailyFeed.GetLastUpdate = () => mutableLastUpdate
- store.get(Max(1)) // populate lastUpdate
+ mutableLastUpdates = ups.filter(_.published).take(7)
+ def clear() =
+ store.underlying.synchronous.invalidateAll()
+ store.get({}) // populate lastUpdate
+ def lastUpdate: DailyFeed.GetLastUpdates = () => mutableLastUpdates
+ store.get({}) // populate lastUpdate
- export cache.store.{ get as recent }
export cache.lastUpdate
- def get(day: LocalDate): Fu[Option[Update]] = coll.one[Update]($id(day))
+ def recent: Fu[List[Update]] = cache.store.get({})
- def set(update: Update, from: Option[Update]): Funit = for
- _ <- from.filter(_.day != update.day).so(up => coll.delete.one($id(up.day)).void)
- _ <- coll.update.one($id(update.day), update, upsert = true).void andDo cache.clear()
- yield ()
+ def recentPublished = recent.map(_.filter(_.published))
- def delete(id: LocalDate): Funit =
+ def get(id: ID): Fu[Option[Update]] = coll.byId[Update](id)
+
+ def set(update: Update): Funit =
+ coll.update.one($id(update.id), update, upsert = true).void andDo cache.clear()
+
+ def delete(id: ID): Funit =
coll.delete.one($id(id)).void andDo cache.clear()
- def form(from: Option[Update]) =
+ case class UpdateData(content: Markdown, public: Boolean, at: Instant):
+ def toUpdate(id: Option[ID]) = Update(id | makeId, content, public, at)
+
+ def form(from: Option[Update]): Form[UpdateData] =
import play.api.data.*
import play.api.data.Forms.*
import lila.common.Form.*
val form = Form:
mapping(
- "day" -> ISODate.mapping
- .verifying(
- "There is already an update for this day",
- day => from.exists(_.day == day) || !existsBlocking(day)
- ),
"content" -> nonEmptyText(maxLength = 20_000).into[Markdown],
- "public" -> boolean
- )(Update.apply)(unapply)
- from.fold(form)(form.fill)
-
- private def existsBlocking(day: LocalDate): Boolean =
- coll.exists($id(day)).await(1.second, "dailyFeed.existsBlocking")
+ "public" -> boolean,
+ "at" -> ISOInstantOrTimestamp.mapping
+ )(UpdateData.apply)(unapply)
+ from.fold(form)(u => form.fill(UpdateData(u.content, u.public, u.at)))
diff --git a/modules/bot/src/main/GameStateStream.scala b/modules/bot/src/main/GameStateStream.scala
index ef0f61f480a84..aab7f08916f65 100644
--- a/modules/bot/src/main/GameStateStream.scala
+++ b/modules/bot/src/main/GameStateStream.scala
@@ -26,10 +26,7 @@ import lila.common.{ Bus, HTTPRequest }
final class GameStateStream(
onlineApiUsers: OnlineApiUsers,
jsonView: BotJsonView
-)(using
- ec: Executor,
- system: ActorSystem
-):
+)(using ec: Executor, system: ActorSystem):
import GameStateStream.*
private val blueprint =
@@ -44,15 +41,13 @@ final class GameStateStream(
// terminate previous one if any
Bus.publish(PoisonPill, uniqChan(init.game pov as))
- blueprint mapMaterializedValue { queue =>
+ blueprint.mapMaterializedValue: queue =>
val actor = system.actorOf(
Props(mkActor(init, as, User(me, me.isBot), queue)),
name = s"GameStateStream:${init.game.id}:${ThreadLocalRandom nextString 8}"
)
- queue.watchCompletion().addEffectAnyway {
+ queue.watchCompletion() addEffectAnyway:
actor ! PoisonPill
- }
- }
private def uniqChan(pov: Pov)(using req: RequestHeader) =
s"gameStreamFor:${pov.fullId}:${HTTPRequest.userAgent(req) | "?"}"
@@ -128,15 +123,13 @@ final class GameStateStream(
def pushChatLine(username: UserName, text: String, player: Boolean) =
queue offer jsonView.chatLine(username, text, player).some
- def opponentGone(claimInSeconds: Option[Int]) = queue offer {
+ def opponentGone(claimInSeconds: Option[Int]) = queue.offer:
claimInSeconds.fold(jsonView.opponentGoneIsBack)(jsonView.opponentGoneClaimIn).some
- }
def onGameOver(g: Option[Game]) =
- g.so(pushState)
- .andDo:
- gameOver = true
- self ! PoisonPill
+ g.so(pushState) andDo:
+ gameOver = true
+ self ! PoisonPill
private object GameStateStream:
diff --git a/modules/chat/src/main/Chat.scala b/modules/chat/src/main/Chat.scala
index 788779f1cc346..84f023fce32bd 100644
--- a/modules/chat/src/main/Chat.scala
+++ b/modules/chat/src/main/Chat.scala
@@ -1,7 +1,7 @@
package lila.chat
import lila.hub.actorApi.shutup.PublicSource
-import lila.user.User
+import lila.user.{ Me, User }
import reactivemongo.api.bson.BSONDocumentHandler
sealed trait AnyChat:
@@ -10,7 +10,7 @@ sealed trait AnyChat:
val loginRequired: Boolean
- def forUser(u: Option[User]): AnyChat
+ def forMe(using Option[Me], AllMessages): AnyChat
def isEmpty = lines.isEmpty
@@ -28,8 +28,8 @@ case class UserChat(
val loginRequired = true
- def forUser(u: Option[User]): UserChat =
- if u.so(_.marks.troll) then this
+ def forMe(using me: Option[Me], all: AllMessages): UserChat =
+ if all.yes || me.exists(_.marks.troll) then this
else copy(lines = lines.filterNot(_.troll))
def markDeleted(u: User) = copy(
@@ -63,8 +63,8 @@ case class MixedChat(
val loginRequired = false
- def forUser(u: Option[User]): MixedChat =
- if u.so(_.marks.troll) then this
+ def forMe(using me: Option[Me], all: AllMessages): MixedChat =
+ if all.yes || me.exists(_.marks.troll) then this
else
copy(lines = lines.filter:
case l: UserLine => !l.troll
diff --git a/modules/chat/src/main/ChatApi.scala b/modules/chat/src/main/ChatApi.scala
index 71b2e89a40751..d12ab7e755864 100644
--- a/modules/chat/src/main/ChatApi.scala
+++ b/modules/chat/src/main/ChatApi.scala
@@ -36,7 +36,7 @@ final class ChatApi(
def invalidate = cache.invalidate
- def findMine(chatId: ChatId)(using me: Option[Me]): Fu[UserChat.Mine] =
+ def findMine(chatId: ChatId)(using Option[Me], AllMessages): Fu[UserChat.Mine] =
cache.get(chatId) flatMap makeMine
def findOption(chatId: ChatId): Fu[Option[UserChat]] =
@@ -48,12 +48,12 @@ final class ChatApi(
def findAll(chatIds: List[ChatId]): Fu[List[UserChat]] =
coll.byStringIds[UserChat](ChatId raw chatIds, _.sec)
- def findMine(chatId: ChatId, cond: Boolean = true)(using Option[Me]): Fu[UserChat.Mine] =
+ def findMine(chatId: ChatId, cond: Boolean = true)(using Option[Me], AllMessages): Fu[UserChat.Mine] =
if cond then find(chatId) flatMap makeMine
else fuccess(UserChat.Mine(Chat.makeUser(chatId), JsonChatLines.empty, timeout = false))
- private def makeMine(chat: UserChat)(using me: Option[Me]): Fu[UserChat.Mine] =
- val mine = chat forUser me
+ private def makeMine(chat: UserChat)(using me: Option[Me], all: AllMessages): Fu[UserChat.Mine] =
+ val mine = chat.forMe
for
lines <- JsonView.asyncLines(mine)
timeout <- me.ifFalse(mine.isEmpty) so:
diff --git a/modules/chat/src/main/package.scala b/modules/chat/src/main/package.scala
index 738db5914c14b..e759cc7dedd9e 100644
--- a/modules/chat/src/main/package.scala
+++ b/modules/chat/src/main/package.scala
@@ -10,3 +10,6 @@ case class Timeout(chatId: ChatId, mod: UserId, userId: UserId, reason: ChatTime
case class OnTimeout(chatId: ChatId, userId: UserId)
case class OnReinstate(chatId: ChatId, userId: UserId)
+
+opaque type AllMessages = Boolean
+object AllMessages extends YesNo[AllMessages]
diff --git a/modules/clas/src/main/ClasMatesCache.scala b/modules/clas/src/main/ClasMatesCache.scala
index b9837bfea50ef..93ecd092550d2 100644
--- a/modules/clas/src/main/ClasMatesCache.scala
+++ b/modules/clas/src/main/ClasMatesCache.scala
@@ -13,10 +13,9 @@ final class ClasMatesCache(colls: ClasColls, cacheApi: CacheApi, studentCache: C
def get(studentId: UserId): Fu[Set[UserId]] =
studentCache.isStudent(studentId) so cache.get(studentId)
- private val cache = cacheApi[UserId, Set[UserId]](256, "clas.mates") {
+ private val cache = cacheApi[UserId, Set[UserId]](256, "clas.mates"):
_.expireAfterWrite(5 minutes)
.buildAsyncFuture(fetchMatesAndTeachers)
- }
private def fetchMatesAndTeachers(studentId: UserId): Fu[Set[UserId]] =
colls.student
@@ -50,7 +49,11 @@ final class ClasMatesCache(colls: ClasColls, cacheApi: CacheApi, studentCache: C
)
)
),
- ReplaceRoot($doc("$arrayElemAt" -> $arr("$mates", 0)))
+ ReplaceRoot:
+ $ifNull(
+ $doc("$arrayElemAt" -> $arr("$mates", 0)),
+ $doc("mates" -> $arr())
+ )
),
"teachers" -> List(
PipelineOperator(
@@ -70,18 +73,21 @@ final class ClasMatesCache(colls: ClasColls, cacheApi: CacheApi, studentCache: C
)
)
),
- ReplaceRoot($doc("$arrayElemAt" -> $arr("$teachers", 0)))
+ ReplaceRoot:
+ $ifNull(
+ $doc("$arrayElemAt" -> $arr("$teachers", 0)),
+ $doc("teachers" -> $arr())
+ )
)
)
),
- ReplaceRoot(
+ ReplaceRoot:
$doc(
"$mergeObjects" -> $arr(
$doc("$arrayElemAt" -> $arr("$mates", 0)),
$doc("$arrayElemAt" -> $arr("$teachers", 0))
)
)
- )
)
.map: docO =>
for
@@ -90,6 +96,3 @@ final class ClasMatesCache(colls: ClasColls, cacheApi: CacheApi, studentCache: C
teachers <- doc.getAsOpt[Set[UserId]]("teachers")
yield mates ++ teachers
.dmap(~_)
- .recover:
- // can happen, probably in case of student cache bloom filter false positive
- case e: DatabaseException if e.getMessage.contains("resulting value was: MISSING") => Set.empty
diff --git a/modules/clas/src/main/ClasStudentCache.scala b/modules/clas/src/main/ClasStudentCache.scala
index 044df7b6c618b..435104c0e66b1 100644
--- a/modules/clas/src/main/ClasStudentCache.scala
+++ b/modules/clas/src/main/ClasStudentCache.scala
@@ -37,4 +37,5 @@ final class ClasStudentCache(colls: ClasColls)(using scheduler: Scheduler)(using
bloomFilter = nextBloom
.monSuccess(_.clas.student.bloomFilter.fu)
- scheduler.scheduleWithFixedDelay(71.seconds, 24.hours) { (() => rebuildBloomFilter()) }
+ scheduler.scheduleWithFixedDelay(71.seconds, 24.hours): () =>
+ rebuildBloomFilter()
diff --git a/modules/common/src/main/HTTPRequest.scala b/modules/common/src/main/HTTPRequest.scala
index f999f76ec96e7..b6fb784fd92de 100644
--- a/modules/common/src/main/HTTPRequest.scala
+++ b/modules/common/src/main/HTTPRequest.scala
@@ -37,15 +37,17 @@ object HTTPRequest:
def userAgent(req: RequestHeader): Option[UserAgent] = UserAgent.from:
req.headers get HeaderNames.USER_AGENT
- val isChrome96Plus = UaMatcher("""Chrome/(?:\d{3,}|9[6-9])""")
- val isChrome113Plus = UaMatcher("""Chrome/(?:11[3-9]|1[2-9]\d)""")
- val isFirefox114Plus = UaMatcher("""Firefox/(?:11[4-9]|1[2-9]\d)""")
- val isMobileBrowser = UaMatcher("""(?i)iphone|ipad|ipod|android.+mobile""")
- def isLichessMobile(req: RequestHeader) = userAgent(req).exists(_.value startsWith "Lichess Mobile/")
- def isLichobile(req: RequestHeader) = userAgent(req).exists(_.value contains "Lichobile/")
+ val isChrome96Plus = UaMatcher("""Chrome/(?:\d{3,}|9[6-9])""")
+ val isChrome113Plus = UaMatcher("""Chrome/(?:11[3-9]|1[2-9]\d)""")
+ val isFirefox114Plus = UaMatcher("""Firefox/(?:11[4-9]|1[2-9]\d)""")
+ val isMobileBrowser = UaMatcher("""(?i)iphone|ipad|ipod|android.+mobile""")
+ def isLichessMobile(ua: UserAgent): Boolean = ua.value startsWith "Lichess Mobile/"
+ def isLichessMobile(req: RequestHeader): Boolean = userAgent(req).exists(isLichessMobile)
+ def isLichobile(req: RequestHeader) = userAgent(req).exists(_.value contains "Lichobile/")
def isLichobileDev(req: RequestHeader) = // lichobile in a browser can't set its user-agent
isLichobile(req) || (appOrigin(req).isDefined && !isLichessMobile(req))
- def isAndroid = UaMatcher("Android")
+ def isAndroid = UaMatcher("Android")
+ def isLitools(req: RequestHeader) = userAgent(req).has(UserAgent("litools"))
def origin(req: RequestHeader): Option[String] = req.headers get HeaderNames.ORIGIN
def referer(req: RequestHeader): Option[String] = req.headers get HeaderNames.REFERER
@@ -61,7 +63,7 @@ object HTTPRequest:
private val crawlerMatcher = UaMatcher:
// spiders/crawlers
- """Googlebot|AdsBot|Google-Read-Aloud|bingbot|BingPreview|facebookexternalhit|SemrushBot|AhrefsBot|PetalBot|Applebot|YandexBot|YandexAdNet|Twitterbot|Baiduspider""" +
+ """Googlebot|AdsBot|Google-Read-Aloud|bingbot|BingPreview|facebookexternalhit|SemrushBot|AhrefsBot|PetalBot|Applebot|YandexBot|YandexAdNet|Twitterbot|Baiduspider|Amazonbot|Bytespider""" +
// http libs
"""|HeadlessChrome|okhttp|axios|wget|curl|python-requests|aiohttp|commons-httpclient|python-urllib|python-httpx|Nessus"""
diff --git a/modules/common/src/main/base/LilaModel.scala b/modules/common/src/main/base/LilaModel.scala
index 16033b9359ede..73af9fd7db03c 100644
--- a/modules/common/src/main/base/LilaModel.scala
+++ b/modules/common/src/main/base/LilaModel.scala
@@ -6,6 +6,13 @@ import java.time.Instant
trait LilaModel:
+ type Update[A] = A => A
+ def UpdateOf[A](f: A => A): Update[A] = f
+ // apply updates to a value, and keep track of the updates
+ // so they can all be replayed on another value
+ case class Updating[A](current: A, reRun: Update[A] = (a: A) => a):
+ def apply(up: Update[A]) = Updating(up(current), up compose reRun)
+
trait OpaqueInstant[A](using A =:= Instant) extends TotalWrapper[A, Instant]
trait Percent[A]:
diff --git a/modules/common/src/main/config.scala b/modules/common/src/main/config.scala
index 87a6556c8d401..7b154779fb33f 100644
--- a/modules/common/src/main/config.scala
+++ b/modules/common/src/main/config.scala
@@ -49,6 +49,20 @@ object config:
opaque type EndpointUrl = String
object EndpointUrl extends OpaqueString[EndpointUrl]
+ case class Credentials(user: String, password: Secret):
+ def show = s"$user:${password.value}"
+ object Credentials:
+ def read(str: String): Option[Credentials] = str.split(":") match
+ case Array(user, password) => Credentials(user, Secret(password)).some
+ case _ => none
+
+ case class HostPort(host: String, port: Int):
+ def show = s"$host:$port"
+ object HostPort:
+ def read(str: String): Option[HostPort] = str.split(":") match
+ case Array(host, port) => port.toIntOption.map(HostPort(host, _))
+ case _ => none
+
case class NetConfig(
domain: NetDomain,
prodDomain: NetDomain,
diff --git a/modules/common/src/main/model.scala b/modules/common/src/main/model.scala
index c64826b065f8c..e0954e462de55 100644
--- a/modules/common/src/main/model.scala
+++ b/modules/common/src/main/model.scala
@@ -95,6 +95,12 @@ object Preload:
def apply[A](value: A): Preload[A] = Preload(value.some)
def none[A] = Preload[A](None)
+final class LazyFu[A](run: () => Fu[A]):
+ lazy val value: Fu[A] = run()
+ def dmap[B](f: A => B): LazyFu[B] = LazyFu(() => value dmap f)
+object LazyFu:
+ def sync[A](v: => A): LazyFu[A] = LazyFu(() => fuccess(v))
+
enum LpvEmbed:
case PublicPgn(pgn: PgnStr)
case PrivateStudy
diff --git a/modules/common/src/main/mon.scala b/modules/common/src/main/mon.scala
index 47789757ec99e..5e1ba7ce4df6c 100644
--- a/modules/common/src/main/mon.scala
+++ b/modules/common/src/main/mon.scala
@@ -259,7 +259,8 @@ object mon:
def moves(official: Boolean, slug: String) = counter("relay.moves").withTags(relay(official, slug))
def fetchTime(official: Boolean, slug: String) = timer("relay.fetch.time").withTags(relay(official, slug))
def syncTime(official: Boolean, slug: String) = timer("relay.sync.time").withTags(relay(official, slug))
- def httpGet(host: String) = future("relay.http.get", tags("host" -> host))
+ def httpGet(host: String, proxy: Option[String]) =
+ future("relay.http.get", tags("host" -> host, "proxy" -> proxy.getOrElse("none")))
object bot:
def moves(username: String) = counter("bot.moves").withTag("name", username)
def chats(username: String) = counter("bot.chats").withTag("name", username)
@@ -547,6 +548,7 @@ object mon:
()
val move = send("move")
val takeback = send("takeback")
+ val draw = send("draw")
val corresAlarm = send("corresAlarm")
val finish = send("finish")
val message = send("message")
@@ -560,6 +562,7 @@ object mon:
val accept = send("challengeAccept")
val googleTokenTime = timer("push.send.googleToken").withoutTags()
def firebaseStatus(status: Int) = counter("push.firebase.status").withTag("status", status)
+ def firebaseType(typ: String) = counter("push.firebase.msgType").withTag("type", typ)
object fishnet:
object client:
object result:
diff --git a/modules/db/src/main/Handlers.scala b/modules/db/src/main/Handlers.scala
index c347693688387..3c8aae980b19b 100644
--- a/modules/db/src/main/Handlers.scala
+++ b/modules/db/src/main/Handlers.scala
@@ -200,6 +200,9 @@ trait Handlers:
)
)
+ val langByCodeHandler: BSONHandler[play.api.i18n.Lang] =
+ stringAnyValHandler(_.code, play.api.i18n.Lang.apply)
+
def valueMapHandler[K, V](mapping: Map[K, V])(toKey: V => K)(using
keyHandler: BSONHandler[K]
): BSONHandler[V] = new:
diff --git a/modules/db/src/main/dsl.scala b/modules/db/src/main/dsl.scala
index d2db7a2cb1d8e..9e7c9677a0989 100644
--- a/modules/db/src/main/dsl.scala
+++ b/modules/db/src/main/dsl.scala
@@ -152,6 +152,9 @@ trait dsl:
def $addOrPull[T: BSONWriter](key: String, value: T, add: Boolean): Bdoc =
$doc((if add then "$addToSet" else "$pull") -> $doc(key -> value))
+ def $ifNull(expr: Bdoc, replacement: Bdoc): Bdoc =
+ $doc("$ifNull" -> $arr(expr, replacement))
+
// End ofTop Level Array Update Operators
// **********************************************************************************************//
diff --git a/modules/evalCache/src/main/Env.scala b/modules/evalCache/src/main/Env.scala
index b5b4f1627b21d..ca3ef0dc51dda 100644
--- a/modules/evalCache/src/main/Env.scala
+++ b/modules/evalCache/src/main/Env.scala
@@ -18,8 +18,7 @@ final class Env(
def cli = new lila.common.Cli:
def process = { case "eval-cache" :: "drop" :: variantKey :: fenParts =>
- Variant(Variant.LilaKey(variantKey)).fold(fufail("Invalid variant")) { variant =>
+ Variant(Variant.LilaKey(variantKey)).fold(fufail("Invalid variant")): variant =>
api.drop(variant, chess.format.Fen.Epd(fenParts mkString " ")) inject
"Done, but the eval can stay in cache for up to 5 minutes"
- }
}
diff --git a/modules/event/src/main/BsonHandlers.scala b/modules/event/src/main/BsonHandlers.scala
index 4383bf86da8a7..6a49f8aebb52c 100644
--- a/modules/event/src/main/BsonHandlers.scala
+++ b/modules/event/src/main/BsonHandlers.scala
@@ -7,6 +7,6 @@ import lila.db.dsl.{ *, given }
private object BsonHandlers:
- private given BSONHandler[Lang] = stringAnyValHandler[Lang](_.code, Lang.apply)
+ private given BSONHandler[Lang] = langByCodeHandler
given BSONDocumentHandler[Event] = Macros.handler
diff --git a/modules/event/src/main/Event.scala b/modules/event/src/main/Event.scala
index de797e3682d2a..2dd098bf22fd7 100644
--- a/modules/event/src/main/Event.scala
+++ b/modules/event/src/main/Event.scala
@@ -1,7 +1,7 @@
package lila.event
import play.api.i18n.Lang
-import ornicar.scalalib.ThreadLocalRandom
+import lila.i18n.Language
case class Event(
_id: String,
@@ -10,7 +10,7 @@ case class Event(
description: Option[Markdown],
homepageHours: Double,
url: String,
- lang: Lang,
+ lang: Language,
enabled: Boolean,
createdBy: UserId,
createdAt: Instant,
@@ -25,10 +25,8 @@ case class Event(
def willStartLater = startsAt.isAfterNow
- def secondsToStart =
- willStartLater option {
- (startsAt.toSeconds - nowSeconds).toInt
- }
+ def secondsToStart = willStartLater option:
+ (startsAt.toSeconds - nowSeconds).toInt
def featureSince = startsAt minusMinutes (homepageHours * 60).toInt
@@ -46,4 +44,5 @@ case class Event(
object Event:
+ import ornicar.scalalib.ThreadLocalRandom
def makeId = ThreadLocalRandom nextString 8
diff --git a/modules/event/src/main/EventApi.scala b/modules/event/src/main/EventApi.scala
index 2a4f7249e5baf..b5ca2588b1014 100644
--- a/modules/event/src/main/EventApi.scala
+++ b/modules/event/src/main/EventApi.scala
@@ -5,42 +5,34 @@ import play.api.mvc.RequestHeader
import lila.db.dsl.{ *, given }
import lila.memo.CacheApi.*
import lila.user.Me
+import lila.i18n.Language
final class EventApi(coll: Coll, cacheApi: lila.memo.CacheApi)(using Executor):
import BsonHandlers.given
- def promoteTo(req: RequestHeader): Fu[List[Event]] =
- promotable.getUnit map {
+ def promoteTo(accepts: Set[Language]): Fu[List[Event]] =
+ promotable.getUnit map:
_.filter: event =>
- event.lang.language == lila.i18n.enLang.language ||
- lila.i18n.I18nLangPicker
- .allFromRequestHeaders(req)
- .exists {
- _.language == event.lang.language
- }
+ accepts(event.lang)
.take(3)
- }
- private val promotable = cacheApi.unit[List[Event]] {
+ private val promotable = cacheApi.unit[List[Event]]:
_.refreshAfterWrite(5 minutes)
.buildAsyncFuture(_ => fetchPromotable)
- }
def fetchPromotable: Fu[List[Event]] =
coll
- .find(
+ .find:
$doc(
"enabled" -> true,
"startsAt" $gt nowInstant.minusDays(1) $lt nowInstant.plusDays(1)
)
- )
.sort($sort asc "startsAt")
.cursor[Event]()
.list(50)
- .dmap {
+ .dmap:
_.filter(_.featureNow) take 10
- }
def list = coll.find($empty).sort($doc("startsAt" -> -1)).cursor[Event]().list(50)
@@ -49,9 +41,8 @@ final class EventApi(coll: Coll, cacheApi: lila.memo.CacheApi)(using Executor):
def one(id: String) = coll.byId[Event](id)
def editForm(event: Event) =
- EventForm.form fill {
+ EventForm.form fill:
EventForm.Data make event
- }
def update(old: Event, data: EventForm.Data)(using Me): Fu[Int] =
(coll.update.one($id(old.id), data.update(old)) andDo promotable.invalidateUnit()).dmap(_.n)
diff --git a/modules/event/src/main/EventForm.scala b/modules/event/src/main/EventForm.scala
index 4d5bb2111e651..ac0a7f3698e31 100644
--- a/modules/event/src/main/EventForm.scala
+++ b/modules/event/src/main/EventForm.scala
@@ -5,7 +5,7 @@ import play.api.data.Forms.*
import play.api.i18n.Lang
import lila.common.Form.{ stringIn, into, PrettyDateTime }
-import lila.i18n.LangList
+import lila.i18n.{ Language, LangList }
import lila.user.Me
object EventForm:
@@ -28,7 +28,7 @@ object EventForm:
"description" -> optional(text(minLength = 5, maxLength = 4000).into[Markdown]),
"homepageHours" -> bigDecimal(10, 2).verifying(d => d >= 0 && d <= 24),
"url" -> nonEmptyText,
- "lang" -> text.verifying(l => LangList.allChoices.exists(_._1 == l)),
+ "lang" -> lila.i18n.LangForm.popularLanguages.mapping,
"enabled" -> boolean,
"startsAt" -> PrettyDateTime.mapping,
"finishesAt" -> PrettyDateTime.mapping,
@@ -42,7 +42,7 @@ object EventForm:
description = none,
homepageHours = 0,
url = "",
- lang = lila.i18n.enLang.code,
+ lang = lila.i18n.defaultLanguage,
enabled = true,
startsAt = nowDateTime,
finishesAt = nowDateTime,
@@ -55,7 +55,7 @@ object EventForm:
description: Option[Markdown],
homepageHours: BigDecimal,
url: String,
- lang: String,
+ lang: Language,
enabled: Boolean,
startsAt: LocalDateTime,
finishesAt: LocalDateTime,
@@ -71,7 +71,7 @@ object EventForm:
description = description,
homepageHours = homepageHours.toDouble,
url = url,
- lang = Lang(lang),
+ lang = lang,
enabled = enabled,
startsAt = startsAt.instant,
finishesAt = finishesAt.instant,
@@ -90,7 +90,7 @@ object EventForm:
description = description,
homepageHours = homepageHours.toDouble,
url = url,
- lang = Lang(lang),
+ lang = lang,
enabled = enabled,
startsAt = startsAt.instant,
finishesAt = finishesAt.instant,
@@ -112,7 +112,7 @@ object EventForm:
description = event.description,
homepageHours = event.homepageHours,
url = event.url,
- lang = event.lang.code,
+ lang = event.lang,
enabled = event.enabled,
startsAt = event.startsAt.dateTime,
finishesAt = event.finishesAt.dateTime,
diff --git a/modules/game/src/main/Pov.scala b/modules/game/src/main/Pov.scala
index be0f60d5bd5ba..1490a9184f82a 100644
--- a/modules/game/src/main/Pov.scala
+++ b/modules/game/src/main/Pov.scala
@@ -5,14 +5,14 @@ import lila.user.User
case class Pov(game: Game, color: Color):
+ export game.{ id as gameId }
+
def player = game player color
def playerId = player.id
def fullId = game fullIdOf color
- def gameId = game.id
-
def opponent = game player !color
def unary_! = Pov(game, !color)
@@ -71,8 +71,8 @@ object Pov:
def ofCurrentTurn(game: Game) = Pov(game, game.turnColor)
- private def orInf(i: Option[Int]) = i getOrElse Int.MaxValue
- private def isFresher(a: Pov, b: Pov) = a.game.movedAt isAfter b.game.movedAt
+ private inline def orInf(inline i: Option[Int]) = i getOrElse Int.MaxValue
+ private def isFresher(a: Pov, b: Pov) = a.game.movedAt isAfter b.game.movedAt
def priority(a: Pov, b: Pov) =
if !a.isMyTurn && !b.isMyTurn then isFresher(a, b)
@@ -99,10 +99,9 @@ object PlayerRef:
PlayerRef(fullId.gameId, fullId.playerId)
case class LightPov(game: LightGame, color: Color):
- def gameId = game.id
+ export game.{ id as gameId }
def player = game player color
def opponent = game player !color
- // def win = game wonBy color
object LightPov:
diff --git a/modules/hub/src/main/AsyncActorSequencer.scala b/modules/hub/src/main/AsyncActorSequencer.scala
index 9126b2dd17b6f..97d324491f107 100644
--- a/modules/hub/src/main/AsyncActorSequencer.scala
+++ b/modules/hub/src/main/AsyncActorSequencer.scala
@@ -16,12 +16,11 @@ final class AsyncActorSequencer(maxSize: Max, timeout: FiniteDuration, name: Str
def run[A <: Matchable](task: Task[A]): Fu[A] = asyncActor.ask[A](TaskWithPromise(task, _))
- private[this] val asyncActor = BoundedAsyncActor(maxSize, name, logging) {
+ private[this] val asyncActor = BoundedAsyncActor(maxSize, name, logging):
case TaskWithPromise(task, promise) =>
promise.completeWith {
task().withTimeout(timeout, s"AsyncActorSequencer $name")
}.future
- }
// Distributes tasks to many sequencers
final class AsyncActorSequencers[K](
diff --git a/modules/hub/src/main/BoundedAsyncActor.scala b/modules/hub/src/main/BoundedAsyncActor.scala
index 982778c2d7b48..f142810f38dc4 100644
--- a/modules/hub/src/main/BoundedAsyncActor.scala
+++ b/modules/hub/src/main/BoundedAsyncActor.scala
@@ -16,24 +16,24 @@ final class BoundedAsyncActor(maxSize: Max, name: String, logging: Boolean = tru
import BoundedAsyncActor.*
def !(msg: Matchable): Boolean =
- stateRef.getAndUpdate { state =>
- Some {
- state.fold(emptyQueue) { q =>
- if q.size >= maxSize.value then q
- else q enqueue msg
- }
- }
- } match
- case None => // previous state was idle, we can run immediately
- run(msg)
- true
- case Some(q) =>
- val success = q.size < maxSize.value
- if !success then
- lila.mon.asyncActor.overflow(name).increment()
- if logging then lila.log("asyncActor").warn(s"[$name] queue is full ($maxSize)")
- else if logging && q.size >= monitorQueueSize then lila.mon.asyncActor.queueSize(name).record(q.size)
- success
+ stateRef
+ .getAndUpdate: state =>
+ Some:
+ state.fold(emptyQueue): q =>
+ if q.size >= maxSize.value then q
+ else q enqueue msg
+ .match
+ case None => // previous state was idle, we can run immediately
+ run(msg)
+ true
+ case Some(q) =>
+ val success = q.size < maxSize.value
+ if !success then
+ lila.mon.asyncActor.overflow(name).increment()
+ if logging then lila.log("asyncActor").warn(s"[$name] queue is full ($maxSize)")
+ else if logging && q.size >= monitorQueueSize then
+ lila.mon.asyncActor.queueSize(name).record(q.size)
+ success
def ask[A](makeMsg: Promise[A] => Matchable): Fu[A] =
val promise = Promise[A]()
diff --git a/modules/hub/src/main/EarlyMultiThrottler.scala b/modules/hub/src/main/EarlyMultiThrottler.scala
index fcfbd928a0df5..c0c1f71f1a53d 100644
--- a/modules/hub/src/main/EarlyMultiThrottler.scala
+++ b/modules/hub/src/main/EarlyMultiThrottler.scala
@@ -1,5 +1,6 @@
package lila.hub
+import scala.util.chaining.*
import akka.actor.*
import lila.log.Logger
@@ -13,11 +14,16 @@ final class EarlyMultiThrottler[K](logger: Logger)(using
system: ActorSystem
):
- private val actor = system.actorOf(Props(new EarlyMultiThrottlerActor(logger)))
+ private val actor = system.actorOf(Props(EarlyMultiThrottlerActor(logger)))
- def apply(id: K, cooldown: FiniteDuration)(run: => Funit) =
+ def apply(id: K, cooldown: FiniteDuration)(run: => Funit): Unit =
actor ! EarlyMultiThrottlerActor.Work(sr(id), run = () => run, cooldown)
+ def ask[A](id: K, cooldown: FiniteDuration)(run: => Fu[A]): Fu[A] =
+ val promise = Promise[A]()
+ actor ! EarlyMultiThrottlerActor.Work(sr(id), run = () => run.tap(promise.completeWith).void, cooldown)
+ promise.future
+
// actor based implementation
final private class EarlyMultiThrottlerActor(logger: Logger)(using Executor) extends Actor:
diff --git a/modules/i18n/src/main/I18nKeys.scala b/modules/i18n/src/main/I18nKeys.scala
index 22de1f406df7d..5232d60c4980d 100644
--- a/modules/i18n/src/main/I18nKeys.scala
+++ b/modules/i18n/src/main/I18nKeys.scala
@@ -216,7 +216,6 @@ object I18nKeys:
val `playingRightNow` = I18nKey("playingRightNow")
val `eventInProgress` = I18nKey("eventInProgress")
val `finished` = I18nKey("finished")
- val `finishesX` = I18nKey("finishesX")
val `abortGame` = I18nKey("abortGame")
val `gameAborted` = I18nKey("gameAborted")
val `standard` = I18nKey("standard")
@@ -297,7 +296,7 @@ object I18nKeys:
val `importGame` = I18nKey("importGame")
val `importGameExplanation` = I18nKey("importGameExplanation")
val `importGameCaveat` = I18nKey("importGameCaveat")
- val `importGameCaveat2` = I18nKey("importGameCaveat2")
+ val `importGameDataPrivacyWarning` = I18nKey("importGameDataPrivacyWarning")
val `thisIsAChessCaptcha` = I18nKey("thisIsAChessCaptcha")
val `clickOnTheBoardToMakeYourMove` = I18nKey("clickOnTheBoardToMakeYourMove")
val `captcha.fail` = I18nKey("captcha.fail")
@@ -698,9 +697,10 @@ object I18nKeys:
val `playVariationToCreateConditionalPremoves` = I18nKey("playVariationToCreateConditionalPremoves")
val `noConditionalPremoves` = I18nKey("noConditionalPremoves")
val `playX` = I18nKey("playX")
+ val `showUnreadLichessMessage` = I18nKey("showUnreadLichessMessage")
+ val `clickHereToReadIt` = I18nKey("clickHereToReadIt")
val `sorry` = I18nKey("sorry")
val `weHadToTimeYouOutForAWhile` = I18nKey("weHadToTimeYouOutForAWhile")
- val `timeoutExpires` = I18nKey("timeoutExpires")
val `why` = I18nKey("why")
val `pleasantChessExperience` = I18nKey("pleasantChessExperience")
val `goodPractice` = I18nKey("goodPractice")
@@ -720,6 +720,7 @@ object I18nKeys:
val `agreementPolicy` = I18nKey("agreementPolicy")
val `searchOrStartNewDiscussion` = I18nKey("searchOrStartNewDiscussion")
val `edit` = I18nKey("edit")
+ val `bullet` = I18nKey("bullet")
val `blitz` = I18nKey("blitz")
val `rapid` = I18nKey("rapid")
val `classical` = I18nKey("classical")
@@ -2480,6 +2481,7 @@ object I18nKeys:
object timeago:
val `justNow` = I18nKey("timeago:justNow")
val `rightNow` = I18nKey("timeago:rightNow")
+ val `completed` = I18nKey("timeago:completed")
val `inNbSeconds` = I18nKey("timeago:inNbSeconds")
val `inNbMinutes` = I18nKey("timeago:inNbMinutes")
val `inNbHours` = I18nKey("timeago:inNbHours")
@@ -2493,6 +2495,8 @@ object I18nKeys:
val `nbWeeksAgo` = I18nKey("timeago:nbWeeksAgo")
val `nbMonthsAgo` = I18nKey("timeago:nbMonthsAgo")
val `nbYearsAgo` = I18nKey("timeago:nbYearsAgo")
+ val `nbMinutesRemaining` = I18nKey("timeago:nbMinutesRemaining")
+ val `nbHoursRemaining` = I18nKey("timeago:nbHoursRemaining")
object oauthScope:
val `newAccessToken` = I18nKey("oauthScope:newAccessToken")
diff --git a/modules/i18n/src/main/I18nLangPicker.scala b/modules/i18n/src/main/I18nLangPicker.scala
index a81e46927a2b3..470aebb856ac9 100644
--- a/modules/i18n/src/main/I18nLangPicker.scala
+++ b/modules/i18n/src/main/I18nLangPicker.scala
@@ -50,11 +50,11 @@ object I18nLangPicker:
def byHref(code: String, req: RequestHeader): ByHref =
Lang get code flatMap findCloser match
- case Some(lang) if fixJavaLanguageCode(lang) == code =>
+ case Some(lang) if fixJavaLanguage(lang).value == code =>
if req.acceptLanguages.isEmpty || req.acceptLanguages.exists(_.language == lang.language)
then ByHref.Found(lang)
else ByHref.Refused(lang)
- case Some(lang) => ByHref.Redir(fixJavaLanguageCode(lang))
+ case Some(lang) => ByHref.Redir(fixJavaLanguage(lang).value)
case None => ByHref.NotFound
enum ByHref:
diff --git a/modules/i18n/src/main/I18nQuantity.scala b/modules/i18n/src/main/I18nQuantity.scala
index 8afaa9924cfea..be5292ccd5aaa 100644
--- a/modules/i18n/src/main/I18nQuantity.scala
+++ b/modules/i18n/src/main/I18nQuantity.scala
@@ -14,11 +14,10 @@ private enum I18nQuantity:
*/
private object I18nQuantity:
- type Language = String
type Selector = Count => I18nQuantity
def apply(lang: Lang, c: Count): I18nQuantity =
- langMap.getOrElse(lang.language, selectors.default)(c)
+ langMap.getOrElse(Language(lang), selectors.default)(c)
private object selectors:
@@ -111,8 +110,8 @@ private object I18nQuantity:
import selectors.*
- private val langMap: Map[Language, Selector] = LangList.all.map { (lang, _) =>
- lang.language -> lang.language.match
+ private val langMap: Map[Language, Selector] = LangList.all.map: (lang, _) =>
+ Language(lang) -> lang.language.match
case "fr" | "ff" | "kab" | "co" | "ak" | "am" | "bh" | "fil" | "tl" | "guw" | "hi" | "ln" | "mg" |
"nso" | "ti" | "wa" =>
@@ -148,4 +147,3 @@ private object I18nQuantity:
selectors.none
case _ => default
- }
diff --git a/modules/i18n/src/main/LangForm.scala b/modules/i18n/src/main/LangForm.scala
new file mode 100644
index 0000000000000..d1a4489718e14
--- /dev/null
+++ b/modules/i18n/src/main/LangForm.scala
@@ -0,0 +1,18 @@
+package lila.i18n
+
+import play.api.i18n.Lang
+import play.api.data.*
+import play.api.data.Forms.*
+
+object LangForm:
+
+ object allLanguages:
+ val choices: List[(Language, String)] = LangList.languageChoices
+ val mapping: Mapping[Language] = languageMapping(choices)
+
+ object popularLanguages:
+ val choices: List[(Language, String)] = LangList.popularLanguageChoices
+ val mapping: Mapping[Language] = languageMapping(choices)
+
+ private def languageMapping(choices: List[(Language, String)]): Mapping[Language] =
+ text.verifying(l => choices.exists(_._1.value == l)).transform(Language(_), _.value)
diff --git a/modules/i18n/src/main/LangList.scala b/modules/i18n/src/main/LangList.scala
index 04086bfd3fdc2..9100276260dc5 100644
--- a/modules/i18n/src/main/LangList.scala
+++ b/modules/i18n/src/main/LangList.scala
@@ -4,7 +4,7 @@ import play.api.i18n.Lang
object LangList:
- val all = Map(
+ val all: Map[Lang, String] = Map(
Lang("en", "GB") -> "English",
Lang("af", "ZA") -> "Afrikaans",
Lang("an", "ES") -> "Aragonés",
@@ -86,6 +86,7 @@ object LangList:
Lang("sa", "IN") -> "संस्कृत",
Lang("sk", "SK") -> "Slovenčina",
Lang("sl", "SI") -> "Slovenščina",
+ Lang("so", "SO") -> "Af Soomaali",
Lang("sq", "AL") -> "Shqip",
Lang("sr", "SP") -> "Српски језик",
Lang("sv", "SE") -> "Svenska",
@@ -119,7 +120,7 @@ object LangList:
private lazy val popular: List[Lang] =
// 26/04/2020 based on db.user4.aggregate({$sortByCount:'$lang'}).toArray()
- val langs =
+ val langs: Map[Lang, Int] =
"en-US en-GB ru-RU es-ES tr-TR fr-FR de-DE pt-BR it-IT pl-PL ar-SA fa-IR nl-NL id-ID nb-NO el-GR sv-SE uk-UA cs-CZ vi-VN sr-SP hr-HR hu-HU pt-PT he-IL fi-FI ca-ES da-DK ro-RO zh-CN bg-BG sk-SK ko-KR az-AZ ja-JP sl-SI lt-LT ka-GE mn-MN bs-BA hy-AM zh-TW lv-LV et-EE th-TH gl-ES sq-AL eu-ES hi-IN mk-MK uz-UZ be-BY ms-MY bn-BD is-IS af-ZA nn-NO ta-IN as-IN la-LA kk-KZ tl-PH mr-IN eo-UY gu-IN ky-KG kn-IN ml-IN cy-GB no-NO fo-FO zu-ZA jv-ID ga-IE ur-PK ur-IN te-IN sw-KE am-ET ia-IA sa-IN si-LK ps-AF mg-MG kmr-TR ne-NP tk-TM fy-NL pa-PK br-FR tt-RU cv-CU tg-TJ tp-TP yo-NG frp-IT pi-IN my-MM pa-IN kab-DZ io-EN gd-GB jbo-EN io-IO ckb-IR ceb-PH an-ES"
.split(' ')
.flatMap(Lang.get)
@@ -127,22 +128,33 @@ object LangList:
.toMap
all.keys.toList.sortBy(l => langs.getOrElse(l, Int.MaxValue))
- lazy val popularNoRegion: List[Lang] = popular.collect {
+ lazy val popularNoRegion: List[Lang] = popular.collect:
case l if defaultRegions.get(l.language).forall(_ == l) => l
- }
- lazy val popularAlternateLanguageCodes: List[String] =
- popularNoRegion.drop(1).take(20).map(fixJavaLanguageCode)
+ lazy val allLanguages: List[Language] = popularNoRegion.map(fixJavaLanguage)
+ lazy val popularLanguages: List[Language] = allLanguages.take(20)
+ lazy val popularAlternateLanguages: List[Language] = allLanguages.drop(1).take(20)
def name(lang: Lang): String = all.getOrElse(lang, lang.code)
def name(code: String): String = Lang.get(code).fold(code)(name)
- def nameByStr(str: String): String = I18nLangPicker.byStr(str).fold(str)(name)
+ def nameByStr(str: String): String = I18nLangPicker.byStr(str).fold(str)(name)
+ def nameByLanguage(l: Language): String = nameByStr(l.value)
+
+ lazy val languageChoices: List[(Language, String)] = all.view
+ .map: (l, name) =>
+ Language(l) -> name
+ .toList
+ .distinctBy(_._1)
+ .sortBy(_._1.value)
+
+ lazy val popularLanguageChoices: List[(Language, String)] =
+ popularNoRegion.flatMap: lang =>
+ all.get(lang).map(Language(lang) -> _)
lazy val allChoices: List[(String, String)] = all.view
- .map { case (l, name) =>
+ .map: (l, name) =>
l.code -> name
- }
.toList
.sortBy(_._1)
diff --git a/modules/i18n/src/main/package.scala b/modules/i18n/src/main/package.scala
index 7af70de706a49..ba193c018fb6a 100644
--- a/modules/i18n/src/main/package.scala
+++ b/modules/i18n/src/main/package.scala
@@ -11,6 +11,17 @@ private type Messages = Map[Lang, MessageMap]
private val logger = lila.log("i18n")
+/* play.api.i18n.Lang is composed of language and country.
+ * Let's make new types for those so we don't mix them.
+ */
+opaque type Language = String
+object Language extends OpaqueString[Language]:
+ def apply(lang: Lang): Language = lang.language
+
+opaque type Country = String
+object Country extends OpaqueString[Country]:
+ def apply(lang: Lang): Country = lang.country
+
private val lichessCodes: Map[String, Lang] = Map(
"fp" -> Lang("frp", "IT"),
"jb" -> Lang("jbo", "EN"),
@@ -18,11 +29,12 @@ private val lichessCodes: Map[String, Lang] = Map(
"tc" -> Lang("zh", "CN")
)
-val enLang = Lang("en", "GB")
-val defaultLang = enLang
+val defaultLanguage: Language = "en"
+val enLang = Lang("en", "GB")
+val defaultLang = enLang
// ffs
-def fixJavaLanguageCode(lang: Lang) =
- val code = lang.language
- if code == "in" then "id"
- else code
+def fixJavaLanguage(lang: Lang): Language =
+ val l = lang.language
+ if l == "in" then "id"
+ else l
diff --git a/modules/irc/src/main/IrcApi.scala b/modules/irc/src/main/IrcApi.scala
index b3e1f99c760e2..56fae7cf2050d 100644
--- a/modules/irc/src/main/IrcApi.scala
+++ b/modules/irc/src/main/IrcApi.scala
@@ -45,9 +45,11 @@ final class IrcApi(
dox = domain == ModDomain.Admin
)
- def nameCloseVote(user: User)(using mod: Me): Funit =
+ def nameCloseVote(user: User, reason: Option[String])(using mod: Me): Funit =
val topic = "/" + user.username
- zulip(_.mod.usernames, topic)(s"created on: ${user.createdAt.date}, ${user.count.game} games") >>
+ zulip(_.mod.usernames, topic)(
+ s"created on: ${user.createdAt.date}, ${user.count.game} games${reason.fold("")(r => s", reason: $r")}"
+ ) >>
zulip
.sendAndGetLink(_.mod.usernames, topic)("/poll Close?\n🔨 Yes\n🍃 No")
.flatMapz: zulipLink =>
diff --git a/modules/memo/src/main/Picfit.scala b/modules/memo/src/main/Picfit.scala
index 0ae7642ae2ce4..1ba90163c46d1 100644
--- a/modules/memo/src/main/Picfit.scala
+++ b/modules/memo/src/main/Picfit.scala
@@ -49,6 +49,7 @@ final class PicfitApi(coll: Coll, val url: PicfitUrl, ws: StandaloneWSClient, co
else
part.contentType
.collect:
+ case "image/webp" => "webp"
case "image/png" => "png"
case "image/jpeg" => "jpg"
.match
diff --git a/modules/memo/src/main/SettingStore.scala b/modules/memo/src/main/SettingStore.scala
index 7d995e223aacd..a455eda1a8082 100644
--- a/modules/memo/src/main/SettingStore.scala
+++ b/modules/memo/src/main/SettingStore.scala
@@ -6,7 +6,7 @@ import reactivemongo.api.bson.BSONHandler
import lila.db.dsl.*
import play.api.data.*, Forms.*
-import lila.common.{ Ints, Iso, Strings, UserIds }
+import lila.common.{ Ints, Iso, Strings, UserIds, config }
final class SettingStore[A: BSONHandler: SettingStore.StringReader: SettingStore.Formable] private (
coll: Coll,
@@ -60,16 +60,18 @@ object SettingStore:
final class StringReader[A](val read: String => Option[A])
object StringReader:
- given StringReader[Boolean] = StringReader[Boolean]({
+ given StringReader[Boolean] = StringReader[Boolean]:
case "on" | "yes" | "true" | "1" => true.some
case "off" | "no" | "false" | "0" => false.some
case _ => none
- })
given StringReader[Int] = StringReader[Int](_.toIntOption)
given StringReader[Float] = StringReader[Float](_.toFloatOption)
given StringReader[String] = StringReader[String](some)
def fromIso[A](using iso: Iso.StringIso[A]) = StringReader[A](v => iso.from(v).some)
+ private type CredOption = Option[lila.common.config.Credentials]
+ private type HostOption = Option[lila.common.config.HostPort]
+
object Strings:
val stringsIso = Iso.strings(",")
given BSONHandler[Strings] = lila.db.dsl.isoHandler(using stringsIso)
@@ -86,6 +88,14 @@ object SettingStore:
val regexIso = Iso.string[Regex](_.r, _.toString)
given BSONHandler[Regex] = lila.db.dsl.isoHandler(using regexIso)
given StringReader[Regex] = StringReader.fromIso(using regexIso)
+ object CredentialsOption:
+ val credentialsIso = Iso.string[CredOption](lila.common.config.Credentials.read, _.so(_.show))
+ given BSONHandler[CredOption] = lila.db.dsl.isoHandler(using credentialsIso)
+ given StringReader[CredOption] = StringReader.fromIso(using credentialsIso)
+ object HostPortOption:
+ val hostPortIso = Iso.string[HostOption](lila.common.config.HostPort.read, _.so(_.show))
+ given BSONHandler[HostOption] = lila.db.dsl.isoHandler(using hostPortIso)
+ given StringReader[HostOption] = StringReader.fromIso(using hostPortIso)
final class Formable[A](val form: A => Form[?])
object Formable:
@@ -97,5 +107,11 @@ object SettingStore:
given Formable[String] = Formable[String](v => Form(single("v" -> text)) fill v)
given Formable[Strings] = Formable[Strings](v => Form(single("v" -> text)) fill Strings.stringsIso.to(v))
given Formable[UserIds] = Formable[UserIds](v => Form(single("v" -> text)) fill UserIds.userIdsIso.to(v))
+ given Formable[CredOption] = stringPair(using CredentialsOption.credentialsIso)
+ given Formable[HostOption] = stringPair(using HostPortOption.hostPortIso)
+ private def stringPair[A](using iso: Iso.StringIso[A]): Formable[A] = Formable[A]: v =>
+ Form(
+ single("v" -> text.verifying(t => t.isEmpty || t.count(_ == ':') == 1))
+ ) fill iso.to(v)
private val dbField = "setting"
diff --git a/modules/mod/src/main/Modlog.scala b/modules/mod/src/main/Modlog.scala
index 8ebe2119fd0ea..8d1f292d75ba6 100644
--- a/modules/mod/src/main/Modlog.scala
+++ b/modules/mod/src/main/Modlog.scala
@@ -81,6 +81,7 @@ case class Modlog(
case Modlog.setKidMode => "set kid mode"
case Modlog.weakPassword => "log in with weak password"
case Modlog.blankedPassword => "log in with blanked password"
+ case Modlog.ublogRankAdjust => "adjust ublog post rank"
case a => a
override def toString = s"$mod $showAction $user $details"
@@ -162,6 +163,7 @@ object Modlog:
val setKidMode = "setKidMode"
val weakPassword = "weakPassword"
val blankedPassword = "blankedPassword"
+ val ublogRankAdjust = "ublogRankAdjust"
private val explainRegex = """^[\w-]{3,}+: (.++)$""".r
def explain(e: Modlog) = (e.index has "team") so ~e.details match
diff --git a/modules/mod/src/main/ModlogApi.scala b/modules/mod/src/main/ModlogApi.scala
index 94f6f92be7403..345f5668a6379 100644
--- a/modules/mod/src/main/ModlogApi.scala
+++ b/modules/mod/src/main/ModlogApi.scala
@@ -225,6 +225,9 @@ final class ModlogApi(repo: ModlogRepo, userRepo: UserRepo, ircApi: IrcApi, pres
def appealPost(user: UserId)(using me: Me) = add:
Modlog(me, user.some, Modlog.appealPost, details = none)
+ def ublogRankAdjust(user: UserId, postId: UblogPostId, adjust: Int)(using me: Me) = add:
+ Modlog(me.some, Modlog.ublogRankAdjust, details = s"$postId by $user, $adjust".some)
+
def wasUnengined(sus: Suspect) = coll.exists:
$doc(
"user" -> sus.user.id,
diff --git a/modules/oauth/src/main/Env.scala b/modules/oauth/src/main/Env.scala
index 0370439ce6288..1d1260b391c62 100644
--- a/modules/oauth/src/main/Env.scala
+++ b/modules/oauth/src/main/Env.scala
@@ -4,10 +4,9 @@ import com.softwaremill.macwire.*
import com.softwaremill.tagging.*
import play.api.Configuration
-import lila.common.config.CollName
+import lila.common.config.{ Secret, CollName }
import lila.common.Strings
import lila.memo.SettingStore.Strings.given
-import lila.common.config.Secret
@Module
final class Env(
diff --git a/modules/pref/src/main/Theme.scala b/modules/pref/src/main/Theme.scala
index 0cf1dd6358fc1..28fffb510fa87 100644
--- a/modules/pref/src/main/Theme.scala
+++ b/modules/pref/src/main/Theme.scala
@@ -40,7 +40,7 @@ object Theme extends ThemeObject:
new Theme("grey", "grey.jpg"),
new Theme("metal", "metal.jpg"),
new Theme("olive", "olive.jpg"),
- new Theme("newspaper", "newspaper.png"),
+ new Theme("newspaper", "svg/newspaper.svg"),
new Theme("purple", "svg/purple.svg"),
new Theme("purple-diag", "purple-diag.png"),
new Theme("pink", "pink-pyramid.png"),
diff --git a/modules/push/src/main/Device.scala b/modules/push/src/main/Device.scala
index 13d425faf50a2..ff06db0e4f4ef 100644
--- a/modules/push/src/main/Device.scala
+++ b/modules/push/src/main/Device.scala
@@ -4,10 +4,11 @@ final private case class Device(
_id: String, // Firebase token
platform: String, // cordova platform (android, ios, firebase)
userId: UserId,
- seenAt: Instant
+ seenAt: Instant,
+ ua: Option[UserAgent]
):
+ def isMobile = ua.exists(lila.common.HTTPRequest.isLichessMobile)
- def deviceId =
- platform match
- case "ios" => _id.grouped(8).mkString("<", " ", ">")
- case _ => _id
+ def deviceId = platform match
+ case "ios" => _id.grouped(8).mkString("<", " ", ">")
+ case _ => _id
diff --git a/modules/push/src/main/DeviceApi.scala b/modules/push/src/main/DeviceApi.scala
index 98f3fbe29a1ec..9dc22e1842e26 100644
--- a/modules/push/src/main/DeviceApi.scala
+++ b/modules/push/src/main/DeviceApi.scala
@@ -1,6 +1,7 @@
package lila.push
import reactivemongo.api.bson.*
+import play.api.mvc.RequestHeader
import lila.db.dsl.{ *, given }
import lila.user.User
@@ -27,7 +28,7 @@ final private class DeviceApi(coll: Coll)(using Executor):
private[push] def findLastOneByUserId(platform: String)(userId: UserId): Fu[Option[Device]] =
findLastManyByUserId(platform, 1)(userId) dmap (_.headOption)
- def register(user: User, platform: String, deviceId: String) =
+ def register(user: User, platform: String, deviceId: String)(using req: RequestHeader) =
lila.mon.push.register.in(platform).increment()
coll.update
.one(
@@ -36,7 +37,8 @@ final private class DeviceApi(coll: Coll)(using Executor):
_id = deviceId,
platform = platform,
userId = user.id,
- seenAt = nowInstant
+ seenAt = nowInstant,
+ ua = lila.common.HTTPRequest.userAgent(req)
),
upsert = true
)
diff --git a/modules/push/src/main/Env.scala b/modules/push/src/main/Env.scala
index 12379d43b5cab..c5bb2ba907445 100644
--- a/modules/push/src/main/Env.scala
+++ b/modules/push/src/main/Env.scala
@@ -1,13 +1,10 @@
package lila.push
import akka.actor.*
-import com.google.auth.oauth2.{ GoogleCredentials, ServiceAccountCredentials }
import com.softwaremill.macwire.*
import lila.common.autoconfig.{ *, given }
import play.api.Configuration
import play.api.libs.ws.StandaloneWSClient
-import java.nio.charset.StandardCharsets.UTF_8
-import scala.jdk.CollectionConverters.*
import lila.common.config.*
@@ -16,7 +13,7 @@ final private class PushConfig(
@ConfigName("collection.device") val deviceColl: CollName,
@ConfigName("collection.subscription") val subscriptionColl: CollName,
val web: WebPush.Config,
- val firebase: FirebasePush.Config
+ val firebase: FirebasePush.BothConfigs
)
@Module
@@ -26,6 +23,7 @@ final class Env(
db: lila.db.Db,
getLightUser: lila.common.LightUser.GetterFallback,
proxyRepo: lila.round.GameProxyRepo,
+ roundMobile: lila.round.RoundMobile,
gameRepo: lila.game.GameRepo,
notifyAllows: lila.notify.GetNotifyAllows,
postApi: lila.forum.ForumPostApi
@@ -35,24 +33,10 @@ final class Env(
def vapidPublicKey = config.web.vapidPublicKey
- private val deviceApi = new DeviceApi(db(config.deviceColl))
- val webSubscriptionApi = new WebSubscriptionApi(db(config.subscriptionColl))
+ private val deviceApi = DeviceApi(db(config.deviceColl))
+ val webSubscriptionApi = WebSubscriptionApi(db(config.subscriptionColl))
- def registerDevice = deviceApi.register
- def unregisterDevices = deviceApi.unregister
-
- private lazy val googleCredentials: Option[GoogleCredentials] =
- try
- config.firebase.json.value.some.filter(_.nonEmpty).map { json =>
- ServiceAccountCredentials
- .fromStream(new java.io.ByteArrayInputStream(json.getBytes(UTF_8)))
- .createScoped(Set("https://www.googleapis.com/auth/firebase.messaging").asJava)
- }
- catch
- case e: Exception =>
- logger.warn("Failed to create google credentials", e)
- none
- if googleCredentials.isDefined then logger.info("Firebase push notifications are enabled.")
+ export deviceApi.{ register as registerDevice, unregister as unregisterDevices }
private lazy val firebasePush = wire[FirebasePush]
@@ -71,7 +55,7 @@ final class Env(
"offerEventCorres",
"tourSoon",
"notifyPush"
- ) {
+ ):
case lila.game.actorApi.FinishGame(game, _) =>
logUnit { pushApi finish game }
case lila.hub.actorApi.round.CorresMoveEvent(move, _, pushable, _, _) if pushable =>
@@ -90,4 +74,3 @@ final class Env(
logUnit { pushApi notifyPush (to, content) }
case t: lila.hub.actorApi.push.TourSoon =>
logUnit { pushApi tourSoon t }
- }
diff --git a/modules/push/src/main/FirebasePush.scala b/modules/push/src/main/FirebasePush.scala
index dc9cdf7dc2a9a..74ab911c5f463 100644
--- a/modules/push/src/main/FirebasePush.scala
+++ b/modules/push/src/main/FirebasePush.scala
@@ -1,107 +1,128 @@
package lila.push
-import com.google.auth.oauth2.{ AccessToken, GoogleCredentials }
-import lila.common.autoconfig.*
+import com.google.auth.oauth2.{ AccessToken, GoogleCredentials, ServiceAccountCredentials }
import play.api.libs.json.*
import play.api.libs.ws.JsonBodyWritables.*
import play.api.libs.ws.StandaloneWSClient
import scala.concurrent.blocking
-import lila.common.Chronometer
+import lila.common.{ Chronometer, LazyFu }
import lila.memo.FrequencyThreshold
import play.api.ConfigLoader
import lila.common.config.Max
final private class FirebasePush(
- credentialsOpt: Option[GoogleCredentials],
deviceApi: DeviceApi,
ws: StandaloneWSClient,
- config: FirebasePush.Config
-)(using
- ec: Executor,
- scheduler: Scheduler
-):
+ configs: FirebasePush.BothConfigs
+)(using Executor, Scheduler):
+
+ if configs.lichobile.googleCredentials.isDefined then
+ logger.info("Lichobile Firebase push notifications are enabled.")
+ if configs.mobile.googleCredentials.isDefined then
+ logger.info("Mobile Firebase push notifications are enabled.")
private val workQueue =
lila.hub.AsyncActorSequencer(maxSize = Max(512), timeout = 10 seconds, name = "firebasePush")
- def apply(userId: UserId, data: => PushApi.Data): Funit =
- credentialsOpt so { creds =>
- deviceApi.findLastManyByUserId("firebase", 3)(userId) flatMap {
- case Nil => funit
- // access token has 1h lifetime and is requested only if expired
- case devices =>
- workQueue {
- Future {
- Chronometer.syncMon(_.blocking time "firebase") {
- blocking {
- creds.refreshIfExpired()
- creds.getAccessToken
- }
- }
- }
- }.chronometer.mon(_.push.googleTokenTime).result flatMap { token =>
- // TODO http batch request is possible using a multipart/mixed content
- // unfortunately it doesn't seem easily doable with play WS
- devices.map(send(token, _, data)).parallel.void
- }
+ def apply(userId: UserId, data: LazyFu[PushApi.Data]): Funit =
+ deviceApi.findLastManyByUserId("firebase", 3)(userId) flatMap:
+ _.traverse_ { device =>
+ val config = if device.isMobile then configs.mobile else configs.lichobile
+ config.googleCredentials.so: creds =>
+ for
+ data <- data.value
+ _ <-
+ if data.firebaseMod.contains(PushApi.Data.FirebaseMod.DataOnly) && !device.isMobile
+ then funit // don't send data messages to lichobile
+ else
+ for
+ // access token has 1h lifetime and is requested only if expired
+ token <- workQueue {
+ Future:
+ Chronometer.syncMon(_.blocking time "firebase"):
+ creds.refreshIfExpired()
+ creds.getAccessToken()
+ }.chronometer.mon(_.push.googleTokenTime).result
+ _ <- send(token, device, config, data)
+ yield ()
+ yield ()
}
- }
opaque type StatusCode = Int
object StatusCode extends OpaqueInt[StatusCode]
private val errorCounter = FrequencyThreshold[StatusCode](50, 10 minutes)
- private def send(token: AccessToken, device: Device, data: => PushApi.Data): Funit =
+ private def send(
+ token: AccessToken,
+ device: Device,
+ config: FirebasePush.Config,
+ data: PushApi.Data
+ ): Funit =
ws.url(config.url)
.withHttpHeaders(
"Authorization" -> s"Bearer ${token.getTokenValue}",
"Accept" -> "application/json",
"Content-type" -> "application/json; UTF-8"
)
- .post(
+ .post:
Json.obj(
"message" -> Json
.obj(
"token" -> device._id,
- // firebase doesn't support nested data object and we only use what is
- // inside userData
- "data" -> (data.payload \ "userData").asOpt[JsObject].map(transform(_)),
- "notification" -> Json.obj(
- "body" -> data.body,
- "title" -> data.title
- )
+ "data" -> toDataKeyValue:
+ data.firebaseMod.match
+ case Some(PushApi.Data.FirebaseMod.NotifOnly(mod)) => mod(data.payload.userData)
+ case _ => data.payload.userData
)
- .add(
- "apns" -> data.iosBadge.map(number =>
- Json.obj(
- "payload" -> Json.obj(
+ .add:
+ "notification" -> data.firebaseMod.match
+ case Some(PushApi.Data.FirebaseMod.DataOnly) => none
+ case _ => Json.obj("body" -> data.body, "title" -> data.title).some
+ .add:
+ "apns" -> data.iosBadge.map: number =>
+ Json.obj:
+ "payload" -> Json.obj:
"aps" -> Json.obj("badge" -> number)
- )
- )
- )
- )
)
- ) flatMap { res =>
- lila.mon.push.firebaseStatus(res.status).increment()
- if res.status == 200 then funit
- else if res.status == 404 then
- logger.info(s"Delete missing firebase device $device")
- deviceApi delete device
- else
- if errorCounter(res.status) then logger.warn(s"[push] firebase: ${res.status}")
- funit
- }
+ .flatMap: res =>
+ lila.mon.push.firebaseStatus(res.status).increment()
+ lila.mon.push
+ .firebaseType(data.firebaseMod.fold("both"):
+ case PushApi.Data.FirebaseMod.DataOnly => "data"
+ case PushApi.Data.FirebaseMod.NotifOnly(_) => "notif"
+ )
+ .increment()
+ if res.status == 200 then funit
+ else if res.status == 404 then
+ logger.info(s"Delete missing firebase device $device")
+ deviceApi delete device
+ else
+ if errorCounter(res.status) then logger.warn(s"[push] firebase: ${res.status}")
+ funit
- // filter out any non string value, otherwise Firebase API silently rejects
- // the request
- private def transform(obj: JsObject): JsObject =
- JsObject(obj.fields.collect {
- case (k, v: JsString) => s"lichess.$k" -> v
- case (k, v: JsNumber) => s"lichess.$k" -> JsString(v.toString)
- })
+ private def toDataKeyValue(data: PushApi.Data.KeyValue): JsObject = JsObject:
+ data.view
+ .map: (k, v) =>
+ s"lichess.$k" -> JsString(v)
+ .toMap
private object FirebasePush:
- final class Config(val url: String, val json: lila.common.config.Secret)
- given ConfigLoader[Config] = AutoConfig.loader[Config]
+ final class Config(val url: String, val json: lila.common.config.Secret):
+ lazy val googleCredentials: Option[GoogleCredentials] =
+ try
+ json.value.some.filter(_.nonEmpty) map: json =>
+ import java.nio.charset.StandardCharsets.UTF_8
+ import scala.jdk.CollectionConverters.*
+ ServiceAccountCredentials
+ .fromStream(new java.io.ByteArrayInputStream(json.getBytes(UTF_8)))
+ .createScoped(Set("https://www.googleapis.com/auth/firebase.messaging").asJava)
+ catch
+ case e: Exception =>
+ logger.warn("Failed to create google credentials", e)
+ none
+ final class BothConfigs(val lichobile: Config, val mobile: Config)
+ import lila.common.autoconfig.*
+ given ConfigLoader[Config] = AutoConfig.loader[Config]
+ given ConfigLoader[BothConfigs] = AutoConfig.loader[BothConfigs]
diff --git a/modules/push/src/main/PushApi.scala b/modules/push/src/main/PushApi.scala
index f74350f4f481f..612f22ea46327 100644
--- a/modules/push/src/main/PushApi.scala
+++ b/modules/push/src/main/PushApi.scala
@@ -2,10 +2,11 @@ package lila.push
import akka.actor.*
import play.api.libs.json.*
+import play.api.libs.json.Json.obj
import lila.challenge.Challenge
import lila.common.String.shorten
-import lila.common.{ LilaFuture, LightUser }
+import lila.common.{ LilaFuture, LightUser, LazyFu }
import lila.common.Json.given
import lila.game.{ Game, Namer, Pov }
import lila.hub.actorApi.map.Tell
@@ -18,11 +19,15 @@ final private class PushApi(
firebasePush: FirebasePush,
webPush: WebPush,
proxyRepo: lila.round.GameProxyRepo,
+ roundMobile: lila.round.RoundMobile,
gameRepo: lila.game.GameRepo,
notifyAllows: lila.notify.GetNotifyAllows,
postApi: lila.forum.ForumPostApi
)(using Executor, Scheduler)(using lightUser: LightUser.GetterFallback):
+ import PushApi.*
+ import PushApi.Data.payload
+
private[push] def notifyPush(to: Iterable[NotifyAllows], content: NotificationContent): Funit =
content match
case PrivateMessage(sender, text) =>
@@ -35,266 +40,218 @@ final private class PushApi(
lightUser(invitedBy).flatMap(luser => invitedToStudy(to.head, luser.titleName, studyName, studyId))
case _ => funit
+ private val offlineRoundNotif = Data.FirebaseMod.NotifOnly(_.filterNot(_._1 == "round")).some
+
def finish(game: Game): Funit =
if !game.isCorrespondence || game.hasAi then funit
else
- game.userIds
- .map { userId =>
- Pov(game, userId) so { pov =>
- IfAway(pov) {
- gameRepo.countWhereUserTurn(userId) flatMap { nbMyTurn =>
- asyncOpponentName(pov) flatMap { opponent =>
- maybePush(
- userId,
- _.finish,
- NotificationPref.GameEvent,
- PushApi.Data(
- title = pov.win match
- case Some(true) => "You won!"
- case Some(false) => "You lost."
- case _ => "It's a draw."
- ,
- body = s"Your game with $opponent is over.",
- stacking = Stacking.GameFinish,
- urgency = Urgency.VeryLow,
- payload = Json.obj(
- "userId" -> userId,
- "userData" -> Json.obj(
- "type" -> "gameFinish",
- "gameId" -> game.id,
- "fullId" -> pov.fullId
- )
- ),
- iosBadge = nbMyTurn.some.filter(0 <)
- )
- )
- }
- }
- }
- }
- }
- .parallel
- .void
+ game.userIds.traverse_ { userId =>
+ Pov(game, userId) so: pov =>
+ IfAway(pov):
+ maybePush(
+ userId,
+ _.finish,
+ NotificationPref.GameEvent,
+ data = LazyFu: () =>
+ for
+ nbMyTurn <- gameRepo.countWhereUserTurn(userId)
+ opponent <- asyncOpponentName(pov)
+ yield Data(
+ title = pov.win match
+ case Some(true) => "You won!"
+ case Some(false) => "You lost."
+ case _ => "It's a draw."
+ ,
+ body = s"Your game with $opponent is over.",
+ stacking = Stacking.GameFinish,
+ urgency = Urgency.VeryLow,
+ payload = payload(userId)(
+ "type" -> "gameFinish",
+ "gameId" -> game.id.value,
+ "fullId" -> pov.fullId.value
+ ),
+ iosBadge = nbMyTurn.some.filter(0 <)
+ )
+ )
+ }
def move(move: MoveEvent): Funit =
- LilaFuture.delay(2 seconds) {
- proxyRepo.game(move.gameId) flatMap {
- _.filter(_.playable) so { game =>
- val pov = Pov(game, game.player.color)
- game.player.userId so { userId =>
- IfAway(pov) {
- gameRepo.countWhereUserTurn(userId) flatMap { nbMyTurn =>
- asyncOpponentName(pov) flatMap { opponent =>
- game.sans.lastOption so { sanMove =>
- maybePush(
- userId,
- _.move,
- NotificationPref.GameEvent,
- PushApi.Data(
- title = "It's your turn!",
- body = s"$opponent played $sanMove",
- stacking = Stacking.GameMove,
- urgency = Urgency.Normal,
- payload = Json.obj(
- "userId" -> userId,
- "userData" -> corresGameJson(pov, "gameMove")
- ),
- iosBadge = nbMyTurn.some.filter(0 <)
- )
- )
- }
- }
- }
- }
- }
- }
- }
- }
+ LilaFuture.delay(2 seconds):
+ proxyRepo.game(move.gameId) flatMap:
+ _.filter(_.playable) so: game =>
+ game.sans.lastOption.so: sanMove =>
+ val pov = Pov(game, game.player.color)
+ game.player.userId so: userId =>
+ val data = LazyFu: () =>
+ for
+ nbMyTurn <- gameRepo.countWhereUserTurn(userId)
+ opponent <- asyncOpponentName(pov)
+ payload <- corresGamePayload(pov, "gameMove", userId)
+ yield Data(
+ title = "It's your turn!",
+ body = s"$opponent played $sanMove",
+ stacking = Stacking.GameMove,
+ urgency = Urgency.Normal,
+ payload = payload,
+ iosBadge = nbMyTurn.some.filter(0 <),
+ firebaseMod = offlineRoundNotif
+ )
+ IfAway(pov)(maybePush(userId, _.move, NotificationPref.GameEvent, data)) >>
+ alwaysPushFirebaseData(userId, _.move, data)
def takebackOffer(gameId: GameId): Funit =
- LilaFuture.delay(1 seconds) {
- proxyRepo.game(gameId) flatMap {
- _.filter(_.playable).so { game =>
+ LilaFuture.delay(1 seconds):
+ proxyRepo.game(gameId) flatMap:
+ _.filter(_.playable).so: game =>
game.players.collect {
case p if p.isProposingTakeback => Pov(game, game opponent p)
} so { pov => // the pov of the receiver
- pov.player.userId so { userId =>
- IfAway(pov) {
- asyncOpponentName(pov) flatMap { opponent =>
- maybePush(
- userId,
- _.takeback,
- NotificationPref.GameEvent,
- PushApi
- .Data(
- title = "Takeback offer",
- body = s"$opponent proposes a takeback",
- stacking = Stacking.GameTakebackOffer,
- urgency = Urgency.Normal,
- payload = Json.obj(
- "userId" -> userId,
- "userData" -> corresGameJson(pov, "gameTakebackOffer")
- )
- )
- )
- }
- }
- }
+ pov.player.userId so: userId =>
+ val data = LazyFu: () =>
+ for
+ opponent <- asyncOpponentName(pov)
+ payload <- corresGamePayload(pov, "gameTakebackOffer", userId)
+ yield Data(
+ title = "Takeback offer",
+ body = s"$opponent proposes a takeback",
+ stacking = Stacking.GameTakebackOffer,
+ urgency = Urgency.Normal,
+ payload = payload,
+ firebaseMod = offlineRoundNotif
+ )
+ IfAway(pov)(maybePush(userId, _.takeback, NotificationPref.GameEvent, data)) >>
+ alwaysPushFirebaseData(userId, _.takeback, data)
}
- }
- }
- }
def drawOffer(gameId: GameId): Funit =
- LilaFuture.delay(1 seconds) {
- proxyRepo.game(gameId) flatMap {
- _.filter(_.playable).so { game =>
+ LilaFuture.delay(1 seconds):
+ proxyRepo.game(gameId) flatMap:
+ _.filter(_.playable).so: game =>
game.players.collect {
case p if p.isOfferingDraw => Pov(game, game opponent p)
} so { pov => // the pov of the receiver
- pov.player.userId so { userId =>
- IfAway(pov) {
- asyncOpponentName(pov) flatMap { opponent =>
- maybePush(
- userId,
- _.takeback,
- NotificationPref.GameEvent,
- PushApi.Data(
- title = "Draw offer",
- body = s"$opponent offers a draw",
- stacking = Stacking.GameDrawOffer,
- urgency = Urgency.Normal,
- payload = Json.obj(
- "userId" -> userId,
- "userData" -> corresGameJson(pov, "gameDrawOffer")
- )
- )
- )
- }
- }
- }
+ pov.player.userId so: userId =>
+ val data = LazyFu: () =>
+ for
+ opponent <- asyncOpponentName(pov)
+ payload <- corresGamePayload(pov, "gameDrawOffer", userId)
+ yield Data(
+ title = "Draw offer",
+ body = s"$opponent offers a draw",
+ stacking = Stacking.GameDrawOffer,
+ urgency = Urgency.Normal,
+ payload = payload,
+ firebaseMod = offlineRoundNotif
+ )
+ IfAway(pov)(maybePush(userId, _.draw, NotificationPref.GameEvent, data)) >>
+ alwaysPushFirebaseData(userId, _.draw, data)
}
- }
- }
- }
def corresAlarm(pov: Pov): Funit =
pov.player.userId.so: userId =>
- asyncOpponentName(pov).flatMap { opponent =>
- maybePush(
- userId,
- _.corresAlarm,
- NotificationPref.GameEvent,
- PushApi.Data(
- title = "Time is almost up!",
- body = s"You are about to lose on time against $opponent",
- stacking = Stacking.GameMove,
- urgency = Urgency.High,
- payload = Json.obj(
- "userId" -> userId,
- "userData" -> corresGameJson(pov, "corresAlarm")
- )
- )
+ val data = LazyFu: () =>
+ for
+ opponent <- asyncOpponentName(pov)
+ payload <- corresGamePayload(pov, "corresAlarm", userId)
+ yield Data(
+ title = "Time is almost up!",
+ body = s"You are about to lose on time against $opponent",
+ stacking = Stacking.GameMove,
+ urgency = Urgency.High,
+ payload = payload,
+ firebaseMod = offlineRoundNotif
)
- }
+ maybePush(userId, _.corresAlarm, NotificationPref.GameEvent, data) >>
+ alwaysPushFirebaseData(userId, _.corresAlarm, data)
- private def corresGameJson(pov: Pov, typ: String) =
- Json.obj(
- "type" -> typ,
- "gameId" -> pov.gameId,
- "fullId" -> pov.fullId
- )
+ private def corresGamePayload(pov: Pov, typ: String, userId: UserId): Fu[Data.Payload] =
+ roundMobile
+ .offline(pov.game, pov.fullId.anyId)
+ .map: round =>
+ payload(userId)(
+ "type" -> typ,
+ "gameId" -> pov.gameId.value,
+ "fullId" -> pov.fullId.value,
+ "round" -> Json.stringify(round)
+ )
def privateMessage(to: NotifyAllows, senderId: UserId, senderName: String, text: String): Funit =
filterPush(
to,
_.message,
- PushApi.Data(
- title = senderName,
- body = text,
- stacking = Stacking.PrivateMessage,
- urgency = Urgency.Normal,
- payload = Json.obj(
- "userId" -> to.userId,
- "userData" -> Json.obj(
+ LazyFu.sync:
+ Data(
+ title = senderName,
+ body = text,
+ stacking = Stacking.PrivateMessage,
+ urgency = Urgency.Normal,
+ payload = payload(to.userId)(
"type" -> "newMessage",
- "threadId" -> senderId
+ "threadId" -> senderId.value
)
)
- )
)
def invitedToStudy(to: NotifyAllows, invitedBy: String, studyName: StudyName, studyId: StudyId): Funit =
filterPush(
to,
_.message,
- PushApi.Data(
- title = studyName.value,
- body = s"$invitedBy invited you to $studyName",
- stacking = Stacking.InvitedStudy,
- urgency = Urgency.Normal,
- payload = Json.obj(
- "userId" -> to.userId,
- "userData" -> Json.obj(
+ LazyFu.sync:
+ Data(
+ title = studyName.value,
+ body = s"$invitedBy invited you to $studyName",
+ stacking = Stacking.InvitedStudy,
+ urgency = Urgency.Normal,
+ payload = payload(to.userId)(
"type" -> "invitedStudy",
"invitedBy" -> invitedBy,
- "studyName" -> studyName,
- "studyId" -> studyId,
+ "studyName" -> studyName.value,
+ "studyId" -> studyId.value,
"url" -> s"https://lichess.org/study/$studyId"
)
)
- )
)
def challengeCreate(c: Challenge): Funit =
- c.destUser so { dest =>
- c.challengerUser.ifFalse(c.hasClock) so { challenger =>
- lightUser(challenger.id) flatMap { lightChallenger =>
+ c.destUser.so: dest =>
+ c.challengerUser.ifFalse(c.hasClock) so: challenger =>
+ lightUser(challenger.id) flatMap: lightChallenger =>
maybePush(
dest.id,
_.challenge.create,
NotificationPref.Challenge,
- PushApi.Data(
- title = s"${lightChallenger.titleName} (${challenger.rating.show}) challenges you!",
- body = describeChallenge(c),
- stacking = Stacking.ChallengeCreate,
- urgency = Urgency.Normal,
- payload = Json.obj(
- "userId" -> dest.id,
- "userData" -> Json.obj(
+ LazyFu.sync:
+ Data(
+ title = s"${lightChallenger.titleName} (${challenger.rating.show}) challenges you!",
+ body = describeChallenge(c),
+ stacking = Stacking.ChallengeCreate,
+ urgency = Urgency.Normal,
+ payload = payload(dest.id)(
"type" -> "challengeCreate",
- "challengeId" -> c.id
+ "challengeId" -> c.id.value
)
)
- )
)
- }
- }
- }
def challengeAccept(c: Challenge, joinerId: Option[UserId]): Funit =
- c.challengerUser.ifTrue(c.finalColor.white && !c.hasClock) so { challenger =>
- joinerId so lightUser.optional flatMap { lightJoiner =>
+ c.challengerUser.ifTrue(c.finalColor.white && !c.hasClock) so: challenger =>
+ joinerId so lightUser.optional flatMap: lightJoiner =>
maybePush(
challenger.id,
_.challenge.accept,
NotificationPref.Challenge,
- PushApi.Data(
- title = s"${lightJoiner.fold("A player")(_.titleName)} accepts your challenge!",
- body = describeChallenge(c),
- stacking = Stacking.ChallengeAccept,
- urgency = Urgency.Normal,
- payload = Json.obj(
- "userId" -> challenger.id,
- "userData" -> Json.obj(
+ LazyFu.sync:
+ Data(
+ title = s"${lightJoiner.fold("A player")(_.titleName)} accepts your challenge!",
+ body = describeChallenge(c),
+ stacking = Stacking.ChallengeAccept,
+ urgency = Urgency.Normal,
+ payload = payload(challenger.id)(
"type" -> "challengeAccept",
- "challengeId" -> c.id
+ "challengeId" -> c.id.value
)
)
- )
)
- }
- }
def tourSoon(tour: TourSoon): Funit =
tour.userIds.toList.traverse_ : userId =>
@@ -302,73 +259,62 @@ final private class PushApi(
userId,
_.tourSoon,
NotificationPref.TournamentSoon,
- PushApi
- .Data(
+ LazyFu.sync:
+ Data(
title = tour.tourName,
body = "The tournament is about to start!",
stacking = Stacking.ChallengeAccept,
urgency = Urgency.Normal,
- payload = Json
- .obj(
- "userId" -> userId,
- "userData" -> Json.obj(
- "type" -> "tourSoon",
- "tourId" -> tour.tourId,
- "tourName" -> tour.tourName,
- "path" -> s"/${if tour.swiss then "swiss" else "tournament"}/${tour.tourId}"
- )
- )
+ payload = payload(userId)(
+ "type" -> "tourSoon",
+ "tourId" -> tour.tourId,
+ "tourName" -> tour.tourName,
+ "path" -> s"/${if tour.swiss then "swiss" else "tournament"}/${tour.tourId}"
+ )
)
)
def forumMention(to: NotifyAllows, mentionedBy: String, topicName: String, postId: ForumPostId): Funit =
- postApi.getPost(postId) flatMap { post =>
- filterPush(
- to,
- _.forumMention,
- PushApi.Data(
- title = topicName,
- body = post.fold(topicName)(p => shorten(p.text, 57 - 3, "...")),
- stacking = Stacking.ForumMention,
- urgency = Urgency.Low,
- payload = Json.obj(
- "userId" -> to.userId,
- "userData" -> Json.obj(
+ filterPush(
+ to,
+ _.forumMention,
+ LazyFu: () =>
+ postApi.getPost(postId) map: post =>
+ Data(
+ title = topicName,
+ body = post.fold(topicName)(p => shorten(p.text, 57 - 3, "...")),
+ stacking = Stacking.ForumMention,
+ urgency = Urgency.Low,
+ payload = payload(to.userId)(
"type" -> "forumMention",
"mentionedBy" -> mentionedBy,
"topic" -> topicName,
- "postId" -> postId,
+ "postId" -> postId.value,
"url" -> s"https://lichess.org/forum/redirect/post/$postId"
)
)
- )
- )
- }
+ )
def streamStart(recips: Iterable[NotifyAllows], streamerId: UserId, streamerName: String): Funit =
- val pushData = PushApi.Data(
- title = streamerName,
- body = streamerName + " started streaming",
- stacking = Stacking.StreamStart,
- urgency = Urgency.Low,
- payload = Json.obj(
- "userData" -> Json.obj(
+ val pushData = LazyFu.sync:
+ Data(
+ title = streamerName,
+ body = streamerName + " started streaming",
+ stacking = Stacking.StreamStart,
+ urgency = Urgency.Low,
+ payload = payload(
"type" -> "streamStart",
- "streamerId" -> streamerId,
+ "streamerId" -> streamerId.value,
"url" -> s"https://lichess.org/streamer/$streamerId/redirect"
)
)
- )
val webRecips = recips.collect { case u if u.allows.web => u.userId }
webPush(webRecips, pushData).addEffects { res =>
lila.mon.push.send.streamStart("web", res.isSuccess, webRecips.size)
- } andDo {
- recips collect { case u if u.allows.device => u.userId } foreach {
- firebasePush(_, pushData).addEffects { res =>
+ } andDo:
+ recips collect { case u if u.allows.device => u.userId } foreach:
+ firebasePush(_, pushData).addEffects: res =>
lila.mon.push.send.streamStart("firebase", res.isSuccess, 1)
- }
- }
- }
private type MonitorType = lila.mon.push.send.type => ((String, Boolean, Int) => Unit)
@@ -376,20 +322,23 @@ final private class PushApi(
userId: UserId,
monitor: MonitorType,
event: NotificationPref.Event,
- data: PushApi.Data
+ data: LazyFu[Data]
): Funit =
notifyAllows(userId, event).flatMap: allows =>
filterPush(NotifyAllows(userId, allows), monitor, data)
- private def filterPush(to: NotifyAllows, monitor: MonitorType, data: PushApi.Data): Funit = for
- _ <- to.allows.web so webPush(to.userId, data).addEffects(res =>
+ private def filterPush(to: NotifyAllows, monitor: MonitorType, data: LazyFu[Data]): Funit = for
+ _ <- to.allows.web so webPush(to.userId, data).addEffects: res =>
monitor(lila.mon.push.send)("web", res.isSuccess, 1)
- )
- _ <- to.allows.device so firebasePush(to.userId, data).addEffects(res =>
+ _ <- to.allows.device so firebasePush(to.userId, data).addEffects: res =>
monitor(lila.mon.push.send)("firebase", res.isSuccess, 1)
- )
yield ()
+ // ignores notification preferences
+ private def alwaysPushFirebaseData(userId: UserId, monitor: MonitorType, data: LazyFu[Data]): Funit =
+ firebasePush(userId, data.dmap(_.copy(firebaseMod = Data.FirebaseMod.DataOnly.some))).addEffects: res =>
+ monitor(lila.mon.push.send)("firebase", res.isSuccess, 1)
+
private def describeChallenge(c: Challenge) =
import lila.challenge.Challenge.TimeControl.*
List(
@@ -405,10 +354,9 @@ final private class PushApi(
private def IfAway(pov: Pov)(f: => Funit): Funit =
lila.common.Bus.ask[Boolean]("roundSocket") { p =>
Tell(pov.gameId.value, IsOnGame(pov.color, p))
- } flatMap {
+ } flatMap:
if _ then funit
else f
- }
private def asyncOpponentName(pov: Pov): Fu[String] =
Namer.playerText(pov.opponent)(using lightUser.optional)
@@ -420,6 +368,20 @@ private object PushApi:
body: String,
stacking: Stacking,
urgency: Urgency,
- payload: JsObject,
- iosBadge: Option[Int] = None
+ payload: Data.Payload,
+ iosBadge: Option[Int] = None,
+ // https://firebase.google.com/docs/cloud-messaging/concept-options#data_messages
+ firebaseMod: Option[Data.FirebaseMod] = None
)
+
+ object Data:
+ // firebase doesn't support nested data object
+ type KeyValue = Seq[(String, String)]
+ case class Payload(userId: Option[UserId], userData: KeyValue)
+ def payload(userId: UserId)(pairs: (String, String)*): Payload = Payload(userId.some, pairs)
+ def payload(pairs: (String, String)*): Payload = Payload(none, pairs)
+
+ type KeyValueMod = Data.KeyValue => Data.KeyValue
+ enum FirebaseMod(val mod: KeyValueMod):
+ case NotifOnly(m: KeyValueMod) extends FirebaseMod(m)
+ case DataOnly extends FirebaseMod(identity)
diff --git a/modules/push/src/main/WebPush.scala b/modules/push/src/main/WebPush.scala
index 193c63986895d..096d907137d33 100644
--- a/modules/push/src/main/WebPush.scala
+++ b/modules/push/src/main/WebPush.scala
@@ -6,6 +6,8 @@ import play.api.libs.ws.JsonBodyWritables.*
import play.api.libs.ws.StandaloneWSClient
import play.api.ConfigLoader
+import lila.common.LazyFu
+import lila.common.Json.given
final private class WebPush(
webSubscriptionApi: WebSubscriptionApi,
@@ -13,17 +15,17 @@ final private class WebPush(
ws: StandaloneWSClient
)(using Executor):
- def apply(userId: UserId, data: => PushApi.Data): Funit =
- webSubscriptionApi.getSubscriptions(5)(userId) flatMap { subscriptions =>
- subscriptions.toNel so send(data)
- }
+ def apply(userId: UserId, data: LazyFu[PushApi.Data]): Funit =
+ webSubscriptionApi.getSubscriptions(5)(userId) flatMap sendTo(data)
- def apply(userIds: Iterable[UserId], data: => PushApi.Data): Funit =
- webSubscriptionApi.getSubscriptions(userIds, 5) flatMap { subs =>
- subs.toNel so send(data)
- }
+ def apply(userIds: Iterable[UserId], data: LazyFu[PushApi.Data]): Funit =
+ webSubscriptionApi.getSubscriptions(userIds, 5) flatMap sendTo(data)
+
+ private def sendTo(data: LazyFu[PushApi.Data])(subs: List[WebSubscription]): Funit =
+ subs.toNel.so: subs =>
+ data.value flatMap send(subs)
- private def send(data: => PushApi.Data)(subscriptions: NonEmptyList[WebSubscription]): Funit =
+ private def send(subscriptions: NonEmptyList[WebSubscription])(data: PushApi.Data): Funit =
ws.url(config.url)
.withHttpHeaders("ContentType" -> "application/json")
.post(
@@ -39,10 +41,12 @@ final private class WebPush(
}.toList),
"payload" -> Json
.obj(
- "title" -> data.title,
- "body" -> data.body,
- "tag" -> data.stacking.key,
- "payload" -> data.payload
+ "title" -> data.title,
+ "body" -> data.body,
+ "tag" -> data.stacking.key,
+ "payload" -> Json
+ .obj("userData" -> data.payload.userData.toMap)
+ .add("userId" -> data.payload.userId)
)
.toString,
"topic" -> data.stacking.key,
diff --git a/modules/rating/src/main/PerfType.scala b/modules/rating/src/main/PerfType.scala
index 724c687f591d8..dc66e5136468b 100644
--- a/modules/rating/src/main/PerfType.scala
+++ b/modules/rating/src/main/PerfType.scala
@@ -254,6 +254,7 @@ object PerfType:
byVariant(variant).fold(licon.CrownElite)(_.icon)
def trans(pt: PerfType)(using Lang): String = pt match
+ case Bullet => I18nKeys.bullet.txt()
case Blitz => I18nKeys.blitz.txt()
case Rapid => I18nKeys.rapid.txt()
case Classical => I18nKeys.classical.txt()
diff --git a/modules/relay/src/main/BSONHandlers.scala b/modules/relay/src/main/BSONHandlers.scala
index 9104f907f3747..5d87027758886 100644
--- a/modules/relay/src/main/BSONHandlers.scala
+++ b/modules/relay/src/main/BSONHandlers.scala
@@ -33,7 +33,9 @@ object BSONHandlers:
given BSONDocumentHandler[RelayRound] = Macros.handler
- given BSONDocumentHandler[RelayTour] = Macros.handler
+ private given BSONHandler[play.api.i18n.Lang] = langByCodeHandler
+ given BSONDocumentHandler[RelayTour.Spotlight] = Macros.handler
+ given BSONDocumentHandler[RelayTour] = Macros.handler
def readRoundWithTour(doc: Bdoc): Option[RelayRound.WithTour] = for
round <- doc.asOpt[RelayRound]
diff --git a/modules/relay/src/main/Env.scala b/modules/relay/src/main/Env.scala
index 033e52b0bc1c9..b6974deaa6f3f 100644
--- a/modules/relay/src/main/Env.scala
+++ b/modules/relay/src/main/Env.scala
@@ -1,11 +1,14 @@
package lila.relay
import akka.actor.*
+import scala.util.matching.Regex
import com.softwaremill.macwire.*
import com.softwaremill.tagging.*
import play.api.libs.ws.StandaloneWSClient
import lila.common.config.*
+import lila.memo.SettingStore
+import lila.memo.SettingStore.Formable.given
@Module
final class Env(
@@ -21,6 +24,7 @@ final class Env(
pgnDump: lila.game.PgnDump,
gameProxy: lila.round.GameProxyRepo,
cacheApi: lila.memo.CacheApi,
+ settingStore: SettingStore.Builder,
irc: lila.irc.IrcApi,
baseUrl: BaseUrl
)(using
@@ -60,6 +64,29 @@ final class Env(
private lazy val delay = wire[RelayDelay]
+ import SettingStore.CredentialsOption.given
+ val proxyCredentials = settingStore[Option[Credentials]](
+ "relayProxyCredentials",
+ default = none,
+ text =
+ "Broadcast: proxy credentials to fetch from external sources. Leave empty to use no proxy. Format: username:password".some
+ ).taggedWith[ProxyCredentials]
+
+ import SettingStore.HostPortOption.given
+ val proxyHostPort = settingStore[Option[HostPort]](
+ "relayProxyHostPort",
+ default = none,
+ text =
+ "Broadcast: proxy host and port to fetch from external sources. Leave empty to use no proxy. Format: host:port".some
+ ).taggedWith[ProxyHostPort]
+
+ import SettingStore.Regex.given
+ val proxyDomainRegex = settingStore[Regex](
+ "relayProxyDomainRegex",
+ default = "-".r,
+ text = "Broadcast: source domains that use a proxy, as a regex".some
+ ).taggedWith[ProxyDomainRegex]
+
// start the sync scheduler
wire[RelayFetch]
@@ -71,9 +98,10 @@ final class Env(
api.onStudyRemove(studyId)
},
"relayToggle" -> { case lila.study.actorApi.RelayToggle(id, v, who) =>
- studyApi.isContributor(id, who.u) foreach {
- _ so api.requestPlay(id into RelayRoundId, v)
- }
+ studyApi
+ .isContributor(id, who.u)
+ .foreach:
+ _ so api.requestPlay(id into RelayRoundId, v)
},
"kickStudy" -> { case lila.study.actorApi.Kick(studyId, userId, who) =>
roundRepo.tourIdByStudyId(studyId).flatMapz(api.kickBroadcast(userId, _, who))
@@ -90,3 +118,7 @@ private class RelayColls(mainDb: lila.db.Db, yoloDb: lila.db.AsyncDb @@ lila.db.
val round = mainDb(CollName("relay"))
val tour = mainDb(CollName("relay_tour"))
val delay = yoloDb(CollName("relay_delay"))
+
+private trait ProxyCredentials
+private trait ProxyHostPort
+private trait ProxyDomainRegex
diff --git a/modules/relay/src/main/JsonView.scala b/modules/relay/src/main/JsonView.scala
index 037a302916242..0eacbb52983d3 100644
--- a/modules/relay/src/main/JsonView.scala
+++ b/modules/relay/src/main/JsonView.scala
@@ -5,6 +5,7 @@ import play.api.libs.json.*
import lila.common.config.BaseUrl
import lila.common.Json.given
import lila.study.Chapter
+import lila.user.Me
final class JsonView(baseUrl: BaseUrl, markup: RelayMarkup, leaderboardApi: RelayLeaderboardApi)(using
Executor
@@ -13,45 +14,65 @@ final class JsonView(baseUrl: BaseUrl, markup: RelayMarkup, leaderboardApi: Rela
import JsonView.given
import lila.study.JsonView.given
+ given OWrites[RelayTour] = OWrites: t =>
+ Json
+ .obj(
+ "id" -> t.id,
+ "name" -> t.name,
+ "slug" -> t.slug,
+ "description" -> t.description
+ )
+ .add("official" -> t.official)
+
+ given OWrites[RelayRound] = OWrites: r =>
+ Json
+ .obj(
+ "id" -> r.id,
+ "name" -> r.name,
+ "slug" -> r.slug
+ )
+ .add("finished" -> r.finished)
+ .add("ongoing" -> (r.hasStarted && !r.finished))
+ .add("startsAt" -> r.startsAt.orElse(r.startedAt))
+
def apply(trs: RelayTour.WithRounds, withUrls: Boolean = false): JsObject =
Json
.obj(
"tour" -> Json
- .obj(
- "id" -> trs.tour.id,
- "name" -> trs.tour.name,
- "slug" -> trs.tour.slug,
- "description" -> trs.tour.description
- )
+ .toJsObject(trs.tour)
.add("markup" -> trs.tour.markup.map(markup(trs.tour)))
- .add("url" -> withUrls.option(s"$baseUrl/broadcast/${trs.tour.slug}/${trs.tour.id}"))
- .add("official" -> trs.tour.official),
- "rounds" -> trs.rounds.map { round =>
+ .add("url" -> withUrls.option(s"$baseUrl${trs.tour.path}")),
+ "rounds" -> trs.rounds.map: round =>
if withUrls then withUrl(round withTour trs.tour) else apply(round)
- }
)
- def apply(round: RelayRound): JsObject =
- Json
- .obj(
- "id" -> round.id,
- "name" -> round.name,
- "slug" -> round.slug
- )
- .add("finished" -> round.finished)
- .add("ongoing" -> (round.hasStarted && !round.finished))
- .add("startsAt" -> round.startsAt.orElse(round.startedAt))
+ def apply(round: RelayRound): JsObject = Json.toJsObject(round)
def withUrl(rt: RelayRound.WithTour): JsObject =
- apply(rt.round).add("url" -> s"$baseUrl${rt.path}".some)
+ apply(rt.round) ++ Json.obj(
+ "tour" -> rt.tour,
+ "url" -> s"$baseUrl${rt.path}"
+ )
- def withUrlAndGames(rt: RelayRound.WithTour, games: List[Chapter.Metadata]): JsObject =
- withUrl(rt) ++ Json.obj("games" -> games.map { g =>
- Json.toJsObject(g) + ("url" -> JsString(s"$baseUrl${rt.path}/${g._id}"))
- })
+ def withUrlAndGames(rt: RelayRound.WithTourAndStudy, games: List[Chapter.Metadata])(using
+ Option[Me]
+ ): JsObject =
+ myRound(rt) ++
+ Json.obj("games" -> games.map { g =>
+ Json.toJsObject(g) + ("url" -> JsString(s"$baseUrl${rt.path}/${g._id}"))
+ })
def sync(round: RelayRound) = Json toJsObject round.sync
+ def myRound(r: RelayRound.WithTourAndStudy)(using me: Option[Me]) = Json
+ .obj(
+ "round" -> apply(r.relay)
+ .add("url" -> s"$baseUrl${r.path}".some)
+ .add("delay" -> r.relay.sync.delay),
+ "tour" -> r.tour,
+ "study" -> Json.obj("writeable" -> me.exists(r.study.canContribute))
+ )
+
def makeData(
trs: RelayTour.WithRounds,
currentRoundId: RelayRoundId,
diff --git a/modules/relay/src/main/RelayApi.scala b/modules/relay/src/main/RelayApi.scala
index 35afd43118d5a..11d55831578e0 100644
--- a/modules/relay/src/main/RelayApi.scala
+++ b/modules/relay/src/main/RelayApi.scala
@@ -15,6 +15,8 @@ import lila.security.Granter
import lila.user.{ User, Me, MyId }
import lila.relay.RelayTour.ActiveWithSomeRounds
import lila.i18n.I18nKeys.streamer.visibility
+import lila.common.config.Max
+import lila.relay.RelayRound.WithTour
final class RelayApi(
roundRepo: RelayRoundRepo,
@@ -33,7 +35,7 @@ final class RelayApi(
def byId(id: RelayRoundId) = roundRepo.coll.byId[RelayRound](id)
- def byIdWithTour(id: RelayRoundId): Fu[Option[RelayRound.WithTour]] =
+ def byIdWithTour(id: RelayRoundId): Fu[Option[WithTour]] =
roundRepo.coll
.aggregateOne(): framework =>
import framework.*
@@ -50,13 +52,12 @@ final class RelayApi(
relay withTour tour
def byIdWithStudy(id: RelayRoundId): Fu[Option[RelayRound.WithTourAndStudy]] =
- byIdWithTour(id) flatMapz { case RelayRound.WithTour(relay, tour) =>
- studyApi.byId(relay.studyId) dmap2 {
+ byIdWithTour(id) flatMapz { case WithTour(relay, tour) =>
+ studyApi.byId(relay.studyId) dmap2:
RelayRound.WithTourAndStudy(relay, tour, _)
- }
}
- def byTourOrdered(tour: RelayTour): Fu[List[RelayRound.WithTour]] =
+ def byTourOrdered(tour: RelayTour): Fu[List[WithTour]] =
roundRepo.byTourOrdered(tour).dmap(_.map(_ withTour tour))
def roundIdsById(tourId: RelayTour.Id): Fu[List[StudyId]] =
@@ -64,7 +65,7 @@ final class RelayApi(
def kickBroadcast(userId: UserId, tourId: RelayTour.Id, who: MyId): Funit =
roundIdsById(tourId).flatMap:
- _.map(studyApi.kick(_, userId, who)).parallel.void
+ _.traverse_(studyApi.kick(_, userId, who))
def withRounds(tour: RelayTour) = roundRepo.byTourOrdered(tour).dmap(tour.withRounds)
@@ -81,7 +82,7 @@ final class RelayApi(
object defaultRoundToShow:
export cache.get
private val cache =
- cacheApi[RelayTour.Id, Option[RelayRound]](16, "relay.lastAndNextRounds"):
+ cacheApi[RelayTour.Id, Option[RelayRound]](32, "relay.lastAndNextRounds"):
_.expireAfterWrite(5 seconds).buildAsyncFuture: tourId =>
val chronoSort = $doc("startsAt" -> 1, "createdAt" -> 1)
val lastStarted = roundRepo.coll
@@ -150,15 +151,14 @@ final class RelayApi(
round.startsAt.fold(Long.MaxValue)(_.toMillis) // then by next round date
)
.flatMap:
- _.map: (tour, round) =>
+ _.traverse: (tour, round) =>
defaultRoundToShow
.get(tour.id)
.map: link =>
RelayTour.ActiveWithSomeRounds(tour, display = round, link = link | round)
- .parallel
.addEffect: trs =>
spotlightCache = trs
- .filter(_.tour.tier.has(RelayTour.Tier.BEST))
+ .filter(_.tour.spotlight.exists(_.enabled))
.filterNot(_.display.finished)
.filter: tr =>
tr.display.hasStarted || tr.display.startsAt.exists(_.isBefore(nowInstant.plusMinutes(30)))
@@ -177,21 +177,43 @@ final class RelayApi(
def tourById(id: RelayTour.Id) = tourRepo.coll.byId[RelayTour](id)
- private[relay] def toSync(official: Boolean, maxDocs: Int = 30) =
+ private def toSyncSelect = $doc(
+ "sync.until" $exists true,
+ "sync.nextAt" $lt nowInstant
+ )
+
+ private[relay] def toSyncOfficial(max: Max): Fu[List[WithTour]] =
roundRepo.coll
- .aggregateList(maxDocs, _.pri): framework =>
+ .aggregateList(max.value, _.pri): framework =>
import framework.*
- Match(
- $doc(
- "sync.until" $exists true,
- "sync.nextAt" $lt nowInstant
- )
- ) -> List(
+ Match(toSyncSelect) -> List(
PipelineOperator(tourRepo lookup "tourId"),
UnwindField("tour"),
- Match($doc("tour.tier" $exists official)),
- Sort(Descending("tour.tier")),
- Limit(maxDocs)
+ Match($doc("tour.tier" $exists true)),
+ Sort(Descending("tour.tier"), Ascending("sync.nextAt")),
+ Limit(max.value)
+ )
+ .map(_ flatMap readRoundWithTour)
+
+ private[relay] def toSyncUser(max: Max, maxPerUser: Max = Max(5)): Fu[List[WithTour]] =
+ roundRepo.coll
+ .aggregateList(max.value, _.pri): framework =>
+ import framework.*
+ Match(toSyncSelect) -> List(
+ PipelineOperator(tourRepo lookup "tourId"),
+ UnwindField("tour"),
+ Match($doc("tour.tier" $exists false)),
+ Sort(Ascending("sync.nextAt")),
+ GroupField("tour.ownerId")("relays" -> PushField("$ROOT")),
+ Project:
+ $doc(
+ "_id" -> false,
+ "relays" -> $doc("$slice" -> $arr("$relays", maxPerUser))
+ )
+ ,
+ UnwindField("relays"),
+ ReplaceRootField("relays"),
+ Limit(max.value)
)
.map(_ flatMap readRoundWithTour)
@@ -203,14 +225,14 @@ final class RelayApi(
tourRepo.coll.update.one($id(tour.id), data.update(tour)).void andDo
leaderboard.invalidate(tour.id)
- def create(data: RelayRoundForm.Data, tour: RelayTour)(using me: Me): Fu[RelayRound] =
+ def create(data: RelayRoundForm.Data, tour: RelayTour)(using me: Me): Fu[RelayRound.WithTourAndStudy] =
roundRepo.lastByTour(tour) flatMapz { last =>
studyRepo.byId(last.studyId)
} flatMap { lastStudy =>
import lila.study.{ StudyMember, StudyMembers }
val relay = data.make(me, tour)
- roundRepo.coll.insert.one(relay) >>
- studyApi.create(
+ for
+ study <- studyApi.create(
StudyMaker.ImportGame(
id = relay.studyId.some,
name = relay.name.into(StudyName).some,
@@ -230,9 +252,11 @@ final class RelayApi(
_.copy(
members = lastStudy.fold(StudyMembers.empty)(_.members) + StudyMember(me, StudyMember.Role.Write)
)
- ) >>
- tourRepo.setActive(tour.id, true) >>
- studyApi.addTopics(relay.studyId, List(StudyTopic.broadcast)) inject relay
+ ) orFail s"Can't create study for relay $relay"
+ _ <- roundRepo.coll.insert.one(relay)
+ _ <- tourRepo.setActive(tour.id, true)
+ _ <- studyApi.addTopics(relay.studyId, List(StudyTopic.broadcast.value))
+ yield relay.withTour(tour).withStudy(study.study)
}
def requestPlay(id: RelayRoundId, v: Boolean): Funit =
@@ -242,23 +266,26 @@ final class RelayApi(
if v then r.withSync(_.play) else r.withSync(_.pause)
.void
- def update(from: RelayRound)(f: RelayRound => RelayRound): Fu[RelayRound] =
+ def reFetchAndUpdate(round: RelayRound)(f: Update[RelayRound]): Fu[RelayRound] =
+ byId(round.id).orFail(s"Relay round ${round.id} not found").flatMap(update(_)(f))
+
+ def update(from: RelayRound)(f: Update[RelayRound]): Fu[RelayRound] =
val round = f(from).pipe: r =>
if r.sync.upstream != from.sync.upstream then r.withSync(_.clearLog) else r
- studyApi.rename(round.studyId, round.name into StudyName) >> {
- if round == from then fuccess(round)
- else
- for
- _ <- roundRepo.coll.update.one($id(round.id), round).void
- _ <- (round.sync.playing != from.sync.playing) so
- sendToContributors(round.id, "relaySync", jsonView sync round)
- _ <- (round.finished != from.finished) so denormalizeTourActive(round.tourId)
- yield
- round.sync.log.events.lastOption.ifTrue(round.sync.log != from.sync.log).foreach { event =>
+ if round == from then fuccess(round)
+ else
+ for
+ _ <- (from.name != round.name) so studyApi.rename(round.studyId, round.name into StudyName)
+ _ <- roundRepo.coll.update.one($id(round.id), round).void
+ _ <- (round.sync.playing != from.sync.playing) so
+ sendToContributors(round.id, "relaySync", jsonView sync round)
+ _ <- (round.finished != from.finished) so denormalizeTourActive(round.tourId)
+ yield
+ round.sync.log.events.lastOption
+ .ifTrue(round.sync.log != from.sync.log)
+ .foreach: event =>
sendToContributors(round.id, "relayLog", Json.toJsObject(event))
- }
- round
- }
+ round
def reset(old: RelayRound)(using me: Me): Funit =
WithRelay(old.id) { relay =>
@@ -288,7 +315,7 @@ final class RelayApi(
_ <- rounds.map(_ into StudyId).traverse_(studyApi.deleteById)
yield true
- def getOngoing(id: RelayRoundId): Fu[Option[RelayRound.WithTour]] =
+ def getOngoing(id: RelayRoundId): Fu[Option[WithTour]] =
roundRepo.coll.one[RelayRound]($doc("_id" -> id, "finished" -> false)) flatMapz { relay =>
tourById(relay.tourId) map2 relay.withTour
}
@@ -335,7 +362,7 @@ final class RelayApi(
visibility = Study.Visibility.Public
)
) >>
- studyApi.addTopics(round.studyId, List(StudyTopic.broadcast))
+ studyApi.addTopics(round.studyId, List(StudyTopic.broadcast.value))
_ <- roundRepo.coll.insert.one(round)
yield round
@@ -380,6 +407,17 @@ final class RelayApi(
.take(nb)
end officialTourStream
+ def myRounds(perSecond: MaxPerSecond, max: Option[Max])(using
+ me: Me
+ ): Source[RelayRound.WithTourAndStudy, ?] =
+ studyRepo
+ .sourceByMember(me.userId, isMe = true, select = studyRepo.selectBroadcast)
+ .mapAsync(1): study =>
+ byIdWithTour(study.id into RelayRoundId).map2(_.withStudy(study))
+ .mapConcat(identity)
+ .throttle(perSecond.value, 1 second)
+ .take(max.fold(9999)(_.value))
+
private[relay] def autoStart: Funit =
roundRepo.coll
.list[RelayRound](
@@ -391,10 +429,10 @@ final class RelayApi(
)
)
.flatMap:
- _.map: relay =>
+ _.traverse_ { relay =>
logger.info(s"Automatically start $relay")
requestPlay(relay.id, v = true)
- .parallel.void
+ }
private[relay] def autoFinishNotSyncing: Funit =
roundRepo.coll
@@ -409,10 +447,10 @@ final class RelayApi(
)
)
.flatMap:
- _.map: relay =>
+ _.traverse_ { relay =>
logger.info(s"Automatically finish $relay")
update(relay)(_.finish)
- .parallel.void
+ }
private[relay] def WithRelay[A: Zero](id: RelayRoundId)(f: RelayRound => Fu[A]): Fu[A] =
byId(id) flatMapz f
diff --git a/modules/relay/src/main/RelayDelay.scala b/modules/relay/src/main/RelayDelay.scala
index ce573ab4a97f2..c24e2e244159e 100644
--- a/modules/relay/src/main/RelayDelay.scala
+++ b/modules/relay/src/main/RelayDelay.scala
@@ -6,6 +6,7 @@ import lila.common.Seconds
import lila.db.dsl.{ *, given }
import lila.study.MultiPgn
import chess.format.pgn.PgnStr
+import lila.common.config.Max
final private class RelayDelay(colls: RelayColls)(using Executor):
@@ -14,7 +15,7 @@ final private class RelayDelay(colls: RelayColls)(using Executor):
def apply(
url: UpstreamUrl,
rt: RelayRound.WithTour,
- doFetchUrl: (UpstreamUrl, Int) => Fu[RelayGames]
+ doFetchUrl: (UpstreamUrl, Max) => Fu[RelayGames]
): Fu[RelayGames] =
dedupCache(url, rt.round, () => doFetchUrl(url, RelayFetch.maxChapters(rt.tour)))
.flatMap: latest =>
@@ -28,7 +29,7 @@ final private class RelayDelay(colls: RelayColls)(using Executor):
private val cache = CacheApi.scaffeineNoScheduler
.initialCapacity(8)
- .maximumSize(64)
+ .maximumSize(128)
.build[UpstreamUrl, GamesSeenBy]()
.underlying
@@ -54,16 +55,20 @@ final private class RelayDelay(colls: RelayColls)(using Executor):
def putIfNew(upstream: UpstreamUrl, games: RelayGames): Funit =
val newPgn = RelayGame.iso.from(games).toPgnStr
- getPgn(upstream, Seconds(0)).flatMap:
+ getLatestPgn(upstream).flatMap:
case Some(latestPgn) if latestPgn == newPgn => funit
case _ =>
- val doc = $doc("_id" -> idOf(upstream, nowInstant), "at" -> nowInstant, "pgn" -> newPgn)
+ val now = nowInstant
+ val doc = $doc("_id" -> idOf(upstream, now), "at" -> now, "pgn" -> newPgn)
colls.delay:
_.insert.one(doc).void
def get(upstream: UpstreamUrl, delay: Seconds): Fu[Option[RelayGames]] =
getPgn(upstream, delay).map2: pgn =>
- RelayGame.iso.to(MultiPgn.split(pgn, 999))
+ RelayGame.iso.to(MultiPgn.split(pgn, Max(999)))
+
+ private def getLatestPgn(upstream: UpstreamUrl): Fu[Option[PgnStr]] =
+ getPgn(upstream, Seconds(0))
private def getPgn(upstream: UpstreamUrl, delay: Seconds): Fu[Option[PgnStr]] =
colls.delay:
diff --git a/modules/relay/src/main/RelayFetch.scala b/modules/relay/src/main/RelayFetch.scala
index 7ee213d75c5c9..ae9499491ebab 100644
--- a/modules/relay/src/main/RelayFetch.scala
+++ b/modules/relay/src/main/RelayFetch.scala
@@ -15,6 +15,8 @@ import lila.round.GameProxyRepo
import lila.study.MultiPgn
import lila.tree.Node.Comments
import RelayRound.Sync.{ UpstreamIds, UpstreamUrl }
+import RelayFormat.CanProxy
+import lila.common.config.Max
final private class RelayFetch(
sync: RelaySync,
@@ -27,23 +29,26 @@ final private class RelayFetch(
gameProxy: GameProxyRepo
)(using Executor, Scheduler):
- LilaScheduler("RelayFetch.official", _.Every(500 millis), _.AtMost(15 seconds), _.Delay(30 seconds)):
+ import RelayFetch.*
+
+ LilaScheduler("RelayFetch.official", _.Every(500 millis), _.AtMost(15 seconds), _.Delay(15 seconds)):
syncRelays(official = true)
- LilaScheduler("RelayFetch.user", _.Every(750 millis), _.AtMost(10 seconds), _.Delay(1 minute)):
+ LilaScheduler("RelayFetch.user", _.Every(750 millis), _.AtMost(10 seconds), _.Delay(33 seconds)):
syncRelays(official = false)
- private def syncRelays(official: Boolean) =
- api
- .toSync(official)
+ private val maxRelaysToSync = Max(50)
+
+ private def syncRelays(official: Boolean): Funit =
+ val relays = if official then api.toSyncOfficial(maxRelaysToSync) else api.toSyncUser(maxRelaysToSync)
+ relays
.flatMap: relays =>
lila.mon.relay.ongoing(official).update(relays.size)
relays
.map: rt =>
if rt.round.sync.ongoing then
- processRelay(rt) flatMap { newRelay =>
- api.update(rt.round)(_ => newRelay)
- }
+ processRelay(rt) flatMap: updating =>
+ api.reFetchAndUpdate(rt.round)(updating.reRun)
else if rt.round.hasStarted then
logger.info(s"Finish by lack of activity ${rt.round}")
api.update(rt.round)(_.finish)
@@ -52,69 +57,84 @@ final private class RelayFetch(
logger.info(s"$msg ${rt.round}")
if rt.tour.official then irc.broadcastError(rt.round.id, rt.fullName, msg)
api.update(rt.round)(_.finish)
- else fuccess(rt.round)
+ else funit
.parallel
- .void
+ .void
// no writing the relay; only reading!
- private def processRelay(rt: RelayRound.WithTour): Fu[RelayRound] =
- if !rt.round.sync.playing then fuccess(rt.round.withSync(_.play))
+ // this can take a long time if the source is slow
+ private def processRelay(rt: RelayRound.WithTour): Fu[Updating[RelayRound]] =
+ val updating = Updating(rt.round)
+ if !rt.round.sync.playing then fuccess(updating(_.withSync(_.play)))
else
fetchGames(rt)
.map(games => rt.tour.players.fold(games)(_ update games))
.mon(_.relay.fetchTime(rt.tour.official, rt.round.slug))
.addEffect(gs => lila.mon.relay.games(rt.tour.official, rt.round.slug).update(gs.size))
.flatMap: games =>
- sync(rt, games)
+ sync
+ .updateStudyChapters(rt, games)
.withTimeoutError(7 seconds, SyncResult.Timeout)
.mon(_.relay.syncTime(rt.tour.official, rt.round.slug))
.map: res =>
- res -> rt.round
- .withSync(_ addLog SyncLog.event(res.nbMoves, none))
- .copy(finished = games.forall(_.end.isDefined))
+ res -> updating:
+ _.withSync(_ addLog SyncLog.event(res.nbMoves, none))
+ .copy(finished = games.nonEmpty && games.forall(_.ending.isDefined))
.recover:
case e: Exception =>
- e.match {
+ val result = e.match
case SyncResult.Timeout =>
if rt.tour.official then logger.info(s"Sync timeout ${rt.round}")
SyncResult.Timeout
case _ =>
if rt.tour.official then logger.info(s"Sync error ${rt.round} ${e.getMessage take 80}")
SyncResult.Error(e.getMessage)
- } -> rt.round.withSync(_ addLog SyncLog.event(0, e.some))
- .map: (result, newRelay) =>
- afterSync(result, newRelay withTour rt.tour)
+ result -> updating:
+ _.withSync(_ addLog SyncLog.event(0, e.some))
+ .map: (result, updatingRelay) =>
+ afterSync(result, rt.tour, updatingRelay)
- private def afterSync(result: SyncResult, rt: RelayRound.WithTour): RelayRound =
+ private def afterSync(
+ result: SyncResult,
+ tour: RelayTour,
+ updating: Updating[RelayRound]
+ ): Updating[RelayRound] =
+ val round = updating.current
result match
- case result: SyncResult.Ok if result.nbMoves == 0 => continueRelay(rt)
- case result: SyncResult.Ok =>
- lila.mon.relay.moves(rt.tour.official, rt.round.slug).increment(result.nbMoves)
- if !rt.round.hasStarted && !rt.tour.official then irc.broadcastStart(rt.round.id, rt.fullName)
- continueRelay(rt.round.ensureStarted.resume withTour rt.tour)
- case _ => continueRelay(rt)
+ case result: SyncResult.Ok if result.nbMoves > 0 =>
+ lila.mon.relay.moves(tour.official, round.slug).increment(result.nbMoves)
+ if !round.hasStarted && !tour.official then
+ irc.broadcastStart(round.id, round.withTour(tour).fullName)
+ continueRelay(tour, updating(_.ensureStarted.resume))
+ case _ => continueRelay(tour, updating)
- private def continueRelay(rt: RelayRound.WithTour): RelayRound =
- rt.round.sync.upstream.fold(rt.round): upstream =>
+ private def continueRelay(tour: RelayTour, updating: Updating[RelayRound]): Updating[RelayRound] =
+ val round = updating.current
+ round.sync.upstream.fold(updating): upstream =>
val seconds: Seconds =
- if rt.round.sync.log.alwaysFails then
- rt.round.sync.log.events.lastOption
+ if round.sync.log.alwaysFails then
+ round.sync.log.events.lastOption
.filterNot(_.isTimeout)
.flatMap(_.error)
- .ifTrue(rt.tour.official && rt.round.shouldHaveStarted)
+ .ifTrue(tour.official && round.shouldHaveStarted)
.filterNot(_ contains "Cannot parse moves")
.filterNot(_ contains "Found an empty PGN")
- .foreach { irc.broadcastError(rt.round.id, rt.fullName, _) }
+ .foreach { irc.broadcastError(round.id, round.withTour(tour).fullName, _) }
Seconds(60)
- else rt.round.sync.period | Seconds(if upstream.local then 3 else 6)
- rt.round.withSync:
- _.copy(
- nextAt = nowInstant plusSeconds {
- seconds.atLeast {
- if rt.round.sync.log.justTimedOut then 10 else 2
- }.value
- } some
- )
+ else
+ round.sync.period | Seconds:
+ if upstream.local then 3
+ else if upstream.asUrl.exists(_.isLcc) && !tour.official then 10
+ else 5
+ updating:
+ _.withSync:
+ _.copy(
+ nextAt = nowInstant plusSeconds {
+ seconds.atLeast {
+ if round.sync.log.justTimedOut then 10 else 2
+ }.value
+ } some
+ )
private val gameIdsUpstreamPgnFlags = PgnDump.WithFlags(
clocks = true,
@@ -135,21 +155,20 @@ final private class RelayFetch(
gameRepo.withInitialFens flatMap { games =>
if games.size == ids.size then
val pgnFlags = gameIdsUpstreamPgnFlags.copy(delayMoves = !rt.tour.official)
- games.map { (game, fen) =>
- pgnDump(game, fen, pgnFlags).dmap(_.render)
- }.parallel dmap MultiPgn.apply
+ games
+ .traverse: (game, fen) =>
+ pgnDump(game, fen, pgnFlags).dmap(_.render)
+ .dmap(MultiPgn.apply)
else
- throw LilaInvalid(
+ throw LilaInvalid:
s"Invalid game IDs: ${ids.filter(id => !games.exists(_._1.id == id)) mkString ", "}"
- )
- } flatMap {
- RelayFetch.multiPgnToGames(_).toFuture
- }
+ } flatMap:
+ multiPgnToGames(_).toFuture
case url: UpstreamUrl =>
- delayer(url, rt, doFetchUrl)
+ delayer(url, rt, fetchFromUpstream(using CanProxy(rt.tour.official)))
- private def doFetchUrl(upstream: UpstreamUrl, max: Int): Fu[RelayGames] =
- import RelayFetch.DgtJson.*
+ private def fetchFromUpstream(using canProxy: CanProxy)(upstream: UpstreamUrl, max: Max): Fu[RelayGames] =
+ import DgtJson.*
formatApi get upstream.withRound flatMap {
case RelayFormat.SingleFile(doc) =>
doc.format match
@@ -157,13 +176,12 @@ final private class RelayFetch(
case RelayFormat.DocFormat.Pgn => httpGetPgn(doc.url) map { MultiPgn.split(_, max) }
// maybe a single JSON game? Why not
case RelayFormat.DocFormat.Json =>
- httpGetJson[GameJson](doc.url) map { game =>
+ httpGetJson[GameJson](doc.url) map: game =>
MultiPgn(List(game.toPgn()))
- }
case RelayFormat.ManyFiles(indexUrl, makeGameDoc) =>
- httpGetJson[RoundJson](indexUrl) flatMap { round =>
- round.pairings
- .mapWithIndex: (pairing, i) =>
+ httpGetJson[RoundJson](indexUrl) flatMap: round =>
+ round.pairings.zipWithIndex
+ .map: (pairing, i) =>
val number = i + 1
val gameDoc = makeGameDoc(number)
gameDoc.format
@@ -173,28 +191,25 @@ final private class RelayFetch(
httpGetJson[GameJson](gameDoc.url).recover { case _: Exception =>
GameJson(moves = Nil, result = none)
} map { _.toPgn(pairing.tags) }
+ .recover: _ =>
+ PgnStr(s"${pairing.tags}\n\n${pairing.result}")
.map(number -> _)
.parallel
- .map { results =>
+ .map: results =>
MultiPgn(results.sortBy(_._1).map(_._2))
- }
- }
- } flatMap { RelayFetch.multiPgnToGames(_).toFuture }
+ } flatMap { multiPgnToGames(_).toFuture }
- private def httpGetPgn(url: URL): Fu[PgnStr] = PgnStr from formatApi.httpGet(url)
+ private def httpGetPgn(url: URL)(using CanProxy): Fu[PgnStr] = PgnStr from formatApi.httpGet(url)
- private def httpGetJson[A: Reads](url: URL): Fu[A] = for
+ private def httpGetJson[A: Reads](url: URL)(using CanProxy): Fu[A] = for
str <- formatApi.httpGet(url)
json <- Future(Json parse str) // Json.parse throws exceptions (!)
- data <-
- summon[Reads[A]]
- .reads(json)
- .fold(err => fufail(s"Invalid JSON from $url: $err"), fuccess)
+ data <- summon[Reads[A]].reads(json).fold(err => fufail(s"Invalid JSON from $url: $err"), fuccess)
yield data
-private[relay] object RelayFetch:
+private object RelayFetch:
- def maxChapters(tour: RelayTour) =
+ def maxChapters(tour: RelayTour) = Max:
lila.study.Study.maxChapters * (if tour.official then 2 else 1)
private[relay] object DgtJson:
@@ -223,7 +238,7 @@ private[relay] object RelayFetch:
given Reads[RoundJson] = Json.reads
case class GameJson(moves: List[String], result: Option[String]):
- def toPgn(extraTags: Tags = Tags.empty) =
+ def toPgn(extraTags: Tags = Tags.empty): PgnStr =
val strMoves = moves
.map(_ split ' ')
.mapWithIndex: (move, index) =>
@@ -272,5 +287,5 @@ private[relay] object RelayFetch:
comments = Comments.empty,
children = res.root.children.updateMainline(_.copy(comments = Comments.empty))
),
- end = res.end
+ ending = res.end
)
diff --git a/modules/relay/src/main/RelayFormat.scala b/modules/relay/src/main/RelayFormat.scala
index a5b831333077a..498e05accc605 100644
--- a/modules/relay/src/main/RelayFormat.scala
+++ b/modules/relay/src/main/RelayFormat.scala
@@ -1,33 +1,48 @@
package lila.relay
import io.mola.galimatias.URL
+import com.softwaremill.tagging.*
+import scala.util.matching.Regex
import play.api.libs.json.*
-import play.api.libs.ws.StandaloneWSClient
+import play.api.libs.ws.{ StandaloneWSClient, StandaloneWSRequest, DefaultWSProxyServer }
import play.api.libs.ws.DefaultBodyReadables.*
import chess.format.pgn.PgnStr
import lila.study.MultiPgn
-import lila.memo.CacheApi
import lila.memo.CacheApi.*
+import lila.memo.{ CacheApi, SettingStore }
+import lila.common.config.{ Max, Credentials, HostPort }
-final private class RelayFormatApi(ws: StandaloneWSClient, cacheApi: CacheApi)(using Executor):
+final private class RelayFormatApi(
+ ws: StandaloneWSClient,
+ cacheApi: CacheApi,
+ proxyCredentials: SettingStore[Option[Credentials]] @@ ProxyCredentials,
+ proxyHostPort: SettingStore[Option[HostPort]] @@ ProxyHostPort,
+ proxyDomainRegex: SettingStore[Regex] @@ ProxyDomainRegex
+)(using Executor):
import RelayFormat.*
import RelayRound.Sync.UpstreamUrl
- private val cache = cacheApi[UpstreamUrl.WithRound, RelayFormat](8, "relay.format"):
- _.refreshAfterWrite(10 minutes).expireAfterAccess(20 minutes).buildAsyncFuture(guessFormat)
+ private val cache = cacheApi[(UpstreamUrl.WithRound, CanProxy), RelayFormat](32, "relay.format"):
+ _.refreshAfterWrite(10 minutes)
+ .expireAfterAccess(20 minutes)
+ .buildAsyncFuture: (url, proxy) =>
+ guessFormat(url)(using proxy)
- def get(upstream: UpstreamUrl.WithRound): Fu[RelayFormat] = cache get upstream
+ def get(upstream: UpstreamUrl.WithRound)(using proxy: CanProxy): Fu[RelayFormat] =
+ cache get (upstream -> proxy)
- def refresh(upstream: UpstreamUrl.WithRound): Unit = cache invalidate upstream
+ def refresh(upstream: UpstreamUrl.WithRound): Unit =
+ CanProxy.from(List(false, true)) foreach: proxy =>
+ cache invalidate (upstream -> proxy)
- private def guessFormat(upstream: UpstreamUrl.WithRound): Fu[RelayFormat] = {
+ private def guessFormat(upstream: UpstreamUrl.WithRound)(using CanProxy): Fu[RelayFormat] = {
val originalUrl = URL parse upstream.url
// http://view.livechesscloud.com/ed5fb586-f549-4029-a470-d590f8e30c76
- def guessLcc(url: URL): Fu[Option[RelayFormat]] =
+ def guessLcc(url: URL)(using CanProxy): Fu[Option[RelayFormat]] =
url.toString match
case UpstreamUrl.LccRegex(id) =>
guessManyFiles:
@@ -35,15 +50,14 @@ final private class RelayFormatApi(ws: StandaloneWSClient, cacheApi: CacheApi)(u
s"http://1.pool.livechesscloud.com/get/$id/round-${upstream.round | 1}/index.json"
case _ => fuccess(none)
- def guessSingleFile(url: URL): Fu[Option[RelayFormat]] =
+ def guessSingleFile(url: URL)(using CanProxy): Fu[Option[RelayFormat]] =
List(
url.some,
!url.pathSegments.contains(mostCommonSingleFileName) option addPart(url, mostCommonSingleFileName)
- ).flatten.distinct.findM(looksLikePgn) dmap2 { (u: URL) =>
+ ).flatten.distinct.findM(looksLikePgn) dmap2: (u: URL) =>
SingleFile(pgnDoc(u))
- }
- def guessManyFiles(url: URL): Fu[Option[RelayFormat]] =
+ def guessManyFiles(url: URL)(using CanProxy): Fu[Option[RelayFormat]] =
(List(url) ::: mostCommonIndexNames
.filterNot(url.pathSegments.contains)
.map(addPart(url, _)))
@@ -52,9 +66,8 @@ final private class RelayFormatApi(ws: StandaloneWSClient, cacheApi: CacheApi)(u
val jsonUrl = (n: Int) => jsonDoc(replaceLastPart(index, s"game-$n.json"))
val pgnUrl = (n: Int) => pgnDoc(replaceLastPart(index, s"game-$n.pgn"))
looksLikeJson(jsonUrl(1).url).map(_ option jsonUrl) orElse
- looksLikePgn(pgnUrl(1).url).map(_ option pgnUrl) dmap2 {
+ looksLikePgn(pgnUrl(1).url).map(_ option pgnUrl) dmap2:
ManyFiles(index, _)
- }
guessLcc(originalUrl) orElse
guessSingleFile(originalUrl) orElse
@@ -63,31 +76,51 @@ final private class RelayFormatApi(ws: StandaloneWSClient, cacheApi: CacheApi)(u
logger.info(s"guessed format of $upstream: $format")
}
- private[relay] def httpGet(url: URL): Fu[String] =
- ws.url(url.toString)
- .withRequestTimeout(4.seconds)
- .withFollowRedirects(false)
+ private[relay] def httpGet(url: URL)(using CanProxy): Fu[String] =
+ val (req, proxy) = addProxy(url):
+ ws.url(url.toString)
+ .withRequestTimeout(4.seconds)
+ .withFollowRedirects(false)
+ req
.get()
.flatMap: res =>
if res.status == 200 then fuccess(res.body)
else fufail(s"[${res.status}] $url")
- .monSuccess(_.relay.httpGet(url.host.toString))
-
- private def looksLikePgn(body: String): Boolean =
- MultiPgn.split(PgnStr(body), 1).value.headOption so { pgn =>
+ .monSuccess(_.relay.httpGet(url.host.toString, proxy))
+
+ private def addProxy(url: URL)(ws: StandaloneWSRequest)(using
+ allowed: CanProxy
+ ): (StandaloneWSRequest, Option[String]) =
+ def server = for
+ hostPort <- proxyHostPort.get()
+ if allowed.yes
+ if proxyDomainRegex.get().unanchored.matches(url.host.toString)
+ creds <- proxyCredentials.get()
+ yield DefaultWSProxyServer(
+ host = hostPort.host,
+ port = hostPort.port,
+ principal = Some(creds.user),
+ password = Some(creds.password.value)
+ )
+ server.foldLeft(ws)(_ withProxyServer _) -> server.map(_.host)
+
+ private def looksLikePgn(body: String)(using CanProxy): Boolean =
+ MultiPgn.split(PgnStr(body), Max(1)).value.headOption so: pgn =>
lila.study.PgnImport(pgn, Nil).isRight
- }
- private def looksLikePgn(url: URL): Fu[Boolean] = httpGet(url) map looksLikePgn
+ private def looksLikePgn(url: URL)(using CanProxy): Fu[Boolean] = httpGet(url) map looksLikePgn
private def looksLikeJson(body: String): Boolean =
try Json.parse(body) != JsNull
catch case _: Exception => false
- private def looksLikeJson(url: URL): Fu[Boolean] = httpGet(url) map looksLikeJson
+ private def looksLikeJson(url: URL)(using CanProxy): Fu[Boolean] = httpGet(url) map looksLikeJson
sealed private trait RelayFormat
private object RelayFormat:
+ opaque type CanProxy = Boolean
+ object CanProxy extends YesNo[CanProxy]
+
enum DocFormat:
case Json, Pgn
diff --git a/modules/relay/src/main/RelayGame.scala b/modules/relay/src/main/RelayGame.scala
index 1064251972583..6573b13ff43f3 100644
--- a/modules/relay/src/main/RelayGame.scala
+++ b/modules/relay/src/main/RelayGame.scala
@@ -9,7 +9,7 @@ case class RelayGame(
tags: Tags,
variant: chess.variant.Variant,
root: Root,
- end: Option[PgnImport.End]
+ ending: Option[PgnImport.End]
):
def staticTagsMatch(chapterTags: Tags): Boolean =
@@ -27,7 +27,7 @@ case class RelayGame(
def resetToSetup = copy(
root = root.withoutChildren,
- end = None,
+ ending = None,
tags = tags.copy(value = tags.value.filter(_.name != Tag.Result))
)
diff --git a/modules/relay/src/main/RelayLeaderboard.scala b/modules/relay/src/main/RelayLeaderboard.scala
index 1a8a34ddef078..1b52396226aae 100644
--- a/modules/relay/src/main/RelayLeaderboard.scala
+++ b/modules/relay/src/main/RelayLeaderboard.scala
@@ -11,16 +11,15 @@ object RelayLeaderboard:
case class Player(name: String, score: Double, played: Int, rating: Option[Int])
import play.api.libs.json.*
- given OWrites[Player] = OWrites { p =>
+ given OWrites[Player] = OWrites: p =>
Json.obj("name" -> p.name, "score" -> p.score, "played" -> p.played).add("rating", p.rating)
- }
final class RelayLeaderboardApi(
tourRepo: RelayTourRepo,
roundRepo: RelayRoundRepo,
chapterRepo: ChapterRepo,
cacheApi: CacheApi
-)(using ec: Executor, scheduler: Scheduler):
+)(using Executor, Scheduler):
import BSONHandlers.given
@@ -39,14 +38,13 @@ final class RelayLeaderboardApi(
tour <- tourRepo.coll.byId[RelayTour](id) orFail s"No such relay tour $id"
roundIds <- roundRepo.idsByTourOrdered(tour)
tags <- chapterRepo.tagsByStudyIds(roundIds.map(_ into StudyId))
- players = tags.foldLeft(Map.empty[String, (Double, Int, Option[Int], Option[String])]) { (lead, game) =>
- chess.Color.all.foldLeft(lead) { case (lead, color) =>
- game(color.name).fold(lead) { name =>
- val (score, played) = game.outcome.fold((0d, 0)) {
+ players = tags.foldLeft(Map.empty[String, (Double, Int, Option[Int], Option[String])]): (lead, game) =>
+ chess.Color.all.foldLeft(lead): (lead, color) =>
+ game(color.name).fold(lead): name =>
+ val (score, played) = game.outcome.fold((0d, 0)):
case Outcome(None) => (0.5, 1)
case Outcome(Some(winner)) if winner == color => (1d, 1)
case _ => (0d, 1)
- }
val rating = game(s"${color}Elo").flatMap(_.toIntOption)
val title = game(s"${color}Title")
lead.getOrElse(name, (0d, 0, none, none)) match
@@ -55,12 +53,8 @@ final class RelayLeaderboardApi(
name,
(prevScore + score, prevPlayed + played, rating orElse prevRating, title orElse prevTitle)
)
- }
- }
- }
- yield RelayLeaderboard {
+ yield RelayLeaderboard:
players.toList.sortBy(-_._2._1) map { case (name, (score, played, rating, title)) =>
val fullName = title.fold(name)(t => s"$t $name")
RelayLeaderboard.Player(fullName, score, played, rating)
}
- }
diff --git a/modules/relay/src/main/RelayPush.scala b/modules/relay/src/main/RelayPush.scala
index 3be2eedbdd481..c6e31c4c7b232 100644
--- a/modules/relay/src/main/RelayPush.scala
+++ b/modules/relay/src/main/RelayPush.scala
@@ -4,32 +4,41 @@ import akka.actor.*
import chess.format.pgn.PgnStr
import lila.study.MultiPgn
+import lila.base.LilaInvalid
-final class RelayPush(sync: RelaySync, api: RelayApi)(using ActorSystem, Executor):
+final class RelayPush(sync: RelaySync, api: RelayApi, irc: lila.irc.IrcApi)(using ActorSystem, Executor):
private val throttler = lila.hub.EarlyMultiThrottler[RelayRoundId](logger)
- def apply(rt: RelayRound.WithTour, pgn: PgnStr): Fu[Option[String]] =
+ type Result = Either[LilaInvalid, Int]
+
+ def apply(rt: RelayRound.WithTour, pgn: PgnStr): Fu[Result] =
if rt.round.sync.hasUpstream
- then fuccess("The relay has an upstream URL, and cannot be pushed to.".some)
+ then fuccess(Left(LilaInvalid("The relay has an upstream URL, and cannot be pushed to.")))
else
- fuccess:
- throttler(rt.round.id, if rt.tour.official then 3.seconds else 7.seconds):
- pushNow(rt, pgn)
- none
+ throttler.ask[Result](rt.round.id, 1.seconds):
+ pushNow(rt, pgn)
- private def pushNow(rt: RelayRound.WithTour, pgn: PgnStr): Funit =
+ private def pushNow(rt: RelayRound.WithTour, pgn: PgnStr): Fu[Result] =
RelayFetch
.multiPgnToGames(MultiPgn.split(pgn, RelayFetch.maxChapters(rt.tour)))
- .toFuture
- .flatMap: games =>
- sync(rt, games)
- .map: res =>
- SyncLog.event(res.nbMoves, none)
- .recover:
- case e: Exception => SyncLog.event(0, e.some)
- .flatMap: event =>
- api
- .update(rt.round):
- _.withSync(_ addLog event).copy(finished = games.forall(_.end.isDefined))
- .void
+ .fold(
+ err => fuccess(Left(err)),
+ games =>
+ sync
+ .updateStudyChapters(rt, games)
+ .map: res =>
+ SyncLog.event(res.nbMoves, none)
+ .recover:
+ case e: Exception => SyncLog.event(0, e.some)
+ .flatMap: event =>
+ if !rt.round.hasStarted && !rt.tour.official && event.hasMoves then
+ irc.broadcastStart(rt.round.id, rt.fullName)
+ api
+ .update(rt.round): r1 =>
+ val r2 = r1.withSync(_ addLog event)
+ val r3 = if event.hasMoves then r2.ensureStarted.resume else r2
+ r3.copy(finished = games.nonEmpty && games.forall(_.ending.isDefined))
+ .inject:
+ event.error.fold(Right(event.moves))(err => Left(LilaInvalid(err)))
+ )
diff --git a/modules/relay/src/main/RelayRound.scala b/modules/relay/src/main/RelayRound.scala
index afce443979abd..7ffdbc909ab21 100644
--- a/modules/relay/src/main/RelayRound.scala
+++ b/modules/relay/src/main/RelayRound.scala
@@ -115,10 +115,10 @@ object RelayRound:
def local = asUrl.fold(true)(_.isLocal)
case class UpstreamUrl(url: String) extends Upstream:
def isLocal = url.contains("://127.0.0.1") || url.contains("://[::1]") || url.contains("://localhost")
- def withRound =
- url.split(" ", 2) match
- case Array(u, round) => UpstreamUrl.WithRound(u, round.toIntOption)
- case _ => UpstreamUrl.WithRound(url, none)
+ def withRound = url.split(" ", 2) match
+ case Array(u, round) => UpstreamUrl.WithRound(u, round.toIntOption)
+ case _ => UpstreamUrl.WithRound(url, none)
+ def isLcc: Boolean = UpstreamUrl.LccRegex.matches(url)
object UpstreamUrl:
case class WithRound(url: String, round: Option[Int])
val LccRegex = """.*view\.livechesscloud\.com/#?([0-9a-f\-]+)""".r
@@ -139,5 +139,6 @@ object RelayRound:
def withStudy(study: Study) = WithTourAndStudy(round, tour, study)
case class WithTourAndStudy(relay: RelayRound, tour: RelayTour, study: Study):
- def path = WithTour(relay, tour).path
- def fullName = WithTour(relay, tour).fullName
+ def withTour = WithTour(relay, tour)
+ def path = withTour.path
+ def fullName = withTour.fullName
diff --git a/modules/relay/src/main/RelaySync.scala b/modules/relay/src/main/RelaySync.scala
index 6eda16d728df8..f8d470604d483 100644
--- a/modules/relay/src/main/RelaySync.scala
+++ b/modules/relay/src/main/RelaySync.scala
@@ -14,7 +14,7 @@ final private class RelaySync(
leaderboard: RelayLeaderboardApi
)(using Executor):
- def apply(rt: RelayRound.WithTour, games: RelayGames): Fu[SyncResult.Ok] = for
+ def updateStudyChapters(rt: RelayRound.WithTour, games: RelayGames): Fu[SyncResult.Ok] = for
study <- studyApi.byId(rt.round.studyId).orFail("Missing relay study!")
chapters <- chapterRepo.orderedByStudy(study.id)
sanitizedGames <- RelayInputSanity(chapters, games).fold(x => fufail(x.msg), fuccess)
@@ -38,7 +38,7 @@ final private class RelaySync(
chapterRepo
.countByStudyId(study.id)
.flatMap:
- case nb if nb >= RelayFetch.maxChapters(rt.tour) => fuccess(none)
+ case nb if RelayFetch.maxChapters(rt.tour) <= nb => fuccess(none)
case _ =>
createChapter(study, game).flatMap: chapter =>
chapters.find(_.isEmptyInitial).ifTrue(chapter.order == 2).so { initial =>
@@ -83,13 +83,12 @@ final private class RelaySync(
chapter.root.nodeAt(path) match
case None => parentPath -> gameNode.some
case Some(existing) =>
- gameNode.clock.filter(c => !existing.clock.has(c)) so { c =>
+ gameNode.clock.filter(c => !existing.clock.has(c)) so: c =>
studyApi.setClock(
studyId = study.id,
position = Position(chapter, path).ref,
clock = c.some
)(who)
- }
path -> none
case (found, _) => found
} match
@@ -131,7 +130,7 @@ final private class RelaySync(
val gameTags = game.tags.value.foldLeft(Tags(Nil)): (newTags, tag) =>
if !chapter.tags.value.has(tag) then newTags + tag
else newTags
- val newEndTag = game.end
+ val newEndTag = game.ending
.ifFalse(gameTags(_.Result).isDefined)
.filterNot(end => chapter.tags(_.Result).has(end.resultText))
.map(end => Tag(_.Result, end.resultText))
diff --git a/modules/relay/src/main/RelayTour.scala b/modules/relay/src/main/RelayTour.scala
index b2e731908dca2..149e0cfd6abb6 100644
--- a/modules/relay/src/main/RelayTour.scala
+++ b/modules/relay/src/main/RelayTour.scala
@@ -1,8 +1,9 @@
package lila.relay
-import ornicar.scalalib.ThreadLocalRandom
+import play.api.i18n.Lang
import lila.user.User
+import lila.i18n.Language
case class RelayTour(
_id: RelayTour.Id,
@@ -14,6 +15,7 @@ case class RelayTour(
tier: Option[RelayTour.Tier], // if present, it's an official broadcast
active: Boolean, // a round is scheduled or ongoing
syncedAt: Option[Instant], // last time a round was synced
+ spotlight: Option[RelayTour.Spotlight] = None,
autoLeaderboard: Boolean = true,
players: Option[RelayPlayers] = None
):
@@ -54,6 +56,10 @@ object RelayTour:
case (t, n) if t == tier.toString => n
} | "???"
+ case class Spotlight(enabled: Boolean, language: Language, title: Option[String]):
+ def isEmpty = !enabled && specialLanguage.isEmpty && title.isEmpty
+ def specialLanguage: Option[Language] = language != lila.i18n.defaultLanguage option language
+
case class WithRounds(tour: RelayTour, rounds: List[RelayRound])
case class ActiveWithSomeRounds(tour: RelayTour, display: RelayRound, link: RelayRound)
@@ -64,4 +70,5 @@ object RelayTour:
def link = round
def display = round
+ import ornicar.scalalib.ThreadLocalRandom
def makeId = Id(ThreadLocalRandom nextString 8)
diff --git a/modules/relay/src/main/RelayTourForm.scala b/modules/relay/src/main/RelayTourForm.scala
index 1484ec8bf3683..da3595ef450b8 100644
--- a/modules/relay/src/main/RelayTourForm.scala
+++ b/modules/relay/src/main/RelayTourForm.scala
@@ -6,11 +6,17 @@ import play.api.data.Forms.*
import lila.common.Form.{ cleanText, formatter, into }
import lila.security.Granter
import lila.user.Me
+import lila.i18n.LangForm
final class RelayTourForm:
import RelayTourForm.*
+ val spotlightMapping =
+ mapping("enabled" -> boolean, "lang" -> LangForm.popularLanguages.mapping, "title" -> optional(text))(
+ RelayTour.Spotlight.apply
+ )(unapply)
+
val form = Form(
mapping(
"name" -> cleanText(minLength = 3, maxLength = 80),
@@ -18,7 +24,8 @@ final class RelayTourForm:
"markdown" -> optional(cleanText(maxLength = 20_000).into[Markdown]),
"tier" -> optional(number(min = RelayTour.Tier.NORMAL, max = RelayTour.Tier.BEST)),
"autoLeaderboard" -> boolean,
- "players" -> optional(of(formatter.stringFormatter[RelayPlayers](_.text, RelayPlayers.apply)))
+ "players" -> optional(of(formatter.stringFormatter[RelayPlayers](_.text, RelayPlayers.apply))),
+ "spotlight" -> optional(spotlightMapping)
)(Data.apply)(unapply)
)
@@ -34,7 +41,8 @@ object RelayTourForm:
markup: Option[Markdown],
tier: Option[RelayTour.Tier],
autoLeaderboard: Boolean,
- players: Option[RelayPlayers]
+ players: Option[RelayPlayers],
+ spotlight: Option[RelayTour.Spotlight]
):
def update(tour: RelayTour)(using Me) =
@@ -45,7 +53,8 @@ object RelayTourForm:
markup = markup,
tier = tier ifTrue Granter(_.Relay),
autoLeaderboard = autoLeaderboard,
- players = players
+ players = players,
+ spotlight = spotlight.filterNot(_.isEmpty)
)
.reAssignIfOfficial
@@ -61,7 +70,8 @@ object RelayTourForm:
createdAt = nowInstant,
syncedAt = none,
autoLeaderboard = autoLeaderboard,
- players = players
+ players = players,
+ spotlight = spotlight.filterNot(_.isEmpty)
).reAssignIfOfficial
object Data:
@@ -73,5 +83,6 @@ object RelayTourForm:
markup = tour.markup,
tier = tour.tier,
autoLeaderboard = tour.autoLeaderboard,
- players = tour.players
+ players = tour.players,
+ spotlight = tour.spotlight
)
diff --git a/modules/relay/src/main/SyncLog.scala b/modules/relay/src/main/SyncLog.scala
index 9c7a472d42af0..e12240a4b6edb 100644
--- a/modules/relay/src/main/SyncLog.scala
+++ b/modules/relay/src/main/SyncLog.scala
@@ -29,8 +29,8 @@ object SyncLog:
error: Option[String],
at: Instant
):
- def isOk = error.isEmpty
- def isKo = error.nonEmpty
+ export error.{ isEmpty as isOk, nonEmpty as isKo }
+ def hasMoves = moves > 0
def isTimeout = error has SyncResult.Timeout.getMessage
def event(moves: Int, e: Option[Exception]) =
diff --git a/modules/round/src/main/Env.scala b/modules/round/src/main/Env.scala
index 63bf522c85fa9..c9205ce56606a 100644
--- a/modules/round/src/main/Env.scala
+++ b/modules/round/src/main/Env.scala
@@ -83,7 +83,7 @@ final class Env(
private lazy val roundDependencies = wire[RoundAsyncActor.Dependencies]
private given lila.user.FlairApi.Getter = flairApi.getter
- lazy val roundSocket: RoundSocket = wire[RoundSocket]
+ lazy val roundSocket: RoundSocket = wire[RoundSocket]
private def resignAllGamesOf(userId: UserId) =
gameRepo allPlaying userId foreach:
@@ -199,5 +199,5 @@ final class Env(
if pov.game.abortableByUser then tellRound(pov.gameId, Abort(pov.playerId))
else if pov.game.resignable then tellRound(pov.gameId, Resign(pov.playerId))
-trait SelfReportEndGame
-trait SelfReportMarkUser
+private trait SelfReportEndGame
+private trait SelfReportMarkUser
diff --git a/modules/round/src/main/RoundAsyncActor.scala b/modules/round/src/main/RoundAsyncActor.scala
index b200689dd4a74..9722c0c4f0d45 100644
--- a/modules/round/src/main/RoundAsyncActor.scala
+++ b/modules/round/src/main/RoundAsyncActor.scala
@@ -416,7 +416,7 @@ final private[round] class RoundAsyncActor(
publishBoardBotGone(pov, millis.some)
private def publishBoardBotGone(pov: Pov, millis: Option[Long]) =
- if lila.game.Game.isBoardOrBotCompatible(pov.game) then
+ if Game.isBoardOrBotCompatible(pov.game) then
lila.common.Bus.publish(
lila.game.actorApi.BoardGone(pov, millis.map(m => (m.atLeast(0) / 1000).toInt)),
lila.game.actorApi.BoardGone makeChan gameId
@@ -470,7 +470,7 @@ final private[round] class RoundAsyncActor(
object RoundAsyncActor:
case class HasUserId(userId: UserId, promise: Promise[Boolean])
- case class SetGameInfo(game: lila.game.Game, goneWeights: (Float, Float))
+ case class SetGameInfo(game: Game, goneWeights: (Float, Float))
case object Tick
case object Stop
case object WsBoot
diff --git a/modules/round/src/main/RoundMobile.scala b/modules/round/src/main/RoundMobile.scala
index 41a3289764a8d..69283e3fc5b5f 100644
--- a/modules/round/src/main/RoundMobile.scala
+++ b/modules/round/src/main/RoundMobile.scala
@@ -12,7 +12,16 @@ import chess.{ Color, ByColor }
import lila.pref.Pref
import lila.chat.Chat
-final private class RoundMobile(
+object RoundMobile:
+
+ enum UseCase(val socketStatus: Option[SocketStatus], val chat: Boolean, val prefs: Boolean):
+ // full round for every-day use
+ case Online(socket: SocketStatus) extends UseCase(socket.some, chat = true, prefs = true)
+ // correspondence game sent through firebase data
+ // https://github.com/lichess-org/mobile/blob/main/lib/src/model/correspondence/offline_correspondence_game.dart
+ case Offline extends UseCase(none, chat = false, prefs = false)
+
+final class RoundMobile(
lightUserGet: LightUser.Getter,
gameRepo: GameRepo,
jsonView: lila.game.JsonView,
@@ -23,62 +32,70 @@ final private class RoundMobile(
chatApi: lila.chat.ChatApi
)(using Executor, lila.user.FlairApi):
+ import RoundMobile.*
private given play.api.i18n.Lang = lila.i18n.defaultLang
- def json(gameSockets: List[GameAndSocketStatus])(using me: Me): Fu[JsArray] =
+ def online(gameSockets: List[GameAndSocketStatus])(using me: Me): Fu[JsArray] =
gameSockets
.flatMap: gs =>
Pov(gs.game, me).map(_ -> gs.socket)
.traverse: (pov, socket) =>
- json(pov.game, pov.fullId.anyId, socket.some)
+ online(pov.game, pov.fullId.anyId, socket)
.map(JsArray(_))
- def json(game: Game, id: GameAnyId, socket: Option[SocketStatus]): Fu[JsObject] = for
- initialFen <- gameRepo.initialFen(game)
- myPlayer = id.playerId.flatMap(game.player(_))
- users <- game.userIdPair.traverse(_ so lightUserGet)
- prefs <- prefApi.byId(game.userIdPair)
- takebackable <- takebacker.isAllowedIn(game, Preload(prefs))
- moretimeable <- moretimer.isAllowedIn(game, Preload(prefs))
- chat <- getPlayerChat(game, myPlayer.exists(_.hasUser))
- chatLines <- chat.map(_.chat) soFu lila.chat.JsonView.asyncLines
- yield
- def playerJson(color: Color) =
- val player = game player color
- jsonView
- .player(player, users(color))
- .add("isGone" -> (game.forceDrawable && socket.exists(_.isGone(player.color))))
- .add("onGame" -> (player.isAi || socket.exists(_.onGame(player.color))))
- Json
- .obj(
- "game" -> {
- jsonView.base(game, initialFen) ++ Json
- .obj("pgn" -> game.sans.mkString(" "))
- .add("drawOffers" -> (!game.drawOffers.isEmpty).option(game.drawOffers.normalizedPlies))
- },
- "white" -> playerJson(Color.White),
- "black" -> playerJson(Color.Black),
- "socket" -> socket.so(_.version).value
- )
- .add("expiration" -> game.expirable.option:
- Json.obj(
- "idleMillis" -> (nowMillis - game.movedAt.toMillis),
- "millisToMove" -> game.timeForFirstMove.millis
+ def online(game: Game, id: GameAnyId, socket: SocketStatus): Fu[JsObject] =
+ forUseCase(game, id, UseCase.Online(socket))
+
+ def offline(game: Game, id: GameAnyId): Fu[JsObject] =
+ forUseCase(game, id, UseCase.Offline)
+
+ private def forUseCase(game: Game, id: GameAnyId, use: UseCase): Fu[JsObject] =
+ for
+ initialFen <- gameRepo.initialFen(game)
+ myPlayer = id.playerId.flatMap(game.player(_))
+ users <- game.userIdPair.traverse(_ so lightUserGet)
+ prefs <- use.prefs soFu prefApi.byId(game.userIdPair)
+ takebackable <- takebacker.isAllowedIn(game, Preload(prefs))
+ moretimeable <- moretimer.isAllowedIn(game, Preload(prefs))
+ chat <- use.chat so getPlayerChat(game, myPlayer.exists(_.hasUser))
+ chatLines <- chat.map(_.chat) soFu lila.chat.JsonView.asyncLines
+ yield
+ def playerJson(color: Color) =
+ val player = game player color
+ jsonView
+ .player(player, users(color))
+ .add("isGone" -> (game.forceDrawable && use.socketStatus.exists(_.isGone(player.color))))
+ .add("onGame" -> (player.isAi || use.socketStatus.exists(_.onGame(player.color))))
+ Json
+ .obj(
+ "game" -> {
+ jsonView.base(game, initialFen) ++ Json
+ .obj("pgn" -> game.sans.mkString(" "))
+ .add("drawOffers" -> (!game.drawOffers.isEmpty).option(game.drawOffers.normalizedPlies))
+ },
+ "white" -> playerJson(Color.White),
+ "black" -> playerJson(Color.Black)
+ )
+ .add("socket" -> use.socketStatus.map(_.version))
+ .add("expiration" -> game.expirable.option:
+ Json.obj(
+ "idleMillis" -> (nowMillis - game.movedAt.toMillis),
+ "millisToMove" -> game.timeForFirstMove.millis
+ )
+ )
+ .add("clock", game.clock.map(roundJson.clockJson))
+ .add("correspondence", game.correspondenceClock)
+ .add("takebackable" -> takebackable)
+ .add("moretimeable" -> moretimeable)
+ .add("youAre", myPlayer.map(_.color))
+ .add("prefs", myPlayer.flatMap(p => prefs.map(_(p.color))).map(prefsJson(game, _)))
+ .add(
+ "chat",
+ chat.map: c =>
+ Json
+ .obj("lines" -> chatLines)
+ .add("restricted", c.restricted)
)
- )
- .add("clock", game.clock.map(roundJson.clockJson))
- .add("correspondence", game.correspondenceClock)
- .add("takebackable" -> takebackable)
- .add("moretimeable" -> moretimeable)
- .add("youAre", myPlayer.map(_.color))
- .add("prefs", myPlayer.map(p => prefs(p.color)).map(prefsJson(game, _)))
- .add(
- "chat",
- chat.map: c =>
- Json
- .obj("lines" -> chatLines)
- .add("restricted", c.restricted)
- )
private def prefsJson(game: Game, pref: Pref): JsObject = Json
.obj(
diff --git a/modules/round/src/main/RoundSocket.scala b/modules/round/src/main/RoundSocket.scala
index cf809f4025093..a50623840e98e 100644
--- a/modules/round/src/main/RoundSocket.scala
+++ b/modules/round/src/main/RoundSocket.scala
@@ -145,7 +145,7 @@ final class RoundSocket(
case Protocol.In.GetGame(reqId, anyId) =>
for
game <- rounds.ask[GameAndSocketStatus](anyId.gameId)(GetGameAndSocketStatus.apply)
- data <- mobileSocket.json(game.game, anyId, game.socket.some)
+ data <- mobileSocket.online(game.game, anyId, game.socket)
yield sendForGameId(anyId.gameId)(Protocol.Out.respond(reqId, data))
case Protocol.In.WsLatency(millis) => MoveLatMonitor.wsLatency.set(millis)
diff --git a/modules/security/src/main/Permission.scala b/modules/security/src/main/Permission.scala
index edfc59dba61ca..b064020649046 100644
--- a/modules/security/src/main/Permission.scala
+++ b/modules/security/src/main/Permission.scala
@@ -71,7 +71,7 @@ object Permission:
case object Streamers extends Permission("STREAMERS", "Manage streamers")
case object Verified extends Permission("VERIFIED", "Verified badge")
case object Prismic extends Permission("PRISMIC", "Prismic preview")
- case object DailyFeed extends Permission("DAILY_FEED", "Daily News")
+ case object DailyFeed extends Permission("DAILY_FEED", "Feed updates")
case object MonitoredCheatMod extends Permission("MONITORED_MOD_CHEAT", "Monitored mod: cheat")
case object MonitoredBoostMod extends Permission("MONITORED_MOD_BOOST", "Monitored mod: boost")
case object MonitoredCommMod extends Permission("MONITORED_MOD_COMM", "Monitored mod: comms")
diff --git a/modules/simul/src/main/SimulRepo.scala b/modules/simul/src/main/SimulRepo.scala
index afe7574fc4427..c5e6a22e6d07e 100644
--- a/modules/simul/src/main/SimulRepo.scala
+++ b/modules/simul/src/main/SimulRepo.scala
@@ -99,7 +99,7 @@ final private[simul] class SimulRepo(val coll: Coll)(using Executor):
.sort(createdSort)
.hint(coll hint $doc("hostSeenAt" -> -1))
.cursor[Simul]()
- .list(100) map {
+ .list(50) map {
_.foldLeft(List.empty[Simul]) {
case (acc, sim) if acc.exists(_.hostId == sim.hostId) => acc
case (acc, sim) => sim :: acc
diff --git a/modules/streamer/src/main/LiveStream.scala b/modules/streamer/src/main/LiveStream.scala
index 24661384be82a..377a5b8904b4c 100644
--- a/modules/streamer/src/main/LiveStream.scala
+++ b/modules/streamer/src/main/LiveStream.scala
@@ -3,6 +3,7 @@ package lila.streamer
import play.api.mvc.RequestHeader
import lila.memo.CacheApi.*
+import lila.i18n.Language
case class LiveStreams(streams: List[Stream]):
@@ -13,12 +14,11 @@ case class LiveStreams(streams: List[Stream]):
def get(streamer: Streamer) = streams.find(_ is streamer)
- def homepage(max: Int, req: RequestHeader, userLang: Option[String]) = LiveStreams:
- val langs = req.acceptLanguages.view.map(_.language).toSet + "en" ++ userLang.toSet
+ def homepage(max: Int, accepts: Set[Language]) = LiveStreams:
streams
.takeWhile(_.streamer.approval.tier > 0)
.foldLeft(Vector.empty[Stream]):
- case (selected, s) if langs(s.lang) && {
+ case (selected, s) if accepts(s.language) && {
selected.sizeIs < max || s.streamer.approval.tier == Streamer.maxTier
} && {
s.streamer.approval.tier > 1 || selected.sizeIs < 2
diff --git a/modules/streamer/src/main/Stream.scala b/modules/streamer/src/main/Stream.scala
index 9002097b6e98b..13faf0ed18f16 100644
--- a/modules/streamer/src/main/Stream.scala
+++ b/modules/streamer/src/main/Stream.scala
@@ -1,26 +1,27 @@
package lila.streamer
import play.api.libs.json.*
+import play.api.i18n.Lang
import lila.common.String.html.unescapeHtml
import lila.common.String.removeMultibyteSymbols
import lila.common.Json.given
+import lila.i18n.Language
trait Stream:
def serviceName: String
val status: Html
val streamer: Streamer
- val language: String
+ val lang: Lang
def is(s: Streamer): Boolean = streamer is s
def is(userId: UserId): Boolean = streamer is userId
def twitch = serviceName == "twitch"
def youTube = serviceName == "youTube"
+ def language = Language(lang)
lazy val cleanStatus = status.map(s => removeMultibyteSymbols(s).trim)
- lazy val lang: String = (language.length == 2) so language.toLowerCase
-
object Stream:
case class Keyword(value: String) extends AnyRef with StringValue:
@@ -33,7 +34,7 @@ object Stream:
case class Pagination(cursor: Option[String])
case class Result(data: Option[List[TwitchStream]], pagination: Option[Pagination]):
def liveStreams = (~data).filter(_.isLive)
- case class Stream(userId: String, status: Html, streamer: Streamer, language: String)
+ case class Stream(userId: String, status: Html, streamer: Streamer, lang: Lang)
extends lila.streamer.Stream:
def serviceName = "twitch"
private given Reads[TwitchStream] = Json.reads
@@ -62,7 +63,7 @@ object Stream:
unescapeHtml(item.snippet.title),
item.id,
_,
- ~item.snippet.defaultAudioLanguage
+ item.snippet.defaultAudioLanguage.flatMap(Lang.get) | lila.i18n.defaultLang
)
}
}
@@ -71,7 +72,7 @@ object Stream:
status: Html,
videoId: String,
streamer: Streamer,
- language: String
+ lang: Lang
) extends lila.streamer.Stream:
def serviceName = "youTube"
diff --git a/modules/streamer/src/main/Streamer.scala b/modules/streamer/src/main/Streamer.scala
index d994e10d95057..bc3134ed4dd79 100644
--- a/modules/streamer/src/main/Streamer.scala
+++ b/modules/streamer/src/main/Streamer.scala
@@ -4,6 +4,7 @@ import cats.derived.*
import lila.memo.PicfitImage
import lila.user.User
+import lila.i18n.Language
case class Streamer(
_id: Streamer.Id,
@@ -19,7 +20,7 @@ case class Streamer(
liveAt: Option[Instant], // last seen streaming
createdAt: Instant,
updatedAt: Instant,
- lastStreamLang: Option[String] // valid 2 char language code or None
+ lastStreamLang: Option[Language]
):
inline def id = _id
diff --git a/modules/streamer/src/main/StreamerApi.scala b/modules/streamer/src/main/StreamerApi.scala
index f381c324e6822..fcfe0e625b568 100644
--- a/modules/streamer/src/main/StreamerApi.scala
+++ b/modules/streamer/src/main/StreamerApi.scala
@@ -71,7 +71,7 @@ final class StreamerApi(
q = $id(s.streamer.id),
u = $set(
"liveAt" -> nowInstant,
- "lastStreamLang" -> Lang.get(s.lang).map(_.language)
+ "lastStreamLang" -> s.language
)
)
}.parallel
diff --git a/modules/streamer/src/main/Streaming.scala b/modules/streamer/src/main/Streaming.scala
index 9e9a52066c2e5..58ee3c8b2e75f 100644
--- a/modules/streamer/src/main/Streaming.scala
+++ b/modules/streamer/src/main/Streaming.scala
@@ -4,6 +4,7 @@ import scala.util.chaining.*
import ornicar.scalalib.ThreadLocalRandom
import lila.common.{ Bus, LilaScheduler }
+import play.api.i18n.Lang
final private class Streaming(
api: StreamerApi,
@@ -29,13 +30,13 @@ final private class Streaming(
streamers <- api byIds activeIds
(twitchStreams, youTubeStreams) <-
twitchApi.fetchStreams(streamers, 0, None) map {
- _.collect { case Twitch.TwitchStream(name, title, _, language) =>
+ _.collect { case Twitch.TwitchStream(name, title, _, langStr) =>
streamers.find { s =>
s.twitch.exists(_.userId.toLowerCase == name.toLowerCase) && {
title.value.toLowerCase.contains(keyword.toLowerCase) ||
alwaysFeatured().value.contains(s.userId)
}
- } map { Twitch.Stream(name, title, _, language) }
+ } map { Twitch.Stream(name, title, _, Lang.get(langStr) | lila.i18n.defaultLang) }
}.flatten
} zip ytApi.fetchStreams(streamers)
streams = LiveStreams {
diff --git a/modules/study/src/main/BSONHandlers.scala b/modules/study/src/main/BSONHandlers.scala
index a6ad27987cec0..8f662b8f4ce09 100644
--- a/modules/study/src/main/BSONHandlers.scala
+++ b/modules/study/src/main/BSONHandlers.scala
@@ -339,7 +339,7 @@ object BSONHandlers:
private[study] given (using handler: BSONHandler[Map[String, DbMember]]): BSONHandler[StudyMembers] =
handler.as[StudyMembers](
members =>
- StudyMembers(members map { case (id, dbMember) =>
+ StudyMembers(members map { (id, dbMember) =>
UserId(id) -> StudyMember(UserId(id), dbMember.role)
}),
_.members.view.map((id, m) => id.value -> DbMember(m.role)).toMap
diff --git a/modules/study/src/main/MultiPgn.scala b/modules/study/src/main/MultiPgn.scala
index 9f2cc4974475e..5fd155fe727a7 100644
--- a/modules/study/src/main/MultiPgn.scala
+++ b/modules/study/src/main/MultiPgn.scala
@@ -1,6 +1,7 @@
package lila.study
import chess.format.pgn.PgnStr
+import lila.common.config.Max
case class MultiPgn(value: List[PgnStr]) extends AnyVal:
@@ -9,7 +10,11 @@ case class MultiPgn(value: List[PgnStr]) extends AnyVal:
object MultiPgn:
private[this] val splitPat = """\n\n(?=\[)""".r.pattern
- def split(str: PgnStr, max: Int) =
- MultiPgn {
- PgnStr from splitPat.split(str.value.replaceIf('\r', ""), max + 1).take(max).toList
- }
+
+ def split(str: PgnStr, max: Max) = MultiPgn:
+ PgnStr from splitPat
+ .split(str.value.replaceIf('\r', ""), max.value + 1)
+ .view
+ .filter(_.nonEmpty)
+ .take(max.value)
+ .toList
diff --git a/modules/study/src/main/StudyFlatTree.scala b/modules/study/src/main/StudyFlatTree.scala
index 42b4e427553f6..a6ede289af11d 100644
--- a/modules/study/src/main/StudyFlatTree.scala
+++ b/modules/study/src/main/StudyFlatTree.scala
@@ -16,11 +16,10 @@ private object StudyFlatTree:
val depth = path.depth
def toNodeWithChildren(children: Option[Branches]): Option[Branch] =
- path.lastId.flatMap { readBranch(data, _) }.map {
+ path.lastId.flatMap { readBranch(data, _) } map:
_.copy(children = children | Branches.empty)
- }
- def toNodeWithChildren1(child: Option[NewTree]): Option[NewTree] =
+ def toNodeWithChild(child: Option[NewTree]): Option[NewTree] =
readNewBranch(data, path).map(NewTree(_, child, Nil))
object reader:
@@ -29,44 +28,46 @@ private object StudyFlatTree:
Chronometer.syncMon(_.study.tree.read):
traverse:
flatTree.elements.toList
- .collect {
+ .collect:
case el if el.name != UciPathDb.rootDbKey =>
FlatNode(UciPathDb.decodeDbKey(el.name), el.value.asOpt[Bdoc].get)
- }
.sortBy(-_.depth)
def newRoot(flatTree: Bdoc): Option[NewTree] =
Chronometer.syncMon(_.study.tree.read):
traverseN:
flatTree.elements.toList
- .collect {
+ .collect:
case el if el.name != UciPathDb.rootDbKey =>
FlatNode(UciPathDb.decodeDbKey(el.name), el.value.asOpt[Bdoc].get)
- }
.sortBy(-_.depth)
private def traverse(children: List[FlatNode]): Branches =
children
.foldLeft(Map.empty[UciPath, Branches]) { (roots, flat) =>
// assumes that node has a greater depth than roots (sort beforehand)
- flat.toNodeWithChildren(roots get flat.path).fold(roots) { node =>
- roots.removed(flat.path).updatedWith(flat.path.parent) {
- case None => Branches(List(node)).some
- case Some(siblings) => siblings.addNode(node).some
- }
- }
+ flat
+ .toNodeWithChildren(roots get flat.path)
+ .fold(roots): node =>
+ roots
+ .removed(flat.path)
+ .updatedWith(flat.path.parent):
+ case None => Branches(List(node)).some
+ case Some(siblings) => siblings.addNode(node).some
}
.get(UciPath.root) | Branches.empty
private def traverseN(xs: List[FlatNode]): Option[NewTree] =
xs.nonEmpty so
xs.foldLeft(Map.empty[UciPath, NewTree]) { (roots, flat) =>
- flat.toNodeWithChildren1(roots.get(flat.path)).fold(roots) { node =>
- roots.removed(flat.path).updatedWith(flat.path.parent) {
- case None => node.some
- case Some(siblings) => siblings.addVariation(node.toVariation).some
- }
- }
+ flat
+ .toNodeWithChild(roots.get(flat.path))
+ .fold(roots): node =>
+ roots
+ .removed(flat.path)
+ .updatedWith(flat.path.parent):
+ case None => node.some
+ case Some(siblings) => siblings.addVariation(node.toVariation).some
}.get(UciPath.root)
object writer:
@@ -77,9 +78,8 @@ private object StudyFlatTree:
def newRootChildren(root: NewRoot): List[(String, Bdoc)] =
Chronometer.syncMon(_.study.tree.write):
- root.tree so {
+ root.tree.so:
_.foldLeft(List.empty)((acc, branch) => acc :+ writeBranch_(branch))
- }
private def writeBranch_(branch: NewBranch) =
val order = branch.path.computeIds.size > 1 option
@@ -87,9 +87,8 @@ private object StudyFlatTree:
UciPathDb.encodeDbKey(branch.path) -> writeNewBranch(branch, order)
private def traverse(node: Branch, parentPath: UciPath): List[(String, Bdoc)] =
- (parentPath.depth < Node.MAX_PLIES) so {
+ (parentPath.depth < Node.MAX_PLIES) so:
val path = parentPath + node.id
node.children.nodes.flatMap {
traverse(_, path)
} appended (UciPathDb.encodeDbKey(path) -> writeBranch(node))
- }
diff --git a/modules/study/src/main/StudyForm.scala b/modules/study/src/main/StudyForm.scala
index 9a4203c97e6c5..920b49f90cf56 100644
--- a/modules/study/src/main/StudyForm.scala
+++ b/modules/study/src/main/StudyForm.scala
@@ -5,9 +5,10 @@ import chess.format.pgn.PgnStr
import chess.variant.Variant
import play.api.data.*
import play.api.data.Forms.*
+import play.api.data.format.Formatter
import lila.common.Form.{ cleanNonEmptyText, formatter, into, defaulting, given }
-import play.api.data.format.Formatter
+import lila.common.config.Max
object StudyForm:
@@ -84,7 +85,7 @@ object StudyForm:
):
def toChapterDatas: List[ChapterMaker.Data] =
- val pgns = MultiPgn.split(pgn, max = 32).value
+ val pgns = MultiPgn.split(pgn, max = Max(32)).value
pgns.mapWithIndex: (onePgn, index) =>
ChapterMaker.Data(
// only the first chapter can be named
diff --git a/modules/study/src/main/StudyRepo.scala b/modules/study/src/main/StudyRepo.scala
index f896d43c966c2..bf3477d2f550e 100644
--- a/modules/study/src/main/StudyRepo.scala
+++ b/modules/study/src/main/StudyRepo.scala
@@ -79,8 +79,8 @@ final class StudyRepo(private[study] val coll: AsyncColl)(using
def lookup(local: String) = $lookup.simple(coll, "study", local, "_id")
- private[study] def selectOwnerId(ownerId: UserId) = $doc("ownerId" -> ownerId)
- private[study] def selectMemberId(memberId: UserId) = $doc(F.uids -> memberId)
+ private[study] def selectOwnerId(ownerId: UserId) = $doc("ownerId" -> ownerId)
+ def selectMemberId(memberId: UserId) = $doc(F.uids -> memberId)
private[study] val selectPublic = $doc:
"visibility" -> (Study.Visibility.Public: Study.Visibility)
private[study] val selectPrivateOrUnlisted =
@@ -91,6 +91,7 @@ final class StudyRepo(private[study] val coll: AsyncColl)(using
$doc("ownerId" $ne userId) ++
$doc(s"members.$userId.role" -> "w")
private[study] def selectTopic(topic: StudyTopic) = $doc(F.topics -> topic)
+ def selectBroadcast = selectTopic(StudyTopic.broadcast)
private[study] def selectNotBroadcast = $doc(F.topics $ne StudyTopic.broadcast)
def countByOwner(ownerId: UserId) = coll(_.countSel(selectOwnerId(ownerId)))
@@ -103,6 +104,14 @@ final class StudyRepo(private[study] val coll: AsyncColl)(using
.cursor[Study]()
.documentSource()
+ def sourceByMember(memberId: UserId, isMe: Boolean, select: Bdoc = $empty): Source[Study, ?] =
+ Source.futureSource:
+ coll.map:
+ _.find(selectMemberId(memberId) ++ select ++ (!isMe so selectPublic))
+ .sort($sort desc "rank")
+ .cursor[Study]()
+ .documentSource()
+
def insert(s: Study): Funit =
coll:
_.insert.one:
diff --git a/modules/study/src/main/StudyTopic.scala b/modules/study/src/main/StudyTopic.scala
index 91ad50a902e2b..bea5af6e624a1 100644
--- a/modules/study/src/main/StudyTopic.scala
+++ b/modules/study/src/main/StudyTopic.scala
@@ -12,9 +12,9 @@ import lila.common.config.Max
opaque type StudyTopic = String
object StudyTopic extends OpaqueString[StudyTopic]:
- val minLength = 2
- val maxLength = 50
- val broadcast = "Broadcast"
+ val minLength = 2
+ val maxLength = 50
+ val broadcast: StudyTopic = "Broadcast"
def fromStr(str: String): Option[StudyTopic] =
str.trim match
diff --git a/modules/ublog/src/main/Env.scala b/modules/ublog/src/main/Env.scala
index db3f84837a555..0de1707bc42e6 100644
--- a/modules/ublog/src/main/Env.scala
+++ b/modules/ublog/src/main/Env.scala
@@ -43,8 +43,8 @@ final class Env(
cacheApi.unit[List[UblogPost.PreviewPost]]:
_.refreshAfterWrite(10 seconds).buildAsyncFuture: _ =>
import ornicar.scalalib.ThreadLocalRandom
- val lookInto = 5
- val keep = 2
+ val lookInto = 7
+ val keep = 3
api
.latestPosts(lookInto)
.map:
diff --git a/modules/ublog/src/main/UblogApi.scala b/modules/ublog/src/main/UblogApi.scala
index a843fc36fe54d..b6e2569d0354f 100644
--- a/modules/ublog/src/main/UblogApi.scala
+++ b/modules/ublog/src/main/UblogApi.scala
@@ -60,6 +60,9 @@ final class UblogApi(
def getBlog(id: UblogBlog.Id): Fu[Option[UblogBlog]] = colls.blog.byId[UblogBlog](id.full)
+ def isBlogVisible(userId: UserId): Fu[Option[Boolean]] =
+ getBlog(UblogBlog.Id.User(userId)).dmap(_.map(_.visible))
+
def getPost(id: UblogPostId): Fu[Option[UblogPost]] = colls.post.byId[UblogPost](id)
def findByUserBlogOrAdmin(id: UblogPostId)(using me: Me): Fu[Option[UblogPost]] =
@@ -144,6 +147,11 @@ final class UblogApi(
.one($id(blog), $set("modTier" -> tier, "tier" -> tier), upsert = true)
.void
+ def setRankAdjust(id: UblogPostId, adjust: Int): Funit =
+ colls.post.update
+ .one($id(id), if adjust == 0 then $unset("rankAdjustDays") else $set("rankAdjustDays" -> adjust))
+ .void
+
def postCursor(user: User): AkkaStreamCursor[UblogPost] =
colls.post.find($doc("blog" -> s"user:${user.id}")).cursor[UblogPost](ReadPref.priTemp)
diff --git a/modules/ublog/src/main/UblogBsonHandlers.scala b/modules/ublog/src/main/UblogBsonHandlers.scala
index 7fb7ca248b823..60610011e26b0 100644
--- a/modules/ublog/src/main/UblogBsonHandlers.scala
+++ b/modules/ublog/src/main/UblogBsonHandlers.scala
@@ -15,7 +15,7 @@ private object UblogBsonHandlers:
)
given BSONDocumentHandler[UblogBlog] = Macros.handler
- given BSONHandler[Lang] = stringAnyValHandler(_.code, Lang.apply)
+ given BSONHandler[Lang] = langByCodeHandler
given BSONDocumentHandler[Recorded] = Macros.handler
given BSONDocumentHandler[UblogImage] = Macros.handler
given BSONDocumentHandler[UblogPost] = Macros.handler
diff --git a/modules/ublog/src/main/UblogForm.scala b/modules/ublog/src/main/UblogForm.scala
index e35afe0dcccad..2c18620bd7a76 100644
--- a/modules/ublog/src/main/UblogForm.scala
+++ b/modules/ublog/src/main/UblogForm.scala
@@ -5,7 +5,7 @@ import play.api.data.Forms.*
import ornicar.scalalib.ThreadLocalRandom
import lila.common.Form.{ cleanNonEmptyText, stringIn, into, given }
-import lila.i18n.{ defaultLang, LangList }
+import lila.i18n.{ defaultLanguage, LangList, Language, LangForm }
import lila.user.User
import play.api.i18n.Lang
@@ -20,7 +20,7 @@ final class UblogForm(val captcher: lila.hub.actors.Captcher) extends lila.hub.C
"markdown" -> cleanNonEmptyText(minLength = 0, maxLength = 100_000).into[Markdown],
"imageAlt" -> optional(cleanNonEmptyText(minLength = 3, maxLength = 200)),
"imageCredit" -> optional(cleanNonEmptyText(minLength = 3, maxLength = 200)),
- "language" -> optional(stringIn(LangList.popularNoRegion.map(_.code).toSet)),
+ "language" -> optional(LangForm.popularLanguages.mapping),
"topics" -> optional(text),
"live" -> boolean,
"discuss" -> boolean,
@@ -38,7 +38,7 @@ final class UblogForm(val captcher: lila.hub.actors.Captcher) extends lila.hub.C
markdown = removeLatex(post.markdown),
imageAlt = post.image.flatMap(_.alt),
imageCredit = post.image.flatMap(_.credit),
- language = post.language.code.some,
+ language = post.language.some,
topics = post.topics.mkString(", ").some,
live = post.live,
discuss = ~post.discuss,
@@ -58,7 +58,7 @@ object UblogForm:
markdown: Markdown,
imageAlt: Option[String],
imageCredit: Option[String],
- language: Option[String],
+ language: Option[Language],
topics: Option[String],
live: Boolean,
discuss: Boolean,
@@ -66,8 +66,6 @@ object UblogForm:
move: String
):
- def realLanguage = language flatMap Lang.get
-
def create(user: User) =
UblogPost(
id = UblogPostId(ThreadLocalRandom nextString 8),
@@ -75,7 +73,7 @@ object UblogForm:
title = title,
intro = intro,
markdown = markdown,
- language = LangList.removeRegion(realLanguage.orElse(user.realLang) | defaultLang),
+ language = language.orElse(user.language) | defaultLanguage,
topics = topics so UblogTopic.fromStrList,
image = none,
live = false,
@@ -84,7 +82,8 @@ object UblogForm:
updated = none,
lived = none,
likes = UblogPost.Likes(1),
- views = UblogPost.Views(0)
+ views = UblogPost.Views(0),
+ rankAdjustDays = none
)
def update(user: User, prev: UblogPost) =
@@ -94,7 +93,7 @@ object UblogForm:
markdown = markdown,
image = prev.image.map: i =>
i.copy(alt = imageAlt, credit = imageCredit),
- language = LangList.removeRegion(realLanguage | prev.language),
+ language = language | prev.language,
topics = topics so UblogTopic.fromStrList,
live = live,
discuss = Option(discuss),
diff --git a/modules/ublog/src/main/UblogPaginator.scala b/modules/ublog/src/main/UblogPaginator.scala
index fa3ed792e2101..b4d7cda54dec5 100644
--- a/modules/ublog/src/main/UblogPaginator.scala
+++ b/modules/ublog/src/main/UblogPaginator.scala
@@ -8,8 +8,8 @@ import lila.db.dsl.{ *, given }
import lila.db.paginator.Adapter
import lila.user.User
import reactivemongo.api.bson.BSONNull
-import play.api.i18n.Lang
import lila.user.Me
+import lila.i18n.Language
final class UblogPaginator(
colls: UblogColls,
@@ -39,11 +39,11 @@ final class UblogPaginator(
maxPerPage = maxPerPage
)
- def liveByCommunity(lang: Option[Lang], page: Int): Fu[Paginator[PreviewPost]] =
+ def liveByCommunity(language: Option[Language], page: Int): Fu[Paginator[PreviewPost]] =
Paginator(
adapter = new AdapterLike[PreviewPost]:
- val select = $doc("live" -> true, "topics" $ne UblogTopic.offTopic) ++ lang.so: l =>
- $doc("language" -> l.code)
+ val select = $doc("live" -> true, "topics" $ne UblogTopic.offTopic) ++ language.so: l =>
+ $doc("language" -> l)
def nbResults: Fu[Int] = fuccess(10 * maxPerPage.value)
def slice(offset: Int, length: Int) = aggregateVisiblePosts(select, offset, length)
,
@@ -57,7 +57,7 @@ final class UblogPaginator(
collection = colls.post,
selector = $doc("live" -> true, "likers" -> me.userId),
projection = previewPostProjection.some,
- sort = $sort desc "rank",
+ sort = $sort desc "lived.at",
_.sec
),
currentPage = page,
diff --git a/modules/ublog/src/main/UblogPost.scala b/modules/ublog/src/main/UblogPost.scala
index d094e01294524..d18a95d6388b6 100644
--- a/modules/ublog/src/main/UblogPost.scala
+++ b/modules/ublog/src/main/UblogPost.scala
@@ -4,6 +4,7 @@ import lila.memo.{ PicfitImage, PicfitUrl }
import lila.user.User
import play.api.i18n.Lang
import reactivemongo.api.bson.Macros.Annotations.Key
+import lila.i18n.Language
case class UblogPost(
@Key("_id") id: UblogPostId,
@@ -11,7 +12,7 @@ case class UblogPost(
title: String,
intro: String,
markdown: Markdown,
- language: Lang,
+ language: Language,
image: Option[UblogImage],
topics: List[UblogTopic],
live: Boolean,
@@ -20,7 +21,8 @@ case class UblogPost(
updated: Option[UblogPost.Recorded],
lived: Option[UblogPost.Recorded],
likes: UblogPost.Likes,
- views: UblogPost.Views
+ views: UblogPost.Views,
+ rankAdjustDays: Option[Int]
) extends UblogPost.BasePost:
def isBy[U: UserIdOf](u: U) = created.by is u
diff --git a/modules/ublog/src/main/UblogRank.scala b/modules/ublog/src/main/UblogRank.scala
index aaa8c84fbbaf4..b3db4cfabfc09 100644
--- a/modules/ublog/src/main/UblogRank.scala
+++ b/modules/ublog/src/main/UblogRank.scala
@@ -1,13 +1,16 @@
package lila.ublog
+import java.time.Duration;
import akka.stream.scaladsl.*
import play.api.i18n.Lang
import reactivemongo.akkastream.cursorProducer
import reactivemongo.api.*
+import reactivemongo.api.bson.*
import lila.db.dsl.{ *, given }
import lila.hub.actorApi.timeline.{ Propagate, UblogPostLike }
import lila.user.{ Me, User }
+import lila.i18n.Language
final class UblogRank(
colls: UblogColls,
@@ -38,12 +41,13 @@ final class UblogRank(
UnwindField("blog"),
Project(
$doc(
- "tier" -> "$blog.tier",
- "likes" -> $doc("$size" -> "$likers"), // do not use denormalized field
- "at" -> "$lived.at",
- "language" -> true,
- "title" -> true,
- "imageId" -> "$image.id"
+ "tier" -> "$blog.tier",
+ "likes" -> $doc("$size" -> "$likers"), // do not use denormalized field
+ "at" -> "$lived.at",
+ "language" -> true,
+ "title" -> true,
+ "imageId" -> "$image.id",
+ "rankAdjustDays" -> true
)
)
)
@@ -54,13 +58,14 @@ final class UblogRank(
likes <- doc.getAsOpt[UblogPost.Likes]("likes")
liveAt <- doc.getAsOpt[Instant]("at")
tier <- doc.getAsOpt[UblogBlog.Tier]("tier")
- language <- doc.getAsOpt[Lang]("language")
+ language <- doc.getAsOpt[Language]("language")
title <- doc string "title"
+ adjust = ~doc.int("rankAdjustDays")
hasImage = doc.contains("imageId")
- yield (id, likes, liveAt, tier, language, title, hasImage)
+ yield (id, likes, liveAt, tier, language, title, hasImage, adjust)
.flatMap:
- case None => fuccess(UblogPost.Likes(0))
- case Some((id, likes, liveAt, tier, language, title, hasImage)) =>
+ case None => fuccess(UblogPost.Likes(0))
+ case Some((id, likes, liveAt, tier, language, title, hasImage, adjust)) =>
// Multiple updates may race to set denormalized likes and rank,
// but values should be approximately correct, match a real like
// count (though perhaps not the latest one), and any uncontended
@@ -69,37 +74,43 @@ final class UblogRank(
$id(postId),
$set(
"likes" -> likes,
- "rank" -> computeRank(likes, liveAt, language, tier, hasImage)
+ "rank" -> computeRank(likes, liveAt, language, tier, hasImage, adjust)
)
) andDo {
if res.nModified > 0 && v && tier >= UblogBlog.Tier.LOW
then timeline ! (Propagate(UblogPostLike(me, id.value, title)) toFollowersOf me)
} inject likes
- def recomputeRankOfAllPostsOfBlog(blogId: UblogBlog.Id): Funit =
- colls.blog.byId[UblogBlog](blogId.full) flatMapz recomputeRankOfAllPostsOfBlog
+ def recomputePostRank(post: UblogPost): Funit =
+ recomputeRankOfAllPostsOfBlog(post.blog, post.id.some)
- def recomputeRankOfAllPostsOfBlog(blog: UblogBlog): Funit =
+ def recomputeRankOfAllPostsOfBlog(blogId: UblogBlog.Id, only: Option[UblogPostId] = none): Funit =
+ colls.blog.byId[UblogBlog](blogId.full).flatMapz(recomputeRankOfAllPostsOfBlog(_, only))
+
+ def recomputeRankOfAllPostsOfBlog(blog: UblogBlog, only: Option[UblogPostId]): Funit =
colls.post
.find(
- $doc("blog" -> blog.id),
- $doc("likes" -> true, "lived" -> true, "language" -> true).some
+ $doc("blog" -> blog.id) ++ only.so($id),
+ $doc(List("likes", "lived", "language", "rankAdjustDays", "image").map(_ -> BSONBoolean(true))).some
)
.cursor[Bdoc](ReadPref.sec)
.list(500)
.flatMap:
_.traverse_ : doc =>
- (
- doc.string("_id"),
- doc.getAsOpt[UblogPost.Likes]("likes"),
- doc.getAsOpt[UblogPost.Recorded]("lived"),
- doc.getAsOpt[Lang]("language"),
- doc.contains("image").some
- ).tupled so { (id, likes, lived, language, hasImage) =>
- colls.post
- .updateField($id(id), "rank", computeRank(likes, lived.at, language, blog.tier, hasImage))
- .void
- }
+ ~(for
+ id <- doc.string("_id")
+ likes <- doc.getAsOpt[UblogPost.Likes]("likes")
+ lived <- doc.getAsOpt[UblogPost.Recorded]("lived")
+ language <- doc.getAsOpt[Language]("language")
+ hasImage = doc.contains("image")
+ adjust = ~doc.int("rankAdjustDays")
+ yield colls.post
+ .updateField(
+ $id(id),
+ "rank",
+ computeRank(likes, lived.at, language, blog.tier, hasImage, adjust)
+ )
+ .void)
def recomputeRankOfAllPosts: Funit =
colls.blog
@@ -107,20 +118,28 @@ final class UblogRank(
.sort($sort desc "tier")
.cursor[UblogBlog](ReadPref.sec)
.documentSource()
- .mapAsyncUnordered(4)(recomputeRankOfAllPostsOfBlog)
+ .mapAsyncUnordered(4)(recomputeRankOfAllPostsOfBlog(_, none))
.runWith(lila.common.LilaStream.sinkCount)
.map(nb => println(s"Recomputed rank of $nb blogs"))
def computeRank(blog: UblogBlog, post: UblogPost): Option[UblogPost.RankDate] =
post.lived.map: lived =>
- computeRank(post.likes, lived.at, post.language, blog.tier, post.image.nonEmpty)
+ computeRank(
+ post.likes,
+ lived.at,
+ post.language,
+ blog.tier,
+ post.image.nonEmpty,
+ ~post.rankAdjustDays
+ )
private def computeRank(
likes: UblogPost.Likes,
liveAt: Instant,
- language: Lang,
+ language: Language,
tier: UblogBlog.Tier,
- hasImage: Boolean
+ hasImage: Boolean,
+ days: Int
) = UblogPost.RankDate {
import UblogBlog.Tier.*
if tier < LOW || !hasImage then liveAt minusMonths 3
@@ -133,9 +152,9 @@ final class UblogRank(
case BEST => 15
case _ => 0
- val likesBonus = math.sqrt(likes.value * 25) + likes.value / 100
-
- val langBonus = if language.language == lila.i18n.defaultLang.language then 0 else -24 * 10
+ val adjustBonus = 24 * days
+ val likesBonus = math.sqrt(likes.value * 25) + likes.value / 100
+ val langBonus = if language == lila.i18n.defaultLanguage then 0 else -24 * 10
- (tierBase + likesBonus + langBonus).toInt
+ (tierBase + likesBonus + langBonus + adjustBonus).toInt
}
diff --git a/modules/user/src/main/Flags.scala b/modules/user/src/main/Flags.scala
index 9b433dc308ac4..381c71639eb96 100644
--- a/modules/user/src/main/Flags.scala
+++ b/modules/user/src/main/Flags.scala
@@ -94,7 +94,7 @@ object Flags:
C("ES", "Spain"),
C("ES-AN", "Andalusia"),
C("ES-AR", "Aragon"),
- C("ES-AS", "Asturia"),
+ C("ES-AS", "Asturias"),
C("ES-CT", "Catalonia"),
C("ES-EU", "Basque Country"),
C("ES-GA", "Galicia"),
diff --git a/modules/user/src/main/User.scala b/modules/user/src/main/User.scala
index e58c1a5a912e2..3638a711a511f 100644
--- a/modules/user/src/main/User.scala
+++ b/modules/user/src/main/User.scala
@@ -1,7 +1,7 @@
package lila.user
import play.api.i18n.Lang
-
+import lila.i18n.Language
import lila.common.{ EmailAddress, LightUser, NormalizedEmailAddress }
import lila.rating.{ Perf, PerfType }
import reactivemongo.api.bson.{ BSONDocument, BSONDocumentHandler, Macros }
@@ -39,7 +39,8 @@ case class User(
def realNameOrUsername = profileOrDefault.nonEmptyRealName | username.value
- def realLang = lang flatMap Lang.get
+ def realLang: Option[Lang] = lang flatMap Lang.get
+ def language: Option[Language] = realLang.map(Language.apply)
def titleUsername: String = title.fold(username.value)(t => s"$t $username")
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 948fb6bb6c569..54ef3396639d6 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -104,8 +104,8 @@ importers:
specifier: workspace:*
version: link:../chess
chessops:
- specifier: ^0.12.7
- version: 0.12.7
+ specifier: ^0.13.0
+ version: 0.13.0
common:
specifier: workspace:*
version: link:../common
@@ -143,8 +143,8 @@ importers:
specifier: ^0.2.13
version: 0.2.13
chessops:
- specifier: ^0.12.7
- version: 0.12.7
+ specifier: ^0.13.0
+ version: 0.13.0
common:
specifier: workspace:*
version: link:../common
@@ -275,14 +275,14 @@ importers:
ui/dgt:
dependencies:
chessops:
- specifier: ^0.12.7
- version: 0.12.7
+ specifier: ^0.13.0
+ version: 0.13.0
ui/editor:
dependencies:
chessops:
- specifier: ^0.12.7
- version: 0.12.7
+ specifier: ^0.13.0
+ version: 0.13.0
common:
specifier: workspace:*
version: link:../common
@@ -425,8 +425,8 @@ importers:
specifier: workspace:*
version: link:../chess
chessops:
- specifier: ^0.12.7
- version: 0.12.7
+ specifier: ^0.13.0
+ version: 0.13.0
snabbdom:
specifier: ^3.5.1
version: 3.5.1
@@ -470,8 +470,8 @@ importers:
ui/puz:
dependencies:
chessops:
- specifier: ^0.12.7
- version: 0.12.7
+ specifier: ^0.13.0
+ version: 0.13.0
common:
specifier: workspace:*
version: link:../common
@@ -494,8 +494,8 @@ importers:
specifier: workspace:*
version: link:../chess
chessops:
- specifier: ^0.12.7
- version: 0.12.7
+ specifier: ^0.13.0
+ version: 0.13.0
common:
specifier: workspace:*
version: link:../common
@@ -521,8 +521,8 @@ importers:
specifier: workspace:*
version: link:../chess
chessops:
- specifier: ^0.12.7
- version: 0.12.7
+ specifier: ^0.13.0
+ version: 0.13.0
common:
specifier: workspace:*
version: link:../common
@@ -656,8 +656,8 @@ importers:
specifier: workspace:*
version: link:../chess
chessops:
- specifier: ^0.12.7
- version: 0.12.7
+ specifier: ^0.13.0
+ version: 0.13.0
common:
specifier: workspace:*
version: link:../common
@@ -2348,6 +2348,12 @@ packages:
'@badrap/result': 0.2.13
dev: false
+ /chessops@0.13.0:
+ resolution: {integrity: sha512-1hEY22Ajp3nxMItLhUSgPWYWdTIAeVurCrN6EJMgYjqyV9jWK2Q8u2B28beDMbcB4ORWRCVpoxds5cFSp5BJQA==}
+ dependencies:
+ '@badrap/result': 0.2.13
+ dev: false
+
/chokidar@3.5.3:
resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==}
engines: {node: '>= 8.10.0'}
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index 3d43822c9e164..78cd039d27e36 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -17,8 +17,7 @@ object Dependencies {
val alleycats = "org.typelevel" %% "alleycats-core" % "2.10.0"
val scalalib = "com.github.ornicar" %% "scalalib" % "9.5.5"
val hasher = "com.roundeights" %% "hasher" % "1.3.1"
- val jodaTime = "joda-time" % "joda-time" % "2.12.5"
- val chess = "org.lichess" %% "scalachess" % "15.6.11"
+ val chess = "org.lichess" %% "scalachess" % "15.7.1"
val compression = "org.lichess" %% "compression" % "1.10"
val maxmind = "com.maxmind.geoip2" % "geoip2" % "4.0.1"
val prismic = "io.prismic" %% "scala-kit" % "1.2.19_lila-3.2"
diff --git a/public/flair/img/activity.chess.webp b/public/flair/img/activity.chess.webp
index c4f00b1cb6fb4..88bf29330d5b1 100644
Binary files a/public/flair/img/activity.chess.webp and b/public/flair/img/activity.chess.webp differ
diff --git a/public/flair/img/activity.lichess-horsey.webp b/public/flair/img/activity.lichess-horsey.webp
index b20ec17c693e0..9a5ba4cb5fb18 100644
Binary files a/public/flair/img/activity.lichess-horsey.webp and b/public/flair/img/activity.lichess-horsey.webp differ
diff --git a/public/flair/img/activity.lichess.webp b/public/flair/img/activity.lichess.webp
index 65a4b4b4a51ce..ebca08db3cdac 100644
Binary files a/public/flair/img/activity.lichess.webp and b/public/flair/img/activity.lichess.webp differ
diff --git a/public/flair/img/activity.shogi-bigsby.webp b/public/flair/img/activity.shogi-bigsby.webp
index 55f1c7839aa2d..724fd3e5e2c19 100644
Binary files a/public/flair/img/activity.shogi-bigsby.webp and b/public/flair/img/activity.shogi-bigsby.webp differ
diff --git a/public/flair/img/activity.shogi-king.webp b/public/flair/img/activity.shogi-king.webp
index b6661da5afb25..553f4de545c86 100644
Binary files a/public/flair/img/activity.shogi-king.webp and b/public/flair/img/activity.shogi-king.webp differ
diff --git a/public/flair/img/nature.octopus-howard.webp b/public/flair/img/nature.octopus-howard.webp
index eba165274cc7b..5be6a099277c2 100644
Binary files a/public/flair/img/nature.octopus-howard.webp and b/public/flair/img/nature.octopus-howard.webp differ
diff --git a/public/flair/img/nature.xmas-tree.webp b/public/flair/img/nature.xmas-tree.webp
index a14fab24363f2..ad0bc628f4277 100644
Binary files a/public/flair/img/nature.xmas-tree.webp and b/public/flair/img/nature.xmas-tree.webp differ
diff --git a/public/flair/img/objects.xmas-bell.webp b/public/flair/img/objects.xmas-bell.webp
index fdda2fc2ac1d9..bb63721d45340 100644
Binary files a/public/flair/img/objects.xmas-bell.webp and b/public/flair/img/objects.xmas-bell.webp differ
diff --git a/public/flair/img/objects.xmas-candle.webp b/public/flair/img/objects.xmas-candle.webp
index 45066500e4387..5bc648fbcf4af 100644
Binary files a/public/flair/img/objects.xmas-candle.webp and b/public/flair/img/objects.xmas-candle.webp differ
diff --git a/public/flair/img/objects.xmas-hat.webp b/public/flair/img/objects.xmas-hat.webp
index 8bf63253ce08f..f496851dc7e06 100644
Binary files a/public/flair/img/objects.xmas-hat.webp and b/public/flair/img/objects.xmas-hat.webp differ
diff --git a/public/flair/img/symbols.esperanto.webp b/public/flair/img/symbols.esperanto.webp
index 139274610ab55..a9a3c182d943f 100644
Binary files a/public/flair/img/symbols.esperanto.webp and b/public/flair/img/symbols.esperanto.webp differ
diff --git a/public/flair/img/symbols.move-blunder.webp b/public/flair/img/symbols.move-blunder.webp
new file mode 100644
index 0000000000000..f1e6c05b08c05
Binary files /dev/null and b/public/flair/img/symbols.move-blunder.webp differ
diff --git a/public/flair/img/symbols.move-brilliant.webp b/public/flair/img/symbols.move-brilliant.webp
new file mode 100644
index 0000000000000..18be1b1e5fd0e
Binary files /dev/null and b/public/flair/img/symbols.move-brilliant.webp differ
diff --git a/public/flair/img/symbols.move-dubious.webp b/public/flair/img/symbols.move-dubious.webp
new file mode 100644
index 0000000000000..0ed1ddfa3f866
Binary files /dev/null and b/public/flair/img/symbols.move-dubious.webp differ
diff --git a/public/flair/img/symbols.move-good.webp b/public/flair/img/symbols.move-good.webp
new file mode 100644
index 0000000000000..d4a32f876d1ba
Binary files /dev/null and b/public/flair/img/symbols.move-good.webp differ
diff --git a/public/flair/img/symbols.move-interesting.webp b/public/flair/img/symbols.move-interesting.webp
new file mode 100644
index 0000000000000..10009a6de855d
Binary files /dev/null and b/public/flair/img/symbols.move-interesting.webp differ
diff --git a/public/flair/img/symbols.move-mistake.webp b/public/flair/img/symbols.move-mistake.webp
new file mode 100644
index 0000000000000..578d4f83262f0
Binary files /dev/null and b/public/flair/img/symbols.move-mistake.webp differ
diff --git a/public/flair/img/symbols.puzzle-racer.webp b/public/flair/img/symbols.puzzle-racer.webp
new file mode 100644
index 0000000000000..08c6896af9a6a
Binary files /dev/null and b/public/flair/img/symbols.puzzle-racer.webp differ
diff --git a/public/flair/img/symbols.puzzle-streak.webp b/public/flair/img/symbols.puzzle-streak.webp
new file mode 100644
index 0000000000000..b64e9161213e4
Binary files /dev/null and b/public/flair/img/symbols.puzzle-streak.webp differ
diff --git a/public/flair/img/travel-places.earth-blue.webp b/public/flair/img/travel-places.earth-blue.webp
index a210102cc60e3..56722c7a58ea8 100644
Binary files a/public/flair/img/travel-places.earth-blue.webp and b/public/flair/img/travel-places.earth-blue.webp differ
diff --git a/public/flair/img/travel-places.wooden-ship.webp b/public/flair/img/travel-places.wooden-ship.webp
index 894d1e47713df..dc49529d3481b 100644
Binary files a/public/flair/img/travel-places.wooden-ship.webp and b/public/flair/img/travel-places.wooden-ship.webp differ
diff --git a/public/flair/list.txt b/public/flair/list.txt
index 20ad0fdacca54..0271660032dae 100644
--- a/public/flair/list.txt
+++ b/public/flair/list.txt
@@ -3182,6 +3182,12 @@ symbols.mending-heart
symbols.mens-room
symbols.minus
symbols.mobile-phone-off
+symbols.move-blunder
+symbols.move-brilliant
+symbols.move-dubious
+symbols.move-good
+symbols.move-interesting
+symbols.move-mistake
symbols.multiply
symbols.name-badge
symbols.new-button
@@ -3214,7 +3220,9 @@ symbols.play-or-pause-button
symbols.plus
symbols.potable-water
symbols.purple-heart
+symbols.puzzle-racer
symbols.puzzle-storm
+symbols.puzzle-streak
symbols.question-mark
symbols.radio-button
symbols.rainbow-flag
diff --git a/public/images/board/newspaper.png b/public/images/board/newspaper.png
deleted file mode 100644
index 8385a740f45bc..0000000000000
Binary files a/public/images/board/newspaper.png and /dev/null differ
diff --git a/public/images/board/newspaper.thumbnail.png b/public/images/board/newspaper.thumbnail.png
deleted file mode 100644
index c18709956e162..0000000000000
Binary files a/public/images/board/newspaper.thumbnail.png and /dev/null differ
diff --git a/public/images/board/svg/newspaper.svg b/public/images/board/svg/newspaper.svg
new file mode 100644
index 0000000000000..55249c7e10268
--- /dev/null
+++ b/public/images/board/svg/newspaper.svg
@@ -0,0 +1,25 @@
+
+
diff --git a/public/images/trophy/atomicwc23.png b/public/images/trophy/atomicwc23.png
new file mode 100644
index 0000000000000..30fe491ce2255
Binary files /dev/null and b/public/images/trophy/atomicwc23.png differ
diff --git a/translation/dest/arena/an-ES.xml b/translation/dest/arena/an-ES.xml
index 786c64a68930e..aeae3b2106f4c 100644
--- a/translation/dest/arena/an-ES.xml
+++ b/translation/dest/arena/an-ES.xml
@@ -38,7 +38,7 @@ Chuga rapido y torna a lo recibidor pa chugar mas partidas y ganar mas puntos.
Este ye un torneyo privauComparte este vinclo pa que atras personas s\'unan: %s
- Tongada de taulas: Quan un chugador fa taulas en quantas partidas consecutivas en un torneyo, nomás conceden un punto la primera d\'ellas y las que haigan durau %s movimientos u mas en partidas estándard. Una tongada de taulas nomás puede trencar-se per medio d\'una victoria, no sirven ni una redota ni unas taulas.
+ Tongada de taulas: Quan un chugador fa taulas en quantas partidas consecutivas en un torneyo, nomás conceden un punto la primera d\'ellas y las que haigan durau %s movimientos u mas en partidas estándard. Una tongada de taulas nomás puede trencar-se per medio d\'una victoria, no sirven ni una redota ni unas taulas.Lo numero minimo de chugadas pa que una partida empazada sume puntos ye diferent per cada variant. La taula siguient amuestra lo limite pa cada variant.VariantNumero minimo de chugadas
@@ -55,9 +55,9 @@ Chuga rapido y torna a lo recibidor pa chugar mas partidas y ganar mas puntos.
Permite que los chugadors se comuniquen en una sala de chatRachas de torneyoDimpués de 2 vicotiras consecutivas, atorga 4 puntos en cuenta de 2.
- No se permite lo Berkserk
+ No se permite lo BerkserkSin rachas de torneyoRendimiento meyoPuntuación meya
- Los míos torneyos
+ Los míos torneyos
diff --git a/translation/dest/arena/ar-SA.xml b/translation/dest/arena/ar-SA.xml
index 12e0a36e7d1fa..8cc2b9d4fb42e 100644
--- a/translation/dest/arena/ar-SA.xml
+++ b/translation/dest/arena/ar-SA.xml
@@ -2,11 +2,11 @@
مسابقات الساحةهل هي مقيمة؟
- سيتم إعلامك عند بدء البطولة، لذا فإنه من الآمن اللعب في علامة تبويب أخرى أثناء الانتظار.
+ سيتم إعلامك عند بدء البطولة، لذا يمكنك اللعب في علامة تبويب أخرى أثناء الانتظار.هذه البطولة مقيمة وسوف تؤثر على تقييمك.
- هذه البطولة *ليست* مقيمة *ولن* تؤثر على تقييمك.
- بعض البطولات تكون مقيمة وسوف تؤثر على تقييمك.
- كيف يتم احتساب النقاط؟
+ هذه البطولة ليست مقيمة ولن تؤثر على تقييمك.
+ بعض البطولات تكون مقيمة وتؤثر على تقييمك.
+ كيف تحتسب النقاط؟الفوز نتيجته الأساسية 2 نقطة، التعادل: 1 نقطة، والخسارة 0 نقطة.
إذا ربحت مباراتين على التوالي سوف تبدأ مرحلة مضاعفة النقاط، ويمثلها رمز الشعلة.
وسوف تستمر المباريات التالية مضاعفة النقاط حتى تفشل في الفوز في مباراة.
@@ -14,12 +14,13 @@
كمثال، انتصاران يليهما تعادل سيساوي 6 نقاط: 2 + 2 + (2 × 1)مخاطرة الساحةعندما يضغط اللاعب زر المخاطرة في بداية المباراة سيفقد اللاعب نصف وقته لكنه في حال الفوز يحصل على نقطة إضافية.
-المخاطرة في حالة الوقت المتزايد يلغي الزيادة مع كل نقلة (يستثنى من ذلك نمط دقيقة +2 ث زيادة/نقلة ، ستلغى الزيادة لكن الوقت سيكون دقيقة فقط)
+المخاطرة في حالة الوقت المتزايد يلغي الزيادة مع كل نقلة (يستثنى من ذلك نمط دقيقة +2 ث زيادة/نقلة ، ستلغى الزيادة لكن الوقت سيكون دقيقة كاملة)
المخاطرة غير متاحة للمباريات ذات التوقيت صفر +زيادة بالثواني (0+1ث/نقلة, 0+2ث).
المخاطرة تضمن النقطة الإضافية إذا لعبت على الأقل 7 نقلات.كيف يحدد الفائز؟
- اللاعب (اللاعبون) ذو النقاط الأعلى في نهاية الوقت المحدد للبطولة يتم إعلانه/م فائز/ين.
- كيف يتم التزويج؟
+ اللاعب (اللاعبون) ذو النقاط الأعلى في نهاية الوقت المحدد للبطولة يتم إعلانه/م فائز/ين.
+عندما يحصل لاعبان أو أكثر على نفس عدد النقاط، يكون معدل الأداء في البطولة هو كسر التعادل.
+ كيف يتم تحديد الخصوم؟في بداية البطولة، يتم إزواج اللاعبين على أساس تقييمهم. بمجرد الانتهاء من مباراتك، والعودة إلى بهو البطولة: سيتم ازواجك مع لاعب قريب من ترتيبك. وهذا ما يضمن وقت إنتظار أقل،بأي حال قد لا تواجه سائر اللاعبين في هذه البطولة. العب سريعًا وعد إلى المسابقة للعب مباريات أكثر واكسب المزيد من النقاط.كيف تنتهي البطولة؟للبطولة ساعة عد تنازلي. عندما تصل إلى الصفر، يتم تجميد ترتيب مراكز البطولة، ويتم الإعلان عن الفائز. يجب أن يتم الانتهاء من المباريات قيد اللعب، ومع ذلك فإنها لا تحتسب نتائجها في البطولة.
diff --git a/translation/dest/arena/be-BY.xml b/translation/dest/arena/be-BY.xml
index 498eef0ac8677..95b959d2078a3 100644
--- a/translation/dest/arena/be-BY.xml
+++ b/translation/dest/arena/be-BY.xml
@@ -55,4 +55,6 @@
Дазволіць гульцам абмеркаванне ў чацеСерыі АрэныПасля 2 перамог запар, наступныя перамогі прынясуць 4 ачкі, замест 2.
+ Берсерк не дазволены
+ Мае турніры
diff --git a/translation/dest/arena/bg-BG.xml b/translation/dest/arena/bg-BG.xml
index fa337beaef8ee..3e18474518f98 100644
--- a/translation/dest/arena/bg-BG.xml
+++ b/translation/dest/arena/bg-BG.xml
@@ -54,7 +54,7 @@
Позволи на играчите да обсъждат в чатаПоредици в аренатаСлед 2 победи, всяка последователна победа носи 4 точки вместо 2.
- No Berserk allowed
+ No Berserk allowedNo Arena streaksСредна производителностAverage score
diff --git a/translation/dest/arena/el-GR.xml b/translation/dest/arena/el-GR.xml
index 0b0949eebfb46..757d78f2b3e54 100644
--- a/translation/dest/arena/el-GR.xml
+++ b/translation/dest/arena/el-GR.xml
@@ -38,7 +38,7 @@ To Berserk δεν ισχύει για παρτίδες με μηδενικό α
Αυτός ο διαγωνισμός είναι ιδιωτικόςΜοιραστείτε αυτήν την διεύθυνση URL για να συμμετάσχουν άλλα άτομα: %s
- Συνεχόμενες ισοπαλίες: Όταν ένας παίκτης έχει συνεχόμενες ισοπαλίες στην αρένα, είτε μόνο η πρώτη θα δώσει ένα πόντο, είτε ισοπαλίες που διήρκησαν πάνω από %s κινήσεις. Οι συνεχόμενες ισοπαλίες σταματούν να θεωρούνται συνεχόμενες μόνο εφόσον τις ακολουθήσει μία νίκη και όχι μία ήττα ή ισοπαλία.
+ Συνεχόμενες ισοπαλίες: Όταν ένας παίκτης έχει συνεχόμενες ισοπαλίες στην αρένα, είτε μόνο η πρώτη θα δώσει ένα πόντο, είτε ισοπαλίες που διήρκησαν πάνω από %s κινήσεις. Οι συνεχόμενες ισοπαλίες σταματούν να θεωρούνται συνεχόμενες μόνο εφόσον τις ακολουθήσει μία νίκη και όχι μία ήττα ή ισοπαλία.Ο ελάχιστος αριθμός των κινήσεων που απαιτούνται στα ισόπαλα παιχνίδια για να δώσουν πόντους εξαρτάται από την κάθε παραλλαγή, όπως φαίνεται και στον παρακάτω πίνακα.ΕκδοχήΕλάχιστος αριθμός κινήσεων
@@ -57,5 +57,5 @@ To Berserk δεν ισχύει για παρτίδες με μηδενικό α
Μετά από 2 νίκες, επιπλέον διαδοχικές νίκες σας 4 πόντους αντί για 2.Μέση επίδοσηΜέση βαθμολογία
- Τα τουρνουά μου
+ Τα τουρνουά μου
diff --git a/translation/dest/arena/fa-IR.xml b/translation/dest/arena/fa-IR.xml
index 02bf9b951c76e..8239b66efd534 100644
--- a/translation/dest/arena/fa-IR.xml
+++ b/translation/dest/arena/fa-IR.xml
@@ -36,7 +36,7 @@
این یک تورنومنت خصوصی استاین لینک را برای پیوستن دیگران به اشتراک بگذارید.%s
- سلسله تساوی: وقتی یک بازیکن در Arena چند تساوی پشت سر هم بدست بیاورد، تنها اولین تساوی یا تساویهایی با حداقل %s حرکت، دارای امتیاز خواهند بود. سلسله تساوی تنها با برد شکسته خواهد شد، نه با باخت یا تساوی.
+ سلسله تساوی: وقتی یک بازیکن در Arena چند تساوی پشت سر هم بدست بیاورد، تنها اولین تساوی یا تساویهایی با حداقل %s حرکت، دارای امتیاز خواهند بود. سلسله تساوی تنها با برد شکسته خواهد شد، نه با باخت یا تساوی.حداقل طول بازی برای بازی های قرعه کشی شده برای کسب امتیاز بر اساس نوع بازی متفاوت است. جدول زیر آستانه انواع مختلف را فهرست می کند.نوعحداقل طول بازی
@@ -53,7 +53,7 @@
اجازه دادن بحث به بازیکنان در چت رومآرنا استریکزبعد از دو برد، بردهای پی در پی بهجای 2 امتیاز 4 امتیاز می دهند.
- برزرک مجاز نیست
+ برزرک مجاز نیستسلسله برد ناموجودمیانگین عملکردمیانگین امتیاز
diff --git a/translation/dest/arena/gl-ES.xml b/translation/dest/arena/gl-ES.xml
index a3990929e717d..7df7c1e302314 100644
--- a/translation/dest/arena/gl-ES.xml
+++ b/translation/dest/arena/gl-ES.xml
@@ -54,7 +54,7 @@ Xoga rápido e volta á sala de espera para xogar máis partidas e gañar máis
Permite que os xogadores se comuniquen nunha sala de conversasSecuencia de vitorias na ArenaTras 2 vitorias, as seguintes vitorias consecutivas dan 4 puntos en lugar de 2.
- Non está permitido facer o Berserk
+ Non está permitido facer o BerserkArena sen secuencias de vitoriasRendemento medioPuntuación media
diff --git a/translation/dest/arena/ka-GE.xml b/translation/dest/arena/ka-GE.xml
index a4faab02bf133..b2757a3403a81 100644
--- a/translation/dest/arena/ka-GE.xml
+++ b/translation/dest/arena/ka-GE.xml
@@ -30,4 +30,8 @@
დროა გამოყოფილი თქვენს პირველ სვლაზე. თუ ვერ ჩაეტევით დროში, მოწინააღმდეგე გაიმარჯვებს.ეს არის პირადი ტურნირიგაუზიარე ლინკი სხვებს რათა შემოგიერთდნენ: %s
+ ვარიანტი
+ არენას ჯაჭვი
+ საშუალო ქულა
+ ჩემი ტურნირები
diff --git a/translation/dest/arena/nl-NL.xml b/translation/dest/arena/nl-NL.xml
index 8b6da0359f7d4..998f7a3f87dbe 100644
--- a/translation/dest/arena/nl-NL.xml
+++ b/translation/dest/arena/nl-NL.xml
@@ -37,7 +37,7 @@ Speel snel en ga terug naar de toernooilobby om meer partijen te spelen en meer
Dit is een privétoernooiDeel deze URL om mensen deel te laten nemen: %s
- Reeksen remises: wanneer een speler opeenvolgende keren gelijk speelt, zal enkel de eerste remise een punt opleveren. Bij standaard partijen leveren ook remises van meer dan %s zetten punten op. Deze reeks van remises kan enkel door een winst verbroken worden, niet door verlies of remise.
+ Reeksen remises: wanneer een speler opeenvolgende keren gelijk speelt, zal enkel de eerste remise een punt opleveren. Bij standaard partijen leveren ook remises van meer dan %s zetten punten op. Deze reeks van remises kan enkel door een winst verbroken worden, niet door verlies of remise.De minimale lengte die nodig is opdat een remisepartij punten zou opleveren hangt af van de variant. De tabel hieronder geeft een lijst van de ondergrens voor elke variant.VariantMinimale spellengte
@@ -54,7 +54,7 @@ Speel snel en ga terug naar de toernooilobby om meer partijen te spelen en meer
Spelers kunnen chatten in de chatruimteArena streaksNa 2 overwinningen geven opeenvolgende overwinningen 4 punten in plaats van 2.
- Berserk niet toegestaan
+ Berserk niet toegestaanGeen Arena-streaksGemiddelde prestatieGemiddelde score
diff --git a/translation/dest/arena/ro-RO.xml b/translation/dest/arena/ro-RO.xml
index 6a9eca3a8d25d..ca9a4b5dfec53 100644
--- a/translation/dest/arena/ro-RO.xml
+++ b/translation/dest/arena/ro-RO.xml
@@ -58,7 +58,7 @@ Joacă rapid și întoarce-te la lobby pentru a juca mai multe partide și pentr
Permiteți jucătorilor să discute într-o cameră de chatSerie de victorii in arenăDupă 2 partide câştigate, câştigurile consecutive aduc 4 puncte în loc de 2.
- Niciun Berserk permis
+ Niciun Berserk permisNicio serie de victorii în arenăPerformanță medieScor mediu
diff --git a/translation/dest/arena/sv-SE.xml b/translation/dest/arena/sv-SE.xml
index b3611a18fcdf8..3fa393d9984a1 100644
--- a/translation/dest/arena/sv-SE.xml
+++ b/translation/dest/arena/sv-SE.xml
@@ -31,7 +31,7 @@ Berserk ger en extrapoäng endast om du spelar minst 7 drag i partiet.
Detta är en privat turneringDela denna länk för att låta spelare delta: %s
- Remiserier: När en spelare har konsekutiva remier i en arena så kommer bara den första att ge ett poäng, eller remier som varar mer än %s drag i standardpartier. Remiserien kan bara brytas av en vinst, inte en förlust eller remi.
+ Remiserier: När en spelare har konsekutiva remier i en arena så kommer bara den första att ge ett poäng, eller remier som varar mer än %s drag i standardpartier. Remiserien kan bara brytas av en vinst, inte en förlust eller remi.Minsta partilängden för att remier ska ge poäng är olika mellan varianter. Tabellen nedan listar gränsvärden för varje variant.VariantMinsta partilängd
@@ -48,7 +48,7 @@ Berserk ger en extrapoäng endast om du spelar minst 7 drag i partiet.
Låt spelare diskutera i ett chattrumArenavinster i radEfter 2 vinster, ger efterföljande vinster 4 poäng i stället för 2.
- Bärsärk tillåts ej
+ Bärsärk tillåts ejInga arena streaksGenomsnittlig prestandaMedelpoäng
diff --git a/translation/dest/arena/ta-IN.xml b/translation/dest/arena/ta-IN.xml
index 51d83b01d46d8..a8b4ab9526026 100644
--- a/translation/dest/arena/ta-IN.xml
+++ b/translation/dest/arena/ta-IN.xml
@@ -39,7 +39,7 @@
இது ஒரு தனிப்பட்ட போட்டிமக்கள் சேர இந்த URL ஐப் பகிரவும்: %s
- சமநிலை தொடர்கள்: ஒரு ஆட்டக்காரர் ஒரு கோதாவில் தொடர்ச்சியாகச் சமநிலைகளைப் பெற்றால், முதல் சமநிலை மட்டுமே ஒரு புள்ளியை ஏற்படுத்தும் அல்லது நிலையான விளையாட்டுகளில் %s நகர்வுகளுக்கு மேல் நீடிக்கும். சமநிலை தொடரை ஒரு வெற்றியால் மட்டுமே உடைக்க முடியும், தோல்வி அல்லது சமநிலை அல்ல.
+ சமநிலை தொடர்கள்: ஒரு ஆட்டக்காரர் ஒரு கோதாவில் தொடர்ச்சியாகச் சமநிலைகளைப் பெற்றால், முதல் சமநிலை மட்டுமே ஒரு புள்ளியை ஏற்படுத்தும் அல்லது நிலையான விளையாட்டுகளில் %s நகர்வுகளுக்கு மேல் நீடிக்கும். சமநிலை தொடரை ஒரு வெற்றியால் மட்டுமே உடைக்க முடியும், தோல்வி அல்லது சமநிலை அல்ல.புள்ளிகளைப் பெற வரையப்பட்ட ஆடங்களுக்கான குறைந்தபட்ச விளையாட்டு நீளம் மாறுபாட்டின் அடிப்படையில் வேறுபடும். கீழே உள்ள அட்டவணை ஒவ்வொரு மாறுபாட்டிற்கான வரம்பைப் பட்டியலிடுகிறது.மாறுபாடுகுறைந்தபட்ச விளையாட்டு நீளம்
@@ -56,9 +56,9 @@
வீரர்கள் அரட்டை அறையில் விவாதிக்கலாம்கோதா தொடர்2 வெற்றிகளுக்குப் பிறகு, தொடர்ச்சியான வெற்றிகள் 2க்குப் பதிலாக 4 புள்ளிகளை வழங்குகின்றன.
- மூர்க்கம் அனுமதிக்கப்படவில்லை
+ மூர்க்கம் அனுமதிக்கப்படவில்லைகோதா தொடர் இல்லைசராசரி செயல்திறன்சராசரி மதிப்பெண்
- எனது போட்டிகள்
+ எனது போட்டிகள்
diff --git a/translation/dest/arena/th-TH.xml b/translation/dest/arena/th-TH.xml
index 7abce3ababd38..f279667647f09 100644
--- a/translation/dest/arena/th-TH.xml
+++ b/translation/dest/arena/th-TH.xml
@@ -29,7 +29,7 @@
นี่คือทัวร์นาเมนต์ส่วนตัวแชร์ URL นี้เพื่อให้คนอื่นๆได้เข้าร่วม: %s
- การเสมอต่อเนื่อง: เมึ่อผู้เล่นเสมอต่อเนื่องใน Arena
+ การเสมอต่อเนื่อง: เมึ่อผู้เล่นเสมอต่อเนื่องใน Arena
จะนับคะแนน การเสมอครั้งแรก หรือการเสมอที่มีตาเดินมากกว่า %s ตาเดินในเกมมาตรฐานเท่านั้น
การเสมอต่อเนื่อง จะสามารถจบลงได้ด้วยการชนะเท่านั้น การแพ้หรือเสมอจะไม่ทำให้จบการเสมอต่อเนื่องความยาวเกมอย่างต่ำเพี่อที่จะให้เกมที่เสมอนั้นนับคะแนนจะต่างกันไปในแต่ละตัวแปรกติกา ตารางข้างล่างนี้แสดงจุดเริ่มต้นคิดคะแนนสำหรับแต่ละตัวแปรกติกา
@@ -46,7 +46,7 @@
ผู้เล่นจะสามารถพูดคุยในห้องแชตได้เพิ่มคะแนนเมื่อชนะต่อเนื่องหลังจากชนะ 2 ครั้ง การชนะต่อเนื่องจะให้คะแนน 4 คะแนน แทน 2 คะแนน
- ไม่มีการเบอร์เซิร์ก
+ ไม่มีการเบอร์เซิร์กไม่มีสตรีคอารีน่าประสิทธิภาพโดยเฉลี่ยคะแนนเฉลี่ย
diff --git a/translation/dest/arena/tp-TP.xml b/translation/dest/arena/tp-TP.xml
index 5e5b7799cc5f5..06922b394e9bd 100644
--- a/translation/dest/arena/tp-TP.xml
+++ b/translation/dest/arena/tp-TP.xml
@@ -54,7 +54,7 @@ o musi lon tenpo lili la o tawa tawa tomo awen. tan ni la sina ken musi mute li
jan musi li ken toki lon tomo tokinanpa pona sin pi pini pona pokajan li pini pona lon tenpo 2, pini pona kama li pana e nanpa pona 4, li pana ala e nanpa pona 2.
- Berserk ala ken
+ Berserk ala kenmeso wawameso nanpa
diff --git a/translation/dest/arena/vi-VN.xml b/translation/dest/arena/vi-VN.xml
index 8d6474f5055ee..59fa8c85e29ae 100644
--- a/translation/dest/arena/vi-VN.xml
+++ b/translation/dest/arena/vi-VN.xml
@@ -22,7 +22,7 @@ Berserk không áp dụng cho ván đấu không có thời gian bắt đầu (0
Berserk chỉ thêm điểm nếu bạn chơi ít nhất 7 nước trong một ván.Cách xác định người chiến thắng?
- (Những) kỳ thủ có điểm cao nhất khi kết thúc giải đấu sẽ là (những) người thắng cuộc.
+ (Những) kỳ thủ có điểm cao nhất sau khi giải đấu kết thúc sẽ là (những) người thắng cuộc.
Nếu hai kỳ thủ bằng điểm nhau, kết quả sẽ quyết định qua tie break.Cặp đấu được chọn ra sao?
@@ -37,7 +37,7 @@ Chơi nhanh và trở lại phòng chờ để chơi được nhiều ván và g
Đây là giải đấu riêng tưChia sẻ URL này để mọi người tham gia: %s
- Chuỗi hòa: Khi một người chơi hòa liên tục ở một đấu trường, chỉ có ván hòa đầu tiên mới được tính điểm hoặc các ván tiêu chuẩn hòa mà có nhiều hơn %s nước đi. Chuỗi hòa chỉ có thể bị phá vỡ bởi một ván thắng chứ không phải một ván thua hay hòa khác.
+ Chuỗi hòa: Khi một người chơi hòa liên tục ở một đấu trường, chỉ có ván hòa đầu tiên mới được tính điểm hoặc các ván tiêu chuẩn hòa mà có nhiều hơn %s nước đi. Chuỗi hòa chỉ có thể bị phá vỡ bởi một ván thắng chứ không phải một ván thua hay hòa khác.Độ dài ván đấu tối thiểu cho các ván hòa để vẫn có điểm là khác nhau theo từng biến thể. Bảng dưới đây liệt kê ngưỡng cho từng biến thể.Biến thểĐộ dài ván đấu tối thiểu
@@ -53,9 +53,9 @@ Chơi nhanh và trở lại phòng chờ để chơi được nhiều ván và g
Cho phép các kỳ thủ trò chuyện trong phòng trò chuyệnChuỗi đấu trườngSau 2 ván thắng, mỗi ván thắng liên tiếp sẽ được 4 điểm thay vì 2 điểm.
- Không cho phép Berserk
+ Không cho phép BerserkKhông có chuỗi Đấu trườngHiệu suất trung bìnhĐiểm trung bình
- Giải đấu của tôi
+ Giải đấu của tôi
diff --git a/translation/dest/broadcast/ar-SA.xml b/translation/dest/broadcast/ar-SA.xml
index c6f6f60d34f51..3753cf47350ca 100644
--- a/translation/dest/broadcast/ar-SA.xml
+++ b/translation/dest/broadcast/ar-SA.xml
@@ -1,6 +1,7 @@
البثوث
+ بثي%s بث%s بث
@@ -11,10 +12,15 @@
بث البطولة المباشرةبث مباشر جديد
+ حول البث
+ لا جولات بعد.
+ كيفية استخدام بث ليتشيس.
+ ستضم الجولة الجديدة الأعضاء والمساهمين عينهم الذين اشتركوا في الجولة السابق.إضافة جولةالجاريةالقادمةالمكتملة
+ يعرف ليتشيس بانتهاء الجولة استناداً إلى المصدر، استخدم هذا التبديل إذا لم يكن هناك مصدر.اسم الجولةرقم الجولة (الشوط)اسم البطولة
@@ -38,4 +44,14 @@
تعديل دراسة الجولةحذف هذه المسابقةقم بحذف البطولة جميعها و جميع جولاتها و جميع ألعابها.
+ لوحة متصدرين تلقائية
+ حساب النتائج وعرض لوحة متصدرين بسيطة بناء على تلك النتائج
+ اختياري: استبدال أسماء اللاعبين وتقييماتهم وألقابهم
+ سطر واحد لكل لاعب، على النحو التالي:
+الاسم الأصلي؛ الاسم البديل؛ التقييم البديل (اختياري)؛ لقب بديل (اختياري)
+مثال:
+DrNykterstein;Magnus Carlsen;2863
+AnishGiri;Anish Giri;2764;GM
+ المدّة بالثواني
+ أختياري، كم مدة الانتظار بين الطلبات، تتراوح المدة بين الثانيتين والدقيقة، يحدد الإعداد الافتراضي بناء على عدد المشاهدين.
diff --git a/translation/dest/broadcast/be-BY.xml b/translation/dest/broadcast/be-BY.xml
index 185f060fd079a..7e41521fc0e6e 100644
--- a/translation/dest/broadcast/be-BY.xml
+++ b/translation/dest/broadcast/be-BY.xml
@@ -1,8 +1,12 @@
Трансляцыі
+ Мае трансляцыіПрамыя трансляцыі турніраўНовая прамая трансляцыя
+ Пра трансляцыіі
+ Пакуль няма тураў.
+ Як карыстацца трансляцыямі Lichess.Дадаць турБягучыяНадыходзячыя
@@ -25,4 +29,9 @@
Спампаваць усе турыСкасаваць гэты турВыдаліць гэты тур
+ Канчаткова выдаліць тур і ўсе яго гульні.
+ Выдаліць усе гульні гэтага тура. Для іх паўторнага стварэння крыніца павінна быць актыўнай.
+ Рэдагаваць навучанне туру
+ Выдаліць гэты турнір
+ Канчаткова выдаліць увесь турнір, усе яго туры і ўсе гульні.
diff --git a/translation/dest/broadcast/de-DE.xml b/translation/dest/broadcast/de-DE.xml
index 3ef13e47384a8..a4ec07e7352de 100644
--- a/translation/dest/broadcast/de-DE.xml
+++ b/translation/dest/broadcast/de-DE.xml
@@ -10,10 +10,13 @@
Neue LiveübertragungÜber ÜbertragungenNoch keine Runden.
+ Wie man Lichess-Übertragungen benutzt.
+ Die nächste Runde wird die gleichen Mitspieler und Mitwirkende haben wie die vorhergehende.Eine Runde hinzufügenLaufendDemnächstBeendet
+ Lichess erkennt den Abschluss einer Runde anhand der Quellspiele. Verwenden Sie diesen Schalter, wenn keine Quelle vorhanden ist.RundennameRundennummerTurniername
@@ -37,5 +40,14 @@
Rundenstudie bearbeitenDieses Turnier löschenLösche definitiv das gesamte Turnier, alle seine Runden und Partien.
+ Automatische Rangliste
+ Berechne und zeige eine einfache Rangliste basierend auf den Spielergebnissen
+ Optional: Spielernamen, Wertungen und Titel ersetzen
+ Eine Zeile pro Spieler, wie folgt formatiert:
+Originalname; Ersatzname; optional Ersatzwertung; optional Ersatztitel
+Beispiel:
+DrNykterstein;Magnus Carlsen;2863
+AnishGiri;Anish Giri;2764;GMDauer in Sekunden
+ Optional, wie lange zwischen den Anfragen gewartet werden soll. Mindestens 2s, maximal 60s. Standardmäßig auf der Zuschaueranzahl basierend.
diff --git a/translation/dest/broadcast/eu-ES.xml b/translation/dest/broadcast/eu-ES.xml
index a4aea42bb450d..52760464c90d1 100644
--- a/translation/dest/broadcast/eu-ES.xml
+++ b/translation/dest/broadcast/eu-ES.xml
@@ -8,10 +8,15 @@
Txapelketen zuzeneko emanaldiakZuzeneko emanaldi berria
+ Zuzeneko emanaldiei buruz
+ Ez dago txandarik.
+ Nola erabili Lichessen Zuzenekoak.
+ Txanda berriak aurrekoak beste kide eta laguntzaile izango ditu.Gehitu txanda batOrain martxanHurrengo emanaldiakAmaitutako emanaldiak
+ Txanda amaitu dela jatorrizko partidekin detektatzen du Lichessek. Erabili aukera hau jatorririk ez badago.Txandaren izenaTxanda zenbakiTxapelketaren izena
@@ -35,4 +40,14 @@
Editatu txandako azterlanaEzabatu txapelketa hauTxapelketa behin betiko ezabatu, bere txanda eta partida guztiak barne.
+ Sailkapen automatikoa
+ Partiden emaitzetan oinarritutako sailkapen sinplea kalkulatu eta erakutsi
+ Hautazkoa: aldatu jokalarien izen, puntuazio eta tituluak
+ Lerro bat jokalari bakotizeko, horrela formateatuta:
+Jatorrizko izena; Ordezko izena; Ordezko puntuazioa (hautazkoa); Ordezko titulua (hautazkoa)
+Adibidea:
+DrNykterstein;Magnus Carlsen;2863
+AnishGiri;Anish Giri;2764;GM
+ Aldia segundotan
+ Hautazkoa, zenbat itxaron eskaeren artean. Gutxienez 2 segundo, gehienez 60 segundo. Automatikora itzuliko da ikusle kopuruaren arabera.
diff --git a/translation/dest/broadcast/fr-FR.xml b/translation/dest/broadcast/fr-FR.xml
index 1aadc9a7dd7df..e762ae23b6ec7 100644
--- a/translation/dest/broadcast/fr-FR.xml
+++ b/translation/dest/broadcast/fr-FR.xml
@@ -40,7 +40,7 @@
Modifier l\'étude de la rondeSupprimer ce tournoiSupprimer définitivement le tournoi, toutes ses rondes et toutes ses parties.
- Échiquier principal automatique
+ Classement automatiqueCalculer et afficher un échiquier principal simplifié basé sur les résultats de partiesFacultatif : remplacer les noms des joueurs, les cotes et les titresUne ligne par joueur, formatée comme suit :
diff --git a/translation/dest/broadcast/gsw-CH.xml b/translation/dest/broadcast/gsw-CH.xml
index 4eef4bbda9ef0..cd01d363550e4 100644
--- a/translation/dest/broadcast/gsw-CH.xml
+++ b/translation/dest/broadcast/gsw-CH.xml
@@ -1,16 +1,16 @@
Überträgige
- Mini Überträgige
+ Eigeni Überträgige%s Überträgige%s Überträgige
- Live Turnier Überträgige
- Neui Live Überträgige
+ Live Turnier-Überträgige
+ Neui Live-ÜberträgigeÜber ÜberträgigeNo kei Rundene.
- Wie mer Lichess Überträgige benutzt.
+ Wie mer Lichess-Überträgige benutzt.Die neu Runde wird us de gliche Mitglieder und Mitwürkende beschtah, wie die Vorherig.E Rundi zuefüegeLaufend
diff --git a/translation/dest/broadcast/it-IT.xml b/translation/dest/broadcast/it-IT.xml
index 6ee3a2c82aa83..13b559c9b1f64 100644
--- a/translation/dest/broadcast/it-IT.xml
+++ b/translation/dest/broadcast/it-IT.xml
@@ -8,10 +8,15 @@
Tornei in direttaNuova diretta
+ Informazioni sulle trasmissioni
+ Ancora nessun turno.
+ Istruzioni delle trasmissioni Lichess.
+ Il nuovo turno avrà gli stessi membri e contributori del precedente.Aggiungi un turnoIn corsoProssimamenteConclusa
+ Lichess rileva il completamento del turno a seconda delle partite di origine. Utilizza questo interruttore se non è presente alcuna origine.Nome turnoTurno numeroNome del torneo
@@ -35,4 +40,14 @@
Modifica lo studio del turnoElimina questo torneoElimina definitivamente l\'intero torneo, tutti i turni e tutte le partite.
+ Classifica automatica
+ Calcola e mostra una semplice classifica basata sui risultati delle partite
+ Facoltativo: sostituisci i nomi dei giocatori, i punteggi e i titoli
+ Una riga per giocatore, formattata come segue:
+Nome; Nome sostitutivo; Punteggio sostitutivo (facoltativo); Titolo sostitutivo (facoltativo)
+Esempi:
+DrNykterstein;Magnus Carlsen;2863
+AnishGiri;Anish Giri;2764;GM
+ Intervallo in secondi
+ Facoltativo: quanto a lungo aspettare tra due richieste. Minimo 2s, massimo 60s. Il default è scelto in base al numeri di spettatori.
diff --git a/translation/dest/broadcast/sq-AL.xml b/translation/dest/broadcast/sq-AL.xml
index d6927f97164b6..0946a1d811acd 100644
--- a/translation/dest/broadcast/sq-AL.xml
+++ b/translation/dest/broadcast/sq-AL.xml
@@ -8,10 +8,15 @@
Transmetime të drejtpërdrejta turneshTransmetim i ri i drejtpërdrejtë
+ Rreth transmetimeve
+ Ende pa raunde.
+ Si të përdoren Transmetimet Lichess.
+ Raundi i ri do të ketë të njëjtën anëtarë dhe kontribues si i mëparshmi.Shtoni një raundNë zhvillimI ardhshëmI mbaruar
+ Lichess-i e pikas plotësimin e raundit bazuar në lojërat burim. Përdoreni këtë buton, nëse s’ka burim.Emër raundiNumër raundiEmër turneu
@@ -34,4 +39,14 @@
Përpunoni analizë raundiFshije këtë turneFshihe përfundimisht krejt turneun, krejt raundet e tij dhe krejt lojërat në të.
+ Tabelë automatike
+ Harto dhe shfaq një tabelë të thjeshtë bazuar në përfundime lojërash
+ Opsionale: zëvendësoni emra lojëtarësh, vlerësime dhe tituj
+ Një rresht për lojtar, formatuar kështu:
+Emri origjinal name; Emri zëvendësim; Vlerësim opsional zëvendësim; Titull opsional zëvendësim
+Shembull:
+DrNykterstein;Magnus Carlsen;2863
+AnishGiri;Anish Giri;2764;GM
+ Periudhë, në sekonda
+ Opsionale, sa gjatë të pritet mes kërkesash. Min. 2s, maks. 60s. Si parazgjedhje, përdoret vlera automatike bazuar në numrin e parësve.
diff --git a/translation/dest/broadcast/tr-TR.xml b/translation/dest/broadcast/tr-TR.xml
index 62c7c07a98853..75fe893cb6669 100644
--- a/translation/dest/broadcast/tr-TR.xml
+++ b/translation/dest/broadcast/tr-TR.xml
@@ -8,6 +8,8 @@
Canlı Turnuva YayınlarıCanlı Turnuva Ekle
+ Canlı Turnuvalar hakkında
+ Lichess Canlı Turnuvaları nasıl kullanılır.Bir tur ekleDevam eden turnuvalarYaklaşan turnuvalar
diff --git a/translation/dest/broadcast/uk-UA.xml b/translation/dest/broadcast/uk-UA.xml
index a39a12b1743ca..eb47c0ddc2889 100644
--- a/translation/dest/broadcast/uk-UA.xml
+++ b/translation/dest/broadcast/uk-UA.xml
@@ -13,6 +13,7 @@
Про трансляціюПоки що немає раундів.Як користуватися Lichess трансляціями.
+ У новому раунді будуть ті самі учасники та редактори, що й у попередньому.Додати турПоточніМайбутні
@@ -41,8 +42,8 @@
Редагувати дослідження туруВидалити турнірОстаточно видалити весь турнір, всі його раунди та всі його ігри.
- Автоматична таблиця лідерів
- Розрахунки та показ простої таблиці лідерів на основі результатів гри
+ Автоматична таблиця лідерів
+ Розрахунки та показ простої таблиці лідерів на основі результатів гриОпціонально: замінити імена гравців, рейтинги та заголовкиОдин рядок на одного гравця, приклад форматування нижче:
Справжнє ім\'я; ім\'я на заміну; замінити рейтинг (опціонально); замінити звання (опціонально)
diff --git a/translation/dest/broadcast/vi-VN.xml b/translation/dest/broadcast/vi-VN.xml
index 5e36af89b59b5..ca99a331a5658 100644
--- a/translation/dest/broadcast/vi-VN.xml
+++ b/translation/dest/broadcast/vi-VN.xml
@@ -1,13 +1,13 @@
- Phát sóng
+ Các phát sóngCác buổi phát sóng của tôi%s phát sóngGiải đấu phát trực tuyếnPhát sóng trực tiếp mới
- Giới thiệu phát sóng
+ Giới thiệu về phát sóngChưa có vòng nào.Cách sử dụng Phát sóng Lichess.Vòng mới sẽ có các thành viên và cộng tác viên giống như vòng trước.
@@ -39,8 +39,8 @@
Chỉnh sửa vòng nghiên cứuXóa giải đấu nàyXóa dứt khoát toàn bộ giải đấu, tất cả các vòng và tất cả ván cờ trong đó.
- Bảng xếp hạng tự động
- Tính toán và hiển thị bảng xếp hạng đơn giản dựa trên kết quả ván đấu
+ Bảng xếp hạng tự động
+ Tính toán và hiển thị bảng xếp hạng đơn giản dựa trên kết quả ván đấuTùy chọn: biệt danh, hệ số Elo và danh hiệuMột dòng cho mỗi người chơi, được định dạng như sau:
Tên khai sinh; Tên thay thế; Hệ số Elo thay thế tùy chọn; Danh hiệu thay thế tùy chọn
diff --git a/translation/dest/challenge/an-ES.xml b/translation/dest/challenge/an-ES.xml
index c47b80a31a096..4ba55264fab4d 100644
--- a/translation/dest/challenge/an-ES.xml
+++ b/translation/dest/challenge/an-ES.xml
@@ -1,6 +1,6 @@
- Desafíos: %1$s
+ Desafíos: %1$sDesafiar a una partidaDesafío refusauDesafío acceptau!
@@ -12,15 +12,15 @@
No puez ninviar desafíos perque la tuya puntuación de %s ye provisional.%s nomás accepta desafíos d\'os suyos amigos.No accepto desafíos en este momento.
- No me viene bien agora, torna-me-lo a demandar mas enta debant.
- Este control de tiempos ye masiau rapido pa yo, per favor, desafía-me a una partida mas lenta.
- Este control de tiempos ye masiau lento pa yo, per favor, desafiame a una partida mas rapida.
- No accepto desafíos con este control de tiempos.
- Per favor, desafía-me a una partida puntuable.
- Per favor, desafía-me a una partida amistosa.
- No accepto desafía chugar variants en este momento.
- No quiero chugar esta variant en este momento.
+ No me viene bien agora, torna-me-lo a demandar mas enta debant.
+ Este control de tiempos ye masiau rapido pa yo, per favor, desafía-me a una partida mas lenta.
+ Este control de tiempos ye masiau lento pa yo, per favor, desafiame a una partida mas rapida.
+ No accepto desafíos con este control de tiempos.
+ Per favor, desafía-me a una partida puntuable.
+ Per favor, desafía-me a una partida amistosa.
+ No accepto desafía chugar variants en este momento.
+ No quiero chugar esta variant en este momento.No accepto desafíos de bots.Nomás accepto desafíos de bots.
- U convida a un usuario de Lichess:
+ U convida a un usuario de Lichess:
diff --git a/translation/dest/challenge/br-FR.xml b/translation/dest/challenge/br-FR.xml
index a69f4dc84911d..a74bdf1f4ccb9 100644
--- a/translation/dest/challenge/br-FR.xml
+++ b/translation/dest/challenge/br-FR.xml
@@ -12,11 +12,11 @@
Ne c\'hallit ket daeañ abalamour d\'ar renkadur %s da c\'hortoz.Ne vez ket degemeret daeoù nemet digant mignoned gant %s.Ne zegemeran ket daeoù er mare-mañ.
- N\'on ket dijabl evit poent, kasit ur goulenn din en-dro diwezhatoc\'h mar plij ganeoc\'h.
- Kasit din un dae renket kentoc\'h, mar plij ganeoc\'h.
- Kasit un dae ordin din kentoc\'h mar plij ganeoc\'h.
- Ne fell ket din c\'hoari an doare echedoù-mañ evit poent.
+ N\'on ket dijabl evit poent, kasit ur goulenn din en-dro diwezhatoc\'h mar plij ganeoc\'h.
+ Kasit din un dae renket kentoc\'h, mar plij ganeoc\'h.
+ Kasit un dae ordin din kentoc\'h mar plij ganeoc\'h.
+ Ne fell ket din c\'hoari an doare echedoù-mañ evit poent.Ne zegemeran ket daeoù kaset gant robotoù.Daeoù kaset gant robotoù a zegemeran nemetken.
- Pediñ ur c\'hoarier war Lichess mod-all:
+ Pediñ ur c\'hoarier war Lichess mod-all:
diff --git a/translation/dest/challenge/gl-ES.xml b/translation/dest/challenge/gl-ES.xml
index 6bf79acc9bdde..cef75706fd3f1 100644
--- a/translation/dest/challenge/gl-ES.xml
+++ b/translation/dest/challenge/gl-ES.xml
@@ -12,15 +12,15 @@
Non podes desafiar dado que a túa puntuación en %s é provisional.%s só acepta desafíos de amigos.Neste momento non acepto desafíos.
- Agora mesmo non podo. Por favor, téntao máis tarde.
- Paréceme pouco tempo. Proba cun ritmo máis lento.
- Paréceme moito tempo. Proba cun ritmo máis rápido.
- Non acepto desafíos a este ritmo.
- Por favor, rétame a unha partida puntuada.
- Por favor, rétame a unha partida amigable.
- Agora mesmo non acepto desafíos en variantes.
- Neste momento non me apetece xogar esa variante.
+ Agora mesmo non podo. Por favor, téntao máis tarde.
+ Paréceme pouco tempo. Proba cun ritmo máis lento.
+ Paréceme moito tempo. Proba cun ritmo máis rápido.
+ Non acepto desafíos a este ritmo.
+ Por favor, rétame a unha partida puntuada.
+ Por favor, rétame a unha partida amigable.
+ Agora mesmo non acepto desafíos en variantes.
+ Neste momento non me apetece xogar esa variante.Non acepto desafíos de bots.Só acepto desafíos de bots.
- Ou convida a un usuario de Lichess:
+ Ou convida a un usuario de Lichess:
diff --git a/translation/dest/challenge/ms-MY.xml b/translation/dest/challenge/ms-MY.xml
index 5c7d09716ba64..dc87f8c768240 100644
--- a/translation/dest/challenge/ms-MY.xml
+++ b/translation/dest/challenge/ms-MY.xml
@@ -11,14 +11,14 @@
Tidak boleh mencabar orang lain kerana penilaian %s hanya sementara.%s hanya menerima cabaran daripada rakan.Saya tidak menerima cabaran pada masa ini.
- Ini bukan masa yang sesuai untuk saya, sila tanya lagi nanti.
- Kawalan masa ini terlalu cepat untuk saya, sila cabar lagi dengan permainan yang lebih perlahan.
- Kawalan masa ini terlalu perlahan untuk saya, sila cabar lagi dengan permainan yang lebih cepat.
- Saya tidak menerima cabaran dengan kawalan masa ini.
- Sila hantarkan saya dengan cabaran yang dirating.
- Sila hantar cabaran santai kepada saya.
- Saya tidak menerima cabaran variasi sekarang.
- Saya tidak ingin bermain variasi ini sekarang.
+ Ini bukan masa yang sesuai untuk saya, sila tanya lagi nanti.
+ Kawalan masa ini terlalu cepat untuk saya, sila cabar lagi dengan permainan yang lebih perlahan.
+ Kawalan masa ini terlalu perlahan untuk saya, sila cabar lagi dengan permainan yang lebih cepat.
+ Saya tidak menerima cabaran dengan kawalan masa ini.
+ Sila hantarkan saya dengan cabaran yang dirating.
+ Sila hantar cabaran santai kepada saya.
+ Saya tidak menerima cabaran variasi sekarang.
+ Saya tidak ingin bermain variasi ini sekarang.Saya tidak menerima cabaran daripada bot.Saya hanya menerima cabaran daripada bot.
diff --git a/translation/dest/challenge/tp-TP.xml b/translation/dest/challenge/tp-TP.xml
index b5a9f733fd598..30bc07cd0946d 100644
--- a/translation/dest/challenge/tp-TP.xml
+++ b/translation/dest/challenge/tp-TP.xml
@@ -1,6 +1,6 @@
- utala: %1$s
+ utala: %1$swile musiona li wile ala musiona li wile musi a!
@@ -12,15 +12,15 @@
sina ken ala utala musi e jan. nasin %s la nanpa wawa sina li ken pini.jan pona li ken utala musi e jan %s. sina li jan pona ala.mi wile ala musi lon tenpo ni.
- mi ken ala musi lon tenpo ni. toki tawa mi lon tenpo ante.
- tenpo musi ni li lili mute tawa mi. musi pi tenpo mute la o utala musi e mi.
- tenpo musi ni li suli mute tawa mi. musi pi tenpo lili la o utala musi e mi.
- tenpo li ni la mi wile ala musi.
- mi wile kama jo e nanpa wawa. sina wile e ni la o utala musi e mi.
- mi wile ala kama jo e nanpa wawa. sina wile ala e ni la o utala musi e mi.
- mi wile ala musi kepeken nasin ante lon tenpo ni.
- mi wile ala musi kepeken nasin ni lon tenpo ni.
+ mi ken ala musi lon tenpo ni. toki tawa mi lon tenpo ante.
+ tenpo musi ni li lili mute tawa mi. musi pi tenpo mute la o utala musi e mi.
+ tenpo musi ni li suli mute tawa mi. musi pi tenpo lili la o utala musi e mi.
+ tenpo li ni la mi wile ala musi.
+ mi wile kama jo e nanpa wawa. sina wile e ni la o utala musi e mi.
+ mi wile ala kama jo e nanpa wawa. sina wile ala e ni la o utala musi e mi.
+ mi wile ala musi kepeken nasin ante lon tenpo ni.
+ mi wile ala musi kepeken nasin ni lon tenpo ni.mi wile ala e ni: jan ilo li utala musi e mi.mi wile e ni: jan ilo li utala musi e mi.
- anu tawa e jan ilo Lichess:
+ anu tawa e jan ilo Lichess:
diff --git a/translation/dest/challenge/vi-VN.xml b/translation/dest/challenge/vi-VN.xml
index 3239f84b497bd..8e39dc857e376 100644
--- a/translation/dest/challenge/vi-VN.xml
+++ b/translation/dest/challenge/vi-VN.xml
@@ -1,8 +1,8 @@
- Thách đấu (%1$s)
+ Thách đấu (%1$s)Yêu cầu chơi
- Lời thách đấu bị từ chối
+ Lời thách đấu bị từ chối.Lời thách đấu được chấp nhận!Lời thách đấu bị hủy bỏ.Xin vui lòng đăng ký để gửi những lời thách đấu.
@@ -12,15 +12,15 @@
Không thể thách đấu do xếp hạng %s tạm thời.%s chỉ chấp nhận những thách đấu từ bạn bè.Hiện tại tôi đang không chấp nhận thách đấu.
- Tôi chưa sẵn sàng, hãy hỏi lại sau.
- Tùy chọn thời gian quá nhanh đối với tôi, hãy thách đấu lại với một tùy chọn chậm hơn.
- Tùy chọn thời gian quá chậm đối với tôi, hãy thách đấu lại với một tùy chọn nhanh hơn.
- Tôi không chấp nhận thách đấu với tùy chọn thời gian này.
- Hãy gửi yêu cầu thách đấu có tính xếp hạng cho tôi.
- Hãy gửi tôi yêu cầu thách đấu không tính elo.
- Tôi không muốn chơi biến thể bây giờ.
- Tôi chưa sẵn sàng chơi biến thể này bây giờ.
+ Tôi chưa sẵn sàng, hãy hỏi lại sau.
+ Tùy chọn thời gian quá nhanh đối với tôi, hãy thách đấu lại với một tùy chọn chậm hơn.
+ Tùy chọn thời gian quá chậm đối với tôi, hãy thách đấu lại với một tùy chọn nhanh hơn.
+ Tôi không chấp nhận thách đấu với tùy chọn thời gian này.
+ Hãy gửi yêu cầu thách đấu có tính xếp hạng cho tôi.
+ Hãy gửi tôi yêu cầu thách đấu không xếp hạng.
+ Tôi không muốn chơi biến thể bây giờ.
+ Tôi chưa sẵn sàng chơi biến thể này bây giờ.Tôi không chấp nhận thách đấu từ bot.Tôi chỉ chấp nhận thách đấu từ bot.
- Hoặc mời một Người dùng Lichess:
+ Hoặc mời một người dùng Lichess:
diff --git a/translation/dest/challenge/zh-TW.xml b/translation/dest/challenge/zh-TW.xml
index d36b2ef0ce42f..c613fd83462ec 100644
--- a/translation/dest/challenge/zh-TW.xml
+++ b/translation/dest/challenge/zh-TW.xml
@@ -1,5 +1,6 @@
+ 挑戰: %1$s邀請對弈對弈邀請已拒絕對弈邀請已接受
@@ -21,4 +22,5 @@
我現在不想玩這個變體。我不接受機器人的對弈。我目前只接受機器人的對弈。
+ 或邀請一位 Lichess 用户:
diff --git a/translation/dest/class/gl-ES.xml b/translation/dest/class/gl-ES.xml
index 0fde6bbbeac10..88406933a65f5 100644
--- a/translation/dest/class/gl-ES.xml
+++ b/translation/dest/class/gl-ES.xml
@@ -100,7 +100,7 @@ Aquí está a ligazón para acceder.Separa as novas con ---
Amosará unha liña separadora horizontal.Convidar
- Fuches convidado por %s.
+ Fuches convidado por %s.Aceptaches esta invitación.Rexeitaches esta invitación.ou
diff --git a/translation/dest/class/gsw-CH.xml b/translation/dest/class/gsw-CH.xml
index bbd4e1ed9f6c5..6b9b6e11b8ef6 100644
--- a/translation/dest/class/gsw-CH.xml
+++ b/translation/dest/class/gsw-CH.xml
@@ -100,7 +100,7 @@ Da isch de Link für de Zuegriff uf die Klass.Tränn Neuigkeite mit ---
Es wird als horizontali Linie azeigt.ilade
- Du bisch iglade vu %s.
+ Du bisch iglade vu %s.Du häsch die Iladig agnah.Du häsch die Iladig abglehnt.oder
diff --git a/translation/dest/class/kn-IN.xml b/translation/dest/class/kn-IN.xml
index 9437cb1c06cd6..489113ece74e4 100644
--- a/translation/dest/class/kn-IN.xml
+++ b/translation/dest/class/kn-IN.xml
@@ -100,7 +100,7 @@
ಇದರೊಂದಿಗೆ ಪ್ರತ್ಯೇಕ ಸುದ್ದಿ ---
ಇದು ಸಮತಲ ರೇಖೆಯನ್ನು ಪ್ರದರ್ಶಿಸುತ್ತದೆ.ಆಹ್ವಾನಿಸಿ
- ನಿಮ್ಮನ್ನು %s ಅವರು ಆಹ್ವಾನಿಸಿದ್ದಾರೆ.
+ ನಿಮ್ಮನ್ನು %s ಅವರು ಆಹ್ವಾನಿಸಿದ್ದಾರೆ.ನೀವು ಈ ಆಹ್ವಾನವನ್ನು ಒಪ್ಪಿಕೊಂಡಿದ್ದೀರಿ.ನೀವು ಆಹ್ವಾನವನ್ನು ನಿರಾಕರಿಸಿದ್ದೀರಿ.ಅಥವಾ
diff --git a/translation/dest/class/lb-LU.xml b/translation/dest/class/lb-LU.xml
index b13eb9da9330f..b8415497bb1d2 100644
--- a/translation/dest/class/lb-LU.xml
+++ b/translation/dest/class/lb-LU.xml
@@ -92,7 +92,7 @@ Hei ass de Link fir op de Cours zouzegräifen.Trenn déi verschidden Nouvellë mat ---
Doduerch gëtt eng hotizontal Linn ugewisen.Invitéieren
- Du goufs vum %s invitéiert.
+ Du goufs vum %s invitéiert.Du hues dës Invitatioun ugeholl.Du hues dës Invitatioun ofgeleent.oder
diff --git a/translation/dest/class/pt-PT.xml b/translation/dest/class/pt-PT.xml
index bba7c02da7750..2f44d94833fa7 100644
--- a/translation/dest/class/pt-PT.xml
+++ b/translation/dest/class/pt-PT.xml
@@ -99,7 +99,7 @@ Aqui está o link para acederes à aula.
Adiciona as novidades recentes no topo. Não excluas novidades anteriores.Separa as novidades com --- tal irá mostrar uma linha horizontal.Convidar
- Foi convidado por %s.
+ Foi convidado por %s.Aceitou este convite.Recusou este convite.ou
diff --git a/translation/dest/class/sk-SK.xml b/translation/dest/class/sk-SK.xml
index d805256687caa..78df55878152f 100644
--- a/translation/dest/class/sk-SK.xml
+++ b/translation/dest/class/sk-SK.xml
@@ -24,7 +24,7 @@
Viditeľné aj pre učiteľov aj pre študentov triedyUčitelia triedyPridať Lichess užívateľov za účelom ich pozvania ako učiteľov. Jeden na riadok.
- Obnoviť heslo
+ Vynulovať hesloTeraz si zkopírujte alebo zapíšte heslo! Nebude možné aby ste ho videli znovu.Heslo: %sVygenerovať nové heslo pre študenta
diff --git a/translation/dest/class/vi-VN.xml b/translation/dest/class/vi-VN.xml
index 6e9eb7c9799cd..f737c4f38c02d 100644
--- a/translation/dest/class/vi-VN.xml
+++ b/translation/dest/class/vi-VN.xml
@@ -12,7 +12,7 @@
Giáo viên: %sLớp học mớiĐóng lớp học
- Đã xóa bởi %s
+ Đã bị xóa bởi %sMở lạiLoại bỏ học viênĐã loại bỏ
@@ -25,12 +25,12 @@
Giáo viên của lớpThêm tên người dùng Lichess để mời họ làm giáo viên. Mỗi người một dòng.Đặt lại mật khẩu
- Hãy chắc chắn sao chép hoặc ghi lại mật khẩu ngay bây giờ. Bạn sẽ không thể nhìn thấy nó sau này nữa!
+ Hãy chắc chắn sao chép hoặc ghi lại mật khẩu ngay bây giờ. Bạn sẽ không bao giờ có thể nhìn thấy nó nữa!Mật khẩu: %sTạo mật khẩu mới cho học viên%1$s được mời bởi %2$sTên thật
- Riêng tư. Sẽ không bao giờ được hiển thị bên ngoài lớp học. Giúp nhớ học viên là ai.
+ Riêng tư. Sẽ không bao giờ được hiển thị bên ngoài lớp học. Giúp bạn nhớ học viên là ai.Thêm học viênĐã tạo %1$s làm hồ sơ Lichess cho %2$s.Học viên:%1$s
@@ -48,7 +48,7 @@ Mật khẩu: %3$sNếu họ đã có, hãy sử dụng biểu mẫu mời thay thế.Chỉ được tạo tài khoản cho học sinh có thật. Không được lợi dụng để tạo nhiều tài khoản cho bản thân. Bạn sẽ bị ban.Tên người dùng Lichess
- Tạo một tài khoản mới
+ Gợi ý tạo một tên người dùng mớiChào mừng đến với lớp học của bạn: %s.
Đây là đường link để tham gia lớp học.Bạn được mời đến vào lớp học \"%s\" với tư cách là một học viên.
@@ -58,12 +58,12 @@ Mật khẩu: %3$s
Chỉ hiển thị cho các giáo viênHoạt động
- Quản lý
+ Được quản lýTài khoản học viên này đã được quản lý
- Nâng cấp từ được quản lý lên tự chủ
+ Nâng cấp từ bị quản lý sang tự chủBản phát hànhGiải phóng tài khoản để học viên có thể quản lý nó một cách tự chủ.
- Một tài khoản đã được giải phóng không thể được quản lý lại. Học viên sẽ có thể chuyển đổi chế độ trẻ em và tự đặt lại mật khẩu.
+ Một tài khoản đã được tốt nghiệp không thể được quản lý lại. Học viên sẽ có thể chuyển đổi chế độ trẻ em và tự đặt lại mật khẩu.Học viên sẽ vẫn ở trong lớp sau khi tài khoản của họ được giải phóng.Địa chỉ email thực, duy nhất của học viên. Chúng tôi sẽ gửi email xác nhận tới đó, kèm theo liên kết để giải phóng tài khoản.Đóng tài khoản
@@ -79,11 +79,11 @@ Mật khẩu: %3$s
Học viênTiến trình
- Không có học viên nào trong lớp.
- Không học viên nào được loại bỏ.
- Qua nhiều ngày
+ Chưa có học viên nào trong lớp.
+ Không có học viên nào bị loại bỏ.
+ Qua số ngàyThời gian chơi
- %1$s qua %2$s cuối
+ %1$s qua %2$s quaTỉ lệ thắngKhông xác địnhTổng quan
@@ -91,12 +91,12 @@ Mật khẩu: %3$s
Tin tức mới về lớp họcSửa tin tứcThông báo tất cả học viên
- Không có gì ở đây.
+ Chưa có gì ở đây.Tất cả các tin tức lớp học trong một lĩnh vực đơn lẻ.Thêm những tin tức mới nhất lên đầu trang. Đừng xóa tin tức trước đó.Phân cách tin tức bởi --- nó sẽ hiển thị một đường ngang.Mời
- Bạn được mời bởi %s.
+ Bạn được mời bởi %s.Bạn đã chấp nhận lời mời.Bạn đã từ chối lời mời.hoặc
diff --git a/translation/dest/coach/vi-VN.xml b/translation/dest/coach/vi-VN.xml
index 7bca5e3d6a05f..469aa0d39d097 100644
--- a/translation/dest/coach/vi-VN.xml
+++ b/translation/dest/coach/vi-VN.xml
@@ -14,7 +14,7 @@
Đang nhận học viênHiện tại không nhận học viên%s huấn luyện học viên
- Xem hồ sơ của %s
+ Xem hồ sơ Lichess của %sGửi tin nhắn riêngGiới thiệu và những thông tin về tôiKinh nghiệm chơi cờ
diff --git a/translation/dest/contact/be-BY.xml b/translation/dest/contact/be-BY.xml
index 845b94d48db0e..6ae22945e9543 100644
--- a/translation/dest/contact/be-BY.xml
+++ b/translation/dest/contact/be-BY.xml
@@ -55,6 +55,9 @@
У пэўных абставінах, калі гуляючы супраць уліковых запісаў ботаў, рэйтангавая партыя можа не прыводзіць да змены рэйтынгу. Калі ўстаноўлена, што гулец злоўжывае бота дзеля павялячэння рэйтынгу.Старонка памылкіКалі вы траміце на старонку з памылкай, вы можаце паведаміць пра яе:
+ Я хачу трансляваць турнір
+ Дазнайцеся, як карыстацца трансляцыямі Lichess
+ Вы можаце звязацца з камандай трансляцый пра афіцыйныя эфіры.Абскарджанне бана ўліковага запісу ці абмежавання IP-адрасаВыкарыстанне рухавіка або несумленная гульняВы можаце накіраваць апеляцыю праз %s.
diff --git a/translation/dest/contact/gsw-CH.xml b/translation/dest/contact/gsw-CH.xml
index 5c055c517b656..0e6becb19946b 100644
--- a/translation/dest/contact/gsw-CH.xml
+++ b/translation/dest/contact/gsw-CH.xml
@@ -57,7 +57,7 @@
Wänn du e Fählersite entdeckt häsch, chasch sie mälde:Ich wott es Turnier überträgeLern wie du - uf Lichess - dini eigeni Überträgig machsch
- Du chasch - für offizielli Überträgige - au s\'broadcast-team kontaktiere.
+ Du chasch - für offizielli Überträgige - au s\'Broadcast-Team kontaktiere.Ischpruch gäge en Usschluss oder IP-BeschränkigMarkierig vu Computer Underschtützig oder BetrugDu chasch en Ischpruch a %s sände.
diff --git a/translation/dest/contact/lb-LU.xml b/translation/dest/contact/lb-LU.xml
index 0d481162f181b..f4c9369168088 100644
--- a/translation/dest/contact/lb-LU.xml
+++ b/translation/dest/contact/lb-LU.xml
@@ -23,8 +23,18 @@
Géi op dës Säit, fir d\'Grouss- a Klengschreiwung vun dengem Benotzernumm ze ännerenEch wëll mäin Verlaf oder meng Wäertung läschenEch wëll een Spiller mellen
+ Fir ee Spiller ze mellen, benotz de Mellformulaire
+ Du kënns och op déi Säit, andeems de op den %s-Mell-Knäppchen op enger Profilsäit klicks.
+ Mell keng Spiller am Forum.
+ Schéck eis keng Mell-E-Mailen.
+ Schéck wgl. keng direkt Messagen un d\'Moderatoren.
+ Just d\'Melle vu Spiller iwwert de Mellformulaire hëlleft eppes.Ech wëll een Bug mellen
+ An der Lichess-Feedback-Sektioun vum Forum
+ Als e Lichess-Websäiteproblem op GitHub
+ Als e Lichess-Mobil-Applikatiouns-Problem op GitHubAm Lichess-Discordserver
+ Beschreif wgl., wéi de Feeler ausgesäit, wat s de amplaz erwaart hues a wat ee maache muss, fir de Feeler ze reproduzéieren.Illegalen BauerenschlagzuchDat gëtt „en passant“ genannt an ass eng vun de Reegele vum Schach.Probéiert dëst klengt, interaktiivt Spill fir méi iwwer „en passant“ ze léieren.
@@ -43,6 +53,7 @@
Asproch géint een Bann oder eng IP RestriktiounEngine- oder BedruchsmarkéierungDu kanns däin Asproch un %s schécken.
+ Falsch-Positiv-Resultater kënnen heiansdo virkommen a mir entschëllegen eis dofir.Aner AschränkungZesummenaarbecht, Legales, KommerziellesLichess monetiséieren
diff --git a/translation/dest/contact/tp-TP.xml b/translation/dest/contact/tp-TP.xml
index 9f2257fe697db..c8c9adc8c53e1 100644
--- a/translation/dest/contact/tp-TP.xml
+++ b/translation/dest/contact/tp-TP.xml
@@ -3,6 +3,15 @@
tokio toki tawa jan pi ilo Lichessmi wile e ni: o pana lukin e nimi wawa mi lon lipu \"Lichess\"
+ mi wile pini e lipu mi
+ sina ken pini e lipu sina lon lipu ni
+ mi wile toki e jan ike tawa sina
+ mi wile toki e pakala tawa sina
+ lon ma Siko pi lipu Lichess
+ o toki lon ni tu wan:
+pakala ni li seme?
+pakala ni li weka la ilo li pali ante seme?
+nasin seme la mi ken kama sin e pakala ni?ijo musi li ken anpa ike e ijo \"jan utala\"anpa ni li \"anpa tawa\" (\"en passant\"). ona li pona tawa nasin musi.sina wile kama sona e \"anpa tawa\", o lukin e musi lili ni.
diff --git a/translation/dest/contact/vi-VN.xml b/translation/dest/contact/vi-VN.xml
index ba5a32b1fae3c..2aa4c0592cb71 100644
--- a/translation/dest/contact/vi-VN.xml
+++ b/translation/dest/contact/vi-VN.xml
@@ -23,7 +23,7 @@
Tôi muốn thay đổi tên đăng nhập của mìnhXem trang này để thay đổi chữ cái tên người dùngChúng tôi không thể đổi nhiều chữ cái hơn. Vì lý do kỹ thuật, điều đó dứt khoát không thể.
- Tuy nhiên, bạn có thể đóng tài khoản hiện tại của mình, và tạo một tài khoản mới.
+ Tuy nhiên, bạn có thể đóng tài khoản hiện tại của mình và tạo một tài khoản mới.Tôi muốn xóa lịch sử hoặc hệ sốBạn không thể xóa lịch sử các ván đấu, câu đố hay hệ số.Tôi muốn báo cáo một người chơi
@@ -46,13 +46,13 @@
Nhập thành chỉ bị cấm khi vua đi qua ô bị đối phương kiểm soát.Hãy chắc việc bạn hiểu luật nhập thànhThử trò chơi tương tác nhỏ này để thực hành về nhập thành trong cờ vua
- Nếu bạn nhập ván đấu, hoặc bắt đầu từ một vị trí ván đấu, đảm bảo rằng bạn đặt đúng quyền nhập thành.
+ Nếu bạn nhập ván đấu hoặc bắt đầu từ một thế cờ, đảm bảo rằng bạn đặt đúng quyền nhập thành.Không đủ quân để chiếu hếtTheo Luật cờ vua của FIDE §6.9, nếu tình huống chiếu hết có thể xảy ra với bất kì thứ tự nước đi hợp lệ nào sau đó, ván cờ không được tính là hòaCó thể xẩy ra trường hợp chiếu hết chỉ với một quân mã hoặc một quân tượng, nếu đối thủ có nhiều hơn chỉ có vua trên bàn cờ.Không có hệ số Elo nào được tăngXin hãy chắc việc bạn chơi ván cờ xếp hạng. Các ván đấu thường sẽ không ảnh hưởng đến hệ số của người chơi.
- Trong một số trường hợp nhất định khi chơi với tài khoản bot, trò chơi được xếp hạng có thể không trao điểm nếu xác định được rằng người chơi đang lạm dụng bot để lấy điểm xếp hạng.
+ Trong một số trường hợp nhất định khi chơi với tài khoản Bot, ván cờ có xếp hạng có thể không trao điểm nếu xác định được rằng người chơi đang lạm dụng Bot để lấy điểm xếp hạng.Trang lỗiNếu bạn gặp một trang lỗi, bạn có thẻ báo cáo nó:Tôi muốn phát sóng một giải đấu
@@ -64,7 +64,7 @@
Những giá trị dương do đánh giá sai đôi khi vẫn xẩy ra và chúng tôi xin thứ lỗi vì điều đó.Nếu khiếu nại của bạn hợp lệ, chúng tôi sẽ dỡ bỏ lệnh cấm sớm nhất có thể.Tuy nhiên nếu bạn quả thực dùng máy tính trợ giúp, dù chỉ một lần, tài khoản của bạn sẽ bị mất.
- Đừng phủ nhận đã lừa dối. Nếu bạn muốn được phép tạo một tài khoản mới, chỉ cần thừa nhận những gì bạn đã làm và cho thấy rằng bạn hiểu rằng đó là một sai lầm.
+ Đừng phủ nhận đã gian lận. Nếu bạn muốn được phép tạo một tài khoản mới, chỉ cần thừa nhận những gì bạn đã làm và chứng tỏ bạn hiểu rằng đó là một sai lầm.Hạn chế khácCộng tác, hợp pháp, thương mạiKiếm tiền với Lichess
diff --git a/translation/dest/coordinates/vi-VN.xml b/translation/dest/coordinates/vi-VN.xml
index 1fddd0f9449bd..3366fa4d5e225 100644
--- a/translation/dest/coordinates/vi-VN.xml
+++ b/translation/dest/coordinates/vi-VN.xml
@@ -4,7 +4,7 @@
Luyện tập tầm nhìnĐiểm trung bình khi cầm quân trắng: %sĐiểm trung bình khi cầm quân đen: %s
- Biết tọa độ bàn cờ là kỹ năng rất quan trọng khi chơi cờ:
+ Biết tọa độ bàn cờ là một kỹ năng rất quan trọng vì nhiều lý do:Hầu hết các khóa học và bài tập cờ vua dùng ký hiệu đại số rộng rãi.Nó giúp bạn cờ dễ nói chuyện với nhau hơn, vì cả hai đều hiểu \"ngôn ngữ cờ vua\".Bạn sẽ phân tích ván cờ hiệu quả hơn nếu bạn có thể tìm ra ngay ô cờ từ tọa độ.
diff --git a/translation/dest/dgt/ar-SA.xml b/translation/dest/dgt/ar-SA.xml
index 3ea04e700dfa8..0c6f274d12115 100644
--- a/translation/dest/dgt/ar-SA.xml
+++ b/translation/dest/dgt/ar-SA.xml
@@ -1,2 +1,48 @@
-
+
+ رقعة DGT
+ ليتشيس & DGT
+ متطلبات رقعة DGT
+ قيود استخدام رقعة DGT
+ هذه الصفحة تسمح لك بربط رقعة DGT الخاصة بك في ليتشيس، واستخدامها لعرض المباريات عليه.
+ لربط رقعة DGT الإلكترونية، يجب عليك أن تثبت%s.
+ يمكنك تنزيل البرنامَج من هنا:%s.
+ إذا كان %1$s يعمل على هذا الحاسوب، يمكنك التحقق من اتصالك به بواسطة%2$s.
+ فتح هذا الرابط
+ إذا كان %1$s يعمل على جهاز أو شبكة أخرى، يجب أن تعين عنوان IP والمنفذ في%2$s.
+ قسم الإعدادات
+ يجب أن تظل صفحة التشغيل مفتوحة على متصفحك.، ليس من الضروري أن تكون ظاهرة، يمكنك تصغيرها أو وضعها جنبًا إلى جنب مع صفحة المباراة على Lichess، لكن لا تغلقها وإلا ستتوقف رقعتك عن العمل.
+ الرقعة ستتصل تِلْقائيًا بأي مباراة تلعب، أو تبدأ، كما يمكنها تحديد المباريات التي ستبدأ قريبا.
+ يمكن ضبط الوقت في المباريات غير المقيمة على: قياسي، والسريع والمراسلة فقط.
+ يمكن ضبط الوقت في المباريات المقيمة على: القياسي، والمراسلة وبعض فئات الشطرنج السريع مثل: 10+15 و0+20
+ عندما تكون رقعتك جاهزة، انقر على%s.
+ إذا لم تكتشف الرقعة النقلة
+ تأكد أن خصمك لعب نقلته على رقعة DGT أولاً، ثم أعد لعب نقلتك، إذا لم تحل المشكلة، اعد المباراة من البداية.
+ كملاذ أخير، اعد اللوحة بشكل متطابق مع Lichess، ثم %s
+ أعد تحويل هذه الصفحة
+ تهيئة رقعة DGT
+ الربط بليتشيس
+ يجب أن يكون لديك رمز OAuth مناسب لتشغيل رقعة DGT.
+ يضيف إدخال %s المباراة إلى أعلى قائمة المباريات الخاصة بك.
+ لم ينشأ رمز OAuth.
+ اتصال لوحة DGT
+ اضغط لإنشاء رمز
+ %s رابط الويب
+ استخدم \"%1$s\" إذا لم يكن %2$s يعمل على جهاز آخر أو شبكة أخرى.
+ تحويل النص إلى كلام
+ إصدار صوت ينطق بالحركات التي تلعب، مما يساعدك على التركيز في حركاتك.
+ تمكين ربط الكلام
+ صوت الكلام
+ نطق كل الحركات
+ اختر نعم لنطق نقلاتك ونقلات خصمك، حدد لا لنطق نقلات خصمك فقط.
+ طريقة نطق الحركة
+ SAN هو نظام التدوين على Lichess مثل Nf6.
+UCI شائع في المحركات مثل \"g8f6\".
+ الكلمات المفتاحية
+ الكلمات المفتاحية بتنسيق JSON. تستخدم لترجمة التحركات والنتائج إلى لغتك، اللغة الافتراضية هي الإنجليزية، ولكن لا تتردد في تغييرها.
+ تصحيح الأخطاء
+ التسجيل المطول
+ لرؤية رسالة وحدة التحكم اضغط Command + Option + C (ماك) أو Control + Shift + C (ويندوز، لينكس ،كروم OS)
+ اللعب على رقعة DGT
+ تهيئة
+
diff --git a/translation/dest/dgt/de-DE.xml b/translation/dest/dgt/de-DE.xml
index 565192e18b000..2324c4ac1f99d 100644
--- a/translation/dest/dgt/de-DE.xml
+++ b/translation/dest/dgt/de-DE.xml
@@ -1,6 +1,6 @@
- DGT-Schachbrett
+ DGT-BrettLichess & DGTDGT-Brett-AnforderungenDGT-Brett-Beschränkungen
@@ -8,18 +8,18 @@
Um das elektronische DGT-Brett zu verbinden, musst du %s installieren.Du kannst die Software hier herunterladen: %s.Falls %1$s auf diesem Computer läuft, kannst du die Verbindung mittels %2$s überprüfen.
- Öffne diesen Link
+ Öffnen dieses LinksFalls %1$s auf einem anderen System oder Port läuft, musst du die IP-Adresse und Port hier in %2$s eingeben.Konfigurations-AbschnittDie Partieseite muss in deinem Browser geöffnet bleiben. Sie muss nicht sichtbar sein, du kannst sie minimieren oder neben die Lichess Partieseite ziehen, jedoch nicht schließen, da ansonsten das Brett nicht mehr funktionieren wird.Das Brett wird sich automatisch zu der Partie verbinden, die gerade im Gange ist, oder zu einer neuen Partie, sobald diese gestartet wurde. Die Möglichkeit auszuwählen, welcher Partie beigetreten werden soll, folgt in Kürze.Die möglichen Zeitkontrollen für ungewertete Partien sind: Klassisch, Fernschach und Schnellschach.
- Die Zeitkontrollen für gewertete Partien sind: Klassisch, Fernschach und einige Schnellschachformate wie 15+10 und 20+0
+ Zeitkontrollen für gewertete Partien: Klassisch, Fernschach und einige Schnellschachvarianten einschließlich 15+10 und 20+0Wenn du bereit bist, stelle dein Brett auf und klicke auf %s.Fallls ein Zug nicht erkannt wurdeÜberprüfe, ob du den Zug deines Gegners auf dem DGT-Brett bereits ausgeführt hast. Nimm deinen Zug zurück. Ziehe erneut.Als letztes Hilfsmittel, stelle dein Brett identisch zu dem von Lichess auf, dann %s
- Seitenansicht neu laden
+ Diese Seite neu ladenDGT - KonfigurierenLichess KonnektivitätDu hast einen OAuth Schlüssel, der für DGT-Partien geeignet ist.
@@ -28,9 +28,9 @@
DGT-Brett-VerbindungKlicke, um einen zu generieren%s WebSocket-URL
- Benutze \"%1$s\" wenn %2$s nicht auf einem anderen Rechner oder einem anderen Gerät läuft.
+ Benutze \"%1$s\" wenn %2$s nicht auf einem anderen Rechner oder einem anderen Port läuft.Text-zu-Sprache
- Konfiguriere die Sprachausgabe des gespieten Zugs, damit du deine Augen auf dem Brett behalten kannst.
+ Konfiguriere die Sprachausgabe der gespielten Züge, damit du deine Augen auf dem Brett behalten kannst.Sprachsynthese aktivierenStimme der SprachsyntheseAlle Züge vorlesen
diff --git a/translation/dest/dgt/tr-TR.xml b/translation/dest/dgt/tr-TR.xml
index c51d58584ce53..82e0d9a67129a 100644
--- a/translation/dest/dgt/tr-TR.xml
+++ b/translation/dest/dgt/tr-TR.xml
@@ -12,6 +12,7 @@
Eğer bir hamle algılanmadıysaSayfayı yeniden yükleDGT - Yapılandır
+ Lichess bağlantısı%2$s farklı bir bilgisayarda veya portta çalışmıyorsa \"%1$s\" sayfasını kullanın.Hamlelerin seslendirilişini yapılandırın, böylece tahtaya odaklanmaya devam edebilirsiniz.Bütün Hamleleri Seslendir
diff --git a/translation/dest/dgt/vi-VN.xml b/translation/dest/dgt/vi-VN.xml
index d0abf811578ba..5d0eab4132337 100644
--- a/translation/dest/dgt/vi-VN.xml
+++ b/translation/dest/dgt/vi-VN.xml
@@ -12,8 +12,8 @@
Nếu %1$s đang chạy trên một máy khác hoặc cổng khác, bạn sẽ cần đặt địa chỉ IP và cổng tại đây trong %2$s.Phần cấu hìnhTrang ván đấu cần được mở trên trình duyệt của bạn. Nó không cần phải hiển thị, bạn có thể thu nhỏ nó hoặc đặt nó cạnh trang ván đấu Lichess, nhưng đừng đóng nó nếu không bàn cờ sẽ ngừng hoạt động.
- Bàn cờ sẽ tự động kết nối với bất kỳ ván đấu nào đang diễn ra hoặc bất kỳ trò chơi mới nào bắt đầu. Khả năng chọn ván đấu để chơi sắp ra mắt.
- Kiểm soát thời gian cho các ván cờ không tính Elo: Chỉ Cờ Nhanh, Cờ Chậm và Cờ qua thư.
+ Bàn cờ sẽ tự động kết nối với bất kỳ ván đấu nào đang diễn ra hoặc bất kỳ ván cờ mới nào bắt đầu. Khả năng chọn ván đấu để chơi sắp ra mắt.
+ Kiểm soát thời gian cho các ván cờ không xếp hạng: chỉ Cờ nhanh, Cờ chậm và Cờ qua thư.Kiểm soát thời gian cho các ván cờ có tính Elo: Cờ Chậm, Cờ qua thư và một số thể loại Cờ Nhanh bao gồm 15+10 và 20+0Khi đã sẵn sàng, hãy thiết lập bàn cờ của bạn rồi nhấp vào %s.Nếu không phát hiện được nước cờ
diff --git a/translation/dest/emails/da-DK.xml b/translation/dest/emails/da-DK.xml
index bd8f55692111e..260236677cbf6 100644
--- a/translation/dest/emails/da-DK.xml
+++ b/translation/dest/emails/da-DK.xml
@@ -5,7 +5,7 @@
Hvis du ikke har registreret dig hos Lichess, kan du roligt ignorere denne meddelelse.Nulstil din adgangskode til lichess.org, %sVi har modtaget en anmodning om at nulstille adgangskoden til din konto.
- Hvis du har foretaget denne anmodning, skal du klikke på linket nedenfor. Hvis ikke, kan du ignorere denne email.
+ Hvis du har foretaget denne anmodning, skal du klikke på linket nedenfor. Hvis ikke, kan du ignorere denne e-mail.Bekræft ny e-mailadresse, %sDu har anmodet om at ændre din e-mailadresse.For at bekræfte at du har adgang til denne e-mail, skal du klikke på linket nedenfor:
@@ -16,7 +16,7 @@ Her er din profilside: %1$s. Du kan tilpasse den på %2$s.
God fornøjelse, og må din brikker altid finde vej til din modstanders konge!
Log ind på lichess.org, %s
- (Fungerer det ikke at klikke? Prøv at kopiere linket ind i din browser!)
+ (Fungerer det ikke at klikke? Prøv at indsætte linket i din browser!)Dette er en servicemail vedrørende din brug af %s.Vil du kontakte os, bedes du bruge %s.
diff --git a/translation/dest/emails/so-SO.xml b/translation/dest/emails/so-SO.xml
index f7b8454898cf6..81eb984006617 100644
--- a/translation/dest/emails/so-SO.xml
+++ b/translation/dest/emails/so-SO.xml
@@ -1,22 +1,22 @@
- Hubi boggaaga lichess.org, %s
- Riix laynkan hoose si aad u furtid boggaaga Lichess:
+ Hubi akoonkaaga lichess.org, %s
+ Riix laynkan hoose si aad u furtid akoonkaaga Lichess:Haddii aanad iska diwaan-gelin Lichess uma baahnid farriintan.
- Cusboonaysii furaha boggaaga lichess.org, %s
- Wuu na soo gaadhay codsigii cusboonaysiinta furaha boggaagu.
+ Cusboonaysii furahaaga lichess.org, %s
+ Wuu na soo gaadhay codsigii cusboonaysiinta furaha akoonkaagu.Haddii uu codsigani adiga kaa yimid, riix laynkan hoose. Haddii kale, iska dhaaf farriintan.Hubi cinwaankaaga cusub, %sWaxaad codsatay beddelidda cinwaankaaga.Si aad u caddeysid in aad cinwaankan leedahay, fadlan riix laynkan hoose:Ku soo dhawow lichess.org, %s
- Waxaad si guul leh bog uga samaysatay https://lichess.org.
+ Waxaad si guul leh akoon uga samaysatay https://lichess.org.
-Waakan boggaagii kuu gaarka ahaa: %1$s. Halkan ka qurxiso %2$s.
+Waakan boggaagii gaarka ahaa: %1$s. Halkan ka qurxiso %2$s.
-Ciyaar, oo boqorka colka baallaha ka yaac!
+Baashaal, oo boqorka colka baallaha ka yaac!Gal lichess.org, %s
- (Lama riixi karo? Ku day in ad laynka toos ugu qortid internetka!)
- Kani waa cinwaan adeeg oo la xididha isticmaalkaaga %s.
+ (Lama riixi karo? Ku day in ad laynka toos ugu qortid interneedka!)
+ Kani waa cinwaan adeeg oo la xidiidha isticmaalkaaga %s.Si aad noolasoo xidhiidhid, fadlan isticmaal %s.
diff --git a/translation/dest/emails/uk-UA.xml b/translation/dest/emails/uk-UA.xml
index 36f8e01f842a4..63683d9620763 100644
--- a/translation/dest/emails/uk-UA.xml
+++ b/translation/dest/emails/uk-UA.xml
@@ -1,6 +1,6 @@
- Підтвердьте ваш обліковий запис на lichess.org, %s
+ Підтвердьте свій обліковий запис на lichess.org, %sНатисніть на посилання для активації облікового запису Lichess:Якщо ви не реєструвалися на Lichess, просто проігноруйте це повідомлення.%s, скидання вашого паролю lichess.org
diff --git a/translation/dest/faq/ar-SA.xml b/translation/dest/faq/ar-SA.xml
index e25af8ba303be..a4ad22af87521 100644
--- a/translation/dest/faq/ar-SA.xml
+++ b/translation/dest/faq/ar-SA.xml
@@ -54,6 +54,7 @@
وفي حالات نادرة قد يكون من الصعب تحديد ذلك تلقائيًا (سلسلة من التحركات القسرية) وبشكل افتراضي، فإننا نقف دائمًا إلى جانب اللاعب الذي لم ينته وقته.
ضع في اعتبارك أن كش ملك ممكن مع كل من الحصان أو الفيل إذا كان لدى الخصم قطعة أو بيدق يمكنها صد ملكه.
+ دليل الاتحاد الدَّوْليّ للشطرنجدليل \"الاتحاد الدولي للشطرنج\" %sلماذا يمكنك أن يأكل بيدق الاخر عندما يكون قد سلك بالفعل؟ (الأخذ بالتجاوز)إنها نقلة قانونية تُعرف باسم \"الأخذ بالتجاوز\". وتقدم ويكيبيديا مقالة %1$s:
@@ -138,4 +139,28 @@
يرسل Lichess بشكل إختياري إشعارات منبثقة، على سبيل المثال عندما تحصل على رسالة خاصة أو عندما يكون دورك في اللعب.
اضغط على زر القفل بجانب رابط lichess.org في متصفحك.
ثم اختر إما تفعيل أو تعطيل الإشعارات من موقعنا.
+ تمكين التشغيل التلقائي للأصوات؟
+ يمكن لمعظم المتصفحات منع تشغيل الصوت على الصفحات المحملة حديثًا وذل لحماية المستخدمين. تخيل لو كان بإمكان كل موقع ويب أن يفاجئك بإعلان صوتي فور دخولك.
+
+تظهر أيقونة كتم الصوت الحمراء عندما يمنع متصفحك موقعنا من تشغيل الصوت، يزال هذا القيد عادة بمجرد النقر في أي مكان، لكن في بعض متصفحات الأجهزة المحمولة، لا يعد سحب القطعة بمثابة نقرة، لذا يجب عليك النقر على الرقعة لتشغيل الصوت في بداية كل مباراة تلعبها.
+
+نعرض الرمز الأحمر لتنبيهك عند حدوث ذلك، في كثير من الأحيان يمكنك السماح لنا بتشغيل الأصوات عن طريق متصفحك، وفيما يلي تعليمات للقيام بذلك على الإصدارات الحديثة من بعض المتصفحات الشائعة.
+ سطح المكتب
+ 1. اذهب إلى lichess.org
+2. اضغط Ctrl-i في نظامي Linux/Windows أو cmd-i على MacOS
+3. انقر فوق علامة تبويب الأذونات
+4. اختر السماح للصوت والفيديو على lichess.org
+ 1. انتقل إلى lichess.org
+2. انقر على أيقونة القُفْل في شريط العنوان
+3. انقر فوق إعدادات الموقع
+4.اختر السماح بالصوت
+ 1. انتقل إلى lichess.org
+2. انقر على Safari في شريط القوائم
+3. انقر فوق إعدادات lichess.org ...
+4. اختر السماح للاجراءات التلقائي
+ 1. انقر فوق النُّقَط الثلاث في الزاوية اليمنى العلوية
+2. اختر الإعدادات
+3. انقر على ملفات تعريف الارتباط و أذونات الموقع
+4. مرر للأسفل وانقر على Media Autoplay
+5. أضف lichess.org للقائمة المسموح بها
diff --git a/translation/dest/faq/de-DE.xml b/translation/dest/faq/de-DE.xml
index f77b1aa044597..a1724bbd4a6be 100644
--- a/translation/dest/faq/de-DE.xml
+++ b/translation/dest/faq/de-DE.xml
@@ -143,7 +143,7 @@ Klicke auf das Schlosssymbol neben der lichess.org Adresse in der URL-Leiste dei
Wähle dann aus, ob Benachrichtigungen von Lichess erlaubt oder blockiert werden sollen.
Das automatische Abspielen von Tönen erlauben?
- Die meisten Browser können die Tonwiedergabe auf einer neu geladenen Seite verhindern, um die Nutzer zu schützen. Stell dir vor, jede Website könnte dich sofort mit Audio-Werbung bombardieren.
+ Die meisten Browser können die Tonwiedergabe auf einer neu geladenen Seite unterbinden, um die Nutzer nicht zu belästigen. Stell dir vor, jede Website könnte dich sofort mit Audio-Werbung bombardieren.
Das rote Stummschaltungssymbol erscheint, wenn dein Browser verhindert, dass lichess.org einen Ton abspielt. Normalerweise wird diese Einschränkung aufgehoben, sobald du etwas anklickst. Bei einigen mobilen Browsern gilt das Ziehen einer Figur durch Berührung nicht als Klick. In diesem Fall musst du zu Beginn einer Partie auf das Brett tippen, um den Ton zu aktivieren.
diff --git a/translation/dest/faq/gsw-CH.xml b/translation/dest/faq/gsw-CH.xml
index e9c25c3b6f294..6a26772c7b33b 100644
--- a/translation/dest/faq/gsw-CH.xml
+++ b/translation/dest/faq/gsw-CH.xml
@@ -102,7 +102,7 @@ Also verlang de Titel nöd.Einzigartigi Trophä\'äDie Trophäe isch einzigartig i de Gschicht vo Lichess, niemert usser em %1$s wird si jemals ha.Er hät si übercho, will er 100%% vu de Partie vumene %s mit Berserk gschpillt- und gunne hät.
- e schtündlichi Bullet Arena
+ es schtündlichs Bullet-TurnierDe Schpiller \"ZugAddict\" hät mal - ime Stream - über 2 Schtund lang versuecht de Computer, uf Schtufe 8, im Bullet 1+0 z\'besiege - aber ohni Erfolg. De \"Thibault\" hät ihm dänn e einzigartigi Trophäe versproche, falls er das doch mal schaffe würd. Ei Schtund spöter hät de \"ZugAddicr\" de \"Stockfish\" besiegt und das Verschpräche isch ighalte worde.Lichess-WertigeWas für es Wertigs-System benutzt Lichess?
@@ -135,7 +135,7 @@ Es isch am beschte, Wertige als \"relativi\" Wert z\'betrachte (im Gägesatz zu
Aktivier de Zen-Modus i de %1$s oder truck %2$s während de Partie.Azeige-IschtelligeIch han es Schpiel wäge Lag / Verbindigsunderbruch verlore. Chann ich mini Wertigspünkt zrugg verlange?
- Leider chönnd mir kei Wertigspünkt zrugg geh, wo wäge Lag oder Underbrüch verlore worde sind. Unabhängig devoo, ob das Problem vu dinere oder vu eusere Site - was sehr sälte passiert - verursacht worde isch. Pass also au uf, wänn Mäldige für en Lichess Neustart chömmed, will bim Abefahre vum Syschtem werded alli Partie eifach abbroche und mer chann so unfair verlüre.
+ Leider chönd mir kei Wertigspünkt zrugg geh, wo wäge Lag oder eme Underbruch verlore worde sind. Unabhängig devo, ob das Problem vu dinere oder vu eusere Site - was sehr sälte passiert - verursacht worde isch. Pass also au uf, wänn Mäldige für en Neuschtart chömmed, will bim Abefahre vum Syschtem werded alli Partie abbroche und mer chann es Schpiel unfair verlüre.Wiä chan ich...Benochrichtigungs-Popups aktiviärä oder deaktiviärä?Site Informations-Popup azeige
@@ -151,19 +151,19 @@ Das roti Stummschaltigssymbol erschint, wänn de Browser verhinderet, dass liche
Das roti Symbol macht dich druf ufmerksam, dass das passiert. Oft chann mer lichess.org usdrücklich erlaube, Tön ab z\'schpille. Da findsch du e Aleitig, wie mer das - ide neuschte Versione, vu einige gängige Browser - ischtelle chann.Desktop
- 1. Gang zu Lichess.org
+ 1. Gang zu Lichess.org
2. Taschte Ctrl-i bi Linux / Windos oder cmd-i bi MacOS
3. Klick de Tab \"Permissions\"
4. Erlaub \"Audio and Video\" uf Lichess.org
- 1. Gang zu Lichess.org
+ 1. Gang zu Lichess.org
2. Klick i de Adrässzile ufs Schloss-Icon
3. Klick d\'Site \"Settings\"
4. Erlaub \"Sound\"
- 1. Gang zu Lichess.org
+ 1. Gang zu Lichess.org
2. Klick ide Menuezile uf \"Safari\"
3. Klick \"Settings für lichess.org\"...
4. Erlaub \"All Autoplay\"
- 1. Klick uf die 3 Pünkt ganz obe, im rächte Egge
+ 1. Klick uf die 3 Pünkt ganz obe, im rächte Egge
2. Klick \"Settings\"
3. Klick \"Cookies and Site Permissions\"
4. Scroll abwärts und klick \"Media autoplay\"
diff --git a/translation/dest/faq/ko-KR.xml b/translation/dest/faq/ko-KR.xml
index 354bcf55a968c..e9727759ad88d 100644
--- a/translation/dest/faq/ko-KR.xml
+++ b/translation/dest/faq/ko-KR.xml
@@ -143,4 +143,10 @@ LM 타이틀에 대해 문의하지 마세요.
브라우저의 URL 표시 줄에서 lichess.org 주소 옆에 있는 자물쇠 아이콘을 클릭합니다.
그런 다음 Lichess의 알림을 허용할지 차단할지 선택합니다.
+ 소리 자동 재생을 활성화하고 싶어요.
+ 대부분의 브라우저가 사용자를 보호하기 위해 새롭게 로드된 페이지가 소리를 재생하는 것을 방지할 수 있습니다. 모든 웹사이트가 들어가자마자 음성 광고를 쏟아붓는다고 상상해 보세요.
+
+브라우저에서 lichess.org가 소리를 재생하는 것을 방지했을 때 붉은 음소거 아이콘이 나타납니다. 보통 무언가를 클릭하면 이 제한은 없어집니다. 일부 모바일 브라우저에서는, 기물을 드래그하는 것은 클릭으로 간주되지 않습니다. 이 경우 소리 재생을 허용하려면 매 게임이 시작할 때마다 체스판을 탭해야 합니다.
+
+리체스는 이러한 경우가 발생했을 때 알려주기 위하여 붉은색 아이콘을 보여줍니다. 당신은 lichess.org가 소리를 재생하도록 명시적으로 허용할 수 있습니다. 다음은 최근 버전의 몇몇 인기 브라우저에서 소리 재생을 허용하는 방법입니다.
diff --git a/translation/dest/lag/ko-KR.xml b/translation/dest/lag/ko-KR.xml
index 2164156517b95..6f3738f5e86b7 100644
--- a/translation/dest/lag/ko-KR.xml
+++ b/translation/dest/lag/ko-KR.xml
@@ -9,7 +9,7 @@
리체스 서버 지연행마를 서버에서 처리하는 시간. 모두에게 동일하고, 서버의 작업량에만 관련이 있습니다. 사람이 많아질수록 커지지만, 리체스 개발자들은 이 수치가 낮도록 최선을 다합니다. 거의 10ms를 넘지 않습니다.리체스와 귀하의 컴퓨터 사이의 네트워크
- 당신의 컴퓨터에서 리체서 서버로 행마를 전송하고, 다시 수신하는 시간. 당신과 리체스(프랑스) 사이의 거리, 당신의 인터넷 품질에 딷라 정해집니다. 리체스 개발자는 당신의 와이파이를 고칠 수 없고 빛이 더 빨리 가도록 할 수도 없습니다.
+ 당신의 컴퓨터에서 리체스 서버로 행마를 전송하고, 다시 수신하는 시간. 당신과 리체스(프랑스) 사이의 거리, 당신의 인터넷 품질에 따라 정해집니다. 리체스 개발자는 당신의 와이파이를 고칠 수 없고 빛이 더 빨리 가도록 할 수도 없습니다.상단 바의 사용자명을 클릭해서 언제든 이 두 수치를 확인할 수 있습니다.렉 보상리체스는 렉에 대한 보상을 합니다. 지속되는 렉과 갑자기 일어나는 렉에 대해 최대한 공평하게 보상을 하고 있습니다. 그렇기 때문에 상대보다 네트워크에 렉이 심해도 크게 불리해지지 않습니다.
diff --git a/translation/dest/lag/sl-SI.xml b/translation/dest/lag/sl-SI.xml
index cf0e5d66ec9c8..db715ec91ffb0 100644
--- a/translation/dest/lag/sl-SI.xml
+++ b/translation/dest/lag/sl-SI.xml
@@ -5,12 +5,12 @@
Ne. In vaše omrežje je v redu.Ne. In vaše omrežje je slabo.Da. Kmalu bo rešeno!
- Pa še doljše pojasnilo. Zakasnitev igre je sestavljena iz dveh nepovezanih vrednosti (nižje je boljše):
+ Pa še daljše pojasnilo. Zakasnitev igre je sestavljena iz dveh nepovezanih vrednosti (nižje je boljše):Zakasnitev strežnika LichessČas, potreben za obdelavo poteze na strežniku. Enak je za vse in je odvisen samo od obremenitve strežnika. Večje število igralcev ga podaljšuje, a Lichessovi razvijalci se trudijo, da je kratek. Redko presega 10 ms.Omrežje med Lichessom in vamiČas, ki je potreben, da vaša poteza prispe iz vašega računalnika na Lichessov strežnik in da prejmete njegov odgovor. Odvisen je od vaše oddaljenosti do Lichesovega strežnika (v Franciji) in hitrosti vaše internetne povezave. Lichessovi razvijalci ne morejo popraviti vašega wifija ali pospešiti vaše internetne povezave.Obe vrednosti lahko kadar koli najdete s klikom na uporabniško ime v zgornji vrstici.Nadomestilo zaostajanja
- Lichess kompenzira zaostajanje omrežja. To vključuje trajne zamike in občasne zaostanke. Zaenkrat obstajajo omejitve in hevristike, ki temeljijo na nadzoru časa in kompenziranem zaostajanju, tako da bi moral biti rezultat razumljiv za oba igralca. Posledično zaradi večjega zaostajanja omrežja od nasprotnika ni ovira!
+ Lichess kompenzira časovni zamik omrežja. To vključuje trajne zamike in občasne večje zaostanke. Zaenkrat obstajajo omejitve in metode, ki temeljijo na nadzoru časa in kompenziranem zaostajanju, tako da bi morala biti končna izkušnja sprejemljiva za oba igralca. Zaradi daljšega časovnega zamika omrežja od nasprotnikovega zato niste prikrajšani!
diff --git a/translation/dest/learn/ar-SA.xml b/translation/dest/learn/ar-SA.xml
index 1c1de9da2dd35..030b97b5893c3 100644
--- a/translation/dest/learn/ar-SA.xml
+++ b/translation/dest/learn/ar-SA.xml
@@ -6,161 +6,162 @@
التقدم: %sإعادة ضبط تقدميسيتم محو كل سجلات تقييمك!
- اِلعب!
+ اِلعب!قطع الشطرنجالرختتحرك في خطوط مستقيمة
- الرخ قطعة قوية. أ أنت مستعد لقيادتها؟
+ الرخ قطعة قوية، هل أنت مستعد لقيادتها؟انقر على الرخ لتحضرها إلى النجمة!
- إجمع كل النجوم!
+ اجمع كل النجوم!كلما قلت النقلات التي تقوم بها ، كلما كسبت المزيد من النقاط!استخدم الرخين لتسريع الأمور!
- تهانينا! لقد أتقنت الرخ بنجاح.
+ تهانينا! لقد أتقنت استعمال الرخ.الفيل
- إنه يتحرك وتريًا
+ يتحرك بشكل قطريالتالي سنتعلم كيفية المناورة بالفيل!
- فيل واحد للمربعات الفاتحة، وفيل للمربعات الداكنة. ستحتاج كلاهما!
- تهانينا! يمكنك قيادة الفيل.
- الملكة
- الملكة = رخ + فيل
- أقوى قطعة شطرنج تتقدم. صاحبة الجلالة الملكة!
- تهانينا! الملكات لدى لا تخفي أسرار عنك.
+ فيل للمربعات الفاتحة، والآخر للمربعات الداكنة. ستحتاج كليهما!
+ تهانينا! لقد تعلمت قيادة الفيل.
+ الوزير
+ الوزير = رخ + فيل
+ اسمحوا لي أن أقدم قطعة الشطرنج الأقوى. إنه الوزير!
+ تهانينا! الوزراء لا يخفون أسرارا عنك.الملكأهم قطعةأنت الملك. إذا سقطت في المعركة، خسرت المباراة.الملك بطيء.
- أخر واحد!
+ آخر واحدة!يمكنك الآن قيادة القائد!الحصان
- أنه يتحرك بشكل L
- هنا تحدي لك. الحصان.. قطعة صعبة.
+ يتحرك بشكل حرف L
+ هذا تحد لك. الحصان قطعة ماكرة.للأحصنة طريقتها المميزة للقفز بالأرجاء!
- الأحصنة يمكنها القفز فوق العوائق!
-الهروب وإخضاع النجوم!
- تهانينا! لقد أتقنت الحصان.
+ يمكن للأحصنة القفز فوق العوائق!
+اقفز ثم اجمع كل النجوم!
+ تهانينا! لقد روضت الحصان.البيدقيتحرك إلى الأمام فقط
- البيادق ضعيفة، إلا أنها مليئة بالكثير من الإمكانيات.
- البيادق تتحرك مربع واحد فقط. ولكن عندما تصل إلى الجانب الآخر من الرقعة، فإنها تصبح قطعة أقوى!
- معظم الوقت الترقية إلى الملكة تكون الأفضل.
-ولكن في بعض الأحيان الحصان يمكن أن يأتي بالمساعدة!
+ البيادق ضعيفة، إلا أنها تملك الكثير من القدرات.
+ البيادق تتحرك لمربع واحد فقط، ولكن عندما تصل إلى الجانب الآخر من الرقعة، فإنها تصبح قطعة أقوى!
+ في معظم الحالات الترقية إلى وزير هي الأفضل.
+ولكن في بعض الأحيان يمكن أن يكون الحصان أفضل!البيادق تتحرك إلى الأمام، ولكن تأسر وتريًا!التقط، ثم رقي!استخدم جميع البيادق!
لا حاجة للترقية.
- البيدق على الصف الثاني يمكن أن يتحرك مربعان مرة واحدة!
+ البيدق على الصف الثاني يمكن أن يتحرك لمربعين في نقلة واحدة!إجمع كل النجوم!
لا حاجة للترقية.
- تهانينا! البيادق لا تخفي أسرار عنك.
+ تهانينا! لقد أصبحت تعرف كل شيء عن البيادق.ترقية البيدق
- بلغ بيدقك نهاية الرقعة!
- يرقى الآن إلى قطعة أقوى.
- حدد القطعة التي تريدها!
+ وصل بيدقك إلى الصف الأخير!
+ يمكنك ترقيته الآن إلى قطعة أقوى.
+ حدد القطعة التي تريد الترقية إليها!أساسياتالأسر
- إلتقط قطع المنافس
- حدد قطع المنافس الغير محمية، والتقطها!
- إلتقط القطع السوداء!
- التقط القطع السوداء! ولا تفقد قطعك.
- تهانينا! تعرف كيفية القتال بقطع الشطرنج!
+ ألتقط قطع خصمك
+ حدد قطع خصمك غير المحمية، والتقطها!
+ ألتقط القطع السوداء!
+ التقط القطع السوداء! دون أن تفقد قطعك.
+ تهانينا! لقد تعلمتَ كيف تأسر قطع خصمك!الحماية
- حافظ على قطعك آمنة
- تحديد قطعك التي يهاجمها المنافس، ودافع عنها!
- تهانينا! القطعة التي لا تفقدها هي قطعة تربحها!
- أنت تحت الهجوم! إهرب من التهديد!
- لا مفر، ولكن يمكنك الدفاع!
- لا تسمح لهم بأخذ أي قطعة غير محمية!
+ ابقِ قطعك آمنة
+ حدد قطعك التي هاجمها خصمك، ودافع عنها!
+ تهانينا! القطعة التي لم تفقدها هي قطعة ربحتها!
+ الخَصْم يهاجمك! أهرب من التهديد!
+ لا مهرب لقطعك، ولكن يمكنك الدفاع عنها!
+ أمنع خصمك من أخذ أي قطعة غير محمية!الاشتباك
- الإلتقاط والدفاع عن القطع
- المحارب الجيد يعرف الهجوم والدفاع على حد سواء!
- تهانينا! تعرف كيفية القتال بقطع الشطرنج!
- كش في واحد
- الهجوم على ملك الخصم
+ ألتقط قطع خصمك ودافع عن قطعك
+ المحارب الجيد يجيد الهجوم والدفاع!
+ تهانينا! تعلمت كيف تقاتل بقطع الشطرنج!
+ كش ملك خصمك بنقلة واحدة
+ هاجم ملك خصمكلكش خصمك، هاجم ملكه. فتجبره على الدفاع عنه!استهدف الملك المنافس في نقلة واحدة!تهانينا! لقد قمت بكش خصمك، واجبرته على الدفاع عن ملكه!البعد عن الكشدافع عن ملككأنت في كش! يجب عليك الهروب أو إعاقة الهجوم.
- الهروب بالملك!
+ اهرب بالملك!الملك لا يمكنه الهروب، ولكن يمكنك إعاقة الهجوم!يمكنك الخروج من الكش بأخذ القطعة المهاجمة.هذا الحصان يعطي كش متجاوزًا دفاعاتك!
- الهروب بالملك أو إعاقة الهجوم!
- تهانينا! لا يمكن ابدأ أن يؤخذ ملكك، تأكد من أنك يمكنك الدفاع ضد الكش!
- مات في واحد
- إهزم ملك المنافس
- يمكنك الفوز عندما لا يمكن لمنافسك الدفاع ضد الكش.
- هاجم ملك منافسك بطريقة لا يمكنه الدفاع عنه!
- تهانينا! هذا هو كيف يمكنك الفوز بمبارة شطرنج!
+ اهرب بالملك أو أعق الهجوم!
+ تهانينا! لا يمكن ابدأ أن يؤخذ ملكك، تأكد دائما أنك تستطيع الدفاع ضد الكش!
+ كش مات بنقلة واحدة
+ اقتل ملك خصمك
+ ستفوز إذا لم يتمكن خصمك من الدفاع عن ملكه من الكش.
+ هاجم ملك خصمك بحيث لا يستطيع الدفاع عنه!
+ تهانينا! هكذا تُكسب مباراة الشطرنج!متوسطرص الرقعةكيف تبدأ المباراةالجيشان يواجه بعضهما البعض، مستعدان للمعركة.
- وهذا هو الموقف الأولي لكل دور شطرنج! قم بأي نقلة للاستمرار.
- أولاً ضع الرخان! في الزوايا.
- ثم ضع الحصانان! إلى جانب الرخان.
- ضع الفيلان! إلى جانب الحصانان.
- مكان الملكة! في نفس لونها.
- مكان الملك! بجوار الملكة.
- البيادق تشكل الخط الأمامي. قم بأي نقلة للإستمرار.
- تهانينا! يمكنك معرفة كيفية إعداد رقعة الشطرنج.
- تبييت
- نقلة الملك الخاصة
- إحضار ملكك إلى بر الأمان، ونشر رخك للهجوم!
- حرك ملكك مربعان لتبيت في جناح الملك!
- حرك ملكك مربعان لتبيت في جناح الملكة!
- الحصان في الطريق! إنقله ، ثم بيت في جناح-الملك.
- بيت في جناح-الملك! تحتاج إلى نقل القطع بعيدًا أولاً.
- بيت في جناح-الملكة! تحتاج إلى نقل القطع بعيدًا أولاً.
- لا يمكنك التبييت إذا كان الملك قد تحرك بالفعل أو الرخ قد تحركت بالفعل.
+ هذا هو الموقف الابتدائي لكل مباراة شطرنج! العب أي نقلة للاستمرار.
+ أولاً ضع الرخين في الزوايا.
+ ثم ضع الحصانين! إلى جانب الرخين.
+ ضع الفيلين! إلى جانب الحصانين.
+ مكان الوزير في مربع من لونه نفسه.
+ مكان الملك بجوار وزيره.
+ تصطف البيادق أمام القطع.
+العب أي نقلة للمتابعة.
+ تهانينا! تعلمت كيف ترتب القطع على رقعة الشطرنج.
+ التبييت
+ نقلة الملك المميزة
+ أمن ملكك، وأحضر رخك إلى وَسَط الرقعة!
+ حرك ملكك مربعين للتبييت في جناح الملك!
+ حرك ملكك مربعين للتبييت في جناح الوزير!
+ الحصان في الطريق! أبعده، ثم بيت في جناح-الملك.
+ بيت في جناح-الملك! تحتاج إلى تحريك القطع بعيدًا أولاً.
+ بيت في جناح-الوزير! تحتاج إلى نقل القطع بعيدًا أولاً.
+ تفقد الحق في التبييت إذا تحرك ملكك أو تحركت الرخ.لا يمكنك التبييت إذا هوجم الملك في الطريق. إغلق الكش ثم بيت!جد طريقة للتبييت في جناح-الملك!جد طريقة للتبييت في جناح-الملكة!
- تهانينا! يجب أن تبيت غالبًا دائمًا في أي مباراة.
- أسر بالمرور
- نقلة البيدق الخاصة
+ تهانينا! يفضل أن تبيت في كل مباراة.
+ الأخذ بالتجاوز
+ نقلة البيدق المميزةعند تحرك بيدق الخصم مربعين، يمكنك أن تأخذه كما لو أنه تحرك مربع واحد فقط.الأسود للتو حرك البيدق مربعين! التقطه بالمرور.الأخذ بالمرور يكون بعد تحريك بيدق المنافس مباشرة.
- يعمل الأخذ بالمرور فقط إذا كان البيدق على الصف الخامسة.
- إلتقط جميع البيادق بالمرور!
- تهانينا! يمكنك الآن أن تأسر بالمرور.
- الملك مخنوق
- المباراة تعادل
- عندما يكون اللاعب ليس في كش وليس لديه نقلة قانونية، يكون مات مخنوق. المباراة تعادل: لا فائز، ولا خاسر.
- لتميت الأسود خنقًا:
--الأسود لا يمكنه التتحرك لأي مكان
+ يمكن تنفيذ طريقة بالتجاوز فقط
+إذا كان البيدق في الصف الخامس.
+ التقط جميع البيادق بالمرور!
+ تهانينا! تعلمت كيف تأسر بالمرور.
+ الملك المخنوق
+ تنتهي المباراة بالتعادل
+ عندما لا يكون ملك اللاعب في كش، ولكنه لا يملك أي نقلة قانونية، يكون الملك مخنوقا، وتنتهي المباراة بالتعادل: لا فائز، ولا خاسر.
+ أخنق ملك الأسود حيث:
+-لا يملك أي حركة قانونية
-لا يوجد كش.
- تهانينا! الموت خنقًا أفضل من الكش مات!
+ تهانينا! أن يخنق ملكك أفضل من أن يموت!متقدمقيمة القطعةتقييم قوة قطعةالقطع ذات القدرة العالية على الحركة لها قيمة أعلى!
- الملكة = 9
+ الوزير = 9
الرخ= 5
الفيل = 3
الحصان= 3
البيدق = 1
الملك لا يقدر بثمن! فقدانه يعني خسارة المباراة.
- خذ القطعة ذات أعلى قيمة! الملكة > الفيل
- كل القطعة التي لها أعلى قيمة!
-لا تستبدل
-قطعة ذات قيمة عليا بأخرى أدنى قيمة.
+ خذ القطعة التي لها قيمة أعلى! الوزير > الفيل
+ خذ القطعة ذات القيمة الأعلى!
+لا تستبدل قطعة ذات قيمة عالية بأخرى أدنى قيمة.كل القطعة
التي لها أعلى قيمة!
تأكد من أن نقلتك قانونية!خذ القطعة ذات أعلى قيمة!
- تهانينا! أنت تعرف القيم المادية!
-الملكة =9
+ تهانينا! أنت تعرف قيم القطع!
+الوزير =9
الرخ = 5
الفيل= 3
الحصان = 3
البيدق = 1
- كش في اثنين
- نقلتين لإعطاء كش
- جد التكوينة الصحيحة من نقلتين لإعطاء كش لملك المنافس!
- استهدف الملك المنافس في نقلتين!
+ كش بنقلتين
+ نقلتان لإعطاء كش
+ جِدّ النقلتين الصحيحتين لإعطاء كش لملك المنافس!
+ هاجم الملك المنافس بنقلتين!تهانينا! لقد قمت بكش خصمك، واجبرته على الدفاع عن ملكه!ما التالي؟أنت تعرف كيف تلعب الشطرنج، تهانينا! هل تريد أن تصبح لاعبًا أقوى؟
@@ -190,7 +191,7 @@
على الطريق الصحيح!المرحلة %s تمتالتالي
- موالي: %s
+ التالي: %sالعودة إلى القائمةلقد خسرت اللغز!إعادة المحاولة
diff --git a/translation/dest/learn/da-DK.xml b/translation/dest/learn/da-DK.xml
index 58abeffb16ef1..df612f042cda9 100644
--- a/translation/dest/learn/da-DK.xml
+++ b/translation/dest/learn/da-DK.xml
@@ -6,7 +6,7 @@
Status: %sNulstil min statusDu vil miste alle dine fremskridt!
- spil!
+ spil!SkakbrikkerTårnetDet bevæger sig i lige linjer
@@ -175,7 +175,7 @@ Bonde = 1Få en gratis Lichess kontoØvelserLær almindelige skakstillinger
- Opgaver
+ TaktikopgaverTræn dine taktiske evnerVideoerSe lærerige skakvideoer
@@ -199,6 +199,6 @@ Bonde = 1NæsteNæste: %sTilbage til menu
- Øvelse mislykkedes!
+ Opgave mislykkedes!Prøv igen
diff --git a/translation/dest/learn/so-SO.xml b/translation/dest/learn/so-SO.xml
index 10f293fd79085..bc5d10bec5ee7 100644
--- a/translation/dest/learn/so-SO.xml
+++ b/translation/dest/learn/so-SO.xml
@@ -1,6 +1,16 @@
ciyaar!
+ Qalcadda
+ Waxay u dhaqaaqdaa toos afarta jiho
+ Qalcaddu waa mid awood badan. Diyaar ma u tahay in ad masmusho?
+ Maroodiga
+ Wuxu u socdaa janjeedh
+ Abaanduulaha
+ Abaanduule = qalcad + maroodi
+ Boqorka
+ Faraska
+ AskarigaHalxiraalahaCajiib!Wanaag!
diff --git a/translation/dest/oauthScope/ar-SA.xml b/translation/dest/oauthScope/ar-SA.xml
index 260c518fc70d3..5a1fa5985675b 100644
--- a/translation/dest/oauthScope/ar-SA.xml
+++ b/translation/dest/oauthScope/ar-SA.xml
@@ -8,27 +8,47 @@
ما الذي يمكن أن تفعله الtoken بالنيابة عنك:الرمز المميز سيمنح الوصول إلى حسابك. لا تقم بمشاركته مع أي شخص!احفظ أو اكتب كلمة المرور الخاصة بتطبيق. لن تراها مجددا!
- قراءة التفضيلات
- اكتب مرجع
- اقرا عنوان البريد
- قراءة التحديات الواردة
- إرسال وقبول ورفض التحديات
+ قراءة التفضيلات
+ اكتب مرجع
+ اقرا عنوان البريد
+ قراءة التحديات الواردة
+ إرسال وقبول ورفض التحدياتإنشاء العديد من الألعاب مرة واحدة للاعبين الآخرين
- قراءة الدراسات والبث الخاص
- إنشاء، تحديث، حذف الدراسات والبث
- إنشاء وتحديث والالتحاق بالبطولات
- إنشاء سباقات الألغاز والانضمام إليها
- اقرا نشاط الالغاز
- قراءة معلومات الفريق الخاص
- انضم او غادر الفرق
+ قراءة الدراسات والبث الخاص
+ إنشاء، تحديث، حذف الدراسات والبث
+ إنشاء وتحديث والالتحاق بالبطولات
+ إنشاء سباقات الألغاز والانضمام إليها
+ اقرا نشاط الالغاز
+ قراءة معلومات الفريق الخاص
+ انضم او غادر الفرقإدارة الفرق التي تقودها: إرسال الرسائل الشخصية، طرد الأعضاء
- قراءة اللاعبين المتابعين
- متابعة/ الغاء متابعة اللاعبين الاخرين
- ارسال رسالة خاصة للاعبين الاخرين
- العب مبارايات ضد بوت API
- العب مبارايات ضد بوت api
+ قراءة اللاعبين المتابعين
+ متابعة/ الغاء متابعة اللاعبين الاخرين
+ ارسال رسالة خاصة للاعبين الاخرين
+ العب مبارايات ضد بوت API
+ العب مبارايات ضد بوت apiعرض و استخدام المحركات الخارجيةإنشاء وتحديث محركات خارجية
- إنشاء جلسات موقع مصادقة (منح الوصول الكامل!)
- استخدام أدوات المشرف (ضمن حدود الأذونات الخاصة بك)
+ إنشاء جلسات موقع مصادقة (منح الوصول الكامل!)
+ استخدام أدوات المشرف (ضمن حدود الأذونات الخاصة بك)
+ رمز وصول API الشخصي
+ يمكنك إنشاء طلبات OAuth دون المرور خلال%s.
+ التحقق من رمز التفويض
+ بدلاً من ذلك، يمكنك استخدام %s في طلبات API.
+ إنشاء رمز وصول شخصي
+ استخدم هذه الرموز بعناية! مثل كلمات المرور، مِيزة استخدام هذه الرموز بدلا من كلمة المرور العادية هي سهولة إلغاءها، وإنشاء الكثير منها.
+ انظر %1$s و%2$s.
+ مثال على تطبيق للرمز الشخصي
+ دليلُ واجهة برمجة التطبيقات
+ رمز الوصول الجديد
+ رموز الوصول إلى API
+ أنشئت %s
+ آخر استخدام %s
+ لقد لعبتَ المباريات فعلًا!
+ ملاحظة لانتباه المطورين فقط:
+ من الممكن ملء هذا الاستمارة مسبقاً بتغيير معلمات الاستفسار الخاصة بالرابط.
+ على سبيل المثال:%s
+ حدد النطاقين %1$s و%2$s، ثم عين الرمز المميز.
+ يمكن العثور على رموز النطاق في كود HTML الخاص بالنموذج.
+ إن إعطاء عناوين URL المحددة مسبقًا لمستخدميك سيساعدهم في الحصول على نطاق الرمز المميز الصحيح.
diff --git a/translation/dest/oauthScope/be-BY.xml b/translation/dest/oauthScope/be-BY.xml
index 610a3e1cc1a95..a2c843d12001f 100644
--- a/translation/dest/oauthScope/be-BY.xml
+++ b/translation/dest/oauthScope/be-BY.xml
@@ -8,10 +8,11 @@
Што токен можа рабіць ад вашага імя:Токен дасць доступ да вашага ўліковага запісу. НЕ дзяліцеся ім ні з кім!Не забудзьцеся скапіраваць свой новы асабісты токен доступу. Вы не зможаце ўбачыць яго зноў!
- Паглядзець налады
- Змяніць налады
- Паглядзець адрас электроннай пошты
- Паглядзець уваходныя выклікі
+ Паглядзець налады
+ Змяніць налады
+ Паглядзець адрас электроннай пошты
+ Паглядзець уваходныя выклікі
+ Стварыць, абнавіць і далучыцца да турніраўПрагледзець і выкарыстаць вашы знешнія рухавікіСтварыць і абнавіць знешнія рухавікі
diff --git a/translation/dest/oauthScope/de-DE.xml b/translation/dest/oauthScope/de-DE.xml
index 0c8f6240df4d3..b37e2778cc15c 100644
--- a/translation/dest/oauthScope/de-DE.xml
+++ b/translation/dest/oauthScope/de-DE.xml
@@ -8,33 +8,33 @@
Was der Zugangsschlüssel in deinem Namen tun kann:Der Zugangsschlüssel wird Zugriff auf dein Konto ermöglichen. Teile ihn NIEMALS!Stelle sicher, dass du deinen persönlichen Zugangsschlüssel jetzt kopierst. Du wirst ihn nicht erneut sehen können!
- Einstellungen lesen
- Einstellungen ändern
- E-Mail-Adresse lesen
- Eingehende Herausforderungen lesen
- Herausforderungen senden, akzeptieren und ablehnen
+ Einstellungen lesen
+ Einstellungen ändern
+ E-Mail-Adresse lesen
+ Eingehende Herausforderungen lesen
+ Herausforderungen senden, akzeptieren und ablehnenErstelle viele Partien gleichzeitig für andere Spieler
- Private Studien und Übertragungen lesen
- Erstelle, aktualisiere und lösche Studien und Übertragungen
- Erstelle, aktualisiere und trete Turnieren bei
- Erstelle und trete Aufgaben-Rennen bei
- Aufgaben-Aktivität lesen
- Private Team-Informationen lesen
- Teams beitreten und verlassen
+ Private Studien und Übertragungen lesen
+ Erstelle, aktualisiere und lösche Studien und Übertragungen
+ Erstelle, aktualisiere und trete Turnieren bei
+ Erstelle und trete Aufgaben-Rennen bei
+ Aufgaben-Aktivität lesen
+ Private Team-Informationen lesen
+ Teams beitreten und verlassenVerwalte von dir geleitete Teams: Sende PMs, entferne Mitglieder
- Gefolgte Spieler lesen
- Anderen Spielern folgen und entfolgen
- Anderen Spielern private Nachrichten senden
- Partien mit der Board-API spielen
- Partien mit der Bot-API spielen
+ Gefolgte Spieler lesen
+ Anderen Spielern folgen und entfolgen
+ Anderen Spielern private Nachrichten senden
+ Partien mit der Board-API spielen
+ Partien mit der Bot-API spielenDeine externen Engines anzeigen und benutzenExterne Engines erstellen und aktualisieren
- Authentifizierte Website-Sitzungen erstellen (gewährt vollen Zugriff!)
- Moderator-Werkzeuge verwenden (innerhalb der Grenzen deiner Berechtigung)
+ Authentifizierte Website-Sitzungen erstellen (gewährt vollen Zugriff!)
+ Moderator-Werkzeuge verwenden (innerhalb der Grenzen deiner Berechtigung)Persönlicher API-ZugangsschlüsselDu kannst OAuth-Anfragen erstellen, ohne den %s zu durchlaufen.
- Autorisierungs-Code Prozess
- Stattdessen, %s, den du direkt in deinen API-Anfragen benutzen kannst.
+ Autorisierungs-Code-Prozess
+ Stattdessen %s, den du direkt in deinen API-Anfragen benutzen kannst.generiere einen persönlichen Zugangs-SchlüsselHüte diese Schlüssel gewissenhaft! Sie sind wie Passwörter. Der Vorteil bei der Verwendung von Schlüsseln anstelle von Passwörtern in Skripten ist, dass Schlüssel widerrufen werden können und du viele von ihnen generieren kannst.Hier ist ein %1$s und die %2$s.
@@ -43,12 +43,12 @@
Neuer Zugangs-SchlüsselAPI-Zugangs-SchlüsselErstellt: %s
- Zuletzt benutzt: %s
+ Zuletzt benutzt: %sDu hast bereits Partien gespielt!Hinweis nur für Entwickler:
- Es ist möglich, dieses Formular vorauszufüllen, indem du die Abfrageparameter der URL bearbeitest.
+ Es ist möglich, dieses Formular im vorab auszufüllen, indem du die Abfrageparameter der URL bearbeitest.Zum Beispiel: %s%1$s und %2$s wählt die Bereiche aus und setzt die Schlüsselbeschreibung fest.
- Die Bereichs-Codes können im HTML-Cide des Formulars gefunden werden.
- Deinen Nutzern vorausgefüllte URLs bereit zu stellen, wird ihnen helfen, die passenden Zugangs-Schlüssel zu erhalten.
+ Die Bereichs-Codes können im HTML-Code des Formulars gefunden werden.
+ Die Bereitstellung von im vorab ausgefüllter URLs wird deinen Nutzern helfen, die passenden Zugangs-Schlüssel zu erhalten.
diff --git a/translation/dest/oauthScope/vi-VN.xml b/translation/dest/oauthScope/vi-VN.xml
index ee4d2f1ee5b5e..46533b55fafd2 100644
--- a/translation/dest/oauthScope/vi-VN.xml
+++ b/translation/dest/oauthScope/vi-VN.xml
@@ -8,47 +8,47 @@
Những gì mà Token có thể làm thay bạn:Token sẽ cấp quyền truy cập vào tài khoản của bạn. KHÔNG chia sẻ nó với bất kỳ ai!Đảm bảo rằng bạn đã sao chép mã truy cập cá nhân mới ngay bây giờ. Bạn sẽ không thể nhìn thấy nó lần nữa!
- Tùy chọn đọc
- Tùy chọn viết
- Đọc địa chỉ email
- Đọc các lời thách đấu được gửi đến
- Gửi, chấp nhận và từ chối thách đấu
+ Tùy chọn đọc
+ Tùy chọn viết
+ Đọc địa chỉ email
+ Đọc các lời thách đấu được gửi đến
+ Gửi, chấp nhận và từ chối thách đấuTạo nhiều ván đấu cùng lúc với nhiều người chơi
- Đọc các bài học riêng và các chương trình phát sóng
- Tạo, cập nhật, xóa các bài học và các chương trình phát sóng
- Tạo, cập nhật và tham gia các giải đấu
- Tạo và tham gia đua câu đố
- Đọc các hoạt động câu đố
- Đọc thông tin riêng của đội
- Tham gia và rời khỏi đội
+ Đọc các bài học riêng và các chương trình phát sóng
+ Tạo, cập nhật, xóa các bài học và các chương trình phát sóng
+ Tạo, cập nhật và tham gia các giải đấu
+ Tạo và tham gia đua câu đố
+ Đọc các hoạt động câu đố
+ Đọc thông tin riêng của đội
+ Tham gia và rời khỏi độiQuản lý các đội bạn đứng đầu: gửi tin nhắn riêng, xóa thành viên
- Đọc người chơi đã theo dõi
- Theo dõi và bỏ theo dõi những người chới khác
- Gửi tin nhắn riêng đến những người chơi khác
- Chơi với bàn cờ API
- Chơi cờ với bot API
+ Đọc người chơi đã theo dõi
+ Theo dõi và bỏ theo dõi những người chới khác
+ Gửi tin nhắn riêng đến những người chơi khác
+ Chơi với bàn cờ API
+ Chơi cờ với bot APIXem và sử dụng động cơ máy tính bên ngoàiTạo và cập nhật các động cơ máy tính
- Tạo các phiên trang web đã được xác thực (cấp toàn quyền truy cập!)
- Sử dụng các công cụ quản trị (nằm trong quyền kiểm soát của bạn)
+ Tạo các phiên trang web đã được xác thực (cấp toàn quyền truy cập!)
+ Sử dụng các công cụ quản trị (nằm trong quyền kiểm soát của bạn)Khóa truy cập API cá nhânBạn có thể thực hiện các yêu cầu OAuth mà không cần thông qua %s.
- luồng mã ủy quyền
+ luồng mã ủy quyềnThay vào đó, %s mà bạn có thể sử dụng trực tiếp trong các yêu cầu API.
- tạo khóa truy cập cá nhân
+ tạo khóa truy cập cá nhânHãy bảo vệ những khóa này một cách cẩn thận! Chúng giống như mật khẩu. Ưu điểm của việc sử dụng mã thông báo thay vì đặt mật khẩu của bạn vào tập lệnh là mã thông báo có thể bị thu hồi và bạn có thể tạo nhiều mã thông báo.Đây là %1$s và %2$s.
- ví dụ về ứng dụng khóa cá nhân
- Tài liệu API
+ ví dụ về ứng dụng khóa cá nhân
+ Tài liệu APIKhoá truy cập mớiKhoá truy cập APIĐã tạo %s
- Lần cuối sử dụng %s
+ Lần cuối sử dụng %sBạn đã từng chơi ván cờ rồi!Lưu ý chỉ dành cho nhà phát triển:Có thể điền trước biểu mẫu này bằng cách điều chỉnh các tham số truy vấn của URL.
- Ví dụ như: %s
- đánh dấu vào phạm vi %1$s và %2$s, đồng thời đặt mô tả khóa.
- Mã phạm vi có thể được tìm thấy trong mã HTML của biểu mẫu.
+ Ví dụ như: %s
+ đánh dấu vào phạm vi %1$s và %2$s, đồng thời đặt mô tả khóa.
+ Mã phạm vi có thể được tìm thấy trong mã HTML của biểu mẫu.Việc cung cấp các URL điền sẵn này cho người dùng của bạn sẽ giúp họ có được phạm vi mã thông báo phù hợp.
diff --git a/translation/dest/onboarding/ar-SA.xml b/translation/dest/onboarding/ar-SA.xml
index 3ea04e700dfa8..d28f7b3a3cb8d 100644
--- a/translation/dest/onboarding/ar-SA.xml
+++ b/translation/dest/onboarding/ar-SA.xml
@@ -1,2 +1,17 @@
-
+
+ مرحبا!
+ مرحبًا بك في lichess.org!
+ هذه هي صفحتك الشخصية.
+ هل يستخدم هذا الحساب طفل؟ قد ترغب في تمكين %s.
+ ماذا الآن؟ فيما يلي بعض الاقتراحات:
+ تعلم قواعد الشطرنج
+ تحسن عن طريق ألغاز الشطرنج التكتيكية.
+ العب ضد الذكاء الصناعي.
+ العب ضد خصوم من جميع أنحاء العالم.
+ تابع اصدقائك على ليتشيس.
+ شارك في بطولات.
+ تعلم من %1$s و%2$s.
+ خصص ليتشيس حسب رغبتك.
+ استكشف الموقع و استمتع :)
+
diff --git a/translation/dest/onboarding/be-BY.xml b/translation/dest/onboarding/be-BY.xml
index 3ea04e700dfa8..452d88dc48ea3 100644
--- a/translation/dest/onboarding/be-BY.xml
+++ b/translation/dest/onboarding/be-BY.xml
@@ -1,2 +1,4 @@
-
+
+ Гуляйце ў турнірах.
+
diff --git a/translation/dest/onboarding/br-FR.xml b/translation/dest/onboarding/br-FR.xml
index 3ea04e700dfa8..ddffb8fd135d9 100644
--- a/translation/dest/onboarding/br-FR.xml
+++ b/translation/dest/onboarding/br-FR.xml
@@ -1,2 +1,7 @@
-
+
+ Donemat!
+ Donemat war lichess.org!
+ Deskiñ reolennoù an echedoù
+ Kemer perzh e tournamantoù.
+
diff --git a/translation/dest/onboarding/ca-ES.xml b/translation/dest/onboarding/ca-ES.xml
index 9ba0cc2d3855f..e327ea6f6fb53 100644
--- a/translation/dest/onboarding/ca-ES.xml
+++ b/translation/dest/onboarding/ca-ES.xml
@@ -3,7 +3,7 @@
Benvingut/da!Benvingut/da a lichess.org!Aquesta és la teva pàgina de perfil.
- Utilitzarà un nen aquest compte? Potser vols habilitar %s.
+ Utilitzarà un nen aquest compte? Potser vols habilitar %s.I ara què? Aquí tens alguns suggeriments:Aprèn les regles dels escacs.Millora amb els problemes de tàctiques d\'escacs.
@@ -11,6 +11,7 @@
Juga contra oponents de tot el món.Segueix als teus amics en Lichess.Juga en tornejos.
+ Apreneu de %1$s i de %2$s.Configura Lichess al teu gust.Explora la web i diverteix-te :)
diff --git a/translation/dest/onboarding/da-DK.xml b/translation/dest/onboarding/da-DK.xml
index 8844da993d5f4..0f5529d08931d 100644
--- a/translation/dest/onboarding/da-DK.xml
+++ b/translation/dest/onboarding/da-DK.xml
@@ -3,7 +3,7 @@
Velkommen!Velkommen til lichess.org!Dette er din profilside.
- Skal et barn bruge denne konto? Det kan være en god idé at aktivere %s.
+ Skal et barn bruge denne konto? Det kan være en god idé at aktivere %s.Hvad nu? Her er et par forslag:Lær reglerne for skakBliv bedre med taktiske skakopgaver.
diff --git a/translation/dest/onboarding/de-DE.xml b/translation/dest/onboarding/de-DE.xml
index b72a2abec7676..467873a18be25 100644
--- a/translation/dest/onboarding/de-DE.xml
+++ b/translation/dest/onboarding/de-DE.xml
@@ -6,7 +6,7 @@
Wird ein Kind dieses Konto verwenden? Vielleicht möchtest du den %s aktivieren.Was nun? Hier sind ein paar Vorschläge:Schachregeln lernen
- Verbessere dein Schach mit Schachtaktik-Rätseln.
+ Verbessere dein Schach mit Taktik-Aufgaben.Spiele gegen die künstliche Intelligenz.Spiele gegen Gegner aus der ganzen Welt.Folge deinen Freunden auf Lichess.
diff --git a/translation/dest/onboarding/eo-UY.xml b/translation/dest/onboarding/eo-UY.xml
index 3ea04e700dfa8..d996e402aacff 100644
--- a/translation/dest/onboarding/eo-UY.xml
+++ b/translation/dest/onboarding/eo-UY.xml
@@ -1,2 +1,17 @@
-
+
+ Bonvenon!
+ Bonvenon al lichess.org!
+ Ĉi tiu estas via profilpaĝon.
+ Ĉu infano uzos ĉi tiun konton? Vi eble volas ebligi %s.
+ Kio nun? Jen kelkaj sugestoj:
+ Lernu ŝakajn regulojn.
+ Pliboniĝu per ŝakaj taktikaj puzloj.
+ Ludu kontraŭ la artefaritan intelekton.
+ Ludu kontraŭ kontraŭulojn de la tuta mondo.
+ Sekvu viajn geamikojn sur Lichess.
+ Ludu en turniroj.
+ Lernu de %1$s kaj %2$s.
+ Agordu Lichess laŭ via plaĉo.
+ Esploru la retpaĝon kaj amuziĝu :)
+
diff --git a/translation/dest/onboarding/he-IL.xml b/translation/dest/onboarding/he-IL.xml
index 9dd7433f51689..d6916e5cfbc04 100644
--- a/translation/dest/onboarding/he-IL.xml
+++ b/translation/dest/onboarding/he-IL.xml
@@ -2,4 +2,16 @@
ברוכים הבאים!ברוכים הבאים ל-lichess.org!
+ זה עמוד הפרופיל שלך.
+ אם החשבון הזה מיועד לילד/ה, מומלץ להפעיל את %s.
+ מה עכשיו? הנה כמה הצעות:
+ למדו את חוקי השחמט
+ השתפרו על ידי פתירת חידות שחמט.
+ שחקו נגד בינה מלאכותית.
+ שחקו נגד יריבים מרחבי העולם.
+ עקבו אחר חבריכם בליצ׳ס.
+ שחקו בטורנירים.
+ למדו מ%1$s ומ%2$s.
+ התאימו את ליצ׳ס להעדפותיכם.
+ שוטטו ברחבי האתר ותהנו :)
diff --git a/translation/dest/onboarding/hi-IN.xml b/translation/dest/onboarding/hi-IN.xml
index 3ea04e700dfa8..1b0e2e7c46a81 100644
--- a/translation/dest/onboarding/hi-IN.xml
+++ b/translation/dest/onboarding/hi-IN.xml
@@ -1,2 +1,14 @@
-
+
+ स्वागत!
+ Lichess.org में आपका स्वागत है!
+ यह आपका प्रोफ़ाइल पृष्ठ है.
+ क्या कोई बच्चा इस खाते का उपयोग करेगा? हो सकता है आप %s को सक्षम करना चाहें।
+ अब क्या? यहां कुछ सुझाव दिए गए हैं:
+ शतरंज के नियम सीखें
+ शतरंज की रणनीति पहेलियों में सुधार करें।
+ आर्टिफिशियल इंटेलिजेंस खेलें।
+ दुनिया भर के विरोधियों से खेलें।
+ Lichess पर अपने दोस्तों का अनुसरण करें।
+ टूर्नामेंट में खेलें।
+
diff --git a/translation/dest/onboarding/it-IT.xml b/translation/dest/onboarding/it-IT.xml
index 3ea04e700dfa8..5daab81b17159 100644
--- a/translation/dest/onboarding/it-IT.xml
+++ b/translation/dest/onboarding/it-IT.xml
@@ -1,2 +1,17 @@
-
+
+ Benvenuto!
+ Benvenuto su lichess.org!
+ Questa è la tua pagina profilo.
+ Questo account sarà usato da un bambino? In tal caso potresti voler attivare %s.
+ E adesso? Ecco alcuni suggerimenti:
+ Impara le regole degli scacchi
+ Migliora con esercizi di tattica degli scacchi.
+ Gioca contro un\'intelligenza artificiale.
+ Gioca contro avversari da tutto il mondo.
+ Segui i tuoi amici su Lichess.
+ Partecipa a tornei.
+ Impara da %1$s e %2$s.
+ Configura Lichess a tuo piacimento.
+ Esplora il sito e divertiti :)
+
diff --git a/translation/dest/onboarding/ja-JP.xml b/translation/dest/onboarding/ja-JP.xml
index 3ea04e700dfa8..2eabf3d0a3c49 100644
--- a/translation/dest/onboarding/ja-JP.xml
+++ b/translation/dest/onboarding/ja-JP.xml
@@ -1,2 +1,16 @@
-
+
+ ようこそ!
+ Lichess.org へようこそ!
+ これはあなたのプロフィールページです。
+ このアカウントを使用するのは子供ですか? %s も使えます。
+ では何をしましょう? たとえば…
+ チェスのルールを覚える。
+ タクティクス問題で腕をきたえる。
+ AI と対戦する。
+ 世界中の人と対戦する。
+ Lichess で友達をフォローする。
+ トーナメントに参加する。
+ Lichess の設定を好みに合わせて変える。
+ あれこれ触ってお楽しみを :)
+
diff --git a/translation/dest/onboarding/lb-LU.xml b/translation/dest/onboarding/lb-LU.xml
index 3ea04e700dfa8..efd2b043940c6 100644
--- a/translation/dest/onboarding/lb-LU.xml
+++ b/translation/dest/onboarding/lb-LU.xml
@@ -1,2 +1,16 @@
-
+
+ Wëllkomm!
+ Wëllkomm op lichess.org!
+ Dëst ass deng Profilsäit.
+ Soll dëse Kont vun emgem Kand benotzt ginn? Villäicht wëlls de de %s aktivéieren.
+ Wat elo? Hei sinn e puer Virschléi:
+ D\'Schachreegele léieren
+ Gëff besser am Schach, andeems de Taktik-Aufgabe léis.
+ Spill géint d\'kënschtlech Intelligenz.
+ Spill géint Géigner aus der ganzer Welt.
+ Spill op Turnéieren.
+ Léier vu(n) %1$s a(n) %2$s.
+ Konfiguréier Lichess esou, wéi et dir gefält.
+ Exploréier d\'Säit an ameséier dech dobäi :)
+
diff --git a/translation/dest/onboarding/mr-IN.xml b/translation/dest/onboarding/mr-IN.xml
index 3ea04e700dfa8..8188569b5969c 100644
--- a/translation/dest/onboarding/mr-IN.xml
+++ b/translation/dest/onboarding/mr-IN.xml
@@ -1,2 +1,6 @@
-
+
+ सुस्वागतम!
+ Lichess.org वर स्वागत!
+ बुद्धीबळाचे नियम शिका
+
diff --git a/translation/dest/onboarding/sq-AL.xml b/translation/dest/onboarding/sq-AL.xml
index 3ea04e700dfa8..ec8ffc06b804b 100644
--- a/translation/dest/onboarding/sq-AL.xml
+++ b/translation/dest/onboarding/sq-AL.xml
@@ -1,2 +1,17 @@
-
+
+ Mirë se vini!
+ Mirë se vini në lichess.org!
+ Kjo është faqja e profilit tuaj.
+ A do ta përdorë këtë llogari ndonjë fëmijë? Mund të doni të aktivizoni %s.
+ Po më? Ja ndoca sugjerime:
+ Mësoni rregullat e shahut
+ Përmirësohuni, përmes puzzle-esh taktikash shahu.
+ Luani kundër Inteligjencës Artificiale.
+ Luani me kundërshtarë nga anembanë bota.
+ Ndiqni shokët tuaj në Lichess.
+ Luani në turne.
+ Mësoni nga %1$s dhe %2$s.
+ Formësojeni Lichess-in si t’ju pëlqejë.
+ Eksploroni sajtin dhe zbavituni :)
+
diff --git a/translation/dest/onboarding/th-TH.xml b/translation/dest/onboarding/th-TH.xml
index 3ea04e700dfa8..09e2dd12e37c0 100644
--- a/translation/dest/onboarding/th-TH.xml
+++ b/translation/dest/onboarding/th-TH.xml
@@ -1,2 +1,17 @@
-
+
+ ยินดีต้อนรับ!
+ ยินดีต้อนรับสู่ lichess.org
+ นี้คือหน้าโปรไฟล์ของคุณ
+ เด็กจะใช้บัญชีนี้หรือไม่ คุณอาจต้องการเปิดใช้งาน %s
+ แล้วอย่างไรต่อ? นี้คือคำแนะนำเล็กๆน้อยๆ
+ เรียนรู้กฎของหมากรุก
+ เก่งขึ้นด้วยปริศนากลยุทธ์ของหมากรุก
+ เล่นกับปัญญาประดิษฐ์
+ เล่นกับคู่แข่งทั่วโลก
+ ติดตามเพื่อนของคุณบน Lichess
+ แข่งขันในทัวร์นาเมนต์
+ เรียนรู้จาก %1$s และ %2$s
+ ตั้งค่า Lichess ตามความชอบของคุณ
+ สำรวจเว็บไซต์ให้สนุกนะ :)
+
diff --git a/translation/dest/onboarding/tp-TP.xml b/translation/dest/onboarding/tp-TP.xml
index 3ea04e700dfa8..e83f1a3bd65cb 100644
--- a/translation/dest/onboarding/tp-TP.xml
+++ b/translation/dest/onboarding/tp-TP.xml
@@ -1,2 +1,17 @@
-
+
+ o kama pona!
+ o kama pona tawa lipu Lichess!
+ ni li lipu sina
+ jan lili li kepeken ala kepeken sijelo ni? kepeken la, ken la sina wile open e %s
+ tenpo ni la seme? ken la sina wile ni:
+ o kama sona e lawa pi musi ni
+ o wawa e sona sina kepeken musi lili
+ o utala e ilo
+ o utala e jan ante lon musi ni
+ o kama tawa poka pi jan pona sina
+ o musi lon utala suli a
+ o kama sona tan %1$s tan %2$s
+ o ante e ilo Lichess tawa wile sina
+ o musi pona a!
+
diff --git a/translation/dest/onboarding/uk-UA.xml b/translation/dest/onboarding/uk-UA.xml
index 3ea04e700dfa8..c69e5cd6c1490 100644
--- a/translation/dest/onboarding/uk-UA.xml
+++ b/translation/dest/onboarding/uk-UA.xml
@@ -1,2 +1,17 @@
-
+
+ Фу!
+ Ласкаво просимо на lichess.org!
+ Перевірте свою сторінку профілю.
+ Буде використовувати дочірній обліковий запис? Ви можете увімкнути %s.
+ Що зараз? Ось кілька пропозицій:
+ Вивчайте шахові правила
+ Покращуйтесь із шаховою тактичною головоломкою.
+ Зіграйте штучний інтелект.
+ Грайте з гравцями з усього світу
+ Слідкуйте за своїми друзями на Lichess.
+ Грайте в турнірах.
+ Вивчайте з %1$s та %2$s.
+ Налаштуйте Lichess на свій смак.
+ Досліджуйте сайт і веселіться :)
+
diff --git a/translation/dest/onboarding/zh-CN.xml b/translation/dest/onboarding/zh-CN.xml
index 3ea04e700dfa8..1aeaea2ca35ed 100644
--- a/translation/dest/onboarding/zh-CN.xml
+++ b/translation/dest/onboarding/zh-CN.xml
@@ -1,2 +1,7 @@
-
+
+ 欢迎!
+ 欢迎来到 lichess.org。
+ 这是你的个人资料页面。
+ 未成年人会使用此帐户吗?您可能想要启用 %s。
+
diff --git a/translation/dest/patron/ar-SA.xml b/translation/dest/patron/ar-SA.xml
index 21a0e88ca01e5..6e0d4c5a3f1c3 100644
--- a/translation/dest/patron/ar-SA.xml
+++ b/translation/dest/patron/ar-SA.xml
@@ -69,6 +69,7 @@
الدفعة القادمةسيتم خصم %1$s في %2$s.قم بتبرع إضافي الآن
+ امنح أجنحة الراعييتم إعطاء المتبرع شعار الجناح كهديةتحديثتغيير المبلغ الشهري (%s)
diff --git a/translation/dest/patron/gsw-CH.xml b/translation/dest/patron/gsw-CH.xml
index 6f8ba0547f069..b067dd89bfcd5 100644
--- a/translation/dest/patron/gsw-CH.xml
+++ b/translation/dest/patron/gsw-CH.xml
@@ -17,8 +17,8 @@
Du häsch es läbelangs Gönnerkonto. Das isch grossartig!Du häsch es Gönnerkonto bis %s.Falls du nöd erneuere tuesch, wird dis Konto in es Regulärs umgwandlet.
- Mir sind e \"Non-Profit-Organisation\", will mir glaubed, dass jede sött Zuegang zu ere gratis, \"Wältklass Schach Plattform\" ha.
- Mir sind uf d\'Underschtützig vu so Lüt wie du agwise, zum das möglich z\'mache. Wänn dir Lichess g\'fallt, chasch du eus underschtütze, indem du schpändisch und so en Lichess Gönner wirsch!
+ Als \"Non-Profit-Organisation\" glaubed mir, dass jede sött Zuegang zunere gratis, \"Wältklass Schach Plattform\" ha.
+ Mir sind uf Underschtützig vu Lüt wie du agwise, um das z\'ermögliche. Wänn dir Lichess g\'fallt, chasch eus underschtütze, idem du schpändsch und en Lichess Gönner wirsch!LäbeslangZahl eimal %s und wird für immer en Lichess Gönner!Läbeslange Lichess Gönner
@@ -30,12 +30,12 @@
Bitte gib en Betrag in %s iKreditcharteAmälde zum Schpände
- Mir sind es chlises Team, drum macht dini Underschtützig en riese Underschid!
- Die g\'firete Gönner, wo Lichess möglich mached
+ Mir sind es chlises Team, drum hät dini Underschtützig e grossi Würkig!
+ Gönner wo Lichess möglich machedWohi flüsst das Gäld?Ganz zerscht in leischtigsstarchi Server.
Dänn zahled mir en Vollzit-Entwickler: %s, de Gründer vu Lichess.
- Da die detailliert Choschte Uufteilig
+ Da die detailliert Choschte-UfteiligIsch Lichess e offizielli Non-Profit-Organisation?Ja, da isch d\'Gründigsurkund (uf französisch)Chann ich mini monetlichi Underschtützig ändere/chünde?
diff --git a/translation/dest/patron/vi-VN.xml b/translation/dest/patron/vi-VN.xml
index 47f82d12cdcf0..c45a8b93bfe21 100644
--- a/translation/dest/patron/vi-VN.xml
+++ b/translation/dest/patron/vi-VN.xml
@@ -2,7 +2,7 @@
Ủng hộỦng hộ bằng tài khoản %s
- Bảo trợ Lichess
+ Người bảo trợ LichessTài khoản miễn phíTrở thành một Người bảo trợ Lichess%s đã trở thành một Người bảo trợ Lichess
@@ -10,9 +10,8 @@
%1$s là Người bảo trợ Lichess trong %2$s thángNhững Người bảo trợ mới
- Cờ Vua Miễn Phí Cho Mọi Người!
-Mãi Mãi!
- Không có quảng cáo, không cần đóng tiền; nhưng có mã nguồn mở và sự đam mê.
+ Cờ Vua Miễn Phí Cho Mọi Người! Mãi Mãi!
+ Không có quảng cáo, không tốn tiền; nhưng có mã nguồn mở và sự đam mê.Cảm ơn sự ủng hộ của bạn!Bạn có một tài khoản Người bảo trợ trọn đời. Khá tuyệt vời đấy!Bạn có tài khoản người Bảo Trợ cho đến %s.
@@ -48,7 +47,7 @@ Hoặc bạn có thể %s.Xin lưu ý rằng chỉ có hình thức ủng hộ ở trên mới được cấp trạng thái Người bảo trợ.Có tính năng nào được dành riêng cho những Người bảo trợ không?Không, bởi vì Lichess hoàn toàn miễn phí, mãi mãi và dành cho tất cả mọi người. Đó là lời hứa của chúng tôi.
-Tuy nhiên, Patron được quyền khoe khoang với những cánh Người bảo trợ mới thú vị hiển thị trên hồ sơ của bạn.
+Tuy nhiên, Patron được quyền khoe khoang với những đôi cánh Người bảo trợ mới thú vị hiển thị trên hồ sơ của bạn.Xem so sánh các tính năng chi tiếtBảo trợ Lichess trong %s tháng
@@ -59,8 +58,8 @@ Tuy nhiên, Patron được quyền khoe khoang với những cánh Người b
Lần thanh toán tiếp theoBạn sẽ trả %1$s vào ngày %2$s.Ủng hộ thêm ngay bây giờ
- Tặng cánh Người bảo trợ
- Tặng cánh Người bảo trợ cho kỳ thủ
+ Tặng đôi cánh Người bảo trợ
+ Tặng đôi cánh Người bảo trợ cho kỳ thủCập nhậtĐổi số tiền hàng tháng (%s)Hủy sự ủng hộ của bạn
diff --git a/translation/dest/perfStat/vi-VN.xml b/translation/dest/perfStat/vi-VN.xml
index 3e40acacaf180..1f1b5ec45ad55 100644
--- a/translation/dest/perfStat/vi-VN.xml
+++ b/translation/dest/perfStat/vi-VN.xml
@@ -9,8 +9,8 @@
Giá trị thấp hơn nghĩa là Elo ổn định hơn. Ở ngưỡng trên %1$s, Elo được coi là tạm thời. Để được xếp trong bảng xếp hạng, giá trị này phải ở dưới ngưỡng %2$s (cờ tiêu chuẩn) hoặc %3$s (các biến thể).Tổng số ván cờCác ván cờ có xếp hạng
- Trò chơi trong giải đấu
- Số ván cờ chơi Berserk
+ Số ván chơi trong giải đấu
+ Số ván chơi BerserkThời gian đã chơiĐối thủ trung bìnhThắng
diff --git a/translation/dest/preferences/ar-SA.xml b/translation/dest/preferences/ar-SA.xml
index dfb7553dc73c1..254e53beb43a2 100644
--- a/translation/dest/preferences/ar-SA.xml
+++ b/translation/dest/preferences/ar-SA.xml
@@ -7,15 +7,16 @@
المؤثرات الحركية للقطعةالفرق الماديتميز معالم الرقعة (آخر نقلة والكش)
- نقلات القطعة ( النقلات المتاحة والنقلات الاستباقية)
+ إظهار النقلات القانونية (النقلات المتاحة والنقلات الاستباقية)إحداثيات الرقعة (A-H, 1-8)قائمة النقلات خلال المباراةتدوين النقلةرمز قطعة الشطرنجحروف (K, Q, R, B, N)وضع التأمل
- إظهار مستويات اللاعب
- هذا يسمح بإخفاء جميع التقييمات من الموقع، للمساعدة في التركيز على الشطرنج. لا يزال من الممكن تقييم المباريات ، هذا فقط حول ما يمكنك رؤيته.
+ إظهار تقييمات اللاعب
+ إظهار ميول اللاعب
+ هذا يخفي جميع التقييمات من الموقع، للمساعدة في التركيز على مباراة الشطرنج. لا يزال من الممكن لعب مباريات مقيمة، هذا فقط يحدد ما تراه.أظهر زر تعديل حجم الرقعةخلال الوضع المبدئي فقطفي اللعبة فقط
@@ -24,8 +25,8 @@
أجزاء الثانيةعندما يقل الوقت عن 10 ثوانٍالشريط الأخضر للساعة
- الصوت عندما يقارب الوقت على الإنتهاء
- إعطاء مزيد من الوقت
+ إصدار صوت عندما يقارب الوقت الانتهاء
+ منح الوقتإعدادات اللعبةكيف يمكنك تحريك القطع؟النقر فوق مربعين
@@ -35,7 +36,7 @@
التراجع عن النقلات (بموافقة الخصم)في المباريات غير المقيمة فقطالترقية إلى وزير آلياً
- اضغط مفتاح<ctrl> اثناء الترقية لتعطيل الترقية التلقائية مؤقتاً
+ اضغط مفتاح<ctrl> عند الترقية لتعطيل الترقية التلقائية مؤقتاًعند النقلة الاستباقيةمطالبة بالتعادل لتكرار نفس النقلات ثلاث مرات بشكل تلقائيعندما يقل الوقت عن 30 ثانية
diff --git a/translation/dest/preferences/de-DE.xml b/translation/dest/preferences/de-DE.xml
index 46c01809f3f81..496fe17154732 100644
--- a/translation/dest/preferences/de-DE.xml
+++ b/translation/dest/preferences/de-DE.xml
@@ -15,7 +15,7 @@
Buchstaben (K, Q, R, B, N)Zen-ModusWertungszahl von Spielern anzeigen
- Spieler-Dekoration anzeigen
+ Spieler-Flairs anzeigenVersteckt alle Wertungen auf der Website, damit du dich voll auf das Schach zu konzentrieren kannst. Partien können immer noch gewertet sein, es geht nur darum, was du zu sehen bekommst.Regler zum Ändern der Brettgröße anzeigenNur in der Anfangsstellung
diff --git a/translation/dest/preferences/it-IT.xml b/translation/dest/preferences/it-IT.xml
index ddaf733cca9ea..688c04da4351f 100644
--- a/translation/dest/preferences/it-IT.xml
+++ b/translation/dest/preferences/it-IT.xml
@@ -15,6 +15,7 @@
Lettera (K, Q, R, B, N)Modalità ZenMostra punteggi giocatori
+ Mostra le icone del giocatoreQuesta funzionalità permette di nascondere i punteggi dei giocatori per aiutare a concentrarti sulla partita. Le partite possono comunque essere classificate, questa impostazione riguarda solo ciò che vedi.Mostra l\'icona di ridimensionamento della scacchieraSolo sulla posizione iniziale
diff --git a/translation/dest/preferences/lb-LU.xml b/translation/dest/preferences/lb-LU.xml
index 5f1b9968c00ba..742def57d8930 100644
--- a/translation/dest/preferences/lb-LU.xml
+++ b/translation/dest/preferences/lb-LU.xml
@@ -34,7 +34,7 @@
Virauszich (wärend dem Géigner sengem Zuch spillen)Zeréckhuelen (mat Zoustemmung vum Géigner)Just an ongewäerten Partien
- Automatesch zur Damm ëmwandelen
+ Automatesch an eng Damm ëmwandelenDréck ob deng <ctrl> Tasten während der Emwandlung fir temporär déi automatesch Emwandlung ze desaktivéierenWann VirauszuchRemis duerch dräifach Stellungswidderhuelung reklaméieren
diff --git a/translation/dest/preferences/vi-VN.xml b/translation/dest/preferences/vi-VN.xml
index 8e91ff84fc200..38ef23535188c 100644
--- a/translation/dest/preferences/vi-VN.xml
+++ b/translation/dest/preferences/vi-VN.xml
@@ -16,7 +16,7 @@
Chế độ tập trungHiển thị hệ số của người chơiHiển thị biểu tượng của người chơi
- Điều này cho phép ẩn toàn bộ hệ số Elo từ trang web, giúp tập trung vào ván cờ. Ván đấu vẫn có thể tính Elo, điều này chỉ là về những thứ bạn muốn nhìn thấy.
+ Điều này sẽ ẩn toàn bộ hệ số Elo khỏi Lichess để giúp tập trung vào ván cờ. Ván đấu có xếp hạng vẫn ảnh hưởng đến hệ số Elo của bạn, đây chỉ là những thứ bạn có thể nhìn thấy.Hiện nút thay đổi kích cỡ bàn cờChỉ ở thế cờ ban đầuChỉ trong ván cờ
@@ -41,7 +41,7 @@
Tự động hoà khi lặp cờ ba lầnKhi thời gian còn lại < 30 giâyXác nhận nước đi
- Có thể bị vô hiệu hóa trong trò chơi với mục lục bàn cờ
+ Có thể bị vô hiệu hóa trong ván cờ với mục lục bàn cờCờ qua thưCờ qua thư và không giới hạnXác nhận chịu thua và đề nghị hòa
diff --git a/translation/dest/puzzle/da-DK.xml b/translation/dest/puzzle/da-DK.xml
index bd21f50517e68..6ab4e652a9123 100644
--- a/translation/dest/puzzle/da-DK.xml
+++ b/translation/dest/puzzle/da-DK.xml
@@ -18,18 +18,18 @@
Din opgave-rating vil ikke ændre sig. Bemærk at opgaver ikke er en konkurrence. Rating hjælper med at vælge de bedste opgaver i forhold til dine nuværende færdigheder.Find det bedste træk for hvid.Find det bedste træk for sort.
- For at få personlige opgaver:
- Opgave %s
+ For at få personlige taktikopgaver:
+ Taktikopgave %sDagens opgave
- Daglig opgave
+ Daglig taktikopgaveKlik for at løseGodt trækBedste træk!Bliv ved…Korrekt!
- Opgave løst!
+ Taktikpgave løst!Efter åbninger
- Opgaver efter åbninger
+ Taktikopgaver efter åbningerÅbninger du har spillet mest i ratede partierBrug \"Find på side\" i browsermenuen til at finde din foretrukne åbning!Brug Ctrl+f til at finde din foretrukne åbning!
@@ -59,26 +59,26 @@
EksempelTilføj et andet tema
- Næste opgave
- Spring straks videre til næste opgave
+ Næste taktikopgave
+ Spring straks videre til næste taktikopgaveOpgave-kontrolpanelForbedringsområderStyrkeOpgavehistorikløstmislykket
- Løs opgaver af stigende sværhedsgrad og opbyg en sejrsstime. Der er intet ur, så tag dig god tid. Ét forkert træk og spillet er ovre! Men du kan springe ét træk over per session.
- Din stime: %s
+ Løs taktikopgaver af stigende sværhedsgrad og opbyg en sejrsstime. Der er intet ur, så tag dig god tid. Ét forkert træk og spillet er ovre! Men du kan springe ét træk over per session.
+ Din stime: %sSpring dette træk over for at bevare din stime! Virker kun én gang per gennemløb.Fortsæt stimenNy stimeFra mine partier
- Søg opgaver fra en spillers partier
- Opgaver fra %s\' partier
- Søg opgaver
- Du har ingen opgaver i databasen, men Lichess elsker dig alligevel.
-Spil hurtige (rapid) og klassiske (classical) partier for at forøge chancerne for at en af dine opgave tilføjes!
- %1$s opgaver fundet i %2$s partier
+ Søg taktikopgaver fra en spillers partier
+ Taktikopgaver fra %s\' partier
+ Søg taktikopgaver
+ Du har ingen taktikopgaver i databasen, men Lichess elsker dig alligevel.
+Spil hurtige (rapid) og klassiske (classical) partier for at forøge chancerne for at en af dine taktikopgave tilføjes!
+ %1$s taktikopgaver fundet i %2$s partierTræn, analysér, forbedr%s spillet
diff --git a/translation/dest/puzzle/vi-VN.xml b/translation/dest/puzzle/vi-VN.xml
index 3e962948af6be..1bf3f6a5b13f8 100644
--- a/translation/dest/puzzle/vi-VN.xml
+++ b/translation/dest/puzzle/vi-VN.xml
@@ -4,7 +4,7 @@
Chủ đề câu đốĐề xuấtGiai đoạn
- Chủ đề khác
+ Các mô-típNâng caoThời lượngChiếu hết
@@ -75,7 +75,7 @@
Tìm câu đốBạn không có câu đố nào trong dữ liệu, nhưng Lichess vẫn rất yêu mến bạn.
-Hãy chơi thêm nhiều ván cờ nhanh và cờ chậm để có cơ hội có một câu đố từ ván đấu của riêng bạn!
+Hãy chơi thêm nhiều ván cờ nhanh và cờ chậm để có cơ hội có một câu đố từ ván cờ của riêng bạn!Đã tìm được %1$s câu đố trong các ván đấu của %2$sRèn luyện, phân tích, cải thiện
diff --git a/translation/dest/puzzleTheme/da-DK.xml b/translation/dest/puzzleTheme/da-DK.xml
index a8d6c2792c66b..1d082554ebfbd 100644
--- a/translation/dest/puzzleTheme/da-DK.xml
+++ b/translation/dest/puzzleTheme/da-DK.xml
@@ -62,9 +62,9 @@
Lang opgaveTre træk for at vinde.Mesterpartier
- Opgaver fra partier af spillere med titel.
+ Taktikopgaver fra partier af spillere med titel.Mester mod mester partier
- Opgaver fra partier mellem to spillere med titel.
+ Taktikopgaver fra partier mellem to spillere med titel.MatVind spillet med stil.Mat i 1
@@ -80,7 +80,7 @@
MidtspilEn taktik i den anden fase af spillet.Et-træks opgave
- En opgave der kun er ét træk lang.
+ En taktikopgave der kun er ét træk lang.ÅbningEn taktik i den første fase af spillet.Bondeslutspil
@@ -101,19 +101,19 @@
Et slutspil med kun tårne og bønder.OfferEn taktik der består i at opgive materiale på kort sigt for igen at få en fordel efter en tvungen træksekvens.
- Kort opgave
+ Kort taktikopgaveTo træk for at vinde.SpidEn manøvre hvor en brik af høj værdi angribes og må flyttes, hvorved en brik af lavere værdi bagved kan tages eller trues. Det omvendte af en binding.Kvalt matEn skakmat leveret af en springer, hvor den matte konge er ude af stand til at bevæge sig, fordi den er omgivet (eller kvalt) af sine egne brikker.Superstormester-partier
- Opgaver fra partier spillet af verdens bedste spillere.
+ Taktikopgaver fra partier spillet af verdens bedste spillere.Fastlåste brikkerEn brik er ude af stand til at undslippe fangst, da den har begrænsede trækmuligheder.UnderforvandlingForvandling til en springer, løber eller tårn.
- Meget lang opgave
+ Meget lang taktikopgaveFire træk eller mere for at vinde.RøngtenangrebEn brik angriber eller forsvarer et felt gennem en af modstanderens brikker.
@@ -122,6 +122,6 @@
Sund blandingLidt af hvert. Du kan ikke vide, hvad du skal forvente, så du skal være klar til alt! Præcis som i rigtige spil.Spiller-partier
- Find opgaver lavet ud fra dine egne partier eller fra en anden spillers partier.
+ Find taktikopgaver lavet ud fra dine egne partier eller fra en anden spillers partier.Disse opgaver er i offentligt domæne og kan downloades fra %s.
diff --git a/translation/dest/puzzleTheme/gl-ES.xml b/translation/dest/puzzleTheme/gl-ES.xml
index 6c565dcb01872..936776748550c 100644
--- a/translation/dest/puzzleTheme/gl-ES.xml
+++ b/translation/dest/puzzleTheme/gl-ES.xml
@@ -47,7 +47,7 @@
Táctica que involucra a captura ao paso, onde un peón pode capturar a un peón opoñente que o deixou atrás usando o seu movemento inicial de dúas casas.Rei expostoTáctica que involucra a un rei con pouca defensa ó seu redor, a miúdo conducindo a xaque mate.
- Pinza
+ GarfoMovemento no que a peza movida ataca a dúas pezas opoñentes á vez.Peza colgadaUnha táctica que involucra unha peza do opoñente que non está suficientemente defendida e que por tanto se pode capturar.
diff --git a/translation/dest/puzzleTheme/nn-NO.xml b/translation/dest/puzzleTheme/nn-NO.xml
index 7b4ee69d7d5dd..c8166a1dceb2f 100644
--- a/translation/dest/puzzleTheme/nn-NO.xml
+++ b/translation/dest/puzzleTheme/nn-NO.xml
@@ -120,7 +120,7 @@
TrekktvangEi stilling der alle moglege trekk skadar stillinga.Blanda drops
- Litt av kvart. Du veit ikkje kva du blir møtt med, så du må vera førebudd på det meste. Nett som i verkelege parti.
+ Litt av alt. Du veit ikkje kva du blir møtt med, så du må vera førebudd på det meste. Nett som i verkelege parti.Spelar partiFinn oppgåver generert frå dine eller andre sine parti.Desse oppgåvene er offentleg eigedom og kan lastast ned frå %s.
diff --git a/translation/dest/puzzleTheme/vi-VN.xml b/translation/dest/puzzleTheme/vi-VN.xml
index 3d8beea57e1bb..c396945751f4f 100644
--- a/translation/dest/puzzleTheme/vi-VN.xml
+++ b/translation/dest/puzzleTheme/vi-VN.xml
@@ -66,7 +66,7 @@
Ván đấu giữa 2 kiện tướngCâu đố từ các ván đấu giữa hai người chơi có danh hiệu.Chiếu hết
- Chiến thắng trò chơi với phong cách.
+ Chiến thắng ván cờ với phong cách.Chiếu hết trong 1 nướcChiếu hết trong một nước cờ.Chiếu hết trong 2 nước
diff --git a/translation/dest/search/gsw-CH.xml b/translation/dest/search/gsw-CH.xml
index e850f332d9f63..8ef2a8f80a053 100644
--- a/translation/dest/search/gsw-CH.xml
+++ b/translation/dest/search/gsw-CH.xml
@@ -12,11 +12,11 @@
Gägner NameVerlürer
- Vum
+ vobisOb de Gägner en Mänsch oder en Computer gsi ischK.I. Stärchi
- Quälle
+ SpielortAzahl ZügErgäbnisSieger Farb
diff --git a/translation/dest/settings/da-DK.xml b/translation/dest/settings/da-DK.xml
index 7da98d49c5ccd..f03d9a83b117b 100644
--- a/translation/dest/settings/da-DK.xml
+++ b/translation/dest/settings/da-DK.xml
@@ -4,8 +4,8 @@
Luk kontoDin konto er under administration og kan ikke lukkes.Lukning er uigenkaldelig. Der er ingen fortrydelsesret. Er du sikker?
- Du vil ikke få lov til at åbne en ny konto med det samme navn, selv hvis du ændrer store og små bogstaver.
+ Du vil ikke få lov til at åbne en ny konto med det samme navn, selv hvis du ændrer på store og små bogstaver.Jeg har skiftet mening, lad være med at lukke min konto
- Er du sikker på, at du vil lukke din konto? Lukning af din konto er en permanent beslutning. Du vil ALDRIG NOGENSINDE kunne logge ind igen.
+ Er du sikker på, at du vil lukke din konto? Lukning af din konto er en permanent beslutning. Du vil ALDRIG kunne logge ind igen.Denne konto er lukket.
diff --git a/translation/dest/site/af-ZA.xml b/translation/dest/site/af-ZA.xml
index ddef480dc553e..73267a5c21871 100644
--- a/translation/dest/site/af-ZA.xml
+++ b/translation/dest/site/af-ZA.xml
@@ -272,7 +272,6 @@
In spelTans besig om te speelKlaar
- maak klaar %sStaak spelSpel gestaakStandaard
@@ -402,7 +401,7 @@
Plak \'n wedstryd PGN om dit deursoekbaar te herspeel,
rekenaar analise, kletskamer en deelbare URL te kry.Variasies sal uitgevee word. Voer die PGN in d.m.v. \'n studie om hulle te behou.
- Hierdie PGN is toeganklik vir die algemene publiek. Gebruik \'n studie om \'n spel privaat in te voer.
+ Hierdie PGN is toeganklik vir die algemene publiek. Gebruik \'n studie om \'n spel privaat in te voer.%s ingevoerde spel%s het spelle ingetrek
@@ -854,9 +853,10 @@ rekenaar analise, kletskamer en deelbare URL te kry.
en stoor %s voorafskuif variasieen stoor %s voorafskuif variasies
+ Jy het \'n privaatboodskap van Lichess ontvang.
+ Klik hier om dit te leesSkies :(Jy moet vir \'n rukkie in die hoek sit.
- Jy mag weer uitkom %s.Hoekom?Ons poog om \'n aangename ervaring aan alle spelers te gee.Om dit reg te kry, moet ons seker maak dat alle spelers goeie praktyke handhaaf.
@@ -876,6 +876,7 @@ rekenaar analise, kletskamer en deelbare URL te kry.
Ek stem in om al Lichess se beleide na te volg.Soek of begin \'n nuwe gesprekPas aan
+ BulletBlitsRapidKlassieke
diff --git a/translation/dest/site/an-ES.xml b/translation/dest/site/an-ES.xml
index 338cc865c80e7..663e3a1eedc5a 100644
--- a/translation/dest/site/an-ES.xml
+++ b/translation/dest/site/an-ES.xml
@@ -272,7 +272,6 @@
En chuego agora mesmoSe ye chugando agoraRematau
- remata %sCancelar partidaPartida canceladaStandard
@@ -405,7 +404,6 @@
Importar partidaApegando lo PGN d\'una partida, s\'obtiene una repetición navegable, una analisi per ordinador, un chat de partida y un vinclo pa compartir.Se borrarán las variants. Pa mantenir-la, importa lo PGN a traviés d\'un estudio.
- Este PGN ye accesible per lo publico. Pa importar una partida de manera privada, fe servir un estudio.%s partida importada%s partidas importadas
@@ -507,7 +505,7 @@
Editar perfilNombreApellido
- Define lo tuyo estilo:
+ Define lo tuyo estiloEstiloI hai un achuste pa amagar toz es estilos d\'usuario en tot lo puesto web.Biografía
@@ -862,9 +860,10 @@
y cabida %s linia de premovimientoy cabida %s linias de premovimiento
+ Has recibiu un mensache privau de Lichess.
+ Fe clic aquí pa leyer-loLo sentimos :(Hemos habiu de que suspender-te temporalment.
- La suspensión expira en %s.Per qué?Lo nuestro obchectivo ye proporcionar una experiencia agradable a toz.Pa ixo, hemos d\'asegurar-nos que toz los chugadors se comportan como cal.
@@ -884,6 +883,7 @@
Me comprometo a seguir las normas de Lichess.Buscar u empecipiar una nueva conversaciónEditar
+ BulletBlitzRapidasClasica
diff --git a/translation/dest/site/ar-SA.xml b/translation/dest/site/ar-SA.xml
index c5b8f254d47b2..a7307a9b8a3d1 100644
--- a/translation/dest/site/ar-SA.xml
+++ b/translation/dest/site/ar-SA.xml
@@ -17,7 +17,7 @@
مات مخنوقأبيضأسود
- الأبيض
+ بالأبيضالأسودلون عشوائيإنشاء مباراة
@@ -26,7 +26,7 @@
أنت تلعب بالقطع البيضاءأنت تلعب بالقطع السوداءإنه دورك!
- ثم تحديد حالة غش
+ تحديد حالة غِشّالملك في الوسطكش ملك ثلاثانهاية السباق
@@ -53,8 +53,8 @@
الأسود استسلمالأبيض ترك المباراةالأسود ترك المباراة
- لم يقم الأبيض بالحركة
- لم يقم الأبيض بالحركة
+ لم يلعب الأبيض بعد
+ لم يلعب الأسود بعداطلب تحليل حاسبتحليل الحاسوبتحليل الحاسوب متاح
@@ -63,7 +63,7 @@
عمق التحليل %sاستخدام تحليل الخادمتحميل المحرك...
- حساب النقلات...
+ جاري حساب النقلات...خطأ في تحميل المحركتحليل سحابيتحليل أعمق
@@ -71,9 +71,10 @@
باستخدام متصفحكالتبديل للتحليل بالمتصفحرفع سلسلة الحركات
- رفع الى التسلسل الرئيسي
+ كتابة التفريع الرئيسياحذف من هنافرض التسلسل
+ انسخ التفريع بصيغة PGNالتقلةخسارة بطريقة خاصةفوز بطريقة خاصة
@@ -90,20 +91,20 @@
متوسط التقييم: %sأحدث المبارياتأفضل الالعاب
- اثنان مليون مباراة من قاعدة بيانات الاساتذة تقييم %1$s+ للاعبين من %2$s إلى %3$s
+ قاعدة بيانات مباريات الأساتذة تقييم %1$s+ للاعبين من %2$s إلى %3$sمات في %s نصف-نقلةمات في %s نصف-نقلةمات في %s نصف-نقلةمات في %s نصف-نقلة
- مات في %s نصف-نقلة
- مات في %s نصف-نقلة
+ كش مات في %s نقلة
+ كش مات في %s نقلة
- DTZ50\'\' مع تقريب ، استنادًا إلى عدد من نصف التحركات حتى التقاط أو نقل بياض التالي
+ DTZ50\'\' هي عدد الحركات حتى حصول أخذ أو تحريك بيدقلم يتم العثور على مبارياتتم الوصول إلى أقصى عمق!أترغب في ضم مباريات أكثر من قائمة التفضيلات؟
- الافتتاح
+ الافتتاحياتمستكشف الافتتاحياتمستكشف نهاية/بداية الدورمستكشف افتتاحيات %s
@@ -123,6 +124,7 @@
فتح دراسةتفعيلسهم أفضل نقلة
+ أظهر سلسلة النقلات المرشحةمقياس التقييمعدد الخطوطالمعالجات
@@ -182,6 +184,14 @@
%s مباراة%s مباراة
+
+ التصنيف للشطرنج %1$s بعد %2$s مباراة
+ التصنيف للشطرنج %1$s بعد %2$s مباراة واحدة
+ التصنيف للشطرنج %1$s بعد %2$s مباراتين
+ تصنيفك في %1$s بعد %2$s مباراة
+ تصنيفك في %1$s بعد %2$s مباراة
+ التصنيف للشطرنج %1$s بعد %2$s مباراة
+ %s مباراة مفضلة%s مباراة مفضلة
@@ -322,7 +332,6 @@
يلعب الآنيلعب الآنانتهت
- تنتهي %sإلغاء اللعبةاللعبة ألغيتعادي
@@ -431,6 +440,14 @@
%s دراسات%s دراسة
+
+ %s خصم
+ %s خصم واحد
+ %s خَصمان في الوقت نفسه
+ %s خصوم في الوقت نفسه
+ %s خَصم في الوقت نفسه
+ %s خَصم في الوقت نفسه
+ شاهد المسابقةعودة للمسابقةلا يمكنك التعادل قبل لعب 30 حركة في بطولة سويسرية.
@@ -467,7 +484,7 @@
يجب أن تلعب %s مباراة مقيمة أخرىيجب أن تلعب %s مباراة مقيمة أخرى
- تقييمك %s مؤقت
+ تقييمك في %s مؤقتتقييمك في %1$s وقدره %2$s عالي جدًاتقييمك الأسبوعي في %1$s وقدره %2$s عالي جدًاتقييمك في %1$s وقدره %2$s منخفض جدًا
@@ -499,6 +516,7 @@
استورد مباراةعند لصق مباراة PGN تحصل على إمكانية كرار استعراضها وتحليل حاسوبي ودردشة للمباراة ورابط قابل للمشاركة.سيتم محو التغييرات. للحفاظ عليها، يرجى استيراد PGN (تنسيق لعبة الشطرنج المحمول) عبر تبويب دراسة.
+ يمكن لأي أحد الوصول إلى PGN، إذا أردت إنشاء تحليل خاص، استخدم قسم دراسة.مباراة مستوردة %sمباراة مستوردة %s
@@ -599,6 +617,7 @@
نقلات اللعبفوز الأبيضفوز الأسود
+ معدل التعادلتعادلبطولة ال %s التالية:معدل الخصم
@@ -619,6 +638,9 @@
حرر الملف الشخصيالاسم الأولاسم العائلة
+ اختيار الشارة
+ الشارة
+ يستخدم هذا الإعداد لإخفاء جميع شارات المستخدمين في الموقع.نبذة عنكالبلد أو المنطقةشكرًا لك!
@@ -788,8 +810,10 @@
معارض التزامنياتالمضيفلون المضيف: %s
+ مبارياتك المعلقةتزامنيات مُنشأة حديثاًاستضف تزامنية جديدة
+ سجل لاستضافة أو الانضمام إلى محاكاةالتزامنية غير موجودةمعرض هذه التزامنية غير موجود.العودة لصفحة التزامنية
@@ -814,6 +838,7 @@
اختصارات لوحة المفاتيحتحرك للخلف/للأماماذهب للبداية/للنهاية
+ التفريع المحددأظهر/أخفِ التعليقاتمتغير دخول/خروجاطلب تحليل الحاسوب وتعلم من أخطائك
@@ -821,6 +846,13 @@
الخطأ الفادح التاليالخطأ التاليالنقلة غير الدقيقة التالية
+ التفريع السابق
+ التفريع القادم
+ تبديل أسهم النقلات المرشحة
+ الدورة السابقة/التفريع التالي
+ تبديل الرموز التوضيحية
+ أسمهم النقلات تسمح لك بلعبها دون استخدام قائمة النقلات المرشحة.
+ لعب النقلة المحددةمسابقة جديدةمسابقات بانواع شطرنج مختلفة وساعات مختلفةالعب مسابقات شطرنج بكل السرعات. انضم للمسابقات الرسمية المجدولة، أو ابدأ مسابقاتك الخاصة.
@@ -867,6 +899,7 @@
مع الأصدقاءمع الجميعموقع الأطفال
+ وضع الطفل مفعل.هذا يتعلق بالأمان. في نمط الطفل، يتم تعطيل كافة اتصالات المواقع. تمكين هذا للأطفال والطلاب، لحمايتهم من مستخدمي الإنترنت الأخرين.في نمط الطفل، يكون لشعار ليتشيس رمز %s، لكي تعرف أن أطفالك آمنين.يتم إدارة حسابك. اسأل معلم الشطرنج الخاص بك عن رفع وضع الطفل.
@@ -996,9 +1029,10 @@
واحفظ عدد %s تفريع نقلة مسبقةواحفظ عدد %s تفريع نقلة مسبقة
+ تسلّمتَ رسالة خاصة من ليتشيس.
+ اضغط هنا بمتابعة القراءةنأسف :(اضطررنا إلى حظرك لفترة قصيرة.
- ينتهي الحظر خلال %s.لماذا ؟نحن نهدف الى توفير تجربة شطرنج ممتعة للجميع.لهذا الغرض، علينا التأكد أن جميع اللاعبين يسلكون سلوكاً حسناً.
@@ -1018,12 +1052,14 @@
أوافق على أني سأتبع سياسات الموقع.البحث أو بدء محادثة جديدةتعديل
- سريع
+ الرصاصة
+ الخاطف
+ السريعتقليديمباراةٌ جنونية السرعة: أقل من ثلاثين ثانيةمباراةٌ سريعةٌ جداً: أقل من ٣ دقائق
- مبارياتٌ سريعة: ٣ - ٨ دقائق
- مباراياتٌ خاطفةٌ: ٨-٢٥ دقائق
+ مبارياتٌ خاطفة: ٣ - ٨ دقائق
+ مباريات سريعة: ٨-٢٥ دقائقمبارياتٌ كلاسيكيةٌ: ٢٥ دقيقة و أكثرألعاب المراسلة: يوم أو عدة أيام لكل نقلةمدرب تكتيكات الشطرنج
@@ -1115,6 +1151,8 @@
تبديل جهة اللعبإغلاق حسابك سوف تخسر تقدمكنصائحنا لتنظيم الأحداث
+ التعليمات
+ اظهر لي كل شيءLichess هو موقع خيري و مجاني بشكل كامل ومفتوح المصدر.
كافة التكاليف التشغيلية و التطويرية و المحتوى يتم الحصول عليه من قبل تبرعات المستخدمين.
diff --git a/translation/dest/site/av-DA.xml b/translation/dest/site/av-DA.xml
index 03bce9659f45c..80b901c6871c8 100644
--- a/translation/dest/site/av-DA.xml
+++ b/translation/dest/site/av-DA.xml
@@ -270,7 +270,6 @@
ХӀай унеб буго гьабсагӀатХӀай унеб буго гьабсагӀатТӀубана
- %s лъугӀулаХӀай лъугӀизабичӀого тезеХӀай лъугӀизабичӀого танаГӀадатаб
@@ -831,7 +830,6 @@
%s гьабеТӀаса лъугьа :(Дур хӀалтӀи заманалде гьоркьоб къотӀизабизе ккана.
- Балагьун чӀеялъул заман %s лъугӀула.Щай?Нилъеца киназего шахматазда лъикӀаб хӀалбихьи щвезе жигар бахъула.Гьеб щвезе, нилъеда щивас къагӀидаби цӀунулел ругелали ракӀчӀун лъазе ккола.
diff --git a/translation/dest/site/az-AZ.xml b/translation/dest/site/az-AZ.xml
index 0e0057f38b4c9..62610ecd842fe 100644
--- a/translation/dest/site/az-AZ.xml
+++ b/translation/dest/site/az-AZ.xml
@@ -272,7 +272,6 @@
Hal-hazırda oyundadırHal-hazırda oynayırBitdi
- %s sonra başa çatacaqOyunu ləğv etOyun ləğv olunduStandart
@@ -818,7 +817,6 @@
Təəssüf :(Sizi bir müddətlik oyunlardan xaric etmək məcburiyyətində qaldıq.
- Zaman aşımı %s bitəcək.Niyə?Biz hər kəs üçün xoş bir şahmat təcrübəsi təmin etməyi hədəfləyirik.Bunun üçün bütün oyunçuların düzgün təcrübəni izləməsini təmin etməliyik.
diff --git a/translation/dest/site/be-BY.xml b/translation/dest/site/be-BY.xml
index e1ad98cb3c85a..71a92b734cc65 100644
--- a/translation/dest/site/be-BY.xml
+++ b/translation/dest/site/be-BY.xml
@@ -292,7 +292,6 @@
Граецца заразГраецца заразСкончана
- завяршыцца %sСкасаваць гульнюГульня скасаванаСтандартная
@@ -556,6 +555,7 @@
ІмяПрозвішчаБіяграфія
+ Краіна або рэгіёнДзякуй!Спасылкі на сацсеткіАдзін URL на радок.
@@ -911,7 +911,6 @@
Прабачце :(Нам прыйшлося на нейкі час выдаць вам тайм-аўт.
- Вы зможаце вярнуцца праз %s.Чаму?Мы імкнемся даставіць задавальненне ад шахмат ўсім гульцам.Каб дамагчыся гэтага, мы павінны забяспечыць, каб усе гульцы вынікалі правілам добрага тону.
diff --git a/translation/dest/site/bg-BG.xml b/translation/dest/site/bg-BG.xml
index a27413af94919..d7710a5697a77 100644
--- a/translation/dest/site/bg-BG.xml
+++ b/translation/dest/site/bg-BG.xml
@@ -271,7 +271,6 @@
Играещи сегаИграещи сегаПриключи
- свършва след %sОтмени игратаИграта е отмененаОбикновен
@@ -847,7 +846,6 @@
Съжалявам :(Наложи се да Ви сложим в принудителна почивка за известно време.
- Принудителната почивка приключва %s.Защо?Стремим се да доставим приятно удоволствие от Шаха на всеки.С тази цел трябва да подсигурим, че всички играчи спазват добри практики.
diff --git a/translation/dest/site/bn-BD.xml b/translation/dest/site/bn-BD.xml
index 3ef1ebc32cff3..0e7b09b0f7de0 100644
--- a/translation/dest/site/bn-BD.xml
+++ b/translation/dest/site/bn-BD.xml
@@ -245,7 +245,6 @@
এই মুহূর্তে খেলছেনএই মুহূর্তে খেলতেছেসমাপ্ত
- শেষ করেছে %sখেলা বন্ধ করুনখেলা বন্ধ করা হয়েছেআদর্শ
@@ -808,7 +807,6 @@
দুঃখিত :(আমাদের কিছুক্ষণের জন্য আপনাকে বিরতি হল।
- সময় লাগবে %s.কেনআমাদের লক্ষ্য প্রত্যেকের জন্য একটি আনন্দদায়ক দাবা খেলার অভিজ্ঞতা দেয়া।সেই লক্ষ্যে আমাদের অবশ্যই নিশ্চিত করতে হবে যে সমস্ত খেলোয়াড় ভাল অভ্যাস চর্চা করে।
diff --git a/translation/dest/site/br-FR.xml b/translation/dest/site/br-FR.xml
index 84b6ac44657e9..4613f338ef536 100644
--- a/translation/dest/site/br-FR.xml
+++ b/translation/dest/site/br-FR.xml
@@ -218,7 +218,7 @@
%s eur%s eur%s eur
- %s eurvezhioù
+ %s eur%s vunutenn
@@ -305,7 +305,6 @@
O c\'hoari bremañO c\'hoari bremañEchu
- a vo echu e %sNullañ ar c\'hrogadKrogad nulletNormal
@@ -939,9 +938,10 @@
hag enrollit %s linennoù raktaolioùhag enrollit %s linennoù raktaolioù
+ Resevet ho peus ur gemennadenn brevez eus perzh Lichess.
+ Klikit amañ evit lenn anezhiHon digarezit :(Ret e oa deomp ho skarzhañ e-pad ur prantad.
- Ne veoc\'h ket mui forbannet a-benn %s.Perak?Plijout a rafe deomp e vourrfe pep hini c\'hoari echedoù war Lichess.Evit hen ober e dav deomp bezañ sur e vez graet gant doareoù dereat gant an holl.
diff --git a/translation/dest/site/bs-BA.xml b/translation/dest/site/bs-BA.xml
index 78abafd058551..b7603b20cfc41 100644
--- a/translation/dest/site/bs-BA.xml
+++ b/translation/dest/site/bs-BA.xml
@@ -286,7 +286,6 @@
Upravo igrajuUpravo igrajuZavršeno
- završava %sOtkažite partijuPartija otkazanaStandardna
@@ -889,7 +888,6 @@ računarsku analizu, mogućnost dopisivanja i link za slanje drugima.
Izvinjavamo se :(Morali smo Vam staviti privremenu zabranu na igranje.
- Privremena zabrana ističe %s.Zašto?Naš cilj je da svima pružimo ugodno šahovsko iskustvo.Zbog toga, moramo osigurati da svi igrači ispravno postupaju.
diff --git a/translation/dest/site/ca-ES.xml b/translation/dest/site/ca-ES.xml
index 8ca23023638c2..6ea586ca6cb5c 100644
--- a/translation/dest/site/ca-ES.xml
+++ b/translation/dest/site/ca-ES.xml
@@ -272,7 +272,6 @@
En jocJugant-se ara mateixAcabat
- finalitza %sAvortar la partidaPartida avortadaEstàndard
@@ -405,7 +404,7 @@
Importa una partidaEnganxa el PGN d\'una partida per obtenir una repetició navegable, anàlisi computeritzada, xat de joc i enllaç per compartir.S\'esborraran les variants. Per mantenir-les, importeu el PGN mitjançant un estudi.
- El públic pot accedir a aquest PGN. Per a importar un joc de manera privada, utilitza un estudi.
+ Aquest PGN és accessible públicament. Per a importar un joc de manera privada, utilitza un estudi.%s partides importades%s partides importades
@@ -507,7 +506,7 @@
Edita el perfilNomCognoms
- Defineix el teu estil:
+ Defineix el teu estilIconaExisteix una configuració per amagar els estils dels jugadors a tot el lloc web.Biografia
@@ -862,9 +861,10 @@
i guardar %s línia anticipadai guardar %s línies anticipades
+ Has rebut un missatge privat de Lichess.
+ Fes clic aquí per llegir-loHo sentim :(Hem hagut de penalitzar-te una estona.
- La penalització acaba en %s.Perquè?Intentem donar una bona experiència d’escacs a tothom.Per això, hem de garantir que tots els jugadors segueixin bones pràctiques.
@@ -884,6 +884,7 @@
Estic d’acord que seguiré totes les polítiques de Lichess.Cerca o inicia una nova discusióEdita
+ BulletBlitzRàpidesClàssic
diff --git a/translation/dest/site/ckb-IR.xml b/translation/dest/site/ckb-IR.xml
index d4724b7f17eda..b1327b3f61701 100644
--- a/translation/dest/site/ckb-IR.xml
+++ b/translation/dest/site/ckb-IR.xml
@@ -272,7 +272,6 @@
یارییەکانی ئێستائێستا یاری دەکەنتەواوبوو
- تەواوبوو %sهەڵوەشاندنەوەی یارییارییەکە هەڵوەشایەوەئاسایی
@@ -405,7 +404,6 @@
یارییەکان هەناردەبکەدوای پەیستکردنی PGN لینکێکت دەست دەکەوێت، دەتوانی لە وێبگەرەکان بەکاری بێنیت بۆ شیکاریکردنی یارییەکە، گفتوگۆکردن و بەشداریپێکردن.گۆڕانکارییەکان دەسڕدرێنەوە. بۆ پاراستنی، لە ڕێگەی PGN توێژینەوەکە هاوردە بکە.
- ئەم PGN ـە دەتوانرێت لەلایەن خەڵکەوە دەستی پێ بگات. بۆ هاوردەکردنی یارییەک بە شێوەیەکی تایبەت یان توێژینەوەیەک بەکاردێت.%s یاری هاوردەکراو%s یارییە هاوردەکراوەکان
@@ -507,7 +505,7 @@
دەستکاریکردنی پڕۆفایلناوناوی باپیرت
- توانای خۆت دابنێ:
+ توانای خۆت دابنێبەهرەڕێکخستنێک هەیە بۆ شاردنەوەی هەموو تواناکانی بەکارهێنەر لە سەرانسەری ماڵپەڕەکەدا.ژیاننامە
@@ -852,9 +850,10 @@
بەم جۆرە %s ـی پێش جوڵە پاشەکەوتبکەبەم جۆرە %s ـی پێش جوڵە پاشەکەوتبکە
+ پەیامێکی تایبەتت لە لایەن لیچێسەوە پێگەیشتووە.
+ بۆ خوێندنەوەی کلیک لێرە بکەببوورە :(ناچاربووین بۆ ماوەیەک دوورت بخەینەوە.
- کاتە دیاریکراوەکەت %s ـیتربەسەر دەچێت.بۆ?ئامانجمان دابینکردنی ئەزموونێکی خۆشی شەترەنجە بۆ هەمووان.بۆ ئەو مەبەستەش دەبێت دڵنیابین لەوەی کە هەموو یاریزانەکان مەشقی باش پەیڕەو دەکەن.
@@ -874,6 +873,7 @@
منیش لەگەڵ ئەوەدام کە هەموو سیاسەتەکانی لیچێس پەیڕەو دەکەم.گەڕان یان دەستپێکردنی گفتوگۆی نوێدەستکاریکردن
+ بوڵێت \"Bullet\"ئاگرین\"بڵیتز\"خێراکلاسیک
diff --git a/translation/dest/site/co-FR.xml b/translation/dest/site/co-FR.xml
index cdfabda456cae..bb0f60e9bbd56 100644
--- a/translation/dest/site/co-FR.xml
+++ b/translation/dest/site/co-FR.xml
@@ -238,7 +238,6 @@
Si ghjoca avàSi ghjoca avàFinita
- finisce %sAburtì a partitaPartita aburtitaClassica
@@ -793,7 +792,6 @@
Scusate :(Vi avemu messu fora pè un mumentu.
- A suspensione finisce %s.Perchè?Vulemu prupone una stonda di scacchi piacevule pè tutti.A tale scopu, vulemu assicurà chì tutti i ghjucadori seguitinu e bone pratiche.
diff --git a/translation/dest/site/cs-CZ.xml b/translation/dest/site/cs-CZ.xml
index 3727e06dc16de..33e929d581d47 100644
--- a/translation/dest/site/cs-CZ.xml
+++ b/translation/dest/site/cs-CZ.xml
@@ -301,7 +301,6 @@
Právě se hrajePrávě se hrajeDokončeno
- končí za %sZrušit hruHra byla zrušenaStandard
@@ -930,7 +929,6 @@
Omlouváme se :(Museli jsme tě odpojit na chvíli.
- Odpojení skončí za %s.Proč?Snažíme se všem poskytnout příjemnou hru.Proto se musíme ujistit, že se všichni chovají správně.
diff --git a/translation/dest/site/cv-CU.xml b/translation/dest/site/cv-CU.xml
index 4eb32853b0ef2..16aaf33455fed 100644
--- a/translation/dest/site/cv-CU.xml
+++ b/translation/dest/site/cv-CU.xml
@@ -144,7 +144,6 @@
СирХаллех пыраканВӗҫленнисем
- %s вӗҫленетВӑййине пӑрахӑҫлаВӑййине пӑрахӑҫланӑСтандартла
diff --git a/translation/dest/site/cy-GB.xml b/translation/dest/site/cy-GB.xml
index b1ab76132c588..8eb19543db6d9 100644
--- a/translation/dest/site/cy-GB.xml
+++ b/translation/dest/site/cy-GB.xml
@@ -210,7 +210,6 @@
Ar ei hannerChwarae ar hyn o brydWedi dod i ben.
- yn gorffen mewn %sErthylu gêmGêm wedi\'i herthyluNormal
diff --git a/translation/dest/site/da-DK.xml b/translation/dest/site/da-DK.xml
index 558ab1c873ab8..9e50258e2a4a5 100644
--- a/translation/dest/site/da-DK.xml
+++ b/translation/dest/site/da-DK.xml
@@ -175,7 +175,7 @@
Forum%1$s skrev i forum %2$sSeneste debatindlæg
- Skakspillere
+ SpillereVennerSamtalerI dag
@@ -248,8 +248,8 @@
Ranking opdateres hvert %s minut
- %s opgave
- %s opgaver
+ %s taktikopgave
+ %s taktikopgaverAntal partier spillet
@@ -272,7 +272,6 @@
Spilles lige nuSpilles lige nuAfsluttet
- afsluttes %sAfbryd partiPartiet blev afbrudtStandard
@@ -405,7 +404,7 @@
Importér partiNår du indsætter et partis PGN, får du en afspillelig gengivelse, en computeranalyse, en spilchat og en URL til deling.Varianter vil blive slettet. Hvis du vil beholde dem, skal du importere PGN\'en via et studie.
- Denne PGN kan tilgås af offentligheden. For at importere et parti privat, skal du bruge et studie.
+ Denne PGN kan tilgås af offentligheden. For at importere et parti privat, skal du bruge et studie.%s importerede spil%s importerede spil
@@ -507,7 +506,7 @@
Redigér profilFornavnEfternavn
- Indstil dit ikon:
+ Indstil dit ikonIkonDer er en indstilling til at skjule alle brugerikoner på tværs af hele webstedet.Biografi
@@ -529,7 +528,7 @@
Automatisk videre til næste spil efter trækAutoskift
- Opgaver
+ TaktikopgaverTurneringsvindereNavnBeskrivelse
@@ -862,9 +861,10 @@
og gem %s serie af forhåndstrækog gem %s serier af forhåndstræk
+ Du har modtaget en privat besked fra Lichess.
+ Klik her for at læse denBeklager :(Vi måtte give dig en timeout.
- Timeouten udløber %s.Hvorfor?Vi tilstræber at give alle en behagelig skakoplevelse.I den forbindelse må vi sikre, at alle spillere følger god praksis.
@@ -884,7 +884,8 @@
Jeg lover, at jeg vil overholde alle Lichess-politikker.Søg eller start ny diskussionRediger
- Lynskak
+ Bullet
+ BlitzRapidClassicalVanvittigt hurtige spil: mindre end 30 sekunder
diff --git a/translation/dest/site/de-DE.xml b/translation/dest/site/de-DE.xml
index f1273c355a6cd..66a7b8ce98a00 100644
--- a/translation/dest/site/de-DE.xml
+++ b/translation/dest/site/de-DE.xml
@@ -272,7 +272,6 @@
Partie läuftLaufende PartienBeendet
- endet in %sPartie abbrechenPartie abgebrochenStandard
@@ -405,7 +404,7 @@
Partie importierenWenn du ein PGN einfügst, hast du Zugriff auf eine Spielwiederholung, eine Computeranalyse, einen Spielchat und eine teilbare URL.Varianten werden gelöscht. Importiere die PGN mittels einer Studie, um sie zu behalten.
- Diese PGN ist öffentlich zugänglich. Nutze eine Studie, um eine Partie nur für dich zu importieren.
+ Diese PGN ist öffentlich zugänglich. Nutze eine Studie, um eine Partie nur für dich zu importieren.%s importierte Partie%s importierte Partien
@@ -507,9 +506,9 @@
Profil bearbeitenVornameNachname
- Setze deine Dekoration:
+ Setze dein FlairFlair
- Es gibt eine Einstellung, um alle persönlichen Stilelemente der Benutzer auf der gesamten Website zu verbergen.
+ Es existiert eine Einstellungsmöglichkeit, um alle Benutzerflairs auf der gesamten Seite zu verbergen.ProfiltextLand oder RegionVielen Dank!
@@ -663,10 +662,10 @@
SimultanschachVeranstalterFarbe des Ausrichters: %s
- Deine ausstehenden Simultan-Vorstellungen
+ Deine ausstehenden SimultanpartienNeue SimultanspieleEin Simultan veranstalten
- Melde dich an, um eine Simultan-Vorstellung zu geben, oder einer beizutreten
+ Melde dich an, um eine Simultanpartie auszutragen oder sich einer anzuschließenSimultan nicht gefundenDieses Simultan existiert nicht.Zurück zur Simultan Homepage
@@ -691,7 +690,7 @@
TastenkürzelZug zurück/vorzum Anfang/Ende
- Durch die ausgewählte Variation schalten
+ Durch die ausgewählte Variante schaltenzeige/verberge KommentareVariante wählen/verlassenHole dir eine Computer-Analyse, lerne aus deinen Fehlern
@@ -701,10 +700,10 @@
Die nächste UngenauigkeitVorherige VerzweigungNächste Verzweigung
- Variationspfeile an/auschalten
+ Variantenpfeile ein-/auschaltenDurch vorherige/nächste Variante schalten
- Glyph-Anmerkungen umschalten
- Mit Variationspfeilen navigierst du durch die Zugliste.
+ Schalten der Zeichen-Anmerkungen
+ Mit Variantenpfeilen navigierst du durch die Zugliste.den ausgewählten Zug ausführenNeues TurnierSchachturnier mit verschiedenen Zeitkontrollen und Schachvarianten
@@ -862,9 +861,10 @@
und speichere %s Variante im Vorausund speichere %s Varianten im Voraus
+ Du hast eine private Nachricht von Lichess erhalten.
+ Hier klicken zum LesenEntschuldigung :(Wir haben dich mit einer vorübergehenden Spielsperre belegt.
- Die Sperre endet in %s.Warum?Wir möchten allen eine möglichst gute Schach-Erfahrung bieten.Um dies zu erreichen, müssen wir sicherstellen, dass sich alle Spieler korrekt verhalten.
@@ -884,6 +884,7 @@
Ich stimme zu, dass ich allen Lichess-Richtlinien folgen werde.Suche oder beginne eine neue KonversationBearbeiten
+ BulletBlitzSchnellschachKlassisch
@@ -982,7 +983,7 @@ Leer lassen, um Partien von der normalen Ausgangsstellung aus zu starten.andere Farbe
Dein Benutzerkonto zu schließen wird auch deinen Einspruch zurückziehenUnsere Tipps für die Organisation von Veranstaltungen
- Anweisungen
+ AnleitungAlles zeigenLichess ist eine Wohltätigkeitsorganisation und eine völlig kostenlose/freie Open-Source-Software.
Alle Betriebskosten, Entwicklung und Inhalte werden ausschließlich durch Benutzerspenden finanziert.
diff --git a/translation/dest/site/el-GR.xml b/translation/dest/site/el-GR.xml
index e6322814e92f5..6343220af488a 100644
--- a/translation/dest/site/el-GR.xml
+++ b/translation/dest/site/el-GR.xml
@@ -270,7 +270,6 @@
Παιχνίδι σε εξέλιξη τώραΣε εξέλιξη τώραΟλοκληρωμένα
- τελειώνει %sΕγκαταλείψτε το παιχνίδιΠαιχνίδι εγκατελήφθηΚανονικό
@@ -847,7 +846,6 @@
Λυπούμαστε :(Πρέπει να σας αποκλείσουμε για λίγο.
- Ο αποκλεισμός λήγει σε %s.Γιατί;Ο σκοπός μας είναι να προσφέρουμε μια ευχάριστη σκακιστική ατμόσφαιρα για όλους.Για να πραγματοποιηθεί αυτό, πρέπει να βεβαιωθούμε ότι όλοι οι παίκτες ακολουθούν τους κανόνες.
@@ -867,6 +865,7 @@
Αποδέχομαι ότι θα συμμορφωθώ με όλες τις πολιτικές του Lichess.Αναζήτηση ή έναρξη νέας συνομιλίαςΕπεξεργασία
+ BulletBlitzRapidΚλασικό
diff --git a/translation/dest/site/en-US.xml b/translation/dest/site/en-US.xml
index 2f9d051e4122c..0667db2b56dfe 100644
--- a/translation/dest/site/en-US.xml
+++ b/translation/dest/site/en-US.xml
@@ -272,7 +272,6 @@
Playing right nowPlaying right nowFinished
- finishes %sAbort gameGame abortedStandard
@@ -406,7 +405,7 @@
Paste a game PGN to get a browsable replay,
computer analysis, game chat and shareable URL.Variations will be erased. To keep them, import the PGN via a study.
- This PGN can be accessed by the public. To import a game privately, use a study.
+ This PGN can be accessed by the public. To import a game privately, use a study.%s imported game%s imported games
@@ -508,7 +507,7 @@ computer analysis, game chat and shareable URL.
Edit profileFirst nameLast name
- Set your flair:
+ Set your flairFlairThere is a setting to hide all user flairs across the entire site.Biography
@@ -863,9 +862,10 @@ computer analysis, game chat and shareable URL.
and save %s premove lineand save %s premove lines
+ You have received a private message from Lichess.
+ Click here to read itSorry :(We had to time you out for a while.
- The timeout expires %s.Why?We aim to provide a pleasant chess experience for everyone.To that effect, we must ensure that all players follow good practice.
@@ -885,6 +885,7 @@ computer analysis, game chat and shareable URL.
I agree that I will follow all Lichess policies.Search or start new discussionEdit
+ BulletBlitzRapidClassical
diff --git a/translation/dest/site/eo-UY.xml b/translation/dest/site/eo-UY.xml
index 918cc6ad0c857..0421064a723c7 100644
--- a/translation/dest/site/eo-UY.xml
+++ b/translation/dest/site/eo-UY.xml
@@ -70,6 +70,7 @@
Ĉefigi linionForigi ekde tie ĉiDevigi variaĵon
+ Kopi varianto PGNMovoMalvenka variaĵoVenka variaĵo
@@ -271,7 +272,6 @@
Ludate nunLudantaFinite
- finiĝos %sĈesigi ludonLudo estis ĉesigitaNormale
@@ -505,6 +505,8 @@
Redakti profilonPersona nomoFamilia nomo
+ Agordi viaj emoĝio
+ EmoĝioBiografioDankon!Ligiloj al sociaj retejoj
@@ -732,6 +734,7 @@
Kun amikojKun ĉiujInfana reĝimo
+ Infana reĝimo estas enŝaltita.Ĉi tio temas pri sekureco. En infana reĝimo, ĉiuj retejaj komunikadoj estas malebligitaj. Aktivigu ĉi tiun por viaj infanoj kaj lernejaj studentoj, por protekti ilin de aliaj retaj uzantoj.En infana reĝimo, la lichess-emblemo ekhavas %s-emblemon, por ke vi sciu ke viaj infanoj sekuras.Via konto estas mastrumita. Petu vian ŝako-instruiston pri malaktivigado de \"infana reĝimo\".
@@ -845,9 +848,10 @@
kaj savi %s antaŭmovan viconkaj savi %s antaŭmovajn vicojn
+ Vi ricevis privatan mesaĝon de Lichess.
+ Alklaku ĉi tie por legi ĝinBedaŭrinde :(Ni devis provizore suspendi vin.
- La suspendo finiĝos %s.Kial?Nia celo estas provizi plaĉan ŝakan sperton al ĉiuj.Pro tio, ni devas certigi ke ĉiuj ludantoj sekvas bonna etiketon.
@@ -867,6 +871,7 @@
Mi konsentas sekvi ĉiujn politikojn de Lichess.Serĉi aŭ komenci novan konversacionRedakti
+ BulletBlitzRapidaKlasika
diff --git a/translation/dest/site/es-ES.xml b/translation/dest/site/es-ES.xml
index d2d80aa2bc8b3..b409ddbca4976 100644
--- a/translation/dest/site/es-ES.xml
+++ b/translation/dest/site/es-ES.xml
@@ -272,7 +272,6 @@
En juego ahora mismoSe está jugando ahoraTerminado
- termina %sCancelar partidaPartida canceladaEstándar
@@ -405,7 +404,7 @@
Importar partidaAl pegar el PGN de una partida, se obtiene una repetición navegable, un análisis por ordenador, un chat de juego y un enlace para compartir.Las variaciones serán borradas. Para mantenerlas, importa el PGN a través de un estudio.
- Este PGN puede ser accedido públicamente. Para importar un juego privadamente, utiliza un estudio.
+ Este PGN es de acceso público. Para importar una partida de forma privada, utiliza un estudio.%s partida importada%s partidas importadas
@@ -507,7 +506,7 @@
Editar perfilNombreApellido
- Configura tu entorno:
+ Configura tu entornoEntornoExiste una opción para ocultar la configuración de entorno en todo el sitio.Biografía
@@ -862,9 +861,10 @@
y ahorra %s línea de premovimientoy ahorra %s líneas de premovimiento
+ Has recibido un mensaje privado de Lichess.
+ Haz clic aquí para leerloLo sentimos :(Hemos tenido que suspenderte temporalmente.
- La suspensión expira en %s.¿Por qué?Nuestro objetivo es proporcionar una experiencia agradable a todo el mundo.Para ello, debemos asegurarnos de que todos los jugadores se comportan como es debido.
@@ -884,6 +884,7 @@
Me comprometo a seguir las normas de Lichess.Buscar o empezar una nueva conversaciónEditar
+ BalaBlitzRápidaClásica
diff --git a/translation/dest/site/et-EE.xml b/translation/dest/site/et-EE.xml
index 0d9bdc80d9642..d83c3c28b53e1 100644
--- a/translation/dest/site/et-EE.xml
+++ b/translation/dest/site/et-EE.xml
@@ -262,7 +262,6 @@
Mängimas praeguPraegu mängimasLõpetatud
- lõppeb %sKatkesta mängMäng katkestatudStandard
@@ -826,7 +825,6 @@ arvutianalüüsi, mängu jututoa ning jagatava URL-i.
Vabandust :(Me pidime su natukeseks ära blokeerima.
- Blokeering aegub %s.Miks?Me tahame pakkuda häid malekogemusi kõigile.Selleks peame olema kindlad, et kõik meie kasutajad järgivad reegleid.
diff --git a/translation/dest/site/eu-ES.xml b/translation/dest/site/eu-ES.xml
index 6316250c4ac8b..b9e3084921f31 100644
--- a/translation/dest/site/eu-ES.xml
+++ b/translation/dest/site/eu-ES.xml
@@ -272,7 +272,6 @@
Oraintxe jokatzenOraintxe jokatzenAmaituta
- bukaera: %sPartida geldiaraziGeldiarazitako partidaOhikoa
@@ -405,7 +404,7 @@
Partida inportatuPGN partida bat itsastean ikusi daitekeen partida bat lortuko duzu, partidare eta analisiarekin, txatarekin eta elkarbanatu dezakezun helbide batekin.Aldaerak ezabatu egingo dira. Mantendu nahi badituzu inportatu PGNa azterlan gisa.
- PGN hau edonork deskargatu dezake. Partida bat era pribatuan inportatzeko azterlan bat erabili behar duzu.
+ PGN hau edonork deskargatu dezake. Partida bat era pribatuan inportatzeko azterlan bat erabili behar duzu.%s inportatutako partidak%s inportatutako partidak
@@ -507,7 +506,8 @@
Nire profila editatuIzenaAbizena
- Ezarri zure iruditxoa:
+ Ezarri zure iruditxoa
+ IruditxoaWebgune guztian zehar erabiltzaile guztien iruditxoak ezkutatzeko ezarpen bat dago.BiografiaHerrialdea
@@ -747,6 +747,7 @@
LagunekinEdonorekinHaurren modua
+ Haur-modua aktibatuta dago.Hau segurtasunari buruzkoa da. Haurren moduan, webguneko komunikazio guztiak desaktibatuta daude. Aktibatu zure haur eta ikasleei beste Internet erabiltzaileengandik babesteko.Haurren moduan, lichess logoak %s ikonoa du, haurrak seguru daudela jakin dezazun.Zure kontua beste norbaitek kudeatzen du. Eskatu zure irakasleari haur modua desaktibatzeko.
@@ -860,9 +861,10 @@
eta aurrejokaldi linea %s gordeeta %s aurrejokaldi linea gorde
+ Lichessek bidalitako mezu pribatu bat jaso duzu.
+ Egin klik hemen irakurtzekoBarkatu :(Denbora tarte baterako bota egin behar izan zaitugu.
- Kanporaketaren amaiera: %s.Zergatik?Guztioi xakean jokatzeko aukera atsegina eskaintzea da gure helburua.Horretarako, jokalari guztiek praktika onak jarraitzen dituztela ziur izan behar dugu.
@@ -882,6 +884,7 @@
Lichessek ezarritako politikak beteko ditut.Hizketaldia bilatu edo berria hasiAldatu
+ BulletBlitzAktiboaEstandarra
diff --git a/translation/dest/site/fa-IR.xml b/translation/dest/site/fa-IR.xml
index d7b9dc27fd610..cfce911640e73 100644
--- a/translation/dest/site/fa-IR.xml
+++ b/translation/dest/site/fa-IR.xml
@@ -271,7 +271,6 @@
بازی در حال انجامبازی در حال انجامتمام شده
- به پایان میرسد %sانصراف از بازیبازی لغو شداستاندارد
@@ -404,7 +403,7 @@
بارگذاری بازیدر صورت بارگذاری فایل PGN،آنالیز کامپیوتری و لینک قابل به اشتراک گذاری در اختیار شما قرار خواهد گرفت.تغییرات پاک خواهند شد. برای حفظ آنها، PGN را از طریق مطالعه وارد کنید.
- این PGN برای عموم در دسترس است، برای وارد کردن خصوصی یک بازی، یک مطالعه ایجاد کنید.
+ این PGN برای عموم در دسترس است، برای وارد کردن خصوصی یک بازی، یک مطالعه ایجاد کنید.%s بارگزاری شده%s بارگزاری شده
@@ -506,7 +505,7 @@
ویرایش پروفایلنامنام خانوادگی
- تعیین کردن شکلک:
+ تعیین کردن شکلکتنظیماتی برای مخفی کردن همه شکلکهای کاربر در کل ویگاه وجود دارد.زندگی نامهکشور یا منطقه
@@ -859,9 +858,10 @@
و پیش حرکت %s را حفظ کنیدو پیش حرکت های %s را حفظ کنید
+ شما یک پیام خصوصی از لیچس دریافت کردهاید.
+ برای خواندن آنها اینجا کلیک کنیدمتاسفم :(شما برای مدتی مسدود شدید.
- تعداد %s به پایان رسیدن زمان.چرا؟هدف ما مهیا ساختن تجربه لذت بخش شطرنج به همه افراد است.به همین منظور، ما باید اطمینان حاصل کنیم که تمام بازیکنان تمرین خوب را دنبال میکنند.
@@ -881,6 +881,7 @@
من تضمین میکنم که به تمام قوانین و خط مشی های لیچس پایبند باشم .جستجو یا شروع کردن مکالمه جدیدویرایش
+ بولتبرقآساسریعکلاسیک
diff --git a/translation/dest/site/fi-FI.xml b/translation/dest/site/fi-FI.xml
index 037bab5f554ef..91ba6b6fa82d3 100644
--- a/translation/dest/site/fi-FI.xml
+++ b/translation/dest/site/fi-FI.xml
@@ -272,7 +272,6 @@
Pelaamassa juuri nytParhaillaan menossaPäättynyt
- päättyy %sKeskeytä peliPeli keskeytettyTavallinen
@@ -405,7 +404,7 @@
Tuo peliLiitä pelin PGN, niin voit selata peliä ja saat sille tietokoneanalyysin, keskusteluhuoneen sekä URL:n, jonka voit jakaa.Muunnelmat poistetaan. Jos haluat säilyttää ne, tuo PGN ensin tutkielmaan.
- Tähän PGN:ään on julkinen pääsy. Käytä tutkielmaa halutessasi tuoda pelin ja pitää sen yksityisenä.
+ Tähän PGN:ään on julkinen pääsy. Käytä tutkielmaa halutessasi tuoda pelin ja pitää sen yksityisenä.%s Tuotua peliä%s Tuotua peliä
@@ -507,7 +506,7 @@
Muokkaa profiiliaEtunimiSukunimi
- Valitse tyylisi:
+ Valitse tyylisiOn olemassa asetus, jolla voit piilottaa kaikkien käyttäjien tyylit koko sivustolla.KuvausMaa tai alue
@@ -859,9 +858,10 @@
ja tallenna %s esisiirtolinjaja tallenna %s esisiirtolinjaa
+ Olet saanut henkilökohtaisen viestin Lichessiltä.
+ Lue se napsauttamalla tästäPahoittelumme :(Meidän täytyi komentaa sinut jäähylle hetkeksi.
- Jäähy päättyy %s.Miksi?Haluamme tarjota mukavan shakkielämyksen kaikille.Siksi meidän on huolehdittava siitä, että kaikki käyttäytyvät asiallisesti.
@@ -881,6 +881,7 @@
Vakuutan että noudatan Lichessin sääntöjä.Hae tai aloita uusi keskusteluMuokkaa
+ BulletPikapeliNopeaKlassinen
@@ -919,7 +920,7 @@
Olet hävinnyt Lichessin käyttöehtoja rikkoneelle henkilölleHyvitys: %1$s %2$s vahvuuslukupistettä.Aika on melkein lopussa!
- [Paljasta sähköpostiosoite napsauttamalla tätä]
+ [Paljasta sähköpostiosoite napsauttamalla tästä]LataaValmentaja-asetuksetStriimausasetukset
diff --git a/translation/dest/site/fo-FO.xml b/translation/dest/site/fo-FO.xml
index 4a2d3b5123082..f0b1e0913c315 100644
--- a/translation/dest/site/fo-FO.xml
+++ b/translation/dest/site/fo-FO.xml
@@ -231,7 +231,6 @@
Verður telvað beint núTelvað beint núLiðugt
- endar %sEnda talviðTalvið varð brotið avVanligt
@@ -769,7 +768,6 @@
Orsaka :(Vit noyddust at geva tær leikbrá eina tíð.
- Leikbráið gongur út %s.Hví?Vit miða ímóti at veita øllum eina góða talvuppliving.Tískil mugu vit vissa okkum, at allir telvarar sýna góðan atburð.
diff --git a/translation/dest/site/fr-FR.xml b/translation/dest/site/fr-FR.xml
index ad9401539ea22..a633eb6e2c5eb 100644
--- a/translation/dest/site/fr-FR.xml
+++ b/translation/dest/site/fr-FR.xml
@@ -272,7 +272,6 @@
En coursParties en coursTerminé
- se termine dans %sAnnuler la partiePartie annuléeStandard
@@ -405,7 +404,7 @@
Importer une partieQuand vous collez une partie en PGN vous pouvez la rejouer, consulter l\'analyse de l\'ordinateur, utiliser le tchat et partager le lien.Les variantes seront effacées. Pour les conserver, importez le PGN dans une étude.
- Cette partie en format PGN peut être vue en public. Pour importer une partie en privé, utilisez une étude.
+ Cette partie en format PGN n\'est pas privée. Pour importer une partie en privé, utilisez une étude.%s partie importée%s parties importées
@@ -507,8 +506,8 @@
Modifier le profilPrénomNom
- Choisissez votre émoji :
- Flair
+ Choisir votre émoji
+ ÉmojiUn paramètre permet de cacher les émojis d\'un utilisateur sur tout le site.BiographiePays ou région
@@ -691,6 +690,7 @@
Raccourcis clavieravancer/reculeraller au début/à la fin
+ Changer de variantemontrer/cacher les annotationsentrer dans/sortir d\'une varianteDemandez une analyse informatique, apprenez de vos erreurs
@@ -861,9 +861,10 @@
et enregistrer %s variante de précoupset enregistrer %s variantes de précoups
+ Vous avez reçu un message privé de Lichess.
+ Cliquez ici pour le lire.Désolé :(Nous avons dû temporairement vous suspendre.
- Le délai d\'attente expire dans %s.Pourquoi ?Nous souhaitons procurer à chacun une agréable expérience du jeu d\'échecs.Dans ce but, nous devons veiller à ce que tous les joueurs adoptent les bonnes pratiques.
@@ -883,6 +884,7 @@
Je m\'engage à respecter toutes les règles de Lichess.Rechercher ou démarrer une nouvelle conversationÉditer
+ BulletBlitzRapideClassique
diff --git a/translation/dest/site/fy-NL.xml b/translation/dest/site/fy-NL.xml
index f536138464c94..a808837195d1b 100644
--- a/translation/dest/site/fy-NL.xml
+++ b/translation/dest/site/fy-NL.xml
@@ -193,7 +193,6 @@
No oan it spyljeNo dwaandeDien
- dien %sFerlit it spulSpul ferlitteStandert
diff --git a/translation/dest/site/ga-IE.xml b/translation/dest/site/ga-IE.xml
index 088329a682443..9ed7c99e9f037 100644
--- a/translation/dest/site/ga-IE.xml
+++ b/translation/dest/site/ga-IE.xml
@@ -306,7 +306,6 @@
Á imirt anoisÁ imirt anoisCríochnaithe
- ag críochnú i %sÉirigh asÉiríodh as an chluicheCaighdeán
@@ -949,7 +948,6 @@ anailís ríomhaire, comhrá cluiche agus URL inroinnte.
Tá brón orainn :(Bhí orainn tú a chur ar fionraí ar feadh tréimhse.
- Beidh an tréimhse fionraithe thart %s.Cén fáth?Ba mhaith linn atmaisféar maith fichille a chur ar fáil do chách.De bhrí sin, ní mór dúinn a chinntiú go leanann gach ficheallaí dea-chleachtas.
diff --git a/translation/dest/site/gl-ES.xml b/translation/dest/site/gl-ES.xml
index b68042391bb4f..68bdfcd29b2cb 100644
--- a/translation/dest/site/gl-ES.xml
+++ b/translation/dest/site/gl-ES.xml
@@ -272,7 +272,6 @@
Xogando agora mesmoXogando agora mesmoFinalizado
- remata %sAbortar partidaPartida abortadaEstándar
@@ -405,7 +404,7 @@
Importar partidaPega o PGN dunha partida para obter unha versión navegable, análise por ordenador, sala de conversa e unha ligazón pública para compartila.As variantes borraranse. Pra conservalas, importa o PGN mediante un estudo.
- Este PGN é de acceso público. Para importar unha partida de xeito privado, emprega un estudo.
+ Este PGN é de acceso público. Para importar unha partida de xeito privado, emprega un estudo.%s partida importada%s partidas importadas
@@ -507,7 +506,7 @@
Editar perfilNomeApelido(s)
- Escolle a túa habelencia:
+ Escolle a túa habelenciaHabelenciaNas preferencias podes agochar por completo as habelencias dos xogadores en todo o sitio.Biografía
@@ -774,7 +773,7 @@
Análise da partida%1$s crea %2$s%1$s únese a %2$s
- a %1$s gústalle %2$s
+ A %1$s gústalle %2$sEmparellamento rápidoRetosAnónimo
@@ -860,9 +859,10 @@
e garda %s variante de premovementose garda %s variantes de premovementos
+ Recibiches unha mensaxe privada de Lichess.
+ Fai clic aquí para lelaSentímolo :(Tivemos que suspenderte temporalmente.
- A suspensión expira %s.Por que?O noso obxectivo é proporcionar unha experiencia amena no xadrez pra todo o mundo.Para iso, debemos asegurarnos de que todos os xogadores se comportan como é debido.
@@ -882,6 +882,7 @@
Comprométome a seguir as normas de Lichess.Busca ou comeza unha nova conversaEditar
+ BalaLóstregoRápidasClásicas
diff --git a/translation/dest/site/gsw-CH.xml b/translation/dest/site/gsw-CH.xml
index 6fe773cf57949..70704bd3c4bef 100644
--- a/translation/dest/site/gsw-CH.xml
+++ b/translation/dest/site/gsw-CH.xml
@@ -273,7 +273,6 @@ Au vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multiko
Partie isch am laufeLauft jetztBeändet
- ändet %sPartie abbrächePartie abbrocheStandard
@@ -406,7 +405,7 @@ Au vum Erschtelle vu mehrere Konte wird dringend abgrate - übermässigs Multiko
Partie importiereFüeg e Schpiel-PGN i, für Zuegriff uf Schpielwiderholig, Computeranalyse, Chat und e teilbari URL.d\'Variazione werded glöscht. Zums b\'halte, muesch d\'PGN mit ere Schtudie importiere.
- De PGN isch öffentlich zuegänglich. Zum es Schpiel privat importiere, nimmsch e Schtudie.
+ De PGN isch öffentlich zuegänglich. Zum es Schpiel privat importiere, nimmsch e Schtudie.%s importierti Partie%s importierti Partie
@@ -509,7 +508,7 @@ zum bewise dass du en Mänsch bisch.
Profil bearbeiteVornameNachname
- Wähl dis Emoji:
+ Wähl dis EmojiEmojiAlli Benutzer-Emojis chönnd - uf de ganze Site - usbländet werde.Biografie
@@ -871,9 +870,10 @@ gits nöd und es isch immer ungwertet.
und speicher %s Voruszug Zileund speicher %s Voruszug Zile
+ Lichess hät dir e privati Nachricht g\'schickt.
+ Klick da zum läseÄxgüsi :(Mir händ dich für es Zitli müesse schperre.
- Die Schperrig ändet in %s.Wieso?Mir wänd allne e möglichscht gueti Schach-Erfahrig büte.Zum dem Zwäck müend mir sicher si, dass sich all Schpiller korräkt verhalted.
@@ -893,6 +893,7 @@ gits nöd und es isch immer ungwertet.
Ich schtimme zue, dass ich alli Lichess-Richtlinie befolge.Suech e Underhaltig oder fang e Neui aBearbeite
+ BulletBlitzSchnällschachKlassisch
diff --git a/translation/dest/site/gu-IN.xml b/translation/dest/site/gu-IN.xml
index 5775ea0e763b2..99f176efb76e4 100644
--- a/translation/dest/site/gu-IN.xml
+++ b/translation/dest/site/gu-IN.xml
@@ -123,6 +123,7 @@
મેમરીઅનંત વિશ્લેષણગહનતાનિ મર્યાદા હટાવે, અને તમારા કમ્પ્યુટરને ગરમ રાખે છે
+ એન્જિન મેનેજરમોટી ભૂલભૂલચૂક
@@ -150,6 +151,7 @@
પૂર્ણ કદ જુઓસાઇન આઉટ કરોસાઇન ઇન કરો
+ મને લોગીન કરેલો રાખોતમારે તે કરવા માટે એક ખાતાનિ જરૂર છેનોંધણીકોમ્પ્યૂટરો અને કોમ્પ્યુટર ની મદદ લેનારા ખેલાડીઓને રમવા માટે મંજૂરી નથી. રમતી વખતે મહેરબાની કરીને શતરંજના મશીનો, ડેટાબેઝો, અથવા બીજા ખેલાડીઓ દ્વારા મદદ ના લેવી. એ પણ નોંધ લેશો કે બહુવિધ ખાતા બનાવવાનું સખત નિરુત્સાહ છે અને અતિશય બહુવિધ ખાતા બનાવવા પર પ્રતિબંધ મૂકવામાં આવશે.
@@ -178,6 +180,10 @@
%s કલાક%s કલાક
+
+ %s મિનિટ
+ %s મિનિટ
+ સમયરેટિંગરેટિંગ નો આલેખ
@@ -194,6 +200,15 @@
ઇમેઇલપાસવર્ડ રીસેટપાસવર્ડ ભૂલી ગયાં?
+ આ પાસવર્ડ અત્યંત સામાન્ય છે અને અનુમાન લગાવવા માટે ખૂબ જ સરળ છે.
+ કૃપા કરીને તમારા વપરાશકર્તા નામનો ઉપયોગ તમારા પાસવર્ડ તરીકે કરશો નહીં.
+ તમે બીજી સાઇટ પર સમાન પાસવર્ડનો ઉપયોગ કર્યો છે અને તે સાઇટ સાથે ચેડા કરવામાં આવ્યા છે. તમારા લિચેસ એકાઉન્ટની સલામતી સુનિશ્ચિત કરવા માટે, અમારે તમારે નવો પાસવર્ડ સેટ કરવાની જરૂર છે. તમારી સમજ બદલ આભાર.
+ તમે લિચેસ છોડી રહ્યા છો
+ અન્ય સાઇટ પર તમારો લિચેસ પાસવર્ડ ક્યારેય ટાઇપ કરશો નહીં!
+ %s પર આગળ વધો
+ કોઈ બીજા દ્વારા સૂચવવામાં આવેલ પાસવર્ડ સેટ કરશો નહીં. તેઓ તેનો ઉપયોગ તમારું એકાઉન્ટ ચોરી કરવા માટે કરશે.
+ કોઈ બીજા દ્વારા સૂચવવામાં આવેલ ઈમેલ સરનામું સેટ કરશો નહીં. તેઓ તેનો ઉપયોગ તમારું એકાઉન્ટ ચોરી કરવા માટે કરશે.
+ ઈમેલની ખાતરીમાં મદદ કરશોશ્રેણીશ્રેણી %sરમતો રમ્યા
@@ -210,7 +225,6 @@
અત્યારે રમાય છેઅત્યારે રમાય છેસમાપ્ત થઈ
- %s માં પૂરી થાય છેરમતનો ત્યાગ કરોરમતનો ત્યાગ કર્યોસામાન્ય
diff --git a/translation/dest/site/he-IL.xml b/translation/dest/site/he-IL.xml
index 126a85129114b..2fff6b2dfbbc4 100644
--- a/translation/dest/site/he-IL.xml
+++ b/translation/dest/site/he-IL.xml
@@ -302,7 +302,6 @@
מתקיים עכשיומתקיים עכשיוהסתיים
- נגמר %sביטול המשחקהמשחק בוטלרגיל
@@ -461,7 +460,7 @@
ייבוא משחקכשמדביקים משחק בפורמט PGN מקבלים אפשרות לצפות במשחק ולדפדף בו, ניתוח ממוחשב, צ׳אט וקישור לשיתוף.וריאציות — כלומר רצפי מהלכים שאינם המסעים הראשיים (mainline) — יימחקו. כדי לשמור אותן, ייבאו את ה־PGN כלוח למידה.
- ה-PGN הזה הוא ציבורי. כדי לייצא משחק באופן פרטי, השתמשו בלוח למידה.
+ ה־PGN הזה זמין לציבור. כדי לייצא את המשחק באופן פרטי, השתמשו בלוח למידה.משחק מיובא %s%s משחקים מיובאים
@@ -573,7 +572,7 @@
עריכת פרופילשם פרטישם משפחה
- הגדירו את הסמליל שלכם:
+ הגדירו את הסמליל שלכםסמלילישנה הגדרה שמאפשרת להסתיר את כל הסמלילים באתר.ביוגרפיה
@@ -946,9 +945,10 @@
ושמרו %s המשכים מוגדרים מראשושמרו %s המשכים מוגדרים מראש
+ קיבלתם הודעה פרטית מ-Lichess.
+ לחצו כאן כדי לקרוא אותהמצטערים :(נאלצנו להשעות אותך לזמן מה.
- ההשעיה תסתיים %s.למה?אנחנו מנסים לספק חווית שח נעימה לכולם.בעקבות זאת, אנחנו חייבים לוודא שכל השחקנים ינהגו בכבוד.
@@ -968,6 +968,7 @@
אני מסכימ/ה לציית לכל מדיניות של Lichess.חפשו את התחילו שיחה חדשהעריכה
+ BulletBlitzRapidClassical
diff --git a/translation/dest/site/hi-IN.xml b/translation/dest/site/hi-IN.xml
index d38950154282d..7a9a01d96a3a7 100644
--- a/translation/dest/site/hi-IN.xml
+++ b/translation/dest/site/hi-IN.xml
@@ -264,7 +264,6 @@
अभी खेला जा रहाअभी खेला जा रहासमाप्त
- %s में खत्मखेल रद्द करेंखेल रद्द किया गयासाधारण
@@ -831,7 +830,6 @@
खेद :(हमें आपको कुछ समय के लिए प्रतिबंधित करना पड़ा।
- आपका प्रतिबंध %s समाप्त हो जाएगा।क्यों?हम सभी के लिए एक सुखद शतरंज अनुभव प्रदान करना चाहते हैं।इसलिए, हमें यह सुनिश्चित करना होगा कि सभी खिलाड़ी अच्छे अभ्यास का पालन करें।
diff --git a/translation/dest/site/hr-HR.xml b/translation/dest/site/hr-HR.xml
index e2f38065e7a06..a38e3c960d881 100644
--- a/translation/dest/site/hr-HR.xml
+++ b/translation/dest/site/hr-HR.xml
@@ -278,7 +278,6 @@
Upravo igrajuUpravo igrajuZavršeno
- završava %sPrekini igruIgra prekinutaStandardno
@@ -868,7 +867,6 @@ računalnu analizu, chat partije i URL za dijeljenje.
Oprosti :(Trebali smo te na neko vrijeme izbaciti.
- Vrijeme izbačaja ističe za %s.Zašto?Nama je u cilju da pružimo ugodno šahovsko iskustvo.Stoga moramo osigurati da svi igrači dobro postupaju.
diff --git a/translation/dest/site/hu-HU.xml b/translation/dest/site/hu-HU.xml
index 448b5a8854aa9..6b5649c9a4e5a 100644
--- a/translation/dest/site/hu-HU.xml
+++ b/translation/dest/site/hu-HU.xml
@@ -271,7 +271,6 @@
Játszma folyamatbanÉppen zajlikBefejezett
- véget ér: %sJátszma elvetéseJátszma elvetveHagyományos
@@ -846,7 +845,6 @@
SajnáljukKénytelenek vagyunk egy kis időre visszatartani.
- Feloldás %s múlva.Miért?Célunk mindenki számára jó felhasználói élményt biztosítani.Ennek érdekében minden játékosnak követnie kell megfelelő magatartást.
diff --git a/translation/dest/site/hy-AM.xml b/translation/dest/site/hy-AM.xml
index 36439dbd7af85..4161b205fa811 100644
--- a/translation/dest/site/hy-AM.xml
+++ b/translation/dest/site/hy-AM.xml
@@ -271,7 +271,6 @@
Այս պահին խաղում ենԽաղում են այս պահինԱվարտվել է
- ավարտվում է %sԿասեցնել խաղըԽաղը կասեցված էՍտանդարտ
@@ -845,7 +844,6 @@
Ներողություն :(Մենք ստիպված ենք անջատել Ձեզ որոշ ժամանակով։
- Դուք կարող եք վերադառնալ %s անց։Ինչու՞Մեր նպատակը շախմատը բոլորի համար հետաքրքիր դարձնելն է։Դրան հասնելու համար մենք պետք է այնպես անենք, որ բոլոր խաղացողները հետևեն բարեկրթության կանոններին։
diff --git a/translation/dest/site/ia-IA.xml b/translation/dest/site/ia-IA.xml
index 3303a90060748..0579318b3d6e0 100644
--- a/translation/dest/site/ia-IA.xml
+++ b/translation/dest/site/ia-IA.xml
@@ -203,7 +203,6 @@
Jocante ora mesmoJocante ora mesmoFinite
- fini %sCancellar partitaPartita cancellateStandard
diff --git a/translation/dest/site/id-ID.xml b/translation/dest/site/id-ID.xml
index 05229b3554ffa..05bbfd1f3825a 100644
--- a/translation/dest/site/id-ID.xml
+++ b/translation/dest/site/id-ID.xml
@@ -250,7 +250,6 @@
Bermain saat iniMainkan sekarangSelesai
- berakhir dalam %sBatalkan permainanPermainan dibatalkanStandar
@@ -791,7 +790,6 @@
Maaf :(Kami harus mengatur waktu Anda untuk sementara waktu.
- Batas waktu berakhir %s lagi.Mengapa?Kami bertujuan dan menjaga untuk memberikan pengalaman catur yang menyenangkan bagi semua orang.Untuk itu, kami harus memastikan bahwa semua pemain harus mengikuti perlakuan yang baik.
diff --git a/translation/dest/site/is-IS.xml b/translation/dest/site/is-IS.xml
index a0eab46bafd5d..37200563c228f 100644
--- a/translation/dest/site/is-IS.xml
+++ b/translation/dest/site/is-IS.xml
@@ -236,7 +236,6 @@
Spilandi núnaSpilandi þessa stundinaLokið
- lýkur eftir %sHverfa frá leikHætt við skákStaðlað
@@ -738,7 +737,6 @@
Leika %sAfsakaðu :(Við þurftum að setja þig í stutt leikbann.
- Leikbannið rennur út eftir: %s.Afhverju?Hvernig er hægt að komast hjá þessu?Tefldu allar skákir sem þú byrjar.
diff --git a/translation/dest/site/it-IT.xml b/translation/dest/site/it-IT.xml
index 9e52d5f97e0e8..711c138c0f2bc 100644
--- a/translation/dest/site/it-IT.xml
+++ b/translation/dest/site/it-IT.xml
@@ -272,7 +272,6 @@
Partita in corsoIn corsoTerminati
- finisce %sInterrompi la partitaPartita interrottaStandard
@@ -406,6 +405,7 @@
Quando incolli una partita tramite PGN potrai rivederla,
analizzarla con il computer, commentarla in chat, e condividerla tramite un indirizzo URL.Le varianti saranno cancellate. Per salvarle, importa il PGN in uno studio.
+ Questo PGN è accessibile pubblicamente. Per importare una partita privatamente, utilizza uno studio.%s partita importata%s partite importate
@@ -486,6 +486,7 @@ analizzarla con il computer, commentarla in chat, e condividerla tramite un indi
Mosse giocateIl Bianco vinceIl Nero vince
+ Tasso di pareggioPatteProssimo torneo %s:Punteggio medio degli avversari
@@ -506,6 +507,9 @@ analizzarla con il computer, commentarla in chat, e condividerla tramite un indi
Modifica profiloNomeCognome
+ Imposta la tua icona
+ Icona
+ Esiste un\'impostazione per nascondere le icone di tutti gli utenti, sull\'intero sito.BiografiaNazione o regioneGrazie!
@@ -744,6 +748,7 @@ analizzarla con il computer, commentarla in chat, e condividerla tramite un indi
Con gli amiciCon tuttiModalità bambino
+ La modalità bambini è attiva.Questa modalità riguarda la sicurezza: in modalità bambino tutte le comunicazioni sono disabilitate. Si consiglia di attivare questa modalità per bambini e studenti, in modo da proteggerli dagli altri utenti.In \"modalità bambino\", al logo di lichess viene aggiunto %s, in questo modo sai che il bambino è sicuro.Il tuo account è gestito esternamente. Chiedi al tuo istruttore di disattivare la \"modalità bambino\".
@@ -857,9 +862,10 @@ analizzarla con il computer, commentarla in chat, e condividerla tramite un indi
e salva %s linea pre-mossae salva %s linee pre-mossa
+ Hai ricevuto un messaggio privato da Lichess.
+ Clicca qui per leggerloCi dispiace :(Abbiamo dovuto bloccarti per un po\' di tempo.
- Sarai sbloccato %s.Perché?Vogliamo offrire a tutti una esperienza di scacchi piacevole.A tal fine, dobbiamo assicurarci che tutti i giocatori si comportino bene.
@@ -879,6 +885,7 @@ analizzarla con il computer, commentarla in chat, e condividerla tramite un indi
Dichiaro di acconsentire a tutte le politiche di Lichess.Cerca o inizia una nuova conversazioneModifica
+ BulletBlitzRapidClassical
diff --git a/translation/dest/site/ja-JP.xml b/translation/dest/site/ja-JP.xml
index 07bd81581433f..8fe45c1d515ca 100644
--- a/translation/dest/site/ja-JP.xml
+++ b/translation/dest/site/ja-JP.xml
@@ -257,7 +257,6 @@
対局中対局中終了したトーナメント
- 終了は %s対局を中止する対局を中止しましたスタンダード
@@ -378,7 +377,7 @@
ゲームの PGN を貼りつけると、ブラウザ上でのリプレイ、
コンピュータ解析、ゲームチャット、共有可能 URL が得られます。変化手順は消えます。残したい場合は研究を経由して PGN をインポートしてください。
- この PGN はすべての人に公開されます。非公開の状態で棋譜をインポートするには「研究」機能でどうぞ。
+ この PGN はすべての人に公開されます。非公開の状態で棋譜をインポートするには「研究」機能でどうぞ。%s 局をインポート
@@ -475,7 +474,8 @@
プロフィールの編集名姓
- フレアを設定:
+ フレアを設定
+ フレアサイト全体でフレアを非表示にする設定があります。自己紹介国・地域
@@ -708,6 +708,7 @@
友達にだけ誰にでもキッズモード
+ キッズモードが有効です。これは安全対策です。「キッズモード」ではサイト上の会話がすべて無効になります。子供や生徒のアカウントでこのモードを有効にしておけば、彼らを他のユーザーから守ることができます。キッズモードでは Lichess のロゴに %s のアイコンが付き、安全であることを示します。あなたのアカウントは管理されています。キッズモードの停止は講師に依頼してください。
@@ -817,9 +818,10 @@
%s 種のコンディショナルムーブを設定
+ Lichess からプライベートメッセージが来ました。
+ ここをクリックして読む残念です :(しばらく対局を禁止します。
- 禁止は %s 後に解けます。どうして?Lichess ではすべての人に楽しいチェス体験を提供しています。そのためには、すべての人にマナーを守っていただく必要があります。
@@ -839,6 +841,7 @@
私は Lichess のすべてのポリシーに従うことに同意します。検索または新しいトピックを始める編集
+ ブレットブリッツラピッドクラシカル
diff --git a/translation/dest/site/ka-GE.xml b/translation/dest/site/ka-GE.xml
index ecf169efc403c..d5dad7b58dfaa 100644
--- a/translation/dest/site/ka-GE.xml
+++ b/translation/dest/site/ka-GE.xml
@@ -271,7 +271,6 @@
თამაშობს ამ დროსთამაშობს ამ დროსდამთავრებული
- რჩება %sთამაშის შეწყვეტათამაში შეწყვეტილიასტანდარტი
@@ -846,7 +845,6 @@
ბოდიში :(გარკვეული დროით უნდა შეგიჩეროთ თამაში.
- დროის ამოწურვა მთავრდება %s-ში.რატომ?ჩვენი მიზანია შევქმნათ სასიამოვნო გამოცდილება ჭადრაკის სათამაშოდ ყველასათვის.ამიტომაც, ჩვენ უნდა დავრწმუნდეთ, რომ ყველა მოთამაშე იცავს თამაშის ეტიკეტს.
diff --git a/translation/dest/site/kk-KZ.xml b/translation/dest/site/kk-KZ.xml
index da34fce350108..c34755e393f4e 100644
--- a/translation/dest/site/kk-KZ.xml
+++ b/translation/dest/site/kk-KZ.xml
@@ -272,7 +272,6 @@
Қазір ойнап отырҚазір болып жатырАяқталды
- %s аяқталадыОйынды тоқтатуОйын тоқтатылдыКлассикалық
@@ -850,7 +849,6 @@
Өкінішті-ақ :(Сізді уақытша шектеуге мәжбүрміз.
- Шектеудің аяқталуына %s қалды.Себебі қандай?Біз әрбіреудің ойны жайлы өтуін мақсат етеміз.Сол үшін барлық ойыншылардың тәртібін қадағалауға тура келеді.
diff --git a/translation/dest/site/kmr-TR.xml b/translation/dest/site/kmr-TR.xml
index 6ea5314920d2c..4e6ab49b1c4e9 100644
--- a/translation/dest/site/kmr-TR.xml
+++ b/translation/dest/site/kmr-TR.xml
@@ -228,7 +228,6 @@
Vê gavê tê lîstinVê gavê tê lîstinQedîya
- xelas dibe nav %s deLîstikê betal bikeLîstik hate betalkirinStandard
@@ -756,7 +755,6 @@
Bibore :(Em ji bo demekê te bi dûr xistin.
- Xelasbûna wextê bidûrxistinê: %s.Çima?Em armanc dikin ku ji bo herkesî serpêhatiyeke bi xweşî pêşkêş bikin.Ji bo vê yekê, divê em piştrast bibin ku hemû lîzer mêtodeke baş dişopînin.
diff --git a/translation/dest/site/kn-IN.xml b/translation/dest/site/kn-IN.xml
index d6c7577598490..714c3299c3b98 100644
--- a/translation/dest/site/kn-IN.xml
+++ b/translation/dest/site/kn-IN.xml
@@ -270,7 +270,6 @@
ಆಟ ಪ್ರಗತಿಯಲ್ಲಿದೆಆಟಗಳು ಪ್ರಗತಿಯಲ್ಲಿವೆಮುಕ್ತಾಯಗೊಂಡಿದೆ
- %s ಗಳಲ್ಲಿ ಮುಕ್ತಾಯಗೊಳ್ಳಲಿದೆಆಟವನ್ನು ತ್ಯಜಿಸುಆಟವನ್ನು ತ್ಯಜಿಸಲಾಗಿದೆಸಾಮಾನ್ಯ
@@ -845,7 +844,6 @@
ಕ್ಷಮಿಸಿ :(ನಿಮ್ಮ ಪ್ರವೇಶವನ್ನು ಸ್ವಲ್ಪ ಹೊತ್ತು ತಡೆಹಿಡಿಯಬೇಕಾಗಿ ಬಂದಿದೆ.
- ನಿಮ್ಮ ನಿಷೇಧ ಕೊನೆಗೊಳ್ಳಲು ಇರುವ ಬಾಕಿ ಸಮಯ %s.ಯಾಕೆ?ಎಲ್ಲರಿಗೂ ಆನಂದದಾಯಕವಾದ ಚೆಸ್ ಅನುಭವವನ್ನು ಕೊಡಬೇಕು ಎಂಬುದು ನಮ್ಮ ಆಶಯವಾಗಿದೆ.ಅದರ ಸಲುವಾಗಿ, ಎಲ್ಲಾ ಆಟಗಾರರೂ ಒಳ್ಳೆಯ ಅಭ್ಯಾಸಗಳನ್ನು ಅನುಸರಿಸುತ್ತಿದ್ದಾರೆ ಎಂಬುದನ್ನು ನಾವು ಖಚಿತಪಡಿಸಬೇಕಾಗಿದೆ.
diff --git a/translation/dest/site/ko-KR.xml b/translation/dest/site/ko-KR.xml
index 5872de4ceda36..58d39616b3f0f 100644
--- a/translation/dest/site/ko-KR.xml
+++ b/translation/dest/site/ko-KR.xml
@@ -254,7 +254,6 @@
대국 중지금 대국 중종료
- %s 남음게임 중단게임 중단됨표준
@@ -799,7 +798,6 @@
죄송합니다 :(짧은 시간동안 정지를 받으셨습니다.
- 타임아웃은 %s 후에 후에 만료됩니다.왜 그런가요?우리는 모두에게 즐거운 체스 경험을 제공하는 것을 목표로 합니다.그러기 위해서는, 우리는 모든 플레이어가 좋은 관행을 따르도록 보장해야 합니다.
diff --git a/translation/dest/site/la-LA.xml b/translation/dest/site/la-LA.xml
index 2a272a0990c79..29bfafc494109 100644
--- a/translation/dest/site/la-LA.xml
+++ b/translation/dest/site/la-LA.xml
@@ -225,7 +225,6 @@
Nunc ludendusNunc ludendusPerfectus
- %s ceteriLusionem tollereSublata lusioCommunis
diff --git a/translation/dest/site/lb-LU.xml b/translation/dest/site/lb-LU.xml
index d99213e883f9c..eb60e9dc2e19c 100644
--- a/translation/dest/site/lb-LU.xml
+++ b/translation/dest/site/lb-LU.xml
@@ -51,21 +51,21 @@
Schwaarz huet d\'Partie verloossWäiss huet net gezunnSchwaarz huet net gezunn
- Eng Computer Analyse ufroen
+ Eng Computeranalys ufroenComputeranalys
- Computer Analyse disponibel
- Computer Analyse desaktivéiert
+ Computeranalys disponibel
+ Computeranalys desaktivéiertAnalysebrietDéift %s
- Server Analyse gëtt benotzt
+ Serveranalys gëtt benotztEngine luet...Zich ginn gerechent...Feeler beim Lueden
- Cloud Analyse
- Déift erhiewen
- Bedrohung weisen
+ Cloudanalys
+ Déift eropsetzen
+ Bedroung weisenam lokalen Browser
- Lokal Computer Analyse aktivéieren/desaktivéieren
+ Lokal Computeranalys aktivéieren/desaktivéierenVariant opwäertenHaaptvariant maachenVun hei läschen
@@ -110,7 +110,7 @@
PGN importéierenLäschenImportéiert Partie läschen?
- Replay Modus
+ Replay-ModusRealzäitNo CPLStudie opmaachen
@@ -118,10 +118,10 @@
Beschten Zuch FeilVariantefeiler weisenEvaluatioun weisen
- Puer Linnen
+ Méi VariantenCPUsAarbechtsspäicher
- Endlos Analyse
+ Endlos AnalysEntfernt d\'Déifenbegrenzung an hält däin Computer waarmEngineverwaltungGaffe
@@ -272,7 +272,6 @@
Partie leeftPartien lafenFäerdeg
- ass fäerdeg %sPartie ofbriechenPartie ofgebrachStandard
@@ -485,17 +484,18 @@
Zich gespilltWäiss VictoirenSchwaarz Victoiren
+ RemisquotRemisNächsten %s Turnéier:Duerschnëttlechen Géigner
- Briet Editor
+ Briet-EditorBriet opbauen
- Beléift Eröffnungen
+ Beléift ErëffnungenEndspill PositiounenChess960 Start Positioun: %s
- Start Positioun
+ AusgangsstellungBriet opraumen
- Positioun lueden
+ Stellung luedenPrivat%s den Moderatoren mellenProfil vollstänneg zu: %s
@@ -506,6 +506,7 @@
VirnummNummBiographie
+ Land oder RegiounMerci!Sozial Medien LinksEng URL pro Zeil.
@@ -586,7 +587,7 @@
Außerhalb vum BrietAn luesen PartienËmmer
- Nie
+ Ni%1$s hëlt deel bei %2$sVictoireDefaite
@@ -685,7 +686,7 @@
bei Start/Schluss goenKommentarer weisen/verstoppenVariant wiehlen/verloossen
- Computer-Analyse ufroen, léier aus dengen Feeler
+ Computeranalys ufroen, léier aus denge FeelerNächst (Aus dengen Feeler léieren)Nächst GaffeNächsten Feeler
@@ -732,6 +733,7 @@
Mat KolleegenMat jidderengemKannermodus
+ De Kannermodus ass aktiv.Eng Sécherheetsastellung. Am Kannermodus sin all Kommunikatiounsweeër blockéiert. Aktivéier dës Optioun fir Kanner an Schüler virun Internetbenotzer ze schützen.Am Kannermodus huet den Lichess logo en %s Icon, sou weess du dass däin Kand sécher ass.Däin Konto gëtt verwalt. Fro däin Schachtrainer fir den Kannermodus opzehiewen.
@@ -756,7 +758,7 @@
Disponibel an %s Sprooch!Disponibel an %s Sproochen!
- Spill Analyse
+ Analys vun der Partie%1$s huet %2$s kreéiert%1$s mëscht mat bäi %2$s%1$s gefällt %2$s
@@ -772,9 +774,9 @@
Mam Gerät synchroniséierenURL vum Hannergrondbild:Briet Geometrie
- Briet Design
+ BrietdesignBriet Gréisst
- Figuren Set
+ FigurestilAn Websäit anbettenDësen Benotzernumm gëtt schonn benotzt, wiel wannechgelift een aneren.De Benotzernumm muss mat engem Buschtaf ufänken.
@@ -820,7 +822,7 @@
Probéier een aneren Zuch fir WäissProbéier een aneren Zuch fir SchwaarzLéisung
- Waarden op Analyse
+ Waarden op d\'AnalysKeng Feeler vun Wäiss fonntKeng Feeler vun Schwaarz fonntWäiss Feeler all nogekuckt
@@ -845,9 +847,10 @@
an späicher %s bedingten Virauszuchan späicher %s bedingt Virauszich
+ Du krus eng privat Noriicht vu Lichess.
+ Klick hei fir se ze liesenSorry :(Mir missten dir eng Auszeit ginn.
- D\'Auszeit ass riwwer %s.Firwat?Mir wëllen jidderengem eng méiglechst gudd Schacherfahrung offréieren.Fir dat ze erreechen mussen mer sécherstellen dass all Spiller sech korrekt verhalen.
@@ -867,6 +870,7 @@
Ech stëmmen zou dass ech den Lichess-Richtlinnen folgen wäert.Sichen oder nei Konversatioun startenÄnneren
+ BulletBlitzRapidKlassesch
@@ -941,10 +945,10 @@ Looss et eidel, fir Partie aus der normaler Ausgangspositioun ze starten.Just Ekippenmemberen
Duerch den Zuchbam navigéierenMaus Tricks
- Lokal Computer Analyse aktivéieren/desaktivéieren
- All Computer Analyse aktivéieren/desaktivéieren
+ Lokal Computeranalys aktivéieren/desaktivéieren
+ All Computeranalysen aktivéieren/desaktivéierenBeschten Computer Zuch spillen
- Analyse Optiounen
+ AnalysoptiounenChat fokusséierenDësen Hëllefdialog weisenKonto nei opmaachen
@@ -965,6 +969,8 @@ Looss et eidel, fir Partie aus der normaler Ausgangspositioun ze starten.Faarf wiesselen
Däin Benotzerkonto zou ze maachen wäert och däin Asproch zeréckzéihenEis Tipps fir d\'Organiséieren vun Turnéier
+ Instruktiounen
+ Alles weisenLichess ass eng Wohltätegkeetsorganisatioun an eng komplett kostenfrei/open source Software.
All Betriebskäschten, Entwécklung an Inhalter ginn ausschließlech vun Benotzerspenden finanzéiert.
diff --git a/translation/dest/site/lt-LT.xml b/translation/dest/site/lt-LT.xml
index 746f0ac5429bf..8237e11d6dfaa 100644
--- a/translation/dest/site/lt-LT.xml
+++ b/translation/dest/site/lt-LT.xml
@@ -302,7 +302,6 @@
Vyksta šiuo metuVyksta šiuo metuBaigėsi
- baigiasi %sNutraukti partijąPartija nutrauktaStandartinis
@@ -935,7 +934,6 @@ kompiuterinę analizę, partijos pokalbį bei URL dalinimuisi.
Atsiprašome :(Turėjome jus laikinai apriboti.
- Apribojimas baigiasi %s.Kodėl?Mes stengiamės suteikti galimybę visiems patirti šachmatų malonumą.Dėl to turime užtikrinti, kad visi žaidėjai laikytųsi gerųjų praktikų.
diff --git a/translation/dest/site/lv-LV.xml b/translation/dest/site/lv-LV.xml
index 2de8b80ca5fee..f27472475cbcb 100644
--- a/translation/dest/site/lv-LV.xml
+++ b/translation/dest/site/lv-LV.xml
@@ -285,7 +285,6 @@
Šobrīd spēlēŠobrīd notiekBeidzies
- beidzas %sAtcelt spēliSpēle atceltaStandarta
@@ -886,7 +885,6 @@
Lūdzu piedodiet :(Mums nācās jūs apturēt uz laiku.
- Pārtraukums beigsies %s.Kāpēc?Mēs cenšamies visiem piedāvāt patīkamu šaha pieredzi.Līdz ar to, mums jānodrošina, lai visi spēlētāji pieturas pie labas prakses.
diff --git a/translation/dest/site/mg-MG.xml b/translation/dest/site/mg-MG.xml
index c26d1872c875b..0a7f0811bb291 100644
--- a/translation/dest/site/mg-MG.xml
+++ b/translation/dest/site/mg-MG.xml
@@ -166,7 +166,6 @@
Milalao amin\'izao fotoanaMilalao amin\'izao fotoanaVita
- tapitra afaka %sAjanona ny lalaoTapaka ny lalaoStandard
diff --git a/translation/dest/site/mk-MK.xml b/translation/dest/site/mk-MK.xml
index 545ccfaf1ceb4..08d3bf4932b6f 100644
--- a/translation/dest/site/mk-MK.xml
+++ b/translation/dest/site/mk-MK.xml
@@ -266,7 +266,6 @@
Моментално играМоментално играЗавршено
- Завршува за %sОткажи ја игратаИграта е откажанаСтандарден
@@ -841,7 +840,6 @@
Извини :(Моравме да те исклучиме на кратко.
- Исклучувањето престанува %s.Зошто?Се стремиме да пружиме пријатно искуство за сите.Поради тоа, мораме да се осигуриме дека сите играчи се чесни.
diff --git a/translation/dest/site/ml-IN.xml b/translation/dest/site/ml-IN.xml
index 1e30946dd3554..ed490bf1616e3 100644
--- a/translation/dest/site/ml-IN.xml
+++ b/translation/dest/site/ml-IN.xml
@@ -228,7 +228,6 @@
ഇപ്പോള് കളിച്ചുക്കൊണ്ടിരിക്കുന്നുഇപ്പോൾ കളിച്ചുകൊണ്ടിരിക്കുന്നുപൂര്ത്തിയായി
- %s-ൽ തീരുന്നുമത്സരം ഉപേക്ഷിക്കുകമത്സരം ഉപേക്ഷിച്ചുസ്റ്റാന്ഡേര്ഡ്
@@ -761,7 +760,6 @@
ക്ഷമിക്കുക :(ഞങ്ങൾക്ക് നിങ്ങളെ കുറച്ചു സമയം പുറത്തു നിർത്തേണ്ടി വന്നു.
- ടൈം ഔട്ട് %s ഉള്ളിൽ കഴിയും.എന്തുകൊണ്ട്?ഞങ്ങൾ എല്ലാവര്ക്കും സുഗമമായ ചെസ്സ് അനുഭവം നൽകാൻ ലക്ഷ്യമിടുന്നു.അതിനാൽ, എല്ലാ കളിക്കാരും നല്ല രീതിയിൽ പ്രവർത്തിക്കുന്നു എന്ന് ഞങ്ങൾക്ക് ഉറപ്പു വരുത്തണം.
diff --git a/translation/dest/site/mn-MN.xml b/translation/dest/site/mn-MN.xml
index a9f678e5227c8..46088a74eddbf 100644
--- a/translation/dest/site/mn-MN.xml
+++ b/translation/dest/site/mn-MN.xml
@@ -225,7 +225,6 @@
Яг одоо тоглож байнаЭнэ мөчид тэмцээн, нэгэн зэрэг үзэсгэлэн буюу үйл явдал үргэлжилж байгааг харуулж байна.Өндөрлөсөн тэмцээнүүд
- %s дууснаӨргийг цуцлахӨрөг цуцлагдлааСтандарт
diff --git a/translation/dest/site/mr-IN.xml b/translation/dest/site/mr-IN.xml
index 6bbb3f98a2ad1..de57862969a8f 100644
--- a/translation/dest/site/mr-IN.xml
+++ b/translation/dest/site/mr-IN.xml
@@ -243,7 +243,6 @@
आता खेळत आहेआता खेळत आहेतसमाप्त
- समाप्ती %sडाव बंद कराडाव बंद केला गेलामानक
@@ -811,7 +810,6 @@
माफ कराआम्हाला आपल्याला काही वेळ खेळू देता येणार नाही.
- आपला प्रतिबंध समाप्त होईल %s.कारण?प्रत्येकासाठी एक सुखद बुद्धिबळ अनुभव देण्याचे आमचे ध्येय आहे.त्या दृष्टीने, आम्ही हे सुनिश्चित केले पाहिजे की सर्व खेळाडूंनी चांगल्या कार्यपद्धतीचे अनुसरण केले आहे.
diff --git a/translation/dest/site/ms-MY.xml b/translation/dest/site/ms-MY.xml
index a5e0fc4be6b63..ccb6ea67706f0 100644
--- a/translation/dest/site/ms-MY.xml
+++ b/translation/dest/site/ms-MY.xml
@@ -249,7 +249,6 @@
Sedang bermain sekarangSedang bermain sekarangTamat
- tamat dalam %sBatalkan permainanPermainan dibatalkanStandard
@@ -719,7 +718,6 @@ analisis komputer, perbualan dalam permainan dan URL kongsi bersama.
Maaf :(Kami terpaksa memberikan time out buat sementara.
- Timeout berakhir %s.Kenapa?Matlamat kami adalah untuk memberikan pengalaman catur yang baik untuk semua.Dengan itu, kami perlu memastikan semua pemain mengikuti amalan yang baik.
diff --git a/translation/dest/site/nb-NO.xml b/translation/dest/site/nb-NO.xml
index a773a6842ab41..6dd91602688f0 100644
--- a/translation/dest/site/nb-NO.xml
+++ b/translation/dest/site/nb-NO.xml
@@ -272,7 +272,6 @@
PågårPågårFerdig
- avsluttes %sAvbryt partietPartiet er avbruttStandard
@@ -405,7 +404,7 @@
Importer partiLim inn PGN for gjennomblaing, maskinanalyse, partisamtale og delbar URL.Varianter importeres ikke. Bruk en studie for å importere PGN med varianter.
- Denne PGN-en er offentlig tilgjengelig. Bruk en studie for å importere et parti privat.
+ Denne PGN-en er offentlig tilgjengelig. Bruk en studie for å importere et parti privat.%s importert parti%s importerte partier
@@ -507,7 +506,7 @@
Rediger profilFornavnEtternavn
- Velg flair:
+ Velg flairFlairDet finnes en innstilling for å skjule alle brukerflairer på hele nettstedet.Biografi
@@ -862,9 +861,10 @@
og lagre %s linje med forhåndstrekkog lagre %s linjer med forhåndstrekk
+ Du har mottatt en privat melding fra Lichess.
+ Klikk her for å lese denBeklager :(Vi måtte gi deg en timeout.
- Timeouten utløper %s.Hvorfor?Vi forsøker å gi alle en god sjakkopplevelse.For å oppnå det må vi sikre at alle spillere følger god praksis.
@@ -884,6 +884,7 @@
Jeg lover å respektere alle Lichess\' retningslinjer.Søk eller start en ny diskusjonRediger
+ BulletBlitzHurtigsjakkKlassisk
diff --git a/translation/dest/site/ne-NP.xml b/translation/dest/site/ne-NP.xml
index 26061a1ce0e73..b54e056e1b6ca 100644
--- a/translation/dest/site/ne-NP.xml
+++ b/translation/dest/site/ne-NP.xml
@@ -203,7 +203,6 @@
खेल चालु छखेल चालु छसमाप्त भयो
- समापन हुदैं %sखेल रद्द गरौखेल रद्द गरियोसामान्य
@@ -723,7 +722,6 @@
माफ पाउँ :(तपाइलाई केही समयको लागि रोक लगाउन पर्ने भयो।
- बाकि समय: %s।किन?हामी सम्पूर्ण खेलाडीहरुको लागि बुद्धिचालको सुमधुर अनुभव श्रीजना गर्न चाहान्छौं।त्यसको लागि सम्पूर्ण खेलाडीहरुको चालचलन राम्रो हुन जरुरी छ।
diff --git a/translation/dest/site/nl-NL.xml b/translation/dest/site/nl-NL.xml
index 1aaeb6a79a4bf..a54842c141a5d 100644
--- a/translation/dest/site/nl-NL.xml
+++ b/translation/dest/site/nl-NL.xml
@@ -272,7 +272,6 @@
Nu aan het spelenNu aan het spelenAfgelopen
- klaar in %sPartij afbrekenPartij afgebrokenStandaard
@@ -405,7 +404,7 @@
Importeer partijAls je een PGN in het venster plakt, krijg je een doorzoekbare replay, een computeranalyse, een chatbox bij de partij en een deelbare URL.Variaties worden gewist. Om ze te behouden, importeer de PGN via een studie.
- Deze PGN kan toegankelijk zijn voor het publiek. Gebruik een studie om een partij privé te importeren.
+ Deze PGN kan toegankelijk zijn voor het publiek. Gebruik een studie om een partij privé te importeren.%s Geïmporteerde partij%s Geïmporteerde partijen
@@ -507,7 +506,7 @@
Pas profiel aanVoornaamAchternaam
- Stel je flair in:
+ Stel je flair inSymboolEr bestaat een instelling om alle gebruikersflairs over de hele site te verbergen.Biografie
@@ -862,9 +861,10 @@
en bewaar %s premove-varianten bewaar %s premove-varianten
+ Je hebt een privébericht van Lichess ontvangen.
+ Klik hier om het te bekijkenSorry :(We moesten je voor een korte tijd een time-out geven.
- The time-out verloopt %s.Waarom?Wij streven ernaar om iedereen een prettige schaakervaring te bieden.Daarom moeten we ervoor zorgen dat alle spelers goede praktijken volgen.
@@ -884,6 +884,7 @@
Ik ga ermee akkoord dat ik de Lichess-regels zal volgen.Zoek of start een nieuwe discussieWijzigen
+ BulletBlitzRapidKlassiek
diff --git a/translation/dest/site/nn-NO.xml b/translation/dest/site/nn-NO.xml
index 5f4fb2f4f4463..0a79d65c6bddc 100644
--- a/translation/dest/site/nn-NO.xml
+++ b/translation/dest/site/nn-NO.xml
@@ -272,7 +272,6 @@
PågårPågårSlutt
- endar %sAvbryt partietPartiet blei avbroteStandard
@@ -406,7 +405,7 @@
Når du limer inn eit PGN-parti kan du bla gjennom partiet,
få en computeranalyse, chatte eller dele ein URL.Variantar vert sletta. Vil du behalde dei kan du importere PGN\'en via ein studie.
- Denne PGN er offentleg tilgjengeleg. Bruk ein studie for å importere eit parti berre for deg.
+ Denne PGN-fila er offentleg tilgjengeleg. Bruk ein studie for å importere eit parti berre for deg.%s importert parti%s importerte parti
@@ -508,7 +507,7 @@ få en computeranalyse, chatte eller dele ein URL.
Rediger profilenFørenamnEtternamn
- Vel ikonet ditt:
+ Vel ikonet dittIkonDet finst ei innstilling for å skjule alle brukarikonar på heile nettstaden.Biografi
@@ -673,7 +672,7 @@ få en computeranalyse, chatte eller dele ein URL.
Returner til simultanheimesidaSimultanframsyning inneber at ein spelar møter fleire motspelarar samstundes.Mot 50 motspelarar, fekk Fischer 47 sigrar, to remisar og eit tap.
- Konseptet liknar på verkelege simultansjakkframsyningar, der verten for arrangementet går frå bord til bord og spelar mot fleire motstandarar samstundes.
+ Konseptet er henta frå verkelege framsyningar der verten for arrangementet går frå bord til bord og spelar mot fleire motstandarar samstundes.Når simultanframsyninga byrjar, startar alle deltakarane eit parti mot den som er vert. Verten får spela med dei kvite brikkene. Simultanoppvisinga endar når alle partia er ferdigspelte.Simultanframsyningar er alltid urangerte. Omstart, attendetrekk og \"meirtid\" er slått av.Opprett
@@ -765,7 +764,7 @@ få en computeranalyse, chatte eller dele ein URL.
AnnonsefriAlle funksjonarMobiltelefon og nettbrett
- kvikksjakk, lynsjakk, saktesjakk
+ Bullet, lynsjakk, saktesjakkFjernsjakkSpel online og offlineVis løysinga
@@ -863,9 +862,10 @@ få en computeranalyse, chatte eller dele ein URL.
og lagra %s line med førehandstrekkog lagra %s liner med førehandstrekk
+ Du har fått ei privat melding frå Lichess.
+ Klikk her for å lesa denBeklagar :(Vi måtte gje deg ein timeout.
- Timeouten tek slutt %s.Kvifor?Vi freistar å gje alle ei god sjakk-oppleving.For å oppnå det må vi sikre at alle spelarane respekterar god praksis.
@@ -885,6 +885,7 @@ få en computeranalyse, chatte eller dele ein URL.
Eg lovar at eg vil følge alle reglane til Lichess.Søk eller start ein ny diskusjonRediger
+ BulletBlitzSnøggsjakkLangsjakk
diff --git a/translation/dest/site/os-SE.xml b/translation/dest/site/os-SE.xml
index 303629d1614d0..1552c25e55942 100644
--- a/translation/dest/site/os-SE.xml
+++ b/translation/dest/site/os-SE.xml
@@ -215,7 +215,6 @@
Хъазт цæуыНыртæккæ цæуыФæудгонд
- фæуы %sХъазт аивынХъазт аивд уКлассикон шахмæттæ
diff --git a/translation/dest/site/pa-IN.xml b/translation/dest/site/pa-IN.xml
index 7338813e4fc1b..c1df8af6d6f61 100644
--- a/translation/dest/site/pa-IN.xml
+++ b/translation/dest/site/pa-IN.xml
@@ -75,7 +75,6 @@
ਹੁਣੇ ਖੇਡ ਰਿਹਾ ਹੈਹੁਣੇ ਖੇਡ ਰਿਹਾ ਹੈਸਮਾਪਤ ਹੋਇਆ
- ਮੁਕੰਮਲ %sਖੇਡੋਭੇਜੋਮੁਫਤ ਦਾ ਸ਼ਤਰੰਜ
diff --git a/translation/dest/site/pl-PL.xml b/translation/dest/site/pl-PL.xml
index eb997269e7e4e..9b7557a37d5a2 100644
--- a/translation/dest/site/pl-PL.xml
+++ b/translation/dest/site/pl-PL.xml
@@ -302,7 +302,6 @@
W tokuW tokuZakończone
- kończy się %sPrzerwij partięPartia została przerwanaStandard
@@ -461,7 +460,7 @@
Importuj partięWklejenie PGN partii daje możliwość jej odtworzenia, analizy komputerowej, rozmowy i udostępnienia.Warianty zostaną usunięte. Aby je zachować, zaimportuj plik PGN jako opracowanie.
- Ten zapis PGN będzie dostępny publicznie. Aby zaimportować partię tylko dla siebie, stwórz prywatne opracowanie.
+ Ten zapis PGN będzie dostępny publicznie. Aby zaimportować partię tylko dla siebie, stwórz prywatne opracowanie.%s zaimportowana partia%s zaimportowana partie
@@ -573,7 +572,7 @@
Edycja profiluImięNazwisko
- Ustaw swój emblemat:
+ Ustaw swój emblematEmblematIstnieje ustawienie, pozwalające ukryć wszystkie emblematy użytkowników na lichess.O mnie
@@ -945,9 +944,10 @@
i zapisz %s wariantów warunkowychi zapisz %s wariantów warunkowych
+ Otrzymałeś prywatną wiadomość od Lichess.
+ Kliknij tutaj, aby ją odczytaćPrzykro nam :(Na pewien czas musieliśmy wykluczyć Cię z gry.
- Wykluczenie skończy się %s.Dlaczego?Naszym celem jest zapewnienie wszystkim przyjemności z gry.W tym celu musimy zapewnić przestrzeganie dobrych praktyk przez wszystkich graczy.
@@ -967,6 +967,7 @@
Zgadzam się przestrzegać wszystkich zasad Lichess.Szukaj lub rozpocznij nową rozmowęEdytuj
+ BulletBlitzSzybkieKlasyczne
diff --git a/translation/dest/site/pt-BR.xml b/translation/dest/site/pt-BR.xml
index 72b215334a4a0..4723789d77a34 100644
--- a/translation/dest/site/pt-BR.xml
+++ b/translation/dest/site/pt-BR.xml
@@ -272,7 +272,6 @@
Jogando agoraJogando agoraTerminado
- termina %sCancelar partidaPartida canceladaPadrão
@@ -405,7 +404,7 @@
Importar partidaApós colar uma partida em PGN você poderá revisá-la interativamente, consultar uma análise de computador, utilizar o chat e compartilhar um link.As variantes serão apagadas. Para salvá-las, importe o PGN em um estudo.
- Este PGN pode ser acessado publicamente. Use um estudo para importar um jogo privado.
+ Este PGN pode ser acessado publicamente. Use um estudo para importar um jogo privado.%s de partidas importadas%s de partidas importadas
@@ -507,7 +506,7 @@
Editar perfilPrimeiro nomeSobrenome
- Escolha seu emote:
+ Escolha seu emoteEstiloVocê pode esconder todos os emotes de usuário no site.Biografia
@@ -858,9 +857,10 @@
e salvar a linha de pré-lance de %se salvar as linhas de pré-lance de %s
+ Você recebeu uma mensagem privada do Lichess.
+ Clique aqui para lerDesculpa :(Tivemos de bloqueá-lo por um tempo.
- O tempo expirará %s.Por quê?Buscamos oferecer uma experiência agradável de xadrez para todos.Para isso, precisamos assegurar que nossos jogadores sigam boas práticas.
@@ -880,6 +880,7 @@
Eu concordo que seguirei todas as normas do Lichess.Procurar ou iniciar nova conversaEditar
+ BulletBlitzRápidaClássico
diff --git a/translation/dest/site/pt-PT.xml b/translation/dest/site/pt-PT.xml
index 2148c967e113a..5680737ce5d09 100644
--- a/translation/dest/site/pt-PT.xml
+++ b/translation/dest/site/pt-PT.xml
@@ -272,7 +272,6 @@
A jogar agoraA decorrer agoraTerminado
- termina %sCancelar a partidaPartida canceladaPadrão
@@ -406,7 +405,6 @@
Coloca aqui o PGN de um jogo, para teres acesso a navegar pela repetição,
análise de computador, sala de chat do jogo e link de partilha.As variações serão apagadas. Para mantê-las, importe o PGN através de um estudo.
- Este PGN pode ser acessado pelo público. Para importar um jogo de forma privada, use um estudo.%s partida importada%s partidas importadas
@@ -508,7 +506,7 @@ análise de computador, sala de chat do jogo e link de partilha.
Editar o perfilNome próprioApelido
- Defina o teu estilo:
+ Defina o teu estiloEstiloHá uma opção para ocultar todos os estilos dos utilizadores em todo o site.Biografia
@@ -863,9 +861,10 @@ análise de computador, sala de chat do jogo e link de partilha.
e guarda %s variante de movimentos antecipadose guarda %s variantes de movimentos antecipados
+ Recebestes uma mensagem privada do Lichess.
+ Clica aqui para lerDesculpa :(Tivemos de te banir por algum tempo.
- O banimento expira %s.Porquê?Tencionamos proporcionar uma experiência de xadrez agradável a todos.Para isso, temos de nos assegurar que todos os jogadores seguem boas práticas.
@@ -885,6 +884,7 @@ análise de computador, sala de chat do jogo e link de partilha.
Concordo que seguirei todas as políticas do Lichess.Pesquisa ou começa uma nova conversaEditar
+ BulletRápidasSemi-rápidasClássicas
diff --git a/translation/dest/site/ro-RO.xml b/translation/dest/site/ro-RO.xml
index c4d243b06d42f..14156ebdf6236 100644
--- a/translation/dest/site/ro-RO.xml
+++ b/translation/dest/site/ro-RO.xml
@@ -287,7 +287,6 @@
În desfășurare...În desfășurareTerminat
- se termină %sAbandonați partidaPartidă abandonatăStandard
@@ -433,7 +432,7 @@
Importați partidaCopiați o partidă în format PGN pentru a putea apoi sa o rejucati, sa cereti o analiză a computerului, sa folositi functia de chat și sa obtineti un URL pentru distribuire.Variațiile vor fi șterse. Pentru a le păstra, importați PGN-ul printr-un studiu.
- Acest PGN poate fi accesat public. Pentru a importa un joc în mod privat, folosește un studiu.
+ Acest PGN poate fi accesat public. Pentru a importa un joc în mod privat, folosește un studiu.%s partidă importată%s partide importate
@@ -901,7 +900,6 @@
Scuze :(Am fost nevoiți să suspendăm activitatea pentru puțin timp.
- Suspendarea se termină %s.De ce?Dorim să oferim o experiență plăcută tuturor jucătorilor de șah.Ca acest lucru să se întâmple, trebuie să ne asigurăm că toți jucătorii respectă bunele practici.
diff --git a/translation/dest/site/ru-RU.xml b/translation/dest/site/ru-RU.xml
index 51caea5f088a7..b9aae0b56ed70 100644
--- a/translation/dest/site/ru-RU.xml
+++ b/translation/dest/site/ru-RU.xml
@@ -302,7 +302,6 @@
Идёт играИдёт прямо сейчасЗавершён
- завершится %sОтменить игруИгра отмененаКлассические шахматы
@@ -461,7 +460,7 @@
Импортировать партиюВставьте запись партии в формате PGN, и вы получите возможность переигрывать партию, выполнять компьютерный анализ, общаться в чате и делиться ссылкой на эту игру.Варианты будут удалены. Чтобы их сохранить, импортируйте PGN в студии.
- Этот PGN-файл может быть доступен публично. Чтобы импортировать игру приватно, используйте студию.
+ Этот PGN-файл может быть доступен публично. Чтобы импортировать игру приватно, используйте студию.%s импортированная%s импортированные
@@ -573,7 +572,7 @@
Редактировать профильИмяФамилия
- Задайте свой эмодзи:
+ Задайте свой эмодзиЭмодзиЭта настройка скрывает все эмодзи пользователей на всём сайте.О себе
@@ -946,9 +945,10 @@
и сохранить %s последовательностейи сохранить %s последовательностей
+ Вы получили личное сообщение от Lichess.
+ Нажмите здесь, чтобы прочитать егоИзвините :(Мы вынуждены прервать вас на время.
- Вы можете вернуться через %s.Почему?Наша цель — сделать шахматы интересными для всех.Чтобы этого добиться, мы должны сделать так, чтобы все игроки следовали правилам хорошего тона.
@@ -968,6 +968,7 @@
Подтверждаю, что я буду следовать всем правилам Lichess.Найти обсуждение или начать новоеИзменить
+ ПуляБлицРапидКлассика
diff --git a/translation/dest/site/ry-UA.xml b/translation/dest/site/ry-UA.xml
index 68f793c5ba2af..35a5c99585833 100644
--- a/translation/dest/site/ry-UA.xml
+++ b/translation/dest/site/ry-UA.xml
@@ -277,7 +277,6 @@
Бавит ся тепирьБавит ся тепирьДовершеный
- кончит ся %sОдмінити бавкуБавка одміненаПрості шахматы
diff --git a/translation/dest/site/sco-GB.xml b/translation/dest/site/sco-GB.xml
index ade789bc3c27f..44c0335884500 100644
--- a/translation/dest/site/sco-GB.xml
+++ b/translation/dest/site/sco-GB.xml
@@ -230,7 +230,6 @@
Spielin richt nooSpielin richt nooFeenisht
- feenshes %sMisgae gemmGemm misganeOrdinar
diff --git a/translation/dest/site/si-LK.xml b/translation/dest/site/si-LK.xml
index 2819f80cb0251..8d3201661b3ef 100644
--- a/translation/dest/site/si-LK.xml
+++ b/translation/dest/site/si-LK.xml
@@ -251,7 +251,6 @@ PlayFirstOpeningEndgameExplorerMove
දැන් තරඟ කරයිදැන් තරඟ කරයිඅවසානයි
- %sකින් ඉවරවේතරඟය නවත්වන්නතරගය අත්හැර දැමුනිසම්මත
diff --git a/translation/dest/site/sk-SK.xml b/translation/dest/site/sk-SK.xml
index ace18581dbe3b..cd85e3ce72af5 100644
--- a/translation/dest/site/sk-SK.xml
+++ b/translation/dest/site/sk-SK.xml
@@ -302,7 +302,6 @@
Práve hrajúPráve sa hráUkončené
- končí %sZrušiť hruHra bola zrušenáŠtandard
@@ -461,7 +460,7 @@
Importovať partiuVložením partie vo formáte PGN získate možnosť jej prehrania, počítačovú analýzu, chat k partii ako aj URL pre jej zdieľanie.Variácie sa vymažú. Ak ich chcete zachovať, importujte PGN prostredníctvom štúdie.
- Toto PGN je verejne dostupné. Ak chcete partiu importovať súkromne, použite štúdiu!
+ Toto PGN je verejne dostupné. Ak chcete partiu importovať súkromne, použite štúdiu!%s importovaná partia%s importované partie
@@ -573,7 +572,7 @@
Upraviť profilMenoPriezvisko
- Nastavte si svoju ikonku štýlu:
+ Nastavte si svoju ikonku štýluIkonka štýluV nastaveniach je možné skryť všetky ikonky štýlu používateľov na celej stránke.Životopis
@@ -938,7 +937,6 @@
Prepáčte :(Na chvíľu sme Vás museli odstaviť.
- Vaša odstávka skončí o %s.Prečo?Naším cieľom je poskytovať všetkým príjemný šachový zážitok.Aby sme to docielili, musíme sa uistiť že sa všetci hráči správajú športovo.
@@ -958,6 +956,7 @@
Súhlasím, že budem dodržiavať všetky pravidlá Lichessu.Prehľadávať alebo začať novú konverzáciuUpraviť
+ BulletBlitzRapidKlasický šach
diff --git a/translation/dest/site/sl-SI.xml b/translation/dest/site/sl-SI.xml
index 0085ffb8ae091..533de30cafbf0 100644
--- a/translation/dest/site/sl-SI.xml
+++ b/translation/dest/site/sl-SI.xml
@@ -301,7 +301,6 @@
Trenutno se igraPravkar potekaKončano
- končal %sOpusti igroIgra je opuščenaObičajno
@@ -460,6 +459,7 @@
Uvozi igroKo prilepite PGN partijo imate na voljo brskanje partije, računalniško analizo, klepet o igri in povezavo, ki jo lahko delite.Variante bodo izbrisane. Če jih želite obdržat, uvozite PGN kot študijo.
+ Ta PGN je javno dostopen. Za zasebni uvoz igre uporabite študijo.%s uvožena igra%s uvoženi igri
@@ -571,7 +571,7 @@
Uredi profilImePriimek
- Določite svoj okus:
+ Določite svoj okusSimbolObstaja nastavitev za skrivanje vseh uporabniških čustev na celotnem spletnem mestu.Biografija
@@ -936,9 +936,10 @@
in shranite %s predpotezne variantein shranite %s predpoteznih variant
+ Prejeli ste zasebno sporočilo od Lichess.
+ Kliknite tukaj, če ga želite prebratiOprostite :(Morali smo vas za nekaj časa onemogočiti.
- Omejitev poteče čez %s.Zakaj?Želimo, da imajo z igranjem vsi prijetno izkušnjo.Da bi to dosegli, moramo zagotoviti, da se vsi igralci držijo dobre prakse.
@@ -958,6 +959,7 @@
Strinjam se, da bom spoštoval vsa pravila Lichess strani.Poišči ali prični nov pogovorUredi
+ Hitri šahHitropotezni šahPospešeniKlasični šah
diff --git a/translation/dest/site/so-SO.xml b/translation/dest/site/so-SO.xml
index 453c100cce9d0..08cd77dc0505c 100644
--- a/translation/dest/site/so-SO.xml
+++ b/translation/dest/site/so-SO.xml
@@ -13,7 +13,7 @@
Fur luuqaU qorIs dhiib
- Jiq-dam
+ Jeg-meydIsmariwaaCadaanMadow
@@ -28,7 +28,7 @@
Waa markaagii!Dareen khiyaamoDhexdu boqran
- Sadex jiq
+ Sadex jegBaratanki wuu dhamaadayDhamaadka noocanHorjeede cusub
@@ -49,8 +49,8 @@
Madow wa iscasilayCadaan wuu ka baxay ciyaartaMadow wuu ka baxay ciyaarta
- Cadaan waxba ma dhaqaaqijin
- Madow waxba ma dhaqaajin
+ Cadaanku muu dhaqaaqin
+ Madowgu muu dhaqaaqinDalbo in computer falanqeeyoFalanqaynta computerkafalanqaynta computerka wa diyaar
@@ -62,83 +62,312 @@
Cabirid dhaqdhaqaaq...Qalad adeegeFalanqayn hawo
- Gudaha usii gal
+ Sii gudagalTus khatartaaaladdaadaDooro flanqaynta aaladdaada
+ Hore u wad faracan
+ Sida ugu caansan ka dhig
+ Ka tuur halkan
+ Khasab faracan
+ Koobi garee PGNka faracanDhaqaaq
+ Guukdarro noocNooc badis
+ Qalab yari
+ Dhaqaaq askari
+ QabashoXidhGuuleystaLuminBarbardhacLama garanayo
+ Keydka weyn
+ Caddaan / Baraaje / Madow
+ Darajada isku celceliska ah: %s
+ Ciyaarihii ugu bambeeyey
+ Ciyaaraha ugu sareeya
+ Ciyaaraha MD ee %1$s+ ciyaartoyda FIDE ee %2$s ilaa %3$s
+
+ Mayd %s badh-dhaqaaq gudihii
+ Mayd %s badh-dhaqaaq gudohood
+
+ DTZ50\" la soo gaabiyey, kuna salaysan tirada badh-dhaqaaq ee ka hadhay qabashada ama dhaqaaqa askari ee xiga
+ Ciyaari ma jirto
+ Muggii ugu weynaa la gaadh!
+ Malaha kaga soo dar ciyaaro kale meesha doorashada?
+ Furitaanada
+ Furitaan baadhaha
+ Furitaan/dhamaad baadhaha
+ Furitaan baadhaha %s
+ Ciyaar dhaqaaqa ugu horeeya ee furitaan/dhamaad-baadhaha
+ Xeerka 50 tallaabo ayaa diiday guul
+ Xeerka 50 tallaabo ayaa diiday guuldarro
+ Guul ama 50 tallaabo khalad hore dartii
+ Guuldarro ama 50 tallaabo khalad hore dartii
+ Diyaar!
+ Soo geli PGN
+ Tuur
+ Tuur ciyaartan la soo geliyey?
+ Qaabka ku soo celintaWaqtiga dhabta ah
+ Inta muhiinka ah
+ Fur casharkaFur
+ Falaadha tallaabada ugu fiican
+ Tus falaadhaha faracyada
+ Qiinqaynta tallaabada
+ WaddooyinCPU
+ Baaxad
+ Falanqayn bilaa xad ah
+ Mug bilaa xad ah oo aaladdaada kululaynayaQaladQaladKhalad
+
+ %s qalad
+ %s qaladaad
+
+
+ %s halmaam
+ %s halmaamyo
+
+
+ %s aan fiicnayn
+ %s aan fiicnayn
+
+ Wakhtiyada tallaabo
+ Rog looxa
+ Ku celin sadex jeer
+ Ku dhawaaq baraaje
+ Codso baraaje
+ Baraaje
+
+ %s ciyaartow
+ %s ciyaartoy
+
+ Heshiis baraaje
+ Konton dhaqaaq bilaa natiijo
+ Ciyaaraha hadda
+
+ %s ciyaar
+ %s ciyaarood
+
+
+ Darajada %1$s ee %2$s ciyaar
+ Darajada %1$s ee %2$s ciyaarood
+
+
+ %s calaamad
+ %s calaamadood
+
+ Weynee daaqadda
+ Ka bax
+ Gal
+ Ha ka bixin
+ Akoon baad u baahanahay
+ Is diwaangeli
+ Kombiyuutar iyo qof qish kombiyuutar isticmaalaya lama ogola. Fadlan ha isticmaalin qish kombiyuutar ama caawin qof kale markad ciyaaraysid. Iyana ogow in samays akoono badan aad looga soo hor jeedo oo haddii ad samaysid akoonkaaga la xidhi karo.CiyaarahaMadal
+ %1$s ayaa ku soo qoray sheekada %2$s
+ Qoraalkii ugu dambeeyay ee barxaddaCiyaartoydaSaaxiibo
+ SheekaysiyadaMaantaShalay
+ Daqiiqadaha dhinacyada
+ Farac
+ Faracyada
+ Xadka wakhtiga
+ Imika
+ Maalinle
+ Maalmood doorkiiba
+ Hal maalin
+
+ %s maalin
+ %s maalmood
+
+
+ %s saacad
+ %s saacadood
+
+
+ %s daqiiqo
+ %s daqiiqo
+ Waqtiga
+ Qiimayn
+ Daraasada qiinqaynta
+ Magaca-akoon
+ Magac-akoon ama iimayl
+ Beddel magac-akoonka
+ Weynida xarfaha oo kaliiyaa is bedeli kara. Tusaale ahaan \"hebel\" iyo \"Hebel\".
+ Magac-koonkaaga beddel. Mar keliyaa kuu banana oo aad bedeli karto weynida xarfaha uun.
+ Magac-akoon edeb leh dooro. Ma bedeli kartid mar dambe waana la xidhi doonaa magicii edeb darro ah!
+ Waxan u isticmaalaynaa cusbaynta furahaaga oo keli ah.Erey sir
+ Beddel furaha
+ Beddel iimaylka
+ Iimayl
+ Cusboonaysii furaha
+ Ma ilowday furaha?
+ Furahani waa caan, si fidud baa loo nasiibin karaa.
+ Fadlan magaca-akoonka boggaaga ha ka dhigan fure.
+ Fure la mid ah ayaad u isticmaashay bog kale. Bogaasna waa la jabsaday. Si ad u ilaaliso nabada akoonkaaga Lichess, waxad u baahantahay fure cusub. Waad ku mahadsantay in ad na fahantay.
+ Waad ka baxaysaa Lichess
+ Weligaa ha ku qorin furaha Lichess bog kale!
+ Ku sii soco %s
+ Fure laguu sheegay ha ka dhigan fure. Waxay Kaa xadi karaan akoonka.
+ Wax laguu sheegay ha ka dhigan iimayl. Waxa lagaa xadi karaan akoonka.
+ Caawin hubinta iimaylka
+ Maad helin iimaylka hubinta diwaangelin ka dib?
+ Magac-akoonkee baad u isticmaashay diwaangelinta?
+ Maanu helin akoon magacan leh: %s.
+ Magacan-akoonkan waxad ku samayn kartaa akoon cusub
+ Iimayl baan u dirnay %s.
+ Wakhti yar sii si ay kuu soo gaadho.
+ Sug 5 daqiiqo oo ka dib baadh iimaylkaaga.
+ Sidoo kale eeg spam folderkaaga, halkaasaad ka heli kartaa. Haddii ay sidaa tahay, u beddel \"not spam\".
+ Haddii ay waxba shaqayn waayaan, noosoo dir iimaylkan:
+ Koobi garee qoraalka sare oo u soo dir %s
+ Waanu kuu soo laaban in yar ka dib si an kaaga caawinno diwaangelinta.
+ Akoonka %s waa la hubiyay si guul leh.
+ Hadda waxad ku geli kartaa %s.
+ Uma baahnid iimayl hubsasho ah.
+ Akoonka %s wuu xidhanyahay.
+ Akoonka %s wuxuu diwaangashanyay bilaa iimayl.DarajoDarajo: %s
+
+ Qiimayntu waxay is bedeshaa daqiiqo kasta
+ Qiimayntu waxay is bedeshaa %s daqiiqo kasta
+
- %s halxiraalaha
- %s xujooyinka
+ %s xujooyinka
+ %s xujo
+ Ciyaaraha dhamaaday
+
+ %s mar buu kula kulmay
+ %s mar buu kula kulmay
+
+ Ka noqo
+ Wakhtigii ba ka dhamaaday cadaanka
+ Wakhtigii ba ka dhamaaday madowga
+ La dir dalbasho baraaje
+ La aqbal baraaje
+ Laga noqoy dalbasho baraaje
+ Cadaankaa dalbaday baraaje
+ Madowgaa dalbaday baraaje
+ Caddaanku wudu diiday baraaje
+ Madowgu wudu diiday baraajeHorjeedahaagu wuxu kaa dalbaday baraajeAqbalDiidCiyaarta haddaCiyaarta haddaDhameystiray
+ Tuur ciyaartan
+ La tuur ciyaartanHeerka
- Xadidneyn
+ Bilaa xad
+ Nooca
+ Aan qiimaysnayn
+ Qiimasan
+ Aan qiimaysnayn
+ Qiimaysan
+ Ciyaartani way qiimaysantay
+ Ku celi
+ La dir codsi ku celin ah
+ La aqbal codsi ku celin ah
+ Laga noqoy codsi ku celin ah
+ La diid codsi ku celin ah
+ Ka noqo codsi ku celin ah
+ Eeg ciyaarta ku celinta ah
+ Hubso tallaabadaCiyaarSanduuqa
+ Luuqa
+ Gal luuqa
+ Wakhtigii baa kaa dhamaaday.
+ Qolka daawadaha
+ Farriin samee
+ HordhacDir
+
+ %s ciyaaraya
+ %s ciyaaraya
+ Horjeedahaagu wuxuu kaa dalbaday ka noqoshoKu noqo tartanka
- Jees bilaash ah oo onlayn ah. Ku ciyaar jees madal nadiif ah. Uma baahna diwaangelin, ma leh xayeysiis, umana baahna adeeg kale. La ciyaar jees aaladdaada, asxaabtaada amma dadka hawada ku jira.
+ Ku noqo ciyaarta
+ Jes bilaash ah oo onlayn ah. Ku ciyaar jes madal nadiif ah. Uma baahna diwaangelin, ma leh xayeysiis, umana baahna adeeg kale. La ciyaar jes aaladdaada, asxaabtaada amma dadka hawada ku jira.
+ %1$s wuxu ku biiray kooxda %2$sGoobtaDib u dajinBadbaadiyoHorjeedayaasha aad jeceshay
+ La soco %s
+ Ka hadh %sKa badanCiyaaryahanKu biirDhibcoHorjeedaha celceliska ahGaar ah
- Halxiraalaha
+
+ %s ciyaar baa socota
+ %s ciyaarood baa socda
+
+ Xujooyin
+ Waxa soo geliyey %s
+ Qoraalada
+ Halkan ku qor qoraalo kuu gooni ah
+ Magacan-akoonkan ama iimayl khaldanCodMarnaGuulaysoHorjeedeBulshadaQalabka
+ Daawo ciyaaro50 ciyaarood Fischer wuxuu ka keenay 47 guulood, 2 baraaje iyo 1 guuldarro.Abuur
+ Jes maalinle ah
+ Xalka eeg
+ Kulan degdeg ah
+ Dooro kulanQarsoodiDhibcahaaga: %sLuqaddaIftiinMadowHufan
+ Magacan-akoonkan waa la haystaa, fadlan tijaabi mid kale.
+ Magacan-akoonku waa inuu ku bilaabmo xaraf.
+ Magacan-akoonku waa inuu ku dhamaado xaraf ama tiro.
+ Magacan-akoonka waxa u banana xarfo, tiro, xarriiq hoose, iyo xarriiq isku xidhe ah. Xarriiquhu iskuma xigi karaan.
+ Magacan-akoonkan lama aqbali karo.TababarayaalXigaXalkaWaan ka xumahay :(
- Ganaaxaaga waxa ka hadhay: %s.Waayo?Dhibcaha abid
+ Jes maalinle ah: hal ama dhowr cisho dhaqaaqiiba
+ Tababaraha xeeladaha jestaCiyaarta >< %1$s
+ Caawintan tus
+ Is deji!Waxaad la ciyaaraysaa hadda %s.
+ Tuur ciyaarta
+ Is dhiib ciyaartan
+ Ciyaar kale ma bilaabi kartid inta aanay tani fhamaan.
+ Ka dib
+ Ka hor
+ Ciyaaraha qiimaysan ee ka dhacay Lichess
+ Beddel kooxda
diff --git a/translation/dest/site/sq-AL.xml b/translation/dest/site/sq-AL.xml
index a7987ce9b771e..ff0db42b4902b 100644
--- a/translation/dest/site/sq-AL.xml
+++ b/translation/dest/site/sq-AL.xml
@@ -271,7 +271,6 @@
Po luhet taniPo luhet taniPërfundoi
- përfundon %sNdërprite lojënLoja u ndërpreStandard
@@ -738,6 +737,7 @@ loje dhe URL për ta ndarë me të tjerë.
Me shokëtMe gjithkëndMënyra për fëmijë
+ Mënyra për fëmijë është e aktivizuar.Kjo është për sigurinë. Nën mënyrën për fëmijë, krejt komunikimet në sajt janë të çaktivizuara. Aktivizojeni këtë për fëmijët dhe nxënësit tuaj të shkollave, për t’i mbrojtur ata nga të tjerë përdorues të internetit.Nën mënyrën për fëmijë, stemës së Lichess-it i vihet një ikonë %s, që ta dini se fëmijët tuaj janë të parrezik.Llogaria juaj administrohet. Rreth heqjes së mënyrës “fëmijë” pyetni mëuesin tuaj të shahut.
@@ -851,9 +851,10 @@ loje dhe URL për ta ndarë me të tjerë.
edhe ruaj %s linja premoveedhe ruaj %s linja premove
+ Morët një mesazh privat nga Lichess.
+ Klikoni këtu që ta lexoniNa ndjeni :(Na u desh t’ju ndalnim për ca kohë.
- Ndalesa skadon më %s.Pse?Synojmë të ofrojmë përvojë të këndshme me shahun për këdo.Për këtë qëllim, duhet të garantojmë që krejt tarët të ndjekin praktikat e mira.
diff --git a/translation/dest/site/sr-SP.xml b/translation/dest/site/sr-SP.xml
index addb4f90390ea..3e80054bcf9d1 100644
--- a/translation/dest/site/sr-SP.xml
+++ b/translation/dest/site/sr-SP.xml
@@ -230,7 +230,6 @@
Управо играУправо играЗавршен
- завршава се %sПрекините партијуПартија прекинутаСтандардно
@@ -792,7 +791,6 @@
Извините :(Морали смо Вас привремено избацити.
- Тајм аут истиче %s.Зашто?Желимо да свима обезбедимо пријатно искуство у шаху.Баш због тога, ми се морамо потрудити да се сви играчи држе добре праксе.
diff --git a/translation/dest/site/sv-SE.xml b/translation/dest/site/sv-SE.xml
index 4b287bb1c0f9b..655982e649ed3 100644
--- a/translation/dest/site/sv-SE.xml
+++ b/translation/dest/site/sv-SE.xml
@@ -272,7 +272,6 @@
Spelas just nuSpelas just nuSlut
- slutar %sAvbryt partietPartiet avbrötsStandard
@@ -405,7 +404,7 @@
Importera partiKlistra in ett partis PGN-kod så får du en bläddringsbar uppspelning, en datoranalys, en spel-chatt och en delbar URL.Variationer kommer att raderas. För att behålla dem, importera PGN:en via en studie.
- Denna PGN kan nås av allmänheten. För att importera ett parti privat, använd en studie.
+ Denna PGN kan nås av allmänheten. För att importera ett parti privat, använd en studie.%s Importerat parti%s Importerade partier
@@ -507,7 +506,7 @@
Ändra profilFörnamnEfternamn
- Ställ in din flair:
+ Ställ in din flairFlairDet finns en inställning för att dölja alla användarflairs över hela webbplatsen.Biografi
@@ -862,9 +861,10 @@
och spara %s linje av förhandsdragoch spara %s linje av förhandsdrag
+ Du har fått ett privat meddelande från Lichess.
+ Klicka här för att läsa denBeklagar :(Vi är tvugna att stänga av dig en stund.
- Avstängningen upphör om %s.Varför?Vårt mål är att tillhandahålla alla en behaglig schackupplevelse till alla.Därför måste vi försäkra oss om att alla spelare följer god sed.
@@ -884,6 +884,7 @@
Jag instämmer med att jag kommer att följa alla Lichess-regler.Sök eller starta ny konversationRedigera
+ BulletBlitzSnabbschackClassical
diff --git a/translation/dest/site/sw-KE.xml b/translation/dest/site/sw-KE.xml
index eaf3ffdea45ab..789b7dae29b07 100644
--- a/translation/dest/site/sw-KE.xml
+++ b/translation/dest/site/sw-KE.xml
@@ -120,6 +120,5 @@
InachezwaInaendeleaImekamilika
- itakamilika ndani ya %scheza tena
diff --git a/translation/dest/site/ta-IN.xml b/translation/dest/site/ta-IN.xml
index 60d89fe709aae..cd171df462e73 100644
--- a/translation/dest/site/ta-IN.xml
+++ b/translation/dest/site/ta-IN.xml
@@ -272,7 +272,6 @@
இப்பொழுது ஆடுகின்றதுஇப்பொழுது ஓடிக்கொண்டிருக்கின்றனமுடிந்தது
- %s வினாடிகளில் முடியும்ஆட்டத்தைக் கலைஆட்டம் கலைந்ததுமரபு
@@ -494,7 +493,7 @@
தகவல்களை மாற்றுமுதல் பெயர்கடைசி பெயர்
- உங்கள் திறமையை அமைக்கவும்:
+ உங்கள் திறமையை அமைக்கவும்தளம் முழுவதும் அனைத்து பயனர் திறமைகளையும் மறைக்க ஒரு அமைப்பு உள்ளது.வாழ்க்கை சரித்திரம்நாடு அல்லது வட்டாரம்
@@ -797,7 +796,6 @@
மன்னிக்கவும் :(நாங்கள் உங்களை சிறிது நேரம் வெளியேற்ற வேண்டியிருந்தது.
- நேரம் முடிந்தது %s.ஏன்?அனைவருக்கும் இனிமையான சதுரங்க அனுபவத்தை வழங்குவதை நோக்கமாகக் கொண்டுள்ளோம்.அந்த வகையில், அனைத்து வீரர்களும் நல்ல பயிற்சியைப் பின்பற்றுவதை உறுதி செய்ய வேண்டும்.
diff --git a/translation/dest/site/te-IN.xml b/translation/dest/site/te-IN.xml
index 3a9ceeb7ec222..42aaf7a680bfe 100644
--- a/translation/dest/site/te-IN.xml
+++ b/translation/dest/site/te-IN.xml
@@ -225,7 +225,6 @@
ఆట ఆడుతున్నారుఆట ఆడుతున్నారుపూర్తయింది
- మిగిలిన సమయం %sఆట ఆగిపోయిందిగేమ్ ఆగిపోయినదిప్రామాణికం
diff --git a/translation/dest/site/th-TH.xml b/translation/dest/site/th-TH.xml
index e0ea6c625c5f7..648e3af0a7ff0 100644
--- a/translation/dest/site/th-TH.xml
+++ b/translation/dest/site/th-TH.xml
@@ -253,7 +253,6 @@
กำลังเล่นกำลังแข่งจบแล้ว
- เสร็จสิ้น%sยกเลิกเกมเกมถูกยกเลิกมาตรฐาน
@@ -373,7 +372,7 @@
นำเข้าเกมเมื่อวาง PGN ของเกมแล้ว คุณจะได้รับความสามารถเรียกดูการเล่นซ้ำ, การวิเคราะห์ด้วยคอมพิวเตอร์, แชทของเกม และ URL ที่สามารถแชร์ได้การเดินรูปแบบต่างๆ จะถูกลบทิ้ง หากต้องการเก็บไว้ ให้นำเข้า PGN ผ่านการศึกษา
- PGN นี้เป็นสาธารณะ หากอยากจะนำเข้าเกม โปรดใช้หน้ากรณีศึกษา
+ PGN นี้เป็นสาธารณะ หากอยากจะนำเข้าเกมแบบส่วนตัว โปรดใช้หน้ากรณีศึกษา%s เกมที่ถูกนำเข้า
@@ -471,6 +470,7 @@
ชื่อจริงนามสกุลตั้งค่ารูปตกแต่ง
+ รูปตกแต่งมันมีการตั้งค่าที่ทำให้ไม่สามารถเห็นรูปตกแต่งของผู้ใช้ได้ทั้งเวบไซต์ชีวประวัติประเทศหรือภูมิภาค
@@ -705,6 +705,7 @@
กับเพื่อนๆกับทุกคนโหมดเด็ก
+ โหมดเด็กถูกเปิดใช้งานสิ่งนี้เป็นเรื่องเกี่ยวกับความปลอดภัย ในโหมดสำหรับเด็ก การสื่อสารของไซต์ทั้งหมดจะถูกปิด จงใช้งานสิ่งนี้สำหรับเด็กและนักเรียนของคุณ เพื่อปกป้องพวกเขาจากผู้ใช้อินเทอร์เน็ตอื่นๆในโหมดสำหรับเด็ก โลโก้ lichess จะมีไอคอน %s เพื่อให้คุณรู้ว่าเด็กๆของคุณปลอดภัยบัญชีของคุณได้รับการจัดการ ถามครูหมากรุกของคุณเกี่ยวกับการยกระดับโหมดเด็ก
@@ -814,9 +815,10 @@
และบันทึก %s เส้นทางเดินล่วงหน้า
+ คุณได้รับข้อความส่วนตัวจาก Lichess
+ คลิกที่นี้เพื่ออ่านขออภัย :(เราจำเป็นต้องให้คุณรอคอยสักช่วงหนึ่ง
- เวลารอคอย สิ้นสุดใน %sทำไม?เรามุ่งหวังจะมอบประสบการณ์เกมหมากรุกที่น่าพอใจให้กับทุกคนเพื่อให้ได้ผล เราต้องแน่ใจว่าผู้เล่นทุกคนได้ปฏิบัติตามเป็นอย่างดี
@@ -836,6 +838,7 @@
ฉันยอมรับว่า ฉันจะปฏิบัติตามทุกนโยบายของ Lichessค้นหา หรือเริ่มการสนทนาใหม่แก้ไข
+ บุลเล็ตบลิตซ์แรปพิดคลาสสิก
diff --git a/translation/dest/site/tk-TM.xml b/translation/dest/site/tk-TM.xml
index d8559422824a2..c577e55fc56e3 100644
--- a/translation/dest/site/tk-TM.xml
+++ b/translation/dest/site/tk-TM.xml
@@ -198,7 +198,6 @@
Häzir oýnalýarProgressiýadaTamamlanan
- gutarýar %sOýny abort etOýun abort edildiStandart
diff --git a/translation/dest/site/tl-PH.xml b/translation/dest/site/tl-PH.xml
index 72c09152169d1..2f913a2ec859f 100644
--- a/translation/dest/site/tl-PH.xml
+++ b/translation/dest/site/tl-PH.xml
@@ -220,7 +220,6 @@
Maglalaro ngayonNaglalaro ngayonTapos na
- matatapos ng %sItigil ang laroAng laro ay natigilPamantayan
@@ -773,7 +772,6 @@
Pasensya na :(Kinailangan naming tapusin ang oras mo pansamantala.
- Ang pagtatapos ng oras ay magwawakas sa loob ng %s.Bakit?Nilalayon naming makapagbigay ng isang nakalulugod na karanasan sa ahedres para sa lahat.Sa ganoong kahulugan, kailangan naming siguruhin na ang lahat ng mga manlalaro ay susunod sa mabuting gawi.
diff --git a/translation/dest/site/tp-TP.xml b/translation/dest/site/tp-TP.xml
index 956fce94f819f..db8d4f39fea41 100644
--- a/translation/dest/site/tp-TP.xml
+++ b/translation/dest/site/tp-TP.xml
@@ -255,7 +255,6 @@
musi li lonmusi lon tenpo nini li pini
- pini %so pini e musimusi li pininasin pi ante ala
@@ -486,6 +485,7 @@ o musi lon lipu pona. sina wile ala pana e nimi li wile ala e ilo. sitelen esun
ante e sona jannimi jannimi mama
+ sitelen namakosona jan sinapona tawa sina!nimi nasin pi lipu toki jan
@@ -816,9 +816,10 @@ sina wile e ni la, o pali e tawa ken pi jan musi tu.
sina pali e ni la, tawa %s pi tenpo kama ken li awensina pali e ni la, tawa %s pi tenpo kama ken li awen
+ jan Lichess li toki len tawa sina
+ sina wile lukin e toki la o luka e nipakala a :(sina wile awen lon tenpo lili.
- tenpo kama la sina ken %s lon sin.tan seme?mi wile e musi pona tawa jan ale.tan ni la, mi wile e ni: jan ale li pona tawa nasin pi musi pona.
@@ -838,6 +839,7 @@ sina wile e ni la, o pali e tawa ken pi jan musi tu.
mi toki wawa e ni: mi pali ala e ijo ni: ona li ike tawa nasin lawa pi ilo Lichess.o lukin anu open e tokio ante
+ musi pi nasin Bulletnasin Rapidnasin Classicalmusi pi tenpo lili lili lili (tenpo Sekunta 30 li suli)
diff --git a/translation/dest/site/tr-TR.xml b/translation/dest/site/tr-TR.xml
index 41e0d1ce038ff..dd19bab3ffe45 100644
--- a/translation/dest/site/tr-TR.xml
+++ b/translation/dest/site/tr-TR.xml
@@ -272,7 +272,6 @@
Şu an oynanıyorŞu anda oynanıyorBitti
- %s sona erecekOyunu iptal etOyun iptal edildiStandart
@@ -405,7 +404,7 @@
Oyun yükleGöz atılabilir bir oyun tekrarı, bilgisayar analizi, oyun sohbeti ve paylaşılabilir bir URL edinmek için bir oyun PGN\'si yapıştırın.Varyasyonlar silinecek. Varyasyonları saklamak için bir çalışma aracılığıyla PGN\'yi içe aktarın.
- Bu PGN herkes tarafından erişilebilir. Bir oyunu özel olarak içe aktarmak isterseniz bir çalışma kullanın.
+ Bu PGN herkes tarafından erişilebilir. Bir oyunu özel olarak yüklemek istiyorsanız bir çalışma kullanın.%s yüklenen oyun%s yüklenen oyun
@@ -858,9 +857,10 @@
ve %s önceki varyantları kaydedinve %s önceki varyantları kaydedin
+ Lichess size bir özel mesaj gönderdi.
+ Okumak için buraya tıklayınÜzgünüz :(Sizi bir süreliğine oyunlardan men etmek zorunda kaldık.
- Men süresi %s dolacak.Neden?Herkese keyifli bir satranç deneyimi sunmayı amaçlıyoruz.Bu nedenle, bütün oyuncuların doğru davranışlar sergilemesine özen gösteriyoruz.
@@ -880,6 +880,7 @@
Lichess kurallarını takip edeceğim.Tartışma ara veya yenisini başlatDüzenle
+ KurşunYıldırımHızlıKlasik
diff --git a/translation/dest/site/tt-RU.xml b/translation/dest/site/tt-RU.xml
index 72df32c8322ac..ab0c7076f9e09 100644
--- a/translation/dest/site/tt-RU.xml
+++ b/translation/dest/site/tt-RU.xml
@@ -206,7 +206,6 @@
Хәзер уйналаХәзер уйналаТәмамланган
- %s сон бетәУенны туктатуУен туктатылдыСтандартлы
@@ -712,7 +711,6 @@
Юаныч :(Без сезне бераз вакытка чыгарырга тиеш идек.
- Тоту вакыты %s.Нигә?Без һәркемгә шаһмат тәҗрибәсен тәкъдим итәбез.Моның өчен без барлык уенчыларның да яхшы практиканы үтәргә тиеш.
diff --git a/translation/dest/site/uk-UA.xml b/translation/dest/site/uk-UA.xml
index 5f35d947037d0..6617eff55e7d4 100644
--- a/translation/dest/site/uk-UA.xml
+++ b/translation/dest/site/uk-UA.xml
@@ -302,7 +302,6 @@
Грається заразГрається просто заразЗавершено
- завершиться %sСкасувати груГру скасованоСтандартний
@@ -462,7 +461,6 @@
Вставте PGN гри щоб отримати повтор в браузері,
комп\'ютерний аналіз, ігровий чат та посилання, яким можна поділитися.Варіації будуть видалені. Для збереження імпортуйте PGN через дослідження.
- Цей PGN може бути у вільному доступі. Для імпорту гри в приватному режимі використовуйте студії.%s імпортована гра%s імпортовані гри
@@ -574,7 +572,7 @@
Редагувати профільІм\'яПрізвище
- Оберіть свій тотем:
+ Оберіть свій тотемЦе налаштування вимикає аватари всіх користувачів сайту.БіографіяКраїна чи область
@@ -942,7 +940,6 @@
Вибачте :(Нам довелося забанити вас на певний час.
- Вам залишилося відпочивати ще %s.Чому?Ми хочемо, щоб усім було приємно грати у нас в шахи.Щоб домігтися цього ефекту, ми повинні впевнитися, що всі гравці добре поводяться.
diff --git a/translation/dest/site/ur-PK.xml b/translation/dest/site/ur-PK.xml
index 03124d50d4816..3804cc48ae832 100644
--- a/translation/dest/site/ur-PK.xml
+++ b/translation/dest/site/ur-PK.xml
@@ -221,7 +221,6 @@
فی الحال کھیل میں مصروف ہےفی الحال کھیل میں مصروف ہےختم شد
- ختم ہو گا %sکھیل منسوخ کريںمنسوخ شدمعروف
@@ -744,7 +743,6 @@
معذرت :(ہمیں آپ پر کچھ عرصے کے لیے پابندی لگانا پڑی.
- پابندی %s کے بعد ختم ہو گی.کیوں?ہم سب کو خوشگوار شطرنج کا تجربہ دینا چاہتے ہیں.اس مقصد کے لیے، ہمیں یقینی بنانا چاہئے کہ تمام کھلاڑی اچھی پریکٹس کا مظاہرہ کریں.
diff --git a/translation/dest/site/uz-UZ.xml b/translation/dest/site/uz-UZ.xml
index 2345ff2330106..ff17b514a6c86 100644
--- a/translation/dest/site/uz-UZ.xml
+++ b/translation/dest/site/uz-UZ.xml
@@ -270,7 +270,6 @@
Xozirni o\'zidayoq o\'ynashXozirni o\'zidayoq o\'ynashTugadi
- %s tugaydiO\'yinni bekor qilishO\'yin bekor qilindiStandart
@@ -845,7 +844,6 @@
Kechirasiz :(Biz sizga bir muncha vaqtga taym aut berishimiz kerak edi.
- Taym aut %s dan keyin tugaydi.Nima uchun?Biz har bir kishiga shahmat malakasini oshirishni maqsad qilganmiz.Buning uchun biz barcha o\'yinchilar yaxshi amaliyot ortidan ketishi uchun imkoniyat yaratishimiz kerak.
diff --git a/translation/dest/site/vi-VN.xml b/translation/dest/site/vi-VN.xml
index b7c994c01fd3d..d56ab0e7b6ec8 100644
--- a/translation/dest/site/vi-VN.xml
+++ b/translation/dest/site/vi-VN.xml
@@ -15,8 +15,8 @@
Chịu thuaChiếu hếtHòa pat
- Trắng
- Đen
+ Quân Trắng
+ Quân Đenkhi chơi quân trắngkhi chơi quân đenChọn màu quân ngẫu nhiên
@@ -101,9 +101,9 @@
Chơi nước đầu tiên của người khám phá khai cuộc/tàn cuộcChiến thắng bị ngăn cản bởi luật 50 nướcVán đấu được cứu thua bởi luật 50 nước
- Giành chiến thắng hoặc 50 lần di chuyển do sai lầm trước
+ Giành chiến thắng hoặc 50 nước đi do sai lầm trướcThua hoặc 50 lần nước đi do nhầm lẫn trước đó
- Chỉ được đảm bảo thắng/thua nếu dòng tàn cuộc được đề xuất đã được tuân theo kể từ lần bắt hoặc ăn quân cuối cùng, do có thể làm tròn các giá trị DTZ trong sách tàn cuộc Syzygy.
+ Thắng/thua chỉ được đảm bảo nếu dòng tàn cuộc được đề xuất đã được tuân theo kể từ lần ăn quân hoặc tiến tốt cuối cùng, do có thể làm tròn các giá trị DTZ trong sách tàn cuộc Syzygy.Đã xong!Nhập PGNXóa
@@ -257,12 +257,11 @@
Đang diễn raĐang diễn raHoàn thành
- kết thúc trong %sHủy ván cờVán cờ đã bị hủy bỏTiêu chuẩnKhông giới hạn
- Chế độ
+ Thể loạiKhông xếp hạngCó xếp hạngKhông xếp hạng
@@ -283,7 +282,7 @@
Bạn đã bị tạm dừng trò chuyện.Phòng khán giảSoạn tin nhắn
- Chủ đề
+ Tiêu đềGửiGia tăng theo giâyChơi Cờ Vua Trực Tuyến Miễn Phí
@@ -379,7 +378,7 @@ Sự Kiện Cờ Đồng Loạt
Dán PGN của ván đấu để xem lại trên trình duyệt, phân tích bằng máy tính,
trò chuyện trong ván đấu và có một URL có thể chia sẻ công khai.Các biến sẽ bị xóa. Để giữ chúng, hãy nhập PGN thông qua một nghiên cứu.
- Ai cũng có thể truy cập PGN này. Để nhập ván cờ một cách riêng tư, hãy sử dụng nghiên cứu.
+ Ai cũng có thể truy cập PGN này. Để nhập ván cờ một cách riêng tư, hãy sử dụng nghiên cứu.%s ván cờ đã nhập
@@ -476,7 +475,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ công khai
Chỉnh sửa thông tin cá nhânTênHọ
- Đặt biểu tượng của bạn:
+ Đặt biểu tượng của bạnBiểu tượngCó một cài đặt để ẩn tất cả biểu tượng của người dùng trên toàn bộ trang web.Tiểu sử
@@ -489,7 +488,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ công khai
Xóa nước cờVán trước được lên Lichess TVCác kỳ thủ đang trực tuyến
- Các kỳ thủ tích cực
+ Những kỳ thủ tích cựcLưu ý, ván cờ có xếp hạng nhưng không tính thời gian!Thành công
@@ -617,7 +616,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ công khai
Thư viện videoCác StreamerỨng dụng Điện thoại
- Các nhà phát triển web
+ Nhà phát triển webVề chúng tôiGiới thiệu về %s%1$s là một máy chủ cờ vua miễn phí (%2$s), có mã nguồn mở và không có quảng cáo.
@@ -639,7 +638,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ công khai
Trong số 50 đối thủ, Fischer thắng 47, hoà 2 và thua 1.Ý tưởng được lấy từ các sự kiện có thật. Trong đời thực, một người chủ trì cờ đồng loạt sẽ di chuyển từ bàn này qua bàn khác và đánh một nước mỗi bàn.Khi cờ đồng loạt bắt đầu, mỗi người chơi sẽ bắt đầu ván cờ với người chủ trì. Cờ đồng loạt kết thúc khi tất cả các ván cờ hoàn tất.
- Cờ đồng loạt luôn không tính Elo. Việc tái đấu, đi lại hay cho thêm thời gian đều bị vô hiệu.
+ Cờ đồng loạt luôn không tính xếp hạng. Việc tái đấu, đi lại hay cho thêm thời gian đều bị vô hiệu.TạoKhi bạn tạo một sự kiện cờ đồng loạt, bạn sẽ chơi với nhiều người cùng một lúc.Nếu bạn chọn nhiều biến thể, mỗi người chơi sẽ được lựa chọn chơi biến thể nào.
@@ -673,7 +672,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ công khai
chơi nước đi đã chọnGiải đấu mớiGiải đấu cờ vua với nhiều thiết lập thời gian và biến thể phong phú
- Chơi các giải đấu cờ vua nhịp độ nhanh! Tham gia một giải đấu chính thức hoặc tự tạo giải đấu của bạn. Cờ Siêu Chớp, cờ Chớp, cờ Nhanh, cờ Chậm, Chess960, King of the Hill, Threecheck và nhiều lựa chọn khác cho niềm vui đánh cờ vô tận.
+ Chơi các giải đấu cờ vua nhịp độ nhanh! Tham gia một giải đấu chính thức hoặc tự tạo giải đấu của bạn. Cờ Đạn, cờ Chớp, cờ Nhanh, cờ Chậm, Chess960, King of the Hill, Threecheck và nhiều lựa chọn khác cho niềm vui đánh cờ vô tận.Không tìm thấy giải đấuGiải đấu này không tồn tại.Giải đấu có thể đã bị huỷ, nếu tất cả người chơi rời giải trước khi giải đấu bắt đầu.
@@ -761,7 +760,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ công khai
Tên người dùng phải kết thúc với một chữ cái hoặc một số.Tên người dùng chỉ được chứa chữ cái, số, dấu gạch nối và dấu gạch dưới. Dấu gạch dưới và dấu gạch nối không được liên tiếp nhau.Tên người dùng này không được chấp nhận.
- Chơi cờ vua phong cách
+ Chơi cờ vua theo phong cáchCờ cơ bảnHuấn luyện viênPGN không hợp lệ
@@ -822,9 +821,10 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ công khai
và lưu %s nước đi trước
+ Bạn đã nhận được một tin nhắn riêng từ Lichess.
+ Nhấn vào đây để đọc nóRất tiếc :(Chúng tôi phải ngừng bạn lại một thời gian.
- Hết hiệu lực trong %s.Tại sao?Mục tiêu của chúng tôi là cung cấp trải nghiệm chơi cờ vui vẻ cho mọi người.Để đạt được mục đích, chúng tôi phải chắc chắn rằng mọi người phải tuân thủ tốt.
@@ -844,6 +844,7 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ công khai
Tôi đồng ý rằng, tôi sẽ luôn tuân thủ các chính sách của Lichess.Tìm hoặc bắt đầu một cuộc trò chuyệnChỉnh sửa
+ Cờ đạnCờ chớpCờ nhanhCờ chậm
@@ -888,11 +889,11 @@ trò chuyện trong ván đấu và có một URL có thể chia sẻ công khai
Quản lý trang StreamerHủy giải đấuMô tả giải đấu
- Có điều gì đặc biệt bạn muốn nói với những người tham gia không? Cố gắng viết ngắn gọn. Các liên kết cấu trúc Markdown có sẵn: [name](https://url)
- Các ván đấu có tính Elo và tác động đến hệ số Elo của người chơi
+ Có điều gì đặc biệt bạn muốn nói với những người tham gia không? Cố gắng viết ngắn gọn. Các liên kết cấu trúc Markdown có sẵn: [Văn bản](https://url)
+ Các ván đấu có xếp hạng và ảnh hưởng đến hệ số Elo của người chơiChỉ thành viên trong độiKhông giới hạn
- Số ván đã tính Elo tối thiểu
+ Số ván đã xếp hạng tối thiểuElo tối thiểuElo cao nhất trong tuầnChỉ những kỳ thủ có danh hiệu
@@ -944,5 +945,5 @@ Bỏ trống để bắt đầu tất cả ván đấu bằng thế trận ban
Hướng dẫnCho tôi xem mọi thứ nàoLichess là một tổ chức phi lợi nhuận và là một phần mềm mã nguồn mở/hoàn toàn miễn phí.
-Tất cả chi phí vận hành, phát triển và nội dung được tài trợ bởi sự đóng góp của người dùng.
+Tất cả chi phí vận hành, phát triển và nội dung được tài trợ bởi những đóng góp của người dùng.
diff --git a/translation/dest/site/zh-CN.xml b/translation/dest/site/zh-CN.xml
index f85eb2c2ef63a..1f95ffe0a2453 100644
--- a/translation/dest/site/zh-CN.xml
+++ b/translation/dest/site/zh-CN.xml
@@ -257,7 +257,6 @@
正在对局正在进行已结束
- 结束:%s中止本局棋局已中止标准国际象棋
@@ -473,7 +472,7 @@
编辑个人资料名姓
- 设置你的图标:
+ 设置你的图标有一个设置可以隐藏整个站点上的所有用户图标。个人简介国家或地区
@@ -815,7 +814,6 @@
抱歉 :(我们必须将你停止一段时间。
- 剩余时间:%s为什么?我们致力于为所有人提供一个愉悦的下棋体验。因此,我们必须确保每个玩家都要遵循规范。
@@ -933,5 +931,6 @@
更换所持颜色关闭账户将撤回你的上诉举办赛事的小建议
+ 全部展示Lichess 是一个非盈利、完全免费自由的开源软件。所有的运维成本、开发以及内容完全来自用户捐赠。
diff --git a/translation/dest/site/zh-TW.xml b/translation/dest/site/zh-TW.xml
index f5368813edebc..abc288c787b8a 100644
--- a/translation/dest/site/zh-TW.xml
+++ b/translation/dest/site/zh-TW.xml
@@ -69,6 +69,7 @@
將這步棋導入主要流程中從這處開始刪除移除變化
+ 複製變體 PGN走棋您因特殊規則而輸了您因特殊規則而贏了
@@ -85,7 +86,7 @@
平均評分: %s最近的棋局評分最高的棋局
- 兩百萬局來自%2$s到%3$s年國際棋聯積分%1$s以上的棋手對局棋譜
+ 來自%2$s到%3$s年國際棋聯積分%1$s以上的棋手對局棋譜在%s步內將死對手
@@ -97,6 +98,7 @@
開局瀏覽器開局與終局瀏覽器%s開局瀏覽器
+ 在開局/殘局瀏覽器走第一步棋在不違反50步和局規則下贏得這局棋藉由50步和局規則來避免輸掉棋局贏棋或因先前錯誤50步作和
@@ -112,6 +114,7 @@
打開研究視窗開啟最佳移動的箭頭
+ 顯示變體箭頭棋力估計表路線分析線CPU
@@ -254,7 +257,6 @@
正在對局正在進行已結束
- %s 後結束中止本局棋局已中止標準
@@ -449,6 +451,7 @@
步數白方獲勝黑方獲勝
+ 和棋率和棋下一個%s錦標賽平均對手評分
@@ -469,7 +472,11 @@
編輯資料名姓
+ 設置你的圖標
+ 圖標
+ 有一個設置可以隱藏整個網站上所有用户圖標。個人簡介
+ 國家或地區謝謝!官方社群連結每行一個網址
@@ -617,8 +624,10 @@
車輪戰主持主持者使用旗子顏色:%s
+ 你待處理的車輪戰最近开始的同步赛主持新同步赛
+ 註冊以舉辦或參與車輪戰找不到该同步赛此車輪戰不存在。返回表演赛主页
@@ -643,6 +652,7 @@
快捷键后退/前进跳到开始/结束
+ 循環已選取的變體显示/隐藏评论进入/退出变体請求引擎分析,從你的失誤中學習
@@ -650,6 +660,13 @@
下一個漏著下一個錯著下一個疑著
+ 上一個分支
+ 下一個分支
+ 切換變體箭頭
+ 循環上一個/下一個變體
+ 切換圖形標註
+ 變體箭頭讓你不需棋步列表導航
+ 走已選的棋步新比赛国际象棋赛事均设有不同的时间控制和变体加入快節奏的國際象棋比賽!加入定時賽事,或創建自己的。子彈,閃電,經典,菲舍爾任意制,王到中心,三次將軍,並提供更多的選擇為無盡的國際象棋樂趣。
@@ -691,6 +708,7 @@
與好友分享與所有人分享兒童模式
+ 已啓用兒童模式考量安全,在兒童模式中,網站上全部的文字交流將會被關閉。開啟此模式來保護你的孩子及學生不被網路上的人傷害。在兒童模式下,Lichess的標誌會有一個%s圖示,讓你知道你的孩子是安全的。你的帳戶被管理,詢問你的老師解除兒童模式。
@@ -800,9 +818,10 @@
以儲存%s列預走的棋步
+ 你收到一個來自 Lichess 的私人信息。
+ 點擊閱讀抱歉:(您被封鎖了,在一陣子的時間內將不能下棋
- 封鎖結束至%s為什麼?我們的目的在於為所有人提供愉快的國際象棋體驗為此,我們必須確保所有參與者都遵循良好做法
@@ -822,9 +841,10 @@
我同意我將會遵守Lichess的規則尋找或開始聊天編輯
+ 快棋快速模式經典
- 瘋狂速度模式:低於30秒
+ 瘋狂速度模式: 低於30秒非常速度模式:低於3分鐘快速模式:3到8分鐘一般模式:8到25分鐘
@@ -919,6 +939,8 @@
更換所持顏色關閉帳戶將會收回你的上訴舉辦賽事的小建議
+ 說明
+ 全部顯示Lichess是個慈善、完全免費之開源軟件。
一切營運成本、開發和內容皆來自用戶之捐贈。
diff --git a/translation/dest/site/zu-ZA.xml b/translation/dest/site/zu-ZA.xml
index e00f6e50b7d49..8d4c9990c3d2d 100644
--- a/translation/dest/site/zu-ZA.xml
+++ b/translation/dest/site/zu-ZA.xml
@@ -199,7 +199,6 @@
Iyadlala manjeIyadlala manjeKuqediwe
- kuqeda %sLahla umdlaloUmdlalo uhoxisiweOkujwayelekile
diff --git a/translation/dest/storm/ar-SA.xml b/translation/dest/storm/ar-SA.xml
index 5aef1e99c94df..a240eb7f144eb 100644
--- a/translation/dest/storm/ar-SA.xml
+++ b/translation/dest/storm/ar-SA.xml
@@ -1,12 +1,12 @@
حَرِك لتبدأ
- أنت تلعب القطع البيضاء في جميع الألغاز
- أنت تلعب القطع السوداء في جميع الألغاز
- الألغاز المحلولة
- أعلى نتيجة يومية جديدة!
- أعلى نتيجة أسبوعية جديدة!
- أعلى نتيجة شهرية جديدة!
+ أنت تلعب بالقطع البيضاء في جميع الألغاز
+ أنت تلعب بالقطع السوداء في جميع الألغاز
+ الألغاز التي حللتها سابقا
+ حققت نتيجة يومية جديدة!
+ حققت نتيجة أسبوعية جديدة!
+ حققت نتيجة شهرية جديدة!أعلى مستوى جديد على الإطلاق!النتيجة العالية السابقة كانت %sإلعب مرة أخرى
@@ -34,12 +34,12 @@
الوقتالوقت لكل نقلةتقييم أصعب لغز تم حله
- الألغاز الملعوبة
+ الألغاز التي لعبتها سابقاسباق جديدانهاء السباقأعلى النتائجاعرض أفضل الجولات
- أفضل جولة في اليوم
+ أفضل جولة لك اليومجولاتاستعد!ينتظر انضمام مزيد من الاعبين...
@@ -60,7 +60,7 @@
تخطى هذه الحركة للحفاظ على سلسلة الانتصارات.الألغاز التي فشلت في حلهاألغاز بطيئة
- الالغاز التي تم تخطيها
+ الألغاز التي تخطيتهاهذا الأسبوعهذا الشهركل الأوقات
diff --git a/translation/dest/storm/da-DK.xml b/translation/dest/storm/da-DK.xml
index 37861789f16cb..75bb1adc834c3 100644
--- a/translation/dest/storm/da-DK.xml
+++ b/translation/dest/storm/da-DK.xml
@@ -3,7 +3,7 @@
Ryk for at starteDu har de hvide brikker i alle opgaverDu har de sorte brikker i alle opgaver
- opgaver løst
+ taktikopgaver løstNy dagsrekord!Ny ugerekord!Ny månedsrekord!
@@ -26,7 +26,7 @@
TidTid per trækSværeste løst
- Spillede opgaver
+ Spillede taktikopgaverNy runde (genvejstast: Mellemrum)Afslut runde (genvejstast: Enter)Rekorder
@@ -50,9 +50,9 @@
spring overNYT! Du kan springe ét træk over pr. race:Spring dette træk over for at bevare din kombo! Virker kun én gang pr. race.
- Mislykkede opgaver
- Langsomme opgaver
- Oversprunget opgave
+ Mislykkede taktikopgaver
+ Langsomme taktikopgaver
+ Oversprunget taktikopgaveDenne ugeDenne månedAlle tiders
diff --git a/translation/dest/streamer/be-BY.xml b/translation/dest/streamer/be-BY.xml
index 128eb010ebaa0..8e11d4b329df3 100644
--- a/translation/dest/streamer/be-BY.xml
+++ b/translation/dest/streamer/be-BY.xml
@@ -4,8 +4,8 @@
Стрымер на LichessУ ЭФІРЫ!АФЛАЙН
- Зараз стрыміць: %s
- Апошні стрым %s
+ Зараз стрыміць: %s
+ Апошні стрым %sСтаць страмерам на LichessУ вас ёсць каналы на Twitch або YouTube?Пачынаем!
@@ -29,9 +29,10 @@
Ваш стрым разглядаецца мадэратарамі.Калі ласка, запоўніце інфармацыю наконт стрыму і загрузіце выяву.Калі будзіце гатовы быць адлюстраваным як стрымер Lichess, %s
- запрасіць прагляд мадэратара
- Ваша імя карыстальніка/спасылка на Twich
+ запрасіць прагляд мадэратара
+ Ваша імя карыстальніка/спасылка на TwichАпцыянальна. Калі няма, пакіньце пустым
+ ID вашага YouTube каналаВаша стрымерскае імя на LichessСцісла: максімальна %s сімвал
diff --git a/translation/dest/study/fi-FI.xml b/translation/dest/study/fi-FI.xml
index 980e1a5c7eefa..12e9cd8953c6b 100644
--- a/translation/dest/study/fi-FI.xml
+++ b/translation/dest/study/fi-FI.xml
@@ -62,7 +62,7 @@
Tutkielman PGNLataa kaikki pelitLuvun PGN
- Kopioi PGN
+ Kopioi PGNKopioi tämän luvun PGN leikepöydälle.Lataa peliTutkielman URL
@@ -131,7 +131,7 @@
Haluatko poistaa tutkielman keskusteluhistorian? Et voi palauttaa sitä enää!Poista tutkielmaPoistetaanko koko tutkielma? Et voi palauttaa sitä enää. Vahvista poisto kirjoittamalla tutkielman nimen: %s
- Missä haluat tutkia?
+ Missä haluat tutkia tätä?Hyvä siirtoVirheLoistava siirto
diff --git a/translation/dest/study/gsw-CH.xml b/translation/dest/study/gsw-CH.xml
index 5ea7f88582bba..ddaedd93b8ded 100644
--- a/translation/dest/study/gsw-CH.xml
+++ b/translation/dest/study/gsw-CH.xml
@@ -62,7 +62,7 @@
Schtudie PGNLad alli Partie abeKapitel PGN
- PGN kopiere
+ PGN kopierePGN-Kapitel id Zwüscheablag kopiere.Lad die Partie abeSchtudie URL
@@ -109,8 +109,8 @@
URL vu de PartiePartie vu %1$s oder %2$s ladeKapitäl ärschtelä
- Schtuudiä ärschtelä
- Schtuudiä bearbeitä
+ Schtudie erschtelle
+ Schtudie bearbeiteSichtbarkeitÖffentlichUnglischtet
@@ -128,10 +128,10 @@
SchtartSchpeichäräTschätt löschä
- Tschättverlauf vu de Schtudie lösche? Das chann nüme rückgängig gmacht werde!
- Schtuudiä löschä
+ Chatverlauf vu de Schtudie lösche? Das chann nüme rückgängig gmacht werde!
+ Schtudie löscheDie ganz Schtudie lösche? Es git keis Zrugg! Gib zur Beschtätigung de Name vu de Schtudie i: %s
- Weli Schtuudiä wöttsch bruuchä?
+ Welli Schtudie wottsch bruche?Guete ZugFählerBriliantä Zug
diff --git a/translation/dest/study/vi-VN.xml b/translation/dest/study/vi-VN.xml
index 72f5da67091bb..45a676d9f3d38 100644
--- a/translation/dest/study/vi-VN.xml
+++ b/translation/dest/study/vi-VN.xml
@@ -59,7 +59,7 @@
PGN nghiên cứuTải xuống tất cả ván đấuPGN chương
- Sao chép PGN
+ Sao chép PGNSao chép PGN chương vào bảng nhớ tạm.Tải xuống ván cờURL nghiên cứu
@@ -97,12 +97,12 @@
Bắt đầu từ thế cờ tùy chỉnhTải ván cờ bằng URLTải thế cờ từ chuỗi FEN
- Tải thế cờ từ PGN
+ Tải ván cờ từ PGNTự độngDán PGN ở đây, tối đa %s ván
- Đường dẫn của các ván, một đường dẫn mỗi dòng
+ URL của các ván, một URL mỗi dòngTải ván cờ từ %1$s hoặc %2$sTạo chươngTạo nghiên cứu
@@ -141,7 +141,7 @@
Bên trắng có một chút lợi thếBên đen có một chút lợi thếLợi thế bên trắng
- Lợi thế bên đen
+ Bên đen lợi thế hơnBên trắng đang thắng dầnBên đen đang thắng dầnNước cờ mới
diff --git a/translation/dest/swiss/ar-SA.xml b/translation/dest/swiss/ar-SA.xml
index 1eea702aeb60d..e606cfa288f51 100644
--- a/translation/dest/swiss/ar-SA.xml
+++ b/translation/dest/swiss/ar-SA.xml
@@ -89,34 +89,39 @@
كم من البايت يستطيع اللاعب الحصول عليها؟اللاعب يحصل على نقطة واحدة في كل مرة لا يستطيع فيها نظام الإقران العثور على إقران لهم.
بالإضافة إلى ذلك، يتم منحهم نصف نقطة عندما ينضم لاعب متأخر إلى البطولة.
+ ماذا سيحدث في حالة التعادلات المبكرة؟
+ في البطولات السويسرية، لا يمكن للاعبين أن يتفقوا على التعادل قبل إكمال 30 نقلة. ورغم أن هذا التدبير لا يمنع الاتفاق المسبق على التعادل، لكنه يزيد من صعوبة حدوثها.ماذا يحدث إذا لم يلعب اللاعب المباراة؟سيمر وقتهم ويخسرون بسبب الوقت.
وعندها سيقوم النظام بإخراجهم من البطولة لكي لا يخسروا مزيداً من الجولات.
يمكنهم إعادة الانضمام للبطولة متى ما شاؤوا.
+ ماذا يحدث للبطولات غير المعروضة؟
+ اللاعبون الذين يسجلون في البطولات السويسرية، ولكنهم لا يلعبون مبارياتهم قد يسببون مشكلة.
+لذا، يُمنع اللاعبين الذين يفعلون ذلك من الانضمام إلى حدث سويسري جديد لفترة زمنية معينة، لكن يمكن لمنشئ البطولة السويسرية أن يقرر ضمهم إليها مع ذلك.هل يمكن للاعب الانضمام متأخراً؟نعم، إلى أن يبدأ أكثر من نصف المباريات؛ على سبيل المثال في إحدى عشرة جولة يمكن للاعبين أن ينضموا قبل أن تبدأ الجولة السادسة وفي 12 جولة قبل بدء الجولة السابعة.
الانضمام المتأخر يحصل على جزء واحد، حتى لو فاتهم عدة جولات.هل ستحل البطولات السويسرية محل البطولات العادية؟لا، هذه ميزات تكميلية.ماذا عن جولة روبين؟
- نود أن نضيفه، لكن لسوء الحظ أن جولة روبين لا تعمل على الإنترنت.
-السبب هو أنه ليس لديها طريقة عادلة للتعامل مع الأشخاص الذين يغادرون المسابقة مبكرا. لا يمكننا أن نتوقع أن يلعب جميع اللاعبين كل ألعابهم في حدث على الانترنت. لن يحدث ذلك فقط، ونتيجة لذلك فإن معظم بطولات روبين ستكون معيبة وغير عادلة.
-أقرب ما يمكنك الوصول إلى جولة روبن على الإنترنت هو لعب بطولة سويسرية بعدد كبير جدا من الجولات. ثم سيتم تشغيل جميع الأزواج الممكنة قبل انتهاء البطولة.
- ماذا عن أنظمة البطولة الأخرى؟
- نحن لا نخطط لإضافة المزيد من أنظمة البطولة إلى ليشيس في الوقت الحالي.
- في بطولة سويسرية %1$s، لا يلعب كل منافس بالضرورة جميع الاعبين الآخرين. يلتقي المنافسون الواحد تلو الآخر في كل جولة من الجولات ويقترنون باستخدام مجموعة من القواعد المصممة لضمان أن يلعب كل منافس معارضين بنفس الدرجة من التشغيل. ولكن ليس نفس الخصم أكثر من مرة. الفائز هو المنافس ذو أعلى مجموع النقاط المكتسبة في جميع الجولات. كل المنافسين يلعبون في كل جولة ما لم يكن هناك عدد فردي من اللاعبين.\"
- لا يمكن إنشاء البطولات السويسرية إلا من قبل قادة الفريق، ولا يمكن لعبها إلا من قبل أعضاء الفريق.
-%1$s لبدء اللعب في البطولات.
+ نود أن نضيفها، لكن لسوء الحظ أن لا يمكن لعب جولة روبين على الإنترنت.
+وذلك لعدم توفر طريقة عادلة للتعامل مع الأشخاص الذين يغادرون المسابقة مبكرا. فلا يمكننا أن نضمن أن يلعب جميع اللاعبين كل مبارياتهم في بطولة على الانترنت، ونتيجة لذلك فإن معظم بطولات روبين ستكون معيبة وغير عادلة.
+إذا أردت لعب جولة روبن على الإنترنت فيمكنك لعب بطولة سويسرية بعدد كبير جدا من الجولات، بحيث تلعب مع كل المشاركين في البطولة.
+ ماذا عن أنماط البطولات الأخرى؟
+ نحن لا نخطط لإضافة المزيد من أنماط البطولات إلى ليتشيس في الوقت الحالي.
+ في البطولات السويسرية %1$s، لا يلعب كل لاعب بالضرورة مع جميع اللاعبين الآخرين، وإنما يلتقي خصما واحدا في كل جولة من الجولات، يحدد بواسطة مجموعة من القواعد لضمان أن يلعب كل لاعب مع خصوم من مستواهم نفسه، ولكن لا يمكن أن يلعب ضد الخصم نفسه أكثر من مرة. الفائز هو اللاعب الذي جمع أكبر عدد من النقاط في البطولة، سيلعب جميع اللاعبين في كل الجولات ما لم يكن عددهم فرديا.
+ لا يمكن إنشاء البطولات السويسرية إلا من قبل قادة الفرق، ولا يمكن المشاركة فيها إلا من قبل أعضاء الفريق.
+سجل في فريق%1$s لبدء اللعب في البطولات.انضم أو أنشأ فريق
- الملعوبة حالياً
+ البطولات الملعوبة الآنستبدأ قريباًالمقارنةمدة المسابقةمدة محددة مسبقاً في دقائقالحد الأقصى للجولات المحددة مسبقاً، ولكن المدة غير معروفةعدد المباريات
- أكبر عدد ممكن من اللعب في الفترة المخصصة
- تقرر مسبقاً، نفس الشيء لجميع اللاعبين
+ أكبر عدد ممكن من المباريات في المدّة المختارة
+ تقرر مسبقاً، يطبق على جميع اللاعبيننظام الاقترانأي خصم متاح حيث يكون الخصم مقارب بالتصنيفافضل ربط بين اللاعبين اعتماداً على النقاط و نتيجة كسر التعادل
@@ -134,5 +139,6 @@
مشابه للبطولات التي تقام على الرقعةغير محدود و مجانيالسماح فقط للمستخدمين المحددين مسبقاً بالانضمام
- إذا كانت هذه القائمة غير فارغة، فسيتم منع أسماء المستخدمين المتغيبين من هذه القائمة من الانضمام. اسم مستخدم واحد لكل سطر.
+ إذا كانت هذه القائمة غير فارغة، فسيمنع اللاعبون غير الواردة أسماءهم في هذه القائمة من الانضمام إلى البطولة. اسم مستخدم واحد لكل سطر.
+ ألعب مبارياتك
diff --git a/translation/dest/swiss/tp-TP.xml b/translation/dest/swiss/tp-TP.xml
index aba00d0963526..b72ea21b2875b 100644
--- a/translation/dest/swiss/tp-TP.xml
+++ b/translation/dest/swiss/tp-TP.xml
@@ -1,6 +1,10 @@
utala musi pi nasin Suwasi
+
+ musi %s
+ musi %s
+ lipu lawa pi kulupu FIDEo awennanpa sin pi tenpo lili en nanpa sin pi pona mute poka
diff --git a/translation/dest/swiss/vi-VN.xml b/translation/dest/swiss/vi-VN.xml
index 8a6d52617ed10..bacf086337659 100644
--- a/translation/dest/swiss/vi-VN.xml
+++ b/translation/dest/swiss/vi-VN.xml
@@ -61,8 +61,8 @@ Ngoài ra, 0.5 điểm \"bye\" cộng một lần duy nhất sẽ được cộn
Sau đó, hệ thống sẽ tự động rút họ khỏi giải đấu để tránh thua thêm những ván khác.
Họ có thể tham gia lại bất cứ lúc nào.
Những gì được thực hiện liên quan đến vắng mặt?
- Người chơi đăng ký các sự kiện của giải hệ Thụy Sĩ nhưng không chơi trò chơi của họ có thể gặp vấn đề.
-Để giải quyết vấn đề này, Lichess ngăn những người chơi không chơi được trò chơi tham gia một sự kiện hệ Thụy Sĩ mới trong một khoảng thời gian nhất định.
+ Người chơi đăng ký các sự kiện của giải hệ Thụy Sĩ nhưng không chơi ván của họ có thể gặp vấn đề.
+Để giải quyết vấn đề này, Lichess ngăn những người chơi không chơi được ván đấu tham gia một sự kiện hệ Thụy Sĩ mới trong một khoảng thời gian nhất định.
Người tạo ở sự kiện hệ Thụy Sĩ vẫn có thể quyết định cho phép họ tham gia sự kiện đó.Người chơi có thể tham gia muộn không?Có, trước khi quá nửa số vòng của giải đã bắt đầu; ví dụ: trong 1 giải hệ Thụy Sĩ 11 vòng, người chơi có thể tham gia trước khi vòng 6 bắt đầu, còn trong giải hệ Thụy Sĩ 12 ván sẽ là trước khi vòng 7 bắt đầu.
@@ -84,7 +84,7 @@ Thứ gần nhất bạn có thể có với một giải đấu vòng tròn là
Khoảng thời gian của giải đấuChọn trước khoảng thời gian theo phútChọn trước khoảng thời gian theo vòng, nhưng không xác định về thời gian
- Số lượng trò chơi
+ Số lượng ván đấuSố lượng tối đa ván đấu có thể chơi cho tới khi hết giờGiới hạn số ván, đồng đều cho tất cả người chơiHệ thống bắt cặp
diff --git a/translation/dest/team/gsw-CH.xml b/translation/dest/team/gsw-CH.xml
index 8ce0578d6d46e..651bf8441dc4f 100644
--- a/translation/dest/team/gsw-CH.xml
+++ b/translation/dest/team/gsw-CH.xml
@@ -9,7 +9,7 @@
Alli TeamsVu dir gleiteti TeamsNeus Team
- Mini Teams
+ Eigeni TeamsKeis Team g\'fundeTritt em Team biGib dem Team en neue Leiter, bevor du gahsch - oder lös das Team uf.
@@ -39,7 +39,7 @@
Benachrichtig alli MitgliderSchick e privati Nachricht a alli Mitglider vom TeamSchick e privati Nachricht a ALLI Mitglider vu dem Team. Du chasch die Funktion benutze, um Schpiller uf z\'fordere sich ame Turnier oder ame Team-Kampf z\'beteilige. Schpiller wo die Nachrichte nöd wänd empfange, chönd s\'Team verlah.
- Teams wonich leitä
+ Teams wo ich leiteWottsch eis vu de kommende Turnier verlinke?Teamleiter-ChatTeam uflöösä
diff --git a/translation/dest/team/lb-LU.xml b/translation/dest/team/lb-LU.xml
index f8f4f95a0b18c..49a74aba7b495 100644
--- a/translation/dest/team/lb-LU.xml
+++ b/translation/dest/team/lb-LU.xml
@@ -25,7 +25,10 @@
All d\'Memberen kontaktéierenEkipp opléisenLéist d\'Ekipp fir ëmmer op.
+ Falsche Bäitrëttscode.Dës Ekipp gëtt et schonn.
+ Nächst Turnéieren
+ Vergaangen TurnéierenOfgeleenten UfroenEkippensäit
diff --git a/translation/dest/team/vi-VN.xml b/translation/dest/team/vi-VN.xml
index 88ebbacab5214..e3b7f3e6201b6 100644
--- a/translation/dest/team/vi-VN.xml
+++ b/translation/dest/team/vi-VN.xml
@@ -25,10 +25,10 @@
%s yêu cầu tham giaYêu cầu tham gia của bạn sẽ được xem xét bởi đội trưởng.
- Yêu cầu tham gia của bạn đang được xem xét bởi đội trưởng.
- Yêu cầu tham gia của bạn bị từ chối bởi đội trưởng.
+ Yêu cầu tham gia của bạn đang được đội trưởng xem xét.
+ Yêu cầu tham gia của bạn đã bị từ chối bởi một đội trưởng.Đăng kí nhận tin nhắn của đội
- Giải đấu đa đội
+ Giải đa độiMột trận đấu giữa nhiều đội, mỗi người chơi ghi điểm cho đội của họGiải đấu trong độiMột giải đấu Đấu trường mà chỉ thành viên trong đội của bạn có thể tham gia
@@ -36,7 +36,7 @@
Nhắn tin tới tất cả thành viênGửi một tin nhắn riêng cho mỗi thành viên của độiGửi tin nhắn riêng tới TẤT CẢ các thành viên của đội.
-Bạn có thể dùng chức năng này để gọi các người chơi đến tham gia một giải đấu hoặc giải đấu đa đội.
+Bạn có thể dùng chức năng này để gọi các người chơi đến tham gia một giải đấu hoặc giải đa đội.
Những người chơi không muốn nhận tin nhắn của bạn có thể sẽ rời khỏi đội.Những đội mà tôi làm đội trưởngCó thể bạn muốn đường dẫn tới một trong những giải đấu sắp tới?
diff --git a/translation/dest/tfa/ar-SA.xml b/translation/dest/tfa/ar-SA.xml
index ae894f2acb728..dfa96d742afff 100644
--- a/translation/dest/tfa/ar-SA.xml
+++ b/translation/dest/tfa/ar-SA.xml
@@ -1,15 +1,18 @@
- المصادقة الثنائية
- المصادقة الثنائية تضيف طبقة أمان أخرى الى حسابك.
+ التوثيق ذو العاملين
+ التوثيق ذو العاملين يضيف طبقة أمان أخرى إلى حسابك.
+ احصل على تطبيق يحوي مزيّة التوثيق ذو العاملين. نوصي بالتطبيقات التالية:أمسح رمز ال QR باستخدام التطبيق.
- أدخل كلمة المرور الخاصة بك ورمز المصادقة الذي تم إنشاؤه بواسطة التطبيق لإكمال الإعداد. سوف تحتاج إلى رمز المصادقة في كل مرة تقوم بتسجيل الدخول.
- إذا لم تتمكن من مسح الرمز ، قم بإدخال السر %s في التطبيق الخاص بك.
+ أدخل كلمة المرور الخاصة بك ورمز المصادقة من التطبيق لإكمال الإعداد. سوف تحتاج إلى رمز المصادقة في كل مرة تسجل فيها الدخول.
+ إذا لم تتمكن من مسح الرمز، أدخل الرمز %s في التطبيق الخاص بك.رمز التحقق
- تفعيل المصادقة الثنائية
- تعطيل المصادقة الثنائية
- المصادقة الثنائية مفعلة
- افتح تطبيق المصادقة الثنائية على جهازك لعرض رمز المصادقة الخاص بك والتحقق من هويتك.
- الرجاء تمكين المصادقة الثنائية لتأمين حسابك على https://lichess.org/account/twofactor.
-لقد تلقيت هذه الرسالة لأن حسابك لديه مسؤوليات خاصة مثل قائد الفريق أو المدرب أو المعلم أو صاحب بث
+ ملاحظة: إذا فقدت الوصول إلى رموز التوثيق ذو العاملين، فيمكنك إعادة تعيين كلمة المرور عبر%s البريد الإلكتروني.
+ تفعيل التوثيق ذو العاملين
+ تعطيل التوثيق ذو العاملين
+ التوثيق ذو العاملين مفعل
+ تحتاج إلى كلمة المرور الخاصة بحسابك ورمز التفعيل من التطبيق الذي تستخدمه لتعطيل التوثيق ذو العاملين.
+ افتح تطبيق التوثيق ذو العاملين على جهازك لعرض رمز التوثيق الخاص بك والتحقق من هويتك.
+ يرجى تمكين التوثيق ذو العاملين لتأمين حسابك على https://lichess.org/account/twofactor.
+لقد تلقيت هذه الرسالة لأن حسابك لديه صلاحيات خاصة مثل قائد الفريق أو المدرب أو المعلم أو صاحب بث.
diff --git a/translation/dest/tfa/gsw-CH.xml b/translation/dest/tfa/gsw-CH.xml
index 6290c2295e58c..6e48da2421fb7 100644
--- a/translation/dest/tfa/gsw-CH.xml
+++ b/translation/dest/tfa/gsw-CH.xml
@@ -1,19 +1,19 @@
- Zwei-Faktorä Autentifizierig
- Mit de \"Zwei-Faktor Authentifizierig\" machsch dis Konto es Level sicherer.
+ Zwei-Faktor-Autentifizierig
+ Mit de \"Zwei-Faktor-Authentifizierig\" machsch dis Konto um es Level sicherer.B\'sorg dir doch e App, für die Zwei-Faktor-Authentifizierig. Mir empfehled folgendi Apps:
- Scann dä QR Code mit dä App.
+ Scann de QR Code mit de App.Tipp dis Passwort ine und de Authentifizierigscode, wo vu de App generiert worde isch, zum Abschlüsse vu dere Irichtig. Künftig bruchsch, zum Amälde, jedesmal so en Code.
- Falls dä Code nicht einscannä chasch, tipp dä gheim Code %s i dini App.
+ Falls du de Code nöd chasch iscänne, tipp de G\'heimcode %s i dini App.AuthentifizierigscodeHiwis: Wänn du de Zuegriff uf dini Cods für d\'Zwei-Faktor-Authentifizierig verlürsch, chasch per E-Mail en %s schicke.Aktivier d\'Zwei-Faktor-AuthentifizierigDeaktivier d\'Zwei-Faktor-Authentifizierig
- Zwei-Faktor Authentifizierig isch aktiviert
- Du bruchsch dis Passwort und en Code vu dinere Authenticator-App, zum Deaktiviere vu de Zwei-Faktor-Authentifizierig.
- Mach uf dim Grät d\'App für die zweischtufig Authentifizierig uf, det findsch din Authentifizierigscode, wo dini Identität beschtätigt.
- Bitte tue d\'Zwei-Faktore-Authentifizierig aktiviere und demit dis Konto uf -
-https://lichess.org/account/twofactor - sichere!
-Du häsch de Hiwis übercho, will du - mit dim Konto - bsunderi Funkzione usüebsch, z.B. als Teamleiter, als Trainer, als Lehrer oder Streamer.
+ Zwei-Faktor-Authentifizierig isch aktiviert
+ Zur Deaktivierig vu de Zwei-Faktor-Authentifizierig bruchsch dis Passwort und en Code vu de Authenticator-App.
+ Mach d\'App für d\'Zwei-Faktor-Authentifizierig uf, det findsch de Code, wo dini Identität beschtätigt.
+ Bitte d\'Zwei-Faktore-Authentifizierig aktiviere und dis Konto uf -
+https://lichess.org/account/twofactor - sichere!
+Du chunsch die Mäldig über, will du - mit dim Konto - b\'sunderi Funkzione usüebsch, z.B. als Teamleiter, als Trainer, als Lehrer oder Streamer.
diff --git a/translation/dest/tfa/it-IT.xml b/translation/dest/tfa/it-IT.xml
index 53ea761dde250..51a0e80548824 100644
--- a/translation/dest/tfa/it-IT.xml
+++ b/translation/dest/tfa/it-IT.xml
@@ -7,9 +7,11 @@
Inserisci la tua password e il codice di autenticazione generato dall\'app per completare l\'installazione. Avrai bisogno di un codice di autenticazione ogni volta che effettui l\'accesso.Se non riesci a scansionare il codice, inserisci il codice segreto %s nell\'app.Codice di autenticazione
+ Nota: Se perdi l\'accesso ai tuoi codici d\'autenticazione a due fattori, puoi eseguire un %s tramite email.Abilita l\'autenticazione a due fattoriDisabilita l\'autenticazione a due fattoriAutenticazione a due fattori attivata
+ Necessiti della tua password e di un codice dalla tua app d\'autenticazione, per disabilitare l\'autenticazione a due fattori.Apri l\'app di autenticazione a due fattori sul tuo dispositivo per visualizzare il tuo codice di autenticazione e verificare la tua identità.Sei pregato di abilitare l\'autenticazione a due fattori per proteggere il tuo profilo, su https://lichess.org/account/twofactor.
Hai ricevuto questo messaggio perché il tuo profilo ha responsabilità speciali, come capo squadra, istruttore, insegnante o streamer
diff --git a/translation/dest/timeago/af-ZA.xml b/translation/dest/timeago/af-ZA.xml
index 56c8f9bfa997b..b1a49cf491fa5 100644
--- a/translation/dest/timeago/af-ZA.xml
+++ b/translation/dest/timeago/af-ZA.xml
@@ -54,4 +54,13 @@
%s jaar gelede%s jare gelede
+
+ nog %s minuut oor
+ nog %s minute oor
+
+
+ nog %s uur oor
+ nog %s ure oor
+
+ voltooi
diff --git a/translation/dest/timeago/ar-SA.xml b/translation/dest/timeago/ar-SA.xml
index c81ca011c5082..2be15692ceb09 100644
--- a/translation/dest/timeago/ar-SA.xml
+++ b/translation/dest/timeago/ar-SA.xml
@@ -106,4 +106,21 @@
منذ %s سنةمنذ %s سنة
+
+ %sدقيقة متبقية
+ %sدقيقة متبقية
+ %sدقيقتان متبقيتان
+ %sدقائق متبقية
+ %sدقيقة متبقية
+ %sدقائق متبقية
+
+
+ %sساعة متبقية
+ %sساعة واحدة متبقية
+ %sساعتان متبقيتان
+ %s ساعات متبقية
+ %sساعة متبقية
+ %sساعة متبقية
+
+ مكتمل
diff --git a/translation/dest/timeago/ckb-IR.xml b/translation/dest/timeago/ckb-IR.xml
index e574ebcde86d4..5c0e2aa00eead 100644
--- a/translation/dest/timeago/ckb-IR.xml
+++ b/translation/dest/timeago/ckb-IR.xml
@@ -54,4 +54,13 @@
%s ساڵ لەمەوبەر%s ساڵ لەمەوبەر
+
+ %s خولەک ماوە
+ %s خولەک ماوە
+
+
+ %s کاژێر ماوە
+ %s کاژێر ماوە
+
+ تەواو بوو
diff --git a/translation/dest/timeago/da-DK.xml b/translation/dest/timeago/da-DK.xml
index a354420e80ec0..5ebd304561369 100644
--- a/translation/dest/timeago/da-DK.xml
+++ b/translation/dest/timeago/da-DK.xml
@@ -54,4 +54,13 @@
%s år siden%s år siden
+
+ %s minut tilbage
+ %s minutter tilbage
+
+
+ %s time tilbage
+ %s timer tilbage
+
+ afsluttet
diff --git a/translation/dest/timeago/de-DE.xml b/translation/dest/timeago/de-DE.xml
index 9842c8a38b7c9..1ef3f6d9df39e 100644
--- a/translation/dest/timeago/de-DE.xml
+++ b/translation/dest/timeago/de-DE.xml
@@ -54,4 +54,13 @@
vor %s Jahrvor %s Jahren
+
+ %s Minute verbleibend
+ %s Minuten verbleibend
+
+
+ %s Stunde übrig
+ %s Stunden übrig
+
+ vervollständigt
diff --git a/translation/dest/timeago/en-US.xml b/translation/dest/timeago/en-US.xml
index 5a403c12e17e6..72492b7f85b06 100644
--- a/translation/dest/timeago/en-US.xml
+++ b/translation/dest/timeago/en-US.xml
@@ -54,4 +54,13 @@
%s year ago%s years ago
+
+ %s minute remaining
+ %s minutes remaining
+
+
+ %s hour remaining
+ %s hours remaining
+
+ completed
diff --git a/translation/dest/timeago/es-ES.xml b/translation/dest/timeago/es-ES.xml
index ff97bc3df6fe1..cab0d4cefd804 100644
--- a/translation/dest/timeago/es-ES.xml
+++ b/translation/dest/timeago/es-ES.xml
@@ -54,4 +54,13 @@
hace %s añohace %s años
+
+ %s minutos restantes
+ %s minutos restantes
+
+
+ %s horas restantes
+ %s horas restantes
+
+ completado
diff --git a/translation/dest/timeago/fa-IR.xml b/translation/dest/timeago/fa-IR.xml
index 593c42930d888..deddf2eda237f 100644
--- a/translation/dest/timeago/fa-IR.xml
+++ b/translation/dest/timeago/fa-IR.xml
@@ -54,4 +54,5 @@
%s سال پیش%s سال پیش
+ کامل شده
diff --git a/translation/dest/timeago/fr-FR.xml b/translation/dest/timeago/fr-FR.xml
index af1a45d7ca3e0..5762aa2550458 100644
--- a/translation/dest/timeago/fr-FR.xml
+++ b/translation/dest/timeago/fr-FR.xml
@@ -54,4 +54,5 @@
il y a %s anil y a %s ans
+ terminé
diff --git a/translation/dest/timeago/gl-ES.xml b/translation/dest/timeago/gl-ES.xml
index 2bc48b5ce9ac1..17ea2696aa460 100644
--- a/translation/dest/timeago/gl-ES.xml
+++ b/translation/dest/timeago/gl-ES.xml
@@ -54,4 +54,13 @@
Hai %s anoHai %s anos
+
+ %s minuto restante
+ %s minutos restantes
+
+
+ %s hora restante
+ %s horas restantes
+
+ completado
diff --git a/translation/dest/timeago/gsw-CH.xml b/translation/dest/timeago/gsw-CH.xml
index e89ae11f0687f..8d0d87d4f8e23 100644
--- a/translation/dest/timeago/gsw-CH.xml
+++ b/translation/dest/timeago/gsw-CH.xml
@@ -54,4 +54,13 @@
vor %s Jahrvor %s Jahr
+
+ %s Minute blibt
+ %s Minute blibed
+
+
+ %s Schtund blibt
+ %s Schtunde blibed
+
+ beändet
diff --git a/translation/dest/timeago/he-IL.xml b/translation/dest/timeago/he-IL.xml
index eb67e91a1f0a2..014cab7c6e55c 100644
--- a/translation/dest/timeago/he-IL.xml
+++ b/translation/dest/timeago/he-IL.xml
@@ -80,4 +80,17 @@
לפני %s שניםלפני %s שנים
+
+ דקה %s נותרה
+ %s דקות נותרו
+ %s דקות נותרו
+ %s דקות נותרו
+
+
+ שעה %s נותרה
+ %s שעות נותרו
+ %s שעות נותרו
+ %s שעות נותרו
+
+ הושלם
diff --git a/translation/dest/timeago/it-IT.xml b/translation/dest/timeago/it-IT.xml
index cf39688c8d4b4..e44adecb7faba 100644
--- a/translation/dest/timeago/it-IT.xml
+++ b/translation/dest/timeago/it-IT.xml
@@ -54,4 +54,5 @@
%s anno fa%s anni fa
+ completato
diff --git a/translation/dest/timeago/ja-JP.xml b/translation/dest/timeago/ja-JP.xml
index f9cc0e3e65821..c1c945b31d6fc 100644
--- a/translation/dest/timeago/ja-JP.xml
+++ b/translation/dest/timeago/ja-JP.xml
@@ -41,4 +41,11 @@
%s 年前
+
+ 残り %s 分
+
+
+ 残り %s 時間
+
+ 完了
diff --git a/translation/dest/timeago/lb-LU.xml b/translation/dest/timeago/lb-LU.xml
index 21fdf38292b89..dc0ce06e7b638 100644
--- a/translation/dest/timeago/lb-LU.xml
+++ b/translation/dest/timeago/lb-LU.xml
@@ -54,4 +54,12 @@
virun %s Joervirun %s Joer
+
+ %s Minutt iwwereg
+ %s Minutten iwwereg
+
+
+ %s Stonn iwwereg
+ %s Stonnen iwwereg
+
diff --git a/translation/dest/timeago/nb-NO.xml b/translation/dest/timeago/nb-NO.xml
index 9bb693b1a4048..72c9457fbe223 100644
--- a/translation/dest/timeago/nb-NO.xml
+++ b/translation/dest/timeago/nb-NO.xml
@@ -54,4 +54,13 @@
for %s år sidenfor %s år siden
+
+ %s minutt igjen
+ %s minutter igjen
+
+
+ %s time igjen
+ %s timer igjen
+
+ fullført
diff --git a/translation/dest/timeago/nl-NL.xml b/translation/dest/timeago/nl-NL.xml
index 0858b4af9a9d6..a1e73aaa90689 100644
--- a/translation/dest/timeago/nl-NL.xml
+++ b/translation/dest/timeago/nl-NL.xml
@@ -54,4 +54,13 @@
%s jaar geleden%s jaar geleden
+
+ %s minuut resterend
+ %s minuten resterend
+
+
+ %s uur resterend
+ %s uur resterend
+
+ voltooid
diff --git a/translation/dest/timeago/nn-NO.xml b/translation/dest/timeago/nn-NO.xml
index 7c11e96953301..d5ae6452d2902 100644
--- a/translation/dest/timeago/nn-NO.xml
+++ b/translation/dest/timeago/nn-NO.xml
@@ -54,4 +54,13 @@
%s år sidan%s år sidan
+
+ %s minutt igjen
+ %s minutt igjen
+
+
+ %s time igjen
+ %s timar igjen
+
+ fullført
diff --git a/translation/dest/timeago/pl-PL.xml b/translation/dest/timeago/pl-PL.xml
index 928954ec993d6..e897b657f6c22 100644
--- a/translation/dest/timeago/pl-PL.xml
+++ b/translation/dest/timeago/pl-PL.xml
@@ -80,4 +80,17 @@
%s lat temu%s lat temu
+
+ Pozostała %s minuta
+ Pozostały %s minuty
+ Pozostało %s minut
+ Pozostało %s minut
+
+
+ Pozostała %s godzina
+ Pozostały %s godziny
+ Pozostało %s godzin
+ Pozostało %s godzin
+
+ ukończone
diff --git a/translation/dest/timeago/pt-BR.xml b/translation/dest/timeago/pt-BR.xml
index 50b07c187023f..a4a5c2d8b853c 100644
--- a/translation/dest/timeago/pt-BR.xml
+++ b/translation/dest/timeago/pt-BR.xml
@@ -54,4 +54,13 @@
%s ano atrás%s anos atrás
+
+ %s minuto restante
+ %s minutos restantes
+
+
+ %s hora restante
+ %s horas restantes
+
+ concluído
diff --git a/translation/dest/timeago/pt-PT.xml b/translation/dest/timeago/pt-PT.xml
index 7d89d72ff82c0..ad067adc5f483 100644
--- a/translation/dest/timeago/pt-PT.xml
+++ b/translation/dest/timeago/pt-PT.xml
@@ -54,4 +54,13 @@
há %s anohá %s anos
+
+ %s minuto restante
+ %s minutos restantes
+
+
+ %s hora restante
+ %s horas restantes
+
+ concluído
diff --git a/translation/dest/timeago/ru-RU.xml b/translation/dest/timeago/ru-RU.xml
index f822d3313f811..f99d144827db5 100644
--- a/translation/dest/timeago/ru-RU.xml
+++ b/translation/dest/timeago/ru-RU.xml
@@ -80,4 +80,17 @@
%s лет назад%s лет назад
+
+ осталась %s минута
+ осталось %s минуты
+ осталось %s минут
+ осталось %s минут
+
+
+ остался %s час
+ осталось %s часа
+ осталось %s часов
+ осталось %s часов
+
+ завершено
diff --git a/translation/dest/timeago/sl-SI.xml b/translation/dest/timeago/sl-SI.xml
index 7ee0e28b1fbca..8e6a140cb6976 100644
--- a/translation/dest/timeago/sl-SI.xml
+++ b/translation/dest/timeago/sl-SI.xml
@@ -80,4 +80,17 @@
Pred %s letiPred %s leti
+
+ še %s minuta
+ še %s minuti
+ še %s minute
+ še %s minut
+
+
+ še %s ura
+ še %s uri
+ še %s ure
+ še %s ur
+
+ končano
diff --git a/translation/dest/timeago/so-SO.xml b/translation/dest/timeago/so-SO.xml
index 51c2daf6f7a52..97a8ef749555d 100644
--- a/translation/dest/timeago/so-SO.xml
+++ b/translation/dest/timeago/so-SO.xml
@@ -10,9 +10,58 @@
%s daqiiqo gudaheed%s daqiiqadood gudohood
+
+ %s saacad gudeheed
+ %s saacadood gudohood
+ %s maalin gudeheed%s maalmood gudohood
+
+ %s usbuuc gudihii
+ %s usbuuc gudohood
+
+
+ %s bil gudeheed
+ %s bilood gudohood
+
+
+ %s sanad gudihii
+ %s sanadood gudohood
+ immika
+
+ %s daqiiqad ka hor
+ %s daqiiqadood ka hor
+
+
+ %s saacad ka hor
+ %s saacadood ka hor
+
+
+ %s maalin ka hor
+ %s maalmood ka hor
+
+
+ %s usbuuc ka hor
+ %s usbuuc ka hor
+
+
+ %s bil ka hor
+ %s bilood ka hor
+
+
+ %s sanad ka hor
+ %s sanadood ka hor
+
+
+ %s daqiiqaa hadhay
+ %s daqiiqaa hadhay
+
+
+ %s saac baa hadhay
+ %s saac baa hadhay
+
+ dhamaad
diff --git a/translation/dest/timeago/sv-SE.xml b/translation/dest/timeago/sv-SE.xml
index ef085fe0ebdc0..1bb48639ee881 100644
--- a/translation/dest/timeago/sv-SE.xml
+++ b/translation/dest/timeago/sv-SE.xml
@@ -54,4 +54,13 @@
%s år sedan%s år sedan
+
+ %s minut återstår
+ %s minuter återstår
+
+
+ %s timme återstår
+ %s timmar återstår
+
+ slutfört
diff --git a/translation/dest/timeago/th-TH.xml b/translation/dest/timeago/th-TH.xml
index 643d183045691..9569a596161e5 100644
--- a/translation/dest/timeago/th-TH.xml
+++ b/translation/dest/timeago/th-TH.xml
@@ -41,4 +41,11 @@
%s ปีที่แล้ว
+
+ เหลือ %s นาที
+
+
+ เหลือ %s ชั่วโมง
+
+ เสร็จสมบูรณ์
diff --git a/translation/dest/timeago/tp-TP.xml b/translation/dest/timeago/tp-TP.xml
index 985d34dc001c0..3de0db3150bd6 100644
--- a/translation/dest/timeago/tp-TP.xml
+++ b/translation/dest/timeago/tp-TP.xml
@@ -54,4 +54,13 @@
lon tenpo sike pini %slon tenpo sike pini %s
+
+ tenpo lili %s li lon
+ tenpo lili %s li lon
+
+
+ tenpo suli %s li lon
+ tenpo suli %s li lon
+
+ pini
diff --git a/translation/dest/timeago/uk-UA.xml b/translation/dest/timeago/uk-UA.xml
index 926a1f8af5310..fed9026917c98 100644
--- a/translation/dest/timeago/uk-UA.xml
+++ b/translation/dest/timeago/uk-UA.xml
@@ -80,4 +80,5 @@
%s років тому%s років тому
+ завершено
diff --git a/translation/dest/timeago/vi-VN.xml b/translation/dest/timeago/vi-VN.xml
index 38da7f6515cbb..4ad0c2b295c94 100644
--- a/translation/dest/timeago/vi-VN.xml
+++ b/translation/dest/timeago/vi-VN.xml
@@ -41,4 +41,11 @@
%s năm trước
+
+ Còn lại %s phút
+
+
+ Còn lại %s giờ
+
+ đã hoàn thành
diff --git a/translation/dest/timeago/zh-CN.xml b/translation/dest/timeago/zh-CN.xml
index 9325c138aec68..653d8781cb3b7 100644
--- a/translation/dest/timeago/zh-CN.xml
+++ b/translation/dest/timeago/zh-CN.xml
@@ -41,4 +41,11 @@
%s年前
+
+ 还剩 %s 分钟
+
+
+ 还剩 %s 小时
+
+ 已完成
diff --git a/translation/dest/tourname/vi-VN.xml b/translation/dest/tourname/vi-VN.xml
index 68ea289f90a9c..913db03f6295e 100644
--- a/translation/dest/tourname/vi-VN.xml
+++ b/translation/dest/tourname/vi-VN.xml
@@ -40,7 +40,7 @@
Khiên Cờ chậmGiải đấu Khiên %sKhiên %s
- Giải đấu đa Đội %s
+ Giải đa Đội %sGiải đấu %s Chuyên Nghiệp%s Chuyên nghiệpĐấu trường %s
diff --git a/translation/dest/ublog/ar-SA.xml b/translation/dest/ublog/ar-SA.xml
index 135c79dea1181..3367369fc2228 100644
--- a/translation/dest/ublog/ar-SA.xml
+++ b/translation/dest/ublog/ar-SA.xml
@@ -2,15 +2,15 @@
مدونة %sمنشور جديد
- تعديل منشور مدونتك
- حفظ المسودة
+ عدل منشور مدونتك
+ حفظ المُسَوَّدَةعنوان المنشورمقدمة المنشورنص المنشورالمسوداتالمنشورةتمكين التعليقات
- سيتم إنشاء موضوع المنتدى للأشخاص للتعليق على مشاركتك
+ سيوضع منتدى للأشخاص الذين يريدون التعليق على مشاركتكالمنشور على مدونتكإذا تم تحديد الخيار سيتم نشر المقال على مدونتك، اذا لم يتم تحديد الخيار سيتم حفظها بشكل خاص في مسوداتك
@@ -37,7 +37,7 @@
لا يوجد مسودات لعرضها.أخر منشورات المدونة
- عرض مقالة
+ عرض كل %s المنشوراتعرض مقالةعرض مقالتينعرض %s مقالات
@@ -55,4 +55,5 @@
يرجى نشر المحتوى الحصري و المفيد فقط. لا تنسخ محتوى احد اخر.اي شيء غير لائق من الممكن أن يتسبب باغلاق حسابك.نصائحنا البسيطة لكتابة مقالة رائعة
+ ناقش منشور المدونة هذا في المنتدى
diff --git a/translation/dest/ublog/it-IT.xml b/translation/dest/ublog/it-IT.xml
index dddab620d8b68..3d5ec599ce000 100644
--- a/translation/dest/ublog/it-IT.xml
+++ b/translation/dest/ublog/it-IT.xml
@@ -43,4 +43,5 @@
Per favore scrivi solo messaggi educati e rispettosi. Non copiare i contenuti di qualcun altro.Qualsiasi contenuto inappropriato potrebbe portare alla chiusura del tuo account.I nostri suggerimenti per scrivere ottimi messaggi sul blog
+ Discuti di questo post del blog nel forum
diff --git a/translation/dest/ublog/tp-TP.xml b/translation/dest/ublog/tp-TP.xml
index 6e1327bcf794d..7ab2594e296a7 100644
--- a/translation/dest/ublog/tp-TP.xml
+++ b/translation/dest/ublog/tp-TP.xml
@@ -43,4 +43,5 @@
o sitelen ala e ijo jaki e ijo ike. o lanpan ala e lipu pi jan antesina sitelen anu pana e ijo jaki la sina ken kama ken ala kepeken ilo Lichesssona pona mi tawa pali lipu
+ o toki lon lipu ni lon ma toki
diff --git a/translation/dest/ublog/vi-VN.xml b/translation/dest/ublog/vi-VN.xml
index 6073d9406bbbc..cc7e6565154cd 100644
--- a/translation/dest/ublog/vi-VN.xml
+++ b/translation/dest/ublog/vi-VN.xml
@@ -40,5 +40,5 @@
Vui lòng chỉ đăng những nội dung an toàn và có tính tôn trọng. Không được phép sao chép nội dung của bất kì ai khác.Chỉ một hành động không phù hợp, tài khoản của bạn có thể bị đóng.Vài mẹo đơn giản để giúp viết ra một bài blog tuyệt vời
- Thảo luận về bài blog này trong diễn đàn
+ Thảo luận về bài blog này trong diễn đàn
diff --git a/translation/dest/voiceCommands/ar-SA.xml b/translation/dest/voiceCommands/ar-SA.xml
index 3ea04e700dfa8..d298c2b9ed975 100644
--- a/translation/dest/voiceCommands/ar-SA.xml
+++ b/translation/dest/voiceCommands/ar-SA.xml
@@ -1,2 +1,22 @@
-
+
+ الأوامر الصوتية
+ شاهد الفيديو التوضيحي
+ استخدم زر %1$s لتبديل الصوت، الزر %2$s لفتح حوار المساعدة، وقائمة %3$s لتغيير إعدادات الكلام.
+ تشير الأسهم إلى عدة نقلات حين لا نكون واثقين، حدد لون أو رَقْم السهم الذي يشير إلى النقلة لاختيارها.
+ إذا ظهرت علامة الرادار، فستُلعب نقلتك عند اكتمال الدائرة. خلال ذلك، يمكنك قول %1$s للعب النقلة على الفور، أو قول %2$s لإلغاء الحركة واختيار أخرى بلون/رقم سهم مختلف. يمكن تعديل هذا المؤقت أو إيقاف تشغيله في الإعدادات.
+ مكن %s إذا كنت في محيط صاخب. اضغط (shift) عندما يكون هذا الإعداد قيد التشغيل لتسجيل الأوامر.
+ استخدم الأبجدية الصوتية لتسهيل التعرف على أعمدة رقعة الشطرنج.
+ %s يشرح إعدادات الأوامر الصوتية بالتفصيل.
+ هذا المنشور
+ انقل إلى e4 أو أختر القطعة في مربع e4
+ أختر أو ألتقط الفيل
+ خذ الرخ بواسطة الوزير
+ بيّت (في أي من الجناحين)
+ الأبجدية الصوتية هي الأفضل
+ ألغ المؤقت أو أرفض الطلب
+ لعب النقلة المفضلة أو التأكد من شيئ ما
+ وضع النوم (إذا تم تمكين كلمة الاستيقاظ)
+ إيقاف التعرف على الصوت
+ إظهار حل اللغز
+
diff --git a/translation/dest/voiceCommands/de-DE.xml b/translation/dest/voiceCommands/de-DE.xml
index 56ca6fab26293..6e2ca9954ec5d 100644
--- a/translation/dest/voiceCommands/de-DE.xml
+++ b/translation/dest/voiceCommands/de-DE.xml
@@ -4,9 +4,9 @@
Video-Tutorial ansehenBenutze den %1$s-Knopf um die Spracherkennung einzuschalten, den %2$s-Knopf, um diese Hilfe aufzurufen und den %3$s-Knopf, um die Spracheinstellungen zu ändern.Wir zeigen Pfeile für mehrere Züge an, falls wir nicht sicher sind. Nenne die Farbe oder die Zahl eines Zug-Pfeils, um ihn auszuwählen.
- Wenn ein Pfeil einen sich schließenden Ladekreis anzeigt, wird dieser Zug nach Vollendung des Kreises gespielt. Während dieser Zeit kannst du nur %1$s sagen, um den Zug sofort zu spielen, %2$s, um den Zug abzubrechen, oder die Farbe/Nummer eines anderen Pfeils. Dieser Timer kann in den Einstellungen angepasst oder deaktiviert werden.
+ Wenn ein Pfeil ein kreisendes Radar anzeigt, wird der Zug nach Vollendung des Kreises gespielt. Während dieser Zeit kannst du nur %1$s sagen, um den Zug sofort zu spielen, %2$s, um den Zug abzubrechen, oder nenne die Farbe/Nummer eines anderen Pfeils. Die Zeitvorgabe kann in den Einstellungen geändert oder ausgeschaltet werden.Aktiviere %s in lauten Umgebungen. Halte Shift während des Sprechens gedrückt, wenn diese Option aktiviert ist.
- Verwende das phonetische Alphabet, um die Spracherkennung von Linien auf dem Schachbrett zu verbessern. (a-h-Linie)
+ Verwende das phonetische Alphabet, um die Spracherkennung von Linien auf dem Schachbrett zu verbessern (a-h-Linie).%s erklärt die Zugeinstellungen per Spracheingabe im Detail.Dieser BlogbeitragZiehe nach e4 oder wähle die Figur auf e4 aus
@@ -14,9 +14,9 @@
Schlage den Turm mit der DameRochiere (egal welche Seite)Das phonetische Alphabet ist am besten
- Timer abbrechen oder Anfrage ablehnen
+ Zeitvorgabe ausschalten oder Anfrage ablehnenBevorzugten Zug abspielen oder etwas bestätigenSchlafen (falls Wort zum Aufwecken aktiviert)Spracherkennung ausschalten
- Aufgaben-Lösung anzeigen
+ Lösung der Aufgabe anzeigen
diff --git a/translation/dest/voiceCommands/gsw-CH.xml b/translation/dest/voiceCommands/gsw-CH.xml
index bfd6b67296729..5f26714f8b13f 100644
--- a/translation/dest/voiceCommands/gsw-CH.xml
+++ b/translation/dest/voiceCommands/gsw-CH.xml
@@ -4,8 +4,6 @@
Lueg s\'Video-TutorialBenutz d\'Schaltflächi %1$s, zum d\'Spracherkännig aktivieren, d\'Schaltflächi %2$s, zum de Hilfedialog ufmache, und s\'Menü %3$s, zum d\'Sprachinschtellige ändere.Mir zeiged Pfil für mehreri Züg a, wänn mir nöd sicher sind. Sprich eifach d\'Farb oder d\'Nummere vu dem Zugpfil, wo du wotsch wähle.
- Läsed die änglisch Version, uf Schwizerdütsch git das Chrämpf:
-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 color/number of a different arrow. This timer can be adjusted or turned off in settings.Aktiviert %s in luter Umgebig. Bim Spräche vu Befehl d\'Umschalttaschte truckt halte, wänn die Funktion aktiviert isch.Benutz s\'Ponetischi-Alphabet, zum d\'Verständlichkeit verbessere. %s erchlärt d\'Ischtellige vu de Sprachschtürig im Detail.
diff --git a/translation/dest/voiceCommands/pt-BR.xml b/translation/dest/voiceCommands/pt-BR.xml
index 54257d131abf3..0ebfe62db26ad 100644
--- a/translation/dest/voiceCommands/pt-BR.xml
+++ b/translation/dest/voiceCommands/pt-BR.xml
@@ -9,4 +9,7 @@
Capturar a torre com a rainhaRoque (qualquer lado)O alfabeto fonético é melhor
+ Cancelar relógio ou negar um pedido
+ Fazer lance preferido ou confirmar algo
+ Mostrar solução do quebra-cabeça
diff --git a/translation/dest/voiceCommands/pt-PT.xml b/translation/dest/voiceCommands/pt-PT.xml
index 4c52a241a793d..4cb780f00465f 100644
--- a/translation/dest/voiceCommands/pt-PT.xml
+++ b/translation/dest/voiceCommands/pt-PT.xml
@@ -3,6 +3,10 @@
Comandos de vozVer o vídeo tutorialUsa o botão %1$s para alternar o reconhecimento de voz, o botão %2$s para abrir esta caixa de diálogo de ajuda e o menu %3$s para alterar as configurações de voz.
+ Mostramos setas para vários movimentos quando não temos certeza. Fala a cor ou o número de uma seta de movimento para selecioná-la.
+ Ative %s em ambientes barulhentos. Segura shift enquanto falas comandos quando estiver ativado.
+ Usa o alfabeto fonético para melhorar o reconhecimento dos arquivos do tabuleiro.
+ %s explica detalhadamente as configurações do movimento de voz.Este post no blogMover para e4 ou selecionar a peça de e4Seleciona ou captura um bispo
diff --git a/translation/dest/voiceCommands/uk-UA.xml b/translation/dest/voiceCommands/uk-UA.xml
index 3ea04e700dfa8..c0f2a7d6132c3 100644
--- a/translation/dest/voiceCommands/uk-UA.xml
+++ b/translation/dest/voiceCommands/uk-UA.xml
@@ -1,2 +1,6 @@
-
+
+ Голосові команди
+ Фонетичний алфавіт найкращий
+ Вимкнути розпізнавання голосу
+
diff --git a/translation/source/arena.xml b/translation/source/arena.xml
index 0973965580f65..de2d492a779bd 100644
--- a/translation/source/arena.xml
+++ b/translation/source/arena.xml
@@ -8,9 +8,9 @@
Some tournaments are rated and will affect your rating.How are scores calculated?A win has a base score of 2 points, a draw 1 point, and a loss is worth no points.
-If you win two games consecutively you will start a double point streak, represented by a flame icon.
+If you win two games consecutively you will start a double-point streak, represented by a flame icon.
The following games will continue to be worth double points until you fail to win a game.
-That is, a win will be worth 4 points, a draw 2 points, and a loss will still not award any points.
+That is, a win will be worth 4 points, a draw 2 points and a loss will still not award any points.
For example, two wins followed by a draw will be worth 6 points: 2 + 2 + (2 x 1)Arena Berserk
@@ -22,15 +22,15 @@ Berserk is not available for games with zero initial time (0+1, 0+2).
Berserk only grants an extra point if you play at least 7 moves in the game.
How is the winner decided?
- The player(s) with the most points at the conclusion of the tournament's set time limit will be announced winner(s).
+ The player(s) with the most points after the tournament's set time limit will be announced the winner(s).
When two or more players have the same number of points, the tournament performance is the tie break.How does the pairing work?At the beginning of the tournament, players are paired based on their rating.
-As soon as you finish a game, return to the tournament lobby: you will then be paired with a player close to your ranking. This ensures minimum wait time, however you may not face all other players in the tournament.
+As soon as you finish a game, return to the tournament lobby: you will then be paired with a player close to your ranking. This ensures minimum wait time, however, you may not face all other players in the tournament.
Play fast and return to the lobby to play more games and win more points.How does it end?
- 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.
+ 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.Other important rulesThere is a countdown for your first move. Failing to make a move within this time will forfeit the game to your opponent.
@@ -39,7 +39,7 @@ Play fast and return to the lobby to play more games and win more points.
This is a private tournamentShare this URL to let people join: %s
- 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.
+ 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.The minimum game length for drawn games to award points differs by variant. The table below lists the threshold for each variant.VariantMinimum game length
diff --git a/translation/source/challenge.xml b/translation/source/challenge.xml
index c5cb7374edbd2..c30b03ffa784d 100644
--- a/translation/source/challenge.xml
+++ b/translation/source/challenge.xml
@@ -2,7 +2,7 @@
Challenges: %1$sChallenge to a game
- Challenge declined
+ Challenge declined.Challenge accepted!Challenge cancelled.Please register to send challenges to this user.
@@ -22,5 +22,5 @@
I'm not willing to play this variant right now.I'm not accepting challenges from bots.I'm only accepting challenges from bots.
- Or invite a Lichess User:
+ Or invite a Lichess user:
diff --git a/translation/source/class.xml b/translation/source/class.xml
index 5e15c2098057e..c037cb3407514 100644
--- a/translation/source/class.xml
+++ b/translation/source/class.xml
@@ -25,12 +25,12 @@
Teachers of the classAdd Lichess usernames to invite them as teachers. One per line.Reset password
- Make sure to copy or write down the password now. You won’t be able to see it again!
+ Make sure to copy or write down the password now. You won’t ever be able to see it again!Password: %sGenerate a new password for the studentInvited to %1$s by %2$sReal name
- Private. Will never be shown outside the class. Helps remember who the student is.
+ Private. Will never be shown outside the class. Helps you remember who the student is.Add studentLichess profile %1$s created for %2$s.Student: %1$s
@@ -43,7 +43,7 @@ Password: %3$sNever send unsolicited invites to arbitrary players.Create a new Lichess accountIf the student doesn't have a Lichess account yet, you can create one for them here.
- No email address is required. A password will be generated, and you will have to transmit it to the student, so they can log in.
+ 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.Important: a student must not have multiple accounts.If they already have one, use the invite form instead.Only create accounts for real students. Do not use this to make multiple accounts for yourself. You would get banned.
@@ -64,13 +64,13 @@ Here is the link to access the class.Upgrade from managed to autonomousGraduateGraduate the account so the student can manage it autonomously.
- A graduated account cannot be made managed again. The student will be able to toggle kid mode and reset password themselves.
+ A graduated account cannot be managed again. The student will be able to toggle kid mode and reset password themselves.The student will remain in the class after their account is graduated.Real, unique email address of the student. We will send a confirmation email to it, with a link to graduate the account.Close accountClose the student account permanently.The student will never be able to use this account again. Closing is final. Make sure the student understands and agrees.
- You may want to give the student control over the account instead, so that they can continue using it.
+ You may want to give the student control over the account instead so that they can continue using it.TeachersTeacher
diff --git a/translation/source/coordinates.xml b/translation/source/coordinates.xml
index 7a65e1f74d73b..2db28790f3197 100644
--- a/translation/source/coordinates.xml
+++ b/translation/source/coordinates.xml
@@ -4,7 +4,7 @@
Coordinate trainingAverage score as white: %sAverage score as black: %s
- Knowing the chessboard coordinates is a very important chess skill:
+ Knowing the chessboard coordinates is a very important skill for several reasons:Most chess courses and exercises use the algebraic notation extensively.It makes it easier to talk to your chess friends, since you both understand the 'language of chess'.You can analyse a game more effectively if you can quickly recognise coordinates.
diff --git a/translation/source/preferences.xml b/translation/source/preferences.xml
index 93fba69600ff0..2cdbe487e10d6 100644
--- a/translation/source/preferences.xml
+++ b/translation/source/preferences.xml
@@ -16,7 +16,7 @@
Zen modeShow player ratingsShow player flairs
- This allows hiding all ratings from the website, to help focus on the chess. Games can still be rated, this is only about what you get to see.
+ 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.Show board resize handleOnly on initial positionIn-game only
diff --git a/translation/source/site.xml b/translation/source/site.xml
index 85e8903dfe0fb..5e249b9525de2 100644
--- a/translation/source/site.xml
+++ b/translation/source/site.xml
@@ -273,7 +273,6 @@
Playing right nowPlaying nowFinished
- finishes %sAbort gameGame abortedStandard
@@ -404,10 +403,9 @@
Continue from hereStudyImport game
- Paste a game PGN to get a browsable replay,
-computer analysis, game chat and public shareable URL.
+ Paste a game PGN to get a browsable replay, computer analysis, game chat and public shareable URL.Variations will be erased. To keep them, import the PGN via a study.
- This PGN can be accessed by the public. To import a game privately, use a study.
+ This PGN can be accessed by the public. To import a game privately, use a study.%s imported game%s imported games
@@ -509,7 +507,7 @@ computer analysis, game chat and public shareable URL.
Edit profileFirst nameSurname
- Set your flair:
+ Set your flairFlairThere is a setting to hide all user flairs across the entire site.Biography
@@ -864,9 +862,10 @@ computer analysis, game chat and public shareable URL.
and save %s premove lineand save %s premove lines
+ You have received a private message from Lichess.
+ Click here to read itSorry :(We had to time you out for a while.
- The timeout expires %s.Why?We aim to provide a pleasant chess experience for everyone.To that effect, we must ensure that all players follow good practice.
@@ -886,6 +885,7 @@ computer analysis, game chat and public shareable URL.
I agree that I will follow all Lichess policies.Search or start new conversationEdit
+ BulletBlitzRapidClassical
diff --git a/translation/source/timeago.xml b/translation/source/timeago.xml
index 688c6df7d41ab..09b80e223bb9f 100644
--- a/translation/source/timeago.xml
+++ b/translation/source/timeago.xml
@@ -54,4 +54,13 @@
%s year ago%s years ago
+
+ %s minute remaining
+ %s minutes remaining
+
+
+ %s hour remaining
+ %s hours remaining
+
+ completed
diff --git a/ui/@types/lichess/index.d.ts b/ui/@types/lichess/index.d.ts
index 795acc3611e3f..93735fe8a0648 100644
--- a/ui/@types/lichess/index.d.ts
+++ b/ui/@types/lichess/index.d.ts
@@ -199,7 +199,7 @@ interface Pubsub {
}
interface LichessStorageHelper {
- make(k: string): LichessStorage;
+ make(k: string, ttl?: number): LichessStorage;
boolean(k: string): LichessBooleanStorage;
get(k: string): string | null;
set(k: string, v: string): void;
@@ -305,6 +305,7 @@ declare namespace Editor {
orientation?: Color;
onChange?: (fen: string) => void;
inlineCastling?: boolean;
+ coordinates?: boolean;
}
export interface OpeningPosition {
diff --git a/ui/analyse/package.json b/ui/analyse/package.json
index 38dfdc6693ee4..ae446c8c63890 100644
--- a/ui/analyse/package.json
+++ b/ui/analyse/package.json
@@ -23,7 +23,7 @@
"chart": "workspace:*",
"chat": "workspace:*",
"chess": "workspace:*",
- "chessops": "^0.12.7",
+ "chessops": "^0.13.0",
"common": "workspace:*",
"debounce-promise": "^3.1.2",
"game": "workspace:*",
diff --git a/ui/analyse/src/crazy/crazyView.ts b/ui/analyse/src/crazy/crazyView.ts
index 21766872481de..b407a552014e5 100644
--- a/ui/analyse/src/crazy/crazyView.ts
+++ b/ui/analyse/src/crazy/crazyView.ts
@@ -38,11 +38,7 @@ export default function (ctrl: AnalyseCtrl, color: Color, position: Position) {
h(
'div.pocket-c2',
h('piece.' + role + '.' + color, {
- attrs: {
- 'data-role': role,
- 'data-color': color,
- 'data-nb': nb,
- },
+ attrs: { 'data-role': role, 'data-color': color, 'data-nb': nb },
}),
),
);
diff --git a/ui/analyse/src/ctrl.ts b/ui/analyse/src/ctrl.ts
index e7a7d93e848f8..d88a9e249dc39 100644
--- a/ui/analyse/src/ctrl.ts
+++ b/ui/analyse/src/ctrl.ts
@@ -768,7 +768,7 @@ export default class AnalyseCtrl {
!chap?.practice &&
chap?.conceal === undefined &&
!this.study?.gamebookPlay &&
- !this.retro &&
+ !this.retro?.isSolving() &&
this.variationArrowsProp() &&
this.node.children.filter(x => !x.comp || this.showComputer()).length > 1
);
diff --git a/ui/analyse/src/explorer/explorerConfig.ts b/ui/analyse/src/explorer/explorerConfig.ts
index 25936e1e0f04a..c49b88533aa9b 100644
--- a/ui/analyse/src/explorer/explorerConfig.ts
+++ b/ui/analyse/src/explorer/explorerConfig.ts
@@ -150,10 +150,7 @@ export function view(ctrl: ExplorerConfigCtrl): VNode[] {
'section.save',
h(
'button.button.button-green.text',
- {
- attrs: dataIcon(licon.Checkmark),
- hook: bind('click', ctrl.toggleOpen),
- },
+ { attrs: dataIcon(licon.Checkmark), hook: bind('click', ctrl.toggleOpen) },
ctrl.root.trans.noarg('allSet'),
),
),
@@ -353,11 +350,7 @@ const playerModal = (ctrl: ExplorerConfigCtrl) => {
},
hook: onInsert(input =>
lichess.asset
- .userComplete({
- input,
- tag: 'span',
- onSelect: v => onSelect(v.name),
- })
+ .userComplete({ input, tag: 'span', onSelect: v => onSelect(v.name) })
.then(() => input.focus()),
),
}),
@@ -371,27 +364,19 @@ const playerModal = (ctrl: ExplorerConfigCtrl) => {
...ctrl.data.playerName.previous(),
]),
].map(name =>
- h(
- 'div',
- {
- key: name,
- },
- [
- h(
- `button.button${nameToOptionalColor(name)}`,
- {
- hook: bind('click', () => onSelect(name)),
- },
- name,
- ),
- name && ctrl.data.playerName.previous().includes(name)
- ? h('button.remove', {
- attrs: dataIcon(licon.X),
- hook: bind('click', () => ctrl.removePlayer(name), ctrl.root.redraw),
- })
- : null,
- ],
- ),
+ h('div', { key: name }, [
+ h(
+ `button.button${nameToOptionalColor(name)}`,
+ { hook: bind('click', () => onSelect(name)) },
+ name,
+ ),
+ name && ctrl.data.playerName.previous().includes(name)
+ ? h('button.remove', {
+ attrs: dataIcon(licon.X),
+ hook: bind('click', () => ctrl.removePlayer(name), ctrl.root.redraw),
+ })
+ : null,
+ ]),
),
),
],
diff --git a/ui/analyse/src/explorer/explorerView.ts b/ui/analyse/src/explorer/explorerView.ts
index bce5396778ad6..8ad9d0b251860 100644
--- a/ui/analyse/src/explorer/explorerView.ts
+++ b/ui/analyse/src/explorer/explorerView.ts
@@ -1,8 +1,8 @@
-import { h, VNode } from 'snabbdom';
+import { VNode } from 'snabbdom';
import * as licon from 'common/licon';
import { numberFormat } from 'common/number';
import { perf } from 'game/perf';
-import { bind, dataIcon, MaybeVNode, MaybeVNodes } from 'common/snabbdom';
+import { bind, dataIcon, MaybeVNode, LooseVNodes, looseH as h } from 'common/snabbdom';
import { view as renderConfig } from './explorerConfig';
import { moveArrowAttributes, ucfirst } from './explorerUtil';
import AnalyseCtrl from '../ctrl';
@@ -24,9 +24,7 @@ function resultBar(move: OpeningMoveStats): VNode {
const percent = (move[key] * 100) / sum;
return h(
'span.' + key,
- {
- attrs: { style: 'width: ' + Math.round((move[key] * 1000) / sum) / 10 + '%' },
- },
+ { attrs: { style: 'width: ' + Math.round((move[key] * 1000) / sum) / 10 + '%' } },
percent > 12 ? Math.round(percent) + (percent > 20 ? '%' : '') : '',
);
};
@@ -64,21 +62,12 @@ function showMoveTable(ctrl: AnalyseCtrl, data: OpeningData): VNode | null {
moveArrowAttributes(ctrl, { fen: data.fen, onClick: (_, uci) => uci && ctrl.explorerMove(uci) }),
movesWithCurrent.map(move => {
const total = move.white + move.draws + move.black;
- return h(
- `tr${move.uci ? '' : '.sum'}`,
- {
- key: move.uci,
- attrs: {
- 'data-uci': move.uci,
- },
- },
- [
- h('td', move.san[0] === 'P' ? move.san.slice(1) : move.san),
- h('td', ((total / sumTotal) * 100).toFixed(0) + '%'),
- h('td', numberFormat(total)),
- h('td', { attrs: { title: moveTooltip(ctrl, move) } }, resultBar(move)),
- ],
- );
+ return h(`tr${move.uci ? '' : '.sum'}`, { key: move.uci, attrs: { 'data-uci': move.uci } }, [
+ h('td', move.san[0] === 'P' ? move.san.slice(1) : move.san),
+ h('td', ((total / sumTotal) * 100).toFixed(0) + '%'),
+ h('td', numberFormat(total)),
+ h('td', { attrs: { title: moveTooltip(ctrl, move) } }, resultBar(move)),
+ ]);
}),
),
]);
@@ -131,39 +120,25 @@ function showGameTable(ctrl: AnalyseCtrl, fen: Fen, title: string, games: Openin
games.map(game => {
return openedId === game.id
? gameActions(ctrl, game)
- : h(
- 'tr',
- {
- key: game.id,
- attrs: { 'data-id': game.id, 'data-uci': game.uci || '' },
- },
- [
- ctrl.explorer.opts.showRatings
- ? h(
- 'td',
- [game.white, game.black].map(p => h('span', '' + p.rating)),
- )
- : null,
+ : h('tr', { key: game.id, attrs: { 'data-id': game.id, 'data-uci': game.uci || '' } }, [
+ ctrl.explorer.opts.showRatings &&
h(
'td',
- [game.white, game.black].map(p => h('span', p.name)),
+ [game.white, game.black].map(p => h('span', '' + p.rating)),
),
- h('td', showResult(game.winner)),
- h('td', game.month || game.year),
- isMasters
- ? undefined
- : h(
- 'td',
- game.speed &&
- h('i', {
- attrs: {
- title: ucfirst(game.speed),
- ...dataIcon(perf.icons[game.speed]),
- },
- }),
- ),
- ],
- );
+ h(
+ 'td',
+ [game.white, game.black].map(p => h('span', p.name)),
+ ),
+ h('td', showResult(game.winner)),
+ h('td', game.month || game.year),
+ !isMasters &&
+ h(
+ 'td',
+ game.speed &&
+ h('i', { attrs: { title: ucfirst(game.speed), ...dataIcon(perf.icons[game.speed]) } }),
+ ),
+ ]);
}),
),
]);
@@ -183,73 +158,44 @@ function gameActions(ctrl: AnalyseCtrl, game: OpeningGame): VNode {
ctrl.explorer.gameMenu(null);
ctrl.redraw();
};
- return h(
- 'tr',
- {
- key: game.id + '-m',
- },
- [
+ return h('tr', { key: game.id + '-m' }, [
+ h('td.game_menu', { attrs: { colspan: ctrl.explorer.db() == 'masters' ? 4 : 5 } }, [
h(
- 'td.game_menu',
- {
- attrs: { colspan: ctrl.explorer.db() == 'masters' ? 4 : 5 },
- },
- [
+ 'div.game_title',
+ `${game.white.name} - ${game.black.name}, ${showResult(game.winner).text}, ${game.year}`,
+ ),
+ h('div.menu', [
+ h(
+ 'a.text',
+ { attrs: dataIcon(licon.Eye), hook: bind('click', _ => openGame(ctrl, game.id)) },
+ 'View',
+ ),
+ ctrl.study &&
h(
- 'div.game_title',
- `${game.white.name} - ${game.black.name}, ${showResult(game.winner).text}, ${game.year}`,
+ 'a.text',
+ { attrs: dataIcon(licon.BubbleSpeech), hook: bind('click', _ => send(false), ctrl.redraw) },
+ 'Cite',
),
- h('div.menu', [
- h(
- 'a.text',
- {
- attrs: dataIcon(licon.Eye),
- hook: bind('click', _ => openGame(ctrl, game.id)),
- },
- 'View',
- ),
- ...(ctrl.study
- ? [
- h(
- 'a.text',
- {
- attrs: dataIcon(licon.BubbleSpeech),
- hook: bind('click', _ => send(false), ctrl.redraw),
- },
- 'Cite',
- ),
- h(
- 'a.text',
- {
- attrs: dataIcon(licon.PlusButton),
- hook: bind('click', _ => send(true), ctrl.redraw),
- },
- 'Insert',
- ),
- ]
- : []),
- h(
- 'a.text',
- {
- attrs: dataIcon(licon.X),
- hook: bind('click', _ => ctrl.explorer.gameMenu(null), ctrl.redraw),
- },
- 'Close',
- ),
- ]),
- ],
- ),
- ],
- );
+ ctrl.study &&
+ h(
+ 'a.text',
+ { attrs: dataIcon(licon.PlusButton), hook: bind('click', _ => send(true), ctrl.redraw) },
+ 'Insert',
+ ),
+ h(
+ 'a.text',
+ { attrs: dataIcon(licon.X), hook: bind('click', _ => ctrl.explorer.gameMenu(null), ctrl.redraw) },
+ 'Close',
+ ),
+ ]),
+ ]),
+ ]);
}
const closeButton = (ctrl: AnalyseCtrl): VNode =>
h(
'button.button.button-empty.text',
- {
- attrs: dataIcon(licon.X),
- hook: bind('click', ctrl.toggleExplorer, ctrl.redraw),
- },
+ { attrs: dataIcon(licon.X), hook: bind('click', ctrl.toggleExplorer, ctrl.redraw) },
ctrl.trans.noarg('close'),
);
@@ -264,9 +210,8 @@ const showEmpty = (ctrl: AnalyseCtrl, data?: OpeningData): VNode =>
),
data?.queuePosition
? h('p.explanation', `Indexing ${data.queuePosition} other players first ...`)
- : !ctrl.explorer.config.fullHouse()
- ? h('p.explanation', ctrl.trans.noarg('maybeIncludeMoreGamesFromThePreferencesMenu'))
- : null,
+ : !ctrl.explorer.config.fullHouse() &&
+ h('p.explanation', ctrl.trans.noarg('maybeIncludeMoreGamesFromThePreferencesMenu')),
]),
]);
@@ -285,22 +230,9 @@ const openingTitle = (ctrl: AnalyseCtrl, data?: OpeningData) => {
const title = opening ? `${opening.eco} ${opening.name}` : '';
return h(
'div.title',
- {
- attrs: opening ? { title } : {},
- },
+ { attrs: opening ? { title } : {} },
opening
- ? [
- h(
- 'a',
- {
- attrs: {
- href: `/opening/${opening.name}`,
- target: '_blank',
- },
- },
- title,
- ),
- ]
+ ? [h('a', { attrs: { href: `/opening/${opening.name}`, target: '_blank' } }, title)]
: [showTitle(ctrl, ctrl.data.game.variant)],
);
};
@@ -385,7 +317,7 @@ const explorerTitle = (explorer: ExplorerCtrl) => {
},
explorer.root.trans('player'),
);
- const active = (nodes: MaybeVNodes, title: string) =>
+ const active = (nodes: LooseVNodes, title: string) =>
h(
'span.active.text.' + db,
{
@@ -402,9 +334,7 @@ const explorerTitle = (explorer: ExplorerCtrl) => {
return h('div.explorer-title', [
db == 'masters'
? active([h('strong', 'Masters'), ' database'], masterDbExplanation)
- : explorer.config.allDbs.includes('masters')
- ? otherLink('Masters', masterDbExplanation)
- : undefined,
+ : explorer.config.allDbs.includes('masters') && otherLink('Masters', masterDbExplanation),
db == 'lichess'
? active([h('strong', 'Lichess'), ' database'], lichessDbExplanation)
: otherLink('Lichess', lichessDbExplanation),
@@ -414,15 +344,15 @@ const explorerTitle = (explorer: ExplorerCtrl) => {
[
h(`strong${playerName.length > 14 ? '.long' : ''}`, playerName),
' ' + explorer.root.trans(explorer.config.data.color() == 'white' ? 'asWhite' : 'asBlack'),
- explorer.isIndexing() && !explorer.config.data.open()
- ? h('i.ddloader', {
- attrs: {
- title: queuePosition
- ? `Indexing ${queuePosition} other players first ...`
- : 'Indexing ...',
- },
- })
- : undefined,
+ explorer.isIndexing() &&
+ !explorer.config.data.open() &&
+ h('i.ddloader', {
+ attrs: {
+ title: queuePosition
+ ? `Indexing ${queuePosition} other players first ...`
+ : 'Indexing ...',
+ },
+ }),
],
explorer.root.trans('switchSides'),
)
@@ -465,10 +395,7 @@ export default function (ctrl: AnalyseCtrl): VNode | undefined {
return h(
`section.explorer-box.sub-box${configOpened ? '.explorer__config' : ''}`,
{
- class: {
- loading,
- reduced: !configOpened && (!!explorer.failing() || explorer.movesAway() > 2),
- },
+ class: { loading, reduced: !configOpened && (!!explorer.failing() || explorer.movesAway() > 2) },
hook: {
insert: vnode => ((vnode.elm as HTMLElement).scrollTop = 0),
postpatch(_, vnode) {
diff --git a/ui/analyse/src/explorer/tablebaseView.ts b/ui/analyse/src/explorer/tablebaseView.ts
index 43cec0d45870b..d56ee6846f4b4 100644
--- a/ui/analyse/src/explorer/tablebaseView.ts
+++ b/ui/analyse/src/explorer/tablebaseView.ts
@@ -18,14 +18,10 @@ export function showTablebase(
'tbody',
moveArrowAttributes(ctrl, { fen, onClick: (_, uci) => uci && ctrl.explorerMove(uci) }),
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)])],
- ),
+ h('tr', { key: move.uci, attrs: { 'data-uci': move.uci } }, [
+ h('td', move.san),
+ h('td', [showDtz(ctrl, fen, move), showDtm(ctrl, fen, move)]),
+ ]),
),
),
]),
@@ -37,9 +33,7 @@ function showDtm(ctrl: AnalyseCtrl, fen: Fen, move: TablebaseMoveStats) {
return h(
'result.' + winnerOf(fen, move),
{
- attrs: {
- title: ctrl.trans.pluralSame('mateInXHalfMoves', Math.abs(move.dtm)) + ' (Depth To Mate)',
- },
+ attrs: { title: ctrl.trans.pluralSame('mateInXHalfMoves', Math.abs(move.dtm)) + ' (Depth To Mate)' },
},
'DTM ' + Math.abs(move.dtm),
);
diff --git a/ui/analyse/src/forecast/forecastView.ts b/ui/analyse/src/forecast/forecastView.ts
index 64651217c7b5d..8057979219b1e 100644
--- a/ui/analyse/src/forecast/forecastView.ts
+++ b/ui/analyse/src/forecast/forecastView.ts
@@ -50,59 +50,53 @@ function makeCnodes(ctrl: AnalyseCtrl, fctrl: ForecastCtrl): ForecastStep[] {
export default function (ctrl: AnalyseCtrl, fctrl: ForecastCtrl): VNode {
const cNodes = makeCnodes(ctrl, fctrl);
const isCandidate = fctrl.isCandidate(cNodes);
- 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.list',
- fctrl.forecasts().map((nodes, i) =>
- h(
- 'button.entry.text',
- {
- attrs: dataIcon(licon.PlayTriangle),
- hook: bind(
- 'click',
- _ => {
- const path = fctrl.showForecast(findCurrentPath(ctrl) || '', ctrl.tree, nodes);
- ctrl.userJump(path);
- },
- ctrl.redraw,
- ),
- },
- [
- h('button.del', {
- hook: bind('click', _ => fctrl.removeIndex(i), ctrl.redraw),
- attrs: { 'data-icon': licon.X, type: 'button' },
- }),
- h('sans', renderNodesHtml(nodes)),
- ],
- ),
+ 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.list',
+ fctrl.forecasts().map((nodes, i) =>
+ h(
+ 'button.entry.text',
+ {
+ attrs: dataIcon(licon.PlayTriangle),
+ hook: bind(
+ 'click',
+ () => {
+ const path = fctrl.showForecast(findCurrentPath(ctrl) || '', ctrl.tree, nodes);
+ ctrl.userJump(path);
+ },
+ ctrl.redraw,
+ ),
+ },
+ [
+ h('button.del', {
+ hook: bind('click', _ => fctrl.removeIndex(i), ctrl.redraw),
+ attrs: { 'data-icon': licon.X, type: 'button' },
+ }),
+ h('sans', renderNodesHtml(nodes)),
+ ],
),
),
- h(
- 'button.add.text',
- {
- class: { enabled: isCandidate },
- attrs: dataIcon(isCandidate ? licon.PlusButton : licon.InfoCircle),
- hook: bind('click', _ => fctrl.addNodes(makeCnodes(ctrl, fctrl)), ctrl.redraw),
- },
- [
- isCandidate
- ? h('span', [
- h('span', ctrl.trans.noarg('addCurrentVariation')),
- h('sans', renderNodesHtml(cNodes)),
- ])
- : h('span', ctrl.trans.noarg('playVariationToCreateConditionalPremoves')),
- ],
- ),
- ]),
- fctrl.onMyTurn() ? onMyTurn(ctrl, fctrl, cNodes) : null,
- ],
- );
+ ),
+ h(
+ 'button.add.text',
+ {
+ class: { enabled: isCandidate },
+ attrs: dataIcon(isCandidate ? licon.PlusButton : licon.InfoCircle),
+ hook: bind('click', _ => fctrl.addNodes(makeCnodes(ctrl, fctrl)), ctrl.redraw),
+ },
+ [
+ isCandidate
+ ? h('span', [
+ h('span', ctrl.trans.noarg('addCurrentVariation')),
+ h('sans', renderNodesHtml(cNodes)),
+ ])
+ : h('span', ctrl.trans.noarg('playVariationToCreateConditionalPremoves')),
+ ],
+ ),
+ ]),
+ fctrl.onMyTurn() ? onMyTurn(ctrl, fctrl, cNodes) : null,
+ ]);
}
diff --git a/ui/analyse/src/fork.ts b/ui/analyse/src/fork.ts
index 0df8b7d25112d..e3a7f47900a05 100644
--- a/ui/analyse/src/fork.ts
+++ b/ui/analyse/src/fork.ts
@@ -35,11 +35,7 @@ export function make(ctrl: AnalyseCtrl): ForkCtrl {
prev = node;
selected = 0;
}
- return {
- node,
- selected,
- displayed: displayed(),
- };
+ return { node, selected, displayed: displayed() };
},
next() {
if (!displayed()) return false;
@@ -94,7 +90,7 @@ const eventToIndex = (e: MouseEvent): number | undefined => {
};
export function view(ctrl: AnalyseCtrl, concealOf?: ConcealOf) {
- if (ctrl.retro) return;
+ if (ctrl.retro?.isSolving()) return;
const state = ctrl.fork.state();
if (!state.displayed) return;
const isMainline = concealOf && ctrl.onMainline;
@@ -120,16 +116,9 @@ export function view(ctrl: AnalyseCtrl, concealOf?: ConcealOf) {
if (!conceal)
return h(
'move',
- {
- class: classes,
- attrs: { 'data-it': it },
- },
+ { class: classes, attrs: { 'data-it': it } },
renderIndexAndMove(
- {
- withDots: true,
- showEval: ctrl.showComputer(),
- showGlyphs: ctrl.showComputer(),
- },
+ { withDots: true, showEval: ctrl.showComputer(), showGlyphs: ctrl.showComputer() },
node,
)!,
);
diff --git a/ui/analyse/src/ground.ts b/ui/analyse/src/ground.ts
index d3b78d5f4b0ea..2df268bd849ab 100644
--- a/ui/analyse/src/ground.ts
+++ b/ui/analyse/src/ground.ts
@@ -23,18 +23,7 @@ export const render = (ctrl: AnalyseCtrl): VNode =>
export function promote(ground: CgApi, key: Key, role: cg.Role) {
const piece = ground.state.pieces.get(key);
if (piece && piece.role == 'pawn') {
- ground.setPieces(
- new Map([
- [
- key,
- {
- color: piece.color,
- role,
- promoted: true,
- },
- ],
- ]),
- );
+ ground.setPieces(new Map([[key, { color: piece.color, role, promoted: true }]]));
}
}
diff --git a/ui/analyse/src/plugins/nvui.ts b/ui/analyse/src/plugins/nvui.ts
index 5162f52e39621..f7d72b869ae16 100644
--- a/ui/analyse/src/plugins/nvui.ts
+++ b/ui/analyse/src/plugins/nvui.ts
@@ -87,24 +87,13 @@ export function initModule(ctrl: AnalyseController) {
h('p', `${d.game.rated ? 'Rated' : 'Casual'} ${d.game.perf}`),
d.clock ? h('p', `Clock: ${d.clock.initial / 60} + ${d.clock.increment}`) : null,
h('h2', 'Moves'),
- h(
- 'p.moves',
- {
- attrs: {
- role: 'log',
- 'aria-live': 'off',
- },
- },
- renderCurrentLine(ctrl, style),
- ),
+ h('p.moves', { attrs: { role: 'log', 'aria-live': 'off' } }, renderCurrentLine(ctrl, style)),
...(!ctrl.studyPractice
? [
h(
'button',
{
- attrs: {
- 'aria-pressed': `${ctrl.explorer.enabled()}`,
- },
+ attrs: { 'aria-pressed': `${ctrl.explorer.enabled()}` },
hook: bind('click', _ => ctrl.explorer.toggle(), ctrl.redraw),
},
ctrl.trans.noarg('openingExplorerAndTablebase'),
@@ -118,12 +107,7 @@ export function initModule(ctrl: AnalyseController) {
h('h2', 'Current position'),
h(
'p.position.lastMove',
- {
- attrs: {
- 'aria-live': 'assertive',
- 'aria-atomic': 'true',
- },
- },
+ { attrs: { 'aria-live': 'assertive', 'aria-atomic': 'true' } },
// make sure consecutive positions are different so that they get re-read
renderCurrentNode(ctrl, style) + (ctrl.node.ply % 2 === 0 ? '' : ' '),
),
@@ -144,12 +128,7 @@ export function initModule(ctrl: AnalyseController) {
h('label', [
'Command input',
h('input.move.mousetrap', {
- attrs: {
- name: 'move',
- type: 'text',
- autocomplete: 'off',
- autofocus: true,
- },
+ attrs: { name: 'move', type: 'text', autocomplete: 'off', autofocus: true },
}),
]),
],
@@ -196,10 +175,7 @@ export function initModule(ctrl: AnalyseController) {
h(
'div.boardstatus',
{
- attrs: {
- 'aria-live': 'polite',
- 'aria-atomic': 'true',
- },
+ attrs: { 'aria-live': 'polite', 'aria-atomic': 'true' },
},
'',
),
@@ -367,18 +343,10 @@ function renderResult(ctrl: AnalyseController): VNode[] {
}
return [
h('h2', 'Game status'),
- h(
- 'div.status',
- {
- attrs: {
- role: 'status',
- 'aria-live': 'assertive',
- 'aria-atomic': 'true',
- },
- },
-
- [h('div.result', result), h('div.status', viewStatus(ctrl))],
- ),
+ h('div.status', { attrs: { role: 'status', 'aria-live': 'assertive', 'aria-atomic': 'true' } }, [
+ h('div.result', result),
+ h('div.status', viewStatus(ctrl)),
+ ]),
];
}
return [];
@@ -473,12 +441,7 @@ function renderAcpl(ctrl: AnalyseController, style: Style): MaybeVNodes | undefi
.map(node =>
h(
'option',
- {
- attrs: {
- value: node.ply,
- selected: node.ply === ctrl.node.ply,
- },
- },
+ { attrs: { value: node.ply, selected: node.ply === ctrl.node.ply } },
[plyToTurn(node.ply), renderSan(node.san!, node.uci, style), renderComments(node, style)].join(
' ',
),
@@ -501,17 +464,13 @@ function requestAnalysisButton(
'button',
{
hook: bind('click', _ =>
- xhr
- .text(`/${ctrl.data.game.id}/request-analysis`, {
- method: 'post',
- })
- .then(
- () => {
- inProgress(true);
- notify('Server-side analysis in progress');
- },
- _ => notify('Cannot run server-side analysis'),
- ),
+ xhr.text(`/${ctrl.data.game.id}/request-analysis`, { method: 'post' }).then(
+ () => {
+ inProgress(true);
+ notify('Server-side analysis in progress');
+ },
+ () => notify('Cannot run server-side analysis'),
+ ),
),
},
'Request a computer analysis',
@@ -560,9 +519,7 @@ function userHtml(ctrl: AnalyseController, player: Player) {
? h('span', [
h(
'a',
- {
- attrs: { href: '/@/' + user.username },
- },
+ { attrs: { href: '/@/' + user.username } },
user.title ? `${user.title} ${user.username}` : user.username,
),
rating ? ` ${rating}` : ``,
diff --git a/ui/analyse/src/practice/practiceView.ts b/ui/analyse/src/practice/practiceView.ts
index 20e8ce32b3900..2cec508a7b219 100644
--- a/ui/analyse/src/practice/practiceView.ts
+++ b/ui/analyse/src/practice/practiceView.ts
@@ -88,9 +88,7 @@ function renderRunning(root: AnalyseCtrl, ctrl: PracticeCtrl): VNode {
ctrl.isMyTurn()
? h(
'a',
- {
- hook: bind('click', () => root.practice!.hint(), ctrl.redraw),
- },
+ { hook: bind('click', () => root.practice!.hint(), ctrl.redraw) },
root.trans.noarg(
hint ? (hint.mode === 'piece' ? 'seeBestMove' : 'hideBestMove') : 'getAHint',
),
diff --git a/ui/analyse/src/retrospect/retroView.ts b/ui/analyse/src/retrospect/retroView.ts
index 6e327ef3cf3a4..a77a3e3a78c4e 100644
--- a/ui/analyse/src/retrospect/retroView.ts
+++ b/ui/analyse/src/retrospect/retroView.ts
@@ -8,31 +8,16 @@ 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) }, ctrl.noarg('viewTheSolution')),
+ h('a', { hook: bind('click', ctrl.skip) }, ctrl.noarg('skipThisMove')),
]);
}
function jumpToNext(ctrl: RetroCtrl) {
- return h(
- 'a.half.continue',
- {
- hook: bind('click', ctrl.jumpToNext),
- },
- [h('i', { attrs: dataIcon(licon.PlayTriangle) }), ctrl.noarg('next')],
- );
+ return h('a.half.continue', { hook: bind('click', ctrl.jumpToNext) }, [
+ h('i', { attrs: dataIcon(licon.PlayTriangle) }),
+ ctrl.noarg('next'),
+ ]);
}
const minDepth = 8;
@@ -64,11 +49,7 @@ const feedback = {
h(
'move',
renderIndexAndMove(
- {
- withDots: true,
- showGlyphs: true,
- showEval: false,
- },
+ { withDots: true, showGlyphs: true, showEval: false },
ctrl.current()!.fault.node,
)!,
),
@@ -88,13 +69,7 @@ const feedback = {
h('div.instruction', [
h('strong', ctrl.noarg('youBrowsedAway')),
h('div.choices.off', [
- h(
- 'a',
- {
- hook: bind('click', ctrl.jumpToNext),
- },
- ctrl.noarg('resumeLearning'),
- ),
+ h('a', { hook: bind('click', ctrl.jumpToNext) }, ctrl.noarg('resumeLearning')),
]),
]),
]),
@@ -135,13 +110,7 @@ const feedback = {
'bestWasX',
h(
'strong',
- renderIndexAndMove(
- {
- withDots: true,
- showEval: false,
- },
- ctrl.current()!.solution.node,
- )!,
+ renderIndexAndMove({ withDots: true, showEval: false }, ctrl.current()!.solution.node)!,
),
),
),
@@ -230,10 +199,7 @@ export default function (root: AnalyseCtrl): VNode | undefined {
h('span', `${Math.min(completion[0] + 1, completion[1])} / ${completion[1]}`),
h('button.fbt', {
hook: bind('click', root.toggleRetro, root.redraw),
- attrs: {
- 'data-icon': licon.X,
- 'aria-label': 'Close learn window',
- },
+ attrs: { 'data-icon': licon.X, 'aria-label': 'Close learn window' },
}),
]),
h('div.feedback.' + fb, renderFeedback(root, fb)),
diff --git a/ui/analyse/src/study/chapterEditForm.ts b/ui/analyse/src/study/chapterEditForm.ts
index cdb8de456d1a5..d2828b7e1e5c9 100644
--- a/ui/analyse/src/study/chapterEditForm.ts
+++ b/ui/analyse/src/study/chapterEditForm.ts
@@ -26,10 +26,7 @@ export class StudyChapterEditForm {
) {}
open = (data: StudyChapterMeta) => {
- this.current({
- id: data.id,
- name: data.name,
- });
+ this.current({ id: data.id, name: data.name });
this.chapterConfig(data.id).then(d => {
this.current(d!);
this.redraw();
@@ -89,18 +86,9 @@ 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' } }, noarg('name')),
h('input#chapter-name.form-control', {
- attrs: {
- minlength: 2,
- maxlength: 80,
- },
+ attrs: { minlength: 2, maxlength: 80 },
hook: onInsert(el => {
if (!el.value) {
el.value = data.name;
@@ -133,44 +121,22 @@ function viewLoaded(ctrl: StudyChapterEditForm, data: StudyChapterConfig): VNode
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' } }, noarg('orientation')),
h(
'select#chapter-orientation.form-control',
- ['white', 'black'].map(function (color) {
- return option(color, data.orientation, noarg(color));
- }),
+ ['white', 'black'].map(color => option(color, data.orientation, noarg(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' } }, noarg('analysisMode')),
h(
'select#chapter-mode.form-control',
- chapterForm.modeChoices.map(c => {
- return option(c[0], mode, noarg(c[1]));
- }),
+ chapterForm.modeChoices.map(c => option(c[0], mode, noarg(c[1]))),
),
]),
]),
h('div.form-group', [
- h(
- 'label.form-label',
- {
- attrs: { for: 'chapter-description' },
- },
- noarg('pinnedChapterComment'),
- ),
+ h('label.form-label', { attrs: { for: 'chapter-description' } }, noarg('pinnedChapterComment')),
h(
'select#chapter-description.form-control',
[
@@ -185,7 +151,7 @@ function viewLoaded(ctrl: StudyChapterEditForm, data: StudyChapterConfig): VNode
{
hook: bind(
'click',
- _ => {
+ () => {
if (confirm(noarg('clearAllCommentsInThisChapter'))) ctrl.clearAnnotations(data.id);
},
ctrl.redraw,
@@ -199,7 +165,7 @@ function viewLoaded(ctrl: StudyChapterEditForm, data: StudyChapterConfig): VNode
{
hook: bind(
'click',
- _ => {
+ () => {
if (confirm(noarg('clearVariations'))) ctrl.clearVariations(data.id);
},
ctrl.redraw,
@@ -215,7 +181,7 @@ function viewLoaded(ctrl: StudyChapterEditForm, data: StudyChapterConfig): VNode
{
hook: bind(
'click',
- _ => {
+ () => {
if (confirm(noarg('deleteThisChapter'))) ctrl.delete(data.id);
},
ctrl.redraw,
@@ -224,13 +190,7 @@ function viewLoaded(ctrl: StudyChapterEditForm, data: StudyChapterConfig): VNode
},
noarg('deleteChapter'),
),
- h(
- 'button.button',
- {
- attrs: { type: 'submit' },
- },
- noarg('saveChapter'),
- ),
+ h('button.button', { attrs: { type: 'submit' } }, noarg('saveChapter')),
]),
];
}
diff --git a/ui/analyse/src/study/chapterNewForm.ts b/ui/analyse/src/study/chapterNewForm.ts
index 17d331e91c342..5f568eabdc1a1 100644
--- a/ui/analyse/src/study/chapterNewForm.ts
+++ b/ui/analyse/src/study/chapterNewForm.ts
@@ -2,10 +2,10 @@ import { parseFen } from 'chessops/fen';
import { defined, prop, Prop, toggle } from 'common';
import * as licon from 'common/licon';
import { snabDialog } from 'common/dialog';
-import { bind, bindSubmit, onInsert } from 'common/snabbdom';
+import { bind, bindSubmit, onInsert, looseH as h } from 'common/snabbdom';
import { storedStringProp } from 'common/storage';
import * as xhr from 'common/xhr';
-import { h, VNode } from 'snabbdom';
+import { VNode } from 'snabbdom';
import AnalyseCtrl from '../ctrl';
import { StudySocketSend } from '../socket';
import { spinnerVdom as spinner } from 'common/spinner';
@@ -66,11 +66,7 @@ export class StudyChapterNewForm {
};
submit = (d: Omit) => {
const study = this.root.study!;
- const dd = {
- ...d,
- sticky: study.vm.mode.sticky,
- initial: this.initial(),
- };
+ const dd = { ...d, sticky: study.vm.mode.sticky, initial: this.initial() };
if (!dd.pgn) this.send('addChapter', dd);
else importPgn(study.data.id, dd);
this.isOpen(false);
@@ -128,45 +124,34 @@ export function view(ctrl: StudyChapterNewForm): VNode {
noClickAway: true,
onInsert: dlg => dlg.show(),
vnodes: [
- activeTab === 'edit'
- ? null
- : h('h2', [
- noarg('newChapter'),
- h('i.help', {
- attrs: { 'data-icon': licon.InfoCircle },
- hook: bind('click', ctrl.startTour),
- }),
- ]),
+ activeTab !== 'edit' &&
+ h('h2', [
+ noarg('newChapter'),
+ h('i.help', { attrs: { 'data-icon': licon.InfoCircle }, hook: bind('click', ctrl.startTour) }),
+ ]),
h(
'form.form3',
{
- hook: bindSubmit(e => {
- ctrl.submit({
- name: fieldValue(e, 'name'),
- game: fieldValue(e, 'game'),
- variant: fieldValue(e, 'variant') as VariantKey,
- pgn: fieldValue(e, 'pgn'),
- orientation: fieldValue(e, 'orientation') as Orientation,
- mode: fieldValue(e, 'mode') as ChapterMode,
- fen: fieldValue(e, 'fen') || (ctrl.tab() === 'edit' ? ctrl.editorFen() : null),
- isDefaultName: ctrl.isDefaultName(),
- });
- }, ctrl.redraw),
+ hook: bindSubmit(
+ e =>
+ ctrl.submit({
+ name: fieldValue(e, 'name'),
+ game: fieldValue(e, 'game'),
+ variant: fieldValue(e, 'variant') as VariantKey,
+ pgn: fieldValue(e, 'pgn'),
+ orientation: fieldValue(e, 'orientation') as Orientation,
+ mode: fieldValue(e, 'mode') as ChapterMode,
+ fen: fieldValue(e, 'fen') || (ctrl.tab() === 'edit' ? ctrl.editorFen() : null),
+ isDefaultName: ctrl.isDefaultName(),
+ }),
+ ctrl.redraw,
+ ),
},
[
h('div.form-group', [
- h(
- 'label.form-label',
- {
- attrs: { for: 'chapter-name' },
- },
- noarg('name'),
- ),
+ h('label.form-label', { attrs: { for: 'chapter-name' } }, noarg('name')),
h('input#chapter-name.form-control', {
- attrs: {
- minlength: 2,
- maxlength: 80,
- },
+ attrs: { minlength: 2, maxlength: 80 },
hook: onInsert(el => {
if (!el.value) {
el.value = trans('chapterX', ctrl.initial() ? 1 : ctrl.chapters().length + 1);
@@ -184,156 +169,124 @@ export function view(ctrl: StudyChapterNewForm): VNode {
makeTab('fen', 'FEN', noarg('loadAPositionFromFen')),
makeTab('pgn', 'PGN', noarg('loadAGameFromPgn')),
]),
- activeTab === 'edit'
- ? h(
- 'div.board-editor-wrap',
- {
- hook: {
- insert(vnode) {
- xhr.json('/editor.json').then(async data => {
- data.el = vnode.elm;
- data.fen = ctrl.root.node.fen;
- data.embed = true;
- data.options = {
- inlineCastling: true,
- orientation: currentChapter.setup.orientation,
- onChange: ctrl.editorFen,
- };
- ctrl.editor = await lichess.asset.loadEsm('editor', { init: data });
- ctrl.editorFen(ctrl.editor.getFen());
- });
- },
- destroy: _ => {
- ctrl.editor = null;
- },
+ activeTab === 'edit' &&
+ h(
+ 'div.board-editor-wrap',
+ {
+ hook: {
+ insert(vnode) {
+ xhr.json('/editor.json').then(async data => {
+ data.el = vnode.elm;
+ data.fen = ctrl.root.node.fen;
+ data.embed = true;
+ data.options = {
+ inlineCastling: true,
+ orientation: currentChapter.setup.orientation,
+ onChange: ctrl.editorFen,
+ coordinates: true,
+ };
+ ctrl.editor = await lichess.asset.loadEsm('editor', { init: data });
+ ctrl.editorFen(ctrl.editor.getFen());
+ });
},
+ destroy: () => (ctrl.editor = null),
},
- [spinner()],
- )
- : null,
- activeTab === 'game'
- ? h('div.form-group', [
- h(
- 'label.form-label',
- {
- attrs: { for: 'chapter-game' },
- },
- trans('loadAGameFromXOrY', 'lichess.org', 'chessgames.com'),
- ),
- h('textarea#chapter-game.form-control', {
- attrs: {
- placeholder: noarg('urlOfTheGame'),
- },
- hook: onInsert((el: HTMLTextAreaElement) => {
- el.addEventListener('change', () => el.reportValidity());
- el.addEventListener('input', _ => {
- const ok = el.value
- .trim()
- .split('\n')
- .every(line =>
- line
- .trim()
- .match(
- new RegExp(
- `^((.*${location.host}/\\w{8,12}.*)|\\w{8}|\\w{12}|(.*chessgames\\.com/.*[?&]gid=\\d+.*)|)$`,
- ),
+ },
+ [spinner()],
+ ),
+ activeTab === 'game' &&
+ h('div.form-group', [
+ h(
+ 'label.form-label',
+ { attrs: { for: 'chapter-game' } },
+ trans('loadAGameFromXOrY', 'lichess.org', 'chessgames.com'),
+ ),
+ h('textarea#chapter-game.form-control', {
+ attrs: { placeholder: noarg('urlOfTheGame') },
+ hook: onInsert((el: HTMLTextAreaElement) => {
+ el.addEventListener('change', () => el.reportValidity());
+ el.addEventListener('input', () => {
+ const ok = el.value
+ .trim()
+ .split('\n')
+ .every(line =>
+ line
+ .trim()
+ .match(
+ new RegExp(
+ `^((.*${location.host}/\\w{8,12}.*)|\\w{8}|\\w{12}|(.*chessgames\\.com/.*[?&]gid=\\d+.*)|)$`,
),
- );
- el.setCustomValidity(ok ? '' : 'Invalid game ID(s) or URL(s)');
- });
- }),
+ ),
+ );
+ el.setCustomValidity(ok ? '' : 'Invalid game ID(s) or URL(s)');
+ });
}),
- ])
- : null,
- activeTab === 'fen'
- ? h('div.form-group', [
- h('input#chapter-fen.form-control', {
- attrs: {
- value: ctrl.root.node.fen,
- placeholder: noarg('loadAPositionFromFen'),
- },
- hook: onInsert((el: HTMLInputElement) => {
- el.addEventListener('change', () => el.reportValidity());
- el.addEventListener('input', _ =>
- el.setCustomValidity(parseFen(el.value.trim()).isOk ? '' : 'Invalid FEN'),
- );
- }),
- }),
- ])
- : null,
- activeTab === 'pgn'
- ? h('div.form-group', [
- h('textarea#chapter-pgn.form-control', {
- attrs: {
- placeholder: trans.pluralSame('pasteYourPgnTextHereUpToNbGames', ctrl.multiPgnMax),
- },
+ }),
+ ]),
+ activeTab === 'fen' &&
+ h('div.form-group', [
+ h('input#chapter-fen.form-control', {
+ attrs: { value: ctrl.root.node.fen, placeholder: noarg('loadAPositionFromFen') },
+ hook: onInsert((el: HTMLInputElement) => {
+ el.addEventListener('change', () => el.reportValidity());
+ el.addEventListener('input', _ =>
+ el.setCustomValidity(parseFen(el.value.trim()).isOk ? '' : 'Invalid FEN'),
+ );
}),
- h(
- 'button.button.button-empty.import-from__chapter',
- {
- hook: bind('click', () => {
+ }),
+ ]),
+ activeTab === 'pgn' &&
+ h('div.form-group', [
+ h('textarea#chapter-pgn.form-control', {
+ attrs: {
+ placeholder: trans.pluralSame('pasteYourPgnTextHereUpToNbGames', ctrl.multiPgnMax),
+ },
+ }),
+ h(
+ 'button.button.button-empty.import-from__chapter',
+ {
+ hook: bind(
+ 'click',
+ () => {
xhr
.text(`/study/${study.data.id}/${currentChapter.id}.pgn`)
.then(pgnData => $('#chapter-pgn').val(pgnData));
- }),
- },
- trans('importFromChapterX', study.currentChapter().name),
- ),
- window.FileReader
- ? h('input#chapter-pgn-file.form-control', {
- attrs: {
- type: 'file',
- accept: '.pgn',
- },
- hook: bind('change', e => {
- const file = (e.target as HTMLInputElement).files![0];
- if (!file) return;
- const reader = new FileReader();
- reader.onload = function () {
- (document.getElementById('chapter-pgn') as HTMLTextAreaElement).value =
- reader.result as string;
- };
- reader.readAsText(file);
- }),
- })
- : null,
- ])
- : null,
- h('div.form-split', [
- h('div.form-group.form-half', [
- h(
- 'label.form-label',
- {
- attrs: { for: 'chapter-variant' },
+ return false;
+ },
+ undefined,
+ false,
+ ),
},
- noarg('Variant'),
+ trans('importFromChapterX', study.currentChapter().name),
),
+ window.FileReader &&
+ h('input#chapter-pgn-file.form-control', {
+ attrs: { type: 'file', accept: '.pgn' },
+ hook: bind('change', e => {
+ const file = (e.target as HTMLInputElement).files![0];
+ if (!file) return;
+ const reader = new FileReader();
+ reader.onload = function () {
+ (document.getElementById('chapter-pgn') as HTMLTextAreaElement).value =
+ reader.result as string;
+ };
+ reader.readAsText(file);
+ }),
+ }),
+ ]),
+ h('div.form-split', [
+ h('div.form-group.form-half', [
+ h('label.form-label', { attrs: { for: 'chapter-variant' } }, noarg('Variant')),
h(
'select#chapter-variant.form-control',
- {
- attrs: { disabled: gameOrPgn },
- },
+ { attrs: { disabled: gameOrPgn } },
gameOrPgn
- ? [
- h(
- 'option',
- {
- attrs: { value: 'standard' },
- },
- noarg('automatic'),
- ),
- ]
+ ? [h('option', { attrs: { value: 'standard' } }, noarg('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' } }, noarg('orientation')),
h(
'select#chapter-orientation.form-control',
{
@@ -349,13 +302,7 @@ export function view(ctrl: StudyChapterNewForm): VNode {
]),
]),
h('div.form-group', [
- h(
- 'label.form-label',
- {
- attrs: { for: 'chapter-mode' },
- },
- noarg('analysisMode'),
- ),
+ h('label.form-label', { attrs: { for: 'chapter-mode' } }, noarg('analysisMode')),
h(
'select#chapter-mode.form-control',
modeChoices.map(c => option(c[0], mode, noarg(c[1]))),
@@ -363,13 +310,7 @@ export function view(ctrl: StudyChapterNewForm): VNode {
]),
h(
'div.form-actions.single',
- h(
- 'button.button',
- {
- attrs: { type: 'submit' },
- },
- noarg('createChapter'),
- ),
+ h('button.button', { attrs: { type: 'submit' } }, noarg('createChapter')),
),
],
),
diff --git a/ui/analyse/src/study/commentForm.ts b/ui/analyse/src/study/commentForm.ts
index 3f91fb18e813f..5929baeb93fa4 100644
--- a/ui/analyse/src/study/commentForm.ts
+++ b/ui/analyse/src/study/commentForm.ts
@@ -20,21 +20,12 @@ export class CommentForm {
doSubmit = throttle(500, (text: string) => {
const cur = this.current();
- if (cur)
- this.root.study!.makeChange('setComment', {
- ch: cur.chapterId,
- path: cur.path,
- text,
- });
+ if (cur) this.root.study!.makeChange('setComment', { ch: cur.chapterId, path: cur.path, text });
});
start = (chapterId: string, path: Tree.Path, node: Tree.Node): void => {
this.opening(true);
- this.current({
- chapterId,
- path,
- node,
- });
+ this.current({ chapterId, path, node });
this.root.userJump(path);
};
@@ -47,11 +38,7 @@ export class CommentForm {
}
};
delete = (chapterId: string, path: Tree.Path, id: string) => {
- this.root.study!.makeChange('deleteComment', {
- ch: chapterId,
- path,
- id,
- });
+ this.root.study!.makeChange('deleteComment', { ch: chapterId, path, id });
};
}
@@ -83,9 +70,7 @@ export function view(root: AnalyseCtrl): VNode {
return h(
'div.study__comments',
- {
- hook: onInsert(() => root.enableWiki(root.data.game.variant.key === 'standard')),
- },
+ { hook: onInsert(() => root.enableWiki(root.data.game.variant.key === 'standard')) },
[
currentComments(root, !study.members.canContribute()),
h('form.form3', [
diff --git a/ui/analyse/src/study/description.ts b/ui/analyse/src/study/description.ts
index 783232523933f..34e4021ae9720 100644
--- a/ui/analyse/src/study/description.ts
+++ b/ui/analyse/src/study/description.ts
@@ -1,6 +1,6 @@
-import { h, VNode } from 'snabbdom';
+import { VNode } from 'snabbdom';
import * as licon from 'common/licon';
-import { bind, onInsert } from 'common/snabbdom';
+import { bind, onInsert, looseH as h } from 'common/snabbdom';
import { richHTML } from 'common/richText';
import StudyCtrl from './studyCtrl';
@@ -35,49 +35,24 @@ export function view(study: StudyCtrl, chapter: boolean): VNode | undefined {
const isEmpty = desc.text === '-';
if (!desc.text || (isEmpty && !contrib)) return;
return h(`div.study-desc${chapter ? '.chapter-desc' : ''}${isEmpty ? '.empty' : ''}`, [
- contrib && !isEmpty
- ? h('div.contrib', [
- h('span', descTitle(chapter)),
- isEmpty
- ? null
- : h('a', {
- attrs: {
- 'data-icon': licon.Pencil,
- title: 'Edit',
- },
- hook: bind(
- 'click',
- _ => {
- desc.edit = true;
- },
- desc.redraw,
- ),
- }),
+ contrib &&
+ !isEmpty &&
+ h('div.contrib', [
+ h('span', descTitle(chapter)),
+ !isEmpty &&
h('a', {
- attrs: {
- 'data-icon': licon.Trash,
- title: 'Delete',
- },
- hook: bind('click', () => {
- if (confirm('Delete permanent description?')) desc.save('');
- }),
+ attrs: { 'data-icon': licon.Pencil, title: 'Edit' },
+ hook: bind('click', () => (desc.edit = true), desc.redraw),
}),
- ])
- : null,
+ h('a', {
+ attrs: { 'data-icon': licon.Trash, title: 'Delete' },
+ hook: bind('click', () => {
+ if (confirm('Delete permanent description?')) desc.save('');
+ }),
+ }),
+ ]),
isEmpty
- ? h(
- 'a.text.button',
- {
- hook: bind(
- 'click',
- _ => {
- desc.edit = true;
- },
- desc.redraw,
- ),
- },
- descTitle(chapter),
- )
+ ? h('a.text.button', { hook: bind('click', () => (desc.edit = true), desc.redraw) }, descTitle(chapter))
: h('div.text', { hook: richHTML(desc.text) }),
]);
}
@@ -87,17 +62,8 @@ const edit = (ctrl: DescriptionCtrl, id: string, chapter: boolean): VNode =>
h('div.title', [
descTitle(chapter),
h('button.button.button-empty.button-green', {
- attrs: {
- 'data-icon': licon.Checkmark,
- title: 'Save and close',
- },
- hook: bind(
- 'click',
- () => {
- ctrl.edit = false;
- },
- ctrl.redraw,
- ),
+ attrs: { 'data-icon': licon.Checkmark, title: 'Save and close' },
+ hook: bind('click', () => (ctrl.edit = false), ctrl.redraw),
}),
]),
h('form.form3', [
@@ -105,9 +71,7 @@ const edit = (ctrl: DescriptionCtrl, id: string, chapter: boolean): VNode =>
h('textarea#form-control.desc-text.' + id, {
hook: onInsert(el => {
el.value = ctrl.text === '-' ? '' : ctrl.text || '';
- el.oninput = () => {
- ctrl.save(el.value.trim());
- };
+ el.oninput = () => ctrl.save(el.value.trim());
el.focus();
}),
}),
diff --git a/ui/analyse/src/study/gamebook/gamebookButtons.ts b/ui/analyse/src/study/gamebook/gamebookButtons.ts
index 844b4aa0bca45..648abbd9a854e 100644
--- a/ui/analyse/src/study/gamebook/gamebookButtons.ts
+++ b/ui/analyse/src/study/gamebook/gamebookButtons.ts
@@ -1,6 +1,6 @@
-import { h, VNode } from 'snabbdom';
+import { VNode } from 'snabbdom';
import * as licon from 'common/licon';
-import { bind, dataIcon } from 'common/snabbdom';
+import { bind, dataIcon, looseH as h } from 'common/snabbdom';
import AnalyseCtrl from '../../ctrl';
import StudyCtrl from '../studyCtrl';
@@ -12,32 +12,24 @@ export function playButtons(root: AnalyseCtrl): VNode | undefined {
fb = state.feedback,
myTurn = fb === 'play';
return h('div.gamebook-buttons', [
- root.path
- ? h(
- 'button.fbt.text.back',
- {
- attrs: {
- 'data-icon': licon.LessThan,
- type: 'button',
- },
- hook: bind('click', () => root.userJump(''), ctrl.redraw),
- },
- root.trans.noarg('back'),
- )
- : null,
- myTurn
- ? h(
- 'button.fbt.text.solution',
- {
- attrs: {
- 'data-icon': licon.PlayTriangle,
- type: 'button',
- },
- hook: bind('click', ctrl.solution, ctrl.redraw),
- },
- root.trans.noarg('viewTheSolution'),
- )
- : undefined,
+ root.path &&
+ h(
+ 'button.fbt.text.back',
+ {
+ attrs: { 'data-icon': licon.LessThan, type: 'button' },
+ hook: bind('click', () => root.userJump(''), ctrl.redraw),
+ },
+ root.trans.noarg('back'),
+ ),
+ myTurn &&
+ h(
+ 'button.fbt.text.solution',
+ {
+ attrs: { 'data-icon': licon.PlayTriangle, type: 'button' },
+ hook: bind('click', ctrl.solution, ctrl.redraw),
+ },
+ root.trans.noarg('viewTheSolution'),
+ ),
overrideButton(study),
]);
}
@@ -50,15 +42,10 @@ export function overrideButton(study: StudyCtrl): VNode | undefined {
'button.fbt.text.preview',
{
class: { active: o === 'play' },
- attrs: {
- 'data-icon': licon.Eye,
- type: 'button',
- },
+ attrs: { 'data-icon': licon.Eye, type: 'button' },
hook: bind(
'click',
- () => {
- study.setGamebookOverride(o === 'play' ? undefined : 'play');
- },
+ () => study.setGamebookOverride(o === 'play' ? undefined : 'play'),
study.redraw,
),
},
@@ -75,9 +62,7 @@ export function overrideButton(study: StudyCtrl): VNode | undefined {
attrs: dataIcon(licon.Microscope),
hook: bind(
'click',
- () => {
- study.setGamebookOverride(isAnalyse ? undefined : 'analyse');
- },
+ () => study.setGamebookOverride(isAnalyse ? undefined : 'analyse'),
study.redraw,
),
},
diff --git a/ui/analyse/src/study/gamebook/gamebookEdit.ts b/ui/analyse/src/study/gamebook/gamebookEdit.ts
index d21fe5b87db58..8b27906f1dd84 100644
--- a/ui/analyse/src/study/gamebook/gamebookEdit.ts
+++ b/ui/analyse/src/study/gamebook/gamebookEdit.ts
@@ -41,25 +41,18 @@ export function render(ctrl: AnalyseCtrl): VNode {
if (!ctrl.path) {
if (isMyMove)
content = [
- h(
- 'div.legend.todo.clickable',
- {
- hook: commentHook,
- class: { done: isCommented },
- },
- [iconTag(licon.BubbleSpeech), h('p', 'Help the player find the initial move, with a comment.')],
- ),
+ h('div.legend.todo.clickable', { hook: commentHook, class: { done: isCommented } }, [
+ iconTag(licon.BubbleSpeech),
+ h('p', 'Help the player find the initial move, with a comment.'),
+ ]),
renderHint(ctrl),
];
else
content = [
- h(
- 'div.legend.clickable',
- {
- hook: commentHook,
- },
- [iconTag(licon.BubbleSpeech), h('p', 'Introduce the gamebook with a comment')],
- ),
+ h('div.legend.clickable', { hook: commentHook }, [
+ iconTag(licon.BubbleSpeech),
+ h('p', 'Introduce the gamebook with a comment'),
+ ]),
h('div.legend.todo', { class: { done: !!ctrl.node.children[0] } }, [
iconTag(licon.PlayTriangle),
h('p', "Put the opponent's first move on the board."),
@@ -68,66 +61,41 @@ export function render(ctrl: AnalyseCtrl): VNode {
} else if (ctrl.onMainline) {
if (isMyMove)
content = [
- h(
- 'div.legend.todo.clickable',
- {
- hook: commentHook,
- class: { done: isCommented },
- },
- [
- iconTag(licon.BubbleSpeech),
- h('p', 'Explain the opponent move, and help the player find the next move, with a comment.'),
- ],
- ),
+ h('div.legend.todo.clickable', { hook: commentHook, class: { done: isCommented } }, [
+ iconTag(licon.BubbleSpeech),
+ h('p', 'Explain the opponent move, and help the player find the next move, with a comment.'),
+ ]),
renderHint(ctrl),
];
else
content = [
- h(
- 'div.legend.clickable',
- {
- hook: commentHook,
- },
- [
- iconTag(licon.BubbleSpeech),
- h(
- 'p',
- "You may reflect on the player's correct move, with a comment; or leave empty to jump immediately to the next move.",
- ),
- ],
- ),
+ h('div.legend.clickable', { hook: commentHook }, [
+ iconTag(licon.BubbleSpeech),
+ h(
+ 'p',
+ "You may reflect on the player's correct move, with a comment; or leave empty to jump immediately to the next move.",
+ ),
+ ]),
hasVariation
? null
- : h(
- 'div.legend.clickable',
- {
- hook: bind('click', () => control.prev(ctrl), ctrl.redraw),
- },
- [
- iconTag(licon.PlayTriangle),
- h('p', 'Add variation moves to explain why specific other moves are wrong.'),
- ],
- ),
+ : h('div.legend.clickable', { hook: bind('click', () => control.prev(ctrl), ctrl.redraw) }, [
+ iconTag(licon.PlayTriangle),
+ h('p', 'Add variation moves to explain why specific other moves are wrong.'),
+ ]),
renderDeviation(ctrl),
];
} else
content = [
- h(
- 'div.legend.todo.clickable',
- {
- hook: commentHook,
- class: { done: isCommented },
- },
- [iconTag(licon.BubbleSpeech), h('p', 'Explain why this move is wrong in a comment')],
- ),
+ h('div.legend.todo.clickable', { hook: commentHook, class: { done: isCommented } }, [
+ iconTag(licon.BubbleSpeech),
+ h('p', 'Explain why this move is wrong in a comment'),
+ ]),
h('div.legend', [h('p', 'Or promote it as the mainline if it is the right move.')]),
];
return h(
'div.gamebook-edit',
- {
- hook: { insert: _ => lichess.asset.loadCssPath('analyse.gamebook.edit') },
- },
+ { hook: { insert: () => lichess.asset.loadCssPath('analyse.gamebook.edit') } },
content,
);
}
diff --git a/ui/analyse/src/study/gamebook/gamebookPlayView.ts b/ui/analyse/src/study/gamebook/gamebookPlayView.ts
index d4a7d92fac747..5ceccc73ff60f 100644
--- a/ui/analyse/src/study/gamebook/gamebookPlayView.ts
+++ b/ui/analyse/src/study/gamebook/gamebookPlayView.ts
@@ -1,60 +1,38 @@
-import { h, VNode } from 'snabbdom';
+import { VNode } from 'snabbdom';
import GamebookPlayCtrl from './gamebookPlayCtrl';
import * as licon from 'common/licon';
-import { iconTag, bind, dataIcon } from 'common/snabbdom';
+import { iconTag, bind, dataIcon, looseH as h } from 'common/snabbdom';
import { richHTML } from 'common/richText';
// eslint-disable-next-line no-duplicate-imports
import { State } from './gamebookPlayCtrl';
export function render(ctrl: GamebookPlayCtrl): VNode {
const state = ctrl.state;
- return h(
- 'div.gamebook',
- {
- hook: { insert: _ => lichess.asset.loadCssPath('analyse.gamebook.play') },
- },
- [
- state.comment || state.feedback == 'play' || state.feedback == 'end'
- ? h(
- 'div.comment',
- {
- class: { hinted: state.showHint },
- },
- [
- state.comment
- ? h('div.content', { hook: richHTML(state.comment) })
- : h(
- 'div.content',
- state.feedback == 'play'
- ? ctrl.trans('whatWouldYouPlay')
- : state.feedback == 'end'
- ? ctrl.trans('youCompletedThisLesson')
- : undefined,
- ),
- hintZone(ctrl),
- ],
- )
- : undefined,
- h('div.floor', [
- renderFeedback(ctrl, state),
- h('img.mascot', {
- attrs: {
- width: 120,
- height: 120,
- src: lichess.asset.url('images/mascot/octopus.svg'),
- },
- }),
+ return h('div.gamebook', { hook: { insert: _ => lichess.asset.loadCssPath('analyse.gamebook.play') } }, [
+ (state.comment || state.feedback == 'play' || state.feedback == 'end') &&
+ h('div.comment', { class: { hinted: state.showHint } }, [
+ state.comment
+ ? h('div.content', { hook: richHTML(state.comment) })
+ : h(
+ 'div.content',
+ state.feedback == 'play'
+ ? ctrl.trans('whatWouldYouPlay')
+ : state.feedback == 'end' && ctrl.trans('youCompletedThisLesson'),
+ ),
+ hintZone(ctrl),
]),
- ],
- );
+ h('div.floor', [
+ renderFeedback(ctrl, state),
+ h('img.mascot', {
+ attrs: { width: 120, height: 120, src: lichess.asset.url('images/mascot/octopus.svg') },
+ }),
+ ]),
+ ]);
}
function hintZone(ctrl: GamebookPlayCtrl) {
const state = ctrl.state,
- buttonData = () => ({
- attrs: { type: 'button' },
- hook: bind('click', ctrl.hint, ctrl.redraw),
- });
+ 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'));
return undefined;
@@ -66,24 +44,14 @@ function renderFeedback(ctrl: GamebookPlayCtrl, state: State) {
if (fb === 'bad')
return h(
'button.feedback.act.bad' + (state.comment ? '.com' : ''),
- {
- attrs: { type: 'button' },
- hook: bind('click', ctrl.retry),
- },
+ { attrs: { type: 'button' }, hook: bind('click', ctrl.retry) },
[iconTag(licon.Reload), h('span', ctrl.trans.noarg('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('kbd', ''),
- ],
- );
+ 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('kbd', ''),
+ ]);
if (fb === 'end') return renderEnd(ctrl);
return h(
'div.feedback.info.' + fb + (state.init ? '.init' : ''),
@@ -108,26 +76,19 @@ function renderFeedback(ctrl: GamebookPlayCtrl, state: State) {
function renderEnd(ctrl: GamebookPlayCtrl) {
const study = ctrl.root.study!;
return h('div.feedback.end', [
- study.nextChapter()
- ? h(
- 'button.next.text',
- {
- attrs: {
- 'data-icon': licon.PlayTriangle,
- type: 'button',
- },
- hook: bind('click', study.goToNextChapter),
- },
- study.trans.noarg('nextChapter'),
- )
- : undefined,
+ study.nextChapter() &&
+ h(
+ 'button.next.text',
+ {
+ attrs: { 'data-icon': licon.PlayTriangle, type: 'button' },
+ hook: bind('click', study.goToNextChapter),
+ },
+ study.trans.noarg('nextChapter'),
+ ),
h(
'button.retry',
{
- attrs: {
- 'data-icon': licon.Reload,
- type: 'button',
- },
+ attrs: { 'data-icon': licon.Reload, type: 'button' },
hook: bind('click', () => ctrl.root.userJump(''), ctrl.redraw),
},
study.trans.noarg('playAgain'),
@@ -135,10 +96,7 @@ function renderEnd(ctrl: GamebookPlayCtrl) {
h(
'button.analyse',
{
- attrs: {
- 'data-icon': licon.Microscope,
- type: 'button',
- },
+ attrs: { 'data-icon': licon.Microscope, type: 'button' },
hook: bind('click', () => study.setGamebookOverride('analyse'), ctrl.redraw),
},
study.trans.noarg('analysis'),
diff --git a/ui/analyse/src/study/inviteForm.ts b/ui/analyse/src/study/inviteForm.ts
index a1bf6632879b5..91afe0436a373 100644
--- a/ui/analyse/src/study/inviteForm.ts
+++ b/ui/analyse/src/study/inviteForm.ts
@@ -75,10 +75,7 @@ export function view(ctrl: ReturnType): VNode {
h('div.input-wrapper', [
// because typeahead messes up with snabbdom
h('input', {
- attrs: {
- placeholder: ctrl.trans.noarg('searchByUsername'),
- spellcheck: 'false',
- },
+ attrs: { placeholder: ctrl.trans.noarg('searchByUsername'), spellcheck: 'false' },
hook: onInsert(input =>
lichess.asset
.userComplete({
@@ -100,12 +97,7 @@ export function view(ctrl: ReturnType): VNode {
candidates.map(function (username: string) {
return h(
'span.button.button-metal',
- {
- key: username,
- hook: bind('click', _ => {
- ctrl.invite(username);
- }),
- },
+ { key: username, hook: bind('click', () => ctrl.invite(username)) },
username,
);
}),
diff --git a/ui/analyse/src/study/multiBoard.ts b/ui/analyse/src/study/multiBoard.ts
index cfdd4c6a606a0..872c59a469d18 100644
--- a/ui/analyse/src/study/multiBoard.ts
+++ b/ui/analyse/src/study/multiBoard.ts
@@ -121,9 +121,7 @@ function renderPlayingToggle(ctrl: MultiBoardCtrl): VNode {
return h('label.playing', [
h('input', {
attrs: { type: 'checkbox', checked: ctrl.playing },
- hook: bind('change', e => {
- ctrl.setPlaying((e.target as HTMLInputElement).checked);
- }),
+ hook: bind('change', e => ctrl.setPlaying((e.target as HTMLInputElement).checked)),
}),
ctrl.trans.noarg('playing'),
]);
@@ -140,10 +138,7 @@ function renderPagerNav(pager: Paginator, ctrl: MultiBoardCtrl):
pagerButton(ctrl.trans.noarg('next'), licon.JumpNext, ctrl.nextPage, page < pager.nbPages, ctrl),
pagerButton(ctrl.trans.noarg('last'), licon.JumpLast, ctrl.lastPage, page < pager.nbPages, ctrl),
h('button.fbt', {
- attrs: {
- 'data-icon': licon.Search,
- title: 'Search',
- },
+ attrs: { 'data-icon': licon.Search, title: 'Search' },
hook: bind('click', () => lichess.pubsub.emit('study.search.open')),
}),
]);
@@ -157,11 +152,7 @@ function pagerButton(
ctrl: MultiBoardCtrl,
): VNode {
return h('button.fbt', {
- attrs: {
- 'data-icon': icon,
- disabled: !enable,
- title: text,
- },
+ attrs: { 'data-icon': icon, disabled: !enable, title: text },
hook: bind('mousedown', click, ctrl.redraw),
});
}
@@ -170,9 +161,7 @@ const makePreview = (study: StudyCtrl) => (preview: ChapterPreview) =>
h(
`a.mini-game.mini-game-${preview.id}.mini-game--init.is2d`,
{
- attrs: {
- 'data-state': `${preview.fen},${preview.orientation},${preview.lastMove}`,
- },
+ attrs: { 'data-state': `${preview.fen},${preview.orientation},${preview.lastMove}` },
class: {
active: !study.multiBoard.loading && study.vm.chapterId == preview.id && !study.relay?.tourShow(),
},
diff --git a/ui/analyse/src/study/nextChapter.ts b/ui/analyse/src/study/nextChapter.ts
index 6b9b4806db0b1..8979f85f042a2 100644
--- a/ui/analyse/src/study/nextChapter.ts
+++ b/ui/analyse/src/study/nextChapter.ts
@@ -9,14 +9,9 @@ export const renderNextChapter = (ctrl: AnalyseCtrl) =>
? h(
'button.next.text',
{
- attrs: {
- 'data-icon': licon.PlayTriangle,
- type: 'button',
- },
+ attrs: { 'data-icon': licon.PlayTriangle, type: 'button' },
hook: bind('click', ctrl.study.goToNextChapter),
- class: {
- highlighted: !!ctrl.outcome() || ctrl.node == treeOps.last(ctrl.mainline),
- },
+ class: { highlighted: !!ctrl.outcome() || ctrl.node == treeOps.last(ctrl.mainline) },
},
ctrl.trans.noarg('nextChapter'),
)
diff --git a/ui/analyse/src/study/playerBars.ts b/ui/analyse/src/study/playerBars.ts
index 6f68a29dacd47..19882713e7938 100644
--- a/ui/analyse/src/study/playerBars.ts
+++ b/ui/analyse/src/study/playerBars.ts
@@ -14,10 +14,7 @@ export default function (ctrl: AnalyseCtrl): VNode[] | undefined {
const study = ctrl.study;
if (!study) return;
const tags = study.data.chapter.tags,
- playerNames = {
- white: findTag(tags, 'white')!,
- black: findTag(tags, 'black')!,
- };
+ playerNames = { white: findTag(tags, 'white')!, black: findTag(tags, 'black')! };
const clocks = renderClocks(ctrl),
ticking = !isFinished(study.data.chapter) && ctrl.turnColor(),
@@ -50,22 +47,16 @@ function renderPlayer(
const title = findTag(tags, `${color}title`),
elo = hideRatings ? undefined : findTag(tags, `${color}elo`),
result = resultOf(tags, color === 'white');
- return h(
- `div.study__player.study__player-${top ? 'top' : 'bot'}`,
- {
- class: { ticking },
- },
- [
- h('div.left', [
- result && h('span.result', result),
- h('span.info', [
- title && h('span.utitle', title == 'BOT' ? { attrs: { 'data-bot': true } } : {}, title + ' '),
- h('span.name', playerNames[color]),
- elo && h('span.elo', elo),
- ]),
+ return h(`div.study__player.study__player-${top ? 'top' : 'bot'}`, { class: { ticking } }, [
+ h('div.left', [
+ result && h('span.result', result),
+ h('span.info', [
+ title && h('span.utitle', title == 'BOT' ? { attrs: { 'data-bot': true } } : {}, title + ' '),
+ h('span.name', playerNames[color]),
+ elo && h('span.elo', elo),
]),
- materialDiffs[top ? 0 : 1],
- clocks?.[color === 'white' ? 0 : 1],
- ],
- );
+ ]),
+ materialDiffs[top ? 0 : 1],
+ clocks?.[color === 'white' ? 0 : 1],
+ ]);
}
diff --git a/ui/analyse/src/study/practice/studyPracticeView.ts b/ui/analyse/src/study/practice/studyPracticeView.ts
index f0494706c2b9d..fa6e4e79fcbf2 100644
--- a/ui/analyse/src/study/practice/studyPracticeView.ts
+++ b/ui/analyse/src/study/practice/studyPracticeView.ts
@@ -12,25 +12,13 @@ import StudyCtrl from '../studyCtrl';
const selector = (data: StudyPracticeData) =>
h(
'select.selector',
- {
- hook: bind('change', e => {
- location.href = '/practice/' + (e.target as HTMLInputElement).value;
- }),
- },
+ { hook: bind('change', e => (location.href = '/practice/' + (e.target as HTMLInputElement).value)) },
[
- h(
- 'option',
- {
- attrs: { disabled: true, selected: true },
- },
- 'Practice list',
- ),
+ h('option', { attrs: { disabled: true, selected: true } }, 'Practice list'),
...data.structure.map(section =>
h(
'optgroup',
- {
- attrs: { label: section.name },
- },
+ { attrs: { label: section.name } },
section.studies.map(study =>
option(section.id + '/' + study.slug + '/' + study.id, '', study.name),
),
@@ -73,24 +61,17 @@ export function underboard(ctrl: StudyCtrl): MaybeVNodes {
h(
'a.feedback.win',
ctrl.nextChapter()
- ? {
- hook: bind('click', ctrl.goToNextChapter),
- }
- : {
- attrs: { href: '/practice' },
- },
+ ? { hook: bind('click', ctrl.goToNextChapter) }
+ : { attrs: { href: '/practice' } },
[h('span', 'Success!'), ctrl.nextChapter() ? 'Go to next exercise' : 'Back to practice menu'],
),
];
case false:
return [
- h(
- 'a.feedback.fail',
- {
- hook: bind('click', p.reset, ctrl.redraw),
- },
- [h('span', [renderGoal(p, p.goal().moves!)]), h('strong', 'Click to retry')],
- ),
+ h('a.feedback.fail', { hook: bind('click', p.reset, ctrl.redraw) }, [
+ h('span', [renderGoal(p, p.goal().moves!)]),
+ h('strong', 'Click to retry'),
+ ]),
];
default:
return [
@@ -143,10 +124,7 @@ export function side(ctrl: StudyCtrl): VNode {
'a.ps__chapter',
{
key: chapter.id,
- attrs: {
- href: data.url + '/' + chapter.id,
- 'data-id': chapter.id,
- },
+ attrs: { href: data.url + '/' + chapter.id, 'data-id': chapter.id },
class: { active, loading },
},
[
@@ -164,13 +142,7 @@ export function side(ctrl: StudyCtrl): VNode {
.reduce((a, b) => a.concat(b), []),
),
h('div.finally', [
- h('a.back', {
- attrs: {
- 'data-icon': licon.LessThan,
- href: '/practice',
- title: 'More practice',
- },
- }),
+ h('a.back', { attrs: { 'data-icon': licon.LessThan, href: '/practice', title: 'More practice' } }),
thunk('select.selector', selector, [data]),
]),
]);
diff --git a/ui/analyse/src/study/relay/relayCtrl.ts b/ui/analyse/src/study/relay/relayCtrl.ts
index 0e8725ba1adf2..8a647864fce0c 100644
--- a/ui/analyse/src/study/relay/relayCtrl.ts
+++ b/ui/analyse/src/study/relay/relayCtrl.ts
@@ -81,7 +81,7 @@ export default class RelayCtrl {
}, 4500);
this.redraw();
if (event.error) {
- lichess.sound.play('error');
+ if (this.data.sync.log.slice(-2).every(e => e.error)) lichess.sound.play('error');
console.warn(`relay synchronisation error: ${event.error}`);
}
},
diff --git a/ui/analyse/src/study/relay/relayManagerView.ts b/ui/analyse/src/study/relay/relayManagerView.ts
index 930dfeff69760..9a3901b0e99d9 100644
--- a/ui/analyse/src/study/relay/relayManagerView.ts
+++ b/ui/analyse/src/study/relay/relayManagerView.ts
@@ -7,27 +7,16 @@ import { memoize } from 'common';
export default function (ctrl: RelayCtrl): VNode | undefined {
return ctrl.members.canContribute()
- ? h(
- 'div.relay-admin',
- {
- hook: onInsert(_ => lichess.asset.loadCssPath('analyse.relay-admin')),
- },
- [
- h('h2', [
- h('span.text', { attrs: dataIcon(licon.RadioTower) }, 'Broadcast manager'),
- h('a', {
- attrs: {
- href: `/broadcast/round/${ctrl.id}/edit`,
- 'data-icon': licon.Gear,
- },
- }),
- ]),
- ctrl.data.sync?.url || ctrl.data.sync?.ids
- ? (ctrl.data.sync.ongoing ? stateOn : stateOff)(ctrl)
- : null,
- renderLog(ctrl),
- ],
- )
+ ? h('div.relay-admin', { hook: onInsert(_ => lichess.asset.loadCssPath('analyse.relay-admin')) }, [
+ h('h2', [
+ h('span.text', { attrs: dataIcon(licon.RadioTower) }, 'Broadcast manager'),
+ h('a', { attrs: { href: `/broadcast/round/${ctrl.id}/edit`, 'data-icon': licon.Gear } }),
+ ]),
+ ctrl.data.sync?.url || ctrl.data.sync?.ids
+ ? (ctrl.data.sync.ongoing ? stateOn : stateOff)(ctrl)
+ : null,
+ renderLog(ctrl),
+ ])
: undefined;
}
@@ -44,25 +33,10 @@ function renderLog(ctrl: RelayCtrl) {
.map(e => {
const err =
e.error &&
- h(
- 'a',
- url
- ? {
- attrs: {
- href: url,
- target: '_blank',
- rel: 'noopener nofollow',
- },
- }
- : {},
- e.error,
- );
+ h('a', url ? { attrs: { href: url, target: '_blank', rel: 'noopener nofollow' } } : {}, e.error);
return h(
'div' + (err ? '.err' : ''),
- {
- key: e.at,
- attrs: dataIcon(err ? licon.CautionCircle : licon.Checkmark),
- },
+ { key: e.at, attrs: dataIcon(err ? licon.CautionCircle : licon.Checkmark) },
[h('div', [...(err ? [err] : logSuccess(e)), h('time', dateFormatter()(new Date(e.at)))])],
);
});
@@ -76,10 +50,7 @@ function stateOn(ctrl: RelayCtrl) {
ids = sync?.ids;
return h(
'div.state.on.clickable',
- {
- hook: bind('click', _ => ctrl.setSync(false)),
- attrs: dataIcon(licon.ChasingArrows),
- },
+ { hook: bind('click', _ => ctrl.setSync(false)), attrs: dataIcon(licon.ChasingArrows) },
[
h(
'div',
@@ -100,10 +71,7 @@ function stateOn(ctrl: RelayCtrl) {
const stateOff = (ctrl: RelayCtrl) =>
h(
'div.state.off.clickable',
- {
- hook: bind('click', _ => ctrl.setSync(true)),
- attrs: dataIcon(licon.PlayTriangle),
- },
+ { hook: bind('click', _ => ctrl.setSync(true)), attrs: dataIcon(licon.PlayTriangle) },
[h('div.fat', 'Click to connect')],
);
diff --git a/ui/analyse/src/study/relay/relayTourView.ts b/ui/analyse/src/study/relay/relayTourView.ts
index 6cdfedebb337b..b4a71e011e4f5 100644
--- a/ui/analyse/src/study/relay/relayTourView.ts
+++ b/ui/analyse/src/study/relay/relayTourView.ts
@@ -1,8 +1,8 @@
import AnalyseCtrl from '../../ctrl';
import RelayCtrl from './relayCtrl';
import * as licon from 'common/licon';
-import { bind, dataIcon, onInsert } from 'common/snabbdom';
-import { h, VNode } from 'snabbdom';
+import { bind, dataIcon, onInsert, looseH as h } from 'common/snabbdom';
+import { VNode } from 'snabbdom';
import { innerHTML } from 'common/richText';
import { RelayRound } from './interfaces';
import { RelayTab } from '../interfaces';
@@ -92,24 +92,19 @@ const overview = (relay: RelayCtrl, study: StudyCtrl) => {
' ',
round.ongoing
? study.trans.noarg('playingRightNow')
- : round.startsAt
- ? h(
+ : round.startsAt &&
+ h(
'time.timeago',
- {
- hook: onInsert(el => el.setAttribute('datetime', '' + round.startsAt)),
- },
+ { hook: onInsert(el => el.setAttribute('datetime', '' + round.startsAt)) },
lichess.timeago(round.startsAt),
- )
- : null,
+ ),
],
),
relay.data.tour.markup
- ? h('div', {
- hook: innerHTML(relay.data.tour.markup, () => relay.data.tour.markup!),
- })
+ ? h('div', { hook: innerHTML(relay.data.tour.markup, () => relay.data.tour.markup!) })
: h('div', relay.data.tour.description),
]),
- study.looksNew() ? null : multiBoardView(study.multiBoard, study),
+ !study.looksNew() && multiBoardView(study.multiBoard, study),
];
};
@@ -124,16 +119,7 @@ const schedule = (relay: RelayCtrl): VNode[] => [
'tbody',
relay.data.rounds.map(round =>
h('tr', [
- h(
- 'th',
- h(
- 'a.link',
- {
- attrs: { href: relay.roundPath(round) },
- },
- round.name,
- ),
- ),
+ h('th', h('a.link', { attrs: { href: relay.roundPath(round) } }, round.name)),
h('td', round.startsAt ? lichess.dateFormat()(new Date(round.startsAt)) : undefined),
h(
'td',
@@ -150,45 +136,22 @@ const schedule = (relay: RelayCtrl): VNode[] => [
const roundStateIcon = (round: RelayRound) =>
round.ongoing
? h('ongoing', { attrs: { ...dataIcon(licon.DiscBig), title: 'Ongoing' } })
- : round.finished
- ? h('finished', { attrs: { ...dataIcon(licon.Checkmark), title: 'Finished' } })
- : null;
+ : round.finished && h('finished', { attrs: { ...dataIcon(licon.Checkmark), title: 'Finished' } });
export function rounds(ctrl: StudyCtrl): VNode {
const canContribute = ctrl.members.canContribute();
const relay = ctrl.relay!;
return h(
'div.study__relay__rounds',
- {
- hook: onInsert(el => scrollToInnerSelector(el, '.active')),
- },
+ { hook: onInsert(el => scrollToInnerSelector(el, '.active')) },
relay.data.rounds
.map(round =>
- h(
- 'div',
- {
- key: round.id,
- class: { active: ctrl.data.id == round.id },
- },
- [
- h(
- 'a.link',
- {
- attrs: { href: relay.roundPath(round) },
- },
- round.name,
- ),
- roundStateIcon(round),
- canContribute
- ? h('a.act', {
- attrs: {
- ...dataIcon(licon.Gear),
- href: `/broadcast/round/${round.id}/edit`,
- },
- })
- : null,
- ],
- ),
+ h('div', { key: round.id, class: { active: ctrl.data.id == round.id } }, [
+ h('a.link', { attrs: { href: relay.roundPath(round) } }, round.name),
+ roundStateIcon(round),
+ canContribute &&
+ h('a.act', { attrs: { ...dataIcon(licon.Gear), href: `/broadcast/round/${round.id}/edit` } }),
+ ]),
)
.concat(
canContribute
@@ -198,10 +161,7 @@ export function rounds(ctrl: StudyCtrl): VNode {
h(
'a.text',
{
- attrs: {
- href: `/broadcast/${relay.data.tour.id}/new`,
- 'data-icon': licon.PlusButton,
- },
+ attrs: { href: `/broadcast/${relay.data.tour.id}/new`, 'data-icon': licon.PlusButton },
},
ctrl.trans.noarg('addRound'),
),
diff --git a/ui/analyse/src/study/serverEval.ts b/ui/analyse/src/study/serverEval.ts
index bd1e207debbb9..6f1c896cff7ae 100644
--- a/ui/analyse/src/study/serverEval.ts
+++ b/ui/analyse/src/study/serverEval.ts
@@ -67,10 +67,7 @@ function requestButton(ctrl: ServerEval) {
h(
'a.button.text',
{
- attrs: {
- 'data-icon': licon.BarChart,
- disabled: root.mainline.length < 5,
- },
+ attrs: { 'data-icon': licon.BarChart, disabled: root.mainline.length < 5 },
hook: bind('click', ctrl.request, root.redraw),
},
noarg('requestAComputerAnalysis'),
diff --git a/ui/analyse/src/study/studyChapters.ts b/ui/analyse/src/study/studyChapters.ts
index e78f02e522db5..e3750760c49c3 100644
--- a/ui/analyse/src/study/studyChapters.ts
+++ b/ui/analyse/src/study/studyChapters.ts
@@ -1,7 +1,7 @@
import { prop, Prop, scrollToInnerSelector } from 'common';
import * as licon from 'common/licon';
-import { bind, dataIcon, iconTag } from 'common/snabbdom';
-import { h, VNode } from 'snabbdom';
+import { bind, dataIcon, iconTag, looseH as h } from 'common/snabbdom';
+import { VNode } from 'snabbdom';
import AnalyseCtrl from '../ctrl';
import { StudySocketSend } from '../socket';
import { StudyChapterEditForm } from './chapterEditForm';
@@ -139,24 +139,19 @@ export function view(ctrl: StudyCtrl): VNode {
[
h('span', loading ? h('span.ddloader') : ['' + (i + 1)]),
h('h3', chapter.name),
- chapter.ongoing
- ? h('ongoing', { attrs: { ...dataIcon(licon.DiscBig), title: 'Ongoing' } })
- : null,
- !chapter.ongoing && chapter.res ? h('res', chapter.res) : null,
- canContribute ? h('i.act', { attrs: { ...dataIcon(licon.Gear), title: 'Edit chapter' } }) : null,
+ chapter.ongoing && h('ongoing', { attrs: { ...dataIcon(licon.DiscBig), title: 'Ongoing' } }),
+ !chapter.ongoing && chapter.res && h('res', chapter.res),
+ canContribute && h('i.act', { attrs: { ...dataIcon(licon.Gear), title: 'Edit chapter' } }),
],
);
})
.concat(
ctrl.members.canContribute()
? [
- h(
- 'div.add',
- {
- hook: bind('click', ctrl.chapters.toggleNewForm, ctrl.redraw),
- },
- [h('span', iconTag(licon.PlusButton)), h('h3', ctrl.trans.noarg('addNewChapter'))],
- ),
+ h('div.add', { hook: bind('click', ctrl.chapters.toggleNewForm, ctrl.redraw) }, [
+ h('span', iconTag(licon.PlusButton)),
+ h('h3', ctrl.trans.noarg('addNewChapter')),
+ ]),
]
: [],
),
diff --git a/ui/analyse/src/study/studyComments.ts b/ui/analyse/src/study/studyComments.ts
index b08ca03d1dadb..d0c37481d1866 100644
--- a/ui/analyse/src/study/studyComments.ts
+++ b/ui/analyse/src/study/studyComments.ts
@@ -15,13 +15,7 @@ export type Author = AuthorObj | string;
function authorDom(author: Author): string | VNode {
if (!author) return 'Unknown';
if (typeof author === 'string') return author;
- return h(
- 'span.user-link.ulpt',
- {
- attrs: { 'data-href': '/@/' + author.id },
- },
- author.name,
- );
+ return h('span.user-link.ulpt', { attrs: { 'data-href': '/@/' + author.id } }, author.name);
}
export const isAuthorObj = (author: Author): author is AuthorObj => typeof author === 'object';
@@ -45,13 +39,10 @@ export function currentComments(ctrl: AnalyseCtrl, includingMine: boolean): VNod
return h('div.study__comment.' + comment.id, [
study.members.canContribute() && study.vm.mode.write
? h('a.edit', {
- attrs: {
- 'data-icon': licon.Trash,
- title: 'Delete',
- },
+ attrs: { 'data-icon': licon.Trash, title: 'Delete' },
hook: bind(
'click',
- _ => {
+ () => {
if (confirm('Delete ' + authorText(by) + "'s comment?"))
study.commentForm.delete(chapter.id, ctrl.path, comment.id);
},
diff --git a/ui/analyse/src/study/studyForm.ts b/ui/analyse/src/study/studyForm.ts
index 47ff9bb898544..a8159b00f0c14 100644
--- a/ui/analyse/src/study/studyForm.ts
+++ b/ui/analyse/src/study/studyForm.ts
@@ -1,8 +1,8 @@
-import { h, VNode } from 'snabbdom';
+import { VNode } from 'snabbdom';
import * as licon from 'common/licon';
import { snabDialog } from 'common/dialog';
import { prop } from 'common';
-import { bindSubmit, bindNonPassive } from 'common/snabbdom';
+import { bindSubmit, bindNonPassive, looseH as h } from 'common/snabbdom';
import { emptyRedButton } from '../view/util';
import { StudyData } from './interfaces';
import { Redraw } from '../interfaces';
@@ -56,27 +56,10 @@ export class StudyForm {
const select = (s: Select): VNode =>
h('div.form-group.form-half', [
- h(
- 'label.form-label',
- {
- attrs: { for: 'study-' + s.key },
- },
- s.name,
- ),
+ h('label.form-label', { attrs: { for: 'study-' + s.key } }, s.name),
h(
`select#study-${s.key}.form-control`,
- s.choices.map(function (o) {
- return h(
- 'option',
- {
- attrs: {
- value: o[0],
- selected: s.selected === o[0],
- },
- },
- o[1],
- );
- }),
+ s.choices.map(o => h('option', { attrs: { value: o[0], selected: s.selected === o[0] } }, o[1])),
),
]);
@@ -135,10 +118,7 @@ export function view(ctrl: StudyForm): VNode {
h('div.form-group' + (ctrl.relay ? '.none' : ''), [
h('label.form-label', { attrs: { for: 'study-name' } }, ctrl.trans.noarg('name')),
h('input#study-name.form-control', {
- attrs: {
- minlength: 3,
- maxlength: 100,
- },
+ attrs: { minlength: 3, maxlength: 100 },
hook: {
insert: vnode => updateName(vnode, false),
postpatch: (_, vnode) => updateName(vnode, true),
@@ -211,36 +191,30 @@ export function view(ctrl: StudyForm): VNode {
selected: '' + data.settings.description,
}),
]),
- ctrl.relay
- ? h('div.form-actions-secondary', [
- h(
- 'a.text',
- {
- attrs: {
- 'data-icon': licon.RadioTower,
- href: `/broadcast/${ctrl.relay.data.tour.id}/edit`,
- },
- },
- 'Tournament settings',
- ),
- h(
- 'a.text',
- {
- attrs: { 'data-icon': licon.RadioTower, href: `/broadcast/round/${data.id}/edit` },
+ ctrl.relay &&
+ h('div.form-actions-secondary', [
+ h(
+ 'a.text',
+ {
+ attrs: {
+ 'data-icon': licon.RadioTower,
+ href: `/broadcast/${ctrl.relay.data.tour.id}/edit`,
},
- 'Round settings',
- ),
- ])
- : null,
+ },
+ 'Tournament settings',
+ ),
+ h(
+ 'a.text',
+ { attrs: { 'data-icon': licon.RadioTower, href: `/broadcast/round/${data.id}/edit` } },
+ 'Round settings',
+ ),
+ ]),
h('div.form-actions', [
h('div', { attrs: { style: 'display: flex' } }, [
h(
'form',
{
- attrs: {
- action: '/study/' + data.id + '/delete',
- method: 'post',
- },
+ attrs: { action: '/study/' + data.id + '/delete', method: 'post' },
hook: bindNonPassive(
'submit',
_ =>
@@ -250,29 +224,19 @@ export function view(ctrl: StudyForm): VNode {
},
[h(emptyRedButton, ctrl.trans.noarg(isNew ? 'cancel' : 'deleteStudy'))],
),
- isNew
- ? null
- : h(
- 'form',
- {
- attrs: {
- action: '/study/' + data.id + '/clear-chat',
- method: 'post',
- },
- hook: bindNonPassive('submit', _ =>
- confirm(ctrl.trans.noarg('deleteTheStudyChatHistory')),
- ),
- },
- [h(emptyRedButton, ctrl.trans.noarg('clearChat'))],
- ),
+ !isNew &&
+ h(
+ 'form',
+ {
+ attrs: { action: '/study/' + data.id + '/clear-chat', method: 'post' },
+ hook: bindNonPassive('submit', _ =>
+ confirm(ctrl.trans.noarg('deleteTheStudyChatHistory')),
+ ),
+ },
+ [h(emptyRedButton, ctrl.trans.noarg('clearChat'))],
+ ),
]),
- h(
- 'button.button',
- {
- attrs: { type: 'submit' },
- },
- ctrl.trans.noarg(isNew ? 'start' : 'save'),
- ),
+ h('button.button', { attrs: { type: 'submit' } }, ctrl.trans.noarg(isNew ? 'start' : 'save')),
]),
],
),
diff --git a/ui/analyse/src/study/studyGlyph.ts b/ui/analyse/src/study/studyGlyph.ts
index bf262de92eea0..9e43df056a704 100644
--- a/ui/analyse/src/study/studyGlyph.ts
+++ b/ui/analyse/src/study/studyGlyph.ts
@@ -18,9 +18,7 @@ const renderGlyph = (ctrl: GlyphForm, node: Tree.Node) => (glyph: Tree.Glyph) =>
{
hook: bind('click', () => ctrl.toggleGlyph(glyph.id)),
attrs: { 'data-symbol': glyph.symbol, type: 'button' },
- class: {
- active: !!node.glyphs && !!node.glyphs.find(g => g.id === glyph.id),
- },
+ class: { active: !!node.glyphs && !!node.glyphs.find(g => g.id === glyph.id) },
},
[glyph.name],
);
@@ -52,9 +50,7 @@ export function view(ctrl: GlyphForm): VNode {
return h(
'div.study__glyphs' + (all ? '' : '.empty'),
- {
- hook: { insert: ctrl.loadGlyphs },
- },
+ { hook: { insert: ctrl.loadGlyphs } },
all
? [
h('div.move', all.move.map(renderGlyph(ctrl, node))),
diff --git a/ui/analyse/src/study/studyMembers.ts b/ui/analyse/src/study/studyMembers.ts
index b3590b266c4d8..40ac597bff36e 100644
--- a/ui/analyse/src/study/studyMembers.ts
+++ b/ui/analyse/src/study/studyMembers.ts
@@ -1,7 +1,7 @@
import { AnalyseSocketSend } from '../socket';
-import { h, VNode } from 'snabbdom';
+import { VNode } from 'snabbdom';
import * as licon from 'common/licon';
-import { iconTag, bind, onInsert, dataIcon, bindNonPassive } from 'common/snabbdom';
+import { iconTag, bind, onInsert, dataIcon, bindNonPassive, looseH as h } from 'common/snabbdom';
import { makeCtrl as inviteFormCtrl, StudyInviteFormCtrl } from './inviteForm';
import { NotifCtrl } from './notif';
import { prop, Prop, scrollTo } from 'common';
@@ -163,16 +163,13 @@ export function view(ctrl: StudyCtrl): VNode {
attrs: dataIcon(licon.Gear),
hook: bind(
'click',
- _ => members.confing(members.confing() == member.user.id ? null : member.user.id),
+ () => members.confing(members.confing() == member.user.id ? null : member.user.id),
ctrl.redraw,
),
});
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: ctrl.trans.noarg('leaveTheStudy') },
hook: bind('click', members.leave, ctrl.redraw),
});
return undefined;
@@ -190,16 +187,10 @@ export function view(ctrl: StudyCtrl): VNode {
h('div.role', [
h('div.switch', [
h('input.cmn-toggle', {
- attrs: {
- id: roleId,
- type: 'checkbox',
- checked: member.role === 'w',
- },
+ attrs: { id: roleId, type: 'checkbox', checked: member.role === 'w' },
hook: bind(
'change',
- e => {
- members.setRole(member.user.id, (e.target as HTMLInputElement).checked ? 'w' : 'r');
- },
+ e => members.setRole(member.user.id, (e.target as HTMLInputElement).checked ? 'w' : 'r'),
ctrl.redraw,
),
}),
@@ -211,10 +202,7 @@ export function view(ctrl: StudyCtrl): VNode {
'div.kick',
h(
'a.button.button-red.button-empty.text',
- {
- attrs: dataIcon(licon.X),
- hook: bind('click', _ => members.kick(member.user.id), ctrl.redraw),
- },
+ { attrs: dataIcon(licon.X), hook: bind('click', _ => members.kick(member.user.id), ctrl.redraw) },
ctrl.trans.noarg('kick'),
),
),
@@ -224,59 +212,39 @@ export function view(ctrl: StudyCtrl): VNode {
const ordered: StudyMember[] = members.ordered();
- return h(
- 'div.study__members',
- {
- hook: onInsert(() => lichess.pubsub.emit('chat.resize')),
- },
- [
- ...ordered
- .map(member => {
- const confing = members.confing() === member.user.id;
- return [
- h(
- 'div',
- {
- key: member.user.id,
- class: { editing: !!confing },
- },
- [
- h('div.left', [statusIcon(member), userLink({ ...member.user, line: false })]),
- configButton(ctrl, member),
- ],
- ),
- confing ? memberConfig(member) : null,
- ];
- })
- .reduce((a, b) => a.concat(b), []),
- isOwner && ordered.length < members.max
- ? h(
- 'div.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')),
- ]),
- ],
- )
- : null,
- !members.canContribute() && ctrl.data.admin
- ? h(
- 'form.admin',
- {
- key: ':admin',
- hook: bindNonPassive('submit', () => {
- xhrTextRaw(`/study/${ctrl.data.id}/admin`, { method: 'post' }).then(() => location.reload());
- return false;
- }),
- },
- [h('button.button.button-red.button-thin', 'Enter as admin')],
- )
- : null,
- ],
- );
+ return h('div.study__members', { hook: onInsert(() => lichess.pubsub.emit('chat.resize')) }, [
+ ...ordered
+ .map(member => {
+ const confing = members.confing() === member.user.id;
+ return [
+ h('div', { key: member.user.id, class: { editing: !!confing } }, [
+ h('div.left', [statusIcon(member), userLink({ ...member.user, line: false })]),
+ configButton(ctrl, member),
+ ]),
+ confing && memberConfig(member),
+ ];
+ })
+ .reduce((a, b) => a.concat(b), []),
+ isOwner &&
+ ordered.length < members.max &&
+ h('div.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')),
+ ]),
+ ]),
+ !members.canContribute() &&
+ ctrl.data.admin &&
+ h(
+ 'form.admin',
+ {
+ key: ':admin',
+ hook: bindNonPassive('submit', () => {
+ xhrTextRaw(`/study/${ctrl.data.id}/admin`, { method: 'post' }).then(() => location.reload());
+ return false;
+ }),
+ },
+ [h('button.button.button-red.button-thin', 'Enter as admin')],
+ ),
+ ]);
}
diff --git a/ui/analyse/src/study/studySearch.ts b/ui/analyse/src/study/studySearch.ts
index 95cfe032065bc..d70ef96ec7e21 100644
--- a/ui/analyse/src/study/studySearch.ts
+++ b/ui/analyse/src/study/studySearch.ts
@@ -68,30 +68,24 @@ export function view(ctrl: SearchCtrl) {
// dynamic extra class necessary to fully redraw the results and produce innerHTML
`div.study-search__results.search-query-${cleanQuery}`,
ctrl.results().map(c =>
- h(
- 'div',
- {
- hook: bind('click', () => ctrl.setChapter(c.id)),
- },
- [
- h(
- 'h3',
- {
- hook: highlightRegex
- ? {
- insert(vnode: VNode) {
- const el = vnode.elm as HTMLElement;
- el.innerHTML = c.name.replace(highlightRegex, '$&');
- },
- }
- : {},
- },
- c.name,
- ),
- c.ongoing ? h('ongoing', { attrs: { ...dataIcon(licon.DiscBig), title: 'Ongoing' } }) : null,
- !c.ongoing && c.res ? h('res', c.res) : null,
- ],
- ),
+ h('div', { hook: bind('click', () => ctrl.setChapter(c.id)) }, [
+ h(
+ 'h3',
+ {
+ hook: highlightRegex
+ ? {
+ insert(vnode: VNode) {
+ const el = vnode.elm as HTMLElement;
+ el.innerHTML = c.name.replace(highlightRegex, '$&');
+ },
+ }
+ : {},
+ },
+ c.name,
+ ),
+ c.ongoing ? h('ongoing', { attrs: { ...dataIcon(licon.DiscBig), title: 'Ongoing' } }) : null,
+ !c.ongoing && c.res ? h('res', c.res) : null,
+ ]),
),
),
],
diff --git a/ui/analyse/src/study/studyShare.ts b/ui/analyse/src/study/studyShare.ts
index b4f4ecb36623a..31b9ff75ab0a8 100644
--- a/ui/analyse/src/study/studyShare.ts
+++ b/ui/analyse/src/study/studyShare.ts
@@ -1,34 +1,27 @@
import { prop } from 'common';
import * as licon from 'common/licon';
-import { bind, dataIcon } from 'common/snabbdom';
+import { bind, dataIcon, looseH as h } from 'common/snabbdom';
import { text as xhrText, url as xhrUrl } from 'common/xhr';
-import { h, VNode } from 'snabbdom';
+import { VNode } from 'snabbdom';
import { renderIndexAndMove } from '../view/moveView';
import { baseUrl } from '../view/util';
import { StudyChapterMeta, StudyData } from './interfaces';
import RelayCtrl from './relay/relayCtrl';
function fromPly(ctrl: StudyShare): VNode {
- const renderedMove = renderIndexAndMove(
- {
- withDots: true,
- showEval: false,
- },
- ctrl.currentNode(),
- );
+ const renderedMove = renderIndexAndMove({ withDots: true, showEval: false }, ctrl.currentNode());
return h(
'div.ply-wrap',
- ctrl.onMainline()
- ? h('label.ply', [
- h('input', {
- attrs: { type: 'checkbox', checked: ctrl.withPly() },
- 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')]),
- ])
- : null,
+ ctrl.onMainline() &&
+ h('label.ply', [
+ h('input', {
+ attrs: { type: 'checkbox', checked: ctrl.withPly() },
+ 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')]),
+ ]),
);
}
@@ -71,12 +64,7 @@ async function writePgnClipboard(url: string): Promise {
}
const copyButton = (rel: string) =>
- h('button.button.copy', {
- attrs: {
- 'data-rel': rel,
- ...dataIcon(licon.Clipboard),
- },
- });
+ h('button.button.copy', { attrs: { 'data-rel': rel, ...dataIcon(licon.Clipboard) } });
export function view(ctrl: StudyShare): VNode {
const studyId = ctrl.studyId,
@@ -95,18 +83,12 @@ export function view(ctrl: StudyShare): VNode {
ctrl.shareable()
? [
h('div.downloads', [
- ctrl.cloneable()
- ? h(
- 'a.button.text',
- {
- attrs: {
- ...dataIcon(licon.StudyBoard),
- href: `/study/${studyId}/clone`,
- },
- },
- ctrl.trans.noarg('cloneStudy'),
- )
- : null,
+ ctrl.cloneable() &&
+ h(
+ 'a.button.text',
+ { attrs: { ...dataIcon(licon.StudyBoard), href: `/study/${studyId}/clone` } },
+ ctrl.trans.noarg('cloneStudy'),
+ ),
ctrl.relay &&
h(
'a.button.text',
@@ -220,14 +202,12 @@ export function view(ctrl: StudyShare): VNode {
h('label.form-label', ctrl.trans.noarg(i18n)),
h('div.form-control-with-clipboard', [
h(`input#study-share-${i18n}.form-control.copyable.autoselect`, {
- attrs: {
- readonly: true,
- value: `${baseUrl()}${path}`,
- },
+ attrs: { readonly: true, value: `${baseUrl()}${path}` },
}),
copyButton(`study-share-${i18n}`),
]),
- ...(pastable ? [fromPly(ctrl), !isPrivate ? youCanPasteThis() : null] : []),
+ pastable && fromPly(ctrl),
+ pastable && isPrivate && youCanPasteThis(),
]),
),
h(
@@ -274,10 +254,7 @@ export function view(ctrl: StudyShare): VNode {
h('label.form-label', 'FEN'),
h('div.form-control-with-clipboard', [
h('input#study-share-fen.form-control.copyable.autoselect', {
- attrs: {
- readonly: true,
- value: ctrl.currentNode().fen,
- },
+ attrs: { readonly: true, value: ctrl.currentNode().fen },
}),
copyButton(`study-share-fen`),
]),
diff --git a/ui/analyse/src/study/studyTags.ts b/ui/analyse/src/study/studyTags.ts
index 6e1eca1111e73..8184bac6e099e 100644
--- a/ui/analyse/src/study/studyTags.ts
+++ b/ui/analyse/src/study/studyTags.ts
@@ -42,10 +42,7 @@ const doRender = (root: StudyCtrl): VNode =>
const editable = (value: string, submit: (v: string, el: HTMLInputElement) => void): VNode =>
h('input', {
key: value, // force to redraw on change, to visibly update the input value
- attrs: {
- spellcheck: 'false',
- value,
- },
+ attrs: { spellcheck: 'false', value },
hook: onInsert(el => {
el.onblur = () => submit(el.value, el);
el.onkeydown = e => {
@@ -90,17 +87,12 @@ function renderPgnTags(tags: TagsForm, trans: Trans, hideRatings?: boolean): VNo
});
});
},
- postpatch: (_, vnode) => {
- tags.selectedType((vnode.elm as HTMLInputElement).value);
- },
+ postpatch: (_, vnode) => tags.selectedType((vnode.elm as HTMLInputElement).value),
},
},
[
h('option', trans.noarg('newTag')),
- ...tags.types.map(t => {
- if (!existingTypes.includes(t)) return option(t, '', t);
- return undefined;
- }),
+ ...tags.types.map(t => (!existingTypes.includes(t) ? option(t, '', t) : undefined)),
],
),
editable('', (value, el) => {
diff --git a/ui/analyse/src/study/studyView.ts b/ui/analyse/src/study/studyView.ts
index 04b839053b911..0b6487816fa0c 100644
--- a/ui/analyse/src/study/studyView.ts
+++ b/ui/analyse/src/study/studyView.ts
@@ -2,9 +2,9 @@ import * as commentForm from './commentForm';
import * as glyphForm from './studyGlyph';
import * as practiceView from './practice/studyPracticeView';
import AnalyseCtrl from '../ctrl';
-import { h, VNode } from 'snabbdom';
+import { VNode } from 'snabbdom';
import * as licon from 'common/licon';
-import { iconTag, bind, dataIcon, MaybeVNodes } from 'common/snabbdom';
+import { iconTag, bind, dataIcon, MaybeVNodes, looseH as h } from 'common/snabbdom';
import { playButtons as gbPlayButtons, overrideButton as gbOverrideButton } from './gamebook/gamebookButtons';
import { rounds as relayTourRounds } from './relay/relayTourView';
import { Tab, ToolTab } from './interfaces';
@@ -48,7 +48,7 @@ function toolButton(opts: ToolButtonOpts): VNode {
opts.ctrl.redraw,
),
},
- [opts.count ? h('count.data-count', { attrs: { 'data-count': opts.count } }) : null, opts.icon],
+ [opts.count && h('count.data-count', { attrs: { 'data-count': opts.count } }), opts.icon],
);
}
@@ -60,34 +60,27 @@ function buttons(root: AnalyseCtrl): VNode {
return h('div.study__buttons', [
h('div.left-buttons.tabs-horiz', { attrs: { role: 'tablist' } }, [
// distinct classes (sync, write) allow snabbdom to differentiate buttons
- showSticky
- ? h(
- 'a.mode.sync',
- {
- attrs: { title: noarg('allSyncMembersRemainOnTheSamePosition') },
- class: { on: ctrl.vm.mode.sticky },
- hook: bind('click', ctrl.toggleSticky),
- },
- [ctrl.vm.behind ? h('span.behind', '' + ctrl.vm.behind) : h('i.is'), 'SYNC'],
- )
- : null,
- ctrl.members.canContribute()
- ? h(
- 'a.mode.write',
- {
- attrs: { title: noarg('shareChanges') },
- class: { on: ctrl.vm.mode.write },
- hook: bind('click', ctrl.toggleWrite),
- },
- [h('i.is'), 'REC'],
- )
- : null,
- toolButton({
- ctrl,
- tab: 'tags',
- hint: noarg('pgnTags'),
- icon: iconTag(licon.Tag),
- }),
+ showSticky &&
+ h(
+ 'a.mode.sync',
+ {
+ attrs: { title: noarg('allSyncMembersRemainOnTheSamePosition') },
+ class: { on: ctrl.vm.mode.sticky },
+ hook: bind('click', ctrl.toggleSticky),
+ },
+ [ctrl.vm.behind ? h('span.behind', '' + ctrl.vm.behind) : h('i.is'), 'SYNC'],
+ ),
+ ctrl.members.canContribute() &&
+ h(
+ 'a.mode.write',
+ {
+ attrs: { title: noarg('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: 'comments',
@@ -98,15 +91,14 @@ function buttons(root: AnalyseCtrl): VNode {
},
count: (root.node.comments || []).length,
}),
- canContribute
- ? toolButton({
- ctrl,
- tab: 'glyphs',
- hint: noarg('annotateWithGlyphs'),
- icon: h('i.glyph-icon'),
- count: (root.node.glyphs || []).length,
- })
- : null,
+ canContribute &&
+ toolButton({
+ ctrl,
+ tab: 'glyphs',
+ hint: noarg('annotateWithGlyphs'),
+ icon: h('i.glyph-icon'),
+ count: (root.node.glyphs || []).length,
+ }),
toolButton({
ctrl,
tab: 'serverEval',
@@ -114,24 +106,14 @@ function buttons(root: AnalyseCtrl): VNode {
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),
- }),
- !ctrl.relay && !ctrl.data.chapter.gamebook
- ? h('span.help', {
- attrs: { title: 'Need help? Get the tour!', ...dataIcon(licon.InfoCircle) },
- hook: bind('click', ctrl.startTour),
- })
- : null,
+ toolButton({ ctrl, tab: 'multiBoard', hint: 'Multiboard', icon: iconTag(licon.Multiboard) }),
+ toolButton({ ctrl, tab: 'share', hint: noarg('shareAndExport'), icon: iconTag(licon.NodeBranching) }),
+ !ctrl.relay &&
+ !ctrl.data.chapter.gamebook &&
+ h('span.help', {
+ attrs: { title: 'Need help? Get the tour!', ...dataIcon(licon.InfoCircle) },
+ hook: bind('click', ctrl.startTour),
+ }),
]),
h('div.right', [gbOverrideButton(ctrl)]),
]);
@@ -184,41 +166,32 @@ export function side(ctrl: StudyCtrl): VNode {
{
class: { active: tourShown },
hook: bind('mousedown', () => tourShow(true), ctrl.redraw),
- attrs: {
- ...dataIcon(licon.RadioTower),
- role: 'tab',
- },
+ attrs: { ...dataIcon(licon.RadioTower), role: 'tab' },
},
'Broadcast',
);
const chaptersTab =
- tourShow && ctrl.looksNew() && !ctrl.members.canContribute()
- ? null
- : makeTab(
- 'chapters',
- ctrl.trans.pluralSame(ctrl.relay ? 'nbGames' : 'nbChapters', ctrl.chapters.list().length),
- );
+ (tourShow && ctrl.looksNew() && !ctrl.members.canContribute()) ||
+ makeTab(
+ 'chapters',
+ ctrl.trans.pluralSame(ctrl.relay ? 'nbGames' : 'nbChapters', ctrl.chapters.list().length),
+ );
const tabs = h('div.tabs-horiz', { attrs: { role: 'tablist' } }, [
tourTab,
chaptersTab,
- !tourTab || ctrl.members.canContribute() || ctrl.data.admin
- ? makeTab('members', ctrl.trans.pluralSame('nbMembers', ctrl.members.size()))
- : null,
+ (!tourTab || ctrl.members.canContribute() || ctrl.data.admin) &&
+ makeTab('members', ctrl.trans.pluralSame('nbMembers', ctrl.members.size())),
h('span.search.narrow', {
- attrs: {
- ...dataIcon(licon.Search),
- title: 'Search',
- },
+ attrs: { ...dataIcon(licon.Search), title: 'Search' },
hook: bind('click', () => ctrl.search.open(true)),
}),
- ctrl.members.isOwner()
- ? h('span.more.narrow', {
- attrs: { ...dataIcon(licon.Hamburger), title: 'Edit study' },
- hook: bind('click', () => ctrl.form.open(!ctrl.form.open()), ctrl.redraw),
- })
- : null,
+ ctrl.members.isOwner() &&
+ h('span.more.narrow', {
+ attrs: { ...dataIcon(licon.Hamburger), title: 'Edit study' },
+ hook: bind('click', () => ctrl.form.open(!ctrl.form.open()), ctrl.redraw),
+ }),
]);
const content = tourShown
diff --git a/ui/analyse/src/study/topics.ts b/ui/analyse/src/study/topics.ts
index f1947fa081b74..d0010d93c7350 100644
--- a/ui/analyse/src/study/topics.ts
+++ b/ui/analyse/src/study/topics.ts
@@ -21,21 +21,15 @@ export default class TopicsCtrl {
export const view = (ctrl: StudyCtrl): VNode =>
h('div.study__topics', [
- ...ctrl.topics.getTopics().map(topic =>
- h(
- 'a.topic',
- {
- attrs: { href: `/study/topic/${encodeURIComponent(topic)}/hot` },
- },
- topic,
+ ...ctrl.topics
+ .getTopics()
+ .map(topic =>
+ h('a.topic', { attrs: { href: `/study/topic/${encodeURIComponent(topic)}/hot` } }, topic),
),
- ),
ctrl.members.canContribute()
? h(
'a.manage',
- {
- hook: bind('click', () => ctrl.topics.open(true), ctrl.redraw),
- },
+ { hook: bind('click', () => ctrl.topics.open(true), ctrl.redraw) },
ctrl.trans.noarg('manageTopics'),
)
: null,
@@ -66,18 +60,10 @@ export const formView = (ctrl: TopicsCtrl, userId?: string): VNode =>
[
h(
'textarea',
- {
- hook: onInsert(elm => setupTagify(elm as HTMLTextAreaElement, userId)),
- },
+ { 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' }, ctrl.trans.noarg('save')),
],
),
],
@@ -90,10 +76,7 @@ export const formView = (ctrl: TopicsCtrl, userId?: string): VNode =>
function setupTagify(elm: HTMLInputElement | HTMLTextAreaElement, userId?: string) {
lichess.asset.loadCssPath('tagify');
lichess.asset.loadIife('npm/tagify/tagify.min.js').then(() => {
- const tagi = (tagify = new (window.Tagify as typeof Tagify)(elm, {
- pattern: /.{2,}/,
- maxTags: 30,
- }));
+ const tagi = (tagify = new (window.Tagify as typeof Tagify)(elm, { pattern: /.{2,}/, maxTags: 30 }));
let abortCtrl: AbortController | undefined; // for aborting the call
tagi.on('input', e => {
const term = (e.detail as Tagify.TagData).value.trim();
diff --git a/ui/analyse/src/treeView/columnView.ts b/ui/analyse/src/treeView/columnView.ts
index f19b9a84dc2f9..60eba42bdf6c2 100644
--- a/ui/analyse/src/treeView/columnView.ts
+++ b/ui/analyse/src/treeView/columnView.ts
@@ -1,6 +1,6 @@
-import { h, VNode } from 'snabbdom';
+import { VNode } from 'snabbdom';
import { isEmpty } from 'common';
-import { MaybeVNodes } from 'common/snabbdom';
+import { LooseVNodes, looseH as h } from 'common/snabbdom';
import { fixCrazySan } from 'chess';
import { path as treePath, ops as treeOps } from 'tree';
import * as moveView from '../view/moveView';
@@ -29,16 +29,10 @@ interface Opts extends BaseOpts {
function emptyMove(conceal?: Conceal): VNode {
const c: { conceal?: true; hide?: true } = {};
if (conceal) c[conceal] = true;
- return h(
- 'move.empty',
- {
- class: c,
- },
- '...',
- );
+ return h('move.empty', { class: c }, '...');
}
-function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): MaybeVNodes | undefined {
+function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): LooseVNodes | undefined {
const cs = node.children.filter(x => ctx.showComputer || !x.comp),
main = cs[0];
if (!main) return;
@@ -52,54 +46,41 @@ function renderChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): MaybeVNodes |
nonEmpty,
);
if (!cs[1] && isEmpty(commentTags) && !main.forceVariation)
- return ((isWhite ? [moveView.renderIndex(main.ply, false)] : []) as MaybeVNodes).concat(
- renderMoveAndChildrenOf(ctx, main, {
- parentPath: opts.parentPath,
- isMainline: true,
- conceal,
- }) || [],
- );
- const mainChildren = main.forceVariation
- ? undefined
- : renderChildrenOf(ctx, main, {
- parentPath: opts.parentPath + main.id,
- isMainline: true,
- conceal,
- });
- const passOpts = {
- parentPath: opts.parentPath,
- isMainline: !main.forceVariation,
- conceal,
- };
- return (isWhite ? [moveView.renderIndex(main.ply, false)] : ([] as MaybeVNodes))
- .concat(
- main.forceVariation
- ? []
- : [renderMoveOf(ctx, main, passOpts), isWhite ? emptyMove(passOpts.conceal) : null],
- )
- .concat([
- h(
- 'interrupt',
- commentTags.concat(
- renderLines(ctx, main.forceVariation ? cs : cs.slice(1), {
- parentPath: opts.parentPath,
- isMainline: passOpts.isMainline,
- conceal,
- noConceal: !conceal,
- }),
- ),
+ return [
+ isWhite && moveView.renderIndex(main.ply, false),
+ ...renderMoveAndChildrenOf(ctx, main, { parentPath: opts.parentPath, isMainline: true, conceal }),
+ ];
+ const mainChildren =
+ !main.forceVariation &&
+ renderChildrenOf(ctx, main, { parentPath: opts.parentPath + main.id, isMainline: true, conceal });
+
+ const passOpts = { parentPath: opts.parentPath, isMainline: !main.forceVariation, conceal };
+
+ return [
+ isWhite && moveView.renderIndex(main.ply, false),
+ !main.forceVariation && renderMoveOf(ctx, main, passOpts),
+ isWhite && !main.forceVariation && emptyMove(conceal),
+ h(
+ 'interrupt',
+ commentTags.concat(
+ renderLines(ctx, main.forceVariation ? cs : cs.slice(1), {
+ parentPath: opts.parentPath,
+ isMainline: passOpts.isMainline,
+ conceal,
+ noConceal: !conceal,
+ }),
),
- ] as MaybeVNodes)
- .concat(
- isWhite && mainChildren ? [moveView.renderIndex(main.ply, false), emptyMove(passOpts.conceal)] : [],
- )
- .concat(mainChildren || []);
+ ),
+ isWhite && mainChildren && moveView.renderIndex(main.ply, false),
+ isWhite && mainChildren && emptyMove(conceal),
+ ...(mainChildren || []),
+ ];
}
if (!cs[1]) return renderMoveAndChildrenOf(ctx, main, opts);
return renderInlined(ctx, cs, opts) || [renderLines(ctx, cs, opts)];
}
-function renderInlined(ctx: Ctx, nodes: Tree.Node[], opts: Opts): MaybeVNodes | undefined {
+function renderInlined(ctx: Ctx, nodes: Tree.Node[], opts: Opts): LooseVNodes | undefined {
// only 2 branches
if (!nodes[1] || nodes[2]) return;
// only if second branch has no sub-branches
@@ -115,9 +96,7 @@ function renderInlined(ctx: Ctx, nodes: Tree.Node[], opts: Opts): MaybeVNodes |
function renderLines(ctx: Ctx, nodes: Tree.Node[], opts: Opts): VNode {
return h(
'lines',
- {
- class: { single: !nodes[1] },
- },
+ { class: { single: !nodes[1] } },
nodes.map(n => {
return (
retroLine(ctx, n) ||
@@ -144,49 +123,26 @@ function renderMainlineMoveOf(ctx: Ctx, node: Tree.Node, opts: Opts): VNode {
const path = opts.parentPath + node.id,
classes = nodeClasses(ctx, node, path);
if (opts.conceal) classes[opts.conceal as string] = true;
- return h(
- 'move',
- {
- attrs: { p: path },
- class: classes,
- },
- moveView.renderMove(ctx, node),
- );
+ return h('move', { attrs: { p: path }, class: classes }, moveView.renderMove(ctx, node));
}
function renderVariationMoveOf(ctx: Ctx, node: Tree.Node, opts: Opts): VNode {
const withIndex = opts.withIndex || node.ply % 2 === 1,
path = opts.parentPath + node.id,
- content: MaybeVNodes = [withIndex ? moveView.renderIndex(node.ply, true) : null, fixCrazySan(node.san!)],
+ content: LooseVNodes = [withIndex && moveView.renderIndex(node.ply, true), fixCrazySan(node.san!)],
classes = nodeClasses(ctx, node, path);
if (opts.conceal) classes[opts.conceal as string] = true;
if (node.glyphs) node.glyphs.forEach(g => content.push(moveView.renderGlyph(g)));
- return h(
- 'move',
- {
- attrs: { p: path },
- class: classes,
- },
- content,
- );
+ return h('move', { attrs: { p: path }, class: classes }, content);
}
-function renderMoveAndChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): MaybeVNodes {
+function renderMoveAndChildrenOf(ctx: Ctx, node: Tree.Node, opts: Opts): LooseVNodes {
const path = opts.parentPath + node.id;
- if (opts.truncate === 0)
- return [
- h(
- 'move',
- {
- attrs: { p: path },
- },
- [h('index', '[...]')],
- ),
- ];
+ if (opts.truncate === 0) return [h('move', { attrs: { p: path } }, [h('index', '[...]')])];
return [
renderMoveOf(ctx, node, opts),
...renderInlineCommentsOf(ctx, node, path),
- opts.inline ? renderInline(ctx, opts.inline, opts) : null,
+ opts.inline && renderInline(ctx, opts.inline, opts),
...(renderChildrenOf(ctx, node, {
parentPath: path,
isMainline: opts.isMainline,
@@ -215,7 +171,7 @@ function renderMainlineCommentsOf(
conceal: Conceal,
withColor: boolean,
path: string,
-): MaybeVNodes {
+): LooseVNodes {
if (!ctx.ctrl.showComments || isEmpty(node.comments)) return [];
const colorClass = withColor ? (node.ply % 2 === 0 ? '.black ' : '.white ') : '';
@@ -242,29 +198,18 @@ export default function (ctrl: AnalyseCtrl, concealOf?: ConcealOf): VNode {
ctrl,
truncateComments: false,
concealOf: concealOf || emptyConcealOf,
- showComputer: ctrl.showComputer() && !ctrl.retro,
+ showComputer: ctrl.showComputer() && !ctrl.retro?.isSolving(),
showGlyphs: !!ctrl.study || ctrl.showComputer(),
showEval: ctrl.showComputer(),
currentPath: findCurrentPath(ctrl),
};
//I hardcoded the root path, I'm not sure if there's a better way for that to be done
const commentTags = renderMainlineCommentsOf(ctx, root, false, false, '');
- return h(
- 'div.tview2.tview2-column',
- {
- hook: mainHook(ctrl),
- },
- (
- [
- isEmpty(commentTags) ? null : h('interrupt', commentTags),
- root.ply & 1 ? moveView.renderIndex(root.ply, false) : null,
- root.ply & 1 ? emptyMove() : null,
- ] as MaybeVNodes
- ).concat(
- renderChildrenOf(ctx, root, {
- parentPath: '',
- isMainline: true,
- }) || [],
- ),
- );
+
+ return h('div.tview2.tview2-column', { hook: mainHook(ctrl) }, [
+ !isEmpty(commentTags) && h('interrupt', commentTags),
+ root.ply & 1 && moveView.renderIndex(root.ply, false),
+ root.ply & 1 && emptyMove(),
+ ...(renderChildrenOf(ctx, root, { parentPath: '', isMainline: true }) || []),
+ ]);
}
diff --git a/ui/analyse/src/treeView/common.ts b/ui/analyse/src/treeView/common.ts
index a9cd232528517..27703b2eeec95 100644
--- a/ui/analyse/src/treeView/common.ts
+++ b/ui/analyse/src/treeView/common.ts
@@ -20,10 +20,7 @@ export function mainHook(ctrl: AnalyseCtrl): Hooks {
const ctxMenuCallback = (e: MouseEvent) => {
const path = eventPath(e);
if (path !== null) {
- contextMenu(e, {
- path,
- root: ctrl,
- });
+ contextMenu(e, { path, root: ctrl });
}
ctrl.redraw();
return false;
diff --git a/ui/analyse/src/treeView/contextMenu.ts b/ui/analyse/src/treeView/contextMenu.ts
index f0395a9f62902..32c0e857c00f2 100644
--- a/ui/analyse/src/treeView/contextMenu.ts
+++ b/ui/analyse/src/treeView/contextMenu.ts
@@ -1,6 +1,6 @@
import * as licon from 'common/licon';
-import { bind, onInsert } from 'common/snabbdom';
-import { h, VNode } from 'snabbdom';
+import { bind, onInsert, looseH as h } from 'common/snabbdom';
+import { VNode } from 'snabbdom';
import AnalyseCtrl from '../ctrl';
import * as studyView from '../study/studyView';
import { patch, nodeFullName } from '../view/util';
@@ -28,11 +28,7 @@ const elementId = 'analyse-cm';
function getPosition(e: MouseEvent | TouchEvent): Coords | null {
let pos = e as PageOrClientPos;
if ('touches' in e && e.touches.length > 0) pos = e.touches[0];
- if (pos.pageX || pos.pageY)
- return {
- x: pos.pageX!,
- y: pos.pageY!,
- };
+ if (pos.pageX || pos.pageY) return { x: pos.pageX!, y: pos.pageY! };
else if (pos.clientX || pos.clientY)
return {
x: pos.clientX! + document.body.scrollLeft + document.documentElement!.scrollLeft,
@@ -57,14 +53,7 @@ function positionMenu(menu: HTMLElement, coords: Coords): void {
}
function action(icon: string, text: string, handler: () => void): VNode {
- return h(
- 'a',
- {
- attrs: { 'data-icon': icon },
- hook: bind('click', handler),
- },
- text,
- );
+ return h('a', { attrs: { 'data-icon': icon }, hook: bind('click', handler) }, text);
}
function view(opts: Opts, coords: Coords): VNode {
@@ -85,23 +74,23 @@ function view(opts: Opts, coords: Coords): VNode {
},
[
h('p.title', nodeFullName(node)),
- onMainline
- ? null
- : action(licon.UpTriangle, trans('promoteVariation'), () => ctrl.promote(opts.path, false)),
- onMainline ? null : action(licon.Checkmark, trans('makeMainLine'), () => ctrl.promote(opts.path, true)),
+
+ !onMainline &&
+ action(licon.UpTriangle, trans('promoteVariation'), () => ctrl.promote(opts.path, false)),
+
+ !onMainline && action(licon.Checkmark, trans('makeMainLine'), () => ctrl.promote(opts.path, true)),
+
action(licon.Trash, trans('deleteFromHere'), () => ctrl.deleteNode(opts.path)),
- ]
- .concat(ctrl.study ? studyView.contextMenu(ctrl.study, opts.path, node) : [])
- .concat([
- onMainline
- ? action(licon.InternalArrow, trans('forceVariation'), () => ctrl.forceVariation(opts.path, true))
- : null,
- ])
- .concat([
- action(licon.Clipboard, trans('copyVariationPgn'), () =>
- navigator.clipboard.writeText(renderVariationPgn(opts.root.tree.getNodeList(opts.path))),
- ),
- ]),
+
+ ...(ctrl.study ? studyView.contextMenu(ctrl.study, opts.path, node) : []),
+
+ onMainline &&
+ action(licon.InternalArrow, trans('forceVariation'), () => ctrl.forceVariation(opts.path, true)),
+
+ action(licon.Clipboard, trans('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 0761b60024005..1834e60c6a856 100644
--- a/ui/analyse/src/treeView/inlineView.ts
+++ b/ui/analyse/src/treeView/inlineView.ts
@@ -112,11 +112,7 @@ function renderInline(ctx: Ctx, node: Tree.Node, opts: Opts): VNode {
if (retro) return h('interrupt', h('lines', retro));
return h(
'inline',
- renderMoveAndChildrenOf(ctx, node, {
- withIndex: true,
- parentPath: opts.parentPath,
- isMainline: false,
- }),
+ renderMoveAndChildrenOf(ctx, node, { withIndex: true, parentPath: opts.parentPath, isMainline: false }),
);
}
@@ -127,36 +123,23 @@ function renderMoveOf(ctx: Ctx, node: Tree.Node, opts: Opts): VNode {
fixCrazySan(node.san!),
];
if (node.glyphs && ctx.showGlyphs) node.glyphs.forEach(g => content.push(moveView.renderGlyph(g)));
- return h(
- 'move',
- {
- attrs: { p: path },
- class: nodeClasses(ctx, node, path),
- },
- content,
- );
+ return h('move', { attrs: { p: path }, class: nodeClasses(ctx, node, path) }, content);
}
export default function (ctrl: AnalyseCtrl): VNode {
const ctx: Ctx = {
ctrl,
truncateComments: false,
- showComputer: ctrl.showComputer() && !ctrl.retro,
+ showComputer: ctrl.showComputer() && !ctrl.retro?.isSolving(),
showGlyphs: !!ctrl.study || ctrl.showComputer(),
showEval: !!ctrl.study || ctrl.showComputer(),
currentPath: findCurrentPath(ctrl),
};
- return h(
- 'div.tview2.tview2-inline',
- {
- hook: mainHook(ctrl),
- },
- [
- ...renderInlineCommentsOf(ctx, ctrl.tree.root, ''),
- ...(renderChildrenOf(ctx, ctrl.tree.root, {
- parentPath: '',
- isMainline: true,
- }) || []),
- ],
- );
+ return h('div.tview2.tview2-inline', { hook: mainHook(ctrl) }, [
+ ...renderInlineCommentsOf(ctx, ctrl.tree.root, ''),
+ ...(renderChildrenOf(ctx, ctrl.tree.root, {
+ parentPath: '',
+ isMainline: true,
+ }) || []),
+ ]);
}
diff --git a/ui/analyse/src/view/actionMenu.ts b/ui/analyse/src/view/actionMenu.ts
index a9099c75f2d44..5f58884e982dc 100644
--- a/ui/analyse/src/view/actionMenu.ts
+++ b/ui/analyse/src/view/actionMenu.ts
@@ -2,8 +2,8 @@ import { isEmpty } from 'common';
import * as licon from 'common/licon';
import { domDialog } from 'common/dialog';
import { isTouchDevice } from 'common/device';
-import { bind, dataIcon, MaybeVNodes } from 'common/snabbdom';
-import { h, VNode } from 'snabbdom';
+import { bind, dataIcon, MaybeVNodes, looseH as h } from 'common/snabbdom';
+import { VNode } from 'snabbdom';
import { AutoplayDelay } from '../autoplay';
import { toggle, ToggleSettings } from 'common/controls';
import AnalyseCtrl from '../ctrl';
@@ -16,14 +16,8 @@ interface AutoplaySpeed {
}
const baseSpeeds: AutoplaySpeed[] = [
- {
- name: 'fast',
- delay: 1000,
- },
- {
- name: 'slow',
- delay: 5000,
- },
+ { name: 'fast', delay: 1000 },
+ { name: 'slow', delay: 5000 },
];
const realtimeSpeed: AutoplaySpeed = {
@@ -52,10 +46,7 @@ function autoplayButtons(ctrl: AnalyseCtrl): VNode {
return h(
'a.button',
{
- class: {
- active,
- 'button-empty': !active,
- },
+ class: { active, 'button-empty': !active },
hook: bind('click', () => ctrl.togglePlay(speed.delay), ctrl.redraw),
},
ctrl.trans.noarg(speed.name),
@@ -84,10 +75,7 @@ function studyButton(ctrl: AnalyseCtrl) {
return h(
'form',
{
- attrs: {
- method: 'post',
- action: '/study/as',
- },
+ attrs: { method: 'post', action: '/study/as' },
hook: bind('submit', e => {
const pgnInput = (e.target as HTMLElement).querySelector('input[name=pgn]') as HTMLInputElement;
if (pgnInput && (ctrl.synthetic || ctrl.persistence?.isDirty)) {
@@ -96,21 +84,12 @@ function studyButton(ctrl: AnalyseCtrl) {
}),
},
[
- !ctrl.synthetic ? hiddenInput('gameId', ctrl.data.game.id) : null,
+ !ctrl.synthetic && hiddenInput('gameId', ctrl.data.game.id),
hiddenInput('pgn', ''),
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 } }, ctrl.trans.noarg('toStudy')),
],
);
}
@@ -126,61 +105,50 @@ export function view(ctrl: AnalyseCtrl): VNode {
h('div.action-menu__tools', [
h(
'a',
- {
- hook: bind('click', ctrl.flip),
- attrs: {
- 'data-icon': licon.ChasingArrows,
- title: 'Hotkey: f',
- },
- },
+ { hook: bind('click', ctrl.flip), attrs: { 'data-icon': licon.ChasingArrows, title: 'Hotkey: f' } },
noarg('flipBoard'),
),
- ctrl.ongoing
- ? null
- : h(
- 'a',
- {
- attrs: {
- href: d.userAnalysis
- ? '/editor?' +
- new URLSearchParams({
- fen: ctrl.node.fen,
- variant: d.game.variant.key,
- color: ctrl.chessground.state.orientation,
- })
- : `/${d.game.id}/edit?fen=${ctrl.node.fen}`,
- 'data-icon': licon.Pencil,
- rel: 'nofollow',
- },
- },
- noarg('boardEditor'),
- ),
- canContinue
- ? h(
- 'a',
- {
- hook: bind('click', () =>
- domDialog({ cash: $('.continue-with.g_' + d.game.id), show: 'modal' }),
- ),
- attrs: dataIcon(licon.Swords),
+ !ctrl.ongoing &&
+ h(
+ 'a',
+ {
+ attrs: {
+ href: d.userAnalysis
+ ? '/editor?' +
+ new URLSearchParams({
+ fen: ctrl.node.fen,
+ variant: d.game.variant.key,
+ color: ctrl.chessground.state.orientation,
+ })
+ : `/${d.game.id}/edit?fen=${ctrl.node.fen}`,
+ 'data-icon': licon.Pencil,
+ rel: 'nofollow',
},
- noarg('continueFromHere'),
- )
- : null,
+ },
+ noarg('boardEditor'),
+ ),
+ canContinue &&
+ h(
+ 'a',
+ {
+ hook: bind('click', () => domDialog({ cash: $('.continue-with.g_' + d.game.id), show: 'modal' })),
+ attrs: dataIcon(licon.Swords),
+ },
+ noarg('continueFromHere'),
+ ),
studyButton(ctrl),
- ctrl.persistence?.isDirty
- ? h(
- 'a',
- {
- attrs: {
- title: noarg('clearSavedMoves'),
- 'data-icon': licon.Trash,
- },
- hook: bind('click', ctrl.persistence.clear),
+ ctrl.persistence?.isDirty &&
+ h(
+ 'a',
+ {
+ attrs: {
+ title: noarg('clearSavedMoves'),
+ 'data-icon': licon.Trash,
},
- noarg('clearSavedMoves'),
- )
- : null,
+ hook: bind('click', ctrl.persistence.clear),
+ },
+ noarg('clearSavedMoves'),
+ ),
]),
];
@@ -240,30 +208,28 @@ export function view(ctrl: AnalyseCtrl): VNode {
},
ctrl,
),
- isTouchDevice()
- ? null
- : ctrlToggle(
- {
- name: 'showVariationArrows',
- title: 'Variation navigation arrows',
- id: 'variationArrows',
- checked: ctrl.variationArrowsProp(),
- change: ctrl.toggleVariationArrows,
- },
- ctrl,
- ),
- ctrl.ongoing
- ? null
- : ctrlToggle(
- {
- name: 'Annotations on board',
- title: 'Display analysis symbols on the board',
- id: 'move-annotation',
- checked: ctrl.showMoveAnnotation(),
- change: ctrl.toggleMoveAnnotation,
- },
- ctrl,
- ),
+ !isTouchDevice() &&
+ ctrlToggle(
+ {
+ name: 'showVariationArrows',
+ title: 'Variation navigation arrows',
+ id: 'variationArrows',
+ checked: ctrl.variationArrowsProp(),
+ change: ctrl.toggleVariationArrows,
+ },
+ ctrl,
+ ),
+ !ctrl.ongoing &&
+ ctrlToggle(
+ {
+ name: 'Annotations on board',
+ title: 'Display analysis symbols on the board',
+ id: 'move-annotation',
+ checked: ctrl.showMoveAnnotation(),
+ change: ctrl.toggleMoveAnnotation,
+ },
+ ctrl,
+ ),
];
return h('div.action-menu', [
@@ -271,33 +237,32 @@ export function view(ctrl: AnalyseCtrl): VNode {
...displayConfig,
...cevalConfig,
...(ctrl.mainline.length > 4 ? [h('h2', noarg('replayMode')), autoplayButtons(ctrl)] : []),
- canContinue
- ? h('div.continue-with.none.g_' + d.game.id, [
- h(
- 'a.button',
- {
- attrs: {
- href: d.userAnalysis
- ? '/?fen=' + ctrl.encodeNodeFen() + '#ai'
- : contRoute(d, 'ai') + '?fen=' + ctrl.node.fen,
- rel: 'nofollow',
- },
+ canContinue &&
+ h('div.continue-with.none.g_' + d.game.id, [
+ h(
+ 'a.button',
+ {
+ attrs: {
+ href: d.userAnalysis
+ ? '/?fen=' + ctrl.encodeNodeFen() + '#ai'
+ : contRoute(d, 'ai') + '?fen=' + ctrl.node.fen,
+ rel: 'nofollow',
},
- noarg('playWithTheMachine'),
- ),
- h(
- 'a.button',
- {
- attrs: {
- href: d.userAnalysis
- ? '/?fen=' + ctrl.encodeNodeFen() + '#friend'
- : contRoute(d, 'friend') + '?fen=' + ctrl.node.fen,
- rel: 'nofollow',
- },
+ },
+ noarg('playWithTheMachine'),
+ ),
+ h(
+ 'a.button',
+ {
+ attrs: {
+ href: d.userAnalysis
+ ? '/?fen=' + ctrl.encodeNodeFen() + '#friend'
+ : contRoute(d, 'friend') + '?fen=' + ctrl.node.fen,
+ rel: 'nofollow',
},
- noarg('playWithAFriend'),
- ),
- ])
- : null,
+ },
+ noarg('playWithAFriend'),
+ ),
+ ]),
]);
}
diff --git a/ui/analyse/src/view/clocks.ts b/ui/analyse/src/view/clocks.ts
index 174bc3583d9c9..74a2bb4afcc1b 100644
--- a/ui/analyse/src/view/clocks.ts
+++ b/ui/analyse/src/view/clocks.ts
@@ -34,13 +34,7 @@ export default function renderClocks(ctrl: AnalyseCtrl): [VNode, VNode] | undefi
}
const renderClock = (centis: number | undefined, active: boolean, cls: string, showTenths: boolean): VNode =>
- h(
- 'div.analyse__clock.' + cls,
- {
- class: { active },
- },
- clockContent(centis, showTenths),
- );
+ h('div.analyse__clock.' + cls, { class: { active } }, clockContent(centis, showTenths));
function clockContent(centis: number | undefined, showTenths: boolean): Array {
if (!centis && centis !== 0) return ['-'];
diff --git a/ui/analyse/src/view/moveView.ts b/ui/analyse/src/view/moveView.ts
index e3c307ead0864..49dab721ead25 100644
--- a/ui/analyse/src/view/moveView.ts
+++ b/ui/analyse/src/view/moveView.ts
@@ -11,13 +11,7 @@ export interface Ctx {
}
export const renderGlyph = (glyph: Tree.Glyph): VNode =>
- h(
- 'glyph',
- {
- attrs: { title: glyph.name },
- },
- glyph.symbol,
- );
+ h('glyph', { attrs: { title: glyph.name } }, glyph.symbol);
const renderEval = (e: string): VNode => h('eval', e.replace('-', '−'));
diff --git a/ui/analyse/src/view/roundTraining.ts b/ui/analyse/src/view/roundTraining.ts
index 32989434f1ba6..ca89738fbea85 100644
--- a/ui/analyse/src/view/roundTraining.ts
+++ b/ui/analyse/src/view/roundTraining.ts
@@ -17,13 +17,11 @@ interface Advice {
const renderPlayer = (ctrl: AnalyseCtrl, color: Color): VNode => {
const p = game.getPlayer(ctrl.data, color);
if (p.user)
- return h(
- 'a.user-link.ulpt',
- {
- attrs: { href: '/@/' + p.user.username },
- },
- [p.user.username, ' ', ratingDiff(p)],
- );
+ return h('a.user-link.ulpt', { attrs: { href: '/@/' + p.user.username } }, [
+ p.user.username,
+ ' ',
+ ratingDiff(p),
+ ]);
return h(
'span',
p.name ||
@@ -56,11 +54,7 @@ function playerTable(ctrl: AnalyseCtrl, color: Color): VNode {
ctrl.trans.noarg('accuracy'),
' ',
h('a', {
- attrs: {
- 'data-icon': licon.InfoCircle,
- href: '/page/accuracy',
- target: '_blank',
- },
+ attrs: { 'data-icon': licon.InfoCircle, href: '/page/accuracy', target: '_blank' },
}),
]),
]),
diff --git a/ui/analyse/src/view/util.ts b/ui/analyse/src/view/util.ts
index 3637affc6944a..030f3b104ae57 100644
--- a/ui/analyse/src/view/util.ts
+++ b/ui/analyse/src/view/util.ts
@@ -21,13 +21,4 @@ export function titleNameToId(titleName: string): string {
}
export const option = (value: string, current: string | undefined, name: string) =>
- h(
- 'option',
- {
- attrs: {
- value: value,
- selected: value === current,
- },
- },
- name,
- );
+ h('option', { attrs: { value: value, selected: value === current } }, name);
diff --git a/ui/analyse/src/view/view.ts b/ui/analyse/src/view/view.ts
index 4d8d295d7267f..c7fee1c2655ab 100644
--- a/ui/analyse/src/view/view.ts
+++ b/ui/analyse/src/view/view.ts
@@ -2,13 +2,13 @@ import { view as cevalView } from 'ceval';
import { parseFen } from 'chessops/fen';
import { defined } from 'common';
import * as licon from 'common/licon';
-import { bind, bindNonPassive, onInsert, dataIcon } from 'common/snabbdom';
+import { bind, bindNonPassive, onInsert, dataIcon, looseH as h } from 'common/snabbdom';
import { bindMobileMousedown, isMobile } from 'common/device';
import { playable } from 'game';
import * as router from 'game/router';
import * as materialView from 'game/view/material';
import statusView from 'game/view/status';
-import { h, VNode, VNodeChildren } from 'snabbdom';
+import { VNode, VNodeChildren } from 'snabbdom';
import { path as treePath } from 'tree';
import { render as trainingView } from './roundTraining';
import { view as actionMenu } from './actionMenu';
@@ -53,10 +53,7 @@ function makeConcealOf(ctrl: AnalyseCtrl): ConcealOf | undefined {
}
const jumpButton = (icon: string, effect: string, enabled: boolean): VNode =>
- h('button.fbt', {
- class: { disabled: !enabled },
- attrs: { 'data-act': effect, 'data-icon': icon },
- });
+ h('button.fbt', { class: { disabled: !enabled }, attrs: { 'data-act': effect, 'data-icon': icon } });
const dataAct = (e: Event): string | null => {
const target = e.target as HTMLElement;
@@ -84,18 +81,15 @@ function inputs(ctrl: AnalyseCtrl): VNode | undefined {
h('div.pair', [
h('label.name', 'FEN'),
h('input.copyable.autoselect.analyse__underboard__fen', {
- attrs: {
- spellcheck: 'false',
- enterkeyhint: 'done',
- },
+ attrs: { spellcheck: 'false', enterkeyhint: 'done' },
hook: {
insert: vnode => {
const el = vnode.elm as HTMLInputElement;
el.value = defined(ctrl.fenInput) ? ctrl.fenInput : ctrl.node.fen;
- el.addEventListener('change', _ => {
+ el.addEventListener('change', () => {
if (el.value !== ctrl.node.fen && el.reportValidity()) ctrl.changeFen(el.value.trim());
});
- el.addEventListener('input', _ => {
+ el.addEventListener('input', () => {
ctrl.fenInput = el.value;
el.setCustomValidity(parseFen(el.value.trim()).isOk ? '' : 'Invalid FEN');
});
@@ -137,19 +131,18 @@ function inputs(ctrl: AnalyseCtrl): VNode | undefined {
},
},
}),
- isMobile()
- ? null
- : h(
- 'button.button.button-thin.action.text',
- {
- attrs: dataIcon(licon.PlayTriangle),
- hook: bind('click', _ => {
- const pgn = $('.copyables .pgn textarea').val() as string;
- if (pgn !== pgnExport.renderFullTxt(ctrl)) ctrl.changePgn(pgn, true);
- }),
- },
- ctrl.trans.noarg('importPgn'),
- ),
+ !isMobile() &&
+ h(
+ 'button.button.button-thin.action.text',
+ {
+ attrs: dataIcon(licon.PlayTriangle),
+ hook: bind('click', _ => {
+ const pgn = $('.copyables .pgn textarea').val() as string;
+ if (pgn !== pgnExport.renderFullTxt(ctrl)) ctrl.changePgn(pgn, true);
+ }),
+ },
+ ctrl.trans.noarg('importPgn'),
+ ),
]),
]),
]);
@@ -183,11 +176,7 @@ function controls(ctrl: AnalyseCtrl) {
ctrl.studyPractice
? [
h('button.fbt', {
- attrs: {
- title: noarg('analysis'),
- 'data-act': 'analysis',
- 'data-icon': licon.Microscope,
- },
+ attrs: { title: noarg('analysis'), 'data-act': 'analysis', 'data-icon': licon.Microscope },
}),
]
: [
@@ -202,19 +191,17 @@ function controls(ctrl: AnalyseCtrl) {
active: ctrl.explorer.enabled(),
},
}),
- ctrl.ceval.possible && ctrl.ceval.allowed() && !ctrl.isGamebook()
- ? h('button.fbt', {
- attrs: {
- title: noarg('practiceWithComputer'),
- 'data-act': 'practice',
- 'data-icon': licon.Bullseye,
- },
- class: {
- hidden: menuIsOpen || !!ctrl.retro,
- active: !!ctrl.practice,
- },
- })
- : null,
+ ctrl.ceval.possible &&
+ ctrl.ceval.allowed() &&
+ !ctrl.isGamebook() &&
+ h('button.fbt', {
+ attrs: {
+ title: noarg('practiceWithComputer'),
+ 'data-act': 'practice',
+ 'data-icon': licon.Bullseye,
+ },
+ class: { hidden: menuIsOpen || !!ctrl.retro, active: !!ctrl.practice },
+ }),
],
),
h('div.jumps', [
@@ -227,11 +214,7 @@ function controls(ctrl: AnalyseCtrl) {
? h('div.noop')
: h('button.fbt', {
class: { active: menuIsOpen },
- attrs: {
- title: noarg('menu'),
- 'data-act': 'menu',
- 'data-icon': licon.Hamburger,
- },
+ attrs: { title: noarg('menu'), 'data-act': 'menu', 'data-icon': licon.Hamburger },
}),
],
);
@@ -306,14 +289,14 @@ export default function (deps?: typeof studyDeps) {
const renderAnalyse = (ctrl: AnalyseCtrl, concealOf?: ConcealOf) =>
h('div.analyse__moves.areplay', [
h(`div.areplay__v${ctrl.treeVersion}`, [renderTreeView(ctrl, concealOf), ...renderResult(ctrl)]),
- !ctrl.practice && !deps?.gbEdit.running(ctrl) ? renderNextChapter(ctrl) : null,
+ !ctrl.practice && !deps?.gbEdit.running(ctrl) && renderNextChapter(ctrl),
]);
return function (ctrl: AnalyseCtrl): VNode {
if (ctrl.nvui) return ctrl.nvui.render();
const concealOf = makeConcealOf(ctrl),
study = ctrl.study,
- showCevalPvs = !(ctrl.retro && ctrl.retro.isSolving()) && !ctrl.practice,
+ showCevalPvs = !ctrl.retro?.isSolving() && !ctrl.practice,
menuIsOpen = ctrl.actionMenu(),
gamebookPlay = ctrl.gamebookPlay(),
gamebookPlayView = gamebookPlay && deps?.gbPlay.render(gamebookPlay),
@@ -367,8 +350,8 @@ export default function (deps?: typeof studyDeps) {
},
},
[
- ctrl.keyboardHelp ? keyboardView(ctrl) : null,
- study ? deps?.studyView.overboard(study) : null,
+ ctrl.keyboardHelp && keyboardView(ctrl),
+ study && deps?.studyView.overboard(study),
tour ||
h(
addChapterId(study, 'div.analyse__board.main-board'),
@@ -396,44 +379,42 @@ export default function (deps?: typeof studyDeps) {
},
[
...(playerStrips || []),
- playerBars ? playerBars[ctrl.bottomIsWhite() ? 1 : 0] : null,
+ playerBars?.[ctrl.bottomIsWhite() ? 1 : 0],
chessground.render(ctrl),
- playerBars ? playerBars[ctrl.bottomIsWhite() ? 0 : 1] : null,
+ playerBars?.[ctrl.bottomIsWhite() ? 0 : 1],
ctrl.promotion.view(ctrl.data.game.variant.key === 'antichess'),
],
),
- gaugeOn && !tour ? cevalView.renderGauge(ctrl) : null,
- menuIsOpen || tour ? null : crazyView(ctrl, ctrl.topColor(), 'top'),
+ gaugeOn && !tour && cevalView.renderGauge(ctrl),
+ !menuIsOpen && !tour && crazyView(ctrl, ctrl.topColor(), 'top'),
gamebookPlayView ||
- (tour
- ? null
- : h(addChapterId(study, 'div.analyse__tools'), [
- ...(menuIsOpen
- ? [actionMenu(ctrl)]
- : [
- ...cevalView.renderCeval(ctrl),
- showCevalPvs ? cevalView.renderPvs(ctrl) : null,
- renderAnalyse(ctrl, concealOf),
- gamebookEditView,
- forkView(ctrl, concealOf),
- retroView(ctrl) || practiceView(ctrl) || explorerView(ctrl),
- ]),
- ])),
- menuIsOpen || tour ? null : crazyView(ctrl, ctrl.bottomColor(), 'bottom'),
- gamebookPlayView || tour ? null : controls(ctrl),
- tour
- ? null
- : h(
- 'div.analyse__underboard',
- {
- hook:
- ctrl.synthetic || playable(ctrl.data)
- ? undefined
- : onInsert(elm => serverSideUnderboard(elm, ctrl)),
- },
- study ? deps?.studyView.underboard(ctrl) : [inputs(ctrl)],
- ),
- tour ? null : trainingView(ctrl),
+ (!tour &&
+ h(addChapterId(study, 'div.analyse__tools'), [
+ ...(menuIsOpen
+ ? [actionMenu(ctrl)]
+ : [
+ ...cevalView.renderCeval(ctrl),
+ showCevalPvs && cevalView.renderPvs(ctrl),
+ renderAnalyse(ctrl, concealOf),
+ gamebookEditView,
+ forkView(ctrl, concealOf),
+ retroView(ctrl) || practiceView(ctrl) || explorerView(ctrl),
+ ]),
+ ])),
+ !menuIsOpen && !tour && crazyView(ctrl, ctrl.bottomColor(), 'bottom'),
+ !gamebookPlayView && !tour && controls(ctrl),
+ !tour &&
+ h(
+ 'div.analyse__underboard',
+ {
+ hook:
+ ctrl.synthetic || playable(ctrl.data)
+ ? undefined
+ : onInsert(elm => serverSideUnderboard(elm, ctrl)),
+ },
+ study ? deps?.studyView.underboard(ctrl) : [inputs(ctrl)],
+ ),
+ !tour && trainingView(ctrl),
ctrl.studyPractice
? deps?.studyPracticeView.side(study!)
: h(
@@ -449,28 +430,26 @@ export default function (deps?: typeof studyDeps) {
: study
? [deps?.studyView.side(study)]
: [
- ctrl.forecast ? forecastView(ctrl, ctrl.forecast) : null,
- !ctrl.synthetic && playable(ctrl.data)
- ? h(
- 'div.back-to-game',
- h(
- 'a.button.button-empty.text',
- {
- attrs: {
- href: router.game(ctrl.data, ctrl.data.player.color),
- 'data-icon': licon.Back,
- },
+ ctrl.forecast && forecastView(ctrl, ctrl.forecast),
+ !ctrl.synthetic &&
+ playable(ctrl.data) &&
+ h(
+ 'div.back-to-game',
+ h(
+ 'a.button.button-empty.text',
+ {
+ attrs: {
+ href: router.game(ctrl.data, ctrl.data.player.color),
+ 'data-icon': licon.Back,
},
- ctrl.trans.noarg('backToGame'),
- ),
- )
- : null,
+ },
+ ctrl.trans.noarg('backToGame'),
+ ),
+ ),
],
),
study && study.relay && deps?.relayManager(study.relay),
- h('div.chat__members.none', {
- hook: onInsert(lichess.watchers),
- }),
+ h('div.chat__members.none', { hook: onInsert(lichess.watchers) }),
],
);
};
diff --git a/ui/board/src/menu.ts b/ui/board/src/menu.ts
index b82b0d749bfc7..3780782fd56b3 100644
--- a/ui/board/src/menu.ts
+++ b/ui/board/src/menu.ts
@@ -9,10 +9,7 @@ import * as controls from 'common/controls';
export const toggleButton = (toggle: Toggle, title: string) =>
h('button.fbt.board-menu-toggle', {
class: { active: toggle() },
- attrs: {
- title,
- 'data-icon': licon.Hamburger,
- },
+ attrs: { title, 'data-icon': licon.Hamburger },
hook: onInsert(bindMobileMousedown(toggle.toggle)),
});
@@ -25,9 +22,7 @@ export const menu = (
toggle()
? h(
'div.board-menu',
- {
- hook: onInsert(onClickAway(() => toggle(false))),
- },
+ { hook: onInsert(onClickAway(() => toggle(false))) },
content(new BoardMenu(trans, redraw)),
)
: undefined;
@@ -45,10 +40,7 @@ export class BoardMenu {
'button.button.text',
{
class: { active },
- attrs: {
- title: 'Hotkey: f',
- ...dataIcon(licon.ChasingArrows),
- },
+ attrs: { title: 'Hotkey: f', ...dataIcon(licon.ChasingArrows) },
hook: onInsert(bindMobileMousedown(onChange)),
},
name,
diff --git a/ui/ceval/package.json b/ui/ceval/package.json
index e02a1c75d86c1..307264068efd4 100644
--- a/ui/ceval/package.json
+++ b/ui/ceval/package.json
@@ -16,7 +16,7 @@
"license": "AGPL-3.0-or-later",
"dependencies": {
"@badrap/result": "^0.2.13",
- "chessops": "^0.12.7",
+ "chessops": "^0.13.0",
"common": "workspace:*",
"idb-keyval": "^6.2.1",
"snabbdom": "^3.5.1",
diff --git a/ui/ceval/src/engines/externalEngine.ts b/ui/ceval/src/engines/externalEngine.ts
index 5c14ac7c3c273..427a6f9debad9 100644
--- a/ui/ceval/src/engines/externalEngine.ts
+++ b/ui/ceval/src/engines/externalEngine.ts
@@ -85,8 +85,8 @@ export class ExternalEngine implements CevalEngine {
this.state = CevalState.Initial;
this.status?.();
- } catch (err: unknown) {
- if ((err as Error).name !== 'AbortError') {
+ } catch (err: any) {
+ if (err.name !== 'AbortError') {
console.error(err);
this.state = CevalState.Failed;
this.status?.({ error: String(err) });
diff --git a/ui/ceval/src/util.ts b/ui/ceval/src/util.ts
index 18401ec58c74b..6a2b0dc317f00 100644
--- a/ui/ceval/src/util.ts
+++ b/ui/ceval/src/util.ts
@@ -43,10 +43,13 @@ export function showEngineError(engine: string, error: string) {
domDialog({
class: 'engine-error',
htmlText:
- `
${lichess.escapeHtml(engine)} error
` +
- `${lichess.escapeHtml(error)}
Things to try
` +
- '
Decrease memory slider in engine settings
Clear site settings for lichess.org
' +
- '
Select another engine
Update your browser
',
+ `
${lichess.escapeHtml(engine)} error
` + error.includes('Status 503')
+ ? `
Your external engine does not appear to be connected.
+
Please check the network and restart your provider if possible.