@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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { getByPath, ServerResponse } from "../../shared";
|
|
2
|
+
import { translateContent } from "../../modules/translation-pipeline";
|
|
3
|
+
import { FieldTranslationInputSchema, MAX_FIELD_VALUE_BYTES } from "./model";
|
|
4
|
+
import { resolveFieldSubtree } from "./resolveFieldSubtree";
|
|
5
|
+
const byteLength = (value)=>new TextEncoder().encode(JSON.stringify(value) ?? "").length;
|
|
6
|
+
const noop = (value, level, message)=>({
|
|
7
|
+
status: "noop",
|
|
8
|
+
value,
|
|
9
|
+
notice: {
|
|
10
|
+
level,
|
|
11
|
+
message
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
/**
|
|
15
|
+
* Synchronous single-field translation: read the field's value from the saved document in the
|
|
16
|
+
* chosen source locale, resolve the declared field path to its schema subtree, run
|
|
17
|
+
* `translateContent`, and return the translated value. No persistence — the result is written to
|
|
18
|
+
* form state by the caller.
|
|
19
|
+
*
|
|
20
|
+
* From-locale only: `source_lng` + `doc_id` are required (validated by the schema), so there is
|
|
21
|
+
* always exactly one DB read. Reserves HTTP errors for genuine errors — "nothing to translate"
|
|
22
|
+
* and "couldn't resolve the block" come back as a 200 `noop` with a notice.
|
|
23
|
+
*/ export class TranslateFieldHandler {
|
|
24
|
+
config;
|
|
25
|
+
constructor(config){
|
|
26
|
+
this.config = config;
|
|
27
|
+
}
|
|
28
|
+
async handle(req) {
|
|
29
|
+
const parsed = FieldTranslationInputSchema.safeParse(await req.json?.());
|
|
30
|
+
if (parsed.error) return ServerResponse.validationError(parsed.error.issues);
|
|
31
|
+
const { collection_slug, field_path, target_lng, source_lng, doc_id } = parsed.data;
|
|
32
|
+
const fields = this.config.schemaMap.get(collection_slug);
|
|
33
|
+
if (!fields) return ServerResponse.badRequest(`Collection "${collection_slug}" is not available for translation`);
|
|
34
|
+
// Read the source value from the saved document in `source_lng` (fallbackLocale: false so an
|
|
35
|
+
// empty source reads as empty → noop, not a fallback). The doc also lets the resolver
|
|
36
|
+
// disambiguate `blocks` — their `blockType` lives in the data.
|
|
37
|
+
const sourceDoc = await req.payload.findByID({
|
|
38
|
+
collection: collection_slug,
|
|
39
|
+
id: doc_id,
|
|
40
|
+
locale: source_lng,
|
|
41
|
+
fallbackLocale: false,
|
|
42
|
+
depth: 0
|
|
43
|
+
});
|
|
44
|
+
const sourceValue = getByPath(sourceDoc, field_path);
|
|
45
|
+
// Guard the *translated* payload (held synchronously through the provider call), not the
|
|
46
|
+
// request body, which now carries no field value.
|
|
47
|
+
if (byteLength(sourceValue) > MAX_FIELD_VALUE_BYTES) {
|
|
48
|
+
return ServerResponse.custom(`Field value exceeds the ${MAX_FIELD_VALUE_BYTES}-byte limit`, 413);
|
|
49
|
+
}
|
|
50
|
+
const resolution = resolveFieldSubtree(fields, field_path, sourceValue, sourceDoc);
|
|
51
|
+
if (resolution.status === "not-found") {
|
|
52
|
+
return ServerResponse.badRequest(`Field path "${field_path}" was not found in collection "${collection_slug}"`);
|
|
53
|
+
}
|
|
54
|
+
if (resolution.status === "inside-blocks") {
|
|
55
|
+
return ServerResponse.success(noop(sourceValue, "info", "Couldn't resolve the block for this field in the source document"));
|
|
56
|
+
}
|
|
57
|
+
if (resolution.status === "localized-list-ancestor") {
|
|
58
|
+
// Inside a localized blocks/array: its order/content is independent per locale, so the path
|
|
59
|
+
// index can't be matched to the source locale. Translate the whole document instead.
|
|
60
|
+
return ServerResponse.success(noop(sourceValue, "warning", "This field is inside a localized block — translate the whole document instead, so blocks stay aligned across locales"));
|
|
61
|
+
}
|
|
62
|
+
if (resolution.status === "not-translatable") {
|
|
63
|
+
return ServerResponse.success(noop(sourceValue, "info", "Nothing to translate in this field"));
|
|
64
|
+
}
|
|
65
|
+
// No `strategy`/`targetData`: a per-field translate is an explicit "translate this field now",
|
|
66
|
+
// so `translateContent` always overwrites (its default). skip_existing has no meaning here.
|
|
67
|
+
const translated = await translateContent({
|
|
68
|
+
schema: resolution.schema,
|
|
69
|
+
sourceData: resolution.sourceData,
|
|
70
|
+
sourceLng: source_lng,
|
|
71
|
+
targetLng: target_lng,
|
|
72
|
+
translationProvider: this.config.translationProvider
|
|
73
|
+
});
|
|
74
|
+
if (!translated) {
|
|
75
|
+
return ServerResponse.success(noop(sourceValue, "info", "Nothing to translate in this field"));
|
|
76
|
+
}
|
|
77
|
+
const result = {
|
|
78
|
+
status: "translated",
|
|
79
|
+
value: translated[resolution.fieldName]
|
|
80
|
+
};
|
|
81
|
+
return ServerResponse.success(result);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//# sourceMappingURL=handler.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { createFieldRoute } from "./route";
|
|
2
|
+
export type { CreateFieldRouteArgs } from "./route";
|
|
3
|
+
export { TranslateFieldHandler } from "./handler";
|
|
4
|
+
export { resolveFieldSubtree } from "./resolveFieldSubtree";
|
|
5
|
+
export type { FieldSubtreeResolution } from "./resolveFieldSubtree";
|
|
6
|
+
export type { FieldTranslationConfig, FieldTranslationInput, FieldTranslationNotice, FieldTranslationResult } from "./model";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { CollectionSchemaMap } from "../translate-document";
|
|
3
|
+
import type { TranslationProvider } from "../../modules/translation-providers";
|
|
4
|
+
/**
|
|
5
|
+
* Max serialized size of the value the endpoint will translate — the value read from the
|
|
6
|
+
* source-locale document (the request body no longer carries one; this is from-locale only). The
|
|
7
|
+
* field endpoint is synchronous — it holds the request for the whole translation — so an unbounded
|
|
8
|
+
* payload is a DoS surface. 256 KiB comfortably covers a long `richText` document; still a guard,
|
|
9
|
+
* not yet configurable.
|
|
10
|
+
*/
|
|
11
|
+
export declare const MAX_FIELD_VALUE_BYTES: number;
|
|
12
|
+
/**
|
|
13
|
+
* Request body for `POST {basePath}/field`. Snake_case to match the other translator routes.
|
|
14
|
+
*
|
|
15
|
+
* Single mode — **from locale**: the server reads the saved document (`doc_id`) in `source_lng`,
|
|
16
|
+
* takes its value at `field_path`, and translates it into `target_lng`. Both `source_lng` and
|
|
17
|
+
* `doc_id` are therefore required (a saved document). `doc_id` reuses {@link JobIdSchema} — the
|
|
18
|
+
* canonical id guard, ID-format-agnostic (string UUID/ObjectId or numeric autoincrement),
|
|
19
|
+
* normalized to a string.
|
|
20
|
+
*/
|
|
21
|
+
export declare const FieldTranslationInputSchema: z.ZodObject<{
|
|
22
|
+
collection_slug: z.ZodString;
|
|
23
|
+
field_path: z.ZodString;
|
|
24
|
+
target_lng: z.ZodString;
|
|
25
|
+
source_lng: z.ZodString;
|
|
26
|
+
doc_id: z.ZodEffects<z.ZodUnion<[z.ZodEffects<z.ZodString, string, string>, z.ZodNumber]>, string, string | number>;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
collection_slug: string;
|
|
29
|
+
source_lng: string;
|
|
30
|
+
target_lng: string;
|
|
31
|
+
field_path: string;
|
|
32
|
+
doc_id: string;
|
|
33
|
+
}, {
|
|
34
|
+
collection_slug: string;
|
|
35
|
+
source_lng: string;
|
|
36
|
+
target_lng: string;
|
|
37
|
+
field_path: string;
|
|
38
|
+
doc_id: string | number;
|
|
39
|
+
}>;
|
|
40
|
+
export type FieldTranslationInput = z.infer<typeof FieldTranslationInputSchema>;
|
|
41
|
+
/** Handler dependencies — the schema source of truth + the translation backend. */
|
|
42
|
+
export type FieldTranslationConfig = {
|
|
43
|
+
schemaMap: CollectionSchemaMap;
|
|
44
|
+
translationProvider: TranslationProvider;
|
|
45
|
+
};
|
|
46
|
+
export type FieldTranslationNotice = {
|
|
47
|
+
level: "info" | "warning";
|
|
48
|
+
message: string;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Successful response. Never an error for "couldn't translate": a field with no
|
|
52
|
+
* localized content (or a path our resolver can't handle yet) is a `noop` with a
|
|
53
|
+
* calm notice, not an HTTP error.
|
|
54
|
+
*/
|
|
55
|
+
export type FieldTranslationResult = {
|
|
56
|
+
status: "translated";
|
|
57
|
+
value: unknown;
|
|
58
|
+
} | {
|
|
59
|
+
status: "noop";
|
|
60
|
+
value: unknown;
|
|
61
|
+
notice: FieldTranslationNotice;
|
|
62
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { JobIdSchema } from "../../shared";
|
|
3
|
+
/**
|
|
4
|
+
* Max serialized size of the value the endpoint will translate — the value read from the
|
|
5
|
+
* source-locale document (the request body no longer carries one; this is from-locale only). The
|
|
6
|
+
* field endpoint is synchronous — it holds the request for the whole translation — so an unbounded
|
|
7
|
+
* payload is a DoS surface. 256 KiB comfortably covers a long `richText` document; still a guard,
|
|
8
|
+
* not yet configurable.
|
|
9
|
+
*/ export const MAX_FIELD_VALUE_BYTES = 256 * 1024;
|
|
10
|
+
/**
|
|
11
|
+
* Request body for `POST {basePath}/field`. Snake_case to match the other translator routes.
|
|
12
|
+
*
|
|
13
|
+
* Single mode — **from locale**: the server reads the saved document (`doc_id`) in `source_lng`,
|
|
14
|
+
* takes its value at `field_path`, and translates it into `target_lng`. Both `source_lng` and
|
|
15
|
+
* `doc_id` are therefore required (a saved document). `doc_id` reuses {@link JobIdSchema} — the
|
|
16
|
+
* canonical id guard, ID-format-agnostic (string UUID/ObjectId or numeric autoincrement),
|
|
17
|
+
* normalized to a string.
|
|
18
|
+
*/ export const FieldTranslationInputSchema = z.object({
|
|
19
|
+
collection_slug: z.string().nonempty(),
|
|
20
|
+
field_path: z.string().nonempty(),
|
|
21
|
+
target_lng: z.string().nonempty(),
|
|
22
|
+
source_lng: z.string().nonempty(),
|
|
23
|
+
doc_id: JobIdSchema
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=model.js.map
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Field } from "payload";
|
|
2
|
+
/**
|
|
3
|
+
* Outcome of mapping a declared field path to a translatable subtree.
|
|
4
|
+
*
|
|
5
|
+
* - `resolved` — the path lands on a translatable leaf; `schema` + `sourceData`
|
|
6
|
+
* are ready for `translateContent`, and `fieldName` is the key to unwrap the result.
|
|
7
|
+
* - `not-found` — the path resolves to no field at all (a typo) → caller returns 400.
|
|
8
|
+
* - `not-translatable` — resolves to a field that isn't a text-like leaf → caller no-ops.
|
|
9
|
+
* - `inside-blocks` — the path descends through a polymorphic `blocks` field that couldn't be
|
|
10
|
+
* resolved: no `doc` was supplied to read the element's `blockType` from → caller no-ops.
|
|
11
|
+
* - `localized-list-ancestor` — the path descends through a `localized` `blocks`/`array` field,
|
|
12
|
+
* whose per-locale order/content is independent, so a positional path can't be matched across
|
|
13
|
+
* locales → caller no-ops (translate the whole document instead).
|
|
14
|
+
*/
|
|
15
|
+
export type FieldSubtreeResolution = {
|
|
16
|
+
status: "resolved";
|
|
17
|
+
schema: Field[];
|
|
18
|
+
sourceData: Record<string, unknown>;
|
|
19
|
+
fieldName: string;
|
|
20
|
+
} | {
|
|
21
|
+
status: "not-found";
|
|
22
|
+
} | {
|
|
23
|
+
status: "not-translatable";
|
|
24
|
+
} | {
|
|
25
|
+
status: "inside-blocks";
|
|
26
|
+
} | {
|
|
27
|
+
status: "localized-list-ancestor";
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Map a declared `fieldPath` to the `{ schema, sourceData }` pair `translateContent` expects.
|
|
31
|
+
* Walks the schema via the shared {@link findFieldByPath}. Descends transparently through
|
|
32
|
+
* presentational containers (row, collapsible, unnamed tabs) and through named group/array/tab
|
|
33
|
+
* containers; array element indices select the data item but not the schema (shared across items).
|
|
34
|
+
*
|
|
35
|
+
* Pass `doc` (the document the path belongs to) to descend through polymorphic `blocks` fields:
|
|
36
|
+
* the element's `blockType` in `doc` picks the block schema. Without `doc`, a path through a
|
|
37
|
+
* `blocks` field returns `inside-blocks`.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resolveFieldSubtree(rootFields: Field[], fieldPath: string, value: unknown, doc?: unknown): FieldSubtreeResolution;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { isTranslatableField } from "../../shared";
|
|
2
|
+
import { findFieldByPath } from "../../shared/field-traversal";
|
|
3
|
+
/**
|
|
4
|
+
* Map a declared `fieldPath` to the `{ schema, sourceData }` pair `translateContent` expects.
|
|
5
|
+
* Walks the schema via the shared {@link findFieldByPath}. Descends transparently through
|
|
6
|
+
* presentational containers (row, collapsible, unnamed tabs) and through named group/array/tab
|
|
7
|
+
* containers; array element indices select the data item but not the schema (shared across items).
|
|
8
|
+
*
|
|
9
|
+
* Pass `doc` (the document the path belongs to) to descend through polymorphic `blocks` fields:
|
|
10
|
+
* the element's `blockType` in `doc` picks the block schema. Without `doc`, a path through a
|
|
11
|
+
* `blocks` field returns `inside-blocks`.
|
|
12
|
+
*/ export function resolveFieldSubtree(rootFields, fieldPath, value, doc) {
|
|
13
|
+
const segments = fieldPath.split(".").map((segment)=>segment.trim()).filter((segment)=>segment.length > 0);
|
|
14
|
+
if (segments.length === 0) return {
|
|
15
|
+
status: "not-found"
|
|
16
|
+
};
|
|
17
|
+
const result = findFieldByPath(rootFields, segments, doc);
|
|
18
|
+
switch(result.status){
|
|
19
|
+
case "leaf":
|
|
20
|
+
return isTranslatableField(result.field) ? {
|
|
21
|
+
status: "resolved",
|
|
22
|
+
schema: [
|
|
23
|
+
result.field
|
|
24
|
+
],
|
|
25
|
+
sourceData: {
|
|
26
|
+
[result.field.name]: value
|
|
27
|
+
},
|
|
28
|
+
fieldName: result.field.name
|
|
29
|
+
} : {
|
|
30
|
+
status: "not-translatable"
|
|
31
|
+
};
|
|
32
|
+
case "container":
|
|
33
|
+
return {
|
|
34
|
+
status: "not-translatable"
|
|
35
|
+
};
|
|
36
|
+
case "inside-blocks":
|
|
37
|
+
return {
|
|
38
|
+
status: "inside-blocks"
|
|
39
|
+
};
|
|
40
|
+
case "localized-list-ancestor":
|
|
41
|
+
return {
|
|
42
|
+
status: "localized-list-ancestor"
|
|
43
|
+
};
|
|
44
|
+
case "not-found":
|
|
45
|
+
return {
|
|
46
|
+
status: "not-found"
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=resolveFieldSubtree.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Endpoint } from "payload";
|
|
2
|
+
import type { AccessGuard } from "../../shared";
|
|
3
|
+
import type { FieldTranslationConfig } from "./model";
|
|
4
|
+
export type CreateFieldRouteArgs = FieldTranslationConfig & {
|
|
5
|
+
access?: AccessGuard;
|
|
6
|
+
basePath?: string;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Creates the synchronous field-translation endpoint: `POST {basePath}/field`.
|
|
10
|
+
* Wired with the same access + error-envelope plumbing as the document routes.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createFieldRoute({ schemaMap, translationProvider, access, basePath }: CreateFieldRouteArgs): Endpoint;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { withAccessCheck, withErrorHandler } from "../../shared";
|
|
2
|
+
import { TranslateFieldHandler } from "./handler";
|
|
3
|
+
/**
|
|
4
|
+
* Creates the synchronous field-translation endpoint: `POST {basePath}/field`.
|
|
5
|
+
* Wired with the same access + error-envelope plumbing as the document routes.
|
|
6
|
+
*/ export function createFieldRoute({ schemaMap, translationProvider, access, basePath = "/translate" }) {
|
|
7
|
+
const handler = new TranslateFieldHandler({
|
|
8
|
+
schemaMap,
|
|
9
|
+
translationProvider
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
path: `${basePath}/field`,
|
|
13
|
+
method: "post",
|
|
14
|
+
handler: withAccessCheck(withErrorHandler(handler.handle.bind(handler)), access)
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=route.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CollectionConfig, Config, Endpoint } from "payload";
|
|
2
2
|
import type { AccessGuard } from "../../../types/AccessGuard";
|
|
3
3
|
import type { RawPayloadComponentExport } from "../../../client/shared/types/PayloadComponentExport";
|
|
4
|
+
import type { CollectionSchemaMap } from "../../features/translate-document";
|
|
5
|
+
import type { TranslationProvider } from "../translation-providers";
|
|
4
6
|
import type { TaskRunnerFactory } from "../task-runner";
|
|
5
7
|
import type { CollectionAdminSlot, LevelContext } from "./types";
|
|
6
8
|
type ConfigModifier = (config: Config) => Config;
|
|
@@ -9,6 +11,8 @@ export type PluginConfigBuilderDeps = {
|
|
|
9
11
|
basePath: string;
|
|
10
12
|
access?: AccessGuard;
|
|
11
13
|
taskRunnerFactory: TaskRunnerFactory;
|
|
14
|
+
schemaMap: CollectionSchemaMap;
|
|
15
|
+
translationProvider: TranslationProvider;
|
|
12
16
|
};
|
|
13
17
|
/**
|
|
14
18
|
* The single place that mutates the Payload `config`. Levels (through the narrow
|
|
@@ -26,6 +30,8 @@ export declare class PluginConfigBuilder implements LevelContext {
|
|
|
26
30
|
readonly basePath: string;
|
|
27
31
|
readonly access?: AccessGuard;
|
|
28
32
|
readonly taskRunnerFactory: TaskRunnerFactory;
|
|
33
|
+
readonly schemaMap: CollectionSchemaMap;
|
|
34
|
+
readonly translationProvider: TranslationProvider;
|
|
29
35
|
private readonly endpoints;
|
|
30
36
|
private readonly collectionComponents;
|
|
31
37
|
private readonly adminProviders;
|
|
@@ -32,6 +32,8 @@ function attachToSlot(collection, slot, component) {
|
|
|
32
32
|
basePath;
|
|
33
33
|
access;
|
|
34
34
|
taskRunnerFactory;
|
|
35
|
+
schemaMap;
|
|
36
|
+
translationProvider;
|
|
35
37
|
endpoints = [];
|
|
36
38
|
collectionComponents = [];
|
|
37
39
|
adminProviders = [];
|
|
@@ -41,6 +43,8 @@ function attachToSlot(collection, slot, component) {
|
|
|
41
43
|
this.basePath = deps.basePath;
|
|
42
44
|
this.access = deps.access;
|
|
43
45
|
this.taskRunnerFactory = deps.taskRunnerFactory;
|
|
46
|
+
this.schemaMap = deps.schemaMap;
|
|
47
|
+
this.translationProvider = deps.translationProvider;
|
|
44
48
|
}
|
|
45
49
|
addEndpoints(endpoints) {
|
|
46
50
|
this.endpoints.push(...endpoints);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { TranslationLevel } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Field-level translation surface: a synchronous `POST {basePath}/field` endpoint that translates
|
|
4
|
+
* a single declared field **from a chosen source locale** — it reads the field's value from the
|
|
5
|
+
* saved document in that locale and translates it into the active locale. No runner, no queue, no
|
|
6
|
+
* persistence. Unlike `documentLevel` / `collectionLevel`, it contributes no admin component: the
|
|
7
|
+
* per-field control UI is wired separately at field-declaration time via `withFieldTranslation(field)`.
|
|
8
|
+
*
|
|
9
|
+
* Not part of the default `levels`, so it stays strictly opt-in.
|
|
10
|
+
*
|
|
11
|
+
* @returns an opaque {@link TranslationLevel} to list in `translatorPlugin({ levels })`
|
|
12
|
+
* @since 0.6.0
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* translatorPlugin({
|
|
16
|
+
* collections: [Posts],
|
|
17
|
+
* translationProvider: createOpenAIProvider({ apiKey: process.env.OPENAI_API_KEY }),
|
|
18
|
+
* runner: createPayloadJobsRunner(),
|
|
19
|
+
* levels: [documentLevel(), fieldLevel()],
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export declare function fieldLevel(): TranslationLevel;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createFieldRoute } from "../../features/translate-field";
|
|
2
|
+
/**
|
|
3
|
+
* Field-level translation surface: a synchronous `POST {basePath}/field` endpoint that translates
|
|
4
|
+
* a single declared field **from a chosen source locale** — it reads the field's value from the
|
|
5
|
+
* saved document in that locale and translates it into the active locale. No runner, no queue, no
|
|
6
|
+
* persistence. Unlike `documentLevel` / `collectionLevel`, it contributes no admin component: the
|
|
7
|
+
* per-field control UI is wired separately at field-declaration time via `withFieldTranslation(field)`.
|
|
8
|
+
*
|
|
9
|
+
* Not part of the default `levels`, so it stays strictly opt-in.
|
|
10
|
+
*
|
|
11
|
+
* @returns an opaque {@link TranslationLevel} to list in `translatorPlugin({ levels })`
|
|
12
|
+
* @since 0.6.0
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* translatorPlugin({
|
|
16
|
+
* collections: [Posts],
|
|
17
|
+
* translationProvider: createOpenAIProvider({ apiKey: process.env.OPENAI_API_KEY }),
|
|
18
|
+
* runner: createPayloadJobsRunner(),
|
|
19
|
+
* levels: [documentLevel(), fieldLevel()],
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/ export function fieldLevel() {
|
|
23
|
+
return {
|
|
24
|
+
extend (ctx) {
|
|
25
|
+
ctx.addEndpoints([
|
|
26
|
+
createFieldRoute({
|
|
27
|
+
schemaMap: ctx.schemaMap,
|
|
28
|
+
translationProvider: ctx.translationProvider,
|
|
29
|
+
access: ctx.access,
|
|
30
|
+
basePath: ctx.basePath
|
|
31
|
+
})
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
//# sourceMappingURL=fieldLevel.js.map
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CollectionConfig, Endpoint } from "payload";
|
|
2
2
|
import type { AccessGuard } from "../../../types/AccessGuard";
|
|
3
3
|
import type { RawPayloadComponentExport } from "../../../client/shared/types/PayloadComponentExport";
|
|
4
|
+
import type { CollectionSchemaMap } from "../../features/translate-document";
|
|
5
|
+
import type { TranslationProvider } from "../translation-providers";
|
|
4
6
|
import type { TaskRunnerFactory } from "../task-runner";
|
|
5
7
|
export type CollectionAdminSlot = "beforeDocumentControls" | "beforeListTable";
|
|
6
8
|
/**
|
|
@@ -27,7 +29,6 @@ export interface TranslationLevel {
|
|
|
27
29
|
*
|
|
28
30
|
* A level contributes via these generic primitives; it never mutates the raw
|
|
29
31
|
* Payload config. The plugin deduplicates endpoints by method + path on apply.
|
|
30
|
-
* (fieldLevel — Phase 2 — will also read `schemaMap` + `translationProvider`.)
|
|
31
32
|
* @internal
|
|
32
33
|
*/
|
|
33
34
|
export interface LevelContext {
|
|
@@ -35,6 +36,10 @@ export interface LevelContext {
|
|
|
35
36
|
readonly basePath: string;
|
|
36
37
|
readonly access?: AccessGuard;
|
|
37
38
|
readonly taskRunnerFactory: TaskRunnerFactory;
|
|
39
|
+
/** Deep-cloned localized field schema per managed collection slug. */
|
|
40
|
+
readonly schemaMap: CollectionSchemaMap;
|
|
41
|
+
/** The configured translation backend (used by the synchronous field level). */
|
|
42
|
+
readonly translationProvider: TranslationProvider;
|
|
38
43
|
/** Register endpoints. Deduplicated by method + path when applied. */
|
|
39
44
|
addEndpoints(endpoints: Endpoint[]): void;
|
|
40
45
|
/** Attach an admin component to a slot on every managed collection. */
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*
|
|
4
4
|
* A level contributes via these generic primitives; it never mutates the raw
|
|
5
5
|
* Payload config. The plugin deduplicates endpoints by method + path on apply.
|
|
6
|
-
* (fieldLevel — Phase 2 — will also read `schemaMap` + `translationProvider`.)
|
|
7
6
|
* @internal
|
|
8
7
|
*/ export { };
|
|
9
8
|
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import type { Field } from
|
|
1
|
+
import type { Field } from "payload";
|
|
2
2
|
/**
|
|
3
|
-
* Deep
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Logic:
|
|
7
|
-
* - If targetValue exists and is not empty → use targetValue
|
|
8
|
-
* - Otherwise → use sourceValue
|
|
3
|
+
* Deep-merges source and target data with target priority, producing the full document shape
|
|
4
|
+
* Payload validation needs. Built on the shared {@link walkFields} engine.
|
|
9
5
|
*
|
|
10
6
|
* Stage 1 of the translation pipeline.
|
|
11
7
|
*/
|
|
@@ -14,12 +10,10 @@ export declare class DataReconciler {
|
|
|
14
10
|
constructor(schema: Field[]);
|
|
15
11
|
/**
|
|
16
12
|
* Reconciles source and target data into a complete document shape.
|
|
17
|
-
* The result contains all fields needed for Payload validation.
|
|
18
13
|
*
|
|
19
14
|
* @param sourceData - Source locale document data
|
|
20
15
|
* @param targetData - Target locale document data (may be empty/partial)
|
|
21
16
|
* @returns Complete document shape with reconciled field values
|
|
22
17
|
*/
|
|
23
18
|
reconcile(sourceData: Record<string, unknown>, targetData: Record<string, unknown>): Record<string, unknown>;
|
|
24
|
-
private reconcileFields;
|
|
25
19
|
}
|