UI String Translations
Chatium provides a comprehensive and convenient toolset for translating interface strings into different languages:
-
The current interface language is stored in the
ctx.langvariable and is determined based on a combination of user, account, and client (browser or mobile app) settings. -
All strings requiring translation should be wrapped by the developer in a call to the special
ctx.t()function, which selects the appropriate translation by key and current language and supports both simple and complex translatable strings (dynamic substitutions, plural forms, and any other word forms). Example (showing Russian as a key language here for demonstration purposes, although usually English is used as a key language):
// simple
ctx.t('Открыть файл')
// complex, "Ivan completed 31 task" or "Lena completed 11 tasks"
ctx.t('{name} {made(gender)} {count} {tasks(count)}', {
// simple dynamic substitution
name: student.name,
// student's gender to determine word form of "completed"
gender: student.gender,
// word form depending on gender
made: {
male: 'сделал',
female: 'сделала',
$other: 'сделал(а)',
},
count: completedTasksCount, // any integer
// plural forms
tasks: {
one: 'задание', // 1, 21, 101
few: 'задания', // 2, 3, 4, 32, 143
$other: 'заданий', // 0, 5, 11, 26, 100
},
})
- The translations themselves are written in special lang-files (e.g.,
ru.lang.yml), which can be created in any account folder and will be automatically processed and loaded by the platform. They use YAML format and are convenient for both human editing and various automation tasks (auto-translation, editing interfaces, etc.). Example (en.lang.yml, corresponding to the example above):
# simple
- key: Открыть файл
val: Open file
# complex, "21 tasks has been completed by Ivan"
- key: '{name} {completed(gender)} {count} {tasks(count)}'
val:
# different word order, no genders in English
$msg: '{count} {tasks(count)} has been completed by {name}'
# plural forms in English are different
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 a lang-file to find the actual string by key. For authomatic translations it's important to have a consistent key language throught the whole account's code base. However, if the developer prefers, they can use abstract keys instead of real phrases.
The ctx.t() function - how to mark translatable strings
The ctx.t() function is central to the translation subsystem and performs 3 functions simultaneously:
-
Selects and substitutes the most relevant translation for the given key for the current user/client, performing all dynamic variable substitutions and intelligent selection of word forms along the way.
-
Defines the actual value of the string in the base language, which is specified in the
i18n.keyLangsetting in the.chatiumrcfile. -
Marks translatable keys in the source code, allowing the compiler to collect information about keys and use it for various automations.
Usage/Signature
ctx.t(key, translateArgs)
Arguments
key: string | TranslationKey | null | undefined
-
string— translation key in the base language. -
TranslationKey— result of calling thet()function, which allows declaring a translatable string in a static context and then using it in a dynamic one -
null | undefined— ergonomics support, so 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 the preferred namespace
$ns: string
The namespace in which to first search for a translation of this key.
<dynVarName>: string | number | boolean
Dynamic value corresponding to the variable specified in the translation key in the format Translation key with {dynVarName} (see details below).
<selectorName>: InCodeSelector
Description of the selector (word forms) corresponding to the variable specified in the translation key in the format Translation key with {selectorName} or Translation key with {selectorName(dynVarName)} with a possible dynamic value (see details below).
Return Value: string | null | undefined
Translated string in the selected language, or null | undefined,
if they were passed as input
In the simplest case, a simple phrase without any dynamics and without a second argument is passed to the function, and the result is a corresponding translated phrase exactly as specified in the lang-file, or the passed key itself if no translation is found.
A slightly more complex case is when the same key can be translated differently in different contexts, then you can add a second argument specifying the preferred namespace (key $ns).
// simple
ctx.t('Open file')
// using namespace
ctx.t('Exit', { $ns: 'auth' })
ctx.t('Exit', { $ns: 'default' })
However, the function supports much more complex scenarios of translatable strings that can occur in real use. More on this below...
Simple Dynamic Substitutions
Often it is necessary to substitute some dynamic variable into some place in a translatable phrase, for example - a username or some number. This usually cannot be bypassed by splitting the phrase into several parts and concatenating them, since in different translation languages this dynamic substitution may end up in completely different places. For example: 13 tasks has been done -> Выполнено 13 заданий. And in general, the translation will be of better quality if you translate phrases as a whole, not in parts.
To insert a dynamic variable into a translatable phrase, you need to specify the name of this variable in curly braces inside the phrase itself, and specify a 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 without spaces and may only contain Latin letters (lowercase and uppercase), digits, or underscore. The dollar sign $ is reserved for special variable names.
In lang-files, in the translation of the phrase, this same variable name in the same format can be inserted anywhere in the phrase. There may also be a situation where in the translation it is irrelevant and can be omitted.
The value of a dynamic variable can only be a simple string, number, or 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 singular/plural 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 part of a phrase depending on the value of a dynamic variable. To do this, for a dynamic variable in the second argument, not only the value of the variable itself is specified, but also a static "translation map" for various values:
ctx.t('You are {role}', {
role: {
$val: user.role,
Admin: 'privileged user',
Normal: 'a stranger',
},
})
In the example, you can see that instead of a simple value, the dynamic variable is specified as an object containing a special key $val (to specify the dynamic value) and keys with value variants of user.role and corresponding translations that should be substituted instead of {role} in the original phrase.
If the value actually passed in $val is absent among the selector keys, then the variable will behave as a simple dynamic substitution, i.e., the passed value itself will be substituted into the final phrase as is.
The $other variant
To set a default translation variant for 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 in the translation for the $other key, you can use the special dynamic variable {$val}, which will substitute the original passed value of the variable (without selectors). Instead of it, you can also use the variable name itself {role}.
This is rarely needed, but it's important to know that the selector value string is a full-fledged template, just like the original phrase. It can use all dynamic variables from the original translatable phrase. Selectors will also work, except for "their own" (to avoid infinite recursion).
Plural forms
If a number (typeof === 'number') is passed as the value of a dynamic variable, then in addition to the above, selector keys with plural form names returned by the Intl.PluralRules.select() function (zero, one, two, few, many, other) are supported. Depending on the ctx.lang language, there may be different sets of variants. For Russian, these are 'one', 'few', and 'many', for English - 'one' and 'many', etc.
ctx.t('{foundCount}', {
foundCount: {
$val: goods.length > 500 ? 500 : goods.length,
0: 'No goods found',
1: 'Only one good found',
one: 'Found {foundCount} good',
$other: 'Found {$val} goods',
500: 'Found a lot of goods',
},
})
Note that in the example, in addition to the special values like 'one' or 'few', specific number values are also used to translate the phrase even more naturally (so that there is no "Found 0 goods").
You can always replace the last value of a special numeric selector (many or other) with the special key
$other, which ensures that you don't miss any option and don't end up with just a number as a result of selector rendering. This is done in the example above, but it is not required.
Ordinal number forms (the $pluralType parameter)
By default, special plural forms work for cardinal numbers. You can switch the selector to ordinal number mode using the special key $pluralType, which can take 2 values:
cardinal — cardinal number mode (default)
ordinal — ordinal number mode - for example, to determine the ending of 1st, 2nd, 33rd in English:
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-sufficient" because they contain both the value of the variable and the translation map for these values. However, sometimes it is convenient when the selected selector value depends on another dynamic variable. This may be desirable if a dynamic variable is inserted into the phrase as is in one place and at the same time affects the form of some word in another place in the phrase.
To make a selector dependent on another variable, you need to add the variable name in parentheses to the selector name in curly braces 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 another variable:
ctx.t('{foundCount}. Are you sure you want to delete {them(foundCount)}?', {
foundCount: {
$val: goods.length,
one: 'Found {$val} good',
few: 'Found {foundCount} goods',
$other: 'Found {$val} goods',
},
them: {
1: 'it',
$other: 'them',
},
})
This functionality can help avoid duplication and in some cases make the key more readable for the programmer and translator.
Special characters in translation keys
Since the opening curly brace plays a special role in translatable phrase keys, it is necessary to escape this character if the key should contain the { character directly. For escaping, you need to use the backslash character \. Below is a list of all supported escaped characters:
\"-> "\\-> \\/-> /\{-> {\}-> }\b-> \b\f-> \f\n-> \n\r-> \r\t-> \t
Translation language value format
The translation language is specified in one of two formats:
-
Short - two-letter ISO 639-1 language code (not to be confused with country code, Ukrainian language 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, the language is specified everywhere: in user settings, in .chatiumrc, in lang-file names, etc. Case does not matter. In the case of a file name, the underscore can be replaced with a hyphen.
Lang-files - how to create and edit translation files
Translations of keys used in ctx.t() must be written in special lang-files, which are YAML format files with the .lang.yml extension, located in any account directory, and having a specific structure.
A lang-file can be created as any other file, just must have the proper extension .lang.yml and follow naming rules below.
Lang-file name
The lang-file name must have a specific strict format, as it defines a number of important parameters. It consists of several parts separated by a dot .:
-
Translation language, in one of the following formats:
ru,en_gb, oren-au(case does not matter). This part determines which language all translations described in this file belong to. -
(Optional) arbitrary descriptive part for convenience, may contain dots
-
auto(optional) - special additional extension reserved for automatically generated translation files. Translations in such files always have lower priority relative to similar translations in human-translated files (without.auto.lang.ymlextension). -
lang.yml- the actual extension that identifies a lang-file.
Examples of valid names:
ru.lang.ymlen-us.cms.lang.ymlfr_CA.auto.lang.ymlkz.main.auto.lang.yml
Examples of invalid names:
ru.lang.yamlcms.lang.ymlfr_CA.ymlmain.kz.auto.lang.yml
Lang-files location
- Lang-files can be created in any account folder.
- There can be as many lang-files per language as needed for the developer's convenience.
- All translations found in all lang-files for the particular language are merged into a single set of translations per namespace for that language. If a key in the same namespace is translated several times, precedence is not defined, one of translations will win (however, automatic translation files with the
.auto.lang.ymlextension always have lower priority than human-translated files).
Lang-file structure, namespaces
At the top level, a lang-file can have one of two structure variants:
- Simple list of translations:
- key: Key 1
val: Translation 1
- key: Key 2
val: Translation 2
- Map, whose keys are namespace names, and values are translation lists for that namespace (recommended format):
default:
- key: Key 1
val: Translation 1
- key: Key 2
val: Translation 2
namespace1:
- key: Key 1
val: Different translation 1
- key: Key 2
val: Different translation 2
In the first variant, all translations belong to the default namespace.
Translation key structure
A translation unit in a lang-file is always an object with two keys:
key — translation key exactly as it is written in the source code in the first argument of ctx.t
val — the actual translation, which can have two forms:
-
If the translation does not contain selectors requiring translation, then it is simply 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, then the value of
valis an object that contains a special key$msgwith the actual translation template string and other keys with selectors whose structure fully corresponds to the structure of selectors in thectx.t()function.
- key: 'Found {countUsers}'
val:
$msg: {countUsers}
countUsers:
one: Найден {$val} пользователь
few: Найдено {countUsers} пользователя
$other: Найдено {$val} пользователей
The structure of complex substitutions in a lang-file fully corresponds to the similar structure in the second argument of the
ctx.t()function. It is important to understand that by definition, lang-files can only contain static data. Values of dynamic variables that can affect translation are always taken from keys passed in the second argument of thectx.t()call (note the absence of the$valkey in the example above).
Nuances: introducing new selectors in a lang-file
The language of the original key phrase written in the source code and the translation language in the lang-file can differ greatly in structure. Because of this, what can be expressed in the base language as a simple dynamic substitution may require complex word forms in the translation language. Consider an example:
ctx.t('{name} completed the challenge', {
name: student.name,
gender: student.gender,
})
# ru.lang.yml
- key: '{name} completed the challenge'
val:
$msg: '{name} {completed(gender)} испытание'
completed:
male: завершил
female: завершила
$other: завершил(а)
In the example above, you can see that the translator can declare a new selector in the lang-file that was not provided by the developer in the original key phrase. However, such selectors can only depend on dynamic variables that the code developer passes in the ctx.t() call.
The translation structure in the example above is slightly overcomplicated for clarity. It can be written a bit simpler, but the essence doesn't change: the translation structure can be more complex and contain more selectors than the key phrase in the code - given that the developer provides all the necessary variables.
Nuances: "simplifying" selectors in a lang-file, template instead of selector
Based on the previous example, the reverse situation is also possible, when the translation of a phrase in the base language is more "sophisticated" than in the translation language. Because of this, in a lang-file you can translate a selector as a simple template/string, rather than as a set of variants:
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',
},
})
# ru.lang.yml
- key: 'You are on the {rating} place'
val:
$msg: 'Вы на {rating} месте'
rating: '{rating}м'
Or even, if it's more convenient for the translator, simply not use and not translate part of the selectors:
# ru.lang.yml
- key: 'You are on the {rating} place'
val: 'Вы на {rating}м месте'
Main idea: selectors in translation are based on the translation template, not the key template.
The translation template may contain new selectors (however, their "dynamics" can only depend on 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/location) with the same key
Often it happens that the same key phrase in the source language can be translated differently in different contexts in another language. To resolve this contradiction, when calling ctx.t you can define a namespace for this specific context using the $ns option and declare different translations for different namespaces for the same phrase. By default, all keys belong to the default namespace. Example:
<input placeholder={ctx.t('Enter', { $ns: 'input' })}/>
<button>{ctx.t('Enter')}<button>
# ru.lang.yml
default:
- key: Enter
val: Войти
input:
- key: Enter
val: Ввести
In the example above, the first occurrence will be translated as "Ввести" (to input/type), and the second - as "Войти" (to enter/sign in).
- If a translation for the key is not defined in the explicitly specified namespace, an attempt will be made to find the translation in the
defaultnamespace. - If there is no translation in the
defaultnamespace either, the first available one will be selected from any namespace.
Marking translatable strings in static context (where there is no ctx)
Sometimes it is necessary to declare a translatable phrase in a place in the code where the ctx variable is not available - in a so-called static context, usually this is the top level of any module. For example, if you have some set with a fixed set of values:
const roles = ['Owner', 'Admin', 'Guest'] as const
type Roles = typeof roles[number]
To declare translatable strings in such a place, you can use the special t() function, which takes a translation key as input and returns an object of type TranslationKey, which can then be passed to ctx.t() in a dynamic context. Example:
import { t } from '@app/i18n'
const roleLabels = {
Owner: t('Owner', { $ns: 'userRoles' }),
Admin: t('Admin'),
Guest: t('Guest'),
}
// somewhere in a dynamic context with access to ctx
const label = ctx.t(roleLabels[user.role])
The t() function also supports a second argument with options, similar to the second argument of ctx.t() (for example, you can specify a namespace via $ns or complex selectors), except for dynamic variables, which must be passed when calling ctx.t().
How the current interface language is determined
The interface language, which is written to the ctx.lang property, is determined in the following order:
-
The highest priority is given to the language explicitly selected by the user in their profile. It is stored in
ctx.user.lang, but by default it is not set. -
If the user language is not explicitly set and the request comes from a mobile application, then the mobile application's interface language setting is used.
-
The next priority is the global account language setting
account.lang(may not be set). -
Then the standard http header
accept-languageis used. -
The next priority is the
i18n.keyLangparameter (the language of translation keys in the account code), which can be defined in the account settings file.chatiumrc. -
If none of the above is set, the default value is
en.
