@focus-reactive/payload-plugin-translator 0.8.2 → 0.9.1

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 (148) hide show
  1. package/README.md +37 -0
  2. package/dist/auto-translate-config.d.ts +40 -0
  3. package/dist/auto-translate-config.js +46 -0
  4. package/dist/client/entities/translation/index.d.ts +1 -0
  5. package/dist/client/entities/translation/index.js +1 -0
  6. package/dist/client/entities/translation/model/autoTranslateSummary.d.ts +15 -0
  7. package/dist/client/entities/translation/model/autoTranslateSummary.js +22 -0
  8. package/dist/client/entities/translation/ui/AutoTranslateMarker/AutoTranslateMarker.d.ts +14 -0
  9. package/dist/client/entities/translation/ui/AutoTranslateMarker/AutoTranslateMarker.js +34 -0
  10. package/dist/client/entities/translation/ui/AutoTranslateMarker/index.d.ts +1 -0
  11. package/dist/client/entities/translation/ui/AutoTranslateMarker/index.js +3 -0
  12. package/dist/client/entities/translation/ui/AutoTranslateMarker/styles.module.scss +30 -0
  13. package/dist/client/shared/lib/assets/icons/AutoTranslateIcon.d.ts +3 -0
  14. package/dist/client/shared/lib/assets/icons/AutoTranslateIcon.js +14 -0
  15. package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.d.ts +3 -1
  16. package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.js +14 -5
  17. package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.server.js +4 -1
  18. package/dist/client/widgets/bulk-translation-dashboard/ui/styles.module.scss +9 -0
  19. package/dist/client/widgets/translate-document/ui/TranslateDocument.d.ts +3 -1
  20. package/dist/client/widgets/translate-document/ui/TranslateDocument.js +14 -5
  21. package/dist/client/widgets/translate-document/ui/TranslateDocument.server.js +4 -1
  22. package/dist/client/widgets/translate-document/ui/styles.module.scss +9 -0
  23. package/dist/core/domain/auto-translate/getAutoTranslateConfig.d.ts +14 -0
  24. package/dist/core/domain/auto-translate/getAutoTranslateConfig.js +29 -0
  25. package/dist/core/domain/auto-translate/hasSourceContentChanged.d.ts +21 -0
  26. package/dist/core/domain/auto-translate/hasSourceContentChanged.js +25 -0
  27. package/dist/core/domain/auto-translate/index.d.ts +4 -0
  28. package/dist/core/domain/auto-translate/index.js +5 -0
  29. package/dist/core/domain/auto-translate/types.d.ts +32 -0
  30. package/dist/core/domain/auto-translate/types.js +7 -0
  31. package/dist/core/{content-projection → domain/content-projection}/computeSourceFingerprint.d.ts +1 -1
  32. package/dist/core/{content-projection → domain/content-projection}/contentProjector.d.ts +1 -1
  33. package/dist/core/{content-projection → domain/content-projection}/contentProjector.js +2 -2
  34. package/dist/core/{content-projection → domain/content-projection}/translatableLeaf.d.ts +1 -1
  35. package/dist/core/{content-projection → domain/content-projection}/translatableLeaf.js +2 -2
  36. package/dist/core/index.d.ts +10 -10
  37. package/dist/core/index.js +5 -5
  38. package/dist/core/translation-pipeline/TranslationPipeline.d.ts +1 -1
  39. package/dist/core/translation-pipeline/stages/data-reconciler/DataReconciler.d.ts +1 -1
  40. package/dist/core/translation-pipeline/stages/data-reconciler/DataReconciler.js +3 -3
  41. package/dist/core/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts +1 -1
  42. package/dist/core/translation-pipeline/stages/field-collector/FieldChunkCollector.js +3 -3
  43. package/dist/core/translation-pipeline/stages/text-expander/RichTextExpander.js +1 -1
  44. package/dist/core/translation-pipeline/stages/translation/Translation.stage.d.ts +1 -1
  45. package/dist/core/translation-pipeline/strategies/Overwrite.strategy.js +1 -1
  46. package/dist/core/translation-pipeline/strategies/SkipExisting.strategy.js +3 -3
  47. package/dist/core/translation-pipeline/translateContent.d.ts +2 -2
  48. package/dist/core/translation-pipeline/types/FieldChunk.d.ts +1 -1
  49. package/dist/core/translation-pipeline/types/Pipeline.d.ts +1 -1
  50. package/dist/core/translation-pipeline/types/PipelineContext.d.ts +1 -1
  51. package/dist/core/translation-pipeline/types/TextChunk.d.ts +1 -1
  52. package/dist/field-config.d.ts +1 -1
  53. package/dist/field-config.js +1 -1
  54. package/dist/index.d.ts +2 -0
  55. package/dist/index.js +4 -0
  56. package/dist/plugin.d.ts +1 -1
  57. package/dist/plugin.js +5 -0
  58. package/dist/server/features/translate-document/handler.d.ts +1 -1
  59. package/dist/server/features/translate-document/handler.js +9 -1
  60. package/dist/server/features/translate-document/wireTranslateRunner.d.ts +1 -1
  61. package/dist/server/features/translate-field/model.d.ts +1 -1
  62. package/dist/server/features/translate-field/resolveFieldSubtree.js +1 -1
  63. package/dist/server/modules/auto-translate/AutoTranslate.policy.d.ts +79 -0
  64. package/dist/server/modules/auto-translate/AutoTranslate.policy.js +77 -0
  65. package/dist/server/modules/auto-translate/AutoTranslate.shapes.d.ts +22 -0
  66. package/dist/server/modules/auto-translate/AutoTranslate.shapes.js +3 -0
  67. package/dist/server/modules/auto-translate/AutoTranslate.wiring.d.ts +21 -0
  68. package/dist/server/modules/auto-translate/AutoTranslate.wiring.js +68 -0
  69. package/dist/server/modules/auto-translate/AutoTranslateEnqueue.hook.d.ts +39 -0
  70. package/dist/server/modules/auto-translate/AutoTranslateEnqueue.hook.js +94 -0
  71. package/dist/server/modules/auto-translate/index.d.ts +4 -0
  72. package/dist/server/modules/auto-translate/index.js +7 -0
  73. package/dist/server/modules/index.d.ts +1 -1
  74. package/dist/server/modules/index.js +1 -1
  75. package/dist/server/modules/provenance/Provenance.service.d.ts +1 -1
  76. package/dist/server/modules/provenance/Provenance.service.js +2 -2
  77. package/dist/server/modules/provenance/Provenance.store.d.ts +1 -1
  78. package/dist/server/modules/provenance/index.js +1 -1
  79. package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsTaskRunner.js +4 -0
  80. package/dist/server/modules/task-runner/types.d.ts +7 -0
  81. package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +1 -1
  82. package/dist/server/modules/translation-levels/types.d.ts +1 -1
  83. package/dist/server/shared/index.d.ts +5 -5
  84. package/dist/server/shared/index.js +3 -3
  85. package/dist/server/shared/utils/filterLocalizedFields.js +2 -2
  86. package/dist/server/shared/utils/getByPath.js +1 -1
  87. package/dist/server/shared/utils/index.d.ts +2 -2
  88. package/dist/server/shared/utils/index.js +2 -2
  89. package/dist/server/shared/utils/setByPath.js +1 -1
  90. package/dist/translation-providers/index.d.ts +1 -1
  91. package/dist/translation-providers/openai/OpenAITranslation.provider.d.ts +1 -1
  92. package/dist/translation-providers/openai/OpenAITranslation.provider.js +1 -1
  93. package/dist/types/AutoTranslateContext.d.ts +10 -0
  94. package/dist/types/AutoTranslateContext.js +11 -0
  95. package/package.json +1 -1
  96. /package/dist/core/{content-projection → domain/content-projection}/computeSourceFingerprint.js +0 -0
  97. /package/dist/core/{content-projection → domain/content-projection}/fingerprinter.d.ts +0 -0
  98. /package/dist/core/{content-projection → domain/content-projection}/fingerprinter.js +0 -0
  99. /package/dist/core/{content-projection → domain/content-projection}/idPath.d.ts +0 -0
  100. /package/dist/core/{content-projection → domain/content-projection}/idPath.js +0 -0
  101. /package/dist/core/{field-config → domain/field-config}/getFieldConfig.d.ts +0 -0
  102. /package/dist/core/{field-config → domain/field-config}/getFieldConfig.js +0 -0
  103. /package/dist/core/{field-config → domain/field-config}/index.d.ts +0 -0
  104. /package/dist/core/{field-config → domain/field-config}/index.js +0 -0
  105. /package/dist/core/{field-config → domain/field-config}/types.d.ts +0 -0
  106. /package/dist/core/{field-config → domain/field-config}/types.js +0 -0
  107. /package/dist/core/{provenance → domain/provenance}/ProvenanceStore.interface.d.ts +0 -0
  108. /package/dist/core/{provenance → domain/provenance}/ProvenanceStore.interface.js +0 -0
  109. /package/dist/core/{provenance → domain/provenance}/index.d.ts +0 -0
  110. /package/dist/core/{provenance → domain/provenance}/index.js +0 -0
  111. /package/dist/core/{provenance → domain/provenance}/staleness.d.ts +0 -0
  112. /package/dist/core/{provenance → domain/provenance}/staleness.js +0 -0
  113. /package/dist/core/{translation-providers → domain/translation-providers}/TranslationProvider.interface.d.ts +0 -0
  114. /package/dist/core/{translation-providers → domain/translation-providers}/TranslationProvider.interface.js +0 -0
  115. /package/dist/core/{translation-providers → domain/translation-providers}/index.d.ts +0 -0
  116. /package/dist/core/{translation-providers → domain/translation-providers}/index.js +0 -0
  117. /package/dist/core/{field-traversal → kernel/field-traversal}/findFieldByPath.d.ts +0 -0
  118. /package/dist/core/{field-traversal → kernel/field-traversal}/findFieldByPath.js +0 -0
  119. /package/dist/core/{field-traversal → kernel/field-traversal}/guards.d.ts +0 -0
  120. /package/dist/core/{field-traversal → kernel/field-traversal}/guards.js +0 -0
  121. /package/dist/core/{field-traversal → kernel/field-traversal}/index.d.ts +0 -0
  122. /package/dist/core/{field-traversal → kernel/field-traversal}/index.js +0 -0
  123. /package/dist/core/{field-traversal → kernel/field-traversal}/kernel.d.ts +0 -0
  124. /package/dist/core/{field-traversal → kernel/field-traversal}/kernel.js +0 -0
  125. /package/dist/core/{field-traversal → kernel/field-traversal}/predicates.d.ts +0 -0
  126. /package/dist/core/{field-traversal → kernel/field-traversal}/predicates.js +0 -0
  127. /package/dist/core/{field-traversal → kernel/field-traversal}/types.d.ts +0 -0
  128. /package/dist/core/{field-traversal → kernel/field-traversal}/types.js +0 -0
  129. /package/dist/core/{field-traversal → kernel/field-traversal}/walkFields.d.ts +0 -0
  130. /package/dist/core/{field-traversal → kernel/field-traversal}/walkFields.js +0 -0
  131. /package/dist/core/{lexical → kernel/lexical}/collectTextNodes.d.ts +0 -0
  132. /package/dist/core/{lexical → kernel/lexical}/collectTextNodes.js +0 -0
  133. /package/dist/core/{lexical → kernel/lexical}/guards.d.ts +0 -0
  134. /package/dist/core/{lexical → kernel/lexical}/guards.js +0 -0
  135. /package/dist/core/{lexical → kernel/lexical}/index.d.ts +0 -0
  136. /package/dist/core/{lexical → kernel/lexical}/index.js +0 -0
  137. /package/dist/core/{lexical → kernel/lexical}/isEmptyRichText.d.ts +0 -0
  138. /package/dist/core/{lexical → kernel/lexical}/isEmptyRichText.js +0 -0
  139. /package/dist/core/{lexical → kernel/lexical}/traverseLexicalTree.d.ts +0 -0
  140. /package/dist/core/{lexical → kernel/lexical}/traverseLexicalTree.js +0 -0
  141. /package/dist/core/{lexical → kernel/lexical}/types.d.ts +0 -0
  142. /package/dist/core/{lexical → kernel/lexical}/types.js +0 -0
  143. /package/dist/core/{utils → kernel/utils}/index.d.ts +0 -0
  144. /package/dist/core/{utils → kernel/utils}/index.js +0 -0
  145. /package/dist/core/{utils → kernel/utils}/isEmpty.d.ts +0 -0
  146. /package/dist/core/{utils → kernel/utils}/isEmpty.js +0 -0
  147. /package/dist/core/{utils → kernel/utils}/isObject.d.ts +0 -0
  148. /package/dist/core/{utils → kernel/utils}/isObject.js +0 -0
