@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
@@ -1,12 +1,76 @@
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 { matchElementById, 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.
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.
6
10
  *
7
- * Logic:
8
- * - If targetValue exists and is not empty → use targetValue
9
- * - Otherwise use sourceValue
11
+ * Array/block elements are paired with their target counterpart by `id`, not by position (see
12
+ * {@link matchElementById}); output order always follows `source`.
13
+ */ const reconcileWalker = {
14
+ enterObject (field, cursor) {
15
+ const sourceValue = cursor.source[field.name];
16
+ if (!isObject(sourceValue)) return "skip"; // undefined / non-object source → drop the field
17
+ return {
18
+ source: sourceValue,
19
+ target: asObject(cursor.target[field.name])
20
+ };
21
+ },
22
+ enterList (field, cursor) {
23
+ const sourceValue = cursor.source[field.name];
24
+ if (!Array.isArray(sourceValue)) return "skip";
25
+ const targetValue = cursor.target[field.name];
26
+ const targetArr = Array.isArray(targetValue) ? targetValue : [];
27
+ const isBlocks = field.type === "blocks";
28
+ const children = [];
29
+ sourceValue.forEach((item, index)=>{
30
+ if (!isObject(item)) return; // non-object element → passthrough (rebuilt in combine)
31
+ const fields = isBlocks ? resolveBlockFields(field, item) : field.fields;
32
+ if (!fields) return; // unknown blockType → passthrough
33
+ // Pair by id, not position: target[index] may be a different element under per-locale ordering.
34
+ children.push({
35
+ cursor: {
36
+ source: item,
37
+ target: matchElementById(targetArr, item, isBlocks)
38
+ },
39
+ fields,
40
+ key: index
41
+ });
42
+ });
43
+ return children;
44
+ },
45
+ leaf (field, cursor) {
46
+ const sourceValue = cursor.source[field.name];
47
+ if (sourceValue === undefined) return undefined; // no source value → field dropped
48
+ const targetValue = cursor.target[field.name];
49
+ return isEmpty(targetValue) ? sourceValue : targetValue; // target priority, fallback to source
50
+ },
51
+ combine (container, children, cursor) {
52
+ if (container.kind === "list") {
53
+ // Rebuild the full array in source order: reconciled objects keyed by source position, raw
54
+ // source items (non-object / unknown block) otherwise.
55
+ const sourceArr = cursor.source[container.key] ?? [];
56
+ const byIndex = new Map(children.map((child)=>[
57
+ child.key,
58
+ child.out
59
+ ]));
60
+ return sourceArr.map((item, index)=>byIndex.has(index) ? byIndex.get(index) : item);
61
+ }
62
+ const result = {};
63
+ for (const child of children)result[child.key] = child.out;
64
+ // Block elements keep their blockType; id is intentionally stripped (Postgres rejects it on update).
65
+ if (container.kind === "element" && container.field.type === "blocks") {
66
+ result.blockType = cursor.source.blockType;
67
+ }
68
+ return result;
69
+ }
70
+ };
71
+ /**
72
+ * Deep-merges source and target data with target priority, producing the full document shape
73
+ * Payload validation needs. Built on the shared {@link walkFields} engine.
10
74
  *
11
75
  * Stage 1 of the translation pipeline.
12
76
  */ export class DataReconciler {
@@ -16,81 +80,16 @@ import { hasFields, isBlockItem, isTabsField, isEmpty, isObject } from '../../..
16
80
  }
17
81
  /**
18
82
  * Reconciles source and target data into a complete document shape.
19
- * The result contains all fields needed for Payload validation.
20
83
  *
21
84
  * @param sourceData - Source locale document data
22
85
  * @param targetData - Target locale document data (may be empty/partial)
23
86
  * @returns Complete document shape with reconciled field values
24
87
  */ 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;
88
+ const root = {
89
+ source: sourceData,
90
+ target: targetData ?? {}
91
+ };
92
+ return walkFields(this.schema, root, reconcileWalker) ?? {};
94
93
  }
