@focus-reactive/payload-plugin-translator 0.9.1 → 0.10.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 +25 -0
- package/dist/client/entities/translation/api/mutations/useQueueDocumentTranslation.d.ts +1 -1
- package/dist/client/entities/translation/ui/AutoTranslateMarker/AutoTranslateMarker.d.ts +3 -4
- package/dist/client/entities/translation/ui/AutoTranslateMarker/AutoTranslateMarker.js +8 -12
- package/dist/client/entities/translation/ui/AutoTranslateMarker/styles.module.scss +3 -12
- package/dist/client/features/collection-translation-form/model/schema.d.ts +3 -3
- package/dist/client/features/collection-translation-form/model/schema.js +8 -1
- package/dist/client/features/collection-translation-form/ui/CollectionTranslationForm.d.ts +3 -1
- package/dist/client/features/collection-translation-form/ui/CollectionTranslationForm.js +36 -6
- package/dist/client/features/translate-document-form/model/schema.d.ts +3 -3
- package/dist/client/features/translate-document-form/model/schema.js +8 -1
- package/dist/client/features/translate-document-form/ui/DocumentTranslationForm.d.ts +3 -1
- package/dist/client/features/translate-document-form/ui/DocumentTranslationForm.js +36 -6
- package/dist/client/shared/lib/forms/pruneSourceFromTarget.d.ts +7 -0
- package/dist/client/shared/lib/forms/pruneSourceFromTarget.js +14 -0
- package/dist/client/shared/ui/InfoPopover/InfoPopover.d.ts +21 -0
- package/dist/client/shared/ui/InfoPopover/InfoPopover.js +45 -0
- package/dist/client/shared/ui/InfoPopover/index.d.ts +1 -0
- package/dist/client/shared/ui/InfoPopover/index.js +3 -0
- package/dist/client/shared/ui/InfoPopover/styles.module.scss +49 -0
- package/dist/client/shared/ui/MultiSelect/MultiSelect.d.ts +40 -0
- package/dist/client/shared/ui/MultiSelect/MultiSelect.js +129 -0
- package/dist/client/shared/ui/MultiSelect/index.d.ts +2 -0
- package/dist/client/shared/ui/MultiSelect/index.js +3 -0
- package/dist/client/shared/ui/MultiSelect/multiSelectModel.d.ts +25 -0
- package/dist/client/shared/ui/MultiSelect/multiSelectModel.js +41 -0
- package/dist/client/shared/ui/MultiSelect/styles.module.scss +126 -0
- package/dist/client/shared/ui/form/FormCheckboxPublish/FormCheckboxPublish.js +4 -7
- package/dist/client/shared/ui/form/FormMultiSelect/FormMultiSelect.d.ts +22 -0
- package/dist/client/shared/ui/form/FormMultiSelect/FormMultiSelect.js +70 -0
- package/dist/client/shared/ui/form/FormMultiSelect/index.d.ts +1 -0
- package/dist/client/shared/ui/form/FormMultiSelect/index.js +3 -0
- package/dist/client/shared/ui/form/FormSelectStrategy/FormSelectStrategy.js +4 -7
- package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.d.ts +3 -1
- package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.export.d.ts +3 -1
- package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.export.js +5 -2
- package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.js +8 -3
- package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.server.d.ts +2 -0
- package/dist/client/widgets/bulk-translation-dashboard/ui/BulkTranslationDashboard.server.js +2 -1
- package/dist/client/widgets/translate-document/ui/TranslateDocument.d.ts +3 -1
- package/dist/client/widgets/translate-document/ui/TranslateDocument.export.d.ts +3 -1
- package/dist/client/widgets/translate-document/ui/TranslateDocument.export.js +5 -2
- package/dist/client/widgets/translate-document/ui/TranslateDocument.js +7 -3
- package/dist/client/widgets/translate-document/ui/TranslateDocument.server.d.ts +2 -0
- package/dist/client/widgets/translate-document/ui/TranslateDocument.server.js +2 -1
- package/dist/composition/levels/collectionLevel.js +1 -1
- package/dist/composition/levels/documentLevel.js +1 -1
- package/dist/core/kernel/field-traversal/index.d.ts +1 -0
- package/dist/core/kernel/field-traversal/index.js +1 -0
- package/dist/core/kernel/field-traversal/projectFieldLike.d.ts +32 -0
- package/dist/core/kernel/field-traversal/projectFieldLike.js +60 -0
- package/dist/index.d.ts +1 -0
- package/dist/plugin.d.ts +10 -0
- package/dist/plugin.js +11 -11
- package/dist/server/features/enqueue-translation/handler.js +28 -8
- package/dist/server/features/enqueue-translation/model.d.ts +3 -3
- package/dist/server/features/enqueue-translation/model.js +6 -1
- package/dist/server/features/enqueue-translation/resolveTargetLocales.d.ts +29 -0
- package/dist/server/features/enqueue-translation/resolveTargetLocales.js +41 -0
- package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +3 -3
- package/dist/server/modules/auto-translate/index.d.ts +2 -0
- package/dist/server/modules/auto-translate/index.js +2 -0
- package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +2 -0
- package/dist/server/modules/translation-levels/PluginConfigBuilder.js +2 -0
- package/dist/server/modules/translation-levels/types.d.ts +4 -0
- package/dist/types/CollectionSchemaMap.d.ts +7 -4
- package/dist/types/CollectionSchemaMap.js +4 -2
- package/dist/types/TargetSelection.d.ts +10 -0
- package/dist/types/TargetSelection.js +11 -0
- package/package.json +1 -1
- package/dist/client/entities/translation/api/mutations/useQueueDocumentTranslations.d.ts +0 -8
- package/dist/client/entities/translation/api/mutations/useQueueDocumentTranslations.js +0 -20
- package/dist/client/shared/ui/form/FormSelectLocale/FormSelectLocale.d.ts +0 -11
- package/dist/client/shared/ui/form/FormSelectLocale/FormSelectLocale.js +0 -19
- package/dist/client/shared/ui/form/FormSelectLocale/index.d.ts +0 -1
- package/dist/client/shared/ui/form/FormSelectLocale/index.js +0 -3
|
@@ -9,7 +9,7 @@ import { DocumentTranslationFormModel } from "../../../features/translate-docume
|
|
|
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, autoTranslate })=>{
|
|
12
|
+
const TranslateDocument = ({ hasDrafts, autoTranslate, targetSelection })=>{
|
|
13
13
|
const locale = useLocale();
|
|
14
14
|
const params = useCollectionDocumentUrlParams();
|
|
15
15
|
const queueTranslationApi = TranslationsApi.useQueueDocumentTranslation();
|
|
@@ -42,12 +42,15 @@ const TranslateDocument = ({ hasDrafts, autoTranslate })=>{
|
|
|
42
42
|
]);
|
|
43
43
|
const initialValues = useMemo(()=>({
|
|
44
44
|
[FORM_FIELDS.SOURCE_LNG]: locale.code,
|
|
45
|
+
// Multi mode binds an array; single mode a string. Seed the matching empty value.
|
|
46
|
+
[FORM_FIELDS.TARGET_LNG]: targetSelection === "multi" ? [] : "",
|
|
45
47
|
[FORM_FIELDS.HIDDEN_COLLECTION_SLUG]: params.collection,
|
|
46
48
|
[FORM_FIELDS.HIDDEN_COLLECTION_ID]: params.id
|
|
47
49
|
}), [
|
|
48
50
|
locale.code,
|
|
49
51
|
params.collection,
|
|
50
|
-
params.id
|
|
52
|
+
params.id,
|
|
53
|
+
targetSelection
|
|
51
54
|
]);
|
|
52
55
|
const { form } = DocumentTranslationFormModel.useForm({
|
|
53
56
|
initialValues
|
|
@@ -95,7 +98,8 @@ const TranslateDocument = ({ hasDrafts, autoTranslate })=>{
|
|
|
95
98
|
/*#__PURE__*/ _jsx(DocumentTranslationForm, {
|
|
96
99
|
form: form,
|
|
97
100
|
onSubmit: (formData)=>handleSubmit(formData, close),
|
|
98
|
-
hasDrafts: hasDrafts
|
|
101
|
+
hasDrafts: hasDrafts,
|
|
102
|
+
targetSelection: targetSelection
|
|
99
103
|
})
|
|
100
104
|
]
|
|
101
105
|
}),
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { BeforeDocumentControlsServerProps, CollectionConfig } from "payload";
|
|
2
2
|
import type { AccessGuard } from "../../../../types/AccessGuard";
|
|
3
|
+
import type { TargetSelectionMode } from "../../../../types/TargetSelection";
|
|
3
4
|
type TranslateDocumentServerProps = BeforeDocumentControlsServerProps & {
|
|
4
5
|
collection: CollectionConfig;
|
|
5
6
|
access: AccessGuard;
|
|
7
|
+
targetSelection: TargetSelectionMode;
|
|
6
8
|
};
|
|
7
9
|
declare function TranslateDocumentServer(props: TranslateDocumentServerProps): Promise<import("react/jsx-runtime").JSX.Element | null>;
|
|
8
10
|
export default TranslateDocumentServer;
|
|
@@ -18,7 +18,8 @@ async function TranslateDocumentServer(props) {
|
|
|
18
18
|
const autoTranslate = resolveAutoTranslateSummary(props.collection, props.payload.config.localization ? props.payload.config.localization.defaultLocale : undefined);
|
|
19
19
|
return /*#__PURE__*/ _jsx(TranslateDocument, {
|
|
20
20
|
hasDrafts: hasDrafts,
|
|
21
|
-
autoTranslate: autoTranslate
|
|
21
|
+
autoTranslate: autoTranslate,
|
|
22
|
+
targetSelection: props.targetSelection
|
|
22
23
|
});
|
|
23
24
|
}
|
|
24
25
|
export default TranslateDocumentServer;
|
|
@@ -16,7 +16,7 @@ import { useDocTranslationApi } from "./useDocTranslationApi";
|
|
|
16
16
|
return {
|
|
17
17
|
extend (ctx) {
|
|
18
18
|
useDocTranslationApi(ctx);
|
|
19
|
-
ctx.addCollectionComponent("beforeListTable", ()=>new BulkDocumentTranslationDashboard(ctx.access));
|
|
19
|
+
ctx.addCollectionComponent("beforeListTable", ()=>new BulkDocumentTranslationDashboard(ctx.access, ctx.targetSelection));
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
}
|
|
@@ -20,7 +20,7 @@ import { useDocTranslationApi } from "./useDocTranslationApi";
|
|
|
20
20
|
return {
|
|
21
21
|
extend (ctx) {
|
|
22
22
|
useDocTranslationApi(ctx);
|
|
23
|
-
ctx.addCollectionComponent("beforeDocumentControls", (collection)=>new TranslateDocumentExport(collection, ctx.access));
|
|
23
|
+
ctx.addCollectionComponent("beforeDocumentControls", (collection)=>new TranslateDocumentExport(collection, ctx.access, ctx.targetSelection));
|
|
24
24
|
}
|
|
25
25
|
};
|
|
26
26
|
}
|
|
@@ -2,6 +2,7 @@ export { findFieldByPath } from "./findFieldByPath";
|
|
|
2
2
|
export type { FieldPathResult } from "./findFieldByPath";
|
|
3
3
|
export { hasFields, isBlockItem, isTabsField } from "./guards";
|
|
4
4
|
export { classifyField, matchElementById, resolveBlockFields, tabScopes } from "./kernel";
|
|
5
|
+
export { projectFieldsToFieldLike } from "./projectFieldLike";
|
|
5
6
|
export { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName, } from "./predicates";
|
|
6
7
|
export type { ArrayFieldLike, BlockLike, BlocksFieldLike, ChildCursor, ChildOutput, ContainerInfo, FieldLike, FieldStructure, FieldWalker, GroupFieldLike, LeafField, LeafFieldLike, TabLike, TabScope, TabsFieldLike, WalkSignal, } from "./types";
|
|
7
8
|
export { walkFields } from "./walkFields";
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { findFieldByPath } from "./findFieldByPath";
|
|
2
2
|
export { hasFields, isBlockItem, isTabsField } from "./guards";
|
|
3
3
|
export { classifyField, matchElementById, resolveBlockFields, tabScopes } from "./kernel";
|
|
4
|
+
export { projectFieldsToFieldLike } from "./projectFieldLike";
|
|
4
5
|
export { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName } from "./predicates";
|
|
5
6
|
export { walkFields } from "./walkFields";
|
|
6
7
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { FieldLike } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Deep-project a field schema into an independent {@link FieldLike} tree, copying ONLY the properties
|
|
4
|
+
* this layer reads. Replaces the `JSON.parse(JSON.stringify(fields))` clone the plugin used to snapshot
|
|
5
|
+
* a collection's schema before Payload's sanitizer mutates the originals.
|
|
6
|
+
*
|
|
7
|
+
* Why a bespoke projection rather than a structural clone:
|
|
8
|
+
* - **Independence is mandatory, not cosmetic.** Payload's `sanitizeFields` does `delete field.localized`
|
|
9
|
+
* **in place** on any field nested under a localized ancestor (group/array/named tab). The pipeline
|
|
10
|
+
* reads per-field `localized`, so a shared reference would silently lose it and the field would stop
|
|
11
|
+
* being translated. Every field/container here is a NEW object, and `localized` is copied by value at
|
|
12
|
+
* projection time (before sanitize runs), so the snapshot is preserved.
|
|
13
|
+
* - **`structuredClone` can't be used.** Field definitions embed Lexical editor configs containing async
|
|
14
|
+
* functions, which `structuredClone` throws on. The old JSON round-trip "worked" only by silently
|
|
15
|
+
* dropping every function; this projection drops them explicitly by copying a typed whitelist.
|
|
16
|
+
*
|
|
17
|
+
* The contract is exactly what the traversal/pipeline consumes: `type` (classify), `name`/`localized`
|
|
18
|
+
* (leaf data + translation gate), `custom` (only `custom.translateKit.exclude` is read), and the
|
|
19
|
+
* container members `fields` (group/array/row/collapsible/unnamed-group), `blocks` (with the
|
|
20
|
+
* load-bearing `block.slug` used for block-type dispatch), and `tabs` (named + unnamed). Every field is
|
|
21
|
+
* kept 1:1 (including presentational `ui`/`row`/`collapsible`) so downstream classification is unchanged;
|
|
22
|
+
* only the property set is narrowed.
|
|
23
|
+
*
|
|
24
|
+
* `custom` is copied by reference: it is a passthrough bag Payload does not mutate during sanitize (only
|
|
25
|
+
* `localized` is deleted, which we snapshot by value), and deep-copying it would re-introduce the
|
|
26
|
+
* `structuredClone`-on-functions problem this projection exists to avoid.
|
|
27
|
+
*
|
|
28
|
+
* @param fields - The source field list (Payload's `Field[]` is structurally assignable to `FieldLike[]`).
|
|
29
|
+
* @returns A new, independent `FieldLike[]` unaffected by later mutation of the source objects.
|
|
30
|
+
* @since 0.9.1
|
|
31
|
+
*/
|
|
32
|
+
export declare function projectFieldsToFieldLike(fields: FieldLike[]): FieldLike[];
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
function projectField(field) {
|
|
2
|
+
const out = {
|
|
3
|
+
type: field.type
|
|
4
|
+
};
|
|
5
|
+
if (field.name !== undefined) out.name = field.name;
|
|
6
|
+
if (field.localized !== undefined) out.localized = field.localized;
|
|
7
|
+
if (field.custom !== undefined) out.custom = field.custom;
|
|
8
|
+
if (field.fields) out.fields = field.fields.map(projectField);
|
|
9
|
+
if (field.blocks) {
|
|
10
|
+
out.blocks = field.blocks.map((block)=>({
|
|
11
|
+
slug: block.slug,
|
|
12
|
+
fields: block.fields.map(projectField)
|
|
13
|
+
}));
|
|
14
|
+
}
|
|
15
|
+
if (field.tabs) {
|
|
16
|
+
out.tabs = field.tabs.map((tab)=>{
|
|
17
|
+
const projected = {
|
|
18
|
+
fields: tab.fields.map(projectField)
|
|
19
|
+
};
|
|
20
|
+
if (tab.name !== undefined) projected.name = tab.name;
|
|
21
|
+
if (tab.localized !== undefined) projected.localized = tab.localized;
|
|
22
|
+
return projected;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Deep-project a field schema into an independent {@link FieldLike} tree, copying ONLY the properties
|
|
29
|
+
* this layer reads. Replaces the `JSON.parse(JSON.stringify(fields))` clone the plugin used to snapshot
|
|
30
|
+
* a collection's schema before Payload's sanitizer mutates the originals.
|
|
31
|
+
*
|
|
32
|
+
* Why a bespoke projection rather than a structural clone:
|
|
33
|
+
* - **Independence is mandatory, not cosmetic.** Payload's `sanitizeFields` does `delete field.localized`
|
|
34
|
+
* **in place** on any field nested under a localized ancestor (group/array/named tab). The pipeline
|
|
35
|
+
* reads per-field `localized`, so a shared reference would silently lose it and the field would stop
|
|
36
|
+
* being translated. Every field/container here is a NEW object, and `localized` is copied by value at
|
|
37
|
+
* projection time (before sanitize runs), so the snapshot is preserved.
|
|
38
|
+
* - **`structuredClone` can't be used.** Field definitions embed Lexical editor configs containing async
|
|
39
|
+
* functions, which `structuredClone` throws on. The old JSON round-trip "worked" only by silently
|
|
40
|
+
* dropping every function; this projection drops them explicitly by copying a typed whitelist.
|
|
41
|
+
*
|
|
42
|
+
* The contract is exactly what the traversal/pipeline consumes: `type` (classify), `name`/`localized`
|
|
43
|
+
* (leaf data + translation gate), `custom` (only `custom.translateKit.exclude` is read), and the
|
|
44
|
+
* container members `fields` (group/array/row/collapsible/unnamed-group), `blocks` (with the
|
|
45
|
+
* load-bearing `block.slug` used for block-type dispatch), and `tabs` (named + unnamed). Every field is
|
|
46
|
+
* kept 1:1 (including presentational `ui`/`row`/`collapsible`) so downstream classification is unchanged;
|
|
47
|
+
* only the property set is narrowed.
|
|
48
|
+
*
|
|
49
|
+
* `custom` is copied by reference: it is a passthrough bag Payload does not mutate during sanitize (only
|
|
50
|
+
* `localized` is deleted, which we snapshot by value), and deep-copying it would re-introduce the
|
|
51
|
+
* `structuredClone`-on-functions problem this projection exists to avoid.
|
|
52
|
+
*
|
|
53
|
+
* @param fields - The source field list (Payload's `Field[]` is structurally assignable to `FieldLike[]`).
|
|
54
|
+
* @returns A new, independent `FieldLike[]` unaffected by later mutation of the source objects.
|
|
55
|
+
* @since 0.9.1
|
|
56
|
+
*/ export function projectFieldsToFieldLike(fields) {
|
|
57
|
+
return fields.map(projectField);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//# sourceMappingURL=projectFieldLike.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { translatorPlugin } from "./plugin";
|
|
2
2
|
export type { TranslatorPluginConfig } from "./plugin";
|
|
3
|
+
export type { TargetSelectionMode } from "./types/TargetSelection";
|
|
3
4
|
export type { TranslationTask, TranslationLifecycleCallbacks } from "./server/modules/lifecycle";
|
|
4
5
|
export type { TranslationProvenanceRecord } from "./core";
|
|
5
6
|
export type { AccessGuard, AccessGuardRequest } from "./types/AccessGuard";
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CollectionConfig, Config } from "payload";
|
|
2
2
|
import type { AccessGuard } from "./types/AccessGuard";
|
|
3
|
+
import type { TargetSelectionMode } from "./types/TargetSelection";
|
|
3
4
|
import type { TranslationProvider } from "./core/domain/translation-providers";
|
|
4
5
|
import type { TaskRunnerProvider } from "./server/modules/task-runner";
|
|
5
6
|
import type { TranslationLifecycleCallbacks } from "./server/modules/lifecycle";
|
|
@@ -69,6 +70,15 @@ export type TranslatorPluginConfig = {
|
|
|
69
70
|
* @since 0.7.0
|
|
70
71
|
*/
|
|
71
72
|
lifecycle?: TranslationLifecycleCallbacks;
|
|
73
|
+
/**
|
|
74
|
+
* How the target-language field behaves in the translation forms. `'single'` (default) keeps
|
|
75
|
+
* today's one-locale-per-run behaviour, byte-identical. `'multi'` renders a compact multi-select so
|
|
76
|
+
* an editor can queue several target locales in one run — the run fans out one translation per
|
|
77
|
+
* `(document × target locale)`. Opt-in and fully backward-compatible; no schema, no migration.
|
|
78
|
+
* @default 'single'
|
|
79
|
+
* @since 0.10.0
|
|
80
|
+
*/
|
|
81
|
+
targetSelection?: TargetSelectionMode;
|
|
72
82
|
};
|
|
73
83
|
/** @deprecated Use `TranslatorPluginConfig` instead */
|
|
74
84
|
export type TranslateCollectionPluginConfig = TranslatorPluginConfig;
|
package/dist/plugin.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { CacheProviderExport } from "./client/app/cache/CacheProvider.export";
|
|
2
2
|
import { configureAutoTranslate } from "./server/modules/auto-translate";
|
|
3
3
|
import { configureProvenance } from "./server/modules/provenance";
|
|
4
|
+
import { projectFieldsToFieldLike } from "./core/kernel/field-traversal";
|
|
4
5
|
import { wireTranslateRunner } from "./server/features/translate-document";
|
|
5
6
|
import { documentLevel, collectionLevel } from "./composition/levels";
|
|
6
7
|
import { PluginConfigBuilder } from "./server/modules/translation-levels/PluginConfigBuilder";
|
|
@@ -12,18 +13,16 @@ import { normalizePath } from "./server/shared";
|
|
|
12
13
|
}
|
|
13
14
|
init() {
|
|
14
15
|
return async (config)=>{
|
|
15
|
-
const { access, translationProvider, runner, collections, levels, provenance, lifecycle, basePath: rawBasePath = "/translate" } = this.pluginConfig;
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// `
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
// pipeline (name, type, localized, fields, blocks, tabs, custom) to make the contract explicit
|
|
23
|
-
// and avoid reliance on JSON round-trip.
|
|
16
|
+
const { access, translationProvider, runner, collections, levels, provenance, lifecycle, targetSelection = "single", basePath: rawBasePath = "/translate" } = this.pluginConfig;
|
|
17
|
+
// Snapshot each collection's schema as an independent FieldLike tree BEFORE Payload's sanitizer
|
|
18
|
+
// mutates the originals (it deletes `localized` from fields nested under a localized ancestor).
|
|
19
|
+
// `projectFieldsToFieldLike` deep-copies only the properties the pipeline reads — an explicit,
|
|
20
|
+
// typed contract, replacing the old JSON round-trip (which "worked" only by silently dropping the
|
|
21
|
+
// Lexical editor's async functions that structuredClone chokes on). Payload's `Field[]` is
|
|
22
|
+
// structurally assignable to `FieldLike[]`, so the projection happens right here at the boundary.
|
|
24
23
|
const schemaMap = new Map(collections.map((col)=>[
|
|
25
24
|
col.slug,
|
|
26
|
-
|
|
25
|
+
projectFieldsToFieldLike(col.fields)
|
|
27
26
|
]));
|
|
28
27
|
const collectionSlugs = new Set(schemaMap.keys());
|
|
29
28
|
const basePath = normalizePath(rawBasePath);
|
|
@@ -51,7 +50,8 @@ import { normalizePath } from "./server/shared";
|
|
|
51
50
|
taskRunnerFactory,
|
|
52
51
|
schemaMap,
|
|
53
52
|
translationProvider,
|
|
54
|
-
provenanceServiceFactory: provenanceModule.serviceFactory
|
|
53
|
+
provenanceServiceFactory: provenanceModule.serviceFactory,
|
|
54
|
+
targetSelection
|
|
55
55
|
});
|
|
56
56
|
for (const level of activeLevels)level.extend(builder);
|
|
57
57
|
builder.addConfigModifier(runnerConfigModifier);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { ServerResponse } from "../../shared";
|
|
2
|
+
import { extractLocaleCodes } from "../../modules/auto-translate";
|
|
2
3
|
import { isCollectionAvailable, getAllCollectionIds } from "../_lib/collection-utils";
|
|
3
4
|
import { EnqueueInputSchema } from "./model";
|
|
5
|
+
import { resolveTargetLocales } from "./resolveTargetLocales";
|
|
4
6
|
/**
|
|
5
7
|
* Enqueues translation tasks for documents
|
|
6
8
|
*/ export class EnqueueTranslationHandler {
|
|
@@ -16,16 +18,34 @@ import { EnqueueInputSchema } from "./model";
|
|
|
16
18
|
const { source_lng, target_lng, collection_slug, collection_id, select_all, strategy, publish_on_translation } = validationResult.data;
|
|
17
19
|
const collectionSlug = isCollectionAvailable(collection_slug, this.config.availableCollections);
|
|
18
20
|
if (!collectionSlug) return ServerResponse.badRequest("Content of this collection is not available for translation");
|
|
21
|
+
// Normalize the scalar-or-array target into the concrete locales to fan out to: de-dup, exclude the
|
|
22
|
+
// source, and drop locales that are not configured (unknown locales would burn a provider call and
|
|
23
|
+
// corrupt data — Postgres locale enum / orphaned Mongo rows). `config` is optional-chained because a
|
|
24
|
+
// localization-less (or minimally-mocked) payload has none, which correctly disables the filter.
|
|
25
|
+
const knownLocales = extractLocaleCodes(req.payload.config?.localization);
|
|
26
|
+
const { targets, droppedUnknown } = resolveTargetLocales({
|
|
27
|
+
target_lng,
|
|
28
|
+
source_lng,
|
|
29
|
+
knownLocales
|
|
30
|
+
});
|
|
31
|
+
if (droppedUnknown.length > 0) {
|
|
32
|
+
req.payload.logger?.warn(`[payload-plugin-translator] enqueue on "${collectionSlug}": ignoring unknown target locale(s) ${droppedUnknown.join(", ")} (configured locales: ${knownLocales ? [
|
|
33
|
+
...knownLocales
|
|
34
|
+
].join(", ") : "n/a"}).`);
|
|
35
|
+
}
|
|
36
|
+
if (targets.length === 0) return ServerResponse.badRequest("No valid target locales to translate into (all requested locales were the source or unknown)");
|
|
19
37
|
const collectionIds = select_all ? await getAllCollectionIds(req.payload, collectionSlug) : collection_id;
|
|
20
38
|
const runner = this.taskRunnerFactory.create(req.payload);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
39
|
+
// One task per (document x target locale). The runner keys/supersedes per (document, targetLng),
|
|
40
|
+
// so N concurrent targets of one document coexist (PR #75) — no runner change needed.
|
|
41
|
+
const tasks = collectionIds.flatMap((id)=>targets.map((targetLng)=>({
|
|
42
|
+
collectionSlug,
|
|
43
|
+
collectionId: id,
|
|
44
|
+
sourceLng: source_lng,
|
|
45
|
+
targetLng,
|
|
46
|
+
strategy: strategy,
|
|
47
|
+
publishOnTranslation: publish_on_translation
|
|
48
|
+
})));
|
|
29
49
|
await runner.enqueue(tasks);
|
|
30
50
|
return ServerResponse.success({
|
|
31
51
|
success: true,
|
|
@@ -5,7 +5,7 @@ import type { CollectionSlug } from "payload";
|
|
|
5
5
|
*/
|
|
6
6
|
export declare const EnqueueInputSchema: z.ZodObject<{
|
|
7
7
|
source_lng: z.ZodString;
|
|
8
|
-
target_lng: z.ZodString
|
|
8
|
+
target_lng: z.ZodUnion<[z.ZodString, z.ZodArray<z.ZodString, "many">]>;
|
|
9
9
|
collection_slug: z.ZodString;
|
|
10
10
|
collection_id: z.ZodArray<z.ZodString, "atleastone">;
|
|
11
11
|
select_all: z.ZodOptional<z.ZodBoolean>;
|
|
@@ -15,7 +15,7 @@ export declare const EnqueueInputSchema: z.ZodObject<{
|
|
|
15
15
|
collection_slug: string;
|
|
16
16
|
collection_id: [string, ...string[]];
|
|
17
17
|
source_lng: string;
|
|
18
|
-
target_lng: string;
|
|
18
|
+
target_lng: string | string[];
|
|
19
19
|
strategy: "overwrite" | "skip_existing";
|
|
20
20
|
publish_on_translation: boolean;
|
|
21
21
|
select_all?: boolean | undefined;
|
|
@@ -23,7 +23,7 @@ export declare const EnqueueInputSchema: z.ZodObject<{
|
|
|
23
23
|
collection_slug: string;
|
|
24
24
|
collection_id: [string, ...string[]];
|
|
25
25
|
source_lng: string;
|
|
26
|
-
target_lng: string;
|
|
26
|
+
target_lng: string | string[];
|
|
27
27
|
strategy?: "overwrite" | "skip_existing" | undefined;
|
|
28
28
|
publish_on_translation?: boolean | undefined;
|
|
29
29
|
select_all?: boolean | undefined;
|
|
@@ -3,7 +3,12 @@ import { z } from "zod";
|
|
|
3
3
|
* Input validation schema
|
|
4
4
|
*/ export const EnqueueInputSchema = z.object({
|
|
5
5
|
source_lng: z.string().nonempty(),
|
|
6
|
-
|
|
6
|
+
// Accept a single locale (back-compat) OR a non-empty list, for multi-target fan-out. The empty
|
|
7
|
+
// array is rejected here so an empty multi-select is a validation error, not a silent no-op.
|
|
8
|
+
target_lng: z.union([
|
|
9
|
+
z.string().nonempty(),
|
|
10
|
+
z.array(z.string().nonempty()).min(1)
|
|
11
|
+
]),
|
|
7
12
|
collection_slug: z.string().nonempty(),
|
|
8
13
|
collection_id: z.array(z.coerce.string()).nonempty(),
|
|
9
14
|
select_all: z.boolean().optional(),
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolved target locales for a manual enqueue, plus what was dropped so the handler can log precisely.
|
|
3
|
+
*/
|
|
4
|
+
export type ResolvedTargetLocales = {
|
|
5
|
+
/** The concrete locales to fan out to — de-duplicated, source excluded, unknown removed. */
|
|
6
|
+
targets: string[];
|
|
7
|
+
/** Requested locales that are not configured (dropped) — empty when localization is unknown/disabled. */
|
|
8
|
+
droppedUnknown: string[];
|
|
9
|
+
/** Whether the source locale was requested as a target and excluded. */
|
|
10
|
+
droppedSource: boolean;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Normalize the enqueue `target_lng` input (scalar or array) into the concrete list of target locales
|
|
14
|
+
* to translate into. Applies, in order: array-coercion, de-duplication (first-seen order preserved),
|
|
15
|
+
* source-locale exclusion, and — when the configured locale set is known — dropping unknown locales.
|
|
16
|
+
*
|
|
17
|
+
* De-dup and unknown-dropping are the manual-enqueue counterparts of the auto-translate policy filter:
|
|
18
|
+
* the runner only supersedes against already-stored jobs, so duplicates within one enqueue must be
|
|
19
|
+
* collapsed here; and an unknown locale must never reach the pipeline — it burns a provider call and
|
|
20
|
+
* either errors on a Postgres locale enum or writes orphaned, invisible data on Mongo/SQLite.
|
|
21
|
+
*
|
|
22
|
+
* @param knownLocales - the configured locale codes, or `null` when localization is disabled/absent
|
|
23
|
+
* (then no unknown-dropping is applied — every requested locale except the source is kept).
|
|
24
|
+
*/
|
|
25
|
+
export declare function resolveTargetLocales(args: {
|
|
26
|
+
target_lng: string | string[];
|
|
27
|
+
source_lng: string;
|
|
28
|
+
knownLocales: Set<string> | null;
|
|
29
|
+
}): ResolvedTargetLocales;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolved target locales for a manual enqueue, plus what was dropped so the handler can log precisely.
|
|
3
|
+
*/ /**
|
|
4
|
+
* Normalize the enqueue `target_lng` input (scalar or array) into the concrete list of target locales
|
|
5
|
+
* to translate into. Applies, in order: array-coercion, de-duplication (first-seen order preserved),
|
|
6
|
+
* source-locale exclusion, and — when the configured locale set is known — dropping unknown locales.
|
|
7
|
+
*
|
|
8
|
+
* De-dup and unknown-dropping are the manual-enqueue counterparts of the auto-translate policy filter:
|
|
9
|
+
* the runner only supersedes against already-stored jobs, so duplicates within one enqueue must be
|
|
10
|
+
* collapsed here; and an unknown locale must never reach the pipeline — it burns a provider call and
|
|
11
|
+
* either errors on a Postgres locale enum or writes orphaned, invisible data on Mongo/SQLite.
|
|
12
|
+
*
|
|
13
|
+
* @param knownLocales - the configured locale codes, or `null` when localization is disabled/absent
|
|
14
|
+
* (then no unknown-dropping is applied — every requested locale except the source is kept).
|
|
15
|
+
*/ export function resolveTargetLocales(args) {
|
|
16
|
+
const { target_lng, source_lng, knownLocales } = args;
|
|
17
|
+
const requested = Array.isArray(target_lng) ? target_lng : [
|
|
18
|
+
target_lng
|
|
19
|
+
];
|
|
20
|
+
const deduped = [
|
|
21
|
+
...new Set(requested)
|
|
22
|
+
];
|
|
23
|
+
const droppedSource = deduped.includes(source_lng);
|
|
24
|
+
const withoutSource = deduped.filter((target)=>target !== source_lng);
|
|
25
|
+
if (!knownLocales) {
|
|
26
|
+
return {
|
|
27
|
+
targets: withoutSource,
|
|
28
|
+
droppedUnknown: [],
|
|
29
|
+
droppedSource
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const droppedUnknown = withoutSource.filter((target)=>!knownLocales.has(target));
|
|
33
|
+
const targets = withoutSource.filter((target)=>knownLocales.has(target));
|
|
34
|
+
return {
|
|
35
|
+
targets,
|
|
36
|
+
droppedUnknown,
|
|
37
|
+
droppedSource
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=resolveTargetLocales.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { FieldLike } from "../../../core/kernel/field-traversal";
|
|
2
2
|
/**
|
|
3
3
|
* Outcome of mapping a declared field path to a translatable subtree.
|
|
4
4
|
*
|
|
@@ -14,7 +14,7 @@ import type { Field } from "payload";
|
|
|
14
14
|
*/
|
|
15
15
|
export type FieldSubtreeResolution = {
|
|
16
16
|
status: "resolved";
|
|
17
|
-
schema:
|
|
17
|
+
schema: FieldLike[];
|
|
18
18
|
sourceData: Record<string, unknown>;
|
|
19
19
|
fieldName: string;
|
|
20
20
|
} | {
|
|
@@ -36,4 +36,4 @@ export type FieldSubtreeResolution = {
|
|
|
36
36
|
* the element's `blockType` in `doc` picks the block schema. Without `doc`, a path through a
|
|
37
37
|
* `blocks` field returns `inside-blocks`.
|
|
38
38
|
*/
|
|
39
|
-
export declare function resolveFieldSubtree(rootFields:
|
|
39
|
+
export declare function resolveFieldSubtree(rootFields: FieldLike[], fieldPath: string, value: unknown, doc?: unknown): FieldSubtreeResolution;
|
|
@@ -2,3 +2,5 @@ export { configureAutoTranslate } from "./AutoTranslate.wiring";
|
|
|
2
2
|
export type { AutoTranslateModule } from "./AutoTranslate.wiring";
|
|
3
3
|
export { makeAutoTranslateHook, injectAutoTranslateHook } from "./AutoTranslateEnqueue.hook";
|
|
4
4
|
export type { AutoTranslatePolicyResolver, NormalizedAutoTranslatePolicy, } from "./AutoTranslate.policy";
|
|
5
|
+
export { extractLocaleCodes } from "./AutoTranslate.policy";
|
|
6
|
+
export type { LocalizationLike } from "./AutoTranslate.policy";
|
|
@@ -3,5 +3,7 @@
|
|
|
3
3
|
// wiring + the afterChange hook that enqueues translations on a source-locale change (#51).
|
|
4
4
|
export { configureAutoTranslate } from "./AutoTranslate.wiring";
|
|
5
5
|
export { makeAutoTranslateHook, injectAutoTranslateHook } from "./AutoTranslateEnqueue.hook";
|
|
6
|
+
// Generic locale-set extraction, reused by the manual enqueue path to validate multi-target input.
|
|
7
|
+
export { extractLocaleCodes } from "./AutoTranslate.policy";
|
|
6
8
|
|
|
7
9
|
//# sourceMappingURL=index.js.map
|
|
@@ -2,6 +2,7 @@ import type { CollectionConfig, Config, Endpoint } from "payload";
|
|
|
2
2
|
import type { AccessGuard } from "../../../types/AccessGuard";
|
|
3
3
|
import type { RawPayloadComponentExport } from "../../../types/PayloadComponentExport";
|
|
4
4
|
import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
|
|
5
|
+
import type { TargetSelectionMode } from "../../../types/TargetSelection";
|
|
5
6
|
import type { ConfigModifier } from "../../../types/ConfigModifier";
|
|
6
7
|
import type { TranslationProvider } from "../../../core/domain/translation-providers";
|
|
7
8
|
import type { TaskRunnerFactory } from "../task-runner";
|
|
@@ -28,6 +29,7 @@ export declare class PluginConfigBuilder implements LevelContext {
|
|
|
28
29
|
readonly schemaMap: CollectionSchemaMap;
|
|
29
30
|
readonly translationProvider: TranslationProvider;
|
|
30
31
|
readonly provenanceServiceFactory?: ProvenanceServiceFactory;
|
|
32
|
+
readonly targetSelection: TargetSelectionMode;
|
|
31
33
|
private readonly endpoints;
|
|
32
34
|
private readonly collectionComponents;
|
|
33
35
|
private readonly adminProviders;
|
|
@@ -35,6 +35,7 @@ function attachToSlot(collection, slot, component) {
|
|
|
35
35
|
schemaMap;
|
|
36
36
|
translationProvider;
|
|
37
37
|
provenanceServiceFactory;
|
|
38
|
+
targetSelection;
|
|
38
39
|
endpoints = [];
|
|
39
40
|
collectionComponents = [];
|
|
40
41
|
adminProviders = [];
|
|
@@ -47,6 +48,7 @@ function attachToSlot(collection, slot, component) {
|
|
|
47
48
|
this.schemaMap = deps.schemaMap;
|
|
48
49
|
this.translationProvider = deps.translationProvider;
|
|
49
50
|
this.provenanceServiceFactory = deps.provenanceServiceFactory;
|
|
51
|
+
this.targetSelection = deps.targetSelection;
|
|
50
52
|
}
|
|
51
53
|
addEndpoints(endpoints) {
|
|
52
54
|
this.endpoints.push(...endpoints);
|
|
@@ -2,6 +2,7 @@ import type { CollectionConfig, Endpoint } from "payload";
|
|
|
2
2
|
import type { AccessGuard } from "../../../types/AccessGuard";
|
|
3
3
|
import type { RawPayloadComponentExport } from "../../../types/PayloadComponentExport";
|
|
4
4
|
import type { CollectionSchemaMap } from "../../../types/CollectionSchemaMap";
|
|
5
|
+
import type { TargetSelectionMode } from "../../../types/TargetSelection";
|
|
5
6
|
import type { TranslationProvider } from "../../../core/domain/translation-providers";
|
|
6
7
|
import type { TaskRunnerFactory } from "../task-runner";
|
|
7
8
|
import type { ProvenanceServiceFactory } from "../provenance";
|
|
@@ -23,6 +24,9 @@ export type TranslationContext = {
|
|
|
23
24
|
readonly translationProvider: TranslationProvider;
|
|
24
25
|
/** Builds a provenance service; absent when provenance is disabled (staleness then reports empty). */
|
|
25
26
|
readonly provenanceServiceFactory?: ProvenanceServiceFactory;
|
|
27
|
+
/** Resolved target-language selection mode (`'single'` default) — drives which target control the
|
|
28
|
+
* admin forms render. */
|
|
29
|
+
readonly targetSelection: TargetSelectionMode;
|
|
26
30
|
};
|
|
27
31
|
/**
|
|
28
32
|
* A composable translation surface (document / collection / field).
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
import type { CollectionSlug
|
|
1
|
+
import type { CollectionSlug } from "payload";
|
|
2
|
+
import type { FieldLike } from "../core/kernel/field-traversal";
|
|
2
3
|
/**
|
|
3
|
-
* Map of collection slug to
|
|
4
|
-
*
|
|
4
|
+
* Map of collection slug to its projected field schema — an independent {@link FieldLike} snapshot
|
|
5
|
+
* taken (via `projectFieldsToFieldLike`) before Payload's sanitizer mutates the originals, so nested
|
|
6
|
+
* `localized` flags survive. Typed as `FieldLike[]` (not Payload's `Field[]`) because every consumer
|
|
7
|
+
* reads it structurally as `FieldLike`; the projection is the boundary that makes that explicit.
|
|
5
8
|
*/
|
|
6
|
-
export type CollectionSchemaMap = Map<CollectionSlug,
|
|
9
|
+
export type CollectionSchemaMap = Map<CollectionSlug, FieldLike[]>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Map of collection slug to
|
|
3
|
-
*
|
|
2
|
+
* Map of collection slug to its projected field schema — an independent {@link FieldLike} snapshot
|
|
3
|
+
* taken (via `projectFieldsToFieldLike`) before Payload's sanitizer mutates the originals, so nested
|
|
4
|
+
* `localized` flags survive. Typed as `FieldLike[]` (not Payload's `Field[]`) because every consumer
|
|
5
|
+
* reads it structurally as `FieldLike`; the projection is the boundary that makes that explicit.
|
|
4
6
|
*/ export { };
|
|
5
7
|
|
|
6
8
|
//# sourceMappingURL=CollectionSchemaMap.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the target-language field behaves in the translation forms (bulk dashboard + per-document panel).
|
|
3
|
+
*
|
|
4
|
+
* - `'single'` (default): one target locale per run — today's behaviour, unchanged.
|
|
5
|
+
* - `'multi'`: the editor picks several target locales and the run fans out one translation per
|
|
6
|
+
* `(document × target locale)`; the target field renders a compact multi-select.
|
|
7
|
+
*
|
|
8
|
+
* @since 0.10.0
|
|
9
|
+
*/
|
|
10
|
+
export type TargetSelectionMode = "single" | "multi";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the target-language field behaves in the translation forms (bulk dashboard + per-document panel).
|
|
3
|
+
*
|
|
4
|
+
* - `'single'` (default): one target locale per run — today's behaviour, unchanged.
|
|
5
|
+
* - `'multi'`: the editor picks several target locales and the run fans out one translation per
|
|
6
|
+
* `(document × target locale)`; the target field renders a compact multi-select.
|
|
7
|
+
*
|
|
8
|
+
* @since 0.10.0
|
|
9
|
+
*/ export { };
|
|
10
|
+
|
|
11
|
+
//# sourceMappingURL=TargetSelection.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.10.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",
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
type Variables = {
|
|
2
|
-
source_lng: string;
|
|
3
|
-
target_lng: string;
|
|
4
|
-
collection_slug: string;
|
|
5
|
-
collection_id: Array<string | number>;
|
|
6
|
-
};
|
|
7
|
-
export declare function useQueueDocumentTranslations(): import("@tanstack/react-query").UseMutationResult<void, Error, Variables, unknown>;
|
|
8
|
-
export {};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { useMutation } from "@tanstack/react-query";
|
|
2
|
-
import { ofetch } from "ofetch";
|
|
3
|
-
import { useTranslateKitConfig } from "../../../../app/config";
|
|
4
|
-
import { handleNextApiError } from "../../../../shared/lib/errors/handleApiError";
|
|
5
|
-
export function useQueueDocumentTranslations() {
|
|
6
|
-
const { basePath } = useTranslateKitConfig();
|
|
7
|
-
return useMutation({
|
|
8
|
-
mutationKey: [
|
|
9
|
-
"queue-document-translations"
|
|
10
|
-
],
|
|
11
|
-
mutationFn: async (variables)=>{
|
|
12
|
-
await handleNextApiError(()=>ofetch(`/api${basePath}/enqueue-many`, {
|
|
13
|
-
method: "post",
|
|
14
|
-
body: variables
|
|
15
|
-
}));
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
//# sourceMappingURL=useQueueDocumentTranslations.js.map
|