UI string translations
Chatium provides a convenient comprehensive toolkit for translating UI strings into different languages:
-
The current interface language is stored in the ctx.lang variable and is determined based on a combination of user, account, and client (browser or mobile app) settings.
-
All strings requiring translation are wrapped in a call to a special function ctx.t() , which selects the desired translation based on the key and the current language and supports both simple and complex translation strings (dynamic substitutions, plural forms and any other word forms) Example:
// simple
ctx.t('Open file')
// complex, "Ivan did 31 assignments" or "Lena did 11 assignments"
ctx.t('{name} {made(gender)} {count} {tasks(count)}', {
// simple dynamic substitution
name: student.name,
// student.gender to determine the word form of the word "made".
gender: student.gender,
// word form depending on gender
made: {
male: 'made',
female: 'made',
$other: 'made',
},
count: completedTasksCount, // any integer
// plural forms
tasks: {
one: 'task', // 1, 21, 101
few: 'tasks', // 2, 3, 4, 32, 143
$other: 'tasks', // 0, 5, 11, 26, 100
},
})
- The translations themselves are written to special lang files (e.g. ru.lang.yml ) that can be created in any folder of the account, which will be automatically processed and uploaded by the platform. They use the YAML format and are convenient for both human editing and various automation (auto-translations, editing interface, etc.). Example ( en.lang.yml , corresponding to the example above):
# simple
- key: Open file
val: Open file
# complex, "21 tasks have been made by Ivan".
- key: '{name} {made(gender)} {count} {tasks(count)}'
val:
# word order is different, there are no genera in English
$msg: '{count} {tasks(count)} has been made by {name}'
# plural forms are different in English
tasks:
one: task
$other: tasks
- It is assumed that the string in the base language spoken by the developer is written directly in the code - this makes it much easier to read the code without having to refer to the lang file to find the actual string by key. However, if the developer prefers, they can use abstract keys instead of actual phrases.
Function ctx.t() - how to mark translated strings
The ctx.t()
function is a key function in the translation subsystem and performs 3 functions simultaneously:
-
Selects and substitutes the most relevant translation for the current user/client for a given key, performing all dynamic variable substitutions and smart word-form selection along the way.
-
Sets the string value itself in the base language, which is set in the
i18n.keyLang
setting in the.chatiumrc
file. -
Marks the translated keys in the source code, allowing the compiler to collect information about the keys and use it for various kinds of automations.
Usage/signature
ctx.t(key, translateArgs)
Arguments
key*: string | TranslationKey | null | undefined
-
string
is the translation key in the base language. -
TranslationKey
is the result of a call to thet()
function, which allows you to declare a translation string in a static context and then use it in a dynamic context -
null | undefined
- support for ergonomics, so that you don't have to writemaybe && ctx.t(maybe)
.
translationArgs: object
.
This argument specifies dynamic parameters/substitutions, selectors (word forms) in the base language and/or preferred namespace
$ns: string
.
The namespace in which the translation of this key will be searched first.
<dynVarName>: string | number | boolean
.
A dynamic value corresponding to the variable specified in the translation key in the Translation key with {dynVarName} format (see below for details).
<selectorName>: InCodeSelector
.
Description of the selector (wordform) corresponding to the variable specified in the translation key in the format Translation key with {selectorName}
or Translation key with {selectorName(dynVarName)}
with possible dynamic value (see below for details).
Return value: string | null | undefined
.
The translated string in the selected language, or null | undefined ,
if they were in the input
In the simplest case, a simple phrase without any dynamics and without a second argument is given to the input of the function, and the result returns the corresponding translated phrase one-to-one as specified in the lang file, or the passed key itself if no translation is detected.
A slightly more complicated case is when the same key can be translated differently in different contexts, then you can add a second argument specifying the preferred namespace ($ns
key).
// simple
ctx.t('Open file')
// namespace usage
ctx.t('Exit', { $ns: 'auth' })
ctx.t('Exit', { $ns: 'default' })
However, the function supports much more complex translation string scenarios that may be encountered in real-world usage. More on that below...
Simple dynamic substitutions
Often some dynamic variable needs to be substituted at some place in a translated phrase, for example - a username or some number. This usually cannot be circumvented by phrase splitting and concatenation, because this dynamic substitution may end up in completely different places in different target languages. For example: 13 tasks has been done -> 13 tasks have been done. In general, the translation will be better if you translate phrases as a whole instead of in parts.
To insert a dynamic variable into a translated phrase, you should specify the name of this variable in curly brackets inside the phrase itself, and specify the key with the name of this variable and the corresponding value in the second argument of the ctx.t()
function:
ctx.t('{count} tasks has been done', { count: doneTasksCount })
The variable name must be strictly inside curly braces with no spaces and can only contain Latin letters (lowercase and uppercase), numbers, or the underscore character. The dollar symbol $
is reserved for special variable names.
In lang files, the same variable name in the same format may be inserted anywhere in the phrase translation. There may also be a situation where it is irrelevant in the translation and may be omitted.
The value of a dynamic variable can only be a simple string, a number, or a boolean value. Complex types - objects, arrays, etc. are not supported.
Selectors (complex word forms)
Sometimes, when using a phrase with a dynamic variable, some parts of the phrase change their form depending on the specific value of the variable. The most common case is the singular/multiple forms. For such situations, the
ctx.t()
function supports selectors. Depending on the language and the developer's imagination, this mechanism can be used for many other situations.
Selectors allow you to specify different translations of a part of a phrase depending on the value of a dynamic variable. For this purpose, for a dynamic variable, the second argument specifies not only the value of the variable itself, but also a static "translation map" for different values:
ctx.t('You are {role}', {
role: {
$val: user.role,
Admin: 'privileged user',
Normal: 'a stranger',
},
})
In the example, you can see that the value of the dynamic variable is not a simple value, but an object containing a special key $val
(to specify the dynamic value) and keys with variants of the values user.role
and the corresponding translations to be substituted instead of {role} in the original phrase.
If the value actually passed in $val
is not among the selector keys, the variable will behave as a simple dynamic substitution, i.e. the passed value itself will be substituted into the final phrase as is.
Variant $other
To set the default translation variant of the selector, i.e. for all other values of the dynamic variable that are not explicitly listed, you can use the special key $other :
ctx.t('You are {role}', {
role: {
$val: user.role,
Admin: 'privileged user',
$other: 'an intruder with role {$val}',
},
})
Note that the translation can use a special dynamic variable {$val}
for the $other
key, which will substitute the original passed value of the variable (without selectors). You can also use the variable name itself {role}
instead.
This may rarely be needed, but it is important to know that the selector value string is as much a complete template as the original phrase. That said, it can use all the dynamic variables of the original translated phrase. The selectors will work too, except for "its" (to avoid infinite recursion).
Plural Forms
If a number is passed as the value of a dynamic variable (typeof === 'number'), then in addition to the above, selector keys with the names of the plural form returned by Intl.PluralRules.select() (zero, one, two, few, many, other) are supported. Depending on the ctx.lang
language, there may be different plural choices. For Russian it is 'one', 'few' and 'many', for English it is 'one' and 'many', etc.
ctx.t('{foundCount}', {
foundCount: {
$val: goods.length > 500 ? 500 : goods.length,
0: 'No goods found',
1: 'Only one item found',
one: 'Found {$val} item',
few: 'Found {foundCount} item',
$other: 'Found {$val} items',
500: 'Found {$found {$val} item',
},
})
Note that in addition to the special values of 'one' and 'few', the example also uses specific number values to translate the phrase even more naturally (so that it is not "0 items found").
You can always replace the last value of a special numeric selector (many or other) with a special
$other
key, which ensures that you don't miss an option and get just a number as a result of rendering the selector. This is done in the example above, but it is not necessary.
Forms of ordinal numbers (parameter $pluralType
)
By default, special plural forms work for quantitative numbers. You can switch the selector to ordinal numbers with the special key $pluralType
, which can take 2 values:
cardinal
- mode of quantitative numbers (by default)
ordinal
- the mode of ordinal numbers - for example, to determine the endings 1st, 2nd, 33rd in English:
``typescript ctx.t('{name} was born in the {century} century', { name: author.name century: { $val: ~~(author.birthday.getFullYear() / 100) + 1, $pluralType: 'ordinal', one: '{$val}st', two: '{$val}nd', few: '{$val}rd', $other: '{$val}th', }, })
### Selectors parameterized by another variable
>The variable selectors described above can be called "self-contained" because they contain both the variable value and the translation map for those values. However, it is sometimes convenient when the selector value to be selected depends on another dynamic variable. This may be desirable if a dynamic variable is inserted into a phrase as it is in one place and affects the form of a word elsewhere in the phrase.
To make a selector dependent on another variable, you need to add the name of the variable in parentheses to the name of the selector in curly brackets in the translation phrase (similar to calling a selector function that is passed another dynamic variable as an argument) and not specify the `$val` key, since the value in this case is taken from the other variable:
```typescript
ctx.t('{foundCount}. Are you sure you want {them(foundCount)} deleted?", {
foundCount: {
$val: goods.length,
one: 'Found {$val} goods',
few: 'Found {foundCount} goods',
$other: 'Found {$val} goods',
},
them: {
1: 'his',
$other: 'them',
},
})
This functionality can help avoid duplication and in some cases make the key easier to read for the programmer and translator.
Special characters in translation keys
Since the opening curly brace plays a special role in translation phrase keys, it is necessary to escape this character if the key should contain the {
symbol directly. The backslash character \
must be used for escaping. Below is a list of all supported escaped characters:
-
\"
-> " -
\\
-> \ -
\/
-> / -
\{
-> { -
\}
-> } -
\b
-> \b -
\f
-> \f -
\n
-> \n -
\r
-> \r -
\t
-> \t
Format of the target language value
The target language is set in one of two formats:
-
Short - ISO 639-1 two-letter language code (not to be confused with the country code, Ukrainian is
uk
, notua
). Examples:en
,hy
,he
. -
Full with region - two-letter language code + underscore + two-letter country/region code. For example:
ru_KZ
,en_AU
,pt_BR
.
In this format language is set everywhere: in user settings, in .chatiumrc
in lang-files names, etc. The case does not matter. In case of a file name, the underscore can be replaced by a hyphen.
Lang-files - how to create and edit translation files
Translations of keys used in ctx.t()
should be written in special lang-files, which are YAML files with the extension .lang.yml
located in any directory of the account and having a certain structure.
A lang file can be created in the webIDE via the Add File -> Translation File button, or in VScode by simply creating a file with the correct file name format.
Name of lang file
The lang file name must have a certain strict format, as it defines a number of important parameters. It consists of several parts separated by .
:
-
Translation language, in one of the following formats:
ru
,en_gb
oren-au
(case is not important). This part defines which language all translations described in this file belong to. -
(Optional) arbitrary descriptive part for convenience, may contain points
-
auto
(optional) - a special additional extension, which is reserved for automatically generated translation files. Translations in such files always have a lower priority than similar translations in other "manual" files. -
lang.yml
- the actual extension that identifies the lang file.
Examples of valid names:
-
ru.lang.yml
. -
en-us.cms.lang.yml
. -
fr_CA.auto.lang.yml
-
kz.main.auto.lang.yml
Examples of invalid names:
-
ru.lang.yaml
-
cms.lang.yml
-
fr_CA.yml
-
main.kz.auto.lang.yml
lang file structure, namespaces
At the top level, a lang-file can have one of two structure variants:
- A simple translation list of the form:
- key: Key 1
val: Translation 1
- key: Keu 2
val: Translation 2
- Map whose keys are namespace names and whose values are a list of translations for that namespace:
default:
- key: Keu 1
val: Translation 1
- key: Key 2
val: Translation 2
namespace1:
- key: Keu 1
val: Other translation 1
- key: Keu 2
val: Other translation 2
```
In the first variant, all translations refer to the `default` namespace.
### Key translation structure
A translation unit in a lang file, is always an object with two keys:
`key` - the translation key exactly as it is written in the sources in the first argument of `ctx.t`.
`val` - the translation itself, which can have two forms:
- If the translation contains no selectors requiring translation, it is just an ordinary string in the target language corresponding to the key. It may contain substitutions of simple dynamic variables that this key supports.
- If the translation contains selectors, the value `val` is an object that contains a special key `$msg` with the translation pattern string itself and keys with selectors whose structure fully corresponds to the structure of the selectors in the `ctx.t()` function.
````typescript
- key: 'Found {countUsers}'
val:
$msg: {countUsers}
countUsers:
one: Found {$val} user
few: Found {countUsers} user
$other: Found {$val} users.
```
> The structure of complex substitutions in the lang-file corresponds exactly to the same structure in the second argument of the `ctx.t()` function . It is important to realize that a lang-file by definition can only contain static data. Values of dynamic variables that may affect translation are always taken from the keys passed in the second argument of the `ctx.t()` call (note the absence of the `$val` key in the example above).
### **Tips: introducing new selectors in lang-file**
The language of the original keyphrase written in the source code and the language of the translation in the lang-file can be very different in structure. Because of this, what in the source language can be expressed by a normal dynamic substitution, in the target language may require complex word forms. Consider an example:
```typescript
ctx.t('{name} completed the challendge', {
name: student.name,
gender: student.gender,
})
```
`````typescript
# ru.lang.yml
- key: '{name} completed the challendge'
val:
$msg: '{name} {completed(gender)} challenge'
completed:
male: completed
female: completed
$other: completed
```
In the example above, you can see that the translator can declare a new selector in the lang file that the developer did not provide in the source code. However, such selectors can only depend on dynamic variables passed by the code developer in the `ctx.t()` call .
The translation structure in the example is slightly complicated for clarity. It can be written a bit simpler, but the essence does not change: what was a simple substitution in the base phrase can become a selector in the translation:
```typescript
# ru.lang.yml
- key: '{name} completed the challendge'
val:
$msg: '{name} {gender} trial'
gender:
male: completed
female: completed
$other: completed
```
#### Subtleties: "simplification" of selectors in lang-file, template instead of selector
Based on the previous example, the opposite situation is also possible, where the translation of a phrase in the base language is more "tricky" than in the target language. Because of this, it is possible to translate a selector in a lang file as a simple pattern/string rather than as a set of variants:
```typescript
ctx.t('You are on the {rating} place', {
rating: {
$val: student.rating,
$pluralType: 'ordinal',
one: '{$val}st',
two: '{$val}nd',
few: '{$val}rd',
$other: '{$val}th',
},
})
```
``````typescript
# ru.lang.yml
- key: 'You are on the {rating} place'
val:
$msg: 'You are on the {rating} place'
rating: '{rating}m'
```
Or even, if it's more convenient for the translator, just don't use or translate some of the selectors:
````typescript
# ru.lang.yml
- key: 'You are on the {rating} place'
val: 'You are on the {rating} place'.
```
>**Basic idea**: selectors in a translation are based on the translation template, not the key template.
The translation pattern may contain new selectors (but their "dynamics" may depend only on the variables that the programmer passed in the code) or may not contain selectors that are present in the key. Both situations are normal.
## Namespaces ($ns) - how to declare different translations (depending on context/place) with the same key
## Translation string markup in static context (where there is no ctx)
## How the current interface language is defined
The interface language, which is written to the `ctx.lang` property, is defined in the following order:
- The language explicitly selected by the user in his profile has the highest priority. This is stored in `ctx.user.lang` , but it is not set by default.
- If the user's language is not explicitly set and the request comes from the mobile application, the mobile application interface language setting is used.
- The next priority is the `i18n.defaultLang` parameter, which can be defined in the account settings file `.chatiumrc`. (TBD)
- If none of the above is set, the default value is `en`. (TBD)