@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.
Files changed (27) hide show
  1. package/dist/client/shared/ui/Button/Button.d.ts +4 -4
  2. package/dist/client/shared/ui/Button/Button.js +18 -18
  3. package/dist/client/shared/ui/Button/styles.module.scss +1 -1
  4. package/dist/client/shared/ui/Popup/Popup.d.ts +10 -4
  5. package/dist/client/shared/ui/Popup/Popup.js +4 -3
  6. package/dist/client/shared/ui/Select/Select.d.ts +9 -7
  7. package/dist/client/shared/ui/Select/Select.js +18 -17
  8. package/dist/client/shared/ui/Select/styles.module.scss +5 -0
  9. package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +31 -0
  10. package/dist/server/features/translate-field/resolveFieldSubtree.js +45 -0
  11. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.d.ts +3 -9
  12. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.js +71 -78
  13. package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts +11 -22
  14. package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js +85 -106
  15. package/dist/server/shared/field-traversal/findFieldByPath.d.ts +40 -0
  16. package/dist/server/shared/field-traversal/findFieldByPath.js +88 -0
  17. package/dist/server/shared/field-traversal/index.d.ts +5 -0
  18. package/dist/server/shared/field-traversal/index.js +5 -0
  19. package/dist/server/shared/field-traversal/kernel.d.ts +85 -0
  20. package/dist/server/shared/field-traversal/kernel.js +145 -0
  21. package/dist/server/shared/field-traversal/types.d.ts +191 -0
  22. package/dist/server/shared/field-traversal/types.js +41 -0
  23. package/dist/server/shared/field-traversal/walkFields.d.ts +51 -0
  24. package/dist/server/shared/field-traversal/walkFields.js +164 -0
  25. package/dist/server/shared/utils/filterLocalizedFields.d.ts +3 -3
  26. package/dist/server/shared/utils/filterLocalizedFields.js +52 -63
  27. package/package.json +2 -2
@@ -1,9 +1,9 @@
1
- import type { ComponentPropsWithRef, ReactNode } from 'react';
2
- type ButtonProps = ComponentPropsWithRef<'button'> & {
1
+ import type { ComponentPropsWithRef, ReactNode } from "react";
2
+ type ButtonProps = ComponentPropsWithRef<"button"> & {
3
3
  $fullWidth?: boolean;
4
- $variant?: 'outlined' | 'filled' | 'unstyled' | 'transparent' | 'light';
4
+ $variant?: "outlined" | "filled" | "unstyled" | "transparent" | "light";
5
5
  $startContent?: ReactNode;
6
- $size?: 'sm' | 'md' | 'lg';
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 'classnames';
3
- import { forwardRef } from 'react';
4
- import Loading from '../Loading';
5
- import styles from './styles.module.scss';
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: '24px',
9
- paddingInline: '0.25rem'
8
+ height: "24px",
9
+ paddingInline: "0.25rem"
10
10
  },
11
11
  md: {
12
- height: '32px',
13
- paddingInline: '0.5rem'
12
+ height: "32px",
13
+ paddingInline: "0.5rem"
14
14
  },
15
15
  lg: {
16
- height: '40px',
17
- paddingInline: '0.75rem'
16
+ height: "40px",
17
+ paddingInline: "0.75rem"
18
18
  }
19
19
  };
