@elytracms/studio 0.0.102 → 0.0.104

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 (31) hide show
  1. package/convex/ingest.test.ts +251 -23
  2. package/convex/ingest.ts +77 -19
  3. package/dist/components/ai/translate-document-dialog.d.ts +43 -0
  4. package/dist/components/ai/translate-document-dialog.js +104 -36
  5. package/dist/components/ai/translate-document-dialog.js.map +1 -1
  6. package/dist/components/ai/translate-skipped.d.ts +19 -3
  7. package/dist/components/ai/translate-skipped.js +33 -7
  8. package/dist/components/ai/translate-skipped.js.map +1 -1
  9. package/dist/components/builder/canvas-theme-notice.js +4 -1
  10. package/dist/components/builder/canvas-theme-notice.js.map +1 -1
  11. package/dist/components/components/components-index.d.ts +0 -7
  12. package/dist/components/components/components-index.js +9 -2
  13. package/dist/components/components/components-index.js.map +1 -1
  14. package/dist/lib/ai/translate-apply.d.ts +69 -0
  15. package/dist/lib/ai/translate-apply.js +36 -0
  16. package/dist/lib/ai/translate-apply.js.map +1 -0
  17. package/dist/lib/ai/translate-document.d.ts +95 -7
  18. package/dist/lib/ai/translate-document.js +195 -57
  19. package/dist/lib/ai/translate-document.js.map +1 -1
  20. package/dist/lib/ai/writer-form.d.ts +33 -2
  21. package/dist/lib/ai/writer-form.js +188 -27
  22. package/dist/lib/ai/writer-form.js.map +1 -1
  23. package/dist/lib/engine-version.d.ts +1 -1
  24. package/dist/lib/engine-version.js +1 -1
  25. package/dist/lib/engine-version.js.map +1 -1
  26. package/dist/lib/i18n/messages.de.js +56 -10
  27. package/dist/lib/i18n/messages.de.js.map +1 -1
  28. package/dist/lib/i18n/messages.en.d.ts +41 -1
  29. package/dist/lib/i18n/messages.en.js +61 -10
  30. package/dist/lib/i18n/messages.en.js.map +1 -1
  31. package/package.json +3 -3