95
94
  }
96
95
 
@@ -1,4 +1,4 @@
1
- import type { PipelineContext, PipelineStage } from '../../types';
1
+ import type { PipelineContext, PipelineStage } from "../../types";
2
2
  /**
3
3
  * Deep merges source and target data with target priority.
4
4
  * Stage 1 of the translation pipeline.
@@ -1,4 +1,4 @@
1
- import { DataReconciler } from './DataReconciler';
1
+ import { DataReconciler } from "./DataReconciler";
2
2
  /**
3
3
  * Deep merges source and target data with target priority.
4
4
  * Stage 1 of the translation pipeline.
@@ -1,18 +1,20 @@
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
11
+ * Array/block elements pair their `source` by position (`filteredData` is built in source order)
12
+ * but their `target` by `id` (see {@link matchElementById}) — the target locale's elements may be
13
+ * independently ordered, so a positional target would feed `strategy.shouldTranslate` the wrong
14
+ * element's value and skip/re-translate the wrong leaf.
13
15
  *
14
- * IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization).
15
- * Original schemas preserve `localized: true` on nested fields.
16
+ * IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization). Original
17
+ * schemas preserve `localized: true` on nested fields.
16
18
  */
17
19
  export declare class FieldChunkCollector {
18
20
  private readonly schema;
@@ -20,15 +22,7 @@ export declare class FieldChunkCollector {
20
22
  private readonly sourceData;
21
23
  private readonly targetData;
22
24
  private readonly strategy;
23
- private chunks;
24
25
  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
- */
26
+ /** Collects translatable field chunks that need translation. */
28
27
  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
28
  }
@@ -1,128 +1,116 @@
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 { matchElementById, 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
11
+ * Array/block elements pair their `source` by position (`filteredData` is built in source order)
12
+ * but their `target` by `id` (see {@link matchElementById}) — the target locale's elements may be
13
+ * independently ordered, so a positional target would feed `strategy.shouldTranslate` the wrong
14
+ * element's value and skip/re-translate the wrong leaf.
12
15
  *
13
- * IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization).
14
- * Original schemas preserve `localized: true` on nested fields.
16
+ * IMPORTANT: Expects ORIGINAL collection schemas (before Payload sanitization). Original
17
+ * schemas preserve `localized: true` on nested fields.
15
18
  */ export class FieldChunkCollector {
16
19
  schema;
17
20
  filteredData;
18
21
  sourceData;
19
22
  targetData;
20
23
  strategy;
21
- chunks;
22
24
  constructor(schema, filteredData, sourceData, targetData, strategy){
23
25
  this.schema = schema;
24
26
  this.filteredData = filteredData;
25
27
  this.sourceData = sourceData;
26
28
  this.targetData = targetData;
27
29
  this.strategy = strategy;
28
- this.chunks = [];
29
30
  }
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
- }
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 : [];
31
+ /** Collects translatable field chunks that need translation. */ collect() {
32
+ const chunks = [];
33
+ const { strategy } = this;
34
+ const walker = {
35
+ enterObject (field, cursor) {
36
+ const value = cursor.data[field.name];
37
+ if (!isObject(value)) return "skip";
38
+ return {
39
+ data: value,
40
+ source: asObject(cursor.source[field.name]),
41
+ target: asObject(cursor.target[field.name]),
42
+ path: [
43
+ ...cursor.path,
44
+ field.name
45
+ ]
46
+ };
47
+ },
48
+ enterList (field, cursor) {
49
+ const value = cursor.data[field.name];
50
+ if (!Array.isArray(value)) return "skip";
51
+ const sourceValue = cursor.source[field.name];
52
+ const targetValue = cursor.target[field.name];
53
+ const sourceArr = Array.isArray(sourceValue) ? sourceValue : [];
54
+ const targetArr = Array.isArray(targetValue) ? targetValue : [];
55
+ const isBlocks = field.type === "blocks";
56
+ const children = [];
81
57
  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,
58
+ if (!isObject(item)) return;
59
+ const fields = isBlocks ? resolveBlockFields(field, item) : field.fields;
60
+ if (!fields) return; // unknown blockType → skip element
61
+ // source pairs by index (filteredData shares source order); target by the source
62
+ // element's id, since the target locale may be reordered independently.
63
+ const sourceItem = asObject(sourceArr[index]);
64
+ children.push({
65
+ cursor: {
66
+ data: item,
67
+ source: sourceItem,
68
+ target: matchElementById(targetArr, sourceItem, isBlocks),
69
+ path: [
70
+ ...cursor.path,
71
+ field.name,
104
72
  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
73
+ ]
74
+ },
75
+ fields,
76
+ key: index
77
+ });
123
78
  });
79
+ return children;
80
+ },
81
+ leaf (field, cursor) {
82
+ const value = cursor.data[field.name];
83
+ if (value === undefined || value === null) return undefined;
84
+ const sourceValue = cursor.source[field.name];
85
+ const targetValue = cursor.target[field.name];
86
+ if (isTranslatableField(field) && isLocalizedField(field) && !isFieldExcludedFromTranslation(field) && strategy.shouldTranslate({
87
+ sourceValue,
88
+ targetValue
89
+ })) {
90
+ cursor.data[field.name] = sourceValue; // write source value into filteredData — this is what gets translated
91
+ chunks.push({
92
+ schema: field,
93
+ dataRef: cursor.data,
94
+ key: field.name,
95
+ path: [
96
+ ...cursor.path,
97
+ field.name
98
+ ]
99
+ });
100
+ }
101
+ return undefined;
102
+ },
103
+ combine () {
104
+ return undefined; // collect-only — nothing to assemble
124
105
  }
125
- }
106
+ };
107
+ walkFields(this.schema, {
108
+ data: this.filteredData,
109
+ source: this.sourceData,
110
+ target: this.targetData,
111
+ path: []
112
+ }, walker);
113
+ return chunks;
126
114
  }
