@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,393 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { getValueAtPath } from './content-extractor.js';
3
+ /**
4
+ * SHA-1 of a stable JSON serialization of a value. Used to fingerprint
5
+ * field values for the `preserveManualEdits` feature so we can detect
6
+ * when a target locale row has drifted from the last plugin-written
7
+ * shape.
8
+ *
9
+ * Stable serialization: object keys sorted, undefined dropped. Same
10
+ * value → same hash across processes / machines.
11
+ */ export function hashFieldValue(value) {
12
+ return createHash('sha1').update(stableStringify(value)).digest('hex');
13
+ }
14
+ function stableStringify(value) {
15
+ if (value === undefined) return 'undefined';
16
+ if (value === null) return 'null';
17
+ if (typeof value === 'string') return JSON.stringify(value);
18
+ if (typeof value !== 'object') return JSON.stringify(value);
19
+ if (Array.isArray(value)) {
20
+ return '[' + value.map(stableStringify).join(',') + ']';
21
+ }
22
+ const entries = Object.entries(value).filter(([, v])=>v !== undefined).sort(([a], [b])=>a < b ? -1 : a > b ? 1 : 0);
23
+ return '{' + entries.map(([k, v])=>JSON.stringify(k) + ':' + stableStringify(v)).join(',') + '}';
24
+ }
25
+ /**
26
+ * Fetch every (fieldPath → lastWrittenHash) row for one
27
+ * (collection, documentId, locale) tuple. Used by the writer before
28
+ * issuing the locale-update so it can filter out fields that have
29
+ * drifted from the stored hash.
30
+ */ export async function loadFieldHashes(payload, metaCollection, collection, documentId, locale) {
31
+ const out = new Map();
32
+ try {
33
+ const result = await payload.find({
34
+ collection: metaCollection,
35
+ limit: 1000,
36
+ where: {
37
+ collection: {
38
+ equals: collection
39
+ },
40
+ documentId: {
41
+ equals: String(documentId)
42
+ },
43
+ locale: {
44
+ equals: locale
45
+ }
46
+ },
47
+ depth: 0,
48
+ overrideAccess: true
49
+ });
50
+ for (const raw of result.docs){
51
+ const row = raw;
52
+ if (row.fieldPath && row.lastWrittenHash) {
53
+ out.set(row.fieldPath, row.lastWrittenHash);
54
+ }
55
+ }
56
+ } catch {
57
+ // First-time write or transient read failure — caller treats empty
58
+ // as "no prior hash, no manual-edit divergence possible yet."
59
+ }
60
+ return out;
61
+ }
62
+ /**
63
+ * Inspect the current target doc and decide which top-level fields in
64
+ * `writeData` would overwrite a manually edited value. Returns the set
65
+ * of field names to DROP from `writeData` before issuing the update.
66
+ *
67
+ * Detection: for each top-level field path that has a stored hash, if
68
+ * the current target value's hash differs from the stored one, the
69
+ * field has been edited since the plugin's last write — skip it.
70
+ */ export function pickManualEditedFields(writeData, targetDoc, storedHashes) {
71
+ const fieldsToDrop = [];
72
+ const skipReasons = new Map();
73
+ if (!targetDoc || storedHashes.size === 0) {
74
+ return {
75
+ fieldsToDrop,
76
+ skipReasons
77
+ };
78
+ }
79
+ for (const topField of Object.keys(writeData)){
80
+ const storedHash = storedHashes.get(topField);
81
+ if (!storedHash) continue; // never written before — not divergent
82
+ const currentValue = getValueAtPath(targetDoc, topField);
83
+ // BUG-24 fix: if the current target value is NULL / undefined / empty,
84
+ // the row was emptied (version restore, manual deletion, fresh insert
85
+ // after a delete) — there's nothing for the editor to have "manually
86
+ // edited," so the stale stored hash should not block recovery. Treat
87
+ // empty targets as "no preserve" so re-translation populates them.
88
+ if (isEmptyTargetValue(currentValue)) continue;
89
+ const currentHash = hashFieldValue(currentValue);
90
+ if (currentHash !== storedHash) {
91
+ fieldsToDrop.push(topField);
92
+ skipReasons.set(topField, 'Target value diverges from last plugin-written hash — assumed manual edit, preserving target');
93
+ }
94
+ }
95
+ return {
96
+ fieldsToDrop,
97
+ skipReasons
98
+ };
99
+ }
100
+ /**
101
+ * "Empty target" means the row has nothing the editor could have
102
+ * manually refined. Includes:
103
+ * - undefined / null
104
+ * - empty string
105
+ * - empty array
106
+ * - empty object (no enumerable keys)
107
+ *
108
+ * NOT empty:
109
+ * - 0, false (legitimate primitive values)
110
+ * - whitespace-only strings (still editor intent)
111
+ */ function isEmptyTargetValue(value) {
112
+ if (value === undefined || value === null) return true;
113
+ if (typeof value === 'string') return value === '';
114
+ if (Array.isArray(value)) return value.length === 0;
115
+ if (typeof value === 'object') return Object.keys(value).length === 0;
116
+ return false;
117
+ }
118
+ /**
119
+ * Upsert per-field hashes after a successful locale write. One row per
120
+ * (collection, documentId, locale, fieldPath). Best-effort — failures
121
+ * here do NOT roll back the translation, they just mean the next write
122
+ * can't detect divergence for that field.
123
+ *
124
+ * `sourceValues` (BUG-21): map of `fieldPath → source value at time of
125
+ * write`. When provided, the upsert also stores `lastSourceHash` so the
126
+ * next translate can compare current-source-hash to stored-source-hash
127
+ * and skip the LLM call entirely if both source AND target are
128
+ * unchanged. Omitted when source values aren't available (rare).
129
+ */ export async function recordFieldHashes(payload, metaCollection, collection, documentId, locale, writeData, sourceValues, /**
130
+ * Optional request context to thread into the inner find/update/create
131
+ * calls. When the caller wraps the locale-write in a fresh transaction,
132
+ * passing the same `req` here keeps the bookkeeping rows on the same
133
+ * connection lifecycle. Omit for ad-hoc / out-of-band invocations.
134
+ */ req) {
135
+ const now = new Date().toISOString();
136
+ const fieldPaths = Object.entries(writeData);
137
+ let written = 0;
138
+ let failed = 0;
139
+ // Empty writeData is legitimate (all fields preserved as manual edits)
140
+ // — log once at debug and return cleanly so callers can distinguish
141
+ // "nothing to record" from "tried and failed."
142
+ if (fieldPaths.length === 0) {
143
+ return {
144
+ written: 0,
145
+ failed: 0
146
+ };
147
+ }
148
+ for (const [fieldPath, value] of fieldPaths){
149
+ const hash = hashFieldValue(value);
150
+ const sourceHash = sourceValues ? hashFieldValue(sourceValues[fieldPath]) : undefined;
151
+ try {
152
+ // Upsert via find + create/update. Payload doesn't have a
153
+ // native upsert; emulate it.
154
+ const existing = await payload.find({
155
+ collection: metaCollection,
156
+ limit: 1,
157
+ where: {
158
+ and: [
159
+ {
160
+ collection: {
161
+ equals: collection
162
+ }
163
+ },
164
+ {
165
+ documentId: {
166
+ equals: String(documentId)
167
+ }
168
+ },
169
+ {
170
+ locale: {
171
+ equals: locale
172
+ }
173
+ },
174
+ {
175
+ fieldPath: {
176
+ equals: fieldPath
177
+ }
178
+ }
179
+ ]
180
+ },
181
+ depth: 0,
182
+ overrideAccess: true,
183
+ ...req ? {
184
+ req
185
+ } : {}
186
+ });
187
+ const updateData = {
188
+ lastWrittenHash: hash,
189
+ lastWrittenAt: now
190
+ };
191
+ if (sourceHash !== undefined) updateData.lastSourceHash = sourceHash;
192
+ if (existing.docs.length > 0) {
193
+ await payload.update({
194
+ collection: metaCollection,
195
+ id: existing.docs[0].id,
196
+ data: updateData,
197
+ overrideAccess: true,
198
+ ...req ? {
199
+ req
200
+ } : {}
201
+ });
202
+ } else {
203
+ await payload.create({
204
+ collection: metaCollection,
205
+ data: {
206
+ collection,
207
+ documentId: String(documentId),
208
+ locale,
209
+ fieldPath,
210
+ lastWrittenHash: hash,
211
+ ...sourceHash !== undefined ? {
212
+ lastSourceHash: sourceHash
213
+ } : {},
214
+ lastWrittenAt: now
215
+ },
216
+ overrideAccess: true,
217
+ ...req ? {
218
+ req
219
+ } : {}
220
+ });
221
+ }
222
+ written++;
223
+ } catch (err) {
224
+ // Manual-edit detection is best-effort, so we never re-throw — but
225
+ // silently swallowing made an empty `ai-translate-meta` collection
226
+ // undebuggable. Surface the per-field failure as an error so it
227
+ // shows up in prod logs without filtering. The loop keeps going so
228
+ // one bad field doesn't drop the rest.
229
+ failed++;
230
+ const msg = err instanceof Error ? err.message : String(err);
231
+ const stack = err instanceof Error && err.stack ? `\n${err.stack}` : '';
232
+ payload.logger?.error?.(`[ai-translate] recordFieldHashes upsert failed for ${collection}/${String(documentId)} locale=${locale} field=${fieldPath}: ${msg}${stack}`);
233
+ }
234
+ }
235
+ return {
236
+ written,
237
+ failed
238
+ };
239
+ }
240
+ /**
241
+ * Fetch every `(fieldPath → { writtenHash, sourceHash })` row for one
242
+ * (collection, documentId, locale) tuple. Variant of `loadFieldHashes`
243
+ * that also returns the BUG-21 source hash so the LLM-skip check can
244
+ * compare current source content to what was translated last time.
245
+ *
246
+ * Returns an empty map on read failure (caller treats as "no prior data,
247
+ * proceed with full translate"). Rows pre-BUG-21 have `sourceHash:
248
+ * undefined` — those are treated as "can't safely skip; run translate."
249
+ */ export async function loadFieldHashesWithSource(payload, metaCollection, collection, documentId, locale) {
250
+ const out = new Map();
251
+ try {
252
+ const result = await payload.find({
253
+ collection: metaCollection,
254
+ limit: 1000,
255
+ where: {
256
+ collection: {
257
+ equals: collection
258
+ },
259
+ documentId: {
260
+ equals: String(documentId)
261
+ },
262
+ locale: {
263
+ equals: locale
264
+ }
265
+ },
266
+ depth: 0,
267
+ overrideAccess: true
268
+ });
269
+ for (const raw of result.docs){
270
+ const row = raw;
271
+ if (row.fieldPath && row.lastWrittenHash) {
272
+ out.set(row.fieldPath, {
273
+ writtenHash: row.lastWrittenHash,
274
+ sourceHash: row.lastSourceHash
275
+ });
276
+ }
277
+ }
278
+ } catch {
279
+ // Best-effort
280
+ }
281
+ return out;
282
+ }
283
+ /**
284
+ * Read stored source+target hashes for `(surfaceSlug, documentId,
285
+ * targetLocale)`, compare against current source + target, and remove
286
+ * units whose top-level field is unchanged on both sides. Returns the
287
+ * remaining units the caller still needs to translate plus synthetic
288
+ * `status: 'success'` results for the skipped units (characterCount = 0,
289
+ * durationMs = 0 — these are the "fieldsHashSkipped" rows in usage).
290
+ *
291
+ * Surface-agnostic: pass a `loadTargetDoc` closure so callers can supply
292
+ * `findByIdNoFallback(payload, collection, docId, locale)` for collections
293
+ * or `findGlobalNoFallback(payload, globalSlug, locale)` for globals.
294
+ *
295
+ * Wrapped in try/catch internally — best-effort optimization, never throws.
296
+ */ export async function applyHashSkip(args) {
297
+ const { payload, metaCollection, surfaceSlug, documentId, targetLocale, units, sourceDoc, loadTargetDoc } = args;
298
+ const fallback = {
299
+ remainingUnits: units,
300
+ hashSkipped: []
301
+ };
302
+ try {
303
+ const sourceTargetHashes = await loadFieldHashesWithSource(payload, metaCollection, surfaceSlug, documentId, targetLocale);
304
+ if (sourceTargetHashes.size === 0) {
305
+ return fallback;
306
+ }
307
+ let targetDoc = null;
308
+ try {
309
+ targetDoc = await loadTargetDoc();
310
+ } catch {
311
+ targetDoc = null;
312
+ }
313
+ if (!targetDoc) {
314
+ return fallback;
315
+ }
316
+ // Group units by top-level field. A top-level field is either hash-
317
+ // skippable as a whole (every unit under it gets skipped) or it's
318
+ // not — units within the same top-field share the same source/target
319
+ // hash bookkeeping.
320
+ const unitsByTop = new Map();
321
+ for (const u of units){
322
+ const top = u.fieldPath.split('.')[0];
323
+ if (!top) continue;
324
+ const arr = unitsByTop.get(top) ?? [];
325
+ arr.push(u);
326
+ unitsByTop.set(top, arr);
327
+ }
328
+ const skippedUnitIds = new Set();
329
+ for (const [topField, fieldUnits] of unitsByTop){
330
+ const stored = sourceTargetHashes.get(topField);
331
+ if (!stored || !stored.sourceHash) continue; // pre-BUG21 row
332
+ const currentSourceHash = hashFieldValue(sourceDoc[topField]);
333
+ if (currentSourceHash !== stored.sourceHash) continue; // source changed
334
+ const currentTargetHash = hashFieldValue(targetDoc[topField]);
335
+ if (currentTargetHash !== stored.writtenHash) continue; // target changed
336
+ for (const u of fieldUnits){
337
+ skippedUnitIds.add(u.id);
338
+ }
339
+ }
340
+ if (skippedUnitIds.size === 0) {
341
+ return fallback;
342
+ }
343
+ const hashSkipped = [];
344
+ for (const u of units){
345
+ if (!skippedUnitIds.has(u.id)) continue;
346
+ hashSkipped.push({
347
+ fieldPath: u.fieldPath,
348
+ locale: targetLocale,
349
+ status: 'success',
350
+ characterCount: 0,
351
+ durationMs: 0
352
+ });
353
+ }
354
+ const remainingUnits = units.filter((u)=>!skippedUnitIds.has(u.id));
355
+ payload.logger?.info?.(`[ai-translate] Hash-skip: ${skippedUnitIds.size}/${units.length} units for ${surfaceSlug}/${String(documentId)} locale=${targetLocale} unchanged since last write — skipping LLM call.`);
356
+ return {
357
+ remainingUnits,
358
+ hashSkipped
359
+ };
360
+ } catch {
361
+ // Hash-skip is purely an optimization — any failure here falls back
362
+ // to "translate everything," never blocks the LLM call.
363
+ return fallback;
364
+ }
365
+ }
366
+ /**
367
+ * Persist per-field source+target hashes after a successful locale
368
+ * write. Surface-agnostic — `surfaceSlug` is the collection slug for
369
+ * collection writes, or the global slug for global writes (matching the
370
+ * existing `bulk-translate` persist contract).
371
+ *
372
+ * Builds the `sourceValues` map from `sourceDoc` for exactly the fields
373
+ * in `writeData`, then defers to `recordFieldHashes`. Wraps the call so
374
+ * upstream errors are logged via the payload logger but never re-thrown
375
+ * — translation succeeded already, bookkeeping failure shouldn't surface
376
+ * to the editor.
377
+ */ export async function recordHashesAfterWrite(args) {
378
+ const { payload, metaCollection, surfaceSlug, documentId, targetLocale, writeData, sourceDoc } = args;
379
+ const sourceValues = {};
380
+ for (const fieldPath of Object.keys(writeData)){
381
+ sourceValues[fieldPath] = sourceDoc[fieldPath];
382
+ }
383
+ try {
384
+ return await recordFieldHashes(payload, metaCollection, surfaceSlug, documentId, targetLocale, writeData, sourceValues);
385
+ } catch (err) {
386
+ const msg = err instanceof Error ? err.message : String(err);
387
+ payload.logger?.error?.(`[ai-translate] recordHashesAfterWrite failed for ${surfaceSlug}/${String(documentId)} locale=${targetLocale}: ${msg}`);
388
+ return {
389
+ written: 0,
390
+ failed: Object.keys(writeData).length
391
+ };
392
+ }
393
+ }
@@ -0,0 +1,48 @@
1
+ import type { ValidationConfig } from '../types.js';
2
+ import type { EditorErrorCode } from './error-messages.js';
3
+ export type ValidateTranslationOptions = {
4
+ /**
5
+ * Source locale of the input text. When provided alongside `targetLocale`
6
+ * and the two differ, the validator rejects translations that are equal to
7
+ * the source after NFC + whitespace normalization — a common failure mode
8
+ * where the model echoes a batched item untranslated. v1.2.7: echo
9
+ * detection runs regardless of source length; a verbatim echo is never
10
+ * written through to the target locale, even for short strings like brand
11
+ * names. The soft-skip preserves the previous target value (or leaves the
12
+ * target empty so the UI falls back to source). This trades the "short
13
+ * loanword is a valid no-op translation" assumption for never silently
14
+ * overwriting an existing target translation with raw source text.
15
+ */
16
+ sourceLocale?: string;
17
+ targetLocale?: string;
18
+ };
19
+ export type ValidationResult = {
20
+ valid: boolean;
21
+ reason?: string;
22
+ /**
23
+ * `true` when the rejection is informational rather than a real error.
24
+ * Today this fires for verbatim echoes — the model may have legitimately
25
+ * returned the same string for a proper noun / brand / URL fragment.
26
+ * Callers should classify these as `skipped`, not `failed`.
27
+ */
28
+ soft?: boolean;
29
+ /**
30
+ * Editor-facing code (one of the `soft-skip.*` values in
31
+ * `lib/error-messages.ts`). Set on every non-valid result so the UI
32
+ * can render a friendly message via `editorMessageFor(code, ctx)`
33
+ * instead of dumping the raw `reason` string. `reason` is kept for
34
+ * disclosure-style "Show technical details" rendering.
35
+ */
36
+ code?: EditorErrorCode;
37
+ /**
38
+ * Context for templating the code's message (e.g. ratio observed,
39
+ * placeholder ids missing). Mirrors `EditorErrorContext` shape.
40
+ */
41
+ context?: {
42
+ ratio?: number;
43
+ ratioThreshold?: number;
44
+ missingPlaceholders?: readonly string[];
45
+ extraPlaceholders?: readonly string[];
46
+ };
47
+ };
48
+ export declare function validateTranslation(source: string, translated: string, config?: ValidationConfig, options?: ValidateTranslationOptions): ValidationResult;
@@ -0,0 +1,148 @@
1
+ import { validatePlaceholderIntegrity } from '../lexical/placeholder-integrity.js';
2
+ const DEFAULT_MIN_RATIO = 0.3;
3
+ const DEFAULT_MAX_RATIO = 3.0;
4
+ const DEFAULT_MIN_SOURCE_LENGTH = 10;
5
+ const REFUSAL_PATTERNS = [
6
+ /I cannot translate/i,
7
+ /As an AI/i,
8
+ /I'm unable to/i,
9
+ /I apologize/i,
10
+ /I'm sorry, but/i
11
+ ];
12
+ const INJECTION_PATTERNS = [
13
+ /<tool_call>/i,
14
+ /<\/tool_call>/i,
15
+ /<function_call>/i,
16
+ /<\/function_call>/i,
17
+ /<system>/i,
18
+ /<\/system>/i
19
+ ];
20
+ // Control characters except \n (0x0A), \t (0x09), \r (0x0D)
21
+ const CONTROL_CHAR_RE = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/;
22
+ /**
23
+ * NFC-normalize, collapse internal whitespace runs, and trim. Used for the
24
+ * echo-equality comparison so that models emitting a subtly-different but
25
+ * display-equivalent variant of the source (smart quote vs ASCII quote,
26
+ * non-breaking space, doubled space, leading/trailing whitespace) still
27
+ * trip echo detection. Without this, gemini-2.5-flash has been observed
28
+ * passing through "Slot Games (test 1780581867023)" as a "translation"
29
+ * with a NBSP swap that escaped a strict `===` check.
30
+ */ function normalizeForEcho(s) {
31
+ return s.normalize('NFC').replace(/\s+/g, ' ').trim();
32
+ }
33
+ export function validateTranslation(source, translated, config, options) {
34
+ const minRatio = config?.minLengthRatio ?? DEFAULT_MIN_RATIO;
35
+ const maxRatio = config?.maxLengthRatio ?? DEFAULT_MAX_RATIO;
36
+ const minSourceLength = config?.minSourceLength ?? DEFAULT_MIN_SOURCE_LENGTH;
37
+ // Echo detection runs regardless of source length (v1.2.7). Short brand
38
+ // names / loanwords used to bypass this gate, which let gemini-2.5-flash
39
+ // overwrite existing target translations with verbatim source for fields
40
+ // like "Bonus Buy". A verbatim echo is a soft-skip: the previous target
41
+ // value is preserved; an empty target stays empty (and the UI falls back
42
+ // to source). The ratio checks below still respect minSourceLength.
43
+ if (options?.sourceLocale && options.targetLocale && options.sourceLocale !== options.targetLocale && normalizeForEcho(translated) === normalizeForEcho(source)) {
44
+ return {
45
+ valid: false,
46
+ soft: true,
47
+ code: 'soft-skip.echoed',
48
+ reason: `Translation matches source verbatim — model likely echoed input for "${options.targetLocale}"`
49
+ };
50
+ }
51
+ if (source.length >= minSourceLength) {
52
+ const ratio = translated.length / source.length;
53
+ if (ratio < minRatio) {
54
+ return {
55
+ // 2026-06-05: all `soft-skip.*` codes now set `soft: true` so the
56
+ // caller (translate.ts:899) routes them to the `skipped` map
57
+ // instead of the retry/error path. The naming `soft-skip.*` always
58
+ // implied this — prior to today only `echoed` carried the flag,
59
+ // which meant a Chinese translation legitimately denser than the
60
+ // English source (ratio 0.10 vs min 0.15) would be classified as
61
+ // a HARD validation failure and the unit aggregator stamped it
62
+ // `permanent.schema` with editor copy that pointed at "needs a
63
+ // configuration fix" — nonsensical for an output-quality issue.
64
+ valid: false,
65
+ soft: true,
66
+ code: 'soft-skip.too-short',
67
+ context: {
68
+ ratio,
69
+ ratioThreshold: minRatio
70
+ },
71
+ reason: `Translation too short: ratio ${ratio.toFixed(2)} (min ${minRatio})`
72
+ };
73
+ }
74
+ if (ratio > maxRatio) {
75
+ return {
76
+ valid: false,
77
+ soft: true,
78
+ code: 'soft-skip.too-long',
79
+ context: {
80
+ ratio,
81
+ ratioThreshold: maxRatio
82
+ },
83
+ reason: `Translation too long: ratio ${ratio.toFixed(2)} (max ${maxRatio})`
84
+ };
85
+ }
86
+ }
87
+ if (CONTROL_CHAR_RE.test(translated)) {
88
+ return {
89
+ valid: false,
90
+ soft: true,
91
+ code: 'soft-skip.control-chars',
92
+ reason: 'Translation contains disallowed control characters'
93
+ };
94
+ }
95
+ // Placeholder integrity: rich-text units carry inline marks (bold,
96
+ // links, etc.) as `<ph id="...">` tokens. The model must echo the
97
+ // exact same set verbatim — added or dropped placeholders mean the
98
+ // deserializer would silently corrupt the Lexical tree.
99
+ if (source.includes('<ph')) {
100
+ const integrity = validatePlaceholderIntegrity(source, translated);
101
+ if (!integrity.valid) {
102
+ const issues = [];
103
+ if (integrity.missingIds.length) issues.push(`missing [${integrity.missingIds.join(', ')}]`);
104
+ if (integrity.extraIds.length) issues.push(`extra [${integrity.extraIds.join(', ')}]`);
105
+ return {
106
+ valid: false,
107
+ soft: true,
108
+ code: 'soft-skip.placeholders',
109
+ context: {
110
+ missingPlaceholders: integrity.missingIds,
111
+ extraPlaceholders: integrity.extraIds
112
+ },
113
+ reason: `Translation lost placeholder integrity — ${issues.join('; ')}`
114
+ };
115
+ }
116
+ }
117
+ const allRefusal = [
118
+ ...REFUSAL_PATTERNS,
119
+ ...config?.extraRefusalPatterns ?? []
120
+ ];
121
+ for (const pattern of allRefusal){
122
+ if (pattern.test(translated)) {
123
+ return {
124
+ valid: false,
125
+ soft: true,
126
+ code: 'soft-skip.refusal',
127
+ reason: `Translation contains refusal pattern: ${pattern.source}`
128
+ };
129
+ }
130
+ }
131
+ const allInjection = [
132
+ ...INJECTION_PATTERNS,
133
+ ...config?.extraInjectionPatterns ?? []
134
+ ];
135
+ for (const pattern of allInjection){
136
+ if (pattern.test(translated)) {
137
+ return {
138
+ valid: false,
139
+ soft: true,
140
+ code: 'soft-skip.injection',
141
+ reason: `Translation contains injection pattern: ${pattern.source}`
142
+ };
143
+ }
144
+ }
145
+ return {
146
+ valid: true
147
+ };
148
+ }
@@ -0,0 +1,16 @@
1
+ import type { Payload, Where } from 'payload';
2
+ export type ReadOptions = {
3
+ /**
4
+ * Read the latest draft version when the collection has drafts enabled.
5
+ * Pass-through to Payload's local API. No-op for non-versioned collections.
6
+ * Default: false (preserves existing read-published behavior for hooks
7
+ * that fire on already-committed docs).
8
+ */
9
+ draft?: boolean;
10
+ };
11
+ export declare function findByIdNoFallback(payload: Payload, collection: string, id: string | number, locale: string, options?: ReadOptions): Promise<Record<string, unknown>>;
12
+ export declare function findNoFallback(payload: Payload, collection: string, where: Where, locale: string, options?: ReadOptions): Promise<{
13
+ docs: Record<string, unknown>[];
14
+ totalDocs: number;
15
+ }>;
16
+ export declare function findGlobalNoFallback(payload: Payload, slug: string, locale: string): Promise<Record<string, unknown>>;
@@ -0,0 +1,51 @@
1
+ // Payload's Local API disables locale fallback when `fallbackLocale: false`.
2
+ // Passing `null` does NOT disable fallback — the runtime falls back to the
3
+ // default chain (typically the source/EN locale), which silently returns
4
+ // source content under a target-locale query. That's exactly the wrong
5
+ // behavior here: the locale-merge logic relies on knowing whether the target
6
+ // locale actually has data so it can pick the fresh-target vs. target-present
7
+ // branch. With the old `null` value, every "fresh target" query returned EN
8
+ // data, the merge entered the target-present branch carrying EN row ids, and
9
+ // Payload rejected the locale write with `"The following field is invalid: id"`
10
+ // because those row ids belong to the source locale's rows.
11
+ //
12
+ // Payload's TypeScript types declare `fallbackLocale` as a locale string, so
13
+ // we cast once here rather than scattering `as unknown as string` across the
14
+ // codebase. The runtime accepts the boolean `false` and disables fallback.
15
+ const NO_FALLBACK = false;
16
+ export async function findByIdNoFallback(payload, collection, id, locale, options = {}) {
17
+ const result = await payload.findByID({
18
+ collection: collection,
19
+ id,
20
+ locale,
21
+ fallbackLocale: NO_FALLBACK,
22
+ draft: options.draft ?? false,
23
+ // depth: 0 returns relationships as scalar IDs (not populated docs).
24
+ // The default depth: 2 populates upload/relationship fields, which
25
+ // become invalid when the merge writes them back to Payload —
26
+ // upload validators reject populated docs containing admin-only
27
+ // fields and server-generated metadata (sizes.*, url, filename, ...).
28
+ depth: 0
29
+ });
30
+ return result;
31
+ }
32
+ export async function findNoFallback(payload, collection, where, locale, options = {}) {
33
+ const result = await payload.find({
34
+ collection: collection,
35
+ where,
36
+ locale,
37
+ fallbackLocale: NO_FALLBACK,
38
+ draft: options.draft ?? false,
39
+ depth: 0
40
+ });
41
+ return result;
42
+ }
43
+ export async function findGlobalNoFallback(payload, slug, locale) {
44
+ const result = await payload.findGlobal({
45
+ slug: slug,
46
+ locale,
47
+ fallbackLocale: NO_FALLBACK,
48
+ depth: 0
49
+ });
50
+ return result;
51
+ }