@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,313 @@
1
+ import { Component, type ComponentOptionsBuilder } from '@lakutata/core';
2
+ import type { I18nOptions } from './interfaces/I18nOptions.js';
3
+ import { type NestedMessages } from './lib/Catalog.js';
4
+ import { type FormatContext, type MessagePart } from './lib/MessageFormat.js';
5
+ import type { ITranslator } from './interfaces/ITranslator.js';
6
+ import type { MessageKey, MessageParams } from './types/I18nMessages.js';
7
+ /**
8
+ * Build the component options of an {@link I18n} component, to register it in the `components` of an application.
9
+ * Name it `i18n`: the {@link I18nMiddleware} and {@link commanderTranslator} use the component of that name by
10
+ * default. The options are validated when the component is created, not here.
11
+ * @param options Where the messages come from (`directory`, `messages`), the default language, the fallbacks, the
12
+ * behavior on a missing message, the currency and time zone of the formatted arguments, the validation messages.
13
+ * Without options, the component has no messages and its default language is `en`.
14
+ * @returns The component options (`{class: I18n, ...options}`).
15
+ * @example
16
+ * ```typescript
17
+ * import {Application} from 'lakutata'
18
+ * import {buildI18nOptions, type I18n} from 'lakutata/com/i18n'
19
+ *
20
+ * Application
21
+ * .alias({'@locales': '/srv/shop/locales'})
22
+ * .run(() => ({
23
+ * id: 'shop.app',
24
+ * name: 'Shop',
25
+ * components: {
26
+ * i18n: buildI18nOptions({
27
+ * //locales/en.json, locales/zh-CN.json, or a directory per language
28
+ * directory: '@locales',
29
+ * defaultLocale: 'en',
30
+ * //A message missing in zh-TW is looked up in zh-CN, then zh, then en
31
+ * fallback: {'zh-TW': 'zh-CN'},
32
+ * currency: 'CNY',
33
+ * timeZone: 'Asia/Shanghai'
34
+ * })
35
+ * }
36
+ * }))
37
+ * .onLaunched(async (app: Application): Promise<void> => {
38
+ * const i18n: I18n = await app.getObject('i18n')
39
+ * console.log(i18n.getLocales())
40
+ * })
41
+ * ```
42
+ */
43
+ export declare const buildI18nOptions: ComponentOptionsBuilder<I18nOptions>;
44
+ /**
45
+ * The internationalization component: the application's messages in each language, by key, written in ICU
46
+ * MessageFormat (arguments, numbers, currencies, dates, plurals, selects), formatted in their language by the `Intl`
47
+ * of Node.js. Register it in the `components` of an application with {@link buildI18nOptions}, named `i18n`, and get
48
+ * it with `getObject()` or `@Inject('i18n')`. In a request, inject the {@link Translator} instead (the
49
+ * {@link I18nMiddleware} gives it the request's language); out of a request, call {@link I18n.t} with a language or
50
+ * use {@link I18n.translator}.
51
+ *
52
+ * When it is created, the component loads the messages of its `directory` option, then adds the ones of its `messages`
53
+ * option, and registers the validation messages of its languages. A message missing in a language is looked up in the
54
+ * languages it falls back to: its `fallback` languages, its less specific tags (`zh-Hant-TW`, `zh-Hant`, `zh`), then
55
+ * the default language (see {@link fallbackChain}). A message is always formatted in the language asked, even when it
56
+ * comes from a fallback. A key found nowhere throws {@link I18nMissingKeyException}, or gives the key itself with the
57
+ * `missing: 'key'` option (the default when `NODE_ENV` is `production`).
58
+ * @example
59
+ * ```typescript
60
+ * import {Application} from 'lakutata'
61
+ * import {buildI18nOptions, type I18n} from 'lakutata/com/i18n'
62
+ *
63
+ * Application
64
+ * .run(() => ({
65
+ * id: 'shop.app',
66
+ * name: 'Shop',
67
+ * components: {
68
+ * i18n: buildI18nOptions({
69
+ * messages: {
70
+ * 'en': {order: {count: '{count, plural, =0 {No orders} one {# order} other {# orders}}', total: 'Total: {price, number, currency}'}},
71
+ * 'zh-CN': {order: {count: '{count, plural, =0 {暂无订单} other {# 个订单}}'}}
72
+ * },
73
+ * currency: 'CNY'
74
+ * })
75
+ * }
76
+ * }))
77
+ * .onLaunched(async (app: Application): Promise<void> => {
78
+ * const i18n: I18n = await app.getObject('i18n')
79
+ * console.log(i18n.t('order.count', {count: 1200})) //1,200 orders
80
+ * console.log(i18n.t('order.count', {count: 0}, 'zh-CN')) //暂无订单
81
+ * //Missing in zh-CN: the English message, formatted in Chinese
82
+ * console.log(i18n.t('order.total', {price: 9.5}, 'zh-CN')) //Total: ¥9.50
83
+ * console.log(i18n.negotiate(['zh-TW', 'en'])) //zh-CN
84
+ * })
85
+ * ```
86
+ */
87
+ export declare class I18n extends Component {
88
+ #private;
89
+ /**
90
+ * The directories holding the messages, read when the component is created (see {@link I18nOptions.directory}):
91
+ * a path, an alias of the application (`@locales`), or several, the later ones replacing the same keys.
92
+ * @default none
93
+ */
94
+ protected readonly directory?: string | string[];
95
+ /**
96
+ * The languages {@link I18n.negotiate} chooses from, as language tags.
97
+ * @default the default language, then the languages of the messages in alphabetical order
98
+ */
99
+ protected readonly locales?: string[];
100
+ /**
101
+ * The default language: the last one a message is looked up in, the one of the calls without a language, and the
102
+ * one chosen when no requested language is supported.
103
+ * @default 'en'
104
+ */
105
+ protected readonly defaultLocale: string;
106
+ /**
107
+ * The languages a message missing in a language is looked up in next, by language (`{'zh-TW': 'zh-CN'}`).
108
+ * @default none
109
+ */
110
+ protected readonly fallback?: Record<string, string | string[]>;
111
+ /**
112
+ * The messages given in the code, by language tag, added after the ones of `directory` and replacing their keys.
113
+ * @default none
114
+ */
115
+ protected readonly messages?: Record<string, NestedMessages>;
116
+ /**
117
+ * What a missing message or argument does: `'throw'` throws; `'key'` gives the key, writes the argument as
118
+ * `{name}`, or formats a `{price, number, currency}` without a currency as a plain number, with a warning, once
119
+ * per message and key, argument or currency.
120
+ * @default 'throw', and 'key' when NODE_ENV is 'production'
121
+ */
122
+ protected readonly missing?: 'throw' | 'key';
123
+ /**
124
+ * The currency (ISO 4217) of the `{price, number, currency}` arguments whose style and arguments give none.
125
+ * @default none
126
+ */
127
+ protected readonly currency?: string;
128
+ /**
129
+ * The time zone (IANA) of the formatted dates and times.
130
+ * @default the process's time zone
131
+ */
132
+ protected readonly timeZone?: string;
133
+ /**
134
+ * Whether the built-in validation error messages of the languages are registered.
135
+ * @default true
136
+ */
137
+ protected readonly validationMessages: boolean;
138
+ /**
139
+ * Load the messages (the directories, then the `messages` option), compute the supported languages and register
140
+ * the validation messages; called when the component is created.
141
+ * @throws {I18nCatalogException} When a directory, a file, a key or a language tag of the options is invalid.
142
+ */
143
+ protected init(): Promise<void>;
144
+ /**
145
+ * Give the supported languages without the `locales` option: the default language, then the languages having
146
+ * messages, in alphabetical order.
147
+ * @returns The canonical tags.
148
+ */
149
+ protected discovered(): string[];
150
+ /**
151
+ * Register the validation error messages of languages, so that the validations run in that language give them. A
152
+ * language's messages are layered from the end of its fallback chain to itself: for each language of the chain,
153
+ * its built-in messages (unless `validationMessages` is `false`), then the application's `validation.<code>`
154
+ * messages (`validation.any.required`), the later ones replacing the earlier ones. Called by `init()` for all the
155
+ * languages, and by {@link I18n.addMessages} for the languages its messages change.
156
+ * @param locales The canonical tags of the languages to register: by default, each supported language and each
157
+ * language having messages.
158
+ * @throws {SchemaDefinitionException} When a `validation.*` message is not a valid template.
159
+ */
160
+ protected registerValidationMessages(locales?: Iterable<string>): Promise<void>;
161
+ /**
162
+ * Normalize a language tag given in the configuration.
163
+ * @param tag The language tag.
164
+ * @param option The name of the option giving it, for the error message.
165
+ * @returns The canonical tag.
166
+ * @throws {I18nCatalogException} When the tag is not a language tag.
167
+ */
168
+ protected locale(tag: string, option: string): string;
169
+ /**
170
+ * Resolve a directory of the configuration to an absolute path: an alias (`@locales/en`) by the application
171
+ * holding the component, any other path from the working directory.
172
+ * @param directory The path or the alias.
173
+ * @returns The absolute path.
174
+ * @throws {I18nCatalogException} When an alias is given and no enclosing module resolves aliases.
175
+ */
176
+ protected resolvePath(directory: string): string;
177
+ /**
178
+ * Tell what a missing message or argument does: the `missing` option, else `'key'` when `NODE_ENV` is
179
+ * `production` and `'throw'` otherwise (read at each call).
180
+ * @returns `'throw'` or `'key'`.
181
+ */
182
+ protected missingMode(): 'throw' | 'key';
183
+ /**
184
+ * Emit a process warning of type `I18nWarning`, once per distinct text for the component's lifetime.
185
+ * @param warning The text of the warning.
186
+ */
187
+ protected warn(warning: string): void;
188
+ /**
189
+ * Give the formatting settings of a language: the language, the configured currency and time zone, and what a
190
+ * missing argument or currency does (thrown, or kept with a warning emitted once by {@link I18n.warn}).
191
+ * @param locale The canonical language tag the message is formatted in.
192
+ * @returns The formatting settings.
193
+ */
194
+ protected context(locale: string): FormatContext;
195
+ /**
196
+ * Give the supported languages: the `locales` option, or the default language then the languages having messages
197
+ * (the ones added by {@link I18n.addMessages} included), in alphabetical order.
198
+ * @returns A copy of the canonical tags.
199
+ */
200
+ getLocales(): string[];
201
+ /**
202
+ * Give the default language.
203
+ * @returns The canonical tag of the `defaultLocale` option (`en` by default).
204
+ */
205
+ getDefaultLocale(): string;
206
+ /**
207
+ * Give the time zone the dates and the times are formatted in.
208
+ * @returns The `timeZone` option; `undefined` when it is not set (the process's time zone is then used).
209
+ */
210
+ getTimeZone(): string | undefined;
211
+ /**
212
+ * List the keys a language has messages for: its own keys, not the ones of the languages it falls back to.
213
+ * @param locale The language tag (normalized; `zh_CN` is `zh-CN`).
214
+ * @returns The full keys (`order.count`), in their loading order; empty for a language without messages.
215
+ */
216
+ getKeys(locale: string): string[];
217
+ /**
218
+ * Add messages to a language at run time, replacing the messages of the same keys; {@link I18n.t} gives them as
219
+ * soon as it returns. As for the messages loaded at the creation, a new language is added to the supported
220
+ * languages ({@link I18n.getLocales}, {@link I18n.negotiate}) unless the `locales` option lists them, and the
221
+ * validation messages of the new language (its built-in ones) and of the `validation.*` messages added are
222
+ * registered, for the language and the languages falling back to it.
223
+ * @param locale The language tag (normalized).
224
+ * @param messages The messages, nested by the parts of their keys (`{order: {count: '...'}}`).
225
+ * @returns A promise resolved once the validation messages are registered (the built-in ones of a language are
226
+ * loaded on demand); it rejects with a `SchemaDefinitionException` when a `validation.*` message is not a valid
227
+ * template.
228
+ * @throws {I18nCatalogException} When the tag is not a language tag, or a key or a value is invalid: thrown by the
229
+ * call, nothing added.
230
+ * @example
231
+ * ```typescript
232
+ * import {type I18n} from 'lakutata/com/i18n'
233
+ *
234
+ * declare const i18n: I18n
235
+ *
236
+ * await i18n.addMessages('fr', {order: {created: 'Commande créée pour {customer}'}, validation: {any: {required: '{{#label}} est obligatoire'}}})
237
+ * console.log(i18n.negotiate(['fr-CA'])) //fr
238
+ * console.log(i18n.t('order.created', {customer: 'Ada'}, 'fr')) //Commande créée pour Ada
239
+ * ```
240
+ */
241
+ addMessages(locale: string, messages: NestedMessages): Promise<void>;
242
+ /**
243
+ * Merge messages into the catalog of a language, its cached parsed messages dropped.
244
+ * @param locale The language tag.
245
+ * @param messages The nested messages.
246
+ * @returns The messages merged, by full key.
247
+ * @throws {I18nCatalogException} When the tag is not a language tag, or a key or a value is invalid.
248
+ */
249
+ protected merge(locale: string, messages: NestedMessages): Map<string, string>;
250
+ /**
251
+ * Choose the supported language for the requested ones (see {@link negotiateLocale}): for each requested language
252
+ * in turn, the same tag, then its language alone (`en-GB` gives `en`), then another tag of its language (`pt-PT`
253
+ * gives `pt-BR`).
254
+ * @param requested The requested language tags, preferred first; the invalid ones are skipped.
255
+ * @returns A supported language, or the default language when none matches.
256
+ */
257
+ negotiate(requested: string[]): string;
258
+ /**
259
+ * Find the message of a key in a language or the languages it falls back to, parsed once and cached.
260
+ * @param key The key of the message.
261
+ * @param locale The canonical language tag.
262
+ * @returns The parsed message and the language it was found in; `undefined` when no language of the chain has it.
263
+ * @throws {I18nMessageSyntaxException} When the message found is not valid ICU MessageFormat.
264
+ */
265
+ protected message(key: string, locale: string): {
266
+ parts: MessagePart[];
267
+ locale: string;
268
+ } | undefined;
269
+ /**
270
+ * Tell whether a key has a message in a language or in the languages it falls back to.
271
+ * @param key The key of the message.
272
+ * @param locale The language tag: the default language by default, and when it is not a language tag.
273
+ * @returns `true` when {@link I18n.t} finds a message for the key in that language.
274
+ */
275
+ has(key: string, locale?: string): boolean;
276
+ /**
277
+ * Give the message of a key in a language, formatted with its arguments. The language is not negotiated: the
278
+ * message is looked up in the language given, then in the languages it falls back to (see {@link I18n}), and is
279
+ * formatted in the language given (its numbers, dates and plural rules) even when it comes from a fallback.
280
+ * @param key The key of the message (`order.count`).
281
+ * @param params The arguments of the message, by their names: the numbers and the bigints are formatted in the
282
+ * language, the `Date` objects as dates. A `currency` argument gives the currency of `{price, number, currency}`.
283
+ * @param locale The language tag: the default language by default, and when it is not a language tag.
284
+ * @returns The formatted message. With `missing: 'key'`, the key itself when no message has it, `{name}` in place
285
+ * of a missing argument, and a plain number for a currency argument without a currency, each with a warning
286
+ * emitted once (per key, or per message and argument).
287
+ * @throws {I18nMissingKeyException} When no message has the key and the `missing` mode is `'throw'`.
288
+ * @throws {I18nMissingParameterException} When an argument is missing, or a currency argument has no currency, and
289
+ * the `missing` mode is `'throw'`.
290
+ * @throws {I18nMessageSyntaxException} When the message is not valid ICU MessageFormat.
291
+ */
292
+ t<K extends MessageKey>(key: K, params?: MessageParams<K>, locale?: string): string;
293
+ /**
294
+ * Format an ICU MessageFormat message which is not in the catalogs (parsed at each call).
295
+ * @param message The message (`Hello, {name}`).
296
+ * @param params The arguments of the message, by their names.
297
+ * @param locale The language tag it is formatted in: the default language by default, and when it is not a
298
+ * language tag.
299
+ * @returns The formatted message. With `missing: 'key'`, `{name}` in place of a missing argument and a plain
300
+ * number for a currency argument without a currency, each with a warning emitted once per message and argument.
301
+ * @throws {I18nMessageSyntaxException} When the message is not valid ICU MessageFormat.
302
+ * @throws {I18nMissingParameterException} When an argument is missing, or a currency argument has no currency, and
303
+ * the `missing` mode is `'throw'`.
304
+ */
305
+ format(message: string, params?: Record<string, unknown>, locale?: string): string;
306
+ /**
307
+ * Create a translator of a language, which gives its messages without the language at each call. The language is
308
+ * not negotiated: it is used as given (its messages falling back as in {@link I18n.t}).
309
+ * @param locale The language tag (normalized): the default language when it is not a language tag.
310
+ * @returns The translator, a {@link LocaleTranslator}.
311
+ */
312
+ translator(locale: string): ITranslator;
313
+ }
@@ -0,0 +1,124 @@
1
+ import { Provider } from '@lakutata/core';
2
+ import type { I18n } from './I18n.js';
3
+ import type { ITranslator } from './interfaces/ITranslator.js';
4
+ import type { MessageKey, MessageParams } from './types/I18nMessages.js';
5
+ /**
6
+ * The messages in the language of the current request. With the {@link I18nMiddleware} on the entrypoint, each request
7
+ * gets one `Translator` of its negotiated language, shared by the objects of the request: the controller, the
8
+ * `@Scoped` providers and the access control rules inject it with `@Inject(Translator)`. It is a scoped provider,
9
+ * autoloaded: it needs no registration. Without the middleware (or out of a request), its methods throw
10
+ * {@link I18nTranslatorException}; use {@link I18n.translator} there.
11
+ * @example
12
+ * ```typescript
13
+ * import {Application} from 'lakutata'
14
+ * import {buildEntrypoints, Controller} from 'lakutata/com/entrypoint'
15
+ * import {expressHTTPEntrypoint} from 'lakutata/adapter/express'
16
+ * import {CLIAction, GET} from 'lakutata/decorator/ctrl'
17
+ * import {Inject} from 'lakutata/decorator/di'
18
+ * import {buildI18nOptions, I18nMiddleware, Translator} from 'lakutata/com/i18n'
19
+ *
20
+ * class OrderController extends Controller {
21
+ * @Inject(Translator)
22
+ * protected readonly translator: Translator
23
+ *
24
+ * //GET /orders/count?lang=zh-CN, or the lang cookie, or Accept-Language
25
+ * @GET('/orders/count')
26
+ * @CLIAction('count-orders')
27
+ * public async count(): Promise<string> {
28
+ * return `${this.translator.locale}: ${this.translator.t('order.count', {count: 3})}`
29
+ * }
30
+ * }
31
+ *
32
+ * Application.run(() => ({
33
+ * id: 'shop.app',
34
+ * name: 'Shop',
35
+ * components: {
36
+ * i18n: buildI18nOptions({
37
+ * messages: {
38
+ * 'en': {order: {count: '{count, plural, one {# order} other {# orders}}'}},
39
+ * 'zh-CN': {order: {count: '{count} 个订单'}}
40
+ * }
41
+ * }),
42
+ * entrypoint: buildEntrypoints({
43
+ * controllers: [OrderController],
44
+ * middlewares: [I18nMiddleware],
45
+ * http: expressHTTPEntrypoint({port: 3000})
46
+ * })
47
+ * },
48
+ * bootstrap: ['entrypoint']
49
+ * }))
50
+ * ```
51
+ */
52
+ export declare class Translator extends Provider implements ITranslator {
53
+ #private;
54
+ /**
55
+ * The request's language tag, negotiated by the {@link I18nMiddleware} among the supported languages (the same as
56
+ * `context.locale`); `undefined` without the middleware.
57
+ */
58
+ readonly locale: string;
59
+ /**
60
+ * The {@link I18n} component giving the messages, set by the {@link I18nMiddleware}.
61
+ */
62
+ protected readonly i18n?: I18n;
63
+ /**
64
+ * Create the translator of the language given by the middleware, when it gave one.
65
+ */
66
+ protected init(): Promise<void>;
67
+ /**
68
+ * The translator of the request's language.
69
+ * @throws {I18nTranslatorException} When the middleware did not give the language (no {@link I18nMiddleware} on
70
+ * the entrypoint, or out of a request).
71
+ */
72
+ protected get translator(): ITranslator;
73
+ /**
74
+ * Give the message of a key in the request's language, formatted with its arguments (see {@link I18n.t}).
75
+ * @param key The key of the message (`order.count`).
76
+ * @param params The arguments of the message, by their names.
77
+ * @returns The formatted message. With `missing: 'key'`, the key itself when it is missing, `{name}` in place of a
78
+ * missing argument, a plain number for a currency argument without a currency (each with a warning, once).
79
+ * @throws {I18nTranslatorException} When the entrypoint has no {@link I18nMiddleware}, or out of a request.
80
+ * @throws {I18nMissingKeyException} When no message has the key and the `missing` option is `'throw'`.
81
+ * @throws {I18nMissingParameterException} When an argument is missing, or a currency argument has no currency, and
82
+ * the `missing` option is `'throw'`.
83
+ * @throws {I18nMessageSyntaxException} When the message is not valid ICU MessageFormat.
84
+ */
85
+ t<K extends MessageKey>(key: K, params?: MessageParams<K>): string;
86
+ /**
87
+ * Tell whether a key has a message in the request's language or in the languages it falls back to.
88
+ * @param key The key of the message.
89
+ * @returns `true` when {@link Translator.t} finds a message for the key.
90
+ * @throws {I18nTranslatorException} When the entrypoint has no {@link I18nMiddleware}, or out of a request.
91
+ */
92
+ has(key: string): boolean;
93
+ /**
94
+ * Format an ICU MessageFormat message which is not in the catalogs, in the request's language.
95
+ * @param message The message (`Hello, {name}`).
96
+ * @param params The arguments of the message, by their names.
97
+ * @returns The formatted message. With `missing: 'key'`, `{name}` in place of a missing argument, a plain number
98
+ * for a currency argument without a currency (each with a warning, once).
99
+ * @throws {I18nTranslatorException} When the entrypoint has no {@link I18nMiddleware}, or out of a request.
100
+ * @throws {I18nMessageSyntaxException} When the message is not valid ICU MessageFormat.
101
+ * @throws {I18nMissingParameterException} When an argument is missing, or a currency argument has no currency, and
102
+ * the `missing` option is `'throw'`.
103
+ */
104
+ format(message: string, params?: Record<string, unknown>): string;
105
+ /**
106
+ * Format a number in the request's language (`1234.5` gives `1,234.5` in English, `1.234,5` in German).
107
+ * @param value The number.
108
+ * @param options The options of `Intl.NumberFormat` (`{style: 'currency', currency: 'EUR'}`); the component's
109
+ * `currency` option is not applied here.
110
+ * @returns The formatted number.
111
+ * @throws {I18nTranslatorException} When the entrypoint has no {@link I18nMiddleware}, or out of a request.
112
+ */
113
+ number(value: number | bigint, options?: Intl.NumberFormatOptions): string;
114
+ /**
115
+ * Format a date (and its time when the options ask for it) in the request's language, in the component's
116
+ * `timeZone` (the process's one without it).
117
+ * @param value The date, or a timestamp in milliseconds.
118
+ * @param options The options of `Intl.DateTimeFormat` (`{dateStyle: 'long'}`); their `timeZone` replaces the
119
+ * component's one.
120
+ * @returns The formatted date.
121
+ * @throws {I18nTranslatorException} When the entrypoint has no {@link I18nMiddleware}, or out of a request.
122
+ */
123
+ date(value: Date | number, options?: Intl.DateTimeFormatOptions): string;
124
+ }
@@ -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,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,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,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,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,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
+ }