@endora-commerce/mod-languages 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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -0
  3. package/dist/backend/entities/language.entity.d.ts +23 -0
  4. package/dist/backend/entities/language.entity.d.ts.map +1 -0
  5. package/dist/backend/entities/language.entity.js +78 -0
  6. package/dist/backend/entities/language.entity.js.map +1 -0
  7. package/dist/backend/index.d.ts +63 -0
  8. package/dist/backend/index.d.ts.map +1 -0
  9. package/dist/backend/index.js +114 -0
  10. package/dist/backend/index.js.map +1 -0
  11. package/dist/backend/routes.d.ts +28 -0
  12. package/dist/backend/routes.d.ts.map +1 -0
  13. package/dist/backend/routes.js +74 -0
  14. package/dist/backend/routes.js.map +1 -0
  15. package/dist/backend/services/language-ports.d.ts +33 -0
  16. package/dist/backend/services/language-ports.d.ts.map +1 -0
  17. package/dist/backend/services/language-ports.js +70 -0
  18. package/dist/backend/services/language-ports.js.map +1 -0
  19. package/dist/backend/services/language-reference-registry.d.ts +30 -0
  20. package/dist/backend/services/language-reference-registry.d.ts.map +1 -0
  21. package/dist/backend/services/language-reference-registry.js +54 -0
  22. package/dist/backend/services/language-reference-registry.js.map +1 -0
  23. package/dist/backend/services/language-seed-service.d.ts +24 -0
  24. package/dist/backend/services/language-seed-service.d.ts.map +1 -0
  25. package/dist/backend/services/language-seed-service.js +40 -0
  26. package/dist/backend/services/language-seed-service.js.map +1 -0
  27. package/dist/backend/services/language-service.d.ts +99 -0
  28. package/dist/backend/services/language-service.d.ts.map +1 -0
  29. package/dist/backend/services/language-service.js +299 -0
  30. package/dist/backend/services/language-service.js.map +1 -0
  31. package/dist/backend/services/locale-service.d.ts +35 -0
  32. package/dist/backend/services/locale-service.d.ts.map +1 -0
  33. package/dist/backend/services/locale-service.js +88 -0
  34. package/dist/backend/services/locale-service.js.map +1 -0
  35. package/dist/manifest.d.ts +169 -0
  36. package/dist/manifest.d.ts.map +1 -0
  37. package/dist/manifest.js +40 -0
  38. package/dist/manifest.js.map +1 -0
  39. package/dist/migrations/20260425T161557_languages_currencies_init.d.ts +13 -0
  40. package/dist/migrations/20260425T161557_languages_currencies_init.d.ts.map +1 -0
  41. package/dist/migrations/20260425T161557_languages_currencies_init.js +60 -0
  42. package/dist/migrations/20260425T161557_languages_currencies_init.js.map +1 -0
  43. package/dist/migrations/index.d.ts +27 -0
  44. package/dist/migrations/index.d.ts.map +1 -0
  45. package/dist/migrations/index.js +27 -0
  46. package/dist/migrations/index.js.map +1 -0
  47. package/docs/languages.md +88 -0
  48. package/package.json +68 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Endora sp. z o.o. and the Endora Commerce contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,50 @@