package/README.md CHANGED
@@ -196,6 +196,43 @@ Dismiss acknowledges the drift without re-translating; the marker stays hidden u
196
196
  changes again. When `provenance` is disabled nothing is shown. Note the fingerprint is text-only, so
197
197
  formatting-only edits to rich text do not mark a locale stale.
198
198
 
199
+ ### Auto-translate on source change
200
+
201
+ _Since v0.9.0._
202
+
203
+ Opt in per collection with `withAutoTranslate` and the plugin queues translations automatically when a
204
+ document's source-locale content changes — no manual trigger. Off by default; a collection is enabled
205
+ only by wrapping it.
206
+
207
+ ```ts
208
+ import { translatorPlugin, withAutoTranslate, createOpenAIProvider, createPayloadJobsRunner } from "@focus-reactive/payload-plugin-translator";
209
+
210
+ translatorPlugin({
211
+ collections: [withAutoTranslate(Posts, { targets: ["de", "fr"], debounceMs: 2000 })],
212
+ translationProvider: createOpenAIProvider({ apiKey: process.env.OPENAI_API_KEY }),
213
+ runner: createPayloadJobsRunner(),
214
+ });
215
+ ```
216
+
217
+ | Option | Type | Default | Meaning |
218
+ | ------ | ---- | ------- | ------- |
219
+ | `targets` | `string[]` | — | Locales to translate into. The source locale is always excluded. |
220
+ | `strategy` | `"overwrite" \| "skip_existing"` | `"overwrite"` | How target content is written. |
221
+ | `debounceMs` | `number` | `0` | Delay before the job runs, coalescing rapid edits (see below). |
222
+ | `sourceLocale` | `string` | `localization.defaultLocale` | Override the source locale for this collection. |
223
+
224
+ Behaviour: fires only on a **published** source save (draft/autosave saves are ignored; a collection
225
+ without drafts treats every save as published); skips when no translatable content actually changed
226
+ (same fingerprint as stale-detection); coalesces rapid edits via `debounceMs`; the translation is saved
227
+ with the source document's status (published source → published translation); never re-triggers on its
228
+ own translation writes; and never fails the editor's save (best-effort — failures are logged).
229
+
230
+ > **Requires a working job runner.** Auto-translate only **enqueues** jobs — they run via the task
231
+ > runner (`createPayloadJobsRunner`) and its autorun loop. On serverless platforms such as **Vercel**,
232
+ > cron-based autorun may not run automatically, so enqueued translations can sit unexecuted until
233
+ > triggered — e.g. an external cron hitting the run endpoint, or a self-hosted worker. Make sure your
234
+ > deployment actually executes queued jobs before relying on auto-translate.
235
+
199
236
  ### Lifecycle callbacks
