Утилиты пакета I18N
разработаны для интернационализации компонентов Gravity UI.
npm install --save @gravity-ui/i18n
Принимает объект options
, включающий необязательный параметр logger
для логирования предупреждений библиотеки.
Логгер должен содержать явно определенный метод log
со следующей сигнатурой:
message
— строка сообщения, которое будет записано в лог;options
— объект параметров логирования:severity
— уровень логирования сообщения, всегда принимает значениеlevel
.logger
— определяет место для записи сообщений библиотеки.extra
— дополнительные параметры с единственным строковым полемtype
, которое всегда принимает значениеi18n
.
{
"wizard": {
"label_error-widget-no-access": "No access to the chart"
}
}
{
"wizard": {
"label_error-widget-no-access": "Нет доступа к чарту"
}
}
const ru = require('./keysets/ru.json');
const en = require('./keysets/en.json');
const {I18N} = require('@gravity-ui/i18n');
const i18n = new I18N();
i18n.registerKeysets('ru', ru);
i18n.registerKeysets('en', en);
i18n.setLang('ru');
console.log(
i18n.i18n('wizard', 'label_error-widget-no-access')
); // -> "Нет доступа к чарту"
i18n.setLang('en');
console.log(
i18n.i18n('wizard', 'label_error-widget-no-access')
); // -> "No access to the chart
// Keyset allows for a simpler translations retrieval
const keyset = i18n.keyset('wizard');
console.log(
keyset('label_error-widget-no-access')
); // -> "No access to the chart"
i18n.setLang('ru');
console.log(
keyset('label_error-widget-no-access')
); // -> "Нет доступа к чарту"
// Checking if keyset has a key
if (i18n.has('wizard', 'label_error-widget-no-access')) {
i18n.i18n('wizard', 'label_error-widget-no-access')
}
Библиотека поддерживает шаблонизацию. Шаблонизируемые переменные заключаются в двойные фигурные скобки, а значения передаются в функцию i18n в форме словаря с парами «ключ-значение»:
{
"label_template": "No matches found for '{{inputValue}}' in '{{folderName}}'"
}
i18n('label_template', {inputValue: 'something', folderName: 'somewhere'}); // => No matches found for "something" in "somewhere"
Для удобной локализации ключей, зависящих от числового значения, можно использовать плюрализацию. Текущая библиотека использует правила плюрализации CLDR через API Intl.PluralRules
.
Может потребоваться добавление полифила для API Intl.Plural Rules
, если он недоступен в браузере.
Существует 6 форм множественного числа (см. resolvedOptions
):
zero
(также используется, когдаcount = 0
, даже если форма не поддерживается в языке);one
(единственное число);two
(двойственное число);few
(паукальное число для обозначения нескольких предметов);many
(множественное; также используется для дробей, если у них есть отдельный класс);other
(общая форма множественного числа, обязательная для всех языков; также используется, если язык поддерживает только одну форму).
{
"label_seconds": {
"one": "{{count}} second is left",
"other":"{{count}} seconds are left",
"zero": "No time left"
}
}
i18n('label_seconds', {count: 1}); // => 1 second
i18n('label_seconds', {count: 3}); // => 3 seconds
i18n('label_seconds', {count: 7}); // => 7 seconds
i18n('label_seconds', {count: 10}); // => 10 seconds
i18n('label_seconds', {count: 0}); // => No time left
Старый формат будет удален в версии 2.
{
"label_seconds": ["{{count}} second is left", "{{count}} seconds are left", "{{count}} seconds are left", "No time left"]
}
Ключ плюрализации содержит 4 значения, каждое из которых соответствует значению перечисления PluralForm
.| Значения перечисления: One
, Few
, Many
и None
соответственно. Имя переменной шаблона плюрализации — count
.
Так как у каждого языка свои правила плюрализации, библиотека предоставляет метод для настройки этих правил для любого выбранного языка.
Функция конфигурации принимает объект с языками в качестве ключей и функциями плюрализации в качестве значений.
Функция плюрализации принимает число и перечисление PluralForm
и должна возвращать одно из значений перечисления в зависимости от переданного числа.
const {I18N} = require('@gravity-ui/i18n');
const i18n = new I18N();
i18n.configurePluralization({
en: (count, pluralForms) => {
if (!count) return pluralForms.None;
if (count === 1) return pluralForms.One;
return pluralForms.Many;
},
});
Библиотека изначально поддерживает два языка: английский и русский.
Ключ языка — en
.
One
соответствует 1 и -1.Few
не используется.Many
соответствует любому числу, кроме 0.None
соответствует 0.
Ключ языка — ru
.
One
соответствует любому числу, оканчивающемуся на 1, кроме ±11.Few
соответствует любому числу, оканчивающемуся на 2, 3 или 4, кроме ±12, ±13 и ±14.Many
соответствует любому прочему числу, кроме 0.None
соответствует 0.
Если для языка не настроена функция плюрализации, используется набор правил для английского языка.
Глубина вложенности ключей ограничена одним уровнем (для глоссария).
Вложенность позволяет ссылаться на другие ключи в переводе, что удобно для формирования глоссариев.
Ключи
{
"nesting1": "1 $t{nesting2}",
"nesting2": "2",
}
Пример
i18n('nesting1'); // -> "1 2"
На ключи из других наборов можно ссылаться, добавляя в качестве префикса необходимо значение keysetName
.
// global/en.json
{
"app": "App"
}
// service/en.json
{
"app-service": "$t{global::app} service"
}
Для типизации функции i18nInstance.i18n
нужно выполнить несколько шагов.
Создайте JSON-файл с набором ключей, чтобы процедура типизации могла получать данные. Добавьте создание дополнительного файла data.json
в месте получения наборов ключей. Для уменьшения размера файла и ускорения парсинга в IDE замените все значения на 'str'
.
async function createFiles(keysets: Record<Lang, LangKeysets>) {
await mkdirp(DEST_PATH);
const createFilePromises = Object.keys(keysets).map((lang) => {
const keysetsJSON = JSON.stringify(keysets[lang as Lang], null, 4);
const content = umdTemplate(keysetsJSON);
const hash = getContentHash(content);
const filePath = path.resolve(DEST_PATH, `${lang}.${hash.slice(0, 8)}.js`);
// <New lines>
let typesPromise;
if (lang === 'ru') {
const keyset = keysets[lang as Lang];
Object.keys(keyset).forEach((keysetName) => {
const keyPhrases = keyset[keysetName];
Object.keys(keyPhrases).forEach((keyName) => {
// mutate object!
keyPhrases[keyName] = 'str';
});
});
const JSONForTypes = JSON.stringify(keyset, null, 4);
typesPromise = writeFile(path.resolve(DEST_PATH, `data.json`), JSONForTypes, 'utf-8');
}
// </New lines>
return Promise.all([typesPromise, writeFile(filePath, content, 'utf-8')]);
});
await Promise.all(createFilePromises);
}
В директории ui/utils/i18n
(место настройки и экспорта i18n
для дальнейшего использования всеми интерфейсами) импортируйте функцию типизации I18NFn
с вашим Keysets
. После настройки i18n
верните функцию с заданным типом.
import {I18NFn} from '@gravity-ui/i18n';
// This must be a typed import!
import type Keysets from '../../../dist/public/build/i18n/data.json';
const i18nInstance = new I18N();
type TypedI18n = I18NFn<typeof Keysets>;
// ...
export const ci18n = (i18nInstance.i18n as TypedI18n).bind(i18nInstance, 'common');
export const cui18n = (i18nInstance.i18n as TypedI18n).bind(i18nInstance, 'common.units');
export const i18n = i18nInstance.i18n.bind(i18nInstance) as TypedI18n;
Логика работы типизации
Примеры использования:
- Вызов функции с передачей ключей литералами строк:
i18n('common', 'label_subnet'); // ok
i18n('dcommon', 'label_dsubnet'); // error: Argument of type '"dcommon"' is not assignable to parameter of type ...
i18n('common', 'label_dsubnet'); // error: Argument of type '"label_dsubnet"' is not assignable to parameter of type ...
- Вызов функции с передачей строк, которые нельзя вычислить в литералы (если
ts
не может распознать тип строки, он не выдает ошибку):
const someUncomputebleString = `label_random-index-${Math.floor(Math.random() * 4)}`;
i18n('some_service', someUncomputebleString); // ok
for (let i = 0; i < 4; i++) {
i18n('some_service', `label_random-index-${i}`); // ok
}
- Вызов функции с передачей строк, которые можно вычислить в литералы:
const labelColors = ['red', 'green', 'yelllow', 'white'] as const;
for (let i = 0; i < 4; i++) {
i18n('some_service', `label_color-${labelColors[i]}`); // ok
}
const labelWrongColors = ['red', 'not-existing', 'yelllow', 'white'] as const;
for (let i = 0; i < 4; i++) {
i18n('some_service', `label_color-${labelWrongColors[i]}`); // error: Argument of type '"not-existing"' is not assignable to parameter of type ...
}
Почему нет типизации через класс
Данная функция может поломать или усложнить некоторые сценарии использования i18n, поэтому была добавлена в качестве дополнительной функциональности. Если она хорошо себя проявит, то в будущем можно будет добавить ее в класс, чтобы не вызывать экспортируемые функции.
Почему могут не работать встроенные методы
Типизация встроенных методов функций достаточно сложна для реализации обхода вложенных структур и условных типов. Именно поэтому типизация работает только в случае использования непосредственного вызова функции и вызова bind
до третьего аргумента.
Почему нельзя генерировать сразу файл .ts
, чтобы типизация выполнялась и для значений ключей
Это можно сделать, передав результирующий тип в I18NFn. Однако при больших объемах файла ts
начинает есть столько ресурсов, что это сильно тормозит IDE, чего не происходит с JSON-файлом.
Почему не типизированы остальные методы класса I18N
В принципе, их можно типизировать, и мы будем рады, если вы нам поможете это осуществить. Дело в том, что эти методы используются в 1% случаев.