@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,237 @@
|
|
|
1
|
+
import { describeAuthRejection } from '../lib/auth-diagnostics.js';
|
|
2
|
+
import { collectBlocksConfig, extractTranslationUnits } from '../lib/content-extractor.js';
|
|
3
|
+
import { getEffectiveExcludePatternsForSurface, getExcludedFieldPaths, isPathExcluded } from '../lib/effective-locales.js';
|
|
4
|
+
import { resolveTranslatableFields } from '../lib/field-resolver.js';
|
|
5
|
+
import { findByIdNoFallback } from '../lib/payload-read.js';
|
|
6
|
+
/**
|
|
7
|
+
* Heuristic: would the validator mark this string as a verbatim echo if the
|
|
8
|
+
* LLM returned it unchanged? Used to subtract "likely skipped" chars from
|
|
9
|
+
* the billable estimate so the editor sees an accurate cost.
|
|
10
|
+
*
|
|
11
|
+
* Conservative — false positives would just make the estimate look smaller
|
|
12
|
+
* than reality, but we'd rather over-estimate than under-estimate. The
|
|
13
|
+
* actual validator runs at translation time; this is a pre-flight guess.
|
|
14
|
+
*/ function isLikelyVerbatim(text) {
|
|
15
|
+
const trimmed = text.trim();
|
|
16
|
+
if (trimmed.length < 3) return true; // too short to translate meaningfully
|
|
17
|
+
if (/^https?:\/\//i.test(trimmed)) return true; // URLs
|
|
18
|
+
if (/^[\d.\s,-]+$/.test(trimmed)) return true; // pure numeric / dates
|
|
19
|
+
if (/^[A-Z0-9_-]+$/.test(trimmed) && trimmed.length < 12) return true; // SKU / code
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
function resolveContext(arg, url) {
|
|
23
|
+
if (typeof arg === 'object' && arg !== null) return arg;
|
|
24
|
+
try {
|
|
25
|
+
const segments = new URL(url).pathname.split('/').filter(Boolean);
|
|
26
|
+
const apiIndex = segments.indexOf('api');
|
|
27
|
+
if (apiIndex >= 0) {
|
|
28
|
+
if (segments[apiIndex + 1] === 'globals' && segments[apiIndex + 2]) {
|
|
29
|
+
return {
|
|
30
|
+
kind: 'global',
|
|
31
|
+
slug: segments[apiIndex + 2]
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (segments[apiIndex + 1]) {
|
|
35
|
+
return {
|
|
36
|
+
kind: 'collection',
|
|
37
|
+
slug: arg ?? segments[apiIndex + 1]
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
// fall through
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
kind: 'collection',
|
|
46
|
+
slug: typeof arg === 'string' ? arg : ''
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export const getEstimateHandler = (arg)=>async (req)=>{
|
|
50
|
+
try {
|
|
51
|
+
if (!req.user) {
|
|
52
|
+
return Response.json({
|
|
53
|
+
error: 'Unauthorized',
|
|
54
|
+
diagnostic: describeAuthRejection(req)
|
|
55
|
+
}, {
|
|
56
|
+
status: 401
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const config = req.payload.config?.custom?.aiTranslate;
|
|
60
|
+
if (config.access?.translate) {
|
|
61
|
+
const allowed = await config.access.translate({
|
|
62
|
+
req
|
|
63
|
+
});
|
|
64
|
+
if (!allowed) {
|
|
65
|
+
return Response.json({
|
|
66
|
+
error: 'Forbidden'
|
|
67
|
+
}, {
|
|
68
|
+
status: 403
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
let body;
|
|
73
|
+
try {
|
|
74
|
+
body = await req.json();
|
|
75
|
+
} catch {
|
|
76
|
+
return Response.json({
|
|
77
|
+
error: 'Invalid JSON body'
|
|
78
|
+
}, {
|
|
79
|
+
status: 400
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const { ids, targetLocales: bodyLocales, draft: bodyDraft } = body;
|
|
83
|
+
const targetLocales = bodyLocales ?? config.targetLocales;
|
|
84
|
+
const draft = bodyDraft ?? true;
|
|
85
|
+
const ctx = resolveContext(arg, req.url ?? '');
|
|
86
|
+
let totalCharacters = 0;
|
|
87
|
+
const allItems = [];
|
|
88
|
+
let documentCount = 0;
|
|
89
|
+
const excludePatterns = await getEffectiveExcludePatternsForSurface(req.payload, config, ctx.slug);
|
|
90
|
+
// Honour admin-configured exclusions so the estimate matches what
|
|
91
|
+
// the translate pipeline will actually send. Without this, the
|
|
92
|
+
// editor sees an inflated cost and may bail unnecessarily.
|
|
93
|
+
const adminExcludedPaths = await getExcludedFieldPaths(req.payload, config, ctx.slug);
|
|
94
|
+
if (ctx.kind === 'global') {
|
|
95
|
+
const globals = req.payload.config?.globals ?? [];
|
|
96
|
+
const globalConfig = globals.find((g)=>g.slug === ctx.slug);
|
|
97
|
+
if (!globalConfig) {
|
|
98
|
+
return Response.json({
|
|
99
|
+
error: `Global "${ctx.slug}" not found`
|
|
100
|
+
}, {
|
|
101
|
+
status: 404
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Globals don't have drafts in Payload's model, so `draft` is a no-op
|
|
105
|
+
// here. Read the source-locale doc with no fallback so we estimate
|
|
106
|
+
// against the actual source content.
|
|
107
|
+
const doc = await req.payload.findGlobal({
|
|
108
|
+
slug: ctx.slug,
|
|
109
|
+
locale: config.sourceLocale,
|
|
110
|
+
fallbackLocale: null
|
|
111
|
+
});
|
|
112
|
+
if (doc) {
|
|
113
|
+
let translatableFields = resolveTranslatableFields(globalConfig.fields);
|
|
114
|
+
if (adminExcludedPaths.size > 0) {
|
|
115
|
+
translatableFields = translatableFields.filter((f)=>!isPathExcluded(f.path, adminExcludedPaths));
|
|
116
|
+
}
|
|
117
|
+
const blocksConfig = collectBlocksConfig(globalConfig.fields);
|
|
118
|
+
const units = extractTranslationUnits(doc, translatableFields, excludePatterns, blocksConfig);
|
|
119
|
+
for (const unit of units){
|
|
120
|
+
totalCharacters += unit.text.length;
|
|
121
|
+
allItems.push({
|
|
122
|
+
id: unit.id,
|
|
123
|
+
text: unit.text,
|
|
124
|
+
kind: unit.kind
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
documentCount = 1;
|
|
128
|
+
}
|
|
129
|
+
} else {
|
|
130
|
+
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
|
131
|
+
return Response.json({
|
|
132
|
+
error: '"ids" must be a non-empty array'
|
|
133
|
+
}, {
|
|
134
|
+
status: 400
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
const collections = req.payload.config?.collections ?? [];
|
|
138
|
+
const collectionConfig = collections.find((c)=>c.slug === ctx.slug);
|
|
139
|
+
if (!collectionConfig) {
|
|
140
|
+
return Response.json({
|
|
141
|
+
error: `Collection "${ctx.slug}" not found`
|
|
142
|
+
}, {
|
|
143
|
+
status: 404
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
const collectionFields = collectionConfig.fields;
|
|
147
|
+
let translatableFields = resolveTranslatableFields(collectionFields);
|
|
148
|
+
if (adminExcludedPaths.size > 0) {
|
|
149
|
+
translatableFields = translatableFields.filter((f)=>!isPathExcluded(f.path, adminExcludedPaths));
|
|
150
|
+
}
|
|
151
|
+
const blocksConfig = collectBlocksConfig(collectionFields);
|
|
152
|
+
for (const docId of ids){
|
|
153
|
+
const doc = await findByIdNoFallback(req.payload, ctx.slug, docId, config.sourceLocale, {
|
|
154
|
+
draft
|
|
155
|
+
});
|
|
156
|
+
if (!doc) continue;
|
|
157
|
+
const units = extractTranslationUnits(doc, translatableFields, excludePatterns, blocksConfig);
|
|
158
|
+
for (const unit of units){
|
|
159
|
+
totalCharacters += unit.text.length;
|
|
160
|
+
allItems.push({
|
|
161
|
+
id: unit.id,
|
|
162
|
+
text: unit.text,
|
|
163
|
+
kind: unit.kind
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
documentCount = ids.length;
|
|
168
|
+
}
|
|
169
|
+
// Tally how many characters we expect the validator to mark as
|
|
170
|
+
// `skipped` (verbatim echo). The estimate stays honest by separating
|
|
171
|
+
// billable from skipped, so the editor isn't surprised when "152 chars"
|
|
172
|
+
// turns into a smaller actual LLM call.
|
|
173
|
+
let likelySkippedCharacters = 0;
|
|
174
|
+
for (const item of allItems){
|
|
175
|
+
if (isLikelyVerbatim(item.text)) {
|
|
176
|
+
likelySkippedCharacters += item.text.length;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const billableCharacters = totalCharacters - likelySkippedCharacters;
|
|
180
|
+
// Token estimation. The previous formula (`billable / 3.5`) under-
|
|
181
|
+
// counted by ~7-30× because it ignored:
|
|
182
|
+
// 1) System prompt + few-shot overhead — the prompt template adds
|
|
183
|
+
// ~SYSTEM_PROMPT_TOKENS regardless of input size.
|
|
184
|
+
// 2) Output tokens — most LLMs return responses of length similar to
|
|
185
|
+
// or slightly larger than the input (translated text is ~1.0-1.2×
|
|
186
|
+
// source for most language pairs).
|
|
187
|
+
// 3) Per-batch repetition — the prompt is sent once per locale.
|
|
188
|
+
//
|
|
189
|
+
// Result: a more realistic projection that lines up within ~2× of
|
|
190
|
+
// billed cost for the typical case rather than being 30× off.
|
|
191
|
+
const SYSTEM_PROMPT_TOKENS_PER_LOCALE_PASS = 500;
|
|
192
|
+
const OUTPUT_TO_INPUT_RATIO = 1.1; // tend to be slightly longer
|
|
193
|
+
const estimatedInputTokens = Math.ceil(billableCharacters / 3.5);
|
|
194
|
+
const estimatedOutputTokens = Math.ceil(estimatedInputTokens * OUTPUT_TO_INPUT_RATIO);
|
|
195
|
+
const overheadTokens = SYSTEM_PROMPT_TOKENS_PER_LOCALE_PASS * targetLocales.length;
|
|
196
|
+
const estimatedTokens = (estimatedInputTokens + estimatedOutputTokens) * targetLocales.length + overheadTokens;
|
|
197
|
+
let estimatedCostUsd;
|
|
198
|
+
if (config.provider.estimate) {
|
|
199
|
+
// Cost estimate is based only on billable items — skipped strings
|
|
200
|
+
// never reach the provider so they don't bill.
|
|
201
|
+
const billableItems = allItems.filter((i)=>!isLikelyVerbatim(i.text));
|
|
202
|
+
const mockRequest = {
|
|
203
|
+
items: billableItems,
|
|
204
|
+
sourceLocale: config.sourceLocale,
|
|
205
|
+
targetLocale: targetLocales[0] ?? '',
|
|
206
|
+
context: {
|
|
207
|
+
collectionSlug: ctx.slug,
|
|
208
|
+
fieldPath: '*'
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const estimate = await config.provider.estimate(mockRequest);
|
|
212
|
+
estimatedCostUsd = estimate.estimatedCostUsd;
|
|
213
|
+
}
|
|
214
|
+
// Multiply by locale count (one translation pass per target locale)
|
|
215
|
+
if (estimatedCostUsd !== undefined) {
|
|
216
|
+
estimatedCostUsd = estimatedCostUsd * targetLocales.length;
|
|
217
|
+
}
|
|
218
|
+
return Response.json({
|
|
219
|
+
totalCharacters,
|
|
220
|
+
billableCharacters,
|
|
221
|
+
likelySkippedCharacters,
|
|
222
|
+
estimatedTokens,
|
|
223
|
+
estimatedInputTokens,
|
|
224
|
+
estimatedOutputTokens,
|
|
225
|
+
estimatedCostUsd,
|
|
226
|
+
documentCount,
|
|
227
|
+
localeCount: targetLocales.length
|
|
228
|
+
});
|
|
229
|
+
} catch (error) {
|
|
230
|
+
const message = error instanceof Error ? error.message : 'Internal server error';
|
|
231
|
+
return Response.json({
|
|
232
|
+
error: message
|
|
233
|
+
}, {
|
|
234
|
+
status: 500
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
};
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { describeAuthRejection } from '../lib/auth-diagnostics.js';
|
|
2
|
+
import { getActiveJobForDoc, getJob, subscribe, subscribeToDoc } from '../lib/progress-store.js';
|
|
3
|
+
const SSE_HEADERS = {
|
|
4
|
+
'Content-Type': 'text/event-stream',
|
|
5
|
+
'Cache-Control': 'no-cache',
|
|
6
|
+
Connection: 'keep-alive'
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Long-lived stream lifetime for the `?docId=X` "wait for any job" mode.
|
|
10
|
+
* After this, the stream closes and the browser auto-reconnects (the
|
|
11
|
+
* `retry: 30000` directive paces idle reconnects). 5 minutes balances:
|
|
12
|
+
* - Don't expire mid-edit-session.
|
|
13
|
+
* - Don't pile up forever per editor when nothing's happening.
|
|
14
|
+
*/ const DOC_STREAM_TTL_MS = 5 * 60_000;
|
|
15
|
+
/**
|
|
16
|
+
* Heartbeat keeps the SSE connection alive across proxies/load balancers
|
|
17
|
+
* that idle-time TCP. Sent as an SSE comment (`:hb`), which clients
|
|
18
|
+
* ignore but counts as activity.
|
|
19
|
+
*/ const HEARTBEAT_MS = 25_000;
|
|
20
|
+
/**
|
|
21
|
+
* Hold-open lifetime for an active-job stream (`?jobId=X` or `?docId=X`
|
|
22
|
+
* that resolved to a job). Jobs typically finish in <30s; 2min is a
|
|
23
|
+
* generous ceiling for long batch translations.
|
|
24
|
+
*/ const JOB_STREAM_TTL_MS = 120_000;
|
|
25
|
+
function jobToEvent(job) {
|
|
26
|
+
return {
|
|
27
|
+
jobId: job.jobId,
|
|
28
|
+
completed: [
|
|
29
|
+
...job.completedLocales
|
|
30
|
+
],
|
|
31
|
+
failed: [
|
|
32
|
+
...job.failedLocales
|
|
33
|
+
],
|
|
34
|
+
total: job.totalLocales,
|
|
35
|
+
status: job.status
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export const getProgressHandler = (collectionSlug)=>(req)=>{
|
|
39
|
+
if (!req.user) {
|
|
40
|
+
return Response.json({
|
|
41
|
+
error: 'Unauthorized',
|
|
42
|
+
diagnostic: describeAuthRejection(req)
|
|
43
|
+
}, {
|
|
44
|
+
status: 401
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const url = new URL(req.url);
|
|
48
|
+
const jobIdParam = url.searchParams.get('jobId');
|
|
49
|
+
const docIdParam = url.searchParams.get('docId');
|
|
50
|
+
// Fast path: explicit jobId — stream that job's events directly.
|
|
51
|
+
if (jobIdParam) {
|
|
52
|
+
const job = getJob(jobIdParam);
|
|
53
|
+
if (job) {
|
|
54
|
+
return new Response(buildJobStream(job.jobId, req.signal), {
|
|
55
|
+
headers: SSE_HEADERS
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
// Unknown jobId — fall through to idle close (client will reconnect
|
|
59
|
+
// and may resolve a different job by then).
|
|
60
|
+
return idleResponse();
|
|
61
|
+
}
|
|
62
|
+
// docId path: subscribe to "any active or future job for this doc".
|
|
63
|
+
if (docIdParam) {
|
|
64
|
+
const slug = collectionSlug ?? resolveSlugFromUrl(req.url ?? '');
|
|
65
|
+
if (!slug) return idleResponse();
|
|
66
|
+
const existing = getActiveJobForDoc(slug, docIdParam);
|
|
67
|
+
if (existing) {
|
|
68
|
+
// Existing in-flight job — stream it.
|
|
69
|
+
return new Response(buildJobStream(existing.jobId, req.signal), {
|
|
70
|
+
headers: SSE_HEADERS
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// No active job. Hold the stream open and wait for `createJob` to
|
|
74
|
+
// fire for this (slug, docId). When it does, push the event and
|
|
75
|
+
// hand off to the per-job stream pattern (replay + subscribe to
|
|
76
|
+
// future updates).
|
|
77
|
+
return new Response(buildDocStream(slug, docIdParam, req.signal), {
|
|
78
|
+
headers: SSE_HEADERS
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// Neither param — return idle and close. Same shape as before to
|
|
82
|
+
// keep the client's parser happy.
|
|
83
|
+
return idleResponse();
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Idle-close response. Used when the request lacks both `jobId` and
|
|
87
|
+
* `docId`, or when an explicit `jobId` references an unknown job.
|
|
88
|
+
* `retry: 30000` paces the browser's auto-reconnect.
|
|
89
|
+
*/ function idleResponse() {
|
|
90
|
+
const body = `retry: 30000\nevent: progress\ndata: ${JSON.stringify({
|
|
91
|
+
jobId: '',
|
|
92
|
+
completed: [],
|
|
93
|
+
failed: [],
|
|
94
|
+
total: 0,
|
|
95
|
+
status: 'idle'
|
|
96
|
+
})}\n\n`;
|
|
97
|
+
return new Response(body, {
|
|
98
|
+
headers: {
|
|
99
|
+
'Content-Type': 'text/event-stream',
|
|
100
|
+
'Cache-Control': 'no-cache'
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Build a stream that subscribes to an existing job and pushes its
|
|
106
|
+
* events until completion (+ a 2s grace period so the client sees the
|
|
107
|
+
* final state before the connection drops).
|
|
108
|
+
*/ function buildJobStream(jobId, reqSignal) {
|
|
109
|
+
let unsubscribe;
|
|
110
|
+
let timeout;
|
|
111
|
+
let heartbeat;
|
|
112
|
+
return new ReadableStream({
|
|
113
|
+
start (controller) {
|
|
114
|
+
const encoder = new TextEncoder();
|
|
115
|
+
controller.enqueue(encoder.encode('retry: 30000\n\n'));
|
|
116
|
+
const tryClose = ()=>{
|
|
117
|
+
try {
|
|
118
|
+
controller.close();
|
|
119
|
+
} catch {
|
|
120
|
+
// already closed
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const send = (event)=>{
|
|
124
|
+
// Only `completed` and `failed` are terminal — `idle` means
|
|
125
|
+
// "waiting for a job", `running` means "in flight". Don't close
|
|
126
|
+
// the stream on idle or running events.
|
|
127
|
+
const isTerminal = event.status === 'completed' || event.status === 'failed';
|
|
128
|
+
const eventType = isTerminal ? 'complete' : 'progress';
|
|
129
|
+
try {
|
|
130
|
+
controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${JSON.stringify(event)}\n\n`));
|
|
131
|
+
} catch {
|
|
132
|
+
// controller closed mid-write
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (isTerminal) {
|
|
136
|
+
cleanup();
|
|
137
|
+
// Brief grace so the client paints the final state before close.
|
|
138
|
+
setTimeout(tryClose, 2000);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const cleanup = ()=>{
|
|
142
|
+
if (unsubscribe) {
|
|
143
|
+
unsubscribe();
|
|
144
|
+
unsubscribe = undefined;
|
|
145
|
+
}
|
|
146
|
+
if (timeout) {
|
|
147
|
+
clearTimeout(timeout);
|
|
148
|
+
timeout = undefined;
|
|
149
|
+
}
|
|
150
|
+
if (heartbeat) {
|
|
151
|
+
clearInterval(heartbeat);
|
|
152
|
+
heartbeat = undefined;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
// Replay current state immediately.
|
|
156
|
+
const currentJob = getJob(jobId);
|
|
157
|
+
if (currentJob) {
|
|
158
|
+
send(jobToEvent(currentJob));
|
|
159
|
+
if (currentJob.status !== 'running') return;
|
|
160
|
+
}
|
|
161
|
+
unsubscribe = subscribe(jobId, send);
|
|
162
|
+
heartbeat = setInterval(()=>{
|
|
163
|
+
try {
|
|
164
|
+
controller.enqueue(encoder.encode(':hb\n\n'));
|
|
165
|
+
} catch {
|
|
166
|
+
cleanup();
|
|
167
|
+
}
|
|
168
|
+
}, HEARTBEAT_MS);
|
|
169
|
+
timeout = setTimeout(()=>{
|
|
170
|
+
cleanup();
|
|
171
|
+
tryClose();
|
|
172
|
+
}, JOB_STREAM_TTL_MS);
|
|
173
|
+
reqSignal?.addEventListener('abort', ()=>{
|
|
174
|
+
cleanup();
|
|
175
|
+
tryClose();
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
cancel () {
|
|
179
|
+
if (unsubscribe) unsubscribe();
|
|
180
|
+
if (timeout) clearTimeout(timeout);
|
|
181
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Doc-level stream. Holds open, waits for a `createJob` matching
|
|
187
|
+
* (slug, docId), then transitions into the regular per-job streaming
|
|
188
|
+
* pattern in-place. Closes after `DOC_STREAM_TTL_MS` so the connection
|
|
189
|
+
* doesn't pile up forever on idle editors.
|
|
190
|
+
*
|
|
191
|
+
* This is the architectural piece that drops on-publish bar-appearance
|
|
192
|
+
* latency from 9-11s to ~50ms. Polling can't beat this — there's no
|
|
193
|
+
* client-side timer fast enough to race the server.
|
|
194
|
+
*/ function buildDocStream(slug, documentId, reqSignal) {
|
|
195
|
+
let unsubscribeDoc;
|
|
196
|
+
let unsubscribeJob;
|
|
197
|
+
let timeout;
|
|
198
|
+
let heartbeat;
|
|
199
|
+
return new ReadableStream({
|
|
200
|
+
start (controller) {
|
|
201
|
+
const encoder = new TextEncoder();
|
|
202
|
+
controller.enqueue(encoder.encode('retry: 30000\n\n'));
|
|
203
|
+
// Send an initial idle event so the client knows the connection is
|
|
204
|
+
// healthy and waiting. Without this, browsers may consider a
|
|
205
|
+
// never-emitting stream "stalled".
|
|
206
|
+
controller.enqueue(encoder.encode(`event: progress\ndata: ${JSON.stringify({
|
|
207
|
+
jobId: '',
|
|
208
|
+
completed: [],
|
|
209
|
+
failed: [],
|
|
210
|
+
total: 0,
|
|
211
|
+
status: 'idle'
|
|
212
|
+
})}\n\n`));
|
|
213
|
+
const tryClose = ()=>{
|
|
214
|
+
try {
|
|
215
|
+
controller.close();
|
|
216
|
+
} catch {
|
|
217
|
+
// already closed
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const cleanup = ()=>{
|
|
221
|
+
if (unsubscribeDoc) {
|
|
222
|
+
unsubscribeDoc();
|
|
223
|
+
unsubscribeDoc = undefined;
|
|
224
|
+
}
|
|
225
|
+
if (unsubscribeJob) {
|
|
226
|
+
unsubscribeJob();
|
|
227
|
+
unsubscribeJob = undefined;
|
|
228
|
+
}
|
|
229
|
+
if (timeout) {
|
|
230
|
+
clearTimeout(timeout);
|
|
231
|
+
timeout = undefined;
|
|
232
|
+
}
|
|
233
|
+
if (heartbeat) {
|
|
234
|
+
clearInterval(heartbeat);
|
|
235
|
+
heartbeat = undefined;
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const send = (event)=>{
|
|
239
|
+
const eventType = event.status === 'running' ? 'progress' : 'complete';
|
|
240
|
+
try {
|
|
241
|
+
controller.enqueue(encoder.encode(`event: ${eventType}\ndata: ${JSON.stringify(event)}\n\n`));
|
|
242
|
+
} catch {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (event.status !== 'running') {
|
|
246
|
+
cleanup();
|
|
247
|
+
setTimeout(tryClose, 2000);
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
// Subscribe FIRST, then re-check for an existing job. This order
|
|
251
|
+
// closes a tiny race window: between the outer handler's
|
|
252
|
+
// `getActiveJobForDoc` (where we decided to take the doc-stream
|
|
253
|
+
// branch) and this point, an after-change hook from a concurrent
|
|
254
|
+
// request could have fired `createJob`. Subscribing first
|
|
255
|
+
// guarantees we'll receive any event that fires from now on; the
|
|
256
|
+
// re-check handles the case where the job already exists.
|
|
257
|
+
unsubscribeDoc = subscribeToDoc(slug, documentId, (initial)=>{
|
|
258
|
+
send(initial);
|
|
259
|
+
if (initial.status === 'running' && !unsubscribeJob) {
|
|
260
|
+
unsubscribeJob = subscribe(initial.jobId, send);
|
|
261
|
+
}
|
|
262
|
+
// If status was already terminal at first event (rare), `send`
|
|
263
|
+
// already triggered cleanup + close above.
|
|
264
|
+
});
|
|
265
|
+
const raceCheck = getActiveJobForDoc(slug, documentId);
|
|
266
|
+
if (raceCheck) {
|
|
267
|
+
send(jobToEvent(raceCheck));
|
|
268
|
+
if (raceCheck.status === 'running' && !unsubscribeJob) {
|
|
269
|
+
unsubscribeJob = subscribe(raceCheck.jobId, send);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
heartbeat = setInterval(()=>{
|
|
273
|
+
try {
|
|
274
|
+
controller.enqueue(encoder.encode(':hb\n\n'));
|
|
275
|
+
} catch {
|
|
276
|
+
cleanup();
|
|
277
|
+
}
|
|
278
|
+
}, HEARTBEAT_MS);
|
|
279
|
+
timeout = setTimeout(()=>{
|
|
280
|
+
cleanup();
|
|
281
|
+
tryClose();
|
|
282
|
+
}, DOC_STREAM_TTL_MS);
|
|
283
|
+
reqSignal?.addEventListener('abort', ()=>{
|
|
284
|
+
cleanup();
|
|
285
|
+
tryClose();
|
|
286
|
+
});
|
|
287
|
+
},
|
|
288
|
+
cancel () {
|
|
289
|
+
if (unsubscribeDoc) unsubscribeDoc();
|
|
290
|
+
if (unsubscribeJob) unsubscribeJob();
|
|
291
|
+
if (timeout) clearTimeout(timeout);
|
|
292
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function resolveSlugFromUrl(url) {
|
|
297
|
+
try {
|
|
298
|
+
const parsed = new URL(url);
|
|
299
|
+
const segments = parsed.pathname.split('/').filter(Boolean);
|
|
300
|
+
const apiIndex = segments.indexOf('api');
|
|
301
|
+
if (apiIndex < 0) return '';
|
|
302
|
+
// Globals pattern: /api/globals/{slug}/ai-translate/progress
|
|
303
|
+
if (segments[apiIndex + 1] === 'globals' && segments[apiIndex + 2]) {
|
|
304
|
+
return segments[apiIndex + 2];
|
|
305
|
+
}
|
|
306
|
+
// Collections pattern: /api/{slug}/ai-translate/progress
|
|
307
|
+
if (segments[apiIndex + 1]) {
|
|
308
|
+
return segments[apiIndex + 1];
|
|
309
|
+
}
|
|
310
|
+
} catch {
|
|
311
|
+
// fall through
|
|
312
|
+
}
|
|
313
|
+
return '';
|
|
314
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PayloadHandler } from 'payload';
|
|
2
|
+
export type HandlerContext = {
|
|
3
|
+
kind: 'collection';
|
|
4
|
+
slug: string;
|
|
5
|
+
} | {
|
|
6
|
+
kind: 'global';
|
|
7
|
+
slug: string;
|
|
8
|
+
};
|
|
9
|
+
type HandlerArg = HandlerContext | string | undefined;
|
|
10
|
+
export declare const getTranslateHandler: (arg?: HandlerArg) => PayloadHandler;
|
|
11
|
+
export {};
|