@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
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { ArrayField, BlocksField, Field, FieldAffectingData, NamedGroupField, NamedTab, TabAsField, TabsField } from "payload";
|
|
2
|
+
/**
|
|
3
|
+
* A data-affecting field the engine routes to `leaf`: a scalar/relational leaf, never a
|
|
4
|
+
* container (`group`/`array`/`blocks`) and never a `TabAsField`. Narrowing `FieldAffectingData`
|
|
5
|
+
* down to these members is what guarantees `name: string` — the raw union does NOT, because
|
|
6
|
+
* an unnamed `TabAsField` carries `name?: string`.
|
|
7
|
+
*
|
|
8
|
+
* @public
|
|
9
|
+
*/
|
|
10
|
+
export type LeafField = Exclude<FieldAffectingData, ArrayField | BlocksField | NamedGroupField | TabAsField>;
|
|
11
|
+
/**
|
|
12
|
+
* Structural classification of a single Payload field — the one place that encodes
|
|
13
|
+
* how Payload field types map onto data boundaries (the dispatch order
|
|
14
|
+
* `tabs → transparent → group → array → blocks → leaf`).
|
|
15
|
+
*
|
|
16
|
+
* Consumed by {@link FieldWalker}/`walkFields` (the exhaustive, data-parallel walks)
|
|
17
|
+
* and by path-navigation; the four hand-rolled traversals collapse onto it so the
|
|
18
|
+
* dispatch lives in exactly one place.
|
|
19
|
+
*
|
|
20
|
+
* - `transparent` — presentational container that does NOT open a data boundary
|
|
21
|
+
* (row, collapsible, unnamed group). Its `fields` live in the SAME data scope as
|
|
22
|
+
* the parent.
|
|
23
|
+
* - `presentational` — a `ui` leaf: no data, no subfields.
|
|
24
|
+
* - `group`/`array`/`blocks` — open a data boundary at `name`.
|
|
25
|
+
* - `leaf` — a data-affecting field with no subfields.
|
|
26
|
+
*
|
|
27
|
+
* @see {@link classifyField} — produces this; {@link walkFields} — consumes it.
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export type FieldStructure = {
|
|
31
|
+
kind: "tabs";
|
|
32
|
+
field: TabsField;
|
|
33
|
+
} | {
|
|
34
|
+
kind: "transparent";
|
|
35
|
+
fields: Field[];
|
|
36
|
+
} | {
|
|
37
|
+
kind: "presentational";
|
|
38
|
+
} | {
|
|
39
|
+
kind: "group";
|
|
40
|
+
name: string;
|
|
41
|
+
fields: Field[];
|
|
42
|
+
field: NamedGroupField;
|
|
43
|
+
} | {
|
|
44
|
+
kind: "array";
|
|
45
|
+
name: string;
|
|
46
|
+
fields: Field[];
|
|
47
|
+
field: ArrayField;
|
|
48
|
+
} | {
|
|
49
|
+
kind: "blocks";
|
|
50
|
+
name: string;
|
|
51
|
+
field: BlocksField;
|
|
52
|
+
} | {
|
|
53
|
+
kind: "leaf";
|
|
54
|
+
name: string;
|
|
55
|
+
field: LeafField;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* A tab flattened by `tabScopes`. A named tab opens a data boundary, so its `NamedTab`
|
|
59
|
+
* is surfaced (the walker passes it to `enterObject`, and `tab.name` is the data key).
|
|
60
|
+
* An unnamed tab flattens into the parent data scope, so only its `fields` are needed.
|
|
61
|
+
*
|
|
62
|
+
* @see {@link tabScopes}
|
|
63
|
+
* @public
|
|
64
|
+
*/
|
|
65
|
+
export type TabScope = {
|
|
66
|
+
named: true;
|
|
67
|
+
tab: NamedTab;
|
|
68
|
+
} | {
|
|
69
|
+
named: false;
|
|
70
|
+
fields: Field[];
|
|
71
|
+
};
|
|
72
|
+
/**
|
|
73
|
+
* Control signal a walker callback can return instead of descending.
|
|
74
|
+
* - `skip` — do not descend this branch; continue with siblings.
|
|
75
|
+
* - `stop` — halt the whole walk (used for path-style navigation / early-exit).
|
|
76
|
+
*
|
|
77
|
+
* @public
|
|
78
|
+
*/
|
|
79
|
+
export type WalkSignal = "skip" | "stop";
|
|
80
|
+
/**
|
|
81
|
+
* One child to recurse under an array/blocks boundary. The caller derives these from its own
|
|
82
|
+
* data (it alone knows how many elements there are and their shape) and returns them from
|
|
83
|
+
* {@link FieldWalker.enterList}.
|
|
84
|
+
*
|
|
85
|
+
* @template Cursor - The walker's data-position type (see {@link FieldWalker}).
|
|
86
|
+
* @public
|
|
87
|
+
*/
|
|
88
|
+
export interface ChildCursor<Cursor extends object> {
|
|
89
|
+
cursor: Cursor;
|
|
90
|
+
/** Child fields to walk under this item. For `blocks`, the resolved `block.fields`. */
|
|
91
|
+
fields: Field[];
|
|
92
|
+
/** Index/key this child sits at; surfaced back to `combine`. */
|
|
93
|
+
key: string | number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* The container the engine asks the walker to assemble, bottom-up.
|
|
97
|
+
*
|
|
98
|
+
* - `root` — the top-level field list (`field` is `null`).
|
|
99
|
+
* - `object` — a `group` or a named tab.
|
|
100
|
+
* - `list` — an `array` or `blocks` collection (children are its elements, keyed by index).
|
|
101
|
+
* - `element` — a single element of a list (children are its fields). `field` is the
|
|
102
|
+
* parent list field; distinguish array vs blocks via `field.type`.
|
|
103
|
+
*
|
|
104
|
+
* @see {@link FieldWalker.combine}
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
107
|
+
export type ContainerInfo = {
|
|
108
|
+
kind: "root";
|
|
109
|
+
field: null;
|
|
110
|
+
} | {
|
|
111
|
+
kind: "object";
|
|
112
|
+
field: NamedGroupField | NamedTab;
|
|
113
|
+
key: string;
|
|
114
|
+
} | {
|
|
115
|
+
kind: "list";
|
|
116
|
+
field: ArrayField | BlocksField;
|
|
117
|
+
key: string;
|
|
118
|
+
} | {
|
|
119
|
+
kind: "element";
|
|
120
|
+
field: ArrayField | BlocksField;
|
|
121
|
+
key: string | number;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* A child's assembled output, tagged with the key it sat at within its parent. Passed to
|
|
125
|
+
* {@link FieldWalker.combine} as the `children` list.
|
|
126
|
+
*
|
|
127
|
+
* @template Out - The walker's per-node output type (see {@link FieldWalker}).
|
|
128
|
+
* @public
|
|
129
|
+
*/
|
|
130
|
+
export interface ChildOutput<Out> {
|
|
131
|
+
key: string | number;
|
|
132
|
+
out: Out;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Behaviour a caller plugs into {@link walkFields}. The engine owns structural dispatch and
|
|
136
|
+
* recursion over the **schema**; the caller owns the **data** via the opaque `Cursor`
|
|
137
|
+
* (1..n parallel data trees plus any path live there) and decides what to produce.
|
|
138
|
+
*
|
|
139
|
+
* Shapes this is designed to subsume (one engine, many callers):
|
|
140
|
+
* - build-tree (filter / reconcile) — reconstruct values in `combine`.
|
|
141
|
+
* - collect + mutate (field-chunk collector) — push/mutate in `leaf`, `combine` is a no-op.
|
|
142
|
+
* - navigate + early-exit (path resolver) — `enter*` returns `'skip'`/`'stop'`.
|
|
143
|
+
*
|
|
144
|
+
* @template Cursor - The data position threaded through the walk. The engine never reads it;
|
|
145
|
+
* it only passes it to your callbacks and stores the cursors you return. Constrained to
|
|
146
|
+
* `object` so a cursor can never collide with a {@link WalkSignal} string (`'skip'`/`'stop'`).
|
|
147
|
+
* @template Out - What you produce per leaf and assemble per container. Use `void` for a
|
|
148
|
+
* collect-only walk (accumulate in a closure, return `undefined` from `combine`).
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* A collect-only walker that records the path of every localized leaf:
|
|
152
|
+
* ```ts
|
|
153
|
+
* type Cursor = { data: Record<string, unknown>; path: string[] };
|
|
154
|
+
* const found: string[] = [];
|
|
155
|
+
*
|
|
156
|
+
* const walker: FieldWalker<Cursor, void> = {
|
|
157
|
+
* enterObject: (field, c) =>
|
|
158
|
+
* isRecord(c.data[field.name])
|
|
159
|
+
* ? { data: c.data[field.name], path: [...c.path, field.name] }
|
|
160
|
+
* : "skip",
|
|
161
|
+
* enterList: (field, c) => toItemCursors(field, c),
|
|
162
|
+
* leaf: (field, c) => {
|
|
163
|
+
* if (field.localized) found.push([...c.path, field.name].join("."));
|
|
164
|
+
* return undefined;
|
|
165
|
+
* },
|
|
166
|
+
* combine: () => undefined, // collect-only: nothing to assemble
|
|
167
|
+
* };
|
|
168
|
+
* ```
|
|
169
|
+
*
|
|
170
|
+
* @see {@link walkFields}
|
|
171
|
+
* @public
|
|
172
|
+
*/
|
|
173
|
+
export interface FieldWalker<Cursor extends object, Out> {
|
|
174
|
+
/** Enter a single-object boundary (named `group` or named tab). Return the child cursor, or a signal. */
|
|
175
|
+
enterObject(field: NamedGroupField | NamedTab, cursor: Cursor): Cursor | WalkSignal;
|
|
176
|
+
/** Enter an `array`/`blocks` boundary. Return one {@link ChildCursor} per element, or a signal. */
|
|
177
|
+
enterList(field: ArrayField | BlocksField, cursor: Cursor): ChildCursor<Cursor>[] | WalkSignal;
|
|
178
|
+
/**
|
|
179
|
+
* Visit a data-affecting leaf. `field` is a {@link LeafField} — the engine has already
|
|
180
|
+
* resolved it via the `fieldAffectsData` guard and excluded containers/tabs, so `field.name`
|
|
181
|
+
* is always present and callers never touch the raw `Field` union. Return its output, or
|
|
182
|
+
* `undefined` to drop it.
|
|
183
|
+
*/
|
|
184
|
+
leaf(field: LeafField, cursor: Cursor): Out | undefined;
|
|
185
|
+
/**
|
|
186
|
+
* Assemble a container from its children's outputs (called bottom-up). Build-tree
|
|
187
|
+
* callers reconstruct here; collectors return `undefined`. Return `undefined` to drop
|
|
188
|
+
* the container from its parent (e.g. an empty object after filtering).
|
|
189
|
+
*/
|
|
190
|
+
combine(container: ContainerInfo, children: ChildOutput<Out>[], cursor: Cursor): Out | undefined;
|
|
191
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behaviour a caller plugs into {@link walkFields}. The engine owns structural dispatch and
|
|
3
|
+
* recursion over the **schema**; the caller owns the **data** via the opaque `Cursor`
|
|
4
|
+
* (1..n parallel data trees plus any path live there) and decides what to produce.
|
|
5
|
+
*
|
|
6
|
+
* Shapes this is designed to subsume (one engine, many callers):
|
|
7
|
+
* - build-tree (filter / reconcile) — reconstruct values in `combine`.
|
|
8
|
+
* - collect + mutate (field-chunk collector) — push/mutate in `leaf`, `combine` is a no-op.
|
|
9
|
+
* - navigate + early-exit (path resolver) — `enter*` returns `'skip'`/`'stop'`.
|
|
10
|
+
*
|
|
11
|
+
* @template Cursor - The data position threaded through the walk. The engine never reads it;
|
|
12
|
+
* it only passes it to your callbacks and stores the cursors you return. Constrained to
|
|
13
|
+
* `object` so a cursor can never collide with a {@link WalkSignal} string (`'skip'`/`'stop'`).
|
|
14
|
+
* @template Out - What you produce per leaf and assemble per container. Use `void` for a
|
|
15
|
+
* collect-only walk (accumulate in a closure, return `undefined` from `combine`).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* A collect-only walker that records the path of every localized leaf:
|
|
19
|
+
* ```ts
|
|
20
|
+
* type Cursor = { data: Record<string, unknown>; path: string[] };
|
|
21
|
+
* const found: string[] = [];
|
|
22
|
+
*
|
|
23
|
+
* const walker: FieldWalker<Cursor, void> = {
|
|
24
|
+
* enterObject: (field, c) =>
|
|
25
|
+
* isRecord(c.data[field.name])
|
|
26
|
+
* ? { data: c.data[field.name], path: [...c.path, field.name] }
|
|
27
|
+
* : "skip",
|
|
28
|
+
* enterList: (field, c) => toItemCursors(field, c),
|
|
29
|
+
* leaf: (field, c) => {
|
|
30
|
+
* if (field.localized) found.push([...c.path, field.name].join("."));
|
|
31
|
+
* return undefined;
|
|
32
|
+
* },
|
|
33
|
+
* combine: () => undefined, // collect-only: nothing to assemble
|
|
34
|
+
* };
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @see {@link walkFields}
|
|
38
|
+
* @public
|
|
39
|
+
*/ export { };
|
|
40
|
+
|
|
41
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { Field } from "payload";
|
|
2
|
+
import type { FieldWalker } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Depth-first walk of a Payload field **schema**, driving a caller-supplied
|
|
5
|
+
* {@link FieldWalker}. The single traversal engine this package's data operations build on
|
|
6
|
+
* (filtering, reconciling, collecting, navigating).
|
|
7
|
+
*
|
|
8
|
+
* The engine owns structural dispatch (via {@link classifyField} / {@link tabScopes}) and the
|
|
9
|
+
* recursion; the caller owns the **data** through the opaque `Cursor` and decides what to
|
|
10
|
+
* produce. The engine never reads the cursor — so the caller can thread one, two, or more
|
|
11
|
+
* parallel data trees (plus a path) inside it. Containers are assembled bottom-up via
|
|
12
|
+
* `combine`, after their children.
|
|
13
|
+
*
|
|
14
|
+
* @template Cursor - The caller's data position threaded through the walk (e.g. the current
|
|
15
|
+
* data object, a `{ source, target }` pair, plus a path). Constrained to `object` so a cursor
|
|
16
|
+
* can never collide with a {@link WalkSignal} string (`'skip'`/`'stop'`).
|
|
17
|
+
* @template Out - What the walker produces per node and assembles per container (e.g. a
|
|
18
|
+
* rebuilt data subtree, or `void` for a collect-only walk).
|
|
19
|
+
*
|
|
20
|
+
* @param fields - The schema to walk (a collection/global/group `fields` array).
|
|
21
|
+
* @param root - The initial `Cursor` paired with the top-level `fields`.
|
|
22
|
+
* @param walker - The behavior to drive: `enterObject` / `enterList` derive child cursors (or
|
|
23
|
+
* return a {@link WalkSignal} to `'skip'` a branch or `'stop'` the whole walk), `leaf`
|
|
24
|
+
* produces a value per data-affecting leaf, and `combine` assembles each container from its
|
|
25
|
+
* children.
|
|
26
|
+
* @returns The root `combine` output. `undefined` if the walk was halted by `'stop'` before the
|
|
27
|
+
* root assembled, OR if the root `combine` itself returned `undefined` (e.g. a collect-only
|
|
28
|
+
* walker). To distinguish "stopped" from "empty result", track it via the cursor, not this value.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* Keep only localized leaves and rebuild the data tree (a "filter" walk):
|
|
32
|
+
* ```ts
|
|
33
|
+
* type Cursor = { data: Record<string, unknown> };
|
|
34
|
+
*
|
|
35
|
+
* const filtered = walkFields<Cursor, unknown>(schema, { data }, {
|
|
36
|
+
* enterObject: (field, c) =>
|
|
37
|
+
* isRecord(c.data[field.name]) ? { data: c.data[field.name] } : "skip",
|
|
38
|
+
* enterList: (field, c) => toItemCursors(field, c), // one ChildCursor per element
|
|
39
|
+
* leaf: (field, c) => (field.localized ? c.data[field.name] : undefined),
|
|
40
|
+
* combine: (container, children) => {
|
|
41
|
+
* if (children.length === 0) return undefined; // drop empty containers
|
|
42
|
+
* if (container.kind === "list") return children.map((ch) => ch.out);
|
|
43
|
+
* return Object.fromEntries(children.map((ch) => [ch.key, ch.out]));
|
|
44
|
+
* },
|
|
45
|
+
* });
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* @see {@link FieldWalker} for the full visitor contract and the caller shapes it subsumes.
|
|
49
|
+
* @public
|
|
50
|
+
*/
|
|
51
|
+
export declare function walkFields<Cursor extends object, Out>(fields: Field[], root: Cursor, walker: FieldWalker<Cursor, Out>): Out | undefined;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { classifyField, tabScopes } from "./kernel";
|
|
2
|
+
/**
|
|
3
|
+
* Internal engine backing {@link walkFields}. Methods return `true` to mean "stop requested —
|
|
4
|
+
* unwind and halt the entire walk". A class is used (over nested functions) because the three
|
|
5
|
+
* steps are mutually recursive; methods can reference each other without use-before-define
|
|
6
|
+
* ordering.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/ class FieldTreeWalker {
|
|
10
|
+
walker;
|
|
11
|
+
constructor(walker){
|
|
12
|
+
this.walker = walker;
|
|
13
|
+
}
|
|
14
|
+
run(fields, root) {
|
|
15
|
+
const out = [];
|
|
16
|
+
if (this.level(fields, root, out)) return undefined;
|
|
17
|
+
return this.walker.combine({
|
|
18
|
+
kind: "root",
|
|
19
|
+
field: null
|
|
20
|
+
}, out, root);
|
|
21
|
+
}
|
|
22
|
+
/** Walk one data level (root, a group/tab body, or a list element), pushing child outputs into `out`. */ level(fields, cursor, out) {
|
|
23
|
+
for (const field of fields){
|
|
24
|
+
const structure = classifyField(field);
|
|
25
|
+
switch(structure.kind){
|
|
26
|
+
case "presentational":
|
|
27
|
+
break;
|
|
28
|
+
case "transparent":
|
|
29
|
+
if (this.level(structure.fields, cursor, out)) return true;
|
|
30
|
+
break;
|
|
31
|
+
case "tabs":
|
|
32
|
+
for (const scope of tabScopes(structure.field)){
|
|
33
|
+
if (scope.named) {
|
|
34
|
+
if (this.object(scope.tab, scope.tab.name, scope.tab.fields, cursor, out)) return true;
|
|
35
|
+
} else if (this.level(scope.fields, cursor, out)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
break;
|
|
40
|
+
case "group":
|
|
41
|
+
if (this.object(structure.field, structure.name, structure.fields, cursor, out)) return true;
|
|
42
|
+
break;
|
|
43
|
+
case "array":
|
|
44
|
+
case "blocks":
|
|
45
|
+
if (this.list(structure.field, structure.name, cursor, out)) return true;
|
|
46
|
+
break;
|
|
47
|
+
case "leaf":
|
|
48
|
+
{
|
|
49
|
+
const leafOut = this.walker.leaf(structure.field, cursor);
|
|
50
|
+
if (leafOut !== undefined) out.push({
|
|
51
|
+
key: structure.name,
|
|
52
|
+
out: leafOut
|
|
53
|
+
});
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
default:
|
|
57
|
+
{
|
|
58
|
+
// Exhaustiveness guard: if a new FieldStructure kind is added, this errors at compile time.
|
|
59
|
+
const exhaustive = structure;
|
|
60
|
+
throw new Error(`unhandled field structure: ${String(exhaustive)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
/** Descend a single-object boundary (named group or named tab) and assemble it. */ object(field, key, childFields, cursor, out) {
|
|
67
|
+
const result = this.walker.enterObject(field, cursor);
|
|
68
|
+
if (result === "stop") return true;
|
|
69
|
+
if (result === "skip") return false;
|
|
70
|
+
const childOut = [];
|
|
71
|
+
if (this.level(childFields, result, childOut)) return true;
|
|
72
|
+
const assembled = this.walker.combine({
|
|
73
|
+
kind: "object",
|
|
74
|
+
field,
|
|
75
|
+
key
|
|
76
|
+
}, childOut, result);
|
|
77
|
+
if (assembled !== undefined) out.push({
|
|
78
|
+
key,
|
|
79
|
+
out: assembled
|
|
80
|
+
});
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
/** Descend an array/blocks boundary, assembling each element then the list itself. */ list(field, key, cursor, out) {
|
|
84
|
+
const result = this.walker.enterList(field, cursor);
|
|
85
|
+
if (result === "stop") return true;
|
|
86
|
+
if (result === "skip") return false;
|
|
87
|
+
const elements = [];
|
|
88
|
+
for (const child of result){
|
|
89
|
+
const childOut = [];
|
|
90
|
+
if (this.level(child.fields, child.cursor, childOut)) return true;
|
|
91
|
+
const elementOut = this.walker.combine({
|
|
92
|
+
kind: "element",
|
|
93
|
+
field,
|
|
94
|
+
key: child.key
|
|
95
|
+
}, childOut, child.cursor);
|
|
96
|
+
if (elementOut !== undefined) elements.push({
|
|
97
|
+
key: child.key,
|
|
98
|
+
out: elementOut
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const listOut = this.walker.combine({
|
|
102
|
+
kind: "list",
|
|
103
|
+
field,
|
|
104
|
+
key
|
|
105
|
+
}, elements, cursor);
|
|
106
|
+
if (listOut !== undefined) out.push({
|
|
107
|
+
key,
|
|
108
|
+
out: listOut
|
|
109
|
+
});
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Depth-first walk of a Payload field **schema**, driving a caller-supplied
|
|
115
|
+
* {@link FieldWalker}. The single traversal engine this package's data operations build on
|
|
116
|
+
* (filtering, reconciling, collecting, navigating).
|
|
117
|
+
*
|
|
118
|
+
* The engine owns structural dispatch (via {@link classifyField} / {@link tabScopes}) and the
|
|
119
|
+
* recursion; the caller owns the **data** through the opaque `Cursor` and decides what to
|
|
120
|
+
* produce. The engine never reads the cursor — so the caller can thread one, two, or more
|
|
121
|
+
* parallel data trees (plus a path) inside it. Containers are assembled bottom-up via
|
|
122
|
+
* `combine`, after their children.
|
|
123
|
+
*
|
|
124
|
+
* @template Cursor - The caller's data position threaded through the walk (e.g. the current
|
|
125
|
+
* data object, a `{ source, target }` pair, plus a path). Constrained to `object` so a cursor
|
|
126
|
+
* can never collide with a {@link WalkSignal} string (`'skip'`/`'stop'`).
|
|
127
|
+
* @template Out - What the walker produces per node and assembles per container (e.g. a
|
|
128
|
+
* rebuilt data subtree, or `void` for a collect-only walk).
|
|
129
|
+
*
|
|
130
|
+
* @param fields - The schema to walk (a collection/global/group `fields` array).
|
|
131
|
+
* @param root - The initial `Cursor` paired with the top-level `fields`.
|
|
132
|
+
* @param walker - The behavior to drive: `enterObject` / `enterList` derive child cursors (or
|
|
133
|
+
* return a {@link WalkSignal} to `'skip'` a branch or `'stop'` the whole walk), `leaf`
|
|
134
|
+
* produces a value per data-affecting leaf, and `combine` assembles each container from its
|
|
135
|
+
* children.
|
|
136
|
+
* @returns The root `combine` output. `undefined` if the walk was halted by `'stop'` before the
|
|
137
|
+
* root assembled, OR if the root `combine` itself returned `undefined` (e.g. a collect-only
|
|
138
|
+
* walker). To distinguish "stopped" from "empty result", track it via the cursor, not this value.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* Keep only localized leaves and rebuild the data tree (a "filter" walk):
|
|
142
|
+
* ```ts
|
|
143
|
+
* type Cursor = { data: Record<string, unknown> };
|
|
144
|
+
*
|
|
145
|
+
* const filtered = walkFields<Cursor, unknown>(schema, { data }, {
|
|
146
|
+
* enterObject: (field, c) =>
|
|
147
|
+
* isRecord(c.data[field.name]) ? { data: c.data[field.name] } : "skip",
|
|
148
|
+
* enterList: (field, c) => toItemCursors(field, c), // one ChildCursor per element
|
|
149
|
+
* leaf: (field, c) => (field.localized ? c.data[field.name] : undefined),
|
|
150
|
+
* combine: (container, children) => {
|
|
151
|
+
* if (children.length === 0) return undefined; // drop empty containers
|
|
152
|
+
* if (container.kind === "list") return children.map((ch) => ch.out);
|
|
153
|
+
* return Object.fromEntries(children.map((ch) => [ch.key, ch.out]));
|
|
154
|
+
* },
|
|
155
|
+
* });
|
|
156
|
+
* ```
|
|
157
|
+
*
|
|
158
|
+
* @see {@link FieldWalker} for the full visitor contract and the caller shapes it subsumes.
|
|
159
|
+
* @public
|
|
160
|
+
*/ export function walkFields(fields, root, walker) {
|
|
161
|
+
return new FieldTreeWalker(walker).run(fields, root);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
//# sourceMappingURL=walkFields.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Field } from
|
|
1
|
+
import type { Field } from "payload";
|
|
2
2
|
/**
|
|
3
|
-
* Filters data to keep only translatable localized fields
|
|
4
|
-
*
|
|
3
|
+
* Filters data to keep only translatable localized fields, recursively for container fields
|
|
4
|
+
* (group, array, blocks, named tabs). Built on the shared {@link walkFields} engine.
|
|
5
5
|
*
|
|
6
6
|
* @param schema - Payload field schema (original, not sanitized)
|
|
7
7
|
* @param data - Document data to filter
|
|
@@ -1,72 +1,61 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { isObject } from
|
|
4
|
-
import { isEmpty } from './isEmpty';
|
|
1
|
+
import { resolveBlockFields, walkFields } from "../field-traversal";
|
|
2
|
+
import { isLocalizedField, isTranslatableField } from "../guards";
|
|
3
|
+
import { isObject } from "./isObject";
|
|
5
4
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Filter walker: keeps only translatable, localized leaves, rebuilding the surrounding
|
|
6
|
+
* object/array/blocks structure and dropping any container left empty. `id` (array/block
|
|
7
|
+
* elements) and `blockType` (block elements) are preserved since they aren't schema fields.
|
|
8
|
+
*/ const filterWalker = {
|
|
9
|
+
enterObject (field, cursor) {
|
|
10
|
+
const value = cursor.data[field.name];
|
|
11
|
+
return isObject(value) ? {
|
|
12
|
+
data: value
|
|
13
|
+
} : "skip";
|
|
14
|
+
},
|
|
15
|
+
enterList (field, cursor) {
|
|
16
|
+
const value = cursor.data[field.name];
|
|
17
|
+
if (!Array.isArray(value)) return "skip";
|
|
18
|
+
const children = [];
|
|
19
|
+
value.forEach((item, index)=>{
|
|
20
|
+
if (!isObject(item)) return; // drop non-object elements
|
|
21
|
+
const fields = field.type === "blocks" ? resolveBlockFields(field, item) : field.fields;
|
|
22
|
+
if (fields) children.push({
|
|
23
|
+
cursor: {
|
|
24
|
+
data: item
|
|
25
|
+
},
|
|
26
|
+
fields,
|
|
27
|
+
key: index
|
|
28
|
+
}); // unknown blockType → fields null → drop
|
|
29
|
+
});
|
|
30
|
+
return children;
|
|
31
|
+
},
|
|
32
|
+
leaf (field, cursor) {
|
|
33
|
+
return isTranslatableField(field) && isLocalizedField(field) ? cursor.data[field.name] : undefined;
|
|
34
|
+
},
|
|
35
|
+
combine (container, children, cursor) {
|
|
36
|
+
if (children.length === 0) return undefined; // drop empty containers/lists
|
|
37
|
+
if (container.kind === "list") return children.map((child)=>child.out);
|
|
38
|
+
const result = {};
|
|
39
|
+
for (const child of children)result[child.key] = child.out;
|
|
40
|
+
if (container.kind === "element") {
|
|
41
|
+
const item = cursor.data;
|
|
42
|
+
if (item.id !== undefined) result.id = item.id;
|
|
43
|
+
if (container.field.type === "blocks") result.blockType = item.blockType;
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Filters data to keep only translatable localized fields, recursively for container fields
|
|
50
|
+
* (group, array, blocks, named tabs). Built on the shared {@link walkFields} engine.
|
|
8
51
|
*
|
|
9
52
|
* @param schema - Payload field schema (original, not sanitized)
|
|
10
53
|
* @param data - Document data to filter
|
|
11
54
|
* @returns Filtered data containing only translatable localized fields
|
|
12
55
|
*/ export function filterLocalizedFields(schema, data) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
for (const tab of field.tabs){
|
|
17
|
-
if (hasFields(tab)) Object.assign(result, filterLocalizedFields(tab.fields, data));
|
|
18
|
-
}
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
if (!fieldAffectsData(field)) {
|
|
22
|
-
if (hasFields(field)) Object.assign(result, filterLocalizedFields(field.fields, data));
|
|
23
|
-
continue;
|
|
24
|
-
}
|
|
25
|
-
const value = data[field.name];
|
|
26
|
-
if (value === undefined) continue;
|
|
27
|
-
if (fieldIsGroupType(field) && isObject(value)) {
|
|
28
|
-
const filtered = filterLocalizedFields(field.fields, value);
|
|
29
|
-
if (!isEmpty(filtered)) result[field.name] = filtered;
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (fieldIsArrayType(field) && Array.isArray(value)) {
|
|
33
|
-
const filteredArray = value.map((item)=>{
|
|
34
|
-
if (isObject(item)) {
|
|
35
|
-
const filtered = filterLocalizedFields(field.fields, item);
|
|
36
|
-
if (!isEmpty(filtered)) return {
|
|
37
|
-
...filtered,
|
|
38
|
-
id: item.id
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
return null;
|
|
42
|
-
}).filter(Boolean);
|
|
43
|
-
if (!isEmpty(filteredArray)) result[field.name] = filteredArray;
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
if (fieldIsBlockType(field) && Array.isArray(value)) {
|
|
47
|
-
const filteredBlocks = value.map((item)=>{
|
|
48
|
-
if (isBlockItem(item)) {
|
|
49
|
-
const block = field.blocks.find((b)=>b.slug === item.blockType);
|
|
50
|
-
if (block) {
|
|
51
|
-
const filtered = filterLocalizedFields(block.fields, item);
|
|
52
|
-
if (!isEmpty(filtered)) return {
|
|
53
|
-
...filtered,
|
|
54
|
-
blockType: item.blockType,
|
|
55
|
-
id: item.id
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
}).filter(Boolean);
|
|
61
|
-
if (!isEmpty(filteredBlocks)) result[field.name] = filteredBlocks;
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
if (isTranslatableField(field) && isLocalizedField(field)) {
|
|
65
|
-
result[field.name] = value;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return result;
|
|
56
|
+
return walkFields(schema, {
|
|
57
|
+
data
|
|
58
|
+
}, filterWalker) ?? {};
|
|
70
59
|
}
|
|
71
60
|
|
|
72
61
|
//# sourceMappingURL=filterLocalizedFields.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@focus-reactive/payload-plugin-translator",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.1",
|
|
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",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"lint:fix": "ultracite fix",
|
|
56
56
|
"test": "vitest run",
|
|
57
57
|
"test:watch": "vitest",
|
|
58
|
-
"check-types": "tsgo --noEmit"
|
|
58
|
+
"check-types": "tsgo --noEmit -p tsconfig.check.json"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@payloadcms/richtext-lexical": "^3.76.0",
|