@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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +52 -0
  3. package/dist/backend/cli/coverage.d.ts +29 -0
  4. package/dist/backend/cli/coverage.d.ts.map +1 -0
  5. package/dist/backend/cli/coverage.js +84 -0
  6. package/dist/backend/cli/coverage.js.map +1 -0
  7. package/dist/backend/cli/reload.d.ts +35 -0
  8. package/dist/backend/cli/reload.d.ts.map +1 -0
  9. package/dist/backend/cli/reload.js +18 -0
  10. package/dist/backend/cli/reload.js.map +1 -0
  11. package/dist/backend/entities/translation-bundle.entity.d.ts +23 -0
  12. package/dist/backend/entities/translation-bundle.entity.d.ts.map +1 -0
  13. package/dist/backend/entities/translation-bundle.entity.js +69 -0
  14. package/dist/backend/entities/translation-bundle.entity.js.map +1 -0
  15. package/dist/backend/index.d.ts +164 -0
  16. package/dist/backend/index.d.ts.map +1 -0
  17. package/dist/backend/index.js +134 -0
  18. package/dist/backend/index.js.map +1 -0
  19. package/dist/backend/routes.admin.d.ts +41 -0
  20. package/dist/backend/routes.admin.d.ts.map +1 -0
  21. package/dist/backend/routes.admin.js +74 -0
  22. package/dist/backend/routes.admin.js.map +1 -0
  23. package/dist/backend/services/bundle-loader.d.ts +49 -0
  24. package/dist/backend/services/bundle-loader.d.ts.map +1 -0
  25. package/dist/backend/services/bundle-loader.js +84 -0
  26. package/dist/backend/services/bundle-loader.js.map +1 -0
  27. package/dist/backend/services/bundle-reconciler.d.ts +82 -0
  28. package/dist/backend/services/bundle-reconciler.d.ts.map +1 -0
  29. package/dist/backend/services/bundle-reconciler.js +38 -0
  30. package/dist/backend/services/bundle-reconciler.js.map +1 -0
  31. package/dist/backend/services/i18n-service.d.ts +67 -0
  32. package/dist/backend/services/i18n-service.d.ts.map +1 -0
  33. package/dist/backend/services/i18n-service.js +323 -0
  34. package/dist/backend/services/i18n-service.js.map +1 -0
  35. package/dist/backend/services/missing-key-logger.d.ts +52 -0
  36. package/dist/backend/services/missing-key-logger.d.ts.map +1 -0
  37. package/dist/backend/services/missing-key-logger.js +37 -0
  38. package/dist/backend/services/missing-key-logger.js.map +1 -0
  39. package/dist/manifest.d.ts +214 -0
  40. package/dist/manifest.d.ts.map +1 -0
  41. package/dist/manifest.js +245 -0
  42. package/dist/manifest.js.map +1 -0
  43. package/dist/migrations/20260507T091405_i18n_admin_i18n_init.d.ts +30 -0
  44. package/dist/migrations/20260507T091405_i18n_admin_i18n_init.d.ts.map +1 -0
  45. package/dist/migrations/20260507T091405_i18n_admin_i18n_init.js +50 -0
  46. package/dist/migrations/20260507T091405_i18n_admin_i18n_init.js.map +1 -0
  47. package/dist/migrations/index.d.ts +23 -0
  48. package/dist/migrations/index.d.ts.map +1 -0
  49. package/dist/migrations/index.js +23 -0
  50. package/dist/migrations/index.js.map +1 -0
  51. package/docs/i18n.md +166 -0
  52. package/i18n/en.json +1957 -0
  53. package/i18n/pl.json +1957 -0
  54. 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"}