@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.
- package/README.md +44 -29
- package/dist/client/shared/ui/Button/Button.d.ts +4 -4
- package/dist/client/shared/ui/Button/Button.js +18 -18
- package/dist/client/shared/ui/Button/styles.module.scss +1 -1
- 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/index.d.ts +16 -14
- package/dist/index.js +9 -7
- package/dist/plugin.d.ts +15 -4
- package/dist/plugin.js +34 -57
- package/dist/server/features/cancel/handler.d.ts +2 -2
- package/dist/server/features/cancel/handler.js +3 -1
- package/dist/server/features/cancel/route.d.ts +4 -4
- package/dist/server/features/cancel/route.js +4 -4
- package/dist/server/features/cancel-by-collection/handler.d.ts +3 -3
- package/dist/server/features/cancel-by-collection/handler.js +1 -1
- package/dist/server/features/cancel-by-collection/route.d.ts +5 -5
- package/dist/server/features/cancel-by-collection/route.js +4 -4
- package/dist/server/features/createTranslationRoutes.d.ts +23 -0
- package/dist/server/features/createTranslationRoutes.js +27 -0
- package/dist/server/features/enqueue-translation/handler.d.ts +3 -3
- package/dist/server/features/enqueue-translation/handler.js +1 -1
- package/dist/server/features/enqueue-translation/route.d.ts +5 -5
- package/dist/server/features/enqueue-translation/route.js +4 -4
- package/dist/server/features/get-collection-status/handler.d.ts +3 -3
- package/dist/server/features/get-collection-status/handler.js +1 -1
- package/dist/server/features/get-collection-status/route.d.ts +5 -5
- package/dist/server/features/get-collection-status/route.js +4 -4
- package/dist/server/features/get-document-status/handler.d.ts +3 -3
- package/dist/server/features/get-document-status/handler.js +1 -1
- package/dist/server/features/get-document-status/route.d.ts +5 -5
- package/dist/server/features/get-document-status/route.js +4 -4
- package/dist/server/features/index.d.ts +7 -6
- package/dist/server/features/index.js +7 -6
- package/dist/server/features/run-translation/handler.d.ts +2 -2
- package/dist/server/features/run-translation/route.d.ts +4 -4
- package/dist/server/features/run-translation/route.js +4 -4
- package/dist/server/features/translate-document/handler.js +7 -11
- package/dist/server/features/translate-field/resolveFieldSubtree.d.ts +31 -0
- package/dist/server/features/translate-field/resolveFieldSubtree.js +45 -0
- package/dist/server/modules/task-runner/TaskRunnerProvider.interface.d.ts +18 -1
- package/dist/server/modules/task-runner/index.d.ts +6 -6
- package/dist/server/modules/task-runner/index.js +2 -2
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.d.ts +16 -1
- package/dist/server/modules/task-runner/payload-jobs-runner/PayloadJobsRunnerProvider.js +16 -1
- package/dist/server/modules/task-runner/sync-runner/SyncRunnerProvider.d.ts +18 -10
- package/dist/server/modules/task-runner/sync-runner/SyncRunnerProvider.js +24 -14
- package/dist/server/modules/translation-levels/PluginConfigBuilder.d.ts +46 -0
- package/dist/server/modules/translation-levels/PluginConfigBuilder.js +108 -0
- package/dist/server/modules/translation-levels/collectionLevel.d.ts +15 -0
- package/dist/server/modules/translation-levels/collectionLevel.js +24 -0
- package/dist/server/modules/translation-levels/documentLevel.d.ts +19 -0
- package/dist/server/modules/translation-levels/documentLevel.js +28 -0
- package/dist/server/modules/translation-levels/index.d.ts +3 -0
- package/dist/server/modules/translation-levels/index.js +4 -0
- package/dist/server/modules/translation-levels/types.d.ts +42 -0
- package/dist/server/modules/translation-levels/types.js +10 -0
- package/dist/server/modules/translation-levels/useDocTranslationApi.d.ts +8 -0
- package/dist/server/modules/translation-levels/useDocTranslationApi.js +18 -0
- package/dist/server/modules/translation-pipeline/index.d.ts +5 -3
- package/dist/server/modules/translation-pipeline/index.js +3 -2
- 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 +71 -78
- package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts +11 -22
- package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js +85 -106
- package/dist/server/modules/translation-pipeline/translateContent.d.ts +35 -0
- package/dist/server/modules/translation-pipeline/translateContent.js +30 -0
- package/dist/server/modules/translation-providers/OpenAITranslation.provider.d.ts +4 -3
- package/dist/server/modules/translation-providers/OpenAITranslation.provider.js +15 -15
- package/dist/server/shared/field-traversal/findFieldByPath.d.ts +40 -0
- package/dist/server/shared/field-traversal/findFieldByPath.js +88 -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 +85 -0
- package/dist/server/shared/field-traversal/kernel.js +145 -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/dist/types/AccessGuard.d.ts +24 -1
- package/dist/types/AccessGuard.js +12 -1
- package/package.json +2 -2
|
@@ -1,12 +1,70 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { isEmpty, isObject } from "../../../../shared";
|
|
2
|
+
import { resolveBlockFields, walkFields } from "../../../../shared/field-traversal";
|
|
3
|
+
const asObject = (value)=>isObject(value) ? value : {};
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
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
|
-
|
|
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;
|
|
82
|
+
const root = {
|
|
83
|
+
source: sourceData,
|
|
84
|
+
target: targetData ?? {}
|
|
85
|
+
};
|
|
86
|
+
return walkFields(this.schema, root, reconcileWalker) ?? {};
|
|
94
87
|
}
|
|
95
88
|
}
|
|
96
89
|
|
package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.d.ts
CHANGED
|
@@ -1,18 +1,15 @@
|
|
|
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
|
-
* - 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
|
}
|
package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js
CHANGED
|
@@ -1,128 +1,107 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
* - 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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
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
|
|
2
|
-
import type { ChatModel } from
|
|
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
|
|
2
|
-
import { isObject } from
|
|
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(
|
|
14
|
+
console.info("[DRY RUN] Translation simulation:", {
|
|
15
15
|
content,
|
|
16
16
|
sourceLang: souceLng,
|
|
17
17
|
targetLang: targetLng,
|
|
18
|
-
provider:
|
|
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:
|
|
29
|
+
role: "system",
|
|
30
30
|
content: systemPrompt
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
|
-
role:
|
|
33
|
+
role: "user",
|
|
34
34
|
content: JSON.stringify(content)
|
|
35
35
|
}
|
|
36
36
|
],
|
|
37
|
-
model: this.config.model ??
|
|
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:
|
|
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}` :
|
|
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 ===
|
|
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(
|
|
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 ===
|
|
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 ===
|
|
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;
|