Skip to content

How translations work

Jonathan Gamble edited this page Oct 21, 2024 · 16 revisions

See also: Working on translations

Basics

Everything starts with the various .xml files in translation/source. They define all the individual phrases that can be translated. Each file contains phrases for a certain concept or part of Lichess, e.g. swiss.xml for swiss tournaments, faq.xml for the FAQ, etc. site.xml is the main file that contains all site-wide phrases and everything that doesn't fit anywhere else.

Each phrase has a key/name and the original text in British English, e.g.

<string name="playWithAFriend">Play with a friend</string>

They can have placeholders marked by %s. These can be replaced by something, i.e. the number of games or a separate translated string that needs to be formatted differently or be a clickable link. %1$s, %2$s, etc. can be used for multiple placeholders.

<string name="xStartedStreaming">%s started streaming</string>
<string name="xStartedFollowingY">%1$s started following %2$s</string>

There are also plurals elements for phrases that need to change depending on the value of a placeholder:

<plurals name="nbBlunders">
  <item quantity="one">%s blunder</item>
  <item quantity="other">%s blunders</item>
</plurals>

The content of those .xml files are automatically uploaded to crowdin.com/project/lichess where volunteers translate them. The resulting translations are again automatically downloaded and regularly merged in PRs named "New Crowdin updates", resulting in another batch of .xml files in translation/dest/<area>/<lang-code>.xml where <area> is the faq, site, etc. from above.

Using translations in Scala

There's a Scala equivalent for every translation key defined in modules/corei18n/src/main/key.scala. This file can be generated automatically from the source .xml files by running pnpm run trans-dump.

It defines an I18nKeys object with values for each translation key:

object I18nKeys:
    val `someSiteKey`: I18nKey = "someSiteKey"
    // ...
    object swiss:
        val `swissTournaments`: I18nKey = "swiss:swissTournaments"
        // ...

That object can be imported with import lila.core.i18n.I18nKey as trans so that keys can be accessed as trans.someSiteKey for values from site.xml. Other areas need to be specified, e.g. trans.swiss.swissTournaments for swiss.xml, etc.

modules/corei18n/src/main/key.scala defines these extension methods which can operate on the keys:

object I18nKey:
  // ...
  def txt(args: Any*)(using trans: Translate): String = // ...
  def pluralTxt(count: Count, args: Any*)(using trans: Translate): String = // ...
  def pluralSameTxt(count: Long)(using trans: Translate): String = pluralTxt(count, count)
  def apply(args: Matchable*)(using trans: Translate): RawFrag = // ...
  def plural(count: Count, args: Matchable*)(using trans: Translate): RawFrag = // ...
  def pluralSame(count: Int)(using trans: Translate): RawFrag = plural(count, count)

There are three different functions, each in two versions, one producing a fragment, i.e. for use in scalatags when generating HTML, and another producing a String.

  • Applying a key directly as trans.theKey() directly gives a translated fragment. Values to replace placeholders can be passed directly as arguments: trans.theKey("abc", 42).
  • To use plural keys you need to use trans.theKey.plural(42, "abc", 42). The first number determines the plural version to use (i.e. "game" vs "games") and everything after that is replacing the placeholders. Note that the initial value has to be repeated in the correct position.
  • For the common case when there is only one value that both determines the plural and needs to be inserted, there is trans.theKey.pluralSame(42).

And then there are the three String version equivalents trans.theKey.txt, trans.theKey.pluralTxt and trans.theKey.pluralSameTxt.

What is using Lang?

All these functions have an implicit Lang parameter that specifies the target language to translate to when they are called. This parameter can be automatically extracted from an implicit Context in scope. Generally, that means adding additional (using ctx: Context) or (using lang: Lang) argument lists to all functions calling one of the translation functions, all the way up the call stack until the controller endpoints which will have an explicit Context available.

Using translations in JavaScript

Translation xmls are compiled into javascript assets by ui/build. The loading page includes those assets using script tags that are injected by modules/web/src/main/layout.scala and app/views/base/page.scala. Any non-embed page will get access to site.xml, timeago.xml, and preferences.xml with no action required. The easiest way to pass additional translation modules to the client is with the i18n(...) helper method in modules/ui/src/main/Page.scala.

Once the server includes the script tag, you may access the javascript keys & translations from the global i18n object. ui/@types/lichess/i18n.d.ts shows the shape of available functions. Here's some examples:

  const translated = i18n.site.someString;
  const pluralTranslation = i18n.site.somePlural(count);
  const translatedAndFormatted = i18n.site.someFormatStringXY(xarg, yarg);
  const asArray = i18n.site.someFormatStringXY.asArray(xarg, yarg);