Переводы 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 функции:

  1. Выбирает и подставляет наиболее релевантный для текущего пользователя/клиента перевод для заданного ключа, по пути выполняя все подстановки динамических переменных и умный выбор словоформ.

  2. Задаёт собственно само значение строки в базовом языке, который задаётся в настройке i18n.keyLang в файле .chatiumrc.

  3. Размечает переводные ключи в исходном коде, позволяя компилятору собирать информацию о ключах и использовать это для разного рода автоматизаций.

Применение/сигнатура

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 - объект, который содержит специальный ключ  $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-файле

Язык оригинальной ключевой фразы, записанной в исходном коде и язык перевода в 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-файле, шаблон вместо селектора

Исходя из предыдущего примера, возможна и обратная ситуация, когда на базовом языке перевод фразы более "навороченный" чем на языке перевода. Из-за этого, в 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)

Как определяется текущий язык интерфейса

Язык интерфейса, который записывается в свойство  ctx.lang , определяется в следующем порядке:

  • Наивысший приоритет имеет язык, явно выбранный пользователем с своём профиле. Он лежит в  ctx.user.lang , но по умолчанию он не задан.

  • Если язык пользователя явно не задан и запрос идёт из мобильного приложения, то используется настройка языка интерфейса мобильного приложения.

  • Следующий приоритет - у параметра  i18n.defaultLang , который можно опрелить в файле настроек аккаунта  .chatiumrc. (TBD)

  • Если ничего из вышеперечисленного не задано, по умолчанию значение en. (TBD)

❤️ Made with love on Chatium

ООО "Чатиум"

Информация о компании