@focus-reactive/payload-plugin-translator 0.4.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 (87) hide show
  1. package/README.md +44 -29
  2. package/dist/client/shared/ui/Button/Button.d.ts +4 -4
  3. package/dist/client/shared/ui/Button/Button.js +18 -18
  4. package/dist/client/shared/ui/Button/styles.module.scss +1 -1
  5. package/dist/client/shared/ui/Popup/Popup.d.ts +10 -4
  6. package/dist/client/shared/ui/Popup/Popup.js +4 -3
  7. package/dist/client/shared/ui/Select/Select.d.ts +9 -7
  8. package/dist/client/shared/ui/Select/Select.js +18 -17
  9. package/dist/client/shared/ui/Select/styles.module.scss +5 -0
  10. package/dist/index.d.ts +16 -14
  11. package/dist/index.js +9 -7
  12. package/dist/plugin.d.ts +15 -4
  13. package/dist/plugin.js +34 -57
  14. package/dist/server/features/cancel/handler.d.ts +2 -2
  15. package/dist/server/features/cancel/handler.js +3 -1
  16. package/dist/server/features/cancel/route.d.ts +4 -4
  17. package/dist/server/features/cancel/route.js +4 -4
  18. package/dist/server/features/cancel-by-collection/handler.d.ts +3 -3
  19. package/dist/server/features/cancel-by-collection/handler.js +1 -1
  20. package/dist/server/features/cancel-by-collection/route.d.ts +5 -5
  21. package/dist/server/features/cancel-by-collection/route.js +4 -4
  22. package/dist/server/features/createTranslationRoutes.d.ts +23 -0
  23. package/dist/server/features/createTranslationRoutes.js +27 -0
  24. package/dist/server/features/enqueue-translation/handler.d.ts +3 -3
  25. package/dist/server/features/enqueue-translation/handler.js +1 -1
  26. package/dist/server/features/enqueue-translation/route.d.ts +5 -5
  27. package/dist/server/features/enqueue-translation/route.js +4 -4
  28. package/dist/server/features/get-collection-status/handler.d.ts +3 -3
  29. package/dist/server/features/get-collection-status/handler.js +1 -1
  30. package/dist/server/features/get-collection-status/route.d.ts +5 -5
  31. package/dist/server/features/get-collection-status/route.js +4 -4
  32. package/dist/server/features/get-document-status/handler.d.ts +3 -3
  33. package/dist/server/features/get-document-status/handler.js +1 -1
  34. package/dist/server/features/get-document-status/route.d.ts +5 -5
  35. package/dist/server/features/get-document-status/route.js +4 -4
  36. package/dist/server/features/index.d.ts +7 -6
  37. package/dist/server/features/index.js +7 -6
  38. package/dist/server/features/run-translation/handler.d.ts +2 -2
  39. package/dist/server/features/run-translation/route.d.ts +4 -4
  40. package/dist/server/features/run-translation/route.js +4 -4
  41. package/dist/server/features/translate-document/handler.js +7 -11
  42. package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +31 -0
  43. package/dist/server/features/translate-field/resolveFieldSubtree.js +45 -0
  44. package/dist/server/modules/task-runner/TaskRunnerProvider.interface.d.ts +18 -1
  45. package/dist/server/modules/task-runner/index.d.ts +6 -6
  46. package/dist/server/modules/task-runner/index.js +2 -2
  47. package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.d.ts +16 -1
  48. package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.js +16 -1
  49. package/dist/server/modules/task-runner/sync-runner/SyncRunnerProvider.d.ts +18 -10
  50. package/dist/server/modules/task-runner/sync-runner/SyncRunnerProvider.js +24 -14
  51. package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +46 -0
  52. package/dist/server/modules/translation-levels/PluginConfigBuilder.js +108 -0
  53. package/dist/server/modules/translation-levels/collectionLevel.d.ts +15 -0
  54. package/dist/server/modules/translation-levels/collectionLevel.js +24 -0
  55. package/dist/server/modules/translation-levels/documentLevel.d.ts +19 -0
  56. package/dist/server/modules/translation-levels/documentLevel.js +28 -0
  57. package/dist/server/modules/translation-levels/index.d.ts +3 -0
  58. package/dist/server/modules/translation-levels/index.js +4 -0
  59. package/dist/server/modules/translation-levels/types.d.ts +42 -0
  60. package/dist/server/modules/translation-levels/types.js +10 -0
  61. package/dist/server/modules/translation-levels/useDocTranslationApi.d.ts +8 -0
  62. package/dist/server/modules/translation-levels/useDocTranslationApi.js +18 -0
  63. package/dist/server/modules/translation-pipeline/index.d.ts +5 -3
  64. package/dist/server/modules/translation-pipeline/index.js +3 -2
  65. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.d.ts +3 -9
  66. package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.js +71 -78
  67. package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts +11 -22
  68. package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js +85 -106
  69. package/dist/server/modules/translation-pipeline/translateContent.d.ts +35 -0
  70. package/dist/server/modules/translation-pipeline/translateContent.js +30 -0
  71. package/dist/server/modules/translation-providers/OpenAITranslation.provider.d.ts +4 -3
  72. package/dist/server/modules/translation-providers/OpenAITranslation.provider.js +15 -15
  73. package/dist/server/shared/field-traversal/findFieldByPath.d.ts +40 -0
  74. package/dist/server/shared/field-traversal/findFieldByPath.js +88 -0
  75. package/dist/server/shared/field-traversal/index.d.ts +5 -0
  76. package/dist/server/shared/field-traversal/index.js +5 -0
  77. package/dist/server/shared/field-traversal/kernel.d.ts +85 -0
  78. package/dist/server/shared/field-traversal/kernel.js +145 -0
  79. package/dist/server/shared/field-traversal/types.d.ts +191 -0
  80. package/dist/server/shared/field-traversal/types.js +41 -0
  81. package/dist/server/shared/field-traversal/walkFields.d.ts +51 -0
  82. package/dist/server/shared/field-traversal/walkFields.js +164 -0
  83. package/dist/server/shared/utils/filterLocalizedFields.d.ts +3 -3
  84. package/dist/server/shared/utils/filterLocalizedFields.js +52 -63
  85. package/dist/types/AccessGuard.d.ts +24 -1
  86. package/dist/types/AccessGuard.js +12 -1
  87. package/package.json +2 -2