200
237
 
201
238
  _Since v0.7.0._
@@ -0,0 +1,40 @@
1
+ import type { CollectionConfig } from "payload";
2
+ import type { AutoTranslateConfig } from "./core/domain/auto-translate";
3
+ export type { AutoTranslateConfig };
4
+ export type { AutoTranslateStrategy } from "./core/domain/auto-translate";
5
+ /**
6
+ * Enable opt-in **auto-translate** for a collection: when a document's source-locale content changes
7
+ * (and is published), the plugin automatically queues translations into the configured target locales.
8
+ * Off by default — a collection is opted in only by wrapping it with this helper. The rule is stamped
9
+ * onto `collection.custom` (the input is not mutated; a new collection is returned), mirroring
10
+ * `withFieldTranslation` at the field level.
11
+ *
12
+ * Behaviour: fires only on a **published** source save (draft/autosave saves are ignored; a collection
13
+ * without drafts treats every save as published); skips when no translatable content actually changed
14
+ * (drift-gate); coalesces rapid edits via `debounceMs`; never re-triggers on its own translation
15
+ * writes; and never fails the editor's save (best-effort).
16
+ *
17
+ * **Requires a working job runner.** Auto-translate only ENQUEUES jobs — they run via the plugin's
18
+ * task runner (`createPayloadJobsRunner`) and its autorun loop. On serverless platforms such as
19
+ * **Vercel**, cron-based autorun may not run automatically, so enqueued translations can sit
20
+ * unexecuted until triggered — e.g. an external cron hitting the run-translation endpoint, or a
21
+ * self-hosted worker. Ensure your deployment actually executes queued jobs before relying on this.
22
+ *
23
+ * @param collection - The collection to opt in.
24
+ * @param config - Target locales (+ optional strategy, debounce, source-locale override).
25
+ * @returns A new collection with the auto-translate rule applied (the input is not mutated).
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * import { withAutoTranslate } from '@focus-reactive/payload-plugin-translator'
30
+ *
31
+ * translatorPlugin({
32
+ * collections: [withAutoTranslate(Posts, { targets: ['de', 'fr'], debounceMs: 2000 })],
33
+ * translationProvider,
34
+ * runner,
35
+ * })
36
+ * ```
37
+ *
38
+ * @since 0.9.0
39
+ */
40
+ export declare function withAutoTranslate(collection: CollectionConfig, config: AutoTranslateConfig): CollectionConfig;
@@ -0,0 +1,46 @@
1
+ import { AUTO_TRANSLATE_CUSTOM_KEY } from "./core/domain/auto-translate";
2
+ /**
3
+ * Enable opt-in **auto-translate** for a collection: when a document's source-locale content changes
4
+ * (and is published), the plugin automatically queues translations into the configured target locales.
5
+ * Off by default — a collection is opted in only by wrapping it with this helper. The rule is stamped
6
+ * onto `collection.custom` (the input is not mutated; a new collection is returned), mirroring
7
+ * `withFieldTranslation` at the field level.
8
+ *
9
+ * Behaviour: fires only on a **published** source save (draft/autosave saves are ignored; a collection
10
+ * without drafts treats every save as published); skips when no translatable content actually changed
11
+ * (drift-gate); coalesces rapid edits via `debounceMs`; never re-triggers on its own translation
12
+ * writes; and never fails the editor's save (best-effort).
13
+ *
14
+ * **Requires a working job runner.** Auto-translate only ENQUEUES jobs — they run via the plugin's
15
+ * task runner (`createPayloadJobsRunner`) and its autorun loop. On serverless platforms such as
16
+ * **Vercel**, cron-based autorun may not run automatically, so enqueued translations can sit
17
+ * unexecuted until triggered — e.g. an external cron hitting the run-translation endpoint, or a
18
+ * self-hosted worker. Ensure your deployment actually executes queued jobs before relying on this.
19
+ *
20
+ * @param collection - The collection to opt in.
21
+ * @param config - Target locales (+ optional strategy, debounce, source-locale override).
22
+ * @returns A new collection with the auto-translate rule applied (the input is not mutated).
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { withAutoTranslate } from '@focus-reactive/payload-plugin-translator'
27
+ *
28
+ * translatorPlugin({
29
+ * collections: [withAutoTranslate(Posts, { targets: ['de', 'fr'], debounceMs: 2000 })],
30
+ * translationProvider,
31
+ * runner,
32
+ * })
33
+ * ```
34
+ *
35
+ * @since 0.9.0
36
+ */ export function withAutoTranslate(collection, config) {
37
+ return {
38
+ ...collection,
39
+ custom: {
40
+ ...collection.custom ?? {},
41
+ [AUTO_TRANSLATE_CUSTOM_KEY]: config
42
+ }
43
+ };
44
+ }
45
+
46
+ //# sourceMappingURL=auto-translate-config.js.map
@@ -7,6 +7,7 @@ import { useCollectionTranslationStatus } from "./api/queries/useCollectionTrans
7
7
  import { useDocumentStaleness } from "./api/queries/useDocumentStaleness";
