@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,70 @@
1
+ /**
2
+ * Alert-banner grouping for HUB-1 (v1.2.6).
3
+ *
4
+ * The Hub Overview used to render every un-dismissed alert as its own
5
+ * banner. With 10+ "Translation failed repeatedly" rows from a single
6
+ * cron-stuck batch, the alert stack consumed ~6 vertical screens before
7
+ * any other Hub content was visible. We now group alerts that share
8
+ * `(alertType, collection, locale, age-bucket)`, render at most 3
9
+ * groups expanded with a "+N more" affordance, and offer a single
10
+ * "Dismiss all" bulk action.
11
+ *
12
+ * Helpers exported for unit tests:
13
+ *
14
+ * - `ageBucket(iso, now)` — bucket an alert by relative age so two
15
+ * bursts of failures hours apart don't fold into one group.
16
+ * - `groupAlerts(rows, now)` — pure reducer; returns ordered groups
17
+ * keyed by `(alertType | collection | locale | age-bucket)`.
18
+ * - `splitVisibleGroups(groups, max)` — split into the first N
19
+ * groups + a tail to be summarised by "+N more".
20
+ *
21
+ * The grouping is deterministic and stable: groups appear in the same
22
+ * order as their newest member, so a new failure surface always
23
+ * appears at the top.
24
+ */
25
+ export type AlertAgeBucket = '<5m' | '5–30m' | '30m–1h' | '1–6h' | '6–24h';
26
+ export interface GroupableAlert {
27
+ id: number;
28
+ alertType: 'persistent-failure' | 'cost-guard-abort' | 'provider-outage';
29
+ collection?: string | null;
30
+ createdAt: string;
31
+ metadata?: Record<string, unknown> | null;
32
+ }
33
+ export interface AlertGroup<T extends GroupableAlert = GroupableAlert> {
34
+ /** Stable, deterministic key — `${alertType}|${collection}|${locale}|${bucket}`. */
35
+ key: string;
36
+ alertType: T['alertType'];
37
+ collection: string | null;
38
+ locale: string | null;
39
+ bucket: AlertAgeBucket;
40
+ /** Alerts in the group, sorted newest-first (matches input ordering convention). */
41
+ alerts: T[];
42
+ /** The most-recent createdAt across the group — used for sort. */
43
+ newestCreatedAt: string;
44
+ }
45
+ /**
46
+ * Bucket an alert's age into a coarse band. Buckets are chosen so that
47
+ * fresh and old failures stay visually separable in the banner stack
48
+ * (an editor seeing two distinct "5m" and "6h" groups for the same
49
+ * locale knows the system is currently failing AND has been for a
50
+ * while).
51
+ */
52
+ export declare function ageBucket(iso: string, now?: Date): AlertAgeBucket;
53
+ /**
54
+ * Group alerts by `(alertType | collection | locale | age-bucket)`.
55
+ * Returns groups ordered by `newestCreatedAt` descending, so the most
56
+ * recent failure surface is always first in the stack — preserves the
57
+ * existing UX expectation that the top banner is the newest event.
58
+ */
59
+ export declare function groupAlerts<T extends GroupableAlert>(rows: T[], now?: Date): AlertGroup<T>[];
60
+ /**
61
+ * Slice the group list into the head shown expanded vs the tail
62
+ * summarised as "+N more". Keep at least 1 visible group so the user
63
+ * always has something to read. Tail is returned as a count of groups
64
+ * and total alerts so the caller can render whichever fits the UI.
65
+ */
66
+ export declare function splitVisibleGroups<T extends GroupableAlert>(groups: AlertGroup<T>[], maxVisible?: number): {
67
+ visible: AlertGroup<T>[];
68
+ hiddenGroupCount: number;
69
+ hiddenAlertCount: number;
70
+ };
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Alert-banner grouping for HUB-1 (v1.2.6).
3
+ *
4
+ * The Hub Overview used to render every un-dismissed alert as its own
5
+ * banner. With 10+ "Translation failed repeatedly" rows from a single
6
+ * cron-stuck batch, the alert stack consumed ~6 vertical screens before
7
+ * any other Hub content was visible. We now group alerts that share
8
+ * `(alertType, collection, locale, age-bucket)`, render at most 3
9
+ * groups expanded with a "+N more" affordance, and offer a single
10
+ * "Dismiss all" bulk action.
11
+ *
12
+ * Helpers exported for unit tests:
13
+ *
14
+ * - `ageBucket(iso, now)` — bucket an alert by relative age so two
15
+ * bursts of failures hours apart don't fold into one group.
16
+ * - `groupAlerts(rows, now)` — pure reducer; returns ordered groups
17
+ * keyed by `(alertType | collection | locale | age-bucket)`.
18
+ * - `splitVisibleGroups(groups, max)` — split into the first N
19
+ * groups + a tail to be summarised by "+N more".
20
+ *
21
+ * The grouping is deterministic and stable: groups appear in the same
22
+ * order as their newest member, so a new failure surface always
23
+ * appears at the top.
24
+ */ /**
25
+ * Bucket an alert's age into a coarse band. Buckets are chosen so that
26
+ * fresh and old failures stay visually separable in the banner stack
27
+ * (an editor seeing two distinct "5m" and "6h" groups for the same
28
+ * locale knows the system is currently failing AND has been for a
29
+ * while).
30
+ */ export function ageBucket(iso, now = new Date()) {
31
+ const ms = now.getTime() - new Date(iso).getTime();
32
+ const m = Math.max(0, Math.floor(ms / 60_000));
33
+ if (m < 5) return '<5m';
34
+ if (m < 30) return '5–30m';
35
+ if (m < 60) return '30m–1h';
36
+ if (m < 6 * 60) return '1–6h';
37
+ return '6–24h';
38
+ }
39
+ /** Extract the locale dimension from an alert's metadata (or null). */ function localeOf(alert) {
40
+ const v = alert.metadata?.['locale'];
41
+ return typeof v === 'string' && v.length > 0 ? v : null;
42
+ }
43
+ /**
44
+ * Group alerts by `(alertType | collection | locale | age-bucket)`.
45
+ * Returns groups ordered by `newestCreatedAt` descending, so the most
46
+ * recent failure surface is always first in the stack — preserves the
47
+ * existing UX expectation that the top banner is the newest event.
48
+ */ export function groupAlerts(rows, now = new Date()) {
49
+ const map = new Map();
50
+ for (const row of rows){
51
+ const collection = row.collection ?? null;
52
+ const locale = localeOf(row);
53
+ const bucket = ageBucket(row.createdAt, now);
54
+ const key = `${row.alertType}|${collection ?? '∅'}|${locale ?? '∅'}|${bucket}`;
55
+ const existing = map.get(key);
56
+ if (existing) {
57
+ existing.alerts.push(row);
58
+ if (row.createdAt > existing.newestCreatedAt) {
59
+ existing.newestCreatedAt = row.createdAt;
60
+ }
61
+ } else {
62
+ map.set(key, {
63
+ key,
64
+ alertType: row.alertType,
65
+ collection,
66
+ locale,
67
+ bucket,
68
+ alerts: [
69
+ row
70
+ ],
71
+ newestCreatedAt: row.createdAt
72
+ });
73
+ }
74
+ }
75
+ return Array.from(map.values()).sort((a, b)=>a.newestCreatedAt < b.newestCreatedAt ? 1 : -1);
76
+ }
77
+ /**
78
+ * Slice the group list into the head shown expanded vs the tail
79
+ * summarised as "+N more". Keep at least 1 visible group so the user
80
+ * always has something to read. Tail is returned as a count of groups
81
+ * and total alerts so the caller can render whichever fits the UI.
82
+ */ export function splitVisibleGroups(groups, maxVisible = 3) {
83
+ const safeMax = Math.max(1, maxVisible);
84
+ if (groups.length <= safeMax) {
85
+ return {
86
+ visible: groups,
87
+ hiddenGroupCount: 0,
88
+ hiddenAlertCount: 0
89
+ };
90
+ }
91
+ const visible = groups.slice(0, safeMax);
92
+ const hidden = groups.slice(safeMax);
93
+ const hiddenAlertCount = hidden.reduce((sum, group)=>sum + group.alerts.length, 0);
94
+ return {
95
+ visible,
96
+ hiddenGroupCount: hidden.length,
97
+ hiddenAlertCount
98
+ };
99
+ }
@@ -0,0 +1,20 @@
1
+ import type { AdminViewServerProps } from 'payload';
2
+ import type React from 'react';
3
+ /**
4
+ * Server entry for the custom `/admin/translation` view.
5
+ *
6
+ * Wraps the client UI in Payload's `<DefaultTemplate>` so the page
7
+ * inherits the standard admin chrome (sidebar nav, top bar, breadcrumbs)
8
+ * — without it, the custom view replaces the entire admin layout and
9
+ * the operator has no way to navigate back to other surfaces.
10
+ *
11
+ * Auth (1.2.8): admin and editor roles. Pre-1.2.8 the gate was
12
+ * admin-only (Decision #31). Editors get a scoped variant of the Hub
13
+ * — own bulk runs in the Recent translations table, count-based KPI
14
+ * cards (no $ / tokens), Configuration / Audit & Cost / Advanced
15
+ * tabs hidden. Non-admin-non-editor users still see the friendly
16
+ * Access Denied page. The role is threaded into the client tree so
17
+ * rendering can adapt without a second roundtrip.
18
+ */
19
+ export declare const TranslationHubView: React.FC<AdminViewServerProps>;
20
+ export default TranslationHubView;
@@ -0,0 +1,109 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { DefaultTemplate } from '@payloadcms/next/templates';
3
+ // Self-import via the package's external subpath. Keeps the "use client"
4
+ // directive intact at runtime — see tsup.config.ts comment above the
5
+ // `ai-translate-views-client` entry. Inlining `./Hub.client` would strip
6
+ // the directive during bundling and break `useState` in production.
7
+ import { TranslationHubClient } from '@purposeinplay/payload-ai-translate/views-client';
8
+ import { redirect } from 'next/navigation';
9
+ import { formatAdminURL } from 'payload/shared';
10
+ /**
11
+ * Server entry for the custom `/admin/translation` view.
12
+ *
13
+ * Wraps the client UI in Payload's `<DefaultTemplate>` so the page
14
+ * inherits the standard admin chrome (sidebar nav, top bar, breadcrumbs)
15
+ * — without it, the custom view replaces the entire admin layout and
16
+ * the operator has no way to navigate back to other surfaces.
17
+ *
18
+ * Auth (1.2.8): admin and editor roles. Pre-1.2.8 the gate was
19
+ * admin-only (Decision #31). Editors get a scoped variant of the Hub
20
+ * — own bulk runs in the Recent translations table, count-based KPI
21
+ * cards (no $ / tokens), Configuration / Audit & Cost / Advanced
22
+ * tabs hidden. Non-admin-non-editor users still see the friendly
23
+ * Access Denied page. The role is threaded into the client tree so
24
+ * rendering can adapt without a second roundtrip.
25
+ */ export const TranslationHubView = ({ initPageResult, params, searchParams })=>{
26
+ const { req, visibleEntities, permissions } = initPageResult;
27
+ // Payload renders CUSTOM admin views as PUBLIC by default — unlike
28
+ // built-in collection/global routes, which redirect unauthenticated
29
+ // visitors to login. Without this check, anonymous visitors got the
30
+ // full admin shell (nav with every collection/global name) plus this
31
+ // view's role message. Mirror the built-in behaviour: no user → login,
32
+ // preserving the deep link via ?redirect=.
33
+ if (!req.user) {
34
+ const adminRoute = req.payload.config.routes?.admin ?? '/admin';
35
+ redirect(formatAdminURL({
36
+ adminRoute,
37
+ path: `/login?redirect=${encodeURIComponent(`${adminRoute}/translation`)}`
38
+ }));
39
+ }
40
+ const user = req.user;
41
+ const roles = user?.roles ?? [];
42
+ const isAdmin = roles.includes('admin');
43
+ const isEditor = roles.includes('editor');
44
+ const isPermitted = isAdmin || isEditor;
45
+ const role = isAdmin ? 'admin' : 'editor';
46
+ return /*#__PURE__*/ _jsx(DefaultTemplate, {
47
+ i18n: req.i18n,
48
+ locale: req.locale,
49
+ params: params,
50
+ payload: req.payload,
51
+ permissions: permissions,
52
+ searchParams: searchParams,
53
+ user: req.user ?? undefined,
54
+ visibleEntities: visibleEntities,
55
+ children: isPermitted ? /*#__PURE__*/ _jsx(TranslationHubClient, {
56
+ role: role,
57
+ currentUserId: user?.id != null ? String(user.id) : null,
58
+ currentUserEmail: user?.email ?? null,
59
+ locales: req.payload.config.localization ? {
60
+ defaultLocale: req.payload.config.localization.defaultLocale,
61
+ locales: req.payload.config.localization.locales.map((l)=>{
62
+ if (typeof l === 'string') {
63
+ return {
64
+ code: l,
65
+ label: l
66
+ };
67
+ }
68
+ const rawLabel = l.label;
69
+ let label = l.code;
70
+ if (typeof rawLabel === 'string') {
71
+ label = rawLabel;
72
+ } else if (rawLabel) {
73
+ label = Object.values(rawLabel)[0] ?? l.code;
74
+ }
75
+ return {
76
+ code: l.code,
77
+ label
78
+ };
79
+ })
80
+ } : {
81
+ defaultLocale: 'en',
82
+ locales: []
83
+ }
84
+ }) : /*#__PURE__*/ _jsxs("div", {
85
+ style: {
86
+ padding: '2rem',
87
+ maxWidth: '600px',
88
+ margin: '4rem auto',
89
+ textAlign: 'center'
90
+ },
91
+ children: [
92
+ /*#__PURE__*/ _jsx("h1", {
93
+ style: {
94
+ fontSize: '1.5rem',
95
+ color: 'var(--theme-elevation-1000)'
96
+ },
97
+ children: "Translation Hub"
98
+ }),
99
+ /*#__PURE__*/ _jsx("p", {
100
+ style: {
101
+ color: 'var(--theme-elevation-700)'
102
+ },
103
+ children: "You need an admin or editor role to access translation settings."
104
+ })
105
+ ]
106
+ })
107
+ });
108
+ };
109
+ export default TranslationHubView;
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Hub tab navigation helpers.
3
+ *
4
+ * NEW-4 (v1.2.6): the Hub tab strip rendered as plain `<button>`
5
+ * elements — no `role="tablist"`, no `role="tab"`, no
6
+ * `aria-selected`, no arrow-key navigation. Screen readers heard four
7
+ * generic buttons; the active tab was conveyed only by a visual
8
+ * `border-bottom`.
9
+ *
10
+ * This module owns the WAI-ARIA Authoring Practices tabs pattern's
11
+ * pure parts: the id naming helpers (tab id / panel id) and the
12
+ * keyboard-driven next-index resolver. The Hub.client component pulls
13
+ * them in, wires the actual ARIA attributes on the DOM nodes, and
14
+ * holds the focus refs (which are inherently a DOM concern).
15
+ *
16
+ * Tested in isolation in __tests__/tabNavigation.test.ts.
17
+ */
18
+ /**
19
+ * Result of a keyboard event applied to the tab strip. `null` means
20
+ * the key was unhandled — the consumer should let the event bubble
21
+ * (e.g. Tab to leave the tablist, Shift+Tab, etc.).
22
+ */
23
+ export type TabKeyAction = {
24
+ kind: 'move';
25
+ nextIndex: number;
26
+ } | {
27
+ kind: 'ignore';
28
+ } | null;
29
+ /**
30
+ * Apply the WAI-ARIA Authoring Practices keyboard model for a
31
+ * horizontal tablist with wrap:
32
+ * - ArrowLeft → previous tab (wraps to last)
33
+ * - ArrowRight → next tab (wraps to first)
34
+ * - Home → first tab
35
+ * - End → last tab
36
+ *
37
+ * Returns the next focused tab index, or `null` if the key is not one
38
+ * of the four navigation keys. `count` must be > 0 — caller ensures
39
+ * the tablist is non-empty before invoking.
40
+ */
41
+ export declare function resolveTabKey(key: string, currentIndex: number, count: number): TabKeyAction;
42
+ /**
43
+ * DOM id for the `<button role="tab">` that controls a given panel.
44
+ * Used by the panel via `aria-labelledby` so the tab's label voices
45
+ * the panel for screen readers.
46
+ */
47
+ export declare function tabIdFor(tab: string): string;
48
+ /**
49
+ * DOM id for the panel a tab controls. Used by the tab via
50
+ * `aria-controls` so AT can pair the two when the user activates the
51
+ * tab.
52
+ */
53
+ export declare function panelIdFor(tab: string): string;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Hub tab navigation helpers.
3
+ *
4
+ * NEW-4 (v1.2.6): the Hub tab strip rendered as plain `<button>`
5
+ * elements — no `role="tablist"`, no `role="tab"`, no
6
+ * `aria-selected`, no arrow-key navigation. Screen readers heard four
7
+ * generic buttons; the active tab was conveyed only by a visual
8
+ * `border-bottom`.
9
+ *
10
+ * This module owns the WAI-ARIA Authoring Practices tabs pattern's
11
+ * pure parts: the id naming helpers (tab id / panel id) and the
12
+ * keyboard-driven next-index resolver. The Hub.client component pulls
13
+ * them in, wires the actual ARIA attributes on the DOM nodes, and
14
+ * holds the focus refs (which are inherently a DOM concern).
15
+ *
16
+ * Tested in isolation in __tests__/tabNavigation.test.ts.
17
+ */ /**
18
+ * Result of a keyboard event applied to the tab strip. `null` means
19
+ * the key was unhandled — the consumer should let the event bubble
20
+ * (e.g. Tab to leave the tablist, Shift+Tab, etc.).
21
+ */ /**
22
+ * Apply the WAI-ARIA Authoring Practices keyboard model for a
23
+ * horizontal tablist with wrap:
24
+ * - ArrowLeft → previous tab (wraps to last)
25
+ * - ArrowRight → next tab (wraps to first)
26
+ * - Home → first tab
27
+ * - End → last tab
28
+ *
29
+ * Returns the next focused tab index, or `null` if the key is not one
30
+ * of the four navigation keys. `count` must be > 0 — caller ensures
31
+ * the tablist is non-empty before invoking.
32
+ */ export function resolveTabKey(key, currentIndex, count) {
33
+ if (count <= 0) {
34
+ return null;
35
+ }
36
+ switch(key){
37
+ case 'ArrowLeft':
38
+ return {
39
+ kind: 'move',
40
+ nextIndex: (currentIndex - 1 + count) % count
41
+ };
42
+ case 'ArrowRight':
43
+ return {
44
+ kind: 'move',
45
+ nextIndex: (currentIndex + 1) % count
46
+ };
47
+ case 'Home':
48
+ return {
49
+ kind: 'move',
50
+ nextIndex: 0
51
+ };
52
+ case 'End':
53
+ return {
54
+ kind: 'move',
55
+ nextIndex: count - 1
56
+ };
57
+ default:
58
+ return null;
59
+ }
60
+ }
61
+ /**
62
+ * DOM id for the `<button role="tab">` that controls a given panel.
63
+ * Used by the panel via `aria-labelledby` so the tab's label voices
64
+ * the panel for screen readers.
65
+ */ export function tabIdFor(tab) {
66
+ return `translation-hub-tab-${tab}`;
67
+ }
68
+ /**
69
+ * DOM id for the panel a tab controls. Used by the tab via
70
+ * `aria-controls` so AT can pair the two when the user activates the
71
+ * tab.
72
+ */ export function panelIdFor(tab) {
73
+ return `translation-hub-panel-${tab}`;
74
+ }
@@ -0,0 +1,33 @@
1
+ /** 1h freshness — long enough that the editor sees the run after lunch
2
+ * but short enough that an 8h-old "Revert this batch" affordance isn't
3
+ * one accidental click away. */
4
+ export declare const TERMINAL_BANNER_FRESHNESS_MS: number;
5
+ /**
6
+ * Decide whether the terminal banner should still be shown for a batch
7
+ * based on its `completedAt` timestamp and the current time. Returns
8
+ * false for batches with no `completedAt` (defensive — a terminal-status
9
+ * batch should always have one, but we don't want to render a stale
10
+ * card if the field is missing).
11
+ */
12
+ export declare function isTerminalBannerFresh(completedAt: string | null | undefined, now?: Date, freshnessMs?: number): boolean;
13
+ /**
14
+ * Read the dismissed-batch-id set from localStorage. Returns an empty
15
+ * set on SSR / quota / parse failure.
16
+ */
17
+ export declare function readDismissedBatchIds(): Set<string>;
18
+ /**
19
+ * Persist the dismissed-batch-id set to localStorage. Bounds the
20
+ * persisted list to the last 50 ids so a power user dismissing dozens
21
+ * of runs over time doesn't grow the entry indefinitely. Swallows any
22
+ * write failures (quota, private-browsing).
23
+ */
24
+ export declare function writeDismissedBatchIds(ids: Set<string>): void;
25
+ /**
26
+ * Hook: returns `{ shouldShow, dismiss }` for the terminal banner of a
27
+ * given batch. `shouldShow` is true only when the batch is fresh
28
+ * (≤ 1h past completedAt) AND not in the dismissed set.
29
+ */
30
+ export declare function useTerminalBannerVisibility(batchId: string | null, completedAt: string | null | undefined): {
31
+ shouldShow: boolean;
32
+ dismiss: () => void;
33
+ };
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Terminal-card freshness + dismissal logic.
3
+ *
4
+ * HUB-3 (v1.2.6): the "Bulk run complete · [Revert this batch]" banner
5
+ * used to persist indefinitely. A batch that completed 8 hours ago kept
6
+ * the destructive Revert button exposed on the Hub Overview. Two fixes
7
+ * applied here:
8
+ *
9
+ * 1. **Auto-hide after 1 hour past `completedAt`.** After 60 minutes
10
+ * the editor has either acked the run or moved on; the row remains
11
+ * reachable from `/admin/translation-runs` for the full 24h revert
12
+ * window. The banner itself just stops being a sticky top-of-Hub
13
+ * element.
14
+ * 2. **Dismiss-and-remember per batch id, per user.** The dismissed
15
+ * list lives in localStorage. Dismissing a batch hides it forever
16
+ * on that browser, including across reloads — the original
17
+ * symptom that prompted the report.
18
+ *
19
+ * The helpers are pure / SSR-safe and exported for unit tests. The
20
+ * `useTerminalBannerVisibility` hook is the integration glue.
21
+ */ import { useCallback, useEffect, useMemo, useState } from 'react';
22
+ const STORAGE_KEY = 'cms-plugins.ai-translate.terminalBannerDismissed';
23
+ /** 1h freshness — long enough that the editor sees the run after lunch
24
+ * but short enough that an 8h-old "Revert this batch" affordance isn't
25
+ * one accidental click away. */ export const TERMINAL_BANNER_FRESHNESS_MS = 60 * 60 * 1000;
26
+ /**
27
+ * Decide whether the terminal banner should still be shown for a batch
28
+ * based on its `completedAt` timestamp and the current time. Returns
29
+ * false for batches with no `completedAt` (defensive — a terminal-status
30
+ * batch should always have one, but we don't want to render a stale
31
+ * card if the field is missing).
32
+ */ export function isTerminalBannerFresh(completedAt, now = new Date(), freshnessMs = TERMINAL_BANNER_FRESHNESS_MS) {
33
+ if (!completedAt) {
34
+ return false;
35
+ }
36
+ const completedMs = new Date(completedAt).getTime();
37
+ if (!Number.isFinite(completedMs)) {
38
+ return false;
39
+ }
40
+ return now.getTime() - completedMs <= freshnessMs;
41
+ }
42
+ /**
43
+ * Read the dismissed-batch-id set from localStorage. Returns an empty
44
+ * set on SSR / quota / parse failure.
45
+ */ export function readDismissedBatchIds() {
46
+ if (typeof window === 'undefined') {
47
+ return new Set();
48
+ }
49
+ try {
50
+ const raw = window.localStorage.getItem(STORAGE_KEY);
51
+ if (!raw) {
52
+ return new Set();
53
+ }
54
+ const parsed = JSON.parse(raw);
55
+ if (!Array.isArray(parsed)) {
56
+ return new Set();
57
+ }
58
+ return new Set(parsed.filter((v)=>typeof v === 'string'));
59
+ } catch {
60
+ return new Set();
61
+ }
62
+ }
63
+ /**
64
+ * Persist the dismissed-batch-id set to localStorage. Bounds the
65
+ * persisted list to the last 50 ids so a power user dismissing dozens
66
+ * of runs over time doesn't grow the entry indefinitely. Swallows any
67
+ * write failures (quota, private-browsing).
68
+ */ export function writeDismissedBatchIds(ids) {
69
+ if (typeof window === 'undefined') {
70
+ return;
71
+ }
72
+ try {
73
+ const list = Array.from(ids).slice(-50);
74
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
75
+ } catch {
76
+ // best-effort — quota / private-browsing failures are non-fatal.
77
+ }
78
+ }
79
+ /**
80
+ * Hook: returns `{ shouldShow, dismiss }` for the terminal banner of a
81
+ * given batch. `shouldShow` is true only when the batch is fresh
82
+ * (≤ 1h past completedAt) AND not in the dismissed set.
83
+ */ export function useTerminalBannerVisibility(batchId, completedAt) {
84
+ const [dismissedIds, setDismissedIds] = useState(()=>new Set());
85
+ // Defer localStorage read to mount so SSR renders deterministically
86
+ // (server can't see localStorage; client hydrates with the persisted
87
+ // set on first effect).
88
+ useEffect(()=>{
89
+ setDismissedIds(readDismissedBatchIds());
90
+ }, []);
91
+ const dismiss = useCallback(()=>{
92
+ if (!batchId) {
93
+ return;
94
+ }
95
+ setDismissedIds((prev)=>{
96
+ if (prev.has(batchId)) {
97
+ return prev;
98
+ }
99
+ const next = new Set(prev);
100
+ next.add(batchId);
101
+ writeDismissedBatchIds(next);
102
+ return next;
103
+ });
104
+ }, [
105
+ batchId
106
+ ]);
107
+ const shouldShow = useMemo(()=>{
108
+ if (!batchId) {
109
+ return false;
110
+ }
111
+ if (dismissedIds.has(batchId)) {
112
+ return false;
113
+ }
114
+ return isTerminalBannerFresh(completedAt ?? null);
115
+ }, [
116
+ batchId,
117
+ completedAt,
118
+ dismissedIds
119
+ ]);
120
+ return {
121
+ shouldShow,
122
+ dismiss
123
+ };
124
+ }
@@ -0,0 +1,49 @@
1
+ import type { BulkTranslateActiveResponse } from './BulkTranslate.types.js';
2
+ interface UseBulkTranslateActiveResult {
3
+ /**
4
+ * The most recent successful response. `null` while loading or after
5
+ * a 404. Distinct from the `loading` flag so consumers can render a
6
+ * skeleton only on the very first request.
7
+ */
8
+ data: BulkTranslateActiveResponse | null;
9
+ /** True only during the first fetch. Subsequent polls update silently. */
10
+ loading: boolean;
11
+ /**
12
+ * Latest error message after a network/server failure. Reset to
13
+ * `null` on the next successful response. The 404 -> empty-batch
14
+ * shortcut does NOT populate this field.
15
+ */
16
+ error: string | null;
17
+ /** True after `OFFLINE_AFTER_FAILED_POLLS` consecutive failures. */
18
+ isOffline: boolean;
19
+ /** Forces an immediate poll. Used by the offline-banner retry button. */
20
+ refetch: () => void;
21
+ }
22
+ interface PollState {
23
+ data: BulkTranslateActiveResponse | null;
24
+ loading: boolean;
25
+ error: string | null;
26
+ failedPollStreak: number;
27
+ }
28
+ /**
29
+ * Acquire a subscription slot for `basePath`. Increments the ref-count
30
+ * and starts polling on first subscriber. Returns an unsubscribe
31
+ * function that decrements + stops polling when the last subscriber
32
+ * unmounts. Exported for the test fixture; consumers should call
33
+ * `useBulkTranslateActive`.
34
+ */
35
+ export declare function subscribeToBulkActive(basePath: string, listener: () => void): () => void;
36
+ /** Test-only: read the current snapshot for a base path. */
37
+ export declare function getBulkActiveSnapshot(basePath: string): PollState;
38
+ /** Test-only: reset the registry between tests. */
39
+ export declare function __resetBulkActiveRegistryForTests(): void;
40
+ /** Test-only: snapshot of refcounts per base path. */
41
+ export declare function __getBulkActiveRefCountsForTests(): Record<string, number>;
42
+ export declare function useBulkTranslateActive(basePath: string): UseBulkTranslateActiveResult;
43
+ /**
44
+ * Exported for tests so they can compare against the same threshold the
45
+ * hook uses internally.
46
+ */
47
+ export declare const BULK_TRANSLATE_OFFLINE_THRESHOLD = 3;
48
+ export declare const BULK_TRANSLATE_POLL_INTERVAL_MS = 5000;
49
+ export {};