@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,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 'payload';
1
+ import type { Field } from "payload";
2
2
  /**
3
- * Filters data to keep only translatable localized fields.
4
- * For container fields (group, array, blocks) recursively filters nested data.
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 { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType } from 'payload/shared';
2
- import { hasFields, isBlockItem, isLocalizedField, isTabsField, isTranslatableField } from '../guards';
3
- import { isObject } from './isObject';
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
- * Filters data to keep only translatable localized fields.
7
- * For container fields (group, array, blocks) recursively filters nested data.
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
- const result = {};
14
- for (const field of schema){
15
- if (isTabsField(field)) {
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.0",
3
+ "version": "0.6.0",
4
4
  "description": "Translation plugin for Payload CMS 3.x. Automatically translate your localized content using any translation provider.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -50,12 +50,12 @@
50
50
  "build:types": "tsc --emitDeclarationOnly --declaration --outDir dist",
51
51
  "copyfiles": "copyfiles -u 1 \"src/**/*.scss\" dist",
52
52
  "clean": "rm -rf dist",
53
- "dev": "bun run build:swc -- --watch",
53
+ "dev": "bun run build:swc -- --watch --copy-files",
54
54
  "lint": "ultracite check",
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",