@@ -224,6 +224,7 @@ declare const MESSAGES: {
224
224
  'operations.error.strictReferencesBlockDeleteOne': import("./types.js").SourceMessage;
225
225
  'operations.error.referencesBlockDelete': import("./types.js").SourceMessage;
226
226
  'operations.error.referencesBlockDeleteOne': import("./types.js").SourceMessage;
227
+ 'operations.error.referenceChangedBack': import("./types.js").SourceMessage;
227
228
  'operations.error.strictReferencesBlockUnpublish': import("./types.js").SourceMessage;
228
229
  'operations.error.strictReferencesBlockUnpublishOne': import("./types.js").SourceMessage;
229
230
  'operations.error.strictTargetsUnpublishedOne': import("./types.js").SourceMessage;
@@ -232,7 +233,17 @@ declare const MESSAGES: {
232
233
  'operations.error.versionNotPinnable': import("./types.js").SourceMessage;
233
234
  'operations.error.changesetBlocked': import("./types.js").SourceMessage;
234
235
  'operations.error.changesetDeleteNotReversible': import("./types.js").SourceMessage;
236
+ 'operations.error.revertReferencedDeleteOne': import("./types.js").SourceMessage;
237
+ 'operations.error.revertReferencedDelete': import("./types.js").SourceMessage;
238
+ 'operations.error.revertStrictReferencedDeleteOne': import("./types.js").SourceMessage;
239
+ 'operations.error.revertStrictReferencedDelete': import("./types.js").SourceMessage;
240
+ 'operations.error.revertUnrecorded': import("./types.js").SourceMessage;
241
+ 'operations.error.revertChangedSince': import("./types.js").SourceMessage;
242
+ 'operations.error.revertGone': import("./types.js").SourceMessage;
235
243
  'operations.errorDetail.andMore': import("./types.js").SourceMessage;
244
+ 'operations.errorDetail.changesetStepRaced': import("./types.js").SourceMessage;
245
+ 'operations.errorDetail.changesetPatchNotInvertible': import("./types.js").SourceMessage;
246
+ 'operations.errorDetail.changesetNoInverse': import("./types.js").SourceMessage;
236
247
  'content.queryError.nonFilterableField': import("./types.js").SourceMessage;
237
248
  'content.queryError.unknownField': import("./types.js").SourceMessage;
238
249
  'content.queryError.unknownCollection': import("./types.js").SourceMessage;
@@ -533,10 +544,13 @@ declare const MESSAGES: {
533
544
  'cms.issueMessage.routeConflict': import("./types.js").SourceMessage;
534
545
  'cms.issueMessage.redirectLoop': import("./types.js").SourceMessage;
535
546
  'cms.issueMessage.duplicateRedirect': import("./types.js").SourceMessage;
547
+ 'cms.issueMessage.redirectFromMalformed': import("./types.js").SourceMessage;
536
548
  'cms.issueMessage.redirectUnreadable': import("./types.js").SourceMessage;
537
549
  'cms.issueMessage.redirectReportUnreadable': import("./types.js").SourceMessage;
538
550
  'cms.issueMessage.allDynamicShadowsMount': import("./types.js").SourceMessage;
539
551
  'cms.issueMessage.mountCannotServe': import("./types.js").SourceMessage;
552
+ 'cms.issueMessage.routeSegmentNotText': import("./types.js").SourceMessage;
553
+ 'cms.issueMessage.routePatternDelimiter': import("./types.js").SourceMessage;
540
554
  'cms.issueMessage.deadCatchAll': import("./types.js").SourceMessage;
541
555
  'cms.issueMessage.mountConflict': import("./types.js").SourceMessage;
542
556
  'cms.issueMessage.routeBoundDocumentNotFound': import("./types.js").SourceMessage;
@@ -1144,15 +1158,35 @@ declare const MESSAGES: {
1144
1158
  'ai.translate.targetsLabel': import("./types.js").SourceMessage;
1145
1159
  'ai.translate.scopeLabel': import("./types.js").SourceMessage;
1146
1160
  'ai.translate.fillMissing': import("./types.js").SourceMessage;
1161
+ 'ai.translate.fillMissingHint': import("./types.js").SourceMessage;
1147
1162
  'ai.translate.retranslateAll': import("./types.js").SourceMessage;
1148
- 'ai.translate.retranslateSoon': import("./types.js").SourceMessage;
1163
+ 'ai.translate.retranslateHint': import("./types.js").SourceMessage;
1164
+ 'ai.translate.planReplaceTitle': import("./types.js").SourceMessage;
1165
+ 'ai.translate.planReplaceNone': import("./types.js").SourceMessage;
1166
+ 'ai.translate.planFillTitle': import("./types.js").SourceMessage;
1167
+ 'ai.translate.planWhole': import("./types.js").SourceMessage;
1168
+ 'ai.translate.planSlugsKept': import("./types.js").SourceMessage;
1169
+ 'ai.translate.planRestorePoint': import("./types.js").SourceMessage;
1170
+ 'ai.translate.planKeptTitle': import("./types.js").SourceMessage;
1149
1171
  'ai.translate.confirm': import("./types.js").SourceMessage;
1172
+ 'ai.translate.confirmReplace': import("./types.js").SourceMessage;
1150
1173
  'ai.translate.preparing': import("./types.js").SourceMessage;
1151
1174
  'ai.translate.progress': import("./types.js").SourceMessage;
1152
1175
  'ai.translate.applying': import("./types.js").SourceMessage;
1176
+ 'ai.translate.snapshotLabel': import("./types.js").SourceMessage;
1177
+ 'ai.translate.snapshotFailed': import("./types.js").SourceMessage;
1153
1178
  'ai.translate.nothing': import("./types.js").SourceMessage;
1179
+ 'ai.translate.retranslateNothing': import("./types.js").SourceMessage;
1154
1180
  'ai.translate.applied': import("./types.js").SourceMessage;
1181
+ 'ai.translate.retranslateDone': import("./types.js").SourceMessage;
1182
+ 'ai.translate.replacedTitle': import("./types.js").SourceMessage;
1183
+ 'ai.translate.filledTitle': import("./types.js").SourceMessage;
1184
+ 'ai.translate.slugsTitle': import("./types.js").SourceMessage;
1185
+ 'ai.translate.keptTitle': import("./types.js").SourceMessage;
1186
+ 'ai.translate.refusedTitle': import("./types.js").SourceMessage;
1187
+ 'ai.translate.restorePoint': import("./types.js").SourceMessage;
1155
1188
  'ai.translate.cancelled': import("./types.js").SourceMessage;
1189
+ 'ai.translate.retranslateCancelled': import("./types.js").SourceMessage;
1156
1190
  'ai.translate.failed': import("./types.js").SourceMessage;
1157
1191
  'ai.translate.close': import("./types.js").SourceMessage;
1158
1192
  'ai.translate.proposalTitle': import("./types.js").SourceMessage;
@@ -1162,6 +1196,9 @@ declare const MESSAGES: {
1162
1196
  'ai.translate.skipped.unreadable': import("./types.js").SourceMessage;
1163
1197
  'ai.translate.skipped.notReturned': import("./types.js").SourceMessage;
1164
1198
  'ai.translate.skipped.structureChanged': import("./types.js").SourceMessage;
1199
+ 'ai.translate.skipped.retranslate.unreadable': import("./types.js").SourceMessage;
1200
+ 'ai.translate.skipped.retranslate.notReturned': import("./types.js").SourceMessage;
1201
+ 'ai.translate.skipped.retranslate.structureChanged': import("./types.js").SourceMessage;
1165
1202
  'ai.translate.skipped.locked': import("./types.js").SourceMessage;
1166
1203
  'ai.update.proposalTitle': import("./types.js").SourceMessage;
1167
1204
  'ai.update.diffBefore': import("./types.js").SourceMessage;
@@ -1179,12 +1216,15 @@ declare const MESSAGES: {
1179
1216
  'cms.issueMessage.propsNotJudged': import("./types.js").SourceMessage;
1180
1217
  'cms.issueMessage.assetHoldsUrl': import("./types.js").SourceMessage;
1181
1218
  'cms.issueMessage.slugNotSegment': import("./types.js").SourceMessage;
1219
+ 'cms.issueMessage.slugNotAddressable': import("./types.js").SourceMessage;
1220
+ 'cms.issueMessage.slugNotEncodable': import("./types.js").SourceMessage;
1182
1221
  'cms.issueMessage.emptyAllow': import("./types.js").SourceMessage;
1183
1222
  'cms.issueMessage.unknownAllowComponent': import("./types.js").SourceMessage;
1184
1223
  'cms.issueMessage.blocksNotTopLevel': import("./types.js").SourceMessage;
1185
1224
  'cms.issueMessage.placementKeyReserved': import("./types.js").SourceMessage;
1186
1225
  'cms.issueMessage.placementNotScalar': import("./types.js").SourceMessage;
1187
1226
  'cms.issueMessage.sourceNotOnAssetCollection': import("./types.js").SourceMessage;
1227
+ 'cms.issueMessage.blocksOnAssetCollection': import("./types.js").SourceMessage;
1188
1228
  'cms.issueMessage.sourceTypeMismatch': import("./types.js").SourceMessage;
1189
1229
  'cms.issueMessage.uniqueNotTopLevel': import("./types.js").SourceMessage;
1190
1230
  'cms.issueMessage.localizedOnProp': import("./types.js").SourceMessage;
@@ -294,6 +294,8 @@ const MESSAGES = {
294
294
  // EC-739 — an UNREFERENCED delete (a cleanup, e.g. Make reusable's Undo): refused by a reference of any strictness.
295
295
  'operations.error.referencesBlockDelete': msg('Cannot delete “{target}”: {count} documents reference it — {named}{more}. This delete runs only while nothing references it.', '{named} is an identifier list “collection:id” (field); {more} is the andMore fragment or empty.'),
296
296
  'operations.error.referencesBlockDeleteOne': msg('Cannot delete “{target}”: {count} document references it — {named}. This delete runs only while nothing references it.'),
297
+ // EC-745 item 2 — the store refused a write on a reference precondition, and the fresh re-judge found nothing wrong.
298
+ 'operations.error.referenceChangedBack': msg('“{target}” was not written: a reference it depends on changed while the write was on its way, and has changed back since — try again.'),
297
299
  'operations.error.strictReferencesBlockUnpublish': msg('Cannot unpublish “{target}”: {count} documents reference it through strict relation fields — {named}{more}. Remove these references first.'),
298
300
  'operations.error.strictReferencesBlockUnpublishOne': msg('Cannot unpublish “{target}”: {count} document references it through strict relation fields — {named}. Remove these references first.'),
299
301
  // EC-589 — the referrer direction of `strict: true`: publish refused while a strict target is missing or unpublished.
@@ -304,7 +306,19 @@ const MESSAGES = {
304
306
  // EC-648 — changesets: apply blocked by an issue, and the step a changeset refuses because no undo can reverse it.
305
307
  'operations.error.changesetBlocked': msg('Changeset “{changeset}” has blocking validation issues and cannot be applied.', '{changeset} is a changeset id, e.g. cs-0001; the blocking issues follow beneath it.'),
306
308
  'operations.error.changesetDeleteNotReversible': msg('Step {step}: “{target}” cannot be deleted inside a changeset — a delete removes its version history and publication, which no undo can restore. Delete it on its own.', 'EC-648: {step} is the 0-based step number; {target} is the document key collection:id.'),
309
+ // EC-745 — a changeset revert refused whole, before any write: it would delete a document referenced since (deleteReferenced is a caller option, an identifier), a step that recorded no undo, or a document that moved.
310
+ 'operations.error.revertReferencedDeleteOne': msg('Revert of changeset “{changeset}” refused: it would delete “{target}”, and {count} document references it — {named}. Nothing was reverted. Revert with deleteReferenced: true to delete it anyway; the reference then dangles.', '“deleteReferenced: true” is a code option — keep it untranslated.'),
311
+ 'operations.error.revertReferencedDelete': msg('Revert of changeset “{changeset}” refused: it would delete “{target}”, and {count} documents reference it — {named}{more}. Nothing was reverted. Revert with deleteReferenced: true to delete it anyway; the references then dangle.', '{named} is an identifier list “collection:id” (field); {more} is the andMore fragment or empty.'),
312
+ 'operations.error.revertStrictReferencedDeleteOne': msg('Revert of changeset “{changeset}” refused: it would delete “{target}”, and {count} document references it through strict relation fields — {named}. Nothing was reverted. Remove these references first.'),
313
+ 'operations.error.revertStrictReferencedDelete': msg('Revert of changeset “{changeset}” refused: it would delete “{target}”, and {count} documents reference it through strict relation fields — {named}{more}. Nothing was reverted. Remove these references first.'),
314
+ 'operations.error.revertUnrecorded': msg('Revert of changeset “{changeset}” refused: step {step} (“{operation}”, {target}): {reason} — nothing was reverted.', '{operation} is a dotted operation name; {reason} is one of the changeset step reasons below.'),
315
+ 'operations.error.revertChangedSince': msg('Revert of changeset “{changeset}” refused: “{target}” changed since the changeset applied (now at revision {revision}, the changeset left it at {left}) — nothing was reverted.', '{revision} and {left} are revision numbers.'),
316
+ 'operations.error.revertGone': msg('Revert of changeset “{changeset}” refused: “{target}” no longer exists — nothing was reverted.'),
307
317
  'operations.errorDetail.andMore': msg(', and {count} more', 'Truncation fragment spliced into the strict-reference sentences; keeps its leading comma and space.'),
318
+ // EC-745 — the reasons a changeset step records for having no undo, spliced into revertUnrecorded's {reason}.
319
+ 'operations.errorDetail.changesetStepRaced': msg('another write landed between the changeset’s read of the document and this step’s own write, so what the step replaced was never recorded'),
320
+ 'operations.errorDetail.changesetPatchNotInvertible': msg('another write landed between the changeset’s read and this patch’s own write, and the patch changed structure (or its patch-log row is gone), which cannot be undone position by position'),
321
+ 'operations.errorDetail.changesetNoInverse': msg('no inverse was recorded'),
308
322
  // EC-494 — content-query errors (`core/content`'s validateWhere/validateSort/
309
323
  // validateLimit). Two studio surfaces render them: the filter builder's per-clause
310
324
  // line and the canvas listing notice.
@@ -674,11 +688,16 @@ const MESSAGES = {
674
688
  'cms.issueMessage.unknownVersion': msg('No version {version} recorded for document "{id}".'),
675
689
  'cms.issueMessage.routeConflict': msg('Routes "{first}" and "{second}" both match the same URLs (pattern "{pattern}").'),
676
690
  'cms.issueMessage.redirectLoop': msg('Redirect chain starting at "{from}" loops (…{chain}).'),
677
- 'cms.issueMessage.duplicateRedirect': msg('Redirect from "{redirectFrom}" duplicates the redirect from "{keptFrom}" — both are "{from}" once trailing slashes are ignored. The redirect from "{keptFrom}" applies; delete or change this one.'),
691
+ 'cms.issueMessage.duplicateRedirect': msg('Redirect from "{redirectFrom}" duplicates the redirect from "{keptFrom}" — both are "{from}" once trailing slashes are ignored and percent-encoding is decoded. The redirect from "{keptFrom}" applies; delete or change this one.'),
692
+ // EC-760 — a redirect `from` that is not a valid percent-encoding matches no request
693
+ 'cms.issueMessage.redirectFromMalformed': msg('Redirect from "{from}" holds a "%" that is not a percent-encoding, so it can match no request and is not served. Write the character itself, or "%25" for a percent sign.'),
678
694
  'cms.issueMessage.redirectUnreadable': msg('Stored redirect {redirect} could not be read and is not served ({reason}).', 'EC-628 (Codex F04): {redirect} is "id" (or #index), {reason} the column findings in English.'),
679
695
  'cms.issueMessage.redirectReportUnreadable': msg("The deployment reported redirect issue #{index}, which this host cannot read — a redirect is not served; run the site on the deployment's engine version.", 'EC-628: the delivery snapshot carried a redirect issue the host (an older engine) cannot parse.'),
680
696
  'cms.issueMessage.allDynamicShadowsMount': msg('Route "{route}" ("{pattern}") has only dynamic segments, so it matches EVERY {depth}-segment URL — hierarchy mount "{mountRoute}" ("{mountPattern}") is consulted at that depth only where the route resolves no document. Nothing is unconditionally unreachable, but precedence there is decided by CONTENT rather than by the routes: {shadowed} are silently shadowed wherever a composed path collides with {claimed}, and neither side is told. Give the route a static prefix ("/blog/:slug") so it claims a namespace instead of a whole depth.'),
681
697
  'cms.issueMessage.mountCannotServe': msg('Hierarchy mount "{route}" ("{pattern}") can never serve a document: {why}. The resolver skips such a mount silently, so every URL under "{space}" 404s with no issue raised.'),
698
+ // EC-760 — the route gate: a static pattern segment is text
699
+ 'cms.issueMessage.routeSegmentNotText': msg('Route "{route}" ("{pattern}") is not served: a pattern segment is not URL text ({segments}). A pattern is written as text — "/über-uns", not "/%C3%BCber-uns" — and its links are encoded, so a "%" is refused; so are a "." or ".." segment (a browser removes it before the request) and half of a character (no URL can carry it). Rewrite the segment.', 'EC-760: {segments} lists the refused pattern segments, each quoted as written in code; "/über-uns" and "/%C3%BCber-uns" are code — keep them as written.'),
700
+ 'cms.issueMessage.routePatternDelimiter': msg('Route "{route}" ("{pattern}") is not served: the pattern holds {delimiters}, which a URL reads as the start of its query or fragment, so the route would end there. A pattern is a path — remove the character.', 'EC-760: {delimiters} is "?" and/or "#", quoted and comma-separated — the characters are code; keep them as written.'),
682
701
  'cms.issueMessage.deadCatchAll': msg('Route "{route}" ("{pattern}") serves nothing in either direction: it ends in a catch-all, so every router drops it, but its prefix has a dynamic segment ({dynamic}), so no hierarchy mount is built for it either. A mount\'s prefix must be static — declare one mount per locale (`{ pattern: \'/de/:path*\', locale: \'de\' }`), or drop the route.'),
683
702
  'cms.issueMessage.mountConflict': msg('{opening} — an exact tie, separated only by declaration order, so "{earlier}" (collection "{earlierCollection}") claims every URL there its own tree resolves, and "{laterCollection}" documents at those URLs are unreachable.'),
684
703
  'cms.issueMessage.routeBoundDocumentNotFound': msg('Route-bound document "{collection}:{id}" was not found.'),
@@ -1073,7 +1092,7 @@ const MESSAGES = {
1073
1092
  'components.index.byCategory': msg('By category'),
1074
1093
  'components.index.guide.title': msg('Registering & extending components'),
1075
1094
  'components.index.guide.badge': msg('for developers'),
1076
- 'components.index.guide.intro': msg('Components are code, owned by your repo. Register a manifest (prop schemas, defaults, inspector controls, slots, and preview fixtures) and the studio browses, inspects, and previews it. These guides, in the Elytra engine repository’s docs, cover the details:'),
1095
+ 'components.index.guide.intro': msg('Components are code, owned by your repo. Register a manifest (prop schemas, defaults, inspector controls, slots, and preview fixtures) and the studio browses, inspects, and previews it. These chapters of the Elytra guide, which installs with @elytracms/core, cover the details:'),
1077
1096
  'components.index.guide.doc.authoring': msg('How to declare and register a component so the studio can browse, inspect, and preview it.'),
1078
1097
  'components.index.guide.doc.components': msg('The guide’s components chapter: the manifest + implementation pair, registering the list, fixtures, reusable elements, styling.'),
1079
1098
  'components.index.guide.doc.guide': msg('The guide’s overview and chapter index: setup, the config API, delivery, the CLI and environment reference.'),
@@ -1262,9 +1281,9 @@ const MESSAGES = {
1262
1281
  'builder.canvas.insertAfter': msg('Insert block after'),
1263
1282
  'builder.canvas.insertInto': msg('Insert block inside'),
1264
1283
  // Canvas-theme classification notices (EC-414 AD-D) — dev-only, never blocking.
1265
- 'builder.canvasTheme.legacyNotice': msg('This project’s canvas.css still bundles the studio design system with the site theme (the legacy merged shape). Point canvas.css at your site’s own full-Tailwind theme entry instead — see the theming guide (docs/theming.md). Everything keeps working meanwhile.', 'Dev-only migration pointer when the canvas stylesheet is the pre-EC-414 merged build.'),
1284
+ 'builder.canvasTheme.legacyNotice': msg('This project’s canvas.css still bundles the studio design system with the site theme (the legacy merged shape). Point canvas.css at your site’s own full-Tailwind theme entry instead — see the guide’s theming chapter ({guide}). Everything keeps working meanwhile.', 'Dev-only migration pointer when the canvas stylesheet is the pre-EC-414 merged build. {guide} is the theming chapter’s path in the project’s node_modules — keep it verbatim.'),
1266
1285
  'builder.canvasTheme.stylesheetErrorNotice': msg('The canvas stylesheet failed to load — blocks render unstyled. Check the studio’s canvas.css build.'),
1267
- 'builder.canvasTheme.noThemeTokensNotice': msg('The canvas stylesheet defines no theme tokens. Make canvas.css import your site’s full-Tailwind theme entry (see docs/theming.md).'),
1286
+ 'builder.canvasTheme.noThemeTokensNotice': msg('The canvas stylesheet defines no theme tokens. Make canvas.css import your site’s full-Tailwind theme entry (see the guide’s theming chapter, {guide}).', '{guide} is the theming chapter’s path in the project’s node_modules — keep it verbatim.'),
1268
1287
  'builder.canvasTheme.noStylesheetNotice': msg('No canvas stylesheet is configured — blocks render unstyled. The app must supply one via setCanvasStylesheetUrl (the create-elytra template wires this).'),
1269
1288
  // Mixed-sequence honesty notice (EC-549 interim): a blocks value carrying
1270
1289
  // typography items the canvas cannot render yet must SAY so — count + pointer,
@@ -1392,21 +1411,42 @@ const MESSAGES = {
1392
1411
  'ai.error.stepCap': msg('The assistant stopped after {max} tool steps. Send a follow-up message to continue.', 'EC-334 per-turn step cap. {max} is a number.'),
1393
1412
  // EC-332 — full-document translation (header action + chat tool)
1394
1413
  'ai.translate.action': msg('Translate document'),
1395
- 'ai.translate.tooltip': msg('Fill empty translations with AI'),
1414
+ 'ai.translate.tooltip': msg('Translate this document into its other languages with AI'),
1396
1415
  'ai.translate.dialogTitle': msg('Translate document'),
1397
- 'ai.translate.dialogDescription': msg('Fill empty locale values from {base} using AI. Existing translations are never overwritten.', '{base} is a language name, e.g. English.'),
1416
+ 'ai.translate.dialogDescription': msg('Translate this document from {base} using AI.', '{base} is a language name, e.g. English.'),
1398
1417
  'ai.translate.targetsLabel': msg('Target languages'),
1399
1418
  'ai.translate.scopeLabel': msg('What to translate'),
1400
1419
  'ai.translate.fillMissing': msg('Fill missing translations only'),
1420
+ 'ai.translate.fillMissingHint': msg('Fills only the values that still follow {base}. A value with its own translation is never overwritten.', '{base} is a language name, e.g. English.'),
1401
1421
  'ai.translate.retranslateAll': msg('Re-translate everything'),
1402
- 'ai.translate.retranslateSoon': msg('Coming soon'),
1422
+ // EC-752 — re-translate everything: the confirm lists what it replaces (from the run's own plan).
1423
+ 'ai.translate.retranslateHint': msg('Translates every value again from {base} — values that already have their own translation are replaced.', '{base} is a language name, e.g. English.'),
1424
+ 'ai.translate.planReplaceTitle': msg('Replaced with a new translation of {base} (up to {count})', 'Heading of the list of field × language values a re-translation overwrites, rebuilt from the source language. "Up to": a value whose translation does not fully arrive is left as it was. {base} is a language name, {count} a number.'),
1425
+ 'ai.translate.planReplaceNone': msg('No value has its own translation yet — nothing is replaced.'),
1426
+ 'ai.translate.planFillTitle': msg('Filled from {base} (up to {count})', '{base} is a language name, {count} a number.'),
1427
+ 'ai.translate.planWhole': msg('A value is replaced only when all of it was translated; otherwise it is left as it was.'),
1428
+ 'ai.translate.planSlugsKept': msg('A slug a language already has is kept — changing it would move the page’s address.'),
1429
+ 'ai.translate.planRestorePoint': msg('A version of the document is saved first, so you can restore any replaced value from Versions.', '"Versions" is the document’s version-history pane (cms.version.paneLabel).'),
1430
+ 'ai.translate.planKeptTitle': msg('Left as they are ({count})', '{count} is a number.'),
1403
1431
  'ai.translate.confirm': msg('Translate'),
1432
+ 'ai.translate.confirmReplace': msg('Re-translate and replace', 'The deliberate confirm of a re-translation that overwrites existing translations.'),
1404
1433
  'ai.translate.preparing': msg('Preparing translation…'),
1405
1434
  'ai.translate.progress': msg('Translating {completed} of {total}…', '{completed}/{total} are numbers.'),
1406
1435
  'ai.translate.applying': msg('Saving translations…'),
1436
+ 'ai.translate.snapshotLabel': msg('Before re-translation', 'The label of the version saved right before a re-translation replaces values.'),
1437
+ 'ai.translate.snapshotFailed': msg('Could not save a version before re-translating — nothing was changed. Please try again.'),
1407
1438
  'ai.translate.nothing': msg('Everything is already translated — there is nothing to fill.'),
1439
+ 'ai.translate.retranslateNothing': msg('There is nothing to re-translate — no field holds text in {base}.', '{base} is a language name, e.g. English.'),
1408
1440
  'ai.translate.applied': msg('Filled {count} translations across {locales} languages.', '{count}/{locales} are numbers.'),
1441
+ 'ai.translate.retranslateDone': msg('Re-translation finished.'),
1442
+ 'ai.translate.replacedTitle': msg('Replaced ({count})', '{count} is a number.'),
1443
+ 'ai.translate.filledTitle': msg('Filled ({count})', '{count} is a number.'),
1444
+ 'ai.translate.slugsTitle': msg('Slugs set from the translation ({count})', '{count} is a number.'),
1445
+ 'ai.translate.keptTitle': msg('Left as they were ({count})', '{count} is a number.'),
1446
+ 'ai.translate.refusedTitle': msg('Changed by someone else meanwhile ({count}) — decide in the editor', '{count} is a number.'),
1447
+ 'ai.translate.restorePoint': msg('The values from before are saved as version v{version} — restore any of them from Versions.', '{version} is a version number. "Versions" is the document’s version-history pane.'),
1409
1448
  'ai.translate.cancelled': msg('Translation cancelled — partial results were kept on the draft.'),
1449
+ 'ai.translate.retranslateCancelled': msg('Re-translation cancelled — values translated completely were replaced; the rest were left as they were.'),
1410
1450
  'ai.translate.failed': msg('Translation failed. Please try again.'),
1411
1451
  'ai.translate.close': msg('Close'),
1412
1452
  'ai.translate.proposalTitle': msg('Translation of {collection}'),
@@ -1417,8 +1457,13 @@ const MESSAGES = {
1417
1457
  'ai.translate.skipped.unreadable': msg('its stored value could not be read'),
1418
1458
  'ai.translate.skipped.notReturned': msg('the assistant returned no text for it'),
1419
1459
  'ai.translate.skipped.structureChanged': msg('the translation lost formatting, links or embedded items — the original was kept'),
1420
- // EC-738 — a bucket whose read-only (or AI-excluded) content is shaped differently from the source's.
1421
- 'ai.translate.skipped.locked': msg('it holds content that cannot be changed here, arranged differently from the source — left as it is'),
1460
+ // EC-752 — in a re-translation a value is written whole or not at all.
1461
+ 'ai.translate.skipped.retranslate.unreadable': msg('its stored value could not be read — the whole value was left as it was'),
1462
+ 'ai.translate.skipped.retranslate.notReturned': msg('the assistant returned no text for it — the whole value was left as it was'),
1463
+ 'ai.translate.skipped.retranslate.structureChanged': msg('the translation lost formatting, links or embedded items — the whole value was left as it was'),
1464
+ // EC-738 — a bucket whose read-only (or AI-excluded) content is shaped differently from the source's;
1465
+ // EC-748/752 — or stored in a shape the configuration no longer reads.
1466
+ 'ai.translate.skipped.locked': msg('it holds content that cannot be changed here, arranged differently from the source or stored in a shape the configuration no longer reads (save it in the editor first) — left as it is'),
1422
1467
  // EC-378 — the update (edit-existing) proposal
1423
1468
  'ai.update.proposalTitle': msg('Changes to {collection}'),
1424
1469
  'ai.update.diffBefore': msg('Current'),
@@ -1442,7 +1487,11 @@ const MESSAGES = {
1442
1487
  'cms.issueMessage.propsNotJudged': msg('The component props in field "{field}" were not judged: the page was resolved without a component registry, so they are delivered as stored and their issues are not reported.'),
1443
1488
  'cms.issueMessage.assetHoldsUrl': msg('Asset field "{field}" holds a URL — an asset value is a library asset, never a URL. Upload the image to the library and pick it, or put an external image’s address in the component’s URL prop (the Image’s “Image URL”).'),
1444
1489
  // EC-733 — the slug role holds what the slug rule writes
1445
- 'cms.issueMessage.slugNotSegment': msg('Slug field "{field}" holds "{value}", which is not a URL segment — the slug rule writes "{slug}" for it. The live URL is built from the stored slug as written; store the slug the rule writes.'),
1490
+ 'cms.issueMessage.slugNotSegment': msg('Slug field "{field}" holds "{value}", which is not a slug by the field\'s slug rule — the rule writes "{slug}" for it. Store the slug the rule writes.'),
1491
+ // EC-749 — a slug the route codec cannot carry as one URL segment
1492
+ 'cms.issueMessage.slugNotAddressable': msg('Slug field "{field}" holds "{value}", which cannot be one URL segment: a browser removes a "." or ".." segment before the request, and a "/" reads as a nested path. No URL is built from it — change the slug.'),
1493
+ // EC-761 — a slug the route codec cannot encode at all
1494
+ 'cms.issueMessage.slugNotEncodable': msg('Slug field "{field}" holds a broken character — half of a character such as an emoji (a lone UTF-16 surrogate), which no URL can carry. No URL is built from it — change the slug.', 'EC-761: the stored slug holds half of a two-part character (what cutting a string in the middle of an emoji yields); the value itself is not shown because half a character cannot be displayed.'),
1446
1495
  // EC-584 — the AllowList parse (config load / registration).
1447
1496
  'cms.issueMessage.emptyAllow': msg('Field "{field}" declares an empty "allow" — a list permits exactly the ids it names, so an empty one permits nothing. Omit "allow" for the type\'s default vocabulary, or list the component ids.'),
1448
1497
  'cms.issueMessage.unknownAllowComponent': msg('Field "{field}" allows "{component}", which is not a component of this project — check the id, or remove it from "allow".', '{component} is the component id as declared in the field\'s allow list.'),
@@ -1450,6 +1499,8 @@ const MESSAGES = {
1450
1499
  'cms.issueMessage.placementKeyReserved': msg('Asset field "{asset}" declares the per-use sub-field "{field}", a name the asset itself owns — it would never reach a component (the asset\'s own "{field}" wins). Rename it; an image\'s alt, credit or AI fact belongs on the asset collection.'),
1451
1500
  'cms.issueMessage.placementNotScalar': msg('Asset field "{asset}" declares the per-use sub-field "{field}" of type "{type}" — a placement holds data about this use of the image (a caption, a layout), never a reference or rich text.'),
1452
1501
  'cms.issueMessage.sourceNotOnAssetCollection': msg('Field "{field}" declares source "{source}" — an embedded-metadata source fills a top-level field of the asset collection only.'),
1502
+ // EC-753 — no canvas on an asset
1503
+ 'cms.issueMessage.blocksOnAssetCollection': msg('Field "{field}" is a "blocks" field on the asset collection "{collection}" — an asset holds facts about its file (alt text, a credit, a caption), never a canvas. Remove the field; a page composition belongs on a document collection.'),
1453
1504
  'cms.issueMessage.sourceTypeMismatch': msg('Field "{field}" declares source "{source}", which cannot fill a "{type}" field (every source fills text; iptc:digitalSourceType also a boolean).'),
1454
1505
  'cms.issueMessage.uniqueNotTopLevel': msg('Sub-field "{field}" declares "unique" — uniqueness is judged across a collection\'s documents, so it applies to top-level fields only.'),
1455
1506
  'cms.issueMessage.localizedOnProp': msg('Component prop "{field}" declares "localized" — a localized value is stored per locale under the document, so only a top-level document field can be localized (localize the field that holds the composition).'),