@orthacms/i18n-server 0.4.3 → 0.5.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.
@@ -1,5 +1,5 @@
1
1
  import { type SQL } from 'drizzle-orm';
2
- import { type Database } from '@orthacms/database';
2
+ import { UnitOfWork, type Database } from '@orthacms/database';
3
3
  import { EntryValidationService, RelationLinkService, type AnyContentType, type ContentEntryExtension, type EntryFilterExtension, type EntryScopeParams, type EntryTransaction, type EntryWriteContext, type EntryWriteFanout } from '@orthacms/content-server';
4
4
  import { LocaleRegistryService } from '../../locales/services/locale-registry.service';
5
5
  /**
@@ -30,7 +30,8 @@ export declare class EntryLocaleExtensionService implements ContentEntryExtensio
30
30
  private readonly locales;
31
31
  private readonly validation;
32
32
  private readonly relations;
33
- constructor(db: Database, locales: LocaleRegistryService, validation: EntryValidationService, relations: RelationLinkService);
33
+ private readonly uow;
34
+ constructor(db: Database, locales: LocaleRegistryService, validation: EntryValidationService, relations: RelationLinkService, uow: UnitOfWork);
34
35
  /** @inheritdoc */
35
36
  listScope(type: AnyContentType, workspaceId: string, params: EntryScopeParams): SQL | undefined;
36
37
  /**
@@ -45,9 +46,11 @@ export declare class EntryLocaleExtensionService implements ContentEntryExtensio
45
46
  createColumns(type: AnyContentType, workspaceId: string, params: EntryScopeParams): Promise<Record<string, unknown>>;
46
47
  /**
47
48
  * Assert a translation group has ≥1 live row in this workspace, so a
48
- * sibling attaches to a real group. Runs before the create transaction —
49
- * the (benign) TOCTOU window is covered by the row staying valid and the
50
- * `(locale_group_id, locale)` unique index still guarding duplicates.
49
+ * sibling attaches to a real group. Runs before the create's own
50
+ * transaction — the (benign) TOCTOU window is covered by the row staying
51
+ * valid and the `(locale_group_id, locale)` unique index still guarding
52
+ * duplicates — but *inside* any enclosing unit of work, which is why it
53
+ * reads through {@link UnitOfWork.current} rather than the base pool.
51
54
  */
52
55
  private assertGroupExists;