127
115
  }
128
116
 
@@ -76,6 +76,23 @@ export type OpenAIProviderConfig = {
76
76
  * @default false
77
77
  */
78
78
  dryRun?: boolean | DryRunConfig;
79
+ /**
80
+ * Per-request timeout in milliseconds for the OpenAI client. A translation job blocks on this
81
+ * call, so the OpenAI SDK default (10 minutes) is usually too long. Omit to keep the SDK default.
82
+ *
83
+ * @example
84
+ * timeout: 60_000 // 60s
85
+ *
86
+ * @since 0.6.0
87
+ */
88
+ timeout?: number;
89
+ /**
90
+ * Maximum automatic retries the OpenAI client performs on transient errors (429, 5xx, network).
91
+ * Omit to keep the SDK default (2). Set `0` to disable retries.
92
+ *
93
+ * @since 0.6.0
94
+ */
95
+ maxRetries?: number;
79
96
  };
80
97
  /** @deprecated Use `createOpenAIProvider` function instead */
81
98
  export declare class OpenAITranslationProvider implements TranslationProvider {
@@ -4,8 +4,12 @@ import { isObject } from "../../shared";
4
4
  openAiClient;
5
5
  config;
6
6
  constructor(config){
7
+ // `timeout`/`maxRetries` are passed through to the OpenAI SDK; `undefined` keeps the SDK
8
+ // defaults (10 min timeout, 2 retries). A blocking translation job rarely wants the full 10 min.
7
9
  this.openAiClient = new OpenAI({
8
- apiKey: config.apiKey
10
+ apiKey: config.apiKey,
11
+ timeout: config.timeout,
12
+ maxRetries: config.maxRetries
9
13
  });
10
14
  this.config = config;
11
15
  }
@@ -43,7 +47,9 @@ import { isObject } from "../../shared";
43
47
  type: "json_object"
44
48
  }
