@focus-reactive/payload-plugin-translator 0.8.1 → 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/api/queries/useDocumentTranslation.d.ts +15 -15
- package/dist/client/entities/translation/api/queries/useDocumentTranslation.js +27 -11
- package/dist/client/entities/translation/index.d.ts +2 -1
- package/dist/client/entities/translation/index.js +2 -1
- 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/model/panelStatus.d.ts +8 -2
- package/dist/client/entities/translation/model/panelStatus.js +16 -0
- package/dist/client/entities/translation/model/statusRows.d.ts +9 -6
- package/dist/client/entities/translation/model/statusRows.js +18 -16
- package/dist/client/entities/translation/model/types.d.ts +6 -1
- 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 +17 -8
- 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/get-document-status/handler.js +4 -2
- package/dist/server/features/get-document-status/model.d.ts +10 -0
- package/dist/server/features/get-document-status/model.js +21 -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 +15 -2
- package/dist/server/modules/task-runner/sync-runner/SyncTaskRunner.js +5 -3
- 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
|
@@ -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, 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();
|
|
@@ -27,15 +27,15 @@ const TranslateDocument = ({ hasDrafts })=>{
|
|
|
27
27
|
]);
|
|
28
28
|
// The panel trigger shows one aggregate marker; the popup shows the full per-locale list.
|
|
29
29
|
const panelStatus = useMemo(()=>derivePanelStatus({
|
|
30
|
-
runStatus: data
|
|
30
|
+
runStatus: deriveDocumentRunStatus(data),
|
|
31
31
|
staleLocales
|
|
32
32
|
}), [
|
|
33
|
-
data
|
|
33
|
+
data,
|
|
34
34
|
staleLocales
|
|
35
35
|
]);
|
|
36
36
|
const statusRows = useMemo(()=>buildTranslationStatusRows({
|
|
37
37
|
staleness,
|
|
38
|
-
|
|
38
|
+
runs: data
|
|
39
39
|
}), [
|
|
40
40
|
staleness,
|
|
41
41
|
data
|
|
@@ -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,6 @@
|
|
|
1
1
|
import { ServerResponse } from "../../shared";
|
|
2
2
|
import { isCollectionAvailable } from "../_lib/collection-utils";
|
|
3
|
-
import { GetDocumentStatusInputSchema, taskToJobStatusOutput } from "./model";
|
|
3
|
+
import { GetDocumentStatusInputSchema, latestTaskPerTargetLocale, taskToJobStatusOutput } from "./model";
|
|
4
4
|
/**
|
|
5
5
|
* Gets the translation status for a specific document
|
|
6
6
|
*/ export class GetDocumentStatusHandler {
|
|
@@ -20,7 +20,9 @@ import { GetDocumentStatusInputSchema, taskToJobStatusOutput } from "./model";
|
|
|
20
20
|
const tasks = await runner.findByCollection(collectionSlug, [
|
|
21
21
|
collection_id
|
|
22
22
|
]);
|
|
23
|
-
|
|
23
|
+
// One job per target locale (latest), not a single job for the whole document — re-translate
|
|
24
|
+
// queues an independent job per locale, and the status panel renders a row per locale.
|
|
25
|
+
return ServerResponse.success(latestTaskPerTargetLocale(tasks).map(taskToJobStatusOutput));
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -56,6 +56,16 @@ export type JobStatusOutput = {
|
|
|
56
56
|
export type GetDocumentStatusConfig = {
|
|
57
57
|
availableCollections: Set<CollectionSlug>;
|
|
58
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* Reduce a document's jobs to the latest one per target locale.
|
|
61
|
+
*
|
|
62
|
+
* `findByCollection` returns every job for the document across all target locales (plus any superseded
|
|
63
|
+
* jobs not yet cancelled). The status panel shows one row per target locale, so we keep — per
|
|
64
|
+
* `targetLng` — the most recently created job (tie-break: most recently updated). Without this the
|
|
65
|
+
* caller sees a single arbitrary job and every other in-flight locale looks idle, which is the
|
|
66
|
+
* concurrent re-translate bug (a second re-translate appearing to overwrite the first's status).
|
|
67
|
+
*/
|
|
68
|
+
export declare function latestTaskPerTargetLocale(tasks: Task[]): Task[];
|
|
59
69
|
/**
|
|
60
70
|
* Transforms a Task to API output format (snake_case for client compatibility)
|
|
61
71
|
*/
|
|
@@ -10,6 +10,27 @@ import { JobIdSchema, toClientErrorMessage } from "../../shared";
|
|
|
10
10
|
collection_id: JobIdSchema,
|
|
11
11
|
collection_slug: z.string().nonempty()
|
|
12
12
|
});
|
|
13
|
+
// A job is "newer" by creation time, tie-broken by last update — both ISO-8601, so a lexicographic
|
|
14
|
+
// string compare is a correct chronological compare (no Date parsing needed).
|
|
15
|
+
const isNewerTask = (candidate, current)=>candidate.createdAt !== current.createdAt ? candidate.createdAt > current.createdAt : candidate.updatedAt > current.updatedAt;
|
|
16
|
+
/**
|
|
17
|
+
* Reduce a document's jobs to the latest one per target locale.
|
|
18
|
+
*
|
|
19
|
+
* `findByCollection` returns every job for the document across all target locales (plus any superseded
|
|
20
|
+
* jobs not yet cancelled). The status panel shows one row per target locale, so we keep — per
|
|
21
|
+
* `targetLng` — the most recently created job (tie-break: most recently updated). Without this the
|
|
22
|
+
* caller sees a single arbitrary job and every other in-flight locale looks idle, which is the
|
|
23
|
+
* concurrent re-translate bug (a second re-translate appearing to overwrite the first's status).
|
|
24
|
+
*/ export function latestTaskPerTargetLocale(tasks) {
|
|
25
|
+
const byLocale = new Map();
|
|
26
|
+
for (const task of tasks){
|
|
27
|
+
const current = byLocale.get(task.input.targetLng);
|
|
28
|
+
if (!current || isNewerTask(task, current)) byLocale.set(task.input.targetLng, task);
|
|
29
|
+
}
|
|
30
|
+
return [
|
|
31
|
+
...byLocale.values()
|
|
32
|
+
];
|
|
33
|
+
}
|
|
13
34
|
/**
|
|
14
35
|
* Transforms a Task to API output format (snake_case for client compatibility)
|
|
15
36
|
*/ export function taskToJobStatusOutput(task) {
|
|
@@ -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[];
|