@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.
Files changed (80) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +347 -0
  3. package/dist/cjs/exports/I18n.d.ts +15 -0
  4. package/dist/cjs/exports/I18n.js +45 -0
  5. package/dist/cjs/exports/I18nMiddleware.d.ts +2 -0
  6. package/dist/cjs/exports/I18nMiddleware.js +21 -0
  7. package/dist/cjs/i18n/I18n.d.ts +313 -0
  8. package/dist/cjs/i18n/I18n.js +462 -0
  9. package/dist/cjs/i18n/Translator.d.ts +124 -0
  10. package/dist/cjs/i18n/Translator.js +156 -0
  11. package/dist/cjs/i18n/exceptions/I18nCatalogException.d.ts +15 -0
  12. package/dist/cjs/i18n/exceptions/I18nCatalogException.js +22 -0
  13. package/dist/cjs/i18n/exceptions/I18nMessageSyntaxException.d.ts +14 -0
  14. package/dist/cjs/i18n/exceptions/I18nMessageSyntaxException.js +21 -0
  15. package/dist/cjs/i18n/exceptions/I18nMissingKeyException.d.ts +12 -0
  16. package/dist/cjs/i18n/exceptions/I18nMissingKeyException.js +19 -0
  17. package/dist/cjs/i18n/exceptions/I18nMissingParameterException.d.ts +13 -0
  18. package/dist/cjs/i18n/exceptions/I18nMissingParameterException.js +20 -0
  19. package/dist/cjs/i18n/exceptions/I18nTranslatorException.d.ts +12 -0
  20. package/dist/cjs/i18n/exceptions/I18nTranslatorException.js +19 -0
  21. package/dist/cjs/i18n/interfaces/I18nOptions.d.ts +70 -0
  22. package/dist/cjs/i18n/interfaces/I18nOptions.js +2 -0
  23. package/dist/cjs/i18n/interfaces/ITranslator.d.ts +57 -0
  24. package/dist/cjs/i18n/interfaces/ITranslator.js +2 -0
  25. package/dist/cjs/i18n/lib/Catalog.d.ts +34 -0
  26. package/dist/cjs/i18n/lib/Catalog.js +104 -0
  27. package/dist/cjs/i18n/lib/CatalogTools.d.ts +86 -0
  28. package/dist/cjs/i18n/lib/CatalogTools.js +218 -0
  29. package/dist/cjs/i18n/lib/Locale.d.ts +95 -0
  30. package/dist/cjs/i18n/lib/Locale.js +182 -0
  31. package/dist/cjs/i18n/lib/LocaleTranslator.d.ts +93 -0
  32. package/dist/cjs/i18n/lib/LocaleTranslator.js +105 -0
  33. package/dist/cjs/i18n/lib/MessageFormat.d.ts +144 -0
  34. package/dist/cjs/i18n/lib/MessageFormat.js +372 -0
  35. package/dist/cjs/i18n/middleware/CommanderTranslator.d.ts +71 -0
  36. package/dist/cjs/i18n/middleware/CommanderTranslator.js +69 -0
  37. package/dist/cjs/i18n/middleware/I18nMiddleware.d.ts +126 -0
  38. package/dist/cjs/i18n/middleware/I18nMiddleware.js +210 -0
  39. package/dist/cjs/i18n/types/I18nMessages.d.ts +24 -0
  40. package/dist/cjs/i18n/types/I18nMessages.js +2 -0
  41. package/dist/cjs/package.json +1 -0
  42. package/dist/esm/exports/I18n.js +14 -0
  43. package/dist/esm/exports/I18nMiddleware.js +2 -0
  44. package/dist/esm/i18n/I18n.js +454 -0
  45. package/dist/esm/i18n/Translator.js +153 -0
  46. package/dist/esm/i18n/exceptions/I18nCatalogException.js +18 -0
  47. package/dist/esm/i18n/exceptions/I18nMessageSyntaxException.js +17 -0
  48. package/dist/esm/i18n/exceptions/I18nMissingKeyException.js +15 -0
  49. package/dist/esm/i18n/exceptions/I18nMissingParameterException.js +16 -0
  50. package/dist/esm/i18n/exceptions/I18nTranslatorException.js +15 -0
  51. package/dist/esm/i18n/interfaces/I18nOptions.js +1 -0
  52. package/dist/esm/i18n/interfaces/ITranslator.js +1 -0
  53. package/dist/esm/i18n/lib/Catalog.js +96 -0
  54. package/dist/esm/i18n/lib/CatalogTools.js +213 -0
  55. package/dist/esm/i18n/lib/Locale.js +173 -0
  56. package/dist/esm/i18n/lib/LocaleTranslator.js +101 -0
  57. package/dist/esm/i18n/lib/MessageFormat.js +367 -0
  58. package/dist/esm/i18n/middleware/CommanderTranslator.js +66 -0
  59. package/dist/esm/i18n/middleware/I18nMiddleware.js +206 -0
  60. package/dist/esm/i18n/types/I18nMessages.js +1 -0
  61. package/dist/types/exports/I18n.d.ts +15 -0
  62. package/dist/types/exports/I18nMiddleware.d.ts +2 -0
  63. package/dist/types/i18n/I18n.d.ts +313 -0
  64. package/dist/types/i18n/Translator.d.ts +124 -0
  65. package/dist/types/i18n/exceptions/I18nCatalogException.d.ts +15 -0
  66. package/dist/types/i18n/exceptions/I18nMessageSyntaxException.d.ts +14 -0
  67. package/dist/types/i18n/exceptions/I18nMissingKeyException.d.ts +12 -0
  68. package/dist/types/i18n/exceptions/I18nMissingParameterException.d.ts +13 -0
  69. package/dist/types/i18n/exceptions/I18nTranslatorException.d.ts +12 -0
  70. package/dist/types/i18n/interfaces/I18nOptions.d.ts +70 -0
  71. package/dist/types/i18n/interfaces/ITranslator.d.ts +57 -0
  72. package/dist/types/i18n/lib/Catalog.d.ts +34 -0
  73. package/dist/types/i18n/lib/CatalogTools.d.ts +86 -0
  74. package/dist/types/i18n/lib/Locale.d.ts +95 -0
  75. package/dist/types/i18n/lib/LocaleTranslator.d.ts +93 -0
  76. package/dist/types/i18n/lib/MessageFormat.d.ts +144 -0
  77. package/dist/types/i18n/middleware/CommanderTranslator.d.ts +71 -0
  78. package/dist/types/i18n/middleware/I18nMiddleware.d.ts +126 -0
  79. package/dist/types/i18n/types/I18nMessages.d.ts +24 -0
  80. 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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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
+ }