@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,376 @@
|
|
|
1
|
+
import { translateDocument, translateGlobal } from '../api.js';
|
|
2
|
+
import { describeAuthRejection } from '../lib/auth-diagnostics.js';
|
|
3
|
+
import { collectBlocksConfig, extractTranslationUnits } from '../lib/content-extractor.js';
|
|
4
|
+
import { getEffectiveExcludePatternsForSurface, getExcludedFieldPaths, getGlobalKillSwitches, isPathExcluded, isSurfaceDisabledByAdmin } from '../lib/effective-locales.js';
|
|
5
|
+
import { resolveTranslatableFields } from '../lib/field-resolver.js';
|
|
6
|
+
import { findByIdNoFallback } from '../lib/payload-read.js';
|
|
7
|
+
import { createJob, getActiveJobForDoc } from '../lib/progress-store.js';
|
|
8
|
+
function resolveContext(arg, url) {
|
|
9
|
+
if (typeof arg === 'object' && arg !== null) return arg;
|
|
10
|
+
// String arg or undefined → derive from URL.
|
|
11
|
+
// Collection: /api/{slug}/ai-translate
|
|
12
|
+
// Global: /api/globals/{slug}/ai-translate
|
|
13
|
+
try {
|
|
14
|
+
const segments = new URL(url).pathname.split('/').filter(Boolean);
|
|
15
|
+
const apiIndex = segments.indexOf('api');
|
|
16
|
+
if (apiIndex >= 0) {
|
|
17
|
+
if (segments[apiIndex + 1] === 'globals' && segments[apiIndex + 2]) {
|
|
18
|
+
return {
|
|
19
|
+
kind: 'global',
|
|
20
|
+
slug: segments[apiIndex + 2]
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (segments[apiIndex + 1]) {
|
|
24
|
+
return {
|
|
25
|
+
kind: 'collection',
|
|
26
|
+
slug: arg ?? segments[apiIndex + 1]
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
// fall through
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
kind: 'collection',
|
|
35
|
+
slug: typeof arg === 'string' ? arg : ''
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export const getTranslateHandler = (arg)=>async (req)=>{
|
|
39
|
+
try {
|
|
40
|
+
if (!req.user) {
|
|
41
|
+
return Response.json({
|
|
42
|
+
error: 'Unauthorized',
|
|
43
|
+
diagnostic: describeAuthRejection(req)
|
|
44
|
+
}, {
|
|
45
|
+
status: 401
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
const config = req.payload.config?.custom?.aiTranslate;
|
|
49
|
+
if (config.access?.translate) {
|
|
50
|
+
const allowed = await config.access.translate({
|
|
51
|
+
req
|
|
52
|
+
});
|
|
53
|
+
if (!allowed) {
|
|
54
|
+
return Response.json({
|
|
55
|
+
error: 'Forbidden'
|
|
56
|
+
}, {
|
|
57
|
+
status: 403
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Plugin-wide manual-translate kill switch (v1.2.8). When admin
|
|
62
|
+
// flipped `globalManualTranslateEnabled` off, every manual entry
|
|
63
|
+
// point — Translate dialog AND per-field button — is refused with
|
|
64
|
+
// a friendly 403. Auto and Bulk are unaffected. The dialog +
|
|
65
|
+
// button hide themselves in the client based on the same flag in
|
|
66
|
+
// the client-config response, but the server gate is the source
|
|
67
|
+
// of truth.
|
|
68
|
+
const globalKillSwitches = await getGlobalKillSwitches(req.payload, config);
|
|
69
|
+
if (!globalKillSwitches.manualEnabled) {
|
|
70
|
+
return Response.json({
|
|
71
|
+
error: 'Manual Translate is paused site-wide. An admin can re-enable it in Translation Settings.'
|
|
72
|
+
}, {
|
|
73
|
+
status: 403
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// BUG-08 fix: honor the per-surface `enabled` kill-switch on manual
|
|
77
|
+
// translate too. Pre-fix this gate was automation-only; editors
|
|
78
|
+
// could manual-translate a surface the admin had explicitly turned
|
|
79
|
+
// off. Admins who want "manual yes, auto no" should use the sibling
|
|
80
|
+
// `autoOnPublish` flag instead, which remains automation-only.
|
|
81
|
+
const ctxForGate = resolveContext(arg, req.url ?? '');
|
|
82
|
+
const isDisabled = await isSurfaceDisabledByAdmin(req.payload, config, ctxForGate.slug);
|
|
83
|
+
if (isDisabled) {
|
|
84
|
+
return Response.json({
|
|
85
|
+
error: 'Translation is turned off for this content type. Ask an admin to enable it in the translation settings.'
|
|
86
|
+
}, {
|
|
87
|
+
status: 403
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
let body;
|
|
91
|
+
try {
|
|
92
|
+
body = typeof req.json === 'function' ? await req.json() : {};
|
|
93
|
+
} catch {
|
|
94
|
+
return Response.json({
|
|
95
|
+
error: 'Invalid JSON body'
|
|
96
|
+
}, {
|
|
97
|
+
status: 400
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const { id, ids, targetLocales, targetPolicy, fields, async: asyncMode, draft: bodyDraft, writeMode, confirmCost } = body;
|
|
101
|
+
const draft = bodyDraft ?? true;
|
|
102
|
+
const ctx = resolveContext(arg, req.url ?? '');
|
|
103
|
+
// Validate caller-supplied target locales against the plugin's
|
|
104
|
+
// configured `targetLocales`. Without this, an invalid locale code
|
|
105
|
+
// (e.g. `"xx"` from a typo or a client that ships ahead of config)
|
|
106
|
+
// still reaches the LLM — the model guesses a language, charges
|
|
107
|
+
// tokens, and Payload silently drops the write because the locale
|
|
108
|
+
// isn't registered. Validate up front and 400 instead.
|
|
109
|
+
if (targetLocales && Array.isArray(targetLocales)) {
|
|
110
|
+
const allowed = new Set(config.targetLocales);
|
|
111
|
+
const invalid = targetLocales.filter((l)=>!allowed.has(l));
|
|
112
|
+
if (invalid.length > 0) {
|
|
113
|
+
return Response.json({
|
|
114
|
+
error: `Invalid target locale(s): ${invalid.map((l)=>`"${l}"`).join(', ')}. Configured targetLocales: ${config.targetLocales.map((l)=>`"${l}"`).join(', ')}`
|
|
115
|
+
}, {
|
|
116
|
+
status: 400
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const effectiveLocales = targetLocales ?? config.targetLocales;
|
|
121
|
+
// Cost guard: pre-flight an estimate when the projected work is large
|
|
122
|
+
// (multi-doc batch, or many locales × one doc). Compares to the
|
|
123
|
+
// configured `bulkConfirmUsdThreshold` and returns 402 unless the
|
|
124
|
+
// caller acknowledges with `confirmCost: true`. Per-doc translation
|
|
125
|
+
// is bounded by `perDocCharCeiling` inside the API; this is the
|
|
126
|
+
// multi-doc / multi-locale fanout guard that was previously
|
|
127
|
+
// declared in config but never enforced.
|
|
128
|
+
if (!confirmCost && ctx.kind === 'collection' && config.provider.estimate) {
|
|
129
|
+
const isBulk = (ids?.length ?? 0) > 1;
|
|
130
|
+
const wideFanout = effectiveLocales.length > 3;
|
|
131
|
+
if (isBulk || wideFanout) {
|
|
132
|
+
try {
|
|
133
|
+
const estimate = await estimateRequestCost({
|
|
134
|
+
req,
|
|
135
|
+
config,
|
|
136
|
+
ctx,
|
|
137
|
+
ids: ids ?? (id !== undefined ? [
|
|
138
|
+
id
|
|
139
|
+
] : []),
|
|
140
|
+
targetLocales: effectiveLocales,
|
|
141
|
+
draft
|
|
142
|
+
});
|
|
143
|
+
if (estimate.estimatedCostUsd !== undefined && estimate.estimatedCostUsd > config.costLimits.bulkConfirmUsdThreshold) {
|
|
144
|
+
return Response.json({
|
|
145
|
+
error: 'Estimated cost exceeds bulkConfirmUsdThreshold',
|
|
146
|
+
estimate: {
|
|
147
|
+
estimatedCostUsd: estimate.estimatedCostUsd,
|
|
148
|
+
documentCount: estimate.documentCount,
|
|
149
|
+
localeCount: effectiveLocales.length,
|
|
150
|
+
threshold: config.costLimits.bulkConfirmUsdThreshold
|
|
151
|
+
},
|
|
152
|
+
hint: 'Re-send with `confirmCost: true` to acknowledge and proceed.'
|
|
153
|
+
}, {
|
|
154
|
+
status: 402
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Estimate failure shouldn't block translation — fall through
|
|
159
|
+
// to the normal path. The provider's per-call limits still
|
|
160
|
+
// apply as a backstop.
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// Globals don't take id — they're singletons identified by slug.
|
|
165
|
+
if (ctx.kind === 'global') {
|
|
166
|
+
if (asyncMode) {
|
|
167
|
+
const jobId = createJob(ctx.slug, ctx.slug, effectiveLocales);
|
|
168
|
+
// Fire-and-forget. The progress endpoint's SSE stream pushes
|
|
169
|
+
// updates as `updateJob` ticks land.
|
|
170
|
+
void translateGlobal(req.payload, {
|
|
171
|
+
global: ctx.slug,
|
|
172
|
+
targetLocales,
|
|
173
|
+
targetPolicy,
|
|
174
|
+
fields,
|
|
175
|
+
req,
|
|
176
|
+
jobId,
|
|
177
|
+
writeMode
|
|
178
|
+
}).catch((err)=>{
|
|
179
|
+
req.payload.logger?.error?.(`[ai-translate] Background translateGlobal failed for ${ctx.slug}: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
180
|
+
});
|
|
181
|
+
return Response.json({
|
|
182
|
+
results: [
|
|
183
|
+
{
|
|
184
|
+
jobId,
|
|
185
|
+
succeeded: [],
|
|
186
|
+
failed: []
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
}, {
|
|
190
|
+
status: 202
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const result = await translateGlobal(req.payload, {
|
|
194
|
+
global: ctx.slug,
|
|
195
|
+
targetLocales,
|
|
196
|
+
targetPolicy,
|
|
197
|
+
fields,
|
|
198
|
+
req,
|
|
199
|
+
writeMode
|
|
200
|
+
});
|
|
201
|
+
return Response.json({
|
|
202
|
+
results: [
|
|
203
|
+
result
|
|
204
|
+
]
|
|
205
|
+
}, {
|
|
206
|
+
status: 200
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (!id && (!ids || ids.length === 0)) {
|
|
210
|
+
return Response.json({
|
|
211
|
+
error: 'Either "id" or "ids" must be provided'
|
|
212
|
+
}, {
|
|
213
|
+
status: 400
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
if (asyncMode && id) {
|
|
217
|
+
// Coalesce concurrent same-doc async requests: if another
|
|
218
|
+
// translation is already running for this (collection, id) pair,
|
|
219
|
+
// return its jobId instead of spawning a second LLM call. The
|
|
220
|
+
// client can subscribe to the existing job's SSE stream. This
|
|
221
|
+
// prevents the 2× cost from rapid duplicate clicks of a
|
|
222
|
+
// per-field translate button or two admins racing on the same
|
|
223
|
+
// doc.
|
|
224
|
+
const existing = getActiveJobForDoc(ctx.slug, id);
|
|
225
|
+
if (existing) {
|
|
226
|
+
return Response.json({
|
|
227
|
+
results: [
|
|
228
|
+
{
|
|
229
|
+
jobId: existing.jobId,
|
|
230
|
+
succeeded: [],
|
|
231
|
+
failed: [],
|
|
232
|
+
coalesced: true
|
|
233
|
+
}
|
|
234
|
+
]
|
|
235
|
+
}, {
|
|
236
|
+
status: 202
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const jobId = createJob(ctx.slug, id, effectiveLocales);
|
|
240
|
+
void translateDocument(req.payload, {
|
|
241
|
+
collection: ctx.slug,
|
|
242
|
+
id,
|
|
243
|
+
targetLocales,
|
|
244
|
+
targetPolicy,
|
|
245
|
+
fields,
|
|
246
|
+
req,
|
|
247
|
+
jobId,
|
|
248
|
+
draft,
|
|
249
|
+
writeMode
|
|
250
|
+
}).catch((err)=>{
|
|
251
|
+
req.payload.logger?.error?.(`[ai-translate] Background translateDocument failed for ${ctx.slug}/${String(id)}: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
252
|
+
});
|
|
253
|
+
return Response.json({
|
|
254
|
+
results: [
|
|
255
|
+
{
|
|
256
|
+
jobId,
|
|
257
|
+
succeeded: [],
|
|
258
|
+
failed: []
|
|
259
|
+
}
|
|
260
|
+
]
|
|
261
|
+
}, {
|
|
262
|
+
status: 202
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
let results;
|
|
266
|
+
if (id) {
|
|
267
|
+
const result = await translateDocument(req.payload, {
|
|
268
|
+
collection: ctx.slug,
|
|
269
|
+
id,
|
|
270
|
+
targetLocales,
|
|
271
|
+
targetPolicy,
|
|
272
|
+
fields,
|
|
273
|
+
req,
|
|
274
|
+
draft,
|
|
275
|
+
writeMode
|
|
276
|
+
});
|
|
277
|
+
results = [
|
|
278
|
+
result
|
|
279
|
+
];
|
|
280
|
+
} else {
|
|
281
|
+
results = await Promise.all(ids.map((docId)=>translateDocument(req.payload, {
|
|
282
|
+
collection: ctx.slug,
|
|
283
|
+
id: docId,
|
|
284
|
+
targetLocales,
|
|
285
|
+
targetPolicy,
|
|
286
|
+
fields,
|
|
287
|
+
req,
|
|
288
|
+
draft,
|
|
289
|
+
writeMode
|
|
290
|
+
})));
|
|
291
|
+
}
|
|
292
|
+
return Response.json({
|
|
293
|
+
results
|
|
294
|
+
}, {
|
|
295
|
+
status: 200
|
|
296
|
+
});
|
|
297
|
+
} catch (error) {
|
|
298
|
+
const message = error instanceof Error ? error.message : 'Internal server error';
|
|
299
|
+
return Response.json({
|
|
300
|
+
error: message
|
|
301
|
+
}, {
|
|
302
|
+
status: 500
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
/**
|
|
307
|
+
* Pre-flight cost estimate used by the `bulkConfirmUsdThreshold` guard.
|
|
308
|
+
* Mirrors the `/ai-translate/estimate` endpoint's calculation but
|
|
309
|
+
* inlined to avoid the round-trip — the caller already has a valid
|
|
310
|
+
* authenticated request, the document IDs, and the configured provider.
|
|
311
|
+
*
|
|
312
|
+
* Returns `estimatedCostUsd: undefined` if the provider doesn't
|
|
313
|
+
* implement `estimate()`. Caller treats `undefined` as "no guard
|
|
314
|
+
* possible" and proceeds (the per-call / per-doc limits still apply).
|
|
315
|
+
*/ async function estimateRequestCost(params) {
|
|
316
|
+
const { req, config, ctx, ids, targetLocales, draft } = params;
|
|
317
|
+
if (ctx.kind !== 'collection') return {
|
|
318
|
+
estimatedCostUsd: undefined,
|
|
319
|
+
documentCount: 0
|
|
320
|
+
};
|
|
321
|
+
const collections = req.payload.config?.collections ?? [];
|
|
322
|
+
const collectionConfig = collections.find((c)=>c.slug === ctx.slug);
|
|
323
|
+
if (!collectionConfig) return {
|
|
324
|
+
estimatedCostUsd: undefined,
|
|
325
|
+
documentCount: 0
|
|
326
|
+
};
|
|
327
|
+
const collectionFields = collectionConfig.fields;
|
|
328
|
+
let translatableFields = resolveTranslatableFields(collectionFields);
|
|
329
|
+
// Admin per-surface exclusions also apply to the internal cost
|
|
330
|
+
// estimate (bulkConfirmUsdThreshold guard) so the user doesn't get
|
|
331
|
+
// a confirmation prompt sized against fields that won't actually
|
|
332
|
+
// be translated.
|
|
333
|
+
const adminExcludedPaths = await getExcludedFieldPaths(req.payload, config, ctx.slug);
|
|
334
|
+
if (adminExcludedPaths.size > 0) {
|
|
335
|
+
translatableFields = translatableFields.filter((f)=>!isPathExcluded(f.path, adminExcludedPaths));
|
|
336
|
+
}
|
|
337
|
+
const blocksConfig = collectBlocksConfig(collectionFields);
|
|
338
|
+
const excludePatterns = await getEffectiveExcludePatternsForSurface(req.payload, config, ctx.slug);
|
|
339
|
+
const allItems = [];
|
|
340
|
+
for (const docId of ids){
|
|
341
|
+
const doc = await findByIdNoFallback(req.payload, ctx.slug, docId, config.sourceLocale, {
|
|
342
|
+
draft
|
|
343
|
+
});
|
|
344
|
+
if (!doc) continue;
|
|
345
|
+
const units = extractTranslationUnits(doc, translatableFields, excludePatterns, blocksConfig);
|
|
346
|
+
for (const unit of units){
|
|
347
|
+
allItems.push({
|
|
348
|
+
id: unit.id,
|
|
349
|
+
text: unit.text,
|
|
350
|
+
kind: unit.kind
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (allItems.length === 0 || !config.provider.estimate) {
|
|
355
|
+
return {
|
|
356
|
+
estimatedCostUsd: undefined,
|
|
357
|
+
documentCount: ids.length
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
const mockRequest = {
|
|
361
|
+
items: allItems,
|
|
362
|
+
sourceLocale: config.sourceLocale,
|
|
363
|
+
targetLocale: targetLocales[0] ?? '',
|
|
364
|
+
context: {
|
|
365
|
+
collectionSlug: ctx.slug,
|
|
366
|
+
fieldPath: '*'
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
const estimate = await config.provider.estimate(mockRequest);
|
|
370
|
+
// Multiply by locale count — provider estimates a single locale pass.
|
|
371
|
+
const totalCost = estimate.estimatedCostUsd !== undefined ? estimate.estimatedCostUsd * targetLocales.length : undefined;
|
|
372
|
+
return {
|
|
373
|
+
estimatedCostUsd: totalCost,
|
|
374
|
+
documentCount: ids.length
|
|
375
|
+
};
|
|
376
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Payload, PayloadRequest } from 'payload';
|
|
2
|
+
import type { AITranslatePluginConfig, BulkTranslateConfig } from '../../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Pre-1.2.8: every translation-hub endpoint was admin-only (Decision
|
|
5
|
+
* #31). v1.2.8 opens trigger + read endpoints to editor-role users so
|
|
6
|
+
* content editors can run bulk translation without needing an admin
|
|
7
|
+
* to fire it for them. Admin-only access is preserved for
|
|
8
|
+
* configuration / cost-limit / force-reset surfaces; per-batch
|
|
9
|
+
* mutations (cancel / retry / revert) are gated by `ownsBatch` so
|
|
10
|
+
* one editor cannot disrupt another's run.
|
|
11
|
+
*
|
|
12
|
+
* Defaults to checking for the literal `'admin'` role. Consumers using
|
|
13
|
+
* a different role naming convention should wrap this in their plugin
|
|
14
|
+
* config's `bulk.requireTotp`/audit-role surfaces; the surface here
|
|
15
|
+
* intentionally stays narrow.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isAdminUser(user: PayloadRequest['user'] | undefined): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Returns `true` for admin OR editor roles. Used by the editor-open
|
|
20
|
+
* endpoints (enqueue / active / status / preflight / list / failures)
|
|
21
|
+
* — the per-batch mutation endpoints layer `ownsBatch` on top of this.
|
|
22
|
+
*
|
|
23
|
+
* `editor` is the conventional role name for content editors across
|
|
24
|
+
* the wild-payload-cms-style consumers; consumers using a different
|
|
25
|
+
* role naming convention should add the synonym to the membership
|
|
26
|
+
* list rather than re-implementing the check.
|
|
27
|
+
*/
|
|
28
|
+
export declare function isEditorOrAdmin(user: PayloadRequest['user'] | undefined): boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Ownership check for per-batch mutations (cancel / retry-failed /
|
|
31
|
+
* revert). Admins always pass — they own the world. Editors must be
|
|
32
|
+
* the user who triggered the batch (matched on `triggeredByUserId`).
|
|
33
|
+
*
|
|
34
|
+
* The batch record may carry `triggeredByUserId` as either `string`
|
|
35
|
+
* (the canonical persisted form — see `enqueue.ts:266`) or `number`
|
|
36
|
+
* depending on the auth collection's id type. Compare stringified to
|
|
37
|
+
* sidestep that.
|
|
38
|
+
*/
|
|
39
|
+
export declare function ownsBatch(user: PayloadRequest['user'] | undefined, batch: {
|
|
40
|
+
triggeredByUserId?: string | number | null;
|
|
41
|
+
} | null | undefined): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Structured 403 for "you can do this kind of action, but not on
|
|
44
|
+
* THIS batch (you didn't trigger it)". Distinct from the 401
|
|
45
|
+
* `unauthorizedResponse` (cookie / CSRF failure) and from the 403
|
|
46
|
+
* we'd return for an editor hitting an admin-only endpoint — the
|
|
47
|
+
* `code` lets the UI render the right copy.
|
|
48
|
+
*/
|
|
49
|
+
export declare function forbiddenOwnershipResponse(): Response;
|
|
50
|
+
export type ErrorBody = {
|
|
51
|
+
error: {
|
|
52
|
+
code: string;
|
|
53
|
+
message: string;
|
|
54
|
+
fields?: Record<string, string>;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Structured error JSON used across the translation-hub endpoints.
|
|
59
|
+
* Mirrors the shape an `api-designer` would draft for a public API —
|
|
60
|
+
* `code` is the stable machine-readable identifier (UI surfaces
|
|
61
|
+
* key off it), `message` is human-readable, `fields` carries per-
|
|
62
|
+
* field validation hints when relevant.
|
|
63
|
+
*/
|
|
64
|
+
export declare function errorResponse(code: string, message: string, status: number, fields?: Record<string, string>): Response;
|
|
65
|
+
/**
|
|
66
|
+
* Structured 401 with a `diagnostic` field naming why auth was rejected —
|
|
67
|
+
* almost always a CSRF allowlist mismatch when the page was opened via the
|
|
68
|
+
* `Network:` URL Next prints in dev. Without this, the response is a
|
|
69
|
+
* silent "Unauthorized" and the next investigator wastes 30 min on JWTs.
|
|
70
|
+
*/
|
|
71
|
+
export declare function unauthorizedResponse(req: PayloadRequest): Response;
|
|
72
|
+
/**
|
|
73
|
+
* Decision #13 v2 + F-SEC-TOTP-BYPASS: TOTP is friction, not the
|
|
74
|
+
* primary security boundary. The daily USD cap is the real
|
|
75
|
+
* enforcement. We validate the supplied code against the TOTP plugin
|
|
76
|
+
* if one is wired up; if the plugin isn't installed, we log a warn
|
|
77
|
+
* and skip the check rather than failing closed (the consumer may be
|
|
78
|
+
* a single-admin setup with no TOTP enrollment).
|
|
79
|
+
*
|
|
80
|
+
* Returns `{ ok: true }` when the code is valid OR the plugin isn't
|
|
81
|
+
* installed AND `requireTotp` was set to `true`. Returns
|
|
82
|
+
* `{ ok: false, ... }` with a structured reason otherwise.
|
|
83
|
+
*
|
|
84
|
+
* The actual TOTP code verification reads the auth-collection's
|
|
85
|
+
* `totp.secret` (encrypted) + delegates to `verifyTOTPToken`. We
|
|
86
|
+
* resolve those lazily so the ai-translate plugin doesn't take a
|
|
87
|
+
* hard dependency on the totp plugin's exports.
|
|
88
|
+
*/
|
|
89
|
+
export type TotpResult = {
|
|
90
|
+
ok: true;
|
|
91
|
+
} | {
|
|
92
|
+
ok: false;
|
|
93
|
+
code: 'missing' | 'invalid' | 'plugin_unavailable';
|
|
94
|
+
message: string;
|
|
95
|
+
};
|
|
96
|
+
export declare function verifyTotpCode(payload: Payload, userId: string | number, totpCode: string | undefined, options: {
|
|
97
|
+
required: boolean;
|
|
98
|
+
allowSkipWhenUnavailable: boolean;
|
|
99
|
+
}): Promise<TotpResult>;
|
|
100
|
+
export declare function getAiTranslateConfig(payload: Payload): AITranslatePluginConfig | undefined;
|
|
101
|
+
export declare function getBulkConfig(payload: Payload): BulkTranslateConfig | undefined;
|
|
102
|
+
/**
|
|
103
|
+
* Reads a JSON body using the same `req.json()` pattern existing
|
|
104
|
+
* handlers in this directory rely on. Returns either the parsed body
|
|
105
|
+
* or an `errorResponse` Response object so the handler can `return`
|
|
106
|
+
* it directly.
|
|
107
|
+
*
|
|
108
|
+
* Returning a Response (instead of throwing) keeps the handler
|
|
109
|
+
* flat — no try/catch around an awaited body read at every call
|
|
110
|
+
* site.
|
|
111
|
+
*/
|
|
112
|
+
export declare function readJsonBody<T>(req: PayloadRequest): Promise<{
|
|
113
|
+
ok: true;
|
|
114
|
+
body: T;
|
|
115
|
+
} | {
|
|
116
|
+
ok: false;
|
|
117
|
+
res: Response;
|
|
118
|
+
}>;
|
|
119
|
+
/**
|
|
120
|
+
* The status endpoint paginates unit drill-down via opaque cursors.
|
|
121
|
+
* The internal representation is `{ offset }` — base64-encoded so
|
|
122
|
+
* clients can't poke at it and depend on internal shape. Cheap and
|
|
123
|
+
* stable; switching to a row-id cursor later is a non-breaking change
|
|
124
|
+
* because the surface is opaque.
|
|
125
|
+
*/
|
|
126
|
+
export declare function encodeCursor(offset: number): string;
|
|
127
|
+
export declare function decodeCursor(cursor: string | undefined): number;
|
|
128
|
+
/**
|
|
129
|
+
* Decision #33 mandates an audit-log entry on revert. We don't take a
|
|
130
|
+
* hard dependency on `auditLogPlugin` — instead we write directly to
|
|
131
|
+
* the audit-logs collection if it exists. Best-effort: failures are
|
|
132
|
+
* logged but don't abort the action.
|
|
133
|
+
*/
|
|
134
|
+
export declare function writeBulkAuditEntry(payload: Payload, params: {
|
|
135
|
+
userId: string | number;
|
|
136
|
+
userEmail?: string;
|
|
137
|
+
action: string;
|
|
138
|
+
batchId: string;
|
|
139
|
+
metadata?: Record<string, unknown>;
|
|
140
|
+
}): Promise<void>;
|