@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.
- package/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/lib/content/controllers/get-entry-locales.controller.d.ts +15 -0
- package/dist/lib/content/controllers/get-entry-locales.controller.d.ts.map +1 -0
- package/dist/lib/content/controllers/get-entry-locales.controller.js +45 -0
- package/dist/lib/content/controllers/locale-summary.controller.d.ts +30 -0
- package/dist/lib/content/controllers/locale-summary.controller.d.ts.map +1 -0
- package/dist/lib/content/controllers/locale-summary.controller.js +61 -0
- package/dist/lib/content/controllers/resolve-type.d.ts +9 -0
- package/dist/lib/content/controllers/resolve-type.d.ts.map +1 -0
- package/dist/lib/content/controllers/resolve-type.js +20 -0
- package/dist/lib/content/dto/locale-summary.dto.d.ts +10 -0
- package/dist/lib/content/dto/locale-summary.dto.d.ts.map +1 -0
- package/dist/lib/content/dto/locale-summary.dto.js +30 -0
- package/dist/lib/content/locale-group-lock.d.ts +34 -0
- package/dist/lib/content/locale-group-lock.d.ts.map +1 -0
- package/dist/lib/content/locale-group-lock.js +38 -0
- package/dist/lib/content/services/entry-locale-extension.service.d.ts +188 -0
- package/dist/lib/content/services/entry-locale-extension.service.d.ts.map +1 -0
- package/dist/lib/content/services/entry-locale-extension.service.js +662 -0
- package/dist/lib/content/services/locale-group.service.d.ts +87 -0
- package/dist/lib/content/services/locale-group.service.d.ts.map +1 -0
- package/dist/lib/content/services/locale-group.service.js +135 -0
- package/dist/lib/copilot/i18n-tool.provider.d.ts +53 -0
- package/dist/lib/copilot/i18n-tool.provider.d.ts.map +1 -0
- package/dist/lib/copilot/i18n-tool.provider.js +157 -0
- package/dist/lib/copilot/translation-proposal.applier.d.ts +87 -0
- package/dist/lib/copilot/translation-proposal.applier.d.ts.map +1 -0
- package/dist/lib/copilot/translation-proposal.applier.js +242 -0
- package/dist/lib/copilot/translation-proposal.provider.d.ts +114 -0
- package/dist/lib/copilot/translation-proposal.provider.d.ts.map +1 -0
- package/dist/lib/copilot/translation-proposal.provider.js +419 -0
- package/dist/lib/domain/errors/index.d.ts +4 -0
- package/dist/lib/domain/errors/index.d.ts.map +1 -0
- package/dist/lib/domain/errors/index.js +9 -0
- package/dist/lib/domain/errors/invalid-locale-set.error.d.ts +10 -0
- package/dist/lib/domain/errors/invalid-locale-set.error.d.ts.map +1 -0
- package/dist/lib/domain/errors/invalid-locale-set.error.js +16 -0
- package/dist/lib/domain/errors/invalid-locale.error.d.ts +10 -0
- package/dist/lib/domain/errors/invalid-locale.error.d.ts.map +1 -0
- package/dist/lib/domain/errors/invalid-locale.error.js +16 -0
- package/dist/lib/domain/errors/unknown-locale.error.d.ts +11 -0
- package/dist/lib/domain/errors/unknown-locale.error.d.ts.map +1 -0
- package/dist/lib/domain/errors/unknown-locale.error.js +18 -0
- package/dist/lib/domain/locale-policy.d.ts +42 -0
- package/dist/lib/domain/locale-policy.d.ts.map +1 -0
- package/dist/lib/domain/locale-policy.js +61 -0
- package/dist/lib/domain/value-objects/locale-set.d.ts +41 -0
- package/dist/lib/domain/value-objects/locale-set.d.ts.map +1 -0
- package/dist/lib/domain/value-objects/locale-set.js +89 -0
- package/dist/lib/domain/value-objects/locale.d.ts +55 -0
- package/dist/lib/domain/value-objects/locale.d.ts.map +1 -0
- package/dist/lib/domain/value-objects/locale.js +73 -0
- package/dist/lib/i18n.constants.d.ts +33 -0
- package/dist/lib/i18n.constants.d.ts.map +1 -0
- package/dist/lib/i18n.constants.js +89 -0
- package/dist/lib/i18n.module.d.ts +14 -0
- package/dist/lib/i18n.module.d.ts.map +1 -0
- package/dist/lib/i18n.module.js +82 -0
- package/dist/lib/insights/http/controllers/localization-coverage.controller.d.ts +23 -0
- package/dist/lib/insights/http/controllers/localization-coverage.controller.d.ts.map +1 -0
- package/dist/lib/insights/http/controllers/localization-coverage.controller.js +46 -0
- package/dist/lib/insights/infrastructure/queries/localization-coverage.query.d.ts +59 -0
- package/dist/lib/insights/infrastructure/queries/localization-coverage.query.d.ts.map +1 -0
- package/dist/lib/insights/infrastructure/queries/localization-coverage.query.js +179 -0
- package/dist/lib/insights/types/i18n-insights-view.d.ts +83 -0
- package/dist/lib/insights/types/i18n-insights-view.d.ts.map +1 -0
- package/dist/lib/insights/types/i18n-insights-view.js +10 -0
- package/dist/lib/locales/controllers/list-locales.controller.d.ts +26 -0
- package/dist/lib/locales/controllers/list-locales.controller.d.ts.map +1 -0
- package/dist/lib/locales/controllers/list-locales.controller.js +48 -0
- package/dist/lib/locales/services/locale-registry.service.d.ts +26 -0
- package/dist/lib/locales/services/locale-registry.service.d.ts.map +1 -0
- package/dist/lib/locales/services/locale-registry.service.js +70 -0
- package/dist/lib/locales/services/orphaned-locale.checker.d.ts +60 -0
- package/dist/lib/locales/services/orphaned-locale.checker.d.ts.map +1 -0
- package/dist/lib/locales/services/orphaned-locale.checker.js +114 -0
- package/dist/lib/types/locale.d.ts +68 -0
- package/dist/lib/types/locale.d.ts.map +1 -0
- package/dist/lib/types/locale.js +8 -0
- package/dist/lib/utils/i18n-plugin.d.ts +41 -0
- package/dist/lib/utils/i18n-plugin.d.ts.map +1 -0
- package/dist/lib/utils/i18n-plugin.js +53 -0
- package/package.json +47 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BulkTranslationProposalApplier = exports.TranslationProposalApplier = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
const drizzle_orm_1 = require("drizzle-orm");
|
|
7
|
+
const database_1 = require("@orthacms/database");
|
|
8
|
+
const content_server_1 = require("@orthacms/content-server");
|
|
9
|
+
const translation_proposal_provider_1 = require("./translation-proposal.provider");
|
|
10
|
+
/**
|
|
11
|
+
* Applies `i18n.entry.translate` by creating the sibling row through
|
|
12
|
+
* {@link EntryWriterService.create} — the same call `POST /api/content/:type`
|
|
13
|
+
* makes, with a `localeGroupId`.
|
|
14
|
+
*
|
|
15
|
+
* **There is no "create translation" write path to reuse, and that is the
|
|
16
|
+
* point**: joining an existing group is what `localeGroupId` on a create
|
|
17
|
+
* already means, and the bound entry extension stamps and validates it inside
|
|
18
|
+
* the write transaction. So this applier passes two extra arguments to the
|
|
19
|
+
* ordinary create rather than reaching for a second mechanism — which is also
|
|
20
|
+
* what makes a duplicate `(group, locale)` a clean 409 instead of a corrupt
|
|
21
|
+
* group.
|
|
22
|
+
*
|
|
23
|
+
* ## Why it reads the source entry first
|
|
24
|
+
*
|
|
25
|
+
* The propose tool accepts **only localized values** — a shared field is one
|
|
26
|
+
* value across the group, so asking a model to translate it is meaningless. But
|
|
27
|
+
* a create is a whole row: `coerceValues` stamps every declared field onto the
|
|
28
|
+
* bag, so a shared field nobody supplied arrives as `null` rather than as
|
|
29
|
+
* "absent". Two things then go wrong at once, and neither is obvious:
|
|
30
|
+
*
|
|
31
|
+
* 1. The new row fails its own required-field validation — `tag.slug` is
|
|
32
|
+
* required and shared, and no translation of a tag ever carries it.
|
|
33
|
+
* 2. Worse, the bound extension treats that `null` as a shared value the save
|
|
34
|
+
* *carries* and pushes it onto every sibling in the group. A published
|
|
35
|
+
* sibling is re-validated and fails, which rolls the create back — but a
|
|
36
|
+
* group whose siblings are all drafts has no such guard, and the shared
|
|
37
|
+
* field is silently blanked in every locale.
|
|
38
|
+
*
|
|
39
|
+
* So the values the create is given are the **source entry's**, with the
|
|
40
|
+
* translated ones layered on top: the same thing the admin's own "create
|
|
41
|
+
* translation" does (it sends the source's values), and the reason that path
|
|
42
|
+
* has never hit this. The shared values then match the group exactly, the
|
|
43
|
+
* extension's `IS DISTINCT FROM` predicate finds nothing to sync, and no
|
|
44
|
+
* sibling is touched at all.
|
|
45
|
+
*
|
|
46
|
+
* Read at **apply** time rather than frozen into the proposal: a shared field
|
|
47
|
+
* can be edited between drafting and applying, and the group's current value is
|
|
48
|
+
* the one that must not be clobbered.
|
|
49
|
+
*/
|
|
50
|
+
let TranslationProposalApplier = class TranslationProposalApplier {
|
|
51
|
+
registry;
|
|
52
|
+
writer;
|
|
53
|
+
grants;
|
|
54
|
+
db;
|
|
55
|
+
kind = translation_proposal_provider_1.I18N_PROPOSAL_KINDS.createTranslation;
|
|
56
|
+
constructor(registry, writer, grants, db) {
|
|
57
|
+
this.registry = registry;
|
|
58
|
+
this.writer = writer;
|
|
59
|
+
this.grants = grants;
|
|
60
|
+
this.db = db;
|
|
61
|
+
}
|
|
62
|
+
async apply(input, actor) {
|
|
63
|
+
const typeName = input.target['typeName'];
|
|
64
|
+
const locale = input.target['locale'];
|
|
65
|
+
const localeGroupId = input.target['localeGroupId'];
|
|
66
|
+
const sourceId = input.target['sourceId'];
|
|
67
|
+
if (typeof typeName !== 'string' ||
|
|
68
|
+
typeof locale !== 'string' ||
|
|
69
|
+
typeof localeGroupId !== 'string' ||
|
|
70
|
+
typeof sourceId !== 'string') {
|
|
71
|
+
throw new Error('This proposal is missing its translation target.');
|
|
72
|
+
}
|
|
73
|
+
// Re-checked rather than trusted from the row: grants can be revoked
|
|
74
|
+
// between proposing and applying, and a stored type name is an
|
|
75
|
+
// argument like any other.
|
|
76
|
+
const type = this.registry.get(typeName);
|
|
77
|
+
const granted = await this.grants.grantedSlugs(actor.workspaceId);
|
|
78
|
+
if (!type || !granted.has(type.name)) {
|
|
79
|
+
throw new Error(`Unknown content type "${typeName}" in this workspace.`);
|
|
80
|
+
}
|
|
81
|
+
const translated = (input.patch['values'] ?? {});
|
|
82
|
+
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
|
+
return {
|
|
85
|
+
entityId: entry.id,
|
|
86
|
+
detail: { typeName: type.name, locale, localeGroupId }
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
exports.TranslationProposalApplier = TranslationProposalApplier;
|
|
91
|
+
exports.TranslationProposalApplier = TranslationProposalApplier = tslib_1.__decorate([
|
|
92
|
+
(0, common_1.Injectable)(),
|
|
93
|
+
tslib_1.__param(0, (0, content_server_1.InjectContentRegistry)()),
|
|
94
|
+
tslib_1.__param(3, (0, database_1.InjectDatabase)()),
|
|
95
|
+
tslib_1.__metadata("design:paramtypes", [Function, content_server_1.EntryWriterService,
|
|
96
|
+
content_server_1.WorkspaceGrantsQuery, Object])
|
|
97
|
+
], TranslationProposalApplier);
|
|
98
|
+
/**
|
|
99
|
+
* The source entry's **shared** field values — everything the new sibling must
|
|
100
|
+
* carry over rather than invent.
|
|
101
|
+
*
|
|
102
|
+
* The exclusions mirror what the extension itself treats as shared, because a
|
|
103
|
+
* value copied here that the extension does *not* consider shared would be a
|
|
104
|
+
* per-locale field silently seeded from another language:
|
|
105
|
+
*
|
|
106
|
+
* - **Localized fields** are what the translation supplies.
|
|
107
|
+
* - **Many and inverse relations** are join-backed rather than column-backed,
|
|
108
|
+
* so `toRecord` already drops them; the extension's own sync is what carries
|
|
109
|
+
* them onto the new sibling.
|
|
110
|
+
* - **Any relation whose stored id differs per locale** — mirrored or unsynced
|
|
111
|
+
* (`isPerLocaleField`). Copying the source's would point the German row at an
|
|
112
|
+
* English one, which the writer rejects as a cross-locale link; for a
|
|
113
|
+
* mirrored relation the extension resolves the right per-locale id itself,
|
|
114
|
+
* after the insert.
|
|
115
|
+
*
|
|
116
|
+
* A free function rather than a method because **both** appliers need it, and
|
|
117
|
+
* needing it is not optional: it is the whole difference between joining a
|
|
118
|
+
* translation group and blanking it.
|
|
119
|
+
*/
|
|
120
|
+
async function sharedValues(db, type, sourceId, workspaceId) {
|
|
121
|
+
const table = type.table;
|
|
122
|
+
const [row] = await db
|
|
123
|
+
.select()
|
|
124
|
+
.from(type.table)
|
|
125
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(table['id'], sourceId), (0, drizzle_orm_1.eq)(table['workspaceId'], workspaceId), ...(type.paranoid ? [(0, drizzle_orm_1.isNull)(table['deletedAt'])] : [])))
|
|
126
|
+
.limit(1);
|
|
127
|
+
if (!row) {
|
|
128
|
+
// The propose tool proved this row existed; by apply time it may not.
|
|
129
|
+
// Failing here beats creating a translation of nothing.
|
|
130
|
+
throw new Error('The entry this translates no longer exists.');
|
|
131
|
+
}
|
|
132
|
+
const source = (0, content_server_1.toRecord)(type, row).values;
|
|
133
|
+
const shared = {};
|
|
134
|
+
for (const [name, spec] of Object.entries(type.fields)) {
|
|
135
|
+
if ((0, content_server_1.isPerLocaleField)(type, spec))
|
|
136
|
+
continue;
|
|
137
|
+
if (!(name in source))
|
|
138
|
+
continue;
|
|
139
|
+
shared[name] = source[name];
|
|
140
|
+
}
|
|
141
|
+
return shared;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Applies `i18n.entry.bulk-translate` — several translations carried out as one
|
|
145
|
+
* change.
|
|
146
|
+
*
|
|
147
|
+
* **Item by item through the same `EntryWriterService.create` the single-entry
|
|
148
|
+
* applier calls**, each with its own source entry's shared values inherited: a
|
|
149
|
+
* batch is a way of asking for N translations, not a second way of writing one.
|
|
150
|
+
* Sequentially rather than concurrently, because every write takes the
|
|
151
|
+
* workspace's shared content lock and (on a localized type) an advisory lock
|
|
152
|
+
* over the translation group — a batch translating one record into five
|
|
153
|
+
* languages contends with itself by construction, so a fan-out would buy
|
|
154
|
+
* nothing but lock waits.
|
|
155
|
+
*
|
|
156
|
+
* **It stops at the first failure and throws**, exactly as the content plugin's
|
|
157
|
+
* bulk applier does. A proposal is one row with one status, so there is no
|
|
158
|
+
* per-item verdict to render; carrying on would grow the number of rows written
|
|
159
|
+
* under a receipt that then reports failure. The error names how many landed,
|
|
160
|
+
* which is what the model reads back to the user.
|
|
161
|
+
*/
|
|
162
|
+
let BulkTranslationProposalApplier = class BulkTranslationProposalApplier {
|
|
163
|
+
registry;
|
|
164
|
+
writer;
|
|
165
|
+
grants;
|
|
166
|
+
db;
|
|
167
|
+
kind = translation_proposal_provider_1.I18N_PROPOSAL_KINDS.bulkTranslation;
|
|
168
|
+
constructor(registry, writer, grants, db) {
|
|
169
|
+
this.registry = registry;
|
|
170
|
+
this.writer = writer;
|
|
171
|
+
this.grants = grants;
|
|
172
|
+
this.db = db;
|
|
173
|
+
}
|
|
174
|
+
async apply(input, actor) {
|
|
175
|
+
const typeName = input.target['typeName'];
|
|
176
|
+
if (typeof typeName !== 'string') {
|
|
177
|
+
throw new Error('This proposal is missing its content type.');
|
|
178
|
+
}
|
|
179
|
+
// Re-checked rather than trusted from the row: grants can be revoked
|
|
180
|
+
// between proposing and applying, and a stored type name is an
|
|
181
|
+
// argument like any other.
|
|
182
|
+
const type = this.registry.get(typeName);
|
|
183
|
+
const granted = await this.grants.grantedSlugs(actor.workspaceId);
|
|
184
|
+
if (!type || !granted.has(type.name)) {
|
|
185
|
+
throw new Error(`Unknown content type "${typeName}" in this workspace.`);
|
|
186
|
+
}
|
|
187
|
+
const items = input.patch['items'];
|
|
188
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
189
|
+
throw new Error('This change names no translations.');
|
|
190
|
+
}
|
|
191
|
+
const created = [];
|
|
192
|
+
const locales = [];
|
|
193
|
+
for (const [index, raw] of items.entries()) {
|
|
194
|
+
const item = (raw ?? {});
|
|
195
|
+
const sourceId = item.sourceId;
|
|
196
|
+
const localeGroupId = item.localeGroupId;
|
|
197
|
+
const locale = item.locale;
|
|
198
|
+
if (typeof sourceId !== 'string' ||
|
|
199
|
+
typeof localeGroupId !== 'string' ||
|
|
200
|
+
typeof locale !== 'string') {
|
|
201
|
+
throw new Error(`Translation ${index + 1} of ${items.length} is missing its target.`);
|
|
202
|
+
}
|
|
203
|
+
try {
|
|
204
|
+
// Read per item and at apply time, not once for the batch: two
|
|
205
|
+
// items may translate two different records, and a shared field
|
|
206
|
+
// edited between drafting and applying must not be clobbered by
|
|
207
|
+
// a stale copy of itself.
|
|
208
|
+
const inherited = await sharedValues(this.db, type, sourceId, actor.workspaceId);
|
|
209
|
+
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
|
+
created.push(entry.id);
|
|
212
|
+
locales.push(locale);
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
throw new Error(`Translation ${index + 1} of ${items.length} (${locale}) failed: ` +
|
|
216
|
+
`${error instanceof Error ? error.message : String(error)}. ` +
|
|
217
|
+
(created.length === 0
|
|
218
|
+
? 'Nothing was saved.'
|
|
219
|
+
: `The first ${created.length} were saved and the rest were not.`));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
// No `entityId`: a batch has no single entity to link to, and
|
|
224
|
+
// naming one of them would send the user to an arbitrary member of
|
|
225
|
+
// the set. The ids are in `detail` instead.
|
|
226
|
+
detail: {
|
|
227
|
+
typeName: type.name,
|
|
228
|
+
created,
|
|
229
|
+
locales,
|
|
230
|
+
count: created.length
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
exports.BulkTranslationProposalApplier = BulkTranslationProposalApplier;
|
|
236
|
+
exports.BulkTranslationProposalApplier = BulkTranslationProposalApplier = tslib_1.__decorate([
|
|
237
|
+
(0, common_1.Injectable)(),
|
|
238
|
+
tslib_1.__param(0, (0, content_server_1.InjectContentRegistry)()),
|
|
239
|
+
tslib_1.__param(3, (0, database_1.InjectDatabase)()),
|
|
240
|
+
tslib_1.__metadata("design:paramtypes", [Function, content_server_1.EntryWriterService,
|
|
241
|
+
content_server_1.WorkspaceGrantsQuery, Object])
|
|
242
|
+
], BulkTranslationProposalApplier);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { type OnModuleInit } from '@nestjs/common';
|
|
2
|
+
import { WorkspaceGrantsQuery, type ContentTypeRegistry } from '@orthacms/content-server';
|
|
3
|
+
import { ToolRegistry } from '@orthacms/tools-server';
|
|
4
|
+
import type { ToolDefinition, ToolProvider } from '@orthacms/tools-server';
|
|
5
|
+
import { LocaleRegistryService } from '../locales/services/locale-registry.service';
|
|
6
|
+
import { LocaleGroupService } from '../content/services/locale-group.service';
|
|
7
|
+
/** The proposal kinds this plugin declares and applies. */
|
|
8
|
+
export declare const I18N_PROPOSAL_KINDS: {
|
|
9
|
+
/** Create a sibling row of an entry in another locale. */
|
|
10
|
+
readonly createTranslation: "i18n.entry.translate";
|
|
11
|
+
/**
|
|
12
|
+
* Create several sibling rows — more locales of one record, more records
|
|
13
|
+
* in one locale, or both — as **one** change.
|
|
14
|
+
*
|
|
15
|
+
* Its own kind rather than a repeated `createTranslation`, for the reason
|
|
16
|
+
* `content.entry.bulk-save` is: a proposal row is the receipt for one tool
|
|
17
|
+
* call, and "translate this into five languages" written as five rows is
|
|
18
|
+
* five cards in the transcript for a single instruction, none of which says
|
|
19
|
+
* what the other four were.
|
|
20
|
+
*/
|
|
21
|
+
readonly bulkTranslation: "i18n.entry.bulk-translate";
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The plugin's **write** tools: `i18n_propose_translation` for one locale of one
|
|
25
|
+
* entry, and `i18n_propose_bulk_translation` for a batch of them.
|
|
26
|
+
*
|
|
27
|
+
* Like every propose tool their handlers **write nothing** — they compute the
|
|
28
|
+
* change and hand it back, and the run engine records and applies it. What they
|
|
29
|
+
* do is the part a model cannot be trusted to get right on its own: resolve the
|
|
30
|
+
* locale slug against the configured set, confirm the target locale does not
|
|
31
|
+
* already exist in the group (a duplicate is a 409 at apply time, after the
|
|
32
|
+
* card has said the change is happening), and name the source entry so the
|
|
33
|
+
* applier can inherit its shared values.
|
|
34
|
+
*
|
|
35
|
+
* Only **localized** fields are translated. A field the type declares as shared
|
|
36
|
+
* is one value across the whole group by definition — writing a per-locale
|
|
37
|
+
* version of it would either be ignored or silently overwrite every sibling,
|
|
38
|
+
* and neither is something to show a reader as a translation.
|
|
39
|
+
*/
|
|
40
|
+
export declare class TranslationProposalToolProvider implements ToolProvider, OnModuleInit {
|
|
41
|
+
private readonly registry;
|
|
42
|
+
private readonly locales;
|
|
43
|
+
private readonly groups;
|
|
44
|
+
private readonly grants;
|
|
45
|
+
private readonly toolRegistry?;
|
|
46
|
+
constructor(registry: ContentTypeRegistry, locales: LocaleRegistryService, groups: LocaleGroupService, grants: WorkspaceGrantsQuery, toolRegistry?: ToolRegistry | undefined);
|
|
47
|
+
/**
|
|
48
|
+
* Register with the shared tool registry once the DI graph is built —
|
|
49
|
+
* the same catalogue the MCP endpoint serves, narrowed to the `copilot`
|
|
50
|
+
* surface by each tool's `surfaces`. `@Optional()` because a deployment
|
|
51
|
+
* may run neither consumer, in which case these simply go unregistered.
|
|
52
|
+
*/
|
|
53
|
+
onModuleInit(): void;
|
|
54
|
+
/** The two i18n write tools — one translation, and the batch. */
|
|
55
|
+
tools(): readonly ToolDefinition[];
|
|
56
|
+
/**
|
|
57
|
+
* A granted, registered, **localized** type — the gate both write tools
|
|
58
|
+
* open with.
|
|
59
|
+
*
|
|
60
|
+
* The two failures are worth separating: "unknown type" is the uniform
|
|
61
|
+
* answer every content tool gives a name it cannot reach (it must not
|
|
62
|
+
* distinguish "no such type" from "not granted here"), while "not
|
|
63
|
+
* localized" describes a type the caller has already proved it can reach
|
|
64
|
+
* and is safe — and useful — to say out loud.
|
|
65
|
+
*/
|
|
66
|
+
private resolveLocalizedType;
|
|
67
|
+
/**
|
|
68
|
+
* The type's **localized** fields, keyed by name — the only ones a
|
|
69
|
+
* translation may carry.
|
|
70
|
+
*
|
|
71
|
+
* A field the type declares as shared is one value across the whole group
|
|
72
|
+
* by definition: writing a per-locale version of it would be fanned back
|
|
73
|
+
* out over every sibling by this plugin's own sync, so a "translated"
|
|
74
|
+
* shared field silently rewrites every other language.
|
|
75
|
+
*/
|
|
76
|
+
private localizedFields;
|
|
77
|
+
/**
|
|
78
|
+
* Rejects anything in `values` that is not a localized field, and an empty
|
|
79
|
+
* bag — the check that makes a translation tool a translation tool.
|
|
80
|
+
*
|
|
81
|
+
* `prefix` names which item is at fault in a batch, and is empty for the
|
|
82
|
+
* single-entry tool so its message is unchanged.
|
|
83
|
+
*/
|
|
84
|
+
private assertTranslatable;
|
|
85
|
+
/** The field's admin label, when it has one — for the card's diff rows. */
|
|
86
|
+
private labelOf;
|
|
87
|
+
/** `i18n_propose_translation` — one entry into one locale. */
|
|
88
|
+
private proposeTranslation;
|
|
89
|
+
/**
|
|
90
|
+
* `i18n_propose_bulk_translation` — several translations as **one** change.
|
|
91
|
+
*
|
|
92
|
+
* The single-entry tool scales badly in both directions users actually push
|
|
93
|
+
* it: "translate this into German, French and Spanish" is three calls, and
|
|
94
|
+
* "translate these eight posts into German" is eight. Each one spent a step
|
|
95
|
+
* of a bounded run and left its own card, so one instruction read back as a
|
|
96
|
+
* pile of receipts, none of which said what the others were.
|
|
97
|
+
*
|
|
98
|
+
* **Why this rather than `content_propose_bulk_save` with a
|
|
99
|
+
* `localeGroupId`.** That is what a model reached for before, and it is the
|
|
100
|
+
* shape that quietly breaks a group: a create is a whole row, so every
|
|
101
|
+
* shared field the batch did not name arrives as `null`, and this plugin's
|
|
102
|
+
* sync then pushes those nulls onto every sibling — see
|
|
103
|
+
* {@link TranslationProposalApplier}. Joining a translation group needs the
|
|
104
|
+
* source's shared values inherited at apply time, which is knowledge that
|
|
105
|
+
* lives here, so the content tools no longer offer `localeGroupId` at all
|
|
106
|
+
* and this is the one route in.
|
|
107
|
+
*
|
|
108
|
+
* An item is one **(source entry, target locale)** pair, so both shapes are
|
|
109
|
+
* the same list: five items naming one `id` and five locales, eight naming
|
|
110
|
+
* eight ids and one locale, or any mixture.
|
|
111
|
+
*/
|
|
112
|
+
private proposeBulkTranslation;
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=translation-proposal.provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translation-proposal.provider.d.ts","sourceRoot":"","sources":["../../../src/lib/copilot/translation-proposal.provider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEzE,OAAO,EAGH,oBAAoB,EACpB,KAAK,mBAAmB,EAE3B,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC3E,OAAO,EAAE,qBAAqB,EAAE,MAAM,6CAA6C,CAAC;AACpF,OAAO,EACH,kBAAkB,EAErB,MAAM,0CAA0C,CAAC;AAElD,2DAA2D;AAC3D,eAAO,MAAM,mBAAmB;IAC5B,0DAA0D;;IAE1D;;;;;;;;;OASG;;CAEG,CAAC;AAEX;;;;;;;;;;;;;;;;GAgBG;AACH,qBACa,+BACT,YAAW,YAAY,EAAE,YAAY;IAIjC,OAAO,CAAC,QAAQ,CAAC,QAAQ;IACzB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACX,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;gBAJzB,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,qBAAqB,EAC9B,MAAM,EAAE,kBAAkB,EAC1B,MAAM,EAAE,oBAAoB,EAChB,YAAY,CAAC,EAAE,YAAY,YAAA;IAG5D;;;;;OAKG;IACH,YAAY,IAAI,IAAI;IAIpB,iEAAiE;IACjE,KAAK,IAAI,SAAS,cAAc,EAAE;IAIlC;;;;;;;;;OASG;YACW,oBAAoB;IAgBlC;;;;;;;;OAQG;IACH,OAAO,CAAC,eAAe;IASvB;;;;;;OAMG;IACH,OAAO,CAAC,kBAAkB;IAoB1B,2EAA2E;IAC3E,OAAO,CAAC,OAAO;IAKf,8DAA8D;IAC9D,OAAO,CAAC,kBAAkB;IA8H1B;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,OAAO,CAAC,sBAAsB;CA2MjC"}
|