Переводы UI-строк
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
},
})
- Сами переводы записываются в специальные lang-файлы (например ru.lang.yml ), которые можно создавать в любой папке аккаунта, которые будут автоматически обработаны и загружены платформой. Они используют формат YAML и удобны как для редактирования человеком, так и для различной автоматизации (автопереводы, интерфейс редактирования и т.п.). Пример ( en.lang.yml , соответствующий примеру выше):
# простой
- 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
- Предполагается, что строка на базовом языке, на котором говорит разработчик, записывается непосредственно в коде - так гораздо проще читать код без необходимости обращаться к lang-файлу, чтобы найти реальную строку по ключу. Однако, если разработчик предпочитает, он может использовать абстрактные ключи вместо реальных фраз.
Функция ctx.t() - как размечать переводные строки
Функция 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-файлов и т.п. Регистр значения не имеет. В случае с названием файла знак подчёркивания может быть заменён на дефис.
Lang-файлы - как создавать и редактировать файлы переводов
Переводы ключей, которые используются в ctx.t() необходимо писать в специальных lang-файлах, которые представляют собой файлы в формате YAML с расширением .lang.yml , расположенные в любой директории аккаунта, и имеющие определённую структуру.
Lang-файл можно создать в веб-IDE через кнопку Добавить файл -> Файл переводов, либо в VScode просто создав файл с правильным форматом названия.
Название lang-файла
Название 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-файла, пространства имён
На верхнем уровне lang-файл может иметь один из двух варинатов структур:
- Простой список переводов вида:
- key: Кey 1
val: Перевод 1
- key: Кey 2
val: Перевод 2
- Map, ключами которого являются названия пространств имён, а значениями - список переводов для этого пространства имён:
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- объект, который содержит специальный ключ$msgc собственно строкой-шаблоном перевода и ключи с селекторами, структура которых полностью соответствует структуре селекторов в функцииctx.t().
- key: 'Found {countUsers}'
val:
$msg: {countUsers}
countUsers:
one: Найден {$val} пользователь
few: Найдено {countUsers} пользователя
$other: Найдено {$val} пользователей
Структура сложных подстановок в lang-файле полностью соответствует аналогичной структуре во втором аргументе функции
ctx.t(). При этом важно понимать, что в lang-файле по определению могут быть только статические данные. Значения динамических переменных, которые могут влиять на перевод, всегда берутся из ключей, переданных во втором аргументе вызоваctx.t()(обратите внимание на отсутствие ключа$valв примере выше).
Тонкости: введение новых селекторов в lang-файле
Язык оригинальной ключевой фразы, записанной в исходном коде и язык перевода в lang-файле могут сильно отличаться по структуре. Из-за этого, то, что в базовом языке может быть выражено обычной динамической подстановкой, в языке перевода может требовать сложных словоформ. Рассмотрим пример:
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: завершил(а)
В примере выше видно, что переводчик может в lang-файле объявить новый селектор, которого не предусмотрел разработчик в исходной ключевой фразе. Однако, такие селекторы могут зависеть только от динамических переменных, которые передаёт разработчик кода в вызове ctx.t() .
Структура перевода в примере выше слегка усложнена для наглядности. Его можно записать чуть проще, но суть от этого не меняется: структура перевода может быть более сложной и содержать больше селекторов, чем ключевая фраза в коде - если разработчик предоставил все необходимые переменные.
Тонкости: "упрощение" селекторов в lang-файле, шаблон вместо селектора
Исходя из предыдущего примера, возможна и обратная ситуация, когда на базовом языке перевод фразы более "навороченный" чем на языке перевода. Из-за этого, в 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}м месте'
Основная идея: селекторы в переводе базируются на шаблоне перевода, а не на шаблоне ключа.
Шаблон перевода может содержать новые селекторы (однако их "динамика" может зависеть только от переменных, которые программист передал в коде) или не содержать селекторы, которые присутствуют в ключе. Обе ситуации - нормальны.
Пространства имён ($ns) - как объявлять разные переводы (в зависимости от контекста/места) с одинаковым ключом
Часто бывает, что одна и та же ключевая фраза в исходном языке может переводиться по разному в разных контекстах на другой язык. Чтобы разрешить это противоречие, при вызове ctx.t можно определить пространство имён для конкретно этого контекста с помощью опции $ns и объявить разные переводы для разных пространств имён для одной и той же фразы. По умолчанию все ключи принадлежат к пространству имён default. Пример:
<input placeholder={ctx.t('Enter', { $ns: 'input' })}/>
<button>{ctx.t('Enter')}<button>
# ru.lang.yml
default:
- key: Enter
val: Войти
input:
- key: Enter
val: Ввести
В примере выше первое вхождение будет переведено как "Ввести", а второе - как "Войти".
- Если в явно указанном пространстве имён перевод для ключа не задан, будет попытка найти перевод в пространстве
default. - Если перевода нет и в пространстве
default, то выберется первый попавшийся из имеющихся.
Разметка переводных строк в статическом контексте (там, где нет ctx)
Иногда возникает необходимость объявить переводимую фразу в том месте кода, где переменная ctx недоступна - в так называемом статическом контексте, обычно это верхний уровень любого модуля. Например, если у вас есть какое-нибудь множество с фиксированным набором значений:
const roles = ['Owner', 'Admin', 'Guest'] as const
type Roles = typeof roles[number]
Чтобы объявить переводимые строки в таком месте, можно использовать специальную функцию t() , которая принимает на вход ключ перевода и возвращает объект типа TranslationKey , который потом можно передать в ctx.t() в динамическом контексте. Пример:
import { t } from '@app/i18n'
const roleLabels = {
Owner: t('Owner', { $ns: 'userRoles' }),
Admin: t('Admin'),
Guest: t('Guest'),
}
// где-то в динамическом контексте с доступом к ctx
const label = ctx.t(roleLabels[user.role])
Функция t() также поддерживает второй аргумент с опциями, аналогично второму аргументу ctx.t() (например, можно указать пространство имён через $ns или сложные селекторы), за исключением динамических переменных, которые должны быть переданы уже при вызове ctx.t().
Как определяется текущий язык интерфейса
Язык интерфейса, который записывается в свойство ctx.lang , определяется в следующем порядке:
-
Наивысший приоритет имеет язык, явно выбранный пользователем с своём профиле. Он лежит в
ctx.user.lang, но по умолчанию он не задан. -
Если язык пользователя явно не задан и запрос идёт из мобильного приложения, то используется настройка языка интерфейса мобильного приложения.
-
Следующий приоритет - глобальная настройка языка аккаунта
account.lang(может быть не задана). -
Далее используется стандартный http-заголовок
accept-language. -
Следующий приоритет - у параметра
i18n.keyLang(язык ключей переводов в коде аккаунта), который можно опрелить в файле настроек аккаунта.chatiumrc. -
Если ничего из вышеперечисленного не задано, по умолчанию значение
en.
