@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,297 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { toast, useConfig } from '@payloadcms/ui';
|
|
4
|
+
import { formatAdminURL } from 'payload/shared';
|
|
5
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
|
+
/**
|
|
7
|
+
* How long to keep the progress bar visible after the job finishes, so the
|
|
8
|
+
* user sees the bar fill to 100% instead of it vanishing on the last tick.
|
|
9
|
+
*/ const COMPLETION_HOLD_MS = 500;
|
|
10
|
+
const SHIMMER_KEYFRAMES = `
|
|
11
|
+
@keyframes ai-translate-shimmer {
|
|
12
|
+
0% { background-position: 0% 0; }
|
|
13
|
+
100% { background-position: 200% 0; }
|
|
14
|
+
}
|
|
15
|
+
@keyframes ai-translate-pulse {
|
|
16
|
+
0%, 100% { opacity: 1; }
|
|
17
|
+
50% { opacity: 0.6; }
|
|
18
|
+
}
|
|
19
|
+
@keyframes ai-translate-indeterminate {
|
|
20
|
+
0% { left: -35%; right: 100%; }
|
|
21
|
+
60% { left: 100%; right: -35%; }
|
|
22
|
+
100% { left: 100%; right: -35%; }
|
|
23
|
+
}
|
|
24
|
+
`;
|
|
25
|
+
const initialState = {
|
|
26
|
+
completed: [],
|
|
27
|
+
failed: [],
|
|
28
|
+
total: 0,
|
|
29
|
+
status: 'idle'
|
|
30
|
+
};
|
|
31
|
+
export function TranslationProgress({ jobId, collectionSlug, globalSlug, docId, onComplete }) {
|
|
32
|
+
const { config } = useConfig();
|
|
33
|
+
const [state, setState] = useState(initialState);
|
|
34
|
+
// The status we actually render. Lags behind `state.status` by
|
|
35
|
+
// COMPLETION_HOLD_MS on the running → completed/failed transition so the
|
|
36
|
+
// bar can visibly fill to 100% before being replaced by the success line.
|
|
37
|
+
const [displayStatus, setDisplayStatus] = useState('idle');
|
|
38
|
+
const onCompleteRef = useRef(onComplete);
|
|
39
|
+
onCompleteRef.current = onComplete;
|
|
40
|
+
const buildUrl = useCallback(()=>{
|
|
41
|
+
const { routes } = config;
|
|
42
|
+
const apiRoute = routes?.api ?? '/api';
|
|
43
|
+
const query = jobId ? `?jobId=${encodeURIComponent(jobId)}` : docId ? `?docId=${encodeURIComponent(String(docId))}` : null;
|
|
44
|
+
if (!query) return null;
|
|
45
|
+
const slugSegment = globalSlug ? `/globals/${globalSlug}` : collectionSlug ? `/${collectionSlug}` : null;
|
|
46
|
+
if (!slugSegment) return null;
|
|
47
|
+
const path = `${slugSegment}/ai-translate/progress${query}`;
|
|
48
|
+
return formatAdminURL({
|
|
49
|
+
apiRoute,
|
|
50
|
+
path
|
|
51
|
+
});
|
|
52
|
+
}, [
|
|
53
|
+
config,
|
|
54
|
+
collectionSlug,
|
|
55
|
+
globalSlug,
|
|
56
|
+
jobId,
|
|
57
|
+
docId
|
|
58
|
+
]);
|
|
59
|
+
useEffect(()=>{
|
|
60
|
+
if (typeof EventSource === 'undefined') return;
|
|
61
|
+
const url = buildUrl();
|
|
62
|
+
if (!url) return;
|
|
63
|
+
const es = new EventSource(url, {
|
|
64
|
+
withCredentials: true
|
|
65
|
+
});
|
|
66
|
+
function handleProgress(e) {
|
|
67
|
+
try {
|
|
68
|
+
const data = JSON.parse(e.data);
|
|
69
|
+
setState({
|
|
70
|
+
completed: data.completed,
|
|
71
|
+
failed: data.failed,
|
|
72
|
+
total: data.total,
|
|
73
|
+
status: data.status
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
// Malformed event — ignore
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function handleComplete(e) {
|
|
80
|
+
try {
|
|
81
|
+
const data = JSON.parse(e.data);
|
|
82
|
+
setState({
|
|
83
|
+
completed: data.completed,
|
|
84
|
+
failed: data.failed,
|
|
85
|
+
total: data.total,
|
|
86
|
+
status: data.status
|
|
87
|
+
});
|
|
88
|
+
if (data.failed.length === 0) {
|
|
89
|
+
toast.success(`Translated to ${data.completed.length} locale(s)`);
|
|
90
|
+
} else {
|
|
91
|
+
// Headline names the actual locales that failed — vague counts
|
|
92
|
+
// ("1/2 locales translated") force the user to look elsewhere
|
|
93
|
+
// for which one and why. Each per-locale toast carries the
|
|
94
|
+
// provider error directly. Sonner stacks them so they dismiss
|
|
95
|
+
// independently.
|
|
96
|
+
const failedCodes = data.failed.map((f)=>f.locale).join(', ');
|
|
97
|
+
if (data.completed.length > 0) {
|
|
98
|
+
toast.error(`Translated to ${data.completed.length} of ${data.total} locales — failed: ${failedCodes}`);
|
|
99
|
+
} else {
|
|
100
|
+
toast.error(`Translation failed for: ${failedCodes}`);
|
|
101
|
+
}
|
|
102
|
+
for (const f of data.failed){
|
|
103
|
+
toast.error(`Failed: ${f.locale} — ${(f.error ?? 'unknown error').slice(0, 200)}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Delay parent's onComplete by the same hold so the parent doesn't
|
|
107
|
+
// unmount this component before the bar finishes filling.
|
|
108
|
+
setTimeout(()=>{
|
|
109
|
+
onCompleteRef.current?.();
|
|
110
|
+
}, COMPLETION_HOLD_MS);
|
|
111
|
+
} catch {
|
|
112
|
+
// Malformed event — ignore
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
es.addEventListener('progress', handleProgress);
|
|
116
|
+
es.addEventListener('complete', handleComplete);
|
|
117
|
+
es.onerror = ()=>{
|
|
118
|
+
// EventSource auto-reconnects; if it fails permanently the browser closes it
|
|
119
|
+
};
|
|
120
|
+
return ()=>{
|
|
121
|
+
es.removeEventListener('progress', handleProgress);
|
|
122
|
+
es.removeEventListener('complete', handleComplete);
|
|
123
|
+
es.close();
|
|
124
|
+
};
|
|
125
|
+
}, [
|
|
126
|
+
buildUrl
|
|
127
|
+
]);
|
|
128
|
+
// Drive displayStatus from state.status. When the job finishes, hold the
|
|
129
|
+
// running view briefly so the user sees the bar fill — otherwise the bar
|
|
130
|
+
// jumps from a partial value straight into the success line.
|
|
131
|
+
useEffect(()=>{
|
|
132
|
+
if (state.status === 'idle' || state.status === 'running') {
|
|
133
|
+
setDisplayStatus(state.status);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const timer = setTimeout(()=>{
|
|
137
|
+
setDisplayStatus(state.status);
|
|
138
|
+
}, COMPLETION_HOLD_MS);
|
|
139
|
+
return ()=>clearTimeout(timer);
|
|
140
|
+
}, [
|
|
141
|
+
state.status
|
|
142
|
+
]);
|
|
143
|
+
// Nothing to render if SSR or no EventSource support
|
|
144
|
+
if (typeof EventSource === 'undefined') return null;
|
|
145
|
+
// Nothing to render in idle state
|
|
146
|
+
if (displayStatus === 'idle' || state.total === 0) return null;
|
|
147
|
+
// While we're holding on the "running" view after the job already finished,
|
|
148
|
+
// pin the bar to 100% so it visibly fills before the view swaps out.
|
|
149
|
+
const finishedButHolding = displayStatus === 'running' && state.status !== 'running';
|
|
150
|
+
const done = state.completed.length + state.failed.length;
|
|
151
|
+
const percent = finishedButHolding ? 100 : state.total > 0 ? Math.round(done / state.total * 100) : 0;
|
|
152
|
+
// Build set of all locale codes for rendering
|
|
153
|
+
const completedSet = new Set(state.completed);
|
|
154
|
+
const failedMap = new Map(state.failed.map((f)=>[
|
|
155
|
+
f.locale,
|
|
156
|
+
f.error
|
|
157
|
+
]));
|
|
158
|
+
return /*#__PURE__*/ _jsxs("div", {
|
|
159
|
+
style: {
|
|
160
|
+
padding: '12px 16px',
|
|
161
|
+
backgroundColor: 'var(--theme-elevation-50)',
|
|
162
|
+
border: '1px solid var(--theme-elevation-200)',
|
|
163
|
+
borderRadius: '4px',
|
|
164
|
+
fontSize: '13px',
|
|
165
|
+
color: 'var(--theme-text)'
|
|
166
|
+
},
|
|
167
|
+
children: [
|
|
168
|
+
displayStatus === 'running' && /*#__PURE__*/ _jsxs(_Fragment, {
|
|
169
|
+
children: [
|
|
170
|
+
/*#__PURE__*/ _jsx("style", {
|
|
171
|
+
children: SHIMMER_KEYFRAMES
|
|
172
|
+
}),
|
|
173
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
174
|
+
style: {
|
|
175
|
+
marginBottom: '8px',
|
|
176
|
+
fontWeight: 500
|
|
177
|
+
},
|
|
178
|
+
children: [
|
|
179
|
+
"Translating: ",
|
|
180
|
+
finishedButHolding ? state.total : done,
|
|
181
|
+
"/",
|
|
182
|
+
state.total,
|
|
183
|
+
" locales"
|
|
184
|
+
]
|
|
185
|
+
}),
|
|
186
|
+
/*#__PURE__*/ _jsx("div", {
|
|
187
|
+
style: {
|
|
188
|
+
position: 'relative',
|
|
189
|
+
width: '100%',
|
|
190
|
+
height: '6px',
|
|
191
|
+
backgroundColor: 'var(--theme-elevation-200)',
|
|
192
|
+
borderRadius: '3px',
|
|
193
|
+
overflow: 'hidden',
|
|
194
|
+
marginBottom: '8px'
|
|
195
|
+
},
|
|
196
|
+
children: done === 0 && !finishedButHolding ? // Indeterminate slider — uses left/right anchors so the
|
|
197
|
+
// pill is always 35% wide regardless of track width.
|
|
198
|
+
/*#__PURE__*/ _jsx("div", {
|
|
199
|
+
style: {
|
|
200
|
+
position: 'absolute',
|
|
201
|
+
top: 0,
|
|
202
|
+
bottom: 0,
|
|
203
|
+
borderRadius: '3px',
|
|
204
|
+
backgroundColor: 'var(--theme-success-500)',
|
|
205
|
+
backgroundImage: 'linear-gradient(90deg, color-mix(in srgb, var(--theme-success-500) 70%, white) 0%, var(--theme-success-500) 50%, color-mix(in srgb, var(--theme-success-500) 70%, white) 100%)',
|
|
206
|
+
animation: 'ai-translate-indeterminate 1.6s ease-in-out infinite'
|
|
207
|
+
}
|
|
208
|
+
}) : // Percent-based fill — shimmer animation runs while still
|
|
209
|
+
// in flight, freezes when the bar is held at 100%.
|
|
210
|
+
/*#__PURE__*/ _jsx("div", {
|
|
211
|
+
style: {
|
|
212
|
+
width: `${percent}%`,
|
|
213
|
+
height: '100%',
|
|
214
|
+
borderRadius: '3px',
|
|
215
|
+
transition: 'width 0.3s ease',
|
|
216
|
+
backgroundColor: 'var(--theme-success-500)',
|
|
217
|
+
backgroundImage: finishedButHolding ? 'none' : 'linear-gradient(90deg, var(--theme-success-500) 0%, color-mix(in srgb, var(--theme-success-500) 70%, white) 50%, var(--theme-success-500) 100%)',
|
|
218
|
+
backgroundSize: '200% 100%',
|
|
219
|
+
animation: finishedButHolding ? 'none' : 'ai-translate-shimmer 1.4s linear infinite, ai-translate-pulse 2.2s ease-in-out infinite'
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
}),
|
|
223
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
224
|
+
style: {
|
|
225
|
+
display: 'flex',
|
|
226
|
+
flexWrap: 'wrap',
|
|
227
|
+
gap: '6px'
|
|
228
|
+
},
|
|
229
|
+
children: [
|
|
230
|
+
state.completed.map((locale)=>/*#__PURE__*/ _jsxs("span", {
|
|
231
|
+
style: {
|
|
232
|
+
color: 'var(--theme-success-500)'
|
|
233
|
+
},
|
|
234
|
+
children: [
|
|
235
|
+
"✓ ",
|
|
236
|
+
locale
|
|
237
|
+
]
|
|
238
|
+
}, locale)),
|
|
239
|
+
state.failed.map((f)=>/*#__PURE__*/ _jsxs("span", {
|
|
240
|
+
style: {
|
|
241
|
+
color: 'var(--theme-error-500)'
|
|
242
|
+
},
|
|
243
|
+
children: [
|
|
244
|
+
"✗ ",
|
|
245
|
+
f.locale
|
|
246
|
+
]
|
|
247
|
+
}, f.locale))
|
|
248
|
+
]
|
|
249
|
+
})
|
|
250
|
+
]
|
|
251
|
+
}),
|
|
252
|
+
displayStatus === 'completed' && /*#__PURE__*/ _jsxs("div", {
|
|
253
|
+
style: {
|
|
254
|
+
color: 'var(--theme-success-500)',
|
|
255
|
+
fontWeight: 500
|
|
256
|
+
},
|
|
257
|
+
children: [
|
|
258
|
+
"✓ Translated to ",
|
|
259
|
+
state.completed.length,
|
|
260
|
+
" locale",
|
|
261
|
+
state.completed.length !== 1 ? 's' : ''
|
|
262
|
+
]
|
|
263
|
+
}),
|
|
264
|
+
displayStatus === 'failed' && /*#__PURE__*/ _jsxs("div", {
|
|
265
|
+
children: [
|
|
266
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
267
|
+
style: {
|
|
268
|
+
color: 'var(--theme-warning-500)',
|
|
269
|
+
fontWeight: 500,
|
|
270
|
+
marginBottom: '6px'
|
|
271
|
+
},
|
|
272
|
+
children: [
|
|
273
|
+
"⚠ ",
|
|
274
|
+
state.completed.length,
|
|
275
|
+
"/",
|
|
276
|
+
state.total,
|
|
277
|
+
" locales translated"
|
|
278
|
+
]
|
|
279
|
+
}),
|
|
280
|
+
state.failed.map((f)=>/*#__PURE__*/ _jsxs("div", {
|
|
281
|
+
style: {
|
|
282
|
+
color: 'var(--theme-error-500)',
|
|
283
|
+
fontSize: '12px',
|
|
284
|
+
marginTop: '2px'
|
|
285
|
+
},
|
|
286
|
+
children: [
|
|
287
|
+
"✗ ",
|
|
288
|
+
f.locale,
|
|
289
|
+
": ",
|
|
290
|
+
f.error
|
|
291
|
+
]
|
|
292
|
+
}, f.locale))
|
|
293
|
+
]
|
|
294
|
+
})
|
|
295
|
+
]
|
|
296
|
+
});
|
|
297
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Collapsible "Translation" sidebar group. Renders ONLY the two
|
|
4
|
+
* operator-facing entry points:
|
|
5
|
+
*
|
|
6
|
+
* 1. "Translation Hub" — custom view at `/admin/translation`
|
|
7
|
+
* 2. "Bulk Translate Runs" — custom view at `/admin/translation-runs`
|
|
8
|
+
*
|
|
9
|
+
* Every other translation-related surface (AI Translate Jobs, AI
|
|
10
|
+
* Translate Meta, Bulk Translate Batches, Bulk Translate Units,
|
|
11
|
+
* Translation Daily Spend, Translation Rate Limits, Translation
|
|
12
|
+
* Usage, OpenRouter Settings, Translation Settings) is DELIBERATELY
|
|
13
|
+
* omitted from the sidebar:
|
|
14
|
+
*
|
|
15
|
+
* - Usage / alerts / in-flight jobs surface inside the Hub's
|
|
16
|
+
* Overview + Audit tabs in a polished form.
|
|
17
|
+
* - OpenRouter + Translation Settings live inside the Hub's
|
|
18
|
+
* Configuration tab.
|
|
19
|
+
* - Bulk batches + units are drilled into from the Runs hub's
|
|
20
|
+
* inline drill-down — opening them as raw collection lists
|
|
21
|
+
* would be a debugging escape hatch, not a daily workflow.
|
|
22
|
+
* - Daily spend + rate-limit counters are internal counter tables;
|
|
23
|
+
* operators never edit them.
|
|
24
|
+
*
|
|
25
|
+
* The collections + globals stay registered with
|
|
26
|
+
* `admin.group: 'Translation'` so they remain reachable via direct
|
|
27
|
+
* URL (`/admin/collections/<slug>`) for debugging — they just don't
|
|
28
|
+
* clutter the sidebar. Payload's auto-generated `Translation` group
|
|
29
|
+
* is hidden by the consumer's `custom.scss` (the
|
|
30
|
+
* `.nav-group.Translation { display: none }` rule is differentiated
|
|
31
|
+
* from our custom group via the `.translation-nav-group-host`
|
|
32
|
+
* wrapper).
|
|
33
|
+
*
|
|
34
|
+
* Wires via `admin.components.beforeNavLinks` /
|
|
35
|
+
* `afterNavLinks` in the consumer's payload.config.ts.
|
|
36
|
+
*
|
|
37
|
+
* Next.js basePath handling: `<Link>` auto-prepends the configured
|
|
38
|
+
* basePath (e.g. `/blog`). Hrefs MUST be the logical paths
|
|
39
|
+
* (`/admin/translation`) — including the basePath manually would
|
|
40
|
+
* produce `/blog/blog/admin/translation` in production. Next 14+'s
|
|
41
|
+
* `usePathname()` returns the pathname EXCLUDING basePath, so the
|
|
42
|
+
* active-state comparison matches the logical href directly.
|
|
43
|
+
*/
|
|
44
|
+
export declare const TranslationNavGroup: React.FC;
|
|
45
|
+
export default TranslationNavGroup;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { NavGroup, useConfig } from '@payloadcms/ui';
|
|
4
|
+
import Link from 'next/link';
|
|
5
|
+
import { usePathname } from 'next/navigation';
|
|
6
|
+
import { useMemo } from 'react';
|
|
7
|
+
/**
|
|
8
|
+
* Collapsible "Translation" sidebar group. Renders ONLY the two
|
|
9
|
+
* operator-facing entry points:
|
|
10
|
+
*
|
|
11
|
+
* 1. "Translation Hub" — custom view at `/admin/translation`
|
|
12
|
+
* 2. "Bulk Translate Runs" — custom view at `/admin/translation-runs`
|
|
13
|
+
*
|
|
14
|
+
* Every other translation-related surface (AI Translate Jobs, AI
|
|
15
|
+
* Translate Meta, Bulk Translate Batches, Bulk Translate Units,
|
|
16
|
+
* Translation Daily Spend, Translation Rate Limits, Translation
|
|
17
|
+
* Usage, OpenRouter Settings, Translation Settings) is DELIBERATELY
|
|
18
|
+
* omitted from the sidebar:
|
|
19
|
+
*
|
|
20
|
+
* - Usage / alerts / in-flight jobs surface inside the Hub's
|
|
21
|
+
* Overview + Audit tabs in a polished form.
|
|
22
|
+
* - OpenRouter + Translation Settings live inside the Hub's
|
|
23
|
+
* Configuration tab.
|
|
24
|
+
* - Bulk batches + units are drilled into from the Runs hub's
|
|
25
|
+
* inline drill-down — opening them as raw collection lists
|
|
26
|
+
* would be a debugging escape hatch, not a daily workflow.
|
|
27
|
+
* - Daily spend + rate-limit counters are internal counter tables;
|
|
28
|
+
* operators never edit them.
|
|
29
|
+
*
|
|
30
|
+
* The collections + globals stay registered with
|
|
31
|
+
* `admin.group: 'Translation'` so they remain reachable via direct
|
|
32
|
+
* URL (`/admin/collections/<slug>`) for debugging — they just don't
|
|
33
|
+
* clutter the sidebar. Payload's auto-generated `Translation` group
|
|
34
|
+
* is hidden by the consumer's `custom.scss` (the
|
|
35
|
+
* `.nav-group.Translation { display: none }` rule is differentiated
|
|
36
|
+
* from our custom group via the `.translation-nav-group-host`
|
|
37
|
+
* wrapper).
|
|
38
|
+
*
|
|
39
|
+
* Wires via `admin.components.beforeNavLinks` /
|
|
40
|
+
* `afterNavLinks` in the consumer's payload.config.ts.
|
|
41
|
+
*
|
|
42
|
+
* Next.js basePath handling: `<Link>` auto-prepends the configured
|
|
43
|
+
* basePath (e.g. `/blog`). Hrefs MUST be the logical paths
|
|
44
|
+
* (`/admin/translation`) — including the basePath manually would
|
|
45
|
+
* produce `/blog/blog/admin/translation` in production. Next 14+'s
|
|
46
|
+
* `usePathname()` returns the pathname EXCLUDING basePath, so the
|
|
47
|
+
* active-state comparison matches the logical href directly.
|
|
48
|
+
*/ export const TranslationNavGroup = ()=>{
|
|
49
|
+
const pathname = usePathname();
|
|
50
|
+
const { config } = useConfig();
|
|
51
|
+
// basePath stripping for the active-state check ONLY. We do NOT
|
|
52
|
+
// prepend basePath to the href because `<Link>` does that itself.
|
|
53
|
+
// Older Next versions occasionally include basePath in
|
|
54
|
+
// `usePathname()`; normalize defensively by stripping the runtime
|
|
55
|
+
// basePath from the pathname before comparing.
|
|
56
|
+
const runtimeBasePath = useMemo(()=>{
|
|
57
|
+
if (typeof window === 'undefined') {
|
|
58
|
+
return config?.routes?.admin?.replace(/\/admin$/, '') ?? '';
|
|
59
|
+
}
|
|
60
|
+
const idx = window.location.pathname.indexOf('/admin');
|
|
61
|
+
return idx > 0 ? window.location.pathname.slice(0, idx) : '';
|
|
62
|
+
}, [
|
|
63
|
+
config?.routes?.admin
|
|
64
|
+
]);
|
|
65
|
+
const cleanPath = runtimeBasePath && pathname?.startsWith(runtimeBasePath) ? pathname.slice(runtimeBasePath.length) : pathname ?? '';
|
|
66
|
+
const hubHref = '/admin/translation';
|
|
67
|
+
const runsHref = '/admin/translation-runs';
|
|
68
|
+
const hubActive = cleanPath === hubHref;
|
|
69
|
+
const runsActive = cleanPath.startsWith(runsHref);
|
|
70
|
+
// The wrapper class `translation-nav-group-host` exists so the
|
|
71
|
+
// consumer's custom.scss can differentiate THIS NavGroup from
|
|
72
|
+
// Payload's auto-generated Translation group (both share className
|
|
73
|
+
// `nav-group Translation`). The recommended consumer CSS pattern:
|
|
74
|
+
//
|
|
75
|
+
// .nav-group.Translation { display: none !important; }
|
|
76
|
+
// .translation-nav-group-host .nav-group.Translation {
|
|
77
|
+
// display: revert !important;
|
|
78
|
+
// }
|
|
79
|
+
return /*#__PURE__*/ _jsx("div", {
|
|
80
|
+
className: "translation-nav-group-host",
|
|
81
|
+
children: /*#__PURE__*/ _jsxs(NavGroup, {
|
|
82
|
+
label: "Translation",
|
|
83
|
+
children: [
|
|
84
|
+
/*#__PURE__*/ _jsx(Link, {
|
|
85
|
+
className: `nav__link${hubActive ? ' active' : ''}`,
|
|
86
|
+
href: hubHref,
|
|
87
|
+
children: /*#__PURE__*/ _jsx("span", {
|
|
88
|
+
className: "nav__link-label",
|
|
89
|
+
children: "Translation Hub"
|
|
90
|
+
})
|
|
91
|
+
}),
|
|
92
|
+
/*#__PURE__*/ _jsx(Link, {
|
|
93
|
+
className: `nav__link${runsActive ? ' active' : ''}`,
|
|
94
|
+
href: runsHref,
|
|
95
|
+
children: /*#__PURE__*/ _jsx("span", {
|
|
96
|
+
className: "nav__link-label",
|
|
97
|
+
children: "Bulk Translate Runs"
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
})
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
export default TranslationNavGroup;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ConcurrencyLimits, RetryConfig } from './types.js';
|
|
2
|
+
export declare const DEFAULT_CONCURRENCY: ConcurrencyLimits;
|
|
3
|
+
export declare const DEFAULT_RETRY: RetryConfig;
|
|
4
|
+
export declare const DEFAULT_TARGET_POLICY: "mirror";
|
|
5
|
+
export declare const DEFAULT_COALESCING_WINDOW_MS = 12000;
|
|
6
|
+
export declare const REENTRY_FLAG: "aiTranslateInternal";
|
|
7
|
+
export declare const SKIP_FLAG: "skipAutoTranslate";
|
|
8
|
+
export declare const DEFAULT_USAGE_COLLECTION_SLUG = "translation-usage";
|
|
9
|
+
export declare const USAGE_TRACKING_CONTEXT_FLAG: "aiTranslateUsageInternal";
|
|
10
|
+
export declare const DEFAULT_ALERTS_COLLECTION_SLUG = "translation-alerts";
|
|
11
|
+
export declare const ALERTS_CONTEXT_FLAG: "aiTranslateAlertsInternal";
|
package/dist/defaults.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const DEFAULT_CONCURRENCY = {
|
|
2
|
+
perDocument: 3,
|
|
3
|
+
perProvider: 5
|
|
4
|
+
};
|
|
5
|
+
export const DEFAULT_RETRY = {
|
|
6
|
+
attempts: 2,
|
|
7
|
+
backoffMs: 1000
|
|
8
|
+
};
|
|
9
|
+
export const DEFAULT_TARGET_POLICY = 'mirror';
|
|
10
|
+
export const DEFAULT_COALESCING_WINDOW_MS = 12_000;
|
|
11
|
+
export const REENTRY_FLAG = 'aiTranslateInternal';
|
|
12
|
+
export const SKIP_FLAG = 'skipAutoTranslate';
|
|
13
|
+
export const DEFAULT_USAGE_COLLECTION_SLUG = 'translation-usage';
|
|
14
|
+
export const USAGE_TRACKING_CONTEXT_FLAG = 'aiTranslateUsageInternal';
|
|
15
|
+
export const DEFAULT_ALERTS_COLLECTION_SLUG = 'translation-alerts';
|
|
16
|
+
export const ALERTS_CONTEXT_FLAG = 'aiTranslateAlertsInternal';
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { PayloadHandler } from 'payload';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the JSON-safe subset of the plugin's config that the admin
|
|
4
|
+
* client needs at render time (Translate drawer locale picker,
|
|
5
|
+
* per-field button visibility, the per-collection field-exclusion
|
|
6
|
+
* widget on the translation-settings global, etc.).
|
|
7
|
+
*
|
|
8
|
+
* Payload's admin client strips function references from `config.custom`
|
|
9
|
+
* during the client-side serialization pass, so `useConfig()` cannot
|
|
10
|
+
* read `config.custom.aiTranslate.targetLocales` directly. This
|
|
11
|
+
* endpoint exposes the safe subset over the API instead.
|
|
12
|
+
*
|
|
13
|
+
* Path: `GET /api/ai-translate/client-config`
|
|
14
|
+
*
|
|
15
|
+
* Auth: any authenticated user (the response carries no secrets — just
|
|
16
|
+
* locale codes already visible in Payload's localization config, plus
|
|
17
|
+
* field paths derivable from the public admin schema).
|
|
18
|
+
*
|
|
19
|
+
* Response shape:
|
|
20
|
+
* {
|
|
21
|
+
* sourceLocale: string,
|
|
22
|
+
* targetLocales: string[],
|
|
23
|
+
* perFieldButton: boolean,
|
|
24
|
+
* translatableFieldsBySlug: Record<string, string[]>,
|
|
25
|
+
* bulkExcludedCollections: string[],
|
|
26
|
+
* globalKillSwitches: { autoEnabled: boolean, manualEnabled: boolean, bulkEnabled: boolean },
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* `globalKillSwitches` (v1.2.8) lets the admin client hide the
|
|
30
|
+
* Translate dialog / per-field button / BulkTranslate trigger when the
|
|
31
|
+
* matching plugin-wide kill switch is off. The server gates remain the
|
|
32
|
+
* source of truth (Translate endpoint 403s, Enqueue endpoint 403s) —
|
|
33
|
+
* the client uses these flags only for UI affordance.
|
|
34
|
+
*
|
|
35
|
+
* `translatableFieldsBySlug` maps each tracked collection / global
|
|
36
|
+
* slug to the list of translatable field paths the resolver would
|
|
37
|
+
* produce for that surface. Used by the `ExcludedFieldsField` admin
|
|
38
|
+
* component on the `translation-settings` global to render a checkbox
|
|
39
|
+
* list whose options change as the editor picks a `slug`. Computed
|
|
40
|
+
* per-request (~tens of microseconds for typical configs) so a
|
|
41
|
+
* dev-time config edit + restart immediately surfaces new fields
|
|
42
|
+
* without a separate cache-bust.
|
|
43
|
+
*/
|
|
44
|
+
export declare const getClientConfigHandler: () => PayloadHandler;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { describeAuthRejection } from '../lib/auth-diagnostics.js';
|
|
2
|
+
import { getEffectiveExcludePatternsForSurface, getGlobalKillSwitches } from '../lib/effective-locales.js';
|
|
3
|
+
import { isExcluded } from '../lib/exclude-fields.js';
|
|
4
|
+
import { resolveTranslatableFields } from '../lib/field-resolver.js';
|
|
5
|
+
/**
|
|
6
|
+
* Returns the JSON-safe subset of the plugin's config that the admin
|
|
7
|
+
* client needs at render time (Translate drawer locale picker,
|
|
8
|
+
* per-field button visibility, the per-collection field-exclusion
|
|
9
|
+
* widget on the translation-settings global, etc.).
|
|
10
|
+
*
|
|
11
|
+
* Payload's admin client strips function references from `config.custom`
|
|
12
|
+
* during the client-side serialization pass, so `useConfig()` cannot
|
|
13
|
+
* read `config.custom.aiTranslate.targetLocales` directly. This
|
|
14
|
+
* endpoint exposes the safe subset over the API instead.
|
|
15
|
+
*
|
|
16
|
+
* Path: `GET /api/ai-translate/client-config`
|
|
17
|
+
*
|
|
18
|
+
* Auth: any authenticated user (the response carries no secrets — just
|
|
19
|
+
* locale codes already visible in Payload's localization config, plus
|
|
20
|
+
* field paths derivable from the public admin schema).
|
|
21
|
+
*
|
|
22
|
+
* Response shape:
|
|
23
|
+
* {
|
|
24
|
+
* sourceLocale: string,
|
|
25
|
+
* targetLocales: string[],
|
|
26
|
+
* perFieldButton: boolean,
|
|
27
|
+
* translatableFieldsBySlug: Record<string, string[]>,
|
|
28
|
+
* bulkExcludedCollections: string[],
|
|
29
|
+
* globalKillSwitches: { autoEnabled: boolean, manualEnabled: boolean, bulkEnabled: boolean },
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* `globalKillSwitches` (v1.2.8) lets the admin client hide the
|
|
33
|
+
* Translate dialog / per-field button / BulkTranslate trigger when the
|
|
34
|
+
* matching plugin-wide kill switch is off. The server gates remain the
|
|
35
|
+
* source of truth (Translate endpoint 403s, Enqueue endpoint 403s) —
|
|
36
|
+
* the client uses these flags only for UI affordance.
|
|
37
|
+
*
|
|
38
|
+
* `translatableFieldsBySlug` maps each tracked collection / global
|
|
39
|
+
* slug to the list of translatable field paths the resolver would
|
|
40
|
+
* produce for that surface. Used by the `ExcludedFieldsField` admin
|
|
41
|
+
* component on the `translation-settings` global to render a checkbox
|
|
42
|
+
* list whose options change as the editor picks a `slug`. Computed
|
|
43
|
+
* per-request (~tens of microseconds for typical configs) so a
|
|
44
|
+
* dev-time config edit + restart immediately surfaces new fields
|
|
45
|
+
* without a separate cache-bust.
|
|
46
|
+
*/ export const getClientConfigHandler = ()=>async (req)=>{
|
|
47
|
+
if (!req.user) {
|
|
48
|
+
return Response.json({
|
|
49
|
+
error: 'Unauthorized',
|
|
50
|
+
diagnostic: describeAuthRejection(req)
|
|
51
|
+
}, {
|
|
52
|
+
status: 401
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const config = req.payload.config?.custom?.aiTranslate;
|
|
56
|
+
if (!config) {
|
|
57
|
+
return Response.json({
|
|
58
|
+
error: 'ai-translate plugin not configured'
|
|
59
|
+
}, {
|
|
60
|
+
status: 500
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
// Build the slug → translatable-paths map. We walk only surfaces
|
|
64
|
+
// the plugin is configured to track — there's no point listing
|
|
65
|
+
// paths for collections the plugin can't translate, and exposing
|
|
66
|
+
// them would invite editor confusion in the admin UI.
|
|
67
|
+
//
|
|
68
|
+
// Use surface-aware exclude patterns so the widget mirrors what
|
|
69
|
+
// the translate pipeline actually sends per surface. When a row
|
|
70
|
+
// toggles `translateSlug: true`, `slug` appears in that surface's
|
|
71
|
+
// path list (and gets a checkbox in the ExcludedFieldsField); for
|
|
72
|
+
// every other surface the slug stays hidden because the static
|
|
73
|
+
// `'slug'` exclusion still applies.
|
|
74
|
+
const trackedCollections = new Set(config.collections ?? []);
|
|
75
|
+
const trackedGlobals = new Set(config.globals ?? []);
|
|
76
|
+
const translatableFieldsBySlug = {};
|
|
77
|
+
for (const collection of req.payload.config.collections ?? []){
|
|
78
|
+
if (!trackedCollections.has(collection.slug)) continue;
|
|
79
|
+
const patterns = await getEffectiveExcludePatternsForSurface(req.payload, config, collection.slug);
|
|
80
|
+
translatableFieldsBySlug[collection.slug] = pathsFor(collection, patterns);
|
|
81
|
+
}
|
|
82
|
+
for (const global of req.payload.config.globals ?? []){
|
|
83
|
+
if (!global.slug || !trackedGlobals.has(global.slug)) continue;
|
|
84
|
+
const patterns = await getEffectiveExcludePatternsForSurface(req.payload, config, global.slug);
|
|
85
|
+
translatableFieldsBySlug[global.slug] = pathsFor(global, patterns);
|
|
86
|
+
}
|
|
87
|
+
// NEW-9 (v1.2.6): surface the bulk-translate exclusion list so the
|
|
88
|
+
// Advanced tab can annotate "translatable everywhere but excluded
|
|
89
|
+
// from bulk runs" (typically `users`). Without this the editor
|
|
90
|
+
// sees `users` listed under "Translatable fields per surface" but
|
|
91
|
+
// can't find it in the Bulk Translate breakdown, which looks like
|
|
92
|
+
// a bug.
|
|
93
|
+
const bulkExcludedCollections = Array.isArray(config.bulk?.excludeCollections) ? [
|
|
94
|
+
...config.bulk?.excludeCollections ?? []
|
|
95
|
+
] : [];
|
|
96
|
+
// v1.2.8: surface the three plugin-wide kill switches so the admin
|
|
97
|
+
// client can hide the Translate dialog / per-field button / Bulk
|
|
98
|
+
// trigger when their matching flag is off. Server-side gates remain
|
|
99
|
+
// the source of truth — these flags only affect UI affordance.
|
|
100
|
+
const globalKillSwitches = await getGlobalKillSwitches(req.payload, config);
|
|
101
|
+
return Response.json({
|
|
102
|
+
sourceLocale: config.sourceLocale,
|
|
103
|
+
targetLocales: [
|
|
104
|
+
...config.targetLocales
|
|
105
|
+
],
|
|
106
|
+
perFieldButton: !!config.perFieldButton,
|
|
107
|
+
translatableFieldsBySlug,
|
|
108
|
+
bulkExcludedCollections,
|
|
109
|
+
globalKillSwitches
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
function pathsFor(surface, excludePatterns) {
|
|
113
|
+
// The plugin injects `_aiTranslateAutoLocales` (select),
|
|
114
|
+
// `_aiTranslateOptOut` (checkbox), and `_aiTranslate` (ui) onto
|
|
115
|
+
// every tracked surface. None are translatable —
|
|
116
|
+
// `resolveTranslatableFields` already filters by type, so they're
|
|
117
|
+
// naturally absent. We still defensively exclude anything starting
|
|
118
|
+
// with an underscore prefix to keep the admin UI clean if someone
|
|
119
|
+
// hand-marks one of them `localized: true`.
|
|
120
|
+
//
|
|
121
|
+
// Dedupe: a `blocks` field with N block types recurses into each
|
|
122
|
+
// block's fields under the same parent path, so shared field names
|
|
123
|
+
// (e.g. every block has its own `blockName`) emit `layout.blockName`
|
|
124
|
+
// N times. The translate pipeline doesn't care (extractor processes
|
|
125
|
+
// each block instance separately), but the admin UI uses paths as
|
|
126
|
+
// React keys — duplicates cause "two children with the same key"
|
|
127
|
+
// warnings and dropped checkboxes. Set dedupes deterministically.
|
|
128
|
+
//
|
|
129
|
+
// Static excludePatterns: paths that the translate pipeline never
|
|
130
|
+
// sends (URLs via the `**.url` default, plus consumer-configured
|
|
131
|
+
// `excludeFields`) are removed here too. Mirrors what actually
|
|
132
|
+
// happens at translation time so the widget doesn't tease the
|
|
133
|
+
// editor with paths they can't influence.
|
|
134
|
+
const fields = surface.fields ?? [];
|
|
135
|
+
const seen = new Set();
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const f of resolveTranslatableFields(fields)){
|
|
138
|
+
if (f.path.split('.').some((seg)=>seg.startsWith('_'))) continue;
|
|
139
|
+
if (isExcluded(f.path, excludePatterns)) continue;
|
|
140
|
+
if (seen.has(f.path)) continue;
|
|
141
|
+
seen.add(f.path);
|
|
142
|
+
out.push(f.path);
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|