@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
@@ -1,12 +1,70 @@
1
- import { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName } from 'payload/shared';
2
- import { hasFields, isBlockItem, isTabsField, isEmpty, isObject } from '../../../../shared';
1
+ import { isEmpty, isObject } from "../../../../shared";
2
+ import { resolveBlockFields, walkFields } from "../../../../shared/field-traversal";
3
+ const asObject = (value)=>isObject(value) ? value : {};
3
4
  /**
4
- * Deep merges source and target data with target priority.
5
- * Creates full document shape needed for Payload validation.
6
- *
7
- * Logic:
8
- * - If targetValue exists and is not empty → use targetValue
9
- * - Otherwise use sourceValue
5
+ * Reconcile walker: produces the full document shape from source + target, with target
6
+ * priority (source fills empty target slots). Iteration is driven by `source`; a field with
7
+ * no source value is dropped, `id` is stripped from array/block elements (Postgres rejects it
8
+ * on update), `blockType` is preserved, and non-object array items / unknown blocks pass
9
+ * through unchanged.
10
+ */ const reconcileWalker = {
11
+ enterObject (field, cursor) {
12
+ const sourceValue = cursor.source[field.name];
13
+ if (!isObject(sourceValue)) return "skip"; // undefined / non-object source → drop the field
14
+ return {
15
+ source: sourceValue,
16
+ target: asObject(cursor.target[field.name])
17
+ };
18
+ },
19
+ enterList (field, cursor) {
20
+ const sourceValue = cursor.source[field.name];
21
+ if (!Array.isArray(sourceValue)) return "skip";
22
+ const targetValue = cursor.target[field.name];
23
+ const targetArr = Array.isArray(targetValue) ? targetValue : [];
24
+ const children = [];
25
+ sourceValue.forEach((item, index)=>{
26
+ if (!isObject(item)) return; // non-object element → passthrough (rebuilt in combine)
27
+ const fields = field.type === "blocks" ? resolveBlockFields(field, item) : field.fields;
28
+ if (!fields) return; // unknown blockType → passthrough
29
+ children.push({
30
+ cursor: {
31
+ source: item,
32
+ target: asObject(targetArr[index])
33
+ },
34
+ fields,
35
+ key: index
36
+ });
37
+ });
38
+ return children;
39
+ },
40
+ leaf (field, cursor) {
41
+ const sourceValue = cursor.source[field.name];
42
+ if (sourceValue === undefined) return undefined; // no source value → field dropped
43
+ const targetValue = cursor.target[field.name];
44
+ return isEmpty(targetValue) ? sourceValue : targetValue; // target priority, fallback to source
45
+ },
46
+ combine (container, children, cursor) {
47
+ if (container.kind === "list") {
48
+ // Rebuild the full array: reconciled objects where present (by index), raw source items otherwise.
49
+ const sourceArr = cursor.source[container.key] ?? [];
50
+ const byIndex = new Map(children.map((child)=>[
51
+ child.key,
52
+ child.out
53
+ ]));
54
+ return sourceArr.map((item, index)=>byIndex.has(index) ? byIndex.get(index) : item);
55
+ }
56
+ const result = {};
57
+ for (const child of children)result[child.key] = child.out;
58
+ // Block elements keep their blockType; id is intentionally stripped (Postgres rejects it on update).
59
+ if (container.kind === "element" && container.field.type === "blocks") {
60
+ result.blockType = cursor.source.blockType;
61
+ }
62
+ return result;
63
+ }
64
+ };
65
+ /**
66
+ * Deep-merges source and target data with target priority, producing the full document shape
67
+ * Payload validation needs. Built on the shared {@link walkFields} engine.
10
68
  *
11
69
  * Stage 1 of the translation pipeline.
12
70
  */ export class DataReconciler {
@@ -16,81 +74,16 @@ import { hasFields, isBlockItem, isTabsField, isEmpty, isObject } from '../../..
16
74
  }
