@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,181 @@
1
+ import type { Payload } from 'payload';
2
+ import type { AITranslatePluginConfig } from '../types.js';
3
+ export declare function invalidateSettingsCache(slug?: string): void;
4
+ /** @internal test helper */
5
+ export declare function _resetEffectiveLocalesCache(): void;
6
+ /**
7
+ * Resolved auto-translate decision for a specific surface.
8
+ *
9
+ * - `locales`: the (possibly empty) list of target locales that should
10
+ * auto-translate on the next publish.
11
+ * - `skipAutoOnPublish`: when `true`, the auto-translate path is gated
12
+ * off — the hook should bail before creating a job. Manual Translate…
13
+ * (via the dialog or per-field button) is NOT affected by this flag.
14
+ * See W-4 in the panel review for the intentional scope: this layer
15
+ * only narrows automation, never the manual entry points.
16
+ */
17
+ export type EffectiveLocaleDecision = {
18
+ locales: string[];
19
+ skipAutoOnPublish: boolean;
20
+ };
21
+ /**
22
+ * Read the per-doc auto-translate locale-narrow override from a saved doc.
23
+ *
24
+ * The plugin injects `_aiTranslateAutoLocales` (`select hasMany`) into
25
+ * every tracked collection / global as a NARROW filter — only locales
26
+ * the editor explicitly picks fan out. Two states are meaningful:
27
+ *
28
+ * - `undefined` / not-an-array / empty array → no narrowing (inherit
29
+ * the collection-level decision; fan out to every enabled locale).
30
+ * - non-empty array → restrict the fan-out to these locales.
31
+ *
32
+ * Editors who want to OPT A DOC OUT of auto-translate entirely use the
33
+ * sibling `_aiTranslateOptOut` boolean field — see `getDocOptOut`.
34
+ *
35
+ * Historical note: pre-fix versions of the plugin documented a 3-state
36
+ * contract where `[]` meant "explicit opt-out." That state was
37
+ * unreachable at the storage layer because Payload's hasMany select
38
+ * serializes both "admin never touched it" and "admin cleared every
39
+ * checkbox" as 0 rows in the join table → empty array on read. The
40
+ * collapse meant auto-translate was effectively dead by default. The
41
+ * fix splits the two responsibilities: this field narrows locales when
42
+ * non-empty; the new boolean opts the whole doc out when true.
43
+ */
44
+ export declare function getDocAutoLocaleOverride(doc: unknown): string[] | undefined;
45
+ /**
46
+ * Read the per-doc opt-out flag. `true` means "skip auto-translate
47
+ * entirely for this document, regardless of collection or site-wide
48
+ * settings." Manual Translate from the dialog is NOT gated by this
49
+ * flag — same scope as the collection-level `enabled` flag.
50
+ *
51
+ * Defaults to `false`: a document that has never had the field touched
52
+ * (or is missing it on legacy/pre-migration data) participates in
53
+ * auto-translate normally.
54
+ */
55
+ export declare function getDocOptOut(doc: unknown): boolean;
56
+ /**
57
+ * Compute the locales that should auto-translate on the next publish for
58
+ * the given surface (collection slug OR global slug), by intersecting:
59
+ *
60
+ * 1. Plugin config `targetLocales` — the universe.
61
+ * 2. Site-wide `enabledTargetLocales` from `translation-settings`.
62
+ * Empty / unset = "all enabled" (back-compat default).
63
+ * 3. Per-surface `perCollection` row, if any:
64
+ * - `enabled: false` → no locales + `skipAutoOnPublish: true`.
65
+ * - `autoOnPublish: false` → no locales + `skipAutoOnPublish: true`.
66
+ * - `targetLocalesOverride` non-empty → narrows further.
67
+ *
68
+ * Each layer NARROWS the previous — never widens.
69
+ *
70
+ * **Fail-safe on read failure**: a `findGlobal` exception (DB blip,
71
+ * permission misconfig, slug mismatch) returns
72
+ * `{ locales: [], skipAutoOnPublish: true }`. Auto-translate skips, the
73
+ * editor doesn't see a phantom job, and no LLM spend is incurred
74
+ * against locales the admin may have disabled but we couldn't read.
75
+ * Manual translate still works because it doesn't route through this
76
+ * function. This was a behavior flip from "fail-open" — see W-3 in the
77
+ * 2026-05-15 panel review for the rationale.
78
+ */
79
+ export declare function getEffectiveAutoLocales(payload: Payload, config: AITranslatePluginConfig, surfaceSlug?: string): Promise<EffectiveLocaleDecision>;
80
+ /**
81
+ * Returns `true` when the admin set `perCollection[surfaceSlug].enabled = false`.
82
+ *
83
+ * Used by the manual-translate endpoint to gate the dialog AND per-field
84
+ * button so the `enabled` checkbox is a true kill switch (BUG-08 fix).
85
+ * The sibling `autoOnPublish` flag remains automation-only — admins
86
+ * who want "manual translate yes, auto-translate no" should use that
87
+ * flag instead of `enabled: false`.
88
+ *
89
+ * **Fail-safe-OPEN on read failure**: returns `false` (allows translate)
90
+ * if settings can't be read. Blocking manual translate on a DB blip is
91
+ * more disruptive than letting the user proceed.
92
+ */
93
+ export declare function isSurfaceDisabledByAdmin(payload: Payload, config: AITranslatePluginConfig, surfaceSlug: string): Promise<boolean>;
94
+ /**
95
+ * Resolved state of the three plugin-wide kill switches added in v1.2.8.
96
+ * Each flag supersedes every per-collection setting — when `false`, the
97
+ * corresponding subsystem refuses to do work regardless of what the
98
+ * `perCollection` row says.
99
+ *
100
+ * Fail-safe direction matches the existing per-domain reasoning:
101
+ *
102
+ * - `autoEnabled` fails CLOSED on read failure (returns `false`). This
103
+ * mirrors `getEffectiveAutoLocales`: when we can't read settings, we
104
+ * don't know whether the admin disabled auto-translate, so we don't
105
+ * spend tokens. The after-change hook becomes a no-op until settings
106
+ * read recovers — same as if every locale were narrowed away.
107
+ *
108
+ * - `manualEnabled` fails OPEN on read failure (returns `true`). The
109
+ * manual entry points are user-initiated; blocking them on a DB blip
110
+ * is more disruptive than letting the editor through. This mirrors
111
+ * `isSurfaceDisabledByAdmin`'s open-on-failure direction.
112
+ *
113
+ * - `bulkEnabled` fails OPEN on read failure (returns `true`). New bulk
114
+ * runs are user-initiated; same reasoning as manual.
115
+ */
116
+ export type GlobalKillSwitches = {
117
+ autoEnabled: boolean;
118
+ manualEnabled: boolean;
119
+ bulkEnabled: boolean;
120
+ };
121
+ export declare function getGlobalKillSwitches(payload: Payload, config: AITranslatePluginConfig): Promise<GlobalKillSwitches>;
122
+ /**
123
+ * Return the set of field paths the admin opted out of translation
124
+ * for `surfaceSlug`, stored on the matching `perCollection` row of the
125
+ * `translation-settings` global.
126
+ *
127
+ * Scope: applies to BOTH manual and automation paths. The admin
128
+ * decision "don't translate `meta.description` on posts" is global —
129
+ * it would be surprising if Manual Translate… ignored it. This is the
130
+ * intentional difference vs `enabled` / `autoOnPublish`, which are
131
+ * automation-only gates.
132
+ *
133
+ * **Fail-safe-OPEN on read failure**: returns an empty set if the
134
+ * settings global can't be read. Blocking a manual translate on a DB
135
+ * blip would be more disruptive than letting the (already-tracked)
136
+ * field through. The locale-resolution function above flips the other
137
+ * direction because it's gating LLM SPEND, not which fields go in.
138
+ */
139
+ export declare function getExcludedFieldPaths(payload: Payload, config: AITranslatePluginConfig, surfaceSlug: string): Promise<Set<string>>;
140
+ /**
141
+ * Surface-aware variant of `getEffectiveExcludePatterns`.
142
+ *
143
+ * Reads the `perCollection[surfaceSlug].translateSlug` flag from the
144
+ * translation-settings global. When `true`, drops `'slug'` from the
145
+ * effective patterns so the resolver picks it up. When `false` (the
146
+ * default), behaviour is identical to `getEffectiveExcludePatterns`.
147
+ *
148
+ * Use this in every translation path that operates on a known
149
+ * surface — `translateDocument`, `translateGlobal`, the estimate
150
+ * endpoint, the cost-guard pre-flight, and the admin UI's
151
+ * client-config map. The static `getEffectiveExcludePatterns` is
152
+ * still appropriate for config-build-time paths (per-field button
153
+ * injection) that have no async context AND no per-row knowledge.
154
+ *
155
+ * **Fail-safe-CLOSED on read failure**: returns the base patterns
156
+ * unchanged. This is the conservative direction — if we can't read
157
+ * the toggle, we keep slug excluded (no per-locale URL churn from a
158
+ * DB blip). Mirrors `getExcludedFieldPaths`' policy of "blocking
159
+ * manual translate on a settings-read blip is more disruptive than
160
+ * letting the field through" — except in this case the opposite
161
+ * applies because the default IS exclusion.
162
+ */
163
+ export declare function getEffectiveExcludePatternsForSurface(payload: Payload, config: AITranslatePluginConfig, surfaceSlug: string): Promise<string[]>;
164
+ /**
165
+ * Prefix-aware match: `path` is excluded if it appears in `excluded`
166
+ * OR any ancestor segment of it does. Excluding `layout` therefore
167
+ * excludes `layout.columns`, `layout.columns.richText`, etc.
168
+ *
169
+ * Why prefix and not exact: editors think in top-level fields. The
170
+ * admin UI offers ONE checkbox per top-level field — unchecking
171
+ * "Layout" should skip everything under it. Exact-match would leave
172
+ * `layout.columns.richText` translating even when the editor opted
173
+ * the entire Layout out. Per-leaf exclusions are still expressible
174
+ * via the more specific path (e.g. `meta.description`).
175
+ *
176
+ * Implementation note: O(D × |excluded|) per call where D is the
177
+ * path's segment depth (small constant in practice). Acceptable for
178
+ * the per-fire filter; would warrant a trie if exclusion lists grew
179
+ * to hundreds.
180
+ */
181
+ export declare function isPathExcluded(path: string, excluded: Set<string>): boolean;
@@ -0,0 +1,302 @@
1
+ import { DEFAULT_SETTINGS_GLOBAL_SLUG } from '../settings-global.js';
2
+ import { getEffectiveExcludePatterns } from './exclude-fields.js';
3
+ const SETTINGS_CACHE_TTL_MS = 30_000;
4
+ const settingsCache = new Map();
5
+ export function invalidateSettingsCache(slug) {
6
+ if (slug) {
7
+ settingsCache.delete(slug);
8
+ } else {
9
+ settingsCache.clear();
10
+ }
11
+ }
12
+ /** @internal test helper */ export function _resetEffectiveLocalesCache() {
13
+ settingsCache.clear();
14
+ }
15
+ async function readSettings(payload, config) {
16
+ const slug = config.settings?.globalSlug ?? DEFAULT_SETTINGS_GLOBAL_SLUG;
17
+ const cached = settingsCache.get(slug);
18
+ const now = Date.now();
19
+ if (cached && cached.expiresAt > now) {
20
+ return {
21
+ failed: false,
22
+ settings: cached.value
23
+ };
24
+ }
25
+ try {
26
+ const settings = await payload.findGlobal({
27
+ slug,
28
+ depth: 0,
29
+ overrideAccess: true
30
+ });
31
+ settingsCache.set(slug, {
32
+ value: settings,
33
+ expiresAt: now + SETTINGS_CACHE_TTL_MS
34
+ });
35
+ return {
36
+ failed: false,
37
+ settings
38
+ };
39
+ } catch (err) {
40
+ payload.logger?.error?.(`[ai-translate] Failed to read "${slug}" global — settings-driven gates will fall back to safe defaults. Underlying error: ${err instanceof Error ? err.message : 'Unknown error'}`);
41
+ // Do NOT cache the failure — next fire should retry.
42
+ return {
43
+ failed: true,
44
+ settings: null
45
+ };
46
+ }
47
+ }
48
+ /**
49
+ * Read the per-doc auto-translate locale-narrow override from a saved doc.
50
+ *
51
+ * The plugin injects `_aiTranslateAutoLocales` (`select hasMany`) into
52
+ * every tracked collection / global as a NARROW filter — only locales
53
+ * the editor explicitly picks fan out. Two states are meaningful:
54
+ *
55
+ * - `undefined` / not-an-array / empty array → no narrowing (inherit
56
+ * the collection-level decision; fan out to every enabled locale).
57
+ * - non-empty array → restrict the fan-out to these locales.
58
+ *
59
+ * Editors who want to OPT A DOC OUT of auto-translate entirely use the
60
+ * sibling `_aiTranslateOptOut` boolean field — see `getDocOptOut`.
61
+ *
62
+ * Historical note: pre-fix versions of the plugin documented a 3-state
63
+ * contract where `[]` meant "explicit opt-out." That state was
64
+ * unreachable at the storage layer because Payload's hasMany select
65
+ * serializes both "admin never touched it" and "admin cleared every
66
+ * checkbox" as 0 rows in the join table → empty array on read. The
67
+ * collapse meant auto-translate was effectively dead by default. The
68
+ * fix splits the two responsibilities: this field narrows locales when
69
+ * non-empty; the new boolean opts the whole doc out when true.
70
+ */ export function getDocAutoLocaleOverride(doc) {
71
+ if (!doc || typeof doc !== 'object') return undefined;
72
+ const value = doc._aiTranslateAutoLocales;
73
+ if (!Array.isArray(value)) return undefined;
74
+ // Defensive copy + ensure all elements are strings (the field is a
75
+ // typed select; this filters out any stray malformed entries from
76
+ // direct DB writes or older data).
77
+ const cleaned = value.filter((v)=>typeof v === 'string');
78
+ // Empty array = inherit. Explicit opt-out lives on the
79
+ // `_aiTranslateOptOut` boolean instead.
80
+ if (cleaned.length === 0) return undefined;
81
+ return cleaned;
82
+ }
83
+ /**
84
+ * Read the per-doc opt-out flag. `true` means "skip auto-translate
85
+ * entirely for this document, regardless of collection or site-wide
86
+ * settings." Manual Translate from the dialog is NOT gated by this
87
+ * flag — same scope as the collection-level `enabled` flag.
88
+ *
89
+ * Defaults to `false`: a document that has never had the field touched
90
+ * (or is missing it on legacy/pre-migration data) participates in
91
+ * auto-translate normally.
92
+ */ export function getDocOptOut(doc) {
93
+ if (!doc || typeof doc !== 'object') return false;
94
+ const value = doc._aiTranslateOptOut;
95
+ return value === true;
96
+ }
97
+ /**
98
+ * Compute the locales that should auto-translate on the next publish for
99
+ * the given surface (collection slug OR global slug), by intersecting:
100
+ *
101
+ * 1. Plugin config `targetLocales` — the universe.
102
+ * 2. Site-wide `enabledTargetLocales` from `translation-settings`.
103
+ * Empty / unset = "all enabled" (back-compat default).
104
+ * 3. Per-surface `perCollection` row, if any:
105
+ * - `enabled: false` → no locales + `skipAutoOnPublish: true`.
106
+ * - `autoOnPublish: false` → no locales + `skipAutoOnPublish: true`.
107
+ * - `targetLocalesOverride` non-empty → narrows further.
108
+ *
109
+ * Each layer NARROWS the previous — never widens.
110
+ *
111
+ * **Fail-safe on read failure**: a `findGlobal` exception (DB blip,
112
+ * permission misconfig, slug mismatch) returns
113
+ * `{ locales: [], skipAutoOnPublish: true }`. Auto-translate skips, the
114
+ * editor doesn't see a phantom job, and no LLM spend is incurred
115
+ * against locales the admin may have disabled but we couldn't read.
116
+ * Manual translate still works because it doesn't route through this
117
+ * function. This was a behavior flip from "fail-open" — see W-3 in the
118
+ * 2026-05-15 panel review for the rationale.
119
+ */ export async function getEffectiveAutoLocales(payload, config, surfaceSlug) {
120
+ const universe = config.targetLocales ?? [];
121
+ if (universe.length === 0) {
122
+ return {
123
+ locales: [],
124
+ skipAutoOnPublish: false
125
+ };
126
+ }
127
+ const read = await readSettings(payload, config);
128
+ if (read.failed) {
129
+ // Locale resolution is fail-safe-skip: we don't know which locales
130
+ // the admin disabled, so spending tokens against any of them is a
131
+ // policy violation. Manual translate still works because it doesn't
132
+ // route through this function.
133
+ return {
134
+ locales: [],
135
+ skipAutoOnPublish: true
136
+ };
137
+ }
138
+ const settings = read.settings;
139
+ // Per-surface override is evaluated FIRST when surfaceSlug is provided.
140
+ // `enabled: false` and `autoOnPublish: false` short-circuit the entire
141
+ // resolution — there's no point computing the site-wide intersection
142
+ // when we're about to return an empty list anyway. Saves a small bit
143
+ // of work per fire, scales meaningfully under bulk-publish.
144
+ if (surfaceSlug && Array.isArray(settings?.perCollection)) {
145
+ const row = settings.perCollection.find((r)=>Boolean(r) && r?.slug === surfaceSlug);
146
+ if (row) {
147
+ if (row.enabled === false) {
148
+ return {
149
+ locales: [],
150
+ skipAutoOnPublish: true
151
+ };
152
+ }
153
+ if (row.autoOnPublish === false) {
154
+ return {
155
+ locales: [],
156
+ skipAutoOnPublish: true
157
+ };
158
+ }
159
+ }
160
+ }
161
+ // Site-wide allow-list. Empty / unset = "all configured target locales".
162
+ const enabled = settings?.enabledTargetLocales;
163
+ let siteWide = universe;
164
+ if (Array.isArray(enabled) && enabled.length > 0) {
165
+ siteWide = universe.filter((l)=>enabled.includes(l));
166
+ }
167
+ // Per-surface target-locale narrowing (applied after the
168
+ // `enabled` / `autoOnPublish` gate above).
169
+ if (surfaceSlug && Array.isArray(settings?.perCollection)) {
170
+ const row = settings.perCollection.find((r)=>r?.slug === surfaceSlug);
171
+ const override = row?.targetLocalesOverride;
172
+ if (Array.isArray(override) && override.length > 0) {
173
+ return {
174
+ locales: siteWide.filter((l)=>override.includes(l)),
175
+ skipAutoOnPublish: false
176
+ };
177
+ }
178
+ }
179
+ return {
180
+ locales: siteWide,
181
+ skipAutoOnPublish: false
182
+ };
183
+ }
184
+ /**
185
+ * Returns `true` when the admin set `perCollection[surfaceSlug].enabled = false`.
186
+ *
187
+ * Used by the manual-translate endpoint to gate the dialog AND per-field
188
+ * button so the `enabled` checkbox is a true kill switch (BUG-08 fix).
189
+ * The sibling `autoOnPublish` flag remains automation-only — admins
190
+ * who want "manual translate yes, auto-translate no" should use that
191
+ * flag instead of `enabled: false`.
192
+ *
193
+ * **Fail-safe-OPEN on read failure**: returns `false` (allows translate)
194
+ * if settings can't be read. Blocking manual translate on a DB blip is
195
+ * more disruptive than letting the user proceed.
196
+ */ export async function isSurfaceDisabledByAdmin(payload, config, surfaceSlug) {
197
+ const read = await readSettings(payload, config);
198
+ if (read.failed || !read.settings?.perCollection) return false;
199
+ const row = read.settings.perCollection.find((r)=>r?.slug === surfaceSlug);
200
+ return row?.enabled === false;
201
+ }
202
+ export async function getGlobalKillSwitches(payload, config) {
203
+ const read = await readSettings(payload, config);
204
+ if (read.failed) {
205
+ return {
206
+ autoEnabled: false,
207
+ manualEnabled: true,
208
+ bulkEnabled: true
209
+ };
210
+ }
211
+ const settings = read.settings;
212
+ return {
213
+ autoEnabled: settings?.globalAutoTranslateEnabled !== false,
214
+ manualEnabled: settings?.globalManualTranslateEnabled !== false,
215
+ bulkEnabled: settings?.globalBulkTranslateEnabled !== false
216
+ };
217
+ }
218
+ /**
219
+ * Return the set of field paths the admin opted out of translation
220
+ * for `surfaceSlug`, stored on the matching `perCollection` row of the
221
+ * `translation-settings` global.
222
+ *
223
+ * Scope: applies to BOTH manual and automation paths. The admin
224
+ * decision "don't translate `meta.description` on posts" is global —
225
+ * it would be surprising if Manual Translate… ignored it. This is the
226
+ * intentional difference vs `enabled` / `autoOnPublish`, which are
227
+ * automation-only gates.
228
+ *
229
+ * **Fail-safe-OPEN on read failure**: returns an empty set if the
230
+ * settings global can't be read. Blocking a manual translate on a DB
231
+ * blip would be more disruptive than letting the (already-tracked)
232
+ * field through. The locale-resolution function above flips the other
233
+ * direction because it's gating LLM SPEND, not which fields go in.
234
+ */ export async function getExcludedFieldPaths(payload, config, surfaceSlug) {
235
+ const read = await readSettings(payload, config);
236
+ if (read.failed || !read.settings) return new Set();
237
+ const rows = read.settings.perCollection;
238
+ if (!Array.isArray(rows)) return new Set();
239
+ const row = rows.find((r)=>r?.slug === surfaceSlug);
240
+ const paths = row?.excludedFieldPaths;
241
+ if (!Array.isArray(paths)) return new Set();
242
+ return new Set(paths.filter((p)=>typeof p === 'string'));
243
+ }
244
+ /**
245
+ * Surface-aware variant of `getEffectiveExcludePatterns`.
246
+ *
247
+ * Reads the `perCollection[surfaceSlug].translateSlug` flag from the
248
+ * translation-settings global. When `true`, drops `'slug'` from the
249
+ * effective patterns so the resolver picks it up. When `false` (the
250
+ * default), behaviour is identical to `getEffectiveExcludePatterns`.
251
+ *
252
+ * Use this in every translation path that operates on a known
253
+ * surface — `translateDocument`, `translateGlobal`, the estimate
254
+ * endpoint, the cost-guard pre-flight, and the admin UI's
255
+ * client-config map. The static `getEffectiveExcludePatterns` is
256
+ * still appropriate for config-build-time paths (per-field button
257
+ * injection) that have no async context AND no per-row knowledge.
258
+ *
259
+ * **Fail-safe-CLOSED on read failure**: returns the base patterns
260
+ * unchanged. This is the conservative direction — if we can't read
261
+ * the toggle, we keep slug excluded (no per-locale URL churn from a
262
+ * DB blip). Mirrors `getExcludedFieldPaths`' policy of "blocking
263
+ * manual translate on a settings-read blip is more disruptive than
264
+ * letting the field through" — except in this case the opposite
265
+ * applies because the default IS exclusion.
266
+ */ export async function getEffectiveExcludePatternsForSurface(payload, config, surfaceSlug) {
267
+ const base = getEffectiveExcludePatterns(config);
268
+ const read = await readSettings(payload, config);
269
+ if (read.failed || !read.settings?.perCollection) return base;
270
+ const row = read.settings.perCollection.find((r)=>r?.slug === surfaceSlug);
271
+ if (row?.translateSlug !== true) return base;
272
+ // Drop only the exact `'slug'` pattern. Don't touch suffix/glob
273
+ // patterns like `'**.slug'` if a consumer ever uses them — those
274
+ // are a deliberate broader exclusion the editor isn't opting out of.
275
+ return base.filter((p)=>p !== 'slug');
276
+ }
277
+ /**
278
+ * Prefix-aware match: `path` is excluded if it appears in `excluded`
279
+ * OR any ancestor segment of it does. Excluding `layout` therefore
280
+ * excludes `layout.columns`, `layout.columns.richText`, etc.
281
+ *
282
+ * Why prefix and not exact: editors think in top-level fields. The
283
+ * admin UI offers ONE checkbox per top-level field — unchecking
284
+ * "Layout" should skip everything under it. Exact-match would leave
285
+ * `layout.columns.richText` translating even when the editor opted
286
+ * the entire Layout out. Per-leaf exclusions are still expressible
287
+ * via the more specific path (e.g. `meta.description`).
288
+ *
289
+ * Implementation note: O(D × |excluded|) per call where D is the
290
+ * path's segment depth (small constant in practice). Acceptable for
291
+ * the per-fire filter; would warrant a trie if exclusion lists grew
292
+ * to hundreds.
293
+ */ export function isPathExcluded(path, excluded) {
294
+ if (excluded.size === 0) return false;
295
+ if (excluded.has(path)) return true;
296
+ const segments = path.split('.');
297
+ for(let i = segments.length - 1; i > 0; i--){
298
+ const prefix = segments.slice(0, i).join('.');
299
+ if (excluded.has(prefix)) return true;
300
+ }
301
+ return false;
302
+ }