53
56
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"entry-locale-extension.service.d.ts","sourceRoot":"","sources":["../../../../src/lib/content/services/entry-locale-extension.service.ts"],"names":[],"mappings":"AAMA,OAAO,EAYH,KAAK,GAAG,EACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EACH,sBAAsB,EACtB,mBAAmB,EAMnB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAE1B,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACxB,MAAM,0BAA0B,CAAC;AASlC,OAAO,EAAE,qBAAqB,EAAE,MAAM,gDAAgD,CAAC;AAuCvF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBACa,2BAA4B,YAAW,qBAAqB;IAE/C,OAAO,CAAC,QAAQ,CAAC,EAAE;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAK3B,OAAO,CAAC,QAAQ,CAAC,SAAS;gBAPS,EAAE,EAAE,QAAQ,EAC9B,OAAO,EAAE,qBAAqB,EAC9B,UAAU,EAAE,sBAAsB,EAKlC,SAAS,EAAE,mBAAmB;IAGnD,kBAAkB;IAClB,SAAS,CACL,IAAI,EAAE,cAAc,EACpB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,gBAAgB,GACzB,GAAG,GAAG,SAAS;IAmClB;;;;;;;;OAQG;IACG,aAAa,CACf,IAAI,EAAE,cAAc,EACpB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAgBnC;;;;;OAKG;YACW,iBAAiB;IAwB/B;;;;;;;;;;;;OAYG;IACG,WAAW,CACb,EAAE,EAAE,gBAAgB,EACpB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC;IAKhB,kBAAkB;IACZ,WAAW,CACb,EAAE,EAAE,gBAAgB,EACpB,IAAI,EAAE,cAAc,EACpB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,iBAAiB,GAC3B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAmBrC,kBAAkB;IACZ,cAAc,CAChB,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAkDxC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,iBAAiB;IAczB;;;;;;;;;;;;;OAaG;YACW,mBAAmB;IAiMjC;;;;;;;;;;;;;;;;;;;;;OAqBG;YACW,yBAAyB;IAmEvC;;;;;;;;;OASG;YACW,uBAAuB;IA0CrC;;;;;;;;;;OAUG;YACW,gBAAgB;IA6C9B,kBAAkB;IAClB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,oBAAoB,GAAG,SAAS;IAcvE;;;;;;;;;;;;;;;;;;;;OAoBG;YACW,iBAAiB;IAmE/B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,aAAa;CAoBxB"}
1
+ {"version":3,"file":"entry-locale-extension.service.d.ts","sourceRoot":"","sources":["../../../../src/lib/content/services/entry-locale-extension.service.ts"],"names":[],"mappings":"AAMA,OAAO,EAYH,KAAK,GAAG,EACX,MAAM,aAAa,CAAC;AACrB,OAAO,EAAkB,UAAU,EAAE,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC/E,OAAO,EACH,sBAAsB,EACtB,mBAAmB,EAMnB,KAAK,cAAc,EACnB,KAAK,qBAAqB,EAE1B,KAAK,oBAAoB,EACzB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACxB,MAAM,0BAA0B,CAAC;AASlC,OAAO,EAAE,qBAAqB,EAAE,MAAM,gDAAgD,CAAC;AAuCvF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBACa,2BAA4B,YAAW,qBAAqB;IAE/C,OAAO,CAAC,QAAQ,CAAC,EAAE;IACrC,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAK3B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,GAAG;gBARe,EAAE,EAAE,QAAQ,EAC9B,OAAO,EAAE,qBAAqB,EAC9B,UAAU,EAAE,sBAAsB,EAKlC,SAAS,EAAE,mBAAmB,EAC9B,GAAG,EAAE,UAAU;IAGpC,kBAAkB;IAClB,SAAS,CACL,IAAI,EAAE,cAAc,EACpB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,gBAAgB,GACzB,GAAG,GAAG,SAAS;IAmClB;;;;;;;;OAQG;IACG,aAAa,CACf,IAAI,EAAE,cAAc,EACpB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,gBAAgB,GACzB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAgBnC;;;;;;;OAOG;YACW,iBAAiB;IAmC/B;;;;;;;;;;;;OAYG;IACG,WAAW,CACb,EAAE,EAAE,gBAAgB,EACpB,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,gBAAgB,GACzB,OAAO,CAAC,IAAI,CAAC;IAKhB,kBAAkB;IACZ,WAAW,CACb,EAAE,EAAE,gBAAgB,EACpB,IAAI,EAAE,cAAc,EACpB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,iBAAiB,GAC3B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAmBrC,kBAAkB;IACZ,cAAc,CAChB,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAkDxC;;;;;;;;;;;;;;OAcG;IACH,OAAO,CAAC,iBAAiB;IAczB;;;;;;;;;;;;;OAaG;YACW,mBAAmB;IAiMjC;;;;;;;;;;;;;;;;;;;;;OAqBG;YACW,yBAAyB;IAmEvC;;;;;;;;;OASG;YACW,uBAAuB;IA0CrC;;;;;;;;;;OAUG;YACW,gBAAgB;IA6C9B,kBAAkB;IAClB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,oBAAoB,GAAG,SAAS;IAcvE;;;;;;;;;;;;;;;;;;;;OAoBG;YACW,iBAAiB;IAmE/B;;;;;;;;;;;;;OAaG;IACH,OAAO,CAAC,aAAa;CAoBxB"}
@@ -70,16 +70,18 @@ let EntryLocaleExtensionService = class EntryLocaleExtensionService {
70
70
  locales;
71
71
  validation;
72
72
  relations;
73
+ uow;
73
74
  constructor(db, locales, validation,
74
75
  // Relation storage is content's, not ours: reading a link set and
75
76
  // replacing one are the same operations the entries pipeline performs,
76
77
  // and a second implementation here is how the join table's ordering
77
78
  // and de-duplication rules would drift.
78
- relations) {
79
+ relations, uow) {
79
80
  this.db = db;
80
81
  this.locales = locales;
81
82
  this.validation = validation;
82
83
  this.relations = relations;
84
+ this.uow = uow;
83
85
  }
84
86
  /** @inheritdoc */
85
87
  listScope(type, workspaceId, params) {
@@ -127,13 +129,26 @@ let EntryLocaleExtensionService = class EntryLocaleExtensionService {
127
129
  }
128
130
  /**
129
131
  * Assert a translation group has ≥1 live row in this workspace, so a
130
- * sibling attaches to a real group. Runs before the create transaction —
131
- * the (benign) TOCTOU window is covered by the row staying valid and the
132
- * `(locale_group_id, locale)` unique index still guarding duplicates.
132
+ * sibling attaches to a real group. Runs before the create's own
133
+ * transaction — the (benign) TOCTOU window is covered by the row staying
134
+ * valid and the `(locale_group_id, locale)` unique index still guarding
135
+ * duplicates — but *inside* any enclosing unit of work, which is why it
136
+ * reads through {@link UnitOfWork.current} rather than the base pool.
133
137
  */
134
138
  async assertGroupExists(type, localeGroupId, workspaceId) {
135
139
  const table = type.table;
136
- const [row] = await this.db
140
+ // `uow.current()`, not `this.db` — the same distinction
141
+ // `EntryWriterService` draws for its relation-target probe. Outside a
142
+ // unit of work the two are one connection and an ordinary save is
143
+ // unaffected. Inside one they are not, and the difference decides
144
+ // whether a *group* can be built in a single transaction: a content
145
+ // import writes the `en` row and then its `de` sibling in one unit of
146
+ // work, and a probe on the base pool cannot see the uncommitted `en`
147
+ // row — so the sibling was refused with "no translation group" for a
148
+ // group created moments earlier in the same transaction, and an
149
+ // imported locale pair arrived as one row plus an error.
150
+ const [row] = await this.uow
151
+ .current()
137
152
  .select({ one: (0, drizzle_orm_1.sql) `1` })
138
153
  .from(type.table)
139
154
  .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(table['localeGroupId'], localeGroupId), (0, drizzle_orm_1.eq)(table['workspaceId'], workspaceId), ...(type.paranoid ? [(0, drizzle_orm_1.isNull)(table['deletedAt'])] : [])))
@@ -658,5 +673,6 @@ exports.EntryLocaleExtensionService = EntryLocaleExtensionService = tslib_1.__de
658
673
  tslib_1.__param(0, (0, database_1.InjectDatabase)()),
659
674
  tslib_1.__metadata("design:paramtypes", [Object, locale_registry_service_1.LocaleRegistryService,
660
675
  content_server_1.EntryValidationService,
661
- content_server_1.RelationLinkService])
676
+ content_server_1.RelationLinkService,
677
+ database_1.UnitOfWork])
662
678
  ], EntryLocaleExtensionService);
