@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.
Files changed (301) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +714 -0
  3. package/dist/alerts-collection.d.ts +21 -0
  4. package/dist/alerts-collection.js +159 -0
  5. package/dist/api.d.ts +4 -0
  6. package/dist/api.js +918 -0
  7. package/dist/bulk-translate-batches-collection.d.ts +29 -0
  8. package/dist/bulk-translate-batches-collection.js +404 -0
  9. package/dist/bulk-translate-units-collection.d.ts +35 -0
  10. package/dist/bulk-translate-units-collection.js +310 -0
  11. package/dist/client/estimated-cost-cell.d.ts +6 -0
  12. package/dist/client/estimated-cost-cell.js +12 -0
  13. package/dist/client/excluded-fields-field.d.ts +45 -0
  14. package/dist/client/excluded-fields-field.js +553 -0
  15. package/dist/client/field-translate-button.d.ts +6 -0
  16. package/dist/client/field-translate-button.js +199 -0
  17. package/dist/client/index.d.ts +6 -0
  18. package/dist/client/index.js +6 -0
  19. package/dist/client/lib/use-global-kill-switches.d.ts +20 -0
  20. package/dist/client/lib/use-global-kill-switches.js +58 -0
  21. package/dist/client/translate-button.d.ts +2 -0
  22. package/dist/client/translate-button.js +228 -0
  23. package/dist/client/translate-modal.d.ts +16 -0
  24. package/dist/client/translate-modal.js +549 -0
  25. package/dist/client/translation-progress.d.ts +10 -0
  26. package/dist/client/translation-progress.js +297 -0
  27. package/dist/components/TranslationNavGroup.d.ts +45 -0
  28. package/dist/components/TranslationNavGroup.js +104 -0
  29. package/dist/defaults.d.ts +11 -0
  30. package/dist/defaults.js +16 -0
  31. package/dist/endpoints/client-config.d.ts +44 -0
  32. package/dist/endpoints/client-config.js +145 -0
  33. package/dist/endpoints/estimate.d.ts +5 -0
  34. package/dist/endpoints/estimate.js +237 -0
  35. package/dist/endpoints/progress.d.ts +2 -0
  36. package/dist/endpoints/progress.js +314 -0
  37. package/dist/endpoints/translate.d.ts +11 -0
  38. package/dist/endpoints/translate.js +376 -0
  39. package/dist/endpoints/translation-hub/_helpers.d.ts +140 -0
  40. package/dist/endpoints/translation-hub/_helpers.js +297 -0
  41. package/dist/endpoints/translation-hub/active.d.ts +21 -0
  42. package/dist/endpoints/translation-hub/active.js +220 -0
  43. package/dist/endpoints/translation-hub/cancel.d.ts +22 -0
  44. package/dist/endpoints/translation-hub/cancel.js +233 -0
  45. package/dist/endpoints/translation-hub/enqueue.d.ts +70 -0
  46. package/dist/endpoints/translation-hub/enqueue.js +529 -0
  47. package/dist/endpoints/translation-hub/failures.d.ts +12 -0
  48. package/dist/endpoints/translation-hub/failures.js +67 -0
  49. package/dist/endpoints/translation-hub/force-reset.d.ts +20 -0
  50. package/dist/endpoints/translation-hub/force-reset.js +144 -0
  51. package/dist/endpoints/translation-hub/index.d.ts +21 -0
  52. package/dist/endpoints/translation-hub/index.js +20 -0
  53. package/dist/endpoints/translation-hub/list.d.ts +40 -0
  54. package/dist/endpoints/translation-hub/list.js +182 -0
  55. package/dist/endpoints/translation-hub/preflight.d.ts +19 -0
  56. package/dist/endpoints/translation-hub/preflight.js +141 -0
  57. package/dist/endpoints/translation-hub/retry-failed.d.ts +38 -0
  58. package/dist/endpoints/translation-hub/retry-failed.js +235 -0
  59. package/dist/endpoints/translation-hub/revert.d.ts +88 -0
  60. package/dist/endpoints/translation-hub/revert.js +405 -0
  61. package/dist/endpoints/translation-hub/status.d.ts +45 -0
  62. package/dist/endpoints/translation-hub/status.js +391 -0
  63. package/dist/endpoints/translation-hub/usage-summary.d.ts +114 -0
  64. package/dist/endpoints/translation-hub/usage-summary.js +481 -0
  65. package/dist/exports/client.d.ts +6 -0
  66. package/dist/exports/client.js +6 -0
  67. package/dist/exports/components.d.ts +6 -0
  68. package/dist/exports/components.js +5 -0
  69. package/dist/exports/index.d.ts +8 -0
  70. package/dist/exports/index.js +7 -0
  71. package/dist/exports/providers.d.ts +9 -0
  72. package/dist/exports/providers.js +5 -0
  73. package/dist/exports/views-client.d.ts +23 -0
  74. package/dist/exports/views-client.js +22 -0
  75. package/dist/exports/views.d.ts +30 -0
  76. package/dist/exports/views.js +29 -0
  77. package/dist/hooks/after-change-global.d.ts +4 -0
  78. package/dist/hooks/after-change-global.js +109 -0
  79. package/dist/hooks/after-change.d.ts +16 -0
  80. package/dist/hooks/after-change.js +205 -0
  81. package/dist/hooks/after-delete.d.ts +30 -0
  82. package/dist/hooks/after-delete.js +95 -0
  83. package/dist/index.d.ts +5 -0
  84. package/dist/index.js +5 -0
  85. package/dist/jobs-collection.d.ts +17 -0
  86. package/dist/jobs-collection.js +139 -0
  87. package/dist/lexical/classifier.d.ts +3 -0
  88. package/dist/lexical/classifier.js +108 -0
  89. package/dist/lexical/deserializer.d.ts +4 -0
  90. package/dist/lexical/deserializer.js +263 -0
  91. package/dist/lexical/placeholder-integrity.d.ts +6 -0
  92. package/dist/lexical/placeholder-integrity.js +21 -0
  93. package/dist/lexical/placeholders.d.ts +21 -0
  94. package/dist/lexical/placeholders.js +117 -0
  95. package/dist/lexical/serializer.d.ts +21 -0
  96. package/dist/lexical/serializer.js +233 -0
  97. package/dist/lexical/types.d.ts +32 -0
  98. package/dist/lexical/types.js +1 -0
  99. package/dist/lib/auth-diagnostics.d.ts +14 -0
  100. package/dist/lib/auth-diagnostics.js +19 -0
  101. package/dist/lib/batch-counts.d.ts +58 -0
  102. package/dist/lib/batch-counts.js +105 -0
  103. package/dist/lib/bulk-translate-migrations.d.ts +92 -0
  104. package/dist/lib/bulk-translate-migrations.js +153 -0
  105. package/dist/lib/coalescing-queue.d.ts +38 -0
  106. package/dist/lib/coalescing-queue.js +69 -0
  107. package/dist/lib/content-extractor.d.ts +16 -0
  108. package/dist/lib/content-extractor.js +410 -0
  109. package/dist/lib/content-hash.d.ts +1 -0
  110. package/dist/lib/content-hash.js +19 -0
  111. package/dist/lib/content-patcher.d.ts +15 -0
  112. package/dist/lib/content-patcher.js +293 -0
  113. package/dist/lib/cost-guards.d.ts +2 -0
  114. package/dist/lib/cost-guards.js +18 -0
  115. package/dist/lib/daily-spend-cap.d.ts +58 -0
  116. package/dist/lib/daily-spend-cap.js +233 -0
  117. package/dist/lib/effective-locales.d.ts +181 -0
  118. package/dist/lib/effective-locales.js +302 -0
  119. package/dist/lib/error-messages.d.ts +245 -0
  120. package/dist/lib/error-messages.js +626 -0
  121. package/dist/lib/events.d.ts +39 -0
  122. package/dist/lib/events.js +146 -0
  123. package/dist/lib/exclude-fields.d.ts +3 -0
  124. package/dist/lib/exclude-fields.js +64 -0
  125. package/dist/lib/field-breadcrumb.d.ts +31 -0
  126. package/dist/lib/field-breadcrumb.js +227 -0
  127. package/dist/lib/field-diff.d.ts +1 -0
  128. package/dist/lib/field-diff.js +25 -0
  129. package/dist/lib/field-empty.d.ts +2 -0
  130. package/dist/lib/field-empty.js +68 -0
  131. package/dist/lib/field-resolver.d.ts +3 -0
  132. package/dist/lib/field-resolver.js +164 -0
  133. package/dist/lib/group-soft-skips.d.ts +39 -0
  134. package/dist/lib/group-soft-skips.js +45 -0
  135. package/dist/lib/locale-merge.d.ts +44 -0
  136. package/dist/lib/locale-merge.js +357 -0
  137. package/dist/lib/locale-row-check.d.ts +30 -0
  138. package/dist/lib/locale-row-check.js +64 -0
  139. package/dist/lib/logger.d.ts +74 -0
  140. package/dist/lib/logger.js +97 -0
  141. package/dist/lib/manual-edit-guard.d.ts +128 -0
  142. package/dist/lib/manual-edit-guard.js +393 -0
  143. package/dist/lib/output-validation.d.ts +48 -0
  144. package/dist/lib/output-validation.js +148 -0
  145. package/dist/lib/payload-read.d.ts +16 -0
  146. package/dist/lib/payload-read.js +51 -0
  147. package/dist/lib/per-doc-claim.d.ts +90 -0
  148. package/dist/lib/per-doc-claim.js +140 -0
  149. package/dist/lib/per-doc-lock.d.ts +94 -0
  150. package/dist/lib/per-doc-lock.js +119 -0
  151. package/dist/lib/persist-usage.d.ts +91 -0
  152. package/dist/lib/persist-usage.js +116 -0
  153. package/dist/lib/progress-store.d.ts +103 -0
  154. package/dist/lib/progress-store.js +314 -0
  155. package/dist/lib/rate-limiter.d.ts +3 -0
  156. package/dist/lib/rate-limiter.js +53 -0
  157. package/dist/lib/snapshot-select.d.ts +43 -0
  158. package/dist/lib/snapshot-select.js +108 -0
  159. package/dist/lib/translate-prompt.d.ts +31 -0
  160. package/dist/lib/translate-prompt.js +66 -0
  161. package/dist/lib/translation-token-bucket.d.ts +57 -0
  162. package/dist/lib/translation-token-bucket.js +365 -0
  163. package/dist/lib/truncate-source-value.d.ts +1 -0
  164. package/dist/lib/truncate-source-value.js +27 -0
  165. package/dist/manual-edit-collection.d.ts +22 -0
  166. package/dist/manual-edit-collection.js +124 -0
  167. package/dist/plugin.d.ts +3 -0
  168. package/dist/plugin.js +934 -0
  169. package/dist/providers/ai-sdk-adapter.d.ts +35 -0
  170. package/dist/providers/ai-sdk-adapter.js +100 -0
  171. package/dist/providers/anthropic.d.ts +31 -0
  172. package/dist/providers/anthropic.js +66 -0
  173. package/dist/providers/custom.d.ts +36 -0
  174. package/dist/providers/custom.js +24 -0
  175. package/dist/providers/gemini.d.ts +20 -0
  176. package/dist/providers/gemini.js +48 -0
  177. package/dist/providers/mock.d.ts +2 -0
  178. package/dist/providers/mock.js +29 -0
  179. package/dist/providers/openai.d.ts +28 -0
  180. package/dist/providers/openai.js +69 -0
  181. package/dist/settings-global.d.ts +74 -0
  182. package/dist/settings-global.js +216 -0
  183. package/dist/tasks/bulk-translate-coordinator.d.ts +115 -0
  184. package/dist/tasks/bulk-translate-coordinator.js +708 -0
  185. package/dist/tasks/bulk-translate-doc-task.d.ts +142 -0
  186. package/dist/tasks/bulk-translate-doc-task.js +1000 -0
  187. package/dist/tasks/bulk-translate-janitor.d.ts +87 -0
  188. package/dist/tasks/bulk-translate-janitor.js +311 -0
  189. package/dist/tasks/translate-job-task.d.ts +51 -0
  190. package/dist/tasks/translate-job-task.js +154 -0
  191. package/dist/translate.d.ts +113 -0
  192. package/dist/translate.js +911 -0
  193. package/dist/translation-daily-spend-collection.d.ts +24 -0
  194. package/dist/translation-daily-spend-collection.js +133 -0
  195. package/dist/translation-rate-limits-collection.d.ts +30 -0
  196. package/dist/translation-rate-limits-collection.js +144 -0
  197. package/dist/types.d.ts +672 -0
  198. package/dist/types.js +1 -0
  199. package/dist/usage-collection.d.ts +14 -0
  200. package/dist/usage-collection.js +377 -0
  201. package/dist/views/BulkRunsHub/BatchRow.d.ts +32 -0
  202. package/dist/views/BulkRunsHub/BatchRow.js +1222 -0
  203. package/dist/views/BulkRunsHub/BucketRow.d.ts +62 -0
  204. package/dist/views/BulkRunsHub/BucketRow.js +982 -0
  205. package/dist/views/BulkRunsHub/BulkRunsHub.client.d.ts +18 -0
  206. package/dist/views/BulkRunsHub/BulkRunsHub.client.js +331 -0
  207. package/dist/views/BulkRunsHub/EmptyState.d.ts +6 -0
  208. package/dist/views/BulkRunsHub/EmptyState.js +64 -0
  209. package/dist/views/BulkRunsHub/FilterBar.d.ts +16 -0
  210. package/dist/views/BulkRunsHub/FilterBar.js +284 -0
  211. package/dist/views/BulkRunsHub/InFlightBanner.d.ts +14 -0
  212. package/dist/views/BulkRunsHub/InFlightBanner.js +59 -0
  213. package/dist/views/BulkRunsHub/StatusBadge.d.ts +64 -0
  214. package/dist/views/BulkRunsHub/StatusBadge.js +248 -0
  215. package/dist/views/BulkRunsHub/SummaryStrip.d.ts +22 -0
  216. package/dist/views/BulkRunsHub/SummaryStrip.js +249 -0
  217. package/dist/views/BulkRunsHub/bucket-grouping.d.ts +200 -0
  218. package/dist/views/BulkRunsHub/bucket-grouping.js +344 -0
  219. package/dist/views/BulkRunsHub/bucketFailureSummary.d.ts +9 -0
  220. package/dist/views/BulkRunsHub/bucketFailureSummary.js +36 -0
  221. package/dist/views/BulkRunsHub/dedupedStatusFetch.d.ts +5 -0
  222. package/dist/views/BulkRunsHub/dedupedStatusFetch.js +45 -0
  223. package/dist/views/BulkRunsHub/index.d.ts +17 -0
  224. package/dist/views/BulkRunsHub/index.js +80 -0
  225. package/dist/views/BulkRunsHub/urlFilters.d.ts +14 -0
  226. package/dist/views/BulkRunsHub/urlFilters.js +50 -0
  227. package/dist/views/BulkRunsHub/useBulkRunsList.d.ts +26 -0
  228. package/dist/views/BulkRunsHub/useBulkRunsList.js +204 -0
  229. package/dist/views/BulkRunsHub/useUrlFilters.d.ts +10 -0
  230. package/dist/views/BulkRunsHub/useUrlFilters.js +88 -0
  231. package/dist/views/TranslationHub/ActiveJobs.d.ts +6 -0
  232. package/dist/views/TranslationHub/ActiveJobs.js +320 -0
  233. package/dist/views/TranslationHub/AdvancedPanel.d.ts +17 -0
  234. package/dist/views/TranslationHub/AdvancedPanel.js +996 -0
  235. package/dist/views/TranslationHub/AlertBanner.d.ts +6 -0
  236. package/dist/views/TranslationHub/AlertBanner.js +568 -0
  237. package/dist/views/TranslationHub/AuditPanel.d.ts +6 -0
  238. package/dist/views/TranslationHub/AuditPanel.helpers.d.ts +44 -0
  239. package/dist/views/TranslationHub/AuditPanel.helpers.js +71 -0
  240. package/dist/views/TranslationHub/AuditPanel.js +1367 -0
  241. package/dist/views/TranslationHub/BulkTranslate.types.d.ts +242 -0
  242. package/dist/views/TranslationHub/BulkTranslate.types.js +36 -0
  243. package/dist/views/TranslationHub/BulkTranslateFailureDrawer.d.ts +19 -0
  244. package/dist/views/TranslationHub/BulkTranslateFailureDrawer.js +332 -0
  245. package/dist/views/TranslationHub/BulkTranslateMonitor.d.ts +28 -0
  246. package/dist/views/TranslationHub/BulkTranslateMonitor.js +305 -0
  247. package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.d.ts +3 -0
  248. package/dist/views/TranslationHub/BulkTranslateNarrowViewportBanner.js +42 -0
  249. package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.d.ts +26 -0
  250. package/dist/views/TranslationHub/BulkTranslatePostEnqueueTransition.js +95 -0
  251. package/dist/views/TranslationHub/BulkTranslatePreflightModal.d.ts +22 -0
  252. package/dist/views/TranslationHub/BulkTranslatePreflightModal.js +879 -0
  253. package/dist/views/TranslationHub/BulkTranslateTerminalCard.d.ts +29 -0
  254. package/dist/views/TranslationHub/BulkTranslateTerminalCard.js +445 -0
  255. package/dist/views/TranslationHub/BulkTranslateTrigger.d.ts +66 -0
  256. package/dist/views/TranslationHub/BulkTranslateTrigger.js +161 -0
  257. package/dist/views/TranslationHub/EditorRecentRunsPanel.d.ts +33 -0
  258. package/dist/views/TranslationHub/EditorRecentRunsPanel.js +290 -0
  259. package/dist/views/TranslationHub/Hub.client.d.ts +74 -0
  260. package/dist/views/TranslationHub/Hub.client.js +357 -0
  261. package/dist/views/TranslationHub/ModelCombobox.d.ts +14 -0
  262. package/dist/views/TranslationHub/ModelCombobox.js +415 -0
  263. package/dist/views/TranslationHub/PerCollectionConfig.d.ts +10 -0
  264. package/dist/views/TranslationHub/PerCollectionConfig.helpers.d.ts +16 -0
  265. package/dist/views/TranslationHub/PerCollectionConfig.helpers.js +19 -0
  266. package/dist/views/TranslationHub/PerCollectionConfig.js +759 -0
  267. package/dist/views/TranslationHub/SettingsRail.d.ts +11 -0
  268. package/dist/views/TranslationHub/SettingsRail.js +382 -0
  269. package/dist/views/TranslationHub/StatusStrip.d.ts +6 -0
  270. package/dist/views/TranslationHub/StatusStrip.js +451 -0
  271. package/dist/views/TranslationHub/UsageTable.d.ts +6 -0
  272. package/dist/views/TranslationHub/UsageTable.helpers.d.ts +69 -0
  273. package/dist/views/TranslationHub/UsageTable.helpers.js +49 -0
  274. package/dist/views/TranslationHub/UsageTable.js +1240 -0
  275. package/dist/views/TranslationHub/alertGrouping.d.ts +70 -0
  276. package/dist/views/TranslationHub/alertGrouping.js +99 -0
  277. package/dist/views/TranslationHub/index.d.ts +20 -0
  278. package/dist/views/TranslationHub/index.js +109 -0
  279. package/dist/views/TranslationHub/tabNavigation.d.ts +53 -0
  280. package/dist/views/TranslationHub/tabNavigation.js +74 -0
  281. package/dist/views/TranslationHub/terminalBannerVisibility.d.ts +33 -0
  282. package/dist/views/TranslationHub/terminalBannerVisibility.js +124 -0
  283. package/dist/views/TranslationHub/useBulkTranslateActive.d.ts +49 -0
  284. package/dist/views/TranslationHub/useBulkTranslateActive.js +251 -0
  285. package/dist/views/TranslationHub/useFocusTrap.d.ts +6 -0
  286. package/dist/views/TranslationHub/useFocusTrap.js +81 -0
  287. package/dist/views/TranslationHub/useTranslationHubUsageSummary.d.ts +77 -0
  288. package/dist/views/TranslationHub/useTranslationHubUsageSummary.js +267 -0
  289. package/dist/views/shared/EditorError.d.ts +97 -0
  290. package/dist/views/shared/EditorError.js +205 -0
  291. package/dist/views/shared/ModelCell.d.ts +18 -0
  292. package/dist/views/shared/ModelCell.js +31 -0
  293. package/dist/views/shared/docHref.d.ts +16 -0
  294. package/dist/views/shared/docHref.js +26 -0
  295. package/dist/views/shared/fetch-error-body.d.ts +25 -0
  296. package/dist/views/shared/fetch-error-body.js +42 -0
  297. package/dist/views/shared/filterPillStyle.d.ts +35 -0
  298. package/dist/views/shared/filterPillStyle.js +40 -0
  299. package/dist/views/shared/format.d.ts +75 -0
  300. package/dist/views/shared/format.js +131 -0
  301. 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";
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ import type { PayloadHandler } from 'payload';
2
+ import type { HandlerContext } from './translate.js';
3
+ type HandlerArg = HandlerContext | string | undefined;
4
+ export declare const getEstimateHandler: (arg?: HandlerArg) => PayloadHandler;
5
+ export {};