@purposeinplay/payload-ai-translate 0.1.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 +714 -0
- package/dist/alerts-collection.d.ts +21 -0
- package/dist/alerts-collection.js +159 -0
- package/dist/api.d.ts +4 -0
- package/dist/api.js +918 -0
- package/dist/bulk-translate-batches-collection.d.ts +29 -0
- package/dist/bulk-translate-batches-collection.js +404 -0
- package/dist/bulk-translate-units-collection.d.ts +35 -0
- package/dist/bulk-translate-units-collection.js +310 -0
- package/dist/client/estimated-cost-cell.d.ts +6 -0
- package/dist/client/estimated-cost-cell.js +12 -0
- package/dist/client/excluded-fields-field.d.ts +45 -0
- package/dist/client/excluded-fields-field.js +553 -0
- package/dist/client/field-translate-button.d.ts +6 -0
- package/dist/client/field-translate-button.js +199 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +6 -0
- package/dist/client/lib/use-global-kill-switches.d.ts +20 -0
- package/dist/client/lib/use-global-kill-switches.js +58 -0
- package/dist/client/translate-button.d.ts +2 -0
- package/dist/client/translate-button.js +228 -0
- package/dist/client/translate-modal.d.ts +16 -0
- package/dist/client/translate-modal.js +549 -0
- package/dist/client/translation-progress.d.ts +10 -0
- package/dist/client/translation-progress.js +297 -0
- package/dist/components/TranslationNavGroup.d.ts +45 -0
- package/dist/components/TranslationNavGroup.js +104 -0
- package/dist/defaults.d.ts +11 -0
- package/dist/defaults.js +16 -0
- package/dist/endpoints/client-config.d.ts +44 -0
- package/dist/endpoints/client-config.js +145 -0
- package/dist/endpoints/estimate.d.ts +5 -0
- package/dist/endpoints/estimate.js +237 -0
- package/dist/endpoints/progress.d.ts +2 -0
- package/dist/endpoints/progress.js +314 -0
- package/dist/endpoints/translate.d.ts +11 -0
- package/dist/endpoints/translate.js +376 -0
- package/dist/endpoints/translation-hub/_helpers.d.ts +140 -0
- package/dist/endpoints/translation-hub/_helpers.js +297 -0
- package/dist/endpoints/translation-hub/active.d.ts +21 -0
- package/dist/endpoints/translation-hub/active.js +220 -0
- package/dist/endpoints/translation-hub/cancel.d.ts +22 -0
- package/dist/endpoints/translation-hub/cancel.js +233 -0
- package/dist/endpoints/translation-hub/enqueue.d.ts +70 -0
- package/dist/endpoints/translation-hub/enqueue.js +529 -0
- package/dist/endpoints/translation-hub/failures.d.ts +12 -0
- package/dist/endpoints/translation-hub/failures.js +67 -0
- package/dist/endpoints/translation-hub/force-reset.d.ts +20 -0
- package/dist/endpoints/translation-hub/force-reset.js +144 -0
- package/dist/endpoints/translation-hub/index.d.ts +21 -0
- package/dist/endpoints/translation-hub/index.js +20 -0
- package/dist/endpoints/translation-hub/list.d.ts +40 -0
- package/dist/endpoints/translation-hub/list.js +182 -0
- package/dist/endpoints/translation-hub/preflight.d.ts +19 -0
- package/dist/endpoints/translation-hub/preflight.js +141 -0
- package/dist/endpoints/translation-hub/retry-failed.d.ts +38 -0
- package/dist/endpoints/translation-hub/retry-failed.js +235 -0
- package/dist/endpoints/translation-hub/revert.d.ts +88 -0
- package/dist/endpoints/translation-hub/revert.js +405 -0
- package/dist/endpoints/translation-hub/status.d.ts +45 -0
- package/dist/endpoints/translation-hub/status.js +391 -0
- package/dist/endpoints/translation-hub/usage-summary.d.ts +114 -0
- package/dist/endpoints/translation-hub/usage-summary.js +481 -0
- package/dist/exports/client.d.ts +6 -0
- package/dist/exports/client.js +6 -0
- package/dist/exports/components.d.ts +6 -0
- package/dist/exports/components.js +5 -0
- package/dist/exports/index.d.ts +8 -0
- package/dist/exports/index.js +7 -0
- package/dist/exports/providers.d.ts +9 -0
- package/dist/exports/providers.js +5 -0
- package/dist/exports/views-client.d.ts +23 -0
- package/dist/exports/views-client.js +22 -0
- package/dist/exports/views.d.ts +30 -0
- package/dist/exports/views.js +29 -0
- package/dist/hooks/after-change-global.d.ts +4 -0
- package/dist/hooks/after-change-global.js +109 -0
- package/dist/hooks/after-change.d.ts +16 -0
- package/dist/hooks/after-change.js +205 -0
- package/dist/hooks/after-delete.d.ts +30 -0
- package/dist/hooks/after-delete.js +95 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/jobs-collection.d.ts +17 -0
- package/dist/jobs-collection.js +139 -0
- package/dist/lexical/classifier.d.ts +3 -0
- package/dist/lexical/classifier.js +108 -0
- package/dist/lexical/deserializer.d.ts +4 -0
- package/dist/lexical/deserializer.js +263 -0
- package/dist/lexical/placeholder-integrity.d.ts +6 -0
- package/dist/lexical/placeholder-integrity.js +21 -0
- package/dist/lexical/placeholders.d.ts +21 -0
- package/dist/lexical/placeholders.js +117 -0
- package/dist/lexical/serializer.d.ts +21 -0
- package/dist/lexical/serializer.js +233 -0
- package/dist/lexical/types.d.ts +32 -0
- package/dist/lexical/types.js +1 -0
- package/dist/lib/auth-diagnostics.d.ts +14 -0
- package/dist/lib/auth-diagnostics.js +19 -0
- package/dist/lib/batch-counts.d.ts +58 -0
- package/dist/lib/batch-counts.js +105 -0
- package/dist/lib/bulk-translate-migrations.d.ts +92 -0
- package/dist/lib/bulk-translate-migrations.js +153 -0
- package/dist/lib/coalescing-queue.d.ts +38 -0
- package/dist/lib/coalescing-queue.js +69 -0
- package/dist/lib/content-extractor.d.ts +16 -0
- package/dist/lib/content-extractor.js +410 -0
- package/dist/lib/content-hash.d.ts +1 -0
- package/dist/lib/content-hash.js +19 -0
- package/dist/lib/content-patcher.d.ts +15 -0
- package/dist/lib/content-patcher.js +293 -0
- package/dist/lib/cost-guards.d.ts +2 -0
- package/dist/lib/cost-guards.js +18 -0
- package/dist/lib/daily-spend-cap.d.ts +58 -0
- package/dist/lib/daily-spend-cap.js +233 -0
- package/dist/lib/effective-locales.d.ts +181 -0
- package/dist/lib/effective-locales.js +302 -0
- package/dist/lib/error-messages.d.ts +245 -0
- package/dist/lib/error-messages.js +626 -0
- package/dist/lib/events.d.ts +39 -0
- package/dist/lib/events.js +146 -0
- package/dist/lib/exclude-fields.d.ts +3 -0
- package/dist/lib/exclude-fields.js +64 -0
- package/dist/lib/field-breadcrumb.d.ts +31 -0
- package/dist/lib/field-breadcrumb.js +227 -0
- package/dist/lib/field-diff.d.ts +1 -0
- package/dist/lib/field-diff.js +25 -0
- package/dist/lib/field-empty.d.ts +2 -0
- package/dist/lib/field-empty.js +68 -0
- package/dist/lib/field-resolver.d.ts +3 -0
- package/dist/lib/field-resolver.js +164 -0
- package/dist/lib/group-soft-skips.d.ts +39 -0
- package/dist/lib/group-soft-skips.js +45 -0
- package/dist/lib/locale-merge.d.ts +44 -0
- package/dist/lib/locale-merge.js +357 -0
- package/dist/lib/locale-row-check.d.ts +30 -0
- package/dist/lib/locale-row-check.js +64 -0
- package/dist/lib/logger.d.ts +74 -0
- package/dist/lib/logger.js +97 -0
- package/dist/lib/manual-edit-guard.d.ts +128 -0
- package/dist/lib/manual-edit-guard.js +393 -0
- package/dist/lib/output-validation.d.ts +48 -0
- package/dist/lib/output-validation.js +148 -0
- package/dist/lib/payload-read.d.ts +16 -0
- package/dist/lib/payload-read.js +51 -0
- package/dist/lib/per-doc-claim.d.ts +90 -0
- package/dist/lib/per-doc-claim.js +140 -0
- package/dist/lib/per-doc-lock.d.ts +94 -0
- package/dist/lib/per-doc-lock.js +119 -0
- package/dist/lib/persist-usage.d.ts +91 -0
- package/dist/lib/persist-usage.js +116 -0
- package/dist/lib/progress-store.d.ts +103 -0
- package/dist/lib/progress-store.js +314 -0
- package/dist/lib/rate-limiter.d.ts +3 -0
- package/dist/lib/rate-limiter.js +53 -0
- package/dist/lib/snapshot-select.d.ts +43 -0
- package/dist/lib/snapshot-select.js +108 -0
- package/dist/lib/translate-prompt.d.ts +31 -0
- package/dist/lib/translate-prompt.js +66 -0
- package/dist/lib/translation-token-bucket.d.ts +57 -0
- package/dist/lib/translation-token-bucket.js +365 -0
- package/dist/lib/truncate-source-value.d.ts +1 -0
- package/dist/lib/truncate-source-value.js +27 -0
- package/dist/manual-edit-collection.d.ts +22 -0
- package/dist/manual-edit-collection.js +124 -0
- package/dist/plugin.d.ts +3 -0
- package/dist/plugin.js +934 -0
- package/dist/providers/ai-sdk-adapter.d.ts +35 -0
- package/dist/providers/ai-sdk-adapter.js +100 -0
- package/dist/providers/anthropic.d.ts +31 -0
- package/dist/providers/anthropic.js +66 -0
- package/dist/providers/custom.d.ts +36 -0
- package/dist/providers/custom.js +24 -0
- package/dist/providers/gemini.d.ts +20 -0
- package/dist/providers/gemini.js +48 -0
- package/dist/providers/mock.d.ts +2 -0
- package/dist/providers/mock.js +29 -0
- package/dist/providers/openai.d.ts +28 -0
- package/dist/providers/openai.js +69 -0
- package/dist/settings-global.d.ts +74 -0
- package/dist/settings-global.js +216 -0
- package/dist/tasks/bulk-translate-coordinator.d.ts +115 -0
- package/dist/tasks/bulk-translate-coordinator.js +708 -0
- package/dist/tasks/bulk-translate-doc-task.d.ts +142 -0
- package/dist/tasks/bulk-translate-doc-task.js +1000 -0
- package/dist/tasks/bulk-translate-janitor.d.ts +87 -0
- package/dist/tasks/bulk-translate-janitor.js +311 -0
- package/dist/tasks/translate-job-task.d.ts +51 -0
- package/dist/tasks/translate-job-task.js +154 -0
- package/dist/translate.d.ts +113 -0
- package/dist/translate.js +911 -0
- package/dist/translation-daily-spend-collection.d.ts +24 -0
- package/dist/translation-daily-spend-collection.js +133 -0
- package/dist/translation-rate-limits-collection.d.ts +30 -0
- package/dist/translation-rate-limits-collection.js +144 -0
- package/dist/types.d.ts +672 -0
- package/dist/types.js +1 -0
- package/dist/usage-collection.d.ts +14 -0
- package/dist/usage-collection.js +377 -0
- package/dist/views/BulkRunsHub/BatchRow.d.ts +32 -0
- package/dist/views/BulkRunsHub/BatchRow.js +1222 -0
- package/dist/views/BulkRunsHub/BucketRow.d.ts +62 -0
- package/dist/views/BulkRunsHub/BucketRow.js +982 -0
- package/dist/views/BulkRunsHub/BulkRunsHub.client.d.ts +18 -0
- package/dist/views/BulkRunsHub/BulkRunsHub.client.js +331 -0
- package/dist/views/BulkRunsHub/EmptyState.d.ts +6 -0
- package/dist/views/BulkRunsHub/EmptyState.js +64 -0
- package/dist/views/BulkRunsHub/FilterBar.d.ts +16 -0
- package/dist/views/BulkRunsHub/FilterBar.js +284 -0
- package/dist/views/BulkRunsHub/InFlightBanner.d.ts +14 -0
- package/dist/views/BulkRunsHub/InFlightBanner.js +59 -0
- package/dist/views/BulkRunsHub/StatusBadge.d.ts +64 -0
- package/dist/views/BulkRunsHub/StatusBadge.js +248 -0
- package/dist/views/BulkRunsHub/SummaryStrip.d.ts +22 -0
- package/dist/views/BulkRunsHub/SummaryStrip.js +249 -0
- package/dist/views/BulkRunsHub/bucket-grouping.d.ts +200 -0
- package/dist/views/BulkRunsHub/bucket-grouping.js +344 -0
- package/dist/views/BulkRunsHub/bucketFailureSummary.d.ts +9 -0
- package/dist/views/BulkRunsHub/bucketFailureSummary.js +36 -0
- package/dist/views/BulkRunsHub/dedupedStatusFetch.d.ts +5 -0
- package/dist/views/BulkRunsHub/dedupedStatusFetch.js +45 -0
- package/dist/views/BulkRunsHub/index.d.ts +17 -0
- package/dist/views/BulkRunsHub/index.js +80 -0
- package/dist/views/BulkRunsHub/urlFilters.d.ts +14 -0
- package/dist/views/BulkRunsHub/urlFilters.js +50 -0
- package/dist/views/BulkRunsHub/useBulkRunsList.d.ts +26 -0
- package/dist/views/BulkRunsHub/useBulkRunsList.js +204 -0
- package/dist/views/BulkRunsHub/useUrlFilters.d.ts +10 -0
- package/dist/views/BulkRunsHub/useUrlFilters.js +88 -0
- package/dist/views/TranslationHub/ActiveJobs.d.ts +6 -0
- package/dist/views/TranslationHub/ActiveJobs.js +320 -0
- package/dist/views/TranslationHub/AdvancedPanel.d.ts +17 -0
- package/dist/views/TranslationHub/AdvancedPanel.js +996 -0
- package/dist/views/TranslationHub/AlertBanner.d.ts +6 -0
- package/dist/views/TranslationHub/AlertBanner.js +568 -0
- package/dist/views/TranslationHub/AuditPanel.d.ts +6 -0
- package/dist/views/TranslationHub/AuditPanel.helpers.d.ts +44 -0
- package/dist/views/TranslationHub/AuditPanel.helpers.js +71 -0
- package/dist/views/TranslationHub/AuditPanel.js +1367 -0
- package/dist/views/TranslationHub/BulkTranslate.types.d.ts +242 -0
- package/dist/views/TranslationHub/BulkTranslate.types.js +36 -0
- package/dist/views/TranslationHub/BulkTranslateFailureDrawer.d.ts +19 -0
- package/dist/views/TranslationHub/BulkTranslateFailureDrawer.js +332 -0
- package/dist/views/TranslationHub/BulkTranslateMonitor.d.ts +28 -0
- package/dist/views/TranslationHub/BulkTranslateMonitor.js +305 -0
- package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.d.ts +3 -0
- package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.js +42 -0
- package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.d.ts +26 -0
- package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.js +95 -0
- package/dist/views/TranslationHub/BulkTranslatePreflightModal.d.ts +22 -0
- package/dist/views/TranslationHub/BulkTranslatePreflightModal.js +879 -0
- package/dist/views/TranslationHub/BulkTranslateTerminalCard.d.ts +29 -0
- package/dist/views/TranslationHub/BulkTranslateTerminalCard.js +445 -0
- package/dist/views/TranslationHub/BulkTranslateTrigger.d.ts +66 -0
- package/dist/views/TranslationHub/BulkTranslateTrigger.js +161 -0
- package/dist/views/TranslationHub/EditorRecentRunsPanel.d.ts +33 -0
- package/dist/views/TranslationHub/EditorRecentRunsPanel.js +290 -0
- package/dist/views/TranslationHub/Hub.client.d.ts +74 -0
- package/dist/views/TranslationHub/Hub.client.js +357 -0
- package/dist/views/TranslationHub/ModelCombobox.d.ts +14 -0
- package/dist/views/TranslationHub/ModelCombobox.js +415 -0
- package/dist/views/TranslationHub/PerCollectionConfig.d.ts +10 -0
- package/dist/views/TranslationHub/PerCollectionConfig.helpers.d.ts +16 -0
- package/dist/views/TranslationHub/PerCollectionConfig.helpers.js +19 -0
- package/dist/views/TranslationHub/PerCollectionConfig.js +759 -0
- package/dist/views/TranslationHub/SettingsRail.d.ts +11 -0
- package/dist/views/TranslationHub/SettingsRail.js +382 -0
- package/dist/views/TranslationHub/StatusStrip.d.ts +6 -0
- package/dist/views/TranslationHub/StatusStrip.js +451 -0
- package/dist/views/TranslationHub/UsageTable.d.ts +6 -0
- package/dist/views/TranslationHub/UsageTable.helpers.d.ts +69 -0
- package/dist/views/TranslationHub/UsageTable.helpers.js +49 -0
- package/dist/views/TranslationHub/UsageTable.js +1240 -0
- package/dist/views/TranslationHub/alertGrouping.d.ts +70 -0
- package/dist/views/TranslationHub/alertGrouping.js +99 -0
- package/dist/views/TranslationHub/index.d.ts +20 -0
- package/dist/views/TranslationHub/index.js +109 -0
- package/dist/views/TranslationHub/tabNavigation.d.ts +53 -0
- package/dist/views/TranslationHub/tabNavigation.js +74 -0
- package/dist/views/TranslationHub/terminalBannerVisibility.d.ts +33 -0
- package/dist/views/TranslationHub/terminalBannerVisibility.js +124 -0
- package/dist/views/TranslationHub/useBulkTranslateActive.d.ts +49 -0
- package/dist/views/TranslationHub/useBulkTranslateActive.js +251 -0
- package/dist/views/TranslationHub/useFocusTrap.d.ts +6 -0
- package/dist/views/TranslationHub/useFocusTrap.js +81 -0
- package/dist/views/TranslationHub/useTranslationHubUsageSummary.d.ts +77 -0
- package/dist/views/TranslationHub/useTranslationHubUsageSummary.js +267 -0
- package/dist/views/shared/EditorError.d.ts +97 -0
- package/dist/views/shared/EditorError.js +205 -0
- package/dist/views/shared/ModelCell.d.ts +18 -0
- package/dist/views/shared/ModelCell.js +31 -0
- package/dist/views/shared/docHref.d.ts +16 -0
- package/dist/views/shared/docHref.js +26 -0
- package/dist/views/shared/fetch-error-body.d.ts +25 -0
- package/dist/views/shared/fetch-error-body.js +42 -0
- package/dist/views/shared/filterPillStyle.d.ts +35 -0
- package/dist/views/shared/filterPillStyle.js +40 -0
- package/dist/views/shared/format.d.ts +75 -0
- package/dist/views/shared/format.js +131 -0
- package/package.json +141 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { classifyNode } from './classifier.js';
|
|
2
|
+
import { wrapFormat, wrapLink } from './placeholders.js';
|
|
3
|
+
/**
|
|
4
|
+
* Walks an embedded Payload block (`type: 'block'` Lexical node, surfaced
|
|
5
|
+
* by `BlocksFeature`) in a deterministic order, emitting translation
|
|
6
|
+
* units for strings and recursing into nested Lexical roots / arrays /
|
|
7
|
+
* objects. Walk order MUST match `applyTranslatedBlockData` in the
|
|
8
|
+
* deserializer so blockId sequence stays in sync between serialize and
|
|
9
|
+
* deserialize passes.
|
|
10
|
+
*
|
|
11
|
+
* No full schema awareness yet — the BlocksFeature config isn't
|
|
12
|
+
* reachable from the extractor at runtime. As a defensive heuristic the
|
|
13
|
+
* walker skips strings that look like select-option values (short,
|
|
14
|
+
* lowercase identifiers like `info`, `warning`, `oneThird`) so the LLM
|
|
15
|
+
* doesn't translate them into invalid values that Payload's locale
|
|
16
|
+
* write validator would reject. The top-level blocks-field path
|
|
17
|
+
* (handled by `extractBlockFields` in `lib/content-extractor.ts`) uses
|
|
18
|
+
* full schema-aware gating.
|
|
19
|
+
*/ export function serializeBlockData(data, units, nextBlockId, innerWalkNodes) {
|
|
20
|
+
for (const [key, value] of Object.entries(data)){
|
|
21
|
+
if (key === 'id' || key === 'blockType' || key === 'blockName') continue;
|
|
22
|
+
if (typeof value === 'string') {
|
|
23
|
+
if (value.trim().length > 0 && !isLikelySelectOption(value)) {
|
|
24
|
+
units.push({
|
|
25
|
+
blockId: nextBlockId(),
|
|
26
|
+
text: value,
|
|
27
|
+
sourceNode: {
|
|
28
|
+
type: 'block-field',
|
|
29
|
+
text: value
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (isLexicalRootValue(value)) {
|
|
36
|
+
// Nested richText inside the block — recurse with the existing walker
|
|
37
|
+
// so its inline formatting placeholders stay consistent.
|
|
38
|
+
innerWalkNodes(value.root.children ?? []);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (Array.isArray(value)) {
|
|
42
|
+
for (const item of value){
|
|
43
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
44
|
+
serializeBlockData(item, units, nextBlockId, innerWalkNodes);
|
|
45
|
+
} else if (typeof item === 'string' && item.trim().length > 0) {
|
|
46
|
+
units.push({
|
|
47
|
+
blockId: nextBlockId(),
|
|
48
|
+
text: item,
|
|
49
|
+
sourceNode: {
|
|
50
|
+
type: 'block-field-array',
|
|
51
|
+
text: item
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (value && typeof value === 'object') {
|
|
59
|
+
serializeBlockData(value, units, nextBlockId, innerWalkNodes);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function isLexicalRootValue(value) {
|
|
64
|
+
if (value === null || typeof value !== 'object') return false;
|
|
65
|
+
const obj = value;
|
|
66
|
+
return 'root' in obj && obj.root !== null && typeof obj.root === 'object';
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Heuristic: a short identifier-shaped string is almost certainly a
|
|
70
|
+
* `select` / `radio` option value (e.g. `info`, `warning`, `oneThird`,
|
|
71
|
+
* `primary`, `dark-mode`) — translating it produces a value Payload's
|
|
72
|
+
* locale-write validator will reject. We can't see the block schema
|
|
73
|
+
* from the embedded-Lexical context so this filter is the next best
|
|
74
|
+
* thing.
|
|
75
|
+
*
|
|
76
|
+
* Tradeoff: a real translatable word like `info` (English `info` →
|
|
77
|
+
* Spanish `info`/`información`) stops being translated inside embedded
|
|
78
|
+
* Lexical blocks. Acceptable: nobody writes prose into select-typed
|
|
79
|
+
* fields, and consumers who DO want their short labels translated can
|
|
80
|
+
* still call the manual translate API or use a top-level blocks field
|
|
81
|
+
* (which has full schema-aware gating).
|
|
82
|
+
*
|
|
83
|
+
* Matches:
|
|
84
|
+
* - `^[a-z][a-z0-9_-]{0,20}$` — lowercase identifier 1-21 chars
|
|
85
|
+
* - `^[a-z]+(?:[A-Z][a-z]+)+$` — camelCase 6-30 chars (e.g. oneThird)
|
|
86
|
+
*/ function isLikelySelectOption(text) {
|
|
87
|
+
const t = text.trim();
|
|
88
|
+
if (t.length === 0 || t.length > 30) return false;
|
|
89
|
+
// No spaces allowed in select option values.
|
|
90
|
+
if (/\s/.test(t)) return false;
|
|
91
|
+
// Lowercase identifier
|
|
92
|
+
if (/^[a-z][a-z0-9_-]{0,20}$/.test(t)) return true;
|
|
93
|
+
// camelCase identifier
|
|
94
|
+
if (/^[a-z]+(?:[A-Z][a-z0-9]+)+$/.test(t) && t.length <= 30) return true;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
// Public API
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
export function serializeLexicalTree(root, registeredNodes) {
|
|
101
|
+
const units = [];
|
|
102
|
+
let counter = 0;
|
|
103
|
+
function nextBlockId() {
|
|
104
|
+
return `blk_${counter++}`;
|
|
105
|
+
}
|
|
106
|
+
function walkNodes(nodes) {
|
|
107
|
+
for (const node of nodes){
|
|
108
|
+
const classified = classifyNode(node, registeredNodes);
|
|
109
|
+
switch(classified.classification){
|
|
110
|
+
case 'recurse':
|
|
111
|
+
{
|
|
112
|
+
// Block-level nodes that contain inline children
|
|
113
|
+
if (node.type === 'paragraph' || node.type === 'heading' || node.type === 'quote' || node.type === 'listitem') {
|
|
114
|
+
const inlineText = serializeInlineChildren(node.children ?? []);
|
|
115
|
+
if (inlineText.trim().length > 0) {
|
|
116
|
+
units.push({
|
|
117
|
+
blockId: nextBlockId(),
|
|
118
|
+
text: inlineText,
|
|
119
|
+
sourceNode: node
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Also walk children for nested lists inside listitems
|
|
123
|
+
if (node.type === 'listitem' && node.children) {
|
|
124
|
+
for (const child of node.children){
|
|
125
|
+
if (child.type === 'list') {
|
|
126
|
+
walkNodes(child.children ?? []);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} else if (node.type === 'list') {
|
|
131
|
+
// Lists themselves recurse into their children (listitems)
|
|
132
|
+
walkNodes(node.children ?? []);
|
|
133
|
+
} else {
|
|
134
|
+
// Other recurse nodes (e.g. link called at top level, autolink)
|
|
135
|
+
walkNodes(node.children ?? []);
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
case 'translate-attr':
|
|
140
|
+
{
|
|
141
|
+
if (node.type === 'upload') {
|
|
142
|
+
const alt = getNodeAlt(node);
|
|
143
|
+
if (alt) {
|
|
144
|
+
units.push({
|
|
145
|
+
blockId: nextBlockId(),
|
|
146
|
+
text: alt,
|
|
147
|
+
sourceNode: node
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
} else if (classified.translatableAttributes) {
|
|
151
|
+
// Custom registered nodes
|
|
152
|
+
for (const attr of classified.translatableAttributes){
|
|
153
|
+
const value = getNestedAttr(node, attr);
|
|
154
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
155
|
+
units.push({
|
|
156
|
+
blockId: nextBlockId(),
|
|
157
|
+
text: value,
|
|
158
|
+
sourceNode: node
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case 'block':
|
|
166
|
+
{
|
|
167
|
+
// Lexical's BlocksFeature embeds Payload blocks inline. Walk
|
|
168
|
+
// the block's `fields` and emit translation units the same way
|
|
169
|
+
// we'd handle them at the top-level blocks-array path. Inner
|
|
170
|
+
// Lexical roots (e.g. `banner.content` richText) recurse back
|
|
171
|
+
// through `walkNodes` so their inline placeholders stay in
|
|
172
|
+
// sync with the rest of the document's blockId sequence.
|
|
173
|
+
const blockFields = node.fields ?? {};
|
|
174
|
+
serializeBlockData(blockFields, units, nextBlockId, walkNodes);
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
case 'skip':
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
walkNodes(root.root.children ?? []);
|
|
183
|
+
return units;
|
|
184
|
+
}
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Inline serialization
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
function serializeInlineChildren(children) {
|
|
189
|
+
return children.map((child)=>serializeInlineNode(child)).join('');
|
|
190
|
+
}
|
|
191
|
+
function serializeInlineNode(node) {
|
|
192
|
+
switch(node.type){
|
|
193
|
+
case 'text':
|
|
194
|
+
{
|
|
195
|
+
const text = node.text ?? '';
|
|
196
|
+
const format = node.format ?? 0;
|
|
197
|
+
return format > 0 ? wrapFormat(text, format) : text;
|
|
198
|
+
}
|
|
199
|
+
case 'link':
|
|
200
|
+
{
|
|
201
|
+
const linkFields = node.fields ?? {};
|
|
202
|
+
const innerText = serializeInlineChildren(node.children ?? []);
|
|
203
|
+
return wrapLink(innerText, linkFields);
|
|
204
|
+
}
|
|
205
|
+
case 'linebreak':
|
|
206
|
+
return '\n';
|
|
207
|
+
case 'list':
|
|
208
|
+
// Nested lists inside listitems are handled at the block walk level
|
|
209
|
+
return '';
|
|
210
|
+
default:
|
|
211
|
+
// For any unknown inline node, try to serialize its children
|
|
212
|
+
if (node.children) {
|
|
213
|
+
return serializeInlineChildren(node.children);
|
|
214
|
+
}
|
|
215
|
+
return '';
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Attribute helpers
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
function getNodeAlt(node) {
|
|
222
|
+
const fromFields = node.fields?.alt;
|
|
223
|
+
const fromNode = node.alt;
|
|
224
|
+
const alt = fromFields ?? fromNode;
|
|
225
|
+
return typeof alt === 'string' ? alt : undefined;
|
|
226
|
+
}
|
|
227
|
+
function getNestedAttr(node, attr) {
|
|
228
|
+
// Check fields first, then the node itself
|
|
229
|
+
if (node.fields && attr in node.fields) {
|
|
230
|
+
return node.fields[attr];
|
|
231
|
+
}
|
|
232
|
+
return node[attr];
|
|
233
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export type LexicalNode = {
|
|
2
|
+
type: string;
|
|
3
|
+
children?: LexicalNode[];
|
|
4
|
+
text?: string;
|
|
5
|
+
format?: number;
|
|
6
|
+
tag?: string;
|
|
7
|
+
listType?: string;
|
|
8
|
+
fields?: Record<string, unknown>;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
};
|
|
11
|
+
export type LexicalRoot = {
|
|
12
|
+
root: LexicalNode;
|
|
13
|
+
};
|
|
14
|
+
export type NodeClassification = 'recurse' | 'translate-attr' | 'skip'
|
|
15
|
+
/**
|
|
16
|
+
* Lexical's `BlocksFeature` embeds Payload blocks inline (`type: 'block'`,
|
|
17
|
+
* `fields: { blockType, ... }`). Treated as a separate classification so
|
|
18
|
+
* the serializer/deserializer can walk the block's fields the same way
|
|
19
|
+
* the top-level `extractBlockFields` walker does, instead of treating
|
|
20
|
+
* the embedded block as opaque.
|
|
21
|
+
*/
|
|
22
|
+
| 'block';
|
|
23
|
+
export type ClassifiedNode = {
|
|
24
|
+
node: LexicalNode;
|
|
25
|
+
classification: NodeClassification;
|
|
26
|
+
translatableAttributes?: string[];
|
|
27
|
+
};
|
|
28
|
+
export type SerializedUnit = {
|
|
29
|
+
blockId: string;
|
|
30
|
+
text: string;
|
|
31
|
+
sourceNode: LexicalNode;
|
|
32
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PayloadRequest } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Build a one-line diagnostic explaining *why* a request was rejected with
|
|
4
|
+
* 401 by `if (!req.user) return 401` gates. Payload's `extractJWT.cookie`
|
|
5
|
+
* silently returns `null` when the request lacks an allowlisted `Origin`
|
|
6
|
+
* AND lacks an acceptable `Sec-Fetch-Site` header — most commonly because
|
|
7
|
+
* the page was opened via Next's `Network:` URL (`127.0.x.x`) instead of
|
|
8
|
+
* `localhost`, which the consumer's `csrf` allowlist doesn't include.
|
|
9
|
+
*
|
|
10
|
+
* Surfacing the cookie/Origin/Sec-Fetch-Site triple in the response body
|
|
11
|
+
* lets the next dev land on the root cause without trawling Payload core.
|
|
12
|
+
* Kept terse — adds maybe 80 bytes to a 401 response.
|
|
13
|
+
*/
|
|
14
|
+
export declare function describeAuthRejection(req: PayloadRequest): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build a one-line diagnostic explaining *why* a request was rejected with
|
|
3
|
+
* 401 by `if (!req.user) return 401` gates. Payload's `extractJWT.cookie`
|
|
4
|
+
* silently returns `null` when the request lacks an allowlisted `Origin`
|
|
5
|
+
* AND lacks an acceptable `Sec-Fetch-Site` header — most commonly because
|
|
6
|
+
* the page was opened via Next's `Network:` URL (`127.0.x.x`) instead of
|
|
7
|
+
* `localhost`, which the consumer's `csrf` allowlist doesn't include.
|
|
8
|
+
*
|
|
9
|
+
* Surfacing the cookie/Origin/Sec-Fetch-Site triple in the response body
|
|
10
|
+
* lets the next dev land on the root cause without trawling Payload core.
|
|
11
|
+
* Kept terse — adds maybe 80 bytes to a 401 response.
|
|
12
|
+
*/ export function describeAuthRejection(req) {
|
|
13
|
+
const cookie = req.headers?.get?.('cookie') ?? '';
|
|
14
|
+
const hasCookie = cookie.includes('payload-token');
|
|
15
|
+
const origin = req.headers?.get?.('origin') ?? null;
|
|
16
|
+
const sfs = req.headers?.get?.('sec-fetch-site') ?? null;
|
|
17
|
+
if (!hasCookie) return 'No payload-token cookie sent.';
|
|
18
|
+
return `Cookie present but rejected. Origin=${origin ?? '(none)'} Sec-Fetch-Site=${sfs ?? '(none)'}. Add Origin to payload.config.csrf or open the admin via an allowlisted host (commonly the "Network:" URL printed by Next dev is the trigger).`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Source-of-truth counts for one or more bulk-translate batches.
|
|
4
|
+
*
|
|
5
|
+
* The plugin used to read `batch.completedUnits` / `failedUnits` /
|
|
6
|
+
* `skippedUnits` straight off the batch row — populated by atomic SQL
|
|
7
|
+
* bumps in the worker (`completed_units = completed_units + 1` on each
|
|
8
|
+
* unit transition). That worked for the happy path but drifted on:
|
|
9
|
+
* - Retries (`retry-failed` flips `failed → pending` without
|
|
10
|
+
* decrementing the previous failure's contribution).
|
|
11
|
+
* - Admin SDK interventions (manually marking a `success` unit as
|
|
12
|
+
* `failed`, then retrying — the original success increment stays
|
|
13
|
+
* and a new one fires when the retry succeeds, so `completedUnits`
|
|
14
|
+
* can exceed `totalUnits`).
|
|
15
|
+
* - Cancel paths (`pending → skipped` writes the unit row but
|
|
16
|
+
* skipping bumps the counter via a separate path that can race).
|
|
17
|
+
*
|
|
18
|
+
* Real fix per editor feedback: API endpoints never trust the cached
|
|
19
|
+
* counters. They always recompute from the underlying
|
|
20
|
+
* `bulk_translate_units` table. The cached counters stay as an
|
|
21
|
+
* internal optimization for the worker's "is this batch done?"
|
|
22
|
+
* transition check, but they're no longer authoritative.
|
|
23
|
+
*
|
|
24
|
+
* One query per call regardless of how many batches: a single `find`
|
|
25
|
+
* with `where { batchId: { in: [...] } }` and a narrow `select`
|
|
26
|
+
* projects (`batchId`, `status`) only. JS aggregates the counts.
|
|
27
|
+
* Realistic plugin batches (≤1000 units each, ~10 batches per list
|
|
28
|
+
* page) finish well under 100ms.
|
|
29
|
+
*
|
|
30
|
+
* For very large batches (10k+ units each), this approach still works
|
|
31
|
+
* but a switch to `drizzle.execute(sql\`SELECT batch_id_id, status,
|
|
32
|
+
* COUNT(*) ... GROUP BY 1, 2\`)` would be O(1) on the wire. We don't
|
|
33
|
+
* ship that today because drizzle is an optional peer dep.
|
|
34
|
+
*/
|
|
35
|
+
export type LiveBatchCounts = {
|
|
36
|
+
total: number;
|
|
37
|
+
pending: number;
|
|
38
|
+
running: number;
|
|
39
|
+
/** Worker-`success` count. Exposed to the API as `completed`. */
|
|
40
|
+
completed: number;
|
|
41
|
+
failed: number;
|
|
42
|
+
skipped: number;
|
|
43
|
+
reverted: number;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Compute fresh status counts for every batch id passed in.
|
|
47
|
+
*
|
|
48
|
+
* Returns a Map keyed by stringified batch id. Batches with no units
|
|
49
|
+
* (newly enqueued or anomalously empty) map to the zero record so
|
|
50
|
+
* callers don't have to null-check.
|
|
51
|
+
*/
|
|
52
|
+
export declare function computeLiveBatchCounts(payload: Payload, unitsSlug: string, batchIds: ReadonlyArray<string | number>): Promise<Map<string, LiveBatchCounts>>;
|
|
53
|
+
/**
|
|
54
|
+
* Convenience wrapper for the single-batch case (status endpoint,
|
|
55
|
+
* worker transition check). Returns the zero record if the batch has
|
|
56
|
+
* no units (anomalous but should never crash).
|
|
57
|
+
*/
|
|
58
|
+
export declare function computeLiveBatchCount(payload: Payload, unitsSlug: string, batchId: string): Promise<LiveBatchCounts>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { getDrizzle, slugToTable } from './bulk-translate-migrations.js';
|
|
2
|
+
const ZERO_COUNTS = {
|
|
3
|
+
total: 0,
|
|
4
|
+
pending: 0,
|
|
5
|
+
running: 0,
|
|
6
|
+
completed: 0,
|
|
7
|
+
failed: 0,
|
|
8
|
+
skipped: 0,
|
|
9
|
+
reverted: 0
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Compute fresh status counts for every batch id passed in.
|
|
13
|
+
*
|
|
14
|
+
* Returns a Map keyed by stringified batch id. Batches with no units
|
|
15
|
+
* (newly enqueued or anomalously empty) map to the zero record so
|
|
16
|
+
* callers don't have to null-check.
|
|
17
|
+
*/ export async function computeLiveBatchCounts(payload, unitsSlug, batchIds) {
|
|
18
|
+
const out = new Map();
|
|
19
|
+
if (batchIds.length === 0) return out;
|
|
20
|
+
// Seed every requested batchId so the caller can lookup without
|
|
21
|
+
// checking for undefined.
|
|
22
|
+
for (const id of batchIds){
|
|
23
|
+
out.set(String(id), {
|
|
24
|
+
...ZERO_COUNTS
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const tally = (bid, status, n)=>{
|
|
28
|
+
const counts = out.get(bid);
|
|
29
|
+
if (!counts) return;
|
|
30
|
+
counts.total += n;
|
|
31
|
+
if (status === 'pending') counts.pending += n;
|
|
32
|
+
else if (status === 'running') counts.running += n;
|
|
33
|
+
else if (status === 'success') counts.completed += n;
|
|
34
|
+
else if (status === 'failed') counts.failed += n;
|
|
35
|
+
else if (status === 'skipped') counts.skipped += n;
|
|
36
|
+
else if (status === 'reverted') counts.reverted += n;
|
|
37
|
+
};
|
|
38
|
+
// Fast path: one GROUP BY on Postgres — O(1) on the wire no matter
|
|
39
|
+
// how many units the batches hold. This matters far more than the
|
|
40
|
+
// original comment assumed: `depth: 0` does NOT drop the
|
|
41
|
+
// `pre_run_snapshot` jsonb column, so the row-fetch path was moving
|
|
42
|
+
// the entire snapshot payload (tens of KB × every unit) through
|
|
43
|
+
// Postgres TOAST + JSON.parse on EVERY Hub poll. On an 11.5k-unit
|
|
44
|
+
// batch that measured 7-12s per poll and was a driver of the
|
|
45
|
+
// 2026-06-10 prod outage.
|
|
46
|
+
if (batchIds.every((id)=>/^\d+$/.test(String(id)))) {
|
|
47
|
+
const db = getDrizzle(payload);
|
|
48
|
+
if (db) {
|
|
49
|
+
try {
|
|
50
|
+
const res = await db.execute(`SELECT batch_id_id AS bid, status, count(*) AS n
|
|
51
|
+
FROM ${slugToTable(unitsSlug)}
|
|
52
|
+
WHERE batch_id_id IN (${batchIds.map((id)=>String(id)).join(', ')})
|
|
53
|
+
GROUP BY 1, 2`);
|
|
54
|
+
for (const row of res?.rows ?? []){
|
|
55
|
+
tally(String(row.bid), String(row.status), Number(row.n ?? 0));
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
} catch (err) {
|
|
59
|
+
payload.logger?.warn?.(`[ai-translate] computeLiveBatchCounts: grouped-count SQL failed, falling back to row scan: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// Fallback (non-Postgres adapters / non-numeric ids): paginated row
|
|
64
|
+
// scan, projected to just (batchId, status) so the snapshot jsonb
|
|
65
|
+
// never leaves the database.
|
|
66
|
+
let page = 1;
|
|
67
|
+
const limit = 5000;
|
|
68
|
+
// eslint-disable-next-line no-constant-condition
|
|
69
|
+
while(true){
|
|
70
|
+
const result = await payload.find({
|
|
71
|
+
collection: unitsSlug,
|
|
72
|
+
where: {
|
|
73
|
+
batchId: {
|
|
74
|
+
in: batchIds.map((id)=>String(id))
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
page,
|
|
78
|
+
limit,
|
|
79
|
+
depth: 0,
|
|
80
|
+
select: {
|
|
81
|
+
batchId: true,
|
|
82
|
+
status: true
|
|
83
|
+
},
|
|
84
|
+
overrideAccess: true
|
|
85
|
+
});
|
|
86
|
+
for (const doc of result.docs){
|
|
87
|
+
tally(String(doc.batchId ?? ''), String(doc.status ?? ''), 1);
|
|
88
|
+
}
|
|
89
|
+
if (!result.hasNextPage) break;
|
|
90
|
+
page += 1;
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Convenience wrapper for the single-batch case (status endpoint,
|
|
96
|
+
* worker transition check). Returns the zero record if the batch has
|
|
97
|
+
* no units (anomalous but should never crash).
|
|
98
|
+
*/ export async function computeLiveBatchCount(payload, unitsSlug, batchId) {
|
|
99
|
+
const map = await computeLiveBatchCounts(payload, unitsSlug, [
|
|
100
|
+
batchId
|
|
101
|
+
]);
|
|
102
|
+
return map.get(batchId) ?? {
|
|
103
|
+
...ZERO_COUNTS
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Idempotent schema-shaping helpers for the bulk-translate engine
|
|
4
|
+
* (PR2 / v1.2.0). Payload's auto-migration generator handles plain
|
|
5
|
+
* column / index changes from collection definitions, but the bulk
|
|
6
|
+
* engine needs:
|
|
7
|
+
*
|
|
8
|
+
* 1. A PARTIAL UNIQUE INDEX on
|
|
9
|
+
* `bulk_translate_units (collection, document_id, locale)
|
|
10
|
+
* WHERE status IN ('pending','running')`. This enforces the
|
|
11
|
+
* F-DA-TOCTOU dedup invariant at the DB level (a second worker
|
|
12
|
+
* cannot insert / upsert a unit for an already-active locale).
|
|
13
|
+
* Payload's index generator does not emit `WHERE`-clauses, so
|
|
14
|
+
* this is hand-written SQL.
|
|
15
|
+
*
|
|
16
|
+
* 2. A composite UNIQUE constraint on
|
|
17
|
+
* `translation_daily_spend (date, consumer_key)` to back the
|
|
18
|
+
* cap utility's atomic upsert. Payload's index generator can
|
|
19
|
+
* emit this from the collection definition, but on existing
|
|
20
|
+
* installs the rename may not propagate — calling this helper
|
|
21
|
+
* ensures it.
|
|
22
|
+
*
|
|
23
|
+
* Both run via `payload.db.drizzle.execute(sql\`...\`)`. SQL uses
|
|
24
|
+
* `CREATE ... IF NOT EXISTS` so the same call across many boots is
|
|
25
|
+
* a no-op after the first success.
|
|
26
|
+
*
|
|
27
|
+
* Two ways to use:
|
|
28
|
+
* - Add to consumer's migration file (recommended for clean
|
|
29
|
+
* migration history): import and call from inside an `up`
|
|
30
|
+
* function.
|
|
31
|
+
* - Call from the plugin's `onInit` (when `bulk.enabled` is true) —
|
|
32
|
+
* idempotent boot-time self-healing. Trade-off: errors at boot
|
|
33
|
+
* surface as plugin failures rather than as migration failures.
|
|
34
|
+
*/
|
|
35
|
+
export interface EnsureBulkTranslateSchemaOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Override the collection slugs if the consumer customised them
|
|
38
|
+
* via plugin config. Defaults pull from the collection modules.
|
|
39
|
+
*/
|
|
40
|
+
bulkTranslateUnitsSlug?: string;
|
|
41
|
+
translationDailySpendSlug?: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function ensureBulkTranslateSchema(payload: Payload, opts?: EnsureBulkTranslateSchemaOptions): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* One-off data migration: prefix existing `ai-translate-meta`
|
|
46
|
+
* source-hash rows with the `v1:` version tag (R2 / F-ENG-07).
|
|
47
|
+
*
|
|
48
|
+
* Without this, the first bulk run after a plugin upgrade would treat
|
|
49
|
+
* every previously-translated field as "source changed" — burning
|
|
50
|
+
* tokens to re-translate everything that was already in sync.
|
|
51
|
+
*
|
|
52
|
+
* Idempotent: rows that already start with `v1:` are skipped. Safe
|
|
53
|
+
* to run repeatedly. Consumer should run ONCE per deploy that bumps
|
|
54
|
+
* past 1.1.16. Returns a count of rows updated.
|
|
55
|
+
*/
|
|
56
|
+
export declare function migrateHashesToV1Prefix(payload: Payload, opts?: {
|
|
57
|
+
metaSlug?: string;
|
|
58
|
+
dryRun?: boolean;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
scanned: number;
|
|
61
|
+
updated: number;
|
|
62
|
+
alreadyMigrated: number;
|
|
63
|
+
}>;
|
|
64
|
+
/**
|
|
65
|
+
* v1.2.4 data migration: align `bulk_translate_batches.status` vocabulary
|
|
66
|
+
* on `'success'` (matching the units collection + the
|
|
67
|
+
* `BulkTranslateBatchStatus` TS union). Pre-1.2.4 the schema declared
|
|
68
|
+
* `'completed'` as the terminal-success value while the type union used
|
|
69
|
+
* `'success'`, so badges, filter chips, and the Hub's "recently
|
|
70
|
+
* terminal" query were all silently dead for successful runs. Worker
|
|
71
|
+
* + schema now write `'success'`; this helper backfills existing rows.
|
|
72
|
+
*
|
|
73
|
+
* Idempotent: only updates rows where status = 'completed'. Safe to run
|
|
74
|
+
* repeatedly; subsequent calls are no-ops.
|
|
75
|
+
*
|
|
76
|
+
* Consumer wiring:
|
|
77
|
+
* - Add to a forward migration's up() OR
|
|
78
|
+
* - Call from plugin onInit() once per deploy (boot-time self-heal).
|
|
79
|
+
*/
|
|
80
|
+
export declare function migrateBatchStatusVocabulary(payload: Payload, opts?: {
|
|
81
|
+
batchesSlug?: string;
|
|
82
|
+
dryRun?: boolean;
|
|
83
|
+
}): Promise<{
|
|
84
|
+
scanned: number;
|
|
85
|
+
updated: number;
|
|
86
|
+
alreadyAligned: number;
|
|
87
|
+
}>;
|
|
88
|
+
export declare function slugToTable(slug: string): string;
|
|
89
|
+
export type Drizzle = {
|
|
90
|
+
execute(sql: string): Promise<unknown>;
|
|
91
|
+
};
|
|
92
|
+
export declare function getDrizzle(payload: Payload): Drizzle | null;
|