Chatium предоставляет удобный комплексный инструментарий для перевода строк интерфейса на разные языки:
Текущий язык интерфейса хранится в переменной ctx.lang и определяется на основе комбинации настроек пользователя, аккаунта и клиента (браузера или мобильного приложения).
Все строки, требующие перевода, разработчик оборачивает в вызов специальной функции ctx.t() , которая выбирает нужный перевод по ключу и текущему языку и поддерживает как простые, так и сложные переводные строки (динамические подстановки, формы множественного числа и любые другие словоформы) Пример:
// простой
ctx.t('Открыть файл')
// сложный, "Иван сделал 31 задание" или "Лена сделала 11 заданий"
ctx.t('{name} {made(gender)} {count} {tasks(count)}', {
// простая динамическая подстановка
name: student.name,
// род студента, чтобы определить словоформу слова "сделал"
gender: student.gender,
// словоформа в зависимости от рода
made: {
male: 'сделал',
female: 'сделала',
$other: 'сделал(а)',
},
count: completedTasksCount, // любое целое число
// формы множественного числа
tasks: {
one: 'задание', // 1, 21, 101
few: 'задания', // 2, 3, 4, 32, 143
$other: 'заданий', // 0, 5, 11, 26, 100
},
})
# простой
- key: Открыть файл
val: Open file
# сложный, "21 tasks has been made by Ivan"
- key: '{name} {made(gender)} {count} {tasks(count)}'
val:
# порядок слов другой, родов в английском нет
$msg: '{count} {tasks(count)} has been made by {name}'
# формы множественного числа в английском другие
tasks:
one: task
$other: tasks
Функция ctx.t()
является ключевой в подсистеме переводов и выполняет одновременно 3 функции:
Выбирает и подставляет наиболее релевантный для текущего пользователя/клиента перевод для заданного ключа, по пути выполняя все подстановки динамических переменных и умный выбор словоформ.
Задаёт собственно само значение строки в базовом языке, который задаётся в настройке i18n.keyLang
в файле .chatiumrc
.
Размечает переводные ключи в исходном коде, позволяя компилятору собирать информацию о ключах и использовать это для разного рода автоматизаций.
ctx.t(key, translateArgs)
key*: string | TranslationKey | null | undefined
string
— ключ перевода на базовом языке.
TranslationKey
— результат вызова функции t()
, которая позволяет объявить переводную строку в статическом контексте, а потом использовать её в динамическом
null | undefined
— поддержка для эргономики, чтобы не надо было писать maybe && ctx.t(maybe)
translationArgs: object
В этом аргументе задаются динамические параметры/подстановки, селекторы (словоформы) в базовом языке и/или предпочитаемое пространство имён
$ns: string
Пространство имён, в котором в первую очередь будет производиться поиск перевода данного ключа.
<dynVarName>: string | number | boolean
Динамическое значение, соответствующее переменной, указанной в ключе перевода в формате Translation key with {dynVarName} (подробнее см. ниже).
<selectorName>: InCodeSelector
Описание селектора (словоформ), соответствующее переменной, указанной в ключе перевода в формате Translation key with {selectorName}
или Translation key with {selectorName(dynVarName)}
с возможным динамическим значением (подробнее см. ниже).
string | null | undefined
Переведённая строка в выбранном языке, либо null | undefined ,
если они были на входе
В простейшем случае на вход функции подаётся простая фраза без всякой динамики и без второго аргумента, а в результате возвращается соответствующая переводная фраза один в один как указано в lang-файле или сам переданный ключ, если перевод не обнаружен.
Чуть более сложный кейс, когда один и тот же ключ можно перевести по разному в разных контекстах, тогда можно добавить второй аргумент с указанием предпочитаемого пространства имён (ключ $ns
).
// простой
ctx.t('Open file')
// использование пространства имён
ctx.t('Exit', { $ns: 'auth' })
ctx.t('Exit', { $ns: 'default' })
Однако, функция поддерживает намного более сложные сценарии переводных строк, которые могут встречаться в реальном использовании. Об этом ниже...
Часто в какое-то место переводной фразы нужно подставить какую-либо динамическую переменную, например - имя пользователя или какое-либо число. Это обычно невозможно обойти разбиением фразы на несколько и конкатенацией, поскольку в разных языках перевода эта динамическая подстановка может оказаться в совершенно разных местах. Например: 13 tasks has been done -> Выполнено 13 заданий. Ну и в целом, перевод будет качественнее, если переводить фразы целиком, а не по частям.
Для вставки в переводную фразу динамической переменной, необходимо указать название этой переменной в фигурных скобках внутри самой фразы, и указать ключ с названием этой переменной и соответствующим значением во втором аргументе функции ctx.t()
:
ctx.t('{count} tasks has been done', { count: doneTasksCount })
Название переменной должно быть строго внутри фигурных скобок без пробелов и может содержать только латинские буквы (строчные и заглавные), цифры или символ подчёркивания. Символ доллара $
зарезервирован для названий специальных переменных.
В lang-файлах в переводе фразы это же название переменной в таком же формате может быть вставлено в любом месте фразы. Может быть, также, ситуация, когда в переводе она нерелевантна и может быть опущена.
Значение динамической переменной может быть только простой строкой, числом или булевым значением. Сложные типы - объекты, массивы и т.п. не поддерживаются.
Иногда, при использовании фразы с динамической переменной, какие-либо части фразы меняют свою форму в зависимости от конкретного значения переменной. Самый распространённый случай - формы единственного/множественного числа. Для таких ситуаций функция
ctx.t()
поддерживает селекторы. В засисимости от языка и фантазии разработчика этот механизм может использоваться для многих других ситуаций.
Селекторы позволяют указать разные переводы части фразы в зависимости от значения динамической переменной. Для этого для динамической переменной во втором аргументе указывается не только само значение переменной, но и статическая "карта переводов" для различных значений:
ctx.t('You are {role}', {
role: {
$val: user.role,
Admin: 'privileged user',
Normal: 'a stranger',
},
})
В примере видно, что в качестве значения динамической переменной указано не простое значение, а объект, содержащий специальный ключ $val
(для указание динамического значения) и ключи с вариантами значений user.role
и соответствующих переводов, которые должны быть подставлены вместо {role} в исходную фразу.
Если фактически переданное в $val
значение отсутствует среди ключей селектора, то переменная будет вести себя как простая динамическая подстановка, т.е. в конечную фразу подставится само переданное значение как есть.
$other
Чтобы задать вариант перевода селектора по умолчанию, т.е. для всех остальных значений динамической переменной, которые явно не перечислены, можно использовать специальный ключ $other :
ctx.t('You are {role}', {
role: {
$val: user.role,
Admin: 'privileged user',
$other: 'an intruder with role {$val}',
},
})
Обратите внимание, что в переводе для ключа $other
может использоваться специальная динамическая переменная {$val}
, которая подставит оригинальное переданное значение переменной (без селекторов). Вместо неё можно также использовать само название переменной {role}
.
Это редко может понадобится, но важно знать, что строка значения селектора является таким же полноценным шаблоном, как и оригинальная фраза. При этом в ней можно использовать все динамические переменные оригинальной переводимой фразы. Селекторы тоже будут работать, за исключением "своего" (чтобы избежать бесконечной рекурсии).
Если в качестве значения динамической переменной передано число (typeof === 'number'), то дополнительно в вышеперечисленному поддерживаются ключи селектора с названиями множественной формы, возвращаемой функцией Intl.PluralRules.select() (zero, one, two, few, many, other). В зависимости от языка ctx.lang
могут быть разные множества вариантов. Для русского языка это 'one', 'few' и 'many', для английского - 'one' и 'many', и т.д.
ctx.t('{foundCount}', {
foundCount: {
$val: goods.length > 500 ? 500 : goods.length,
0: 'Товары не найдены',
1: 'Найден единственный товар',
one: 'Найден {$val} товар',
few: 'Найдено {foundCount} товара',
$other: 'Найдено {$val} товаров',
500: 'Найдено очень много товаров',
},
})
Обратите внимание, что в примере кроме специальных значений 'one' и 'few' используются ещё и конкретные значения чисел, чтобы перевести фразу ещё более естественно (чтобы не было "Найдено 0 товаров").
Всегда можно заменить последнее значение специального числового селектора (many или other) на специальный ключ
$other
, который гарантирует, что вы не упустили какую-нибудь опцию и не получите в этоге просто число в результате рендера селектора. Так сделано в примере выше, но это не обязательно.
$pluralType
)По умолчанию специальные формы множественного числа работают для количественных чисел. Можно переключить селектор в режим порядковых чисел с помощью специального ключа $pluralType
, который может принимать 2 значения:
cardinal
— режим количественных чисел (по умолчанию)
ordinal
— режим порядковых чисел - например для определения окончания 1st, 2nd, 33rd в английском языке:
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',
},
})
Описанные выше переменные-селекторы можно назвать "самодостаточными", поскольку они содержат и значение переменной и карту переводов для этих значений. Однако, иногда удобно, когда выбираемое значение селектора зависит от другой динамической переменной. Этого может захотеться, если динамическая переменная вставляется во фразу как есть в одном месте и при этом влияет на форму какого либо слова в другом месте фразы.
Чтобы сделать селектор зависимым от другой переменной, нужно во фразе перевода к названию селектора в фигурных скобках добавить название переменной в круглых скобках (похоже на вызов функции-селектора, которой в качестве аргумента передают другую динамическую переменную) и не указывать ключ $val
, поскольку значение в этом случае берётся из другой переменной:
ctx.t('{foundCount}. Вы уверены, что хотите {them(foundCount)} удалить?', {
foundCount: {
$val: goods.length,
one: 'Найден {$val} товар',
few: 'Найдено {foundCount} товара',
$other: 'Найдено {$val} товаров',
},
them: {
1: 'его',
$other: 'их',
},
})
Этот функционал может помочь избежать дублирования и в некоторых случаях сделать ключ более легко читаемым для программиста и переводчика.
Поскольку открывающая фигурная скобка играет особую роль в ключах переводных фраз, необходимо экранировать этот символ, если в ключе должен быть непосредственно символ {
. Для экранирования нужно использовать символ обратного слеша \
. Ниже список всех поддерживаемых экранированных символов:
\"
-> "
\\
-> \
\/
-> /
\{
-> {
\}
-> }
\b
-> \b
\f
-> \f
\n
-> \n
\r
-> \r
\t
-> \t
Язык перевода задаётся в одном из двух форматов:
Короткий - двухбуквенный код языка ISO 639-1 (не путать с кодом страны, украинский язык - uk
, а не ua
). Примеры: en
, hy
, he
.
Полный с регионом - двухбуквенный код языка + подчёркивание + двухбуквенный код страны/региона. Например: ru_KZ
, en_AU
, pt_BR
.
В таком формате язык задаётся везде: в настройках пользователя, в .chatiumrc
в названиях lang-файлов и т.п. Регистр значения не имеет. В случае с названием файла знак подчёркивания может быть заменён на дефис.
Переводы ключей, которые используются в ctx.t()
необходимо писать в специальных lang-файлах, которые представляют собой файлы в формате YAML с расширением .lang.yml
, расположенные в любой директории аккаунта, и имеющие определённую структуру.
Lang-файл можно создать в веб-IDE через кнопку Добавить файл -> Файл переводов, либо в VScode просто создав файл с правильным форматом названия.
Название lang-файла должно иметь определённый строгий формат, поскольку оно определяет ряд важных параметров. Оно состоит из нескольких частей, разделённых точкой .
:
Язык перевода, в одном из следующих форматов: ru
, en_gb
или en-au
(регистр значения не имеет). Именно эта часть определяет - к какому языку относятся все переводы, описанные в этом файле.
(Необязательно) произвольная описательная часть для удобства, может содержать точки
auto
(необязательно) - специальное дополнительное расширение, которое зарезервировано для автоматически формируемых файлов переводов. Переводы в таких файлах всегда имеют меньший приоритет по отношению к аналогичным переводам в других "ручных" файлах.
lang.yml
- собственно расширение, которое идентифицирует lang-файл.
Примеры валидных названий:
ru.lang.yml
en-us.cms.lang.yml
fr_CA.auto.lang.yml
kz.main.auto.lang.yml
Примеры невалидных названий:
ru.lang.yaml
cms.lang.yml
fr_CA.yml
main.kz.auto.lang.yml
На верхнем уровне lang-файл может иметь один из двух варинатов структур:
- key: Кey 1
val: Перевод 1
- key: Кey 2
val: Перевод 2
default:
- key: Кey 1
val: Перевод 1
- key: Кey 2
val: Перевод 2
namespace1:
- key: Кey 1
val: Другой перевод 1
- key: Кey 2
val: Другой перевод 2
В первом варианте все переводы относятся к пространству имён default
.
Единица перевода в lang-файле, это всегда объект с двумя ключами:
key
— ключ перевода ровно в том виде, в котором он записан в исходниках в первом аргументе ctx.t
val
— собственно сам перевод, который может иметь две формы:
Если перевод не содержит селекторов, требующих перевода, то это просто обыкновенная строка на целевом языке, соответствующая ключу. Она может содержать подстановки простых динамических переменных, которые поддерживает данный ключ.
Если перевод содержит селекторы, то значение val
- объект, который содержит специальный ключ $msg
c собственно строкой-шаблоном перевода и ключи с селекторами, структура которых полностью соответствует структуре селекторов в функции ctx.t()
.
- key: 'Found {countUsers}'
val:
$msg: {countUsers}
countUsers:
one: Найден {$val} пользователь
few: Найдено {countUsers} пользователя
$other: Найдено {$val} пользователей
Структура сложных подстановок в lang-файле полностью соответствует аналогичной структуре во втором аргументе функции
ctx.t()
. При этом важно понимать, что в lang-файле по определению могут быть только статические данные. Значения динамических переменных, которые могут влиять на перевод, всегда берутся из ключей, переданных во втором аргументе вызоваctx.t()
(обратите внимание на отсутствие ключа$val
в примере выше).
Язык оригинальной ключевой фразы, записанной в исходном коде и язык перевода в lang-файле могут сильно отличаться по структуре. Из-за этого, то, что в базовом языке может быть выражено обычной динамической подстановкой, в языке перевода может требовать сложных словоформ. Рассмотрим пример:
ctx.t('{name} completed the challendge', {
name: student.name,
gender: student.gender,
})
# ru.lang.yml
- key: '{name} completed the challendge'
val:
$msg: '{name} {completed(gender)} испытание'
completed:
male: завершил
female: завершила
$other: завершил(а)
В примере выше видно, что переводчик может в lang-файле объявить новый селектор, которого не предусмотрел разработчик в исходниках. Однако, такие селекторы могут зависеть только от динамических переменных, которые передаёт разработчик кода в вызове ctx.t()
.
Структура перевода в примере слегка усложнена для наглядности. Его можно записать чуть проще, но суть от этого не меняется: то что было простой подстановкой в базовой фразе, может стать селектором в переводе:
# ru.lang.yml
- key: '{name} completed the challendge'
val:
$msg: '{name} {gender} испытание'
gender:
male: завершил
female: завершила
$other: завершил(а)
Исходя из предыдущего примера, возможна и обратная ситуация, когда на базовом языке перевод фразы более "навороченный" чем на языке перевода. Из-за этого, в lang-файле можно переводить селектор как простой шаблон/строку, а не как набор вариантов:
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}м'
Или даже, если так удобнее переводчику, просто не использовать и не переводить часть селекторов:
# ru.lang.yml
- key: 'You are on the {rating} place'
val: 'Вы на {rating}м месте'
Основная идея: селекторы в переводе базируются на шаблоне перевода, а не на шаблоне ключа. Шаблон перевода может содержать новые селекторы (однако их "динамика" может зависеть только от переменных, которые программист передал в коде) или не содержать селекторы, которые присутствуют в ключе. Обе ситуации - нормальны.
Язык интерфейса, который записывается в свойство ctx.lang
, определяется в следующем порядке:
Наивысший приоритет имеет язык, явно выбранный пользователем с своём профиле. Он лежит в ctx.user.lang
, но по умолчанию он не задан.
Если язык пользователя явно не задан и запрос идёт из мобильного приложения, то используется настройка языка интерфейса мобильного приложения.
Следующий приоритет - у параметра i18n.defaultLang
, который можно опрелить в файле настроек аккаунта .chatiumrc
. (TBD)
Если ничего из вышеперечисленного не задано, по умолчанию значение en
. (TBD)