@@ -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
@@ -1,10 +1,33 @@
1
- import type { BasePayload, TypedUser } from 'payload';
1
+ import type { BasePayload, TypedUser } from "payload";
2
+ /**
3
+ * The request context an {@link AccessGuard} receives when deciding whether to
4
+ * allow a translation API call.
5
+ */
2
6
  export type AccessGuardRequest = {
7
+ /** Incoming request headers (e.g. for token/cookie checks). */
3
8
  headers: Headers;
9
+ /** The authenticated user, or `null`/`undefined` if the request is anonymous. */
4
10
  user?: TypedUser | null;
11
+ /** The Payload instance, for any DB/permission lookups the guard needs. */
5
12
  payload: BasePayload;
6
13
  };
14
+ /**
15
+ * Gate for the translation API endpoints. Provide one via `translatorPlugin({ access })`
16
+ * to control who may trigger translations; omit it to leave the endpoints open.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * const adminsOnly: AccessGuard = {
21
+ * check: ({ req }) => req.user?.role === 'admin',
22
+ * }
23
+ * translatorPlugin({ collections, translationProvider, runner, access: adminsOnly })
24
+ * ```
25
+ */
7
26
  export interface AccessGuard {
27
+ /**
28
+ * Return `true` to allow the request, `false` to reject it with `403 Forbidden`.
29
+ * May be async (e.g. to query permissions).
30
+ */
8
31
  check<R extends AccessGuardRequest>({ req }: {
9
32
  req: R;
10
33
  }): Promise<boolean> | boolean;
@@ -1,3 +1,14 @@
1
- export { };
1
+ /**
2
+ * Gate for the translation API endpoints. Provide one via `translatorPlugin({ access })`
3
+ * to control who may trigger translations; omit it to leave the endpoints open.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const adminsOnly: AccessGuard = {
8
+ * check: ({ req }) => req.user?.role === 'admin',
9
+ * }
10
+ * translatorPlugin({ collections, translationProvider, runner, access: adminsOnly })
11
+ * ```
12
+ */ export { };
2
13
 
3
14
  //# sourceMappingURL=AccessGuard.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@focus-reactive/payload-plugin-translator",
3
- "version": "0.4.0",
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",