@@ -1 +1 @@
1
- {"version":3,"file":"translation-proposal.applier.d.ts","sourceRoot":"","sources":["../../../src/lib/copilot/translation-proposal.applier.ts"],"names":[],"mappings":"AAEA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EACH,kBAAkB,EAElB,oBAAoB,EAIpB,KAAK,mBAAmB,EAC3B,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EACR,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,cAAc,EACjB,MAAM,0BAA0B,CAAC;AAMlC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,qBACa,0BAA2B,YAAW,eAAe;IAK1D,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACL,OAAO,CAAC,QAAQ,CAAC,EAAE;IAPzC,QAAQ,CAAC,IAAI,yBAAyC;gBAIjC,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE,kBAAkB,EAC1B,MAAM,EAAE,oBAAoB,EACV,EAAE,EAAE,QAAQ;IAG7C,KAAK,CACP,KAAK,EAAE;QAAE,MAAM,EAAE,cAAc,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,EACjE,KAAK,EAAE,aAAa,GACrB,OAAO,CAAC,mBAAmB,CAAC;CAkDlC;AA0DD;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,8BAA+B,YAAW,eAAe;IAK9D,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACL,OAAO,CAAC,QAAQ,CAAC,EAAE;IAPzC,QAAQ,CAAC,IAAI,8BAAuC;gBAI/B,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE,kBAAkB,EAC1B,MAAM,EAAE,oBAAoB,EACV,EAAE,EAAE,QAAQ;IAG7C,KAAK,CACP,KAAK,EAAE;QAAE,MAAM,EAAE,cAAc,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,EACjE,KAAK,EAAE,aAAa,GACrB,OAAO,CAAC,mBAAmB,CAAC;CA8FlC"}
1
+ {"version":3,"file":"translation-proposal.applier.d.ts","sourceRoot":"","sources":["../../../src/lib/copilot/translation-proposal.applier.ts"],"names":[],"mappings":"AAGA,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnE,OAAO,EACH,kBAAkB,EAElB,oBAAoB,EAIpB,KAAK,mBAAmB,EAC3B,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EACR,aAAa,EACb,eAAe,EACf,mBAAmB,EACnB,cAAc,EACjB,MAAM,0BAA0B,CAAC;AAMlC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,qBACa,0BAA2B,YAAW,eAAe;IAK1D,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACL,OAAO,CAAC,QAAQ,CAAC,EAAE;IAPzC,QAAQ,CAAC,IAAI,yBAAyC;gBAIjC,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE,kBAAkB,EAC1B,MAAM,EAAE,oBAAoB,EACV,EAAE,EAAE,QAAQ;IAG7C,KAAK,CACP,KAAK,EAAE;QAAE,MAAM,EAAE,cAAc,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,EACjE,KAAK,EAAE,aAAa,GACrB,OAAO,CAAC,mBAAmB,CAAC;CAkDlC;AA0DD;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,8BAA+B,YAAW,eAAe;IAK9D,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACL,OAAO,CAAC,QAAQ,CAAC,EAAE;IAPzC,QAAQ,CAAC,IAAI,8BAAuC;gBAI/B,QAAQ,EAAE,mBAAmB,EAC7B,MAAM,EAAE,kBAAkB,EAC1B,MAAM,EAAE,oBAAoB,EACV,EAAE,EAAE,QAAQ;IAG7C,KAAK,CACP,KAAK,EAAE;QAAE,MAAM,EAAE,cAAc,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,EACjE,KAAK,EAAE,aAAa,GACrB,OAAO,CAAC,mBAAmB,CAAC;CA8FlC"}
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BulkTranslationProposalApplier = exports.TranslationProposalApplier = void 0;
4
4
  const tslib_1 = require("tslib");
5
+ const copilot_server_1 = require("@orthacms/copilot-server");
5
6
  const common_1 = require("@nestjs/common");
6
7
  const drizzle_orm_1 = require("drizzle-orm");
7
8
  const database_1 = require("@orthacms/database");
@@ -80,7 +81,7 @@ let TranslationProposalApplier = class TranslationProposalApplier {
80
81
  }
81
82
  const translated = (input.patch['values'] ?? {});
82
83
  const inherited = await sharedValues(this.db, type, sourceId, actor.workspaceId);
