@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,233 @@
1
+ import { classifyNode } from './classifier.js';
2
+ import { wrapFormat, wrapLink } from './placeholders.js';
3
+ /**
4
+ * Walks an embedded Payload block (`type: 'block'` Lexical node, surfaced
5
+ * by `BlocksFeature`) in a deterministic order, emitting translation
6
+ * units for strings and recursing into nested Lexical roots / arrays /
7
+ * objects. Walk order MUST match `applyTranslatedBlockData` in the
8
+ * deserializer so blockId sequence stays in sync between serialize and
9
+ * deserialize passes.
10
+ *
11
+ * No full schema awareness yet — the BlocksFeature config isn't
12
+ * reachable from the extractor at runtime. As a defensive heuristic the
13
+ * walker skips strings that look like select-option values (short,
14
+ * lowercase identifiers like `info`, `warning`, `oneThird`) so the LLM
15
+ * doesn't translate them into invalid values that Payload's locale
16
+ * write validator would reject. The top-level blocks-field path
17
+ * (handled by `extractBlockFields` in `lib/content-extractor.ts`) uses
18
+ * full schema-aware gating.
19
+ */ export function serializeBlockData(data, units, nextBlockId, innerWalkNodes) {
20
+ for (const [key, value] of Object.entries(data)){
21
+ if (key === 'id' || key === 'blockType' || key === 'blockName') continue;
22
+ if (typeof value === 'string') {
23
+ if (value.trim().length > 0 && !isLikelySelectOption(value)) {
24
+ units.push({
25
+ blockId: nextBlockId(),
26
+ text: value,
27
+ sourceNode: {
28
+ type: 'block-field',
29
+ text: value
30
+ }
31
+ });
32
+ }
33
+ continue;
34
+ }
35
+ if (isLexicalRootValue(value)) {
36
+ // Nested richText inside the block — recurse with the existing walker
37
+ // so its inline formatting placeholders stay consistent.
38
+ innerWalkNodes(value.root.children ?? []);
39
+ continue;
40
+ }
41
+ if (Array.isArray(value)) {
42
+ for (const item of value){
43
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
44
+ serializeBlockData(item, units, nextBlockId, innerWalkNodes);
45
+ } else if (typeof item === 'string' && item.trim().length > 0) {
46
+ units.push({
47
+ blockId: nextBlockId(),
48
+ text: item,
49
+ sourceNode: {
50
+ type: 'block-field-array',
51
+ text: item
52
+ }
53
+ });
54
+ }
55
+ }
56
+ continue;
57
+ }
58
+ if (value && typeof value === 'object') {
59
+ serializeBlockData(value, units, nextBlockId, innerWalkNodes);
60
+ }
61
+ }
62
+ }
63
+ function isLexicalRootValue(value) {
64
+ if (value === null || typeof value !== 'object') return false;
65
+ const obj = value;
66
+ return 'root' in obj && obj.root !== null && typeof obj.root === 'object';
67
+ }
68
+ /**
69
+ * Heuristic: a short identifier-shaped string is almost certainly a
70
+ * `select` / `radio` option value (e.g. `info`, `warning`, `oneThird`,
71
+ * `primary`, `dark-mode`) — translating it produces a value Payload's
72
+ * locale-write validator will reject. We can't see the block schema
73
+ * from the embedded-Lexical context so this filter is the next best
74
+ * thing.
75
+ *
76
+ * Tradeoff: a real translatable word like `info` (English `info` →
77
+ * Spanish `info`/`información`) stops being translated inside embedded
78
+ * Lexical blocks. Acceptable: nobody writes prose into select-typed
79
+ * fields, and consumers who DO want their short labels translated can
80
+ * still call the manual translate API or use a top-level blocks field
81
+ * (which has full schema-aware gating).
82
+ *
83
+ * Matches:
84
+ * - `^[a-z][a-z0-9_-]{0,20}$` — lowercase identifier 1-21 chars
85
+ * - `^[a-z]+(?:[A-Z][a-z]+)+$` — camelCase 6-30 chars (e.g. oneThird)
86
+ */ function isLikelySelectOption(text) {
87
+ const t = text.trim();
88
+ if (t.length === 0 || t.length > 30) return false;
89
+ // No spaces allowed in select option values.
90
+ if (/\s/.test(t)) return false;
91
+ // Lowercase identifier
92
+ if (/^[a-z][a-z0-9_-]{0,20}$/.test(t)) return true;
93
+ // camelCase identifier
94
+ if (/^[a-z]+(?:[A-Z][a-z0-9]+)+$/.test(t) && t.length <= 30) return true;
95
+ return false;
96
+ }
97
+ // ---------------------------------------------------------------------------
98
+ // Public API
99
+ // ---------------------------------------------------------------------------
100
+ export function serializeLexicalTree(root, registeredNodes) {
101
+ const units = [];
102
+ let counter = 0;
103
+ function nextBlockId() {
104
+ return `blk_${counter++}`;
105
+ }
106
+ function walkNodes(nodes) {
107
+ for (const node of nodes){
108
+ const classified = classifyNode(node, registeredNodes);
109
+ switch(classified.classification){
110
+ case 'recurse':
111
+ {
112
+ // Block-level nodes that contain inline children
113
+ if (node.type === 'paragraph' || node.type === 'heading' || node.type === 'quote' || node.type === 'listitem') {
114
+ const inlineText = serializeInlineChildren(node.children ?? []);
115
+ if (inlineText.trim().length > 0) {
116
+ units.push({
117
+ blockId: nextBlockId(),
118
+ text: inlineText,
119
+ sourceNode: node
120
+ });
121
+ }
122
+ // Also walk children for nested lists inside listitems
123
+ if (node.type === 'listitem' && node.children) {
124
+ for (const child of node.children){
125
+ if (child.type === 'list') {
126
+ walkNodes(child.children ?? []);
127
+ }
128
+ }
129
+ }
130
+ } else if (node.type === 'list') {
131
+ // Lists themselves recurse into their children (listitems)
132
+ walkNodes(node.children ?? []);
133
+ } else {
134
+ // Other recurse nodes (e.g. link called at top level, autolink)
135
+ walkNodes(node.children ?? []);
136
+ }
137
+ break;
138
+ }
139
+ case 'translate-attr':
140
+ {
141
+ if (node.type === 'upload') {
142
+ const alt = getNodeAlt(node);
143
+ if (alt) {
144
+ units.push({
145
+ blockId: nextBlockId(),
146
+ text: alt,
147
+ sourceNode: node
148
+ });
149
+ }
150
+ } else if (classified.translatableAttributes) {
151
+ // Custom registered nodes
152
+ for (const attr of classified.translatableAttributes){
153
+ const value = getNestedAttr(node, attr);
154
+ if (typeof value === 'string' && value.trim().length > 0) {
155
+ units.push({
156
+ blockId: nextBlockId(),
157
+ text: value,
158
+ sourceNode: node
159
+ });
160
+ }
161
+ }
162
+ }
163
+ break;
164
+ }
165
+ case 'block':
166
+ {
167
+ // Lexical's BlocksFeature embeds Payload blocks inline. Walk
168
+ // the block's `fields` and emit translation units the same way
169
+ // we'd handle them at the top-level blocks-array path. Inner
170
+ // Lexical roots (e.g. `banner.content` richText) recurse back
171
+ // through `walkNodes` so their inline placeholders stay in
172
+ // sync with the rest of the document's blockId sequence.
173
+ const blockFields = node.fields ?? {};
174
+ serializeBlockData(blockFields, units, nextBlockId, walkNodes);
175
+ break;
176
+ }
177
+ case 'skip':
178
+ break;
179
+ }
180
+ }
181
+ }
182
+ walkNodes(root.root.children ?? []);
183
+ return units;
184
+ }
185
+ // ---------------------------------------------------------------------------
186
+ // Inline serialization
187
+ // ---------------------------------------------------------------------------
188
+ function serializeInlineChildren(children) {
189
+ return children.map((child)=>serializeInlineNode(child)).join('');
190
+ }
191
+ function serializeInlineNode(node) {
192
+ switch(node.type){
193
+ case 'text':
194
+ {
195
+ const text = node.text ?? '';
196
+ const format = node.format ?? 0;
197
+ return format > 0 ? wrapFormat(text, format) : text;
198
+ }
199
+ case 'link':
200
+ {
201
+ const linkFields = node.fields ?? {};
202
+ const innerText = serializeInlineChildren(node.children ?? []);
203
+ return wrapLink(innerText, linkFields);
204
+ }
205
+ case 'linebreak':
206
+ return '\n';
207
+ case 'list':
208
+ // Nested lists inside listitems are handled at the block walk level
209
+ return '';
210
+ default:
211
+ // For any unknown inline node, try to serialize its children
212
+ if (node.children) {
213
+ return serializeInlineChildren(node.children);
214
+ }
215
+ return '';
216
+ }
217
+ }
218
+ // ---------------------------------------------------------------------------
219
+ // Attribute helpers
220
+ // ---------------------------------------------------------------------------
221
+ function getNodeAlt(node) {
222
+ const fromFields = node.fields?.alt;
223
+ const fromNode = node.alt;
224
+ const alt = fromFields ?? fromNode;
225
+ return typeof alt === 'string' ? alt : undefined;
226
+ }
227
+ function getNestedAttr(node, attr) {
228
+ // Check fields first, then the node itself
229
+ if (node.fields && attr in node.fields) {
230
+ return node.fields[attr];
231
+ }
232
+ return node[attr];
233
+ }
@@ -0,0 +1,32 @@
1
+ export type LexicalNode = {
2
+ type: string;
3
+ children?: LexicalNode[];
4
+ text?: string;
5
+ format?: number;
6
+ tag?: string;
7
+ listType?: string;
8
+ fields?: Record<string, unknown>;
9
+ [key: string]: unknown;
10
+ };
11
+ export type LexicalRoot = {
12
+ root: LexicalNode;
13
+ };
14
+ export type NodeClassification = 'recurse' | 'translate-attr' | 'skip'
15
+ /**
16
+ * Lexical's `BlocksFeature` embeds Payload blocks inline (`type: 'block'`,
17
+ * `fields: { blockType, ... }`). Treated as a separate classification so
18
+ * the serializer/deserializer can walk the block's fields the same way
19
+ * the top-level `extractBlockFields` walker does, instead of treating
20
+ * the embedded block as opaque.
21
+ */
22
+ | 'block';
23
+ export type ClassifiedNode = {
24
+ node: LexicalNode;
25
+ classification: NodeClassification;
26
+ translatableAttributes?: string[];
27
+ };
28
+ export type SerializedUnit = {
29
+ blockId: string;
30
+ text: string;
31
+ sourceNode: LexicalNode;
32
+ };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,14 @@
1
+ import type { PayloadRequest } from 'payload';
2
+ /**
3
+ * Build a one-line diagnostic explaining *why* a request was rejected with
4
+ * 401 by `if (!req.user) return 401` gates. Payload's `extractJWT.cookie`
5
+ * silently returns `null` when the request lacks an allowlisted `Origin`
6
+ * AND lacks an acceptable `Sec-Fetch-Site` header — most commonly because
7
+ * the page was opened via Next's `Network:` URL (`127.0.x.x`) instead of
8
+ * `localhost`, which the consumer's `csrf` allowlist doesn't include.
9
+ *
10
+ * Surfacing the cookie/Origin/Sec-Fetch-Site triple in the response body
11
+ * lets the next dev land on the root cause without trawling Payload core.
12
+ * Kept terse — adds maybe 80 bytes to a 401 response.
13
+ */
14
+ export declare function describeAuthRejection(req: PayloadRequest): string;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Build a one-line diagnostic explaining *why* a request was rejected with
3
+ * 401 by `if (!req.user) return 401` gates. Payload's `extractJWT.cookie`
4
+ * silently returns `null` when the request lacks an allowlisted `Origin`
5
+ * AND lacks an acceptable `Sec-Fetch-Site` header — most commonly because
6
+ * the page was opened via Next's `Network:` URL (`127.0.x.x`) instead of
7
+ * `localhost`, which the consumer's `csrf` allowlist doesn't include.
8
+ *
9
+ * Surfacing the cookie/Origin/Sec-Fetch-Site triple in the response body
10
+ * lets the next dev land on the root cause without trawling Payload core.
11
+ * Kept terse — adds maybe 80 bytes to a 401 response.
12
+ */ export function describeAuthRejection(req) {
13
+ const cookie = req.headers?.get?.('cookie') ?? '';
14
+ const hasCookie = cookie.includes('payload-token');
15
+ const origin = req.headers?.get?.('origin') ?? null;
16
+ const sfs = req.headers?.get?.('sec-fetch-site') ?? null;
17
+ if (!hasCookie) return 'No payload-token cookie sent.';
18
+ return `Cookie present but rejected. Origin=${origin ?? '(none)'} Sec-Fetch-Site=${sfs ?? '(none)'}. Add Origin to payload.config.csrf or open the admin via an allowlisted host (commonly the "Network:" URL printed by Next dev is the trigger).`;
19
+ }
@@ -0,0 +1,58 @@
1
+ import type { Payload } from 'payload';
2
+ /**
3
+ * Source-of-truth counts for one or more bulk-translate batches.
4
+ *
5
+ * The plugin used to read `batch.completedUnits` / `failedUnits` /
6
+ * `skippedUnits` straight off the batch row — populated by atomic SQL
7
+ * bumps in the worker (`completed_units = completed_units + 1` on each
8
+ * unit transition). That worked for the happy path but drifted on:
9
+ * - Retries (`retry-failed` flips `failed → pending` without
10
+ * decrementing the previous failure's contribution).
11
+ * - Admin SDK interventions (manually marking a `success` unit as
12
+ * `failed`, then retrying — the original success increment stays
13
+ * and a new one fires when the retry succeeds, so `completedUnits`
14
+ * can exceed `totalUnits`).
15
+ * - Cancel paths (`pending → skipped` writes the unit row but
16
+ * skipping bumps the counter via a separate path that can race).
17
+ *
18
+ * Real fix per editor feedback: API endpoints never trust the cached
19
+ * counters. They always recompute from the underlying
20
+ * `bulk_translate_units` table. The cached counters stay as an
21
+ * internal optimization for the worker's "is this batch done?"
22
+ * transition check, but they're no longer authoritative.
23
+ *
24
+ * One query per call regardless of how many batches: a single `find`
25
+ * with `where { batchId: { in: [...] } }` and a narrow `select`
26
+ * projects (`batchId`, `status`) only. JS aggregates the counts.
27
+ * Realistic plugin batches (≤1000 units each, ~10 batches per list
28
+ * page) finish well under 100ms.
29
+ *
30
+ * For very large batches (10k+ units each), this approach still works
31
+ * but a switch to `drizzle.execute(sql\`SELECT batch_id_id, status,
32
+ * COUNT(*) ... GROUP BY 1, 2\`)` would be O(1) on the wire. We don't
33
+ * ship that today because drizzle is an optional peer dep.
34
+ */
35
+ export type LiveBatchCounts = {
36
+ total: number;
37
+ pending: number;
38
+ running: number;
39
+ /** Worker-`success` count. Exposed to the API as `completed`. */
40
+ completed: number;
41
+ failed: number;
42
+ skipped: number;
43
+ reverted: number;
44
+ };
45
+ /**
46
+ * Compute fresh status counts for every batch id passed in.
47
+ *
48
+ * Returns a Map keyed by stringified batch id. Batches with no units
49
+ * (newly enqueued or anomalously empty) map to the zero record so
50
+ * callers don't have to null-check.
51
+ */
52
+ export declare function computeLiveBatchCounts(payload: Payload, unitsSlug: string, batchIds: ReadonlyArray<string | number>): Promise<Map<string, LiveBatchCounts>>;
53
+ /**
54
+ * Convenience wrapper for the single-batch case (status endpoint,
55
+ * worker transition check). Returns the zero record if the batch has
56
+ * no units (anomalous but should never crash).
57
+ */
58
+ export declare function computeLiveBatchCount(payload: Payload, unitsSlug: string, batchId: string): Promise<LiveBatchCounts>;
@@ -0,0 +1,105 @@
1
+ import { getDrizzle, slugToTable } from './bulk-translate-migrations.js';
2
+ const ZERO_COUNTS = {
3
+ total: 0,
4
+ pending: 0,
5
+ running: 0,
6
+ completed: 0,
7
+ failed: 0,
8
+ skipped: 0,
9
+ reverted: 0
10
+ };
11
+ /**
12
+ * Compute fresh status counts for every batch id passed in.
13
+ *
14
+ * Returns a Map keyed by stringified batch id. Batches with no units
15
+ * (newly enqueued or anomalously empty) map to the zero record so
16
+ * callers don't have to null-check.
17
+ */ export async function computeLiveBatchCounts(payload, unitsSlug, batchIds) {
18
+ const out = new Map();
19
+ if (batchIds.length === 0) return out;
20
+ // Seed every requested batchId so the caller can lookup without
21
+ // checking for undefined.
22
+ for (const id of batchIds){
23
+ out.set(String(id), {
24
+ ...ZERO_COUNTS
25
+ });
26
+ }
27
+ const tally = (bid, status, n)=>{
28
+ const counts = out.get(bid);
29
+ if (!counts) return;
30
+ counts.total += n;
31
+ if (status === 'pending') counts.pending += n;
32
+ else if (status === 'running') counts.running += n;
33
+ else if (status === 'success') counts.completed += n;
34
+ else if (status === 'failed') counts.failed += n;
35
+ else if (status === 'skipped') counts.skipped += n;
36
+ else if (status === 'reverted') counts.reverted += n;
37
+ };
38
+ // Fast path: one GROUP BY on Postgres — O(1) on the wire no matter
39
+ // how many units the batches hold. This matters far more than the
40
+ // original comment assumed: `depth: 0` does NOT drop the
41
+ // `pre_run_snapshot` jsonb column, so the row-fetch path was moving
42
+ // the entire snapshot payload (tens of KB × every unit) through
43
+ // Postgres TOAST + JSON.parse on EVERY Hub poll. On an 11.5k-unit
44
+ // batch that measured 7-12s per poll and was a driver of the
45
+ // 2026-06-10 prod outage.
46
+ if (batchIds.every((id)=>/^\d+$/.test(String(id)))) {
47
+ const db = getDrizzle(payload);
48
+ if (db) {
49
+ try {
50
+ const res = await db.execute(`SELECT batch_id_id AS bid, status, count(*) AS n
51
+ FROM ${slugToTable(unitsSlug)}
52
+ WHERE batch_id_id IN (${batchIds.map((id)=>String(id)).join(', ')})
53
+ GROUP BY 1, 2`);
54
+ for (const row of res?.rows ?? []){
55
+ tally(String(row.bid), String(row.status), Number(row.n ?? 0));
56
+ }
57
+ return out;
58
+ } catch (err) {
59
+ payload.logger?.warn?.(`[ai-translate] computeLiveBatchCounts: grouped-count SQL failed, falling back to row scan: ${err instanceof Error ? err.message : String(err)}`);
60
+ }
61
+ }
62
+ }
63
+ // Fallback (non-Postgres adapters / non-numeric ids): paginated row
64
+ // scan, projected to just (batchId, status) so the snapshot jsonb
65
+ // never leaves the database.
66
+ let page = 1;
67
+ const limit = 5000;
68
+ // eslint-disable-next-line no-constant-condition
69
+ while(true){
70
+ const result = await payload.find({
71
+ collection: unitsSlug,
72
+ where: {
73
+ batchId: {
74
+ in: batchIds.map((id)=>String(id))
75
+ }
76
+ },
77
+ page,
78
+ limit,
79
+ depth: 0,
80
+ select: {
81
+ batchId: true,
82
+ status: true
83
+ },
84
+ overrideAccess: true
85
+ });
86
+ for (const doc of result.docs){
87
+ tally(String(doc.batchId ?? ''), String(doc.status ?? ''), 1);
88
+ }
89
+ if (!result.hasNextPage) break;
90
+ page += 1;
91
+ }
92
+ return out;
93
+ }
94
+ /**
95
+ * Convenience wrapper for the single-batch case (status endpoint,
96
+ * worker transition check). Returns the zero record if the batch has
97
+ * no units (anomalous but should never crash).
98
+ */ export async function computeLiveBatchCount(payload, unitsSlug, batchId) {
99
+ const map = await computeLiveBatchCounts(payload, unitsSlug, [
100
+ batchId
101
+ ]);
102
+ return map.get(batchId) ?? {
103
+ ...ZERO_COUNTS
104
+ };
105
+ }
@@ -0,0 +1,92 @@
1
+ import type { Payload } from 'payload';
2
+ /**
3
+ * Idempotent schema-shaping helpers for the bulk-translate engine
4
+ * (PR2 / v1.2.0). Payload's auto-migration generator handles plain
5
+ * column / index changes from collection definitions, but the bulk
6
+ * engine needs:
7
+ *
8
+ * 1. A PARTIAL UNIQUE INDEX on
9
+ * `bulk_translate_units (collection, document_id, locale)
10
+ * WHERE status IN ('pending','running')`. This enforces the
11
+ * F-DA-TOCTOU dedup invariant at the DB level (a second worker
12
+ * cannot insert / upsert a unit for an already-active locale).
13
+ * Payload's index generator does not emit `WHERE`-clauses, so
14
+ * this is hand-written SQL.
15
+ *
16
+ * 2. A composite UNIQUE constraint on
17
+ * `translation_daily_spend (date, consumer_key)` to back the
18
+ * cap utility's atomic upsert. Payload's index generator can
19
+ * emit this from the collection definition, but on existing
20
+ * installs the rename may not propagate — calling this helper
21
+ * ensures it.
22
+ *
23
+ * Both run via `payload.db.drizzle.execute(sql\`...\`)`. SQL uses
24
+ * `CREATE ... IF NOT EXISTS` so the same call across many boots is
25
+ * a no-op after the first success.
26
+ *
27
+ * Two ways to use:
28
+ * - Add to consumer's migration file (recommended for clean
29
+ * migration history): import and call from inside an `up`
30
+ * function.
31
+ * - Call from the plugin's `onInit` (when `bulk.enabled` is true) —
32
+ * idempotent boot-time self-healing. Trade-off: errors at boot
33
+ * surface as plugin failures rather than as migration failures.
34
+ */
35
+ export interface EnsureBulkTranslateSchemaOptions {
36
+ /**
37
+ * Override the collection slugs if the consumer customised them
38
+ * via plugin config. Defaults pull from the collection modules.
39
+ */
40
+ bulkTranslateUnitsSlug?: string;
41
+ translationDailySpendSlug?: string;
42
+ }
43
+ export declare function ensureBulkTranslateSchema(payload: Payload, opts?: EnsureBulkTranslateSchemaOptions): Promise<void>;
44
+ /**
45
+ * One-off data migration: prefix existing `ai-translate-meta`
46
+ * source-hash rows with the `v1:` version tag (R2 / F-ENG-07).
47
+ *
48
+ * Without this, the first bulk run after a plugin upgrade would treat
49
+ * every previously-translated field as "source changed" — burning
50
+ * tokens to re-translate everything that was already in sync.
51
+ *
52
+ * Idempotent: rows that already start with `v1:` are skipped. Safe
53
+ * to run repeatedly. Consumer should run ONCE per deploy that bumps
54
+ * past 1.1.16. Returns a count of rows updated.
55
+ */
56
+ export declare function migrateHashesToV1Prefix(payload: Payload, opts?: {
57
+ metaSlug?: string;
58
+ dryRun?: boolean;
59
+ }): Promise<{
60
+ scanned: number;
61
+ updated: number;
62
+ alreadyMigrated: number;
63
+ }>;
64
+ /**
65
+ * v1.2.4 data migration: align `bulk_translate_batches.status` vocabulary
66
+ * on `'success'` (matching the units collection + the
67
+ * `BulkTranslateBatchStatus` TS union). Pre-1.2.4 the schema declared
68
+ * `'completed'` as the terminal-success value while the type union used
69
+ * `'success'`, so badges, filter chips, and the Hub's "recently
70
+ * terminal" query were all silently dead for successful runs. Worker
71
+ * + schema now write `'success'`; this helper backfills existing rows.
72
+ *
73
+ * Idempotent: only updates rows where status = 'completed'. Safe to run
74
+ * repeatedly; subsequent calls are no-ops.
75
+ *
76
+ * Consumer wiring:
77
+ * - Add to a forward migration's up() OR
78
+ * - Call from plugin onInit() once per deploy (boot-time self-heal).
79
+ */
80
+ export declare function migrateBatchStatusVocabulary(payload: Payload, opts?: {
81
+ batchesSlug?: string;
82
+ dryRun?: boolean;
83
+ }): Promise<{
84
+ scanned: number;
85
+ updated: number;
86
+ alreadyAligned: number;
87
+ }>;
88
+ export declare function slugToTable(slug: string): string;
89
+ export type Drizzle = {
90
+ execute(sql: string): Promise<unknown>;
91
+ };
92
+ export declare function getDrizzle(payload: Payload): Drizzle | null;