@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.
- package/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/backend/entities/language.entity.d.ts +23 -0
- package/dist/backend/entities/language.entity.d.ts.map +1 -0
- package/dist/backend/entities/language.entity.js +78 -0
- package/dist/backend/entities/language.entity.js.map +1 -0
- package/dist/backend/index.d.ts +63 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +114 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.d.ts +28 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +74 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/language-ports.d.ts +33 -0
- package/dist/backend/services/language-ports.d.ts.map +1 -0
- package/dist/backend/services/language-ports.js +70 -0
- package/dist/backend/services/language-ports.js.map +1 -0
- package/dist/backend/services/language-reference-registry.d.ts +30 -0
- package/dist/backend/services/language-reference-registry.d.ts.map +1 -0
- package/dist/backend/services/language-reference-registry.js +54 -0
- package/dist/backend/services/language-reference-registry.js.map +1 -0
- package/dist/backend/services/language-seed-service.d.ts +24 -0
- package/dist/backend/services/language-seed-service.d.ts.map +1 -0
- package/dist/backend/services/language-seed-service.js +40 -0
- package/dist/backend/services/language-seed-service.js.map +1 -0
- package/dist/backend/services/language-service.d.ts +99 -0
- package/dist/backend/services/language-service.d.ts.map +1 -0
- package/dist/backend/services/language-service.js +299 -0
- package/dist/backend/services/language-service.js.map +1 -0
- package/dist/backend/services/locale-service.d.ts +35 -0
- package/dist/backend/services/locale-service.d.ts.map +1 -0
- package/dist/backend/services/locale-service.js +88 -0
- package/dist/backend/services/locale-service.js.map +1 -0
- package/dist/manifest.d.ts +169 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +40 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260425T161557_languages_currencies_init.d.ts +13 -0
- package/dist/migrations/20260425T161557_languages_currencies_init.d.ts.map +1 -0
- package/dist/migrations/20260425T161557_languages_currencies_init.js +60 -0
- package/dist/migrations/20260425T161557_languages_currencies_init.js.map +1 -0
- package/dist/migrations/index.d.ts +27 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +27 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/languages.md +88 -0
- 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":"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"}
|
|
@@ -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"}
|