@endora-commerce/mod-i18n 0.100.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 +21 -0
- package/README.md +52 -0
- package/dist/backend/cli/coverage.d.ts +29 -0
- package/dist/backend/cli/coverage.d.ts.map +1 -0
- package/dist/backend/cli/coverage.js +84 -0
- package/dist/backend/cli/coverage.js.map +1 -0
- package/dist/backend/cli/reload.d.ts +35 -0
- package/dist/backend/cli/reload.d.ts.map +1 -0
- package/dist/backend/cli/reload.js +18 -0
- package/dist/backend/cli/reload.js.map +1 -0
- package/dist/backend/entities/translation-bundle.entity.d.ts +23 -0
- package/dist/backend/entities/translation-bundle.entity.d.ts.map +1 -0
- package/dist/backend/entities/translation-bundle.entity.js +69 -0
- package/dist/backend/entities/translation-bundle.entity.js.map +1 -0
- package/dist/backend/index.d.ts +164 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +134 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.admin.d.ts +41 -0
- package/dist/backend/routes.admin.d.ts.map +1 -0
- package/dist/backend/routes.admin.js +74 -0
- package/dist/backend/routes.admin.js.map +1 -0
- package/dist/backend/services/bundle-loader.d.ts +49 -0
- package/dist/backend/services/bundle-loader.d.ts.map +1 -0
- package/dist/backend/services/bundle-loader.js +84 -0
- package/dist/backend/services/bundle-loader.js.map +1 -0
- package/dist/backend/services/bundle-reconciler.d.ts +82 -0
- package/dist/backend/services/bundle-reconciler.d.ts.map +1 -0
- package/dist/backend/services/bundle-reconciler.js +38 -0
- package/dist/backend/services/bundle-reconciler.js.map +1 -0
- package/dist/backend/services/i18n-service.d.ts +67 -0
- package/dist/backend/services/i18n-service.d.ts.map +1 -0
- package/dist/backend/services/i18n-service.js +323 -0
- package/dist/backend/services/i18n-service.js.map +1 -0
- package/dist/backend/services/missing-key-logger.d.ts +52 -0
- package/dist/backend/services/missing-key-logger.d.ts.map +1 -0
- package/dist/backend/services/missing-key-logger.js +37 -0
- package/dist/backend/services/missing-key-logger.js.map +1 -0
- package/dist/manifest.d.ts +214 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +245 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260507T091405_i18n_admin_i18n_init.d.ts +30 -0
- package/dist/migrations/20260507T091405_i18n_admin_i18n_init.d.ts.map +1 -0
- package/dist/migrations/20260507T091405_i18n_admin_i18n_init.js +50 -0
- package/dist/migrations/20260507T091405_i18n_admin_i18n_init.js.map +1 -0
- package/dist/migrations/index.d.ts +23 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +23 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/i18n.md +166 -0
- package/i18n/en.json +1957 -0
- package/i18n/pl.json +1957 -0
- package/package.json +68 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { lazyPort } from '@endora-commerce/platform/kernel';
|
|
2
|
+
import { registerI18nAdminRoutes } from './routes.admin.js';
|
|
3
|
+
import { I18nService } from './services/i18n-service.js';
|
|
4
|
+
import { reconcileBundles, } from './services/bundle-reconciler.js';
|
|
5
|
+
import { TranslationBundle } from './entities/translation-bundle.entity.js';
|
|
6
|
+
export function registerModule(ctx) {
|
|
7
|
+
ctx.di.providePort('adminI18nService', ctx
|
|
8
|
+
.asFunction(({ emFactory }) => new I18nService({ em: emFactory }))
|
|
9
|
+
.singleton());
|
|
10
|
+
ctx.di.providePort('adminI18nReconciler', ctx
|
|
11
|
+
.asFunction(() => {
|
|
12
|
+
// Feature 072 (D-38b) — `adminI18nService` is resolved per call, not
|
|
13
|
+
// captured in this factory. It is a `providePort` name, so it is a
|
|
14
|
+
// transient gate; awilix strict mode refuses a **singleton** that
|
|
15
|
+
// captures one, and it refuses it unconditionally — warm registry cache
|
|
16
|
+
// or cold. Destructuring it here therefore threw
|
|
17
|
+
// `AwilixResolutionError: … has a shorter lifetime than its ancestor:
|
|
18
|
+
// 'adminI18nReconciler$ungated$'` at every boot, which is the second
|
|
19
|
+
// half of why the backend would not start.
|
|
20
|
+
const i18n = lazyPort(ctx, 'adminI18nService');
|
|
21
|
+
return {
|
|
22
|
+
install: async ({ moduleId, modulePath, bundlesDir }) => {
|
|
23
|
+
const result = await i18n.installBundlesForModule(moduleId, modulePath, bundlesDir);
|
|
24
|
+
return { installed: result.installed };
|
|
25
|
+
},
|
|
26
|
+
remove: async (moduleId) => i18n.removeBundlesForModule(moduleId),
|
|
27
|
+
// Resolved per call for a second reason as well: `reloadAll` is
|
|
28
|
+
// reachable from the admin endpoint and the CLI at any time after
|
|
29
|
+
// boot, and the registry accessor answers `undefined` until
|
|
30
|
+
// `_lifecycle` is built.
|
|
31
|
+
reloadAll: async () => runReconcile(ctx),
|
|
32
|
+
};
|
|
33
|
+
})
|
|
34
|
+
.singleton());
|
|
35
|
+
ctx.routes(async (app) => {
|
|
36
|
+
// Plugin attach, not `onBoot` — see the note at the top of this file. This
|
|
37
|
+
// is the point in a composition where the lifecycle registry exists.
|
|
38
|
+
await runReconcile(ctx);
|
|
39
|
+
const { adminI18nService, requireAdmin, adminContextResolver } = ctx.cradle();
|
|
40
|
+
await registerI18nAdminRoutes(app, {
|
|
41
|
+
i18nService: adminI18nService,
|
|
42
|
+
// Feature 075, Phase C — one method of `admin_users`, resolved per call.
|
|
43
|
+
// The class this replaced brought creation, roles, deletion and
|
|
44
|
+
// impersonation across the boundary for a single language write.
|
|
45
|
+
adminUserPreference: lazyPort(ctx, 'adminUserPreferencePort'),
|
|
46
|
+
requireAdmin,
|
|
47
|
+
resolveAdminContext: adminContextResolver,
|
|
48
|
+
reload: () => runReconcile(ctx),
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
async function runReconcile(ctx) {
|
|
53
|
+
const { lifecycleManifestRegistry, adminI18nService } = ctx.cradle();
|
|
54
|
+
const registry = lifecycleManifestRegistry();
|
|
55
|
+
// Absent registry is a real state, not an error: a composition that never
|
|
56
|
+
// builds `_lifecycle` (several unit tests) still serves the read API, which
|
|
57
|
+
// reads `translation_bundles` directly.
|
|
58
|
+
if (!registry)
|
|
59
|
+
return { installed: 0, skipped: 0, failed: 0, failures: [] };
|
|
60
|
+
return reconcileBundles(registry.modules.values(), adminI18nService, {
|
|
61
|
+
// `reconcileBundles` takes a plain message logger; the kernel's is
|
|
62
|
+
// pino-shaped, so the message goes in the second position.
|
|
63
|
+
info: (msg) => ctx.log.info({}, msg),
|
|
64
|
+
warn: (msg) => ctx.log.warn({}, msg),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The module's own entity classes, as one array — D-168.
|
|
69
|
+
*
|
|
70
|
+
* This is the shape the platform reads when the package is *installed*: the
|
|
71
|
+
* boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
|
|
72
|
+
* and the static declaration reader (`scripts/lib/package-declarations.ts`),
|
|
73
|
+
* which is the third source of `check:module-boundary`'s `table→owner` map and
|
|
74
|
+
* the package pass of `check-entity-tenant-classification`. A missing array is
|
|
75
|
+
* answered with `[]` — zero entities registered, no error anywhere.
|
|
76
|
+
*
|
|
77
|
+
* The class is **not** exported by name beside it (D-168): the ORM registers
|
|
78
|
+
* whichever copy this array carries, and a second, source-reached copy of a
|
|
79
|
+
* MikroORM entity is a lookup miss rather than a type error. A test that needs
|
|
80
|
+
* the runtime class takes it out of this array by name
|
|
81
|
+
* (`backend/test/helpers/package-entities.ts`).
|
|
82
|
+
*/
|
|
83
|
+
export const entities = [
|
|
84
|
+
TranslationBundle,
|
|
85
|
+
];
|
|
86
|
+
/**
|
|
87
|
+
* The rest of this module's published surface, and why each name is on it.
|
|
88
|
+
*
|
|
89
|
+
* Every one of these was reached relatively from `backend/` before the move,
|
|
90
|
+
* and a relative reach into a package's `src` is the defect
|
|
91
|
+
* `check:singleton-identity` refuses: the composed platform loads this
|
|
92
|
+
* package's `dist`, so a second evaluation of the same file is a second copy of
|
|
93
|
+
* everything module-scoped in it. Publishing the names is what gives those
|
|
94
|
+
* callers a spelling that lands on the copy the platform holds.
|
|
95
|
+
*
|
|
96
|
+
* * `buildErrorTranslationTargets` and its shapes **stood here and are gone**
|
|
97
|
+
* (`specs/117-instance-bring-up/` FR-030). The routing map is now
|
|
98
|
+
* `@endora-commerce/platform`'s, at `kernel/i18n/error-translation.ts`,
|
|
99
|
+
* beside `request-language.ts` — the producer of the other
|
|
100
|
+
* `ErrorEnvelopeOptions` member a composition root injects.
|
|
101
|
+
*
|
|
102
|
+
* The line it moved across is worth stating here as well as there, because
|
|
103
|
+
* this is where a reader looks for it: *the routing is derived from
|
|
104
|
+
* manifests, the translation is a service*. `I18nService.translate` — what
|
|
105
|
+
* the envelope's `translateErrorMessage` closure calls — resolves a key
|
|
106
|
+
* against a bundle and is `_i18n`'s. The derivation translated nothing; it
|
|
107
|
+
* read `manifest.errorCodes` off the **resolved manifest set**, which is a
|
|
108
|
+
* composition-root input, and it had no consumer inside this package at
|
|
109
|
+
* all: this barrel re-exported it and nothing here called it. That is
|
|
110
|
+
* T040b's criterion 8, the test `absolutizePublicUrl` moved out of `email`
|
|
111
|
+
* under, met exactly.
|
|
112
|
+
*
|
|
113
|
+
* `ERROR_TRANSLATION_KEYS` and `composeErrorTranslationTargets` stood here
|
|
114
|
+
* too and were **deleted** in feature 090 Phase 4: the first was the prefix
|
|
115
|
+
* chain's static table, the second the transitional composition that laid
|
|
116
|
+
* the declarations over it so the migration could be delivered one module
|
|
117
|
+
* per merge request. All eighteen owners have declared, so there is nothing
|
|
118
|
+
* left to lay over.
|
|
119
|
+
* * `I18nService` — the resolver the reconciler, the CLI commands and the
|
|
120
|
+
* acceptance probe construct over an `EntityManager` of their own.
|
|
121
|
+
* * `MissingKeyLogger` — the resolver's collaborator, constructed the same way.
|
|
122
|
+
* * `loadModuleBundles` / `BundleLoadError` and their shapes — the filesystem
|
|
123
|
+
* reader every module's bundles arrive through.
|
|
124
|
+
* * `reconcileBundles` and its shapes — the boot pass itself, which the
|
|
125
|
+
* acceptance probe runs against a live instance.
|
|
126
|
+
* * `registerI18nAdminRoutes` / `I18nAdminDeps` — the HTTP surface, registered
|
|
127
|
+
* against a bare Fastify instance by its own contract test.
|
|
128
|
+
*/
|
|
129
|
+
export { I18nService } from './services/i18n-service.js';
|
|
130
|
+
export { MissingKeyLogger } from './services/missing-key-logger.js';
|
|
131
|
+
export { BundleLoadError, loadModuleBundles, } from './services/bundle-loader.js';
|
|
132
|
+
export { reconcileBundles, } from './services/bundle-reconciler.js';
|
|
133
|
+
export { registerI18nAdminRoutes } from './routes.admin.js';
|
|
134
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { FastifyInstance, FastifyRequest } from 'fastify';
|
|
2
|
+
import { type AdminUserPreferencePort } from '@endora-commerce/contracts';
|
|
3
|
+
import type { I18nService } from './services/i18n-service.js';
|
|
4
|
+
import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
|
|
5
|
+
/**
|
|
6
|
+
* Admin UI i18n HTTP surface — feature 019 / contracts/admin-http.md.
|
|
7
|
+
*
|
|
8
|
+
* GET /api/v1/admin/i18n/bundles?language=… (E-1)
|
|
9
|
+
* PATCH /api/v1/admin/me/preferred-language (E-2)
|
|
10
|
+
*
|
|
11
|
+
* Both endpoints sit behind the standard admin auth gate — any
|
|
12
|
+
* authenticated admin may call them; no per-permission grant is
|
|
13
|
+
* required (FR-004).
|
|
14
|
+
*/
|
|
15
|
+
export interface I18nAdminDeps {
|
|
16
|
+
i18nService: I18nService;
|
|
17
|
+
/**
|
|
18
|
+
* Owned by `admin_users` (feature 075, Phase C). One method, because the
|
|
19
|
+
* language override is the whole of what this surface writes to an admin
|
|
20
|
+
* row; the service class this used to name carried creation, roles, deletion
|
|
21
|
+
* and impersonation with it. When `admin_users` is off the PATCH answers 503
|
|
22
|
+
* `MODULE_DISABLED`, which is right: there is no admin to hold a preference.
|
|
23
|
+
*/
|
|
24
|
+
adminUserPreference: AdminUserPreferencePort;
|
|
25
|
+
requireAdmin: RequireAdminFactory;
|
|
26
|
+
/** Resolves the calling admin's id; reads `request.actor` in production. */
|
|
27
|
+
resolveAdminContext: (req: FastifyRequest) => {
|
|
28
|
+
adminUserId: string;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Re-reads every module's on-disk i18n bundles into the DB without a
|
|
32
|
+
* restart. Optional — when absent the reload endpoint is not registered.
|
|
33
|
+
*/
|
|
34
|
+
reload?: () => Promise<{
|
|
35
|
+
installed: number;
|
|
36
|
+
skipped: number;
|
|
37
|
+
failed: number;
|
|
38
|
+
}>;
|
|
39
|
+
}
|
|
40
|
+
export declare function registerI18nAdminRoutes(app: FastifyInstance, deps: I18nAdminDeps): Promise<void>;
|
|
41
|
+
//# sourceMappingURL=routes.admin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.d.ts","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE/D,OAAO,EAML,KAAK,uBAAuB,EAI7B,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E;;;;;;;;;GASG;AAEH,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,WAAW,CAAC;IACzB;;;;;;OAMG;IACH,mBAAmB,EAAE,uBAAuB,CAAC;IAC7C,YAAY,EAAE,mBAAmB,CAAC;IAClC,4EAA4E;IAC5E,mBAAmB,EAAE,CAAC,GAAG,EAAE,cAAc,KAAK;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;OAGG;IACH,MAAM,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAChF;AAED,wBAAsB,uBAAuB,CAC3C,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CA+Ff"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { GetBundlesResponseSchema, I18nCoverageQuerySchema, I18nCoverageResponseSchema, PatchPreferredLanguageBodySchema, SupportedAdminLanguageSchema, } from '@endora-commerce/contracts';
|
|
3
|
+
export async function registerI18nAdminRoutes(app, deps) {
|
|
4
|
+
// --- E-1 GET bundles -----------------------------------------------------
|
|
5
|
+
// The Fastify Zod plugin validates the `language` query param against the
|
|
6
|
+
// supported allowlist; any non-matching value yields the standard 400
|
|
7
|
+
// VALIDATION_FAILED envelope (see http/error-envelope.ts).
|
|
8
|
+
const getBundlesQuerySchema = z.object({
|
|
9
|
+
language: SupportedAdminLanguageSchema,
|
|
10
|
+
});
|
|
11
|
+
app.get('/api/v1/admin/i18n/bundles', {
|
|
12
|
+
preHandler: deps.requireAdmin(),
|
|
13
|
+
schema: { querystring: getBundlesQuerySchema },
|
|
14
|
+
}, async (request) => {
|
|
15
|
+
const { language } = getBundlesQuerySchema.parse(request.query);
|
|
16
|
+
const merged = await deps.i18nService.getMergedBundleForLanguage(language);
|
|
17
|
+
const payload = {
|
|
18
|
+
language,
|
|
19
|
+
version: merged.version,
|
|
20
|
+
bundles: merged.bundles,
|
|
21
|
+
};
|
|
22
|
+
// Validate the outgoing shape too — cheap and catches schema drift.
|
|
23
|
+
return { data: GetBundlesResponseSchema.parse(payload) };
|
|
24
|
+
});
|
|
25
|
+
// --- E-4 POST reload bundles (hot-reload on-disk translations) -----------
|
|
26
|
+
// Re-reads every module's i18n JSON into `translation_bundles` (bumping
|
|
27
|
+
// versions) so edited translations appear without a backend restart. Any
|
|
28
|
+
// authenticated admin may trigger it, matching the other i18n endpoints.
|
|
29
|
+
if (deps.reload) {
|
|
30
|
+
const reload = deps.reload;
|
|
31
|
+
app.post('/api/v1/admin/i18n/reload', { preHandler: deps.requireAdmin() }, async () => {
|
|
32
|
+
const result = await reload();
|
|
33
|
+
return { data: result };
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// --- E-2 PATCH self preferred-language ----------------------------------
|
|
37
|
+
app.patch('/api/v1/admin/me/preferred-language', {
|
|
38
|
+
preHandler: deps.requireAdmin(),
|
|
39
|
+
schema: { body: PatchPreferredLanguageBodySchema },
|
|
40
|
+
}, async (request) => {
|
|
41
|
+
const ctx = deps.resolveAdminContext(request);
|
|
42
|
+
const body = PatchPreferredLanguageBodySchema.parse(request.body);
|
|
43
|
+
const updated = await deps.adminUserPreference.setPreferredLanguage(ctx.adminUserId, body.preferredLanguage);
|
|
44
|
+
return {
|
|
45
|
+
data: {
|
|
46
|
+
preferredLanguage: updated.preferredLanguage ?? null,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
// --- E-3 GET coverage diagnostic — feature 021 ---------------------------
|
|
51
|
+
app.get('/api/v1/admin/i18n/coverage', {
|
|
52
|
+
preHandler: deps.requireAdmin(),
|
|
53
|
+
schema: { querystring: I18nCoverageQuerySchema },
|
|
54
|
+
}, async (request) => {
|
|
55
|
+
const q = I18nCoverageQuerySchema.parse(request.query ?? {});
|
|
56
|
+
const moduleIds = q.module === undefined
|
|
57
|
+
? undefined
|
|
58
|
+
: Array.isArray(q.module)
|
|
59
|
+
? q.module
|
|
60
|
+
: [q.module];
|
|
61
|
+
const languageCodes = q.language === undefined
|
|
62
|
+
? undefined
|
|
63
|
+
: Array.isArray(q.language)
|
|
64
|
+
? q.language
|
|
65
|
+
: [q.language];
|
|
66
|
+
const snapshot = await deps.i18nService.getCoverageSnapshot({
|
|
67
|
+
...(moduleIds !== undefined ? { moduleIds } : {}),
|
|
68
|
+
...(languageCodes !== undefined ? { languageCodes } : {}),
|
|
69
|
+
...(q.includeKeys !== undefined ? { includeKeys: q.includeKeys } : {}),
|
|
70
|
+
});
|
|
71
|
+
return { data: I18nCoverageResponseSchema.parse(snapshot) };
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=routes.admin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.admin.js","sourceRoot":"","sources":["../../src/backend/routes.admin.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { type SupportedAdminLanguage, type TranslationBundleEntries } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* Filesystem reader for module-shipped translation bundles —
|
|
4
|
+
* feature 019 / data-model.md §6, contracts/translation-bundle-schema.md.
|
|
5
|
+
*
|
|
6
|
+
* Reads `<modulePath>/<bundlesDir>/<lang>.json` for every supported
|
|
7
|
+
* Admin UI language, validates each file with
|
|
8
|
+
* `TranslationBundleEntriesSchema`, and returns the bundles keyed by
|
|
9
|
+
* language code. Throws a `BundleLoadError` for any rule violation:
|
|
10
|
+
*
|
|
11
|
+
* - filename does not match a supported language code,
|
|
12
|
+
* - JSON does not parse,
|
|
13
|
+
* - the parsed value does not satisfy the entries schema (e.g.
|
|
14
|
+
* nested object, array, null, non-string value, empty key),
|
|
15
|
+
* - the directory exists but no English bundle is present
|
|
16
|
+
* (FR-016: English is the platform-wide fallback).
|
|
17
|
+
*
|
|
18
|
+
* Modules that do NOT ship a bundles directory are exempt and the
|
|
19
|
+
* loader returns an empty map for them.
|
|
20
|
+
*/
|
|
21
|
+
export declare class BundleLoadError extends Error {
|
|
22
|
+
readonly moduleId: string;
|
|
23
|
+
readonly reason: 'unsupported-language-file' | 'parse-failed' | 'invalid-shape' | 'missing-fallback-bundle';
|
|
24
|
+
readonly path?: string | undefined;
|
|
25
|
+
readonly issues?: unknown | undefined;
|
|
26
|
+
constructor(moduleId: string, reason: 'unsupported-language-file' | 'parse-failed' | 'invalid-shape' | 'missing-fallback-bundle', message: string, path?: string | undefined, issues?: unknown | undefined);
|
|
27
|
+
}
|
|
28
|
+
export interface LoadedBundles {
|
|
29
|
+
/**
|
|
30
|
+
* Per-language entries map. Only languages that have a bundle file on
|
|
31
|
+
* disk are present; missing languages are simply absent from the map.
|
|
32
|
+
*/
|
|
33
|
+
byLanguage: Map<SupportedAdminLanguage, TranslationBundleEntries>;
|
|
34
|
+
}
|
|
35
|
+
export interface BundleLoaderOptions {
|
|
36
|
+
/** For tests: override the JSON-file reader (e.g. to point at fixtures). */
|
|
37
|
+
readFile?: (path: string) => string;
|
|
38
|
+
/** For tests: override directory listing. */
|
|
39
|
+
readDir?: (path: string) => string[];
|
|
40
|
+
/** For tests: override existence check. */
|
|
41
|
+
fileExists?: (path: string) => boolean;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Load every supported-language bundle for one module. Returns an empty
|
|
45
|
+
* map when the bundles directory does not exist (the module ships no
|
|
46
|
+
* translatable strings).
|
|
47
|
+
*/
|
|
48
|
+
export declare function loadModuleBundles(moduleId: string, modulePath: string, bundlesDir: string, opts?: BundleLoaderOptions): LoadedBundles;
|
|
49
|
+
//# sourceMappingURL=bundle-loader.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-loader.d.ts","sourceRoot":"","sources":["../../../src/backend/services/bundle-loader.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;;;;;;;;;GAkBG;AAEH,qBAAa,eAAgB,SAAQ,KAAK;aAEtB,QAAQ,EAAE,MAAM;aAChB,MAAM,EAClB,2BAA2B,GAC3B,cAAc,GACd,eAAe,GACf,yBAAyB;aAEb,IAAI,CAAC,EAAE,MAAM;aACb,MAAM,CAAC,EAAE,OAAO;gBARhB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAClB,2BAA2B,GAC3B,cAAc,GACd,eAAe,GACf,yBAAyB,EAC7B,OAAO,EAAE,MAAM,EACC,IAAI,CAAC,EAAE,MAAM,YAAA,EACb,MAAM,CAAC,EAAE,OAAO,YAAA;CAKnC;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,UAAU,EAAE,GAAG,CAAC,sBAAsB,EAAE,wBAAwB,CAAC,CAAC;CACnE;AAED,MAAM,WAAW,mBAAmB;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,6CAA6C;IAC7C,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,EAAE,CAAC;IACrC,2CAA2C;IAC3C,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACxC;AAQD;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,IAAI,GAAE,mBAAwB,GAC7B,aAAa,CA4Df"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { LANGUAGE_FALLBACK, SUPPORTED_LANGUAGES, SupportedAdminLanguageSchema, TranslationBundleEntriesSchema, } from '@endora-commerce/contracts';
|
|
4
|
+
/**
|
|
5
|
+
* Filesystem reader for module-shipped translation bundles —
|
|
6
|
+
* feature 019 / data-model.md §6, contracts/translation-bundle-schema.md.
|
|
7
|
+
*
|
|
8
|
+
* Reads `<modulePath>/<bundlesDir>/<lang>.json` for every supported
|
|
9
|
+
* Admin UI language, validates each file with
|
|
10
|
+
* `TranslationBundleEntriesSchema`, and returns the bundles keyed by
|
|
11
|
+
* language code. Throws a `BundleLoadError` for any rule violation:
|
|
12
|
+
*
|
|
13
|
+
* - filename does not match a supported language code,
|
|
14
|
+
* - JSON does not parse,
|
|
15
|
+
* - the parsed value does not satisfy the entries schema (e.g.
|
|
16
|
+
* nested object, array, null, non-string value, empty key),
|
|
17
|
+
* - the directory exists but no English bundle is present
|
|
18
|
+
* (FR-016: English is the platform-wide fallback).
|
|
19
|
+
*
|
|
20
|
+
* Modules that do NOT ship a bundles directory are exempt and the
|
|
21
|
+
* loader returns an empty map for them.
|
|
22
|
+
*/
|
|
23
|
+
export class BundleLoadError extends Error {
|
|
24
|
+
moduleId;
|
|
25
|
+
reason;
|
|
26
|
+
path;
|
|
27
|
+
issues;
|
|
28
|
+
constructor(moduleId, reason, message, path, issues) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.moduleId = moduleId;
|
|
31
|
+
this.reason = reason;
|
|
32
|
+
this.path = path;
|
|
33
|
+
this.issues = issues;
|
|
34
|
+
this.name = 'BundleLoadError';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const DEFAULT_OPTS = {
|
|
38
|
+
readFile: (p) => readFileSync(p, 'utf8'),
|
|
39
|
+
readDir: (p) => readdirSync(p),
|
|
40
|
+
fileExists: (p) => existsSync(p),
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Load every supported-language bundle for one module. Returns an empty
|
|
44
|
+
* map when the bundles directory does not exist (the module ships no
|
|
45
|
+
* translatable strings).
|
|
46
|
+
*/
|
|
47
|
+
export function loadModuleBundles(moduleId, modulePath, bundlesDir, opts = {}) {
|
|
48
|
+
const o = { ...DEFAULT_OPTS, ...opts };
|
|
49
|
+
const dirPath = join(modulePath, bundlesDir);
|
|
50
|
+
if (!o.fileExists(dirPath)) {
|
|
51
|
+
return { byLanguage: new Map() };
|
|
52
|
+
}
|
|
53
|
+
const supported = new Set(SUPPORTED_LANGUAGES);
|
|
54
|
+
const files = o.readDir(dirPath).filter((f) => f.endsWith('.json'));
|
|
55
|
+
const byLanguage = new Map();
|
|
56
|
+
for (const file of files) {
|
|
57
|
+
const lang = file.replace(/\.json$/, '');
|
|
58
|
+
if (!supported.has(lang)) {
|
|
59
|
+
throw new BundleLoadError(moduleId, 'unsupported-language-file', `[i18n] module "${moduleId}" ships unsupported language bundle "${file}"; supported: ${[
|
|
60
|
+
...supported,
|
|
61
|
+
].join(', ')}`, join(dirPath, file));
|
|
62
|
+
}
|
|
63
|
+
const parsed = SupportedAdminLanguageSchema.parse(lang);
|
|
64
|
+
const filePath = join(dirPath, file);
|
|
65
|
+
let content;
|
|
66
|
+
try {
|
|
67
|
+
content = JSON.parse(o.readFile(filePath));
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
throw new BundleLoadError(moduleId, 'parse-failed', `[i18n] module "${moduleId}" bundle "${file}" is not valid JSON: ${err.message}`, filePath);
|
|
71
|
+
}
|
|
72
|
+
const validated = TranslationBundleEntriesSchema.safeParse(content);
|
|
73
|
+
if (!validated.success) {
|
|
74
|
+
throw new BundleLoadError(moduleId, 'invalid-shape', `[i18n] module "${moduleId}" bundle "${file}" failed validation`, filePath, validated.error.issues);
|
|
75
|
+
}
|
|
76
|
+
byLanguage.set(parsed, validated.data);
|
|
77
|
+
}
|
|
78
|
+
// FR-016 — when the module ships any bundle at all, English MUST be present.
|
|
79
|
+
if (byLanguage.size > 0 && !byLanguage.has(LANGUAGE_FALLBACK)) {
|
|
80
|
+
throw new BundleLoadError(moduleId, 'missing-fallback-bundle', `[i18n] module "${moduleId}" ships translation bundles but is missing the "${LANGUAGE_FALLBACK}.json" fallback (FR-016).`, dirPath);
|
|
81
|
+
}
|
|
82
|
+
return { byLanguage };
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=bundle-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-loader.js","sourceRoot":"","sources":["../../../src/backend/services/bundle-loader.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { I18nService } from './i18n-service.js';
|
|
2
|
+
/** One module whose bundles could not be read, and where they were read from. */
|
|
3
|
+
export interface I18nReconcileFailure {
|
|
4
|
+
moduleId: string;
|
|
5
|
+
/** `dirname(entry.filePath)` — the directory `bundlesDir` was joined to. */
|
|
6
|
+
modulePath: string;
|
|
7
|
+
/** The `BundleLoadError` reason, or `'unknown'` for anything else. */
|
|
8
|
+
reason: string;
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
/** Aggregate outcome of a reconcile/reload pass over every module's bundles. */
|
|
12
|
+
export interface I18nReconcileResult {
|
|
13
|
+
installed: number;
|
|
14
|
+
skipped: number;
|
|
15
|
+
failed: number;
|
|
16
|
+
/**
|
|
17
|
+
* One entry per module counted in {@link failed}.
|
|
18
|
+
*
|
|
19
|
+
* The counts alone were the whole diagnostic surface, and they were enough
|
|
20
|
+
* while every bundle in the platform came out of this repository: a core
|
|
21
|
+
* module's malformed bundle cannot ship, because
|
|
22
|
+
* `test/unit/_i18n/registered-bundles-shape.test.ts` walks the committed
|
|
23
|
+
* registry and fails the build. An **installed package's** bundle passes
|
|
24
|
+
* through no CI of ours, so this pass is the first and only thing that ever
|
|
25
|
+
* reads it — and `failed=1` names nobody.
|
|
26
|
+
*/
|
|
27
|
+
failures: I18nReconcileFailure[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Idempotent reconciler — for every module whose manifest declares an
|
|
31
|
+
* `i18n.bundlesDir`, refresh its `translation_bundles` rows from the
|
|
32
|
+
* JSON files on disk. Errors from the loader (malformed JSON, missing
|
|
33
|
+
* EN bundle, etc.) are logged but do NOT abort the boot — a single
|
|
34
|
+
* bad bundle should not take down the platform; the gap surfaces via
|
|
35
|
+
* FR-014's diagnostic surface and the platform keeps serving.
|
|
36
|
+
*
|
|
37
|
+
* ## Why log-and-skip is still right when the bundle is a stranger's
|
|
38
|
+
*
|
|
39
|
+
* Feature 080 (T032) put an **installed package's** bundles on this path, and
|
|
40
|
+
* that is a different question from the one this policy was written for: a
|
|
41
|
+
* malformed bundle used to be a mistake this repository made and CI refused
|
|
42
|
+
* before it shipped. A package's bundle is a mistake somebody else made, and
|
|
43
|
+
* it arrives on a running shop.
|
|
44
|
+
*
|
|
45
|
+
* The answer does not change, and the reason it does not is that this pass has
|
|
46
|
+
* **no caller to answer**. It runs at boot and on `i18n:reload`, where the only
|
|
47
|
+
* alternative to skipping is refusing to serve — so one vendor's nested JSON
|
|
48
|
+
* object would take a shop off the internet, over labels. That is the same
|
|
49
|
+
* trade `installed-packages.ts` already makes for a `package.json` it cannot
|
|
50
|
+
* parse ("one unreadable stranger must not stop a shop from booting"), and the
|
|
51
|
+
* blast radius here is smaller: the module's routes, permissions and gating are
|
|
52
|
+
* untouched, and its palette entries render raw keys.
|
|
53
|
+
*
|
|
54
|
+
* Where there **is** a caller, the answer is already the opposite one and stays
|
|
55
|
+
* that way: `module:install` reconciles through
|
|
56
|
+
* `orchestrator.ts`'s `i18nReconciler.install(...)`, outside this function,
|
|
57
|
+
* where a `BundleLoadError` bubbles out and aborts the install transaction
|
|
58
|
+
* (FR-016). An operator installing a package with an unreadable bundle is told
|
|
59
|
+
* at the moment they can still choose not to install it.
|
|
60
|
+
*
|
|
61
|
+
* What T032 changed is the part that was genuinely wrong for a stranger:
|
|
62
|
+
* the failure is now **attributable**. The warning names the directory the
|
|
63
|
+
* bundle was read from — for a package that is `node_modules/@vendor/…`, which
|
|
64
|
+
* is the only thing in the line that says whose mistake it is, since D-142
|
|
65
|
+
* makes the module id the platform's identity and not the vendor's — and the
|
|
66
|
+
* result carries {@link I18nReconcileFailure} entries rather than a count.
|
|
67
|
+
*/
|
|
68
|
+
/** One module's manifest + on-disk location, as the reconciler needs it. */
|
|
69
|
+
export interface I18nReconcileEntry {
|
|
70
|
+
manifest: {
|
|
71
|
+
id: string;
|
|
72
|
+
i18n?: {
|
|
73
|
+
bundlesDir: string;
|
|
74
|
+
} | undefined;
|
|
75
|
+
};
|
|
76
|
+
filePath: string;
|
|
77
|
+
}
|
|
78
|
+
export declare function reconcileBundles(entries: Iterable<I18nReconcileEntry>, i18nService: I18nService, log: {
|
|
79
|
+
info(msg: string): void;
|
|
80
|
+
warn(msg: string): void;
|
|
81
|
+
}): Promise<I18nReconcileResult>;
|
|
82
|
+
//# sourceMappingURL=bundle-reconciler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-reconciler.d.ts","sourceRoot":"","sources":["../../../src/backend/services/bundle-reconciler.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,iFAAiF;AACjF,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,MAAM,CAAC;IACjB,4EAA4E;IAC5E,UAAU,EAAE,MAAM,CAAC;IACnB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;;OAUG;IACH,QAAQ,EAAE,oBAAoB,EAAE,CAAC;CAClC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AACH,4EAA4E;AAC5E,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE;YAAE,UAAU,EAAE,MAAM,CAAA;SAAE,GAAG,SAAS,CAAA;KAAE,CAAC;IACpE,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,QAAQ,CAAC,kBAAkB,CAAC,EACrC,WAAW,EAAE,WAAW,EACxB,GAAG,EAAE;IAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,GACxD,OAAO,CAAC,mBAAmB,CAAC,CAyC9B"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { dirname } from 'node:path';
|
|
2
|
+
import { BundleLoadError } from './bundle-loader.js';
|
|
3
|
+
export async function reconcileBundles(entries, i18nService, log) {
|
|
4
|
+
let installed = 0;
|
|
5
|
+
let skipped = 0;
|
|
6
|
+
let failed = 0;
|
|
7
|
+
const failures = [];
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
const i18n = entry.manifest.i18n;
|
|
10
|
+
if (!i18n) {
|
|
11
|
+
skipped += 1;
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
const moduleId = entry.manifest.id;
|
|
15
|
+
const modulePath = dirname(entry.filePath);
|
|
16
|
+
try {
|
|
17
|
+
const result = await i18nService.installBundlesForModule(moduleId, modulePath, i18n.bundlesDir);
|
|
18
|
+
if (result.installed.length > 0) {
|
|
19
|
+
installed += 1;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch (err) {
|
|
23
|
+
failed += 1;
|
|
24
|
+
const isLoadError = err instanceof BundleLoadError;
|
|
25
|
+
const reason = isLoadError ? err.reason : 'unknown';
|
|
26
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27
|
+
failures.push({ moduleId, modulePath, reason, message });
|
|
28
|
+
// `modulePath` is in the line, not only in the structured failure: a
|
|
29
|
+
// module id says which module, and for an installed package it is
|
|
30
|
+
// precisely the thing that does NOT say whose module it is.
|
|
31
|
+
log.warn(`[i18n] reconcile: module "${moduleId}" bundle load failed (${reason}) ` +
|
|
32
|
+
`at ${modulePath}: ${message}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
log.info(`[i18n] reconcile complete — installed=${installed} skipped=${skipped} failed=${failed}`);
|
|
36
|
+
return { installed, skipped, failed, failures };
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=bundle-reconciler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bundle-reconciler.js","sourceRoot":"","sources":["../../../src/backend/services/bundle-reconciler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AA6ErD,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAAqC,EACrC,WAAwB,EACxB,GAAyD;IAEzD,IAAI,SAAS,GAAG,CAAC,CAAC;IAClB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,MAAM,QAAQ,GAA2B,EAAE,CAAC;IAC5C,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,IAAI,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;QACjC,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,OAAO,IAAI,CAAC,CAAC;YACb,SAAS;QACX,CAAC;QACD,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnC,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC3C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,WAAW,CAAC,uBAAuB,CACtD,QAAQ,EACR,UAAU,EACV,IAAI,CAAC,UAAU,CAChB,CAAC;YACF,IAAI,MAAM,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAChC,SAAS,IAAI,CAAC,CAAC;YACjB,CAAC;QACH,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,IAAI,CAAC,CAAC;YACZ,MAAM,WAAW,GAAG,GAAG,YAAY,eAAe,CAAC;YACnD,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;YACpD,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YACjE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC;YACzD,qEAAqE;YACrE,kEAAkE;YAClE,4DAA4D;YAC5D,GAAG,CAAC,IAAI,CACN,6BAA6B,QAAQ,yBAAyB,MAAM,IAAI;gBACtE,MAAM,UAAU,KAAK,OAAO,EAAE,CACjC,CAAC;QACJ,CAAC;IACH,CAAC;IACD,GAAG,CAAC,IAAI,CACN,yCAAyC,SAAS,YAAY,OAAO,WAAW,MAAM,EAAE,CACzF,CAAC;IACF,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;AAClD,CAAC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { EntityManager } from '@mikro-orm/postgresql';
|
|
2
|
+
import { type I18nCoverageResponse, type SupportedAdminLanguage, type TranslationBundleEntries } from '@endora-commerce/contracts';
|
|
3
|
+
import { MissingKeyLogger } from './missing-key-logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* Admin UI i18n resolver and bundle store — feature 019 /
|
|
6
|
+
* data-model.md §8, research §R7 / R9.
|
|
7
|
+
*
|
|
8
|
+
* Public surface (every method is exercised by a test in
|
|
9
|
+
* specs/019-admin-i18n/research.md §R12):
|
|
10
|
+
*
|
|
11
|
+
* - `installBundlesForModule(moduleId, modulePath, bundlesDir)` —
|
|
12
|
+
* called by the lifecycle install hook for any module that declares
|
|
13
|
+
* `manifest.i18n`. UPSERTs one row per language into
|
|
14
|
+
* `translation_bundles`; the row's `version` defaults via the
|
|
15
|
+
* `translation_bundles_version_seq` sequence.
|
|
16
|
+
* - `removeBundlesForModule(moduleId)` — hard-uninstall path. DELETEs
|
|
17
|
+
* every row owned by the module. Soft-uninstall is a no-op
|
|
18
|
+
* (data-model §3).
|
|
19
|
+
* - `getMergedBundleForLanguage(language)` — returns the merged
|
|
20
|
+
* `{ moduleId: entries }` object served to the admin SPA at boot,
|
|
21
|
+
* together with the `version` vector consumed by the SPA's cache.
|
|
22
|
+
* - `translate(moduleId, key, language, params?)` — backend-side lookup
|
|
23
|
+
* with the (requested → en → placeholder) fallback chain.
|
|
24
|
+
*/
|
|
25
|
+
export interface I18nServiceDeps {
|
|
26
|
+
em: () => EntityManager;
|
|
27
|
+
missingKeyLogger?: MissingKeyLogger;
|
|
28
|
+
}
|
|
29
|
+
export declare class I18nService {
|
|
30
|
+
private readonly em;
|
|
31
|
+
private readonly missingKeyLogger;
|
|
32
|
+
private readonly cache;
|
|
33
|
+
constructor(deps: I18nServiceDeps);
|
|
34
|
+
/**
|
|
35
|
+
* Install (or update) every supported-language bundle for one module
|
|
36
|
+
* by reading the JSON files at `<modulePath>/<bundlesDir>/<lang>.json`.
|
|
37
|
+
* The loader rejects malformed input via `BundleLoadError`; the
|
|
38
|
+
* lifecycle orchestrator surfaces that error as the install failure.
|
|
39
|
+
*/
|
|
40
|
+
installBundlesForModule(moduleId: string, modulePath: string, bundlesDir: string, em?: EntityManager): Promise<{
|
|
41
|
+
installed: SupportedAdminLanguage[];
|
|
42
|
+
}>;
|
|
43
|
+
/** Hard-uninstall — soft-uninstall is a no-op (data-model §3). */
|
|
44
|
+
removeBundlesForModule(moduleId: string, em?: EntityManager): Promise<{
|
|
45
|
+
removed: number;
|
|
46
|
+
}>;
|
|
47
|
+
/**
|
|
48
|
+
* Build a `(moduleId, languageCode)` coverage snapshot combining:
|
|
49
|
+
* - the `MissingKeyLogger` accumulator (runtime fallback events), and
|
|
50
|
+
* - a static comparison of the shipped bundle entries per language.
|
|
51
|
+
*
|
|
52
|
+
* Modules and languages can be filtered before serialisation. `includeKeys`
|
|
53
|
+
* defaults to `'missing'`; set `'all'` to return every key the runtime has
|
|
54
|
+
* seen (large; intended for deep inspection).
|
|
55
|
+
*/
|
|
56
|
+
getCoverageSnapshot(filter?: {
|
|
57
|
+
moduleIds?: string[];
|
|
58
|
+
languageCodes?: SupportedAdminLanguage[];
|
|
59
|
+
includeKeys?: 'missing' | 'all';
|
|
60
|
+
}, em?: EntityManager): Promise<I18nCoverageResponse>;
|
|
61
|
+
getMergedBundleForLanguage(language: SupportedAdminLanguage, em?: EntityManager): Promise<{
|
|
62
|
+
version: number;
|
|
63
|
+
bundles: Record<string, TranslationBundleEntries>;
|
|
64
|
+
}>;
|
|
65
|
+
translate(moduleId: string, key: string, language: SupportedAdminLanguage, params?: Record<string, string | number>, em?: EntityManager): Promise<string>;
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=i18n-service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18n-service.d.ts","sourceRoot":"","sources":["../../../src/backend/services/i18n-service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAKL,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,wBAAwB,EAC9B,MAAM,4BAA4B,CAAC;AAGpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,aAAa,CAAC;IACxB,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AASD,qBAAa,WAAW;IACtB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAsB;IACzC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAmB;IACpD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqD;gBAE/D,IAAI,EAAE,eAAe;IASjC;;;;;OAKG;IACG,uBAAuB,CAC3B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,EAAE,CAAC,EAAE,aAAa,GACjB,OAAO,CAAC;QAAE,SAAS,EAAE,sBAAsB,EAAE,CAAA;KAAE,CAAC;IAiCnD,kEAAkE;IAC5D,sBAAsB,CAC1B,QAAQ,EAAE,MAAM,EAChB,EAAE,CAAC,EAAE,aAAa,GACjB,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAiB/B;;;;;;;;OAQG;IACG,mBAAmB,CACvB,MAAM,GAAE;QACN,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;QACrB,aAAa,CAAC,EAAE,sBAAsB,EAAE,CAAC;QACzC,WAAW,CAAC,EAAE,SAAS,GAAG,KAAK,CAAC;KAC5B,EACN,EAAE,CAAC,EAAE,aAAa,GACjB,OAAO,CAAC,oBAAoB,CAAC;IA2F1B,0BAA0B,CAC9B,QAAQ,EAAE,sBAAsB,EAChC,EAAE,CAAC,EAAE,aAAa,GACjB,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,wBAAwB,CAAC,CAAA;KAAE,CAAC;IAyC5E,SAAS,CACb,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,sBAAsB,EAChC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EACxC,EAAE,CAAC,EAAE,aAAa,GACjB,OAAO,CAAC,MAAM,CAAC;CAoEnB"}
|