moost 0.7.2 → 0.7.4
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/dist/index.cjs +245 -30
- package/dist/index.d.ts +130 -22
- package/dist/index.mjs +241 -31
- package/package.json +6 -6
package/dist/index.cjs
CHANGED
|
@@ -48,7 +48,7 @@ let stamped = false;
|
|
|
48
48
|
if (stamped) return;
|
|
49
49
|
stamped = true;
|
|
50
50
|
stampModuleIdentity({
|
|
51
|
-
version: "0.7.
|
|
51
|
+
version: "0.7.4",
|
|
52
52
|
path: typeof __filename === "string" ? __filename : require("url").pathToFileURL(__filename).href
|
|
53
53
|
});
|
|
54
54
|
}
|
|
@@ -658,7 +658,7 @@ function formatNewInstanceArg(a) {
|
|
|
658
658
|
//#endregion
|
|
659
659
|
//#region packages/moost/src/composables/global-key.ts
|
|
660
660
|
var _g;
|
|
661
|
-
const registry
|
|
661
|
+
const registry = (_g = globalThis).__moost_keys ?? (_g.__moost_keys = /* @__PURE__ */ new Map());
|
|
662
662
|
/**
|
|
663
663
|
* Creates a typed event-context key that stays a single instance across duplicate
|
|
664
664
|
* moost loads (dual ESM/CJS, or duplicate installs). Use this for every
|
|
@@ -666,8 +666,8 @@ const registry$1 = (_g = globalThis).__moost_keys ?? (_g.__moost_keys = /* @__PU
|
|
|
666
666
|
*
|
|
667
667
|
* @param name - Debug label AND the registry lookup key; must be unique per slot.
|
|
668
668
|
*/ function globalKey(name) {
|
|
669
|
-
if (!registry
|
|
670
|
-
return registry
|
|
669
|
+
if (!registry.has(name)) registry.set(name, (0, _wooksjs_event_core.key)(name));
|
|
670
|
+
return registry.get(name);
|
|
671
671
|
}
|
|
672
672
|
|
|
673
673
|
//#endregion
|
|
@@ -3184,48 +3184,258 @@ function _define_property(obj, key, value) {
|
|
|
3184
3184
|
|
|
3185
3185
|
//#endregion
|
|
3186
3186
|
//#region packages/moost/src/i18n.ts
|
|
3187
|
-
|
|
3188
|
-
const
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3187
|
+
var _globalThis, _STATE, _state, _state1, _state2, _state3, _state4;
|
|
3188
|
+
const STATE = Symbol.for("moost.i18n");
|
|
3189
|
+
const NO_LOCALES = Object.freeze([]);
|
|
3190
|
+
/** Distinct scopes given memo keys per generation: keeps every memo keyed by `key` bounded. */ const MAX_SCOPES = 32;
|
|
3191
|
+
const state = (_globalThis = globalThis)[_STATE = STATE] ?? (_globalThis[_STATE] = {});
|
|
3192
|
+
(_state = state).locales ?? (_state.locales = NO_LOCALES);
|
|
3193
|
+
(_state1 = state).generation ?? (_state1.generation = 0);
|
|
3194
|
+
(_state2 = state).catalogs ?? (_state2.catalogs = /* @__PURE__ */ new WeakSet());
|
|
3195
|
+
(_state3 = state).templates ?? (_state3.templates = /* @__PURE__ */ new Map());
|
|
3196
|
+
(_state4 = state).memo ?? (_state4.memo = /* @__PURE__ */ new Map());
|
|
3197
|
+
/**
|
|
3198
|
+
* Registers the app's translator (process-wide), or removes it with `undefined`: a
|
|
3199
|
+
* {@link TTranslate} function or a {@link TTranslations} map. Load your languages before the
|
|
3200
|
+
* server listens, or call `setTranslator` again once they are loaded.
|
|
3201
|
+
*
|
|
3202
|
+
* `opts.locales` lists the locales the app serves, the first being the default (`MoostHttp`
|
|
3203
|
+
* negotiates `Accept-Language` against them). For a map they default to its keys — put the
|
|
3204
|
+
* source language first: `{ en: {}, de: {…} }`. Give a function translator `locales`, or
|
|
3205
|
+
* requests get a locale only from your own `resolveLocale` / `useLocale().set()`.
|
|
3194
3206
|
*
|
|
3195
3207
|
* @example
|
|
3196
3208
|
* ```ts
|
|
3197
|
-
*
|
|
3198
|
-
*
|
|
3209
|
+
* setTranslator({ en: {}, de: { NAME_TAKEN: 'Name {name} ist vergeben' } })
|
|
3210
|
+
*
|
|
3211
|
+
* setTranslator(
|
|
3212
|
+
* (id, params, { locale }) =>
|
|
3213
|
+
* locale && i18next.exists(id, { lng: locale }) ? i18next.t(id, { ...params, lng: locale }) : undefined,
|
|
3214
|
+
* { locales: ['en', 'de'] },
|
|
3215
|
+
* )
|
|
3199
3216
|
* ```
|
|
3200
|
-
*/ function setTranslator(translate) {
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3217
|
+
*/ function setTranslator(translate, opts) {
|
|
3218
|
+
const isMap = typeof translate === "object";
|
|
3219
|
+
state.translate = isMap ? mapTranslator(translate) : translate;
|
|
3220
|
+
state.scoping = opts?.scope && {
|
|
3221
|
+
translate: state.translate,
|
|
3222
|
+
get: opts.scope
|
|
3223
|
+
};
|
|
3224
|
+
const locales = opts?.locales ?? (isMap ? Object.keys(translate) : void 0);
|
|
3225
|
+
state.locales = translate && locales?.length ? Object.freeze([...locales]) : NO_LOCALES;
|
|
3226
|
+
state.failureLogged = false;
|
|
3227
|
+
state.scopeFailureLogged = false;
|
|
3228
|
+
newGeneration();
|
|
3229
|
+
}
|
|
3230
|
+
/** The registered translator (a map wrapped as a function), or `undefined`. */ function getTranslator() {
|
|
3231
|
+
return state.translate;
|
|
3232
|
+
}
|
|
3233
|
+
/** The locales of the registered translator, the default first (empty without a translator). */ function getTranslatorLocales() {
|
|
3234
|
+
return state.locales;
|
|
3235
|
+
}
|
|
3236
|
+
/**
|
|
3237
|
+
* Registers message templates by code (`{ MIN_LENGTH: 'At least {min} characters' }`),
|
|
3238
|
+
* process-wide: the English text `useTranslate()` falls back to for the code, and the templates
|
|
3239
|
+
* {@link TTranslation.messages} serves translated. Registering the same catalog object again
|
|
3240
|
+
* does nothing; a later catalog wins for a code both define. A call that adds or changes a
|
|
3241
|
+
* template starts a new generation, like {@link setTranslator} ({@link TTranslation.key}
|
|
3242
|
+
* changes).
|
|
3243
|
+
*/ function registerMessages(catalog) {
|
|
3244
|
+
if (state.catalogs.has(catalog)) return;
|
|
3245
|
+
state.catalogs.add(catalog);
|
|
3246
|
+
let changed = false;
|
|
3247
|
+
for (const code of Object.keys(catalog)) if (state.templates.get(code) !== catalog[code]) {
|
|
3248
|
+
state.templates.set(code, catalog[code]);
|
|
3249
|
+
changed = true;
|
|
3250
|
+
}
|
|
3251
|
+
if (changed) newGeneration();
|
|
3252
|
+
}
|
|
3253
|
+
/** Drops everything derived from the translator and the templates. */ function newGeneration() {
|
|
3254
|
+
state.generation++;
|
|
3255
|
+
state.memo.clear();
|
|
3256
|
+
}
|
|
3257
|
+
const PLACEHOLDER = /\{(\w+)\}/g;
|
|
3258
|
+
/**
|
|
3259
|
+
* Fills `{name}` placeholders with `params` in one pass (arrays: items joined with `, `).
|
|
3260
|
+
* A placeholder without a param stays as it is; filled-in values are never filled again.
|
|
3261
|
+
*/ function formatMessage(template, params) {
|
|
3262
|
+
if (!params || !template.includes("{")) return template;
|
|
3263
|
+
return template.replace(PLACEHOLDER, (match, name) => {
|
|
3264
|
+
const value = Object.hasOwn(params, name) ? params[name] : void 0;
|
|
3265
|
+
if (value === void 0) return match;
|
|
3266
|
+
return Array.isArray(value) ? value.map(String).join(", ") : String(value);
|
|
3267
|
+
});
|
|
3205
3268
|
}
|
|
3206
3269
|
/**
|
|
3207
|
-
* Returns `t(
|
|
3208
|
-
*
|
|
3209
|
-
*
|
|
3270
|
+
* Returns `t(id, params?, fallback?)` for the current event (or `ctx`), in the event's locale
|
|
3271
|
+
* (`useLocale().get()`). `id` is a code or a source text. `t` returns:
|
|
3272
|
+
* - the translator's result, as is (it is final text: params are not filled again);
|
|
3273
|
+
* - else `fallback` as is — it is final too, never filled: pass already-filled English text
|
|
3274
|
+
* (e.g. an error's message), or pass no fallback and {@link registerMessages} a template;
|
|
3275
|
+
* - else the code's registered template ({@link registerMessages}), or `id` itself, with
|
|
3276
|
+
* `params` filled in ({@link formatMessage}).
|
|
3210
3277
|
*
|
|
3211
3278
|
* @example
|
|
3212
3279
|
* ```ts
|
|
3213
3280
|
* const t = useTranslate()
|
|
3214
|
-
* throw new HttpError(409, { message: t('Name taken
|
|
3281
|
+
* throw new HttpError(409, { message: t('NAME_TAKEN', { name }, `Name ${name} is taken`), code: 'NAME_TAKEN', params: { name } })
|
|
3215
3282
|
* ```
|
|
3216
3283
|
*/ function useTranslate(ctx) {
|
|
3217
3284
|
const event = ctx ?? (0, _wooksjs_event_core.current)();
|
|
3218
3285
|
let translateCtx;
|
|
3219
|
-
return (
|
|
3220
|
-
const translate =
|
|
3221
|
-
|
|
3222
|
-
translateCtx ?? (translateCtx = {
|
|
3223
|
-
locale: (0, _wooksjs_event_core.useLocale)(event).get(),
|
|
3224
|
-
event
|
|
3225
|
-
});
|
|
3226
|
-
return translate(text, params, translateCtx) ?? text;
|
|
3286
|
+
return (id, params, fallback) => {
|
|
3287
|
+
const translate = state.translate;
|
|
3288
|
+
return (translate ? callTranslator(translate, id, params, translateCtx ?? (translateCtx = translateContext(event, (0, _wooksjs_event_core.useLocale)(event).get(), readScope(event)))) : void 0) ?? untranslated(id, params, fallback);
|
|
3227
3289
|
};
|
|
3228
3290
|
}
|
|
3291
|
+
/** `t`'s text without a translation: `fallback` as is, else the filled template (or id). */ function untranslated(id, params, fallback) {
|
|
3292
|
+
return fallback ?? formatMessage(state.templates.get(id) ?? id, params);
|
|
3293
|
+
}
|
|
3294
|
+
/**
|
|
3295
|
+
* The translation of the current event (or `ctx`): its locale, a memo key, `t` and the
|
|
3296
|
+
* translated message templates. `undefined` when no translator is registered or the event has
|
|
3297
|
+
* no locale — serve the source texts then.
|
|
3298
|
+
*/ function useTranslation(ctx) {
|
|
3299
|
+
const translate = state.translate;
|
|
3300
|
+
if (!translate) return;
|
|
3301
|
+
const event = ctx ?? (0, _wooksjs_event_core.current)();
|
|
3302
|
+
const locale = (0, _wooksjs_event_core.useLocale)(event).get();
|
|
3303
|
+
if (locale === void 0) return;
|
|
3304
|
+
const scope = readScope(event);
|
|
3305
|
+
const translateCtx = translateContext(event, locale, scope);
|
|
3306
|
+
const generation = state.generation;
|
|
3307
|
+
const partition = typeof scope === "string" ? `${locale}:${scope}` : locale;
|
|
3308
|
+
const key = state.locales.includes(locale) && (scope === void 0 || scope !== null && admitted(scope)) ? `${generation}:${partition}` : void 0;
|
|
3309
|
+
const t = (id, params, fallback) => callTranslator(translate, id, params, translateCtx) ?? untranslated(id, params, fallback);
|
|
3310
|
+
const messages = () => {
|
|
3311
|
+
if (key === void 0 || state.generation !== generation) return translateTemplates(translate, translateCtx);
|
|
3312
|
+
let out = state.memo.get(key);
|
|
3313
|
+
if (!out) {
|
|
3314
|
+
out = translateTemplates(translate, translateCtx);
|
|
3315
|
+
state.memo.set(key, out);
|
|
3316
|
+
}
|
|
3317
|
+
return out;
|
|
3318
|
+
};
|
|
3319
|
+
return {
|
|
3320
|
+
locale,
|
|
3321
|
+
partition,
|
|
3322
|
+
key,
|
|
3323
|
+
t,
|
|
3324
|
+
messages
|
|
3325
|
+
};
|
|
3326
|
+
}
|
|
3327
|
+
function translateContext(event, locale, scope) {
|
|
3328
|
+
return typeof scope === "string" ? {
|
|
3329
|
+
locale,
|
|
3330
|
+
scope,
|
|
3331
|
+
event
|
|
3332
|
+
} : {
|
|
3333
|
+
locale,
|
|
3334
|
+
event
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
/**
|
|
3338
|
+
* The event's scope (`opts.scope`): `undefined` without one, `null` when the scope function
|
|
3339
|
+
* threw (logged once) — translated unscoped then, and not memoized.
|
|
3340
|
+
*/ function readScope(event) {
|
|
3341
|
+
const scoping = state.scoping;
|
|
3342
|
+
if (!scoping || scoping.translate !== state.translate) return;
|
|
3343
|
+
try {
|
|
3344
|
+
const { get } = scoping;
|
|
3345
|
+
const scope = (0, _wooksjs_event_core.tryGetCurrent)() === event ? get() : (0, _wooksjs_event_core.run)(event, get);
|
|
3346
|
+
return typeof scope === "string" ? scope : typeof scope === "number" && Number.isFinite(scope) ? String(scope) : void 0;
|
|
3347
|
+
} catch (error) {
|
|
3348
|
+
logOnce("scopeFailureLogged", event, "The translation scope threw — translating unscoped, without memoizing (logged once)", error);
|
|
3349
|
+
return null;
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3352
|
+
/** Whether `scope` gets memo keys: one of the first {@link MAX_SCOPES} of the generation. */ function admitted(scope) {
|
|
3353
|
+
if (state.admittedScopes?.generation !== state.generation) state.admittedScopes = {
|
|
3354
|
+
generation: state.generation,
|
|
3355
|
+
names: /* @__PURE__ */ new Set()
|
|
3356
|
+
};
|
|
3357
|
+
const { names } = state.admittedScopes;
|
|
3358
|
+
if (names.size < MAX_SCOPES) names.add(scope);
|
|
3359
|
+
return names.has(scope);
|
|
3360
|
+
}
|
|
3361
|
+
/** Logs a failure of the translator or the scope function once per `setTranslator`. */ function logOnce(flag, event, message, error) {
|
|
3362
|
+
if (!state[flag]) {
|
|
3363
|
+
state[flag] = true;
|
|
3364
|
+
(0, _wooksjs_event_core.useLogger)("i18n", event).error(message, error);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
/**
|
|
3368
|
+
* Returns a translated copy of an error body (`{ message, code?, params?, errors? }`) in the
|
|
3369
|
+
* event's locale — the body itself when there is no translation ({@link useTranslation}).
|
|
3370
|
+
*
|
|
3371
|
+
* - each entry of `errors` (and of its entries' `details`): by its `code` (unless `custom`),
|
|
3372
|
+
* else by its message text, its English `message` being the fallback;
|
|
3373
|
+
* - the top `message`: by `code` when the body has one; else, when it is the first entry's
|
|
3374
|
+
* message (`path: message`), the first entry's translation; else by its text.
|
|
3375
|
+
*
|
|
3376
|
+
* `MoostHttp` applies it to every error body when it creates the HTTP app.
|
|
3377
|
+
*/ function translateErrorBody(body, ctx) {
|
|
3378
|
+
const event = ctx ?? (0, _wooksjs_event_core.current)();
|
|
3379
|
+
if (!state.translate || (0, _wooksjs_event_core.useLocale)(event).get() === void 0) return body;
|
|
3380
|
+
const t = useTranslate(event);
|
|
3381
|
+
const source = Array.isArray(body.errors) ? body.errors : void 0;
|
|
3382
|
+
const errors = source && translateEntries(source, t);
|
|
3383
|
+
let message;
|
|
3384
|
+
if (typeof body.code === "string" && body.code) message = t(body.code, body.params, body.message);
|
|
3385
|
+
else if (source && errors && isEntry(source[0]) && body.message === topMessage(source[0])) message = topMessage(errors[0]);
|
|
3386
|
+
else message = t(body.message, body.params, body.message);
|
|
3387
|
+
return errors ? {
|
|
3388
|
+
...body,
|
|
3389
|
+
message,
|
|
3390
|
+
errors
|
|
3391
|
+
} : {
|
|
3392
|
+
...body,
|
|
3393
|
+
message
|
|
3394
|
+
};
|
|
3395
|
+
}
|
|
3396
|
+
function mapTranslator(map) {
|
|
3397
|
+
return (id, params, { locale }) => {
|
|
3398
|
+
const texts = locale !== void 0 && Object.hasOwn(map, locale) ? map[locale] : void 0;
|
|
3399
|
+
return texts && Object.hasOwn(texts, id) ? formatMessage(texts[id], params) : void 0;
|
|
3400
|
+
};
|
|
3401
|
+
}
|
|
3402
|
+
/** The translator's text, `undefined` if it has none or throws (logged once per `setTranslator`). */ function callTranslator(translate, id, params, ctx) {
|
|
3403
|
+
try {
|
|
3404
|
+
const out = translate(id, params, ctx);
|
|
3405
|
+
return typeof out === "string" ? out : void 0;
|
|
3406
|
+
} catch (error) {
|
|
3407
|
+
logOnce("failureLogged", ctx.event, `The translator threw for "${id}" — using the untranslated text (logged once)`, error);
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3411
|
+
/** The registered templates translated with placeholder params, if they differ. */ function translateTemplates(translate, ctx) {
|
|
3412
|
+
const out = {};
|
|
3413
|
+
for (const [code, template] of state.templates) {
|
|
3414
|
+
let params;
|
|
3415
|
+
for (const [placeholder, name] of template.matchAll(PLACEHOLDER)) (params ?? (params = {}))[name] = placeholder;
|
|
3416
|
+
const translated = callTranslator(translate, code, params, ctx);
|
|
3417
|
+
if (translated !== void 0 && translated !== template) out[code] = translated;
|
|
3418
|
+
}
|
|
3419
|
+
return Object.freeze(out);
|
|
3420
|
+
}
|
|
3421
|
+
function isEntry(value) {
|
|
3422
|
+
return typeof value === "object" && value !== null && typeof value.message === "string";
|
|
3423
|
+
}
|
|
3424
|
+
function translateEntries(entries, t) {
|
|
3425
|
+
return entries.map((entry) => {
|
|
3426
|
+
if (!isEntry(entry)) return entry;
|
|
3427
|
+
const id = typeof entry.code === "string" && entry.code && entry.custom !== true ? entry.code : entry.message;
|
|
3428
|
+
const out = {
|
|
3429
|
+
...entry,
|
|
3430
|
+
message: t(id, entry.params, entry.message)
|
|
3431
|
+
};
|
|
3432
|
+
if (Array.isArray(entry.details)) out.details = translateEntries(entry.details, t);
|
|
3433
|
+
return out;
|
|
3434
|
+
});
|
|
3435
|
+
}
|
|
3436
|
+
function topMessage(entry) {
|
|
3437
|
+
return typeof entry.path === "string" && entry.path ? `${entry.path}: ${entry.message}` : entry.message;
|
|
3438
|
+
}
|
|
3229
3439
|
|
|
3230
3440
|
//#endregion
|
|
3231
3441
|
exports.After = After;
|
|
@@ -3337,6 +3547,7 @@ Object.defineProperty(exports, 'eventTypeKey', {
|
|
|
3337
3547
|
}
|
|
3338
3548
|
});
|
|
3339
3549
|
exports.forkEventContext = forkEventContext;
|
|
3550
|
+
exports.formatMessage = formatMessage;
|
|
3340
3551
|
Object.defineProperty(exports, 'getClassKey', {
|
|
3341
3552
|
enumerable: true,
|
|
3342
3553
|
get: function () {
|
|
@@ -3370,6 +3581,7 @@ exports.getMoostInfact = getMoostInfact;
|
|
|
3370
3581
|
exports.getMoostMate = getMoostMate;
|
|
3371
3582
|
exports.getNewMoostInfact = getNewMoostInfact;
|
|
3372
3583
|
exports.getTranslator = getTranslator;
|
|
3584
|
+
exports.getTranslatorLocales = getTranslatorLocales;
|
|
3373
3585
|
exports.globalKey = globalKey;
|
|
3374
3586
|
Object.defineProperty(exports, 'isConstructor', {
|
|
3375
3587
|
enumerable: true,
|
|
@@ -3387,6 +3599,7 @@ Object.defineProperty(exports, 'key', {
|
|
|
3387
3599
|
exports.loggerConsoleTransport = loggerConsoleTransport;
|
|
3388
3600
|
exports.mergeSorted = mergeSorted;
|
|
3389
3601
|
exports.registerEventScope = registerEventScope;
|
|
3602
|
+
exports.registerMessages = registerMessages;
|
|
3390
3603
|
Object.defineProperty(exports, 'replaceContextInjector', {
|
|
3391
3604
|
enumerable: true,
|
|
3392
3605
|
get: function () {
|
|
@@ -3414,6 +3627,7 @@ exports.setOvertake = setOvertake;
|
|
|
3414
3627
|
exports.setTranslator = setTranslator;
|
|
3415
3628
|
exports.stampModuleIdentity = stampModuleIdentity;
|
|
3416
3629
|
exports.stampOnce = stampOnce;
|
|
3630
|
+
exports.translateErrorBody = translateErrorBody;
|
|
3417
3631
|
exports.useControllerContext = useControllerContext;
|
|
3418
3632
|
exports.useHandlerPaths = useHandlerPaths;
|
|
3419
3633
|
exports.useInterceptOriginal = useInterceptOriginal;
|
|
@@ -3433,4 +3647,5 @@ Object.defineProperty(exports, 'useLogger', {
|
|
|
3433
3647
|
exports.useOvertake = useOvertake;
|
|
3434
3648
|
exports.useScopeId = useScopeId;
|
|
3435
3649
|
exports.useTranslate = useTranslate;
|
|
3650
|
+
exports.useTranslation = useTranslation;
|
|
3436
3651
|
exports.withControllerContext = withControllerContext;
|
package/dist/index.d.ts
CHANGED
|
@@ -1771,54 +1771,162 @@ declare function defineInterceptor(def: TInterceptorDef, priority?: TInterceptor
|
|
|
1771
1771
|
*/
|
|
1772
1772
|
declare function definePipeFn<T extends TObject = TEmpty>(fn: TPipeFn<T>, priority?: TPipePriority): TPipeFn<T>;
|
|
1773
1773
|
|
|
1774
|
-
/** What a translator gets besides the text: the event's locale and the event itself. */
|
|
1774
|
+
/** What a translator gets besides the text: the event's locale, its scope and the event itself. */
|
|
1775
1775
|
interface TTranslateContext {
|
|
1776
1776
|
/** `useLocale().get()` of the event the text is translated for. */
|
|
1777
1777
|
locale: string | undefined;
|
|
1778
|
+
/**
|
|
1779
|
+
* The event's translation scope ({@link TTranslatorOptions.scope}) — e.g. a tenant whose
|
|
1780
|
+
* wording differs. `undefined`: the shared wording.
|
|
1781
|
+
*/
|
|
1782
|
+
scope?: string;
|
|
1778
1783
|
/** The event context — read anything else your policy needs from it. */
|
|
1779
1784
|
event: EventContext;
|
|
1780
1785
|
}
|
|
1781
1786
|
/**
|
|
1782
|
-
* The app's translator. `text` is a
|
|
1783
|
-
*
|
|
1784
|
-
*
|
|
1787
|
+
* The app's translator. `text` is a message id: an error/message code (`WF_EXPIRED`,
|
|
1788
|
+
* `MIN_LENGTH`, …) or a source text (gettext-style); `params` are the values it refers to.
|
|
1789
|
+
* Return the final text (with `params` filled in), or `undefined` when there is no translation.
|
|
1785
1790
|
*
|
|
1786
|
-
* Moost never translates on its own: plug in any i18n library (
|
|
1787
|
-
*
|
|
1791
|
+
* Moost never translates on its own: plug in any i18n library (i18next, FormatJS, …) or pass a
|
|
1792
|
+
* plain {@link TTranslations} map to {@link setTranslator}.
|
|
1788
1793
|
*/
|
|
1789
1794
|
type TTranslate = (text: string, params: Record<string, unknown> | undefined, ctx: TTranslateContext) => string | undefined;
|
|
1790
1795
|
/**
|
|
1791
|
-
*
|
|
1792
|
-
*
|
|
1793
|
-
|
|
1794
|
-
|
|
1796
|
+
* Translations as a plain map: `{ [locale]: { [id]: template } }`. Templates use `{name}`
|
|
1797
|
+
* placeholders, filled with {@link formatMessage}.
|
|
1798
|
+
*/
|
|
1799
|
+
type TTranslations = Record<string, Record<string, string>>;
|
|
1800
|
+
/** The options of {@link setTranslator}. */
|
|
1801
|
+
interface TTranslatorOptions {
|
|
1802
|
+
/** The locales the app serves, the default first. For a map: its keys by default. */
|
|
1803
|
+
locales?: readonly string[];
|
|
1804
|
+
/**
|
|
1805
|
+
* The event's translation scope, for wording that differs by more than the locale (per
|
|
1806
|
+
* tenant, brand, user group, …). Called in the event's context each time a translation is
|
|
1807
|
+
* taken — keep it cheap. Its result is passed to the translator ({@link TTranslateContext.scope})
|
|
1808
|
+
* and partitions everything memoized per translation ({@link TTranslation.key}); a finite
|
|
1809
|
+
* number counts as its string, and `undefined` (or any other value) is the shared, unscoped
|
|
1810
|
+
* wording. The translator must return the same text for the same (id, params, locale, scope).
|
|
1811
|
+
*
|
|
1812
|
+
* Derive it from a validated value (the authenticated principal's tenant), never straight from
|
|
1813
|
+
* request input: only the first 32 scopes seen per generation get a {@link TTranslation.key}.
|
|
1814
|
+
*/
|
|
1815
|
+
scope?: () => string | number | undefined;
|
|
1816
|
+
}
|
|
1817
|
+
/** `t(id, params?, fallback?)` — see {@link useTranslate}. */
|
|
1818
|
+
type TTranslateFn = (id: string, params?: Record<string, unknown>, fallback?: string) => string;
|
|
1819
|
+
/** The translation of one event's locale — see {@link useTranslation}. */
|
|
1820
|
+
interface TTranslation {
|
|
1821
|
+
/** The event's locale. */
|
|
1822
|
+
locale: string;
|
|
1823
|
+
/**
|
|
1824
|
+
* What to index memoized results by: the locale, plus the event's scope when it has one
|
|
1825
|
+
* ({@link TTranslatorOptions.scope}). Keep one entry per partition, replaced when
|
|
1826
|
+
* {@link TTranslation.key} changes.
|
|
1827
|
+
*/
|
|
1828
|
+
partition: string;
|
|
1829
|
+
/**
|
|
1830
|
+
* A memo key for anything derived from this translation: the locale, the scope (if any) and
|
|
1831
|
+
* the generation — changes with every {@link setTranslator} call and every
|
|
1832
|
+
* {@link registerMessages} call that adds or changes a template. `undefined` for a locale
|
|
1833
|
+
* outside the translator's `locales`, or a scope past the cap ({@link TTranslatorOptions.scope})
|
|
1834
|
+
* — don't memoize then.
|
|
1835
|
+
*/
|
|
1836
|
+
key: string | undefined;
|
|
1837
|
+
/** {@link useTranslate}'s `t`, bound to this translation. */
|
|
1838
|
+
t: TTranslateFn;
|
|
1839
|
+
/**
|
|
1840
|
+
* The registered message templates ({@link registerMessages}) translated to `locale`, with
|
|
1841
|
+
* their `{name}` placeholders kept — only the ones that differ from the English template.
|
|
1842
|
+
* Memoized per {@link TTranslation.key}.
|
|
1843
|
+
*/
|
|
1844
|
+
messages: () => Readonly<Record<string, string>>;
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Registers the app's translator (process-wide), or removes it with `undefined`: a
|
|
1848
|
+
* {@link TTranslate} function or a {@link TTranslations} map. Load your languages before the
|
|
1849
|
+
* server listens, or call `setTranslator` again once they are loaded.
|
|
1850
|
+
*
|
|
1851
|
+
* `opts.locales` lists the locales the app serves, the first being the default (`MoostHttp`
|
|
1852
|
+
* negotiates `Accept-Language` against them). For a map they default to its keys — put the
|
|
1853
|
+
* source language first: `{ en: {}, de: {…} }`. Give a function translator `locales`, or
|
|
1854
|
+
* requests get a locale only from your own `resolveLocale` / `useLocale().set()`.
|
|
1795
1855
|
*
|
|
1796
1856
|
* @example
|
|
1797
1857
|
* ```ts
|
|
1798
|
-
*
|
|
1799
|
-
*
|
|
1858
|
+
* setTranslator({ en: {}, de: { NAME_TAKEN: 'Name {name} ist vergeben' } })
|
|
1859
|
+
*
|
|
1860
|
+
* setTranslator(
|
|
1861
|
+
* (id, params, { locale }) =>
|
|
1862
|
+
* locale && i18next.exists(id, { lng: locale }) ? i18next.t(id, { ...params, lng: locale }) : undefined,
|
|
1863
|
+
* { locales: ['en', 'de'] },
|
|
1864
|
+
* )
|
|
1800
1865
|
* ```
|
|
1801
1866
|
*/
|
|
1802
|
-
declare function setTranslator(translate: TTranslate | undefined): void;
|
|
1803
|
-
/** The registered translator
|
|
1867
|
+
declare function setTranslator(translate: TTranslate | TTranslations | undefined, opts?: TTranslatorOptions): void;
|
|
1868
|
+
/** The registered translator (a map wrapped as a function), or `undefined`. */
|
|
1804
1869
|
declare function getTranslator(): TTranslate | undefined;
|
|
1805
|
-
/**
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
*
|
|
1870
|
+
/** The locales of the registered translator, the default first (empty without a translator). */
|
|
1871
|
+
declare function getTranslatorLocales(): readonly string[];
|
|
1872
|
+
/**
|
|
1873
|
+
* Registers message templates by code (`{ MIN_LENGTH: 'At least {min} characters' }`),
|
|
1874
|
+
* process-wide: the English text `useTranslate()` falls back to for the code, and the templates
|
|
1875
|
+
* {@link TTranslation.messages} serves translated. Registering the same catalog object again
|
|
1876
|
+
* does nothing; a later catalog wins for a code both define. A call that adds or changes a
|
|
1877
|
+
* template starts a new generation, like {@link setTranslator} ({@link TTranslation.key}
|
|
1878
|
+
* changes).
|
|
1879
|
+
*/
|
|
1880
|
+
declare function registerMessages(catalog: Readonly<Record<string, string>>): void;
|
|
1881
|
+
/**
|
|
1882
|
+
* Fills `{name}` placeholders with `params` in one pass (arrays: items joined with `, `).
|
|
1883
|
+
* A placeholder without a param stays as it is; filled-in values are never filled again.
|
|
1884
|
+
*/
|
|
1885
|
+
declare function formatMessage(template: string, params?: Record<string, unknown>): string;
|
|
1886
|
+
/**
|
|
1887
|
+
* Returns `t(id, params?, fallback?)` for the current event (or `ctx`), in the event's locale
|
|
1888
|
+
* (`useLocale().get()`). `id` is a code or a source text. `t` returns:
|
|
1889
|
+
* - the translator's result, as is (it is final text: params are not filled again);
|
|
1890
|
+
* - else `fallback` as is — it is final too, never filled: pass already-filled English text
|
|
1891
|
+
* (e.g. an error's message), or pass no fallback and {@link registerMessages} a template;
|
|
1892
|
+
* - else the code's registered template ({@link registerMessages}), or `id` itself, with
|
|
1893
|
+
* `params` filled in ({@link formatMessage}).
|
|
1809
1894
|
*
|
|
1810
1895
|
* @example
|
|
1811
1896
|
* ```ts
|
|
1812
1897
|
* const t = useTranslate()
|
|
1813
|
-
* throw new HttpError(409, { message: t('Name taken
|
|
1898
|
+
* throw new HttpError(409, { message: t('NAME_TAKEN', { name }, `Name ${name} is taken`), code: 'NAME_TAKEN', params: { name } })
|
|
1814
1899
|
* ```
|
|
1815
1900
|
*/
|
|
1816
|
-
declare function useTranslate(ctx?: EventContext):
|
|
1901
|
+
declare function useTranslate(ctx?: EventContext): TTranslateFn;
|
|
1902
|
+
/**
|
|
1903
|
+
* The translation of the current event (or `ctx`): its locale, a memo key, `t` and the
|
|
1904
|
+
* translated message templates. `undefined` when no translator is registered or the event has
|
|
1905
|
+
* no locale — serve the source texts then.
|
|
1906
|
+
*/
|
|
1907
|
+
declare function useTranslation(ctx?: EventContext): TTranslation | undefined;
|
|
1908
|
+
/**
|
|
1909
|
+
* Returns a translated copy of an error body (`{ message, code?, params?, errors? }`) in the
|
|
1910
|
+
* event's locale — the body itself when there is no translation ({@link useTranslation}).
|
|
1911
|
+
*
|
|
1912
|
+
* - each entry of `errors` (and of its entries' `details`): by its `code` (unless `custom`),
|
|
1913
|
+
* else by its message text, its English `message` being the fallback;
|
|
1914
|
+
* - the top `message`: by `code` when the body has one; else, when it is the first entry's
|
|
1915
|
+
* message (`path: message`), the first entry's translation; else by its text.
|
|
1916
|
+
*
|
|
1917
|
+
* `MoostHttp` applies it to every error body when it creates the HTTP app.
|
|
1918
|
+
*/
|
|
1919
|
+
declare function translateErrorBody<T extends {
|
|
1920
|
+
message: string;
|
|
1921
|
+
code?: string;
|
|
1922
|
+
params?: Record<string, unknown>;
|
|
1923
|
+
errors?: unknown;
|
|
1924
|
+
}>(body: T, ctx?: EventContext): T;
|
|
1817
1925
|
|
|
1818
1926
|
/** Creates a new `ProstoLogger` instance with sensible defaults (level 4, colored console). */
|
|
1819
1927
|
declare function createLogger(opts?: Partial<TProstoLoggerOptions>): ProstoLogger;
|
|
1820
1928
|
/** Default colored console transport used by Moost loggers. */
|
|
1821
1929
|
declare const loggerConsoleTransport: _prostojs_logger.TProstoLoggerTransportFn<any>;
|
|
1822
1930
|
|
|
1823
|
-
export { After, ApplyDecorators, Before, Circular, Const, ConstFactory, Controller, Description, HandlerPaths, Id, ImportController, Inherit, Inject, InjectEventLogger, InjectFromScope, InjectMoost, InjectMoostLogger, InjectScopeVars, Injectable, Intercept, Interceptor, InterceptorHandler, Label, Locale, LoggerTopic, MOOST_ADAPTER_BRAND, MOOST_MODULE_IDENTITY_KEY, Moost, MoostDispose, MoostInit, OnError, Optional, Overtake, Param, Params, Pipe, Provide, Replace, Required, Resolve, Response, TInterceptorPriority, TPipePriority, Value, createLogger, defineAfterInterceptor, defineBeforeInterceptor, defineErrorInterceptor, defineInfactScope, defineInterceptor, defineMoostEventHandler, definePipeFn, detachedKind, disposeInstances, forkEventContext, getHandlerPaths, getInfactScopeVars, getInfactSingletonInstances, getInstanceOwnMethods, getInstanceOwnProps, getMoostInfact, getMoostMate, getNewMoostInfact, getTranslator, globalKey, isThenable, loggerConsoleTransport, mergeSorted, registerEventScope, resolvePipe, setControllerContext, setInfactLoggingOptions, setInterceptOriginal, setInterceptResult, setOvertake, setTranslator, stampModuleIdentity, stampOnce, useControllerContext, useHandlerPaths, useInterceptOriginal, useInterceptResult, useOvertake, useScopeId, useTranslate, withControllerContext };
|
|
1824
|
-
export type { TAny, TAnyFn, TClassConstructor, TContextInjectorHook, TControllerOverview, TControllerRegistration, TControllersGroup, TDisposeError, TDisposeOptions, TDisposeResult, TEmpty, TForkEventContextOptions, TFunction, TGetHandlerPathsOptions, TInjectableScope, TInterceptorAfterFn, TInterceptorBeforeFn, TInterceptorData, TInterceptorDef, TInterceptorDefFactory, TInterceptorEntry, TInterceptorErrorFn, TIsolatedSlot, TLogger, TMoostAdapter, TMoostAdapterOptions, TMoostEventHandlerHookOptions, TMoostEventHandlerOptions, TMoostHandler, TMoostMetadata, TMoostModuleIdentity, TMoostOptions, TMoostParamsMetadata, TObject, TOvertakeFn, TPipeData, TPipeFn, TPipeMetas, TPrimitives, TRegistryMergeOptions, TRunDetachedOptions, TTranslate, TTranslateContext, TWithControllerContextOptions };
|
|
1931
|
+
export { After, ApplyDecorators, Before, Circular, Const, ConstFactory, Controller, Description, HandlerPaths, Id, ImportController, Inherit, Inject, InjectEventLogger, InjectFromScope, InjectMoost, InjectMoostLogger, InjectScopeVars, Injectable, Intercept, Interceptor, InterceptorHandler, Label, Locale, LoggerTopic, MOOST_ADAPTER_BRAND, MOOST_MODULE_IDENTITY_KEY, Moost, MoostDispose, MoostInit, OnError, Optional, Overtake, Param, Params, Pipe, Provide, Replace, Required, Resolve, Response, TInterceptorPriority, TPipePriority, Value, createLogger, defineAfterInterceptor, defineBeforeInterceptor, defineErrorInterceptor, defineInfactScope, defineInterceptor, defineMoostEventHandler, definePipeFn, detachedKind, disposeInstances, forkEventContext, formatMessage, getHandlerPaths, getInfactScopeVars, getInfactSingletonInstances, getInstanceOwnMethods, getInstanceOwnProps, getMoostInfact, getMoostMate, getNewMoostInfact, getTranslator, getTranslatorLocales, globalKey, isThenable, loggerConsoleTransport, mergeSorted, registerEventScope, registerMessages, resolvePipe, setControllerContext, setInfactLoggingOptions, setInterceptOriginal, setInterceptResult, setOvertake, setTranslator, stampModuleIdentity, stampOnce, translateErrorBody, useControllerContext, useHandlerPaths, useInterceptOriginal, useInterceptResult, useOvertake, useScopeId, useTranslate, useTranslation, withControllerContext };
|
|
1932
|
+
export type { TAny, TAnyFn, TClassConstructor, TContextInjectorHook, TControllerOverview, TControllerRegistration, TControllersGroup, TDisposeError, TDisposeOptions, TDisposeResult, TEmpty, TForkEventContextOptions, TFunction, TGetHandlerPathsOptions, TInjectableScope, TInterceptorAfterFn, TInterceptorBeforeFn, TInterceptorData, TInterceptorDef, TInterceptorDefFactory, TInterceptorEntry, TInterceptorErrorFn, TIsolatedSlot, TLogger, TMoostAdapter, TMoostAdapterOptions, TMoostEventHandlerHookOptions, TMoostEventHandlerOptions, TMoostHandler, TMoostMetadata, TMoostModuleIdentity, TMoostOptions, TMoostParamsMetadata, TObject, TOvertakeFn, TPipeData, TPipeFn, TPipeMetas, TPrimitives, TRegistryMergeOptions, TRunDetachedOptions, TTranslate, TTranslateContext, TTranslation, TTranslations, TTranslatorOptions, TWithControllerContextOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -47,7 +47,7 @@ let stamped = false;
|
|
|
47
47
|
if (stamped) return;
|
|
48
48
|
stamped = true;
|
|
49
49
|
stampModuleIdentity({
|
|
50
|
-
version: "0.7.
|
|
50
|
+
version: "0.7.4",
|
|
51
51
|
path: typeof __filename === "string" ? __filename : import.meta.url
|
|
52
52
|
});
|
|
53
53
|
}
|
|
@@ -657,7 +657,7 @@ function formatNewInstanceArg(a) {
|
|
|
657
657
|
//#endregion
|
|
658
658
|
//#region packages/moost/src/composables/global-key.ts
|
|
659
659
|
var _g;
|
|
660
|
-
const registry
|
|
660
|
+
const registry = (_g = globalThis).__moost_keys ?? (_g.__moost_keys = /* @__PURE__ */ new Map());
|
|
661
661
|
/**
|
|
662
662
|
* Creates a typed event-context key that stays a single instance across duplicate
|
|
663
663
|
* moost loads (dual ESM/CJS, or duplicate installs). Use this for every
|
|
@@ -665,8 +665,8 @@ const registry$1 = (_g = globalThis).__moost_keys ?? (_g.__moost_keys = /* @__PU
|
|
|
665
665
|
*
|
|
666
666
|
* @param name - Debug label AND the registry lookup key; must be unique per slot.
|
|
667
667
|
*/ function globalKey(name) {
|
|
668
|
-
if (!registry
|
|
669
|
-
return registry
|
|
668
|
+
if (!registry.has(name)) registry.set(name, key$1(name));
|
|
669
|
+
return registry.get(name);
|
|
670
670
|
}
|
|
671
671
|
|
|
672
672
|
//#endregion
|
|
@@ -3183,48 +3183,258 @@ function _define_property(obj, key, value) {
|
|
|
3183
3183
|
|
|
3184
3184
|
//#endregion
|
|
3185
3185
|
//#region packages/moost/src/i18n.ts
|
|
3186
|
-
|
|
3187
|
-
const
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3186
|
+
var _globalThis, _STATE, _state, _state1, _state2, _state3, _state4;
|
|
3187
|
+
const STATE = Symbol.for("moost.i18n");
|
|
3188
|
+
const NO_LOCALES = Object.freeze([]);
|
|
3189
|
+
/** Distinct scopes given memo keys per generation: keeps every memo keyed by `key` bounded. */ const MAX_SCOPES = 32;
|
|
3190
|
+
const state = (_globalThis = globalThis)[_STATE = STATE] ?? (_globalThis[_STATE] = {});
|
|
3191
|
+
(_state = state).locales ?? (_state.locales = NO_LOCALES);
|
|
3192
|
+
(_state1 = state).generation ?? (_state1.generation = 0);
|
|
3193
|
+
(_state2 = state).catalogs ?? (_state2.catalogs = /* @__PURE__ */ new WeakSet());
|
|
3194
|
+
(_state3 = state).templates ?? (_state3.templates = /* @__PURE__ */ new Map());
|
|
3195
|
+
(_state4 = state).memo ?? (_state4.memo = /* @__PURE__ */ new Map());
|
|
3196
|
+
/**
|
|
3197
|
+
* Registers the app's translator (process-wide), or removes it with `undefined`: a
|
|
3198
|
+
* {@link TTranslate} function or a {@link TTranslations} map. Load your languages before the
|
|
3199
|
+
* server listens, or call `setTranslator` again once they are loaded.
|
|
3200
|
+
*
|
|
3201
|
+
* `opts.locales` lists the locales the app serves, the first being the default (`MoostHttp`
|
|
3202
|
+
* negotiates `Accept-Language` against them). For a map they default to its keys — put the
|
|
3203
|
+
* source language first: `{ en: {}, de: {…} }`. Give a function translator `locales`, or
|
|
3204
|
+
* requests get a locale only from your own `resolveLocale` / `useLocale().set()`.
|
|
3193
3205
|
*
|
|
3194
3206
|
* @example
|
|
3195
3207
|
* ```ts
|
|
3196
|
-
*
|
|
3197
|
-
*
|
|
3208
|
+
* setTranslator({ en: {}, de: { NAME_TAKEN: 'Name {name} ist vergeben' } })
|
|
3209
|
+
*
|
|
3210
|
+
* setTranslator(
|
|
3211
|
+
* (id, params, { locale }) =>
|
|
3212
|
+
* locale && i18next.exists(id, { lng: locale }) ? i18next.t(id, { ...params, lng: locale }) : undefined,
|
|
3213
|
+
* { locales: ['en', 'de'] },
|
|
3214
|
+
* )
|
|
3198
3215
|
* ```
|
|
3199
|
-
*/ function setTranslator(translate) {
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3216
|
+
*/ function setTranslator(translate, opts) {
|
|
3217
|
+
const isMap = typeof translate === "object";
|
|
3218
|
+
state.translate = isMap ? mapTranslator(translate) : translate;
|
|
3219
|
+
state.scoping = opts?.scope && {
|
|
3220
|
+
translate: state.translate,
|
|
3221
|
+
get: opts.scope
|
|
3222
|
+
};
|
|
3223
|
+
const locales = opts?.locales ?? (isMap ? Object.keys(translate) : void 0);
|
|
3224
|
+
state.locales = translate && locales?.length ? Object.freeze([...locales]) : NO_LOCALES;
|
|
3225
|
+
state.failureLogged = false;
|
|
3226
|
+
state.scopeFailureLogged = false;
|
|
3227
|
+
newGeneration();
|
|
3228
|
+
}
|
|
3229
|
+
/** The registered translator (a map wrapped as a function), or `undefined`. */ function getTranslator() {
|
|
3230
|
+
return state.translate;
|
|
3231
|
+
}
|
|
3232
|
+
/** The locales of the registered translator, the default first (empty without a translator). */ function getTranslatorLocales() {
|
|
3233
|
+
return state.locales;
|
|
3234
|
+
}
|
|
3235
|
+
/**
|
|
3236
|
+
* Registers message templates by code (`{ MIN_LENGTH: 'At least {min} characters' }`),
|
|
3237
|
+
* process-wide: the English text `useTranslate()` falls back to for the code, and the templates
|
|
3238
|
+
* {@link TTranslation.messages} serves translated. Registering the same catalog object again
|
|
3239
|
+
* does nothing; a later catalog wins for a code both define. A call that adds or changes a
|
|
3240
|
+
* template starts a new generation, like {@link setTranslator} ({@link TTranslation.key}
|
|
3241
|
+
* changes).
|
|
3242
|
+
*/ function registerMessages(catalog) {
|
|
3243
|
+
if (state.catalogs.has(catalog)) return;
|
|
3244
|
+
state.catalogs.add(catalog);
|
|
3245
|
+
let changed = false;
|
|
3246
|
+
for (const code of Object.keys(catalog)) if (state.templates.get(code) !== catalog[code]) {
|
|
3247
|
+
state.templates.set(code, catalog[code]);
|
|
3248
|
+
changed = true;
|
|
3249
|
+
}
|
|
3250
|
+
if (changed) newGeneration();
|
|
3251
|
+
}
|
|
3252
|
+
/** Drops everything derived from the translator and the templates. */ function newGeneration() {
|
|
3253
|
+
state.generation++;
|
|
3254
|
+
state.memo.clear();
|
|
3255
|
+
}
|
|
3256
|
+
const PLACEHOLDER = /\{(\w+)\}/g;
|
|
3257
|
+
/**
|
|
3258
|
+
* Fills `{name}` placeholders with `params` in one pass (arrays: items joined with `, `).
|
|
3259
|
+
* A placeholder without a param stays as it is; filled-in values are never filled again.
|
|
3260
|
+
*/ function formatMessage(template, params) {
|
|
3261
|
+
if (!params || !template.includes("{")) return template;
|
|
3262
|
+
return template.replace(PLACEHOLDER, (match, name) => {
|
|
3263
|
+
const value = Object.hasOwn(params, name) ? params[name] : void 0;
|
|
3264
|
+
if (value === void 0) return match;
|
|
3265
|
+
return Array.isArray(value) ? value.map(String).join(", ") : String(value);
|
|
3266
|
+
});
|
|
3204
3267
|
}
|
|
3205
3268
|
/**
|
|
3206
|
-
* Returns `t(
|
|
3207
|
-
*
|
|
3208
|
-
*
|
|
3269
|
+
* Returns `t(id, params?, fallback?)` for the current event (or `ctx`), in the event's locale
|
|
3270
|
+
* (`useLocale().get()`). `id` is a code or a source text. `t` returns:
|
|
3271
|
+
* - the translator's result, as is (it is final text: params are not filled again);
|
|
3272
|
+
* - else `fallback` as is — it is final too, never filled: pass already-filled English text
|
|
3273
|
+
* (e.g. an error's message), or pass no fallback and {@link registerMessages} a template;
|
|
3274
|
+
* - else the code's registered template ({@link registerMessages}), or `id` itself, with
|
|
3275
|
+
* `params` filled in ({@link formatMessage}).
|
|
3209
3276
|
*
|
|
3210
3277
|
* @example
|
|
3211
3278
|
* ```ts
|
|
3212
3279
|
* const t = useTranslate()
|
|
3213
|
-
* throw new HttpError(409, { message: t('Name taken
|
|
3280
|
+
* throw new HttpError(409, { message: t('NAME_TAKEN', { name }, `Name ${name} is taken`), code: 'NAME_TAKEN', params: { name } })
|
|
3214
3281
|
* ```
|
|
3215
3282
|
*/ function useTranslate(ctx) {
|
|
3216
3283
|
const event = ctx ?? current$1();
|
|
3217
3284
|
let translateCtx;
|
|
3218
|
-
return (
|
|
3219
|
-
const translate =
|
|
3220
|
-
|
|
3221
|
-
translateCtx ?? (translateCtx = {
|
|
3222
|
-
locale: useLocale$1(event).get(),
|
|
3223
|
-
event
|
|
3224
|
-
});
|
|
3225
|
-
return translate(text, params, translateCtx) ?? text;
|
|
3285
|
+
return (id, params, fallback) => {
|
|
3286
|
+
const translate = state.translate;
|
|
3287
|
+
return (translate ? callTranslator(translate, id, params, translateCtx ?? (translateCtx = translateContext(event, useLocale$1(event).get(), readScope(event)))) : void 0) ?? untranslated(id, params, fallback);
|
|
3226
3288
|
};
|
|
3227
3289
|
}
|
|
3290
|
+
/** `t`'s text without a translation: `fallback` as is, else the filled template (or id). */ function untranslated(id, params, fallback) {
|
|
3291
|
+
return fallback ?? formatMessage(state.templates.get(id) ?? id, params);
|
|
3292
|
+
}
|
|
3293
|
+
/**
|
|
3294
|
+
* The translation of the current event (or `ctx`): its locale, a memo key, `t` and the
|
|
3295
|
+
* translated message templates. `undefined` when no translator is registered or the event has
|
|
3296
|
+
* no locale — serve the source texts then.
|
|
3297
|
+
*/ function useTranslation(ctx) {
|
|
3298
|
+
const translate = state.translate;
|
|
3299
|
+
if (!translate) return;
|
|
3300
|
+
const event = ctx ?? current$1();
|
|
3301
|
+
const locale = useLocale$1(event).get();
|
|
3302
|
+
if (locale === void 0) return;
|
|
3303
|
+
const scope = readScope(event);
|
|
3304
|
+
const translateCtx = translateContext(event, locale, scope);
|
|
3305
|
+
const generation = state.generation;
|
|
3306
|
+
const partition = typeof scope === "string" ? `${locale}:${scope}` : locale;
|
|
3307
|
+
const key = state.locales.includes(locale) && (scope === void 0 || scope !== null && admitted(scope)) ? `${generation}:${partition}` : void 0;
|
|
3308
|
+
const t = (id, params, fallback) => callTranslator(translate, id, params, translateCtx) ?? untranslated(id, params, fallback);
|
|
3309
|
+
const messages = () => {
|
|
3310
|
+
if (key === void 0 || state.generation !== generation) return translateTemplates(translate, translateCtx);
|
|
3311
|
+
let out = state.memo.get(key);
|
|
3312
|
+
if (!out) {
|
|
3313
|
+
out = translateTemplates(translate, translateCtx);
|
|
3314
|
+
state.memo.set(key, out);
|
|
3315
|
+
}
|
|
3316
|
+
return out;
|
|
3317
|
+
};
|
|
3318
|
+
return {
|
|
3319
|
+
locale,
|
|
3320
|
+
partition,
|
|
3321
|
+
key,
|
|
3322
|
+
t,
|
|
3323
|
+
messages
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
function translateContext(event, locale, scope) {
|
|
3327
|
+
return typeof scope === "string" ? {
|
|
3328
|
+
locale,
|
|
3329
|
+
scope,
|
|
3330
|
+
event
|
|
3331
|
+
} : {
|
|
3332
|
+
locale,
|
|
3333
|
+
event
|
|
3334
|
+
};
|
|
3335
|
+
}
|
|
3336
|
+
/**
|
|
3337
|
+
* The event's scope (`opts.scope`): `undefined` without one, `null` when the scope function
|
|
3338
|
+
* threw (logged once) — translated unscoped then, and not memoized.
|
|
3339
|
+
*/ function readScope(event) {
|
|
3340
|
+
const scoping = state.scoping;
|
|
3341
|
+
if (!scoping || scoping.translate !== state.translate) return;
|
|
3342
|
+
try {
|
|
3343
|
+
const { get } = scoping;
|
|
3344
|
+
const scope = tryGetCurrent() === event ? get() : run$1(event, get);
|
|
3345
|
+
return typeof scope === "string" ? scope : typeof scope === "number" && Number.isFinite(scope) ? String(scope) : void 0;
|
|
3346
|
+
} catch (error) {
|
|
3347
|
+
logOnce("scopeFailureLogged", event, "The translation scope threw — translating unscoped, without memoizing (logged once)", error);
|
|
3348
|
+
return null;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
/** Whether `scope` gets memo keys: one of the first {@link MAX_SCOPES} of the generation. */ function admitted(scope) {
|
|
3352
|
+
if (state.admittedScopes?.generation !== state.generation) state.admittedScopes = {
|
|
3353
|
+
generation: state.generation,
|
|
3354
|
+
names: /* @__PURE__ */ new Set()
|
|
3355
|
+
};
|
|
3356
|
+
const { names } = state.admittedScopes;
|
|
3357
|
+
if (names.size < MAX_SCOPES) names.add(scope);
|
|
3358
|
+
return names.has(scope);
|
|
3359
|
+
}
|
|
3360
|
+
/** Logs a failure of the translator or the scope function once per `setTranslator`. */ function logOnce(flag, event, message, error) {
|
|
3361
|
+
if (!state[flag]) {
|
|
3362
|
+
state[flag] = true;
|
|
3363
|
+
useLogger$1("i18n", event).error(message, error);
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
/**
|
|
3367
|
+
* Returns a translated copy of an error body (`{ message, code?, params?, errors? }`) in the
|
|
3368
|
+
* event's locale — the body itself when there is no translation ({@link useTranslation}).
|
|
3369
|
+
*
|
|
3370
|
+
* - each entry of `errors` (and of its entries' `details`): by its `code` (unless `custom`),
|
|
3371
|
+
* else by its message text, its English `message` being the fallback;
|
|
3372
|
+
* - the top `message`: by `code` when the body has one; else, when it is the first entry's
|
|
3373
|
+
* message (`path: message`), the first entry's translation; else by its text.
|
|
3374
|
+
*
|
|
3375
|
+
* `MoostHttp` applies it to every error body when it creates the HTTP app.
|
|
3376
|
+
*/ function translateErrorBody(body, ctx) {
|
|
3377
|
+
const event = ctx ?? current$1();
|
|
3378
|
+
if (!state.translate || useLocale$1(event).get() === void 0) return body;
|
|
3379
|
+
const t = useTranslate(event);
|
|
3380
|
+
const source = Array.isArray(body.errors) ? body.errors : void 0;
|
|
3381
|
+
const errors = source && translateEntries(source, t);
|
|
3382
|
+
let message;
|
|
3383
|
+
if (typeof body.code === "string" && body.code) message = t(body.code, body.params, body.message);
|
|
3384
|
+
else if (source && errors && isEntry(source[0]) && body.message === topMessage(source[0])) message = topMessage(errors[0]);
|
|
3385
|
+
else message = t(body.message, body.params, body.message);
|
|
3386
|
+
return errors ? {
|
|
3387
|
+
...body,
|
|
3388
|
+
message,
|
|
3389
|
+
errors
|
|
3390
|
+
} : {
|
|
3391
|
+
...body,
|
|
3392
|
+
message
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
function mapTranslator(map) {
|
|
3396
|
+
return (id, params, { locale }) => {
|
|
3397
|
+
const texts = locale !== void 0 && Object.hasOwn(map, locale) ? map[locale] : void 0;
|
|
3398
|
+
return texts && Object.hasOwn(texts, id) ? formatMessage(texts[id], params) : void 0;
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
/** The translator's text, `undefined` if it has none or throws (logged once per `setTranslator`). */ function callTranslator(translate, id, params, ctx) {
|
|
3402
|
+
try {
|
|
3403
|
+
const out = translate(id, params, ctx);
|
|
3404
|
+
return typeof out === "string" ? out : void 0;
|
|
3405
|
+
} catch (error) {
|
|
3406
|
+
logOnce("failureLogged", ctx.event, `The translator threw for "${id}" — using the untranslated text (logged once)`, error);
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
/** The registered templates translated with placeholder params, if they differ. */ function translateTemplates(translate, ctx) {
|
|
3411
|
+
const out = {};
|
|
3412
|
+
for (const [code, template] of state.templates) {
|
|
3413
|
+
let params;
|
|
3414
|
+
for (const [placeholder, name] of template.matchAll(PLACEHOLDER)) (params ?? (params = {}))[name] = placeholder;
|
|
3415
|
+
const translated = callTranslator(translate, code, params, ctx);
|
|
3416
|
+
if (translated !== void 0 && translated !== template) out[code] = translated;
|
|
3417
|
+
}
|
|
3418
|
+
return Object.freeze(out);
|
|
3419
|
+
}
|
|
3420
|
+
function isEntry(value) {
|
|
3421
|
+
return typeof value === "object" && value !== null && typeof value.message === "string";
|
|
3422
|
+
}
|
|
3423
|
+
function translateEntries(entries, t) {
|
|
3424
|
+
return entries.map((entry) => {
|
|
3425
|
+
if (!isEntry(entry)) return entry;
|
|
3426
|
+
const id = typeof entry.code === "string" && entry.code && entry.custom !== true ? entry.code : entry.message;
|
|
3427
|
+
const out = {
|
|
3428
|
+
...entry,
|
|
3429
|
+
message: t(id, entry.params, entry.message)
|
|
3430
|
+
};
|
|
3431
|
+
if (Array.isArray(entry.details)) out.details = translateEntries(entry.details, t);
|
|
3432
|
+
return out;
|
|
3433
|
+
});
|
|
3434
|
+
}
|
|
3435
|
+
function topMessage(entry) {
|
|
3436
|
+
return typeof entry.path === "string" && entry.path ? `${entry.path}: ${entry.message}` : entry.message;
|
|
3437
|
+
}
|
|
3228
3438
|
|
|
3229
3439
|
//#endregion
|
|
3230
|
-
export { After, ApplyDecorators, Before, Circular, Const, ConstFactory, ContextInjector, Controller, Description, HandlerPaths, Id, ImportController, Inherit, Inject, InjectEventLogger, InjectFromScope, InjectMoost, InjectMoostLogger, InjectScopeVars, Injectable, Intercept, Interceptor, InterceptorHandler, Label, Locale, LoggerTopic, MOOST_ADAPTER_BRAND, MOOST_MODULE_IDENTITY_KEY, Moost, MoostDispose, MoostInit, OnError, Optional, Overtake, Param, Params, Pipe, ProstoLogger, Provide, Replace, Required, Resolve, Response, TInterceptorPriority, TPipePriority, Value, cached, clearGlobalWooks, createEventContext, createLogger, createProvideRegistry, createReplaceRegistry, current, defineAfterInterceptor, defineBeforeInterceptor, defineErrorInterceptor, defineInfactScope, defineInterceptor, defineMoostEventHandler, definePipeFn, detachedKind, disposeInstances, eventTypeKey, forkEventContext, getClassKey, getConstructor, getContextInjector, getGlobalWooks, getHandlerPaths, getInfactScopeVars, getInfactSingletonInstances, getInstanceOwnMethods, getInstanceOwnProps, getMoostInfact, getMoostMate, getNewMoostInfact, getTranslator, globalKey, isConstructor, isThenable, key, loggerConsoleTransport, mergeSorted, registerEventScope, replaceContextInjector, resetContextInjector, resolvePipe, run, setControllerContext, setInfactLoggingOptions, setInterceptOriginal, setInterceptResult, setOvertake, setTranslator, stampModuleIdentity, stampOnce, useControllerContext, useHandlerPaths, useInterceptOriginal, useInterceptResult, useLocale, useLogger, useOvertake, useScopeId, useTranslate, withControllerContext };
|
|
3440
|
+
export { After, ApplyDecorators, Before, Circular, Const, ConstFactory, ContextInjector, Controller, Description, HandlerPaths, Id, ImportController, Inherit, Inject, InjectEventLogger, InjectFromScope, InjectMoost, InjectMoostLogger, InjectScopeVars, Injectable, Intercept, Interceptor, InterceptorHandler, Label, Locale, LoggerTopic, MOOST_ADAPTER_BRAND, MOOST_MODULE_IDENTITY_KEY, Moost, MoostDispose, MoostInit, OnError, Optional, Overtake, Param, Params, Pipe, ProstoLogger, Provide, Replace, Required, Resolve, Response, TInterceptorPriority, TPipePriority, Value, cached, clearGlobalWooks, createEventContext, createLogger, createProvideRegistry, createReplaceRegistry, current, defineAfterInterceptor, defineBeforeInterceptor, defineErrorInterceptor, defineInfactScope, defineInterceptor, defineMoostEventHandler, definePipeFn, detachedKind, disposeInstances, eventTypeKey, forkEventContext, formatMessage, getClassKey, getConstructor, getContextInjector, getGlobalWooks, getHandlerPaths, getInfactScopeVars, getInfactSingletonInstances, getInstanceOwnMethods, getInstanceOwnProps, getMoostInfact, getMoostMate, getNewMoostInfact, getTranslator, getTranslatorLocales, globalKey, isConstructor, isThenable, key, loggerConsoleTransport, mergeSorted, registerEventScope, registerMessages, replaceContextInjector, resetContextInjector, resolvePipe, run, setControllerContext, setInfactLoggingOptions, setInterceptOriginal, setInterceptResult, setOvertake, setTranslator, stampModuleIdentity, stampOnce, translateErrorBody, useControllerContext, useHandlerPaths, useInterceptOriginal, useInterceptResult, useLocale, useLogger, useOvertake, useScopeId, useTranslate, useTranslation, withControllerContext };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moost",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "moost",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"composables",
|
|
@@ -41,15 +41,15 @@
|
|
|
41
41
|
"@prostojs/infact": "^0.6.1",
|
|
42
42
|
"@prostojs/logger": "^0.4.3",
|
|
43
43
|
"@prostojs/mate": "^0.5.1",
|
|
44
|
-
"@wooksjs/event-core": "^0.8.
|
|
44
|
+
"@wooksjs/event-core": "^0.8.1",
|
|
45
45
|
"hookable": "^5.5.3",
|
|
46
|
-
"wooks": "^0.8.
|
|
46
|
+
"wooks": "^0.8.1"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@wooksjs/event-http": "^0.8.
|
|
50
|
-
"@wooksjs/http-body": "^0.8.
|
|
49
|
+
"@wooksjs/event-http": "^0.8.1",
|
|
50
|
+
"@wooksjs/http-body": "^0.8.1",
|
|
51
51
|
"vitest": "4.1.12",
|
|
52
|
-
"@moostjs/event-http": "^0.7.
|
|
52
|
+
"@moostjs/event-http": "^0.7.4"
|
|
53
53
|
},
|
|
54
54
|
"scripts": {
|
|
55
55
|
"pub": "pnpm publish --access public",
|