@ditsmod/i18n
i18n - це скорочення від слова internationalization. Модуль @ditsmod/i18n забезпечує базову функціональність для перекладу системних повідомлень (що видає Ditsmod-застосунок під час роботи), і надає можливість для легкого розширення словників. Фактично ви використовуєте звичайні сервіси у якості словників для перекладу, тому текст для перекладу можна брати як з TypeScript-файлів, так і з баз даних. Робота @ditsmod/i18n спроектована таким чином, щоб кожен поточний модуль міг мати власний переклад, і щоб можна було змінити чи доповнити переклад будь-якого імпортованого модуля.
Проглянути код з прикладами використання @ditsmod/i18n можна у репозиторію Ditsmod, хоча зручніше його проглядати локально, тому краще спочатку клонувати його:
git clone https://github.com/ditsmod/ditsmod.git
cd ditsmod
npm i
cd examples/15*
Приклад можна запускати командою:
npm run start:dev
Встановлення
Вище показано як клонувати готовий приклад з @ditsmod/i18n, а коли вам потрібно встановити даний модуль у ваш застосунок, це можна зробити так:
npm i @ditsmod/i18n
Структура каталогів
Рекомендована структура директорій словників з перекладом є такою:
└── modulename
├── ...
├── i18n
│ ├── current
│ │ ├── _base-en
│ │ ├── de
│ │ ├── fr
│ │ ├── pl
│ │ ├── uk
│ │ └── index.ts
│ └── imported
│ ├── one
│ │ ├── de
│ │ ├── fr
│ │ ├── pl
│ │ └── uk
│ ├── two
│ │ ├── de
│ │ ├── fr
│ │ ├── pl
│ │ └── uk
│ └── index.ts
Як бачите, кожен модуль має переклад у теці i18n, яка містить дві теки:
current- переклад для поточного модуля;imported- змінений чи доповнений переклад для імпортованих модулів.
Причому лише тека current містить теку _base-en, де розміщено базові словники (в даному разі - англійською мовою), від яких відгалуджуються словники з перекладами на інші мови. У назві використано символ нижнього підкреслення щоб _base-en була постійно вгорі над іншими теками.
Базові та дочірні класи з перекладами
Як вже було сказано, словники представляють собою звичайні сервіси:
import { Dictionary, ISO639 } from '@ditsmod/i18n';
import { injectable } from '@ditsmod/core';
@injectable()
export class CommonDict implements Dictionary {
getLng(): ISO639 {
return 'en';
}
/**
* Hi, there!
*/
hi = 'Hi, there!';
/**
* Hello, ${name}!
*/
hello(name: string) {
return `Hello, ${name}!`;
}
}
Це базовий словник з англійською локалізацією. Зверніть увагу, що назва класу базового словника не містить локалі, і саме тому в даному разі клас названо не CommonEnDict, а CommonDict. Від цього словника будуть відгалуджуватись дочірні словники, в яких вже буде переклад. Словники з перекладом рекомендується називати за таким патерном: *Dict<locale>, а файл - із закінченням *.dict-<locale>.ts. Наприклад, назва класу CommonDictUk, а його файл common.dict-uk.ts.
Не обов'язково весь переклад вміщати в один словник, можна використовувати інші словники, наприклад, ErrorDict, EmailDict і т.д. Кожен базовий словник повинен імплементувати інтерфейс Dictionary, який має єдину вимогу - щоб у словнику був метод getLng(), що повертає скороч ення назви мови по стандарту ISO 639 (наприклад, скорочення для англійської та української мов - en, uk).
Чому саме метод, а не властивість повертає скорочення назв мов? Справа в тому, що у JavaScript властивість класу не можна проглядати до того моменту, поки не зробити інстанс цього класу. А от метод getLng() можна легко проглянути через YourClass.prototype.getLng(). Це дозволяє ще до використання словників отримувати статистику наявних перекладів.
Кожен клас словника, що містить переклад, повинен розширювати клас базового словника:
import { ISO639 } from '@ditsmod/i18n';
import { injectable } from '@ditsmod/core';
import { CommonDict } from '#dict/second/common.dict';
@injectable()
export class CommonDictUk extends CommonDict {
override getLng(): ISO639 {
return 'uk';
}
override hello(name: string) {
return `Привіт, ${name}!`;
}
}
Як мінімум, кожен словник з перекладом повинен переписувати метод getLng(). Для більш строгого контролю словників з перекладами, рекомендується використовувати TypeScript 4.3+, а також наступне налаштування у tsconfig.json:
{
"compilerOptions": {
"noImplicitOverride": true,
// ...
}
}
В такому разі, у дочірньому класі TypeScript буде вимагати дописувати ключове слово override перед кожним методом чи властивістю з батьківського класу. Це дозволяє покращувати читабельність доч ірнього класу і запобігає помилці у назві методу чи властивості. Якщо ви створили метод з помилкою у назві, наприклад helo замість hello, і позначили його як override, TypeScript видасть попередження, що у батьківському класі такого метода не існує. Той же самий сценарій спрацює, якщо з батьківського класу видалили раніше написаний метод.
Як бачите, назви класів словників з перекладами вже містять локаль CommonDictUk - це словник з українською локалізацією. І оскільки цей словник розширює клас базового словника, то усі відсутні переклади будуть видаватись мовою базового словника. В даному разі, у базовому словнику CommonDict є ось ця властивість:
/**
* Hi, there!
*/
hi = `Hi, there!`;
А у словнику CommonDictUk відсутній переклад цієї фрази, тому при запиті локалізації на українську мову буде використовуватись англійський варіант з базового класу.
Зверніть увагу, що над кожною властивістю чи методом, які безпосередньо використовуються для перекладу, написано коментар з шаблоном тієї фрази, яку вони будуть повертати. Це додає зручності використання словника в коді програми, бо ваша IDE буде показувати ці коментарі:
const dict = this.dictService.getDictionary(CommonDict);
dict.hi;
В даному разі при наведенні на dict.hi IDE покаже Hi, there!.
Збір словників у групи у теці current
Нагадаю, що тека current містить словники з перекладом для поточного модуля. Ці словники повинні бути зібрані в одному масиві у файлі index.ts:
import { DictGroup, getDictGroup } from '@ditsmod/i18n';
import { CommonDict } from '#dict/second/common.dict';
import { CommonDictUk } from './uk/common.dict-uk.js';
import { ErrorDict } from '#dict/second/error.dict';
import { ErrorsDictUk } from './uk/errors.dict-uk.js';
// ...
export const current: DictGroup[] = [
[CommonDict, CommonDictUk, CommonDictPl, CommonDictFr, CommonDictDe],
[ErrorDict, ErrorsDictUk, ErrorsDictPl, ErrorsDictFr, ErrorsDictDe],
// ...
];
Як бачите, в масиві передаються групи словників, де на першому місці завжди повинен йти клас з базовим словником.У даному разі передаються дві групи словників з базовими класами CommonDict та ErrorDict. Не дозволяється змішувати словники з різних груп. Якщо ви змішаєте словники з різних груп, TypeScript не зможе вам про це сказати, тому рекомендується використовувати функцію getDictGroup() для кращого контролю типів класів:
import { DictGroup, getDictGroup } from '@ditsmod/i18n';
import { CommonDict } from '#dict/second/common.dict';
import { CommonDictUk } from './uk/common.dict-uk.js';
import { ErrorDict } from '#dict/second/error.dict';
import { ErrorsDictUk } from './uk/errors.dict-uk.js';
// ...
export const current: DictGroup[] = [
getDictGroup(CommonDict, CommonDictUk, CommonDictPl, CommonDictFr, CommonDictDe),
getDictGroup(ErrorDict, ErrorsDictUk, ErrorsDictPl, ErrorsDictFr, ErrorsDictDe),
// ...
];
Збір словників у групи у теці imported
Нагадаю, що тека imported містить словники з перекладом для імпортованих модулів, причому зверніть увагу, що вона не містить базових словників (не має теки _base-en), оскільки базові словники імпортованих модулів знаходяться у самих цих модулях у каталогах current:
└── modulename
├── ...
├── i18n
│ ├── current
│ │ ├── _base-en
│ │ ├── de
│ │ ├── fr
│ │ ├── pl
│ │ ├── uk
│ │ └── index.ts
│ └── imported
│ ├── one
│ │ ├── de
│ │ ├── fr
│ │ ├── pl
│ │ └── uk
│ ├── two
│ │ ├── de
│ │ ├── fr
│ │ ├── pl
│ │ └── uk
│ └── index.ts
Директорія imported містить окремі теки кожного модуля, для яких потрібно доповнити або переписати переклад. В даному разі тека imported містить доповнення чи перепис перекладу для модулів one і two. Збір груп словників у теці imported відбувається аналогічно до того, як це робиться у current, але базові словники беруться із зовнішніх модулів:
import { DictGroup, getDictGroup } from '@ditsmod/i18n';
import { CommonDict } from '#dict/first/common.dict'; // Базовий словник із зовнішнього модуля із теки current
import { CommonDictUk } from './first/uk/common.dict-uk.js'; // Доповнення перекладу для зовнішнього модуля із теки imported
export const imported: DictGroup[] = [
getDictGroup(CommonDict, CommonDictUk),
];
В даному разі базовий словник CommonDict імпортується з FirstModule, а доповнення перекладу українською мовою береться у поточному модулі з теки imported.
Передача перекладів у модуль
Тепер залишилось передати групи словників у модуль:
import { restRootModule } from '@ditsmod/rest';
import { I18nModule, I18nOptions, I18N_TRANSLATIONS, Translations } from '@ditsmod/i18n';
import { current } from './i18n/current.js';
import { imported } from './i18n/imported.js';
const translations: Translations = { current, imported };
const i18nOptions: I18nOptions = { defaultLng: 'uk' };
@restRootModule({
imports: [
I18nModule,
// ...
],
providersPerMod: [
{ token: I18N_TRANSLATIONS, useValue: translations, multi: true },
{ token: I18nOptions, useValue: i18nOptions },
],
exports: [I18N_TRANSLATIONS]
})
export class SecondModule {}
Як бачите, кожен модуль, що містить переклад, повинен:
- імпортувати
I18nModule; - у масив
providersPerModдодавати мульти-провайдер, що містить токенI18N_TRANSLATIONSта контент з типом данихTranslations, куди якраз і передаються групи словників як для поточного, так і для імпортованого модуля; - у масив
providersPerModможна передавати провайдер з токеномI18nOptions; - у масив
exportsопціонально можна передати токенI18N_TRANSLATIONS, якщо хочете щоб базові словники з поточного модуля були доступними для зовнішніх модулів. При цьому зверніть увагу, що такий експорт потрібен лише якщо ви хочете безпосередньо використовувати базові словники, тобто у коді вашої програми ви імпортуєте їх. А якщо ви експортуєте певний сервіс, який внутрішньо використовує базові словники (інкапсулює їх використання), то експортуватиI18N_TRANSLATIONSне потрібно.
Якщо скористатись хелпером i18nProviders().i18n(), можна трохи скоротити кількість коду:
import { restModule } from '@ditsmod/rest';
import { I18N_TRANSLATIONS, I18nModule, I18nProviders } from '@ditsmod/i18n';
import { current } from './i18n/current.js';
import { imported } from './i18n/imported.js';
@restModule({
imports: [
I18nModule,
// ...
],
providersPerMod: [
...new I18nProviders().i18n({ current, imported }, { defaultLng: 'uk' }),
],
exports: [I18N_TRANSLATIONS]
})
export class SecondModule {}
У якості першого аргументу для i18nProviders().i18n() передається об'єкт з типом Translations, на другому місці передаються опції з типом I18nOptions. Зверніть увагу, що перед хелпером стоїть трикрапка, оскільки він повертає масив, який потрібно змерджити з іншими провайдерами в масиві providersPerMod.