@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,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"}
|