@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,323 @@
1
+ import { LANGUAGE_FALLBACK, SUPPORTED_LANGUAGES, } from '@endora-commerce/contracts';
2
+ import { TranslationBundle } from '../entities/translation-bundle.entity.js';
3
+ import { loadModuleBundles } from './bundle-loader.js';
4
+ import { MissingKeyLogger } from './missing-key-logger.js';
5
+ export class I18nService {
6
+ em;
7
+ missingKeyLogger;
8
+ cache = new Map();
9
+ constructor(deps) {
10
+ this.em = deps.em;
11
+ this.missingKeyLogger = deps.missingKeyLogger ?? new MissingKeyLogger();
12
+ }
13
+ // -------------------------------------------------------------------------
14
+ // Lifecycle hook surface — driven by feature 018's orchestrator.
15
+ // -------------------------------------------------------------------------
16
+ /**
17
+ * Install (or update) every supported-language bundle for one module
18
+ * by reading the JSON files at `<modulePath>/<bundlesDir>/<lang>.json`.
19
+ * The loader rejects malformed input via `BundleLoadError`; the
20
+ * lifecycle orchestrator surfaces that error as the install failure.
21
+ */
22
+ async installBundlesForModule(moduleId, modulePath, bundlesDir, em) {
23
+ const loaded = loadModuleBundles(moduleId, modulePath, bundlesDir);
24
+ const targetEm = em ?? this.em();
25
+ const installed = [];
26
+ for (const [language, entries] of loaded.byLanguage) {
27
+ // Single-statement UPSERT — INSERT bumps `version` via the column
28
+ // default (nextval); the conflict UPDATE bumps it explicitly so
29
+ // every successful install advances the sequence regardless of
30
+ // whether the row existed (research §R9).
31
+ //
32
+ // Through `targetEm.execute`, not `targetEm.getKnex().raw`: the knex
33
+ // instance is connection-level and carries no transaction context, so a
34
+ // caller that passes a transactional `em` — as the signature invites, and
35
+ // as the install path's revert semantics assume — got rows that outlived
36
+ // its rollback (issue #200).
37
+ await targetEm.execute(`insert into "translation_bundles" ("module_id", "language_code", "entries", "installed_at", "updated_at") ` +
38
+ `values (?, ?, ?::jsonb, now(), now()) ` +
39
+ `on conflict ("module_id", "language_code") do update set ` +
40
+ `"entries" = excluded."entries", ` +
41
+ `"version" = nextval('translation_bundles_version_seq'), ` +
42
+ `"updated_at" = now()`, [moduleId, language, JSON.stringify(entries)]);
43
+ installed.push(language);
44
+ }
45
+ // Force the next read for any affected language to rebuild from DB.
46
+ for (const language of installed) {
47
+ this.cache.delete(language);
48
+ }
49
+ return { installed };
50
+ }
51
+ /** Hard-uninstall — soft-uninstall is a no-op (data-model §3). */
52
+ async removeBundlesForModule(moduleId, em) {
53
+ // command-coverage-ignore: idempotent lifecycle reconciler/seed — a system-
54
+ // invariant repair, not an operator-initiated audited write.
55
+ const targetEm = em ?? this.em();
56
+ const removed = await targetEm.nativeDelete(TranslationBundle, { moduleId });
57
+ // Affected languages are unknown without a SELECT-then-DELETE; clear all.
58
+ this.cache.clear();
59
+ // Feature 021: drop accumulator entries so the diagnostic stops reporting
60
+ // the now-uninstalled module.
61
+ this.missingKeyLogger.pruneModule(moduleId);
62
+ return { removed };
63
+ }
64
+ // -------------------------------------------------------------------------
65
+ // Coverage diagnostic — feature 021.
66
+ // -------------------------------------------------------------------------
67
+ /**
68
+ * Build a `(moduleId, languageCode)` coverage snapshot combining:
69
+ * - the `MissingKeyLogger` accumulator (runtime fallback events), and
70
+ * - a static comparison of the shipped bundle entries per language.
71
+ *
72
+ * Modules and languages can be filtered before serialisation. `includeKeys`
73
+ * defaults to `'missing'`; set `'all'` to return every key the runtime has
74
+ * seen (large; intended for deep inspection).
75
+ */
76
+ async getCoverageSnapshot(filter = {}, em) {
77
+ const targetEm = em ?? this.em();
78
+ const moduleFilter = filter.moduleIds && filter.moduleIds.length > 0
79
+ ? new Set(filter.moduleIds)
80
+ : null;
81
+ const langFilter = filter.languageCodes && filter.languageCodes.length > 0
82
+ ? new Set(filter.languageCodes)
83
+ : null;
84
+ // Load every bundle from the DB. The bundles table is small (one row per
85
+ // module-language pair); a full scan is fine for a diagnostic that runs
86
+ // at admin-review cadence.
87
+ const rows = await targetEm.find(TranslationBundle, {});
88
+ const byModule = new Map();
89
+ for (const row of rows) {
90
+ const exposedId = exposedBundleNamespace(row.moduleId);
91
+ if (moduleFilter && !moduleFilter.has(exposedId))
92
+ continue;
93
+ const langs = byModule.get(exposedId) ?? new Map();
94
+ langs.set(row.languageCode, row.entries);
95
+ byModule.set(exposedId, langs);
96
+ }
97
+ // Index the accumulator by (moduleId, languageCode) → Set<key>.
98
+ const runtimeMissing = new Map();
99
+ const runtimeFellBackToEn = new Map();
100
+ for (const entry of this.missingKeyLogger.snapshot()) {
101
+ if (moduleFilter && !moduleFilter.has(entry.moduleId))
102
+ continue;
103
+ const lang = entry.languageCode;
104
+ if (langFilter && !langFilter.has(lang))
105
+ continue;
106
+ const missingMap = runtimeMissing.get(entry.moduleId) ?? new Map();
107
+ const missingSet = missingMap.get(lang) ?? new Set();
108
+ missingSet.add(entry.key);
109
+ missingMap.set(lang, missingSet);
110
+ runtimeMissing.set(entry.moduleId, missingMap);
111
+ if (entry.fellBackTo === 'en') {
112
+ const fbMap = runtimeFellBackToEn.get(entry.moduleId) ?? new Map();
113
+ const fbSet = fbMap.get(lang) ?? new Set();
114
+ fbSet.add(entry.key);
115
+ fbMap.set(lang, fbSet);
116
+ runtimeFellBackToEn.set(entry.moduleId, fbMap);
117
+ }
118
+ }
119
+ const allModuleIds = new Set([
120
+ ...byModule.keys(),
121
+ ...runtimeMissing.keys(),
122
+ ]);
123
+ const modules = [];
124
+ for (const moduleId of Array.from(allModuleIds).sort()) {
125
+ const langs = byModule.get(moduleId) ?? new Map();
126
+ const languages = [];
127
+ for (const lang of SUPPORTED_LANGUAGES) {
128
+ if (langFilter && !langFilter.has(lang))
129
+ continue;
130
+ const entries = langs.get(lang) ?? {};
131
+ const otherEntries = lang === LANGUAGE_FALLBACK
132
+ ? (Array.from(langs.entries()).find(([k]) => k !== LANGUAGE_FALLBACK)?.[1] ?? {})
133
+ : (langs.get(LANGUAGE_FALLBACK) ?? {});
134
+ // Static-scan missing: keys present in any other language but absent
135
+ // in this one.
136
+ const staticMissing = new Set();
137
+ for (const k of Object.keys(otherEntries)) {
138
+ if (!(k in entries))
139
+ staticMissing.add(k);
140
+ }
141
+ // Union with the runtime accumulator's missing set.
142
+ const runtimeSet = runtimeMissing.get(moduleId)?.get(lang) ?? new Set();
143
+ for (const k of runtimeSet)
144
+ staticMissing.add(k);
145
+ const missingKeys = Array.from(staticMissing).sort();
146
+ const fbSet = runtimeFellBackToEn.get(moduleId)?.get(lang) ?? new Set();
147
+ languages.push({
148
+ languageCode: lang,
149
+ totalKeysSeen: Object.keys(entries).length + (filter.includeKeys === 'all' ? 0 : 0),
150
+ missingCount: missingKeys.length,
151
+ missingKeys,
152
+ fellBackToEnCount: fbSet.size,
153
+ fellBackToEnKeys: Array.from(fbSet).sort(),
154
+ });
155
+ }
156
+ modules.push({ moduleId, languages });
157
+ }
158
+ return {
159
+ capturedAt: new Date().toISOString(),
160
+ modules,
161
+ };
162
+ }
163
+ // -------------------------------------------------------------------------
164
+ // Read surface — driven by the GET /api/v1/admin/i18n/bundles endpoint.
165
+ // -------------------------------------------------------------------------
166
+ async getMergedBundleForLanguage(language, em) {
167
+ const targetEm = em ?? this.em();
168
+ // `targetEm.execute`, not `targetEm.getKnex()`: the rows below are read
169
+ // through `targetEm`, so a caller holding a transaction open would have had
170
+ // the freshness probe answer from outside it and the row read from inside —
171
+ // one method, two views of the same table (issue #200).
172
+ const maxRows = (await targetEm.execute(`select max("version") as max from "translation_bundles" where "language_code" = ?`, [language]));
173
+ const liveMax = maxRows[0]?.max == null ? 0 : Number(maxRows[0].max);
174
+ const cached = this.cache.get(language);
175
+ if (cached && cached.version >= liveMax) {
176
+ return { version: cached.version, bundles: cached.merged };
177
+ }
178
+ const rows = await targetEm.find(TranslationBundle, { languageCode: language });
179
+ const merged = {};
180
+ let highest = 0;
181
+ for (const row of rows) {
182
+ // The `_i18n` module ships its own bundle as the synthetic `core`
183
+ // namespace that owns admin-chrome strings (AppShell, login,
184
+ // profile). DB rows stay keyed by `_i18n` so they line up with
185
+ // `module_registrations`; the rename happens at the resolver
186
+ // boundary so consumers (Admin SPA + backend `translate(...)`
187
+ // calls) look it up under `core`. See data-model.md §3 / §4 and
188
+ // research.md §R7.
189
+ const exposedId = exposedBundleNamespace(row.moduleId);
190
+ merged[exposedId] = row.entries;
191
+ if (Number(row.version) > highest)
192
+ highest = Number(row.version);
193
+ }
194
+ this.cache.set(language, { version: highest, merged });
195
+ return { version: highest, bundles: merged };
196
+ }
197
+ // -------------------------------------------------------------------------
198
+ // Resolver — backend-side string lookup for emitted messages
199
+ // (audit-log labels, validation messages, notifications, etc.).
200
+ // -------------------------------------------------------------------------
201
+ async translate(moduleId, key, language, params, em) {
202
+ // The alias, applied on the **read** side — feature 090, Phase 3
203
+ // (`specs/090-module-owned-error-codes/core-block-home.md` §4(c)).
204
+ //
205
+ // This module's bundle is exposed to clients under the synthetic namespace
206
+ // `core`, for the reason `getMergedBundleForLanguage` gives above: the
207
+ // admin SPA has called it that since feature 019 and does so in 132 places.
208
+ // Every caller therefore asked for `core` — until this module declared the
209
+ // platform's 100 error codes in its own manifest, which made the composed
210
+ // routing map answer `_i18n` and `translate('_i18n', 'errors.INTERNAL', …)`
211
+ // return the placeholder. Both composition roots turn a placeholder back
212
+ // into the raiser's untranslated English, silently, so the symptom would
213
+ // have been 41 codes losing their sentences in both shipped languages with
214
+ // no log and no failing check. `backend/test/integration/_i18n/
215
+ // platform-error-sentences.test.ts` is the assertion that refuses it.
216
+ //
217
+ // The alternative was to publish the bundle under both keys in the merged
218
+ // map, and it was refused: that map is the payload
219
+ // `GET /api/v1/admin/i18n/bundles` serves, so it would change the wire
220
+ // shape and duplicate the largest bundle on every admin boot.
221
+ //
222
+ // **A module with no legacy namespace needs nothing equivalent.** This is
223
+ // one fact about one repository's history, not a pattern to copy: a
224
+ // stranger's module is looked up under the id it registers. The remaining
225
+ // three copies of the identity live in
226
+ // `backend/scripts/check-error-translations.ts`, which still reads the
227
+ // prefix chain and so still sees the string `core`; they go with the chain,
228
+ // in the merge request that deletes it.
229
+ const bundleId = exposedBundleNamespace(moduleId);
230
+ const merged = await this.getMergedBundleForLanguage(language, em);
231
+ const requested = merged.bundles[bundleId]?.[key];
232
+ if (requested != null) {
233
+ return interpolate(requested, params);
234
+ }
235
+ if (language !== LANGUAGE_FALLBACK) {
236
+ const fallback = await this.getMergedBundleForLanguage(LANGUAGE_FALLBACK, em);
237
+ const englishValue = fallback.bundles[bundleId]?.[key];
238
+ if (englishValue != null) {
239
+ // The **exposed** id, not the caller's: `getCoverageSnapshot` keys its
240
+ // bundle side by the exposed id too, so logging `_i18n` here would
241
+ // report a second module with no bundle and 0% coverage beside `core`.
242
+ this.missingKeyLogger.logFallback({
243
+ moduleId: bundleId,
244
+ languageCode: language,
245
+ key,
246
+ fellBackTo: 'en',
247
+ });
248
+ return interpolate(englishValue, params);
249
+ }
250
+ }
251
+ this.missingKeyLogger.logFallback({
252
+ moduleId: bundleId,
253
+ languageCode: language,
254
+ key,
255
+ fellBackTo: 'placeholder',
256
+ });
257
+ // The **caller's** id, and this one may not be normalised: both composition
258
+ // roots recognise a miss by comparing the answer to `${moduleId}.${key}`
259
+ // with the id they passed in, and answer the raiser's own message when it
260
+ // matches. Returning `core.errors.INTERNAL` to a caller that asked about
261
+ // `_i18n` would defeat that comparison and put the placeholder itself in
262
+ // front of a client.
263
+ return `${moduleId}.${key}`;
264
+ }
265
+ }
266
+ // `SUPPORTED_LANGUAGES` and `BundleLoadError` were re-exported here "for tests /
267
+ // consumers", and the line went with T040b's packaging. Neither had a consumer:
268
+ // `SUPPORTED_LANGUAGES` is `@endora-commerce/contracts`', which every caller
269
+ // already imports from there, and `BundleLoadError` is published by this
270
+ // package's own `./backend` barrel beside `loadModuleBundles`. What made the
271
+ // line a defect rather than dead weight is that it is a bare `export { … }`
272
+ // over two *imported* bindings, so `readBackendSurface` — which follows the
273
+ // barrel's re-exports to decide whether a package publishes an entity class by
274
+ // name (D-168) — reached a specifier it cannot resolve and reported
275
+ // `unresolvable-reexport`. That verdict is right: a re-export it cannot read is
276
+ // the one case where a silent pass would mean "not looking".
277
+ /** Manifest id of the platform-internal i18n module (= the chrome bundle owner). */
278
+ const I18N_CHROME_MODULE_ID = '_i18n';
279
+ /** Synthetic namespace exposed to clients for the `_i18n` module's bundle. */
280
+ const CORE_NAMESPACE = 'core';
281
+ /**
282
+ * `_i18n` -> `core`, written once — and this is now the **only** statement of it
283
+ * in the tree (D-185, feature 090 Phase 4).
284
+ *
285
+ * It was eight applications over four files from four independently declared
286
+ * constants. Six of them read the prefix chain — three in
287
+ * `backend/scripts/check-error-translations.ts` and three in two of its tests —
288
+ * and went with it when Phase 4 deleted the chain; the two here had already been
289
+ * collapsed into this one function by the platform-block migration. Reaching one
290
+ * copy was therefore done by deleting, never by rewriting.
291
+ *
292
+ * **A module with a legacy client-facing namespace is what needs this, and there
293
+ * is exactly one.** `_i18n`'s bundle has answered to `core` since feature 019
294
+ * and the admin SPA calls it that at 132 sites in 81 files, so the module id the
295
+ * routing map carries and the namespace a client asks for are two different
296
+ * words for one bundle. **A module written today has no such history and needs
297
+ * nothing equivalent** — its id *is* its namespace, and copying this function
298
+ * into another module would invent a second name for a bundle that has one.
299
+ *
300
+ * It is deliberately on the **read** side. The merged bundle map is the payload
301
+ * `GET /api/v1/admin/i18n/bundles` serves, so aliasing it would change the wire
302
+ * shape and duplicate the largest bundle for every admin boot; and the
303
+ * placeholder `translate` returns is not aliased either, because both
304
+ * composition roots recognise a miss by comparing that placeholder to the id
305
+ * they passed in.
306
+ *
307
+ * Without it, 41 codes lose their sentences in **both** shipped languages,
308
+ * silently: `translate('_i18n', …)` would return the raw key and the roots would
309
+ * map a placeholder-equal result back to the raiser's untranslated English, with
310
+ * no log and no finding.
311
+ * `backend/test/integration/_i18n/platform-error-sentences.test.ts` is the
312
+ * assertion, and it is a Polish one on purpose — the degradation is English.
313
+ */
314
+ function exposedBundleNamespace(moduleId) {
315
+ return moduleId === I18N_CHROME_MODULE_ID ? CORE_NAMESPACE : moduleId;
316
+ }
317
+ /** Substitute `{name}` placeholders. Missing params are left as-is. */
318
+ function interpolate(template, params) {
319
+ if (!params)
320
+ return template;
321
+ return template.replace(/\{(\w+)\}/g, (_, name) => params[name] != null ? String(params[name]) : `{${name}}`);
322
+ }
323
+ //# sourceMappingURL=i18n-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n-service.js","sourceRoot":"","sources":["../../../src/backend/services/i18n-service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ import type { ModuleLifecycleLogger } from '@endora-commerce/contracts';
2
+ /**
3
+ * Diagnostic surface for the resolver's fallback path — feature 019 /
4
+ * spec FR-014, research §R8.
5
+ *
6
+ * Every time the resolver falls back from the user's preferred language
7
+ * to English, or from English to the literal `${scope}.${key}` placeholder,
8
+ * a structured log line is emitted so the gap is observable without
9
+ * changing user-facing behaviour.
10
+ *
11
+ * Shape stays stable across callsites — operators run
12
+ * `grep '"event":"i18n.fallback"'` against the platform's existing log
13
+ * tail. The shape is exercised by `test/unit/_i18n/missing-key-logger.unit.test.ts`.
14
+ */
15
+ export interface FallbackLogEntry {
16
+ event: 'i18n.fallback';
17
+ moduleId: string;
18
+ languageCode: string;
19
+ key: string;
20
+ fellBackTo: 'en' | 'placeholder';
21
+ }
22
+ export interface MissingKeyLoggerOptions {
23
+ /** Optional logger override; falls back to the project's standard console-style sink. */
24
+ logger?: Pick<ModuleLifecycleLogger, 'info'> & {
25
+ info(msg: string): void;
26
+ };
27
+ }
28
+ export interface MissingKeyEntry {
29
+ moduleId: string;
30
+ languageCode: string;
31
+ key: string;
32
+ fellBackTo: 'en' | 'placeholder';
33
+ }
34
+ export declare class MissingKeyLogger {
35
+ private readonly logger;
36
+ /**
37
+ * Feature 021: in-process accumulator backing the coverage diagnostic.
38
+ * Keyed by `${moduleId}:${languageCode}:${key}` so repeated resolutions
39
+ * of the same missing entry don't inflate the count.
40
+ */
41
+ private readonly entries;
42
+ constructor(opts?: MissingKeyLoggerOptions);
43
+ logFallback(entry: Omit<FallbackLogEntry, 'event'>): void;
44
+ /** Coverage diagnostic — return a defensive copy of the accumulator. */
45
+ snapshot(): MissingKeyEntry[];
46
+ /**
47
+ * Drop every accumulator entry owned by `moduleId`. Called on module
48
+ * hard-uninstall so the diagnostic doesn't report a phantom module.
49
+ */
50
+ pruneModule(moduleId: string): void;
51
+ }
52
+ //# sourceMappingURL=missing-key-logger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-key-logger.d.ts","sourceRoot":"","sources":["../../../src/backend/services/missing-key-logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAExE;;;;;;;;;;;;GAYG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,IAAI,GAAG,aAAa,CAAC;CAClC;AAED,MAAM,WAAW,uBAAuB;IACtC,yFAAyF;IACzF,MAAM,CAAC,EAAE,IAAI,CAAC,qBAAqB,EAAE,MAAM,CAAC,GAAG;QAAE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;CAC5E;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,IAAI,GAAG,aAAa,CAAC;CAClC;AAED,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;IACrD;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsC;gBAElD,IAAI,GAAE,uBAA4B;IAQ9C,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,gBAAgB,EAAE,OAAO,CAAC,GAAG,IAAI;IAOzD,wEAAwE;IACxE,QAAQ,IAAI,eAAe,EAAE;IAI7B;;;OAGG;IACH,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI;CAKpC"}
@@ -0,0 +1,37 @@
1
+ export class MissingKeyLogger {
2
+ logger;
3
+ /**
4
+ * Feature 021: in-process accumulator backing the coverage diagnostic.
5
+ * Keyed by `${moduleId}:${languageCode}:${key}` so repeated resolutions
6
+ * of the same missing entry don't inflate the count.
7
+ */
8
+ entries = new Map();
9
+ constructor(opts = {}) {
10
+ // Default sink uses `console.warn` so it surfaces under the project's
11
+ // lint policy (which restricts `console.info`). The structured payload
12
+ // already carries `event:'i18n.fallback'`, so consumers grep on event
13
+ // rather than log level — the level is operationally diagnostic only.
14
+ this.logger = opts.logger ?? { info: (msg) => console.warn(msg) };
15
+ }
16
+ logFallback(entry) {
17
+ const payload = { event: 'i18n.fallback', ...entry };
18
+ this.logger.info(JSON.stringify(payload));
19
+ const k = `${entry.moduleId}:${entry.languageCode}:${entry.key}`;
20
+ this.entries.set(k, { ...entry });
21
+ }
22
+ /** Coverage diagnostic — return a defensive copy of the accumulator. */
23
+ snapshot() {
24
+ return Array.from(this.entries.values());
25
+ }
26
+ /**
27
+ * Drop every accumulator entry owned by `moduleId`. Called on module
28
+ * hard-uninstall so the diagnostic doesn't report a phantom module.
29
+ */
30
+ pruneModule(moduleId) {
31
+ for (const k of Array.from(this.entries.keys())) {
32
+ if (k.startsWith(`${moduleId}:`))
33
+ this.entries.delete(k);
34
+ }
35
+ }
36
+ }
37
+ //# sourceMappingURL=missing-key-logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missing-key-logger.js","sourceRoot":"","sources":["../../../src/backend/services/missing-key-logger.ts"],"names":[],"mappings":"AAoCA,MAAM,OAAO,gBAAgB;IACV,MAAM,CAA8B;IACrD;;;;OAIG;IACc,OAAO,GAAG,IAAI,GAAG,EAA2B,CAAC;IAE9D,YAAY,OAAgC,EAAE;QAC5C,sEAAsE;QACtE,uEAAuE;QACvE,sEAAsE;QACtE,sEAAsE;QACtE,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,IAAI,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACpE,CAAC;IAED,WAAW,CAAC,KAAsC;QAChD,MAAM,OAAO,GAAqB,EAAE,KAAK,EAAE,eAAe,EAAE,GAAG,KAAK,EAAE,CAAC;QACvE,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,GAAG,GAAG,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,YAAY,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;QACjE,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,KAAK,EAAE,CAAC,CAAC;IACpC,CAAC;IAED,wEAAwE;IACxE,QAAQ;QACN,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED;;;OAGG;IACH,WAAW,CAAC,QAAgB;QAC1B,KAAK,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;YAChD,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,QAAQ,GAAG,CAAC;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,214 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import { type ModuleCliCommand, type ModuleLifecycleParticipant } from '@endora-commerce/contracts';
3
+ import type { ModuleContext } from '@endora-commerce/platform/kernel';
4
+ /**
5
+ * Admin UI i18n subsystem — feature 019.
6
+ *
7
+ * Platform-internal module (underscore-prefixed exemption per Constitution
8
+ * Principle VI, alongside `auth` / `example` / `_lifecycle`). Owns the
9
+ * `translation_bundles` table, the per-language merged-bundle resolver,
10
+ * the read API consumed by the admin SPA at boot, and the `core`
11
+ * namespace that holds admin-chrome strings (AppShell, navigation,
12
+ * login, profile).
13
+ *
14
+ * Depends on `_lifecycle` because every module's bundle install / hard-
15
+ * uninstall is driven by the lifecycle orchestrator's hook surface.
16
+ */
17
+ export declare const manifest: {
18
+ id: string;
19
+ name: string;
20
+ version: string;
21
+ dependencies: string[];
22
+ description?: string | undefined;
23
+ acknowledgedDependencies?: {
24
+ moduleId: string;
25
+ port: string;
26
+ reason: string;
27
+ }[] | undefined;
28
+ nonBindingDependencies?: {
29
+ moduleId: string;
30
+ name: string;
31
+ kind: "contributes-to" | "degrades-without" | "refuses-without";
32
+ reason: string;
33
+ whenAbsent?: string | undefined;
34
+ }[] | undefined;
35
+ activation?: {
36
+ settingCode: string;
37
+ default: boolean;
38
+ } | {
39
+ nonDeactivatable: true;
40
+ reason: string;
41
+ } | undefined;
42
+ settings?: {
43
+ moduleCode: string;
44
+ groups: {
45
+ code: string;
46
+ name: string;
47
+ salesChannelCodes?: string[] | undefined;
48
+ isSystemProtected?: boolean | undefined;
49
+ }[];
50
+ settings: {
51
+ code: string;
52
+ name: string;
53
+ valueType: "string" | "number" | "boolean" | "json" | "string_list" | "secret" | "credential_ref";
54
+ defaultValue: unknown;
55
+ description?: string | undefined;
56
+ groupCode?: string | undefined;
57
+ previousDefaultValues?: unknown[] | undefined;
58
+ salesChannelCodes?: string[] | undefined;
59
+ enumOptions?: string[] | undefined;
60
+ configurationType?: string | undefined;
61
+ hidden?: boolean | undefined;
62
+ }[];
63
+ } | undefined;
64
+ i18n?: {
65
+ bundlesDir: string;
66
+ } | undefined;
67
+ docs?: false | {
68
+ dir: string;
69
+ } | undefined;
70
+ demo?: false | {
71
+ summary: string;
72
+ seed: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoSeedResult>;
73
+ reset: (context: import("@endora-commerce/contracts").ModuleDemoContext<never>) => Promise<import("@endora-commerce/contracts").DemoResetResult>;
74
+ after?: readonly string[] | undefined;
75
+ package?: string | undefined;
76
+ } | undefined;
77
+ actions?: {
78
+ id: string;
79
+ labelKey: string;
80
+ icon: "Plus" | "Sparkles" | "Settings" | "Search" | "Boxes" | "Layers" | "Menu" | "PlusCircle" | "PlusSquare" | "FilePlus" | "FolderPlus" | "Upload" | "FileUp" | "CloudUpload" | "Download" | "FileDown" | "FileText" | "BookOpen" | "Rss" | "Package" | "Tag" | "ShoppingCart" | "Receipt" | "CreditCard" | "Users" | "UserPlus" | "Inbox" | "ListChecks" | "ClipboardList" | "Image" | "Video" | "LayoutDashboard" | "PanelLeft" | "KeyRound" | "ShieldCheck" | "Edit" | "Archive" | "Box" | "Truck" | "CircleDollarSign" | "Activity" | "LineChart" | "Smartphone" | "Webhook" | "Scale" | "PlugZap" | "PercentDiamond" | "Newspaper" | "Languages" | "Eraser" | "Warehouse" | "TrendingDown" | "Bell" | "PackageOpen" | "Building2" | "Store" | "ClipboardCheck";
81
+ targetRoute: string;
82
+ keywords: string[];
83
+ weight: number;
84
+ descriptionKey?: string | undefined;
85
+ requiredPermission?: string | undefined;
86
+ }[] | undefined;
87
+ permissions?: {
88
+ code: string;
89
+ label: string;
90
+ module?: string | undefined;
91
+ description?: string | undefined;
92
+ requires?: string[] | undefined;
93
+ }[] | undefined;
94
+ transactionalEmails?: {
95
+ code: string;
96
+ name: string;
97
+ variables: {
98
+ key: string;
99
+ label: string;
100
+ sampleValue?: string | undefined;
101
+ description?: string | undefined;
102
+ }[];
103
+ description?: string | undefined;
104
+ group?: string | undefined;
105
+ }[] | undefined;
106
+ capabilities?: string[] | undefined;
107
+ exclusiveCapabilities?: {
108
+ key: string;
109
+ errorCode: string;
110
+ }[] | undefined;
111
+ errorCodes?: {
112
+ code: string;
113
+ tokens?: string[] | undefined;
114
+ }[] | undefined;
115
+ blocks?: {
116
+ name: string;
117
+ labelKey: string;
118
+ category: string;
119
+ contexts: ("invoice" | "email" | "cms" | "newsletter")[];
120
+ fields: Record<string, {
121
+ type: "number" | "object" | "uuid" | "array" | "text" | "textarea" | "select" | "radio" | "external" | "richtext";
122
+ label?: string | undefined;
123
+ required?: boolean | undefined;
124
+ options?: {
125
+ label: string;
126
+ value: string | number;
127
+ }[] | undefined;
128
+ refKind?: string | undefined;
129
+ }>;
130
+ descriptionKey?: string | undefined;
131
+ defaultProps?: Record<string, unknown> | undefined;
132
+ responsiveFields?: string[] | undefined;
133
+ previewIcon?: string | undefined;
134
+ weight?: number | undefined;
135
+ }[] | undefined;
136
+ blockCategories?: {
137
+ key: string;
138
+ titleKey: string;
139
+ contexts: ("invoice" | "email" | "cms" | "newsletter")[];
140
+ weight?: number | undefined;
141
+ visible?: boolean | undefined;
142
+ }[] | undefined;
143
+ env?: {
144
+ name: string;
145
+ describes: {
146
+ en: string;
147
+ pl: string;
148
+ };
149
+ requirement: {
150
+ kind: "required";
151
+ } | {
152
+ kind: "requiredWhen";
153
+ input: string;
154
+ equals: string;
155
+ } | {
156
+ kind: "optional";
157
+ without: {
158
+ en: string;
159
+ pl: string;
160
+ };
161
+ };
162
+ secret: boolean;
163
+ generable: boolean;
164
+ owner: {
165
+ kind: "platform";
166
+ } | {
167
+ kind: "application";
168
+ application: "admin" | "backend" | "storefront";
169
+ } | {
170
+ kind: "module";
171
+ moduleId: string;
172
+ };
173
+ consumers: ("admin" | "backend" | "storefront")[];
174
+ addressOf: "admin" | "backend" | "storefront" | null;
175
+ }[] | undefined;
176
+ };
177
+ /**
178
+ * `translation_bundles` follows the manifest set — feature 080, T036a / D-159.
179
+ *
180
+ * Not an install hook: an install hook fires for *its own* module, and this has
181
+ * to run whenever **any** module is installed, because this module's table is a
182
+ * projection of every other module's `manifest.i18n` declaration. Not a port
183
+ * either: the lifecycle orchestrator also serves the five `module:*` commands,
184
+ * and a platform command composes no container to resolve one from (D-157.2 /
185
+ * D-157.4) — which is exactly how a terminal install came to write no bundle at
186
+ * all while the same install from `/platform/modules` wrote them.
187
+ *
188
+ * The service is imported at call time, not at module load. This file is
189
+ * imported by the generated manifest index, which is in turn imported by every
190
+ * static check script and by `src/db/configured-migrations.ts`; a static import
191
+ * of `I18nService` would pull an ORM-dependent graph into all of them.
192
+ *
193
+ * A fresh `I18nService` per call is correct and not a lost cache:
194
+ * `getMergedBundleForLanguage` revalidates against `MAX(version)` in the table
195
+ * on every read, so the running server picks the new rows up on its next
196
+ * request without having been the instance that wrote them.
197
+ */
198
+ export declare const lifecycleParticipant: ModuleLifecycleParticipant<EntityManager>;
199
+ /**
200
+ * The two operator commands this module declares — feature 080, T042b /
201
+ * D-160.9.
202
+ *
203
+ * They were `scripts/reload.ts` and `scripts/coverage.ts`, each opening its own
204
+ * ORM and building its own `I18nService`. The host composes now and hands the
205
+ * body this module's `ModuleContext`, which is what retired this module's one
206
+ * `check:module-boundary` key: `reload` needed the deployment's module registry,
207
+ * and the registry is a root-supplied name any module may read off its cradle.
208
+ *
209
+ * The bodies are `await import()`ed for the reason the participant above gives:
210
+ * this file is imported by the generated manifest index, and through it by every
211
+ * static check script and by `src/db/configured-migrations.ts`.
212
+ */
213
+ export declare const cliCommands: ReadonlyArray<ModuleCliCommand<ModuleContext>>;
214
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,0BAA0B,EAChC,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAEtE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyKnB,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,oBAAoB,EAAE,0BAA0B,CAAC,aAAa,CAqB1E,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,EAAE,aAAa,CAAC,gBAAgB,CAAC,aAAa,CAAC,CAWtE,CAAC"}