@focus-reactive/payload-plugin-translator 0.8.2 → 0.9.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.
- package/README.md +37 -0
- package/dist/auto-translate-config.d.ts +40 -0
- package/dist/auto-translate-config.js +46 -0
- package/dist/client/entities/translation/index.d.ts +1 -0
- package/dist/client/entities/translation/index.js +1 -0
- package/dist/client/entities/translation/model/autoTranslateSummary.d.ts +15 -0
- package/dist/client/entities/translation/model/autoTranslateSummary.js +22 -0
- package/dist/client/entities/translation/ui/AutoTranslateMarker/AutoTranslateMarker.d.ts +14 -0
- package/dist/client/entities/translation/ui/AutoTranslateMarker/AutoTranslateMarker.js +34 -0
- package/dist/client/entities/translation/ui/AutoTranslateMarker/index.d.ts +1 -0
- package/dist/client/entities/translation/ui/AutoTranslateMarker/index.js +3 -0
- package/dist/client/entities/translation/ui/AutoTranslateMarker/styles.module.scss +30 -0
- package/dist/client/shared/lib/assets/icons/AutoTranslateIcon.d.ts +3 -0
- package/dist/client/shared/lib/assets/icons/AutoTranslateIcon.js +14 -0
- package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.d.ts +3 -1
- package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.js +14 -5
- package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.server.js +4 -1
- package/dist/client/widgets/bulk-translation-dashboard/ui/styles.module.scss +9 -0
- package/dist/client/widgets/translate-document/ui/TranslateDocument.d.ts +3 -1
- package/dist/client/widgets/translate-document/ui/TranslateDocument.js +14 -5
- package/dist/client/widgets/translate-document/ui/TranslateDocument.server.js +4 -1
- package/dist/client/widgets/translate-document/ui/styles.module.scss +9 -0
- package/dist/core/auto-translate/hasSourceContentChanged.d.ts +21 -0
- package/dist/core/auto-translate/hasSourceContentChanged.js +25 -0
- package/dist/core/auto-translate/index.d.ts +1 -0
- package/dist/core/auto-translate/index.js +3 -0
- package/dist/core/auto-translate-config/getAutoTranslateConfig.d.ts +14 -0
- package/dist/core/auto-translate-config/getAutoTranslateConfig.js +29 -0
- package/dist/core/auto-translate-config/index.d.ts +3 -0
- package/dist/core/auto-translate-config/index.js +4 -0
- package/dist/core/auto-translate-config/types.d.ts +32 -0
- package/dist/core/auto-translate-config/types.js +7 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4 -0
- package/dist/plugin.js +5 -0
- package/dist/server/features/translate-document/handler.js +9 -1
- package/dist/server/modules/auto-translate/AutoTranslate.policy.d.ts +79 -0
- package/dist/server/modules/auto-translate/AutoTranslate.policy.js +77 -0
- package/dist/server/modules/auto-translate/AutoTranslate.shapes.d.ts +22 -0
- package/dist/server/modules/auto-translate/AutoTranslate.shapes.js +3 -0
- package/dist/server/modules/auto-translate/AutoTranslate.wiring.d.ts +21 -0
- package/dist/server/modules/auto-translate/AutoTranslate.wiring.js +68 -0
- package/dist/server/modules/auto-translate/AutoTranslateEnqueue.hook.d.ts +39 -0
- package/dist/server/modules/auto-translate/AutoTranslateEnqueue.hook.js +94 -0
- package/dist/server/modules/auto-translate/index.d.ts +4 -0
- package/dist/server/modules/auto-translate/index.js +7 -0
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsTaskRunner.js +4 -0
- package/dist/server/modules/task-runner/types.d.ts +7 -0
- package/dist/types/AutoTranslateContext.d.ts +10 -0
- package/dist/types/AutoTranslateContext.js +11 -0
- package/package.json +1 -1
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/auto-translate-config";
|
|
3
|
+
export type { AutoTranslateConfig };
|
|
4
|
+
export type { AutoTranslateStrategy } from "./core/auto-translate-config";
|
|
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/auto-translate-config";
|
|
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/auto-translate-config";
|
|
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,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__*/
|
|
62
|
-
className: styles.
|
|
63
|
-
children:
|
|
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,
|
package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.server.js
CHANGED
|
@@ -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__*/
|
|
76
|
-
className: styles.
|
|
77
|
-
children:
|
|
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,21 @@
|
|
|
1
|
+
import type { FieldLike } from "../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 @@
|
|
|
1
|
+
export { hasSourceContentChanged } from "./hasSourceContentChanged";
|
|
@@ -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 "../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,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
|
package/dist/index.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export { documentLevel, collectionLevel, fieldLevel } from "./composition/levels
|
|
|
12
12
|
export type { TranslationLevel } from "./server/modules/translation-levels";
|
|
13
13
|
export { withFieldTranslation } from "./field-config";
|
|
14
14
|
export type { FieldTranslationConfig } from "./field-config";
|
|
15
|
+
export { withAutoTranslate } from "./auto-translate-config";
|
|
16
|
+
export type { AutoTranslateConfig, AutoTranslateStrategy } from "./auto-translate-config";
|
|
15
17
|
export { createTranslatePlugin, TranslateCollectionPlugin } from "./plugin";
|
|
16
18
|
export type { TranslateCollectionPluginConfig } from "./plugin";
|
|
17
19
|
export { OpenAITranslationProvider } from "./translation-providers";
|
package/dist/index.js
CHANGED
|
@@ -8,6 +8,10 @@ export { createPayloadJobsRunner, createSyncRunner } from "./server/modules/task
|
|
|
8
8
|
export { documentLevel, collectionLevel, fieldLevel } from "./composition/levels";
|
|
9
9
|
// Field config
|
|
10
10
|
export { withFieldTranslation } from "./field-config";
|
|
11
|
+
// Auto-translate — opt-in, per-collection auto-translation on source-locale change (#51). Wrap a
|
|
12
|
+
// collection with `withAutoTranslate`; requires a working job runner/autorun to execute (see the
|
|
13
|
+
// JSDoc for the Vercel/serverless caveat). Since v0.9.0.
|
|
14
|
+
export { withAutoTranslate } from "./auto-translate-config";
|
|
11
15
|
// Deprecated exports (for backwards compatibility)
|
|
12
16
|
export { createTranslatePlugin, TranslateCollectionPlugin } from "./plugin";
|
|
13
17
|
export { OpenAITranslationProvider } from "./translation-providers";
|
package/dist/plugin.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CacheProviderExport } from "./client/app/cache/CacheProvider.export";
|
|
2
|
+
import { configureAutoTranslate } from "./server/modules/auto-translate";
|
|
2
3
|
import { configureProvenance } from "./server/modules/provenance";
|
|
3
4
|
import { wireTranslateRunner } from "./server/features/translate-document";
|
|
4
5
|
import { documentLevel, collectionLevel } from "./composition/levels";
|
|
@@ -36,6 +37,9 @@ import { normalizePath } from "./server/shared";
|
|
|
36
37
|
lifecycle: lifecycle ?? {},
|
|
37
38
|
collections: Array.from(collectionSlugs)
|
|
38
39
|
});
|
|
40
|
+
// Auto-translate (#51) reads its opt-in from each collection's `custom` (via `withAutoTranslate`);
|
|
41
|
+
// needs the runner's factory, so it wires after `wireTranslateRunner`.
|
|
42
|
+
const autoTranslateModule = configureAutoTranslate(collections, schemaMap, taskRunnerFactory);
|
|
39
43
|
const activeLevels = levels ?? [
|
|
40
44
|
documentLevel(),
|
|
41
45
|
collectionLevel()
|
|
@@ -52,6 +56,7 @@ import { normalizePath } from "./server/shared";
|
|
|
52
56
|
for (const level of activeLevels)level.extend(builder);
|
|
53
57
|
builder.addConfigModifier(runnerConfigModifier);
|
|
54
58
|
builder.addConfigModifier(provenanceModule.configure(collectionSlugs));
|
|
59
|
+
builder.addConfigModifier(autoTranslateModule.configure(collectionSlugs));
|
|
55
60
|
builder.addAdminProvider(new CacheProviderExport(basePath));
|
|
56
61
|
// The single place the Payload config is mutated.
|
|
57
62
|
return builder.applyTo(config);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { APIError } from "payload";
|
|
2
2
|
import { translateContent } from "../../../core/translation-pipeline";
|
|
3
3
|
import { fetchSourceDocument } from "../../shared/payload/sourceDocument";
|
|
4
|
+
import { AUTO_TRANSLATE_SKIP_CONTEXT_KEY } from "../../../types/AutoTranslateContext";
|
|
4
5
|
/**
|
|
5
6
|
* Translates a single document from source language to target language. Provenance is delegated to
|
|
6
7
|
* {@link ProvenanceService}: this handler only decides *when* to capture the source fingerprint
|
|
@@ -73,7 +74,14 @@ import { fetchSourceDocument } from "../../shared/payload/sourceDocument";
|
|
|
73
74
|
data: translatedData,
|
|
74
75
|
autosave: isAutosaveEnabled,
|
|
75
76
|
locale: targetLng,
|
|
76
|
-
fallbackLocale: sourceLng
|
|
77
|
+
fallbackLocale: sourceLng,
|
|
78
|
+
// Mark this as a translator-authored write so the auto-translate afterChange hook (#51) skips it
|
|
79
|
+
// — the loop guard's second barrier, alongside the source-locale check. This write always targets
|
|
80
|
+
// the TARGET locale, so it is already exempt by locale; the flag also covers any future write
|
|
81
|
+
// path that could touch the source locale.
|
|
82
|
+
context: {
|
|
83
|
+
[AUTO_TRANSLATE_SKIP_CONTEXT_KEY]: true
|
|
84
|
+
}
|
|
77
85
|
});
|
|
78
86
|
}
|
|
79
87
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { CollectionSlug } from "payload";
|
|
2
|
+
import type { AutoTranslateConfig } from "../../../core/auto-translate-config";
|
|
3
|
+
import type { TaskInput } from "../task-runner/types";
|
|
4
|
+
/** A collection's auto-translate rule with defaults resolved — the shape the hook consumes. */
|
|
5
|
+
export type NormalizedAutoTranslatePolicy = {
|
|
6
|
+
targets: string[];
|
|
7
|
+
strategy: "overwrite" | "skip_existing";
|
|
8
|
+
debounceMs: number;
|
|
9
|
+
sourceLocale?: string;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the effective auto-translate policy for a document, or `null` when off. The `doc` parameter
|
|
13
|
+
* is the reserved seam for the future document-level manager override (#51 D7): v1 resolves at the
|
|
14
|
+
* collection level and IGNORES `doc`; a later phase supplies a second implementation that consults the
|
|
15
|
+
* document, without changing the hook, drift-gate, debounce, or wiring.
|
|
16
|
+
*/
|
|
17
|
+
export type AutoTranslatePolicyResolver = (collectionSlug: string, doc: Record<string, unknown>) => NormalizedAutoTranslatePolicy | null;
|
|
18
|
+
/** Apply defaults to a raw config: strategy → "overwrite", debounce → 0. Duplicate target locales are
|
|
19
|
+
* de-duplicated so a misconfigured `targets: ["de","de"]` never enqueues two racing jobs for the same
|
|
20
|
+
* (document, locale) in one batch (the runner's supersession only dedupes against already-stored jobs). */
|
|
21
|
+
export declare function normalizeAutoTranslateConfig(config: AutoTranslateConfig): NormalizedAutoTranslatePolicy;
|
|
22
|
+
/**
|
|
23
|
+
* A minimal localization shape — the set of configured locale codes, in either form Payload accepts
|
|
24
|
+
* (`"en"` or `{ code: "en" }`). Payload's `Config["localization"]` is structurally assignable, so the
|
|
25
|
+
* caller passes it straight in with no adapter and tests pass a tiny literal.
|
|
26
|
+
*/
|
|
27
|
+
export type LocalizationLike = false | {
|
|
28
|
+
locales: Array<string | {
|
|
29
|
+
code: string;
|
|
30
|
+
}>;
|
|
31
|
+
};
|
|
32
|
+
/** Extract the configured locale codes, or `null` when localization is disabled/absent. */
|
|
33
|
+
export declare function extractLocaleCodes(localization: LocalizationLike | undefined): Set<string> | null;
|
|
34
|
+
/** Outcome of validating a policy's locales against the configured set — the filtered policy plus what
|
|
35
|
+
* was dropped, so the caller can warn precisely. */
|
|
36
|
+
export type PolicyLocaleFilter = {
|
|
37
|
+
policy: NormalizedAutoTranslatePolicy;
|
|
38
|
+
droppedTargets: string[];
|
|
39
|
+
droppedSourceLocale: string | null;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Drop targets (and a `sourceLocale` override) that are not configured locales, so a mistyped locale in
|
|
43
|
+
* `withAutoTranslate` never reaches the pipeline — where it would silently burn a provider call and
|
|
44
|
+
* either error at the DB (Postgres locale enum) or write orphaned, invisible data (Mongo/SQLite). Pure:
|
|
45
|
+
* returns a new policy and reports the drops; the caller (config-time wiring) emits the warning.
|
|
46
|
+
*/
|
|
47
|
+
export declare function filterPolicyToKnownLocales(policy: NormalizedAutoTranslatePolicy, knownLocales: Set<string>): PolicyLocaleFilter;
|
|
48
|
+
/**
|
|
49
|
+
* The v1 (collection-level) resolver. Ignores `doc` — see {@link AutoTranslatePolicyResolver}. This is
|
|
50
|
+
* the single seam a future document-level manager replaces.
|
|
51
|
+
*/
|
|
52
|
+
export declare function makeCollectionPolicyResolver(policies: Map<string, NormalizedAutoTranslatePolicy>): AutoTranslatePolicyResolver;
|
|
53
|
+
/**
|
|
54
|
+
* Publish-gate (#51 D8): a drafts-enabled collection auto-translates only on a **published** save;
|
|
55
|
+
* autosave/draft saves are ignored. A collection without drafts has no `_status`, so every save
|
|
56
|
+
* qualifies. Applies uniformly to create and update.
|
|
57
|
+
*/
|
|
58
|
+
export declare function passesPublishGate(doc: Record<string, unknown>, hasDrafts: boolean): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Mirror the source document's status onto the translation (#51 D9). Combined with the publish-gate,
|
|
61
|
+
* the source is published whenever we reach enqueue, so translations publish; a no-drafts collection
|
|
62
|
+
* publishes too. Deliberately identical to {@link passesPublishGate} today — kept as a SEPARATE
|
|
63
|
+
* function (not merged) because it diverges once a future document-level manager (R8) can bypass the
|
|
64
|
+
* gate and translate a still-draft source; do not collapse the two.
|
|
65
|
+
*/
|
|
66
|
+
export declare function resolvePublishOnTranslation(doc: Record<string, unknown>, hasDrafts: boolean): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Build one {@link TaskInput} per configured target locale (the source locale is always excluded).
|
|
69
|
+
* `waitUntil` encodes the debounce; `now` is injected so the timestamp is deterministic in tests.
|
|
70
|
+
*/
|
|
71
|
+
export declare function buildAutoTranslateTasks(args: {
|
|
72
|
+
policy: NormalizedAutoTranslatePolicy;
|
|
73
|
+
collectionSlug: CollectionSlug;
|
|
74
|
+
documentId: string;
|
|
75
|
+
sourceLocale: string;
|
|
76
|
+
doc: Record<string, unknown>;
|
|
77
|
+
hasDrafts: boolean;
|
|
78
|
+
now: number;
|
|
79
|
+
}): TaskInput[];
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/** Apply defaults to a raw config: strategy → "overwrite", debounce → 0. Duplicate target locales are
|
|
2
|
+
* de-duplicated so a misconfigured `targets: ["de","de"]` never enqueues two racing jobs for the same
|
|
3
|
+
* (document, locale) in one batch (the runner's supersession only dedupes against already-stored jobs). */ export function normalizeAutoTranslateConfig(config) {
|
|
4
|
+
return {
|
|
5
|
+
targets: [
|
|
6
|
+
...new Set(config.targets)
|
|
7
|
+
],
|
|
8
|
+
strategy: config.strategy ?? "overwrite",
|
|
9
|
+
debounceMs: config.debounceMs ?? 0,
|
|
10
|
+
sourceLocale: config.sourceLocale
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Extract the configured locale codes, or `null` when localization is disabled/absent. */ export function extractLocaleCodes(localization) {
|
|
14
|
+
if (!localization) return null;
|
|
15
|
+
return new Set(localization.locales.map((locale)=>typeof locale === "string" ? locale : locale.code));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Drop targets (and a `sourceLocale` override) that are not configured locales, so a mistyped locale in
|
|
19
|
+
* `withAutoTranslate` never reaches the pipeline — where it would silently burn a provider call and
|
|
20
|
+
* either error at the DB (Postgres locale enum) or write orphaned, invisible data (Mongo/SQLite). Pure:
|
|
21
|
+
* returns a new policy and reports the drops; the caller (config-time wiring) emits the warning.
|
|
22
|
+
*/ export function filterPolicyToKnownLocales(policy, knownLocales) {
|
|
23
|
+
const droppedTargets = policy.targets.filter((target)=>!knownLocales.has(target));
|
|
24
|
+
const sourceUnknown = policy.sourceLocale !== undefined && !knownLocales.has(policy.sourceLocale);
|
|
25
|
+
return {
|
|
26
|
+
policy: {
|
|
27
|
+
...policy,
|
|
28
|
+
targets: policy.targets.filter((target)=>knownLocales.has(target)),
|
|
29
|
+
sourceLocale: sourceUnknown ? undefined : policy.sourceLocale
|
|
30
|
+
},
|
|
31
|
+
droppedTargets,
|
|
32
|
+
droppedSourceLocale: sourceUnknown ? policy.sourceLocale ?? null : null
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The v1 (collection-level) resolver. Ignores `doc` — see {@link AutoTranslatePolicyResolver}. This is
|
|
37
|
+
* the single seam a future document-level manager replaces.
|
|
38
|
+
*/ export function makeCollectionPolicyResolver(policies) {
|
|
39
|
+
return (collectionSlug, _doc)=>policies.get(collectionSlug) ?? null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Publish-gate (#51 D8): a drafts-enabled collection auto-translates only on a **published** save;
|
|
43
|
+
* autosave/draft saves are ignored. A collection without drafts has no `_status`, so every save
|
|
44
|
+
* qualifies. Applies uniformly to create and update.
|
|
45
|
+
*/ export function passesPublishGate(doc, hasDrafts) {
|
|
46
|
+
if (!hasDrafts) return true;
|
|
47
|
+
return doc._status === "published";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Mirror the source document's status onto the translation (#51 D9). Combined with the publish-gate,
|
|
51
|
+
* the source is published whenever we reach enqueue, so translations publish; a no-drafts collection
|
|
52
|
+
* publishes too. Deliberately identical to {@link passesPublishGate} today — kept as a SEPARATE
|
|
53
|
+
* function (not merged) because it diverges once a future document-level manager (R8) can bypass the
|
|
54
|
+
* gate and translate a still-draft source; do not collapse the two.
|
|
55
|
+
*/ export function resolvePublishOnTranslation(doc, hasDrafts) {
|
|
56
|
+
if (!hasDrafts) return true;
|
|
57
|
+
return doc._status === "published";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build one {@link TaskInput} per configured target locale (the source locale is always excluded).
|
|
61
|
+
* `waitUntil` encodes the debounce; `now` is injected so the timestamp is deterministic in tests.
|
|
62
|
+
*/ export function buildAutoTranslateTasks(args) {
|
|
63
|
+
const { policy, collectionSlug, documentId, sourceLocale, doc, hasDrafts, now } = args;
|
|
64
|
+
const publishOnTranslation = resolvePublishOnTranslation(doc, hasDrafts);
|
|
65
|
+
const waitUntil = policy.debounceMs > 0 ? new Date(now + policy.debounceMs) : undefined;
|
|
66
|
+
return policy.targets.filter((target)=>target !== sourceLocale).map((target)=>({
|
|
67
|
+
collectionSlug,
|
|
68
|
+
collectionId: documentId,
|
|
69
|
+
sourceLng: sourceLocale,
|
|
70
|
+
targetLng: target,
|
|
71
|
+
strategy: policy.strategy,
|
|
72
|
+
publishOnTranslation,
|
|
73
|
+
waitUntil
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=AutoTranslate.policy.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CollectionAfterChangeHook } from "payload";
|
|
2
|
+
/**
|
|
3
|
+
* The minimal slice of a Payload collection the auto-translate wiring mutates: its `slug` and the
|
|
4
|
+
* `afterChange` hook slot. A real `CollectionConfig` is structurally assignable to this, so call sites
|
|
5
|
+
* pass the live collection with no adapter and tests pass a `{ slug: "posts" }` literal. Keeps
|
|
6
|
+
* `injectAutoTranslateHook` off the god-`CollectionConfig` type (own shape — provenance's is not reused,
|
|
7
|
+
* per the module-owns-its-shape convention). The only Payload type imported is the hook callback
|
|
8
|
+
* contract, which legitimately stays framework-typed.
|
|
9
|
+
*/
|
|
10
|
+
export type AutoTranslateManagedEntry = {
|
|
11
|
+
slug: string;
|
|
12
|
+
hooks?: {
|
|
13
|
+
afterChange?: CollectionAfterChangeHook[];
|
|
14
|
+
};
|
|
15
|
+
/** The `custom` bag — the opt-in is propagated here so the admin UI can read it back off the
|
|
16
|
+
* REGISTERED collection (which may be a different object than the one `withAutoTranslate` wrapped). */
|
|
17
|
+
custom?: Record<string, unknown>;
|
|
18
|
+
};
|
|
19
|
+
/** The minimal config host: a mutable `collections` array. A real Payload `Config` plugs straight in. */
|
|
20
|
+
export type AutoTranslateManagedConfig = {
|
|
21
|
+
collections?: AutoTranslateManagedEntry[];
|
|
22
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
|
|
2
|
+
import type { ConfigModifier } from "../../../types/ConfigModifier";
|
|
3
|
+
import type { TaskRunnerFactory } from "../task-runner";
|
|
4
|
+
/** A collection as the plugin receives it — only `slug` + `custom` are read to resolve the opt-in. */
|
|
5
|
+
type ConfigurableCollection = {
|
|
6
|
+
slug: string;
|
|
7
|
+
custom?: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
/** Everything the auto-translate module contributes at config time (mirrors `ProvenanceModule`). */
|
|
10
|
+
export type AutoTranslateModule = {
|
|
11
|
+
configure(managedSlugs: Set<string>): ConfigModifier;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Turn the opt-in `withAutoTranslate` config (read from each collection's `custom`) into a
|
|
15
|
+
* self-contained {@link AutoTranslateModule} — mirrors `configureProvenance`. Builds the per-collection
|
|
16
|
+
* policy map + resolver once, then returns a `configure(managedSlugs) → ConfigModifier` that injects a
|
|
17
|
+
* single best-effort `afterChange` hook onto every enabled + managed collection. When no collection
|
|
18
|
+
* opted in, `configure` is a no-op (no hook, no behaviour change).
|
|
19
|
+
*/
|
|
20
|
+
export declare function configureAutoTranslate(collections: ConfigurableCollection[], schemaMap: CollectionSchemaMap, taskRunnerFactory: TaskRunnerFactory): AutoTranslateModule;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { getAutoTranslateConfig } from "../../../core/auto-translate-config";
|
|
2
|
+
import { extractLocaleCodes, filterPolicyToKnownLocales, makeCollectionPolicyResolver, normalizeAutoTranslateConfig } from "./AutoTranslate.policy";
|
|
3
|
+
import { injectAutoTranslateHook, makeAutoTranslateHook, propagateAutoTranslateCustom } from "./AutoTranslateEnqueue.hook";
|
|
4
|
+
const NOOP = (config)=>config;
|
|
5
|
+
/** Emit one clear config-time warning per collection whose auto-translate config named unknown locales. */ function warnDroppedLocales(slug, filtered, knownLocales) {
|
|
6
|
+
const known = [
|
|
7
|
+
...knownLocales
|
|
8
|
+
].join(", ");
|
|
9
|
+
if (filtered.droppedTargets.length > 0) {
|
|
10
|
+
console.warn(`[payload-plugin-translator] auto-translate on "${slug}": ignoring unknown target locale(s) ${filtered.droppedTargets.join(", ")} (configured locales: ${known}).`);
|
|
11
|
+
}
|
|
12
|
+
if (filtered.droppedSourceLocale) {
|
|
13
|
+
console.warn(`[payload-plugin-translator] auto-translate on "${slug}": unknown sourceLocale "${filtered.droppedSourceLocale}" ignored, falling back to the default locale (configured locales: ${known}).`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Turn the opt-in `withAutoTranslate` config (read from each collection's `custom`) into a
|
|
18
|
+
* self-contained {@link AutoTranslateModule} — mirrors `configureProvenance`. Builds the per-collection
|
|
19
|
+
* policy map + resolver once, then returns a `configure(managedSlugs) → ConfigModifier` that injects a
|
|
20
|
+
* single best-effort `afterChange` hook onto every enabled + managed collection. When no collection
|
|
21
|
+
* opted in, `configure` is a no-op (no hook, no behaviour change).
|
|
22
|
+
*/ export function configureAutoTranslate(collections, schemaMap, taskRunnerFactory) {
|
|
23
|
+
const policies = new Map();
|
|
24
|
+
for (const collection of collections){
|
|
25
|
+
const config = getAutoTranslateConfig(collection);
|
|
26
|
+
if (config) policies.set(collection.slug, normalizeAutoTranslateConfig(config));
|
|
27
|
+
}
|
|
28
|
+
if (policies.size === 0) return {
|
|
29
|
+
configure: ()=>NOOP
|
|
30
|
+
};
|
|
31
|
+
const enabledSlugs = new Set(policies.keys());
|
|
32
|
+
const resolvePolicy = makeCollectionPolicyResolver(policies);
|
|
33
|
+
const hook = makeAutoTranslateHook({
|
|
34
|
+
resolvePolicy,
|
|
35
|
+
schemaMap,
|
|
36
|
+
taskRunnerFactory
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
configure: (managedSlugs)=>(config)=>{
|
|
40
|
+
// Inject only onto collections that both opted in AND are plugin-managed.
|
|
41
|
+
const slugs = new Set([
|
|
42
|
+
...enabledSlugs
|
|
43
|
+
].filter((slug)=>managedSlugs.has(slug)));
|
|
44
|
+
// Drop targets / source-locale overrides that are not configured locales, fail-fast with a
|
|
45
|
+
// warning at init — else a mistyped locale silently burns provider calls and orphans data at
|
|
46
|
+
// runtime. Filter the shared `policies` map once here (config-time, before any hook fires), so
|
|
47
|
+
// the hook, the propagated `custom`, and the admin indicator all read the corrected policy.
|
|
48
|
+
const knownLocales = extractLocaleCodes(config.localization);
|
|
49
|
+
if (knownLocales) {
|
|
50
|
+
for (const slug of slugs){
|
|
51
|
+
const policy = policies.get(slug);
|
|
52
|
+
if (!policy) continue;
|
|
53
|
+
const filtered = filterPolicyToKnownLocales(policy, knownLocales);
|
|
54
|
+
warnDroppedLocales(slug, filtered, knownLocales);
|
|
55
|
+
policies.set(slug, filtered.policy);
|
|
56
|
+
}
|
|
57
|
+
} else if (slugs.size > 0) {
|
|
58
|
+
console.warn("[payload-plugin-translator] auto-translate is configured but localization is disabled; no translations will be enqueued.");
|
|
59
|
+
}
|
|
60
|
+
injectAutoTranslateHook(config, slugs, hook);
|
|
61
|
+
// Mirror the opt-in onto the registered collection's `custom` so the admin UI can read it back.
|
|
62
|
+
propagateAutoTranslateCustom(config, slugs, policies);
|
|
63
|
+
return config;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
//# sourceMappingURL=AutoTranslate.wiring.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CollectionAfterChangeHook } from "payload";
|
|
2
|
+
import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
|
|
3
|
+
import type { TaskRunnerFactory } from "../task-runner";
|
|
4
|
+
import type { AutoTranslatePolicyResolver, NormalizedAutoTranslatePolicy } from "./AutoTranslate.policy";
|
|
5
|
+
import type { AutoTranslateManagedConfig } from "./AutoTranslate.shapes";
|
|
6
|
+
type AutoTranslateHookDeps = {
|
|
7
|
+
resolvePolicy: AutoTranslatePolicyResolver;
|
|
8
|
+
schemaMap: CollectionSchemaMap;
|
|
9
|
+
taskRunnerFactory: TaskRunnerFactory;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Build the `afterChange` hook that auto-enqueues translations when a document's source-locale content
|
|
13
|
+
* changes. Thin orchestration only — every decision lives in `AutoTranslate.policy.ts` or the core
|
|
14
|
+
* drift predicate. Best-effort by contract: any failure is logged and swallowed, never failing the
|
|
15
|
+
* editor's save.
|
|
16
|
+
*
|
|
17
|
+
* Order (cheap guards first): (1) skip the translator's own writes via the `req.context` flag;
|
|
18
|
+
* (2) resolve the policy — off ⇒ skip; (3) resolve the source locale (per-collection override else
|
|
19
|
+
* `localization.defaultLocale`) — unresolved ⇒ log + skip; (4) skip non-source-locale writes (the
|
|
20
|
+
* pipeline's target writes never match); (5) publish-gate (D8); (6) drift-gate (D3); then enqueue one
|
|
21
|
+
* job per target locale.
|
|
22
|
+
*/
|
|
23
|
+
export declare function makeAutoTranslateHook(deps: AutoTranslateHookDeps): CollectionAfterChangeHook;
|
|
24
|
+
/**
|
|
25
|
+
* Attach the auto-translate hook to every enabled collection on `config`, appending to any
|
|
26
|
+
* consumer-supplied `afterChange` array. Idempotent: a collection that already carries the marked hook
|
|
27
|
+
* is skipped, so a repeated `init()` never stacks duplicates.
|
|
28
|
+
*/
|
|
29
|
+
export declare function injectAutoTranslateHook(config: AutoTranslateManagedConfig, enabledSlugs: Set<string>, hook: CollectionAfterChangeHook): void;
|
|
30
|
+
/**
|
|
31
|
+
* Propagate each enabled collection's resolved policy onto the REGISTERED collection's `custom` bag, so
|
|
32
|
+
* the admin UI can read the opt-in back via `getAutoTranslateConfig`. This is required because
|
|
33
|
+
* `withAutoTranslate` stamps `custom` on the object passed to the plugin's `collections` param, which
|
|
34
|
+
* can be a DIFFERENT object than the one registered in `buildConfig.collections` (the reader would
|
|
35
|
+
* otherwise see no config and the indicator would disagree with the behaviour). Idempotent + additive:
|
|
36
|
+
* re-stamping the same value is a no-op and the behaviour wiring still reads from the plugin param.
|
|
37
|
+
*/
|
|
38
|
+
export declare function propagateAutoTranslateCustom(config: AutoTranslateManagedConfig, enabledSlugs: Set<string>, policies: Map<string, NormalizedAutoTranslatePolicy>): void;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { hasSourceContentChanged } from "../../../core/auto-translate";
|
|
2
|
+
import { AUTO_TRANSLATE_CUSTOM_KEY } from "../../../core/auto-translate-config";
|
|
3
|
+
import { AUTO_TRANSLATE_SKIP_CONTEXT_KEY } from "../../../types/AutoTranslateContext";
|
|
4
|
+
import { buildAutoTranslateTasks, passesPublishGate } from "./AutoTranslate.policy";
|
|
5
|
+
/**
|
|
6
|
+
* Build the `afterChange` hook that auto-enqueues translations when a document's source-locale content
|
|
7
|
+
* changes. Thin orchestration only — every decision lives in `AutoTranslate.policy.ts` or the core
|
|
8
|
+
* drift predicate. Best-effort by contract: any failure is logged and swallowed, never failing the
|
|
9
|
+
* editor's save.
|
|
10
|
+
*
|
|
11
|
+
* Order (cheap guards first): (1) skip the translator's own writes via the `req.context` flag;
|
|
12
|
+
* (2) resolve the policy — off ⇒ skip; (3) resolve the source locale (per-collection override else
|
|
13
|
+
* `localization.defaultLocale`) — unresolved ⇒ log + skip; (4) skip non-source-locale writes (the
|
|
14
|
+
* pipeline's target writes never match); (5) publish-gate (D8); (6) drift-gate (D3); then enqueue one
|
|
15
|
+
* job per target locale.
|
|
16
|
+
*/ export function makeAutoTranslateHook(deps) {
|
|
17
|
+
const { resolvePolicy, schemaMap, taskRunnerFactory } = deps;
|
|
18
|
+
const hook = async ({ doc, previousDoc, req, collection })=>{
|
|
19
|
+
try {
|
|
20
|
+
if (req.context?.[AUTO_TRANSLATE_SKIP_CONTEXT_KEY]) return doc;
|
|
21
|
+
const policy = resolvePolicy(collection.slug, doc);
|
|
22
|
+
if (!policy) return doc;
|
|
23
|
+
const localization = req.payload.config.localization;
|
|
24
|
+
const sourceLocale = policy.sourceLocale ?? (localization ? localization.defaultLocale : undefined);
|
|
25
|
+
if (!sourceLocale) {
|
|
26
|
+
req.payload.logger.warn({
|
|
27
|
+
collection: collection.slug,
|
|
28
|
+
documentId: String(doc.id),
|
|
29
|
+
msg: "translator: auto-translate skipped — no source locale resolvable (set localization.defaultLocale or a per-collection sourceLocale)"
|
|
30
|
+
});
|
|
31
|
+
return doc;
|
|
32
|
+
}
|
|
33
|
+
if (req.locale !== sourceLocale) return doc;
|
|
34
|
+
const hasDrafts = Boolean(collection.versions && collection.versions.drafts);
|
|
35
|
+
if (!passesPublishGate(doc, hasDrafts)) return doc;
|
|
36
|
+
const schema = schemaMap.get(collection.slug);
|
|
37
|
+
if (schema && !hasSourceContentChanged(previousDoc, doc, schema)) return doc;
|
|
38
|
+
const tasks = buildAutoTranslateTasks({
|
|
39
|
+
policy,
|
|
40
|
+
collectionSlug: collection.slug,
|
|
41
|
+
documentId: String(doc.id),
|
|
42
|
+
sourceLocale,
|
|
43
|
+
doc,
|
|
44
|
+
hasDrafts,
|
|
45
|
+
now: Date.now()
|
|
46
|
+
});
|
|
47
|
+
if (tasks.length === 0) return doc;
|
|
48
|
+
await taskRunnerFactory.create(req.payload).enqueue(tasks);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
req.payload.logger.error({
|
|
51
|
+
err: error,
|
|
52
|
+
collection: collection.slug,
|
|
53
|
+
documentId: String(doc.id),
|
|
54
|
+
msg: "translator: auto-translate hook failed"
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return doc;
|
|
58
|
+
};
|
|
59
|
+
hook.__translatorAutoTranslate = true;
|
|
60
|
+
return hook;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Attach the auto-translate hook to every enabled collection on `config`, appending to any
|
|
64
|
+
* consumer-supplied `afterChange` array. Idempotent: a collection that already carries the marked hook
|
|
65
|
+
* is skipped, so a repeated `init()` never stacks duplicates.
|
|
66
|
+
*/ export function injectAutoTranslateHook(config, enabledSlugs, hook) {
|
|
67
|
+
for (const collection of config.collections ?? []){
|
|
68
|
+
if (!enabledSlugs.has(collection.slug)) continue;
|
|
69
|
+
collection.hooks ??= {};
|
|
70
|
+
collection.hooks.afterChange ??= [];
|
|
71
|
+
const alreadyInjected = collection.hooks.afterChange.some((existing)=>existing.__translatorAutoTranslate === true);
|
|
72
|
+
if (!alreadyInjected) collection.hooks.afterChange.push(hook);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Propagate each enabled collection's resolved policy onto the REGISTERED collection's `custom` bag, so
|
|
77
|
+
* the admin UI can read the opt-in back via `getAutoTranslateConfig`. This is required because
|
|
78
|
+
* `withAutoTranslate` stamps `custom` on the object passed to the plugin's `collections` param, which
|
|
79
|
+
* can be a DIFFERENT object than the one registered in `buildConfig.collections` (the reader would
|
|
80
|
+
* otherwise see no config and the indicator would disagree with the behaviour). Idempotent + additive:
|
|
81
|
+
* re-stamping the same value is a no-op and the behaviour wiring still reads from the plugin param.
|
|
82
|
+
*/ export function propagateAutoTranslateCustom(config, enabledSlugs, policies) {
|
|
83
|
+
for (const collection of config.collections ?? []){
|
|
84
|
+
if (!enabledSlugs.has(collection.slug)) continue;
|
|
85
|
+
const policy = policies.get(collection.slug);
|
|
86
|
+
if (!policy) continue;
|
|
87
|
+
collection.custom = {
|
|
88
|
+
...collection.custom ?? {},
|
|
89
|
+
[AUTO_TRANSLATE_CUSTOM_KEY]: policy
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
//# sourceMappingURL=AutoTranslateEnqueue.hook.js.map
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { configureAutoTranslate } from "./AutoTranslate.wiring";
|
|
2
|
+
export type { AutoTranslateModule } from "./AutoTranslate.wiring";
|
|
3
|
+
export { makeAutoTranslateHook, injectAutoTranslateHook } from "./AutoTranslateEnqueue.hook";
|
|
4
|
+
export type { AutoTranslatePolicyResolver, NormalizedAutoTranslatePolicy, } from "./AutoTranslate.policy";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Auto-translate adapter (Payload-backed). The payload-free drift predicate + config reader live in
|
|
2
|
+
// the core (src/core/auto-translate*, src/core/content-projection); this module is the config-time
|
|
3
|
+
// wiring + the afterChange hook that enqueues translations on a source-locale change (#51).
|
|
4
|
+
export { configureAutoTranslate } from "./AutoTranslate.wiring";
|
|
5
|
+
export { makeAutoTranslateHook, injectAutoTranslateHook } from "./AutoTranslateEnqueue.hook";
|
|
6
|
+
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -31,6 +31,10 @@ const documentLocaleKey = (collectionId, targetLng)=>`${String(collectionId)}:${
|
|
|
31
31
|
await Promise.all(tasks.map((task)=>this.payload.jobs.queue({
|
|
32
32
|
task: this.config.taskName,
|
|
33
33
|
queue: this.config.queueName,
|
|
34
|
+
// Debounce: when set, Payload holds the job until this instant. A superseding enqueue for
|
|
35
|
+
// the same (document, targetLng) cancels the pending delayed job first (see enqueue above),
|
|
36
|
+
// so rapid source edits coalesce to the final one. Undefined for the manual path.
|
|
37
|
+
waitUntil: task.waitUntil,
|
|
34
38
|
input: {
|
|
35
39
|
// Flat text reference (ID-agnostic). Stored as a string — no
|
|
36
40
|
// relationship type validation against the collection's ID type,
|
|
@@ -23,6 +23,13 @@ export type TaskInput = {
|
|
|
23
23
|
targetLng: string;
|
|
24
24
|
strategy: "overwrite" | "skip_existing";
|
|
25
25
|
publishOnTranslation: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Optional scheduled-run time (debounce). When set, the job runs no earlier than this instant;
|
|
28
|
+
* omitted/`undefined` = run as soon as the runner picks it up (every existing caller). Honored by
|
|
29
|
+
* the Payload Jobs runner via `payload.jobs.queue({ waitUntil })`; the sync (dev) runner ignores it
|
|
30
|
+
* and runs immediately.
|
|
31
|
+
*/
|
|
32
|
+
waitUntil?: Date;
|
|
26
33
|
};
|
|
27
34
|
/**
|
|
28
35
|
* Normalized task representation
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `req.context` flag the translator sets on its OWN document writes so the auto-translate
|
|
3
|
+
* `afterChange` hook skips them — the loop guard's second barrier (#51 D5). A single exported constant
|
|
4
|
+
* shared by the setter (`TranslateDocumentHandler.saveTranslatedDocument`) and the reader (the
|
|
5
|
+
* auto-translate hook), so the set-side and honor-side keys can never diverge.
|
|
6
|
+
*
|
|
7
|
+
* Lives in `types/` (a leaf contract) so both `server/features/translate-document` and
|
|
8
|
+
* `server/modules/auto-translate` import it without creating a cross-module edge.
|
|
9
|
+
*/
|
|
10
|
+
export declare const AUTO_TRANSLATE_SKIP_CONTEXT_KEY = "translatorSkipAutoTranslate";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `req.context` flag the translator sets on its OWN document writes so the auto-translate
|
|
3
|
+
* `afterChange` hook skips them — the loop guard's second barrier (#51 D5). A single exported constant
|
|
4
|
+
* shared by the setter (`TranslateDocumentHandler.saveTranslatedDocument`) and the reader (the
|
|
5
|
+
* auto-translate hook), so the set-side and honor-side keys can never diverge.
|
|
6
|
+
*
|
|
7
|
+
* Lives in `types/` (a leaf contract) so both `server/features/translate-document` and
|
|
8
|
+
* `server/modules/auto-translate` import it without creating a cross-module edge.
|
|
9
|
+
*/ export const AUTO_TRANSLATE_SKIP_CONTEXT_KEY = "translatorSkipAutoTranslate";
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=AutoTranslateContext.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@focus-reactive/payload-plugin-translator",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Translation plugin for Payload CMS 3.x. Automatically translate your localized content using any translation provider.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|