@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
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
import { createAlertsCollection } from './alerts-collection.js';
|
|
2
|
+
import { translateDocument, translateGlobal } from './api.js';
|
|
3
|
+
// Bulk-translate engine (v1.2.0+). All gated on `options.bulk?.enabled`.
|
|
4
|
+
import { createBulkTranslateBatchesCollection, DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG } from './bulk-translate-batches-collection.js';
|
|
5
|
+
import { createBulkTranslateUnitsCollection, DEFAULT_BULK_TRANSLATE_UNITS_COLLECTION_SLUG } from './bulk-translate-units-collection.js';
|
|
6
|
+
import { DEFAULT_ALERTS_COLLECTION_SLUG, DEFAULT_COALESCING_WINDOW_MS, DEFAULT_CONCURRENCY, DEFAULT_USAGE_COLLECTION_SLUG } from './defaults.js';
|
|
7
|
+
import { getClientConfigHandler } from './endpoints/client-config.js';
|
|
8
|
+
import { getEstimateHandler } from './endpoints/estimate.js';
|
|
9
|
+
import { getProgressHandler } from './endpoints/progress.js';
|
|
10
|
+
import { getTranslateHandler } from './endpoints/translate.js';
|
|
11
|
+
import { getBulkTranslateActiveHandler } from './endpoints/translation-hub/active.js';
|
|
12
|
+
import { getBulkTranslateCancelHandler } from './endpoints/translation-hub/cancel.js';
|
|
13
|
+
import { getBulkTranslateEnqueueHandler } from './endpoints/translation-hub/enqueue.js';
|
|
14
|
+
import { getBulkTranslateFailuresHandler } from './endpoints/translation-hub/failures.js';
|
|
15
|
+
import { getBulkTranslateForceResetHandler } from './endpoints/translation-hub/force-reset.js';
|
|
16
|
+
import { getBulkTranslateListHandler } from './endpoints/translation-hub/list.js';
|
|
17
|
+
import { getBulkTranslatePreflightHandler } from './endpoints/translation-hub/preflight.js';
|
|
18
|
+
import { getBulkTranslateRetryFailedHandler } from './endpoints/translation-hub/retry-failed.js';
|
|
19
|
+
import { getBulkTranslateRevertHandler } from './endpoints/translation-hub/revert.js';
|
|
20
|
+
import { getBulkTranslateStatusHandler } from './endpoints/translation-hub/status.js';
|
|
21
|
+
import { getTranslationHubUsageSummaryHandler } from './endpoints/translation-hub/usage-summary.js';
|
|
22
|
+
import { createTranslateAfterChangeHook } from './hooks/after-change.js';
|
|
23
|
+
import { createTranslateGlobalAfterChangeHook } from './hooks/after-change-global.js';
|
|
24
|
+
import { createAiTranslateAfterDeleteHook } from './hooks/after-delete.js';
|
|
25
|
+
import { createJobsCollection, DEFAULT_JOBS_COLLECTION_SLUG } from './jobs-collection.js';
|
|
26
|
+
import { ensureBulkTranslateSchema } from './lib/bulk-translate-migrations.js';
|
|
27
|
+
import { createCoalescingQueue } from './lib/coalescing-queue.js';
|
|
28
|
+
import { setAlertsContext } from './lib/events.js';
|
|
29
|
+
import { getEffectiveExcludePatterns, isExcluded } from './lib/exclude-fields.js';
|
|
30
|
+
import { setPersistenceContext } from './lib/progress-store.js';
|
|
31
|
+
import { createRateLimiter } from './lib/rate-limiter.js';
|
|
32
|
+
import { createManualEditMetaCollection, DEFAULT_MANUAL_EDIT_COLLECTION_SLUG } from './manual-edit-collection.js';
|
|
33
|
+
import { createSettingsGlobal } from './settings-global.js';
|
|
34
|
+
import { BULK_TRANSLATE_COORDINATOR_SLUG, BULK_TRANSLATE_DOC_TASK_SLUG, buildBulkTranslateCoordinator } from './tasks/bulk-translate-coordinator.js';
|
|
35
|
+
import { buildBulkTranslateDocTask } from './tasks/bulk-translate-doc-task.js';
|
|
36
|
+
import { BULK_TRANSLATE_JANITOR_SLUG, buildBulkTranslateJanitor, runJanitorSweep } from './tasks/bulk-translate-janitor.js';
|
|
37
|
+
import { buildTranslateDocumentTask, buildTranslateGlobalTask, TRANSLATE_DOCUMENT_TASK_SLUG, TRANSLATE_GLOBAL_TASK_SLUG } from './tasks/translate-job-task.js';
|
|
38
|
+
import { createTranslationDailySpendCollection } from './translation-daily-spend-collection.js';
|
|
39
|
+
import { createTranslationRateLimitsCollection } from './translation-rate-limits-collection.js';
|
|
40
|
+
import { createUsageCollection } from './usage-collection.js';
|
|
41
|
+
export function aiTranslatePlugin(options) {
|
|
42
|
+
return (incomingConfig)=>{
|
|
43
|
+
const config = {
|
|
44
|
+
...incomingConfig
|
|
45
|
+
};
|
|
46
|
+
config.custom = {
|
|
47
|
+
...config.custom ?? {},
|
|
48
|
+
aiTranslate: options
|
|
49
|
+
};
|
|
50
|
+
// Expose a serializable subset of plugin config to the admin client
|
|
51
|
+
// bundle. The full `options` object contains provider function
|
|
52
|
+
// references (`translate`, `estimate`) that Payload's admin builder
|
|
53
|
+
// strips during client serialization — the client-side modal can
|
|
54
|
+
// only read JSON-safe fields. Writing this subset to a stable path
|
|
55
|
+
// (`config.custom.aiTranslateClient`) lets the Translate drawer
|
|
56
|
+
// restrict its locale picker to `targetLocales` without an extra
|
|
57
|
+
// round-trip.
|
|
58
|
+
config.custom.aiTranslateClient = {
|
|
59
|
+
sourceLocale: options.sourceLocale,
|
|
60
|
+
targetLocales: [
|
|
61
|
+
...options.targetLocales
|
|
62
|
+
],
|
|
63
|
+
perFieldButton: !!options.perFieldButton
|
|
64
|
+
};
|
|
65
|
+
if (options.enabled === false) {
|
|
66
|
+
return config;
|
|
67
|
+
}
|
|
68
|
+
// Validate required config
|
|
69
|
+
if (!options.sourceLocale) {
|
|
70
|
+
throw new Error('[ai-translate] sourceLocale is required');
|
|
71
|
+
}
|
|
72
|
+
if (!options.targetLocales?.length) {
|
|
73
|
+
throw new Error('[ai-translate] targetLocales must contain at least one locale');
|
|
74
|
+
}
|
|
75
|
+
if (!options.provider) {
|
|
76
|
+
throw new Error('[ai-translate] provider is required');
|
|
77
|
+
}
|
|
78
|
+
if (!options.costLimits) {
|
|
79
|
+
throw new Error('[ai-translate] costLimits is required');
|
|
80
|
+
}
|
|
81
|
+
if (!options.costLimits.perCallCharLimit) {
|
|
82
|
+
throw new Error('[ai-translate] costLimits.perCallCharLimit is required');
|
|
83
|
+
}
|
|
84
|
+
if (!options.costLimits.perDocCharCeiling) {
|
|
85
|
+
throw new Error('[ai-translate] costLimits.perDocCharCeiling is required');
|
|
86
|
+
}
|
|
87
|
+
if (!options.costLimits.bulkConfirmUsdThreshold) {
|
|
88
|
+
throw new Error('[ai-translate] costLimits.bulkConfirmUsdThreshold is required');
|
|
89
|
+
}
|
|
90
|
+
// Validate locale overlap
|
|
91
|
+
if (options.targetLocales.includes(options.sourceLocale)) {
|
|
92
|
+
console.warn(`[ai-translate] targetLocales includes sourceLocale "${options.sourceLocale}". It will be skipped during translation.`);
|
|
93
|
+
}
|
|
94
|
+
// Create rate limiter for provider calls
|
|
95
|
+
const rpm = options.concurrency?.perProvider ?? DEFAULT_CONCURRENCY.perProvider;
|
|
96
|
+
options._rateLimiter = createRateLimiter(rpm);
|
|
97
|
+
// Top-level client-config endpoint. Payload's admin bundle strips
|
|
98
|
+
// function refs from `config.custom`, so the Translate drawer can't
|
|
99
|
+
// read `targetLocales` directly via `useConfig()`. This endpoint
|
|
100
|
+
// exposes the JSON-safe subset (`sourceLocale`, `targetLocales`,
|
|
101
|
+
// `perFieldButton`) over `/api/ai-translate/client-config`, which
|
|
102
|
+
// the drawer fetches on open. Without it, the drawer falls back to
|
|
103
|
+
// listing every non-source locale Payload has registered — fine,
|
|
104
|
+
// just shows more choices than the plugin can translate to.
|
|
105
|
+
config.endpoints = [
|
|
106
|
+
...Array.isArray(config.endpoints) ? config.endpoints : [],
|
|
107
|
+
{
|
|
108
|
+
handler: getClientConfigHandler(),
|
|
109
|
+
method: 'get',
|
|
110
|
+
path: '/ai-translate/client-config'
|
|
111
|
+
}
|
|
112
|
+
];
|
|
113
|
+
// Auto-register the usage-tracking collection. Mirrors auditLogPlugin's
|
|
114
|
+
// collection-registration call site. No-op when feature is disabled or
|
|
115
|
+
// not configured, preserving today's behavior for projects that opt out.
|
|
116
|
+
if (options.usageTracking?.enabled) {
|
|
117
|
+
const usageSlug = options.usageTracking.collectionSlug ?? DEFAULT_USAGE_COLLECTION_SLUG;
|
|
118
|
+
const usageCollection = createUsageCollection(options.usageTracking.access?.read, usageSlug);
|
|
119
|
+
config.collections = [
|
|
120
|
+
...config.collections ?? [],
|
|
121
|
+
usageCollection
|
|
122
|
+
];
|
|
123
|
+
}
|
|
124
|
+
// Auto-register the alerts collection. Gated on usageTracking — if a
|
|
125
|
+
// consumer hasn't opted into usage tracking they likely don't want
|
|
126
|
+
// alerts persistence either (both feed the Hub). The collection is
|
|
127
|
+
// additive (creates a new table), so opt-in is the safe default.
|
|
128
|
+
// Alert persistence is best-effort in `emitAlert`; the consumer's
|
|
129
|
+
// `onAlert` callback still fires regardless for back-compat.
|
|
130
|
+
if (options.usageTracking?.enabled) {
|
|
131
|
+
const alertsSlug = options.alertsCollectionSlug ?? DEFAULT_ALERTS_COLLECTION_SLUG;
|
|
132
|
+
const alertsCollection = createAlertsCollection(options.usageTracking.access?.read, alertsSlug);
|
|
133
|
+
config.collections = [
|
|
134
|
+
...config.collections ?? [],
|
|
135
|
+
alertsCollection
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
// Auto-register the manual-edit-meta sidecar collection. Holds per-
|
|
139
|
+
// (collection, doc, locale, field) hashes of the last value the
|
|
140
|
+
// plugin wrote, so the writer can detect manual edits and skip
|
|
141
|
+
// overwriting them. See `preserveManualEdits` in types.ts.
|
|
142
|
+
if (options.preserveManualEdits) {
|
|
143
|
+
const metaSlug = options.manualEditCollectionSlug ?? DEFAULT_MANUAL_EDIT_COLLECTION_SLUG;
|
|
144
|
+
const metaCollection = createManualEditMetaCollection(undefined, metaSlug);
|
|
145
|
+
config.collections = [
|
|
146
|
+
...config.collections ?? [],
|
|
147
|
+
metaCollection
|
|
148
|
+
];
|
|
149
|
+
}
|
|
150
|
+
// Auto-register the jobs sidecar collection. Mirrors in-memory
|
|
151
|
+
// job state so a server restart doesn't lose admin visibility
|
|
152
|
+
// into in-flight translations. See `persistJobs` in types.ts.
|
|
153
|
+
if (options.persistJobs) {
|
|
154
|
+
const jobsSlug = options.jobsCollectionSlug ?? DEFAULT_JOBS_COLLECTION_SLUG;
|
|
155
|
+
const jobsCollection = createJobsCollection(undefined, jobsSlug);
|
|
156
|
+
config.collections = [
|
|
157
|
+
...config.collections ?? [],
|
|
158
|
+
jobsCollection
|
|
159
|
+
];
|
|
160
|
+
// Register the two Payload tasks the coalescing queue enqueues
|
|
161
|
+
// into. Without these, the queue worker's `payload.jobs.queue`
|
|
162
|
+
// call below would fail with "unknown task". Additive — never
|
|
163
|
+
// overwrites consumer-defined tasks.
|
|
164
|
+
config.jobs = config.jobs ?? {
|
|
165
|
+
tasks: []
|
|
166
|
+
};
|
|
167
|
+
const existingTasks = Array.isArray(config.jobs.tasks) ? config.jobs.tasks : [];
|
|
168
|
+
const hasDocTask = existingTasks.some((t)=>t.slug === TRANSLATE_DOCUMENT_TASK_SLUG);
|
|
169
|
+
const hasGlobalTask = existingTasks.some((t)=>t.slug === TRANSLATE_GLOBAL_TASK_SLUG);
|
|
170
|
+
config.jobs.tasks = [
|
|
171
|
+
...existingTasks,
|
|
172
|
+
...hasDocTask ? [] : [
|
|
173
|
+
buildTranslateDocumentTask(options.collections ?? [])
|
|
174
|
+
],
|
|
175
|
+
...hasGlobalTask ? [] : [
|
|
176
|
+
buildTranslateGlobalTask(options.globals ?? [])
|
|
177
|
+
]
|
|
178
|
+
];
|
|
179
|
+
}
|
|
180
|
+
// ---------------------------------------------------------------
|
|
181
|
+
// Bulk-translate engine (v1.2.0+). Auto-registers when the
|
|
182
|
+
// `options.bulk` block is present. Pass `enabled: false`
|
|
183
|
+
// explicitly to opt out — the real cost guards are
|
|
184
|
+
// `dailyUsdCap` + `requireTotp` + admin-role auth, not this
|
|
185
|
+
// flag. (Prior versions defaulted off, requiring `enabled:
|
|
186
|
+
// true` opt-in; the inversion landed in v1.2.3.)
|
|
187
|
+
// ---------------------------------------------------------------
|
|
188
|
+
if (options.bulk && options.bulk.enabled !== false) {
|
|
189
|
+
const bulkConfig = options.bulk;
|
|
190
|
+
const allowedCollectionsForBulk = (options.collections ?? []).filter((slug)=>!(bulkConfig.excludeCollections ?? []).includes(slug));
|
|
191
|
+
const allowedGlobalsForBulk = options.globals ?? [];
|
|
192
|
+
// (1) Collections — batches, units, daily-spend, rate-limits.
|
|
193
|
+
const bulkCollections = [
|
|
194
|
+
createBulkTranslateBatchesCollection(),
|
|
195
|
+
createBulkTranslateUnitsCollection(),
|
|
196
|
+
createTranslationDailySpendCollection(),
|
|
197
|
+
createTranslationRateLimitsCollection()
|
|
198
|
+
];
|
|
199
|
+
config.collections = [
|
|
200
|
+
...config.collections ?? [],
|
|
201
|
+
...bulkCollections
|
|
202
|
+
];
|
|
203
|
+
// (2) Tasks — coordinator, worker, janitor.
|
|
204
|
+
config.jobs = config.jobs ?? {
|
|
205
|
+
tasks: []
|
|
206
|
+
};
|
|
207
|
+
const tasks = Array.isArray(config.jobs.tasks) ? config.jobs.tasks : [];
|
|
208
|
+
const hasCoordinator = tasks.some((t)=>t.slug === BULK_TRANSLATE_COORDINATOR_SLUG);
|
|
209
|
+
const hasBulkWorker = tasks.some((t)=>t.slug === BULK_TRANSLATE_DOC_TASK_SLUG);
|
|
210
|
+
const hasJanitor = tasks.some((t)=>t.slug === BULK_TRANSLATE_JANITOR_SLUG);
|
|
211
|
+
config.jobs.tasks = [
|
|
212
|
+
...tasks,
|
|
213
|
+
...hasCoordinator ? [] : [
|
|
214
|
+
buildBulkTranslateCoordinator()
|
|
215
|
+
],
|
|
216
|
+
...hasBulkWorker ? [] : [
|
|
217
|
+
buildBulkTranslateDocTask({
|
|
218
|
+
allowedCollections: allowedCollectionsForBulk,
|
|
219
|
+
allowedGlobals: allowedGlobalsForBulk,
|
|
220
|
+
callbacks: {
|
|
221
|
+
onBatchComplete: bulkConfig.onBatchComplete,
|
|
222
|
+
onBatchFailed: bulkConfig.onBatchFailed
|
|
223
|
+
}
|
|
224
|
+
})
|
|
225
|
+
],
|
|
226
|
+
...hasJanitor ? [] : [
|
|
227
|
+
buildBulkTranslateJanitor({
|
|
228
|
+
callbacks: {
|
|
229
|
+
onBatchComplete: bulkConfig.onBatchComplete,
|
|
230
|
+
onBatchFailed: bulkConfig.onBatchFailed
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
]
|
|
234
|
+
];
|
|
235
|
+
// (3) Endpoints — POST/GET handlers under /api/translation-hub.
|
|
236
|
+
// Handlers resolve admin roles, providers, locales, etc. at
|
|
237
|
+
// request time from `req.payload.config.custom.aiTranslate`
|
|
238
|
+
// (set at line 50). Plugin config flows through that channel.
|
|
239
|
+
// Factory options here are only slug overrides.
|
|
240
|
+
const bulkEndpointBase = '/translation-hub/bulk-translate';
|
|
241
|
+
const retryFailedHandler = getBulkTranslateRetryFailedHandler();
|
|
242
|
+
config.endpoints = [
|
|
243
|
+
...Array.isArray(config.endpoints) ? config.endpoints : [],
|
|
244
|
+
{
|
|
245
|
+
handler: getBulkTranslateEnqueueHandler(),
|
|
246
|
+
method: 'post',
|
|
247
|
+
path: bulkEndpointBase
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
handler: getBulkTranslateListHandler(),
|
|
251
|
+
method: 'get',
|
|
252
|
+
path: bulkEndpointBase
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
handler: getBulkTranslateActiveHandler(),
|
|
256
|
+
method: 'get',
|
|
257
|
+
path: `${bulkEndpointBase}/active`
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
handler: getBulkTranslatePreflightHandler(),
|
|
261
|
+
method: 'get',
|
|
262
|
+
path: `${bulkEndpointBase}/preflight`
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
handler: getBulkTranslateStatusHandler(),
|
|
266
|
+
method: 'get',
|
|
267
|
+
path: `${bulkEndpointBase}/:id/status`
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
handler: getBulkTranslateFailuresHandler(),
|
|
271
|
+
method: 'get',
|
|
272
|
+
path: `${bulkEndpointBase}/:id/failures`
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
handler: retryFailedHandler,
|
|
276
|
+
method: 'post',
|
|
277
|
+
path: `${bulkEndpointBase}/:id/retry-failed`
|
|
278
|
+
},
|
|
279
|
+
// UI hits `/retry` (shorter); register the same handler under
|
|
280
|
+
// both paths so consumers that call either keep working.
|
|
281
|
+
{
|
|
282
|
+
handler: retryFailedHandler,
|
|
283
|
+
method: 'post',
|
|
284
|
+
path: `${bulkEndpointBase}/:id/retry`
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
handler: getBulkTranslateCancelHandler(),
|
|
288
|
+
method: 'post',
|
|
289
|
+
path: `${bulkEndpointBase}/:id/cancel`
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
handler: getBulkTranslateRevertHandler(),
|
|
293
|
+
method: 'post',
|
|
294
|
+
path: `${bulkEndpointBase}/:id/revert`
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
handler: getBulkTranslateForceResetHandler(),
|
|
298
|
+
method: 'post',
|
|
299
|
+
path: `${bulkEndpointBase}/:id/force-reset`
|
|
300
|
+
},
|
|
301
|
+
// Hub-level (not bulk-translate-specific) — server-side
|
|
302
|
+
// aggregation for Overview + Audit KPIs. NEW-15 (v1.2.6):
|
|
303
|
+
// replaces the prior `/translation-usage?limit=1000` pattern
|
|
304
|
+
// that silently truncated past 1000 rows.
|
|
305
|
+
{
|
|
306
|
+
handler: getTranslationHubUsageSummaryHandler(),
|
|
307
|
+
method: 'get',
|
|
308
|
+
path: '/translation-hub/usage-summary'
|
|
309
|
+
}
|
|
310
|
+
];
|
|
311
|
+
// (4) Schema migration: idempotent CREATE UNIQUE INDEX IF NOT
|
|
312
|
+
// EXISTS for the partial unique index that backs F-DA-TOCTOU.
|
|
313
|
+
// Wraps the consumer's existing onInit so we don't clobber it.
|
|
314
|
+
const consumerOnInit = config.onInit;
|
|
315
|
+
config.onInit = async (payload)=>{
|
|
316
|
+
if (consumerOnInit) {
|
|
317
|
+
await consumerOnInit(payload);
|
|
318
|
+
}
|
|
319
|
+
try {
|
|
320
|
+
await ensureBulkTranslateSchema(payload);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
payload.logger?.warn?.(`[ai-translate] bulk-translate schema bootstrap failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
323
|
+
}
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
// Track which collections / globals are configured for translation
|
|
327
|
+
const trackedCollections = options.collections ?? [];
|
|
328
|
+
const trackedGlobals = options.globals ?? [];
|
|
329
|
+
// Auto-register the translation-settings global. The global carries
|
|
330
|
+
// three sets of fields: an `activeProvider` selector (only meaningful
|
|
331
|
+
// when `providers` map has multiple entries), an `enabledTargetLocales`
|
|
332
|
+
// toggle (always meaningful as long as `targetLocales` are configured),
|
|
333
|
+
// and a `perCollection` array letting admins override the site-wide
|
|
334
|
+
// settings per surface.
|
|
335
|
+
const providerKeys = Object.keys(options.providers ?? {});
|
|
336
|
+
const settingsEnabled = options.targetLocales.length > 0 && options.settings?.enabled !== false;
|
|
337
|
+
if (settingsEnabled) {
|
|
338
|
+
const settingsGlobal = createSettingsGlobal(providerKeys, options.targetLocales, options.settings, [
|
|
339
|
+
...trackedCollections,
|
|
340
|
+
...trackedGlobals
|
|
341
|
+
]);
|
|
342
|
+
config.globals = [
|
|
343
|
+
...config.globals ?? [],
|
|
344
|
+
settingsGlobal
|
|
345
|
+
];
|
|
346
|
+
}
|
|
347
|
+
// Pre-resolve the admin-role gate so both injection sites share the
|
|
348
|
+
// same check function. Defaults to ['admin'] for back-compat. Pass
|
|
349
|
+
// `adminRoles: ['super-admin']` etc. to support different role naming.
|
|
350
|
+
const adminRoles = Array.isArray(options.adminRoles) ? options.adminRoles : [
|
|
351
|
+
'admin'
|
|
352
|
+
];
|
|
353
|
+
// Loose `args: any` because Payload's `FieldAccess` typing has
|
|
354
|
+
// `req.user: UntypedUser | null` (no roles property until consumers
|
|
355
|
+
// generate types). At runtime the user shape always has `roles`
|
|
356
|
+
// when the consumer uses Payload's auth — we read defensively.
|
|
357
|
+
const isAdminFromReq = (args)=>{
|
|
358
|
+
if (adminRoles.length === 0) return true; // explicit "everyone" opt-in
|
|
359
|
+
const roles = args?.req?.user?.roles;
|
|
360
|
+
if (!Array.isArray(roles)) return false;
|
|
361
|
+
return roles.some((r)=>typeof r === 'string' && adminRoles.includes(r));
|
|
362
|
+
};
|
|
363
|
+
// Automation: create coalescing queue if async mode. The queue is shared
|
|
364
|
+
// by both collection and global hooks — they dispatch to the right
|
|
365
|
+
// translate function via `entry.kind`.
|
|
366
|
+
let queue = null;
|
|
367
|
+
if (options.automation && options.automation.mode !== 'inline') {
|
|
368
|
+
const windowMs = options.automation.coalescingWindowMs ?? DEFAULT_COALESCING_WINDOW_MS;
|
|
369
|
+
queue = createCoalescingQueue(windowMs, async (entry)=>{
|
|
370
|
+
// When `persistJobs: true`, hand the work off to Payload's
|
|
371
|
+
// native jobs queue. The translation closure (`req`, doc id,
|
|
372
|
+
// `previousDoc`, target locales) is serialized into the
|
|
373
|
+
// job's input — Payload's cron worker re-runs the task if
|
|
374
|
+
// the process restarts before it completes. Without this
|
|
375
|
+
// flag we fall back to the original in-memory fire-and-
|
|
376
|
+
// forget path, which is faster but loses pending work on
|
|
377
|
+
// restart.
|
|
378
|
+
if (options.persistJobs) {
|
|
379
|
+
try {
|
|
380
|
+
if (entry.kind === 'global') {
|
|
381
|
+
await entry.payload.jobs.queue({
|
|
382
|
+
task: TRANSLATE_GLOBAL_TASK_SLUG,
|
|
383
|
+
input: {
|
|
384
|
+
global: entry.collection,
|
|
385
|
+
jobId: entry.jobId,
|
|
386
|
+
targetLocales: entry.targetLocales ?? null,
|
|
387
|
+
previousDoc: entry.previousDoc ?? null
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
} else {
|
|
391
|
+
await entry.payload.jobs.queue({
|
|
392
|
+
task: TRANSLATE_DOCUMENT_TASK_SLUG,
|
|
393
|
+
input: {
|
|
394
|
+
collection: entry.collection,
|
|
395
|
+
documentId: String(entry.id),
|
|
396
|
+
jobId: entry.jobId,
|
|
397
|
+
targetLocales: entry.targetLocales ?? null,
|
|
398
|
+
previousDoc: entry.previousDoc ?? null,
|
|
399
|
+
draft: true
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
// Best-effort immediate run so the user-visible latency
|
|
404
|
+
// matches the previous in-memory behavior. If `runByID`
|
|
405
|
+
// isn't available on this Payload version (older
|
|
406
|
+
// versions only exposed `run`), the cron will still pick
|
|
407
|
+
// it up.
|
|
408
|
+
void entry.payload.jobs.run?.({
|
|
409
|
+
queue: 'default',
|
|
410
|
+
limit: 1
|
|
411
|
+
}).catch(()=>{});
|
|
412
|
+
return;
|
|
413
|
+
} catch (err) {
|
|
414
|
+
entry.payload.logger?.error?.(`[ai-translate] Failed to enqueue persisted job for ${entry.collection}/${entry.kind === 'global' ? '<global>' : String(entry.id)}: ${err instanceof Error ? err.message : 'Unknown error'}. Falling back to in-process translation.`);
|
|
415
|
+
// Fall through to the original in-process path so a
|
|
416
|
+
// jobs-queue outage doesn't drop the work entirely.
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (entry.kind === 'global') {
|
|
420
|
+
await translateGlobal(entry.payload, {
|
|
421
|
+
global: entry.collection,
|
|
422
|
+
previousDoc: entry.previousDoc,
|
|
423
|
+
jobId: entry.jobId,
|
|
424
|
+
req: entry.req,
|
|
425
|
+
targetLocales: entry.targetLocales
|
|
426
|
+
});
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
await translateDocument(entry.payload, {
|
|
430
|
+
collection: entry.collection,
|
|
431
|
+
id: entry.id,
|
|
432
|
+
previousDoc: entry.previousDoc,
|
|
433
|
+
// Reuse the job seeded by the after-change hook so the polling UI
|
|
434
|
+
// doesn't see two jobs for the same doc (one phantom, one real).
|
|
435
|
+
jobId: entry.jobId,
|
|
436
|
+
req: entry.req,
|
|
437
|
+
targetLocales: entry.targetLocales
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
// Inject endpoints, hooks, and UI field on configured collections
|
|
442
|
+
// IMPORTANT: mutate and return the ORIGINAL collection object.
|
|
443
|
+
// Payload captures object references during buildConfig — shallow copies
|
|
444
|
+
// via spread create new references whose hooks Payload never sees.
|
|
445
|
+
config.collections = (config.collections ?? []).map((collection)=>{
|
|
446
|
+
if (!trackedCollections.includes(collection.slug)) {
|
|
447
|
+
return collection;
|
|
448
|
+
}
|
|
449
|
+
// Use the original — do NOT spread into a copy
|
|
450
|
+
const matchedCollection = collection;
|
|
451
|
+
// --- Endpoints ---
|
|
452
|
+
const existingEndpoints = matchedCollection.endpoints ?? [];
|
|
453
|
+
matchedCollection.endpoints = [
|
|
454
|
+
...Array.isArray(existingEndpoints) ? existingEndpoints : [],
|
|
455
|
+
{
|
|
456
|
+
handler: getTranslateHandler({
|
|
457
|
+
kind: 'collection',
|
|
458
|
+
slug: collection.slug
|
|
459
|
+
}),
|
|
460
|
+
method: 'post',
|
|
461
|
+
path: '/ai-translate'
|
|
462
|
+
},
|
|
463
|
+
{
|
|
464
|
+
handler: getEstimateHandler({
|
|
465
|
+
kind: 'collection',
|
|
466
|
+
slug: collection.slug
|
|
467
|
+
}),
|
|
468
|
+
method: 'post',
|
|
469
|
+
path: '/ai-translate/estimate'
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
handler: getProgressHandler(collection.slug),
|
|
473
|
+
method: 'get',
|
|
474
|
+
path: '/ai-translate/progress'
|
|
475
|
+
}
|
|
476
|
+
];
|
|
477
|
+
// --- Per-doc auto-translate widgets ---
|
|
478
|
+
//
|
|
479
|
+
// Two sibling fields control per-doc auto-translate behavior:
|
|
480
|
+
//
|
|
481
|
+
// 1. `_aiTranslateAutoLocales` (select hasMany) — NARROW which
|
|
482
|
+
// locales fan out on this doc. Empty / unset = inherit
|
|
483
|
+
// collection-level decision (no narrowing). Non-empty =
|
|
484
|
+
// restrict to these locales.
|
|
485
|
+
//
|
|
486
|
+
// 2. `_aiTranslateOptOut` (checkbox) — SKIP auto-translate
|
|
487
|
+
// entirely for this doc. Unchecked = participate normally;
|
|
488
|
+
// checked = bail before any locale work.
|
|
489
|
+
//
|
|
490
|
+
// Why two fields instead of overloading the array's empty state:
|
|
491
|
+
// Payload's hasMany select can't distinguish "admin cleared
|
|
492
|
+
// every checkbox" from "admin never touched the widget" — both
|
|
493
|
+
// serialize to 0 rows. Pre-fix versions treated empty as
|
|
494
|
+
// opt-out, which made auto-translate dead-by-default for every
|
|
495
|
+
// new doc (the inherit state was unreachable). Splitting the
|
|
496
|
+
// intents into distinct fields fixes that: empty array ALWAYS
|
|
497
|
+
// means inherit; opt-out is a separate, explicit boolean.
|
|
498
|
+
//
|
|
499
|
+
// `localized: false` on both: a per-locale value would let one
|
|
500
|
+
// locale clobber another. `access.update` is admin-only because
|
|
501
|
+
// they gate billable LLM spend; editors see the fields but they
|
|
502
|
+
// render read-only.
|
|
503
|
+
matchedCollection.fields = [
|
|
504
|
+
...matchedCollection.fields,
|
|
505
|
+
{
|
|
506
|
+
name: '_aiTranslateOptOut',
|
|
507
|
+
label: 'Skip auto-translate',
|
|
508
|
+
type: 'checkbox',
|
|
509
|
+
defaultValue: false,
|
|
510
|
+
required: false,
|
|
511
|
+
localized: false,
|
|
512
|
+
access: {
|
|
513
|
+
update: isAdminFromReq
|
|
514
|
+
},
|
|
515
|
+
admin: {
|
|
516
|
+
position: 'sidebar',
|
|
517
|
+
description: 'Admin only. Check to skip auto-translate entirely for this document on publish. Manual Translate from the dialog still works. Leave unchecked to participate in normal auto-translate.'
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: '_aiTranslateAutoLocales',
|
|
522
|
+
label: 'Auto-translate locales',
|
|
523
|
+
type: 'select',
|
|
524
|
+
hasMany: true,
|
|
525
|
+
required: false,
|
|
526
|
+
localized: false,
|
|
527
|
+
options: options.targetLocales.map((code)=>({
|
|
528
|
+
label: code,
|
|
529
|
+
value: code
|
|
530
|
+
})),
|
|
531
|
+
// Reject locale codes not in the plugin's configured
|
|
532
|
+
// `targetLocales`. The select's option list constrains the
|
|
533
|
+
// admin form, but the REST/GraphQL API path accepts arbitrary
|
|
534
|
+
// values — without this guard, an API write with
|
|
535
|
+
// `_aiTranslateAutoLocales: ['xx', 'yy']` would persist and
|
|
536
|
+
// silently narrow the intersection to empty downstream.
|
|
537
|
+
validate: (value)=>{
|
|
538
|
+
if (value === null || value === undefined) return true;
|
|
539
|
+
if (!Array.isArray(value)) {
|
|
540
|
+
return 'Must be an array of locale codes.';
|
|
541
|
+
}
|
|
542
|
+
const universe = new Set(options.targetLocales);
|
|
543
|
+
const bad = value.filter((v)=>typeof v !== 'string' || !universe.has(v));
|
|
544
|
+
if (bad.length > 0) {
|
|
545
|
+
return `Unknown locale(s): ${bad.join(', ')}. Must be one of: ${options.targetLocales.join(', ')}.`;
|
|
546
|
+
}
|
|
547
|
+
return true;
|
|
548
|
+
},
|
|
549
|
+
access: {
|
|
550
|
+
update: isAdminFromReq
|
|
551
|
+
},
|
|
552
|
+
admin: {
|
|
553
|
+
position: 'sidebar',
|
|
554
|
+
description: 'Admin only. Narrow which target locales auto-translate on publish for this document. Leave empty to inherit the collection default (translate to every enabled locale). Pick specific locales to translate to ONLY those. To skip auto-translate entirely for this doc, use the "Skip auto-translate" checkbox above — clearing this list does NOT opt out.'
|
|
555
|
+
}
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
name: '_aiTranslate',
|
|
559
|
+
type: 'ui',
|
|
560
|
+
admin: {
|
|
561
|
+
position: 'sidebar',
|
|
562
|
+
condition: (data)=>Boolean(data?.id),
|
|
563
|
+
components: {
|
|
564
|
+
Field: '@purposeinplay/payload-ai-translate/client#TranslateButton'
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
];
|
|
569
|
+
// --- Per-field translate button (opt-in) ---
|
|
570
|
+
if (options.perFieldButton) {
|
|
571
|
+
matchedCollection.fields = injectFieldButtons(matchedCollection.fields, getEffectiveExcludePatterns(options));
|
|
572
|
+
}
|
|
573
|
+
// --- Automation hooks (only if automation config present) ---
|
|
574
|
+
if (options.automation) {
|
|
575
|
+
const automation = options.automation;
|
|
576
|
+
// Drafts detection: warn if on-change trigger + drafts enabled
|
|
577
|
+
const hasDrafts = !!(collection.versions && typeof collection.versions === 'object' && collection.versions.drafts);
|
|
578
|
+
if (hasDrafts && automation.trigger !== 'on-publish') {
|
|
579
|
+
console.warn(`[ai-translate] Collection "${collection.slug}" has drafts enabled but automation trigger is "${automation.trigger ?? 'on-change'}". ` + `This will translate on every autosave. Consider using trigger: 'on-publish'.`);
|
|
580
|
+
}
|
|
581
|
+
const hook = createTranslateAfterChangeHook({
|
|
582
|
+
collectionSlug: collection.slug,
|
|
583
|
+
pluginOptions: options,
|
|
584
|
+
queue,
|
|
585
|
+
hasDrafts
|
|
586
|
+
});
|
|
587
|
+
// Mutate the ORIGINAL collection's hooks array in-place.
|
|
588
|
+
// Payload captures hook references during buildConfig before plugins
|
|
589
|
+
// return. Replacing the hooks object via spread creates a new reference
|
|
590
|
+
// that Payload never sees. We must push onto the original array.
|
|
591
|
+
if (!collection.hooks) {
|
|
592
|
+
collection.hooks = {};
|
|
593
|
+
}
|
|
594
|
+
if (!collection.hooks.afterChange) {
|
|
595
|
+
collection.hooks.afterChange = [];
|
|
596
|
+
}
|
|
597
|
+
collection.hooks.afterChange.unshift(hook);
|
|
598
|
+
}
|
|
599
|
+
// BUG-20 + BUG-26 — after-delete cleanup: cancel in-memory jobs +
|
|
600
|
+
// purge `ai_translate_jobs` and `ai_translate_meta` rows for the
|
|
601
|
+
// deleted doc. Registered unconditionally (no `automation` gate)
|
|
602
|
+
// because the storage tables can hold rows from earlier translate
|
|
603
|
+
// calls regardless of whether automation is on now.
|
|
604
|
+
const deleteHook = createAiTranslateAfterDeleteHook(collection.slug, options);
|
|
605
|
+
if (!collection.hooks.afterDelete) {
|
|
606
|
+
collection.hooks.afterDelete = [];
|
|
607
|
+
}
|
|
608
|
+
collection.hooks.afterDelete.unshift(deleteHook);
|
|
609
|
+
return matchedCollection;
|
|
610
|
+
});
|
|
611
|
+
// Globals: register endpoints + UI + after-change hook on configured globals.
|
|
612
|
+
// Mirrors the collection branch so manual translate, per-field buttons,
|
|
613
|
+
// estimate/progress endpoints, and auto-translate all work for globals too.
|
|
614
|
+
if (trackedGlobals.length > 0 && config.globals) {
|
|
615
|
+
config.globals = config.globals.map((global)=>{
|
|
616
|
+
if (!global.slug || !trackedGlobals.includes(global.slug)) {
|
|
617
|
+
return global;
|
|
618
|
+
}
|
|
619
|
+
const matchedGlobal = global;
|
|
620
|
+
// --- Endpoints ---
|
|
621
|
+
const existingEndpoints = matchedGlobal.endpoints ?? [];
|
|
622
|
+
matchedGlobal.endpoints = [
|
|
623
|
+
...Array.isArray(existingEndpoints) ? existingEndpoints : [],
|
|
624
|
+
{
|
|
625
|
+
handler: getTranslateHandler({
|
|
626
|
+
kind: 'global',
|
|
627
|
+
slug: global.slug
|
|
628
|
+
}),
|
|
629
|
+
method: 'post',
|
|
630
|
+
path: '/ai-translate'
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
handler: getEstimateHandler({
|
|
634
|
+
kind: 'global',
|
|
635
|
+
slug: global.slug
|
|
636
|
+
}),
|
|
637
|
+
method: 'post',
|
|
638
|
+
path: '/ai-translate/estimate'
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
handler: getProgressHandler(global.slug),
|
|
642
|
+
method: 'get',
|
|
643
|
+
path: '/ai-translate/progress'
|
|
644
|
+
}
|
|
645
|
+
];
|
|
646
|
+
// --- Per-doc auto-translate widgets ---
|
|
647
|
+
// See collection branch above for the full rationale on why
|
|
648
|
+
// these are two separate fields instead of a single 3-state array.
|
|
649
|
+
//
|
|
650
|
+
// The descriptions say "on save" rather than "on publish":
|
|
651
|
+
// globals in Payload don't have a publish gate (no draft
|
|
652
|
+
// state by default), so every save fires after-change. Editors
|
|
653
|
+
// should know that auto-translate flows differently here.
|
|
654
|
+
//
|
|
655
|
+
// Globals don't have a numeric `id`, but they always exist once
|
|
656
|
+
// saved. The sidebar Translate button renders unconditionally —
|
|
657
|
+
// the client component reads the global slug from
|
|
658
|
+
// useDocumentInfo to build URLs.
|
|
659
|
+
matchedGlobal.fields = [
|
|
660
|
+
...matchedGlobal.fields,
|
|
661
|
+
{
|
|
662
|
+
name: '_aiTranslateOptOut',
|
|
663
|
+
label: 'Skip auto-translate',
|
|
664
|
+
type: 'checkbox',
|
|
665
|
+
defaultValue: false,
|
|
666
|
+
required: false,
|
|
667
|
+
localized: false,
|
|
668
|
+
access: {
|
|
669
|
+
update: isAdminFromReq
|
|
670
|
+
},
|
|
671
|
+
admin: {
|
|
672
|
+
position: 'sidebar',
|
|
673
|
+
description: 'Admin only. Check to skip auto-translate entirely for this global on save. Manual Translate from the dialog still works. Leave unchecked to participate in normal auto-translate.'
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: '_aiTranslateAutoLocales',
|
|
678
|
+
label: 'Auto-translate locales',
|
|
679
|
+
type: 'select',
|
|
680
|
+
hasMany: true,
|
|
681
|
+
required: false,
|
|
682
|
+
localized: false,
|
|
683
|
+
options: options.targetLocales.map((code)=>({
|
|
684
|
+
label: code,
|
|
685
|
+
value: code
|
|
686
|
+
})),
|
|
687
|
+
validate: (value)=>{
|
|
688
|
+
if (value === null || value === undefined) return true;
|
|
689
|
+
if (!Array.isArray(value)) {
|
|
690
|
+
return 'Must be an array of locale codes.';
|
|
691
|
+
}
|
|
692
|
+
const universe = new Set(options.targetLocales);
|
|
693
|
+
const bad = value.filter((v)=>typeof v !== 'string' || !universe.has(v));
|
|
694
|
+
if (bad.length > 0) {
|
|
695
|
+
return `Unknown locale(s): ${bad.join(', ')}. Must be one of: ${options.targetLocales.join(', ')}.`;
|
|
696
|
+
}
|
|
697
|
+
return true;
|
|
698
|
+
},
|
|
699
|
+
access: {
|
|
700
|
+
update: isAdminFromReq
|
|
701
|
+
},
|
|
702
|
+
admin: {
|
|
703
|
+
position: 'sidebar',
|
|
704
|
+
description: 'Admin only. Narrow which target locales auto-translate on save for this global. Leave empty to inherit defaults (translate to every enabled locale). Pick specific locales to translate to ONLY those. To skip auto-translate entirely, use the "Skip auto-translate" checkbox above — clearing this list does NOT opt out.'
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
name: '_aiTranslate',
|
|
709
|
+
type: 'ui',
|
|
710
|
+
admin: {
|
|
711
|
+
position: 'sidebar',
|
|
712
|
+
components: {
|
|
713
|
+
Field: '@purposeinplay/payload-ai-translate/client#TranslateButton'
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
];
|
|
718
|
+
// --- Per-field translate button (opt-in) ---
|
|
719
|
+
if (options.perFieldButton) {
|
|
720
|
+
matchedGlobal.fields = injectFieldButtons(matchedGlobal.fields, getEffectiveExcludePatterns(options));
|
|
721
|
+
}
|
|
722
|
+
// --- Auto-translate hook ---
|
|
723
|
+
if (options.automation) {
|
|
724
|
+
// Reuse the same coalescing queue collections use, so a burst
|
|
725
|
+
// of saves on a global (e.g. autosave debouncer firing rapidly)
|
|
726
|
+
// collapses into a single translation pass.
|
|
727
|
+
const hook = createTranslateGlobalAfterChangeHook(global.slug, options, queue);
|
|
728
|
+
if (!global.hooks) {
|
|
729
|
+
global.hooks = {};
|
|
730
|
+
}
|
|
731
|
+
if (!global.hooks.afterChange) {
|
|
732
|
+
global.hooks.afterChange = [];
|
|
733
|
+
}
|
|
734
|
+
global.hooks.afterChange.unshift(hook);
|
|
735
|
+
}
|
|
736
|
+
return matchedGlobal;
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
// Wire the progress-store's persistence target. Done in `onInit`
|
|
740
|
+
// because we need a live `payload` instance, which only exists
|
|
741
|
+
// post-config-build. The plugin sequence here only mutates config —
|
|
742
|
+
// the actual mirror writes happen later when `createJob`/
|
|
743
|
+
// `updateJob`/`cleanup` fire during translation.
|
|
744
|
+
if (options.persistJobs) {
|
|
745
|
+
const existingOnInit = config.onInit;
|
|
746
|
+
const jobsSlug = options.jobsCollectionSlug ?? DEFAULT_JOBS_COLLECTION_SLUG;
|
|
747
|
+
config.onInit = async (payload)=>{
|
|
748
|
+
if (existingOnInit) {
|
|
749
|
+
await existingOnInit(payload);
|
|
750
|
+
}
|
|
751
|
+
setPersistenceContext({
|
|
752
|
+
payload: payload,
|
|
753
|
+
collectionSlug: jobsSlug
|
|
754
|
+
});
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
// Wire the alerts persistence target. Gated on usageTracking — same
|
|
758
|
+
// gate that auto-registers the alerts collection. The module-scoped
|
|
759
|
+
// ref is read by `emitAlert` in lib/events.ts on every alert emission.
|
|
760
|
+
if (options.usageTracking?.enabled) {
|
|
761
|
+
const existingOnInit = config.onInit;
|
|
762
|
+
const alertsSlug = options.alertsCollectionSlug ?? DEFAULT_ALERTS_COLLECTION_SLUG;
|
|
763
|
+
config.onInit = async (payload)=>{
|
|
764
|
+
if (existingOnInit) {
|
|
765
|
+
await existingOnInit(payload);
|
|
766
|
+
}
|
|
767
|
+
setAlertsContext({
|
|
768
|
+
payload: payload,
|
|
769
|
+
collectionSlug: alertsSlug
|
|
770
|
+
});
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
// Drive the janitor sweep. The task is registered as a Payload
|
|
774
|
+
// job, but Payload's `autoRun` only runs *queued* jobs — it does
|
|
775
|
+
// not auto-enqueue tasks on a schedule. Without an explicit driver
|
|
776
|
+
// here, the janitor never fires and stuck `running` units stay
|
|
777
|
+
// stuck forever after a worker crash or container restart. We
|
|
778
|
+
// drive it from `onInit` with two layers:
|
|
779
|
+
//
|
|
780
|
+
// 1. Boot sweep — runs once immediately so a process that comes
|
|
781
|
+
// up after a crash reclaims orphaned units from the previous
|
|
782
|
+
// lifetime within ~10s of becoming healthy.
|
|
783
|
+
// 2. Periodic sweep — every `bulk.janitorIntervalMs` (default
|
|
784
|
+
// 5 min) for the life of the process, so a worker dying
|
|
785
|
+
// mid-run also gets cleaned up without waiting for a restart.
|
|
786
|
+
//
|
|
787
|
+
// In serverless / short-lived process deployments the periodic
|
|
788
|
+
// setInterval simply never fires (process is killed between
|
|
789
|
+
// invocations), but the boot sweep still runs on each cold start —
|
|
790
|
+
// which is exactly when stuck units would otherwise be problematic.
|
|
791
|
+
if (options.bulk && options.bulk.enabled !== false) {
|
|
792
|
+
const bulkConfigForJanitor = options.bulk;
|
|
793
|
+
const existingOnInit = config.onInit;
|
|
794
|
+
const unitsSlug = bulkConfigForJanitor.unitsCollectionSlug ?? DEFAULT_BULK_TRANSLATE_UNITS_COLLECTION_SLUG;
|
|
795
|
+
const usageSlug = options.usageTracking?.collectionSlug ?? DEFAULT_USAGE_COLLECTION_SLUG;
|
|
796
|
+
const janitorIntervalMs = bulkConfigForJanitor.janitorIntervalMs ?? 5 * 60_000;
|
|
797
|
+
config.onInit = async (payload)=>{
|
|
798
|
+
if (existingOnInit) {
|
|
799
|
+
await existingOnInit(payload);
|
|
800
|
+
}
|
|
801
|
+
const sweep = async (label)=>{
|
|
802
|
+
try {
|
|
803
|
+
const result = await runJanitorSweep({
|
|
804
|
+
payload,
|
|
805
|
+
unitsSlug,
|
|
806
|
+
batchesSlug: DEFAULT_BULK_TRANSLATE_BATCHES_COLLECTION_SLUG,
|
|
807
|
+
usageSlug,
|
|
808
|
+
minThresholdMs: 10 * 60_000,
|
|
809
|
+
p99Multiplier: 2,
|
|
810
|
+
maxAttempts: 3,
|
|
811
|
+
p99CacheTtlMs: 5 * 60_000,
|
|
812
|
+
callbacks: {
|
|
813
|
+
onBatchComplete: bulkConfigForJanitor.onBatchComplete,
|
|
814
|
+
onBatchFailed: bulkConfigForJanitor.onBatchFailed
|
|
815
|
+
}
|
|
816
|
+
});
|
|
817
|
+
if (result.reset > 0 || result.failed > 0 || result.requeued > 0) {
|
|
818
|
+
payload.logger?.info?.(`[ai-translate] janitor ${label} sweep: reset=${result.reset} failed=${result.failed} requeued=${result.requeued} thresholdMs=${result.thresholdMs}`);
|
|
819
|
+
}
|
|
820
|
+
} catch (err) {
|
|
821
|
+
payload.logger?.warn?.(`[ai-translate] janitor ${label} sweep failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
// Boot sweep — fire-and-forget so onInit doesn't block server
|
|
825
|
+
// start while the sweep runs.
|
|
826
|
+
void sweep('boot');
|
|
827
|
+
// Periodic sweep. `unref()` lets the process exit naturally
|
|
828
|
+
// without the timer keeping it alive (matters for test
|
|
829
|
+
// teardown and serverless-style short-lived processes).
|
|
830
|
+
if (janitorIntervalMs > 0) {
|
|
831
|
+
const timer = setInterval(()=>{
|
|
832
|
+
void sweep('interval');
|
|
833
|
+
}, janitorIntervalMs);
|
|
834
|
+
if (typeof timer.unref === 'function') {
|
|
835
|
+
timer.unref();
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
return config;
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
// ---------------------------------------------------------------------------
|
|
844
|
+
// Per-field button injection
|
|
845
|
+
// ---------------------------------------------------------------------------
|
|
846
|
+
const TRANSLATABLE_FIELD_TYPES = new Set([
|
|
847
|
+
'text',
|
|
848
|
+
'textarea',
|
|
849
|
+
'richText'
|
|
850
|
+
]);
|
|
851
|
+
function injectFieldButtons(fields, excludePatterns, pathPrefix = '') {
|
|
852
|
+
return fields.map((field)=>{
|
|
853
|
+
const f = field;
|
|
854
|
+
const type = f.type;
|
|
855
|
+
const localized = f.localized;
|
|
856
|
+
const name = f.name;
|
|
857
|
+
const fieldPath = name ? pathPrefix ? `${pathPrefix}.${name}` : name : pathPrefix;
|
|
858
|
+
// Inject afterInput on localized translatable fields
|
|
859
|
+
if (type && TRANSLATABLE_FIELD_TYPES.has(type) && localized && name) {
|
|
860
|
+
// Skip injection for fields that are excluded from translation. The
|
|
861
|
+
// server-side flow already filters these out, but rendering the button
|
|
862
|
+
// creates user confusion (e.g. clicking "Translate" on `slug`).
|
|
863
|
+
if (isExcluded(fieldPath, excludePatterns)) {
|
|
864
|
+
return field;
|
|
865
|
+
}
|
|
866
|
+
const admin = f.admin ?? {};
|
|
867
|
+
const components = admin.components ?? {};
|
|
868
|
+
return {
|
|
869
|
+
...field,
|
|
870
|
+
admin: {
|
|
871
|
+
...admin,
|
|
872
|
+
components: {
|
|
873
|
+
...components,
|
|
874
|
+
afterInput: [
|
|
875
|
+
...components.afterInput ?? [],
|
|
876
|
+
'@purposeinplay/payload-ai-translate/client#FieldTranslateButton'
|
|
877
|
+
]
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
// Recurse into container fields
|
|
883
|
+
if (type === 'group' || type === 'array' || type === 'row' || type === 'collapsible') {
|
|
884
|
+
const subFields = f.fields;
|
|
885
|
+
if (subFields) {
|
|
886
|
+
// `row` and `collapsible` don't introduce a name segment in the path
|
|
887
|
+
const childPrefix = type === 'row' || type === 'collapsible' ? pathPrefix : fieldPath;
|
|
888
|
+
return {
|
|
889
|
+
...field,
|
|
890
|
+
fields: injectFieldButtons(subFields, excludePatterns, childPrefix)
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
if (type === 'tabs') {
|
|
895
|
+
const tabs = f.tabs;
|
|
896
|
+
if (tabs) {
|
|
897
|
+
return {
|
|
898
|
+
...field,
|
|
899
|
+
tabs: tabs.map((tab)=>{
|
|
900
|
+
const tabFields = tab.fields;
|
|
901
|
+
if (tabFields) {
|
|
902
|
+
const tabName = tab.name;
|
|
903
|
+
const childPrefix = tabName ? pathPrefix ? `${pathPrefix}.${tabName}` : tabName : pathPrefix;
|
|
904
|
+
return {
|
|
905
|
+
...tab,
|
|
906
|
+
fields: injectFieldButtons(tabFields, excludePatterns, childPrefix)
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
return tab;
|
|
910
|
+
})
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
if (type === 'blocks') {
|
|
915
|
+
const blocks = f.blocks;
|
|
916
|
+
if (blocks) {
|
|
917
|
+
return {
|
|
918
|
+
...field,
|
|
919
|
+
blocks: blocks.map((block)=>{
|
|
920
|
+
const blockFields = block.fields;
|
|
921
|
+
if (blockFields) {
|
|
922
|
+
return {
|
|
923
|
+
...block,
|
|
924
|
+
fields: injectFieldButtons(blockFields, excludePatterns, fieldPath)
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
return block;
|
|
928
|
+
})
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return field;
|
|
933
|
+
});
|
|
934
|
+
}
|