@lakutata/i18n 3.0.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +347 -0
- package/dist/cjs/exports/I18n.d.ts +15 -0
- package/dist/cjs/exports/I18n.js +45 -0
- package/dist/cjs/exports/I18nMiddleware.d.ts +2 -0
- package/dist/cjs/exports/I18nMiddleware.js +21 -0
- package/dist/cjs/i18n/I18n.d.ts +313 -0
- package/dist/cjs/i18n/I18n.js +462 -0
- package/dist/cjs/i18n/Translator.d.ts +124 -0
- package/dist/cjs/i18n/Translator.js +156 -0
- package/dist/cjs/i18n/exceptions/I18nCatalogException.d.ts +15 -0
- package/dist/cjs/i18n/exceptions/I18nCatalogException.js +22 -0
- package/dist/cjs/i18n/exceptions/I18nMessageSyntaxException.d.ts +14 -0
- package/dist/cjs/i18n/exceptions/I18nMessageSyntaxException.js +21 -0
- package/dist/cjs/i18n/exceptions/I18nMissingKeyException.d.ts +12 -0
- package/dist/cjs/i18n/exceptions/I18nMissingKeyException.js +19 -0
- package/dist/cjs/i18n/exceptions/I18nMissingParameterException.d.ts +13 -0
- package/dist/cjs/i18n/exceptions/I18nMissingParameterException.js +20 -0
- package/dist/cjs/i18n/exceptions/I18nTranslatorException.d.ts +12 -0
- package/dist/cjs/i18n/exceptions/I18nTranslatorException.js +19 -0
- package/dist/cjs/i18n/interfaces/I18nOptions.d.ts +70 -0
- package/dist/cjs/i18n/interfaces/I18nOptions.js +2 -0
- package/dist/cjs/i18n/interfaces/ITranslator.d.ts +57 -0
- package/dist/cjs/i18n/interfaces/ITranslator.js +2 -0
- package/dist/cjs/i18n/lib/Catalog.d.ts +34 -0
- package/dist/cjs/i18n/lib/Catalog.js +104 -0
- package/dist/cjs/i18n/lib/CatalogTools.d.ts +86 -0
- package/dist/cjs/i18n/lib/CatalogTools.js +218 -0
- package/dist/cjs/i18n/lib/Locale.d.ts +95 -0
- package/dist/cjs/i18n/lib/Locale.js +182 -0
- package/dist/cjs/i18n/lib/LocaleTranslator.d.ts +93 -0
- package/dist/cjs/i18n/lib/LocaleTranslator.js +105 -0
- package/dist/cjs/i18n/lib/MessageFormat.d.ts +144 -0
- package/dist/cjs/i18n/lib/MessageFormat.js +372 -0
- package/dist/cjs/i18n/middleware/CommanderTranslator.d.ts +71 -0
- package/dist/cjs/i18n/middleware/CommanderTranslator.js +69 -0
- package/dist/cjs/i18n/middleware/I18nMiddleware.d.ts +126 -0
- package/dist/cjs/i18n/middleware/I18nMiddleware.js +210 -0
- package/dist/cjs/i18n/types/I18nMessages.d.ts +24 -0
- package/dist/cjs/i18n/types/I18nMessages.js +2 -0
- package/dist/cjs/package.json +1 -0
- package/dist/esm/exports/I18n.js +14 -0
- package/dist/esm/exports/I18nMiddleware.js +2 -0
- package/dist/esm/i18n/I18n.js +454 -0
- package/dist/esm/i18n/Translator.js +153 -0
- package/dist/esm/i18n/exceptions/I18nCatalogException.js +18 -0
- package/dist/esm/i18n/exceptions/I18nMessageSyntaxException.js +17 -0
- package/dist/esm/i18n/exceptions/I18nMissingKeyException.js +15 -0
- package/dist/esm/i18n/exceptions/I18nMissingParameterException.js +16 -0
- package/dist/esm/i18n/exceptions/I18nTranslatorException.js +15 -0
- package/dist/esm/i18n/interfaces/I18nOptions.js +1 -0
- package/dist/esm/i18n/interfaces/ITranslator.js +1 -0
- package/dist/esm/i18n/lib/Catalog.js +96 -0
- package/dist/esm/i18n/lib/CatalogTools.js +213 -0
- package/dist/esm/i18n/lib/Locale.js +173 -0
- package/dist/esm/i18n/lib/LocaleTranslator.js +101 -0
- package/dist/esm/i18n/lib/MessageFormat.js +367 -0
- package/dist/esm/i18n/middleware/CommanderTranslator.js +66 -0
- package/dist/esm/i18n/middleware/I18nMiddleware.js +206 -0
- package/dist/esm/i18n/types/I18nMessages.js +1 -0
- package/dist/types/exports/I18n.d.ts +15 -0
- package/dist/types/exports/I18nMiddleware.d.ts +2 -0
- package/dist/types/i18n/I18n.d.ts +313 -0
- package/dist/types/i18n/Translator.d.ts +124 -0
- package/dist/types/i18n/exceptions/I18nCatalogException.d.ts +15 -0
- package/dist/types/i18n/exceptions/I18nMessageSyntaxException.d.ts +14 -0
- package/dist/types/i18n/exceptions/I18nMissingKeyException.d.ts +12 -0
- package/dist/types/i18n/exceptions/I18nMissingParameterException.d.ts +13 -0
- package/dist/types/i18n/exceptions/I18nTranslatorException.d.ts +12 -0
- package/dist/types/i18n/interfaces/I18nOptions.d.ts +70 -0
- package/dist/types/i18n/interfaces/ITranslator.d.ts +57 -0
- package/dist/types/i18n/lib/Catalog.d.ts +34 -0
- package/dist/types/i18n/lib/CatalogTools.d.ts +86 -0
- package/dist/types/i18n/lib/Locale.d.ts +95 -0
- package/dist/types/i18n/lib/LocaleTranslator.d.ts +93 -0
- package/dist/types/i18n/lib/MessageFormat.d.ts +144 -0
- package/dist/types/i18n/middleware/CommanderTranslator.d.ts +71 -0
- package/dist/types/i18n/middleware/I18nMiddleware.d.ts +126 -0
- package/dist/types/i18n/types/I18nMessages.d.ts +24 -0
- package/package.json +60 -0
|
@@ -0,0 +1,210 @@
|
|
|
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.I18nMiddleware = void 0;
|
|
13
|
+
const core_1 = require("@lakutata/core");
|
|
14
|
+
const di_1 = require("@lakutata/core/decorator/di");
|
|
15
|
+
const entrypoint_1 = require("@lakutata/entrypoint");
|
|
16
|
+
const Translator_js_1 = require("../Translator.js");
|
|
17
|
+
const Locale_js_1 = require("../lib/Locale.js");
|
|
18
|
+
/**
|
|
19
|
+
* The entrypoint middleware choosing the language of each request. Add it to the `middlewares` of
|
|
20
|
+
* `buildEntrypoints()`. For each request, it reads the languages the request asks for ({@link I18nMiddleware.requested}),
|
|
21
|
+
* negotiates them with the supported languages of the {@link I18n} component ({@link I18n.negotiate}, the default
|
|
22
|
+
* language when none matches), and makes the result:
|
|
23
|
+
* - the request's `context.locale`;
|
|
24
|
+
* - the language of the {@link Translator} the request's objects inject;
|
|
25
|
+
* - the language of the validation errors of the request (its input, `@Accept`/`@Return`, the `DTO.validate` calls of
|
|
26
|
+
* its code);
|
|
27
|
+
* - the language of the exceptions it throws: a lakutata `Exception` whose `errno` has a message `errors.<errno>`
|
|
28
|
+
* (`errors.E_ORDER_NOT_FOUND`) gets that message, formatted with the exception's template parameters.
|
|
29
|
+
*
|
|
30
|
+
* The languages are read, preferred first: for HTTP, the `lang` query parameter, the `lang` cookie, then
|
|
31
|
+
* `Accept-Language`; for Socket.IO, the same from the connection's handshake; for the HTTP service adapter and the
|
|
32
|
+
* JSON-RPC adapter over HTTP or WebSocket, the same from the HTTP request (for WebSocket, the request opening the
|
|
33
|
+
* connection); for the command line and the other service transports, the process's environment
|
|
34
|
+
* ({@link environmentLocales}) then the process's language; for the schedules, none. It uses the component registered as `i18n`; a subclass changes the component, the names of the query
|
|
35
|
+
* parameter and of the cookie, or the languages read (`requested()`).
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* import {type BaseContext} from 'lakutata/com/entrypoint'
|
|
39
|
+
* import {Inject} from 'lakutata/decorator/di'
|
|
40
|
+
* import {I18nMiddleware, type I18n} from 'lakutata/com/i18n'
|
|
41
|
+
*
|
|
42
|
+
* //BuildEntrypoints({middlewares: [LanguageMiddleware], ...}) in place of I18nMiddleware
|
|
43
|
+
* export class LanguageMiddleware extends I18nMiddleware {
|
|
44
|
+
* //The component registered as "lang"
|
|
45
|
+
* @Inject('lang')
|
|
46
|
+
* protected readonly i18n: I18n
|
|
47
|
+
*
|
|
48
|
+
* //?locale=zh-CN
|
|
49
|
+
* protected readonly queryParameter: string = 'locale'
|
|
50
|
+
*
|
|
51
|
+
* protected readonly environmentVariable: string = 'SHOP_LANG'
|
|
52
|
+
*
|
|
53
|
+
* //The language saved by the signed-in user first
|
|
54
|
+
* protected requested(context: BaseContext): string[] {
|
|
55
|
+
* const saved: unknown = context.data.userLanguage
|
|
56
|
+
* return [...(typeof saved === 'string' ? [saved] : []), ...super.requested(context)]
|
|
57
|
+
* }
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
class I18nMiddleware extends entrypoint_1.Middleware {
|
|
62
|
+
constructor() {
|
|
63
|
+
super(...arguments);
|
|
64
|
+
/**
|
|
65
|
+
* The name of the query parameter giving the language of an HTTP request (of the HTTP channel, the HTTP service and
|
|
66
|
+
* JSON-RPC adapters) or a Socket.IO connection (`?lang=zh-CN`); a value which is not a language tag is ignored.
|
|
67
|
+
* @default 'lang'
|
|
68
|
+
*/
|
|
69
|
+
this.queryParameter = 'lang';
|
|
70
|
+
/**
|
|
71
|
+
* The name of the cookie giving the language of an HTTP request (of the HTTP channel, the HTTP service and JSON-RPC
|
|
72
|
+
* adapters) or a Socket.IO connection; a value which is not a language tag is ignored.
|
|
73
|
+
* @default 'lang'
|
|
74
|
+
*/
|
|
75
|
+
this.cookie = 'lang';
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Negotiate the request's language, set `context.locale`, create the request's {@link Translator}, then run the
|
|
79
|
+
* next middlewares and the action with the validations in that language, translating the exception they throw.
|
|
80
|
+
* @param invocation The action invocation: its context and its container.
|
|
81
|
+
* @param next Runs the rest of the chain.
|
|
82
|
+
* @returns The result of the rest of the chain; an exception it throws is rethrown, its message translated by
|
|
83
|
+
* {@link I18nMiddleware.translate}.
|
|
84
|
+
*/
|
|
85
|
+
async handle(invocation, next) {
|
|
86
|
+
const context = invocation.context;
|
|
87
|
+
const locale = this.i18n.negotiate(this.requested(context));
|
|
88
|
+
context.locale = locale;
|
|
89
|
+
//The translator of the request, shared by its objects
|
|
90
|
+
const translator = await invocation.container.getShared(Translator_js_1.Translator, { locale: locale, i18n: this.i18n });
|
|
91
|
+
//Its validations in its language
|
|
92
|
+
return await (0, core_1.runInValidationLanguage)(locale, async () => {
|
|
93
|
+
try {
|
|
94
|
+
return await next();
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
throw this.translate(error, translator);
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Translate an exception thrown by a request into the request's language. A lakutata `Exception` with an `errno`
|
|
103
|
+
* whose key `errors.<errno>` has a message (`errors.E_ORDER_NOT_FOUND`) gets that message as its `message` and
|
|
104
|
+
* `errMsg`, formatted with the parameters of its template (`{id}` for named ones, `{0}`, `{1}` for the ones given one
|
|
105
|
+
* by one); its `errno` and its other properties are kept. The other errors, the exceptions without such a message,
|
|
106
|
+
* and the ones whose message cannot be formatted (a process warning `I18nWarning` is then emitted) are returned
|
|
107
|
+
* unchanged.
|
|
108
|
+
* @param error The thrown value.
|
|
109
|
+
* @param translator The request's translator.
|
|
110
|
+
* @returns The same value, its message replaced when it is translated.
|
|
111
|
+
*/
|
|
112
|
+
translate(error, translator) {
|
|
113
|
+
if (!(error instanceof core_1.Exception) || error.errno === undefined || error.errno === null)
|
|
114
|
+
return error;
|
|
115
|
+
const key = `errors.${error.errno}`;
|
|
116
|
+
if (!translator.has(key))
|
|
117
|
+
return error;
|
|
118
|
+
const params = (0, core_1.exceptionParams)(error);
|
|
119
|
+
let message;
|
|
120
|
+
try {
|
|
121
|
+
message = translator.t(key, Array.isArray(params) ? Object.fromEntries(params.map((value, index) => [String(index), value])) : params ?? {});
|
|
122
|
+
}
|
|
123
|
+
catch (failure) {
|
|
124
|
+
process.emitWarning(`The message ${key} of ${error.name} cannot be formatted: ${failure.message}`, 'I18nWarning');
|
|
125
|
+
return error;
|
|
126
|
+
}
|
|
127
|
+
error.message = message;
|
|
128
|
+
Reflect.set(error, 'errMsg', message);
|
|
129
|
+
return error;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Read the languages a request asks for, preferred first:
|
|
133
|
+
* - HTTP: the query parameter, the cookie, then `Accept-Language`;
|
|
134
|
+
* - Socket.IO: the query parameter, the cookie and `Accept-Language` of the connection's handshake;
|
|
135
|
+
* - the HTTP service adapter, and the JSON-RPC adapter over HTTP or WebSocket: the query parameter, the cookie and
|
|
136
|
+
* `Accept-Language` of the HTTP request (for WebSocket, the request opening the connection);
|
|
137
|
+
* - the command line and the other service transports (Electron IPC, JSON-RPC over stdio): the environment variable
|
|
138
|
+
* of `environmentVariable`, the environment (`LANGUAGE`, `LC_ALL`, `LC_MESSAGES`, `LANG`), then the process's
|
|
139
|
+
* language;
|
|
140
|
+
* - the schedules and the other contexts: none (the default language is chosen).
|
|
141
|
+
* @param context The request's context.
|
|
142
|
+
* @returns The requested language tags, preferred first.
|
|
143
|
+
*/
|
|
144
|
+
requested(context) {
|
|
145
|
+
if (context.type === entrypoint_1.ContextType.HTTP)
|
|
146
|
+
return this.fromRequest(context.request);
|
|
147
|
+
if (context.type === entrypoint_1.ContextType.SERVICE) {
|
|
148
|
+
const handshake = context.socket?.handshake;
|
|
149
|
+
if (handshake)
|
|
150
|
+
return this.fromHeaders(handshake.query?.[this.queryParameter], handshake.headers ?? {});
|
|
151
|
+
//The HTTP service adapter, JSON-RPC over HTTP, and over WebSocket (the request upgraded)
|
|
152
|
+
const request = (0, entrypoint_1.httpRequestOfContext)(context);
|
|
153
|
+
if (request)
|
|
154
|
+
return this.fromRequest(request);
|
|
155
|
+
}
|
|
156
|
+
if (context.type === entrypoint_1.ContextType.CLI || context.type === entrypoint_1.ContextType.SERVICE) {
|
|
157
|
+
return (0, Locale_js_1.processLocales)(this.environmentVariable);
|
|
158
|
+
}
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Read the languages of an HTTP request: its query parameter, its cookie, then its `Accept-Language`.
|
|
163
|
+
* @param request The Node.js HTTP request.
|
|
164
|
+
* @returns The language tags, preferred first.
|
|
165
|
+
*/
|
|
166
|
+
fromRequest(request) {
|
|
167
|
+
return this.fromHeaders(new URL(request.url ?? '/', 'http://localhost').searchParams.get(this.queryParameter), request.headers);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Read the languages of a query parameter, the cookie and `Accept-Language`, in that order; the query and cookie
|
|
171
|
+
* values which are not language tags are skipped.
|
|
172
|
+
* @param query The value of the query parameter; ignored unless it is a string.
|
|
173
|
+
* @param headers The request headers, by lower-case name; for a repeated header, its first value is read.
|
|
174
|
+
* @returns The language tags, preferred first.
|
|
175
|
+
*/
|
|
176
|
+
fromHeaders(query, headers) {
|
|
177
|
+
const single = (value) => Array.isArray(value) ? value[0] : value;
|
|
178
|
+
const requested = [];
|
|
179
|
+
if (typeof query === 'string' && (0, Locale_js_1.normalizeLocale)(query))
|
|
180
|
+
requested.push(query);
|
|
181
|
+
const cookie = this.cookieValue(single(headers.cookie));
|
|
182
|
+
if (cookie && (0, Locale_js_1.normalizeLocale)(cookie))
|
|
183
|
+
requested.push(cookie);
|
|
184
|
+
return [...requested, ...(0, Locale_js_1.parseAcceptLanguage)(single(headers['accept-language']))];
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Read the value of the language cookie (named by `cookie`) in a `Cookie` header, URI-decoded.
|
|
188
|
+
* @param header The value of the `Cookie` header.
|
|
189
|
+
* @returns The value of the first cookie of that name; `undefined` without it, or when it cannot be decoded.
|
|
190
|
+
*/
|
|
191
|
+
cookieValue(header) {
|
|
192
|
+
for (const part of header?.split(';') ?? []) {
|
|
193
|
+
const separator = part.indexOf('=');
|
|
194
|
+
if (separator < 0 || part.slice(0, separator).trim() !== this.cookie)
|
|
195
|
+
continue;
|
|
196
|
+
try {
|
|
197
|
+
return decodeURIComponent(part.slice(separator + 1).trim());
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
exports.I18nMiddleware = I18nMiddleware;
|
|
207
|
+
__decorate([
|
|
208
|
+
(0, di_1.Inject)('i18n'),
|
|
209
|
+
__metadata("design:type", Function)
|
|
210
|
+
], I18nMiddleware.prototype, "i18n", void 0);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The application's message keys and their arguments, declared by the application to type-check {@link I18n.t} and
|
|
3
|
+
* the translators: a wrong key or a missing argument is then a compilation error. `lakutata i18n types` (or
|
|
4
|
+
* {@link generateMessageTypes}) generates the declaration from the messages of the default language, or it is written
|
|
5
|
+
* by hand, in the module the application imports the i18n from:
|
|
6
|
+
*
|
|
7
|
+
* declare module 'lakutata/com/i18n' {
|
|
8
|
+
* interface I18nMessages {
|
|
9
|
+
* 'order.count': { count: number }
|
|
10
|
+
* }
|
|
11
|
+
* }
|
|
12
|
+
*
|
|
13
|
+
* Empty by default: any key and any arguments are then accepted.
|
|
14
|
+
*/
|
|
15
|
+
export interface I18nMessages {
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A message key: one of the keys declared in {@link I18nMessages}, any string when none is declared.
|
|
19
|
+
*/
|
|
20
|
+
export type MessageKey = keyof I18nMessages extends never ? string : Extract<keyof I18nMessages, string>;
|
|
21
|
+
/**
|
|
22
|
+
* The arguments of the message of a key: the ones declared in {@link I18nMessages}, any record for a key not declared.
|
|
23
|
+
*/
|
|
24
|
+
export type MessageParams<K extends string> = K extends keyof I18nMessages ? I18nMessages[K] : Record<string, unknown>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"commonjs"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from '../i18n/I18n.js';
|
|
2
|
+
export * from '../i18n/Translator.js';
|
|
3
|
+
export { LocaleTranslator } from '../i18n/lib/LocaleTranslator.js';
|
|
4
|
+
export * from '../i18n/interfaces/I18nOptions.js';
|
|
5
|
+
export * from '../i18n/interfaces/ITranslator.js';
|
|
6
|
+
export * from '../i18n/types/I18nMessages.js';
|
|
7
|
+
export { checkCatalogs, generateMessageTypes, loadCatalogs } from '../i18n/lib/CatalogTools.js';
|
|
8
|
+
export { environmentLocales, fallbackChain, negotiateLocale, normalizeLocale, parseAcceptLanguage } from '../i18n/lib/Locale.js';
|
|
9
|
+
export { messageArguments, parseMessage } from '../i18n/lib/MessageFormat.js';
|
|
10
|
+
export * from '../i18n/exceptions/I18nCatalogException.js';
|
|
11
|
+
export * from '../i18n/exceptions/I18nMessageSyntaxException.js';
|
|
12
|
+
export * from '../i18n/exceptions/I18nMissingKeyException.js';
|
|
13
|
+
export * from '../i18n/exceptions/I18nMissingParameterException.js';
|
|
14
|
+
export * from '../i18n/exceptions/I18nTranslatorException.js';
|