1
+ <!-- Generated by `pnpm --filter backend run manifests:generate`. Delete this line to take the file over; a README without it is never regenerated. -->
2
+
3
+ # @endora-commerce/mod-languages
4
+
5
+ Storefront/admin language catalog and per-channel locale routing.
6
+
7
+ ## What this is
8
+
9
+ An **Endora Commerce module package**. Its module id is `languages` — the identity of record in the platform's registries, in its migrations and in its setting, permission and translation codes.
10
+
11
+ A module is not imported by application code. The platform discovers the extension packages an instance has installed, reads the manifest on the root subpath and composes the module from it, so installing this package and starting the instance is the whole integration.
12
+
13
+ ## Entry points
14
+
15
+ | Import | Contents |
16
+ | --- | --- |
17
+ | `@endora-commerce/mod-languages` | the module manifest — its id, version, dependencies, settings and activation |
18
+ | `@endora-commerce/mod-languages/backend` | the composition root the platform calls, with the entities, services, routes and workers it registers |
19
+ | `@endora-commerce/mod-languages/migrations` | the module’s own schema migrations, in the order the platform runs them |
20
+
21
+ ## Depends on
22
+
23
+ Everything below is a **peer** dependency, so the application resolves exactly one copy of each — two copies of a host or of React are a runtime failure rather than a type error. An entry marked *optional* is needed only by the layers that use it.
24
+
25
+ **Endora packages**
26
+
27
+ - `@endora-commerce/contracts`
28
+ - `@endora-commerce/platform`
29
+
30
+ **Third-party**
31
+
32
+ - `@mikro-orm/core` ^6
33
+ - `@mikro-orm/migrations` ^6
34
+ - `@mikro-orm/postgresql` ^6
35
+ - `fastify` ^5
36
+
37
+ ## What the tarball carries
38
+
39
+ - `dist/` — the compiled JavaScript and its type declarations
40
+ - `docs/` — the package’s own pages in the Endora Commerce documentation site
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pnpm add @endora-commerce/mod-languages
46
+ ```
47
+
48
+ ## Licence
49
+
50
+ MIT — the text is in `LICENSE`, beside this file.
@@ -0,0 +1,23 @@
1
+ import { OptionalProps } from '@mikro-orm/core';
2
+ /**
3
+ * Language — installation-wide pool of supported BCP-47 language tags
4
+ * (T238 / FR-105). Exactly zero or one row has `isDefault=true`, enforced
5
+ * by a partial unique index in the migration.
6
+ */
7
+ export declare class Language {
8
+ [OptionalProps]?: 'createdAt' | 'updatedAt' | 'isDefault' | 'isActive' | 'sortOrder' | 'nativeLabel' | 'isRtl' | 'fallbackCode';
9
+ /** BCP-47 tag: `en-US`, `pl-PL`, `de`, …. Acts as the natural primary key. */
10
+ code: string;
11
+ label: string;
12
+ /** The language's name in itself (e.g. `Polski`, `Deutsch`). Added by feature 017. */
13
+ nativeLabel: string;
14
+ isRtl: boolean;
15
+ /** Optional fallback code for missing translations. Self-FK; cycles forbidden at the service layer. */
16
+ fallbackCode?: string | null;
17
+ isDefault: boolean;
18
+ isActive: boolean;
19
+ sortOrder: number;
20
+ createdAt: Date;
21
+ updatedAt: Date;
22
+ }
23
+ //# sourceMappingURL=language.entity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language.entity.d.ts","sourceRoot":"","sources":["../../../src/backend/entities/language.entity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,aAAa,EAAwB,MAAM,iBAAiB,CAAC;AAG9E;;;;GAIG;AACH,qBAEa,QAAQ;IACnB,CAAC,aAAa,CAAC,CAAC,EACZ,WAAW,GACX,WAAW,GACX,WAAW,GACX,UAAU,GACV,WAAW,GACX,aAAa,GACb,OAAO,GACP,cAAc,CAAC;IAEnB,8EAA8E;IAE9E,IAAI,EAAG,MAAM,CAAC;IAGd,KAAK,EAAG,MAAM,CAAC;IAEf,sFAAsF;IAEtF,WAAW,EAAE,MAAM,CAAM;IAGzB,KAAK,EAAE,OAAO,CAAS;IAEvB,uGAAuG;IAEvG,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAG7B,SAAS,EAAE,OAAO,CAAS;IAG3B,QAAQ,EAAE,OAAO,CAAQ;IAGzB,SAAS,EAAE,MAAM,CAAK;IAGtB,SAAS,EAAE,IAAI,CAAc;IAG7B,SAAS,EAAE,IAAI,CAAc;CAC9B"}
@@ -0,0 +1,78 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ import { Entity, OptionalProps, PrimaryKey, Property } from '@mikro-orm/core';
11
+ import { GlobalEntity } from '@endora-commerce/platform/tenancy';
12
+ /**
13
+ * Language — installation-wide pool of supported BCP-47 language tags
14
+ * (T238 / FR-105). Exactly zero or one row has `isDefault=true`, enforced
15
+ * by a partial unique index in the migration.
16
+ */
17
+ let Language = class Language {
18
+ [OptionalProps];
19
+ /** BCP-47 tag: `en-US`, `pl-PL`, `de`, …. Acts as the natural primary key. */
20
+ code;
21
+ label;
22
+ /** The language's name in itself (e.g. `Polski`, `Deutsch`). Added by feature 017. */
23
+ nativeLabel = '';
24
+ isRtl = false;
25
+ /** Optional fallback code for missing translations. Self-FK; cycles forbidden at the service layer. */
26
+ fallbackCode;
27
+ isDefault = false;
28
+ isActive = true;
29
+ sortOrder = 0;
30
+ createdAt = new Date();
31
+ updatedAt = new Date();
32
+ };
33
+ __decorate([
34
+ PrimaryKey({ type: 'string', length: 12 }),
35
+ __metadata("design:type", String)
36
+ ], Language.prototype, "code", void 0);
37
+ __decorate([
38
+ Property({ type: 'string', length: 64 }),
39
+ __metadata("design:type", String)
40
+ ], Language.prototype, "label", void 0);
41
+ __decorate([
42
+ Property({ type: 'string', length: 64 }),
43
+ __metadata("design:type", String)
44
+ ], Language.prototype, "nativeLabel", void 0);
45
+ __decorate([
46
+ Property({ type: 'boolean' }),
47
+ __metadata("design:type", Boolean)
48
+ ], Language.prototype, "isRtl", void 0);
49
+ __decorate([
50
+ Property({ type: 'string', length: 12, nullable: true }),
51
+ __metadata("design:type", Object)
52
+ ], Language.prototype, "fallbackCode", void 0);
53
+ __decorate([
54
+ Property({ type: 'boolean' }),
55
+ __metadata("design:type", Boolean)
56
+ ], Language.prototype, "isDefault", void 0);
57
+ __decorate([
58
+ Property({ type: 'boolean' }),
59
+ __metadata("design:type", Boolean)
60
+ ], Language.prototype, "isActive", void 0);
61
+ __decorate([
62
+ Property({ type: 'integer' }),
63
+ __metadata("design:type", Number)
64
+ ], Language.prototype, "sortOrder", void 0);
65
+ __decorate([
66
+ Property({ type: 'datetime', onCreate: () => new Date() }),
67
+ __metadata("design:type", Date)
68
+ ], Language.prototype, "createdAt", void 0);
69
+ __decorate([
70
+ Property({ type: 'datetime', onUpdate: () => new Date() }),
71
+ __metadata("design:type", Date)
72
+ ], Language.prototype, "updatedAt", void 0);
73
+ Language = __decorate([
74
+ GlobalEntity(),
75
+ Entity({ tableName: 'languages' })
76
+ ], Language);
77
+ export { Language };
78
+ //# sourceMappingURL=language.entity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language.entity.js","sourceRoot":"","sources":["../../../src/backend/entities/language.entity.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC9E,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AAEjE;;;;GAIG;AAGI,IAAM,QAAQ,GAAd,MAAM,QAAQ;IACnB,CAAC,aAAa,CAAC,CAQI;IAEnB,8EAA8E;IAE9E,IAAI,CAAU;IAGd,KAAK,CAAU;IAEf,sFAAsF;IAEtF,WAAW,GAAW,EAAE,CAAC;IAGzB,KAAK,GAAY,KAAK,CAAC;IAEvB,uGAAuG;IAEvG,YAAY,CAAiB;IAG7B,SAAS,GAAY,KAAK,CAAC;IAG3B,QAAQ,GAAY,IAAI,CAAC;IAGzB,SAAS,GAAW,CAAC,CAAC;IAGtB,SAAS,GAAS,IAAI,IAAI,EAAE,CAAC;IAG7B,SAAS,GAAS,IAAI,IAAI,EAAE,CAAC;CAC9B,CAAA;AA9BC;IADC,UAAU,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;;sCAC7B;AAGd;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;;uCAC1B;AAIf;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;;6CAChB;AAGzB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;uCACP;AAIvB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;8CAC5B;AAG7B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;2CACH;AAG3B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;0CACL;AAGzB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;;2CACR;AAGtB;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;8BAChD,IAAI;2CAAc;AAG7B;IADC,QAAQ,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;8BAChD,IAAI;2CAAc;AA1ClB,QAAQ;IAFpB,YAAY,EAAE;IACd,MAAM,CAAC,EAAE,SAAS,EAAE,WAAW,EAAE,CAAC;GACtB,QAAQ,CA2CpB"}
@@ -0,0 +1,63 @@
1
+ import type { EntityManager } from '@mikro-orm/postgresql';
2
+ import type { AuditPort } from '@endora-commerce/platform/kernel';
3
+ import { type DictionaryReferenceRegistryPort } from '@endora-commerce/contracts';
4
+ import { type ModuleContext } from '@endora-commerce/platform/kernel';
5
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
6
+ import { LanguageService } from './services/language-service.js';
7
+ import { LocaleService } from './services/locale-service.js';
8
+ import { Language } from './entities/language.entity.js';
9
+ /**
10
+ * `languages` — the stale-accept `currencies` already fixed, still live here
11
+ * (feature 072, wave 2, T105).
12
+ *
13
+ * `LanguageService`'s second constructor argument is a dictionary-cache
14
+ * invalidator, and the factory passed a hard-coded `undefined`:
15
+ *
16
+ * new LanguageService(options.emFactory, undefined, options.auditLog)
17
+ *
18
+ * So deactivating or deleting a language through this module's admin surface
19
+ * dropped nothing. `DictionaryValidator` caches existence and active-state
20
+ * in-process for 60 s (`TTL_MS`), and the Redis `DictionaryCache` holds
21
+ * dictionary reads for an hour by default — so a removed language kept
22
+ * validating for up to a minute in each process and kept being served for up to
23
+ * an hour. Meanwhile `currencies` announces its changes and both caches drop
24
+ * immediately. One validator, one platform, two behaviours.
25
+ *
26
+ * Wave 1 fixed the currency half (`f3bacb52`, "one CurrencyService, and the
27
+ * stale-accept it was caching") and left this half because `languages` was not
28
+ * yet converted. This closes it the same way, and the way matters: the module
29
+ * **announces** rather than calling the dictionary cache. Declaring that call as
30
+ * a dependency produces a real cycle — `dictionaries` reads `Language`, so
31
+ * `languages` calling into `dictionaries` would close a loop — and the cycle is
32
+ * the design saying a language must not know a dictionary cache exists. The
33
+ * root listens and invalidates, exactly as it already does for currencies.
34
+ *
35
+ * `auditLog` stops being optional while we are here: language writes are
36
+ * audited or they are not, and "not" should not be reachable by omission.
37
+ */
38
+ export interface LanguagesCradle {
39
+ readonly emFactory: () => EntityManager;
40
+ readonly auditLogService: AuditPort;
41
+ /** Narrowed the same way `currencies` narrows it: announce, do not type. */
42
+ readonly eventBus: {
43
+ emit: (event: string, payload: unknown) => void;
44
+ };
45
+ readonly requireAdmin: RequireAdminFactory;
46
+ readonly languageService: LanguageService;
47
+ readonly localeService: LocaleService;
48
+ readonly languageReferenceRegistry: DictionaryReferenceRegistryPort;
49
+ }
50
+ export declare function registerModule(ctx: ModuleContext): void;
51
+ /**
52
+ * The module's persisted entity classes, on the `./backend` subpath, as one
53
+ * array and **no named class export** (D-168).
54
+ *
55
+ * This is the shape the platform reads when the package is *installed*: the
56
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
57
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
58
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
59
+ * the package pass of `check-entity-tenant-classification`. A missing array is
60
+ * answered with `[]` — zero entities registered, no error anywhere.
61
+ */
62
+ export declare const entities: (typeof Language)[];
63
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/backend/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAGL,KAAK,+BAA+B,EAIrC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAY,KAAK,aAAa,EAAE,MAAM,kCAAkC,CAAC;AAChF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAC5E,OAAO,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAIjE,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE7D,OAAO,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAEzD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,aAAa,CAAC;IACxC,QAAQ,CAAC,eAAe,EAAE,SAAS,CAAC;IACpC,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE;QAAE,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,CAAA;KAAE,CAAC;IACvE,QAAQ,CAAC,YAAY,EAAE,mBAAmB,CAAC;IAC3C,QAAQ,CAAC,eAAe,EAAE,eAAe,CAAC;IAC1C,QAAQ,CAAC,aAAa,EAAE,aAAa,CAAC;IACtC,QAAQ,CAAC,yBAAyB,EAAE,+BAA+B,CAAC;CACrE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAmHvD;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,qBAEpB,CAAC"}
@@ -0,0 +1,114 @@
1
+ import { LANGUAGE_CHANGED_EVENT, } from '@endora-commerce/contracts';
2
+ import { lazyPort } from '@endora-commerce/platform/kernel';
3
+ import { LanguageService } from './services/language-service.js';
4
+ import { LanguageReadService, createLanguageAdminPort } from './services/language-ports.js';
5
+ import { LanguageReferenceRegistry } from './services/language-reference-registry.js';
6
+ import { LanguageSeedService } from './services/language-seed-service.js';
7
+ import { LocaleService } from './services/locale-service.js';
8
+ import { registerI18nRoutes } from './routes.js';
9
+ import { Language } from './entities/language.entity.js';
10
+ export function registerModule(ctx) {
11
+ ctx.di.providePort('languageService', ctx
12
+ .asFunction(({ emFactory, auditLogService }) => {
13
+ const announce = async () => {
14
+ ctx.cradle().eventBus.emit(LANGUAGE_CHANGED_EVENT, {});
15
+ };
16
+ return new LanguageService(emFactory, announce, auditLogService, () => ctx.cradle().languageReferenceRegistry);
17
+ })
18
+ .singleton());
19
+ // ---------------------------------------------------------------------------
20
+ // Feature 075, Phase P — the published surface.
21
+ //
22
+ // `languageService` above hands out the class; ten of the sixteen inbound
23
+ // sites do not want a service at all, they read the `Language` **entity**
24
+ // to resolve a label, validate a code or walk a fallback chain. These two
25
+ // ports are what both halves rewire to, and neither lets the entity across.
26
+ //
27
+ // The write side crosses a boundary by design rather than by accident:
28
+ // `dictionaries` hosts the admin screen for this table. The invariants —
29
+ // exactly one default, a default may not be deactivated, a fallback chain
30
+ // may not cycle — stay on this side of the port, where they already were.
31
+ // ---------------------------------------------------------------------------
32
+ ctx.di.providePort('languageReadPort', ctx
33
+ .asFunction(({ emFactory }) => new LanguageReadService(emFactory))
34
+ .singleton());
35
+ ctx.di.providePort('languageAdminPort', ctx
36
+ .asFunction(() => createLanguageAdminPort(() => ctx.cradle().languageService))
37
+ .singleton());
38
+ // ---------------------------------------------------------------------------
39
+ // Feature 077 (D-87 drain) — who still points at a language?
40
+ //
41
+ // `LanguageService.remove` used to answer that with six hand-written counts
42
+ // over `megamenu`, `blog` (twice), `cms` and the kernel's channels. Four of
43
+ // the five named another module's table in a string, which no import-level
44
+ // boundary check can see. Contributors push a descriptor from their own boot
45
+ // hook now, and this module asks the registry instead.
46
+ //
47
+ // A plain `ctx.di.register`, not a port: a contributor resolves it from a boot
48
+ // hook, and a boot hook that resolved a transient gate would stop the backend
49
+ // from starting the moment this module was switched off. The descriptor is
50
+ // inert; `languageService`, which reads and writes languages, is the port and
51
+ // does fail closed. Enumeration policy is stated at the class.
52
+ ctx.di.register({
53
+ languageReferenceRegistry: ctx
54
+ .asFunction(() => new LanguageReferenceRegistry())
55
+ .singleton(),
56
+ });
57
+ /**
58
+ * The one write `dictionaries` needs against this table: filling the empty
59
+ * `native_label` migration 038 left behind. It used to run it as raw SQL from
60
+ * its own reconciler.
61
+ */
62
+ ctx.di.providePort('languageSeedPort', ctx
63
+ .asFunction(({ emFactory }) => new LanguageSeedService(emFactory))
64
+ .singleton());
65
+ ctx.di.register({
66
+ // `LocaleService` caches the default locale for 60 s and exposes
67
+ // `invalidateDefault()`, so it has to be a singleton — and a singleton may
68
+ // not capture `languageService`, which is a transient port gate. Awilix's
69
+ // strict mode refuses that outright, and it is right to: a captured port
70
+ // keeps answering after its module is switched off. The delegate resolves
71
+ // per call instead.
72
+ localeService: ctx
73
+ .asFunction(() => new LocaleService({
74
+ getDefault: () => ctx.cradle().languageService.getDefault(),
75
+ }))
76
+ .singleton(),
77
+ });
78
+ ctx.routes(async (app) => {
79
+ const { languageService, localeService, requireAdmin } = ctx.cradle();
80
+ await registerI18nRoutes(app, {
81
+ languageService,
82
+ // Feature 075, Phase C — `GET /api/v1/i18n/config` answers with both
83
+ // catalogues, and `currencies` owns one of them. Its half arrives over
84
+ // the port `currencies` publishes rather than over its service class, so
85
+ // this module names no file of theirs, and the endpoint answers 503
86
+ // `MODULE_DISABLED` if `currencies` ever stops being present. The proxy
87
+ // resolves per call: a port captured in a singleton keeps answering after
88
+ // its owner is switched off.
89
+ //
90
+ // `currencyAdminPort` went with the four `/api/v1/admin/currencies*`
91
+ // routes on 2026-08-29 — that was this module hosting another module's
92
+ // write surface, on the catalogue's permission code, for callers that do
93
+ // not exist. What is left is one composed read.
94
+ currencyRead: lazyPort(ctx, 'currencyReadPort'),
95
+ requireAdmin,
96
+ onConfigChange: () => localeService.invalidateDefault(),
97
+ });
98
+ });
99
+ }
100
+ /**
101
+ * The module's persisted entity classes, on the `./backend` subpath, as one
102
+ * array and **no named class export** (D-168).
103
+ *
104
+ * This is the shape the platform reads when the package is *installed*: the
105
+ * boot-time loader (`src/packages/package-runtime.ts`, `exported['entities']`)
106
+ * and the static declaration reader (`scripts/lib/package-declarations.ts`),
107
+ * which is the third source of `check:module-boundary`'s `table→owner` map and
108
+ * the package pass of `check-entity-tenant-classification`. A missing array is
109
+ * answered with `[]` — zero entities registered, no error anywhere.
110
+ */
111
+ export const entities = [
112
+ Language,
113
+ ];
114
+ //# 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,28 @@
1
+ import type { FastifyInstance } from 'fastify';
2
+ import { type CurrencyReadPort } from '@endora-commerce/contracts';
3
+ import type { LanguageService } from './services/language-service.js';
4
+ import type { RequireAdminFactory } from '@endora-commerce/platform/kernel';
5
+ export interface I18nRoutesDeps {
6
+ languageService: LanguageService;
7
+ /**
8
+ * Owned by `currencies` (feature 075, Phase C), and read by exactly one route
9
+ * here: `GET /api/v1/i18n/config`, which answers with both catalogues and
10
+ * their defaults in one public payload.
11
+ *
12
+ * The four `/api/v1/admin/currencies*` routes this module also served moved to
13
+ * `currencies` on 2026-08-29, and with them the `currencyAdminPort`
14
+ * resolution. That was a write surface for another module's table, gated on
15
+ * `catalog:write`, reached by nothing in this repository — the admin currency
16
+ * screen is `dictionaries`'. What is left is a read this endpoint composes,
17
+ * which is what a public aggregate is for. When `currencies` is off the port
18
+ * fails closed and this route answers 503 `MODULE_DISABLED`: a config payload
19
+ * that guesses a currency is worse than one that refuses, and `currencies` is
20
+ * non-deactivatable in any case.
21
+ */
22
+ currencyRead: CurrencyReadPort;
23
+ requireAdmin: RequireAdminFactory;
24
+ /** Called after any mutation so cached defaults get invalidated. */
25
+ onConfigChange?: () => void;
26
+ }
27
+ export declare function registerI18nRoutes(app: FastifyInstance, deps: I18nRoutesDeps): Promise<void>;
28
+ //# sourceMappingURL=routes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAEL,KAAK,gBAAgB,EAEtB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAEtE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAE5E,MAAM,WAAW,cAAc;IAC7B,eAAe,EAAE,eAAe,CAAC;IACjC;;;;;;;;;;;;;;OAcG;IACH,YAAY,EAAE,gBAAgB,CAAC;IAC/B,YAAY,EAAE,mBAAmB,CAAC;IAClC,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,IAAI,CAAC,CAqEf"}
@@ -0,0 +1,74 @@
1
+ import { upsertLanguageRequestSchema, } from '@endora-commerce/contracts';
2
+ export async function registerI18nRoutes(app, deps) {
3
+ const { languageService, currencyRead, requireAdmin, onConfigChange } = deps;
4
+ // ---- Public (storefront + admin) i18n config ----
5
+ app.get('/api/v1/i18n/config', async () => {
6
+ const [languages, currencies, defaultLanguage, defaultCurrency] = await Promise.all([
7
+ languageService.listActive(),
8
+ currencyRead.listActive(),
9
+ languageService.getDefault(),
10
+ currencyRead.getDefault(),
11
+ ]);
12
+ return {
13
+ data: {
14
+ languages: languages.map(serializeLanguage),
15
+ currencies: currencies.map(serializeCurrency),
16
+ defaultLanguageCode: defaultLanguage?.code ?? null,
17
+ defaultCurrencyCode: defaultCurrency?.code ?? null,
18
+ },
19
+ };
20
+ });
21
+ // ---- Admin: languages ----
22
+ app.get('/api/v1/admin/languages', { preHandler: requireAdmin('catalog:write') }, async () => {
23
+ const rows = await languageService.list();
24
+ return { data: rows.map(serializeLanguage) };
25
+ });
26
+ app.put('/api/v1/admin/languages/:code', {
27
+ preHandler: requireAdmin('catalog:write'),
28
+ schema: { body: upsertLanguageRequestSchema },
29
+ }, async (request) => {
30
+ const body = upsertLanguageRequestSchema.parse(request.body);
31
+ const row = await languageService.upsert({
32
+ code: request.params.code,
33
+ label: body.label,
34
+ ...(body.isActive !== undefined ? { isActive: body.isActive } : {}),
35
+ ...(body.sortOrder !== undefined ? { sortOrder: body.sortOrder } : {}),
36
+ });
37
+ onConfigChange?.();
38
+ return { data: serializeLanguage(row) };
39
+ });
40
+ app.post('/api/v1/admin/languages/:code/default', { preHandler: requireAdmin('catalog:write') }, async (request) => {
41
+ const row = await languageService.setDefault(request.params.code);
42
+ onConfigChange?.();
43
+ return { data: serializeLanguage(row) };
44
+ });
45
+ app.delete('/api/v1/admin/languages/:code', { preHandler: requireAdmin('catalog:write') }, async (request, reply) => {
46
+ await languageService.remove(request.params.code);
47
+ onConfigChange?.();
48
+ return reply.status(204).send();
49
+ });
50
+ }
51
+ function serializeLanguage(l) {
52
+ return {
53
+ code: l.code,
54
+ label: l.label,
55
+ isDefault: l.isDefault,
56
+ isActive: l.isActive,
57
+ sortOrder: l.sortOrder,
58
+ createdAt: l.createdAt.toISOString(),
59
+ updatedAt: l.updatedAt.toISOString(),
60
+ };
61
+ }
62
+ function serializeCurrency(c) {
63
+ return {
64
+ code: c.code,
65
+ label: c.label,
66
+ symbol: c.symbol,
67
+ isDefault: c.isDefault,
68
+ isActive: c.isActive,
69
+ sortOrder: c.sortOrder,
70
+ createdAt: c.createdAt.toISOString(),
71
+ updatedAt: c.updatedAt.toISOString(),
72
+ };
73
+ }
74
+ //# sourceMappingURL=routes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../../src/backend/routes.ts"],"names":[],"mappings":"AACA,OAAO,EACL,2BAA2B,GAG5B,MAAM,4BAA4B,CAAC;AA4BpC,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,GAAoB,EACpB,IAAoB;IAEpB,MAAM,EAAE,eAAe,EAAE,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,GAAG,IAAI,CAAC;IAE7E,oDAAoD;IACpD,GAAG,CAAC,GAAG,CAAC,qBAAqB,EAAE,KAAK,IAAI,EAAE;QACxC,MAAM,CAAC,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAClF,eAAe,CAAC,UAAU,EAAE;YAC5B,YAAY,CAAC,UAAU,EAAE;YACzB,eAAe,CAAC,UAAU,EAAE;YAC5B,YAAY,CAAC,UAAU,EAAE;SAC1B,CAAC,CAAC;QACH,OAAO;YACL,IAAI,EAAE;gBACJ,SAAS,EAAE,SAAS,CAAC,GAAG,CAAC,iBAAiB,CAAC;gBAC3C,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,iBAAiB,CAAC;gBAC7C,mBAAmB,EAAE,eAAe,EAAE,IAAI,IAAI,IAAI;gBAClD,mBAAmB,EAAE,eAAe,EAAE,IAAI,IAAI,IAAI;aACnD;SACF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,6BAA6B;IAC7B,GAAG,CAAC,GAAG,CACL,yBAAyB,EACzB,EAAE,UAAU,EAAE,YAAY,CAAC,eAAe,CAAC,EAAE,EAC7C,KAAK,IAAI,EAAE;QACT,MAAM,IAAI,GAAG,MAAM,eAAe,CAAC,IAAI,EAAE,CAAC;QAC1C,OAAO,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,iBAAiB,CAAC,EAAE,CAAC;IAC/C,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,GAAG,CACL,+BAA+B,EAC/B;QACE,UAAU,EAAE,YAAY,CAAC,eAAe,CAAC;QACzC,MAAM,EAAE,EAAE,IAAI,EAAE,2BAA2B,EAAE;KAC9C,EACD,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,IAAI,GAAG,2BAA2B,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAC7D,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC;YACvC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,IAAI;YACzB,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,GAAG,CAAC,IAAI,CAAC,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACnE,GAAG,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACvE,CAAC,CAAC;QACH,cAAc,EAAE,EAAE,CAAC;QACnB,OAAO,EAAE,IAAI,EAAE,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;IAC1C,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,IAAI,CACN,uCAAuC,EACvC,EAAE,UAAU,EAAE,YAAY,CAAC,eAAe,CAAC,EAAE,EAC7C,KAAK,EAAE,OAAO,EAAE,EAAE;QAChB,MAAM,GAAG,GAAG,MAAM,eAAe,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAClE,cAAc,EAAE,EAAE,CAAC;QACnB,OAAO,EAAE,IAAI,EAAE,iBAAiB,CAAC,GAAG,CAAC,EAAE,CAAC;IAC1C,CAAC,CACF,CAAC;IAEF,GAAG,CAAC,MAAM,CACR,+BAA+B,EAC/B,EAAE,UAAU,EAAE,YAAY,CAAC,eAAe,CAAC,EAAE,EAC7C,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;QACvB,MAAM,eAAe,CAAC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAClD,cAAc,EAAE,EAAE,CAAC;QACnB,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,CAAC,CACF,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,CAAW;IACpC,OAAO;QACL,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE;QACpC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE;KACrC,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,CAAiB;IAC1C,OAAO;QACL,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,KAAK,EAAE,CAAC,CAAC,KAAK;QACd,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,SAAS,EAAE,CAAC,CAAC,SAAS;QACtB,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE;QACpC,SAAS,EAAE,CAAC,CAAC,SAAS,CAAC,WAAW,EAAE;KACrC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,33 @@
1
+ import type { LanguageAdminPort, LanguageReadPort, LanguageRecord } from '@endora-commerce/contracts';
2
+ import type { EntityManager } from '@mikro-orm/postgresql';
3
+ import { Language } from '../entities/language.entity.js';
4
+ import type { LanguageService } from './language-service.js';
5
+ /**
6
+ * The published face of `languages` (feature 075, Phase P).
7
+ *
8
+ * Sixteen inbound sites, and they split the way the module does: ten are
9
+ * `dictionaries` reading the `Language` **entity** to resolve a label,
10
+ * validate a code or walk a fallback chain, and six type themselves against
11
+ * `LanguageService` to call `listActive`.
12
+ *
13
+ * The read port answers both, and neither hands the entity over. `findByCode`
14
+ * is the one method with no direct predecessor: `dictionaries` looks a
15
+ * language up by code in four places with `em.findOne(Language, { code })`,
16
+ * which is the read this replaces.
17
+ */
18
+ export declare class LanguageReadService implements LanguageReadPort {
19
+ private readonly emFactory;
20
+ constructor(emFactory: () => EntityManager);
21
+ list(): Promise<LanguageRecord[]>;
22
+ listActive(): Promise<LanguageRecord[]>;
23
+ getDefault(): Promise<LanguageRecord | null>;
24
+ findByCode(code: string): Promise<LanguageRecord | null>;
25
+ }
26
+ /**
27
+ * The write side, adapting `LanguageService`. The service arrives as a getter
28
+ * so the adapter resolves it per call rather than capturing this module's own
29
+ * gated port into a singleton.
30
+ */
31
+ export declare function createLanguageAdminPort(getService: () => LanguageService): LanguageAdminPort;
32
+ export declare function toLanguageRecord(language: Language): LanguageRecord;
33
+ //# sourceMappingURL=language-ports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language-ports.d.ts","sourceRoot":"","sources":["../../../src/backend/services/language-ports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACf,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAE7D;;;;;;;;;;;;GAYG;AACH,qBAAa,mBAAoB,YAAW,gBAAgB;IAC9C,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAAT,SAAS,EAAE,MAAM,aAAa;IAErD,IAAI,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IASjC,UAAU,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IASvC,UAAU,IAAI,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAK5C,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;CAI/D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,eAAe,GAAG,iBAAiB,CAa5F;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,GAAG,cAAc,CAanE"}
@@ -0,0 +1,70 @@
1
+ import { Language } from '../entities/language.entity.js';
2
+ /**
3
+ * The published face of `languages` (feature 075, Phase P).
4
+ *
5
+ * Sixteen inbound sites, and they split the way the module does: ten are
6
+ * `dictionaries` reading the `Language` **entity** to resolve a label,
7
+ * validate a code or walk a fallback chain, and six type themselves against
8
+ * `LanguageService` to call `listActive`.
9
+ *
10
+ * The read port answers both, and neither hands the entity over. `findByCode`
11
+ * is the one method with no direct predecessor: `dictionaries` looks a
12
+ * language up by code in four places with `em.findOne(Language, { code })`,
13
+ * which is the read this replaces.
14
+ */
15
+ export class LanguageReadService {
16
+ emFactory;
17
+ constructor(emFactory) {
18
+ this.emFactory = emFactory;
19
+ }
20
+ async list() {
21
+ const rows = await this.emFactory().find(Language, {}, { orderBy: { sortOrder: 'asc', code: 'asc' } });
22
+ return rows.map(toLanguageRecord);
23
+ }
24
+ async listActive() {
25
+ const rows = await this.emFactory().find(Language, { isActive: true }, { orderBy: { sortOrder: 'asc', code: 'asc' } });
26
+ return rows.map(toLanguageRecord);
27
+ }
28
+ async getDefault() {
29
+ const row = await this.emFactory().findOne(Language, { isDefault: true });
30
+ return row ? toLanguageRecord(row) : null;
31
+ }
32
+ async findByCode(code) {
33
+ const row = await this.emFactory().findOne(Language, { code });
34
+ return row ? toLanguageRecord(row) : null;
35
+ }
36
+ }
37
+ /**
38
+ * The write side, adapting `LanguageService`. The service arrives as a getter
39
+ * so the adapter resolves it per call rather than capturing this module's own
40
+ * gated port into a singleton.
41
+ */
42
+ export function createLanguageAdminPort(getService) {
43
+ return {
44
+ async create(input) {
45
+ return toLanguageRecord(await getService().create(input));
46
+ },
47
+ async update(code, input) {
48
+ return toLanguageRecord(await getService().update(code, input));
49
+ },
50
+ async setDefault(code) {
51
+ return toLanguageRecord(await getService().setDefault(code));
52
+ },
53
+ remove: (code) => getService().remove(code),
54
+ };
55
+ }
56
+ export function toLanguageRecord(language) {
57
+ return {
58
+ code: language.code,
59
+ label: language.label,
60
+ nativeLabel: language.nativeLabel,
61
+ isRtl: language.isRtl,
62
+ fallbackCode: language.fallbackCode ?? null,
63
+ isDefault: language.isDefault,
64
+ isActive: language.isActive,
65
+ sortOrder: language.sortOrder,
66
+ createdAt: language.createdAt,
67
+ updatedAt: language.updatedAt,
68
+ };
69
+ }
70
+ //# sourceMappingURL=language-ports.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language-ports.js","sourceRoot":"","sources":["../../../src/backend/services/language-ports.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAC;AAG1D;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,mBAAmB;IACD;IAA7B,YAA6B,SAA8B;QAA9B,cAAS,GAAT,SAAS,CAAqB;IAAG,CAAC;IAE/D,KAAK,CAAC,IAAI;QACR,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,CACtC,QAAQ,EACR,EAAE,EACF,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAC/C,CAAC;QACF,OAAO,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,UAAU;QACd,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,IAAI,CACtC,QAAQ,EACR,EAAE,QAAQ,EAAE,IAAI,EAAE,EAClB,EAAE,OAAO,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,CAC/C,CAAC;QACF,OAAO,IAAI,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;IACpC,CAAC;IAED,KAAK,CAAC,UAAU;QACd,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAC1E,OAAO,GAAG,CAAC,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,IAAY;QAC3B,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;QAC/D,OAAO,GAAG,CAAC,CAAC,CAAC,gBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5C,CAAC;CACF;AAED;;;;GAIG;AACH,MAAM,UAAU,uBAAuB,CAAC,UAAiC;IACvE,OAAO;QACL,KAAK,CAAC,MAAM,CAAC,KAAK;YAChB,OAAO,gBAAgB,CAAC,MAAM,UAAU,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5D,CAAC;QACD,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK;YACtB,OAAO,gBAAgB,CAAC,MAAM,UAAU,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;QAClE,CAAC;QACD,KAAK,CAAC,UAAU,CAAC,IAAI;YACnB,OAAO,gBAAgB,CAAC,MAAM,UAAU,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QAC/D,CAAC;QACD,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC;KAC5C,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,QAAkB;IACjD,OAAO;QACL,IAAI,EAAE,QAAQ,CAAC,IAAI;QACnB,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,WAAW,EAAE,QAAQ,CAAC,WAAW;QACjC,KAAK,EAAE,QAAQ,CAAC,KAAK;QACrB,YAAY,EAAE,QAAQ,CAAC,YAAY,IAAI,IAAI;QAC3C,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,SAAS,EAAE,QAAQ,CAAC,SAAS;KAC9B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,30 @@
1
+ import type { DictionaryReference, DictionaryReferenceDescriptor, DictionaryReferenceRegistryPort } from '@endora-commerce/contracts';
2
+ /**
3
+ * In-process registry of "who still points at this language code" descriptors
4
+ * (feature 077, D-87 drain).
5
+ *
6
+ * Registered under `languageReferenceRegistry` and contributed to from other modules'
7
+ * boot hooks. This module consults it before deleting a language, and
8
+ * `dictionaries` consults it to build the orphan report.
9
+ *
10
+ * **Enumeration policy: honoured while the contributing module is absent.**
11
+ * D-39's default is to skip, and honouring needs a written reason: this is
12
+ * referential integrity, not a surface. A switched-off module still owns the
13
+ * rows that carry the code, so skipping its descriptor would let an operator
14
+ * delete a language that comes back as a dangling reference the moment the module
15
+ * is switched on again — data loss caused by an action Constitution XVII
16
+ * promises is non-destructive and reversible.
17
+ *
18
+ * `ownerModuleId` is recorded on every descriptor even though this registry
19
+ * does not filter on it, because the alternative is a registry that could not
20
+ * express the decision either way, and because it attributes a 409 to a module.
21
+ */
22
+ export declare class LanguageReferenceRegistry implements DictionaryReferenceRegistryPort {
23
+ private readonly descriptors;
24
+ register(descriptor: DictionaryReferenceDescriptor): void;
25
+ /** The contributing module of every registered descriptor, in registration order. */
26
+ owners(): readonly string[];
27
+ countReferences(code: string): Promise<DictionaryReference[]>;
28
+ usedCodes(): Promise<DictionaryReference[]>;
29
+ }
30
+ //# sourceMappingURL=language-reference-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language-reference-registry.d.ts","sourceRoot":"","sources":["../../../src/backend/services/language-reference-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,6BAA6B,EAC7B,+BAA+B,EAChC,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,yBAA0B,YAAW,+BAA+B;IAC/E,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAuC;IAEnE,QAAQ,CAAC,UAAU,EAAE,6BAA6B,GAAG,IAAI;IAMzD,qFAAqF;IACrF,MAAM,IAAI,SAAS,MAAM,EAAE;IAIrB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAS7D,SAAS,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;CAQlD"}