17
75
  /**
18
76
  * Reconciles source and target data into a complete document shape.
19
- * The result contains all fields needed for Payload validation.
20
77
  *
21
78
  * @param sourceData - Source locale document data
22
79
  * @param targetData - Target locale document data (may be empty/partial)
23
80
  * @returns Complete document shape with reconciled field values
24
81
  */ reconcile(sourceData, targetData) {
25
- return this.reconcileFields(this.schema, sourceData, targetData ?? {});
26
- }
27
- reconcileFields(fields, source, target) {
28
- const result = {};
29
- for (const field of fields){
30
- if (isTabsField(field)) {
31
- for (const tab of field.tabs){
32
- if (hasFields(tab)) {
33
- if (tabHasName(tab)) {
34
- const tabSource = source[tab.name];
35
- const tabTarget = target[tab.name];
36
- if (isObject(tabSource)) {
37
- result[tab.name] = this.reconcileFields(tab.fields, tabSource, isObject(tabTarget) ? tabTarget : {});
38
- }
39
- } else {
40
- Object.assign(result, this.reconcileFields(tab.fields, source, target));
41
- }
42
- }
43
- }
44
- continue;
45
- }
46
- if (!fieldAffectsData(field)) {
47
- if (hasFields(field)) Object.assign(result, this.reconcileFields(field.fields, source, target));
48
- continue;
49
- }
50
- const sourceValue = source[field.name];
51
- const targetValue = target[field.name];
52
- // Skip undefined source values
53
- if (sourceValue === undefined) continue;
54
- // Group - recursively reconcile
55
- if (fieldIsGroupType(field) && isObject(sourceValue)) {
56
- const targetGroup = isObject(targetValue) ? targetValue : {};
57
- result[field.name] = this.reconcileFields(field.fields, sourceValue, targetGroup);
58
- continue;
59
- }
60
- // Array - recursively reconcile each item (without id - Postgres rejects it)
61
- if (fieldIsArrayType(field) && Array.isArray(sourceValue)) {
62
- const targetArray = Array.isArray(targetValue) ? targetValue : [];
63
- result[field.name] = sourceValue.map((sourceItem, index)=>{
64
- if (isObject(sourceItem)) {
65
- const targetItem = isObject(targetArray[index]) ? targetArray[index] : {};
66
- return this.reconcileFields(field.fields, sourceItem, targetItem);
67
- }
68
- return sourceItem;
69
- });
70
- continue;
71
- }
72
- // Blocks - recursively reconcile each block (without id - Postgres rejects it)
73
- if (fieldIsBlockType(field) && Array.isArray(sourceValue)) {
74
- const targetArray = Array.isArray(targetValue) ? targetValue : [];
75
- result[field.name] = sourceValue.map((sourceItem, index)=>{
76
- if (isBlockItem(sourceItem)) {
77
- const block = field.blocks.find((b)=>b.slug === sourceItem.blockType);
78
- if (block) {
79
- const targetItem = isObject(targetArray[index]) ? targetArray[index] : {};
80
- return {
81
- ...this.reconcileFields(block.fields, sourceItem, targetItem),
82
- blockType: sourceItem.blockType
83
- };
84
- }
85
- }
86
- return sourceItem;
87
- });
88
- continue;
89
- }
90
- // Deep merge: target priority, fallback to source
91
- result[field.name] = isEmpty(targetValue) ? sourceValue : targetValue;
92
- }
93
- return result;
82
+ const root = {
83
+ source: sourceData,
84
+ target: targetData ?? {}
85
+ };
86
+ return walkFields(this.schema, root, reconcileWalker) ?? {};
94
87
  }
95
88
  }
96
89
 
@@ -1,18 +1,15 @@
1
- import type { Field } from 'payload';
2
- import type { FieldChunk } from '../../types';
3
- import type { TranslationStrategy } from '../../strategies';
1
+ import type { Field } from "payload";
2
+ import type { TranslationStrategy } from "../../strategies";
3
+ import type { FieldChunk } from "../../types";
4
4
  /**
5
- * Collects FieldChunks by traversing schema and data in parallel.
6
- * Each chunk contains a reference to the parent data object for later mutation.
5
+ * Collects FieldChunks by walking schema + data with the shared {@link walkFields} engine.
6
+ * Iteration is driven by `filteredData`; each translatable, localized, non-excluded leaf that
7
+ * the strategy approves is written with its source value and pushed as a chunk carrying a
8
+ * mutable reference to its parent object (for later write-back). Collect-only — `combine` is a
9
+ * no-op; results accumulate in a closure.
7
10
  *
8
- * Applies all filtering logic:
9
- * - isTranslatableField
10
- * - isLocalizedField
11
- * - strategy.shouldTranslate(sourceValue, targetValue)
12
- * - !isFieldExcludedFromTranslation
13
- *
14
- * IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization).
15
- * Original schemas preserve `localized: true` on nested fields.
11
+ * IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization). Original
12
+ * schemas preserve `localized: true` on nested fields.
16
13
  */