20
- const Button = /*#__PURE__*/ forwardRef(function Button({ className = '', children, $fullWidth, style, $startContent, $variant, $isLoading, $size = 'md', $isIconButton, ...props }, ref) {
20
+ const Button = /*#__PURE__*/ forwardRef(function Button({ className = "", children, $fullWidth, style, $startContent, $variant, $isLoading, $size = "md", $isIconButton, ...props }, ref) {
21
21
  const _style = {
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',
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 === 'unstyled' ? classNames(styles['unstyled'], className, $isIconButton && styles['icon-button']) : classNames(styles['button'], className, $variant ? styles[$variant] : undefined, $isIconButton && styles['icon-button']);
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['start-content'],
36
+ className: styles["start-content"],
37
37
  children: $startContent
38
38
  }),
39
39
  $isLoading ? /*#__PURE__*/ _jsx(Loading, {
40
- size: $isIconButton ? 'small' : 'medium'
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.1sease-in-out, opacity 0.2sease-in-out, border-color 0.2sease-in-out;
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 'react';
1
+ import type { PropsWithChildren, ReactElement } from "react";
2
2
  type PopupProps = PropsWithChildren<{
3
3
  $trigger: ReactElement;
4
- $align?: 'center' | 'end' | 'start';
5
- $side?: 'top' | 'right' | 'bottom' | 'left';
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 '@radix-ui/react-popover';
3
- import style from './styles.module.scss';
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 'react';
2
- type SelectProps = ComponentPropsWithRef<'select'> & {
1
+ import type { ComponentPropsWithoutRef, ComponentPropsWithRef } from "react";
2
+ type SelectProps = ComponentPropsWithRef<"select"> & {
3
3
  emptyOption?: boolean;
4
4
  hasError?: boolean;
5
- $size?: 'sm' | 'md' | 'lg';
6
- 'aria-describedby'?: string;
7
- 'aria-invalid'?: boolean;
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<'option'>;
10
- type SelectEmptyOptionProps = Omit<SelectOptionProps, 'value' | 'disabled' | 'hidden'>;
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 'classnames';
3
- import { forwardRef } from 'react';
4
- import styles from './styles.module.scss';
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: '24px',
8
- paddingInline: '0.25rem',
9
- fontSize: '0.875rem'
7
+ height: "24px",
8
+ paddingInline: "0.25rem",
9
+ fontSize: "0.875rem"
10
10
  },
11
11
  md: {
12
- height: '32px',
13
- paddingInline: '0.5rem',
14
- fontSize: '1rem'
12
+ height: "32px",
13
+ paddingInline: "0.5rem",
14
+ fontSize: "1rem"
15
15
  },
16
16
  lg: {
17
- height: '40px',
18
- paddingInline: '0.75rem',
19
- fontSize: '1rem'
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 = '', hasError = false, $size = 'lg', style, 'aria-invalid': ariaInvalid, ...props }, ref) {
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
- '--height': sizes[$size].height,
34
- '--padding-inline': sizes[$size].paddingInline,
35
- '--font-size': sizes[$size].fontSize,
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,
@@ -31,6 +31,11 @@
31
31
  }
32
32
  }
33
33
 
34
+ /* Opt out of full width (`$fullWidth={false}`) — size to content, e.g. inline in a row. */
35
+ .auto {
36
+ width: auto;
37
+ }
38
+
34
39
  /* Size-specific styles if needed */
35
40
  .size-sm {
36
41
  background-size: 12px 12px;
@@ -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 'payload';
1
+ import type { Field } from "payload";
2
2
  /**
3
- * Deep merges source and target data with target priority.
4
- * Creates full document shape needed for Payload validation.
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 { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName } from 'payload/shared';
2
- import { hasFields, isBlockItem, isTabsField, isEmpty, isObject } from '../../../../shared';
1
+ import { isEmpty, isObject } from "../../../../shared";
2
+ import { resolveBlockFields, walkFields } from "../../../../shared/field-traversal";
3
+ const asObject = (value)=>isObject(value) ? value : {};
3
4
  /**
4
- * Deep merges source and target data with target priority.
5
- * Creates full document shape needed for Payload validation.
6
- *
7
- * Logic:
8
- * - If targetValue exists and is not empty → use targetValue
9
- * - Otherwise use sourceValue
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
- return this.reconcileFields(this.schema, sourceData, targetData ?? {});
26
- }
27
- reconcileFields(fields, source, target) {
28
- const result = {};
29
- for (const field of fields){
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
 
@@ -1,18 +1,15 @@
1
- import type { Field } from 'payload';
2
- import type { FieldChunk } from '../../types';
3
- import type { TranslationStrategy } from '../../strategies';
1
+ import type { Field } from "payload";
2
+ import type { TranslationStrategy } from "../../strategies";
3
+ import type { FieldChunk } from "../../types";
4
4
  /**
5
- * Collects FieldChunks by traversing schema and data in parallel.
6
- * Each chunk contains a reference to the parent data object for later mutation.
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
- * Applies all filtering logic:
9
- * - isTranslatableField
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
  }