@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,1240 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useConfig } from '@payloadcms/ui';
4
+ import { useEffect, useMemo, useState } from 'react';
5
+ import { editorMessageFor } from '../../lib/error-messages.js';
6
+ import { resolveFieldBreadcrumb } from '../../lib/field-breadcrumb.js';
7
+ import { groupSoftSkipsByValue } from '../../lib/group-soft-skips.js';
8
+ import { docHref as buildDocHref, globalHref } from '../shared/docHref.js';
9
+ import { EditorError } from '../shared/EditorError.js';
10
+ import { readResponseError } from '../shared/fetch-error-body.js';
11
+ import { formatCost, formatDuration } from '../shared/format.js';
12
+ import { ModelCell } from '../shared/ModelCell.js';
13
+ import { deriveBreakdown, deriveStatus } from './UsageTable.helpers.js';
14
+ const SECTION_STYLE = {
15
+ background: 'var(--theme-elevation-50)',
16
+ border: '1px solid var(--theme-elevation-150)',
17
+ borderRadius: '6px',
18
+ padding: '1rem'
19
+ };
20
+ // NEW-14 (v1.2.6): the wrapping `<div style={{ overflowX: 'auto' }}>`
21
+ // only kicks in when the table is wider than its parent. With
22
+ // `width: 100%` alone the browser tries to collapse columns to fit,
23
+ // silently clipping `whiteSpace: nowrap` cells (cost / duration /
24
+ // when) at <= 640px viewports — the user sees missing columns and no
25
+ // scrollbar cue. Pinning `minWidth: 640px` forces the table to its
26
+ // natural size, so the overflow:auto wrapper renders a visible
27
+ // horizontal scrollbar instead of clipping silently. Matches the
28
+ // established codebase pattern (AuditPanel + BulkRunsHub tables).
29
+ const TABLE_STYLE = {
30
+ width: '100%',
31
+ minWidth: '640px',
32
+ borderCollapse: 'collapse',
33
+ fontSize: '0.875rem',
34
+ color: 'var(--theme-elevation-800)'
35
+ };
36
+ const TH_STYLE = {
37
+ textAlign: 'left',
38
+ padding: '0.5rem 0.5rem',
39
+ fontSize: '0.75rem',
40
+ fontWeight: 600,
41
+ textTransform: 'uppercase',
42
+ letterSpacing: '0.05em',
43
+ color: 'var(--theme-elevation-500)',
44
+ borderBottom: '1px solid var(--theme-elevation-150)'
45
+ };
46
+ const TD_STYLE = {
47
+ padding: '0.5rem 0.5rem',
48
+ borderTop: '1px solid var(--theme-elevation-100)',
49
+ whiteSpace: 'nowrap'
50
+ };
51
+ const _STATUS_COLORS = {
52
+ succeeded: 'var(--theme-success-500, #16a34a)',
53
+ failed: 'var(--theme-error-500, #b91c1c)'
54
+ };
55
+ function relTime(iso) {
56
+ const diff = Date.now() - new Date(iso).getTime();
57
+ const s = Math.floor(diff / 1000);
58
+ if (s < 60) {
59
+ return `${s}s ago`;
60
+ }
61
+ if (s < 3600) {
62
+ return `${Math.floor(s / 60)}m ago`;
63
+ }
64
+ if (s < 86_400) {
65
+ return `${Math.floor(s / 3600)}h ago`;
66
+ }
67
+ return `${Math.floor(s / 86_400)}d ago`;
68
+ }
69
+ function fmtNum(n) {
70
+ if (n >= 1_000_000) {
71
+ return `${(n / 1_000_000).toFixed(1)}M`;
72
+ }
73
+ if (n >= 1000) {
74
+ return `${(n / 1000).toFixed(1)}K`;
75
+ }
76
+ return n.toString();
77
+ }
78
+ function resolveCost(r, pricing) {
79
+ if (r.estimatedCostUsd != null && r.estimatedCostUsd > 0) {
80
+ return r.estimatedCostUsd;
81
+ }
82
+ // Stored 0 / null — try to compute from OpenRouter pricing.
83
+ if (!r.model) {
84
+ return null;
85
+ }
86
+ const p = pricing.get(r.model);
87
+ if (!p) {
88
+ return null;
89
+ }
90
+ return r.inputTokens * p.prompt + r.outputTokens * p.completion;
91
+ }
92
+ function docHref(basePath, r, locale) {
93
+ // Globals: /admin/globals/<slug>. The `documentId` field gets populated
94
+ // with the slug for globals (cms-plugins persists it that way) — ignore it.
95
+ if (r.kind === 'global') {
96
+ return globalHref(basePath, r.slug, locale);
97
+ }
98
+ // Collections: need a documentId to point at a row.
99
+ if (r.documentId) {
100
+ return buildDocHref(basePath, r.slug, r.documentId, locale);
101
+ }
102
+ return null;
103
+ }
104
+ // NEW-3 (v1.2.6): the status text on this table failed WCAG AA — the
105
+ // previous `-500` tokens resolve to mid-tones (`#1587BA` success,
106
+ // `#DA4B48` failed) that miss 4.5:1 against the white row background.
107
+ // Darken to `-800` for the coloured statuses and to `-elevation-900`
108
+ // for the neutral / "no-changes" / "preserved" states; that clears AA
109
+ // while preserving the semantic colour signal. Hex fallbacks updated
110
+ // in lockstep.
111
+ const DERIVED_STATUS_COLOR = {
112
+ failed: 'var(--theme-error-800, #7f1d1d)',
113
+ 'needs-review': 'var(--theme-warning-800, #78350f)',
114
+ succeeded: 'var(--theme-success-800, #14532d)',
115
+ 'no-changes': 'var(--theme-elevation-700)',
116
+ preserved: 'var(--theme-elevation-900)',
117
+ legacy: 'var(--theme-success-800, #14532d)'
118
+ };
119
+ const DERIVED_STATUS_LABEL = {
120
+ failed: 'failed',
121
+ 'needs-review': 'needs review',
122
+ succeeded: 'succeeded',
123
+ 'no-changes': 'no changes',
124
+ preserved: 'preserved',
125
+ legacy: 'succeeded'
126
+ };
127
+ function statusTooltipFor(s, b) {
128
+ switch(s){
129
+ case 'needs-review':
130
+ return `${b.softSkipped} field${b.softSkipped === 1 ? '' : 's'} weren’t translated automatically — the AI returned the original text unchanged, or the output didn’t pass quality checks. Open the row to review.`;
131
+ case 'no-changes':
132
+ return 'No translation was needed for this run — every field matched the version last translated.';
133
+ case 'preserved':
134
+ return `${b.preserved} field${b.preserved === 1 ? '' : 's'} were kept as-is because they were manually edited since the last automatic translation.`;
135
+ default:
136
+ return undefined;
137
+ }
138
+ }
139
+ /**
140
+ * Renders a list of labelled segments separated by ` · ` interpuncts.
141
+ * Each segment has a stable string key — separators borrow the next
142
+ * segment's key (e.g. `sep-f`, `sep-r`) so React's reconciliation is
143
+ * stable across renders without resorting to array indices.
144
+ */ function joinSegments(segments) {
145
+ const out = [];
146
+ for(let i = 0; i < segments.length; i++){
147
+ const seg = segments[i];
148
+ if (!seg) {
149
+ continue;
150
+ }
151
+ if (i > 0) {
152
+ out.push(/*#__PURE__*/ _jsx("span", {
153
+ style: {
154
+ color: 'var(--theme-elevation-500)'
155
+ },
156
+ children: ' · '
157
+ }, `sep-${seg.key}`));
158
+ }
159
+ out.push(seg.node);
160
+ }
161
+ return out;
162
+ }
163
+ // Each derived status's Fields-cell label is extracted into its own
164
+ // helper to keep `fieldsLabelFor` under biome's cognitive-complexity
165
+ // ceiling. They each return a FieldsLabel.
166
+ function labelForFailed(b) {
167
+ const segments = [];
168
+ if (b.translated > 0) {
169
+ segments.push({
170
+ key: 't',
171
+ node: /*#__PURE__*/ _jsxs("span", {
172
+ style: {
173
+ color: 'var(--theme-elevation-900)'
174
+ },
175
+ children: [
176
+ b.translated,
177
+ " translated"
178
+ ]
179
+ }, "t")
180
+ });
181
+ }
182
+ segments.push({
183
+ key: 'f',
184
+ node: /*#__PURE__*/ _jsx("span", {
185
+ style: {
186
+ color: 'var(--theme-error-800, #7f1d1d)'
187
+ },
188
+ children: b.failed > 0 ? `${b.failed} failed` : 'failed'
189
+ }, "f")
190
+ });
191
+ return {
192
+ primary: joinSegments(segments)
193
+ };
194
+ }
195
+ function labelForNeedsReview(b) {
196
+ const segments = [];
197
+ if (b.translated > 0) {
198
+ segments.push({
199
+ key: 't',
200
+ node: /*#__PURE__*/ _jsxs("span", {
201
+ style: {
202
+ color: 'var(--theme-elevation-900)'
203
+ },
204
+ children: [
205
+ b.translated,
206
+ " translated"
207
+ ]
208
+ }, "t")
209
+ });
210
+ }
211
+ segments.push({
212
+ key: 'r',
213
+ node: /*#__PURE__*/ _jsxs("span", {
214
+ style: {
215
+ color: 'var(--theme-warning-800, #78350f)'
216
+ },
217
+ children: [
218
+ b.softSkipped,
219
+ " need",
220
+ b.softSkipped === 1 ? 's' : '',
221
+ " review"
222
+ ]
223
+ }, "r")
224
+ });
225
+ return {
226
+ primary: joinSegments(segments),
227
+ tooltip: 'Some fields couldn’t be translated automatically. Open the row to see which fields and why.'
228
+ };
229
+ }
230
+ // Whether the row has anything worth showing in the expansion panel.
231
+ // Plain green "succeeded" with no failures and no soft-skips has
232
+ // nothing to drill into; suppressing the chevron there reduces noise.
233
+ function rowHasDetail(r, status, b) {
234
+ return (r.targetLocales?.length ?? 0) > 0 || (r.softSkippedFields?.length ?? 0) > 0 || (r.preservedFields?.length ?? 0) > 0 || b.failed > 0 || status === 'failed' || status === 'needs-review';
235
+ }
236
+ // Click handler for the row's outer <tr>. Ignores clicks on
237
+ // interactive children so the doc link / retry button work normally.
238
+ function makeRowClickHandler(hasDetail, toggleExpand, id) {
239
+ return (e)=>{
240
+ if (!hasDetail) {
241
+ return;
242
+ }
243
+ const target = e.target;
244
+ if (target.closest('a, button, input, select, textarea')) {
245
+ return;
246
+ }
247
+ toggleExpand(id);
248
+ };
249
+ }
250
+ function fieldsLabelFor(s, b) {
251
+ const totalFields = b.translated + b.hashSkipped + b.preserved + b.softSkipped + b.failed;
252
+ switch(s){
253
+ case 'failed':
254
+ return labelForFailed(b);
255
+ case 'needs-review':
256
+ return labelForNeedsReview(b);
257
+ case 'no-changes':
258
+ {
259
+ // Hash-skip wins. No LLM call happened. Don't show "0 translated"
260
+ // — the user already knows from the status. Be specific about
261
+ // what's true.
262
+ const n = b.hashSkipped + b.preserved; // both contribute to no-changes
263
+ return {
264
+ primary: /*#__PURE__*/ _jsxs("span", {
265
+ style: {
266
+ color: 'var(--theme-elevation-700)'
267
+ },
268
+ children: [
269
+ "All ",
270
+ n,
271
+ " field",
272
+ n === 1 ? '' : 's',
273
+ " unchanged since last run"
274
+ ]
275
+ }),
276
+ tooltip: b.preserved > 0 ? `${b.hashSkipped} unchanged · ${b.preserved} kept (manual edit)` : undefined
277
+ };
278
+ }
279
+ case 'preserved':
280
+ {
281
+ // Every field was manual-edit-guarded. No LLM, no overwrites.
282
+ return {
283
+ primary: /*#__PURE__*/ _jsxs("span", {
284
+ style: {
285
+ color: 'var(--theme-elevation-700)'
286
+ },
287
+ children: [
288
+ b.preserved,
289
+ " manual edit",
290
+ b.preserved === 1 ? '' : 's',
291
+ " kept"
292
+ ]
293
+ }),
294
+ tooltip: "Target locale values diverge from the plugin's last-written hash — assumed manual edits, not overwritten."
295
+ };
296
+ }
297
+ case 'succeeded':
298
+ {
299
+ // Translation actually happened. May have some hash-skipped too.
300
+ const parts = [
301
+ /*#__PURE__*/ _jsxs("span", {
302
+ style: {
303
+ color: 'var(--theme-elevation-900)'
304
+ },
305
+ children: [
306
+ b.translated,
307
+ " translated"
308
+ ]
309
+ }, "t")
310
+ ];
311
+ if (b.hashSkipped > 0 || b.preserved > 0) {
312
+ const skippedNum = b.hashSkipped + b.preserved;
313
+ parts.push(/*#__PURE__*/ _jsx("span", {
314
+ style: {
315
+ color: 'var(--theme-elevation-500)'
316
+ },
317
+ children: ' · '
318
+ }, "sep"));
319
+ parts.push(/*#__PURE__*/ _jsxs("span", {
320
+ style: {
321
+ color: 'var(--theme-elevation-500)'
322
+ },
323
+ children: [
324
+ skippedNum,
325
+ " unchanged"
326
+ ]
327
+ }, "s"));
328
+ }
329
+ let succeededTooltip;
330
+ if (b.preserved > 0) {
331
+ succeededTooltip = `${b.hashSkipped} source unchanged · ${b.preserved} kept (manual edit)`;
332
+ } else if (b.hashSkipped > 0) {
333
+ succeededTooltip = 'Source content matched the last-translated hash — no LLM call for these fields.';
334
+ }
335
+ return {
336
+ primary: parts,
337
+ tooltip: succeededTooltip
338
+ };
339
+ }
340
+ default:
341
+ // Pre-migration row — we don't know what happened, just show
342
+ // counts based on what's in succeededCount.
343
+ return {
344
+ primary: /*#__PURE__*/ _jsx("span", {
345
+ style: {
346
+ color: 'var(--theme-elevation-500)'
347
+ },
348
+ children: totalFields > 0 ? `${totalFields} field${totalFields === 1 ? '' : 's'}` : '—'
349
+ })
350
+ };
351
+ }
352
+ }
353
+ // ---------------------------------------------------------------------------
354
+ // Expanded row detail panel
355
+ // ---------------------------------------------------------------------------
356
+ const DETAIL_SECTION_STYLE = {
357
+ marginBottom: '0.75rem'
358
+ };
359
+ const DETAIL_LABEL_STYLE = {
360
+ fontSize: '0.7rem',
361
+ fontWeight: 600,
362
+ textTransform: 'uppercase',
363
+ letterSpacing: '0.05em',
364
+ color: 'var(--theme-elevation-500)',
365
+ marginBottom: '0.25rem'
366
+ };
367
+ const DETAIL_LIST_STYLE = {
368
+ margin: 0,
369
+ padding: 0,
370
+ listStyle: 'none',
371
+ fontSize: '0.8rem'
372
+ };
373
+ const RETRY_BUTTON_STYLE = {
374
+ padding: '0.4rem 0.9rem',
375
+ background: 'var(--theme-success-500, #16a34a)',
376
+ color: '#fff',
377
+ border: 'none',
378
+ borderRadius: '4px',
379
+ fontSize: '0.8rem',
380
+ cursor: 'pointer',
381
+ fontWeight: 500
382
+ };
383
+ const KEPT_AS_IS_GROUP_STYLE = {
384
+ padding: '0.6rem 0',
385
+ borderBottom: '1px solid var(--theme-elevation-100)',
386
+ fontSize: '0.8rem'
387
+ };
388
+ const KEPT_AS_IS_VALUE_STYLE = {
389
+ fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
390
+ color: 'var(--theme-elevation-1000)',
391
+ fontWeight: 600
392
+ };
393
+ const KEPT_AS_IS_META_STYLE = {
394
+ color: 'var(--theme-elevation-600)',
395
+ marginTop: '0.2rem'
396
+ };
397
+ const KeptAsIsSection = ({ basePath, row, softSkipped, surfaceFields })=>{
398
+ const groups = useMemo(()=>groupSoftSkipsByValue(softSkipped), [
399
+ softSkipped
400
+ ]);
401
+ return /*#__PURE__*/ _jsxs("section", {
402
+ style: DETAIL_SECTION_STYLE,
403
+ children: [
404
+ /*#__PURE__*/ _jsxs("div", {
405
+ style: DETAIL_LABEL_STYLE,
406
+ children: [
407
+ "Kept as English (",
408
+ groups.length,
409
+ " ",
410
+ groups.length === 1 ? 'value' : 'values',
411
+ ")"
412
+ ]
413
+ }),
414
+ /*#__PURE__*/ _jsx("p", {
415
+ style: {
416
+ margin: '0 0 0.5rem',
417
+ fontSize: '0.75rem',
418
+ color: 'var(--theme-elevation-600)'
419
+ },
420
+ children: "The AI returned these values unchanged from the source language. Usually a brand name, term, or short label that shouldn’t translate. Open the document on the relevant locale to confirm — or edit manually if a translation was actually needed."
421
+ }),
422
+ /*#__PURE__*/ _jsx("ul", {
423
+ style: {
424
+ ...DETAIL_LIST_STYLE,
425
+ padding: 0
426
+ },
427
+ children: groups.map((g, gi)=>{
428
+ const message = g.reasonCode ? editorMessageFor(g.reasonCode, {
429
+ locale: g.locales[0] ?? ''
430
+ }) : null;
431
+ const breadcrumbs = g.paths.map((p)=>resolveFieldBreadcrumb({
432
+ path: p,
433
+ fields: surfaceFields
434
+ }));
435
+ const groupKey = g.sourceValue ? `v:${g.sourceValue}:${gi}` : `p:${g.paths.join('|')}:${gi}`;
436
+ return /*#__PURE__*/ _jsxs("li", {
437
+ style: KEPT_AS_IS_GROUP_STYLE,
438
+ children: [
439
+ g.sourceValue ? /*#__PURE__*/ _jsx("div", {
440
+ children: /*#__PURE__*/ _jsxs("span", {
441
+ style: KEPT_AS_IS_VALUE_STYLE,
442
+ children: [
443
+ "“",
444
+ g.sourceValue,
445
+ "”"
446
+ ]
447
+ })
448
+ }) : /*#__PURE__*/ _jsx("div", {
449
+ style: {
450
+ color: 'var(--theme-elevation-700)'
451
+ },
452
+ children: /*#__PURE__*/ _jsx("em", {
453
+ children: "(source value not recorded — legacy run)"
454
+ })
455
+ }),
456
+ /*#__PURE__*/ _jsxs("div", {
457
+ style: KEPT_AS_IS_META_STYLE,
458
+ children: [
459
+ "Field:",
460
+ ' ',
461
+ breadcrumbs.map((crumbs, ci)=>/*#__PURE__*/ _jsxs("span", {
462
+ children: [
463
+ ci > 0 && /*#__PURE__*/ _jsx("span", {
464
+ style: {
465
+ color: 'var(--theme-elevation-400)'
466
+ },
467
+ children: ' · '
468
+ }),
469
+ /*#__PURE__*/ _jsx("span", {
470
+ style: {
471
+ color: 'var(--theme-elevation-800)'
472
+ },
473
+ children: crumbs.join(' › ')
474
+ })
475
+ ]
476
+ }, g.paths[ci]))
477
+ ]
478
+ }),
479
+ /*#__PURE__*/ _jsxs("div", {
480
+ style: KEPT_AS_IS_META_STYLE,
481
+ children: [
482
+ "Kept in ",
483
+ g.locales.length,
484
+ " ",
485
+ g.locales.length === 1 ? 'locale' : 'locales',
486
+ ":",
487
+ ' ',
488
+ g.locales.map((loc, li)=>{
489
+ const href = docHref(basePath, row, loc);
490
+ return /*#__PURE__*/ _jsxs("span", {
491
+ children: [
492
+ li > 0 && /*#__PURE__*/ _jsx("span", {
493
+ style: {
494
+ color: 'var(--theme-elevation-400)'
495
+ },
496
+ children: ', '
497
+ }),
498
+ href ? /*#__PURE__*/ _jsx("a", {
499
+ href: href,
500
+ rel: "noopener noreferrer",
501
+ style: {
502
+ color: 'var(--theme-success-700, #166534)',
503
+ textDecoration: 'underline dotted',
504
+ fontWeight: 500
505
+ },
506
+ target: "_blank",
507
+ title: `Open ${row.slug} on the ${loc} locale`,
508
+ children: loc
509
+ }) : /*#__PURE__*/ _jsx("span", {
510
+ style: {
511
+ color: 'var(--theme-elevation-800)'
512
+ },
513
+ children: loc
514
+ })
515
+ ]
516
+ }, loc);
517
+ })
518
+ ]
519
+ }),
520
+ message && /*#__PURE__*/ _jsx("div", {
521
+ style: {
522
+ ...KEPT_AS_IS_META_STYLE,
523
+ color: 'var(--theme-elevation-700)'
524
+ },
525
+ title: g.reason ?? message.body,
526
+ children: message.body
527
+ })
528
+ ]
529
+ }, groupKey);
530
+ })
531
+ })
532
+ ]
533
+ });
534
+ };
535
+ const RowDetail = ({ basePath, row, rowStatus, isRetrying, retryError, onRetry })=>{
536
+ const targetLocales = row.targetLocales ?? [];
537
+ const softSkipped = row.softSkippedFields ?? [];
538
+ const preserved = row.preservedFields ?? [];
539
+ const { config } = useConfig();
540
+ // Look up the field schema for this surface so the breadcrumb resolver
541
+ // can turn `sidebar.featured_sidebar_items.3.label` into
542
+ // `Sidebar Navigation › Featured Sidebar Items › #4 › Label`. Falls
543
+ // back to a titleized-path render when the schema can't be resolved
544
+ // (deleted collection, plugin not registered for this surface).
545
+ const surfaceFields = useMemo(()=>{
546
+ if (!config) return [];
547
+ if (row.kind === 'global') {
548
+ const g = (config.globals ?? []).find((x)=>x?.slug === row.slug);
549
+ return g?.fields ?? [];
550
+ }
551
+ const c = (config.collections ?? []).find((x)=>x?.slug === row.slug);
552
+ return c?.fields ?? [];
553
+ }, [
554
+ config,
555
+ row.kind,
556
+ row.slug
557
+ ]);
558
+ // Group soft-skipped and preserved by locale so the detail reads
559
+ // "per-locale" rather than as flat lists. Editors think in locales.
560
+ const softByLocale = new Map();
561
+ for (const s of softSkipped){
562
+ const arr = softByLocale.get(s.locale) ?? [];
563
+ arr.push(s);
564
+ softByLocale.set(s.locale, arr);
565
+ }
566
+ const preservedByLocale = new Map();
567
+ for (const p of preserved){
568
+ const arr = preservedByLocale.get(p.locale) ?? [];
569
+ arr.push(p);
570
+ preservedByLocale.set(p.locale, arr);
571
+ }
572
+ return /*#__PURE__*/ _jsxs("div", {
573
+ children: [
574
+ targetLocales.length > 0 && /*#__PURE__*/ _jsxs("section", {
575
+ style: DETAIL_SECTION_STYLE,
576
+ children: [
577
+ /*#__PURE__*/ _jsx("div", {
578
+ style: DETAIL_LABEL_STYLE,
579
+ children: "Per-locale outcome"
580
+ }),
581
+ /*#__PURE__*/ _jsx("ul", {
582
+ style: DETAIL_LIST_STYLE,
583
+ children: targetLocales.map((tl)=>{
584
+ const failedFieldPaths = tl.failedFields?.map((ff)=>ff.path) ?? [];
585
+ const localeSoftSkipped = softByLocale.get(tl.locale) ?? [];
586
+ const localePreserved = preservedByLocale.get(tl.locale) ?? [];
587
+ // Derive per-locale display status:
588
+ // 1. If this specific locale failed → 'failed'
589
+ // 2. If this locale has soft-skipped fields → 'needs-review'
590
+ // 3. Otherwise inherit the row's derived status
591
+ // Avoids the prior bug where each locale just echoed
592
+ // "succeeded" even on a no-changes / preserved / needs-review run.
593
+ let localeStatus = rowStatus;
594
+ if (tl.status === 'failed' || failedFieldPaths.length > 0) {
595
+ localeStatus = 'failed';
596
+ } else if (localeSoftSkipped.length > 0) {
597
+ localeStatus = 'needs-review';
598
+ }
599
+ return /*#__PURE__*/ _jsxs("li", {
600
+ style: {
601
+ padding: '0.5rem 0',
602
+ borderBottom: '1px solid var(--theme-elevation-100)'
603
+ },
604
+ children: [
605
+ /*#__PURE__*/ _jsxs("div", {
606
+ style: {
607
+ display: 'flex',
608
+ gap: '0.5rem',
609
+ alignItems: 'baseline'
610
+ },
611
+ children: [
612
+ (()=>{
613
+ const localeHref = docHref(basePath, row, tl.locale);
614
+ const localeStrong = /*#__PURE__*/ _jsx("strong", {
615
+ style: {
616
+ fontFamily: 'monospace',
617
+ color: 'var(--theme-elevation-1000)'
618
+ },
619
+ children: tl.locale
620
+ });
621
+ return localeHref ? /*#__PURE__*/ _jsx("a", {
622
+ href: localeHref,
623
+ rel: "noopener noreferrer",
624
+ style: {
625
+ color: 'var(--theme-success-500)',
626
+ textDecoration: 'none'
627
+ },
628
+ target: "_blank",
629
+ title: `Open ${row.slug} in ${tl.locale}`,
630
+ children: localeStrong
631
+ }) : localeStrong;
632
+ })(),
633
+ /*#__PURE__*/ _jsx("span", {
634
+ style: {
635
+ color: DERIVED_STATUS_COLOR[localeStatus],
636
+ fontSize: '0.75rem',
637
+ fontWeight: 600
638
+ },
639
+ children: DERIVED_STATUS_LABEL[localeStatus]
640
+ })
641
+ ]
642
+ }),
643
+ (tl.errorCode || tl.error) && /*#__PURE__*/ _jsx("div", {
644
+ style: {
645
+ marginTop: '0.35rem'
646
+ },
647
+ children: /*#__PURE__*/ _jsx(EditorError, {
648
+ code: tl.errorCode ?? undefined,
649
+ compact: true,
650
+ context: {
651
+ locale: tl.locale
652
+ },
653
+ details: tl.error ?? undefined,
654
+ hideAction: true
655
+ })
656
+ }),
657
+ failedFieldPaths.length > 0 && /*#__PURE__*/ _jsxs("div", {
658
+ style: {
659
+ marginTop: '0.25rem',
660
+ fontSize: '0.75rem'
661
+ },
662
+ children: [
663
+ /*#__PURE__*/ _jsx("span", {
664
+ style: {
665
+ color: 'var(--theme-elevation-500)'
666
+ },
667
+ children: "Failed fields: "
668
+ }),
669
+ failedFieldPaths.map((p, i)=>/*#__PURE__*/ _jsxs("code", {
670
+ style: {
671
+ color: 'var(--theme-elevation-800)',
672
+ marginRight: '0.25rem'
673
+ },
674
+ children: [
675
+ p,
676
+ i < failedFieldPaths.length - 1 ? ',' : ''
677
+ ]
678
+ }, p))
679
+ ]
680
+ }),
681
+ localeSoftSkipped.length > 0 && /*#__PURE__*/ _jsxs("div", {
682
+ style: {
683
+ marginTop: '0.25rem',
684
+ fontSize: '0.75rem',
685
+ color: 'var(--theme-elevation-600)'
686
+ },
687
+ children: [
688
+ localeSoftSkipped.length,
689
+ " field",
690
+ localeSoftSkipped.length === 1 ? '' : 's',
691
+ " the AI didn’t translate — see “Kept as English” below."
692
+ ]
693
+ }),
694
+ localePreserved.length > 0 && /*#__PURE__*/ _jsxs("div", {
695
+ style: {
696
+ marginTop: '0.25rem',
697
+ fontSize: '0.75rem'
698
+ },
699
+ children: [
700
+ /*#__PURE__*/ _jsx("span", {
701
+ style: {
702
+ color: 'var(--theme-elevation-700)'
703
+ },
704
+ children: "Preserved (manual edits):"
705
+ }),
706
+ ' ',
707
+ localePreserved.map((p, i)=>/*#__PURE__*/ _jsxs("code", {
708
+ style: {
709
+ color: 'var(--theme-elevation-800)',
710
+ marginRight: '0.25rem'
711
+ },
712
+ children: [
713
+ p.path,
714
+ i < localePreserved.length - 1 ? ',' : ''
715
+ ]
716
+ }, p.path))
717
+ ]
718
+ })
719
+ ]
720
+ }, tl.locale);
721
+ })
722
+ })
723
+ ]
724
+ }),
725
+ softSkipped.length > 0 && /*#__PURE__*/ _jsx(KeptAsIsSection, {
726
+ basePath: basePath,
727
+ row: row,
728
+ softSkipped: softSkipped,
729
+ surfaceFields: surfaceFields
730
+ }),
731
+ /*#__PURE__*/ _jsxs("div", {
732
+ style: {
733
+ display: 'flex',
734
+ gap: '0.75rem',
735
+ alignItems: 'center',
736
+ marginTop: '0.5rem'
737
+ },
738
+ children: [
739
+ /*#__PURE__*/ _jsx("button", {
740
+ disabled: isRetrying,
741
+ onClick: (e)=>{
742
+ e.stopPropagation();
743
+ onRetry();
744
+ },
745
+ style: {
746
+ ...RETRY_BUTTON_STYLE,
747
+ opacity: isRetrying ? 0.6 : 1,
748
+ cursor: isRetrying ? 'wait' : 'pointer'
749
+ },
750
+ type: "button",
751
+ children: isRetrying ? 'Retrying…' : 'Retry this translation'
752
+ }),
753
+ retryError && /*#__PURE__*/ _jsx("span", {
754
+ style: {
755
+ color: 'var(--theme-error-800, #7f1d1d)',
756
+ fontSize: '0.75rem'
757
+ },
758
+ children: retryError
759
+ })
760
+ ]
761
+ })
762
+ ]
763
+ });
764
+ };
765
+ const UsageRowItem = ({ row: r, basePath, pricing, isExpanded, isRetrying, retryError, onToggleExpand, onRetry })=>{
766
+ const href = docHref(basePath, r);
767
+ const breakdown = deriveBreakdown(r);
768
+ const derivedStatus = deriveStatus(r, breakdown);
769
+ const fieldsLabel = fieldsLabelFor(derivedStatus, breakdown);
770
+ const statusLabel = DERIVED_STATUS_LABEL[derivedStatus];
771
+ const statusColor = DERIVED_STATUS_COLOR[derivedStatus];
772
+ const statusTitle = statusTooltipFor(derivedStatus, breakdown);
773
+ const hasDetail = rowHasDetail(r, derivedStatus, breakdown);
774
+ const onRowClick = makeRowClickHandler(hasDetail, onToggleExpand, r.id);
775
+ const docLabel = /*#__PURE__*/ _jsxs(_Fragment, {
776
+ children: [
777
+ /*#__PURE__*/ _jsx("code", {
778
+ style: {
779
+ fontSize: '0.75rem'
780
+ },
781
+ children: r.slug
782
+ }),
783
+ r.documentId && r.kind === 'collection' && /*#__PURE__*/ _jsxs("span", {
784
+ style: {
785
+ color: 'var(--theme-elevation-500)'
786
+ },
787
+ children: [
788
+ " #",
789
+ r.documentId
790
+ ]
791
+ })
792
+ ]
793
+ });
794
+ return /*#__PURE__*/ _jsxs(_Fragment, {
795
+ children: [
796
+ /*#__PURE__*/ _jsxs("tr", {
797
+ onClick: onRowClick,
798
+ style: hasDetail ? {
799
+ cursor: 'pointer',
800
+ background: isExpanded ? 'var(--theme-elevation-100)' : undefined
801
+ } : undefined,
802
+ children: [
803
+ /*#__PURE__*/ _jsxs("td", {
804
+ style: TD_STYLE,
805
+ children: [
806
+ hasDetail && /*#__PURE__*/ _jsx("span", {
807
+ "aria-hidden": "true",
808
+ style: {
809
+ display: 'inline-block',
810
+ width: '1rem',
811
+ color: 'var(--theme-elevation-500)',
812
+ fontSize: '0.65rem',
813
+ transform: isExpanded ? 'rotate(90deg)' : 'rotate(0deg)',
814
+ transition: 'transform 80ms ease',
815
+ marginRight: '0.25rem'
816
+ },
817
+ children: "▶"
818
+ }),
819
+ href ? /*#__PURE__*/ _jsx("a", {
820
+ href: href,
821
+ style: {
822
+ color: 'var(--theme-success-500)',
823
+ textDecoration: 'none'
824
+ },
825
+ title: `Open ${r.slug}${r.kind === 'collection' && r.documentId ? ` #${r.documentId}` : ''}`,
826
+ children: docLabel
827
+ }) : docLabel
828
+ ]
829
+ }),
830
+ /*#__PURE__*/ _jsx("td", {
831
+ style: {
832
+ ...TD_STYLE,
833
+ color: 'var(--theme-elevation-700)',
834
+ fontFamily: 'monospace',
835
+ fontSize: '0.75rem'
836
+ },
837
+ children: /*#__PURE__*/ _jsx(ModelCell, {
838
+ model: r.model
839
+ })
840
+ }),
841
+ /*#__PURE__*/ _jsx("td", {
842
+ style: {
843
+ ...TD_STYLE,
844
+ color: statusColor
845
+ },
846
+ title: statusTitle,
847
+ children: statusLabel
848
+ }),
849
+ /*#__PURE__*/ _jsx("td", {
850
+ style: TD_STYLE,
851
+ children: /*#__PURE__*/ _jsx(UsageRowFieldsCell, {
852
+ breakdown: breakdown,
853
+ label: fieldsLabel,
854
+ row: r
855
+ })
856
+ }),
857
+ /*#__PURE__*/ _jsx("td", {
858
+ style: TD_STYLE,
859
+ children: formatCost(resolveCost(r, pricing))
860
+ }),
861
+ /*#__PURE__*/ _jsx("td", {
862
+ style: TD_STYLE,
863
+ children: formatDuration(r.durationMs)
864
+ }),
865
+ /*#__PURE__*/ _jsx("td", {
866
+ style: {
867
+ ...TD_STYLE,
868
+ color: 'var(--theme-elevation-500)'
869
+ },
870
+ children: relTime(r.createdAt)
871
+ })
872
+ ]
873
+ }),
874
+ isExpanded && hasDetail && /*#__PURE__*/ _jsx("tr", {
875
+ children: /*#__PURE__*/ _jsx("td", {
876
+ colSpan: 7,
877
+ style: {
878
+ ...TD_STYLE,
879
+ background: 'var(--theme-elevation-50)',
880
+ whiteSpace: 'normal',
881
+ padding: '1rem 1.25rem',
882
+ borderTop: 'none'
883
+ },
884
+ children: /*#__PURE__*/ _jsx(RowDetail, {
885
+ basePath: basePath,
886
+ isRetrying: isRetrying,
887
+ onRetry: onRetry,
888
+ retryError: retryError,
889
+ row: r,
890
+ rowStatus: derivedStatus
891
+ })
892
+ })
893
+ })
894
+ ]
895
+ });
896
+ };
897
+ // Fields-cell render — extracted so UsageRowItem's branching stays
898
+ // minimal. Handles both the breakdown case (current) and the legacy
899
+ // pre-1.1.14 fallback that only had token counts.
900
+ const UsageRowFieldsCell = ({ row, breakdown, label })=>{
901
+ if (!breakdown.hasBreakdown) {
902
+ return /*#__PURE__*/ _jsxs("span", {
903
+ style: {
904
+ color: 'var(--theme-elevation-500)'
905
+ },
906
+ children: [
907
+ fmtNum(row.inputTokens),
908
+ " / ",
909
+ fmtNum(row.outputTokens),
910
+ " tokens"
911
+ ]
912
+ });
913
+ }
914
+ return /*#__PURE__*/ _jsxs(_Fragment, {
915
+ children: [
916
+ /*#__PURE__*/ _jsx("div", {
917
+ style: {
918
+ cursor: label.tooltip ? 'help' : undefined,
919
+ textDecoration: label.tooltip ? 'underline dotted var(--theme-elevation-300)' : undefined
920
+ },
921
+ title: label.tooltip || undefined,
922
+ children: label.primary
923
+ }),
924
+ (row.inputTokens > 0 || row.outputTokens > 0) && /*#__PURE__*/ _jsxs("div", {
925
+ style: {
926
+ color: 'var(--theme-elevation-400)',
927
+ fontSize: '0.7rem',
928
+ marginTop: '0.1rem'
929
+ },
930
+ children: [
931
+ fmtNum(row.inputTokens),
932
+ " in · ",
933
+ fmtNum(row.outputTokens),
934
+ " out"
935
+ ]
936
+ })
937
+ ]
938
+ });
939
+ };
940
+ // ---------------------------------------------------------------------------
941
+ // Main table component
942
+ // ---------------------------------------------------------------------------
943
+ export const UsageTable = ({ basePath })=>{
944
+ const [rows, setRows] = useState(null);
945
+ const [error, setError] = useState(null);
946
+ const [filter, setFilter] = useState('all');
947
+ // OpenRouter rows store `estimated_cost_usd = 0` because the smart
948
+ // provider doesn't compute cost (pricing varies per sub-model). We
949
+ // compute it client-side using the catalog's per-token prices for
950
+ // each model id we see in the rows.
951
+ const [pricing, setPricing] = useState(new Map());
952
+ // Per-row expansion state — set of row ids currently expanded.
953
+ // We don't auto-collapse on data refresh; users sometimes leave a row
954
+ // open while inspecting and the polling refresh shouldn't clobber it.
955
+ const [expandedIds, setExpandedIds] = useState(new Set());
956
+ const [retrying, setRetrying] = useState(new Set());
957
+ const [retryError, setRetryError] = useState({});
958
+ function toggleExpand(id) {
959
+ setExpandedIds((prev)=>{
960
+ const next = new Set(prev);
961
+ if (next.has(id)) {
962
+ next.delete(id);
963
+ } else {
964
+ next.add(id);
965
+ }
966
+ return next;
967
+ });
968
+ }
969
+ /**
970
+ * Trigger a retry for a specific row's doc + locales. For collections
971
+ * we POST to `/api/{slug}/ai-translate` with `{ id, sourceLocale, targetLocales }`;
972
+ * for globals to `/api/globals/{slug}/ai-translate`. On success, refresh
973
+ * the usage table so the new row appears at the top.
974
+ *
975
+ * `locales` is optional — when omitted, retries ALL locales that
976
+ * appeared in the original row (so the failed-row default is "retry
977
+ * everything that ran last time"). When the editor selects a subset
978
+ * via the UI, only those locales are retried.
979
+ */ async function retryRow(r, locales) {
980
+ const id = r.id;
981
+ setRetryError((prev)=>{
982
+ const { [id]: _ignored, ...rest } = prev;
983
+ return rest;
984
+ });
985
+ setRetrying((prev)=>new Set(prev).add(id));
986
+ try {
987
+ const targetLocales = locales && locales.length > 0 ? locales : (r.targetLocales ?? []).map((tl)=>tl.locale);
988
+ if (targetLocales.length === 0) {
989
+ throw new Error('No target locales recorded on this row.');
990
+ }
991
+ const endpoint = r.kind === 'collection' ? `${basePath}/api/${r.slug}/ai-translate` : `${basePath}/api/globals/${r.slug}/ai-translate`;
992
+ const body = r.kind === 'collection' ? {
993
+ id: r.documentId,
994
+ sourceLocale: r.sourceLocale,
995
+ targetLocales
996
+ } : {
997
+ sourceLocale: r.sourceLocale,
998
+ targetLocales
999
+ };
1000
+ const res = await fetch(endpoint, {
1001
+ method: 'POST',
1002
+ credentials: 'include',
1003
+ headers: {
1004
+ 'content-type': 'application/json'
1005
+ },
1006
+ body: JSON.stringify(body)
1007
+ });
1008
+ if (!res.ok) {
1009
+ const text = await res.text();
1010
+ throw new Error(`HTTP ${res.status}: ${text.slice(0, 200)}`);
1011
+ }
1012
+ // Refresh the table so the new row lands at the top.
1013
+ const usageRes = await fetch(`${basePath}/api/translation-usage?limit=20&sort=-createdAt&depth=0`, {
1014
+ credentials: 'include'
1015
+ });
1016
+ if (usageRes.ok) {
1017
+ const d = await usageRes.json();
1018
+ setRows(d.docs ?? []);
1019
+ }
1020
+ } catch (e) {
1021
+ setRetryError((prev)=>({
1022
+ ...prev,
1023
+ [id]: e instanceof Error ? e.message : String(e)
1024
+ }));
1025
+ } finally{
1026
+ setRetrying((prev)=>{
1027
+ const next = new Set(prev);
1028
+ next.delete(id);
1029
+ return next;
1030
+ });
1031
+ }
1032
+ }
1033
+ useEffect(()=>{
1034
+ let cancelled = false;
1035
+ fetch(`${basePath}/api/translation-usage?limit=20&sort=-createdAt&depth=0`, {
1036
+ credentials: 'include'
1037
+ }).then(async (r)=>{
1038
+ if (!r.ok) {
1039
+ throw new Error(await readResponseError(r));
1040
+ }
1041
+ return await r.json();
1042
+ }).then((d)=>{
1043
+ if (cancelled) {
1044
+ return;
1045
+ }
1046
+ setRows(d.docs ?? []);
1047
+ }).catch((e)=>{
1048
+ if (cancelled) {
1049
+ return;
1050
+ }
1051
+ setError(e instanceof Error ? e.message : String(e));
1052
+ });
1053
+ return ()=>{
1054
+ cancelled = true;
1055
+ };
1056
+ }, [
1057
+ basePath
1058
+ ]);
1059
+ // Fetch model catalog for pricing lookup. Cheap (server-side cached).
1060
+ useEffect(()=>{
1061
+ let cancelled = false;
1062
+ fetch(`${basePath}/api/openrouter/models`, {
1063
+ credentials: 'include'
1064
+ }).then((r)=>r.ok ? r.json() : Promise.reject(new Error(`HTTP ${r.status}`))).then((d)=>{
1065
+ if (cancelled) {
1066
+ return;
1067
+ }
1068
+ const map = new Map();
1069
+ for (const m of d.models ?? []){
1070
+ if (m.pricing) {
1071
+ map.set(m.id, {
1072
+ prompt: Number.parseFloat(m.pricing.prompt),
1073
+ completion: Number.parseFloat(m.pricing.completion)
1074
+ });
1075
+ }
1076
+ }
1077
+ setPricing(map);
1078
+ }).catch(()=>{
1079
+ // Non-fatal — Cost column just falls back to stored value.
1080
+ });
1081
+ return ()=>{
1082
+ cancelled = true;
1083
+ };
1084
+ }, [
1085
+ basePath
1086
+ ]);
1087
+ const filtered = useMemo(()=>(rows ?? []).filter((r)=>filter === 'all' || r.status === filter), [
1088
+ rows,
1089
+ filter
1090
+ ]);
1091
+ return /*#__PURE__*/ _jsxs("section", {
1092
+ style: SECTION_STYLE,
1093
+ children: [
1094
+ /*#__PURE__*/ _jsxs("header", {
1095
+ style: {
1096
+ display: 'flex',
1097
+ alignItems: 'center',
1098
+ justifyContent: 'space-between',
1099
+ marginBottom: '0.75rem',
1100
+ gap: '1rem'
1101
+ },
1102
+ children: [
1103
+ /*#__PURE__*/ _jsx("h2", {
1104
+ style: {
1105
+ margin: 0,
1106
+ fontSize: '1rem',
1107
+ color: 'var(--theme-elevation-1000)'
1108
+ },
1109
+ children: "Recent translations"
1110
+ }),
1111
+ /*#__PURE__*/ _jsxs("div", {
1112
+ style: {
1113
+ display: 'flex',
1114
+ alignItems: 'center',
1115
+ gap: '0.75rem'
1116
+ },
1117
+ children: [
1118
+ /*#__PURE__*/ _jsxs("select", {
1119
+ onChange: (e)=>setFilter(e.target.value),
1120
+ style: {
1121
+ padding: '0.25rem 0.5rem',
1122
+ fontSize: '0.875rem',
1123
+ background: 'var(--theme-elevation-50)',
1124
+ border: '1px solid var(--theme-elevation-150)',
1125
+ borderRadius: '4px',
1126
+ color: 'var(--theme-elevation-1000)'
1127
+ },
1128
+ value: filter,
1129
+ children: [
1130
+ /*#__PURE__*/ _jsx("option", {
1131
+ value: "all",
1132
+ children: "All statuses"
1133
+ }),
1134
+ /*#__PURE__*/ _jsx("option", {
1135
+ value: "succeeded",
1136
+ children: "Succeeded only"
1137
+ }),
1138
+ /*#__PURE__*/ _jsx("option", {
1139
+ value: "failed",
1140
+ children: "Failed only"
1141
+ })
1142
+ ]
1143
+ }),
1144
+ /*#__PURE__*/ _jsx("a", {
1145
+ href: filter === 'all' ? `${basePath}/admin/collections/translation-usage` : `${basePath}/admin/collections/translation-usage?where[status][equals]=${filter}`,
1146
+ style: {
1147
+ fontSize: '0.875rem',
1148
+ color: 'var(--theme-success-500)',
1149
+ textDecoration: 'none',
1150
+ whiteSpace: 'nowrap'
1151
+ },
1152
+ children: "View all →"
1153
+ })
1154
+ ]
1155
+ })
1156
+ ]
1157
+ }),
1158
+ error && /*#__PURE__*/ _jsx("p", {
1159
+ style: {
1160
+ color: 'var(--theme-error-800, #7f1d1d)',
1161
+ fontSize: '0.875rem',
1162
+ margin: '0 0 0.5rem'
1163
+ },
1164
+ children: error
1165
+ }),
1166
+ rows === null && /*#__PURE__*/ _jsx("p", {
1167
+ style: {
1168
+ margin: 0,
1169
+ color: 'var(--theme-elevation-500)',
1170
+ fontSize: '0.875rem'
1171
+ },
1172
+ children: "Loading…"
1173
+ }),
1174
+ rows !== null && filtered.length === 0 && /*#__PURE__*/ _jsx("p", {
1175
+ style: {
1176
+ margin: 0,
1177
+ color: 'var(--theme-elevation-500)',
1178
+ fontSize: '0.875rem'
1179
+ },
1180
+ children: rows.length === 0 ? 'No translations have run yet. Trigger one from any document.' : `No ${filter} translations in the last 20 runs.`
1181
+ }),
1182
+ rows !== null && filtered.length > 0 && /*#__PURE__*/ _jsx("div", {
1183
+ style: {
1184
+ overflowX: 'auto'
1185
+ },
1186
+ children: /*#__PURE__*/ _jsxs("table", {
1187
+ style: TABLE_STYLE,
1188
+ children: [
1189
+ /*#__PURE__*/ _jsx("thead", {
1190
+ children: /*#__PURE__*/ _jsxs("tr", {
1191
+ children: [
1192
+ /*#__PURE__*/ _jsx("th", {
1193
+ style: TH_STYLE,
1194
+ children: "Collection / doc"
1195
+ }),
1196
+ /*#__PURE__*/ _jsx("th", {
1197
+ style: TH_STYLE,
1198
+ children: "Model"
1199
+ }),
1200
+ /*#__PURE__*/ _jsx("th", {
1201
+ style: TH_STYLE,
1202
+ children: "Status"
1203
+ }),
1204
+ /*#__PURE__*/ _jsx("th", {
1205
+ style: TH_STYLE,
1206
+ children: "Fields"
1207
+ }),
1208
+ /*#__PURE__*/ _jsx("th", {
1209
+ style: TH_STYLE,
1210
+ children: "Cost"
1211
+ }),
1212
+ /*#__PURE__*/ _jsx("th", {
1213
+ style: TH_STYLE,
1214
+ children: "Duration"
1215
+ }),
1216
+ /*#__PURE__*/ _jsx("th", {
1217
+ style: TH_STYLE,
1218
+ children: "When"
1219
+ })
1220
+ ]
1221
+ })
1222
+ }),
1223
+ /*#__PURE__*/ _jsx("tbody", {
1224
+ children: filtered.map((r)=>/*#__PURE__*/ _jsx(UsageRowItem, {
1225
+ basePath: basePath,
1226
+ isExpanded: expandedIds.has(r.id),
1227
+ isRetrying: retrying.has(r.id),
1228
+ onRetry: (locales)=>retryRow(r, locales),
1229
+ onToggleExpand: toggleExpand,
1230
+ pricing: pricing,
1231
+ retryError: retryError[r.id],
1232
+ row: r
1233
+ }, r.id))
1234
+ })
1235
+ ]
1236
+ })
1237
+ })
1238
+ ]
1239
+ });
1240
+ };