@focus-reactive/payload-plugin-translator 0.5.0 → 0.6.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.
Files changed (76) hide show
  1. package/README.md +46 -8
  2. package/dist/client/entities/translation/api/mutations/useTranslateField.d.ts +22 -0
  3. package/dist/client/entities/translation/api/mutations/useTranslateField.js +31 -0
  4. package/dist/client/features/collection-translation-form/ui/CollectionTranslationForm.d.ts +3 -3
  5. package/dist/client/features/collection-translation-form/ui/CollectionTranslationForm.js +15 -13
  6. package/dist/client/features/collection-translation-popup/ui/CollectionTranslationPopup.d.ts +2 -2
  7. package/dist/client/features/collection-translation-popup/ui/CollectionTranslationPopup.js +10 -10
  8. package/dist/client/features/open-document-translation-popup/ui/OpenDocumentTranslationPopup.d.ts +1 -1
  9. package/dist/client/features/open-document-translation-popup/ui/OpenDocumentTranslationPopup.js +9 -9
  10. package/dist/client/features/translate-document-form/ui/DocumentTranslationForm.d.ts +2 -2
  11. package/dist/client/features/translate-document-form/ui/DocumentTranslationForm.js +10 -8
  12. package/dist/client/shared/lib/assets/icons/SendIcon.d.ts +1 -0
  13. package/dist/client/shared/lib/assets/icons/SendIcon.js +17 -0
  14. package/dist/client/shared/ui/Button/Button.d.ts +5 -4
  15. package/dist/client/shared/ui/Button/Button.js +19 -19
  16. package/dist/client/shared/ui/Button/styles.module.scss +27 -4
  17. package/dist/client/shared/ui/Popup/Popup.d.ts +10 -4
  18. package/dist/client/shared/ui/Popup/Popup.js +4 -3
  19. package/dist/client/shared/ui/Select/Select.d.ts +9 -7
  20. package/dist/client/shared/ui/Select/Select.js +18 -17
  21. package/dist/client/shared/ui/Select/styles.module.scss +5 -0
  22. package/dist/client/widgets/translate-field-control/index.d.ts +1 -0
  23. package/dist/client/widgets/translate-field-control/index.js +3 -0
  24. package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.d.ts +26 -0
  25. package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.export.d.ts +13 -0
  26. package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.export.js +14 -0
  27. package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.js +204 -0
  28. package/dist/client/widgets/translate-field-control/ui/styles.module.scss +48 -0
  29. package/dist/field-actions.d.ts +25 -0
  30. package/dist/field-actions.js +23 -0
  31. package/dist/field-config.d.ts +26 -26
  32. package/dist/field-config.js +14 -37
  33. package/dist/index.d.ts +1 -1
  34. package/dist/index.js +1 -1
  35. package/dist/plugin.d.ts +1 -1
  36. package/dist/plugin.js +3 -1
  37. package/dist/server/features/translate-field/handler.d.ts +17 -0
  38. package/dist/server/features/translate-field/handler.js +85 -0
  39. package/dist/server/features/translate-field/index.d.ts +6 -0
  40. package/dist/server/features/translate-field/index.js +5 -0
  41. package/dist/server/features/translate-field/model.d.ts +62 -0
  42. package/dist/server/features/translate-field/model.js +26 -0
  43. package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +39 -0
  44. package/dist/server/features/translate-field/resolveFieldSubtree.js +51 -0
  45. package/dist/server/features/translate-field/route.d.ts +12 -0
  46. package/dist/server/features/translate-field/route.js +18 -0
  47. package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +6 -0
  48. package/dist/server/modules/translation-levels/PluginConfigBuilder.js +4 -0
  49. package/dist/server/modules/translation-levels/fieldLevel.d.ts +23 -0
  50. package/dist/server/modules/translation-levels/fieldLevel.js +37 -0
  51. package/dist/server/modules/translation-levels/index.d.ts +1 -0
  52. package/dist/server/modules/translation-levels/index.js +1 -0
  53. package/dist/server/modules/translation-levels/types.d.ts +6 -1
  54. package/dist/server/modules/translation-levels/types.js +0 -1
  55. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.d.ts +3 -9
  56. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.js +76 -77
  57. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.stage.d.ts +1 -1
  58. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.stage.js +1 -1
  59. package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts +15 -21
  60. package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js +93 -105
  61. package/dist/server/modules/translation-providers/OpenAITranslation.provider.d.ts +17 -0
  62. package/dist/server/modules/translation-providers/OpenAITranslation.provider.js +8 -2
  63. package/dist/server/shared/field-config/types.js +1 -1
  64. package/dist/server/shared/field-traversal/findFieldByPath.d.ts +55 -0
  65. package/dist/server/shared/field-traversal/findFieldByPath.js +133 -0
  66. package/dist/server/shared/field-traversal/index.d.ts +5 -0
  67. package/dist/server/shared/field-traversal/index.js +5 -0
  68. package/dist/server/shared/field-traversal/kernel.d.ts +107 -0
  69. package/dist/server/shared/field-traversal/kernel.js +172 -0
  70. package/dist/server/shared/field-traversal/types.d.ts +191 -0
  71. package/dist/server/shared/field-traversal/types.js +41 -0
  72. package/dist/server/shared/field-traversal/walkFields.d.ts +51 -0
  73. package/dist/server/shared/field-traversal/walkFields.js +164 -0
  74. package/dist/server/shared/utils/filterLocalizedFields.d.ts +3 -3
  75. package/dist/server/shared/utils/filterLocalizedFields.js +52 -63
  76. package/package.json +3 -3
