lakutata 3.0.0 → 3.1.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/README.md +50 -30
- package/README.zh-CN.md +130 -0
- package/dist/cjs/exports/QueueComponent.d.ts +1 -0
- package/dist/cjs/exports/QueueComponent.js +20 -0
- package/dist/esm/exports/QueueComponent.js +1 -0
- package/dist/types/exports/QueueComponent.d.ts +1 -0
- package/doc/en/Application.md +107 -0
- package/doc/en/CLI.md +274 -0
- package/doc/en/Cache.md +379 -0
- package/doc/en/Command.md +110 -0
- package/doc/en/ContainerdIntegration.md +17 -16
- package/doc/en/Controllers.md +256 -0
- package/doc/en/DBus.md +291 -0
- package/doc/en/DTOValidation.md +72 -43
- package/doc/en/Database.md +188 -0
- package/doc/en/DatabaseDrivers.md +287 -0
- package/doc/en/Decorator.md +52 -40
- package/doc/en/DockerIntegration.md +7 -6
- package/doc/en/Entities.md +240 -0
- package/doc/en/Entrypoint.md +216 -0
- package/doc/en/Exceptions.md +174 -0
- package/doc/en/GRPC.md +251 -0
- package/doc/en/GettingStarted.md +165 -0
- package/doc/en/HTTP.md +507 -0
- package/doc/en/HelperFunctions.md +11 -11
- package/doc/en/I18n.md +119 -115
- package/doc/en/Injection.md +144 -0
- package/doc/en/Installers.md +280 -0
- package/doc/en/Logger.md +206 -0
- package/doc/en/MCP.md +293 -0
- package/doc/en/Middlewares.md +213 -0
- package/doc/en/Migrations.md +202 -0
- package/doc/en/MongoEmbedded.md +149 -0
- package/doc/en/Monitor.md +164 -0
- package/doc/en/Mutex.md +27 -25
- package/doc/en/NATS.md +246 -0
- package/doc/en/NativeAddons.md +212 -0
- package/doc/en/Objects.md +143 -0
- package/doc/en/Overview.md +62 -85
- package/doc/en/Packaging.md +193 -0
- package/doc/en/ProjectStructure.md +124 -0
- package/doc/en/Queue.md +269 -0
- package/doc/en/Schedules.md +217 -0
- package/doc/en/Security.md +10 -9
- package/doc/en/ServiceCalls.md +504 -0
- package/doc/en/Time.md +19 -20
- package/doc/en/UpgradingFrom1.md +435 -0
- package/doc/en/UpgradingFrom2.md +315 -0
- package/doc/en/Workspaces.md +189 -0
- package/doc/en/examples/BackgroundJobs.md +677 -0
- package/doc/en/examples/CommandLineTool.md +664 -0
- package/doc/en/examples/DesktopApp.md +809 -0
- package/doc/en/examples/DeviceService.md +607 -0
- package/doc/en/examples/MCPServer.md +653 -0
- package/doc/en/examples/README.md +16 -0
- package/doc/en/examples/RestAPI.md +490 -0
- package/doc/en/examples/ServiceCalls.md +829 -0
- package/doc/en/examples/WebApp.md +568 -0
- package/doc/zh/Application.md +75 -0
- package/doc/zh/CLI.md +250 -0
- package/doc/zh/Cache.md +328 -0
- package/doc/zh/Command.md +78 -0
- package/doc/zh/ContainerdIntegration.md +9 -9
- package/doc/zh/Controllers.md +228 -0
- package/doc/zh/DBus.md +257 -0
- package/doc/zh/DTOValidation.md +49 -19
- package/doc/zh/Database.md +175 -0
- package/doc/zh/DatabaseDrivers.md +261 -0
- package/doc/zh/Decorator.md +25 -18
- package/doc/zh/DockerIntegration.md +6 -6
- package/doc/zh/Entities.md +224 -0
- package/doc/zh/Entrypoint.md +193 -0
- package/doc/zh/Exceptions.md +158 -0
- package/doc/zh/GRPC.md +226 -0
- package/doc/zh/GettingStarted.md +148 -0
- package/doc/zh/HTTP.md +445 -0
- package/doc/zh/HelperFunctions.md +2 -2
- package/doc/zh/I18n.md +26 -26
- package/doc/zh/Injection.md +129 -0
- package/doc/zh/Installers.md +232 -0
- package/doc/zh/Logger.md +188 -0
- package/doc/zh/MCP.md +265 -0
- package/doc/zh/Middlewares.md +198 -0
- package/doc/zh/Migrations.md +178 -0
- package/doc/zh/MongoEmbedded.md +118 -0
- package/doc/zh/Monitor.md +145 -0
- package/doc/zh/Mutex.md +10 -10
- package/doc/zh/NATS.md +207 -0
- package/doc/zh/NativeAddons.md +180 -0
- package/doc/zh/Objects.md +119 -0
- package/doc/zh/Overview.md +46 -71
- package/doc/zh/Packaging.md +154 -0
- package/doc/zh/ProjectStructure.md +102 -0
- package/doc/zh/Queue.md +214 -0
- package/doc/zh/Schedules.md +193 -0
- package/doc/zh/Security.md +6 -6
- package/doc/zh/ServiceCalls.md +459 -0
- package/doc/zh/Time.md +2 -2
- package/doc/zh/UpgradingFrom1.md +347 -0
- package/doc/zh/UpgradingFrom2.md +230 -0
- package/doc/zh/Workspaces.md +150 -0
- package/doc/zh/examples/BackgroundJobs.md +618 -0
- package/doc/zh/examples/CommandLineTool.md +618 -0
- package/doc/zh/examples/DesktopApp.md +748 -0
- package/doc/zh/examples/DeviceService.md +546 -0
- package/doc/zh/examples/MCPServer.md +604 -0
- package/doc/zh/examples/README.md +14 -0
- package/doc/zh/examples/RestAPI.md +458 -0
- package/doc/zh/examples/ServiceCalls.md +765 -0
- package/doc/zh/examples/WebApp.md +520 -0
- package/package.json +46 -17
- package/doc/en/ActionPattern.md +0 -408
- package/doc/en/Core.md +0 -312
- package/doc/en/ORMIntegration.md +0 -341
- package/doc/zh/ActionPattern.md +0 -334
- package/doc/zh/Core.md +0 -256
- package/doc/zh/ORMIntegration.md +0 -319
package/doc/en/I18n.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Internationalization (i18n)
|
|
2
2
|
|
|
3
|
-
`lakutata/com/i18n` (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
`lakutata/com/i18n` (package `@lakutata/i18n`) manages the messages of an application by **keys**: the code uses a key,
|
|
4
|
+
and each language gives the text of that key in its own file. The messages are written in [ICU
|
|
5
|
+
MessageFormat](https://unicode-org.github.io/icu/userguide/format_parse/messages/); their numbers, currencies, dates and
|
|
6
|
+
plurals are formatted for the language by the built-in `Intl` of Node.js, with no third-party library.
|
|
7
7
|
|
|
8
|
-
##
|
|
8
|
+
## Message files
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
One JSON file per language, named after its language tag (BCP 47: `en`, `zh-CN`, `zh-Hant-TW`):
|
|
11
11
|
|
|
12
12
|
```
|
|
13
13
|
locales/
|
|
@@ -24,10 +24,10 @@ locales/
|
|
|
24
24
|
}
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
The nesting of the JSON
|
|
28
|
-
|
|
27
|
+
The nesting of the JSON gives the keys: the keys above are `order.count` and `order.created`. No part of a key may
|
|
28
|
+
contain `.`.
|
|
29
29
|
|
|
30
|
-
A language can also be a directory, the names of
|
|
30
|
+
A language can also be a directory, in which the names of the files (and of the subdirectories) prefix their keys:
|
|
31
31
|
|
|
32
32
|
```
|
|
33
33
|
locales/
|
|
@@ -36,7 +36,7 @@ locales/
|
|
|
36
36
|
admin/user.json → admin.user.*
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
Both layouts can be mixed in
|
|
39
|
+
Both layouts can be mixed in one directory.
|
|
40
40
|
|
|
41
41
|
## Configuration
|
|
42
42
|
|
|
@@ -53,9 +53,9 @@ Application
|
|
|
53
53
|
i18n: buildI18nOptions({
|
|
54
54
|
directory: '@locales', //an alias, or several directories
|
|
55
55
|
defaultLocale: 'en', //the default language, the last fallback (en by default)
|
|
56
|
-
fallback: {'zh-TW': 'zh-CN'}, //the messages missing in zh-TW are
|
|
56
|
+
fallback: {'zh-TW': 'zh-CN'}, //the messages missing in zh-TW are taken from zh-CN
|
|
57
57
|
currency: 'CNY', //the currency of {price, number, currency}
|
|
58
|
-
timeZone: 'Asia/Shanghai' //the time zone of
|
|
58
|
+
timeZone: 'Asia/Shanghai' //the time zone of dates and times (that of the process by default)
|
|
59
59
|
})
|
|
60
60
|
}
|
|
61
61
|
}))
|
|
@@ -63,8 +63,9 @@ Application
|
|
|
63
63
|
|
|
64
64
|
Name the component `i18n`: the middleware and `commanderTranslator` use that name.
|
|
65
65
|
|
|
66
|
-
The messages can also be given in the code instead of a directory: the `messages` option
|
|
67
|
-
object literals or imported JSON. They are then bundled with the code (`lakutata package bundle`), no file read
|
|
66
|
+
The messages can also be given in the code instead of a directory: the `messages` option takes them by language, as
|
|
67
|
+
object literals or imported JSON. They are then bundled with the code (`lakutata package bundle`), and no file is read
|
|
68
|
+
at run time:
|
|
68
69
|
|
|
69
70
|
```typescript
|
|
70
71
|
import {buildI18nOptions} from 'lakutata/com/i18n'
|
|
@@ -82,15 +83,15 @@ const i18n = buildI18nOptions({
|
|
|
82
83
|
|
|
83
84
|
| Option | Description |
|
|
84
85
|
|---|---|
|
|
85
|
-
| `directory` | The directories of the
|
|
86
|
-
| `locales` | The supported languages, which `negotiate()` chooses from; by default the default language
|
|
86
|
+
| `directory` | The directories of the message files; aliases are accepted |
|
|
87
|
+
| `locales` | The supported languages, which `negotiate()` chooses from; by default, the default language followed by the languages found in the messages |
|
|
87
88
|
| `defaultLocale` | The default language, `en` by default |
|
|
88
|
-
| `fallback` |
|
|
89
|
-
| `messages` | Messages by language given in the code, loaded after the directories (the same keys
|
|
90
|
-
| `missing` |
|
|
89
|
+
| `fallback` | Where to look next when a language lacks a message (`{'zh-TW': 'zh-CN'}`, or a list) |
|
|
90
|
+
| `messages` | Messages by language given in the code, loaded after the directories (replacing the same keys); with them, `directory` is not needed |
|
|
91
|
+
| `missing` | What happens to a missing message or argument: `'throw'` throws; `'key'` returns the key (and leaves a missing argument as `{name}`), with a warning the first time. `'throw'` by default, `'key'` when `NODE_ENV=production` |
|
|
91
92
|
| `currency` | The currency when neither the message nor its arguments give one (ISO 4217) |
|
|
92
|
-
| `timeZone` | The time zone of
|
|
93
|
-
| `validationMessages` | Whether the built-in validation messages of the supported languages
|
|
93
|
+
| `timeZone` | The time zone of dates and times (IANA) |
|
|
94
|
+
| `validationMessages` | Whether to register the built-in validation messages of the supported languages, `true` by default (see "Validation errors") |
|
|
94
95
|
|
|
95
96
|
## Usage
|
|
96
97
|
|
|
@@ -111,22 +112,22 @@ class OrderService extends Provider {
|
|
|
111
112
|
|
|
112
113
|
| Method | Description |
|
|
113
114
|
|---|---|
|
|
114
|
-
| `t(key, params?, locale?)` | The message of a key in a language, formatted with
|
|
115
|
+
| `t(key, params?, locale?)` | The message of a key in a language, formatted with the arguments; in the default language when none is given |
|
|
115
116
|
| `has(key, locale?)` | Whether a language (or the ones it falls back to) has a key |
|
|
116
|
-
| `format(message, params?, locale?)` |
|
|
117
|
-
| `translator(locale)` | A translator
|
|
118
|
-
| `negotiate(requested)` | The supported language
|
|
119
|
-
| `addMessages(locale, messages)` |
|
|
120
|
-
| `getLocales()`, `getDefaultLocale()`, `getKeys(locale)`, `getTimeZone()` | The supported languages, the default
|
|
117
|
+
| `format(message, params?, locale?)` | Formats an ICU message that is not in the files |
|
|
118
|
+
| `translator(locale)` | A translator bound to a language (`ITranslator`), so that the language is not passed each time |
|
|
119
|
+
| `negotiate(requested)` | The supported language that best matches the requested ones (preferred first); the default language when none is supported |
|
|
120
|
+
| `addMessages(locale, messages)` | Adds messages to a language at run time, replacing the same keys |
|
|
121
|
+
| `getLocales()`, `getDefaultLocale()`, `getKeys(locale)`, `getTimeZone()` | The supported languages, the default language, the keys a language defines itself, the time zone |
|
|
121
122
|
|
|
122
|
-
`t()`, `has()`, `format()` and `translator()` do not negotiate
|
|
123
|
-
|
|
124
|
-
registered (the messages
|
|
123
|
+
`t()`, `has()`, `format()` and `translator()` do not negotiate: a language that is not supported is looked up as given,
|
|
124
|
+
then falls back. `addMessages()` returns a promise that resolves once the validation messages of a new language are
|
|
125
|
+
registered; `t()` gives the new messages as soon as `addMessages()` returns.
|
|
125
126
|
|
|
126
127
|
### Answering a request in its language
|
|
127
128
|
|
|
128
|
-
With the `I18nMiddleware` on the entrypoint, each request
|
|
129
|
-
`Translator`, the messages
|
|
129
|
+
With the `I18nMiddleware` on the entrypoint, each request gets a language; the objects of the request inject the
|
|
130
|
+
`Translator`, which gives the messages in that language:
|
|
130
131
|
|
|
131
132
|
```typescript
|
|
132
133
|
import {Application} from 'lakutata'
|
|
@@ -165,8 +166,8 @@ Application.run(() => ({
|
|
|
165
166
|
}))
|
|
166
167
|
```
|
|
167
168
|
|
|
168
|
-
The languages a request asks for are read in
|
|
169
|
-
|
|
169
|
+
The languages a request asks for are read from the sources below, in order, then negotiated with the supported languages
|
|
170
|
+
(see "Negotiation and fallbacks"); when none is supported, the request gets the default language:
|
|
170
171
|
|
|
171
172
|
| Entrypoint | Sources (preferred first) |
|
|
172
173
|
|---|---|
|
|
@@ -179,11 +180,11 @@ default language when none is supported:
|
|
|
179
180
|
A query or cookie value that is not a language tag is skipped.
|
|
180
181
|
|
|
181
182
|
- The objects of a request (the controller, the `@Scoped` providers, the access control rules) inject the same
|
|
182
|
-
`Translator`; `translator.locale`
|
|
183
|
-
- The middleware uses the component named `i18n`.
|
|
184
|
-
|
|
185
|
-
(`environmentVariable`), or
|
|
186
|
-
|
|
183
|
+
`Translator`; `translator.locale` and `context.locale` are the language of the request.
|
|
184
|
+
- The middleware uses the component named `i18n`. Extend `I18nMiddleware` to use another component
|
|
185
|
+
(`@Inject('...') protected readonly i18n`), to rename the query parameter or the cookie (`queryParameter`, `cookie`),
|
|
186
|
+
to read an environment variable (`environmentVariable`), or to choose the language another way (for example, the
|
|
187
|
+
language the signed-in user saved):
|
|
187
188
|
|
|
188
189
|
```typescript
|
|
189
190
|
import {type BaseContext} from 'lakutata/com/entrypoint'
|
|
@@ -202,21 +203,22 @@ A query or cookie value that is not a language tag is skipped.
|
|
|
202
203
|
}
|
|
203
204
|
```
|
|
204
205
|
|
|
205
|
-
-
|
|
206
|
+
- Without the `I18nMiddleware`, the methods of the `Translator` throw an `I18nTranslatorException`.
|
|
206
207
|
- A translator (the `Translator`, and the `ITranslator` of `translator(locale)`) has `locale`, `t(key, params)`,
|
|
207
208
|
`has(key)`, `format(message, params)`, `number(value, options)` and `date(value, options)`.
|
|
208
|
-
-
|
|
209
|
-
`translator(locale)
|
|
210
|
-
- To
|
|
211
|
-
the languages of the environment (`zh_CN.UTF-8` becomes `zh-CN`)
|
|
209
|
+
- Outside a request (in the schedules or the singleton components), use the `t(key, params, locale)` and
|
|
210
|
+
`translator(locale)` of the component.
|
|
211
|
+
- To choose the language yourself, `parseAcceptLanguage(header)` reads an `Accept-Language` header and
|
|
212
|
+
`environmentLocales()` reads the languages of the environment (`zh_CN.UTF-8` becomes `zh-CN`); pass their result to
|
|
213
|
+
`negotiate()`.
|
|
212
214
|
|
|
213
|
-
##
|
|
215
|
+
## Help and prompts of a command line tool
|
|
214
216
|
|
|
215
|
-
The `translate` option of the Commander adapter (`lakutata/adapter/commander`)
|
|
216
|
-
command line tool in the language of the user: the descriptions of the program
|
|
217
|
-
and options, the hints
|
|
218
|
-
|
|
219
|
-
|
|
217
|
+
The `translate` option of the Commander adapter (`lakutata/adapter/commander`) shows the help and the prompts of a
|
|
218
|
+
command line tool in the language of the user: the descriptions of the program, of its commands and of their arguments
|
|
219
|
+
and options, the hints about defaults and choices, the headings of the help (Usage:, Options:), the descriptions of
|
|
220
|
+
`--help` and `--version`, and the questions asked for missing required options. The names of commands and options are
|
|
221
|
+
not translated.
|
|
220
222
|
|
|
221
223
|
`commanderTranslator()` translates them with the messages of an i18n component:
|
|
222
224
|
|
|
@@ -242,7 +244,7 @@ Application.run(() => ({
|
|
|
242
244
|
i18n: buildI18nOptions({messages: {'zh-CN': zhCN}}),
|
|
243
245
|
entrypoint: buildEntrypoints({
|
|
244
246
|
controllers: [GreetController],
|
|
245
|
-
//The
|
|
247
|
+
//The controllers that run the commands get a Translator in the same language
|
|
246
248
|
middlewares: [I18nMiddleware],
|
|
247
249
|
//TOOL_LANG=zh-CN tool --help, or LANG=zh_CN.UTF-8
|
|
248
250
|
cli: commanderCLIEntrypoint({bin: 'tool', translate: commanderTranslator({environment: 'TOOL_LANG'})})
|
|
@@ -268,39 +270,40 @@ Application.run(() => ({
|
|
|
268
270
|
}
|
|
269
271
|
```
|
|
270
272
|
|
|
271
|
-
- The keys (
|
|
273
|
+
- The keys (with the prefix `cli.` by default, which the `prefix` option changes):
|
|
272
274
|
- the program: `description`; the commands: `commands.<command>.description`, `commands.<command>.arguments.<name>`,
|
|
273
275
|
`commands.<command>.options.<property>`;
|
|
274
276
|
- the help: `help.usage`, `help.arguments`, `help.options`, `help.commands`, `help.helpOption`, `help.helpCommand`,
|
|
275
277
|
`help.versionOption`;
|
|
276
278
|
- the hints: `help.default` (`default: {value}`), `help.choices` (`choices: {choices}`), `help.hints` (` ({hints})`,
|
|
277
|
-
the hints
|
|
279
|
+
how the hints follow a description), `help.separator` (`, `), `help.negate` (`Turn off {flag}`);
|
|
278
280
|
- the questions: `prompt.json` (`Not a JSON object`).
|
|
279
|
-
- A text without a message
|
|
280
|
-
- The language is chosen once, when the adapter starts: the
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
-
|
|
284
|
-
no
|
|
281
|
+
- A text without a message stays in the English of the code, so only the other languages need messages.
|
|
282
|
+
- The language is chosen once, when the adapter starts: from the variable named by the `environment` option, then
|
|
283
|
+
`LANGUAGE`, `LC_ALL`, `LC_MESSAGES`, `LANG`, then the language of the process, and negotiated with the supported
|
|
284
|
+
languages.
|
|
285
|
+
- Wide characters (Chinese, Japanese, Korean) count as two columns; the long lines of the help can break between them,
|
|
286
|
+
and no line starts with punctuation.
|
|
285
287
|
- `translate` can be given a function too: `translate: () => (text) => messages[text.key] ?? text.text`.
|
|
286
288
|
|
|
287
|
-
##
|
|
289
|
+
## Validation errors
|
|
288
290
|
|
|
289
|
-
With the `I18nMiddleware`, the errors of
|
|
290
|
-
|
|
291
|
-
|
|
291
|
+
With the `I18nMiddleware`, the validation errors of a request are in its language: the errors of its input (the DTO of
|
|
292
|
+
`@HTTPAction`, `@CLIAction`, `@ServiceAction`), of the arguments and results of the methods it calls (`@Accept`,
|
|
293
|
+
`@Return`), and of the validations its code runs (`DTO.validate`, `DTO.validateAsync`).
|
|
292
294
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
+
Validation messages for 20 languages are built in; when it starts, the component registers those of its supported
|
|
296
|
+
languages (only the languages in use are loaded):
|
|
295
297
|
|
|
296
|
-
`en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `fr`, `de`, `es`, `pt-BR`, `it`, `nl`, `pl`, `ru`, `uk`, `tr`, `vi`, `th`, `id`,
|
|
298
|
+
`en`, `zh-CN`, `zh-TW`, `ja`, `ko`, `fr`, `de`, `es`, `pt-BR`, `it`, `nl`, `pl`, `ru`, `uk`, `tr`, `vi`, `th`, `id`,
|
|
299
|
+
`hi`, `ar`
|
|
297
300
|
|
|
298
|
-
|
|
299
|
-
use `zh-TW`;
|
|
300
|
-
messages uses
|
|
301
|
+
Each supported language uses the closest built-in one: `zh`, `zh-Hans`, `zh-SG` use `zh-CN`; `zh-Hant`, `zh-HK`, `zh-MO`
|
|
302
|
+
use `zh-TW`; other tags match by their language part (`pt-PT` uses `pt-BR`, `fr-CA` uses `fr`). A language without
|
|
303
|
+
built-in messages uses those of the languages it falls back to, then English.
|
|
301
304
|
|
|
302
|
-
An application
|
|
303
|
-
|
|
305
|
+
An application can replace or add validation messages in its own messages: under `validation`, keyed by error code, and
|
|
306
|
+
written in the template syntax of the validation (`{{#label}}`, `{{#limit}}`) rather than ICU:
|
|
304
307
|
|
|
305
308
|
```json
|
|
306
309
|
{
|
|
@@ -311,23 +314,23 @@ the syntax of the templates of the validation (`{{#label}}`, `{{#limit}}`), not
|
|
|
311
314
|
}
|
|
312
315
|
```
|
|
313
316
|
|
|
314
|
-
- The validation messages of a language are layered: the built-in and
|
|
315
|
-
falls back to, then its own built-in and application messages
|
|
316
|
-
- `validationMessages: false` registers no built-in messages, the `validation` messages of the application
|
|
317
|
-
- The input of a request is validated as
|
|
318
|
-
request,
|
|
319
|
-
- A few messages contain English
|
|
317
|
+
- The validation messages of a language are layered: first the built-in and application messages of the languages it
|
|
318
|
+
falls back to, then its own built-in and application messages; later layers replace earlier ones.
|
|
319
|
+
- `validationMessages: false` registers no built-in messages, only the `validation` messages of the application.
|
|
320
|
+
- The input of a request is validated as usual; only invalid input is validated a second time, in the language of the
|
|
321
|
+
request, to get the messages. Valid input is not affected at all.
|
|
322
|
+
- A few messages contain English text from the validation library itself: the sort order (`ascending`), the reason for a
|
|
320
323
|
reference error, the message of a custom assertion. These parts stay in English in every language.
|
|
321
|
-
- Without the i18n component, the functions of `lakutata
|
|
322
|
-
messages of a language
|
|
323
|
-
with its `{errors: {language}}` option, or `runInValidationLanguage(language, fn)`
|
|
324
|
-
validation
|
|
324
|
+
- Without the i18n component, use the functions of `lakutata`: `loadValidationMessages(language)` loads the built-in
|
|
325
|
+
messages of a language and `registerValidationMessages(language, messages)` registers them. A validation then chooses
|
|
326
|
+
a language with its `{errors: {language}}` option, or `runInValidationLanguage(language, fn)` sets the language of
|
|
327
|
+
every validation that `fn` runs, its asynchronous calls included.
|
|
325
328
|
|
|
326
|
-
##
|
|
329
|
+
## Exception messages
|
|
327
330
|
|
|
328
|
-
With the `I18nMiddleware`, an exception
|
|
329
|
-
|
|
330
|
-
parameters of the template of the exception:
|
|
331
|
+
With the `I18nMiddleware`, when a request throws an exception that has a message among the messages of the application,
|
|
332
|
+
the exception gets that message in the language of the request. The key is `errors.<errno of the exception>`, and the
|
|
333
|
+
message is formatted with the parameters of the template of the exception:
|
|
331
334
|
|
|
332
335
|
```typescript
|
|
333
336
|
import {Exception} from 'lakutata'
|
|
@@ -343,45 +346,46 @@ throw new OrderNotFoundException('Order {id} not found', {id: 7})
|
|
|
343
346
|
{"errors": {"E_ORDER_NOT_FOUND": "订单 {id} 不存在"}}
|
|
344
347
|
```
|
|
345
348
|
|
|
346
|
-
- A request in Chinese gets `订单 7 不存在`, a request in English the original `Order 7 not found`; the `errno` and
|
|
347
|
-
|
|
348
|
-
-
|
|
349
|
-
- An exception
|
|
350
|
-
warning.
|
|
351
|
-
-
|
|
352
|
-
it.
|
|
349
|
+
- A request in Chinese gets `订单 7 不存在`, a request in English the original `Order 7 not found`; the `errno` and the other
|
|
350
|
+
properties stay unchanged.
|
|
351
|
+
- Parameters passed one by one (`new XxxException('{0} of {1}', a, b)`) are referred to as `{0}`, `{1}` in the message.
|
|
352
|
+
- An exception that has no message in the application stays as it is; so does one whose message cannot be formatted
|
|
353
|
+
(because of a missing parameter, for example), with a warning.
|
|
354
|
+
- Outside a request, `exceptionParams(exception)` (from `lakutata`) gives the parameters of an exception, so that you
|
|
355
|
+
can translate it yourself.
|
|
353
356
|
|
|
354
357
|
## Negotiation and fallbacks
|
|
355
358
|
|
|
356
|
-
**Negotiation** (`negotiate`)
|
|
357
|
-
|
|
358
|
-
|
|
359
|
+
**Negotiation** (`negotiate`) tries each requested language in turn: the same tag, then its language alone (`en-GB` →
|
|
360
|
+
`en`), then another region of the same language (`pt-PT` → `pt-BR`, in the order of the supported languages). When none
|
|
361
|
+
matches, it returns the default language.
|
|
359
362
|
|
|
360
|
-
**Fallbacks**: a
|
|
361
|
-
|
|
362
|
-
|
|
363
|
+
**Fallbacks**: when a language lacks a message, the message is looked up in the languages of its `fallback`, then in its
|
|
364
|
+
shorter tags (`zh-Hant-TW` → `zh-Hant` → `zh`), then in the default language. A message is always formatted in the
|
|
365
|
+
**requested language**: an English message used as the fallback for a German request still writes `1.000`.
|
|
363
366
|
|
|
364
|
-
##
|
|
367
|
+
## Message syntax
|
|
365
368
|
|
|
366
369
|
| Syntax | Example |
|
|
367
370
|
|---|---|
|
|
368
|
-
| Argument | `Hello, {name}`: a number is formatted
|
|
371
|
+
| Argument | `Hello, {name}`: a number is formatted for the language, a `Date` as a date, anything else as a string |
|
|
369
372
|
| Number | `{n, number}`, `{n, number, integer}`, `{n, number, percent}` |
|
|
370
373
|
| Currency | `{price, number, currency}` (the currency of the `currency` argument or of the configuration), `{price, number, currency/EUR}` |
|
|
371
374
|
| Date, time | `{at, date, short}`, `{at, time, long}`, styles `short`, `medium` (the default), `long`, `full`; a `Date`, a timestamp, a date string or an object with `toDate()` |
|
|
372
|
-
| Plural | `{count, plural, =0 {none} one {# item} other {# items}}`: the `=value` first, then the plural rules of the language; `#` is the number
|
|
375
|
+
| Plural | `{count, plural, =0 {none} one {# item} other {# items}}`: the `=value` cases first, then the plural rules of the language; `#` is the formatted number; `offset:1` is supported |
|
|
373
376
|
| Ordinal | `{place, selectordinal, one {#st} two {#nd} few {#rd} other {#th}}` |
|
|
374
377
|
| Select | `{role, select, admin {Administrator} other {User}}` |
|
|
375
378
|
| Quoting | `''` is an apostrophe; `'{literal}'` writes the braces (`'#'` writes `#` in a plural) |
|
|
376
379
|
|
|
377
380
|
`plural`, `selectordinal` and `select` need an `other` option. A message is parsed once, when it is first used; a syntax
|
|
378
|
-
error throws an `I18nMessageSyntaxException`
|
|
381
|
+
error throws an `I18nMessageSyntaxException` that gives the language, the key and the position, e.g.
|
|
382
|
+
`zh-CN: order.count: the "other" option is required at 1`.
|
|
379
383
|
|
|
380
|
-
##
|
|
384
|
+
## Typed keys
|
|
381
385
|
|
|
382
|
-
An application can declare its keys and their arguments
|
|
383
|
-
|
|
384
|
-
it
|
|
386
|
+
An application can declare its keys and their arguments; a wrong key or a missing argument then fails the compilation.
|
|
387
|
+
`lakutata i18n types` generates the declaration from the messages of the default language (see below), or you can write
|
|
388
|
+
it by hand:
|
|
385
389
|
|
|
386
390
|
```typescript
|
|
387
391
|
//src/i18n.d.ts
|
|
@@ -397,18 +401,18 @@ declare module 'lakutata/com/i18n' {
|
|
|
397
401
|
|
|
398
402
|
Without them, `t()` accepts any key and any arguments.
|
|
399
403
|
|
|
400
|
-
##
|
|
404
|
+
## Command line tools
|
|
401
405
|
|
|
402
|
-
The `lakutata` command line (`@lakutata/cli`)
|
|
403
|
-
several
|
|
406
|
+
The `lakutata` command line (`@lakutata/cli`) works on the message directories (`--dir`, `locales` by default; separate
|
|
407
|
+
several with commas):
|
|
404
408
|
|
|
405
409
|
| Command | Description |
|
|
406
410
|
|---|---|
|
|
407
|
-
| `lakutata i18n check` | Checks the messages of
|
|
408
|
-
| `lakutata i18n types` |
|
|
409
|
-
| `lakutata i18n add <locale>` | Adds a language
|
|
411
|
+
| `lakutata i18n check` | Checks the messages of each language against the default language (`--default`, `en` by default). Errors: an ICU syntax error, a key the default language does not have, an argument the default message does not have, an unknown validation code, or a variable that belongs to another code. Warnings: a missing message (which falls back), an unused argument, a message identical to the default one (probably not translated yet). It fails on errors, and also on warnings with `--strict`, for CI |
|
|
412
|
+
| `lakutata i18n types` | Writes the type declaration of the keys of the default language and their arguments (`--out`, `src/i18n.d.ts` by default); add `--module @lakutata/i18n` for applications that import `@lakutata/i18n` |
|
|
413
|
+
| `lakutata i18n add <locale>` | Adds a language as a copy of the messages of the default language, ready to translate: a file or a directory, like the default language, without the `validation` part (the new language uses its built-in validation messages) |
|
|
410
414
|
|
|
411
|
-
The checks are
|
|
415
|
+
The checks are also available as functions: `loadCatalogs(directories)`, `checkCatalogs(catalogs, defaultLocale)`,
|
|
412
416
|
`generateMessageTypes(catalogs, defaultLocale)`.
|
|
413
417
|
|
|
414
418
|
## Exceptions
|
|
@@ -416,7 +420,7 @@ The checks are functions of the code too: `loadCatalogs(directories)`, `checkCat
|
|
|
416
420
|
| Exception | When |
|
|
417
421
|
|---|---|
|
|
418
422
|
| `I18nCatalogException` | An invalid file or configuration: invalid JSON, a file name which is not a language tag, a missing directory, an alias not resolved |
|
|
419
|
-
| `I18nMessageSyntaxException` |
|
|
423
|
+
| `I18nMessageSyntaxException` | A message has a syntax error |
|
|
420
424
|
| `I18nMissingKeyException` | A key is missing (`missing: 'throw'`) |
|
|
421
425
|
| `I18nMissingParameterException` | An argument is missing (`missing: 'throw'`), or a currency message has no currency |
|
|
422
|
-
| `I18nTranslatorException` | The `Translator` used without the `I18nMiddleware`, or
|
|
426
|
+
| `I18nTranslatorException` | The `Translator` is used without the `I18nMiddleware`, or outside a request |
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Lakutata Dependency Injection
|
|
2
|
+
|
|
3
|
+
An object does not create the objects it uses: it declares them, and the container provides them when it creates the
|
|
4
|
+
object. This page covers `@Inject` and `@Autoload`, the options of objects (`@Configurable`), and sharing a feature
|
|
5
|
+
between applications as a package. [Decorator](Decorator.md), section 1, gives every rule of these decorators.
|
|
6
|
+
|
|
7
|
+
## 1. Injecting objects
|
|
8
|
+
|
|
9
|
+
`@Inject` (from `lakutata/decorator/di`) sets a property when the object is created, before `init()`:
|
|
10
|
+
|
|
11
|
+
- `@Inject()`: the object registered under the property name;
|
|
12
|
+
- `@Inject('name')` or `@Inject(symbol)`: the object registered under that name;
|
|
13
|
+
- `@Inject(SomeClass)`: the object registered for the class (`@Inject(Application)` gives the application);
|
|
14
|
+
- `@Inject('name', transform)`: the value returned by `transform(object)` (it may be async).
|
|
15
|
+
|
|
16
|
+
A class marked `@Autoload()` is registered the first time it is requested, instead of failing: a singleton in the root
|
|
17
|
+
container, a scoped class in the nearest scope, a transient class in the container that requested it. A dependency that
|
|
18
|
+
cannot be resolved fails the creation with a `DependencyInjectionException`.
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import {Application, Component, Provider} from 'lakutata'
|
|
22
|
+
import {Autoload, Inject, Scoped} from 'lakutata/decorator/di'
|
|
23
|
+
|
|
24
|
+
//Not declared in the application: registered the first time it is injected
|
|
25
|
+
@Autoload()
|
|
26
|
+
@Scoped()
|
|
27
|
+
class RequestClock extends Provider {
|
|
28
|
+
public readonly startedAt: number = Date.now()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
class Reports extends Component {
|
|
32
|
+
@Inject(RequestClock)
|
|
33
|
+
protected readonly clock: RequestClock
|
|
34
|
+
|
|
35
|
+
@Inject(Application)
|
|
36
|
+
protected readonly app: Application
|
|
37
|
+
|
|
38
|
+
public elapsed(): number {
|
|
39
|
+
return Date.now() - this.clock.startedAt
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Code that is not itself an object of the container (an `onLaunched` handler, for example) gets objects with
|
|
45
|
+
`app.getObject(name)`; `app.getObject(SomeClass)` works for a class registered on its own, such as an autoloaded class,
|
|
46
|
+
not for a component declared under a name.
|
|
47
|
+
|
|
48
|
+
## 2. Options
|
|
49
|
+
|
|
50
|
+
`@Configurable(schema, fn?)` turns a property into an option of the object. Its value comes from the object's
|
|
51
|
+
declaration (`{class: Mailer, host: 'smtp.example.com'}`) or from the second argument of `getObject()`; the schema
|
|
52
|
+
validates it, and `fn`, when given, transforms it. The application checks the options of the objects it declares when
|
|
53
|
+
it launches: an invalid value stops the launch with an `InvalidObjectOptionsException`, and an option that matches no
|
|
54
|
+
configurable property produces a warning.
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import {Application, Component, DTO, type ILogger} from 'lakutata'
|
|
58
|
+
import {Configurable, Inject} from 'lakutata/decorator/di'
|
|
59
|
+
|
|
60
|
+
class Mailer extends Component {
|
|
61
|
+
//An option, set by the component's declaration in the application options
|
|
62
|
+
@Configurable(DTO.String().required())
|
|
63
|
+
protected readonly host: string
|
|
64
|
+
|
|
65
|
+
//25 unless configured: the initializer is the default and is validated like a configured value
|
|
66
|
+
@Configurable(DTO.Number().integer().min(1).max(65535))
|
|
67
|
+
protected readonly port: number = 25
|
|
68
|
+
|
|
69
|
+
//The logger preset by the lakutata package as the "log" component
|
|
70
|
+
@Inject('log')
|
|
71
|
+
protected readonly log: ILogger
|
|
72
|
+
|
|
73
|
+
protected async init(): Promise<void> {
|
|
74
|
+
this.log.info('Mailer connected to %s:%d', this.host, this.port)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async send(to: string, text: string): Promise<void> {
|
|
78
|
+
this.log.info('Mail to %s: %s', to, text)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Application
|
|
83
|
+
.run(() => ({
|
|
84
|
+
id: 'com.example.app',
|
|
85
|
+
name: 'Example',
|
|
86
|
+
components: {
|
|
87
|
+
mailer: {class: Mailer, host: 'smtp.example.com'}
|
|
88
|
+
},
|
|
89
|
+
bootstrap: ['mailer']
|
|
90
|
+
}))
|
|
91
|
+
.onLaunched(async (app: Application): Promise<void> => {
|
|
92
|
+
const mailer: Mailer = await app.getObject<Mailer>('mailer')
|
|
93
|
+
await mailer.send('someone@example.com', 'Hello')
|
|
94
|
+
})
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
[Decorator](Decorator.md), section 1.2, gives the order in which a value is chosen (the configured value, the
|
|
98
|
+
initializer, the default of the schema) and the checks made at launch.
|
|
99
|
+
|
|
100
|
+
## 3. Sharing features between applications
|
|
101
|
+
|
|
102
|
+
To use a feature in several applications, put it in a package. The package exports a `Component` (or a `Provider`) and
|
|
103
|
+
a function that builds its declaration, the way the packages of the framework do (`buildDatabaseOptions` of
|
|
104
|
+
`lakutata/com/database`, `buildQueueOptions` of `lakutata/com/queue`). The application declares the component under a
|
|
105
|
+
name of its choice and passes the options to the function. The component's `@Configurable` schemas check the options
|
|
106
|
+
when the application launches, and the other objects inject the component by its name.
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import {Application, Component, type ComponentOptions, DTO} from 'lakutata'
|
|
110
|
+
import {Configurable, Inject} from 'lakutata/decorator/di'
|
|
111
|
+
|
|
112
|
+
//In the package: the component, and the function that builds its declaration
|
|
113
|
+
export interface BillingOptions {
|
|
114
|
+
currency: string
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export class Billing extends Component {
|
|
118
|
+
@Configurable(DTO.String().length(3).required())
|
|
119
|
+
public readonly currency: string
|
|
120
|
+
|
|
121
|
+
public charge(amount: number): string {
|
|
122
|
+
return `Charged ${amount} ${this.currency}`
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildBillingOptions(options: BillingOptions): ComponentOptions<BillingOptions> {
|
|
127
|
+
return {class: Billing, ...options}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//In the application
|
|
131
|
+
class Checkout extends Component {
|
|
132
|
+
@Inject('billing')
|
|
133
|
+
protected readonly billing: Billing
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
Application.run(() => ({
|
|
137
|
+
id: 'shop.app',
|
|
138
|
+
name: 'Shop',
|
|
139
|
+
components: {
|
|
140
|
+
billing: buildBillingOptions({currency: 'EUR'}),
|
|
141
|
+
checkout: {class: Checkout}
|
|
142
|
+
}
|
|
143
|
+
}))
|
|
144
|
+
```
|