83
- const entry = await this.writer.create(type, { ...inherited, ...translated }, actor.workspaceId, undefined, locale, localeGroupId, { id: actor.userId, email: actor.actorEmail });
84
+ const entry = await this.writer.create(type, { ...inherited, ...translated }, actor.workspaceId, undefined, locale, localeGroupId, (0, copilot_server_1.proposalEventActor)(actor));
84
85
  return {
85
86
  entityId: entry.id,
86
87
  detail: { typeName: type.name, locale, localeGroupId }
@@ -207,7 +208,7 @@ let BulkTranslationProposalApplier = class BulkTranslationProposalApplier {
207
208
  // a stale copy of itself.
208
209
  const inherited = await sharedValues(this.db, type, sourceId, actor.workspaceId);
209
210
  const translated = (item.values ?? {});
210
- const entry = await this.writer.create(type, { ...inherited, ...translated }, actor.workspaceId, undefined, locale, localeGroupId, { id: actor.userId, email: actor.actorEmail });
211
+ const entry = await this.writer.create(type, { ...inherited, ...translated }, actor.workspaceId, undefined, locale, localeGroupId, (0, copilot_server_1.proposalEventActor)(actor));
211
212
  created.push(entry.id);
212
213
  locales.push(locale);
213
214
  }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The i18n plugin's pass over the host's OpenAPI document.
3
+ *
4
+ * Three read routes, all of them answering a hand-written `interface` that
5
+ * `@nestjs/swagger` cannot see (`packages/bootstrap/server/AGENTS.md` → "The
6
+ * response-schema gap"). This is the mechanism content-server already uses:
7
+ * plain schema objects written onto the finished document, no decorator and no
8
+ * runtime-referenceable class.
9
+ *
10
+ * Pure: it takes the document and the configured locale slugs, and touches only
11
+ * the paths this plugin owns.
12
+ */
13
+ import type { OpenApiDocument } from '@orthacms/bootstrap-server';
14
+ /**
15
+ * Adds this plugin's schemas to `document` and attaches them to its own three
16
+ * operations.
17
+ */
18
+ export declare function describeI18nApi(document: OpenApiDocument, localeSlugs: readonly string[]): void;
19
+ //# sourceMappingURL=describe-i18n-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe-i18n-api.d.ts","sourceRoot":"","sources":["../../../src/lib/docs/describe-i18n-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AA2HlE;;;GAGG;AACH,wBAAgB,eAAe,CAC3B,QAAQ,EAAE,eAAe,EACzB,WAAW,EAAE,SAAS,MAAM,EAAE,GAC/B,IAAI,CAgBN"}
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ /**
3
+ * The i18n plugin's pass over the host's OpenAPI document.
4
+ *
5
+ * Three read routes, all of them answering a hand-written `interface` that
6
+ * `@nestjs/swagger` cannot see (`packages/bootstrap/server/AGENTS.md` → "The
7
+ * response-schema gap"). This is the mechanism content-server already uses:
8
+ * plain schema objects written onto the finished document, no decorator and no
9
+ * runtime-referenceable class.
10
+ *
11
+ * Pure: it takes the document and the configured locale slugs, and touches only
12
+ * the paths this plugin owns.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.describeI18nApi = describeI18nApi;
16
+ const i18n_schemas_1 = require("./i18n-schemas");
17
+ /**
18
+ * The locales route, matched on its own full suffix.
19
+ *
20
+ * `/i18n/locales` and not a looser `/i18n` prefix: the insights plugin serves
21
+ * `/api/insights/i18n/coverage`, which a pattern anchored on the bare segment
22
+ * would also match. It would fall out of the route table below and describe
23
+ * nothing — but only by luck, and the next route either plugin adds is where
24
+ * luck runs out.
25
+ */
26
+ const LOCALES_ROUTE_RE = /\/i18n\/locales$/;
27
+ /** Matches `<prefix>/i18n/content/{typeName}<rest>`. */
28
+ const CONTENT_ROUTE_RE = /\/i18n\/content\/\{typeName\}(.*)$/;
29
+ /** Content-scoped routes, keyed by what follows `/i18n/content/{typeName}`. */
30
+ const CONTENT_ROUTES = {
31
+ '/{id}/locales': {
32
+ get: {
33
+ schema: 'I18nEntryLocalesView',
34
+ description: 'The entry’s translation group, one slot per configured locale.'
35
+ }
36
+ },
37
+ '/locale-summary': {
38
+ post: {
39
+ schema: 'I18nLocaleSummaryView',
40
+ description: 'The requested groups and their live members. A read, so it answers 200 rather than a 201 claiming it created something.'
41
+ }
42
+ }
43
+ };
44
+ /** The locales route's one operation. */
45
+ const LOCALES_OPERATION = {
46
+ schema: 'I18nLocalesView',
47
+ description: 'The configured locales, in display order.'
48
+ };
49
+ /**
50
+ * Writes a success schema onto whichever 2xx key the scanner already emitted,
51
+ * so this never invents a status code the API does not return. `POST
52
+ * /locale-summary` carries `@HttpCode(200)`, which is why the key is read
53
+ * rather than assumed.
54
+ */
55
+ function setSuccessResponse(operation, schema, description) {
56
+ const responses = operation.responses ?? {};
57
+ const key = Object.keys(responses).find((code) => /^2\d\d$/.test(code));
58
+ if (!key || key === '204') {
59
+ return;
60
+ }
61
+ responses[key] = {
62
+ description,
63
+ content: { 'application/json': { schema } }
64
+ };
65
+ operation.responses = responses;
66
+ }
67
+ /** Adds a documented failure response, leaving any existing one alone. */
68
+ function addErrorResponse(operation, code, description) {
69
+ const responses = operation.responses ?? {};
70
+ if (responses[code]) {
71
+ return;
72
+ }
73
+ responses[code] = { description };
74
+ operation.responses = responses;
75
+ }
76
+ /** Applies one route table entry to a path item. */
77
+ function describeItem(item, byMethod, contentScoped) {
78
+ for (const [method, operation] of Object.entries(item)) {
79
+ const spec = byMethod[method];
80
+ if (!spec || !operation || typeof operation !== 'object') {
81
+ continue;
82
+ }
83
+ const typed = operation;
84
+ setSuccessResponse(typed, (0, i18n_schemas_1.ref)(spec.schema), spec.description);
85
+ if (contentScoped) {
86
+ // Both content routes resolve `:typeName` through the same
87
+ // `resolveI18nType`, which draws exactly this line: a name nobody
88
+ // registered is a missing resource, a registered type that is not
89
+ // localized is a caller bug.
90
+ addErrorResponse(typed, '400', 'The content type exists but is not localized (`i18n: false`), so it has no translation groups.');
91
+ addErrorResponse(typed, '404', 'Unknown content type — or, for the per-entry panel, no live entry with this id in the open workspace.');
92
+ }
93
+ }
94
+ }
95
+ /**
96
+ * Adds this plugin's schemas to `document` and attaches them to its own three
97
+ * operations.
98
+ */
99
+ function describeI18nApi(document, localeSlugs) {
100
+ document.components ??= {};
101
+ document.components.schemas ??= {};
102
+ Object.assign(document.components.schemas, (0, i18n_schemas_1.buildI18nSchemas)(localeSlugs));
103
+ for (const [route, item] of Object.entries(document.paths)) {
104
+ if (LOCALES_ROUTE_RE.test(route)) {
105
+ describeItem(item, { get: LOCALES_OPERATION }, false);
106
+ continue;
107
+ }
108
+ const match = CONTENT_ROUTE_RE.exec(route);
109
+ const byMethod = match ? CONTENT_ROUTES[match[1]] : undefined;
110
+ if (byMethod) {
111
+ describeItem(item, byMethod, true);
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The response schemas of the i18n plugin's three read routes, as plain
3
+ * OpenAPI objects.
4
+ *
5
+ * They are built rather than reflected because the shapes they describe are
6
+ * TypeScript `interface`s (`LocalesView`, `EntryLocalesView`,
7
+ * `LocaleSummaryView`) — erased at compile time, invisible to
8
+ * `@nestjs/swagger`'s scanner, and impossible to decorate. See
9
+ * `packages/bootstrap/server/AGENTS.md` → "The response-schema gap".
10
+ *
11
+ * Pure: locale slugs in, schema objects out. No document, no Nest, no
12
+ * registry.
13
+ */
14
+ /** A JSON Schema fragment, as it appears in the OpenAPI document. */
15
+ export type OpenApiSchema = Record<string, unknown>;
16
+ /** A `$ref` to one of the schemas registered by {@link buildI18nSchemas}. */
17
+ export declare function ref(name: string): OpenApiSchema;
18
+ /**
19
+ * The i18n plugin's response schemas, keyed by component name.
20
+ *
21
+ * `locale` / `slug` carry the **configured** slugs as an enum, which is a fact
22
+ * about every response rather than a guess: the locales route maps the
23
+ * registry, the per-entry panel iterates the configured set, and the batch
24
+ * summary drops any row whose slug the host no longer declares. A locale the
25
+ * config does not name cannot appear in any of the three.
26
+ */
27
+ export declare function buildI18nSchemas(localeSlugs: readonly string[]): Record<string, OpenApiSchema>;
28
+ //# sourceMappingURL=i18n-schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n-schemas.d.ts","sourceRoot":"","sources":["../../../src/lib/docs/i18n-schemas.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAKH,qEAAqE;AACrE,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAEpD,6EAA6E;AAC7E,wBAAgB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAE/C;AAqED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC5B,WAAW,EAAE,SAAS,MAAM,EAAE,GAC/B,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAoG/B"}
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ /**
3
+ * The response schemas of the i18n plugin's three read routes, as plain
4
+ * OpenAPI objects.
5
+ *
6
+ * They are built rather than reflected because the shapes they describe are
7
+ * TypeScript `interface`s (`LocalesView`, `EntryLocalesView`,
8
+ * `LocaleSummaryView`) — erased at compile time, invisible to
9
+ * `@nestjs/swagger`'s scanner, and impossible to decorate. See
10
+ * `packages/bootstrap/server/AGENTS.md` → "The response-schema gap".
11
+ *
12
+ * Pure: locale slugs in, schema objects out. No document, no Nest, no
13
+ * registry.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.ref = ref;
17
+ exports.buildI18nSchemas = buildI18nSchemas;
18
+ const content_server_1 = require("@orthacms/content-server");
19
+ const i18n_constants_1 = require("../i18n.constants");
20
+ /** A `$ref` to one of the schemas registered by {@link buildI18nSchemas}. */
21
+ function ref(name) {
22
+ return { $ref: `#/components/schemas/${name}` };
23
+ }
24
+ /**
25
+ * The text direction of a locale. Always present on the wire even though it is
26
+ * optional in config: the registry resolves a declared or inferred value before
27
+ * anything is serialized, so no consumer has to decide for itself whether
28
+ * Arabic is right-to-left.
29
+ */
30
+ const DIR_SCHEMA = {
31
+ type: 'string',
32
+ enum: [...i18n_constants_1.LOCALE_DIR],
33
+ description: 'Resolved text direction for content in this locale — the HTML `dir` value to set alongside `lang`.'
34
+ };
35
+ /**
36
+ * The publish state of one row. Present **only on a publishable content type**,
37
+ * which is why it appears in no `required` list: the services spread
38
+ * `status`/`publishedAt` in conditionally, so on a non-publishable type the keys
39
+ * are absent rather than null.
40
+ */
41
+ const STATUS_SCHEMA = {
42
+ type: 'string',
43
+ enum: Object.values(content_server_1.ENTRY_STATUS),
44
+ description: 'Publish state. Present on publishable content types only — absent, not null, on the rest.'
45
+ };
46
+ /** When this row last went live, or `null` if it never has. */
47
+ const PUBLISHED_AT_SCHEMA = {
48
+ type: 'string',
49
+ format: 'date-time',
50
+ nullable: true,
51
+ description: 'When this row last went live, or `null` if it never has. Publishable content types only. Paired with `status` it separates a never-published draft from one carrying unpublished edits over live content.'
52
+ };
53
+ /**
54
+ * The group's row in one locale — the `entry` member of a locale panel item.
55
+ *
56
+ * **Inlined rather than a `$ref`**, because this is the one nullable object in
57
+ * the plugin's responses and OpenAPI 3.0 has no way to spell a nullable
58
+ * reference: every sibling of `$ref` is ignored, so `{ $ref, nullable: true }`
59
+ * reads as if it works and silently drops the null, and `nullable` beside a
60
+ * bare `allOf`/`oneOf` is adjunct to `type` and equally inert. `type: 'object'`
61
+ * with `nullable: true` is the form that actually says what the route returns.
62
+ */
63
+ const ENTRY_ROW_SCHEMA = {
64
+ type: 'object',
65
+ nullable: true,
66
+ description: 'The group’s row in this locale, or `null` when it has not been translated yet. Enough to route to the row and render its state — not its values.',
67
+ properties: {
68
+ id: {
69
+ type: 'string',
70
+ format: 'uuid',
71
+ description: 'The entry id of this locale’s row.'
72
+ },
73
+ status: STATUS_SCHEMA,
74
+ publishedAt: PUBLISHED_AT_SCHEMA,
75
+ updatedAt: {
76
+ type: 'string',
77
+ format: 'date-time',
78
+ description: 'When this row was last written.'
79
+ }
80
+ },
81
+ required: ['id', 'updatedAt']
82
+ };
83
+ /**
84
+ * The i18n plugin's response schemas, keyed by component name.
85
+ *
86
+ * `locale` / `slug` carry the **configured** slugs as an enum, which is a fact
87
+ * about every response rather than a guess: the locales route maps the
88
+ * registry, the per-entry panel iterates the configured set, and the batch
89
+ * summary drops any row whose slug the host no longer declares. A locale the
90
+ * config does not name cannot appear in any of the three.
91
+ */
92
+ function buildI18nSchemas(localeSlugs) {
93
+ const slugSchema = {
94
+ type: 'string',
95
+ enum: [...localeSlugs],
96
+ description: 'A configured locale slug — a BCP-47 language tag, usable verbatim as an HTML `lang` value.'
97
+ };
98
+ return {
99
+ I18nLocale: {
100
+ type: 'object',
101
+ description: 'One configured locale.',
102
+ properties: {
103
+ slug: slugSchema,
104
+ name: {
105
+ type: 'string',
106
+ description: 'Human display name, e.g. "English".'
107
+ },
108
+ isDefault: {
109
+ type: 'boolean',
110
+ description: 'The locale applied when a request names none. Exactly one locale in the list sets it.'
111
+ },
112
+ dir: DIR_SCHEMA
113
+ },
114
+ required: ['slug', 'name', 'isDefault', 'dir']
115
+ },
116
+ I18nLocalesView: {
117
+ type: 'object',
118
+ description: 'The configured locales, in display order. Every field is resolved server-side, so a client needs no locale table of its own.',
119
+ properties: {
120
+ items: { type: 'array', items: ref('I18nLocale') }
121
+ },
122
+ required: ['items']
123
+ },
124
+ I18nEntryLocaleItem: {
125
+ type: 'object',
126
+ description: 'One configured locale’s slot in the translation group.',
127
+ properties: {
128
+ locale: slugSchema,
129
+ dir: DIR_SCHEMA,
130
+ isDefault: {
131
+ type: 'boolean',
132
+ description: 'Whether this is the configured default locale.'
133
+ },
134
+ entry: ENTRY_ROW_SCHEMA
135
+ },
136
+ required: ['locale', 'dir', 'isDefault', 'entry']
137
+ },
138
+ I18nEntryLocalesView: {
139
+ type: 'object',
140
+ description: 'The locale panel of one entry: every configured locale, in config order, with the group’s row in it or `null`.',
141
+ properties: {
142
+ localeGroupId: {
143
+ type: 'string',
144
+ format: 'uuid',
145
+ description: 'The translation group this entry belongs to — the stable identity of the record across languages.'
146
+ },
147
+ items: { type: 'array', items: ref('I18nEntryLocaleItem') }
148
+ },
149
+ required: ['localeGroupId', 'items']
150
+ },
151
+ I18nLocaleSummaryItem: {
152
+ type: 'object',
153
+ description: 'One live member of a translation group.',
154
+ properties: {
155
+ locale: slugSchema,
156
+ entryId: {
157
+ type: 'string',
158
+ format: 'uuid',
159
+ description: 'The member row’s entry id.'
160
+ },
161
+ status: STATUS_SCHEMA,
162
+ publishedAt: PUBLISHED_AT_SCHEMA
163
+ },
164
+ required: ['locale', 'entryId']
165
+ },
166
+ I18nLocaleSummaryView: {
167
+ type: 'object',
168
+ description: 'Per requested translation-group id, that group’s live members in config order.',
169
+ properties: {
170
+ groups: {
171
+ type: 'object',
172
+ description: 'Keyed by the group ids the request asked for — **exactly** those, no more and no fewer. A group in another workspace and one that names nothing at all both come back as an empty array, so a caller learns nothing it did not already supply.',
173
+ additionalProperties: {
174
+ type: 'array',
175
+ items: ref('I18nLocaleSummaryItem')
176
+ }
177
+ }
178
+ },
179
+ required: ['groups']
180
+ }
181
+ };
182
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The localization-coverage widget's pass over the host's OpenAPI document.
3
+ *
4
+ * One route, one shape — and the shape needs saying, because its unit is not
5
+ * the obvious one. Every figure here counts **translation groups**, not rows:
6
+ * a localized entry is one row per language sharing a `locale_group_id`, so
7
+ * counting rows would report a workspace of 40 stories in 3 languages as 120
8
+ * things and make every percentage meaningless.
9
+ *
10
+ * Pure: it takes the document and mutates only this plugin's own operation.
11
+ */
12
+ import type { OpenApiDocument } from '@orthacms/bootstrap-server';
13
+ /** Adds the coverage schema and describes the one operation that returns it. */
14
+ export declare function describeI18nInsightsApi(document: OpenApiDocument): void;
15
+ //# sourceMappingURL=describe-i18n-insights-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describe-i18n-insights-api.d.ts","sourceRoot":"","sources":["../../../../src/lib/insights/docs/describe-i18n-insights-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AA2HlE,gFAAgF;AAChF,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI,CAkCvE"}
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ /**
3
+ * The localization-coverage widget's pass over the host's OpenAPI document.
4
+ *
5
+ * One route, one shape — and the shape needs saying, because its unit is not
6
+ * the obvious one. Every figure here counts **translation groups**, not rows:
7
+ * a localized entry is one row per language sharing a `locale_group_id`, so
8
+ * counting rows would report a workspace of 40 stories in 3 languages as 120
9
+ * things and make every percentage meaningless.
10
+ *
11
+ * Pure: it takes the document and mutates only this plugin's own operation.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.describeI18nInsightsApi = describeI18nInsightsApi;
15
+ /**
16
+ * The four per-type figures, reused for the workspace envelope.
17
+ *
18
+ * `notLocalized` is a **subset** of `requiresLocalization`, not a second slice
19
+ * of it — a record in one language of four both has no translations and needs
20
+ * some. They are separate because they are separate jobs: starting a
21
+ * translation, and finishing one. A client that adds them double-counts.
22
+ */
23
+ function coverageCounts() {
24
+ return {
25
+ records: {
26
+ type: 'integer',
27
+ description: 'Translation groups of this type.'
28
+ },
29
+ localized: {
30
+ type: 'integer',
31
+ description: 'Of those, the ones present in **every** configured locale.'
32
+ },
33
+ notLocalized: {
34
+ type: 'integer',
35
+ description: 'Of those, the ones present in exactly one locale. Always 0 in a single-locale deployment, where a record would otherwise be both fully localized and not localized at all.'
36
+ },
37
+ requiresLocalization: {
38
+ type: 'integer',
39
+ description: 'Of those, the ones missing at least one locale (`records - localized`). Includes every `notLocalized` one.'
40
+ }
41
+ };
42
+ }
43
+ /** The coverage response. */
44
+ const COVERAGE = {
45
+ type: 'object',
46
+ description: 'Localization coverage across every `i18n: true` content type. The unit is a translation group, never a row.',
47
+ required: [
48
+ 'locales',
49
+ 'records',
50
+ 'localized',
51
+ 'notLocalized',
52
+ 'requiresLocalization',
53
+ 'types'
54
+ ],
55
+ properties: {
56
+ locales: {
57
+ type: 'array',
58
+ description: 'One entry per configured locale, in config order.',
59
+ items: {
60
+ type: 'object',
61
+ required: [
62
+ 'locale',
63
+ 'name',
64
+ 'isDefault',
65
+ 'translated',
66
+ 'missing'
67
+ ],
68
+ properties: {
69
+ locale: {
70
+ type: 'string',
71
+ description: 'The configured locale slug.',
72
+ example: 'de'
73
+ },
74
+ name: {
75
+ type: 'string',
76
+ description: 'Its display name, from the host’s config.'
77
+ },
78
+ isDefault: { type: 'boolean' },
79
+ translated: {
80
+ type: 'integer',
81
+ description: 'Records that have a row in this locale.'
82
+ },
83
+ missing: {
84
+ type: 'integer',
85
+ description: 'Records that do not.'
86
+ }
87
+ }
88
+ }
89
+ },
90
+ ...coverageCounts(),
91
+ types: {
92
+ type: 'array',
93
+ description: 'Per-type coverage, most records first. A type the workspace has never used is omitted rather than listed at zero — a permanently empty row would push the types that hold work further down.',
94
+ items: {
95
+ type: 'object',
96
+ required: [
97
+ 'name',
98
+ 'label',
99
+ 'records',
100
+ 'localized',
101
+ 'notLocalized',
102
+ 'requiresLocalization'
103
+ ],
104
+ properties: {
105
+ name: { type: 'string' },
106
+ label: { type: 'string' },
107
+ ...coverageCounts()
108
+ }
109
+ }
110
+ }
111
+ }
112
+ };
113
+ /**
114
+ * Matches `<prefix>/insights/i18n/coverage`.
115
+ *
116
+ * The prefix is matched as segments carrying no `{`, so a nested route ending
117
+ * in the same segments is not described with this shape.
118
+ */
119
+ const ROUTE_RE = /^(?:\/[^/{}]+)*\/insights\/i18n\/coverage$/;
120
+ /** Adds the coverage schema and describes the one operation that returns it. */
121
+ function describeI18nInsightsApi(document) {
122
+ document.components ??= {};
123
+ document.components.schemas ??= {};
124
+ document.components.schemas['InsightsI18nCoverage'] = COVERAGE;
125
+ for (const [route, item] of Object.entries(document.paths)) {
126
+ if (!ROUTE_RE.test(route)) {
127
+ continue;
128
+ }
129
+ const operation = item['get'];
130
+ if (!operation || typeof operation !== 'object') {
131
+ continue;
132
+ }
133
+ // Onto whichever 2xx key the scanner emitted — never a new one.
134
+ const responses = operation.responses ?? {};
135
+ const key = Object.keys(responses).find((code) => /^2\d\d$/.test(code));
136
+ if (!key || key === '204') {
137
+ continue;
138
+ }
139
+ responses[key] = {
140
+ description: 'Coverage per locale and per localized content type, counted in translation groups.',
141
+ content: {
142
+ 'application/json': {
143
+ schema: {
144
+ $ref: '#/components/schemas/InsightsI18nCoverage'
145
+ }
146
+ }
147
+ }
148
+ };
149
+ operation.responses = responses;
150
+ }
151
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"i18n-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/i18n-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAIxD;;;GAGG;AACH,MAAM,WAAW,oBAAqB,SAAQ,YAAY;IACtD,0CAA0C;IAC1C,UAAU,EAAE,gBAAgB,CAAC;CAChC;AAcD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,gBAAgB,CAC5B,MAAM,EAAE,gBAAgB,GACzB,oBAAoB,CAOtB"}
1
+ {"version":3,"file":"i18n-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/utils/i18n-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAMxD;;;GAGG;AACH,MAAM,WAAW,oBAAqB,SAAQ,YAAY;IACtD,0CAA0C;IAC1C,UAAU,EAAE,gBAAgB,CAAC;CAChC;AAcD;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,gBAAgB,CAC5B,MAAM,EAAE,gBAAgB,GACzB,oBAAoB,CAyBtB"}
@@ -2,7 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.I18nServerPlugin = I18nServerPlugin;
4
4
  const locale_set_1 = require("../domain/value-objects/locale-set");
5
+ const describe_i18n_api_1 = require("../docs/describe-i18n-api");
5
6
  const i18n_module_1 = require("../i18n.module");
7
+ const describe_i18n_insights_api_1 = require("../insights/docs/describe-i18n-insights-api");
6
8
  /**
7
9
  * Validate the locale config **eagerly** (like `ContentPlugin`'s registry):
8
10
  * a misconfigured host fails at construction — before boot, before the first
@@ -48,6 +50,21 @@ function I18nServerPlugin(config) {
48
50
  return {
49
51
  name: 'i18n',
50
52
  module: i18n_module_1.I18nModule.forRoot(config),
51
- i18nConfig: config
53
+ i18nConfig: config,
54
+ // The three read routes answer plain `interface`s, which the OpenAPI
55
+ // scanner cannot see and ADR-0003 would not let us decorate anyway.
56
+ // The configured slugs go in with them: which locales exist is runtime
57
+ // data held right here, and it is the one thing that makes the
58
+ // described `locale` fields exact rather than "some string".
59
+ docs: {
60
+ decorate: (document) => {
61
+ (0, describe_i18n_api_1.describeI18nApi)(document, config.locales.map((locale) => locale.slug));
62
+ // `/insights/i18n/coverage` is this plugin's route too, but it
63
+ // belongs to the Insights surface — a different tag, a
64
+ // different audience — so it is described from its own module
65
+ // rather than folded into the locale/content pass above.
66
+ (0, describe_i18n_insights_api_1.describeI18nInsightsApi)(document);
67
+ }
68
+ }
52
69
  };
53
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orthacms/i18n-server",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "@orthacms/i18n-server — part of Ortha CMS.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/ortha-source/ortha-cms/tree/main/packages/i18n/server",
@@ -27,15 +27,15 @@
27
27
  "dependencies": {
28
28
  "@nestjs/common": "^11.0.0",
29
29
  "@nestjs/swagger": "^11.4.6",
30
- "@orthacms/bootstrap-server": "^0.4.3",
31
- "@orthacms/content-server": "^0.4.3",
32
- "@orthacms/copilot-domain": "^0.4.3",
33
- "@orthacms/copilot-server": "^0.4.3",
34
- "@orthacms/database": "^0.4.3",
35
- "@orthacms/identity-server": "^0.4.3",
36
- "@orthacms/tools-server": "^0.4.3",
37
- "@orthacms/utils-server": "^0.4.3",
38
- "@orthacms/workspaces-server": "^0.4.3",
30
+ "@orthacms/bootstrap-server": "^0.5.0",
31
+ "@orthacms/content-server": "^0.5.0",
32
+ "@orthacms/copilot-domain": "^0.5.0",
33
+ "@orthacms/copilot-server": "^0.5.0",
34
+ "@orthacms/database": "^0.5.0",
35
+ "@orthacms/identity-server": "^0.5.0",
36
+ "@orthacms/tools-server": "^0.5.0",
37
+ "@orthacms/utils-server": "^0.5.0",
38
+ "@orthacms/workspaces-server": "^0.5.0",
39
39
  "class-transformer": "^0.5.1",
40
40
  "class-validator": "^0.15.1",
41
41
  "drizzle-orm": "^0.45.0",