17
14
  export declare class FieldChunkCollector {
18
15
  private readonly schema;
@@ -20,15 +17,7 @@ export declare class FieldChunkCollector {
20
17
  private readonly sourceData;
21
18
  private readonly targetData;
22
19
  private readonly strategy;
23
- private chunks;
24
20
  constructor(schema: Field[], filteredData: Record<string, unknown>, sourceData: Record<string, unknown>, targetData: Record<string, unknown>, strategy: TranslationStrategy);
25
- /**
26
- * Collects translatable field chunks that need translation.
27
- */
21
+ /** Collects translatable field chunks that need translation. */
28
22
  collect(): FieldChunk[];
29
- /**
30
- * Traverses fields recursively, collecting translatable fields that need translation.
31
- * Uses strategy.shouldTranslate() to determine if field needs translation.
32
- */
33
- private traverseFields;
34
23
  }
@@ -1,128 +1,107 @@
1
- import { fieldAffectsData, fieldIsArrayType, fieldIsBlockType, fieldIsGroupType, tabHasName } from 'payload/shared';
2
- import { hasFields, isBlockItem, isLocalizedField, isObject, isTabsField, isTranslatableField, isFieldExcludedFromTranslation } from '../../../../shared';
1
+ import { isFieldExcludedFromTranslation, isLocalizedField, isObject, isTranslatableField } from "../../../../shared";
2
+ import { resolveBlockFields, walkFields } from "../../../../shared/field-traversal";
3
+ const asObject = (value)=>isObject(value) ? value : {};
3
4
  /**
4
- * Collects FieldChunks by traversing schema and data in parallel.
5
- * Each chunk contains a reference to the parent data object for later mutation.
5
+ * Collects FieldChunks by walking schema + data with the shared {@link walkFields} engine.
6
+ * Iteration is driven by `filteredData`; each translatable, localized, non-excluded leaf that
7
+ * the strategy approves is written with its source value and pushed as a chunk carrying a
8
+ * mutable reference to its parent object (for later write-back). Collect-only — `combine` is a
9
+ * no-op; results accumulate in a closure.
6
10
  *
7
- * Applies all filtering logic:
8
- * - isTranslatableField
9
- * - isLocalizedField
10
- * - strategy.shouldTranslate(sourceValue, targetValue)
11
- * - !isFieldExcludedFromTranslation
12
- *
13
- * IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization).
14
- * Original schemas preserve `localized: true` on nested fields.
11
+ * IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization). Original
12
+ * schemas preserve `localized: true` on nested fields.
15
13
  */ export class FieldChunkCollector {
16
14
  schema;
17
15
  filteredData;
18
16
  sourceData;
19
17
  targetData;
20
18
  strategy;
21
- chunks;
22
19
  constructor(schema, filteredData, sourceData, targetData, strategy){
23
20
  this.schema = schema;
24
21
  this.filteredData = filteredData;
25
22
  this.sourceData = sourceData;
26
23
  this.targetData = targetData;
27
24
  this.strategy = strategy;
28
- this.chunks = [];
29
- }
30
- /**
31
- * Collects translatable field chunks that need translation.
32
- */ collect() {
33
- this.chunks = [];
34
- this.traverseFields(this.schema, this.filteredData, this.sourceData, this.targetData, []);
35
- return this.chunks;
36
25
  }
37
- /**
38
- * Traverses fields recursively, collecting translatable fields that need translation.
39
- * Uses strategy.shouldTranslate() to determine if field needs translation.
40
- */ traverseFields(fields, data, source, target, path) {
41
- for (const field of fields){
42
- if (isTabsField(field)) {
43
- for (const tab of field.tabs){
44
- if (hasFields(tab)) {
45
- if (tabHasName(tab)) {
46
- const tabData = data[tab.name];
47
- const tabSource = source[tab.name];
48
- const tabTarget = target[tab.name];
49
- if (isObject(tabData)) {
50
- this.traverseFields(tab.fields, tabData, isObject(tabSource) ? tabSource : {}, isObject(tabTarget) ? tabTarget : {}, [
51
- ...path,
52
- tab.name
53
- ]);
54
- }
55
- } else {
56
- this.traverseFields(tab.fields, data, source, target, path);
57
- }
58
- }
59
- }
60
- continue;
61
- }
62
- if (!fieldAffectsData(field)) {
63
- if (hasFields(field)) this.traverseFields(field.fields, data, source, target, path);
64
- continue;
65
- }
66
- const value = data[field.name];
67
- const sourceValue = source[field.name];
68
- const targetValue = target[field.name];
69
- if (value === undefined || value === null) continue;
70
- const currentPath = [
71
- ...path,
72
- field.name
73
- ];
74
- if (fieldIsGroupType(field) && isObject(value)) {
75
- this.traverseFields(field.fields, value, isObject(sourceValue) ? sourceValue : {}, isObject(targetValue) ? targetValue : {}, currentPath);
76
- continue;
77
- }
78
- if (fieldIsArrayType(field) && Array.isArray(value)) {
79
- const sourceArray = Array.isArray(sourceValue) ? sourceValue : [];
80
- const targetArray = Array.isArray(targetValue) ? targetValue : [];
26
+ /** Collects translatable field chunks that need translation. */ collect() {
27
+ const chunks = [];
28
+ const { strategy } = this;
29
+ const walker = {
30
+ enterObject (field, cursor) {
31
+ const value = cursor.data[field.name];
32
+ if (!isObject(value)) return "skip";
33
+ return {
34
+ data: value,
35
+ source: asObject(cursor.source[field.name]),
36
+ target: asObject(cursor.target[field.name]),
37
+ path: [
38
+ ...cursor.path,
39
+ field.name
40
+ ]
41
+ };
42
+ },
43
+ enterList (field, cursor) {
44
+ const value = cursor.data[field.name];
45
+ if (!Array.isArray(value)) return "skip";
46
+ const sourceValue = cursor.source[field.name];
47
+ const targetValue = cursor.target[field.name];
48
+ const sourceArr = Array.isArray(sourceValue) ? sourceValue : [];
49
+ const targetArr = Array.isArray(targetValue) ? targetValue : [];
50
+ const children = [];
81
51
  value.forEach((item, index)=>{
82
- if (isObject(item)) {
83
- const sourceItem = isObject(sourceArray[index]) ? sourceArray[index] : {};
84
- const targetItem = isObject(targetArray[index]) ? targetArray[index] : {};
85
- this.traverseFields(field.fields, item, sourceItem, targetItem, [
86
- ...currentPath,
87
- String(index)
88
- ]);
89
- }
90
- });
91
- continue;
92
- }
93
- if (fieldIsBlockType(field) && Array.isArray(value)) {
94
- const sourceArray = Array.isArray(sourceValue) ? sourceValue : [];
95
- const targetArray = Array.isArray(targetValue) ? targetValue : [];
96
- value.forEach((item, index)=>{
97
- if (isBlockItem(item)) {
98
- const block = field.blocks.find((b)=>b.slug === item.blockType);
99
- if (block) {
100
- const sourceItem = isObject(sourceArray[index]) ? sourceArray[index] : {};
101
- const targetItem = isObject(targetArray[index]) ? targetArray[index] : {};
102
- this.traverseFields(block.fields, item, sourceItem, targetItem, [
103
- ...currentPath,
52
+ if (!isObject(item)) return;
53
+ const fields = field.type === "blocks" ? resolveBlockFields(field, item) : field.fields;
54
+ if (!fields) return; // unknown blockType → skip element
55
+ children.push({
56
+ cursor: {
57
+ data: item,
58
+ source: asObject(sourceArr[index]),
59
+ target: asObject(targetArr[index]),
60
+ path: [
61
+ ...cursor.path,
62
+ field.name,
104
63
  String(index)
105
- ]);
106
- }
107
- }
108
- });
109
- continue;
110
- }
111
- // Collect if: translatable, localized, not excluded, and strategy says to translate
112
- if (isTranslatableField(field) && isLocalizedField(field) && !isFieldExcludedFromTranslation(field) && this.strategy.shouldTranslate({
113
- sourceValue,
114
- targetValue
115
- })) {
116
- // Write sourceValue to filteredData — this is what will be translated
117
- data[field.name] = sourceValue;
118
- this.chunks.push({
119
- schema: field,
120
- dataRef: data,
121
- key: field.name,
122
- path: currentPath
64
+ ]
65
+ },
66
+ fields,
67
+ key: index
68
+ });
123
69
  });
70
+ return children;
71
+ },
72
+ leaf (field, cursor) {
73
+ const value = cursor.data[field.name];
74
+ if (value === undefined || value === null) return undefined;
75
+ const sourceValue = cursor.source[field.name];
76
+ const targetValue = cursor.target[field.name];
77
+ if (isTranslatableField(field) && isLocalizedField(field) && !isFieldExcludedFromTranslation(field) && strategy.shouldTranslate({
78
+ sourceValue,
79
+ targetValue
80
+ })) {
81
+ cursor.data[field.name] = sourceValue; // write source value into filteredData — this is what gets translated
82
+ chunks.push({
83
+ schema: field,
84
+ dataRef: cursor.data,
85
+ key: field.name,
86
+ path: [
87
+ ...cursor.path,
88
+ field.name
89
+ ]
90
+ });
91
+ }
92
+ return undefined;
93
+ },
94
+ combine () {
95
+ return undefined; // collect-only — nothing to assemble
124
96
  }
125
- }
97
+ };
98
+ walkFields(this.schema, {
99
+ data: this.filteredData,
100
+ source: this.sourceData,
101
+ target: this.targetData,
102
+ path: []
103
+ }, walker);
104
+ return chunks;
126
105
  }
127
106
  }
128
107
 
@@ -0,0 +1,35 @@
1
+ import type { Field } from "payload";
2
+ import type { TranslationProvider } from "../translation-providers";
3
+ import type { TranslationStrategyName } from "./strategies";
4
+ export type TranslateContentArgs = {
5
+ /** Schema subtree to translate (e.g. `[declaredFieldConfig]`). */
6
+ schema: Field[];
7
+ /** Source values, rooted to match `schema` (e.g. `{ [fieldName]: value }`). */
8
+ sourceData: Record<string, unknown>;
9
+ /**
10
+ * Existing target-locale values to reconcile against (target wins when
11
+ * non-empty under `skip_existing`). Defaults to `{}` — i.e. translate the
12
+ * source in place (`overwrite`).
13
+ */
14
+ targetData?: Record<string, unknown>;
15
+ /** Source language code, or `''` for provider auto-detect. */
16
+ sourceLng: string;
17
+ targetLng: string;
18
+ translationProvider: TranslationProvider;
19
+ /** @default 'overwrite' */
20
+ strategy?: TranslationStrategyName;
21
+ };
22
+ /**
23
+ * Translate a content object over a schema subtree — no DB, no document.
24
+ *
25
+ * A thin reusable entry over {@link TranslationPipeline}, which is already pure
26
+ * and walks any `Field[]` + matching data (a subtree + partial data works
27
+ * unchanged). The document level routes through this wrapper today (instead of
28
+ * constructing the pipeline inline); the upcoming field level will too —
29
+ * passing a single declared field's subtree + its current unsaved form value.
30
+ *
31
+ * Only `localized` text/richText leaves are translated; non-localized values
32
+ * are reconciled through unchanged. Returns the translated data (same shape as
33
+ * `sourceData`) or `null` when nothing was translatable.
34
+ */
35
+ export declare function translateContent({ schema, sourceData, targetData, sourceLng, targetLng, translationProvider, strategy, }: TranslateContentArgs): Promise<Record<string, unknown> | null>;
@@ -0,0 +1,30 @@
1
+ import { TranslationPipeline } from "./TranslationPipeline";
2
+ import { createTranslationStrategy } from "./strategies";
3
+ /**
4
+ * Translate a content object over a schema subtree — no DB, no document.
5
+ *
6
+ * A thin reusable entry over {@link TranslationPipeline}, which is already pure
7
+ * and walks any `Field[]` + matching data (a subtree + partial data works
8
+ * unchanged). The document level routes through this wrapper today (instead of
9
+ * constructing the pipeline inline); the upcoming field level will too —
10
+ * passing a single declared field's subtree + its current unsaved form value.
11
+ *
12
+ * Only `localized` text/richText leaves are translated; non-localized values
13
+ * are reconciled through unchanged. Returns the translated data (same shape as
14
+ * `sourceData`) or `null` when nothing was translatable.
15
+ */ export async function translateContent({ schema, sourceData, targetData = {}, sourceLng, targetLng, translationProvider, strategy = "overwrite" }) {
16
+ const pipeline = new TranslationPipeline({
17
+ translationProvider,
18
+ translationStrategy: createTranslationStrategy(strategy)
19
+ });
20
+ const result = await pipeline.execute({
21
+ schema,
22
+ sourceData,
23
+ targetData,
24
+ sourceLng,
25
+ targetLng
26
+ });
27
+ return result ? result.translatedData : null;
28
+ }
29
+
30
+ //# sourceMappingURL=translateContent.js.map
@@ -1,5 +1,5 @@
1
- import type { TranslationProvider, TranslationInput, TranslationOutput } from './TranslationProvider.interface';
2
- import type { ChatModel } from 'openai/resources/index.mjs';
1
+ import type { TranslationProvider, TranslationInput, TranslationOutput } from "./TranslationProvider.interface";
2
+ import type { ChatModel } from "openai/resources/index.mjs";
3
3
  /**
4
4
  * Function to transform text in dry run mode.
5
5
  * Receives the original text and returns the transformed text.
@@ -30,6 +30,7 @@ export type SystemPromptContext = {
30
30
  */
31
31
  type SystemPromptBuilder = (context: SystemPromptContext) => string;
32
32
  export type OpenAIProviderConfig = {
33
+ /** OpenAI API key (required). Read it from an env var — never hard-code it. */
33
34
  apiKey: string;
34
35
  /**
35
36
  * OpenAI model to use for translation.
@@ -78,8 +79,8 @@ export type OpenAIProviderConfig = {
78
79
  };
79
80
  /** @deprecated Use `createOpenAIProvider` function instead */
80
81
  export declare class OpenAITranslationProvider implements TranslationProvider {
81
- private readonly config;
82
82
  private openAiClient;
83
+ private readonly config;
83
84
  constructor(config: OpenAIProviderConfig);
84
85
  translate(content: TranslationInput, souceLng: string, targetLng: string): Promise<TranslationOutput | null>;
85
86
  /**
@@ -1,21 +1,21 @@
1
- import OpenAI from 'openai';
2
- import { isObject } from '../../shared';
1
+ import OpenAI from "openai";
2
+ import { isObject } from "../../shared";
3
3
  /** @deprecated Use `createOpenAIProvider` function instead */ export class OpenAITranslationProvider {
4
- config;
5
4
  openAiClient;
5
+ config;
6
6
  constructor(config){
7
- this.config = config;
8
7
  this.openAiClient = new OpenAI({
9
8
  apiKey: config.apiKey
10
9
  });
10
+ this.config = config;
11
11
  }
12
12
  async translate(content, souceLng, targetLng) {
13
13
  if (this.config.dryRun) {
14
- console.info('[DRY RUN] Translation simulation:', {
14
+ console.info("[DRY RUN] Translation simulation:", {
15
15
  content,
16
16
  sourceLang: souceLng,
17
17
  targetLang: targetLng,
18
- provider: 'OpenAI'
18
+ provider: "OpenAI"
19
19
  });
20
20
  const timeout = this.getDryRunTimeout();
21
21
  if (timeout > 0) await new Promise((resolve)=>setTimeout(resolve, timeout));
@@ -26,21 +26,21 @@ import { isObject } from '../../shared';
26
26
  const chatCompletion = await this.openAiClient.chat.completions.create({
27
27
  messages: [
28
28
  {
29
- role: 'system',
29
+ role: "system",
30
30
  content: systemPrompt
31
31
  },
32
32
  {
33
- role: 'user',
33
+ role: "user",
34
34
  content: JSON.stringify(content)
35
35
  }
36
36
  ],
37
- model: this.config.model ?? 'gpt-4o',
37
+ model: this.config.model ?? "gpt-4o",
38
38
  temperature: 0,
39
39
  top_p: 1,
40
40
  frequency_penalty: 0,
41
41
  presence_penalty: 0,
42
42
  response_format: {
43
- type: 'json_object'
43
+ type: "json_object"
44
44
  }
45
45
  });
46
46
  const translatedContent = chatCompletion.choices[0].message.content;
@@ -54,7 +54,7 @@ import { isObject } from '../../shared';
54
54
  /**
55
55
  * Builds the system prompt for translation.
56
56
  */ buildSystemPrompt(sourceLang, targetLang) {
57
- const defaultPrompt = `Translate the values from the JSON that the user will send you${sourceLang ? ` from ${sourceLang}` : ''} into ${targetLang}. Keep all JSON keys exactly as they are, only translate the values.
57
+ const defaultPrompt = `Translate the values from the JSON that the user will send you${sourceLang ? ` from ${sourceLang}` : ""} into ${targetLang}. Keep all JSON keys exactly as they are, only translate the values.
58
58
  The response should be a valid JSON object with the same structure and keys as the input, but with translated values.
59
59
  Maintain any special formatting, placeholders, or variables within the values if they exist.`;
60
60
  if (this.config.systemPrompt) {
@@ -71,17 +71,17 @@ Maintain any special formatting, placeholders, or variables within the values if
71
71
  * If dryRun is an object with transform, returns it.
72
72
  * If dryRun is true, returns the default transformer that reverses text.
73
73
  */ getDryRunTransformer() {
74
- if (typeof this.config.dryRun === 'object' && this.config.dryRun.transform) {
74
+ if (typeof this.config.dryRun === "object" && this.config.dryRun.transform) {
75
75
  return this.config.dryRun.transform;
76
76
  }
77
- return (text)=>text.split('').reverse().join('');
77
+ return (text)=>text.split("").reverse().join("");
78
78
  }
79
79
  /**
80
80
  * Returns the timeout for dry run mode.
81
81
  * If dryRun is an object with timeout, returns it.
82
82
  * Otherwise returns 0 (no delay).
83
83
  */ getDryRunTimeout() {
84
- if (typeof this.config.dryRun === 'object' && this.config.dryRun.timeout) {
84
+ if (typeof this.config.dryRun === "object" && this.config.dryRun.timeout) {
85
85
  return this.config.dryRun.timeout;
86
86
  }
87
87
  return 0;
@@ -89,7 +89,7 @@ Maintain any special formatting, placeholders, or variables within the values if
89
89
  async createMockTranslation(content, transformer) {
90
90
  try {
91
91
  const mockTranslation = await this.transformObjectValues(content, async (value)=>{
92
- if (typeof value === 'string' && value.trim()) return transformer(value);
92
+ if (typeof value === "string" && value.trim()) return transformer(value);
93
93
  return value;
94
94
  });
95
95
  return mockTranslation;
@@ -0,0 +1,40 @@
1
+ import type { Field } from "payload";
2
+ import type { LeafField } from "./types";
3
+ /**
4
+ * Outcome of navigating a field schema by a path of segment names (see {@link findFieldByPath}).
5
+ *
6
+ * - `leaf` — the path lands on a data-affecting leaf field (carries it).
7
+ * - `container` — the path lands on a `group`/`array`/`blocks` field or a named tab (a container,
8
+ * not a leaf).
9
+ * - `inside-blocks` — the path descends THROUGH a `blocks` field, which can't be resolved from the
10
+ * schema alone (the block's `blockType` lives in the data, not the schema).
11
+ * - `not-found` — no field matches a segment, or the path continues past a leaf.
12
+ *
13
+ * @public
14
+ */
15
+ export type FieldPathResult = {
16
+ status: "leaf";
17
+ field: LeafField;
18
+ } | {
19
+ status: "container";
20
+ } | {
21
+ status: "inside-blocks";
22
+ } | {
23
+ status: "not-found";
24
+ };
25
+ /**
26
+ * Navigate a field schema by a path of segment NAMES, descending one matching branch at a time
27
+ * with early-exit (targeted navigation, not an exhaustive walk). Presentational containers
28
+ * (`row`/`collapsible`/unnamed `group`) and unnamed tabs are transparent — searched in the same
29
+ * path scope. Built on {@link classifyField} / {@link tabScopes} so the structural dispatch lives
30
+ * in one place.
31
+ *
32
+ * The caller supplies already-prepared name segments (split, trimmed, with array indices dropped —
33
+ * a field's config is shared across array items, so indices never appear in the schema).
34
+ *
35
+ * @param fields - The schema level to search.
36
+ * @param segments - Remaining path segments (names) to match, head-first.
37
+ * @returns A {@link FieldPathResult}.
38
+ * @public
39
+ */
40
+ export declare function findFieldByPath(fields: Field[], segments: string[]): FieldPathResult;