45
49
  });
46
- const translatedContent = chatCompletion.choices[0].message.content;
50
+ // Guard `choices[0]`: an empty `choices` array (e.g. content-filtered response) would otherwise
51
+ // throw a TypeError instead of the intended graceful `null`.
52
+ const translatedContent = chatCompletion.choices[0]?.message?.content;
47
53
  if (!translatedContent) return null;
48
54
  try {
49
55
  return JSON.parse(translatedContent);
@@ -17,6 +17,6 @@
17
17
  * ```
18
18
  */ /**
19
19
  * Key used to store TranslateKit configuration in field.custom
20
- */ export const TRANSLATE_KIT_CUSTOM_KEY = 'translateKit';
20
+ */ export const TRANSLATE_KIT_CUSTOM_KEY = "translateKit";
21
21
 
22
22
  //# sourceMappingURL=types.js.map
@@ -0,0 +1,55 @@
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, a single block element, or a
8
+ * named tab (a container, not a leaf).
9
+ * - `inside-blocks` — the path descends THROUGH a `blocks` field that can't be resolved: no
10
+ * document data was supplied (so the element's `blockType` is unknown), the data is missing at
11
+ * that index, or the `blockType` matches no defined block.
12
+ * - `localized-list-ancestor` — the path descends THROUGH a `localized` `blocks`/`array` field.
13
+ * Such fields are stored independently per locale (different order/content), so an element index
14
+ * is not a stable cross-locale identity; a caller translating *by path across locales* should
15
+ * refuse rather than resolve positionally. (See the "cross-locale block identity" design.)
16
+ * - `not-found` — no field matches a segment, or the path continues past a leaf.
17
+ *
18
+ * @public
19
+ */
20
+ export type FieldPathResult = {
21
+ status: "leaf";
22
+ field: LeafField;
23
+ } | {
24
+ status: "container";
25
+ } | {
26
+ status: "inside-blocks";
27
+ } | {
28
+ status: "localized-list-ancestor";
29
+ } | {
30
+ status: "not-found";
31
+ };
32
+ /**
33
+ * Navigate a field schema by a path of segment NAMES, descending one matching branch at a time
34
+ * with early-exit (targeted navigation, not an exhaustive walk). Presentational containers
35
+ * (`row`/`collapsible`/unnamed `group`) and unnamed tabs are transparent — searched in the same
36
+ * path scope. Built on {@link classifyField} / {@link tabScopes} / {@link resolveBlockFields} so
37
+ * the structural dispatch lives in one place.
38
+ *
39
+ * **Array indices and `blocks`.** Array element configs are shared, so a numeric segment after an
40
+ * array name selects the data item but not the schema (the schema continues into `array.fields`).
41
+ * A `blocks` field is polymorphic — which block (and thus which fields) sits at an index lives in
42
+ * the *data*, not the schema. So to descend through `blocks` the caller must pass the document
43
+ * `data` and keep the element index in the path: this function reads `data[name][index].blockType`
44
+ * and resolves the matching block via {@link resolveBlockFields}. Without `data` (or with a
45
+ * non-indexed path), descending through `blocks` returns `inside-blocks`.
46
+ *
47
+ * @param fields - The schema level to search.
48
+ * @param segments - Remaining path segments to match, head-first. Keep array/block element indices
49
+ * in the path when navigating with `data`.
50
+ * @param data - The document (sub)tree aligned with `fields`, used only to disambiguate `blocks`.
51
+ * Omit for schema-only navigation.
52
+ * @returns A {@link FieldPathResult}.
53
+ * @public
54
+ */
55
+ export declare function findFieldByPath(fields: Field[], segments: string[], data?: unknown): FieldPathResult;