8
8
  import { useDocumentTranslation } from "./api/queries/useDocumentTranslation";
9
9
  export { TranslationStatusList } from "./ui/TranslationStatusList";
10
+ export { AutoTranslateMarker } from "./ui/AutoTranslateMarker";
10
11
  export declare const TranslationsApi: {
11
12
  useRunDocumentTranslation: typeof useRunDocumentTranslation;
12
13
  useQueueDocumentTranslation: typeof useQueueDocumentTranslation;
@@ -7,6 +7,7 @@ import { useCollectionTranslationStatus } from "./api/queries/useCollectionTrans
7
7
  import { useDocumentStaleness } from "./api/queries/useDocumentStaleness";
8
8
  import { useDocumentTranslation } from "./api/queries/useDocumentTranslation";
9
9
  export { TranslationStatusList } from "./ui/TranslationStatusList";
10
+ export { AutoTranslateMarker } from "./ui/AutoTranslateMarker";
10
11
  export const TranslationsApi = {
11
12
  useRunDocumentTranslation,
12
13
  useQueueDocumentTranslation,
@@ -0,0 +1,15 @@
1
+ /** The auto-translate facts the marker renders — resolved from a collection's opt-in config. */
2
+ export type AutoTranslateSummary = {
3
+ targets: string[];
4
+ sourceLocale: string;
5
+ };
6
+ /**
7
+ * Resolve a collection's auto-translate summary for the popup marker, or `null` when it should not
8
+ * render. Reads the opt-in from the collection's `custom` (propagated onto the registered collection at
9
+ * init), resolves the source locale (per-collection override else the config default), and drops the
10
+ * source from the displayed targets. Returns `null` when off, when no source locale is resolvable, or
11
+ * when no target remains — so the caller renders the marker only when it is both enabled and meaningful.
12
+ */
13
+ export declare function resolveAutoTranslateSummary(collection: {
14
+ custom?: Record<string, unknown>;
15
+ } | undefined, defaultLocale: string | undefined): AutoTranslateSummary | null;
@@ -0,0 +1,22 @@
1
+ import { getAutoTranslateConfig } from "../../../../core/domain/auto-translate";
2
+ /**
3
+ * Resolve a collection's auto-translate summary for the popup marker, or `null` when it should not
4
+ * render. Reads the opt-in from the collection's `custom` (propagated onto the registered collection at
5
+ * init), resolves the source locale (per-collection override else the config default), and drops the
6
+ * source from the displayed targets. Returns `null` when off, when no source locale is resolvable, or
7
+ * when no target remains — so the caller renders the marker only when it is both enabled and meaningful.
8
+ */ export function resolveAutoTranslateSummary(collection, defaultLocale) {
9
+ if (!collection) return null;
10
+ const config = getAutoTranslateConfig(collection);
11
+ if (!config) return null;
12
+ const sourceLocale = config.sourceLocale ?? defaultLocale;
13
+ if (!sourceLocale) return null;
14
+ const targets = config.targets.filter((target)=>target !== sourceLocale);
15
+ if (targets.length === 0) return null;
16
+ return {
17
+ targets,
18
+ sourceLocale
19
+ };
20
+ }
21
+
22
+ //# sourceMappingURL=autoTranslateSummary.js.map
@@ -0,0 +1,14 @@
1
+ type AutoTranslateMarkerProps = {
2
+ /** Target locale codes this collection auto-translates into (source already excluded). */
3
+ targets: string[];
4
+ /** The resolved source locale code changes are watched on. */
5
+ sourceLocale: string;
6
+ };
7
+ /**
8
+ * A quiet, off-to-the-side marker in the translation popups' header (document + collection): a single
9
+ * muted icon that a collection is opted into auto-translate. The detail lives in the tooltip, not the
10
+ * layout — this is ambient config the editor rarely needs, so it stays out of the popup's main vertical
11
+ * flow (title / Translate / Status). Rendered only when auto-translate is enabled.
12
+ */
13
+ export declare function AutoTranslateMarker({ targets, sourceLocale }: AutoTranslateMarkerProps): import("react/jsx-runtime").JSX.Element;
14
+ export {};
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { AutoTranslateIcon } from "../../../../shared/lib/assets/icons/AutoTranslateIcon";
4
+ import Tooltip from "../../../../shared/ui/Tooltip";
5
+ import styles from "./styles.module.scss";
6
+ /**
7
+ * A quiet, off-to-the-side marker in the translation popups' header (document + collection): a single
8
+ * muted icon that a collection is opted into auto-translate. The detail lives in the tooltip, not the
9
+ * layout — this is ambient config the editor rarely needs, so it stays out of the popup's main vertical
10
+ * flow (title / Translate / Status). Rendered only when auto-translate is enabled.
11
+ */ export function AutoTranslateMarker({ targets, sourceLocale }) {
12
+ return /*#__PURE__*/ _jsx(Tooltip, {
13
+ content: /*#__PURE__*/ _jsxs("span", {
14
+ className: styles.tip,
15
+ children: [
16
+ "Auto-translate is on. Publishing changes to the source (",
17
+ /*#__PURE__*/ _jsx("code", {
18
+ children: sourceLocale
19
+ }),
20
+ ") content queues translations into ",
21
+ targets.join(" · "),
22
+ "."
23
+ ]
24
+ }),
25
+ children: /*#__PURE__*/ _jsx("span", {
26
+ className: styles.marker,
27
+ tabIndex: 0,
28
+ "aria-label": "Auto-translate enabled",
29
+ children: /*#__PURE__*/ _jsx(AutoTranslateIcon, {})
30
+ })
31
+ });
32
+ }
33
+
34
+ //# sourceMappingURL=AutoTranslateMarker.js.map
@@ -0,0 +1 @@
1
+ export { AutoTranslateMarker } from "./AutoTranslateMarker";
@@ -0,0 +1,3 @@
1
+ export { AutoTranslateMarker } from "./AutoTranslateMarker";
2
+
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,30 @@
1
+ // A muted, low-emphasis header marker — no border, no fill; it sits at the edge of the title row and
2
+ // carries all detail in its tooltip, so it never competes with the popup's main flow.
3
+ .marker {
4
+ display: inline-flex;
5
+ align-items: center;
6
+ flex-shrink: 0;
7
+ color: var(--theme-elevation-400, #a5a5a5);
8
+ cursor: help;
9
+ outline: none;
10
+ }
11
+
12
+ .marker:hover {
13
+ color: var(--theme-elevation-600, #6d6d6d);
14
+ }
15
+
16
+ .marker:focus-visible {
17
+ outline: 2px solid var(--theme-elevation-400, #a5a5a5);
18
+ outline-offset: 2px;
19
+ border-radius: 3px;
20
+ }
21
+
22
+ .tip {
23
+ display: block;
24
+ max-width: 15rem;
25
+ line-height: 1.35;
26
+ }
27
+
28
+ .tip code {
29
+ font-family: var(--font-mono, ui-monospace, monospace);
30
+ }
@@ -0,0 +1,3 @@
1
+ /** A lightning bolt — connotes "fires automatically" (on the publish event). Used by the
2
+ * auto-translate marker to distinguish automatic translation from the manual translate control. */
3
+ export declare const AutoTranslateIcon: () => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,14 @@
1
+ /** A lightning bolt — connotes "fires automatically" (on the publish event). Used by the
2
+ * auto-translate marker to distinguish automatic translation from the manual translate control. */ import { jsx as _jsx } from "react/jsx-runtime";
3
+ export const AutoTranslateIcon = ()=>/*#__PURE__*/ _jsx("svg", {
4
+ fill: "currentColor",
5
+ width: "1em",
6
+ height: "1em",
7
+ viewBox: "0 0 24 24",
8
+ xmlns: "http://www.w3.org/2000/svg",
9
+ children: /*#__PURE__*/ _jsx("path", {
10
+ d: "M13 2 L4.5 13.5 H11 L10 22 L19.5 10 H12.5 Z"
11
+ })
12
+ });
13
+
14
+ //# sourceMappingURL=AutoTranslateIcon.js.map
@@ -1,5 +1,7 @@
1
+ import type { AutoTranslateSummary } from "../../../entities/translation/model/autoTranslateSummary";
1
2
  type BulkTranslationDashboardProps = {
2
3
  hasDrafts: boolean;
4
+ autoTranslate: AutoTranslateSummary | null;
3
5
  };
4
- export default function BulkTranslationDashboard({ hasDrafts }: BulkTranslationDashboardProps): import("react/jsx-runtime").JSX.Element;
6
+ export default function BulkTranslationDashboard({ hasDrafts, autoTranslate, }: BulkTranslationDashboardProps): import("react/jsx-runtime").JSX.Element;
5
7
  export {};
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { toast, useLocale, useSelection } from "@payloadcms/ui";
4
4
  import { SelectAllStatus } from "@payloadcms/ui/providers/Selection";
5
5
  import { useEffect, useMemo } from "react";
6
- import { deriveCollectionPanelStatus, TranslationsApi } from "../../../entities/translation";
6
+ import { AutoTranslateMarker, deriveCollectionPanelStatus, TranslationsApi } from "../../../entities/translation";
7
7
  import { CollectionTranslationForm, FORM_FIELDS } from "../../../features/collection-translation-form";
8
8
  import { CollectionTranslationFormModel } from "../../../features/collection-translation-form/index.client";
9
9
  import CollectionTranslationPopup from "../../../features/collection-translation-popup";
@@ -11,7 +11,7 @@ import { CollectionTranslationProgress } from "../../../features/collection-tran
11
11
  import { handleFormError } from "../../../shared/lib/forms/handle-form-error";
12
12
  import { useCollectionDashboardUrlParams } from "../../../shared/lib/payload/hooks/useCollectionDashboardUrlParams";
13
13
  import styles from "./styles.module.scss";
14
- export default function BulkTranslationDashboard({ hasDrafts }) {
14
+ export default function BulkTranslationDashboard({ hasDrafts, autoTranslate }) {
15
15
  const locale = useLocale();
16
16
  const { collection } = useCollectionDashboardUrlParams();
17
17
  const documentsSelection = useSelection();
@@ -58,9 +58,18 @@ export default function BulkTranslationDashboard({ hasDrafts }) {
58
58
  status: panelStatus,
59
59
  selectedCount: selectedCount,
60
60
  children: [
61
- /*#__PURE__*/ _jsx("h4", {
62
- className: styles.title,
63
- children: "Bulk translation"
61
+ /*#__PURE__*/ _jsxs("div", {
62
+ className: styles.header,
63
+ children: [
64
+ /*#__PURE__*/ _jsx("h4", {
65
+ className: styles.title,
66
+ children: "Bulk translation"
67
+ }),
68
+ autoTranslate && /*#__PURE__*/ _jsx(AutoTranslateMarker, {
69
+ targets: autoTranslate.targets,
70
+ sourceLocale: autoTranslate.sourceLocale
71
+ })
72
+ ]
64
73
  }),
65
74
  /*#__PURE__*/ _jsxs("section", {
66
75
  className: styles.section,
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { headers as getHeaders } from "next/headers";
3
3
  import { collectionHasDrafts } from "../../../../server/shared/guards";
4
+ import { resolveAutoTranslateSummary } from "../../../entities/translation/model/autoTranslateSummary";
4
5
  import BulkTranslationDashboard from "./BulkTranslationDashboard";
5
6
  const BulkTranslationDashboardServer = async (props)=>{
6
7
  const headers = await getHeaders();
@@ -15,8 +16,10 @@ const BulkTranslationDashboardServer = async (props)=>{
15
16
  if (!props.collectionSlug) return null;
16
17
  const collection = props.payload.collections[props.collectionSlug]?.config;
17
18
  const hasDrafts = collection ? collectionHasDrafts(collection) : false;
19
+ const autoTranslate = resolveAutoTranslateSummary(collection, props.payload.config.localization ? props.payload.config.localization.defaultLocale : undefined);
18
20
  return /*#__PURE__*/ _jsx(BulkTranslationDashboard, {
19
- hasDrafts: hasDrafts
21
+ hasDrafts: hasDrafts,
22
+ autoTranslate: autoTranslate
20
23
  });
21
24
  };
22
25
  export default BulkTranslationDashboardServer;
@@ -1,3 +1,12 @@
1
+ // Title row: the section heading on the left, ambient markers (e.g. auto-translate) pushed to the
2
+ // right edge so they stay out of the popup's main vertical flow.
3
+ .header {
4
+ display: flex;
5
+ align-items: center;
6
+ justify-content: space-between;
7
+ gap: 0.5rem;
8
+ }
9
+
1
10
  .title {
2
11
  margin: 0;
3
12
  font-size: 0.8125rem;
@@ -1,5 +1,7 @@
1
+ import type { AutoTranslateSummary } from "../../../entities/translation/model/autoTranslateSummary";
1
2
  type TranslateDocumentProps = {
2
3
  hasDrafts: boolean;
4
+ autoTranslate: AutoTranslateSummary | null;
3
5
  };
4
- declare const TranslateDocument: ({ hasDrafts }: TranslateDocumentProps) => import("react/jsx-runtime").JSX.Element;
6
+ declare const TranslateDocument: ({ hasDrafts, autoTranslate }: TranslateDocumentProps) => import("react/jsx-runtime").JSX.Element;
5
7
  export default TranslateDocument;
@@ -2,14 +2,14 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { toast, useLocale } from "@payloadcms/ui";
4
4
  import { useEffect, useMemo } from "react";
5
- import { buildTranslationStatusRows, deriveDocumentRunStatus, derivePanelStatus, TranslationsApi, TranslationStatusList } from "../../../entities/translation";
5
+ import { AutoTranslateMarker, buildTranslationStatusRows, deriveDocumentRunStatus, derivePanelStatus, TranslationsApi, TranslationStatusList } from "../../../entities/translation";
6
6
  import { OpenDocumentTranslationPopup } from "../../../features/open-document-translation-popup";
7
7
  import { DocumentTranslationForm, FORM_FIELDS } from "../../../features/translate-document-form";
8
8
  import { DocumentTranslationFormModel } from "../../../features/translate-document-form/index.client";
9
9
  import { handleFormError } from "../../../shared/lib/forms/handle-form-error";
10
10
  import { useCollectionDocumentUrlParams } from "../../../shared/lib/payload/hooks/useCollectionDocumentUrlParams";
11
11
  import styles from "./styles.module.scss";
12
- const TranslateDocument = ({ hasDrafts })=>{
12
+ const TranslateDocument = ({ hasDrafts, autoTranslate })=>{
13
13
  const locale = useLocale();
14
14
  const params = useCollectionDocumentUrlParams();
15
15
  const queueTranslationApi = TranslationsApi.useQueueDocumentTranslation();
@@ -72,9 +72,18 @@ const TranslateDocument = ({ hasDrafts })=>{
72
72
  status: panelStatus,
73
73
  children: ({ close })=>/*#__PURE__*/ _jsxs(_Fragment, {
74
74
  children: [
75
- /*#__PURE__*/ _jsx("h4", {
76
- className: styles.title,
77
- children: "Document translation"
75
+ /*#__PURE__*/ _jsxs("div", {
76
+ className: styles.header,
77
+ children: [
78
+ /*#__PURE__*/ _jsx("h4", {
79
+ className: styles.title,
80
+ children: "Document translation"
81
+ }),
82
+ autoTranslate && /*#__PURE__*/ _jsx(AutoTranslateMarker, {
83
+ targets: autoTranslate.targets,
84
+ sourceLocale: autoTranslate.sourceLocale
85
+ })
86
+ ]
78
87
  }),
79
88
  /*#__PURE__*/ _jsxs("section", {
80
89
  className: styles.section,
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { headers as getHeaders } from "next/headers";
3
3
  import { collectionHasDrafts } from "../../../../server/shared/guards";
4
+ import { resolveAutoTranslateSummary } from "../../../entities/translation/model/autoTranslateSummary";
4
5
  import TranslateDocument from "./TranslateDocument";
5
6
  async function TranslateDocumentServer(props) {
6
7
  const headers = await getHeaders();
@@ -14,8 +15,10 @@ async function TranslateDocumentServer(props) {
14
15
  if (!hasAccess) return null;
15
16
  if (!props.id) return null;
16
17
  const hasDrafts = collectionHasDrafts(props.collection);
18
+ const autoTranslate = resolveAutoTranslateSummary(props.collection, props.payload.config.localization ? props.payload.config.localization.defaultLocale : undefined);
17
19
  return /*#__PURE__*/ _jsx(TranslateDocument, {
18
- hasDrafts: hasDrafts
20
+ hasDrafts: hasDrafts,
21
+ autoTranslate: autoTranslate
19
22
  });
20
23
  }
21
24
  export default TranslateDocumentServer;
@@ -1,3 +1,12 @@
1
+ // Title row: the section heading on the left, ambient markers (e.g. auto-translate) pushed to the
2
+ // right edge so they stay out of the popup's main vertical flow.
3
+ .header {
4
+ display: flex;
5
+ align-items: center;
6
+ justify-content: space-between;
7
+ gap: 0.5rem;
8
+ }
9
+
1
10
  .title {
2
11
  margin: 0;
3
12
  font-size: 0.8125rem;
@@ -0,0 +1,14 @@
1
+ import type { AutoTranslateConfig } from "./types";
2
+ /**
3
+ * Read a collection's auto-translate rule from its `custom` bag, or `null` when the collection is not
4
+ * opted in. Payload-free (only the `custom` extension point is read), mirroring
5
+ * `getFieldTranslationConfig` at the field level. Returns `null` when the key is absent OR holds a value
6
+ * that is not a valid config, so a foreign `custom.{key}` collision can never crash a caller.
7
+ *
8
+ * @param collection - The collection to read (only its `custom` extension point is inspected).
9
+ * @returns The auto-translate config, or `null` when absent/malformed.
10
+ * @since 0.9.0
11
+ */
12
+ export declare function getAutoTranslateConfig(collection: {
13
+ custom?: Record<string, unknown>;
14
+ }): AutoTranslateConfig | null;
@@ -0,0 +1,29 @@
1
+ import { isObject } from "../../kernel/utils/isObject";
2
+ import { AUTO_TRANSLATE_CUSTOM_KEY } from "./types";
3
+ /**
4
+ * Structural guard: is `value` a well-formed auto-translate config? Validates the one crash-critical
5
+ * invariant — `targets` is an array of strings — so a foreign value that a consumer happened to store
6
+ * under {@link AUTO_TRANSLATE_CUSTOM_KEY} (the key is a shared `custom` namespace, so a collision, while
7
+ * unlikely, is possible) is treated as "not opted in" rather than crashing the readers that dereference
8
+ * `targets`.
9
+ */ function isAutoTranslateConfig(value) {
10
+ if (!isObject(value)) return false;
11
+ const { targets } = value;
12
+ return Array.isArray(targets) && targets.every((target)=>typeof target === "string");
13
+ }
14
+ /**
15
+ * Read a collection's auto-translate rule from its `custom` bag, or `null` when the collection is not
16
+ * opted in. Payload-free (only the `custom` extension point is read), mirroring
17
+ * `getFieldTranslationConfig` at the field level. Returns `null` when the key is absent OR holds a value
18
+ * that is not a valid config, so a foreign `custom.{key}` collision can never crash a caller.
19
+ *
20
+ * @param collection - The collection to read (only its `custom` extension point is inspected).
21
+ * @returns The auto-translate config, or `null` when absent/malformed.
22
+ * @since 0.9.0
23
+ */ export function getAutoTranslateConfig(collection) {
24
+ if (!isObject(collection.custom)) return null;
25
+ const value = collection.custom[AUTO_TRANSLATE_CUSTOM_KEY];
26
+ return isAutoTranslateConfig(value) ? value : null;
27
+ }
28
+
29
+ //# sourceMappingURL=getAutoTranslateConfig.js.map
@@ -0,0 +1,21 @@
1
+ import type { FieldLike } from "../../kernel/field-traversal";
2
+ /**
3
+ * Whether a source-locale save actually changed translatable content — the auto-translate drift-gate,
4
+ * in one place. Compares the translatable-content fingerprint of the previous vs the current document
5
+ * (the same hash the provenance write/read path uses via {@link computeSourceFingerprint}), so a save
6
+ * that touched only non-translatable fields never triggers a re-translation.
7
+ *
8
+ * A create (no `previousDoc`) counts as changed: there is nothing to diff against, and the new
9
+ * document's translatable content is by definition not yet translated.
10
+ *
11
+ * Pure and payload-free (sibling of `core/provenance` `isRecordStale`) so the gate is testable without
12
+ * a database; the server hook supplies the two documents Payload already hands to `afterChange` plus
13
+ * the original field schema.
14
+ *
15
+ * @param previousDoc - The source document before this save, or `null`/`undefined` on create.
16
+ * @param nextDoc - The source document after this save.
17
+ * @param schema - The ORIGINAL (un-sanitized) field schema for the collection.
18
+ * @returns `true` when translatable content changed (or on create); `false` when unchanged.
19
+ * @since 0.9.0
20
+ */
21
+ export declare function hasSourceContentChanged(previousDoc: Record<string, unknown> | null | undefined, nextDoc: Record<string, unknown>, schema: FieldLike[]): boolean;
@@ -0,0 +1,25 @@
1
+ import { computeSourceFingerprint } from "../content-projection/computeSourceFingerprint";
2
+ /**
3
+ * Whether a source-locale save actually changed translatable content — the auto-translate drift-gate,
4
+ * in one place. Compares the translatable-content fingerprint of the previous vs the current document
5
+ * (the same hash the provenance write/read path uses via {@link computeSourceFingerprint}), so a save
6
+ * that touched only non-translatable fields never triggers a re-translation.
7
+ *
8
+ * A create (no `previousDoc`) counts as changed: there is nothing to diff against, and the new
9
+ * document's translatable content is by definition not yet translated.
10
+ *
11
+ * Pure and payload-free (sibling of `core/provenance` `isRecordStale`) so the gate is testable without
12
+ * a database; the server hook supplies the two documents Payload already hands to `afterChange` plus
13
+ * the original field schema.
14
+ *
15
+ * @param previousDoc - The source document before this save, or `null`/`undefined` on create.
16
+ * @param nextDoc - The source document after this save.
17
+ * @param schema - The ORIGINAL (un-sanitized) field schema for the collection.
18
+ * @returns `true` when translatable content changed (or on create); `false` when unchanged.
19
+ * @since 0.9.0
20
+ */ export function hasSourceContentChanged(previousDoc, nextDoc, schema) {
21
+ if (!previousDoc) return true;
22
+ return computeSourceFingerprint(previousDoc, schema) !== computeSourceFingerprint(nextDoc, schema);
23
+ }
24
+
25
+ //# sourceMappingURL=hasSourceContentChanged.js.map
@@ -0,0 +1,4 @@
1
+ export { getAutoTranslateConfig } from "./getAutoTranslateConfig";
2
+ export { hasSourceContentChanged } from "./hasSourceContentChanged";
3
+ export { AUTO_TRANSLATE_CUSTOM_KEY } from "./types";
4
+ export type { AutoTranslateConfig, AutoTranslateStrategy } from "./types";
@@ -0,0 +1,5 @@
1
+ export { getAutoTranslateConfig } from "./getAutoTranslateConfig";
2
+ export { hasSourceContentChanged } from "./hasSourceContentChanged";
3
+ export { AUTO_TRANSLATE_CUSTOM_KEY } from "./types";
4
+
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Key under a collection's `custom` bag where its auto-translate rule is stamped by `withAutoTranslate`
3
+ * and read back by {@link getAutoTranslateConfig}. Mirrors the field-level `TRANSLATE_KIT_CUSTOM_KEY`
4
+ * pattern — a single typed key so writer and reader never diverge.
5
+ */
6
+ export declare const AUTO_TRANSLATE_CUSTOM_KEY = "translatorAutoTranslate";
7
+ /**
8
+ * Translation strategy for auto-enqueued jobs (matches the task-runner's strategy union):
9
+ * - `"overwrite"` — (re)translate every target field, replacing any existing target-locale value.
10
+ * - `"skip_existing"` — only fill target fields that are currently empty, leaving existing translations
11
+ * untouched.
12
+ * @since 0.9.0
13
+ */
14
+ export type AutoTranslateStrategy = "overwrite" | "skip_existing";
15
+ /**
16
+ * A collection's opt-in auto-translate rule (developer-configured, v1). Presence of this config on a
17
+ * collection = opted in; absence = off (the default).
18
+ * @since 0.9.0
19
+ */
20
+ export type AutoTranslateConfig = {
21
+ /** Target locales to translate into when the source changes. The source locale is always excluded. */
22
+ targets: string[];
23
+ /** Translation strategy; defaults to `"overwrite"`. */
24
+ strategy?: AutoTranslateStrategy;
25
+ /**
26
+ * Delay (ms) before the queued job runs, coalescing rapid edits via the job runner's
27
+ * per-(document, locale) supersession. `0`/omitted = enqueue immediately.
28
+ */
29
+ debounceMs?: number;
30
+ /** Override the source locale for this collection; defaults to `localization.defaultLocale`. */
31
+ sourceLocale?: string;
32
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Key under a collection's `custom` bag where its auto-translate rule is stamped by `withAutoTranslate`
3
+ * and read back by {@link getAutoTranslateConfig}. Mirrors the field-level `TRANSLATE_KIT_CUSTOM_KEY`
4
+ * pattern — a single typed key so writer and reader never diverge.
5
+ */ export const AUTO_TRANSLATE_CUSTOM_KEY = "translatorAutoTranslate";
6
+
7
+ //# sourceMappingURL=types.js.map
@@ -1,4 +1,4 @@
1
- import type { FieldLike } from "../field-traversal";
1
+ import type { FieldLike } from "../../kernel/field-traversal";
2
2
  /**
3
3
  * The staleness baseline: a stable hash of a document's translatable content.
4
4
  * `CURRENT = computeSourceFingerprint(sourceDoc, schema)` — the value provenance stores at translation