@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),
// ...
];