@@ -0,0 +1,133 @@
1
+ import { classifyField, resolveBlockFields, tabScopes } from "./kernel";
2
+ const isIndexSegment = (segment)=>/^\d+$/u.test(segment);
3
+ /** Safely read `key` off an object/array; `undefined` for non-objects (arrays index by string key). */ const childData = (data, key)=>data != null && typeof data === "object" ? data[key] : undefined;
4
+ /**
5
+ * Navigate a field schema by a path of segment NAMES, descending one matching branch at a time
6
+ * with early-exit (targeted navigation, not an exhaustive walk). Presentational containers
7
+ * (`row`/`collapsible`/unnamed `group`) and unnamed tabs are transparent — searched in the same
8
+ * path scope. Built on {@link classifyField} / {@link tabScopes} / {@link resolveBlockFields} so
9
+ * the structural dispatch lives in one place.
10
+ *
11
+ * **Array indices and `blocks`.** Array element configs are shared, so a numeric segment after an
12
+ * array name selects the data item but not the schema (the schema continues into `array.fields`).
13
+ * A `blocks` field is polymorphic — which block (and thus which fields) sits at an index lives in
14
+ * the *data*, not the schema. So to descend through `blocks` the caller must pass the document
15
+ * `data` and keep the element index in the path: this function reads `data[name][index].blockType`
16
+ * and resolves the matching block via {@link resolveBlockFields}. Without `data` (or with a
17
+ * non-indexed path), descending through `blocks` returns `inside-blocks`.
18
+ *
19
+ * @param fields - The schema level to search.
20
+ * @param segments - Remaining path segments to match, head-first. Keep array/block element indices
21
+ * in the path when navigating with `data`.
22
+ * @param data - The document (sub)tree aligned with `fields`, used only to disambiguate `blocks`.
23
+ * Omit for schema-only navigation.
24
+ * @returns A {@link FieldPathResult}.
25
+ * @public
26
+ */ export function findFieldByPath(fields, segments, data) {
27
+ const [head, ...rest] = segments;
28
+ if (head === undefined) return {
29
+ status: "not-found"
30
+ };
31
+ for (const field of fields){
32
+ const structure = classifyField(field);
33
+ switch(structure.kind){
34
+ case "tabs":
35
+ {
36
+ for (const scope of tabScopes(structure.field)){
37
+ if (scope.named) {
38
+ if (scope.tab.name === head) {
39
+ return rest.length === 0 ? {
40
+ status: "container"
41
+ } : findFieldByPath(scope.tab.fields, rest, childData(data, head));
42
+ }
43
+ } else {
44
+ const found = findFieldByPath(scope.fields, segments, data); // unnamed tab → same path + data scope
45
+ if (found.status !== "not-found") return found;
46
+ }
47
+ }
48
+ break;
49
+ }
50
+ case "transparent":
51
+ {
52
+ const found = findFieldByPath(structure.fields, segments, data); // row/collapsible/unnamed group → same scope
53
+ if (found.status !== "not-found") return found;
54
+ break;
55
+ }
56
+ case "presentational":
57
+ break;
58
+ case "group":
59
+ {
60
+ if (structure.name !== head) break;
61
+ return rest.length === 0 ? {
62
+ status: "container"
63
+ } : findFieldByPath(structure.fields, rest, childData(data, head));
64
+ }
65
+ case "array":
66
+ {
67
+ if (structure.name !== head) break;
68
+ if (rest.length === 0) return {
69
+ status: "container"
70
+ };
71
+ // A localized array is an independent per-locale structure — its element index is not a
72
+ // stable identity across locales, so refuse to navigate THROUGH it by path.
73
+ if (structure.field.localized) return {
74
+ status: "localized-list-ancestor"
75
+ };
76
+ // A numeric segment selects the data element; the schema is shared across elements.
77
+ const [next, ...tail] = rest;
78
+ if (isIndexSegment(next)) {
79
+ return tail.length === 0 ? {
80
+ status: "container"
81
+ } : findFieldByPath(structure.fields, tail, childData(childData(data, head), next));
82
+ }
83
+ return findFieldByPath(structure.fields, rest, childData(data, head));
84
+ }
85
+ case "blocks":
86
+ {
87
+ if (structure.name !== head) break;
88
+ if (rest.length === 0) return {
89
+ status: "container"
90
+ };
91
+ // A localized blocks field is an independent per-locale structure — its element index is
92
+ // not a stable identity across locales, so refuse to navigate THROUGH it by path.
93
+ if (structure.field.localized) return {
94
+ status: "localized-list-ancestor"
95
+ };
96
+ // Polymorphic: need the element index + its data to pick the block schema by `blockType`.
97
+ const [next, ...tail] = rest;
98
+ if (!isIndexSegment(next)) return {
99
+ status: "inside-blocks"
100
+ };
101
+ const item = childData(childData(data, head), next);
102
+ const blockFields = resolveBlockFields(structure.field, item);
103
+ if (!blockFields) return {
104
+ status: "inside-blocks"
105
+ };
106
+ return tail.length === 0 ? {
107
+ status: "container"
108
+ } : findFieldByPath(blockFields, tail, item);
109
+ }
110
+ case "leaf":
111
+ {
112
+ if (structure.name !== head) break;
113
+ return rest.length === 0 ? {
114
+ status: "leaf",
115
+ field: structure.field
116
+ } : {
117
+ status: "not-found"
118
+ };
119
+ }
120
+ default:
121
+ {
122
+ // Exhaustiveness guard: a new FieldStructure kind would error here at compile time.
123
+ const exhaustive = structure;
124
+ throw new Error(`unhandled field structure: ${String(exhaustive)}`);
125
+ }
126
+ }
127
+ }
128
+ return {
129
+ status: "not-found"
130
+ };
131
+ }
132
+
133
+ //# sourceMappingURL=findFieldByPath.js.map
@@ -0,0 +1,5 @@
1
+ export { findFieldByPath } from "./findFieldByPath";
2
+ export type { FieldPathResult } from "./findFieldByPath";
3
+ export { classifyField, matchElementById, resolveBlockFields, tabScopes } from "./kernel";
4
+ export type { ChildCursor, ChildOutput, ContainerInfo, FieldStructure, FieldWalker, LeafField, TabScope, WalkSignal } from "./types";
5
+ export { walkFields } from "./walkFields";
@@ -0,0 +1,5 @@
1
+ export { findFieldByPath } from "./findFieldByPath";
2
+ export { classifyField, matchElementById, resolveBlockFields, tabScopes } from "./kernel";
3
+ export { walkFields } from "./walkFields";
4
+
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,107 @@
1
+ import type { BlocksField, Field, TabsField } from "payload";
2
+ import type { FieldStructure, TabScope } from "./types";
3
+ /**
4
+ * Classify a single Payload {@link Field} into a discriminated {@link FieldStructure} — the
5
+ * one place that encodes how Payload field types map onto data boundaries.
6
+ *
7
+ * Traversals switch on the result instead of re-deriving the predicate chain
8
+ * (`fieldAffectsData` / `fieldIsGroupType` / …), so the dispatch — and any future Payload
9
+ * field type — lives in exactly one place.
10
+ *
11
+ * @param field - Any field from a collection/global `fields` array (original, un-sanitized
12
+ * config).
13
+ * @returns The field's {@link FieldStructure}:
14
+ * - `tabs` — a `tabs` field (expand with {@link tabScopes}).
15
+ * - `transparent` — a presentational container that opens **no** data boundary (`row`,
16
+ * `collapsible`, unnamed `group`); its `fields` live in the parent's data scope.
17
+ * - `presentational` — a `ui` field: no data, no subfields.
18
+ * - `group` / `array` / `blocks` — named data boundaries (carry `name` + child `fields`,
19
+ * except `blocks` whose child fields are per-element via {@link resolveBlockFields}).
20
+ * - `leaf` — a data-affecting field with no subfields.
21
+ *
22
+ * Dispatch order is significant: some fields satisfy more than one predicate and only the
23
+ * order disambiguates them.
24
+ * - `tabs` is checked first — it carries `.tabs` (not `.fields`) and is non-data-affecting,
25
+ * so a later check would mislabel it `presentational`.
26
+ * - `fieldAffectsData` is resolved before the `group` check, so an unnamed group resolves to
27
+ * `transparent`, never a `group` with `name: undefined`.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * const structure = classifyField(field);
32
+ * switch (structure.kind) {
33
+ * case "group": descend(structure.fields, data[structure.name]); break;
34
+ * case "leaf": translate(structure.field, structure.name); break;
35
+ * // ...handle the remaining kinds
36
+ * }
37
+ * ```
38
+ *
39
+ * @see {@link walkFields} — the recursive engine built on this classification.
40
+ * @public
41
+ */
42
+ export declare function classifyField(field: Field): FieldStructure;
43
+ /**
44
+ * Flatten a `tabs` field's tabs into {@link TabScope}s, absorbing the named/unnamed +
45
+ * `hasFields` handling that field traversals would otherwise each repeat.
46
+ *
47
+ * @param field - A Payload `tabs` field.
48
+ * @returns One {@link TabScope} per tab that has fields, in declaration order:
49
+ * - a **named** tab → `{ named: true, tab }` — it opens a data boundary at `tab.name`, and
50
+ * the `NamedTab` is surfaced so callers can read its `name`/`fields`/config;
51
+ * - an **unnamed** tab → `{ named: false, fields }` — its fields live in the parent's data
52
+ * scope, so only the `fields` are needed.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * for (const scope of tabScopes(tabsField)) {
57
+ * if (scope.named) descendInto(data[scope.tab.name], scope.tab.fields);
58
+ * else descendInto(data, scope.fields); // unnamed tab — same data scope as the parent
59
+ * }
60
+ * ```
61
+ * @public
62
+ */
63
+ export declare function tabScopes(field: TabsField): TabScope[];
64
+ /**
65
+ * Resolve the child `fields` for a single `blocks` element by matching its `blockType`
66
+ * against the field's block definitions.
67
+ *
68
+ * @param field - A Payload `blocks` field carrying inline block definitions.
69
+ * @param item - The data for one block element (expected to carry a `blockType` string).
70
+ * @returns The matched block's `fields`, or `null` when `item` is not a block object or its
71
+ * `blockType` matches no defined block.
72
+ *
73
+ * Assumes the **original, un-sanitized** schema, where blocks are inline `Block` objects on
74
+ * `field.blocks`. Payload's sanitized config may instead carry `blockReferences` resolved
75
+ * against the top-level config — not handled here by design, since this package traverses
76
+ * the original schema to preserve nested `localized` flags.
77
+ *
78
+ * @example
79
+ * ```ts
80
+ * const fields = resolveBlockFields(blocksField, item);
81
+ * if (fields) descendInto(item, fields); // null → unknown blockType / not a block → skip
82
+ * ```
83
+ * @public
84
+ */
85
+ export declare function resolveBlockFields(field: BlocksField, item: unknown): Field[] | null;
86
+ /**
87
+ * Find the element of a parallel data array that corresponds to `refItem` by `id` — not by
88
+ * position.
89
+ *
90
+ * Localized `blocks`/`array` fields are stored independently per locale, so the locales can be
91
+ * reordered or differ entirely; pairing two parallel trees by position then cross-contaminates
92
+ * unrelated elements ("blocks don't line up across locales"). Matching by `id` is correct in both
93
+ * regimes: a non-localized field is one shared row with the same `id` across locales (so id-match
94
+ * equals the old positional match), while independent localized content has diverging ids (no
95
+ * match → the caller falls back to `refItem`'s own values, never another element's). For blocks the
96
+ * `blockType` must also match, so an id collision across types can't graft mismatched fields.
97
+ *
98
+ * Shared by the translation pipeline's two multi-tree walkers — reconcile and collect — to pair
99
+ * their `target` array against the source/reference element currently being visited.
100
+ *
101
+ * @param arr - The array to search (e.g. the target-locale elements).
102
+ * @param refItem - The element being matched, carrying the canonical `id` (and `blockType`).
103
+ * @param isBlocks - Whether the field is a `blocks` field (then `blockType` must also match).
104
+ * @returns The matched element, or `{}` when there is no counterpart (or `refItem` carries no `id`).
105
+ * @public
106
+ */
107
+ export declare function matchElementById(arr: unknown[], refItem: Record<string, unknown>, isBlocks: boolean): Record<string, unknown>;
@@ -0,0 +1,172 @@
1
+ import { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName } from "payload/shared";
2
+ import { hasFields, isBlockItem, isTabsField } from "../guards";
3
+ import { isObject } from "../utils";
4
+ /**
5
+ * Classify a single Payload {@link Field} into a discriminated {@link FieldStructure} — the
6
+ * one place that encodes how Payload field types map onto data boundaries.
7
+ *
8
+ * Traversals switch on the result instead of re-deriving the predicate chain
9
+ * (`fieldAffectsData` / `fieldIsGroupType` / …), so the dispatch — and any future Payload
10
+ * field type — lives in exactly one place.
11
+ *
12
+ * @param field - Any field from a collection/global `fields` array (original, un-sanitized
13
+ * config).
14
+ * @returns The field's {@link FieldStructure}:
15
+ * - `tabs` — a `tabs` field (expand with {@link tabScopes}).
16
+ * - `transparent` — a presentational container that opens **no** data boundary (`row`,
17
+ * `collapsible`, unnamed `group`); its `fields` live in the parent's data scope.
18
+ * - `presentational` — a `ui` field: no data, no subfields.
19
+ * - `group` / `array` / `blocks` — named data boundaries (carry `name` + child `fields`,
20
+ * except `blocks` whose child fields are per-element via {@link resolveBlockFields}).
21
+ * - `leaf` — a data-affecting field with no subfields.
22
+ *
23
+ * Dispatch order is significant: some fields satisfy more than one predicate and only the
24
+ * order disambiguates them.
25
+ * - `tabs` is checked first — it carries `.tabs` (not `.fields`) and is non-data-affecting,
26
+ * so a later check would mislabel it `presentational`.
27
+ * - `fieldAffectsData` is resolved before the `group` check, so an unnamed group resolves to
28
+ * `transparent`, never a `group` with `name: undefined`.
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const structure = classifyField(field);
33
+ * switch (structure.kind) {
34
+ * case "group": descend(structure.fields, data[structure.name]); break;
35
+ * case "leaf": translate(structure.field, structure.name); break;
36
+ * // ...handle the remaining kinds
37
+ * }
38
+ * ```
39
+ *
40
+ * @see {@link walkFields} — the recursive engine built on this classification.
41
+ * @public
42
+ */ export function classifyField(field) {
43
+ if (isTabsField(field)) {
44
+ return {
45
+ kind: "tabs",
46
+ field
47
+ };
48
+ }
49
+ if (!fieldAffectsData(field)) {
50
+ return hasFields(field) ? {
51
+ kind: "transparent",
52
+ fields: field.fields
53
+ } : {
54
+ kind: "presentational"
55
+ };
56
+ }
57
+ if (fieldIsGroupType(field)) {
58
+ return {
59
+ kind: "group",
60
+ name: field.name,
61
+ fields: field.fields,
62
+ field
63
+ };
64
+ }
65
+ if (fieldIsArrayType(field)) {
66
+ return {
67
+ kind: "array",
68
+ name: field.name,
69
+ fields: field.fields,
70
+ field
71
+ };
72
+ }
73
+ if (fieldIsBlockType(field)) {
74
+ return {
75
+ kind: "blocks",
76
+ name: field.name,
77
+ field
78
+ };
79
+ }
80
+ const leaf = field;
81
+ return {
82
+ kind: "leaf",
83
+ name: leaf.name,
84
+ field: leaf
85
+ };
86
+ }
87
+ /**
88
+ * Flatten a `tabs` field's tabs into {@link TabScope}s, absorbing the named/unnamed +
89
+ * `hasFields` handling that field traversals would otherwise each repeat.
90
+ *
91
+ * @param field - A Payload `tabs` field.
92
+ * @returns One {@link TabScope} per tab that has fields, in declaration order:
93
+ * - a **named** tab → `{ named: true, tab }` — it opens a data boundary at `tab.name`, and
94
+ * the `NamedTab` is surfaced so callers can read its `name`/`fields`/config;
95
+ * - an **unnamed** tab → `{ named: false, fields }` — its fields live in the parent's data
96
+ * scope, so only the `fields` are needed.
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * for (const scope of tabScopes(tabsField)) {
101
+ * if (scope.named) descendInto(data[scope.tab.name], scope.tab.fields);
102
+ * else descendInto(data, scope.fields); // unnamed tab — same data scope as the parent
103
+ * }
104
+ * ```
105
+ * @public
106
+ */ export function tabScopes(field) {
107
+ const scopes = [];
108
+ for (const tab of field.tabs){
109
+ if (tabHasName(tab)) scopes.push({
110
+ named: true,
111
+ tab
112
+ });
113
+ else if (hasFields(tab)) scopes.push({
114
+ named: false,
115
+ fields: tab.fields
116
+ });
117
+ }
118
+ return scopes;
119
+ }
120
+ /**
121
+ * Resolve the child `fields` for a single `blocks` element by matching its `blockType`
122
+ * against the field's block definitions.
123
+ *
124
+ * @param field - A Payload `blocks` field carrying inline block definitions.
125
+ * @param item - The data for one block element (expected to carry a `blockType` string).
126
+ * @returns The matched block's `fields`, or `null` when `item` is not a block object or its
127
+ * `blockType` matches no defined block.
128
+ *
129
+ * Assumes the **original, un-sanitized** schema, where blocks are inline `Block` objects on
130
+ * `field.blocks`. Payload's sanitized config may instead carry `blockReferences` resolved
131
+ * against the top-level config — not handled here by design, since this package traverses
132
+ * the original schema to preserve nested `localized` flags.
133
+ *
134
+ * @example
135
+ * ```ts
136
+ * const fields = resolveBlockFields(blocksField, item);
137
+ * if (fields) descendInto(item, fields); // null → unknown blockType / not a block → skip
138
+ * ```
139
+ * @public
140
+ */ export function resolveBlockFields(field, item) {
141
+ if (!isBlockItem(item)) return null;
142
+ const block = field.blocks.find((candidate)=>candidate.slug === item.blockType);
143
+ return block ? block.fields : null;
144
+ }
145
+ /**
146
+ * Find the element of a parallel data array that corresponds to `refItem` by `id` — not by
147
+ * position.
148
+ *
149
+ * Localized `blocks`/`array` fields are stored independently per locale, so the locales can be
150
+ * reordered or differ entirely; pairing two parallel trees by position then cross-contaminates
151
+ * unrelated elements ("blocks don't line up across locales"). Matching by `id` is correct in both
152
+ * regimes: a non-localized field is one shared row with the same `id` across locales (so id-match
153
+ * equals the old positional match), while independent localized content has diverging ids (no
154
+ * match → the caller falls back to `refItem`'s own values, never another element's). For blocks the
155
+ * `blockType` must also match, so an id collision across types can't graft mismatched fields.
156
+ *
157
+ * Shared by the translation pipeline's two multi-tree walkers — reconcile and collect — to pair
158
+ * their `target` array against the source/reference element currently being visited.
159
+ *
160
+ * @param arr - The array to search (e.g. the target-locale elements).
161
+ * @param refItem - The element being matched, carrying the canonical `id` (and `blockType`).
162
+ * @param isBlocks - Whether the field is a `blocks` field (then `blockType` must also match).
163
+ * @returns The matched element, or `{}` when there is no counterpart (or `refItem` carries no `id`).
164
+ * @public
165
+ */ export function matchElementById(arr, refItem, isBlocks) {
166
+ const id = refItem.id;
167
+ if (id === undefined || id === null) return {};
168
+ const match = arr.find((candidate)=>isObject(candidate) && candidate.id === id && (!isBlocks || candidate.blockType === refItem.blockType));
169
+ return isObject(match) ? match : {};
170
+ }
171
+
172
+ //# sourceMappingURL=kernel.js.map
@@ -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