@orthacms/i18n-server 0.0.0-reserve.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 (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/index.d.ts +13 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +15 -0
  6. package/dist/lib/content/controllers/get-entry-locales.controller.d.ts +15 -0
  7. package/dist/lib/content/controllers/get-entry-locales.controller.d.ts.map +1 -0
  8. package/dist/lib/content/controllers/get-entry-locales.controller.js +45 -0
  9. package/dist/lib/content/controllers/locale-summary.controller.d.ts +30 -0
  10. package/dist/lib/content/controllers/locale-summary.controller.d.ts.map +1 -0
  11. package/dist/lib/content/controllers/locale-summary.controller.js +61 -0
  12. package/dist/lib/content/controllers/resolve-type.d.ts +9 -0
  13. package/dist/lib/content/controllers/resolve-type.d.ts.map +1 -0
  14. package/dist/lib/content/controllers/resolve-type.js +20 -0
  15. package/dist/lib/content/dto/locale-summary.dto.d.ts +10 -0
  16. package/dist/lib/content/dto/locale-summary.dto.d.ts.map +1 -0
  17. package/dist/lib/content/dto/locale-summary.dto.js +30 -0
  18. package/dist/lib/content/locale-group-lock.d.ts +34 -0
  19. package/dist/lib/content/locale-group-lock.d.ts.map +1 -0
  20. package/dist/lib/content/locale-group-lock.js +38 -0
  21. package/dist/lib/content/services/entry-locale-extension.service.d.ts +188 -0
  22. package/dist/lib/content/services/entry-locale-extension.service.d.ts.map +1 -0
  23. package/dist/lib/content/services/entry-locale-extension.service.js +662 -0
  24. package/dist/lib/content/services/locale-group.service.d.ts +87 -0
  25. package/dist/lib/content/services/locale-group.service.d.ts.map +1 -0
  26. package/dist/lib/content/services/locale-group.service.js +135 -0
  27. package/dist/lib/copilot/i18n-tool.provider.d.ts +53 -0
  28. package/dist/lib/copilot/i18n-tool.provider.d.ts.map +1 -0
  29. package/dist/lib/copilot/i18n-tool.provider.js +157 -0
  30. package/dist/lib/copilot/translation-proposal.applier.d.ts +87 -0
  31. package/dist/lib/copilot/translation-proposal.applier.d.ts.map +1 -0
  32. package/dist/lib/copilot/translation-proposal.applier.js +242 -0
  33. package/dist/lib/copilot/translation-proposal.provider.d.ts +114 -0
  34. package/dist/lib/copilot/translation-proposal.provider.d.ts.map +1 -0
  35. package/dist/lib/copilot/translation-proposal.provider.js +419 -0
  36. package/dist/lib/domain/errors/index.d.ts +4 -0
  37. package/dist/lib/domain/errors/index.d.ts.map +1 -0
  38. package/dist/lib/domain/errors/index.js +9 -0
  39. package/dist/lib/domain/errors/invalid-locale-set.error.d.ts +10 -0
  40. package/dist/lib/domain/errors/invalid-locale-set.error.d.ts.map +1 -0
  41. package/dist/lib/domain/errors/invalid-locale-set.error.js +16 -0
  42. package/dist/lib/domain/errors/invalid-locale.error.d.ts +10 -0
  43. package/dist/lib/domain/errors/invalid-locale.error.d.ts.map +1 -0
  44. package/dist/lib/domain/errors/invalid-locale.error.js +16 -0
  45. package/dist/lib/domain/errors/unknown-locale.error.d.ts +11 -0
  46. package/dist/lib/domain/errors/unknown-locale.error.d.ts.map +1 -0
  47. package/dist/lib/domain/errors/unknown-locale.error.js +18 -0
  48. package/dist/lib/domain/locale-policy.d.ts +42 -0
  49. package/dist/lib/domain/locale-policy.d.ts.map +1 -0
  50. package/dist/lib/domain/locale-policy.js +61 -0
  51. package/dist/lib/domain/value-objects/locale-set.d.ts +41 -0
  52. package/dist/lib/domain/value-objects/locale-set.d.ts.map +1 -0
  53. package/dist/lib/domain/value-objects/locale-set.js +89 -0
  54. package/dist/lib/domain/value-objects/locale.d.ts +55 -0
  55. package/dist/lib/domain/value-objects/locale.d.ts.map +1 -0
  56. package/dist/lib/domain/value-objects/locale.js +73 -0
  57. package/dist/lib/i18n.constants.d.ts +33 -0
  58. package/dist/lib/i18n.constants.d.ts.map +1 -0
  59. package/dist/lib/i18n.constants.js +89 -0
  60. package/dist/lib/i18n.module.d.ts +14 -0
  61. package/dist/lib/i18n.module.d.ts.map +1 -0
  62. package/dist/lib/i18n.module.js +82 -0
  63. package/dist/lib/insights/http/controllers/localization-coverage.controller.d.ts +23 -0
  64. package/dist/lib/insights/http/controllers/localization-coverage.controller.d.ts.map +1 -0
  65. package/dist/lib/insights/http/controllers/localization-coverage.controller.js +46 -0
  66. package/dist/lib/insights/infrastructure/queries/localization-coverage.query.d.ts +59 -0
  67. package/dist/lib/insights/infrastructure/queries/localization-coverage.query.d.ts.map +1 -0
  68. package/dist/lib/insights/infrastructure/queries/localization-coverage.query.js +179 -0
  69. package/dist/lib/insights/types/i18n-insights-view.d.ts +83 -0
  70. package/dist/lib/insights/types/i18n-insights-view.d.ts.map +1 -0
  71. package/dist/lib/insights/types/i18n-insights-view.js +10 -0
  72. package/dist/lib/locales/controllers/list-locales.controller.d.ts +26 -0
  73. package/dist/lib/locales/controllers/list-locales.controller.d.ts.map +1 -0
  74. package/dist/lib/locales/controllers/list-locales.controller.js +48 -0
  75. package/dist/lib/locales/services/locale-registry.service.d.ts +26 -0
  76. package/dist/lib/locales/services/locale-registry.service.d.ts.map +1 -0
  77. package/dist/lib/locales/services/locale-registry.service.js +70 -0
  78. package/dist/lib/locales/services/orphaned-locale.checker.d.ts +60 -0
  79. package/dist/lib/locales/services/orphaned-locale.checker.d.ts.map +1 -0
  80. package/dist/lib/locales/services/orphaned-locale.checker.js +114 -0
  81. package/dist/lib/types/locale.d.ts +68 -0
  82. package/dist/lib/types/locale.d.ts.map +1 -0
  83. package/dist/lib/types/locale.js +8 -0
  84. package/dist/lib/utils/i18n-plugin.d.ts +41 -0
  85. package/dist/lib/utils/i18n-plugin.d.ts.map +1 -0
  86. package/dist/lib/utils/i18n-plugin.js +53 -0
  87. package/package.json +47 -0
@@ -0,0 +1,419 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TranslationProposalToolProvider = exports.I18N_PROPOSAL_KINDS = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const common_1 = require("@nestjs/common");
6
+ const identity_server_1 = require("@orthacms/identity-server");
7
+ const content_server_1 = require("@orthacms/content-server");
8
+ const tools_server_1 = require("@orthacms/tools-server");
9
+ const locale_registry_service_1 = require("../locales/services/locale-registry.service");
10
+ const locale_group_service_1 = require("../content/services/locale-group.service");
11
+ /** The proposal kinds this plugin declares and applies. */
12
+ exports.I18N_PROPOSAL_KINDS = {
13
+ /** Create a sibling row of an entry in another locale. */
14
+ createTranslation: 'i18n.entry.translate',
15
+ /**
16
+ * Create several sibling rows — more locales of one record, more records
17
+ * in one locale, or both — as **one** change.
18
+ *
19
+ * Its own kind rather than a repeated `createTranslation`, for the reason
20
+ * `content.entry.bulk-save` is: a proposal row is the receipt for one tool
21
+ * call, and "translate this into five languages" written as five rows is
22
+ * five cards in the transcript for a single instruction, none of which says
23
+ * what the other four were.
24
+ */
25
+ bulkTranslation: 'i18n.entry.bulk-translate'
26
+ };
27
+ /**
28
+ * The plugin's **write** tools: `i18n_propose_translation` for one locale of one
29
+ * entry, and `i18n_propose_bulk_translation` for a batch of them.
30
+ *
31
+ * Like every propose tool their handlers **write nothing** — they compute the
32
+ * change and hand it back, and the run engine records and applies it. What they
33
+ * do is the part a model cannot be trusted to get right on its own: resolve the
34
+ * locale slug against the configured set, confirm the target locale does not
35
+ * already exist in the group (a duplicate is a 409 at apply time, after the
36
+ * card has said the change is happening), and name the source entry so the
37
+ * applier can inherit its shared values.
38
+ *
39
+ * Only **localized** fields are translated. A field the type declares as shared
40
+ * is one value across the whole group by definition — writing a per-locale
41
+ * version of it would either be ignored or silently overwrite every sibling,
42
+ * and neither is something to show a reader as a translation.
43
+ */
44
+ let TranslationProposalToolProvider = class TranslationProposalToolProvider {
45
+ registry;
46
+ locales;
47
+ groups;
48
+ grants;
49
+ toolRegistry;
50
+ constructor(registry, locales, groups, grants, toolRegistry) {
51
+ this.registry = registry;
52
+ this.locales = locales;
53
+ this.groups = groups;
54
+ this.grants = grants;
55
+ this.toolRegistry = toolRegistry;
56
+ }
57
+ /**
58
+ * Register with the shared tool registry once the DI graph is built —
59
+ * the same catalogue the MCP endpoint serves, narrowed to the `copilot`
60
+ * surface by each tool's `surfaces`. `@Optional()` because a deployment
61
+ * may run neither consumer, in which case these simply go unregistered.
62
+ */
63
+ onModuleInit() {
64
+ this.toolRegistry?.register(this);
65
+ }
66
+ /** The two i18n write tools — one translation, and the batch. */
67
+ tools() {
68
+ return [this.proposeTranslation(), this.proposeBulkTranslation()];
69
+ }
70
+ /**
71
+ * A granted, registered, **localized** type — the gate both write tools
72
+ * open with.
73
+ *
74
+ * The two failures are worth separating: "unknown type" is the uniform
75
+ * answer every content tool gives a name it cannot reach (it must not
76
+ * distinguish "no such type" from "not granted here"), while "not
77
+ * localized" describes a type the caller has already proved it can reach
78
+ * and is safe — and useful — to say out loud.
79
+ */
80
+ async resolveLocalizedType(typeName, workspaceId) {
81
+ const type = this.registry.get(typeName);
82
+ const granted = await this.grants.grantedSlugs(workspaceId);
83
+ if (!type || !granted.has(type.name)) {
84
+ throw new Error(`Unknown content type "${typeName}" in this workspace.`);
85
+ }
86
+ if (!type.i18n) {
87
+ throw new Error(`Content type "${type.name}" is not localized, so it cannot be translated.`);
88
+ }
89
+ return type;
90
+ }
91
+ /**
92
+ * The type's **localized** fields, keyed by name — the only ones a
93
+ * translation may carry.
94
+ *
95
+ * A field the type declares as shared is one value across the whole group
96
+ * by definition: writing a per-locale version of it would be fanned back
97
+ * out over every sibling by this plugin's own sync, so a "translated"
98
+ * shared field silently rewrites every other language.
99
+ */
100
+ localizedFields(typeName) {
101
+ const schema = this.registry.serialize(typeName);
102
+ return new Map((schema?.fields ?? [])
103
+ .filter((field) => field.localized)
104
+ .map((field) => [field.name, field]));
105
+ }
106
+ /**
107
+ * Rejects anything in `values` that is not a localized field, and an empty
108
+ * bag — the check that makes a translation tool a translation tool.
109
+ *
110
+ * `prefix` names which item is at fault in a batch, and is empty for the
111
+ * single-entry tool so its message is unchanged.
112
+ */
113
+ assertTranslatable(values, localized, prefix = '') {
114
+ const rejected = Object.keys(values).filter((name) => !localized.has(name));
115
+ if (rejected.length > 0) {
116
+ throw new Error(`${prefix}These fields are shared across locales and cannot be translated: ` +
117
+ `${rejected.join(', ')}. Only ${[...localized.keys()].join(', ')} ` +
118
+ 'vary per locale on this type.');
119
+ }
120
+ if (Object.keys(values).length === 0) {
121
+ throw new Error(`${prefix}No translated values were supplied.`);
122
+ }
123
+ }
124
+ /** The field's admin label, when it has one — for the card's diff rows. */
125
+ labelOf(field) {
126
+ const label = field?.admin?.['label'];
127
+ return typeof label === 'string' ? label : undefined;
128
+ }
129
+ /** `i18n_propose_translation` — one entry into one locale. */
130
+ proposeTranslation() {
131
+ return {
132
+ name: 'i18n_propose_translation',
133
+ title: 'Propose a translation',
134
+ description: 'Propose a translation of an entry into another locale, creating that locale’s ' +
135
+ 'row in the same translation group. This does NOT save anything — it drafts ' +
136
+ 'the change for the user to approve, and the reply will say so. Read the ' +
137
+ 'source entry first (admin_content_get) and translate its text yourself; only ' +
138
+ 'fields the type marks as localized can differ per locale.',
139
+ inputSchema: {
140
+ type: 'object',
141
+ properties: {
142
+ typeName: {
143
+ type: 'string',
144
+ description: 'The entry’s content type. It must be localized.'
145
+ },
146
+ id: {
147
+ type: 'string',
148
+ description: 'The id of the source entry — the row you translated from.'
149
+ },
150
+ locale: {
151
+ type: 'string',
152
+ maxLength: 35,
153
+ description: 'The locale slug to create, from i18n_locales_list. It must not ' +
154
+ 'already exist in this entry’s translation group.'
155
+ },
156
+ values: {
157
+ type: 'object',
158
+ description: 'The translated values, keyed by field name. Only localized ' +
159
+ 'fields are accepted; shared fields are copied by the CMS.'
160
+ },
161
+ summary: {
162
+ type: 'string',
163
+ maxLength: 200,
164
+ description: 'One line for the approval card, e.g. “German translation of ' +
165
+ 'Spring launch”.'
166
+ }
167
+ },
168
+ required: ['typeName', 'id', 'locale', 'values', 'summary'],
169
+ additionalProperties: false
170
+ },
171
+ // `content:update` rather than `content:create`: the design's v1
172
+ // catalogue says so, and it is the right key — a translation is a
173
+ // change to an existing piece of content, not a new one, which is
174
+ // exactly how an editor thinks about it.
175
+ requires: [identity_server_1.PERMISSIONS.CONTENT_UPDATE],
176
+ readOnly: false,
177
+ effect: 'propose',
178
+ // Copilot-only: it reads the admin services (a viewer must see
179
+ // drafts) or writes through propose-then-apply with the human as
180
+ // actor. MCP's content tools are the public-API set.
181
+ surfaces: ['copilot'],
182
+ handler: async (input, ctx) => {
183
+ const args = (input ?? {});
184
+ const type = await this.resolveLocalizedType(args.typeName, ctx.workspaceId);
185
+ const target = this.locales.get(args.locale);
186
+ if (!target) {
187
+ throw new Error(`Unknown locale "${args.locale}". Call i18n_locales_list for the ` +
188
+ 'configured slugs.');
189
+ }
190
+ // The group read doubles as the existence check on the source
191
+ // entry: it 404s a missing or soft-deleted row, workspace-scoped.
192
+ const group = await this.groups.entryLocales(type, args.id, ctx.workspaceId);
193
+ const existing = group.items.find((item) => item.locale === target.slug);
194
+ if (existing?.entry) {
195
+ throw new Error(`This entry already has a "${target.slug}" translation. Use ` +
196
+ 'content_propose_update on that row instead.');
197
+ }
198
+ const localized = this.localizedFields(type.name);
199
+ const values = args.values ?? {};
200
+ this.assertTranslatable(values, localized);
201
+ return {
202
+ kind: exports.I18N_PROPOSAL_KINDS.createTranslation,
203
+ target: {
204
+ typeName: type.name,
205
+ sourceId: args.id,
206
+ localeGroupId: group.localeGroupId,
207
+ locale: target.slug
208
+ },
209
+ patch: { values },
210
+ summary: args.summary,
211
+ changes: Object.entries(values).map(([field, after]) => {
212
+ const label = this.labelOf(localized.get(field));
213
+ return {
214
+ field,
215
+ ...(label ? { label } : {}),
216
+ after
217
+ };
218
+ })
219
+ };
220
+ }
221
+ };
222
+ }
223
+ /**
224
+ * `i18n_propose_bulk_translation` — several translations as **one** change.
225
+ *
226
+ * The single-entry tool scales badly in both directions users actually push
227
+ * it: "translate this into German, French and Spanish" is three calls, and
228
+ * "translate these eight posts into German" is eight. Each one spent a step
229
+ * of a bounded run and left its own card, so one instruction read back as a
230
+ * pile of receipts, none of which said what the others were.
231
+ *
232
+ * **Why this rather than `content_propose_bulk_save` with a
233
+ * `localeGroupId`.** That is what a model reached for before, and it is the
234
+ * shape that quietly breaks a group: a create is a whole row, so every
235
+ * shared field the batch did not name arrives as `null`, and this plugin's
236
+ * sync then pushes those nulls onto every sibling — see
237
+ * {@link TranslationProposalApplier}. Joining a translation group needs the
238
+ * source's shared values inherited at apply time, which is knowledge that
239
+ * lives here, so the content tools no longer offer `localeGroupId` at all
240
+ * and this is the one route in.
241
+ *
242
+ * An item is one **(source entry, target locale)** pair, so both shapes are
243
+ * the same list: five items naming one `id` and five locales, eight naming
244
+ * eight ids and one locale, or any mixture.
245
+ */
246
+ proposeBulkTranslation() {
247
+ return {
248
+ name: 'i18n_propose_bulk_translation',
249
+ title: 'Propose several translations',
250
+ description: 'Translate one or more entries into one or more locales in a SINGLE change. ' +
251
+ 'Use this instead of calling i18n_propose_translation repeatedly — the user ' +
252
+ 'sees one card listing every translation, and you spend one step instead of ' +
253
+ 'one per language. Each item is one entry translated into one locale: repeat ' +
254
+ 'the same `id` with different locales to translate a record into several ' +
255
+ 'languages, or list different `id`s with the same locale to translate several ' +
256
+ 'records. Read the source entries first (admin_content_get) and translate ' +
257
+ 'their text yourself; only fields the type marks as localized can differ per ' +
258
+ 'locale, and the CMS copies the shared ones. If a translation fails, the ' +
259
+ 'change stops there and the reply says how many landed — do not claim the ' +
260
+ 'whole batch saved without reading it.',
261
+ inputSchema: {
262
+ type: 'object',
263
+ properties: {
264
+ typeName: {
265
+ type: 'string',
266
+ description: 'The content type every item belongs to. It must be localized.'
267
+ },
268
+ items: {
269
+ type: 'array',
270
+ minItems: 1,
271
+ maxItems: content_server_1.BULK_MAX_SAVE_ITEMS,
272
+ description: `The translations to create (1…${content_server_1.BULK_MAX_SAVE_ITEMS}), applied in order.`,
273
+ items: {
274
+ type: 'object',
275
+ properties: {
276
+ id: {
277
+ type: 'string',
278
+ description: 'The id of the source entry — the row you translated from. ' +
279
+ 'Any locale of the record will do; they share one translation ' +
280
+ 'group.'
281
+ },
282
+ locale: {
283
+ type: 'string',
284
+ maxLength: 35,
285
+ description: 'The locale slug to create, from i18n_locales_list. It must ' +
286
+ 'not already exist in this entry’s translation group.'
287
+ },
288
+ values: {
289
+ type: 'object',
290
+ description: 'The translated values for THIS locale, keyed by field name. ' +
291
+ 'Only localized fields are accepted; shared fields are copied ' +
292
+ 'by the CMS.'
293
+ }
294
+ },
295
+ required: ['id', 'locale', 'values'],
296
+ additionalProperties: false
297
+ }
298
+ },
299
+ summary: {
300
+ type: 'string',
301
+ maxLength: 200,
302
+ description: 'One line describing the whole batch, shown to the user on the card. ' +
303
+ 'Write it for a person, e.g. “Translate Spring launch into 3 languages”.'
304
+ }
305
+ },
306
+ required: ['typeName', 'items', 'summary'],
307
+ additionalProperties: false
308
+ },
309
+ // The same key the single-entry tool takes, and for the same
310
+ // reason: a translation is a change to existing content, not a new
311
+ // piece of it. Batching is a way of *asking* — it must not become a
312
+ // route to authority the one-at-a-time tool was never given
313
+ // (ADR-0005 §7).
314
+ requires: [identity_server_1.PERMISSIONS.CONTENT_UPDATE],
315
+ readOnly: false,
316
+ effect: 'propose',
317
+ surfaces: ['copilot'],
318
+ handler: async (input, ctx) => {
319
+ const args = (input ?? {});
320
+ const type = await this.resolveLocalizedType(args.typeName, ctx.workspaceId);
321
+ const items = args.items ?? [];
322
+ if (items.length === 0) {
323
+ throw new Error('Nothing to translate — `items` was empty. Send at least one translation.');
324
+ }
325
+ if (items.length > content_server_1.BULK_MAX_SAVE_ITEMS) {
326
+ throw new Error(`Too many translations in one change: ${items.length}, and the limit ` +
327
+ `is ${content_server_1.BULK_MAX_SAVE_ITEMS}. Split it into smaller batches.`);
328
+ }
329
+ const localized = this.localizedFields(type.name);
330
+ const changes = [];
331
+ const patched = [];
332
+ // One group read per distinct source id: translating one record
333
+ // into five languages is five items naming the same entry, and
334
+ // re-reading its group five times would be five identical
335
+ // queries answering an identical question.
336
+ const groups = new Map();
337
+ // Every (group, locale) this batch already claims. Two items
338
+ // aiming at the same slot is a unique-index violation waiting
339
+ // to happen halfway through the apply — after the first
340
+ // translations have already been written.
341
+ const claimed = new Set();
342
+ for (const [index, item] of items.entries()) {
343
+ const at = `Item ${index + 1}: `;
344
+ if (typeof item.id !== 'string' || item.id.length === 0) {
345
+ throw new Error(`${at}no source entry \`id\`. Every item names the entry it translates.`);
346
+ }
347
+ const target = this.locales.get(item.locale);
348
+ if (!target) {
349
+ throw new Error(`${at}unknown locale "${item.locale}". Call i18n_locales_list for ` +
350
+ 'the configured slugs.');
351
+ }
352
+ // Reads the group, and with it proves the source row exists
353
+ // in this workspace — so a bad id fails HERE, before
354
+ // anything is written, rather than partway through the
355
+ // batch. The whole view is cached, not just its group id:
356
+ // nothing is written during a propose, so which locales
357
+ // exist cannot change under the loop, and every later item
358
+ // naming the same entry answers "is this locale taken?"
359
+ // from it. Keyed by id rather than by group, which is the
360
+ // honest cost of not knowing two ids are siblings until one
361
+ // of them has been read.
362
+ let group = groups.get(item.id);
363
+ if (!group) {
364
+ group = await this.groups.entryLocales(type, item.id, ctx.workspaceId);
365
+ groups.set(item.id, group);
366
+ }
367
+ const existing = group.items.find((candidate) => candidate.locale === target.slug);
368
+ if (existing?.entry) {
369
+ throw new Error(`${at}this entry already has a "${target.slug}" translation. Use ` +
370
+ 'content_propose_update on that row instead.');
371
+ }
372
+ const localeGroupId = group.localeGroupId;
373
+ const slot = `${localeGroupId}:${target.slug}`;
374
+ if (claimed.has(slot)) {
375
+ throw new Error(`${at}an earlier item already translates this record into ` +
376
+ `"${target.slug}". A record has one row per locale, so send each ` +
377
+ 'locale of it once.');
378
+ }
379
+ claimed.add(slot);
380
+ const values = item.values ?? {};
381
+ this.assertTranslatable(values, localized, at);
382
+ patched.push({
383
+ sourceId: item.id,
384
+ localeGroupId,
385
+ locale: target.slug,
386
+ values
387
+ });
388
+ for (const [field, after] of Object.entries(values)) {
389
+ changes.push({
390
+ // Prefixed by position, because the card keys its
391
+ // rows on `field` and five locales translating
392
+ // `title` would otherwise collapse into one row.
393
+ field: `items[${index}].${field}`,
394
+ label: `#${index + 1} ${target.slug} · ${this.labelOf(localized.get(field)) ?? field}`,
395
+ after
396
+ });
397
+ }
398
+ }
399
+ return {
400
+ kind: exports.I18N_PROPOSAL_KINDS.bulkTranslation,
401
+ target: { typeName: type.name },
402
+ patch: { items: patched },
403
+ summary: args.summary,
404
+ changes
405
+ };
406
+ }
407
+ };
408
+ }
409
+ };
410
+ exports.TranslationProposalToolProvider = TranslationProposalToolProvider;
411
+ exports.TranslationProposalToolProvider = TranslationProposalToolProvider = tslib_1.__decorate([
412
+ (0, common_1.Injectable)(),
413
+ tslib_1.__param(0, (0, content_server_1.InjectContentRegistry)()),
414
+ tslib_1.__param(4, (0, common_1.Optional)()),
415
+ tslib_1.__metadata("design:paramtypes", [Function, locale_registry_service_1.LocaleRegistryService,
416
+ locale_group_service_1.LocaleGroupService,
417
+ content_server_1.WorkspaceGrantsQuery,
418
+ tools_server_1.ToolRegistry])
419
+ ], TranslationProposalToolProvider);
@@ -0,0 +1,4 @@
1
+ export { InvalidLocaleError } from './invalid-locale.error';
2
+ export { InvalidLocaleSetError } from './invalid-locale-set.error';
3
+ export { UnknownLocaleError } from './unknown-locale.error';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/domain/errors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UnknownLocaleError = exports.InvalidLocaleSetError = exports.InvalidLocaleError = void 0;
4
+ var invalid_locale_error_1 = require("./invalid-locale.error");
5
+ Object.defineProperty(exports, "InvalidLocaleError", { enumerable: true, get: function () { return invalid_locale_error_1.InvalidLocaleError; } });
6
+ var invalid_locale_set_error_1 = require("./invalid-locale-set.error");
7
+ Object.defineProperty(exports, "InvalidLocaleSetError", { enumerable: true, get: function () { return invalid_locale_set_error_1.InvalidLocaleSetError; } });
8
+ var unknown_locale_error_1 = require("./unknown-locale.error");
9
+ Object.defineProperty(exports, "UnknownLocaleError", { enumerable: true, get: function () { return unknown_locale_error_1.UnknownLocaleError; } });
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Raised by {@link LocaleSet} when the *collection* of locales violates an
3
+ * invariant no single {@link Locale} can guard — an empty set, a duplicate
4
+ * slug, not exactly one default, or removing the default. Transport-agnostic;
5
+ * the plugin factory surfaces it at construction.
6
+ */
7
+ export declare class InvalidLocaleSetError extends Error {
8
+ constructor(message: string);
9
+ }
10
+ //# sourceMappingURL=invalid-locale-set.error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invalid-locale-set.error.d.ts","sourceRoot":"","sources":["../../../../src/lib/domain/errors/invalid-locale-set.error.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,qBAAa,qBAAsB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI9B"}
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InvalidLocaleSetError = void 0;
4
+ /**
5
+ * Raised by {@link LocaleSet} when the *collection* of locales violates an
6
+ * invariant no single {@link Locale} can guard — an empty set, a duplicate
7
+ * slug, not exactly one default, or removing the default. Transport-agnostic;
8
+ * the plugin factory surfaces it at construction.
9
+ */
10
+ class InvalidLocaleSetError extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = 'InvalidLocaleSetError';
14
+ }
15
+ }
16
+ exports.InvalidLocaleSetError = InvalidLocaleSetError;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Raised by {@link Locale.create} when a locale's slug is malformed (not the
3
+ * lowercase BCP-47-ish shape) or its display name is blank. Transport-agnostic
4
+ * — the plugin factory surfaces it at construction, so a misconfigured host
5
+ * fails before boot.
6
+ */
7
+ export declare class InvalidLocaleError extends Error {
8
+ constructor(message: string);
9
+ }
10
+ //# sourceMappingURL=invalid-locale.error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"invalid-locale.error.d.ts","sourceRoot":"","sources":["../../../../src/lib/domain/errors/invalid-locale.error.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI9B"}
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InvalidLocaleError = void 0;
4
+ /**
5
+ * Raised by {@link Locale.create} when a locale's slug is malformed (not the
6
+ * lowercase BCP-47-ish shape) or its display name is blank. Transport-agnostic
7
+ * — the plugin factory surfaces it at construction, so a misconfigured host
8
+ * fails before boot.
9
+ */
10
+ class InvalidLocaleError extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = 'InvalidLocaleError';
14
+ }
15
+ }
16
+ exports.InvalidLocaleError = InvalidLocaleError;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Raised by {@link LocalePolicy.resolve} when a request names a locale slug
3
+ * that isn't configured. Transport-agnostic — {@link LocaleRegistryService}
4
+ * maps it to HTTP 400 (the uniform unknown-locale gate every request path
5
+ * funnels through).
6
+ */
7
+ export declare class UnknownLocaleError extends Error {
8
+ readonly slug: string;
9
+ constructor(slug: string);
10
+ }
11
+ //# sourceMappingURL=unknown-locale.error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"unknown-locale.error.d.ts","sourceRoot":"","sources":["../../../../src/lib/domain/errors/unknown-locale.error.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;aACb,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAI3C"}
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UnknownLocaleError = void 0;
4
+ /**
5
+ * Raised by {@link LocalePolicy.resolve} when a request names a locale slug
6
+ * that isn't configured. Transport-agnostic — {@link LocaleRegistryService}
7
+ * maps it to HTTP 400 (the uniform unknown-locale gate every request path
8
+ * funnels through).
9
+ */
10
+ class UnknownLocaleError extends Error {
11
+ slug;
12
+ constructor(slug) {
13
+ super(`Unknown locale "${slug}".`);
14
+ this.slug = slug;
15
+ this.name = 'UnknownLocaleError';
16
+ }
17
+ }
18
+ exports.UnknownLocaleError = UnknownLocaleError;
@@ -0,0 +1,42 @@
1
+ import type { Locale } from './value-objects/locale';
2
+ import type { LocaleSet } from './value-objects/locale-set';
3
+ /**
4
+ * The locale **domain policy** — the pure fallback-chain and publish rules of
5
+ * the i18n context, framework- and database-free so they can be unit-tested
6
+ * without any content plumbing. It reads a {@link LocaleSet}; it renders no
7
+ * SQL. The extension adapter's `listScope` builds the equivalent query, but
8
+ * the *rule* it encodes lives here as the single source of truth.
9
+ */
10
+ export declare class LocalePolicy {
11
+ private readonly locales;
12
+ constructor(locales: LocaleSet);
13
+ /**
14
+ * Resolve a request's locale slug: absent → the default locale; unknown →
15
+ * {@link UnknownLocaleError}. The uniform gate every request-facing path
16
+ * funnels through.
17
+ */
18
+ resolve(slug: string | undefined): Locale;
19
+ /**
20
+ * Whether a `localeFallback=default` list should **widen** to the default
21
+ * locale's rows: true only when default fallback is requested *and* the
22
+ * requested locale isn't already the default (nothing to widen to
23
+ * otherwise). The pure form of the extension's `listScope` strict-vs-widen
24
+ * branch.
25
+ */
26
+ shouldWidenToDefault(requested: Locale, fallbackRequested: boolean): boolean;
27
+ /**
28
+ * The ordered locales to try when reading a translation group in
29
+ * `requested`: `[requested]` in strict mode, or `[requested, default]` when
30
+ * default fallback applies ({@link shouldWidenToDefault}).
31
+ */
32
+ fallbackChain(requested: Locale, fallbackRequested: boolean): Locale[];
33
+ /**
34
+ * The locales that must hold a row before a translation group is
35
+ * considered fully published: the **default locale is always required** —
36
+ * a group can't go live without its canonical translation. A pure rule the
37
+ * publish flow can consult; the group's own per-sibling validation stays in
38
+ * the extension adapter.
39
+ */
40
+ requiredLocalesForPublish(): Locale[];
41
+ }
42
+ //# sourceMappingURL=locale-policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale-policy.d.ts","sourceRoot":"","sources":["../../../src/lib/domain/locale-policy.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AAE5D;;;;;;GAMG;AACH,qBAAa,YAAY;IACT,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,SAAS;IAE/C;;;;OAIG;IACH,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM;IAOzC;;;;;;OAMG;IACH,oBAAoB,CAChB,SAAS,EAAE,MAAM,EACjB,iBAAiB,EAAE,OAAO,GAC3B,OAAO;IAIV;;;;OAIG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,EAAE,iBAAiB,EAAE,OAAO,GAAG,MAAM,EAAE;IAMtE;;;;;;OAMG;IACH,yBAAyB,IAAI,MAAM,EAAE;CAGxC"}
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.LocalePolicy = void 0;
4
+ const errors_1 = require("./errors");
5
+ /**
6
+ * The locale **domain policy** — the pure fallback-chain and publish rules of
7
+ * the i18n context, framework- and database-free so they can be unit-tested
8
+ * without any content plumbing. It reads a {@link LocaleSet}; it renders no
9
+ * SQL. The extension adapter's `listScope` builds the equivalent query, but
10
+ * the *rule* it encodes lives here as the single source of truth.
11
+ */
12
+ class LocalePolicy {
13
+ locales;
14
+ constructor(locales) {
15
+ this.locales = locales;
16
+ }
17
+ /**
18
+ * Resolve a request's locale slug: absent → the default locale; unknown →
19
+ * {@link UnknownLocaleError}. The uniform gate every request-facing path
20
+ * funnels through.
21
+ */
22
+ resolve(slug) {
23
+ if (slug === undefined)
24
+ return this.locales.default();
25
+ const locale = this.locales.get(slug);
26
+ if (!locale)
27
+ throw new errors_1.UnknownLocaleError(slug);
28
+ return locale;
29
+ }
30
+ /**
31
+ * Whether a `localeFallback=default` list should **widen** to the default
32
+ * locale's rows: true only when default fallback is requested *and* the
33
+ * requested locale isn't already the default (nothing to widen to
34
+ * otherwise). The pure form of the extension's `listScope` strict-vs-widen
35
+ * branch.
36
+ */
37
+ shouldWidenToDefault(requested, fallbackRequested) {
38
+ return fallbackRequested && !requested.equals(this.locales.default());
39
+ }
40
+ /**
41
+ * The ordered locales to try when reading a translation group in
42
+ * `requested`: `[requested]` in strict mode, or `[requested, default]` when
43
+ * default fallback applies ({@link shouldWidenToDefault}).
44
+ */
45
+ fallbackChain(requested, fallbackRequested) {
46
+ return this.shouldWidenToDefault(requested, fallbackRequested)
47
+ ? [requested, this.locales.default()]
48
+ : [requested];
49
+ }
50
+ /**
51
+ * The locales that must hold a row before a translation group is
52
+ * considered fully published: the **default locale is always required** —
53
+ * a group can't go live without its canonical translation. A pure rule the
54
+ * publish flow can consult; the group's own per-sibling validation stays in
55
+ * the extension adapter.
56
+ */
57
+ requiredLocalesForPublish() {
58
+ return [this.locales.default()];
59
+ }
60
+ }
61
+ exports.LocalePolicy = LocalePolicy;