@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,549 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { toast, useConfig, useFormModified } from '@payloadcms/ui';
4
+ import { formatAdminURL } from 'payload/shared';
5
+ import React, { useCallback, useEffect, useState } from 'react';
6
+ import { createPortal } from 'react-dom';
7
+ import { formatCost } from '../views/shared/format.js';
8
+ // Cost rendering uses the shared `formatCost` helper from
9
+ // `views/shared/format.ts` (NEW-17 — single source of truth across
10
+ // every cost render site in the plugin).
11
+ export function TranslateModal({ docId, collectionSlug, globalSlug, onClose, onJobStarted }) {
12
+ const { config } = useConfig();
13
+ // The modal does NOT auto-save the form. A previous implementation
14
+ // called `useForm().submit()` here so unsaved edits would flush
15
+ // before the estimate/translate read the DB — but that save fires
16
+ // the collection's afterChange hooks, including this plugin's own
17
+ // auto-translate hook. The result: clicking "Continue" silently
18
+ // kicked off a real translation in the background while the user
19
+ // was still looking at the cost estimate. Cancel on the cost modal
20
+ // closed the dialog but never aborted the in-flight job, so the
21
+ // explicit "Translate" button (when clicked) actually fired a
22
+ // SECOND translation on top of the first. The estimate + translate
23
+ // endpoints both read DB state directly; if the form has unsaved
24
+ // edits the user is warned in the estimate step so they can save
25
+ // first and reopen the modal.
26
+ const formModified = useFormModified();
27
+ const locales = config.localization ? config.localization.locales : [];
28
+ const defaultLocale = config.localization ? config.localization.defaultLocale : 'en';
29
+ // Restrict the picker to the plugin's configured `targetLocales` (not
30
+ // every locale registered in Payload's `config.localization`). Without
31
+ // this filter, the drawer surfaces locales the plugin can't translate
32
+ // to anyway. Payload's admin bundle strips function refs from
33
+ // `config.custom` so we can't read `targetLocales` synchronously from
34
+ // `useConfig()` — fetch them from the plugin's `/ai-translate/
35
+ // client-config` endpoint on mount instead. Falls back to "every
36
+ // non-source locale" until the fetch resolves (no flash of wrong UI;
37
+ // the modal starts disabled below).
38
+ const [pluginTargetLocales, setPluginTargetLocales] = useState(null);
39
+ const [clientConfigLoaded, setClientConfigLoaded] = useState(false);
40
+ const apiRouteForConfig = config.routes?.api ?? '/api';
41
+ // Use `formatAdminURL` so the consumer's Next.js basePath (e.g. '/blog'
42
+ // for blog-wild) is prepended. Raw `${apiRoute}/...` resolves against
43
+ // `window.location.origin + apiRoute` and 404s on every consumer
44
+ // that uses a basePath. Mirrors the pattern in `excluded-fields-field.tsx`.
45
+ const clientConfigUrl = formatAdminURL({
46
+ apiRoute: apiRouteForConfig,
47
+ path: '/ai-translate/client-config',
48
+ serverURL: ''
49
+ });
50
+ useEffect(()=>{
51
+ let cancelled = false;
52
+ void fetch(clientConfigUrl, {
53
+ credentials: 'include'
54
+ }).then((res)=>res.ok ? res.json() : null).then((data)=>{
55
+ if (cancelled) return;
56
+ if (data && Array.isArray(data.targetLocales)) {
57
+ setPluginTargetLocales(data.targetLocales);
58
+ }
59
+ setClientConfigLoaded(true);
60
+ }).catch(()=>{
61
+ if (cancelled) return;
62
+ // Network / 404 → graceful fallback to "every non-source locale"
63
+ setClientConfigLoaded(true);
64
+ });
65
+ return ()=>{
66
+ cancelled = true;
67
+ };
68
+ }, [
69
+ clientConfigUrl
70
+ ]);
71
+ const targetLocales = pluginTargetLocales && pluginTargetLocales.length > 0 ? locales.filter((l)=>pluginTargetLocales.includes(l.code)) : locales.filter((l)=>l.code !== defaultLocale);
72
+ const [selected, setSelected] = useState(new Set());
73
+ // Once the fetch settles, default-select every plugin-configured locale.
74
+ // Done in an effect so the selection updates after `targetLocales`
75
+ // resolves; without this, the modal renders with an empty selection.
76
+ useEffect(()=>{
77
+ if (clientConfigLoaded) {
78
+ setSelected(new Set(targetLocales.map((l)=>l.code)));
79
+ }
80
+ // eslint-disable-next-line react-hooks/exhaustive-deps
81
+ }, [
82
+ clientConfigLoaded,
83
+ pluginTargetLocales
84
+ ]);
85
+ const [step, setStep] = useState('pick-locales');
86
+ const [estimate, setEstimate] = useState(null);
87
+ const [resultMessage, setResultMessage] = useState('');
88
+ const isGlobal = !!globalSlug;
89
+ const pathPrefix = isGlobal ? `/globals/${globalSlug}` : `/${collectionSlug}`;
90
+ const apiRoute = config.routes?.api ?? '/api';
91
+ const toggleLocale = (code)=>{
92
+ setSelected((prev)=>{
93
+ const next = new Set(prev);
94
+ if (next.has(code)) next.delete(code);
95
+ else next.add(code);
96
+ return next;
97
+ });
98
+ };
99
+ // Step 1: locale picker → fetch estimate (does NOT save the form;
100
+ // see the rationale on `formModified` above).
101
+ const handleConfirmLocales = useCallback(async ()=>{
102
+ if (selected.size === 0) return;
103
+ setStep('estimating');
104
+ try {
105
+ const body = isGlobal ? {
106
+ targetLocales: [
107
+ ...selected
108
+ ]
109
+ } : {
110
+ ids: [
111
+ docId
112
+ ],
113
+ targetLocales: [
114
+ ...selected
115
+ ]
116
+ };
117
+ const estimatePath = `${pathPrefix}/ai-translate/estimate`;
118
+ const res = await fetch(formatAdminURL({
119
+ apiRoute,
120
+ path: estimatePath
121
+ }), {
122
+ method: 'POST',
123
+ credentials: 'include',
124
+ headers: {
125
+ 'Content-Type': 'application/json'
126
+ },
127
+ body: JSON.stringify(body)
128
+ });
129
+ if (!res.ok) {
130
+ // Estimate failed — fall through, allow user to translate without it
131
+ toast.error('Could not fetch cost estimate; you can still proceed.');
132
+ setEstimate(null);
133
+ setStep('estimate-shown');
134
+ return;
135
+ }
136
+ const data = await res.json();
137
+ setEstimate(data);
138
+ setStep('estimate-shown');
139
+ } catch {
140
+ toast.error('Could not fetch cost estimate; you can still proceed.');
141
+ setEstimate(null);
142
+ setStep('estimate-shown');
143
+ }
144
+ }, [
145
+ selected,
146
+ docId,
147
+ pathPrefix,
148
+ apiRoute,
149
+ isGlobal
150
+ ]);
151
+ // Step 2: estimate-shown → fire translation
152
+ const handleTranslate = useCallback(async ()=>{
153
+ setStep('translating');
154
+ try {
155
+ const body = {
156
+ targetLocales: [
157
+ ...selected
158
+ ],
159
+ // Run in the background. The server returns the jobId immediately
160
+ // and the sidebar progress widget tracks completion via SSE.
161
+ async: true
162
+ };
163
+ if (!isGlobal) body.id = docId;
164
+ const translatePath = `${pathPrefix}/ai-translate`;
165
+ const res = await fetch(formatAdminURL({
166
+ apiRoute,
167
+ path: translatePath
168
+ }), {
169
+ method: 'POST',
170
+ credentials: 'include',
171
+ headers: {
172
+ 'Content-Type': 'application/json'
173
+ },
174
+ body: JSON.stringify(body)
175
+ });
176
+ if (!res.ok) {
177
+ const errorData = await res.json().catch(()=>({}));
178
+ const friendlyFallback = res.status >= 500 ? 'The translation service returned an error. Try again, or contact engineering if it keeps happening.' : 'The translation request couldn’t be completed. Try again, or refresh the page.';
179
+ throw new Error(errorData.error ?? friendlyFallback);
180
+ }
181
+ const data = await res.json();
182
+ const result = data.results[0];
183
+ if (result?.jobId) {
184
+ // Async mode — the server will run the translation in the background.
185
+ // Hand the jobId off to the sidebar progress widget and close. Keeping
186
+ // the modal open here would trap the user behind a duplicate progress
187
+ // UI for the same job.
188
+ onJobStarted?.(result.jobId);
189
+ toast.success('Translation started — track progress in the sidebar.');
190
+ onClose();
191
+ return;
192
+ }
193
+ const successCount = result?.succeeded?.length ?? 0;
194
+ const failCount = result?.failed?.length ?? 0;
195
+ setStep('done');
196
+ if (failCount === 0) {
197
+ setResultMessage(`Translated ${successCount} field(s) to ${selected.size} locale(s)`);
198
+ toast.success(`Translation complete: ${successCount} field(s) translated`);
199
+ } else {
200
+ setResultMessage(`${successCount} succeeded, ${failCount} failed`);
201
+ toast.error(`Translation finished, but ${failCount} field${failCount === 1 ? '' : 's'} couldn’t be translated. Open the document to review.`);
202
+ }
203
+ } catch (error) {
204
+ setStep('error');
205
+ const msg = error instanceof Error ? error.message : 'Translation failed';
206
+ setResultMessage(msg);
207
+ toast.error(msg);
208
+ }
209
+ }, [
210
+ selected,
211
+ docId,
212
+ pathPrefix,
213
+ apiRoute,
214
+ isGlobal
215
+ ]);
216
+ // Escape key closes modal (except during active translation)
217
+ useEffect(()=>{
218
+ const handleKeyDown = (e)=>{
219
+ if (e.key === 'Escape' && step !== 'translating' && step !== 'estimating') {
220
+ onClose();
221
+ }
222
+ };
223
+ document.addEventListener('keydown', handleKeyDown);
224
+ return ()=>document.removeEventListener('keydown', handleKeyDown);
225
+ }, [
226
+ onClose,
227
+ step
228
+ ]);
229
+ // Portal to document.body so the modal escapes any sticky/fixed/transform
230
+ // ancestor that would otherwise trap it in a lower stacking context (e.g.
231
+ // the per-field "Translate..." button lives inside the document sidebar,
232
+ // whose `position: sticky` parent ranks below the rich-text fixed toolbar
233
+ // — without a portal the modal renders behind the toolbar).
234
+ if (typeof document === 'undefined') return null;
235
+ return /*#__PURE__*/ createPortal(/*#__PURE__*/ _jsx("div", {
236
+ role: "dialog",
237
+ "aria-modal": "true",
238
+ "aria-labelledby": "ai-translate-modal-title",
239
+ onClick: (e)=>{
240
+ if (e.target === e.currentTarget && step !== 'translating' && step !== 'estimating') {
241
+ onClose();
242
+ }
243
+ },
244
+ style: {
245
+ position: 'fixed',
246
+ top: 0,
247
+ left: 0,
248
+ right: 0,
249
+ bottom: 0,
250
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
251
+ display: 'flex',
252
+ alignItems: 'center',
253
+ justifyContent: 'center',
254
+ zIndex: 10000
255
+ },
256
+ children: /*#__PURE__*/ _jsxs("div", {
257
+ style: {
258
+ backgroundColor: 'var(--theme-elevation-0)',
259
+ borderRadius: '4px',
260
+ padding: '24px',
261
+ minWidth: '360px',
262
+ maxWidth: '480px',
263
+ maxHeight: '80vh',
264
+ overflow: 'auto',
265
+ color: 'var(--theme-text)'
266
+ },
267
+ children: [
268
+ /*#__PURE__*/ _jsx("h3", {
269
+ id: "ai-translate-modal-title",
270
+ style: {
271
+ margin: '0 0 16px',
272
+ fontSize: '18px'
273
+ },
274
+ children: "Translate Document"
275
+ }),
276
+ step === 'pick-locales' && /*#__PURE__*/ _jsxs(_Fragment, {
277
+ children: [
278
+ /*#__PURE__*/ _jsx("p", {
279
+ style: {
280
+ margin: '0 0 12px',
281
+ fontSize: '14px',
282
+ color: 'var(--theme-elevation-500)'
283
+ },
284
+ children: "Select target locales:"
285
+ }),
286
+ /*#__PURE__*/ _jsx("div", {
287
+ style: {
288
+ display: 'flex',
289
+ flexDirection: 'column',
290
+ gap: '8px',
291
+ marginBottom: '16px'
292
+ },
293
+ children: targetLocales.map((locale)=>/*#__PURE__*/ _jsxs("label", {
294
+ style: {
295
+ display: 'flex',
296
+ alignItems: 'center',
297
+ gap: '8px',
298
+ cursor: 'pointer'
299
+ },
300
+ children: [
301
+ /*#__PURE__*/ _jsx("input", {
302
+ type: "checkbox",
303
+ checked: selected.has(locale.code),
304
+ onChange: ()=>toggleLocale(locale.code)
305
+ }),
306
+ /*#__PURE__*/ _jsx("span", {
307
+ children: locale.label ?? locale.code
308
+ })
309
+ ]
310
+ }, locale.code))
311
+ }),
312
+ /*#__PURE__*/ _jsxs("div", {
313
+ style: {
314
+ display: 'flex',
315
+ gap: '8px',
316
+ justifyContent: 'flex-end'
317
+ },
318
+ children: [
319
+ /*#__PURE__*/ _jsx("button", {
320
+ type: "button",
321
+ onClick: onClose,
322
+ style: {
323
+ padding: '8px 16px',
324
+ backgroundColor: 'transparent',
325
+ border: '1px solid var(--theme-elevation-300)',
326
+ borderRadius: '4px',
327
+ color: 'var(--theme-text)',
328
+ cursor: 'pointer'
329
+ },
330
+ children: "Cancel"
331
+ }),
332
+ /*#__PURE__*/ _jsx("button", {
333
+ type: "button",
334
+ onClick: handleConfirmLocales,
335
+ disabled: selected.size === 0,
336
+ style: {
337
+ padding: '8px 16px',
338
+ backgroundColor: 'var(--theme-success-500)',
339
+ border: 'none',
340
+ borderRadius: '4px',
341
+ color: 'white',
342
+ cursor: selected.size === 0 ? 'not-allowed' : 'pointer',
343
+ opacity: selected.size === 0 ? 0.5 : 1
344
+ },
345
+ children: "Continue"
346
+ })
347
+ ]
348
+ })
349
+ ]
350
+ }),
351
+ step === 'estimating' && /*#__PURE__*/ _jsx("div", {
352
+ style: {
353
+ textAlign: 'center',
354
+ padding: '16px 0'
355
+ },
356
+ children: /*#__PURE__*/ _jsx("p", {
357
+ children: "Calculating cost estimate…"
358
+ })
359
+ }),
360
+ step === 'estimate-shown' && /*#__PURE__*/ _jsxs(_Fragment, {
361
+ children: [
362
+ formModified && /*#__PURE__*/ _jsx("div", {
363
+ role: "alert",
364
+ style: {
365
+ margin: '0 0 12px',
366
+ padding: '10px 12px',
367
+ borderRadius: '4px',
368
+ border: '1px solid var(--theme-warning-500)',
369
+ backgroundColor: 'var(--theme-warning-50, rgba(234, 179, 8, 0.08))',
370
+ color: 'var(--theme-warning-800, #78350f)',
371
+ fontSize: '13px',
372
+ lineHeight: 1.4
373
+ },
374
+ children: "You have unsaved edits on this document. The translation will use the last saved version — save first if you want recent changes included, then reopen this dialog."
375
+ }),
376
+ /*#__PURE__*/ _jsx("p", {
377
+ style: {
378
+ margin: '0 0 12px',
379
+ fontSize: '14px',
380
+ color: 'var(--theme-elevation-500)'
381
+ },
382
+ children: "Cost estimate"
383
+ }),
384
+ estimate ? /*#__PURE__*/ _jsxs("dl", {
385
+ style: {
386
+ margin: '0 0 16px',
387
+ fontSize: '14px',
388
+ display: 'grid',
389
+ gridTemplateColumns: 'auto 1fr',
390
+ gap: '4px 12px'
391
+ },
392
+ children: [
393
+ /*#__PURE__*/ _jsx("dt", {
394
+ children: "Characters:"
395
+ }),
396
+ /*#__PURE__*/ _jsxs("dd", {
397
+ style: {
398
+ margin: 0
399
+ },
400
+ children: [
401
+ estimate.totalCharacters.toLocaleString(),
402
+ estimate.likelySkippedCharacters && estimate.likelySkippedCharacters > 0 ? /*#__PURE__*/ _jsxs("span", {
403
+ style: {
404
+ color: 'var(--theme-elevation-500)',
405
+ fontSize: '12px'
406
+ },
407
+ children: [
408
+ ' ',
409
+ "(",
410
+ estimate.billableCharacters.toLocaleString(),
411
+ " billable",
412
+ ' · ',
413
+ estimate.likelySkippedCharacters.toLocaleString(),
414
+ " likely skipped)"
415
+ ]
416
+ }) : null
417
+ ]
418
+ }),
419
+ /*#__PURE__*/ _jsx("dt", {
420
+ children: "Tokens (est.):"
421
+ }),
422
+ /*#__PURE__*/ _jsx("dd", {
423
+ style: {
424
+ margin: 0
425
+ },
426
+ children: estimate.estimatedTokens.toLocaleString()
427
+ }),
428
+ /*#__PURE__*/ _jsx("dt", {
429
+ children: "Locales:"
430
+ }),
431
+ /*#__PURE__*/ _jsx("dd", {
432
+ style: {
433
+ margin: 0
434
+ },
435
+ children: estimate.localeCount
436
+ }),
437
+ /*#__PURE__*/ _jsx("dt", {
438
+ children: "Cost (est.):"
439
+ }),
440
+ /*#__PURE__*/ _jsx("dd", {
441
+ style: {
442
+ margin: 0,
443
+ fontWeight: 500
444
+ },
445
+ children: formatCost(estimate.estimatedCostUsd)
446
+ })
447
+ ]
448
+ }) : /*#__PURE__*/ _jsx("p", {
449
+ style: {
450
+ margin: '0 0 16px',
451
+ fontSize: '13px',
452
+ color: 'var(--theme-warning-500)'
453
+ },
454
+ children: "Cost estimate unavailable."
455
+ }),
456
+ estimate && estimate.totalCharacters === 0 && /*#__PURE__*/ _jsx("p", {
457
+ style: {
458
+ margin: '0 0 16px',
459
+ fontSize: '13px',
460
+ color: 'var(--theme-warning-500)'
461
+ },
462
+ children: "No translatable content found in the source locale. Add content in the source locale before translating."
463
+ }),
464
+ /*#__PURE__*/ _jsxs("div", {
465
+ style: {
466
+ display: 'flex',
467
+ gap: '8px',
468
+ justifyContent: 'flex-end'
469
+ },
470
+ children: [
471
+ /*#__PURE__*/ _jsx("button", {
472
+ type: "button",
473
+ onClick: onClose,
474
+ style: {
475
+ padding: '8px 16px',
476
+ backgroundColor: 'transparent',
477
+ border: '1px solid var(--theme-elevation-300)',
478
+ borderRadius: '4px',
479
+ color: 'var(--theme-text)',
480
+ cursor: 'pointer'
481
+ },
482
+ children: "Cancel"
483
+ }),
484
+ /*#__PURE__*/ _jsxs("button", {
485
+ type: "button",
486
+ onClick: handleTranslate,
487
+ disabled: estimate?.totalCharacters === 0,
488
+ style: {
489
+ padding: '8px 16px',
490
+ backgroundColor: 'var(--theme-success-500)',
491
+ border: 'none',
492
+ borderRadius: '4px',
493
+ color: 'white',
494
+ cursor: estimate?.totalCharacters === 0 ? 'not-allowed' : 'pointer',
495
+ opacity: estimate?.totalCharacters === 0 ? 0.5 : 1
496
+ },
497
+ children: [
498
+ "Translate to ",
499
+ selected.size,
500
+ " locale",
501
+ selected.size !== 1 ? 's' : ''
502
+ ]
503
+ })
504
+ ]
505
+ })
506
+ ]
507
+ }),
508
+ step === 'translating' && /*#__PURE__*/ _jsx("div", {
509
+ style: {
510
+ textAlign: 'center',
511
+ padding: '16px 0'
512
+ },
513
+ children: /*#__PURE__*/ _jsx("p", {
514
+ children: "Starting translation…"
515
+ })
516
+ }),
517
+ (step === 'done' || step === 'error') && /*#__PURE__*/ _jsxs("div", {
518
+ children: [
519
+ /*#__PURE__*/ _jsx("p", {
520
+ style: {
521
+ margin: '0 0 16px'
522
+ },
523
+ children: resultMessage
524
+ }),
525
+ /*#__PURE__*/ _jsx("div", {
526
+ style: {
527
+ display: 'flex',
528
+ justifyContent: 'flex-end'
529
+ },
530
+ children: /*#__PURE__*/ _jsx("button", {
531
+ type: "button",
532
+ onClick: onClose,
533
+ style: {
534
+ padding: '8px 16px',
535
+ backgroundColor: 'var(--theme-elevation-100)',
536
+ border: 'none',
537
+ borderRadius: '4px',
538
+ color: 'var(--theme-text)',
539
+ cursor: 'pointer'
540
+ },
541
+ children: "Close"
542
+ })
543
+ })
544
+ ]
545
+ })
546
+ ]
547
+ })
548
+ }), document.body);
549
+ }
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ type TranslationProgressProps = {
3
+ jobId?: string;
4
+ collectionSlug?: string;
5
+ globalSlug?: string;
6
+ docId?: string | number;
7
+ onComplete?: () => void;
8
+ };
9
+ export declare function TranslationProgress({ jobId, collectionSlug, globalSlug, docId, onComplete, }: TranslationProgressProps): React.JSX.Element | null;
10
+ export {};