@lakutata/i18n 3.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +347 -0
- package/dist/cjs/exports/I18n.d.ts +15 -0
- package/dist/cjs/exports/I18n.js +45 -0
- package/dist/cjs/exports/I18nMiddleware.d.ts +2 -0
- package/dist/cjs/exports/I18nMiddleware.js +21 -0
- package/dist/cjs/i18n/I18n.d.ts +313 -0
- package/dist/cjs/i18n/I18n.js +462 -0
- package/dist/cjs/i18n/Translator.d.ts +124 -0
- package/dist/cjs/i18n/Translator.js +156 -0
- package/dist/cjs/i18n/exceptions/I18nCatalogException.d.ts +15 -0
- package/dist/cjs/i18n/exceptions/I18nCatalogException.js +22 -0
- package/dist/cjs/i18n/exceptions/I18nMessageSyntaxException.d.ts +14 -0
- package/dist/cjs/i18n/exceptions/I18nMessageSyntaxException.js +21 -0
- package/dist/cjs/i18n/exceptions/I18nMissingKeyException.d.ts +12 -0
- package/dist/cjs/i18n/exceptions/I18nMissingKeyException.js +19 -0
- package/dist/cjs/i18n/exceptions/I18nMissingParameterException.d.ts +13 -0
- package/dist/cjs/i18n/exceptions/I18nMissingParameterException.js +20 -0
- package/dist/cjs/i18n/exceptions/I18nTranslatorException.d.ts +12 -0
- package/dist/cjs/i18n/exceptions/I18nTranslatorException.js +19 -0
- package/dist/cjs/i18n/interfaces/I18nOptions.d.ts +70 -0
- package/dist/cjs/i18n/interfaces/I18nOptions.js +2 -0
- package/dist/cjs/i18n/interfaces/ITranslator.d.ts +57 -0
- package/dist/cjs/i18n/interfaces/ITranslator.js +2 -0
- package/dist/cjs/i18n/lib/Catalog.d.ts +34 -0
- package/dist/cjs/i18n/lib/Catalog.js +104 -0
- package/dist/cjs/i18n/lib/CatalogTools.d.ts +86 -0
- package/dist/cjs/i18n/lib/CatalogTools.js +218 -0
- package/dist/cjs/i18n/lib/Locale.d.ts +95 -0
- package/dist/cjs/i18n/lib/Locale.js +182 -0
- package/dist/cjs/i18n/lib/LocaleTranslator.d.ts +93 -0
- package/dist/cjs/i18n/lib/LocaleTranslator.js +105 -0
- package/dist/cjs/i18n/lib/MessageFormat.d.ts +144 -0
- package/dist/cjs/i18n/lib/MessageFormat.js +372 -0
- package/dist/cjs/i18n/middleware/CommanderTranslator.d.ts +71 -0
- package/dist/cjs/i18n/middleware/CommanderTranslator.js +69 -0
- package/dist/cjs/i18n/middleware/I18nMiddleware.d.ts +126 -0
- package/dist/cjs/i18n/middleware/I18nMiddleware.js +210 -0
- package/dist/cjs/i18n/types/I18nMessages.d.ts +24 -0
- package/dist/cjs/i18n/types/I18nMessages.js +2 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/exports/I18n.js +14 -0
- package/dist/esm/exports/I18nMiddleware.js +2 -0
- package/dist/esm/i18n/I18n.js +454 -0
- package/dist/esm/i18n/Translator.js +153 -0
- package/dist/esm/i18n/exceptions/I18nCatalogException.js +18 -0
- package/dist/esm/i18n/exceptions/I18nMessageSyntaxException.js +17 -0
- package/dist/esm/i18n/exceptions/I18nMissingKeyException.js +15 -0
- package/dist/esm/i18n/exceptions/I18nMissingParameterException.js +16 -0
- package/dist/esm/i18n/exceptions/I18nTranslatorException.js +15 -0
- package/dist/esm/i18n/interfaces/I18nOptions.js +1 -0
- package/dist/esm/i18n/interfaces/ITranslator.js +1 -0
- package/dist/esm/i18n/lib/Catalog.js +96 -0
- package/dist/esm/i18n/lib/CatalogTools.js +213 -0
- package/dist/esm/i18n/lib/Locale.js +173 -0
- package/dist/esm/i18n/lib/LocaleTranslator.js +101 -0
- package/dist/esm/i18n/lib/MessageFormat.js +367 -0
- package/dist/esm/i18n/middleware/CommanderTranslator.js +66 -0
- package/dist/esm/i18n/middleware/I18nMiddleware.js +206 -0
- package/dist/esm/i18n/types/I18nMessages.js +1 -0
- package/dist/types/exports/I18n.d.ts +15 -0
- package/dist/types/exports/I18nMiddleware.d.ts +2 -0
- package/dist/types/i18n/I18n.d.ts +313 -0
- package/dist/types/i18n/Translator.d.ts +124 -0
- package/dist/types/i18n/exceptions/I18nCatalogException.d.ts +15 -0
- package/dist/types/i18n/exceptions/I18nMessageSyntaxException.d.ts +14 -0
- package/dist/types/i18n/exceptions/I18nMissingKeyException.d.ts +12 -0
- package/dist/types/i18n/exceptions/I18nMissingParameterException.d.ts +13 -0
- package/dist/types/i18n/exceptions/I18nTranslatorException.d.ts +12 -0
- package/dist/types/i18n/interfaces/I18nOptions.d.ts +70 -0
- package/dist/types/i18n/interfaces/ITranslator.d.ts +57 -0
- package/dist/types/i18n/lib/Catalog.d.ts +34 -0
- package/dist/types/i18n/lib/CatalogTools.d.ts +86 -0
- package/dist/types/i18n/lib/Locale.d.ts +95 -0
- package/dist/types/i18n/lib/LocaleTranslator.d.ts +93 -0
- package/dist/types/i18n/lib/MessageFormat.d.ts +144 -0
- package/dist/types/i18n/middleware/CommanderTranslator.d.ts +71 -0
- package/dist/types/i18n/middleware/I18nMiddleware.d.ts +126 -0
- package/dist/types/i18n/types/I18nMessages.d.ts +24 -0
- package/package.json +60 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.Translator = void 0;
|
|
13
|
+
const core_1 = require("@lakutata/core");
|
|
14
|
+
const di_1 = require("@lakutata/core/decorator/di");
|
|
15
|
+
const I18nTranslatorException_js_1 = require("./exceptions/I18nTranslatorException.js");
|
|
16
|
+
/**
|
|
17
|
+
* The messages in the language of the current request. With the {@link I18nMiddleware} on the entrypoint, each request
|
|
18
|
+
* gets one `Translator` of its negotiated language, shared by the objects of the request: the controller, the
|
|
19
|
+
* `@Scoped` providers and the access control rules inject it with `@Inject(Translator)`. It is a scoped provider,
|
|
20
|
+
* autoloaded: it needs no registration. Without the middleware (or out of a request), its methods throw
|
|
21
|
+
* {@link I18nTranslatorException}; use {@link I18n.translator} there.
|
|
22
|
+
* @example
|
|
23
|
+
* ```typescript
|
|
24
|
+
* import {Application} from 'lakutata'
|
|
25
|
+
* import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
|
|
26
|
+
* import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
|
|
27
|
+
* import {CLIAction, GET} from 'lakutata/decorator/ctrl'
|
|
28
|
+
* import {Inject} from 'lakutata/decorator/di'
|
|
29
|
+
* import {buildI18nOptions, I18nMiddleware, Translator} from 'lakutata/com/i18n'
|
|
30
|
+
*
|
|
31
|
+
* class OrderController extends Controller {
|
|
32
|
+
* @Inject(Translator)
|
|
33
|
+
* protected readonly translator: Translator
|
|
34
|
+
*
|
|
35
|
+
* //GET /orders/count?lang=zh-CN, or the lang cookie, or Accept-Language
|
|
36
|
+
* @GET('/orders/count')
|
|
37
|
+
* @CLIAction('count-orders')
|
|
38
|
+
* public async count(): Promise<string> {
|
|
39
|
+
* return `${this.translator.locale}: ${this.translator.t('order.count', {count: 3})}`
|
|
40
|
+
* }
|
|
41
|
+
* }
|
|
42
|
+
*
|
|
43
|
+
* Application.run(() => ({
|
|
44
|
+
* id: 'shop.app',
|
|
45
|
+
* name: 'Shop',
|
|
46
|
+
* components: {
|
|
47
|
+
* i18n: buildI18nOptions({
|
|
48
|
+
* messages: {
|
|
49
|
+
* 'en': {order: {count: '{count, plural, one {# order} other {# orders}}'}},
|
|
50
|
+
* 'zh-CN': {order: {count: '{count} 个订单'}}
|
|
51
|
+
* }
|
|
52
|
+
* }),
|
|
53
|
+
* entrypoint: buildEntrypoints({
|
|
54
|
+
* controllers: [OrderController],
|
|
55
|
+
* middlewares: [I18nMiddleware],
|
|
56
|
+
* http: expressHTTPEntrypoint({port: 3000})
|
|
57
|
+
* })
|
|
58
|
+
* },
|
|
59
|
+
* bootstrap: ['entrypoint']
|
|
60
|
+
* }))
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
let Translator = class Translator extends core_1.Provider {
|
|
64
|
+
#translator;
|
|
65
|
+
/**
|
|
66
|
+
* Create the translator of the language given by the middleware, when it gave one.
|
|
67
|
+
*/
|
|
68
|
+
async init() {
|
|
69
|
+
if (this.i18n && this.locale)
|
|
70
|
+
this.#translator = this.i18n.translator(this.locale);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The translator of the request's language.
|
|
74
|
+
* @throws {I18nTranslatorException} When the middleware did not give the language (no {@link I18nMiddleware} on
|
|
75
|
+
* the entrypoint, or out of a request).
|
|
76
|
+
*/
|
|
77
|
+
get translator() {
|
|
78
|
+
if (!this.#translator)
|
|
79
|
+
throw new I18nTranslatorException_js_1.I18nTranslatorException('The translator of a request is given by the I18nMiddleware of its entrypoint: buildEntrypoints({middlewares: [I18nMiddleware]})');
|
|
80
|
+
return this.#translator;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Give the message of a key in the request's language, formatted with its arguments (see {@link I18n.t}).
|
|
84
|
+
* @param key The key of the message (`order.count`).
|
|
85
|
+
* @param params The arguments of the message, by their names.
|
|
86
|
+
* @returns The formatted message. With `missing: 'key'`, the key itself when it is missing, `{name}` in place of a
|
|
87
|
+
* missing argument, a plain number for a currency argument without a currency (each with a warning, once).
|
|
88
|
+
* @throws {I18nTranslatorException} When the entrypoint has no {@link I18nMiddleware}, or out of a request.
|
|
89
|
+
* @throws {I18nMissingKeyException} When no message has the key and the `missing` option is `'throw'`.
|
|
90
|
+
* @throws {I18nMissingParameterException} When an argument is missing, or a currency argument has no currency, and
|
|
91
|
+
* the `missing` option is `'throw'`.
|
|
92
|
+
* @throws {I18nMessageSyntaxException} When the message is not valid ICU MessageFormat.
|
|
93
|
+
*/
|
|
94
|
+
t(key, params) {
|
|
95
|
+
return this.translator.t(key, params);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Tell whether a key has a message in the request's language or in the languages it falls back to.
|
|
99
|
+
* @param key The key of the message.
|
|
100
|
+
* @returns `true` when {@link Translator.t} finds a message for the key.
|
|
101
|
+
* @throws {I18nTranslatorException} When the entrypoint has no {@link I18nMiddleware}, or out of a request.
|
|
102
|
+
*/
|
|
103
|
+
has(key) {
|
|
104
|
+
return this.translator.has(key);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Format an ICU MessageFormat message which is not in the catalogs, in the request's language.
|
|
108
|
+
* @param message The message (`Hello, {name}`).
|
|
109
|
+
* @param params The arguments of the message, by their names.
|
|
110
|
+
* @returns The formatted message. With `missing: 'key'`, `{name}` in place of a missing argument, a plain number
|
|
111
|
+
* for a currency argument without a currency (each with a warning, once).
|
|
112
|
+
* @throws {I18nTranslatorException} When the entrypoint has no {@link I18nMiddleware}, or out of a request.
|
|
113
|
+
* @throws {I18nMessageSyntaxException} When the message is not valid ICU MessageFormat.
|
|
114
|
+
* @throws {I18nMissingParameterException} When an argument is missing, or a currency argument has no currency, and
|
|
115
|
+
* the `missing` option is `'throw'`.
|
|
116
|
+
*/
|
|
117
|
+
format(message, params) {
|
|
118
|
+
return this.translator.format(message, params);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Format a number in the request's language (`1234.5` gives `1,234.5` in English, `1.234,5` in German).
|
|
122
|
+
* @param value The number.
|
|
123
|
+
* @param options The options of `Intl.NumberFormat` (`{style: 'currency', currency: 'EUR'}`); the component's
|
|
124
|
+
* `currency` option is not applied here.
|
|
125
|
+
* @returns The formatted number.
|
|
126
|
+
* @throws {I18nTranslatorException} When the entrypoint has no {@link I18nMiddleware}, or out of a request.
|
|
127
|
+
*/
|
|
128
|
+
number(value, options) {
|
|
129
|
+
return this.translator.number(value, options);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Format a date (and its time when the options ask for it) in the request's language, in the component's
|
|
133
|
+
* `timeZone` (the process's one without it).
|
|
134
|
+
* @param value The date, or a timestamp in milliseconds.
|
|
135
|
+
* @param options The options of `Intl.DateTimeFormat` (`{dateStyle: 'long'}`); their `timeZone` replaces the
|
|
136
|
+
* component's one.
|
|
137
|
+
* @returns The formatted date.
|
|
138
|
+
* @throws {I18nTranslatorException} When the entrypoint has no {@link I18nMiddleware}, or out of a request.
|
|
139
|
+
*/
|
|
140
|
+
date(value, options) {
|
|
141
|
+
return this.translator.date(value, options);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
exports.Translator = Translator;
|
|
145
|
+
__decorate([
|
|
146
|
+
(0, di_1.Configurable)(core_1.DTO.String().optional()),
|
|
147
|
+
__metadata("design:type", String)
|
|
148
|
+
], Translator.prototype, "locale", void 0);
|
|
149
|
+
__decorate([
|
|
150
|
+
(0, di_1.Configurable)(core_1.DTO.Any().optional()),
|
|
151
|
+
__metadata("design:type", Function)
|
|
152
|
+
], Translator.prototype, "i18n", void 0);
|
|
153
|
+
exports.Translator = Translator = __decorate([
|
|
154
|
+
(0, di_1.Scoped)(),
|
|
155
|
+
(0, di_1.Autoload)()
|
|
156
|
+
], Translator);
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Exception } from '@lakutata/core';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when the messages or the configuration of the {@link I18n} component are invalid: a missing directory, a file
|
|
4
|
+
* which is not valid JSON, a file or directory name which is not a language tag, a key part containing `.`, a value
|
|
5
|
+
* which is neither a string nor an object, an option (`defaultLocale`, `locales`, `fallback`, `messages`) which is not
|
|
6
|
+
* a language tag, or an alias of `directory` without an application to resolve it. The component throws it when it is
|
|
7
|
+
* created, {@link I18n.addMessages} and {@link loadCatalogs} when they are called, and {@link generateMessageTypes} when
|
|
8
|
+
* the default language has no messages. Its `errno` is `'E_I18N_CATALOG'`.
|
|
9
|
+
*/
|
|
10
|
+
export declare class I18nCatalogException extends Exception {
|
|
11
|
+
/**
|
|
12
|
+
* `'E_I18N_CATALOG'`.
|
|
13
|
+
*/
|
|
14
|
+
errno: string | number;
|
|
15
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.I18nCatalogException = void 0;
|
|
4
|
+
const core_1 = require("@lakutata/core");
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when the messages or the configuration of the {@link I18n} component are invalid: a missing directory, a file
|
|
7
|
+
* which is not valid JSON, a file or directory name which is not a language tag, a key part containing `.`, a value
|
|
8
|
+
* which is neither a string nor an object, an option (`defaultLocale`, `locales`, `fallback`, `messages`) which is not
|
|
9
|
+
* a language tag, or an alias of `directory` without an application to resolve it. The component throws it when it is
|
|
10
|
+
* created, {@link I18n.addMessages} and {@link loadCatalogs} when they are called, and {@link generateMessageTypes} when
|
|
11
|
+
* the default language has no messages. Its `errno` is `'E_I18N_CATALOG'`.
|
|
12
|
+
*/
|
|
13
|
+
class I18nCatalogException extends core_1.Exception {
|
|
14
|
+
constructor() {
|
|
15
|
+
super(...arguments);
|
|
16
|
+
/**
|
|
17
|
+
* `'E_I18N_CATALOG'`.
|
|
18
|
+
*/
|
|
19
|
+
this.errno = 'E_I18N_CATALOG';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.I18nCatalogException = I18nCatalogException;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Exception } from '@lakutata/core';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when a message is not valid ICU MessageFormat: an unclosed brace, an unknown argument type or style, a
|
|
4
|
+
* `plural`, `selectordinal` or `select` without its `other` option, a repeated selector. Its message names the
|
|
5
|
+
* language, the key and the position (`zh-CN: order.count: the "other" option is required at 1`). A catalog message is
|
|
6
|
+
* parsed the first time it is used, so {@link I18n.t} throws it then; {@link I18n.format} and {@link parseMessage}
|
|
7
|
+
* throw it at once. Its `errno` is `'E_I18N_MESSAGE_SYNTAX'`.
|
|
8
|
+
*/
|
|
9
|
+
export declare class I18nMessageSyntaxException extends Exception {
|
|
10
|
+
/**
|
|
11
|
+
* `'E_I18N_MESSAGE_SYNTAX'`.
|
|
12
|
+
*/
|
|
13
|
+
errno: string | number;
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.I18nMessageSyntaxException = void 0;
|
|
4
|
+
const core_1 = require("@lakutata/core");
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when a message is not valid ICU MessageFormat: an unclosed brace, an unknown argument type or style, a
|
|
7
|
+
* `plural`, `selectordinal` or `select` without its `other` option, a repeated selector. Its message names the
|
|
8
|
+
* language, the key and the position (`zh-CN: order.count: the "other" option is required at 1`). A catalog message is
|
|
9
|
+
* parsed the first time it is used, so {@link I18n.t} throws it then; {@link I18n.format} and {@link parseMessage}
|
|
10
|
+
* throw it at once. Its `errno` is `'E_I18N_MESSAGE_SYNTAX'`.
|
|
11
|
+
*/
|
|
12
|
+
class I18nMessageSyntaxException extends core_1.Exception {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(...arguments);
|
|
15
|
+
/**
|
|
16
|
+
* `'E_I18N_MESSAGE_SYNTAX'`.
|
|
17
|
+
*/
|
|
18
|
+
this.errno = 'E_I18N_MESSAGE_SYNTAX';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.I18nMessageSyntaxException = I18nMessageSyntaxException;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Exception } from '@lakutata/core';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by {@link I18n.t} (and the translators) when no message has the key, neither in the language asked nor in the
|
|
4
|
+
* languages it falls back to, and the `missing` option is `'throw'` (the default outside `NODE_ENV=production`). Its
|
|
5
|
+
* message lists the languages looked up. Its `errno` is `'E_I18N_MISSING_KEY'`.
|
|
6
|
+
*/
|
|
7
|
+
export declare class I18nMissingKeyException extends Exception {
|
|
8
|
+
/**
|
|
9
|
+
* `'E_I18N_MISSING_KEY'`.
|
|
10
|
+
*/
|
|
11
|
+
errno: string | number;
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.I18nMissingKeyException = void 0;
|
|
4
|
+
const core_1 = require("@lakutata/core");
|
|
5
|
+
/**
|
|
6
|
+
* Thrown by {@link I18n.t} (and the translators) when no message has the key, neither in the language asked nor in the
|
|
7
|
+
* languages it falls back to, and the `missing` option is `'throw'` (the default outside `NODE_ENV=production`). Its
|
|
8
|
+
* message lists the languages looked up. Its `errno` is `'E_I18N_MISSING_KEY'`.
|
|
9
|
+
*/
|
|
10
|
+
class I18nMissingKeyException extends core_1.Exception {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
/**
|
|
14
|
+
* `'E_I18N_MISSING_KEY'`.
|
|
15
|
+
*/
|
|
16
|
+
this.errno = 'E_I18N_MISSING_KEY';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.I18nMissingKeyException = I18nMissingKeyException;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Exception } from '@lakutata/core';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown when a message is formatted without one of its arguments and the `missing` option is `'throw'` (the default
|
|
4
|
+
* outside `NODE_ENV=production`), or a `{price, number, currency}` argument has no currency (neither in its style, nor
|
|
5
|
+
* in the `currency` argument, nor in the component's `currency` option) and the `missing` option is `'throw'`. Its
|
|
6
|
+
* `errno` is `'E_I18N_MISSING_PARAMETER'`.
|
|
7
|
+
*/
|
|
8
|
+
export declare class I18nMissingParameterException extends Exception {
|
|
9
|
+
/**
|
|
10
|
+
* `'E_I18N_MISSING_PARAMETER'`.
|
|
11
|
+
*/
|
|
12
|
+
errno: string | number;
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.I18nMissingParameterException = void 0;
|
|
4
|
+
const core_1 = require("@lakutata/core");
|
|
5
|
+
/**
|
|
6
|
+
* Thrown when a message is formatted without one of its arguments and the `missing` option is `'throw'` (the default
|
|
7
|
+
* outside `NODE_ENV=production`), or a `{price, number, currency}` argument has no currency (neither in its style, nor
|
|
8
|
+
* in the `currency` argument, nor in the component's `currency` option) and the `missing` option is `'throw'`. Its
|
|
9
|
+
* `errno` is `'E_I18N_MISSING_PARAMETER'`.
|
|
10
|
+
*/
|
|
11
|
+
class I18nMissingParameterException extends core_1.Exception {
|
|
12
|
+
constructor() {
|
|
13
|
+
super(...arguments);
|
|
14
|
+
/**
|
|
15
|
+
* `'E_I18N_MISSING_PARAMETER'`.
|
|
16
|
+
*/
|
|
17
|
+
this.errno = 'E_I18N_MISSING_PARAMETER';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.I18nMissingParameterException = I18nMissingParameterException;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Exception } from '@lakutata/core';
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by the methods of a {@link Translator} which was not given its language by the {@link I18nMiddleware}: the
|
|
4
|
+
* entrypoint has no `I18nMiddleware`, or the `Translator` is used out of a request. Its `errno` is
|
|
5
|
+
* `'E_I18N_TRANSLATOR'`.
|
|
6
|
+
*/
|
|
7
|
+
export declare class I18nTranslatorException extends Exception {
|
|
8
|
+
/**
|
|
9
|
+
* `'E_I18N_TRANSLATOR'`.
|
|
10
|
+
*/
|
|
11
|
+
errno: string | number;
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.I18nTranslatorException = void 0;
|
|
4
|
+
const core_1 = require("@lakutata/core");
|
|
5
|
+
/**
|
|
6
|
+
* Thrown by the methods of a {@link Translator} which was not given its language by the {@link I18nMiddleware}: the
|
|
7
|
+
* entrypoint has no `I18nMiddleware`, or the `Translator` is used out of a request. Its `errno` is
|
|
8
|
+
* `'E_I18N_TRANSLATOR'`.
|
|
9
|
+
*/
|
|
10
|
+
class I18nTranslatorException extends core_1.Exception {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
/**
|
|
14
|
+
* `'E_I18N_TRANSLATOR'`.
|
|
15
|
+
*/
|
|
16
|
+
this.errno = 'E_I18N_TRANSLATOR';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.I18nTranslatorException = I18nTranslatorException;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { NestedMessages } from '../lib/Catalog.js';
|
|
2
|
+
/**
|
|
3
|
+
* The options of the {@link I18n} component, given to {@link buildI18nOptions}. The messages come from `directory`
|
|
4
|
+
* (JSON files read when the component is created), from `messages` (bundled with the code), or from both; without
|
|
5
|
+
* either, the component starts with no messages.
|
|
6
|
+
*/
|
|
7
|
+
export interface I18nOptions {
|
|
8
|
+
/**
|
|
9
|
+
* The directories holding the messages, read when the component is created, the later directories replacing the
|
|
10
|
+
* same keys of the earlier ones. A path is resolved from the working directory; a path starting with `@` is an
|
|
11
|
+
* alias of the application (`@app/locales`). In a directory, each language is either a JSON file named by its tag
|
|
12
|
+
* (`zh-CN.json`) or a directory named by its tag whose files and subdirectories prefix their keys
|
|
13
|
+
* (`zh-CN/order.json` gives `order.*`, `zh-CN/admin/user.json` gives `admin.user.*`). Files not ending in `.json`
|
|
14
|
+
* are ignored; a missing directory, invalid JSON or a name which is not a language tag fails the component's creation
|
|
15
|
+
* with {@link I18nCatalogException}.
|
|
16
|
+
* @default none
|
|
17
|
+
*/
|
|
18
|
+
directory?: string | string[];
|
|
19
|
+
/**
|
|
20
|
+
* The languages {@link I18n.negotiate} chooses from (and the requests are answered in), as language tags.
|
|
21
|
+
* @default the default language, then the languages of the messages in alphabetical order
|
|
22
|
+
*/
|
|
23
|
+
locales?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* The default language: the last language a message is looked up in, the language of {@link I18n.t} called
|
|
26
|
+
* without one, and the language chosen when none of the requested ones is supported.
|
|
27
|
+
* @default 'en'
|
|
28
|
+
*/
|
|
29
|
+
defaultLocale?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The languages a message missing in a language is looked up in next, before the less specific tags and the default
|
|
32
|
+
* language (`{'zh-TW': 'zh-CN'}`, or several in their order: `{'pt-PT': ['pt-BR', 'es']}`). The fallbacks of a
|
|
33
|
+
* fallback are followed too.
|
|
34
|
+
* @default none
|
|
35
|
+
*/
|
|
36
|
+
fallback?: Record<string, string | string[]>;
|
|
37
|
+
/**
|
|
38
|
+
* The messages by language tag, nested by the parts of their keys (object literals or imported JSON), added after
|
|
39
|
+
* the ones of `directory` and replacing their keys. They are bundled with the code: no file is read.
|
|
40
|
+
* @default none
|
|
41
|
+
*/
|
|
42
|
+
messages?: Record<string, NestedMessages>;
|
|
43
|
+
/**
|
|
44
|
+
* What a missing message, argument or currency does: `'throw'` throws {@link I18nMissingKeyException} or
|
|
45
|
+
* {@link I18nMissingParameterException}; `'key'` gives the key in place of a missing message, writes a missing
|
|
46
|
+
* argument as `{name}`, and formats a `{price, number, currency}` argument without a currency as a plain number,
|
|
47
|
+
* each with a process warning `I18nWarning` emitted once (per key and language, or per message and argument).
|
|
48
|
+
* @default 'throw', and 'key' when NODE_ENV is 'production'
|
|
49
|
+
*/
|
|
50
|
+
missing?: 'throw' | 'key';
|
|
51
|
+
/**
|
|
52
|
+
* The currency (ISO 4217: `CNY`, `USD`) of the `{price, number, currency}` arguments whose style and arguments give
|
|
53
|
+
* none (`currency/EUR`, a `currency` argument).
|
|
54
|
+
* @default none (such a message throws I18nMissingParameterException, or is formatted as a plain number with
|
|
55
|
+
* `missing: 'key'`)
|
|
56
|
+
*/
|
|
57
|
+
currency?: string;
|
|
58
|
+
/**
|
|
59
|
+
* The time zone (IANA: `Asia/Shanghai`) of the dates and the times of the messages and of the translators' `date()`.
|
|
60
|
+
* @default the process's time zone
|
|
61
|
+
*/
|
|
62
|
+
timeZone?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Whether the built-in validation error messages of the supported languages are registered when the component is
|
|
65
|
+
* created (and of the languages added by `addMessages()`), so that the validation errors of a request are in its language. The `validation.<code>` messages of the
|
|
66
|
+
* application (`validation.any.required`) are registered either way and replace the built-in ones.
|
|
67
|
+
* @default true
|
|
68
|
+
*/
|
|
69
|
+
validationMessages?: boolean;
|
|
70
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { MessageKey, MessageParams } from '../types/I18nMessages.js';
|
|
2
|
+
/**
|
|
3
|
+
* The messages of one language, without giving the language at each call: {@link I18n.translator} gives one for a
|
|
4
|
+
* language, and the {@link Translator} injected in the objects of a request is the one of the request's language.
|
|
5
|
+
*/
|
|
6
|
+
export interface ITranslator {
|
|
7
|
+
/**
|
|
8
|
+
* The language tag of the translator (`zh-CN`).
|
|
9
|
+
*/
|
|
10
|
+
readonly locale: string;
|
|
11
|
+
/**
|
|
12
|
+
* Give the message of a key in the translator's language, formatted with its arguments (see {@link I18n.t}).
|
|
13
|
+
* @param key The key of the message (`order.count`).
|
|
14
|
+
* @param params The arguments of the message, by their names.
|
|
15
|
+
* @returns The formatted message. With `missing: 'key'`, the key itself when it is missing, `{name}` in place of a
|
|
16
|
+
* missing argument, a plain number for a currency argument without a currency (each with a warning, once).
|
|
17
|
+
* @throws {I18nMissingKeyException} When no message has the key and the `missing` option is `'throw'`.
|
|
18
|
+
* @throws {I18nMissingParameterException} When an argument is missing, or a currency argument has no currency, and
|
|
19
|
+
* the `missing` option is `'throw'`.
|
|
20
|
+
* @throws {I18nMessageSyntaxException} When the message is not valid ICU MessageFormat.
|
|
21
|
+
*/
|
|
22
|
+
t<K extends MessageKey>(key: K, params?: MessageParams<K>): string;
|
|
23
|
+
/**
|
|
24
|
+
* Tell whether a key has a message in the translator's language or in the languages it falls back to.
|
|
25
|
+
* @param key The key of the message.
|
|
26
|
+
* @returns `true` when {@link ITranslator.t} finds a message for the key.
|
|
27
|
+
*/
|
|
28
|
+
has(key: string): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Format an ICU MessageFormat message which is not in the catalogs, in the translator's language.
|
|
31
|
+
* @param message The message (`Hello, {name}`).
|
|
32
|
+
* @param params The arguments of the message, by their names.
|
|
33
|
+
* @returns The formatted message. With `missing: 'key'`, `{name}` in place of a missing argument, a plain number
|
|
34
|
+
* for a currency argument without a currency (each with a warning, once).
|
|
35
|
+
* @throws {I18nMessageSyntaxException} When the message is not valid ICU MessageFormat.
|
|
36
|
+
* @throws {I18nMissingParameterException} When an argument is missing, or a currency argument has no currency, and
|
|
37
|
+
* the `missing` option is `'throw'`.
|
|
38
|
+
*/
|
|
39
|
+
format(message: string, params?: Record<string, unknown>): string;
|
|
40
|
+
/**
|
|
41
|
+
* Format a number in the translator's language (`1234.5` gives `1,234.5` in English, `1.234,5` in German).
|
|
42
|
+
* @param value The number.
|
|
43
|
+
* @param options The options of `Intl.NumberFormat` (`{style: 'currency', currency: 'EUR'}`); the component's
|
|
44
|
+
* `currency` option is not applied here.
|
|
45
|
+
* @returns The formatted number.
|
|
46
|
+
*/
|
|
47
|
+
number(value: number | bigint, options?: Intl.NumberFormatOptions): string;
|
|
48
|
+
/**
|
|
49
|
+
* Format a date (and its time when the options ask for it) in the translator's language, in the component's
|
|
50
|
+
* `timeZone` (the process's one without it).
|
|
51
|
+
* @param value The date, or a timestamp in milliseconds.
|
|
52
|
+
* @param options The options of `Intl.DateTimeFormat` (`{dateStyle: 'long'}`); their `timeZone` replaces the
|
|
53
|
+
* component's one.
|
|
54
|
+
* @returns The formatted date.
|
|
55
|
+
*/
|
|
56
|
+
date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
|
|
57
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The messages of a language, nested by the parts of their keys: `{order: {count: '...'}}` holds the key
|
|
3
|
+
* `order.count`. A key part cannot be empty or contain `.`, and every leaf is a string (ICU MessageFormat, or a
|
|
4
|
+
* validation template under `validation`).
|
|
5
|
+
*/
|
|
6
|
+
export type NestedMessages = {
|
|
7
|
+
[key: string]: string | NestedMessages;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Loaded messages, by canonical language tag then by full key (`order.count`), as {@link loadCatalogs} gives them to
|
|
11
|
+
* {@link checkCatalogs} and {@link generateMessageTypes}.
|
|
12
|
+
*/
|
|
13
|
+
export type Catalogs = Map<string, Map<string, string>>;
|
|
14
|
+
/**
|
|
15
|
+
* The messages of nested objects by their keys: {"order": {"count": "..."}} → order.count
|
|
16
|
+
* @param messages
|
|
17
|
+
* @param source where the messages are, for the errors
|
|
18
|
+
* @param prefix
|
|
19
|
+
*/
|
|
20
|
+
export declare function flattenMessages(messages: unknown, source: string, prefix?: string): Map<string, string>;
|
|
21
|
+
/**
|
|
22
|
+
* Add the messages of a language to catalogs, the ones already there replaced
|
|
23
|
+
* @param catalogs
|
|
24
|
+
* @param locale
|
|
25
|
+
* @param messages
|
|
26
|
+
*/
|
|
27
|
+
export declare function mergeMessages(catalogs: Catalogs, locale: string, messages: Map<string, string>): void;
|
|
28
|
+
/**
|
|
29
|
+
* Load the messages of a directory into catalogs: a file by language (zh-CN.json), or a directory by language whose
|
|
30
|
+
* files (and their directories) prefix their keys (zh-CN/order.json: order.*, zh-CN/admin/user.json: admin.user.*)
|
|
31
|
+
* @param catalogs
|
|
32
|
+
* @param directory
|
|
33
|
+
*/
|
|
34
|
+
export declare function loadDirectory(catalogs: Catalogs, directory: string): Promise<void>;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.flattenMessages = flattenMessages;
|
|
7
|
+
exports.mergeMessages = mergeMessages;
|
|
8
|
+
exports.loadDirectory = loadDirectory;
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const promises_1 = require("node:fs/promises");
|
|
11
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
12
|
+
const I18nCatalogException_js_1 = require("../exceptions/I18nCatalogException.js");
|
|
13
|
+
const Locale_js_1 = require("./Locale.js");
|
|
14
|
+
/**
|
|
15
|
+
* The messages of nested objects by their keys: {"order": {"count": "..."}} → order.count
|
|
16
|
+
* @param messages
|
|
17
|
+
* @param source where the messages are, for the errors
|
|
18
|
+
* @param prefix
|
|
19
|
+
*/
|
|
20
|
+
function flattenMessages(messages, source, prefix = '') {
|
|
21
|
+
const flat = new Map();
|
|
22
|
+
if (messages === null || typeof messages !== 'object' || Array.isArray(messages))
|
|
23
|
+
throw new I18nCatalogException_js_1.I18nCatalogException(`${source}: the messages must be an object${prefix ? ` (${prefix.slice(0, -1)})` : ''}`);
|
|
24
|
+
for (const [key, value] of Object.entries(messages)) {
|
|
25
|
+
if (!key || key.includes('.'))
|
|
26
|
+
throw new I18nCatalogException_js_1.I18nCatalogException(`${source}: invalid key "${prefix}${key}" (the parts of the keys are nested objects)`);
|
|
27
|
+
if (typeof value === 'string')
|
|
28
|
+
flat.set(`${prefix}${key}`, value);
|
|
29
|
+
else
|
|
30
|
+
flattenMessages(value, source, `${prefix}${key}.`).forEach((message, name) => flat.set(name, message));
|
|
31
|
+
}
|
|
32
|
+
return flat;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Add the messages of a language to catalogs, the ones already there replaced
|
|
36
|
+
* @param catalogs
|
|
37
|
+
* @param locale
|
|
38
|
+
* @param messages
|
|
39
|
+
*/
|
|
40
|
+
function mergeMessages(catalogs, locale, messages) {
|
|
41
|
+
let catalog = catalogs.get(locale);
|
|
42
|
+
if (!catalog)
|
|
43
|
+
catalogs.set(locale, catalog = new Map());
|
|
44
|
+
messages.forEach((message, key) => catalog.set(key, message));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The language of a name of a file or a directory of a catalog (zh-CN.json, zh-CN)
|
|
48
|
+
* @param name
|
|
49
|
+
* @param file
|
|
50
|
+
*/
|
|
51
|
+
function localeOfName(name, file) {
|
|
52
|
+
const locale = (0, Locale_js_1.normalizeLocale)(name);
|
|
53
|
+
if (!locale || locale.toLowerCase() !== name.toLowerCase().replace(/_/g, '-'))
|
|
54
|
+
throw new I18nCatalogException_js_1.I18nCatalogException(`${file}: "${name}" is not the tag of a language (en, zh-CN, zh-Hant-TW)`);
|
|
55
|
+
return locale;
|
|
56
|
+
}
|
|
57
|
+
async function readJson(file) {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(await (0, promises_1.readFile)(file, 'utf8'));
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
throw new I18nCatalogException_js_1.I18nCatalogException(`${file}: ${e.message}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Load the messages of a directory into catalogs: a file by language (zh-CN.json), or a directory by language whose
|
|
67
|
+
* files (and their directories) prefix their keys (zh-CN/order.json: order.*, zh-CN/admin/user.json: admin.user.*)
|
|
68
|
+
* @param catalogs
|
|
69
|
+
* @param directory
|
|
70
|
+
*/
|
|
71
|
+
async function loadDirectory(catalogs, directory) {
|
|
72
|
+
if (!(0, node_fs_1.existsSync)(directory))
|
|
73
|
+
throw new I18nCatalogException_js_1.I18nCatalogException(`${directory}: the directory of the messages does not exist`);
|
|
74
|
+
const entries = (await (0, promises_1.readdir)(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
75
|
+
for (const entry of entries) {
|
|
76
|
+
const file = node_path_1.default.join(directory, entry.name);
|
|
77
|
+
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
78
|
+
mergeMessages(catalogs, localeOfName(entry.name.slice(0, -5), file), flattenMessages(await readJson(file), file));
|
|
79
|
+
}
|
|
80
|
+
else if (entry.isDirectory()) {
|
|
81
|
+
const locale = localeOfName(entry.name, file);
|
|
82
|
+
await loadNamespaces(catalogs, locale, file, '');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Load the files of the directory of a language, their names prefixing their keys
|
|
88
|
+
* @param catalogs
|
|
89
|
+
* @param locale
|
|
90
|
+
* @param directory
|
|
91
|
+
* @param prefix
|
|
92
|
+
*/
|
|
93
|
+
async function loadNamespaces(catalogs, locale, directory, prefix) {
|
|
94
|
+
const entries = (await (0, promises_1.readdir)(directory, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name));
|
|
95
|
+
for (const entry of entries) {
|
|
96
|
+
const file = node_path_1.default.join(directory, entry.name);
|
|
97
|
+
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
98
|
+
mergeMessages(catalogs, locale, flattenMessages(await readJson(file), file, `${prefix}${entry.name.slice(0, -5)}.`));
|
|
99
|
+
}
|
|
100
|
+
else if (entry.isDirectory()) {
|
|
101
|
+
await loadNamespaces(catalogs, locale, file, `${prefix}${entry.name}.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|