@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,108 @@
1
+ const RECURSE_TYPES = new Set([
2
+ 'paragraph',
3
+ 'heading',
4
+ 'quote',
5
+ 'listitem',
6
+ 'list',
7
+ 'link'
8
+ ]);
9
+ const SKIP_TYPES = new Set([
10
+ 'text',
11
+ 'linebreak',
12
+ 'code',
13
+ 'horizontalrule'
14
+ ]);
15
+ export function classifyNode(node, registeredNodes) {
16
+ const { type } = node;
17
+ // Built-in recurse types
18
+ if (RECURSE_TYPES.has(type)) {
19
+ return {
20
+ node,
21
+ classification: 'recurse'
22
+ };
23
+ }
24
+ // Built-in skip types
25
+ if (SKIP_TYPES.has(type)) {
26
+ return {
27
+ node,
28
+ classification: 'skip'
29
+ };
30
+ }
31
+ // Lexical's `BlocksFeature` embeds Payload blocks inline as
32
+ // `type: 'block'` nodes with `fields: { blockType, ...blockData }`.
33
+ // Without this branch the classifier falls through to the unknown-type
34
+ // path and silently skips the entire block — losing every translatable
35
+ // field inside it (banner.title, banner.content, etc.).
36
+ if (type === 'block') {
37
+ return {
38
+ node,
39
+ classification: 'block'
40
+ };
41
+ }
42
+ // Upload / image — translate alt text if present
43
+ if (type === 'upload') {
44
+ const alt = node.fields?.alt ?? node.alt;
45
+ if (alt) {
46
+ return {
47
+ node,
48
+ classification: 'translate-attr',
49
+ translatableAttributes: [
50
+ 'alt'
51
+ ]
52
+ };
53
+ }
54
+ return {
55
+ node,
56
+ classification: 'skip'
57
+ };
58
+ }
59
+ // Autolink — skip when the visible text equals the URL
60
+ if (type === 'autolink') {
61
+ const url = node.fields?.url;
62
+ const childText = collectText(node.children);
63
+ if (url && childText === url) {
64
+ return {
65
+ node,
66
+ classification: 'skip'
67
+ };
68
+ }
69
+ return {
70
+ node,
71
+ classification: 'recurse'
72
+ };
73
+ }
74
+ // Custom / registered nodes
75
+ if (registeredNodes?.length) {
76
+ const registration = registeredNodes.find((r)=>r.type === type);
77
+ if (registration) {
78
+ if (registration.translatable && registration.translatableAttributes?.length) {
79
+ return {
80
+ node,
81
+ classification: 'translate-attr',
82
+ translatableAttributes: registration.translatableAttributes
83
+ };
84
+ }
85
+ return {
86
+ node,
87
+ classification: 'skip'
88
+ };
89
+ }
90
+ }
91
+ // Unknown node type — skip with warning
92
+ console.warn(`[ai-translate] Unknown Lexical node type "${type}" — skipping. Register it via lexicalNodes config to translate its attributes.`);
93
+ return {
94
+ node,
95
+ classification: 'skip'
96
+ };
97
+ }
98
+ // ---------------------------------------------------------------------------
99
+ // Helpers
100
+ // ---------------------------------------------------------------------------
101
+ function collectText(children) {
102
+ if (!children) return '';
103
+ return children.map((child)=>{
104
+ if (child.type === 'text') return child.text ?? '';
105
+ if (child.children) return collectText(child.children);
106
+ return '';
107
+ }).join('');
108
+ }
@@ -0,0 +1,4 @@
1
+ import type { LexicalNodeRegistration } from '../types.js';
2
+ import type { LexicalRoot } from './types.js';
3
+ export declare function deserializeLexicalTree(originalRoot: LexicalRoot, translatedUnits: Map<string, string>, registeredNodes?: LexicalNodeRegistration[]): LexicalRoot;
4
+ export { validatePlaceholderIntegrity } from './placeholder-integrity.js';
@@ -0,0 +1,263 @@
1
+ import { classifyNode } from './classifier.js';
2
+ import { parsePlaceholders } from './placeholders.js';
3
+ import { serializeLexicalTree } from './serializer.js';
4
+ // ---------------------------------------------------------------------------
5
+ // Public API
6
+ // ---------------------------------------------------------------------------
7
+ export function deserializeLexicalTree(originalRoot, translatedUnits, registeredNodes) {
8
+ const cloned = structuredClone(originalRoot);
9
+ // Build the same blockId sequence the serializer would produce
10
+ const originalUnits = serializeLexicalTree(originalRoot, registeredNodes);
11
+ const unitMap = new Map();
12
+ for (const unit of originalUnits){
13
+ unitMap.set(unit.blockId, unit);
14
+ }
15
+ // Walk the cloned tree in the same order as serialization
16
+ let counter = 0;
17
+ function nextBlockId() {
18
+ return `blk_${counter++}`;
19
+ }
20
+ function walkNodes(nodes) {
21
+ for (const node of nodes){
22
+ const classified = classifyNode(node, registeredNodes);
23
+ switch(classified.classification){
24
+ case 'recurse':
25
+ {
26
+ if (node.type === 'paragraph' || node.type === 'heading' || node.type === 'quote' || node.type === 'listitem') {
27
+ const hasInlineContent = hasNonListChildren(node);
28
+ if (hasInlineContent) {
29
+ const blockId = nextBlockId();
30
+ const translated = translatedUnits.get(blockId);
31
+ if (translated !== undefined) {
32
+ const parsed = parsePlaceholders(translated);
33
+ // Preserve nested lists — they aren't part of inline content
34
+ const nestedLists = (node.children ?? []).filter((c)=>c.type === 'list');
35
+ const rebuilt = segmentsToNodes(parsed);
36
+ node.children = [
37
+ ...rebuilt,
38
+ ...nestedLists
39
+ ];
40
+ }
41
+ }
42
+ // Walk nested lists inside listitems
43
+ if (node.type === 'listitem' && node.children) {
44
+ for (const child of node.children){
45
+ if (child.type === 'list') {
46
+ walkNodes(child.children ?? []);
47
+ }
48
+ }
49
+ }
50
+ } else if (node.type === 'list') {
51
+ walkNodes(node.children ?? []);
52
+ } else {
53
+ walkNodes(node.children ?? []);
54
+ }
55
+ break;
56
+ }
57
+ case 'translate-attr':
58
+ {
59
+ if (node.type === 'upload') {
60
+ const alt = getNodeAlt(node);
61
+ if (alt) {
62
+ const blockId = nextBlockId();
63
+ const translated = translatedUnits.get(blockId);
64
+ if (translated !== undefined) {
65
+ setNodeAlt(node, translated);
66
+ }
67
+ }
68
+ } else if (classified.translatableAttributes) {
69
+ for (const attr of classified.translatableAttributes){
70
+ const value = getNestedAttr(node, attr);
71
+ if (typeof value === 'string' && value.trim().length > 0) {
72
+ const blockId = nextBlockId();
73
+ const translated = translatedUnits.get(blockId);
74
+ if (translated !== undefined) {
75
+ setNestedAttr(node, attr, translated);
76
+ }
77
+ }
78
+ }
79
+ }
80
+ break;
81
+ }
82
+ case 'block':
83
+ {
84
+ // Mirror of the serializer's 'block' case — walk the embedded
85
+ // block's `fields` in the SAME deterministic order so blockIds
86
+ // line up. Translated values are mutated in-place on the
87
+ // cloned node.
88
+ const blockFields = node.fields ?? {};
89
+ applyTranslatedBlockData(blockFields, translatedUnits, nextBlockId, walkNodes);
90
+ break;
91
+ }
92
+ case 'skip':
93
+ break;
94
+ }
95
+ }
96
+ }
97
+ walkNodes(cloned.root.children ?? []);
98
+ return cloned;
99
+ }
100
+ export { validatePlaceholderIntegrity } from './placeholder-integrity.js';
101
+ // ---------------------------------------------------------------------------
102
+ // Segment → LexicalNode conversion
103
+ // ---------------------------------------------------------------------------
104
+ function segmentsToNodes(segments) {
105
+ const nodes = [];
106
+ for (const segment of segments){
107
+ switch(segment.type){
108
+ case 'text':
109
+ {
110
+ if (segment.content.length > 0) {
111
+ nodes.push({
112
+ type: 'text',
113
+ text: segment.content,
114
+ format: 0
115
+ });
116
+ }
117
+ break;
118
+ }
119
+ case 'format':
120
+ {
121
+ // Each child text segment becomes a text node with this format
122
+ const formatChildren = flattenToTextSegments(segment.children);
123
+ for (const child of formatChildren){
124
+ nodes.push({
125
+ type: 'text',
126
+ text: child.content,
127
+ format: segment.format
128
+ });
129
+ }
130
+ break;
131
+ }
132
+ case 'link':
133
+ {
134
+ nodes.push({
135
+ type: 'link',
136
+ fields: segment.fields,
137
+ children: segmentsToNodes(segment.children)
138
+ });
139
+ break;
140
+ }
141
+ }
142
+ }
143
+ return nodes;
144
+ }
145
+ /**
146
+ * Flattens nested parsed segments down to text segments, preserving text
147
+ * content from format/link children. This is used when a format placeholder
148
+ * contains other segments — we extract all leaf text.
149
+ */ function flattenToTextSegments(segments) {
150
+ const result = [];
151
+ for (const segment of segments){
152
+ switch(segment.type){
153
+ case 'text':
154
+ result.push({
155
+ content: segment.content
156
+ });
157
+ break;
158
+ case 'format':
159
+ case 'link':
160
+ result.push(...flattenToTextSegments(segment.children));
161
+ break;
162
+ }
163
+ }
164
+ return result;
165
+ }
166
+ // ---------------------------------------------------------------------------
167
+ // Helpers
168
+ // ---------------------------------------------------------------------------
169
+ function hasNonListChildren(node) {
170
+ if (!node.children || node.children.length === 0) return false;
171
+ return node.children.some((c)=>c.type !== 'list');
172
+ }
173
+ function getNodeAlt(node) {
174
+ const fromFields = node.fields?.alt;
175
+ const fromNode = node.alt;
176
+ const alt = fromFields ?? fromNode;
177
+ return typeof alt === 'string' ? alt : undefined;
178
+ }
179
+ function setNodeAlt(node, value) {
180
+ if (node.fields && 'alt' in node.fields) {
181
+ node.fields.alt = value;
182
+ } else {
183
+ node.alt = value;
184
+ }
185
+ }
186
+ function getNestedAttr(node, attr) {
187
+ if (node.fields && attr in node.fields) {
188
+ return node.fields[attr];
189
+ }
190
+ return node[attr];
191
+ }
192
+ function setNestedAttr(node, attr, value) {
193
+ if (node.fields && attr in node.fields) {
194
+ node.fields[attr] = value;
195
+ } else {
196
+ node[attr] = value;
197
+ }
198
+ }
199
+ /**
200
+ * Mirror walk of `serializeBlockData` in `serializer.ts`. Visit keys in
201
+ * the same `Object.entries` order, consume blockIds in the same order,
202
+ * and replace translated strings + nested Lexical roots in place. Keep
203
+ * the implementations in lockstep — any divergence drops translations
204
+ * silently.
205
+ */ function applyTranslatedBlockData(data, translatedUnits, nextBlockId, innerWalkNodes) {
206
+ for (const [key, value] of Object.entries(data)){
207
+ if (key === 'id' || key === 'blockType' || key === 'blockName') continue;
208
+ if (typeof value === 'string') {
209
+ // Mirror the serializer's skip — select-option-shaped strings
210
+ // weren't emitted as translation units, so don't consume a
211
+ // blockId for them here either, or the sequence will desync.
212
+ if (value.trim().length > 0 && !isLikelySelectOption(value)) {
213
+ const blockId = nextBlockId();
214
+ const translated = translatedUnits.get(blockId);
215
+ if (translated !== undefined) {
216
+ data[key] = translated;
217
+ }
218
+ }
219
+ continue;
220
+ }
221
+ if (isLexicalRootValue(value)) {
222
+ // Recurse via the outer walker so nested richText placeholders
223
+ // share the parent's blockId sequence + reuse all the existing
224
+ // inline-format/link handling.
225
+ innerWalkNodes(value.root.children ?? []);
226
+ continue;
227
+ }
228
+ if (Array.isArray(value)) {
229
+ for(let i = 0; i < value.length; i++){
230
+ const item = value[i];
231
+ if (item && typeof item === 'object' && !Array.isArray(item)) {
232
+ applyTranslatedBlockData(item, translatedUnits, nextBlockId, innerWalkNodes);
233
+ } else if (typeof item === 'string' && item.trim().length > 0) {
234
+ const blockId = nextBlockId();
235
+ const translated = translatedUnits.get(blockId);
236
+ if (translated !== undefined) {
237
+ value[i] = translated;
238
+ }
239
+ }
240
+ }
241
+ continue;
242
+ }
243
+ if (value && typeof value === 'object') {
244
+ applyTranslatedBlockData(value, translatedUnits, nextBlockId, innerWalkNodes);
245
+ }
246
+ }
247
+ }
248
+ function isLexicalRootValue(value) {
249
+ if (value === null || typeof value !== 'object') return false;
250
+ const obj = value;
251
+ return 'root' in obj && obj.root !== null && typeof obj.root === 'object';
252
+ }
253
+ /**
254
+ * Mirror of `serializer.isLikelySelectOption`. Keep the regexes in
255
+ * lockstep — divergence would desync the blockId sequence.
256
+ */ function isLikelySelectOption(text) {
257
+ const t = text.trim();
258
+ if (t.length === 0 || t.length > 30) return false;
259
+ if (/\s/.test(t)) return false;
260
+ if (/^[a-z][a-z0-9_-]{0,20}$/.test(t)) return true;
261
+ if (/^[a-z]+(?:[A-Z][a-z0-9]+)+$/.test(t) && t.length <= 30) return true;
262
+ return false;
263
+ }
@@ -0,0 +1,6 @@
1
+ export type PlaceholderIntegrityResult = {
2
+ valid: boolean;
3
+ missingIds: string[];
4
+ extraIds: string[];
5
+ };
6
+ export declare function validatePlaceholderIntegrity(original: string, translated: string): PlaceholderIntegrityResult;
@@ -0,0 +1,21 @@
1
+ const PH_ID_REGEX = /<ph id="([^"]+)">/g;
2
+ export function validatePlaceholderIntegrity(original, translated) {
3
+ const originalIds = extractPlaceholderIds(original);
4
+ const translatedIds = extractPlaceholderIds(translated);
5
+ const missingIds = originalIds.filter((id)=>!translatedIds.includes(id));
6
+ const extraIds = translatedIds.filter((id)=>!originalIds.includes(id));
7
+ return {
8
+ valid: missingIds.length === 0 && extraIds.length === 0,
9
+ missingIds,
10
+ extraIds
11
+ };
12
+ }
13
+ function extractPlaceholderIds(text) {
14
+ const ids = [];
15
+ let match;
16
+ const regex = new RegExp(PH_ID_REGEX.source, 'g');
17
+ while((match = regex.exec(text)) !== null){
18
+ ids.push(match[1]);
19
+ }
20
+ return ids;
21
+ }
@@ -0,0 +1,21 @@
1
+ export type ParsedSegment = {
2
+ type: 'text';
3
+ content: string;
4
+ } | {
5
+ type: 'format';
6
+ format: number;
7
+ children: ParsedSegment[];
8
+ } | {
9
+ type: 'link';
10
+ fields: Record<string, unknown>;
11
+ children: ParsedSegment[];
12
+ };
13
+ export declare function encodeLinkData(fields: Record<string, unknown>): string;
14
+ export declare function decodeLinkData(encoded: string): Record<string, unknown>;
15
+ export declare function wrapFormat(text: string, format: number): string;
16
+ export declare function wrapLink(text: string, linkFields: Record<string, unknown>): string;
17
+ /**
18
+ * Parses a placeholder-tagged string into a tree of `ParsedSegment` nodes.
19
+ * Handles arbitrary nesting (e.g. a link containing format spans).
20
+ */
21
+ export declare function parsePlaceholders(input: string): ParsedSegment[];
@@ -0,0 +1,117 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Encoding helpers
3
+ // ---------------------------------------------------------------------------
4
+ export function encodeLinkData(fields) {
5
+ return Buffer.from(JSON.stringify(fields)).toString('base64url');
6
+ }
7
+ export function decodeLinkData(encoded) {
8
+ return JSON.parse(Buffer.from(encoded, 'base64url').toString());
9
+ }
10
+ // ---------------------------------------------------------------------------
11
+ // Wrap helpers — produce placeholder-tagged strings
12
+ // ---------------------------------------------------------------------------
13
+ export function wrapFormat(text, format) {
14
+ return `<ph id="f:${format}">${text}</ph>`;
15
+ }
16
+ export function wrapLink(text, linkFields) {
17
+ return `<ph id="l:${encodeLinkData(linkFields)}">${text}</ph>`;
18
+ }
19
+ // ---------------------------------------------------------------------------
20
+ // Recursive placeholder parser
21
+ // ---------------------------------------------------------------------------
22
+ const PH_OPEN = /<ph id="(f|l):([^"]+)">/;
23
+ const PH_CLOSE = '</ph>';
24
+ /**
25
+ * Parses a placeholder-tagged string into a tree of `ParsedSegment` nodes.
26
+ * Handles arbitrary nesting (e.g. a link containing format spans).
27
+ */ export function parsePlaceholders(input) {
28
+ const segments = [];
29
+ let cursor = 0;
30
+ while(cursor < input.length){
31
+ // Check for closing tag first — signals the end of a recursive call
32
+ if (input.startsWith(PH_CLOSE, cursor)) {
33
+ break;
34
+ }
35
+ // Try to match an opening tag at the current position
36
+ const remaining = input.slice(cursor);
37
+ const openMatch = PH_OPEN.exec(remaining);
38
+ if (!openMatch || openMatch.index !== 0) {
39
+ // Consume text up to the next opening tag or closing tag or end
40
+ const nextOpen = remaining.search(PH_OPEN);
41
+ const nextClose = remaining.indexOf(PH_CLOSE);
42
+ let textEnd;
43
+ if (nextOpen === -1 && nextClose === -1) {
44
+ textEnd = remaining.length;
45
+ } else if (nextOpen === -1) {
46
+ textEnd = nextClose;
47
+ } else if (nextClose === -1) {
48
+ textEnd = nextOpen;
49
+ } else {
50
+ textEnd = Math.min(nextOpen, nextClose);
51
+ }
52
+ if (textEnd > 0) {
53
+ segments.push({
54
+ type: 'text',
55
+ content: remaining.slice(0, textEnd)
56
+ });
57
+ cursor += textEnd;
58
+ }
59
+ continue;
60
+ }
61
+ // We matched an opening tag at position 0
62
+ const [fullMatch, kind, data] = openMatch;
63
+ cursor += fullMatch.length;
64
+ // Recursively parse children until we hit the matching </ph>
65
+ const childResult = parsePlaceholders(input.slice(cursor));
66
+ const childrenConsumed = measureConsumed(input, cursor, childResult);
67
+ cursor += childrenConsumed;
68
+ // Consume the closing </ph>
69
+ if (input.startsWith(PH_CLOSE, cursor)) {
70
+ cursor += PH_CLOSE.length;
71
+ }
72
+ if (kind === 'f') {
73
+ segments.push({
74
+ type: 'format',
75
+ format: Number(data),
76
+ children: childResult
77
+ });
78
+ } else {
79
+ segments.push({
80
+ type: 'link',
81
+ fields: decodeLinkData(data),
82
+ children: childResult
83
+ });
84
+ }
85
+ }
86
+ return segments;
87
+ }
88
+ /**
89
+ * Determines how many characters the recursive `parsePlaceholders` call
90
+ * consumed from the input starting at `offset`.
91
+ *
92
+ * We re-serialize the parsed children and match that length, but to avoid
93
+ * ambiguity we simply scan forward from `offset` until we find the matching
94
+ * `</ph>` at nesting depth 0.
95
+ */ function measureConsumed(input, offset, _children) {
96
+ let depth = 0;
97
+ let i = offset;
98
+ while(i < input.length){
99
+ // Check for closing tag
100
+ if (input.startsWith(PH_CLOSE, i)) {
101
+ if (depth === 0) return i - offset;
102
+ depth--;
103
+ i += PH_CLOSE.length;
104
+ continue;
105
+ }
106
+ // Check for opening tag (increases nesting depth)
107
+ const remaining = input.slice(i);
108
+ const openMatch = PH_OPEN.exec(remaining);
109
+ if (openMatch && openMatch.index === 0) {
110
+ depth++;
111
+ i += openMatch[0].length;
112
+ continue;
113
+ }
114
+ i++;
115
+ }
116
+ return i - offset;
117
+ }
@@ -0,0 +1,21 @@
1
+ import type { LexicalNodeRegistration } from '../types.js';
2
+ import type { LexicalNode, LexicalRoot, SerializedUnit } from './types.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
+ */
20
+ export declare function serializeBlockData(data: Record<string, unknown>, units: SerializedUnit[], nextBlockId: () => string, innerWalkNodes: (nodes: LexicalNode[]) => void): void;
21
+ export declare function serializeLexicalTree(root: LexicalRoot, registeredNodes?: LexicalNodeRegistration[]): SerializedUnit[];