@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.
- package/README.md +46 -8
- package/dist/client/entities/translation/api/mutations/useTranslateField.d.ts +22 -0
- package/dist/client/entities/translation/api/mutations/useTranslateField.js +31 -0
- package/dist/client/features/collection-translation-form/ui/CollectionTranslationForm.d.ts +3 -3
- package/dist/client/features/collection-translation-form/ui/CollectionTranslationForm.js +15 -13
- package/dist/client/features/collection-translation-popup/ui/CollectionTranslationPopup.d.ts +2 -2
- package/dist/client/features/collection-translation-popup/ui/CollectionTranslationPopup.js +10 -10
- package/dist/client/features/open-document-translation-popup/ui/OpenDocumentTranslationPopup.d.ts +1 -1
- package/dist/client/features/open-document-translation-popup/ui/OpenDocumentTranslationPopup.js +9 -9
- package/dist/client/features/translate-document-form/ui/DocumentTranslationForm.d.ts +2 -2
- package/dist/client/features/translate-document-form/ui/DocumentTranslationForm.js +10 -8
- package/dist/client/shared/lib/assets/icons/SendIcon.d.ts +1 -0
- package/dist/client/shared/lib/assets/icons/SendIcon.js +17 -0
- package/dist/client/shared/ui/Button/Button.d.ts +5 -4
- package/dist/client/shared/ui/Button/Button.js +19 -19
- package/dist/client/shared/ui/Button/styles.module.scss +27 -4
- package/dist/client/shared/ui/Popup/Popup.d.ts +10 -4
- package/dist/client/shared/ui/Popup/Popup.js +4 -3
- package/dist/client/shared/ui/Select/Select.d.ts +9 -7
- package/dist/client/shared/ui/Select/Select.js +18 -17
- package/dist/client/shared/ui/Select/styles.module.scss +5 -0
- package/dist/client/widgets/translate-field-control/index.d.ts +1 -0
- package/dist/client/widgets/translate-field-control/index.js +3 -0
- package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.d.ts +26 -0
- package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.export.d.ts +13 -0
- package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.export.js +14 -0
- package/dist/client/widgets/translate-field-control/ui/TranslateFieldControl.js +204 -0
- package/dist/client/widgets/translate-field-control/ui/styles.module.scss +48 -0
- package/dist/field-actions.d.ts +25 -0
- package/dist/field-actions.js +23 -0
- package/dist/field-config.d.ts +26 -26
- package/dist/field-config.js +14 -37
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.d.ts +1 -1
- package/dist/plugin.js +3 -1
- package/dist/server/features/translate-field/handler.d.ts +17 -0
- package/dist/server/features/translate-field/handler.js +85 -0
- package/dist/server/features/translate-field/index.d.ts +6 -0
- package/dist/server/features/translate-field/index.js +5 -0
- package/dist/server/features/translate-field/model.d.ts +62 -0
- package/dist/server/features/translate-field/model.js +26 -0
- package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +39 -0
- package/dist/server/features/translate-field/resolveFieldSubtree.js +51 -0
- package/dist/server/features/translate-field/route.d.ts +12 -0
- package/dist/server/features/translate-field/route.js +18 -0
- package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +6 -0
- package/dist/server/modules/translation-levels/PluginConfigBuilder.js +4 -0
- package/dist/server/modules/translation-levels/fieldLevel.d.ts +23 -0
- package/dist/server/modules/translation-levels/fieldLevel.js +37 -0
- package/dist/server/modules/translation-levels/index.d.ts +1 -0
- package/dist/server/modules/translation-levels/index.js +1 -0
- package/dist/server/modules/translation-levels/types.d.ts +6 -1
- package/dist/server/modules/translation-levels/types.js +0 -1
- package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.d.ts +3 -9
- package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.js +76 -77
- package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.stage.d.ts +1 -1
- package/dist/server/modules/translation-pipeline/stages/data-reconciler/DataReconciler.stage.js +1 -1
- package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts +15 -21
- package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js +93 -105
- package/dist/server/modules/translation-providers/OpenAITranslation.provider.d.ts +17 -0
- package/dist/server/modules/translation-providers/OpenAITranslation.provider.js +8 -2
- package/dist/server/shared/field-config/types.js +1 -1
- package/dist/server/shared/field-traversal/findFieldByPath.d.ts +55 -0
- package/dist/server/shared/field-traversal/findFieldByPath.js +133 -0
- package/dist/server/shared/field-traversal/index.d.ts +5 -0
- package/dist/server/shared/field-traversal/index.js +5 -0
- package/dist/server/shared/field-traversal/kernel.d.ts +107 -0
- package/dist/server/shared/field-traversal/kernel.js +172 -0
- package/dist/server/shared/field-traversal/types.d.ts +191 -0
- package/dist/server/shared/field-traversal/types.js +41 -0
- package/dist/server/shared/field-traversal/walkFields.d.ts +51 -0
- package/dist/server/shared/field-traversal/walkFields.js +164 -0
- package/dist/server/shared/utils/filterLocalizedFields.d.ts +3 -3
- package/dist/server/shared/utils/filterLocalizedFields.js +52 -63
- package/package.json +3 -3
|
@@ -1,12 +1,76 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
|
-
import type { Field } from
|
|
2
|
-
import type {
|
|
3
|
-
import type {
|
|
1
|
+
import type { Field } from "payload";
|
|
2
|
+
import type { TranslationStrategy } from "../../strategies";
|
|
3
|
+
import type { FieldChunk } from "../../types";
|
|
4
4
|
/**
|
|
5
|
-
* Collects FieldChunks by
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
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
|
-
*
|
|
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
|
}
|
package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js
CHANGED
|
@@ -1,128 +1,116 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
5
|
-
*
|
|
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
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* -
|
|
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
|
-
*
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -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;
|