-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
How translations work
See also: Working on translations
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.
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
.
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.
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);