@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,481 @@
1
+ import { DEFAULT_USAGE_COLLECTION_SLUG } from '../../defaults.js';
2
+ import { errorResponse, isAdminUser, unauthorizedResponse } from './_helpers.js';
3
+ /**
4
+ * `GET /api/translation-hub/usage-summary`
5
+ *
6
+ * Server-side aggregation of the `translation-usage` collection for
7
+ * Hub Overview + Audit & Cost KPIs. Replaces the prior client-side
8
+ * pattern where both panels fetched `?limit=1000` and reduced in JS;
9
+ * past 1000 rows the totals silently truncated and the network panel
10
+ * showed two parallel megabyte responses per Hub mount. See NEW-15.
11
+ *
12
+ * Query params:
13
+ * - `range` — one of `7d` | `30d` | `90d` | `all` | `custom`.
14
+ * Defaults to `7d`.
15
+ * - `from` — ISO timestamp; required when `range=custom`.
16
+ * - `to` — ISO timestamp; optional ceiling when `range=custom`.
17
+ *
18
+ * Response shape (`UsageSummaryResponse`):
19
+ * {
20
+ * range: { kind, from?, to?, since?, until? },
21
+ * totals: {
22
+ * runs, succeeded, failed, inputTokens, outputTokens, costUsd,
23
+ * totalMatching, truncated
24
+ * },
25
+ * byCollection: [{ slug, kind, runs, tokens, costUsd }],
26
+ * byModel: [{ model, runs, tokens, costUsd }],
27
+ * byLocale: [{ locale, runs, failed }],
28
+ * samples: [UsageRow] // 20 most recent rows for the "Recent
29
+ * // translations" / "Recent failures" tables
30
+ * }
31
+ *
32
+ * Implementation note: when `payload.db.drizzle` is available (Postgres
33
+ * adapter), aggregates run as `SELECT … SUM(...) GROUP BY ...` against
34
+ * the underlying tables. When it isn't (mock / SQLite / other adapter
35
+ * stacks in tests), we fall back to a paginated `payload.find()` walk
36
+ * that still avoids loading the entire result set into memory at once.
37
+ * Both paths return the same shape — consumers cannot tell the
38
+ * difference. The drizzle path is what makes >1000-row windows
39
+ * correct, which is the NEW-15 contract.
40
+ *
41
+ * Auth: admin-only, identical posture to every other translation-hub
42
+ * endpoint (see `active.ts`, `list.ts`). Editors aren't allowed to
43
+ * read cost data.
44
+ */ const PRESET_MS = {
45
+ '7d': 7 * 24 * 60 * 60 * 1000,
46
+ '30d': 30 * 24 * 60 * 60 * 1000,
47
+ '90d': 90 * 24 * 60 * 60 * 1000
48
+ };
49
+ const SAMPLES_LIMIT = 20;
50
+ /**
51
+ * Parse the `range`/`from`/`to` query params. Exported for testing —
52
+ * mirrors the StatusStrip/AuditPanel resolution rules so the UI and
53
+ * the endpoint agree on what "Last 7 days" means.
54
+ */ export function parseRangeParams(params) {
55
+ const rangeRaw = (params.get('range') ?? '7d').toLowerCase();
56
+ if (rangeRaw === 'all') {
57
+ return {
58
+ range: {
59
+ kind: 'all'
60
+ },
61
+ since: null,
62
+ until: null
63
+ };
64
+ }
65
+ if (rangeRaw === 'custom') {
66
+ const fromRaw = params.get('from');
67
+ const toRaw = params.get('to');
68
+ let since = null;
69
+ let until = null;
70
+ if (fromRaw) {
71
+ const d = new Date(fromRaw);
72
+ if (!Number.isNaN(d.getTime())) {
73
+ since = d.toISOString();
74
+ }
75
+ }
76
+ if (toRaw) {
77
+ const d = new Date(toRaw);
78
+ if (!Number.isNaN(d.getTime())) {
79
+ // Include end-of-day so a "to 2026-01-15" filter matches rows
80
+ // logged that day, not just rows before midnight UTC. Mirrors
81
+ // the StatusStrip + AuditPanel client behaviour.
82
+ d.setUTCHours(23, 59, 59, 999);
83
+ until = d.toISOString();
84
+ }
85
+ }
86
+ return {
87
+ range: {
88
+ kind: 'custom',
89
+ ...since ? {
90
+ from: since
91
+ } : {},
92
+ ...until ? {
93
+ to: until
94
+ } : {}
95
+ },
96
+ since,
97
+ until
98
+ };
99
+ }
100
+ if (rangeRaw === '7d' || rangeRaw === '30d' || rangeRaw === '90d') {
101
+ const since = new Date(Date.now() - PRESET_MS[rangeRaw]).toISOString();
102
+ return {
103
+ range: {
104
+ kind: rangeRaw,
105
+ since
106
+ },
107
+ since,
108
+ until: null
109
+ };
110
+ }
111
+ // Unknown preset — fall back to 7d so a client typo doesn't surface a
112
+ // 400. Same shape every other consumer expects.
113
+ const since = new Date(Date.now() - PRESET_MS['7d']).toISOString();
114
+ return {
115
+ range: {
116
+ kind: '7d',
117
+ since
118
+ },
119
+ since,
120
+ until: null
121
+ };
122
+ }
123
+ export const getTranslationHubUsageSummaryHandler = (options = {})=>async (req)=>{
124
+ if (!req.user) {
125
+ return unauthorizedResponse(req);
126
+ }
127
+ if (!isAdminUser(req.user)) {
128
+ return errorResponse('forbidden', 'Admin role required to read translation usage.', 403);
129
+ }
130
+ const usageSlug = options.usageCollectionSlug ?? DEFAULT_USAGE_COLLECTION_SLUG;
131
+ const url = new URL(req.url ?? 'http://localhost');
132
+ const { range, since, until } = parseRangeParams(url.searchParams);
133
+ try {
134
+ const drizzle = getDrizzleHandle(req.payload);
135
+ const aggregates = drizzle ? await aggregateWithDrizzle(drizzle, usageSlug, since, until) : await aggregateWithPayload(req.payload, usageSlug, since, until);
136
+ const samples = await fetchRecentSamples(req.payload, usageSlug, since, until);
137
+ const body = {
138
+ range,
139
+ totals: aggregates.totals,
140
+ byCollection: aggregates.byCollection,
141
+ byModel: aggregates.byModel,
142
+ byLocale: aggregates.byLocale,
143
+ samples
144
+ };
145
+ return Response.json(body);
146
+ } catch (err) {
147
+ req.payload.logger?.error?.(`[ai-translate] usage-summary: aggregation failed — ${err instanceof Error ? err.message : String(err)}`);
148
+ return errorResponse('aggregation_failed', 'Could not compute usage summary. Try again or narrow the time range.', 500);
149
+ }
150
+ };
151
+ function getDrizzleHandle(payload) {
152
+ const db = payload.db.drizzle;
153
+ if (!db || typeof db.execute !== 'function') return null;
154
+ return db;
155
+ }
156
+ /**
157
+ * Slug → snake_case table identifier. Identical rule to the one in
158
+ * `lib/bulk-translate-migrations.ts`; duplicated locally to keep this
159
+ * endpoint independent of bulk-translate internals.
160
+ */ function slugToTable(slug) {
161
+ if (!/^[a-z][a-z0-9_-]{0,62}$/.test(slug)) {
162
+ throw new Error(`[ai-translate] usage-summary: invalid usage collection slug "${slug}" (must match /^[a-z][a-z0-9_-]{0,62}$/)`);
163
+ }
164
+ return slug.replace(/-/g, '_');
165
+ }
166
+ async function loadSqlTag() {
167
+ try {
168
+ // String-indirected so the bundler/tsc don't try to resolve
169
+ // `drizzle-orm` at build time — consumers without the dep still
170
+ // ship the plugin, the fallback aggregation path catches them.
171
+ const modulePath = 'drizzle-orm';
172
+ const mod = await import(modulePath);
173
+ return mod.sql ?? null;
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+ async function aggregateWithDrizzle(drizzle, usageSlug, since, until) {
179
+ const sql = await loadSqlTag();
180
+ if (!sql) {
181
+ throw new Error('drizzle-orm sql tag unavailable');
182
+ }
183
+ const usageTable = slugToTable(usageSlug);
184
+ const targetLocalesTable = `${usageTable}_target_locales`;
185
+ // We splice the table identifiers via sql.raw — they come from the
186
+ // slug allowlist above (`/^[a-z][a-z0-9_-]{0,62}$/`), not from user
187
+ // input, so there's no injection surface.
188
+ const usageRef = sql.raw(`"${usageTable}"`);
189
+ const targetLocalesRef = sql.raw(`"${targetLocalesTable}"`);
190
+ // Build the WHERE filter as a single fragment so each query reuses it.
191
+ // Bound params are interpolated by drizzle.
192
+ const sinceParam = since ? sql`AND "created_at" >= ${since}` : sql``;
193
+ const untilParam = until ? sql`AND "created_at" <= ${until}` : sql``;
194
+ const whereClause = sql`WHERE 1 = 1 ${sinceParam} ${untilParam}`;
195
+ // ----- totals + truncation count (single row) -----
196
+ // ROUND2-2 canonical definitions:
197
+ // - succeeded = status = 'succeeded' AND fields_translated > 0
198
+ // - preserved = status = 'succeeded' AND COALESCE(fields_translated, 0) <= 0
199
+ // - failed = status = 'failed'
200
+ // Invariant: runs = succeeded + preserved + failed. The Hub Overview
201
+ // and Audit tabs both consume this endpoint and must report the same
202
+ // numbers for the same time window.
203
+ const totalsResult = await drizzle.execute(sql`
204
+ SELECT
205
+ COUNT(*)::bigint AS runs,
206
+ COALESCE(SUM(CASE WHEN "status" = 'succeeded' AND COALESCE("fields_translated", 0) > 0 THEN 1 ELSE 0 END), 0)::bigint AS succeeded,
207
+ COALESCE(SUM(CASE WHEN "status" = 'succeeded' AND COALESCE("fields_translated", 0) <= 0 THEN 1 ELSE 0 END), 0)::bigint AS preserved,
208
+ COALESCE(SUM(CASE WHEN "status" = 'failed' THEN 1 ELSE 0 END), 0)::bigint AS failed,
209
+ COALESCE(SUM("input_tokens"), 0)::numeric AS input_tokens,
210
+ COALESCE(SUM("output_tokens"), 0)::numeric AS output_tokens,
211
+ COALESCE(SUM("estimated_cost_usd"), 0)::numeric AS cost_usd
212
+ FROM ${usageRef}
213
+ ${whereClause}
214
+ `);
215
+ const totalsRow = totalsResult.rows?.[0] ?? {};
216
+ const runs = toFiniteNumber(totalsRow.runs);
217
+ const totals = {
218
+ runs,
219
+ succeeded: toFiniteNumber(totalsRow.succeeded),
220
+ preserved: toFiniteNumber(totalsRow.preserved),
221
+ failed: toFiniteNumber(totalsRow.failed),
222
+ inputTokens: toFiniteNumber(totalsRow.input_tokens),
223
+ outputTokens: toFiniteNumber(totalsRow.output_tokens),
224
+ costUsd: toFiniteNumber(totalsRow.cost_usd),
225
+ totalMatching: runs,
226
+ truncated: false
227
+ };
228
+ // ----- per-collection -----
229
+ const byCollectionResult = await drizzle.execute(sql`
230
+ SELECT
231
+ "slug",
232
+ "kind",
233
+ COUNT(*)::bigint AS runs,
234
+ COALESCE(SUM("input_tokens" + "output_tokens"), 0)::numeric AS tokens,
235
+ COALESCE(SUM("estimated_cost_usd"), 0)::numeric AS cost_usd
236
+ FROM ${usageRef}
237
+ ${whereClause}
238
+ GROUP BY "slug", "kind"
239
+ ORDER BY cost_usd DESC, runs DESC
240
+ `);
241
+ const byCollection = (byCollectionResult.rows ?? []).map((row)=>({
242
+ slug: typeof row.slug === 'string' ? row.slug : '—',
243
+ kind: row.kind === 'global' ? 'global' : 'collection',
244
+ runs: toFiniteNumber(row.runs),
245
+ tokens: toFiniteNumber(row.tokens),
246
+ costUsd: toFiniteNumber(row.cost_usd)
247
+ }));
248
+ // ----- per-model. Rows whose model is null/empty/provider-only
249
+ // (no slash) collapse into a single "__unresolved__" bucket so the
250
+ // UI's "Failed before model selection" pseudo-row stays accurate
251
+ // even when the dataset crosses the 1000-row boundary. Mirrors
252
+ // `isRealModelId` in AuditPanel.helpers.
253
+ const byModelResult = await drizzle.execute(sql`
254
+ SELECT
255
+ CASE
256
+ WHEN "model" IS NULL OR "model" = '' OR POSITION('/' IN "model") = 0
257
+ THEN '__unresolved__'
258
+ ELSE "model"
259
+ END AS model,
260
+ COUNT(*)::bigint AS runs,
261
+ COALESCE(SUM("input_tokens" + "output_tokens"), 0)::numeric AS tokens,
262
+ COALESCE(SUM("estimated_cost_usd"), 0)::numeric AS cost_usd
263
+ FROM ${usageRef}
264
+ ${whereClause}
265
+ GROUP BY model
266
+ ORDER BY cost_usd DESC, runs DESC
267
+ `);
268
+ const byModel = (byModelResult.rows ?? []).map((row)=>({
269
+ model: typeof row.model === 'string' ? row.model : '__unresolved__',
270
+ runs: toFiniteNumber(row.runs),
271
+ tokens: toFiniteNumber(row.tokens),
272
+ costUsd: toFiniteNumber(row.cost_usd)
273
+ }));
274
+ // ----- per-locale (join on target_locales) -----
275
+ // Locale rows are joined to the parent usage row so the same time
276
+ // filter applies. Counts both total target-locale entries and the
277
+ // ones marked `status = 'failed'`.
278
+ const byLocaleResult = await drizzle.execute(sql`
279
+ SELECT
280
+ tl."locale" AS locale,
281
+ COUNT(*)::bigint AS runs,
282
+ COALESCE(SUM(CASE WHEN tl."status" = 'failed' THEN 1 ELSE 0 END), 0)::bigint AS failed
283
+ FROM ${targetLocalesRef} AS tl
284
+ JOIN ${usageRef} AS u ON u."id" = tl."_parent_id"
285
+ WHERE 1 = 1
286
+ ${since ? sql`AND u."created_at" >= ${since}` : sql``}
287
+ ${until ? sql`AND u."created_at" <= ${until}` : sql``}
288
+ GROUP BY tl."locale"
289
+ ORDER BY runs DESC
290
+ `);
291
+ const byLocale = (byLocaleResult.rows ?? []).map((row)=>({
292
+ locale: typeof row.locale === 'string' ? row.locale : '—',
293
+ runs: toFiniteNumber(row.runs),
294
+ failed: toFiniteNumber(row.failed)
295
+ }));
296
+ return {
297
+ totals,
298
+ byCollection,
299
+ byModel,
300
+ byLocale
301
+ };
302
+ }
303
+ // ---------------------------------------------------------------------------
304
+ // Fallback aggregation path (page-by-page payload.find)
305
+ // ---------------------------------------------------------------------------
306
+ // `targetLocales` join-table arrives as a nested array on each doc when
307
+ // read via `payload.find`. The Postgres adapter only inflates it when
308
+ // `depth > 0`, but reading `depth=0` returns the raw IDs. We use
309
+ // `depth=1` for the fallback so per-locale counts are correct.
310
+ const PAGINATION_LIMIT = 500;
311
+ async function aggregateWithPayload(payload, usageSlug, since, until) {
312
+ const whereClauses = [];
313
+ if (since) whereClauses.push({
314
+ createdAt: {
315
+ greater_than_equal: since
316
+ }
317
+ });
318
+ if (until) whereClauses.push({
319
+ createdAt: {
320
+ less_than_equal: until
321
+ }
322
+ });
323
+ const where = whereClauses.length > 0 ? {
324
+ and: whereClauses
325
+ } : undefined;
326
+ let runs = 0;
327
+ let succeeded = 0;
328
+ let preserved = 0;
329
+ let failed = 0;
330
+ let inputTokens = 0;
331
+ let outputTokens = 0;
332
+ let costUsd = 0;
333
+ const perCollection = new Map();
334
+ const perModel = new Map();
335
+ const perLocale = new Map();
336
+ let page = 1;
337
+ // eslint-disable-next-line no-constant-condition
338
+ while(true){
339
+ const result = await payload.find({
340
+ collection: usageSlug,
341
+ where,
342
+ page,
343
+ limit: PAGINATION_LIMIT,
344
+ depth: 0,
345
+ overrideAccess: true
346
+ });
347
+ const docs = result.docs;
348
+ for (const r of docs){
349
+ runs++;
350
+ // ROUND2-2 canonical definitions — mirrors the drizzle path's
351
+ // CASE expressions. `fieldsTranslated > 0` separates LLM-write
352
+ // successes from preserve-only no-ops.
353
+ const fieldsTranslated = toFiniteNumber(r.fieldsTranslated);
354
+ if (r.status === 'succeeded' && fieldsTranslated > 0) succeeded++;
355
+ else if (r.status === 'succeeded') preserved++;
356
+ if (r.status === 'failed') failed++;
357
+ const inT = toFiniteNumber(r.inputTokens);
358
+ const outT = toFiniteNumber(r.outputTokens);
359
+ inputTokens += inT;
360
+ outputTokens += outT;
361
+ const cost = toFiniteNumber(r.estimatedCostUsd);
362
+ costUsd += cost;
363
+ const slug = typeof r.slug === 'string' ? r.slug : '—';
364
+ const kind = r.kind === 'global' ? 'global' : 'collection';
365
+ const collectionKey = `${kind}:${slug}`;
366
+ const cBucket = perCollection.get(collectionKey) ?? {
367
+ slug,
368
+ kind,
369
+ runs: 0,
370
+ tokens: 0,
371
+ costUsd: 0
372
+ };
373
+ cBucket.runs++;
374
+ cBucket.tokens += inT + outT;
375
+ cBucket.costUsd += cost;
376
+ perCollection.set(collectionKey, cBucket);
377
+ const modelRaw = typeof r.model === 'string' ? r.model : '';
378
+ const modelKey = modelRaw.length === 0 || !modelRaw.includes('/') ? '__unresolved__' : modelRaw;
379
+ const mBucket = perModel.get(modelKey) ?? {
380
+ model: modelKey,
381
+ runs: 0,
382
+ tokens: 0,
383
+ costUsd: 0
384
+ };
385
+ mBucket.runs++;
386
+ mBucket.tokens += inT + outT;
387
+ mBucket.costUsd += cost;
388
+ perModel.set(modelKey, mBucket);
389
+ for (const tl of r.targetLocales ?? []){
390
+ const locale = typeof tl?.locale === 'string' ? tl.locale : '—';
391
+ const lBucket = perLocale.get(locale) ?? {
392
+ locale,
393
+ runs: 0,
394
+ failed: 0
395
+ };
396
+ lBucket.runs++;
397
+ if (tl?.status === 'failed') lBucket.failed++;
398
+ perLocale.set(locale, lBucket);
399
+ }
400
+ }
401
+ if (!result.hasNextPage) break;
402
+ page++;
403
+ // Safety cap — don't walk forever if pagination misreports.
404
+ if (page > 200) break;
405
+ }
406
+ return {
407
+ totals: {
408
+ runs,
409
+ succeeded,
410
+ preserved,
411
+ failed,
412
+ inputTokens,
413
+ outputTokens,
414
+ costUsd,
415
+ totalMatching: runs,
416
+ truncated: false
417
+ },
418
+ byCollection: Array.from(perCollection.values()).sort((a, b)=>b.costUsd - a.costUsd || b.runs - a.runs),
419
+ byModel: Array.from(perModel.values()).sort((a, b)=>b.costUsd - a.costUsd || b.runs - a.runs),
420
+ byLocale: Array.from(perLocale.values()).sort((a, b)=>b.runs - a.runs)
421
+ };
422
+ }
423
+ async function fetchRecentSamples(payload, usageSlug, since, until) {
424
+ const whereClauses = [];
425
+ if (since) whereClauses.push({
426
+ createdAt: {
427
+ greater_than_equal: since
428
+ }
429
+ });
430
+ if (until) whereClauses.push({
431
+ createdAt: {
432
+ less_than_equal: until
433
+ }
434
+ });
435
+ const where = whereClauses.length > 0 ? {
436
+ and: whereClauses
437
+ } : undefined;
438
+ const result = await payload.find({
439
+ collection: usageSlug,
440
+ where,
441
+ sort: '-createdAt',
442
+ limit: SAMPLES_LIMIT,
443
+ depth: 0,
444
+ overrideAccess: true
445
+ });
446
+ const docs = result.docs;
447
+ return docs.map((r)=>({
448
+ id: r.id ?? 0,
449
+ createdAt: typeof r.createdAt === 'string' ? r.createdAt : new Date(0).toISOString(),
450
+ slug: typeof r.slug === 'string' ? r.slug : '—',
451
+ kind: r.kind === 'global' ? 'global' : 'collection',
452
+ documentId: typeof r.documentId === 'string' ? r.documentId : null,
453
+ status: r.status === 'failed' ? 'failed' : 'succeeded',
454
+ model: typeof r.model === 'string' ? r.model : null,
455
+ inputTokens: toFiniteNumber(r.inputTokens),
456
+ outputTokens: toFiniteNumber(r.outputTokens),
457
+ estimatedCostUsd: r.estimatedCostUsd == null ? null : toFiniteNumber(r.estimatedCostUsd),
458
+ durationMs: r.durationMs == null ? null : toFiniteNumber(r.durationMs),
459
+ error: typeof r.error === 'string' ? r.error : null,
460
+ failedCount: toFiniteNumber(r.failedCount),
461
+ succeededCount: toFiniteNumber(r.succeededCount),
462
+ targetLocales: Array.isArray(r.targetLocales) ? r.targetLocales.map((tl)=>({
463
+ locale: typeof tl?.locale === 'string' ? tl.locale : '',
464
+ status: tl?.status ?? null
465
+ })) : null
466
+ }));
467
+ }
468
+ function toFiniteNumber(v) {
469
+ if (typeof v === 'number') {
470
+ return Number.isFinite(v) ? v : 0;
471
+ }
472
+ if (typeof v === 'string') {
473
+ // Postgres returns `numeric`/`bigint` as strings — coerce.
474
+ const n = Number.parseFloat(v);
475
+ return Number.isFinite(n) ? n : 0;
476
+ }
477
+ if (typeof v === 'bigint') {
478
+ return Number(v);
479
+ }
480
+ return 0;
481
+ }
@@ -0,0 +1,6 @@
1
+ export { EstimatedCostCell } from '../client/estimated-cost-cell.js';
2
+ export { ExcludedFieldsField } from '../client/excluded-fields-field.js';
3
+ export { FieldTranslateButton } from '../client/field-translate-button.js';
4
+ export { TranslateButton } from '../client/translate-button.js';
5
+ export { TranslateModal } from '../client/translate-modal.js';
6
+ export { TranslationProgress } from '../client/translation-progress.js';
@@ -0,0 +1,6 @@
1
+ export { EstimatedCostCell } from '../client/estimated-cost-cell.js';
2
+ export { ExcludedFieldsField } from '../client/excluded-fields-field.js';
3
+ export { FieldTranslateButton } from '../client/field-translate-button.js';
4
+ export { TranslateButton } from '../client/translate-button.js';
5
+ export { TranslateModal } from '../client/translate-modal.js';
6
+ export { TranslationProgress } from '../client/translation-progress.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Re-exports for admin-sidebar components shipped by the ai-translate
3
+ * plugin. Consumers wire these via `admin.components.beforeNavLinks`
4
+ * or `afterNavLinks` in their payload.config.ts.
5
+ */
6
+ export { default, TranslationNavGroup } from '../components/TranslationNavGroup.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Re-exports for admin-sidebar components shipped by the ai-translate
3
+ * plugin. Consumers wire these via `admin.components.beforeNavLinks`
4
+ * or `afterNavLinks` in their payload.config.ts.
5
+ */ export { default, TranslationNavGroup } from '../components/TranslationNavGroup.js';
@@ -0,0 +1,8 @@
1
+ export { translateDocument, translateGlobal } from '../api.js';
2
+ export { ensureBulkTranslateSchema, migrateBatchStatusVocabulary, migrateHashesToV1Prefix, } from '../lib/bulk-translate-migrations.js';
3
+ export { diffFields } from '../lib/field-diff.js';
4
+ export { isFieldEmpty } from '../lib/field-empty.js';
5
+ export { createScopedLogger, type LogContext, type LogEventFields, type LogLevel, type ScopedLogger, type SerializedError, serializeErr, } from '../lib/logger.js';
6
+ export { type AcquireTokenResult, acquireToken, getBucketStatus, type TokenBucketOptions, } from '../lib/translation-token-bucket.js';
7
+ export { aiTranslatePlugin } from '../plugin.js';
8
+ export type { AITranslatePluginConfig, AutomationConfig, ConcurrencyLimits, CostGuardError, CostLimits, FieldLocaleResult, LexicalNodeRegistration, QualityConfig, RetryConfig, SamplingConfig, TargetPolicy, TranslateDocumentOptions, TranslateDocumentResult, TranslatedItem, TranslateGlobalOptions, TranslateRequest, TranslateResponse, TranslationAlert, TranslationAlertType, TranslationContext, TranslationEstimate, TranslationEvent, TranslationEventType, TranslationItem, TranslationItemKind, TranslationProvider, TranslationSample, TranslationUsage, ValidationConfig, } from '../types.js';
@@ -0,0 +1,7 @@
1
+ export { translateDocument, translateGlobal } from '../api.js';
2
+ export { ensureBulkTranslateSchema, migrateBatchStatusVocabulary, migrateHashesToV1Prefix } from '../lib/bulk-translate-migrations.js';
3
+ export { diffFields } from '../lib/field-diff.js';
4
+ export { isFieldEmpty } from '../lib/field-empty.js';
5
+ export { createScopedLogger, serializeErr } from '../lib/logger.js';
6
+ export { acquireToken, getBucketStatus } from '../lib/translation-token-bucket.js';
7
+ export { aiTranslatePlugin } from '../plugin.js';
@@ -0,0 +1,9 @@
1
+ export type { AnthropicProviderOptions } from '../providers/anthropic.js';
2
+ export { createAnthropicProvider } from '../providers/anthropic.js';
3
+ export type { CustomProviderOptions, CustomProviderPricing } from '../providers/custom.js';
4
+ export { createCustomProvider } from '../providers/custom.js';
5
+ export type { GeminiProviderOptions } from '../providers/gemini.js';
6
+ export { createGeminiProvider } from '../providers/gemini.js';
7
+ export { createMockProvider } from '../providers/mock.js';
8
+ export type { OpenAIProviderOptions } from '../providers/openai.js';
9
+ export { createOpenAIProvider } from '../providers/openai.js';
@@ -0,0 +1,5 @@
1
+ export { createAnthropicProvider } from '../providers/anthropic.js';
2
+ export { createCustomProvider } from '../providers/custom.js';
3
+ export { createGeminiProvider } from '../providers/gemini.js';
4
+ export { createMockProvider } from '../providers/mock.js';
5
+ export { createOpenAIProvider } from '../providers/openai.js';
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Client-side surface for the Translation Hub view.
3
+ *
4
+ * Separated from `./views.ts` (the server entry) because tsup's single-
5
+ * bundle output strips `"use client"` directives from any sub-files
6
+ * (Hub.client.tsx, ActiveJobs.tsx, etc) — so the entire bundle would
7
+ * compile as a server component and React useState/useEffect would
8
+ * throw "useState only works in Client Components" at runtime.
9
+ *
10
+ * Two-entry split:
11
+ * - `ai-translate-views` (server, no banner): exports `TranslationHubView`
12
+ * which uses `DefaultTemplate` from `@payloadcms/next/templates` (server).
13
+ * - `ai-translate-views-client` (this file, banner: '"use client";'): all
14
+ * client sub-components.
15
+ *
16
+ * The server `index.tsx` imports `TranslationHubClient` via the package's
17
+ * own subpath `@purposeinplay/payload-ai-translate/views-client`, which
18
+ * is marked `external` in the server tsup config so it stays as an external
19
+ * import (resolved at runtime via package.json `exports`) instead of being
20
+ * inlined into the server bundle.
21
+ */
22
+ export { BulkRunsHubClient } from '../views/BulkRunsHub/BulkRunsHub.client.js';
23
+ export { TranslationHubClient } from '../views/TranslationHub/Hub.client.js';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Client-side surface for the Translation Hub view.
3
+ *
4
+ * Separated from `./views.ts` (the server entry) because tsup's single-
5
+ * bundle output strips `"use client"` directives from any sub-files
6
+ * (Hub.client.tsx, ActiveJobs.tsx, etc) — so the entire bundle would
7
+ * compile as a server component and React useState/useEffect would
8
+ * throw "useState only works in Client Components" at runtime.
9
+ *
10
+ * Two-entry split:
11
+ * - `ai-translate-views` (server, no banner): exports `TranslationHubView`
12
+ * which uses `DefaultTemplate` from `@payloadcms/next/templates` (server).
13
+ * - `ai-translate-views-client` (this file, banner: '"use client";'): all
14
+ * client sub-components.
15
+ *
16
+ * The server `index.tsx` imports `TranslationHubClient` via the package's
17
+ * own subpath `@purposeinplay/payload-ai-translate/views-client`, which
18
+ * is marked `external` in the server tsup config so it stays as an external
19
+ * import (resolved at runtime via package.json `exports`) instead of being
20
+ * inlined into the server bundle.
21
+ */ export { BulkRunsHubClient } from '../views/BulkRunsHub/BulkRunsHub.client.js';
22
+ export { TranslationHubClient } from '../views/TranslationHub/Hub.client.js';
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Server-rendered admin views provided by the ai-translate plugin.
3
+ *
4
+ * Consumers wire these into Payload's `admin.components.views` config so the
5
+ * plugin owns the canonical UI surface — there's no copy-paste between
6
+ * consumer repos.
7
+ *
8
+ * Example (payload.config.ts):
9
+ *
10
+ * import { TranslationHubView } from '@purposeinplay/payload-ai-translate/views'
11
+ *
12
+ * admin: {
13
+ * components: {
14
+ * views: {
15
+ * TranslationHub: {
16
+ * Component: TranslationHubView,
17
+ * path: '/translation',
18
+ * // ...
19
+ * },
20
+ * },
21
+ * },
22
+ * }
23
+ *
24
+ * NOTE: This entry must NOT carry a "use client" banner — `TranslationHubView`
25
+ * is a Server Component that wraps the client `<TranslationHubClient />` in
26
+ * Payload's `<DefaultTemplate>`. The client subtree lives behind a
27
+ * `"use client"` directive in `Hub.client.tsx` and its children.
28
+ */
29
+ export { BulkRunsHubView, default as BulkRunsHub } from '../views/BulkRunsHub/index.js';
30
+ export { default as TranslationHub, TranslationHubView } from '../views/TranslationHub/index.js';
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Server-rendered admin views provided by the ai-translate plugin.
3
+ *
4
+ * Consumers wire these into Payload's `admin.components.views` config so the
5
+ * plugin owns the canonical UI surface — there's no copy-paste between
6
+ * consumer repos.
7
+ *
8
+ * Example (payload.config.ts):
9
+ *
10
+ * import { TranslationHubView } from '@purposeinplay/payload-ai-translate/views'
11
+ *
12
+ * admin: {
13
+ * components: {
14
+ * views: {
15
+ * TranslationHub: {
16
+ * Component: TranslationHubView,
17
+ * path: '/translation',
18
+ * // ...
19
+ * },
20
+ * },
21
+ * },
22
+ * }
23
+ *
24
+ * NOTE: This entry must NOT carry a "use client" banner — `TranslationHubView`
25
+ * is a Server Component that wraps the client `<TranslationHubClient />` in
26
+ * Payload's `<DefaultTemplate>`. The client subtree lives behind a
27
+ * `"use client"` directive in `Hub.client.tsx` and its children.
28
+ */ export { BulkRunsHubView, default as BulkRunsHub } from '../views/BulkRunsHub/index.js';
29
+ export { default as TranslationHub, TranslationHubView } from '../views/TranslationHub/index.js';
@@ -0,0 +1,4 @@
1
+ import type { GlobalAfterChangeHook } from 'payload';
2
+ import type { CoalescingQueue } from '../lib/coalescing-queue.js';
3
+ import type { AITranslatePluginConfig } from '../types.js';
4
+ export declare function createTranslateGlobalAfterChangeHook(globalSlug: string, pluginOptions: AITranslatePluginConfig, queue?: CoalescingQueue | null): GlobalAfterChangeHook;