@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,109 @@
|
|
|
1
|
+
import { translateGlobal } from '../api.js';
|
|
2
|
+
import { REENTRY_FLAG, SKIP_FLAG } from '../defaults.js';
|
|
3
|
+
import { getDocAutoLocaleOverride, getDocOptOut, getEffectiveAutoLocales, getGlobalKillSwitches } from '../lib/effective-locales.js';
|
|
4
|
+
import { completeJobNow, getActiveJobForDoc, getOrCreateJobForDoc } from '../lib/progress-store.js';
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Hook factory
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
export function createTranslateGlobalAfterChangeHook(globalSlug, pluginOptions, queue) {
|
|
9
|
+
const diffOnly = pluginOptions.automation?.diffOnly !== false; // default true
|
|
10
|
+
return async ({ req, doc, previousDoc })=>{
|
|
11
|
+
// Re-entrancy guard — plugin's own writes and bulk imports skip translation
|
|
12
|
+
const ctx = req.context ?? {};
|
|
13
|
+
if (ctx[REENTRY_FLAG] || ctx[SKIP_FLAG]) {
|
|
14
|
+
return doc;
|
|
15
|
+
}
|
|
16
|
+
// Only translate source-locale saves
|
|
17
|
+
if (req.locale && req.locale !== pluginOptions.sourceLocale) {
|
|
18
|
+
return doc;
|
|
19
|
+
}
|
|
20
|
+
// Plugin-wide auto-translate kill switch (v1.2.8). See after-change.ts
|
|
21
|
+
// for the full reasoning; same gate, mirrored for globals.
|
|
22
|
+
const killSwitches = await getGlobalKillSwitches(req.payload, pluginOptions);
|
|
23
|
+
if (!killSwitches.autoEnabled) {
|
|
24
|
+
return doc;
|
|
25
|
+
}
|
|
26
|
+
// Early-return helper that closes out any stranded job seeded by a
|
|
27
|
+
// parent flow. Mirrors the pattern in after-change.ts.
|
|
28
|
+
const earlyReturn = ()=>{
|
|
29
|
+
const stranded = getActiveJobForDoc(globalSlug, globalSlug);
|
|
30
|
+
if (stranded) completeJobNow(stranded.jobId);
|
|
31
|
+
return doc;
|
|
32
|
+
};
|
|
33
|
+
// Per-doc opt-out gate. Editor explicitly checked "skip
|
|
34
|
+
// auto-translate for this global save" — bail before any other work.
|
|
35
|
+
if (getDocOptOut(doc)) {
|
|
36
|
+
return earlyReturn();
|
|
37
|
+
}
|
|
38
|
+
// Layered locale resolution: plugin → site-wide → per-global →
|
|
39
|
+
// per-doc narrow. Empty result skips dispatch entirely. See
|
|
40
|
+
// `getEffectiveAutoLocales` for the full stack.
|
|
41
|
+
const decision = await getEffectiveAutoLocales(req.payload, pluginOptions, globalSlug);
|
|
42
|
+
if (decision.skipAutoOnPublish || decision.locales.length === 0) {
|
|
43
|
+
return earlyReturn();
|
|
44
|
+
}
|
|
45
|
+
// Per-doc widget narrows further when set. Empty array = inherit;
|
|
46
|
+
// opt-out is the separate boolean flag handled above.
|
|
47
|
+
const docOverride = getDocAutoLocaleOverride(doc);
|
|
48
|
+
let effectiveLocales = decision.locales;
|
|
49
|
+
if (docOverride !== undefined) {
|
|
50
|
+
const allowed = new Set(docOverride);
|
|
51
|
+
effectiveLocales = effectiveLocales.filter((l)=>allowed.has(l));
|
|
52
|
+
}
|
|
53
|
+
if (effectiveLocales.length === 0) {
|
|
54
|
+
return earlyReturn();
|
|
55
|
+
}
|
|
56
|
+
// Atomic get-or-create — see after-change.ts for the rationale.
|
|
57
|
+
// When `created: false`, another fire is already dispatching the
|
|
58
|
+
// translation; this fire returns early to avoid duplicate LLM
|
|
59
|
+
// calls and locale-write races.
|
|
60
|
+
const { jobId, created } = getOrCreateJobForDoc(globalSlug, globalSlug, effectiveLocales);
|
|
61
|
+
if (!created) {
|
|
62
|
+
return doc;
|
|
63
|
+
}
|
|
64
|
+
// Honor diffOnly:false — don't pass previousDoc downstream, so the
|
|
65
|
+
// per-unit diff filter in translateGlobal stays disabled.
|
|
66
|
+
const previousDocForTranslate = diffOnly ? previousDoc : undefined;
|
|
67
|
+
// When async mode is configured, route through the coalescing queue so a
|
|
68
|
+
// burst of saves on the same global collapses into one translation pass.
|
|
69
|
+
// Without the queue (inline mode, or queue not provided), fire-and-forget.
|
|
70
|
+
//
|
|
71
|
+
// Wrap dispatch in try/catch so a queue/translate failure can never
|
|
72
|
+
// propagate up to Payload and 500 the original save. Mirrors the
|
|
73
|
+
// pattern in after-change.ts.
|
|
74
|
+
try {
|
|
75
|
+
if (queue) {
|
|
76
|
+
queue.enqueue({
|
|
77
|
+
payload: req.payload,
|
|
78
|
+
kind: 'global',
|
|
79
|
+
collection: globalSlug,
|
|
80
|
+
id: globalSlug,
|
|
81
|
+
doc: doc,
|
|
82
|
+
previousDoc: previousDocForTranslate ?? {},
|
|
83
|
+
jobId,
|
|
84
|
+
req,
|
|
85
|
+
targetLocales: effectiveLocales
|
|
86
|
+
});
|
|
87
|
+
return doc;
|
|
88
|
+
}
|
|
89
|
+
// Fire-and-forget: never block the save.
|
|
90
|
+
// Awaiting here would deadlock the request transaction against the
|
|
91
|
+
// hook's own payload.updateGlobal calls on the locale rows we just
|
|
92
|
+
// wrote. Let the request commit, then translate in the background.
|
|
93
|
+
void translateGlobal(req.payload, {
|
|
94
|
+
global: globalSlug,
|
|
95
|
+
previousDoc: previousDocForTranslate,
|
|
96
|
+
jobId,
|
|
97
|
+
req,
|
|
98
|
+
targetLocales: effectiveLocales
|
|
99
|
+
}).catch((err)=>{
|
|
100
|
+
const msg = err instanceof Error ? err.message : 'Unknown error';
|
|
101
|
+
req.payload.logger.error(`[ai-translate] Auto-translation failed for global "${globalSlug}": ${msg}`);
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
105
|
+
req.payload.logger.error(`[ai-translate] Auto-translation dispatch failed for global "${globalSlug}": ${msg}`);
|
|
106
|
+
}
|
|
107
|
+
return doc;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CollectionAfterChangeHook } from 'payload';
|
|
2
|
+
import type { CoalescingQueue } from '../lib/coalescing-queue.js';
|
|
3
|
+
import type { AITranslatePluginConfig } from '../types.js';
|
|
4
|
+
export type CreateAfterChangeHookOptions = {
|
|
5
|
+
collectionSlug: string;
|
|
6
|
+
pluginOptions: AITranslatePluginConfig;
|
|
7
|
+
queue: CoalescingQueue | null;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the collection has draft/publish versioning. When false the
|
|
10
|
+
* `_status` field doesn't exist on the doc, so the on-publish trigger gate
|
|
11
|
+
* is skipped and every source-locale save is translated.
|
|
12
|
+
*/
|
|
13
|
+
hasDrafts: boolean;
|
|
14
|
+
};
|
|
15
|
+
export declare function createTranslateAfterChangeHook(options: CreateAfterChangeHookOptions): CollectionAfterChangeHook;
|
|
16
|
+
export declare function createTranslateAfterChangeHook(collectionSlug: string, pluginOptions: AITranslatePluginConfig, queue: CoalescingQueue | null): CollectionAfterChangeHook;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { translateDocument } from '../api.js';
|
|
2
|
+
import { REENTRY_FLAG, SKIP_FLAG } from '../defaults.js';
|
|
3
|
+
import { getDocAutoLocaleOverride, getDocOptOut, getEffectiveAutoLocales, getGlobalKillSwitches } from '../lib/effective-locales.js';
|
|
4
|
+
import { diffFields } from '../lib/field-diff.js';
|
|
5
|
+
import { resolveTranslatableFields } from '../lib/field-resolver.js';
|
|
6
|
+
import { completeJobNow, getActiveJobForDoc, getOrCreateJobForDoc } from '../lib/progress-store.js';
|
|
7
|
+
export function createTranslateAfterChangeHook(arg1, pluginOptionsArg, queueArg) {
|
|
8
|
+
const opts = typeof arg1 === 'string' ? {
|
|
9
|
+
collectionSlug: arg1,
|
|
10
|
+
pluginOptions: pluginOptionsArg,
|
|
11
|
+
queue: queueArg ?? null,
|
|
12
|
+
hasDrafts: true
|
|
13
|
+
} : arg1;
|
|
14
|
+
const { collectionSlug, pluginOptions, queue, hasDrafts } = opts;
|
|
15
|
+
const automation = pluginOptions.automation;
|
|
16
|
+
const trigger = automation?.trigger ?? 'on-change';
|
|
17
|
+
const diffOnly = automation?.diffOnly !== false; // default true
|
|
18
|
+
return async ({ req, doc, previousDoc, operation })=>{
|
|
19
|
+
// Re-entrancy guard — plugin's own writes and bulk imports skip translation
|
|
20
|
+
const ctx = req.context ?? {};
|
|
21
|
+
if (ctx[REENTRY_FLAG] || ctx[SKIP_FLAG]) {
|
|
22
|
+
return doc;
|
|
23
|
+
}
|
|
24
|
+
// Only translate source-locale saves
|
|
25
|
+
if (req.locale && req.locale !== pluginOptions.sourceLocale) {
|
|
26
|
+
return doc;
|
|
27
|
+
}
|
|
28
|
+
// Plugin-wide auto-translate kill switch (v1.2.8). When admin flipped
|
|
29
|
+
// `globalAutoTranslateEnabled` off in the settings global, this hook
|
|
30
|
+
// short-circuits before any other work — no progress job, no LLM
|
|
31
|
+
// dispatch, no restore-version recovery. Manual Translate and Bulk
|
|
32
|
+
// Translate still work because they don't route through this hook.
|
|
33
|
+
// Fail-safe-CLOSED on settings read failure: skip auto. See
|
|
34
|
+
// `getGlobalKillSwitches` for the per-flag reasoning.
|
|
35
|
+
const killSwitches = await getGlobalKillSwitches(req.payload, pluginOptions);
|
|
36
|
+
if (!killSwitches.autoEnabled) {
|
|
37
|
+
return doc;
|
|
38
|
+
}
|
|
39
|
+
// BUG-23 fix: when Payload's restoreVersion fires, target-locale rows
|
|
40
|
+
// get reset because the version snapshot only captured source content.
|
|
41
|
+
// Detect this via the `operation` hook arg (Payload sets it to
|
|
42
|
+
// 'restoreVersion' for restore writes) and FORCE auto-translate to
|
|
43
|
+
// re-populate target locales — even if `trigger: 'on-publish'` and
|
|
44
|
+
// the restore landed on a draft. Without this, editors lose all
|
|
45
|
+
// target-locale content silently and must remember to click Translate.
|
|
46
|
+
const isRestoreVersion = operation === 'restoreVersion';
|
|
47
|
+
if (isRestoreVersion) {
|
|
48
|
+
req.payload.logger?.warn?.(`[ai-translate] Version restore detected for ${collectionSlug}/${doc?.id}. Target-locale rows are being reset by Payload — the plugin will auto-translate to repopulate them. Editors who had manually refined target content should review after the auto-translate completes.`);
|
|
49
|
+
}
|
|
50
|
+
// On-publish trigger: only enforced when the collection actually has
|
|
51
|
+
// drafts. Without versioning, _status doesn't exist on the doc and we
|
|
52
|
+
// would gate forever, never translating. Treat non-versioned collections
|
|
53
|
+
// as on-change.
|
|
54
|
+
//
|
|
55
|
+
// BUG-23: restoreVersion bypasses the publish gate. Target locales
|
|
56
|
+
// get wiped regardless of whether the restored version was a draft
|
|
57
|
+
// or published — recovery must run either way.
|
|
58
|
+
let isPublishTransition = false;
|
|
59
|
+
if (trigger === 'on-publish' && hasDrafts && !isRestoreVersion) {
|
|
60
|
+
const currStatus = doc?._status;
|
|
61
|
+
if (currStatus !== 'published') {
|
|
62
|
+
return doc;
|
|
63
|
+
}
|
|
64
|
+
const prevStatus = previousDoc?._status;
|
|
65
|
+
isPublishTransition = prevStatus !== 'published';
|
|
66
|
+
}
|
|
67
|
+
// Field diff: skip if no translatable fields changed.
|
|
68
|
+
// EXCEPT on a publish-transition (draft → published) where the autosave
|
|
69
|
+
// baseline already matches the published content. In that case the diff
|
|
70
|
+
// is empty but the document was never translated yet — force translation
|
|
71
|
+
// by clearing previousDoc downstream.
|
|
72
|
+
//
|
|
73
|
+
// ALSO except on restoreVersion (BUG-23): the restore reset target
|
|
74
|
+
// locales so we MUST re-translate everything to recover. The diff
|
|
75
|
+
// path would skip because the source content matches a past version.
|
|
76
|
+
let forceTranslateAll = isRestoreVersion;
|
|
77
|
+
if (diffOnly && previousDoc && !isRestoreVersion) {
|
|
78
|
+
const fields = getCollectionFields(req.payload, collectionSlug);
|
|
79
|
+
if (fields) {
|
|
80
|
+
const translatableFields = resolveTranslatableFields(fields);
|
|
81
|
+
const fieldPaths = translatableFields.map((f)=>f.path);
|
|
82
|
+
const changedPaths = diffFields(previousDoc, doc, fieldPaths);
|
|
83
|
+
if (changedPaths.length === 0) {
|
|
84
|
+
if (!isPublishTransition) {
|
|
85
|
+
return doc;
|
|
86
|
+
}
|
|
87
|
+
// Publish-transition with identical content → translate all
|
|
88
|
+
forceTranslateAll = true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const documentId = doc?.id;
|
|
93
|
+
if (!documentId) {
|
|
94
|
+
return doc;
|
|
95
|
+
}
|
|
96
|
+
// Helper: an early-return path that ALSO closes out any active job
|
|
97
|
+
// a parent flow may have seeded. Without this, a stranded `running`
|
|
98
|
+
// job hangs in the UI for ~2 minutes (TTL sweep) showing
|
|
99
|
+
// "Translating…" that never completes.
|
|
100
|
+
const earlyReturn = ()=>{
|
|
101
|
+
const stranded = getActiveJobForDoc(collectionSlug, documentId);
|
|
102
|
+
if (stranded) completeJobNow(stranded.jobId);
|
|
103
|
+
return doc;
|
|
104
|
+
};
|
|
105
|
+
// Per-doc opt-out gate. When the editor explicitly checked
|
|
106
|
+
// "skip auto-translate for this doc," bail before any other work.
|
|
107
|
+
// Manual Translate from the dialog is NOT gated by this — same
|
|
108
|
+
// scope as the collection-level `enabled` flag.
|
|
109
|
+
if (getDocOptOut(doc)) {
|
|
110
|
+
return earlyReturn();
|
|
111
|
+
}
|
|
112
|
+
// Resolve effective locales via the layered stack:
|
|
113
|
+
// plugin config → site-wide → per-collection → per-doc narrow
|
|
114
|
+
// Empty result = admin opted out of every locale (or the per-collection
|
|
115
|
+
// widget unchecked everything), so skip dispatch entirely (no ghost
|
|
116
|
+
// progress job, no LLM cost). Manual Translate button still works
|
|
117
|
+
// against config.targetLocales unaffected — these gates apply to the
|
|
118
|
+
// automation path only.
|
|
119
|
+
const decision = await getEffectiveAutoLocales(req.payload, pluginOptions, collectionSlug);
|
|
120
|
+
if (decision.skipAutoOnPublish || decision.locales.length === 0) {
|
|
121
|
+
return earlyReturn();
|
|
122
|
+
}
|
|
123
|
+
// Per-doc widget narrows the effective set further when set:
|
|
124
|
+
// - undefined / empty → inherit collection-level decision (no narrow)
|
|
125
|
+
// - [...] → restrict to these locales (intersected with decision)
|
|
126
|
+
// Opt-out is expressed via the separate `_aiTranslateOptOut` flag
|
|
127
|
+
// (checked above) — not via empty array here.
|
|
128
|
+
// Stale locales (codes no longer in plugin config) are silently
|
|
129
|
+
// dropped here so a stored ['de','fr','XX-removed'] still narrows
|
|
130
|
+
// to ['de','fr'] rather than failing the intersection.
|
|
131
|
+
const docOverride = getDocAutoLocaleOverride(doc);
|
|
132
|
+
let effectiveLocales = decision.locales;
|
|
133
|
+
if (docOverride !== undefined) {
|
|
134
|
+
const allowed = new Set(docOverride);
|
|
135
|
+
effectiveLocales = effectiveLocales.filter((l)=>allowed.has(l));
|
|
136
|
+
}
|
|
137
|
+
if (effectiveLocales.length === 0) {
|
|
138
|
+
return earlyReturn();
|
|
139
|
+
}
|
|
140
|
+
// When diffOnly is OFF, force a full retranslation by passing undefined —
|
|
141
|
+
// otherwise translateDocument's per-unit diff filter would still narrow
|
|
142
|
+
// to changed fields, which is the opposite of what diffOnly:false means.
|
|
143
|
+
const previousDocForTranslate = forceTranslateAll || !diffOnly ? undefined : previousDoc;
|
|
144
|
+
// Reuse an active job for this doc if one exists (e.g. queued
|
|
145
|
+
// translation still running) — otherwise create a fresh one so the
|
|
146
|
+
// UI can show "Translating..." immediately. The same jobId is
|
|
147
|
+
// threaded through to translateDocument so updateJob ticks land on
|
|
148
|
+
// the job the UI is polling.
|
|
149
|
+
//
|
|
150
|
+
// `getOrCreateJobForDoc` is atomic from the event-loop's
|
|
151
|
+
// perspective: two concurrent fires for the same doc can't both
|
|
152
|
+
// pass the existence check and both create a job. When `created`
|
|
153
|
+
// is false the second caller joins the in-flight job and the
|
|
154
|
+
// duplicate dispatch below becomes a redundant no-op
|
|
155
|
+
// (translateDocument is idempotent w.r.t. an already-completed
|
|
156
|
+
// job because updateJob silently no-ops on terminal status).
|
|
157
|
+
const { jobId, created } = getOrCreateJobForDoc(collectionSlug, documentId, effectiveLocales);
|
|
158
|
+
if (!created) {
|
|
159
|
+
// Another fire already created the job and is dispatching the
|
|
160
|
+
// translation. Don't fire again — the second LLM call would
|
|
161
|
+
// double-bill and race on locale writes.
|
|
162
|
+
return doc;
|
|
163
|
+
}
|
|
164
|
+
// Dispatch translation — never block the save
|
|
165
|
+
try {
|
|
166
|
+
if (queue) {
|
|
167
|
+
// Async mode: coalesce rapid saves
|
|
168
|
+
queue.enqueue({
|
|
169
|
+
payload: req.payload,
|
|
170
|
+
collection: collectionSlug,
|
|
171
|
+
id: documentId,
|
|
172
|
+
doc: doc,
|
|
173
|
+
previousDoc: previousDocForTranslate ?? {},
|
|
174
|
+
jobId,
|
|
175
|
+
req,
|
|
176
|
+
targetLocales: effectiveLocales
|
|
177
|
+
});
|
|
178
|
+
} else {
|
|
179
|
+
// Inline mode: fire-and-forget to avoid transaction deadlock
|
|
180
|
+
// (the hook holds a DB transaction, and translateDocument reads the same doc)
|
|
181
|
+
void translateDocument(req.payload, {
|
|
182
|
+
collection: collectionSlug,
|
|
183
|
+
id: documentId,
|
|
184
|
+
previousDoc: previousDocForTranslate,
|
|
185
|
+
jobId,
|
|
186
|
+
req,
|
|
187
|
+
targetLocales: effectiveLocales
|
|
188
|
+
}).catch((err)=>{
|
|
189
|
+
req.payload.logger.error(`[ai-translate] Inline translation failed for ${collectionSlug}/${String(documentId)}: ${err instanceof Error ? err.message : 'Unknown'}`);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const msg = error instanceof Error ? error.message : 'Unknown error';
|
|
194
|
+
req.payload.logger.error(`[ai-translate] Auto-translation failed for ${collectionSlug}/${String(documentId)}: ${msg}`);
|
|
195
|
+
}
|
|
196
|
+
return doc;
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Helpers
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
function getCollectionFields(payload, slug) {
|
|
203
|
+
const collection = payload.config?.collections?.find((c)=>c.slug === slug);
|
|
204
|
+
return collection?.fields;
|
|
205
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CollectionAfterDeleteHook } from 'payload';
|
|
2
|
+
import type { AITranslatePluginConfig } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* After-delete cleanup hook (BUG-20 + BUG-26).
|
|
5
|
+
*
|
|
6
|
+
* When a tracked-collection document is deleted, two plugin-owned tables
|
|
7
|
+
* still hold rows referencing the doc:
|
|
8
|
+
*
|
|
9
|
+
* - `ai_translate_jobs` — visible "Translating..." rows in the admin
|
|
10
|
+
* Jobs page. Without cleanup, deleted docs leave ghost-running jobs
|
|
11
|
+
* here that the UI shows forever (BUG-26 stuck-running on bulk
|
|
12
|
+
* delete during active translation).
|
|
13
|
+
*
|
|
14
|
+
* - `ai_translate_meta` — manual-edit fingerprints. Without cleanup,
|
|
15
|
+
* stale fingerprints accumulate across the table's lifetime; if a
|
|
16
|
+
* new doc reuses the same numeric id (rare but possible on
|
|
17
|
+
* short-cycle test DBs), the stale fingerprint blocks the first
|
|
18
|
+
* translation. Storage just keeps growing unboundedly (BUG-20).
|
|
19
|
+
*
|
|
20
|
+
* The hook runs AFTER Payload's delete operation completes — best-
|
|
21
|
+
* effort. If cleanup fails the doc deletion still succeeded; we log
|
|
22
|
+
* but don't throw. Brief orphan windows are tolerated; the next save
|
|
23
|
+
* on a different doc isn't affected.
|
|
24
|
+
*
|
|
25
|
+
* In-memory progress-store cleanup: also marks any active in-memory
|
|
26
|
+
* job for this doc as completed (with reason: 'document deleted')
|
|
27
|
+
* so SSE subscribers see the terminal state immediately rather than
|
|
28
|
+
* waiting for the 2-minute TTL sweep.
|
|
29
|
+
*/
|
|
30
|
+
export declare function createAiTranslateAfterDeleteHook(collectionSlug: string, pluginOptions: AITranslatePluginConfig): CollectionAfterDeleteHook;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DEFAULT_JOBS_COLLECTION_SLUG } from '../jobs-collection.js';
|
|
2
|
+
import { completeJobNow, getActiveJobForDoc } from '../lib/progress-store.js';
|
|
3
|
+
import { DEFAULT_MANUAL_EDIT_COLLECTION_SLUG } from '../manual-edit-collection.js';
|
|
4
|
+
/**
|
|
5
|
+
* After-delete cleanup hook (BUG-20 + BUG-26).
|
|
6
|
+
*
|
|
7
|
+
* When a tracked-collection document is deleted, two plugin-owned tables
|
|
8
|
+
* still hold rows referencing the doc:
|
|
9
|
+
*
|
|
10
|
+
* - `ai_translate_jobs` — visible "Translating..." rows in the admin
|
|
11
|
+
* Jobs page. Without cleanup, deleted docs leave ghost-running jobs
|
|
12
|
+
* here that the UI shows forever (BUG-26 stuck-running on bulk
|
|
13
|
+
* delete during active translation).
|
|
14
|
+
*
|
|
15
|
+
* - `ai_translate_meta` — manual-edit fingerprints. Without cleanup,
|
|
16
|
+
* stale fingerprints accumulate across the table's lifetime; if a
|
|
17
|
+
* new doc reuses the same numeric id (rare but possible on
|
|
18
|
+
* short-cycle test DBs), the stale fingerprint blocks the first
|
|
19
|
+
* translation. Storage just keeps growing unboundedly (BUG-20).
|
|
20
|
+
*
|
|
21
|
+
* The hook runs AFTER Payload's delete operation completes — best-
|
|
22
|
+
* effort. If cleanup fails the doc deletion still succeeded; we log
|
|
23
|
+
* but don't throw. Brief orphan windows are tolerated; the next save
|
|
24
|
+
* on a different doc isn't affected.
|
|
25
|
+
*
|
|
26
|
+
* In-memory progress-store cleanup: also marks any active in-memory
|
|
27
|
+
* job for this doc as completed (with reason: 'document deleted')
|
|
28
|
+
* so SSE subscribers see the terminal state immediately rather than
|
|
29
|
+
* waiting for the 2-minute TTL sweep.
|
|
30
|
+
*/ export function createAiTranslateAfterDeleteHook(collectionSlug, pluginOptions) {
|
|
31
|
+
return async ({ id, req })=>{
|
|
32
|
+
if (id === undefined || id === null) return;
|
|
33
|
+
// 1. Close out the in-memory progress-store job (UI feedback)
|
|
34
|
+
try {
|
|
35
|
+
const active = getActiveJobForDoc(collectionSlug, id);
|
|
36
|
+
if (active) {
|
|
37
|
+
completeJobNow(active.jobId);
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
req.payload.logger?.warn?.(`[ai-translate] In-memory job cleanup failed for ${collectionSlug}/${String(id)}: ${err instanceof Error ? err.message : 'Unknown'}`);
|
|
41
|
+
}
|
|
42
|
+
// 2. Delete persisted `ai_translate_jobs` rows for this doc
|
|
43
|
+
if (pluginOptions.persistJobs) {
|
|
44
|
+
const jobsSlug = pluginOptions.jobsCollectionSlug ?? DEFAULT_JOBS_COLLECTION_SLUG;
|
|
45
|
+
try {
|
|
46
|
+
await req.payload.delete({
|
|
47
|
+
collection: jobsSlug,
|
|
48
|
+
where: {
|
|
49
|
+
and: [
|
|
50
|
+
{
|
|
51
|
+
collection: {
|
|
52
|
+
equals: collectionSlug
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
documentId: {
|
|
57
|
+
equals: String(id)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
overrideAccess: true
|
|
63
|
+
});
|
|
64
|
+
} catch (err) {
|
|
65
|
+
req.payload.logger?.warn?.(`[ai-translate] ai_translate_jobs cleanup failed for ${collectionSlug}/${String(id)}: ${err instanceof Error ? err.message : 'Unknown'}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// 3. Delete `ai_translate_meta` fingerprints for this doc
|
|
69
|
+
if (pluginOptions.preserveManualEdits) {
|
|
70
|
+
const metaSlug = pluginOptions.manualEditCollectionSlug ?? DEFAULT_MANUAL_EDIT_COLLECTION_SLUG;
|
|
71
|
+
try {
|
|
72
|
+
await req.payload.delete({
|
|
73
|
+
collection: metaSlug,
|
|
74
|
+
where: {
|
|
75
|
+
and: [
|
|
76
|
+
{
|
|
77
|
+
collection: {
|
|
78
|
+
equals: collectionSlug
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
documentId: {
|
|
83
|
+
equals: String(id)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
},
|
|
88
|
+
overrideAccess: true
|
|
89
|
+
});
|
|
90
|
+
} catch (err) {
|
|
91
|
+
req.payload.logger?.warn?.(`[ai-translate] ai_translate_meta cleanup failed for ${collectionSlug}/${String(id)}: ${err instanceof Error ? err.message : 'Unknown'}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { translateDocument } from './api.js';
|
|
2
|
+
export { ensureBulkTranslateSchema, migrateBatchStatusVocabulary, migrateHashesToV1Prefix, } from './lib/bulk-translate-migrations.js';
|
|
3
|
+
export { createScopedLogger, type LogContext, type LogEventFields, type LogLevel, type ScopedLogger, type SerializedError, serializeErr, } from './lib/logger.js';
|
|
4
|
+
export { type AcquireTokenResult, acquireToken, getBucketStatus, type TokenBucketOptions, } from './lib/translation-token-bucket.js';
|
|
5
|
+
export { aiTranslatePlugin } from './plugin.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { translateDocument } from './api.js';
|
|
2
|
+
export { ensureBulkTranslateSchema, migrateBatchStatusVocabulary, migrateHashesToV1Prefix } from './lib/bulk-translate-migrations.js';
|
|
3
|
+
export { createScopedLogger, serializeErr } from './lib/logger.js';
|
|
4
|
+
export { acquireToken, getBucketStatus } from './lib/translation-token-bucket.js';
|
|
5
|
+
export { aiTranslatePlugin } from './plugin.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Access, CollectionConfig } from 'payload';
|
|
2
|
+
export declare const DEFAULT_JOBS_COLLECTION_SLUG = "ai-translate-jobs";
|
|
3
|
+
/**
|
|
4
|
+
* Optional sidecar collection used by `persistJobs`. Mirrors the in-
|
|
5
|
+
* memory progress-store state so admin views, debug pages, or post-
|
|
6
|
+
* restart inspection can see which translations were in flight.
|
|
7
|
+
*
|
|
8
|
+
* NOT a full work-queue — the actual translation closures (req, doc,
|
|
9
|
+
* previousDoc) stay in process memory. A server restart still loses
|
|
10
|
+
* the in-flight LLM call. This collection persists the STATUS only so
|
|
11
|
+
* admins can spot "ghost" jobs that stopped progressing and re-trigger
|
|
12
|
+
* them manually.
|
|
13
|
+
*
|
|
14
|
+
* For full work resumption, see the follow-up ticket on migrating to
|
|
15
|
+
* Payload's native jobs queue (`payload.jobs.queue`).
|
|
16
|
+
*/
|
|
17
|
+
export declare function createJobsCollection(readAccess?: Access, slug?: string, deleteAccess?: Access): CollectionConfig;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
export const DEFAULT_JOBS_COLLECTION_SLUG = 'ai-translate-jobs';
|
|
2
|
+
/**
|
|
3
|
+
* Optional sidecar collection used by `persistJobs`. Mirrors the in-
|
|
4
|
+
* memory progress-store state so admin views, debug pages, or post-
|
|
5
|
+
* restart inspection can see which translations were in flight.
|
|
6
|
+
*
|
|
7
|
+
* NOT a full work-queue — the actual translation closures (req, doc,
|
|
8
|
+
* previousDoc) stay in process memory. A server restart still loses
|
|
9
|
+
* the in-flight LLM call. This collection persists the STATUS only so
|
|
10
|
+
* admins can spot "ghost" jobs that stopped progressing and re-trigger
|
|
11
|
+
* them manually.
|
|
12
|
+
*
|
|
13
|
+
* For full work resumption, see the follow-up ticket on migrating to
|
|
14
|
+
* Payload's native jobs queue (`payload.jobs.queue`).
|
|
15
|
+
*/ export function createJobsCollection(readAccess, slug = DEFAULT_JOBS_COLLECTION_SLUG, deleteAccess) {
|
|
16
|
+
const isAdminDefault = ({ req })=>{
|
|
17
|
+
const user = req.user;
|
|
18
|
+
const roles = user?.roles;
|
|
19
|
+
if (Array.isArray(roles) && roles.includes('admin')) return true;
|
|
20
|
+
return false;
|
|
21
|
+
};
|
|
22
|
+
return {
|
|
23
|
+
slug,
|
|
24
|
+
// System rows are never edited in the admin document view, so
|
|
25
|
+
// Payload's document-locking buys nothing here — and its lock check
|
|
26
|
+
// costs a second pool connection inside every update transaction
|
|
27
|
+
// (core's checkDocumentLockStatus runs a find without `req`). Under
|
|
28
|
+
// concurrent updates (e.g. dismiss-all on alerts) that exhausted the
|
|
29
|
+
// pool and deadlocked a consumer in prod on 2026-06-10.
|
|
30
|
+
lockDocuments: false,
|
|
31
|
+
labels: {
|
|
32
|
+
singular: 'AI Translate Job (in-flight)',
|
|
33
|
+
plural: 'AI Translate Jobs — in-flight queue'
|
|
34
|
+
},
|
|
35
|
+
admin: {
|
|
36
|
+
group: 'System',
|
|
37
|
+
useAsTitle: 'jobId',
|
|
38
|
+
defaultColumns: [
|
|
39
|
+
'jobId',
|
|
40
|
+
'collection',
|
|
41
|
+
'documentId',
|
|
42
|
+
'status',
|
|
43
|
+
'startedAt'
|
|
44
|
+
],
|
|
45
|
+
hidden: ({ user })=>{
|
|
46
|
+
const roles = user?.roles;
|
|
47
|
+
return !Array.isArray(roles) || !roles.includes('admin');
|
|
48
|
+
},
|
|
49
|
+
description: 'IN-FLIGHT QUEUE ONLY — not a permanent log. Each row mirrors a currently-running or just-finished translation job. Rows auto-expire ~2 minutes after the job completes. If this view is EMPTY, no translations are currently running — that is the normal idle state. For permanent translation history + cost data, see "Translation Usage".'
|
|
50
|
+
},
|
|
51
|
+
access: {
|
|
52
|
+
read: readAccess ?? isAdminDefault,
|
|
53
|
+
create: ()=>false,
|
|
54
|
+
update: ()=>false,
|
|
55
|
+
delete: deleteAccess ?? isAdminDefault
|
|
56
|
+
},
|
|
57
|
+
fields: [
|
|
58
|
+
{
|
|
59
|
+
name: 'jobId',
|
|
60
|
+
type: 'text',
|
|
61
|
+
required: true,
|
|
62
|
+
index: true,
|
|
63
|
+
admin: {
|
|
64
|
+
readOnly: true
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'collection',
|
|
69
|
+
type: 'text',
|
|
70
|
+
required: true,
|
|
71
|
+
index: true,
|
|
72
|
+
admin: {
|
|
73
|
+
readOnly: true
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'documentId',
|
|
78
|
+
type: 'text',
|
|
79
|
+
required: true,
|
|
80
|
+
index: true,
|
|
81
|
+
admin: {
|
|
82
|
+
readOnly: true
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: 'status',
|
|
87
|
+
type: 'select',
|
|
88
|
+
required: true,
|
|
89
|
+
options: [
|
|
90
|
+
{
|
|
91
|
+
label: 'Running',
|
|
92
|
+
value: 'running'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
label: 'Completed',
|
|
96
|
+
value: 'completed'
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
label: 'Failed',
|
|
100
|
+
value: 'failed'
|
|
101
|
+
}
|
|
102
|
+
],
|
|
103
|
+
admin: {
|
|
104
|
+
readOnly: true
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'totalLocales',
|
|
109
|
+
type: 'number',
|
|
110
|
+
required: true,
|
|
111
|
+
admin: {
|
|
112
|
+
readOnly: true
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'completedLocales',
|
|
117
|
+
type: 'json',
|
|
118
|
+
admin: {
|
|
119
|
+
readOnly: true
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'failedLocales',
|
|
124
|
+
type: 'json',
|
|
125
|
+
admin: {
|
|
126
|
+
readOnly: true,
|
|
127
|
+
description: 'Array of `{ locale, error }` for failed locales.'
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: 'startedAt',
|
|
132
|
+
type: 'date',
|
|
133
|
+
admin: {
|
|
134
|
+
readOnly: true
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
};
|
|
139
|
+
}
|