@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,879 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { readResponseError } from '../shared/fetch-error-body.js';
5
+ import { formatCost } from '../shared/format.js';
6
+ import { BulkTranslatePostEnqueueTransition } from './BulkTranslatePostEnqueueTransition.js';
7
+ import { useFocusTrap } from './useFocusTrap.js';
8
+ const OVERLAY_STYLE = {
9
+ position: 'fixed',
10
+ inset: 0,
11
+ background: 'rgba(0,0,0,0.45)',
12
+ display: 'flex',
13
+ alignItems: 'center',
14
+ justifyContent: 'center',
15
+ padding: '2rem 1rem',
16
+ zIndex: 200
17
+ };
18
+ const MODAL_STYLE = {
19
+ width: '100%',
20
+ maxWidth: '480px',
21
+ maxHeight: 'calc(100vh - 4rem)',
22
+ overflowY: 'auto',
23
+ background: 'var(--theme-elevation-0)',
24
+ border: '1px solid var(--theme-elevation-150)',
25
+ borderRadius: '6px',
26
+ boxShadow: '0 10px 30px rgba(0,0,0,0.15)',
27
+ padding: '1.5rem',
28
+ color: 'var(--theme-elevation-1000)'
29
+ };
30
+ const SECTION_LABEL = {
31
+ fontSize: '0.6875rem',
32
+ fontWeight: 600,
33
+ textTransform: 'uppercase',
34
+ letterSpacing: '0.05em',
35
+ color: 'var(--theme-elevation-500)',
36
+ margin: '0 0 0.35rem'
37
+ };
38
+ const SKELETON_STYLE = {
39
+ height: '1.25rem',
40
+ width: '6rem',
41
+ background: 'var(--theme-elevation-100)',
42
+ borderRadius: '4px',
43
+ opacity: 0.7
44
+ };
45
+ const COST_TEXT_STYLE = {
46
+ fontSize: '1.25rem',
47
+ fontWeight: 600,
48
+ color: 'var(--theme-elevation-1000)'
49
+ };
50
+ const INPUT_STYLE = {
51
+ width: '100%',
52
+ padding: '0.4rem 0.6rem',
53
+ background: 'var(--theme-elevation-0)',
54
+ border: '1px solid var(--theme-elevation-300)',
55
+ borderRadius: '4px',
56
+ color: 'var(--theme-elevation-1000)',
57
+ fontSize: '0.875rem',
58
+ fontFamily: 'inherit'
59
+ };
60
+ const ERROR_TEXT = {
61
+ color: 'var(--theme-error-500, #b91c1c)',
62
+ fontSize: '0.8125rem',
63
+ marginTop: '0.4rem'
64
+ };
65
+ const TIMEOUT_MS = 10_000;
66
+ /** Pure helper, exported for tests. */ export function deriveCostState(preflight, timedOut) {
67
+ if (!preflight) {
68
+ return timedOut ? {
69
+ kind: 'timeout'
70
+ } : {
71
+ kind: 'loading'
72
+ };
73
+ }
74
+ if (typeof preflight.estimatedCostUsd === 'number') {
75
+ return {
76
+ kind: 'ready',
77
+ value: preflight.estimatedCostUsd
78
+ };
79
+ }
80
+ return {
81
+ kind: 'unavailable'
82
+ };
83
+ }
84
+ /** Pure helper, exported for tests. */ export function isCapBlocked(preflight) {
85
+ if (!preflight) {
86
+ return false;
87
+ }
88
+ const estimate = typeof preflight.estimatedCostUsd === 'number' ? preflight.estimatedCostUsd : 0;
89
+ return preflight.dailySpend.spentUsd + estimate > preflight.dailySpend.capUsd;
90
+ }
91
+ export const BulkTranslatePreflightModal = ({ basePath, onClose })=>{
92
+ const dialogRef = useRef(null);
93
+ const [preflight, setPreflight] = useState(null);
94
+ const [loadError, setLoadError] = useState(null);
95
+ const [timedOut, setTimedOut] = useState(false);
96
+ const [retryNonce, setRetryNonce] = useState(0);
97
+ const [force, setForce] = useState(false);
98
+ const [totp, setTotp] = useState('');
99
+ const [submitError, setSubmitError] = useState(null);
100
+ const [submitting, setSubmitting] = useState(false);
101
+ const [postEnqueueVisible, setPostEnqueueVisible] = useState(false);
102
+ // Locale selection — initialized once when preflight arrives. Empty
103
+ // until the preflight resolves; defaults to "all selected" on first
104
+ // load. The operator can toggle individual locales off to narrow the
105
+ // run; the displayed cost scales linearly with selected count.
106
+ const [selectedLocales, setSelectedLocales] = useState([]);
107
+ // v1.2.7: scope selection — same pattern as locales. Editor can
108
+ // narrow the run to specific collections / globals. Defaults to "all
109
+ // selected" on first load. The enqueue endpoint already supports
110
+ // narrowed scope via `scope.collections` / `scope.globals` (see
111
+ // endpoints/translation-hub/enqueue.ts:459-463) — when the array is
112
+ // non-empty it overrides the plugin-config defaults.
113
+ const [selectedCollections, setSelectedCollections] = useState([]);
114
+ const [selectedGlobals, setSelectedGlobals] = useState([]);
115
+ useEffect(()=>{
116
+ if (preflight && selectedLocales.length === 0) {
117
+ setSelectedLocales(preflight.scope.locales);
118
+ }
119
+ if (preflight && selectedCollections.length === 0 && selectedGlobals.length === 0) {
120
+ setSelectedCollections(preflight.scope.collections);
121
+ setSelectedGlobals(preflight.scope.globals);
122
+ }
123
+ // eslint-disable-next-line react-hooks/exhaustive-deps
124
+ }, [
125
+ preflight
126
+ ]);
127
+ const toggleLocale = (code)=>{
128
+ setSelectedLocales((prev)=>prev.includes(code) ? prev.filter((c)=>c !== code) : [
129
+ ...prev,
130
+ code
131
+ ]);
132
+ };
133
+ const toggleCollection = (slug)=>{
134
+ setSelectedCollections((prev)=>prev.includes(slug) ? prev.filter((s)=>s !== slug) : [
135
+ ...prev,
136
+ slug
137
+ ]);
138
+ };
139
+ const toggleGlobal = (slug)=>{
140
+ setSelectedGlobals((prev)=>prev.includes(slug) ? prev.filter((s)=>s !== slug) : [
141
+ ...prev,
142
+ slug
143
+ ]);
144
+ };
145
+ useFocusTrap(dialogRef, {
146
+ enabled: !postEnqueueVisible,
147
+ onEscape: onClose
148
+ });
149
+ useEffect(()=>{
150
+ let cancelled = false;
151
+ setPreflight(null);
152
+ setLoadError(null);
153
+ setTimedOut(false);
154
+ const timer = setTimeout(()=>{
155
+ if (!cancelled) {
156
+ setTimedOut(true);
157
+ }
158
+ }, TIMEOUT_MS);
159
+ (async ()=>{
160
+ try {
161
+ const res = await fetch(`${basePath}/api/translation-hub/bulk-translate/preflight`, {
162
+ credentials: 'include'
163
+ });
164
+ if (cancelled) {
165
+ return;
166
+ }
167
+ if (!res.ok) {
168
+ throw new Error(await readResponseError(res));
169
+ }
170
+ const json = await res.json();
171
+ setPreflight(json);
172
+ setTimedOut(false);
173
+ } catch (e) {
174
+ if (!cancelled) {
175
+ setLoadError(e instanceof Error ? e.message : String(e));
176
+ }
177
+ }
178
+ })();
179
+ return ()=>{
180
+ cancelled = true;
181
+ clearTimeout(timer);
182
+ };
183
+ }, [
184
+ basePath,
185
+ retryNonce
186
+ ]);
187
+ // Scale the displayed cost proportionally to the locale selection.
188
+ // The preflight returns cost for the FULL locale set; selecting a
189
+ // subset multiplies by (selected / total). Approximation is exact
190
+ // for evenly-sized locales (which is the common case — same source
191
+ // doc translated N times).
192
+ const scaledPreflight = (()=>{
193
+ if (!preflight) return null;
194
+ const totalLocales = preflight.scope.locales.length;
195
+ const selectedLocaleCount = selectedLocales.length;
196
+ const localeRatio = totalLocales === 0 ? 1 : selectedLocaleCount / totalLocales;
197
+ // v1.2.7: also scale by the ratio of selected docs vs total docs.
198
+ // Sum docs across the selected collections + globals; if everything
199
+ // is selected, ratio is 1. The perCollection rows in `preflight`
200
+ // carry both kinds (collections + globals) keyed by slug, so we
201
+ // sum by slug-membership in either selected list.
202
+ const selectedSlugSet = new Set([
203
+ ...selectedCollections,
204
+ ...selectedGlobals
205
+ ]);
206
+ const totalDocs = preflight.documents.perCollection.reduce((sum, r)=>sum + (r.total ?? 0), 0);
207
+ const selectedDocs = preflight.documents.perCollection.reduce((sum, r)=>sum + (selectedSlugSet.has(r.slug) ? r.total ?? 0 : 0), 0);
208
+ const docRatio = totalDocs === 0 ? 1 : selectedDocs / totalDocs;
209
+ const ratio = localeRatio * docRatio;
210
+ if (ratio === 1) return preflight;
211
+ const scaledCost = typeof preflight.estimatedCostUsd === 'number' ? preflight.estimatedCostUsd * ratio : preflight.estimatedCostUsd;
212
+ return {
213
+ ...preflight,
214
+ estimatedCostUsd: scaledCost,
215
+ documents: {
216
+ ...preflight.documents,
217
+ total: Math.round((preflight.documents.total ?? 0) * ratio)
218
+ }
219
+ };
220
+ })();
221
+ const costState = deriveCostState(scaledPreflight, timedOut);
222
+ const capBlocked = isCapBlocked(scaledPreflight);
223
+ const requireTotp = preflight?.requireTotp === true;
224
+ const totpEnrolled = preflight?.totpEnrolled === true;
225
+ const totpMissing = requireTotp && !totpEnrolled;
226
+ const noLocalesSelected = preflight !== null && selectedLocales.length === 0;
227
+ const noScopeSelected = preflight !== null && selectedCollections.length === 0 && selectedGlobals.length === 0;
228
+ const canSubmit = !submitting && preflight !== null && costState.kind === 'ready' && !capBlocked && !totpMissing && !noLocalesSelected && !noScopeSelected && (!requireTotp || totp.length === 6);
229
+ async function onSubmit() {
230
+ if (!canSubmit) {
231
+ return;
232
+ }
233
+ setSubmitting(true);
234
+ setSubmitError(null);
235
+ try {
236
+ // Narrow the run by locale when the operator has unchecked at
237
+ // least one chip. Omitting `scope.locales` keeps the existing
238
+ // "translate to every configured target locale" behavior for
239
+ // operators who haven't touched the picker.
240
+ const localesNarrowed = preflight !== null && selectedLocales.length > 0 && selectedLocales.length < preflight.scope.locales.length;
241
+ // v1.2.7: same narrowing pattern for collections + globals.
242
+ // enqueue.ts:459-463 reads `scope.collections` / `scope.globals`
243
+ // — when non-empty arrays, the batch is restricted to that set;
244
+ // when omitted/empty, it falls back to the plugin's full config.
245
+ const collectionsNarrowed = preflight !== null && selectedCollections.length < preflight.scope.collections.length;
246
+ const globalsNarrowed = preflight !== null && selectedGlobals.length < preflight.scope.globals.length;
247
+ const scopeNarrowing = {};
248
+ if (localesNarrowed) scopeNarrowing.locales = selectedLocales;
249
+ if (collectionsNarrowed) scopeNarrowing.collections = selectedCollections;
250
+ if (globalsNarrowed) scopeNarrowing.globals = selectedGlobals;
251
+ const res = await fetch(`${basePath}/api/translation-hub/bulk-translate`, {
252
+ method: 'POST',
253
+ credentials: 'include',
254
+ headers: {
255
+ 'Content-Type': 'application/json'
256
+ },
257
+ body: JSON.stringify({
258
+ mode: force ? 'force' : 'changed',
259
+ totp: requireTotp ? totp : undefined,
260
+ scope: Object.keys(scopeNarrowing).length > 0 ? scopeNarrowing : undefined
261
+ })
262
+ });
263
+ if (!res.ok) {
264
+ // Server returns { error: string } for cap/totp/already-running.
265
+ const body = await res.json().catch(()=>null);
266
+ throw new Error(body?.error ?? `HTTP ${res.status}`);
267
+ }
268
+ setPostEnqueueVisible(true);
269
+ } catch (e) {
270
+ setSubmitError(e instanceof Error ? e.message : String(e));
271
+ } finally{
272
+ setSubmitting(false);
273
+ }
274
+ }
275
+ if (postEnqueueVisible) {
276
+ return /*#__PURE__*/ _jsx(BulkTranslatePostEnqueueTransition, {
277
+ basePath: basePath,
278
+ onResolved: onClose
279
+ });
280
+ }
281
+ return /*#__PURE__*/ _jsx("div", {
282
+ onClick: (e)=>{
283
+ // NEW-6 (v1.2.6): backdrop click dismisses. The previous block
284
+ // (`if (e.target === e.currentTarget) {}`) was unreachable —
285
+ // React's synthetic event for an overlay click reports the
286
+ // child dialog as currentTarget when the click propagates from
287
+ // inside, so the equality never held. Stop-propagation on the
288
+ // dialog wrapper guarantees dismiss only fires on backdrop hits.
289
+ onClose();
290
+ },
291
+ style: OVERLAY_STYLE,
292
+ children: /*#__PURE__*/ _jsxs("div", {
293
+ "aria-labelledby": "bulk-translate-modal-title",
294
+ "aria-modal": "true",
295
+ "data-testid": "bulk-translate-preflight-modal",
296
+ onClick: (e)=>e.stopPropagation(),
297
+ ref: dialogRef,
298
+ role: "dialog",
299
+ style: {
300
+ ...MODAL_STYLE,
301
+ position: 'relative'
302
+ },
303
+ children: [
304
+ /*#__PURE__*/ _jsx("button", {
305
+ "aria-label": "Close",
306
+ "data-testid": "bulk-translate-close",
307
+ onClick: onClose,
308
+ style: {
309
+ position: 'absolute',
310
+ top: '0.5rem',
311
+ right: '0.5rem',
312
+ width: '2rem',
313
+ height: '2rem',
314
+ display: 'flex',
315
+ alignItems: 'center',
316
+ justifyContent: 'center',
317
+ background: 'transparent',
318
+ border: 'none',
319
+ borderRadius: '4px',
320
+ color: 'var(--theme-elevation-700)',
321
+ cursor: 'pointer',
322
+ fontSize: '1.25rem',
323
+ lineHeight: 1
324
+ },
325
+ type: "button",
326
+ children: "×"
327
+ }),
328
+ /*#__PURE__*/ _jsx("h2", {
329
+ id: "bulk-translate-modal-title",
330
+ style: {
331
+ margin: '0 0 1rem',
332
+ fontSize: '1.125rem'
333
+ },
334
+ children: "Bulk Translate"
335
+ }),
336
+ /*#__PURE__*/ _jsx(Section, {
337
+ label: "Scope",
338
+ children: preflight ? /*#__PURE__*/ _jsxs("p", {
339
+ style: {
340
+ margin: 0,
341
+ fontSize: '0.875rem'
342
+ },
343
+ children: [
344
+ selectedCollections.length,
345
+ "/",
346
+ preflight.scope.collections.length,
347
+ " collections ·",
348
+ ' ',
349
+ selectedGlobals.length,
350
+ "/",
351
+ preflight.scope.globals.length,
352
+ " global",
353
+ preflight.scope.globals.length === 1 ? '' : 's',
354
+ " · ",
355
+ selectedLocales.length,
356
+ "/",
357
+ preflight.scope.locales.length,
358
+ " target locales · ~",
359
+ scaledPreflight?.documents.total ?? preflight.documents.total,
360
+ " documents"
361
+ ]
362
+ }) : /*#__PURE__*/ _jsx("div", {
363
+ style: SKELETON_STYLE
364
+ })
365
+ }),
366
+ /*#__PURE__*/ _jsx(Section, {
367
+ label: "Collections",
368
+ children: preflight ? preflight.scope.collections.length === 0 ? /*#__PURE__*/ _jsx("p", {
369
+ style: {
370
+ margin: 0,
371
+ fontSize: '0.8125rem',
372
+ fontStyle: 'italic',
373
+ color: 'var(--theme-elevation-700)'
374
+ },
375
+ children: "No collections registered for translation."
376
+ }) : /*#__PURE__*/ _jsx("div", {
377
+ "data-testid": "bulk-translate-collection-chips",
378
+ style: {
379
+ display: 'flex',
380
+ flexWrap: 'wrap',
381
+ gap: '0.35rem'
382
+ },
383
+ children: preflight.scope.collections.map((slug)=>{
384
+ const active = selectedCollections.includes(slug);
385
+ return /*#__PURE__*/ _jsx("button", {
386
+ "aria-pressed": active,
387
+ "data-testid": `bulk-translate-collection-${slug}`,
388
+ onClick: ()=>toggleCollection(slug),
389
+ style: {
390
+ padding: '0.2rem 0.55rem',
391
+ borderRadius: '999px',
392
+ fontSize: '0.75rem',
393
+ fontWeight: active ? 600 : 500,
394
+ border: `1px solid ${active ? 'var(--theme-success-300, #86efac)' : 'var(--theme-elevation-300)'}`,
395
+ background: active ? 'var(--theme-success-100, #dcfce7)' : 'var(--theme-elevation-50)',
396
+ color: active ? 'var(--theme-success-500, #16a34a)' : 'var(--theme-elevation-900)',
397
+ cursor: 'pointer'
398
+ },
399
+ type: "button",
400
+ children: slug
401
+ }, slug);
402
+ })
403
+ }) : /*#__PURE__*/ _jsx("div", {
404
+ style: SKELETON_STYLE
405
+ })
406
+ }),
407
+ /*#__PURE__*/ _jsxs(Section, {
408
+ label: "Globals",
409
+ children: [
410
+ preflight ? preflight.scope.globals.length === 0 ? /*#__PURE__*/ _jsx("p", {
411
+ style: {
412
+ margin: 0,
413
+ fontSize: '0.8125rem',
414
+ fontStyle: 'italic',
415
+ color: 'var(--theme-elevation-700)'
416
+ },
417
+ children: "No globals registered for translation."
418
+ }) : /*#__PURE__*/ _jsx("div", {
419
+ "data-testid": "bulk-translate-global-chips",
420
+ style: {
421
+ display: 'flex',
422
+ flexWrap: 'wrap',
423
+ gap: '0.35rem'
424
+ },
425
+ children: preflight.scope.globals.map((slug)=>{
426
+ const active = selectedGlobals.includes(slug);
427
+ return /*#__PURE__*/ _jsx("button", {
428
+ "aria-pressed": active,
429
+ "data-testid": `bulk-translate-global-${slug}`,
430
+ onClick: ()=>toggleGlobal(slug),
431
+ style: {
432
+ padding: '0.2rem 0.55rem',
433
+ borderRadius: '999px',
434
+ fontSize: '0.75rem',
435
+ fontWeight: active ? 600 : 500,
436
+ border: `1px solid ${active ? 'var(--theme-success-300, #86efac)' : 'var(--theme-elevation-300)'}`,
437
+ background: active ? 'var(--theme-success-100, #dcfce7)' : 'var(--theme-elevation-50)',
438
+ color: active ? 'var(--theme-success-500, #16a34a)' : 'var(--theme-elevation-900)',
439
+ cursor: 'pointer'
440
+ },
441
+ type: "button",
442
+ children: slug
443
+ }, slug);
444
+ })
445
+ }) : /*#__PURE__*/ _jsx("div", {
446
+ style: SKELETON_STYLE
447
+ }),
448
+ noScopeSelected ? /*#__PURE__*/ _jsx("p", {
449
+ role: "alert",
450
+ style: {
451
+ margin: '0.35rem 0 0',
452
+ fontSize: '0.75rem',
453
+ color: 'var(--theme-warning-500, #d97706)'
454
+ },
455
+ children: "Select at least one collection or global."
456
+ }) : null
457
+ ]
458
+ }),
459
+ /*#__PURE__*/ _jsxs(Section, {
460
+ label: "Target locales",
461
+ children: [
462
+ preflight ? /*#__PURE__*/ _jsx("div", {
463
+ "data-testid": "bulk-translate-locale-chips",
464
+ style: {
465
+ display: 'flex',
466
+ flexWrap: 'wrap',
467
+ gap: '0.35rem'
468
+ },
469
+ children: preflight.scope.locales.map((code)=>{
470
+ const active = selectedLocales.includes(code);
471
+ return /*#__PURE__*/ _jsx("button", {
472
+ "aria-pressed": active,
473
+ "data-testid": `bulk-translate-locale-${code}`,
474
+ onClick: ()=>toggleLocale(code),
475
+ style: {
476
+ padding: '0.2rem 0.55rem',
477
+ borderRadius: '999px',
478
+ fontSize: '0.75rem',
479
+ fontFamily: 'monospace',
480
+ fontWeight: active ? 600 : 500,
481
+ border: `1px solid ${active ? 'var(--theme-success-300, #86efac)' : 'var(--theme-elevation-300)'}`,
482
+ background: active ? 'var(--theme-success-100, #dcfce7)' : 'var(--theme-elevation-50)',
483
+ // NEW-20 (v1.2.6): deselected chip text was
484
+ // `elevation-500` (mid-grey ≈ 3.62:1 on the
485
+ // `elevation-50` background, failing WCAG AA
486
+ // 4.5:1). Bumped to `elevation-900` (near-black
487
+ // ≈ 13:1) so the disabled state stays readable
488
+ // without losing the active-vs-inactive visual
489
+ // contrast.
490
+ color: active ? 'var(--theme-success-500, #16a34a)' : 'var(--theme-elevation-900)',
491
+ cursor: 'pointer'
492
+ },
493
+ type: "button",
494
+ children: code
495
+ }, code);
496
+ })
497
+ }) : /*#__PURE__*/ _jsx("div", {
498
+ style: SKELETON_STYLE
499
+ }),
500
+ noLocalesSelected ? /*#__PURE__*/ _jsx("p", {
501
+ role: "alert",
502
+ style: {
503
+ margin: '0.35rem 0 0',
504
+ fontSize: '0.75rem',
505
+ color: 'var(--theme-warning-500, #d97706)'
506
+ },
507
+ children: "Select at least one locale."
508
+ }) : null
509
+ ]
510
+ }),
511
+ /*#__PURE__*/ _jsx(Section, {
512
+ label: "Breakdown",
513
+ children: preflight ? noLocalesSelected ? // NEW-7 (v1.2.6): when zero locales are selected the
514
+ // Scope says "~0 documents" but the Breakdown table
515
+ // used to keep showing full counts (e.g. posts 41/41)
516
+ // — two parts of the same modal disagreeing. Suppress
517
+ // the table here; the Scope summary already conveys
518
+ // the "select at least one locale" state.
519
+ /*#__PURE__*/ _jsx("p", {
520
+ "data-testid": "bulk-translate-breakdown-empty",
521
+ style: {
522
+ margin: 0,
523
+ fontSize: '0.8125rem',
524
+ fontStyle: 'italic',
525
+ color: 'var(--theme-elevation-700)'
526
+ },
527
+ children: "Select at least one locale to see per-collection counts."
528
+ }) : /*#__PURE__*/ _jsx("table", {
529
+ style: {
530
+ width: '100%',
531
+ borderCollapse: 'collapse',
532
+ fontSize: '0.8125rem'
533
+ },
534
+ children: /*#__PURE__*/ _jsx("tbody", {
535
+ children: preflight.documents.perCollection.filter((row)=>{
536
+ // v1.2.7: only show rows that match the active scope
537
+ // selection. The same `slug` may name either a
538
+ // collection or a global — match against both lists.
539
+ return selectedCollections.includes(row.slug) || selectedGlobals.includes(row.slug);
540
+ }).map((row)=>/*#__PURE__*/ _jsxs("tr", {
541
+ children: [
542
+ /*#__PURE__*/ _jsx("td", {
543
+ style: {
544
+ padding: '0.25rem 0',
545
+ color: 'var(--theme-elevation-800)'
546
+ },
547
+ children: row.label
548
+ }),
549
+ /*#__PURE__*/ _jsxs("td", {
550
+ style: {
551
+ padding: '0.25rem 0',
552
+ textAlign: 'right',
553
+ color: 'var(--theme-elevation-700)'
554
+ },
555
+ children: [
556
+ row.changed,
557
+ " / ",
558
+ row.total
559
+ ]
560
+ })
561
+ ]
562
+ }, row.slug))
563
+ })
564
+ }) : /*#__PURE__*/ _jsx("div", {
565
+ style: SKELETON_STYLE
566
+ })
567
+ }),
568
+ /*#__PURE__*/ _jsx(Section, {
569
+ label: "Estimated cost",
570
+ children: /*#__PURE__*/ _jsx(CostBlock, {
571
+ costState: costState,
572
+ onRetry: ()=>{
573
+ setTimedOut(false);
574
+ setRetryNonce((n)=>n + 1);
575
+ }
576
+ })
577
+ }),
578
+ /*#__PURE__*/ _jsx(Section, {
579
+ label: "Duration",
580
+ children: /*#__PURE__*/ _jsx("p", {
581
+ style: {
582
+ margin: 0,
583
+ fontSize: '0.8125rem',
584
+ color: 'var(--theme-elevation-700)'
585
+ },
586
+ children: "Typically 30-50 min. Jobs run in the background — safe to close."
587
+ })
588
+ }),
589
+ capBlocked && preflight ? /*#__PURE__*/ _jsxs("div", {
590
+ "data-testid": "bulk-translate-cap-blocked",
591
+ role: "alert",
592
+ style: {
593
+ marginTop: '0.75rem',
594
+ padding: '0.6rem 0.75rem',
595
+ background: 'var(--theme-error-100, #fee2e2)',
596
+ border: '1px solid var(--theme-error-500, #b91c1c)',
597
+ borderRadius: '4px',
598
+ fontSize: '0.8125rem',
599
+ color: 'var(--theme-error-500, #b91c1c)'
600
+ },
601
+ children: [
602
+ "Today’s translation budget is used up — ",
603
+ formatCost(preflight.dailySpend.spentUsd),
604
+ ' ',
605
+ "spent of the ",
606
+ formatCost(preflight.dailySpend.capUsd),
607
+ " daily limit. The budget resets at midnight UTC, or ask engineering to raise the limit."
608
+ ]
609
+ }) : null,
610
+ /*#__PURE__*/ _jsx("p", {
611
+ "data-testid": "bulk-translate-mode-copy",
612
+ style: {
613
+ marginTop: '0.75rem',
614
+ fontSize: '0.75rem',
615
+ color: 'var(--theme-elevation-700)'
616
+ },
617
+ children: force ? 'Force mode on — every document will be re-translated, even ones already up to date.' : "Documents already up-to-date will be skipped — flip 'Force re-translate' to include them."
618
+ }),
619
+ /*#__PURE__*/ _jsxs("label", {
620
+ style: {
621
+ display: 'flex',
622
+ alignItems: 'center',
623
+ gap: '0.5rem',
624
+ marginTop: '0.75rem',
625
+ fontSize: '0.8125rem'
626
+ },
627
+ children: [
628
+ /*#__PURE__*/ _jsx("input", {
629
+ checked: force,
630
+ "data-testid": "bulk-translate-force",
631
+ onChange: (e)=>setForce(e.target.checked),
632
+ type: "checkbox"
633
+ }),
634
+ /*#__PURE__*/ _jsx("span", {
635
+ children: "Force re-translate (includes docs already up to date)"
636
+ })
637
+ ]
638
+ }),
639
+ requireTotp ? /*#__PURE__*/ _jsxs("div", {
640
+ style: {
641
+ marginTop: '0.75rem'
642
+ },
643
+ children: [
644
+ /*#__PURE__*/ _jsx("label", {
645
+ htmlFor: "bulk-translate-totp",
646
+ style: {
647
+ ...SECTION_LABEL,
648
+ display: 'block'
649
+ },
650
+ children: "Authentication code"
651
+ }),
652
+ totpMissing ? /*#__PURE__*/ _jsx("a", {
653
+ "data-testid": "bulk-translate-totp-missing",
654
+ href: `${basePath}/admin/account`,
655
+ style: {
656
+ fontSize: '0.8125rem',
657
+ color: 'var(--theme-warning-500, #d97706)'
658
+ },
659
+ children: "Two-factor auth not set up — click to enrol"
660
+ }) : /*#__PURE__*/ _jsx("input", {
661
+ autoComplete: "one-time-code",
662
+ "data-testid": "bulk-translate-totp-input",
663
+ id: "bulk-translate-totp",
664
+ inputMode: "numeric",
665
+ maxLength: 6,
666
+ onChange: (e)=>setTotp(e.target.value.replace(/\D/g, '').slice(0, 6)),
667
+ pattern: "[0-9]{6}",
668
+ placeholder: "000000",
669
+ style: INPUT_STYLE,
670
+ type: "text",
671
+ value: totp
672
+ })
673
+ ]
674
+ }) : null,
675
+ loadError ? /*#__PURE__*/ _jsxs("p", {
676
+ style: ERROR_TEXT,
677
+ children: [
678
+ "Couldn’t open the translation settings — ",
679
+ loadError
680
+ ]
681
+ }) : null,
682
+ submitError ? /*#__PURE__*/ _jsx("p", {
683
+ style: ERROR_TEXT,
684
+ children: submitError
685
+ }) : null,
686
+ /*#__PURE__*/ _jsxs("div", {
687
+ style: {
688
+ display: 'flex',
689
+ justifyContent: 'flex-end',
690
+ gap: '0.5rem',
691
+ marginTop: '1.25rem'
692
+ },
693
+ children: [
694
+ /*#__PURE__*/ _jsx("button", {
695
+ onClick: onClose,
696
+ style: {
697
+ padding: '0.4rem 0.85rem',
698
+ background: 'transparent',
699
+ border: 'none',
700
+ borderRadius: '4px',
701
+ color: 'var(--theme-elevation-700)',
702
+ cursor: 'pointer',
703
+ fontSize: '0.8125rem'
704
+ },
705
+ type: "button",
706
+ children: "Cancel"
707
+ }),
708
+ /*#__PURE__*/ _jsx("button", {
709
+ "data-testid": "bulk-translate-start",
710
+ disabled: !canSubmit,
711
+ onClick: onSubmit,
712
+ style: {
713
+ padding: '0.4rem 0.85rem',
714
+ background: 'transparent',
715
+ border: '1px solid var(--theme-success-500, #16a34a)',
716
+ borderRadius: '4px',
717
+ color: 'var(--theme-success-500, #16a34a)',
718
+ cursor: canSubmit ? 'pointer' : 'not-allowed',
719
+ opacity: canSubmit ? 1 : 0.5,
720
+ fontSize: '0.8125rem',
721
+ fontWeight: 600
722
+ },
723
+ type: "button",
724
+ children: "Start Translation"
725
+ })
726
+ ]
727
+ })
728
+ ]
729
+ })
730
+ });
731
+ };
732
+ const Section = ({ label, children })=>/*#__PURE__*/ _jsxs("div", {
733
+ style: {
734
+ marginTop: '0.75rem'
735
+ },
736
+ children: [
737
+ /*#__PURE__*/ _jsx("p", {
738
+ style: SECTION_LABEL,
739
+ children: label
740
+ }),
741
+ children
742
+ ]
743
+ });
744
+ /**
745
+ * NEW-26 (v1.2.6): accessible tooltip for the "~$0.15" cost estimate.
746
+ *
747
+ * The previous `<span title="...">` only revealed the explanation on
748
+ * mouse hover — keyboard focus, screen-reader announcement, and mobile
749
+ * tap were all locked out. Plugin has no Radix / Popover dependency
750
+ * (intentional — keeps bundle small for an admin-only surface), so
751
+ * this is a minimal focus-visible-revealed `<span>` with `aria-describedby`
752
+ * linking the trigger to a `role="tooltip"` payload kept visually-hidden
753
+ * until the trigger receives keyboard focus or hover.
754
+ */ const TOOLTIP_ID = 'bulk-translate-cost-tooltip';
755
+ const CostEstimateInfo = ()=>{
756
+ const [open, setOpen] = useState(false);
757
+ return /*#__PURE__*/ _jsxs("span", {
758
+ style: {
759
+ position: 'relative',
760
+ display: 'inline-block'
761
+ },
762
+ children: [
763
+ /*#__PURE__*/ _jsx("button", {
764
+ "aria-describedby": open ? TOOLTIP_ID : undefined,
765
+ "aria-label": "About the cost estimate",
766
+ "data-testid": "bulk-translate-cost-info",
767
+ onBlur: ()=>setOpen(false),
768
+ onClick: ()=>setOpen((v)=>!v),
769
+ onFocus: ()=>setOpen(true),
770
+ onMouseEnter: ()=>setOpen(true),
771
+ onMouseLeave: ()=>setOpen(false),
772
+ style: {
773
+ background: 'transparent',
774
+ border: '1px solid var(--theme-elevation-300)',
775
+ borderRadius: '999px',
776
+ width: '1.1rem',
777
+ height: '1.1rem',
778
+ padding: 0,
779
+ fontSize: '0.7rem',
780
+ fontWeight: 600,
781
+ color: 'var(--theme-elevation-700)',
782
+ cursor: 'help',
783
+ lineHeight: 1
784
+ },
785
+ type: "button",
786
+ children: "?"
787
+ }),
788
+ /*#__PURE__*/ _jsx("span", {
789
+ id: TOOLTIP_ID,
790
+ role: "tooltip",
791
+ style: {
792
+ position: 'absolute',
793
+ bottom: 'calc(100% + 6px)',
794
+ right: 0,
795
+ minWidth: '14rem',
796
+ padding: '0.4rem 0.6rem',
797
+ background: 'var(--theme-elevation-1000)',
798
+ color: 'var(--theme-elevation-50)',
799
+ fontSize: '0.75rem',
800
+ lineHeight: 1.4,
801
+ borderRadius: '4px',
802
+ boxShadow: '0 4px 12px rgba(0,0,0,0.18)',
803
+ pointerEvents: 'none',
804
+ zIndex: 10,
805
+ // visibility: hidden (not display: none) so the tooltip
806
+ // text stays in the a11y tree and aria-describedby resolves.
807
+ visibility: open ? 'visible' : 'hidden',
808
+ opacity: open ? 1 : 0,
809
+ transition: 'opacity 120ms ease'
810
+ },
811
+ children: "Based on the unchanged-content diff. Actual cost may be lower if more documents are skipped."
812
+ })
813
+ ]
814
+ });
815
+ };
816
+ const CostBlock = ({ costState, onRetry })=>{
817
+ if (costState.kind === 'loading') {
818
+ return /*#__PURE__*/ _jsx("div", {
819
+ "aria-busy": "true",
820
+ "data-testid": "bulk-translate-cost-loading",
821
+ role: "status",
822
+ style: SKELETON_STYLE
823
+ });
824
+ }
825
+ if (costState.kind === 'ready') {
826
+ return /*#__PURE__*/ _jsxs("span", {
827
+ "data-testid": "bulk-translate-cost-ready",
828
+ style: {
829
+ ...COST_TEXT_STYLE,
830
+ display: 'inline-flex',
831
+ alignItems: 'center',
832
+ gap: '0.4rem'
833
+ },
834
+ children: [
835
+ "~",
836
+ formatCost(costState.value),
837
+ /*#__PURE__*/ _jsx(CostEstimateInfo, {})
838
+ ]
839
+ });
840
+ }
841
+ if (costState.kind === 'timeout') {
842
+ return /*#__PURE__*/ _jsxs("p", {
843
+ "data-testid": "bulk-translate-cost-timeout",
844
+ style: {
845
+ fontSize: '0.8125rem',
846
+ color: 'var(--theme-warning-500, #d97706)',
847
+ margin: 0
848
+ },
849
+ children: [
850
+ "Cost estimate isn’t ready yet —",
851
+ ' ',
852
+ /*#__PURE__*/ _jsx("button", {
853
+ onClick: onRetry,
854
+ style: {
855
+ background: 'transparent',
856
+ border: 'none',
857
+ color: 'inherit',
858
+ textDecoration: 'underline',
859
+ cursor: 'pointer',
860
+ padding: 0,
861
+ fontSize: 'inherit'
862
+ },
863
+ type: "button",
864
+ children: "try again"
865
+ }),
866
+ "."
867
+ ]
868
+ });
869
+ }
870
+ return /*#__PURE__*/ _jsx("p", {
871
+ "data-testid": "bulk-translate-cost-unavailable",
872
+ style: {
873
+ fontSize: '0.8125rem',
874
+ color: 'var(--theme-error-500, #b91c1c)',
875
+ margin: 0
876
+ },
877
+ children: "This translation can’t be costed right now and has been blocked as a safety measure. Contact engineering — the AI model’s pricing may need updating."
878
+ });
879
+ };