@focus-reactive/payload-plugin-translator 0.5.0 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/shared/ui/Button/Button.d.ts +4 -4
- package/dist/client/shared/ui/Button/Button.js +18 -18
- package/dist/client/shared/ui/Button/styles.module.scss +1 -1
- package/dist/client/shared/ui/Popup/Popup.d.ts +10 -4
- package/dist/client/shared/ui/Popup/Popup.js +4 -3
- package/dist/client/shared/ui/Select/Select.d.ts +9 -7
- package/dist/client/shared/ui/Select/Select.js +18 -17
- package/dist/client/shared/ui/Select/styles.module.scss +5 -0
- package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +31 -0
- package/dist/server/features/translate-field/resolveFieldSubtree.js +45 -0
- package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.d.ts +3 -9
- package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.js +71 -78
- package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts +11 -22
- package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js +85 -106
- package/dist/server/shared/field-traversal/findFieldByPath.d.ts +40 -0
- package/dist/server/shared/field-traversal/findFieldByPath.js +88 -0
- package/dist/server/shared/field-traversal/index.d.ts +5 -0
- package/dist/server/shared/field-traversal/index.js +5 -0
- package/dist/server/shared/field-traversal/kernel.d.ts +85 -0
- package/dist/server/shared/field-traversal/kernel.js +145 -0
- package/dist/server/shared/field-traversal/types.d.ts +191 -0
- package/dist/server/shared/field-traversal/types.js +41 -0
- package/dist/server/shared/field-traversal/walkFields.d.ts +51 -0
- package/dist/server/shared/field-traversal/walkFields.js +164 -0
- package/dist/server/shared/utils/filterLocalizedFields.d.ts +3 -3
- package/dist/server/shared/utils/filterLocalizedFields.js +52 -63
- package/package.json +2 -2
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { ComponentPropsWithRef, ReactNode } from
|
|
2
|
-
type ButtonProps = ComponentPropsWithRef<
|
|
1
|
+
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
2
|
+
type ButtonProps = ComponentPropsWithRef<"button"> & {
|
|
3
3
|
$fullWidth?: boolean;
|
|
4
|
-
$variant?:
|
|
4
|
+
$variant?: "outlined" | "filled" | "unstyled" | "transparent" | "light";
|
|
5
5
|
$startContent?: ReactNode;
|
|
6
|
-
$size?:
|
|
6
|
+
$size?: "sm" | "md" | "lg";
|
|
7
7
|
$isLoading?: boolean;
|
|
8
8
|
$isIconButton?: boolean;
|
|
9
9
|
};
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import classNames from
|
|
3
|
-
import { forwardRef } from
|
|
4
|
-
import Loading from
|
|
5
|
-
import styles from
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import Loading from "../Loading";
|
|
5
|
+
import styles from "./styles.module.scss";
|
|
6
6
|
const sizes = {
|
|
7
7
|
sm: {
|
|
8
|
-
height:
|
|
9
|
-
paddingInline:
|
|
8
|
+
height: "24px",
|
|
9
|
+
paddingInline: "0.25rem"
|
|
10
10
|
},
|
|
11
11
|
md: {
|
|
12
|
-
height:
|
|
13
|
-
paddingInline:
|
|
12
|
+
height: "32px",
|
|
13
|
+
paddingInline: "0.5rem"
|
|
14
14
|
},
|
|
15
15
|
lg: {
|
|
16
|
-
height:
|
|
17
|
-
paddingInline:
|
|
16
|
+
height: "40px",
|
|
17
|
+
paddingInline: "0.75rem"
|
|
18
18
|
}
|
|
19
19
|
};
|
|
20
|
-
const Button = /*#__PURE__*/ forwardRef(function Button({ className =
|
|
20
|
+
const Button = /*#__PURE__*/ forwardRef(function Button({ className = "", children, $fullWidth, style, $startContent, $variant, $isLoading, $size = "md", $isIconButton, ...props }, ref) {
|
|
21
21
|
const _style = {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
"--button-width": $fullWidth ? "100%" : "auto",
|
|
23
|
+
"--height": sizes[$size].height,
|
|
24
|
+
"--padding-inline": $isIconButton ? "0" : sizes[$size].paddingInline,
|
|
25
|
+
"--width": $isIconButton ? sizes[$size].height : "auto",
|
|
26
26
|
...style
|
|
27
27
|
};
|
|
28
|
-
const buttonClassName = $variant ===
|
|
28
|
+
const buttonClassName = $variant === "unstyled" ? classNames(styles["unstyled"], className, $isIconButton && styles["icon-button"]) : classNames(styles["button"], className, $variant ? styles[$variant] : undefined, $isIconButton && styles["icon-button"]);
|
|
29
29
|
return /*#__PURE__*/ _jsxs("button", {
|
|
30
30
|
...props,
|
|
31
31
|
style: _style,
|
|
@@ -33,11 +33,11 @@ const Button = /*#__PURE__*/ forwardRef(function Button({ className = '', childr
|
|
|
33
33
|
className: buttonClassName,
|
|
34
34
|
children: [
|
|
35
35
|
$startContent && /*#__PURE__*/ _jsx("div", {
|
|
36
|
-
className: styles[
|
|
36
|
+
className: styles["start-content"],
|
|
37
37
|
children: $startContent
|
|
38
38
|
}),
|
|
39
39
|
$isLoading ? /*#__PURE__*/ _jsx(Loading, {
|
|
40
|
-
size: $isIconButton ?
|
|
40
|
+
size: $isIconButton ? "small" : "medium"
|
|
41
41
|
}) : children
|
|
42
42
|
]
|
|
43
43
|
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
justify-content: center;
|
|
8
8
|
align-items: center;
|
|
9
9
|
text-decoration: none;
|
|
10
|
-
transition: background 0.
|
|
10
|
+
transition: background 0.1s ease-in-out, opacity 0.2s ease-in-out, border-color 0.2s ease-in-out;
|
|
11
11
|
background: none;
|
|
12
12
|
padding-block: 0.25rem;
|
|
13
13
|
padding-inline: var(--padding-inline);
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
-
import type { PropsWithChildren, ReactElement } from
|
|
1
|
+
import type { PropsWithChildren, ReactElement } from "react";
|
|
2
2
|
type PopupProps = PropsWithChildren<{
|
|
3
3
|
$trigger: ReactElement;
|
|
4
|
-
$align?:
|
|
5
|
-
$side?:
|
|
4
|
+
$align?: "center" | "end" | "start";
|
|
5
|
+
$side?: "top" | "right" | "bottom" | "left";
|
|
6
6
|
open: boolean;
|
|
7
7
|
onOpenChange: (open: boolean) => void;
|
|
8
|
+
/**
|
|
9
|
+
* Forwarded to Radix `Popover.Content`. Call `event.preventDefault()` to stop the popover
|
|
10
|
+
* from auto-focusing its first focusable child on open — useful when that child is a
|
|
11
|
+
* tooltip trigger (Radix tooltips open on focus, so autofocus would flash the tooltip).
|
|
12
|
+
*/
|
|
13
|
+
onOpenAutoFocus?: (event: Event) => void;
|
|
8
14
|
}>;
|
|
9
|
-
declare function Popup({ open, children, onOpenChange, $trigger, $align, $side }: PopupProps): import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
declare function Popup({ open, children, onOpenChange, $trigger, $align, $side, onOpenAutoFocus }: PopupProps): import("react/jsx-runtime").JSX.Element;
|
|
10
16
|
export default Popup;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import * as Popover from
|
|
3
|
-
import style from
|
|
4
|
-
function Popup({ open, children, onOpenChange, $trigger, $align, $side }) {
|
|
2
|
+
import * as Popover from "@radix-ui/react-popover";
|
|
3
|
+
import style from "./styles.module.scss";
|
|
4
|
+
function Popup({ open, children, onOpenChange, $trigger, $align, $side, onOpenAutoFocus }) {
|
|
5
5
|
return /*#__PURE__*/ _jsxs(Popover.Root, {
|
|
6
6
|
open: open,
|
|
7
7
|
onOpenChange: onOpenChange,
|
|
@@ -16,6 +16,7 @@ function Popup({ open, children, onOpenChange, $trigger, $align, $side }) {
|
|
|
16
16
|
side: $side,
|
|
17
17
|
align: $align,
|
|
18
18
|
className: style.popover,
|
|
19
|
+
onOpenAutoFocus: onOpenAutoFocus,
|
|
19
20
|
children: children
|
|
20
21
|
})
|
|
21
22
|
})
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import type { ComponentPropsWithoutRef, ComponentPropsWithRef } from
|
|
2
|
-
type SelectProps = ComponentPropsWithRef<
|
|
1
|
+
import type { ComponentPropsWithoutRef, ComponentPropsWithRef } from "react";
|
|
2
|
+
type SelectProps = ComponentPropsWithRef<"select"> & {
|
|
3
3
|
emptyOption?: boolean;
|
|
4
4
|
hasError?: boolean;
|
|
5
|
-
$size?:
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
$size?: "sm" | "md" | "lg";
|
|
6
|
+
/** Stretch to fill the container (default). Set `false` to size to content — useful inline. */
|
|
7
|
+
$fullWidth?: boolean;
|
|
8
|
+
"aria-describedby"?: string;
|
|
9
|
+
"aria-invalid"?: boolean;
|
|
8
10
|
};
|
|
9
|
-
type SelectOptionProps = ComponentPropsWithoutRef<
|
|
10
|
-
type SelectEmptyOptionProps = Omit<SelectOptionProps,
|
|
11
|
+
type SelectOptionProps = ComponentPropsWithoutRef<"option">;
|
|
12
|
+
type SelectEmptyOptionProps = Omit<SelectOptionProps, "value" | "disabled" | "hidden">;
|
|
11
13
|
declare const SelectOption: ({ children, ...props }: SelectOptionProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
14
|
declare const SelectEmptyOption: ({ children, ...props }: SelectEmptyOptionProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
15
|
declare const Select: import("react").ForwardRefExoticComponent<Omit<SelectProps, "ref"> & import("react").RefAttributes<HTMLSelectElement>>;
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import classNames from
|
|
3
|
-
import { forwardRef } from
|
|
4
|
-
import styles from
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import styles from "./styles.module.scss";
|
|
5
5
|
const sizes = {
|
|
6
6
|
sm: {
|
|
7
|
-
height:
|
|
8
|
-
paddingInline:
|
|
9
|
-
fontSize:
|
|
7
|
+
height: "24px",
|
|
8
|
+
paddingInline: "0.25rem",
|
|
9
|
+
fontSize: "0.875rem"
|
|
10
10
|
},
|
|
11
11
|
md: {
|
|
12
|
-
height:
|
|
13
|
-
paddingInline:
|
|
14
|
-
fontSize:
|
|
12
|
+
height: "32px",
|
|
13
|
+
paddingInline: "0.5rem",
|
|
14
|
+
fontSize: "1rem"
|
|
15
15
|
},
|
|
16
16
|
lg: {
|
|
17
|
-
height:
|
|
18
|
-
paddingInline:
|
|
19
|
-
fontSize:
|
|
17
|
+
height: "40px",
|
|
18
|
+
paddingInline: "0.75rem",
|
|
19
|
+
fontSize: "1rem"
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
const SelectOption = ({ children, ...props })=>/*#__PURE__*/ _jsx("option", {
|
|
@@ -28,15 +28,16 @@ const SelectEmptyOption = ({ children, ...props })=>/*#__PURE__*/ _jsx("option",
|
|
|
28
28
|
...props,
|
|
29
29
|
children: children
|
|
30
30
|
});
|
|
31
|
-
const Select = /*#__PURE__*/ forwardRef(function Select({ children, className =
|
|
31
|
+
const Select = /*#__PURE__*/ forwardRef(function Select({ children, className = "", hasError = false, $size = "lg", $fullWidth = true, style, "aria-invalid": ariaInvalid, ...props }, ref) {
|
|
32
32
|
const _style = {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
"--height": sizes[$size].height,
|
|
34
|
+
"--padding-inline": sizes[$size].paddingInline,
|
|
35
|
+
"--font-size": sizes[$size].fontSize,
|
|
36
36
|
...style
|
|
37
37
|
};
|
|
38
38
|
const selectClassName = classNames(styles.select, styles[`size-${$size}`], {
|
|
39
|
-
[styles.selectError]: hasError
|
|
39
|
+
[styles.selectError]: hasError,
|
|
40
|
+
[styles.auto]: !$fullWidth
|
|
40
41
|
}, className);
|
|
41
42
|
return /*#__PURE__*/ _jsx("select", {
|
|
42
43
|
ref: ref,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Field } from "payload";
|
|
2
|
+
/**
|
|
3
|
+
* Outcome of mapping a declared field path to a translatable subtree.
|
|
4
|
+
*
|
|
5
|
+
* - `resolved` — the path lands on a translatable leaf; `schema` + `sourceData`
|
|
6
|
+
* are ready for `translateContent`, and `fieldName` is the key to unwrap the result.
|
|
7
|
+
* - `not-found` — the path resolves to no field at all (a typo) → caller returns 400.
|
|
8
|
+
* - `not-translatable` — resolves to a field that isn't a text-like leaf → caller no-ops.
|
|
9
|
+
* - `inside-blocks` — the path descends through a polymorphic `blocks` field, which
|
|
10
|
+
* can't be resolved from the path alone (the blockType lives in the data) → caller no-ops.
|
|
11
|
+
*/
|
|
12
|
+
export type FieldSubtreeResolution = {
|
|
13
|
+
status: "resolved";
|
|
14
|
+
schema: Field[];
|
|
15
|
+
sourceData: Record<string, unknown>;
|
|
16
|
+
fieldName: string;
|
|
17
|
+
} | {
|
|
18
|
+
status: "not-found";
|
|
19
|
+
} | {
|
|
20
|
+
status: "not-translatable";
|
|
21
|
+
} | {
|
|
22
|
+
status: "inside-blocks";
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Map a declared `fieldPath` to the `{ schema, sourceData }` pair `translateContent` expects.
|
|
26
|
+
* Walks the schema by field name (via the shared {@link findFieldByPath}), dropping array indices
|
|
27
|
+
* (a field's config is shared across array items). Descends transparently through presentational
|
|
28
|
+
* containers (row, collapsible, unnamed tabs) and through named group/array/tab containers. A path
|
|
29
|
+
* that descends through a `blocks` field returns `inside-blocks`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveFieldSubtree(rootFields: Field[], fieldPath: string, value: unknown): FieldSubtreeResolution;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { isTranslatableField } from "../../shared";
|
|
2
|
+
import { findFieldByPath } from "../../shared/field-traversal";
|
|
3
|
+
const isIndexSegment = (segment)=>/^\d+$/u.test(segment);
|
|
4
|
+
/**
|
|
5
|
+
* Map a declared `fieldPath` to the `{ schema, sourceData }` pair `translateContent` expects.
|
|
6
|
+
* Walks the schema by field name (via the shared {@link findFieldByPath}), dropping array indices
|
|
7
|
+
* (a field's config is shared across array items). Descends transparently through presentational
|
|
8
|
+
* containers (row, collapsible, unnamed tabs) and through named group/array/tab containers. A path
|
|
9
|
+
* that descends through a `blocks` field returns `inside-blocks`.
|
|
10
|
+
*/ export function resolveFieldSubtree(rootFields, fieldPath, value) {
|
|
11
|
+
const segments = fieldPath.split(".").map((segment)=>segment.trim()).filter((segment)=>segment.length > 0 && !isIndexSegment(segment));
|
|
12
|
+
if (segments.length === 0) return {
|
|
13
|
+
status: "not-found"
|
|
14
|
+
};
|
|
15
|
+
const result = findFieldByPath(rootFields, segments);
|
|
16
|
+
switch(result.status){
|
|
17
|
+
case "leaf":
|
|
18
|
+
return isTranslatableField(result.field) ? {
|
|
19
|
+
status: "resolved",
|
|
20
|
+
schema: [
|
|
21
|
+
result.field
|
|
22
|
+
],
|
|
23
|
+
sourceData: {
|
|
24
|
+
[result.field.name]: value
|
|
25
|
+
},
|
|
26
|
+
fieldName: result.field.name
|
|
27
|
+
} : {
|
|
28
|
+
status: "not-translatable"
|
|
29
|
+
};
|
|
30
|
+
case "container":
|
|
31
|
+
return {
|
|
32
|
+
status: "not-translatable"
|
|
33
|
+
};
|
|
34
|
+
case "inside-blocks":
|
|
35
|
+
return {
|
|
36
|
+
status: "inside-blocks"
|
|
37
|
+
};
|
|
38
|
+
case "not-found":
|
|
39
|
+
return {
|
|
40
|
+
status: "not-found"
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=resolveFieldSubtree.js.map
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import type { Field } from
|
|
1
|
+
import type { Field } from "payload";
|
|
2
2
|
/**
|
|
3
|
-
* Deep
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Logic:
|
|
7
|
-
* - If targetValue exists and is not empty → use targetValue
|
|
8
|
-
* - Otherwise → use sourceValue
|
|
3
|
+
* Deep-merges source and target data with target priority, producing the full document shape
|
|
4
|
+
* Payload validation needs. Built on the shared {@link walkFields} engine.
|
|
9
5
|
*
|
|
10
6
|
* Stage 1 of the translation pipeline.
|
|
11
7
|
*/
|
|
@@ -14,12 +10,10 @@ export declare class DataReconciler {
|
|
|
14
10
|
constructor(schema: Field[]);
|
|
15
11
|
/**
|
|
16
12
|
* Reconciles source and target data into a complete document shape.
|
|
17
|
-
* The result contains all fields needed for Payload validation.
|
|
18
13
|
*
|
|
19
14
|
* @param sourceData - Source locale document data
|
|
20
15
|
* @param targetData - Target locale document data (may be empty/partial)
|
|
21
16
|
* @returns Complete document shape with reconciled field values
|
|
22
17
|
*/
|
|
23
18
|
reconcile(sourceData: Record<string, unknown>, targetData: Record<string, unknown>): Record<string, unknown>;
|
|
24
|
-
private reconcileFields;
|
|
25
19
|
}
|
|
@@ -1,12 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { isEmpty, isObject } from "../../../../shared";
|
|
2
|
+
import { resolveBlockFields, walkFields } from "../../../../shared/field-traversal";
|
|
3
|
+
const asObject = (value)=>isObject(value) ? value : {};
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
5
|
+
* Reconcile walker: produces the full document shape from source + target, with target
|
|
6
|
+
* priority (source fills empty target slots). Iteration is driven by `source`; a field with
|
|
7
|
+
* no source value is dropped, `id` is stripped from array/block elements (Postgres rejects it
|
|
8
|
+
* on update), `blockType` is preserved, and non-object array items / unknown blocks pass
|
|
9
|
+
* through unchanged.
|
|
10
|
+
*/ const reconcileWalker = {
|
|
11
|
+
enterObject (field, cursor) {
|
|
12
|
+
const sourceValue = cursor.source[field.name];
|
|
13
|
+
if (!isObject(sourceValue)) return "skip"; // undefined / non-object source → drop the field
|
|
14
|
+
return {
|
|
15
|
+
source: sourceValue,
|
|
16
|
+
target: asObject(cursor.target[field.name])
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
enterList (field, cursor) {
|
|
20
|
+
const sourceValue = cursor.source[field.name];
|
|
21
|
+
if (!Array.isArray(sourceValue)) return "skip";
|
|
22
|
+
const targetValue = cursor.target[field.name];
|
|
23
|
+
const targetArr = Array.isArray(targetValue) ? targetValue : [];
|
|
24
|
+
const children = [];
|
|
25
|
+
sourceValue.forEach((item, index)=>{
|
|
26
|
+
if (!isObject(item)) return; // non-object element → passthrough (rebuilt in combine)
|
|
27
|
+
const fields = field.type === "blocks" ? resolveBlockFields(field, item) : field.fields;
|
|
28
|
+
if (!fields) return; // unknown blockType → passthrough
|
|
29
|
+
children.push({
|
|
30
|
+
cursor: {
|
|
31
|
+
source: item,
|
|
32
|
+
target: asObject(targetArr[index])
|
|
33
|
+
},
|
|
34
|
+
fields,
|
|
35
|
+
key: index
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
return children;
|
|
39
|
+
},
|
|
40
|
+
leaf (field, cursor) {
|
|
41
|
+
const sourceValue = cursor.source[field.name];
|
|
42
|
+
if (sourceValue === undefined) return undefined; // no source value → field dropped
|
|
43
|
+
const targetValue = cursor.target[field.name];
|
|
44
|
+
return isEmpty(targetValue) ? sourceValue : targetValue; // target priority, fallback to source
|
|
45
|
+
},
|
|
46
|
+
combine (container, children, cursor) {
|
|
47
|
+
if (container.kind === "list") {
|
|
48
|
+
// Rebuild the full array: reconciled objects where present (by index), raw source items otherwise.
|
|
49
|
+
const sourceArr = cursor.source[container.key] ?? [];
|
|
50
|
+
const byIndex = new Map(children.map((child)=>[
|
|
51
|
+
child.key,
|
|
52
|
+
child.out
|
|
53
|
+
]));
|
|
54
|
+
return sourceArr.map((item, index)=>byIndex.has(index) ? byIndex.get(index) : item);
|
|
55
|
+
}
|
|
56
|
+
const result = {};
|
|
57
|
+
for (const child of children)result[child.key] = child.out;
|
|
58
|
+
// Block elements keep their blockType; id is intentionally stripped (Postgres rejects it on update).
|
|
59
|
+
if (container.kind === "element" && container.field.type === "blocks") {
|
|
60
|
+
result.blockType = cursor.source.blockType;
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Deep-merges source and target data with target priority, producing the full document shape
|
|
67
|
+
* Payload validation needs. Built on the shared {@link walkFields} engine.
|
|
10
68
|
*
|
|
11
69
|
* Stage 1 of the translation pipeline.
|
|
12
70
|
*/ export class DataReconciler {
|
|
@@ -16,81 +74,16 @@ import { hasFields, isBlockItem, isTabsField, isEmpty, isObject } from '../../..
|
|
|
16
74
|
}
|
|
17
75
|
/**
|
|
18
76
|
* Reconciles source and target data into a complete document shape.
|
|
19
|
-
* The result contains all fields needed for Payload validation.
|
|
20
77
|
*
|
|
21
78
|
* @param sourceData - Source locale document data
|
|
22
79
|
* @param targetData - Target locale document data (may be empty/partial)
|
|
23
80
|
* @returns Complete document shape with reconciled field values
|
|
24
81
|
*/ reconcile(sourceData, targetData) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (isTabsField(field)) {
|
|
31
|
-
for (const tab of field.tabs){
|
|
32
|
-
if (hasFields(tab)) {
|
|
33
|
-
if (tabHasName(tab)) {
|
|
34
|
-
const tabSource = source[tab.name];
|
|
35
|
-
const tabTarget = target[tab.name];
|
|
36
|
-
if (isObject(tabSource)) {
|
|
37
|
-
result[tab.name] = this.reconcileFields(tab.fields, tabSource, isObject(tabTarget) ? tabTarget : {});
|
|
38
|
-
}
|
|
39
|
-
} else {
|
|
40
|
-
Object.assign(result, this.reconcileFields(tab.fields, source, target));
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (!fieldAffectsData(field)) {
|
|
47
|
-
if (hasFields(field)) Object.assign(result, this.reconcileFields(field.fields, source, target));
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
const sourceValue = source[field.name];
|
|
51
|
-
const targetValue = target[field.name];
|
|
52
|
-
// Skip undefined source values
|
|
53
|
-
if (sourceValue === undefined) continue;
|
|
54
|
-
// Group - recursively reconcile
|
|
55
|
-
if (fieldIsGroupType(field) && isObject(sourceValue)) {
|
|
56
|
-
const targetGroup = isObject(targetValue) ? targetValue : {};
|
|
57
|
-
result[field.name] = this.reconcileFields(field.fields, sourceValue, targetGroup);
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
// Array - recursively reconcile each item (without id - Postgres rejects it)
|
|
61
|
-
if (fieldIsArrayType(field) && Array.isArray(sourceValue)) {
|
|
62
|
-
const targetArray = Array.isArray(targetValue) ? targetValue : [];
|
|
63
|
-
result[field.name] = sourceValue.map((sourceItem, index)=>{
|
|
64
|
-
if (isObject(sourceItem)) {
|
|
65
|
-
const targetItem = isObject(targetArray[index]) ? targetArray[index] : {};
|
|
66
|
-
return this.reconcileFields(field.fields, sourceItem, targetItem);
|
|
67
|
-
}
|
|
68
|
-
return sourceItem;
|
|
69
|
-
});
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
// Blocks - recursively reconcile each block (without id - Postgres rejects it)
|
|
73
|
-
if (fieldIsBlockType(field) && Array.isArray(sourceValue)) {
|
|
74
|
-
const targetArray = Array.isArray(targetValue) ? targetValue : [];
|
|
75
|
-
result[field.name] = sourceValue.map((sourceItem, index)=>{
|
|
76
|
-
if (isBlockItem(sourceItem)) {
|
|
77
|
-
const block = field.blocks.find((b)=>b.slug === sourceItem.blockType);
|
|
78
|
-
if (block) {
|
|
79
|
-
const targetItem = isObject(targetArray[index]) ? targetArray[index] : {};
|
|
80
|
-
return {
|
|
81
|
-
...this.reconcileFields(block.fields, sourceItem, targetItem),
|
|
82
|
-
blockType: sourceItem.blockType
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
return sourceItem;
|
|
87
|
-
});
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
// Deep merge: target priority, fallback to source
|
|
91
|
-
result[field.name] = isEmpty(targetValue) ? sourceValue : targetValue;
|
|
92
|
-
}
|
|
93
|
-
return result;
|
|
82
|
+
const root = {
|
|
83
|
+
source: sourceData,
|
|
84
|
+
target: targetData ?? {}
|
|
85
|
+
};
|
|
86
|
+
return walkFields(this.schema, root, reconcileWalker) ?? {};
|
|
94
87
|
}
|
|
95
88
|
}
|
|
96
89
|
|
package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
1
|
-
import type { Field } from
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
1
|
+
import type { Field } from "payload";
|
|
2
|
+
import type { TranslationStrategy } from "../../strategies";
|
|
3
|
+
import type { FieldChunk } from "../../types";
|
|
4
4
|
/**
|
|
5
|
-
* Collects FieldChunks by
|
|
6
|
-
*
|
|
5
|
+
* Collects FieldChunks by walking schema + data with the shared {@link walkFields} engine.
|
|
6
|
+
* Iteration is driven by `filteredData`; each translatable, localized, non-excluded leaf that
|
|
7
|
+
* the strategy approves is written with its source value and pushed as a chunk carrying a
|
|
8
|
+
* mutable reference to its parent object (for later write-back). Collect-only — `combine` is a
|
|
9
|
+
* no-op; results accumulate in a closure.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - isLocalizedField
|
|
11
|
-
* - strategy.shouldTranslate(sourceValue, targetValue)
|
|
12
|
-
* - !isFieldExcludedFromTranslation
|
|
13
|
-
*
|
|
14
|
-
* IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization).
|
|
15
|
-
* Original schemas preserve `localized: true` on nested fields.
|
|
11
|
+
* IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization). Original
|
|
12
|
+
* schemas preserve `localized: true` on nested fields.
|
|
16
13
|
*/
|
|
17
14
|
export declare class FieldChunkCollector {
|
|
18
15
|
private readonly schema;
|
|
@@ -20,15 +17,7 @@ export declare class FieldChunkCollector {
|
|
|
20
17
|
private readonly sourceData;
|
|
21
18
|
private readonly targetData;
|
|
22
19
|
private readonly strategy;
|
|
23
|
-
private chunks;
|
|
24
20
|
constructor(schema: Field[], filteredData: Record<string, unknown>, sourceData: Record<string, unknown>, targetData: Record<string, unknown>, strategy: TranslationStrategy);
|
|
25
|
-
/**
|
|
26
|
-
* Collects translatable field chunks that need translation.
|
|
27
|
-
*/
|
|
21
|
+
/** Collects translatable field chunks that need translation. */
|
|
28
22
|
collect(): FieldChunk[];
|
|
29
|
-
/**
|
|
30
|
-
* Traverses fields recursively, collecting translatable fields that need translation.
|
|
31
|
-
* Uses strategy.shouldTranslate() to determine if field needs translation.
|
|
32
|
-
*/
|
|
33
|
-
private traverseFields;
|
|
34
23
|
}
|