@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,22 +1,22 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import classNames from
|
|
3
|
-
import { forwardRef } from
|
|
4
|
-
import styles from
|
|
2
|
+
import classNames from "classnames";
|
|
3
|
+
import { forwardRef } from "react";
|
|
4
|
+
import styles from "./styles.module.scss";
|
|
5
5
|
const sizes = {
|
|
6
6
|
sm: {
|
|
7
|
-
height:
|
|
8
|
-
paddingInline:
|
|
9
|
-
fontSize:
|
|
7
|
+
height: "24px",
|
|
8
|
+
paddingInline: "0.25rem",
|
|
9
|
+
fontSize: "0.875rem"
|
|
10
10
|
},
|
|
11
11
|
md: {
|
|
12
|
-
height:
|
|
13
|
-
paddingInline:
|
|
14
|
-
fontSize:
|
|
12
|
+
height: "32px",
|
|
13
|
+
paddingInline: "0.5rem",
|
|
14
|
+
fontSize: "1rem"
|
|
15
15
|
},
|
|
16
16
|
lg: {
|
|
17
|
-
height:
|
|
18
|
-
paddingInline:
|
|
19
|
-
fontSize:
|
|
17
|
+
height: "40px",
|
|
18
|
+
paddingInline: "0.75rem",
|
|
19
|
+
fontSize: "1rem"
|
|
20
20
|
}
|
|
21
21
|
};
|
|
22
22
|
const SelectOption = ({ children, ...props })=>/*#__PURE__*/ _jsx("option", {
|
|
@@ -28,15 +28,16 @@ const SelectEmptyOption = ({ children, ...props })=>/*#__PURE__*/ _jsx("option",
|
|
|
28
28
|
...props,
|
|
29
29
|
children: children
|
|
30
30
|
});
|
|
31
|
-
const Select = /*#__PURE__*/ forwardRef(function Select({ children, className =
|
|
31
|
+
const Select = /*#__PURE__*/ forwardRef(function Select({ children, className = "", hasError = false, $size = "lg", $fullWidth = true, style, "aria-invalid": ariaInvalid, ...props }, ref) {
|
|
32
32
|
const _style = {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
"--height": sizes[$size].height,
|
|
34
|
+
"--padding-inline": sizes[$size].paddingInline,
|
|
35
|
+
"--font-size": sizes[$size].fontSize,
|
|
36
36
|
...style
|
|
37
37
|
};
|
|
38
38
|
const selectClassName = classNames(styles.select, styles[`size-${$size}`], {
|
|
39
|
-
[styles.selectError]: hasError
|
|
39
|
+
[styles.selectError]: hasError,
|
|
40
|
+
[styles.auto]: !$fullWidth
|
|
40
41
|
}, className);
|
|
41
42
|
return /*#__PURE__*/ _jsx("select", {
|
|
42
43
|
ref: ref,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TranslateFieldControlExport } from "./ui/TranslateFieldControl.export";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-field Translate control, injected into the field's `admin.components.beforeInput`.
|
|
3
|
+
* The trigger is an icon button; clicking it opens a compact popup that reuses the plugin's
|
|
4
|
+
* recognizable `source → target` direction pattern (as on the document/collection level):
|
|
5
|
+
* a small **source** locale `Select`, an arrow, then the **current** locale (the target).
|
|
6
|
+
*
|
|
7
|
+
* **Direction.** The target is *always* the locale the user is editing (`useLocale()`) — never
|
|
8
|
+
* derived from the picker; only the **source** is chosen. This is the reverse of the
|
|
9
|
+
* document/collection level (translate *from* the current locale *to* a chosen target): here we
|
|
10
|
+
* pull content *into* where the user stands. Deliberate, and field-level only for now.
|
|
11
|
+
*
|
|
12
|
+
* The source is an explicit locale (no "auto-detect": the field reads the chosen locale's *saved*
|
|
13
|
+
* value, so it needs a saved document — the control is **hidden entirely while creating** a new
|
|
14
|
+
* one). It defaults to Payload's configured `defaultLocale` when
|
|
15
|
+
* that isn't the current locale, otherwise the user picks. On success the result is written back
|
|
16
|
+
* (see {@link writeFieldValue}) and an **Undo** restores the previous value. Field `value` + `path`
|
|
17
|
+
* come from `useField()` via the field path context (`beforeInput` renders inside it — no `path`
|
|
18
|
+
* prop).
|
|
19
|
+
*
|
|
20
|
+
* **Write-back** is one path for every supported field type: a form `UPDATE` bumping both `value`
|
|
21
|
+
* and `initialValue`. A Lexical (`richText`) editor owns its state and only re-reads on
|
|
22
|
+
* `initialValue` change, so it re-mounts with the new content; `text` / `textarea` inputs just
|
|
23
|
+
* re-render from `value`. So no per-type flag is needed.
|
|
24
|
+
*/
|
|
25
|
+
declare const TranslateFieldControl: () => import("react/jsx-runtime").JSX.Element | null;
|
|
26
|
+
export default TranslateFieldControl;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RawPayloadComponentExport } from "../../../shared/types/PayloadComponentExport";
|
|
2
|
+
/**
|
|
3
|
+
* Import-map reference to the per-field Translate control. Appended to a field's
|
|
4
|
+
* `admin.components.beforeInput` by `withFieldTranslation(field)`, so it renders just above the
|
|
5
|
+
* input as an icon button that opens the translate popup.
|
|
6
|
+
*
|
|
7
|
+
* No props: the control reads everything from client context (field value/path via `useField()`,
|
|
8
|
+
* locale via `useLocale`, document via `useDocumentInfo`, available locales via `useConfig`) and
|
|
9
|
+
* writes back uniformly via a form `UPDATE` (works for text/textarea and richText alike).
|
|
10
|
+
*/
|
|
11
|
+
export declare class TranslateFieldControlExport implements RawPayloadComponentExport {
|
|
12
|
+
path: string;
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { clientComponentPath } from "../../../shared/utils/componentPath";
|
|
2
|
+
/**
|
|
3
|
+
* Import-map reference to the per-field Translate control. Appended to a field's
|
|
4
|
+
* `admin.components.beforeInput` by `withFieldTranslation(field)`, so it renders just above the
|
|
5
|
+
* input as an icon button that opens the translate popup.
|
|
6
|
+
*
|
|
7
|
+
* No props: the control reads everything from client context (field value/path via `useField()`,
|
|
8
|
+
* locale via `useLocale`, document via `useDocumentInfo`, available locales via `useConfig`) and
|
|
9
|
+
* writes back uniformly via a form `UPDATE` (works for text/textarea and richText alike).
|
|
10
|
+
*/ export class TranslateFieldControlExport {
|
|
11
|
+
path = clientComponentPath("widgets/translate-field-control/ui/TranslateFieldControl");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=TranslateFieldControl.export.js.map
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { toast, useConfig, useDocumentInfo, useField, useForm, useFormFields, useLocale } from "@payloadcms/ui";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { useTranslateField } from "../../../entities/translation/api/mutations/useTranslateField";
|
|
6
|
+
import { LanguageTranslateIcon } from "../../../shared/lib/assets/icons/LanguageTranslateIcon";
|
|
7
|
+
import { ReloadIcon } from "../../../shared/lib/assets/icons/ReloadIcon";
|
|
8
|
+
import { SendIcon } from "../../../shared/lib/assets/icons/SendIcon";
|
|
9
|
+
import { useLocaleOptions } from "../../../shared/lib/payload/hooks/useLocaleOptions";
|
|
10
|
+
import { useToggle } from "../../../shared/lib/utils/react/useToggle";
|
|
11
|
+
import Button from "../../../shared/ui/Button";
|
|
12
|
+
import Popup from "../../../shared/ui/Popup";
|
|
13
|
+
import Select from "../../../shared/ui/Select";
|
|
14
|
+
import Tooltip from "../../../shared/ui/Tooltip";
|
|
15
|
+
import styles from "./styles.module.scss";
|
|
16
|
+
/**
|
|
17
|
+
* Per-field Translate control, injected into the field's `admin.components.beforeInput`.
|
|
18
|
+
* The trigger is an icon button; clicking it opens a compact popup that reuses the plugin's
|
|
19
|
+
* recognizable `source → target` direction pattern (as on the document/collection level):
|
|
20
|
+
* a small **source** locale `Select`, an arrow, then the **current** locale (the target).
|
|
21
|
+
*
|
|
22
|
+
* **Direction.** The target is *always* the locale the user is editing (`useLocale()`) — never
|
|
23
|
+
* derived from the picker; only the **source** is chosen. This is the reverse of the
|
|
24
|
+
* document/collection level (translate *from* the current locale *to* a chosen target): here we
|
|
25
|
+
* pull content *into* where the user stands. Deliberate, and field-level only for now.
|
|
26
|
+
*
|
|
27
|
+
* The source is an explicit locale (no "auto-detect": the field reads the chosen locale's *saved*
|
|
28
|
+
* value, so it needs a saved document — the control is **hidden entirely while creating** a new
|
|
29
|
+
* one). It defaults to Payload's configured `defaultLocale` when
|
|
30
|
+
* that isn't the current locale, otherwise the user picks. On success the result is written back
|
|
31
|
+
* (see {@link writeFieldValue}) and an **Undo** restores the previous value. Field `value` + `path`
|
|
32
|
+
* come from `useField()` via the field path context (`beforeInput` renders inside it — no `path`
|
|
33
|
+
* prop).
|
|
34
|
+
*
|
|
35
|
+
* **Write-back** is one path for every supported field type: a form `UPDATE` bumping both `value`
|
|
36
|
+
* and `initialValue`. A Lexical (`richText`) editor owns its state and only re-reads on
|
|
37
|
+
* `initialValue` change, so it re-mounts with the new content; `text` / `textarea` inputs just
|
|
38
|
+
* re-render from `value`. So no per-type flag is needed.
|
|
39
|
+
*/ const TranslateFieldControl = ()=>{
|
|
40
|
+
const { value, path } = useField();
|
|
41
|
+
const locale = useLocale();
|
|
42
|
+
const { collectionSlug, id } = useDocumentInfo();
|
|
43
|
+
const { config } = useConfig();
|
|
44
|
+
const { setModified } = useForm();
|
|
45
|
+
const dispatchFields = useFormFields(([, dispatch])=>dispatch);
|
|
46
|
+
const localeOptions = useLocaleOptions();
|
|
47
|
+
const { mutateAsync, isPending } = useTranslateField();
|
|
48
|
+
// `config.localization` is `false | {…}`, so the truthy check is load-bearing (not just a nil guard).
|
|
49
|
+
const defaultLocale = config.localization ? config.localization.defaultLocale : undefined;
|
|
50
|
+
const [isOpen, popup] = useToggle();
|
|
51
|
+
const [sourceLng, setSourceLng] = useState(()=>defaultLocale && defaultLocale !== locale.code ? defaultLocale : "");
|
|
52
|
+
// `null` = no undo available; an object wraps the value so a legitimately `undefined` field value
|
|
53
|
+
// (a real pre-translation state) stays distinguishable from "nothing to undo".
|
|
54
|
+
const [undo, setUndo] = useState(null);
|
|
55
|
+
// fieldLevel is off while creating a document: from-locale reads the *saved* document's value,
|
|
56
|
+
// which doesn't exist yet — so the control is hidden entirely on a new/unsaved doc. (Re-enable
|
|
57
|
+
// once translating the unsaved value in place is supported.) All hooks above run unconditionally.
|
|
58
|
+
if (id === undefined || id === null) return null;
|
|
59
|
+
// Source options: every locale except the one being edited (it's the fixed target).
|
|
60
|
+
const sourceLocaleOptions = localeOptions.filter((option)=>option.value !== locale.code);
|
|
61
|
+
const canTranslate = sourceLng !== "" && !isPending;
|
|
62
|
+
// One write path for every supported field type: a form UPDATE bumping value AND initialValue.
|
|
63
|
+
// A Lexical (richText) editor re-mounts on the initialValue change so it shows the new content;
|
|
64
|
+
// text/textarea inputs just re-render from value. setModified keeps the form dirty/savable, since
|
|
65
|
+
// value === initialValue would otherwise read as unchanged.
|
|
66
|
+
const writeFieldValue = (next)=>{
|
|
67
|
+
dispatchFields({
|
|
68
|
+
type: "UPDATE",
|
|
69
|
+
path,
|
|
70
|
+
value: next,
|
|
71
|
+
initialValue: next
|
|
72
|
+
});
|
|
73
|
+
setModified(true);
|
|
74
|
+
};
|
|
75
|
+
const handleTranslate = async ()=>{
|
|
76
|
+
if (!(collectionSlug && canTranslate)) return;
|
|
77
|
+
const previous = value;
|
|
78
|
+
try {
|
|
79
|
+
const { data } = await mutateAsync({
|
|
80
|
+
collectionSlug,
|
|
81
|
+
fieldPath: path,
|
|
82
|
+
targetLng: locale.code,
|
|
83
|
+
sourceLng,
|
|
84
|
+
docId: id
|
|
85
|
+
});
|
|
86
|
+
if (data.status === "translated") {
|
|
87
|
+
setUndo({
|
|
88
|
+
value: previous
|
|
89
|
+
});
|
|
90
|
+
writeFieldValue(data.value);
|
|
91
|
+
toast.success("Field translated");
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const notify = data.notice.level === "warning" ? toast.warning : toast.info;
|
|
95
|
+
notify(data.notice.message);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
toast.error(error instanceof Error ? error.message : "Translation failed");
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const handleUndo = ()=>{
|
|
101
|
+
if (!undo) return;
|
|
102
|
+
writeFieldValue(undo.value);
|
|
103
|
+
setUndo(null);
|
|
104
|
+
};
|
|
105
|
+
return /*#__PURE__*/ _jsx(Popup, {
|
|
106
|
+
$align: "center",
|
|
107
|
+
$side: "right",
|
|
108
|
+
open: isOpen,
|
|
109
|
+
onOpenChange: popup.setValue,
|
|
110
|
+
// Don't auto-focus the first button on open — it's a Tooltip trigger, and Radix tooltips
|
|
111
|
+
// open on focus, so autofocus would flash the Translate button's tooltip immediately.
|
|
112
|
+
onOpenAutoFocus: (event)=>event.preventDefault(),
|
|
113
|
+
$trigger: /*#__PURE__*/ _jsx("div", {
|
|
114
|
+
className: styles.trigger,
|
|
115
|
+
children: /*#__PURE__*/ _jsx(Tooltip, {
|
|
116
|
+
content: "Translate field",
|
|
117
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
118
|
+
type: "button",
|
|
119
|
+
$variant: "light",
|
|
120
|
+
$size: "sm",
|
|
121
|
+
$isIconButton: true,
|
|
122
|
+
$isLoading: isPending && !isOpen,
|
|
123
|
+
"aria-label": "Translate field",
|
|
124
|
+
onClick: popup.setTrue,
|
|
125
|
+
children: /*#__PURE__*/ _jsx(LanguageTranslateIcon, {})
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
}),
|
|
129
|
+
children: /*#__PURE__*/ _jsx("div", {
|
|
130
|
+
className: styles.popup,
|
|
131
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
132
|
+
className: styles.row,
|
|
133
|
+
children: [
|
|
134
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
135
|
+
"aria-label": `Translation direction: from ${sourceLng || "a source locale"} into ${locale.code}`,
|
|
136
|
+
className: styles.direction,
|
|
137
|
+
role: "group",
|
|
138
|
+
children: [
|
|
139
|
+
/*#__PURE__*/ _jsxs(Select, {
|
|
140
|
+
$fullWidth: false,
|
|
141
|
+
$size: "sm",
|
|
142
|
+
"aria-label": "Source locale",
|
|
143
|
+
value: sourceLng,
|
|
144
|
+
onChange: (event)=>setSourceLng(event.target.value),
|
|
145
|
+
disabled: isPending,
|
|
146
|
+
children: [
|
|
147
|
+
sourceLng === "" && /*#__PURE__*/ _jsx(Select.SelectOption, {
|
|
148
|
+
value: "",
|
|
149
|
+
disabled: true,
|
|
150
|
+
children: "—"
|
|
151
|
+
}),
|
|
152
|
+
sourceLocaleOptions.map((option)=>/*#__PURE__*/ _jsx(Select.SelectOption, {
|
|
153
|
+
value: option.value,
|
|
154
|
+
children: option.value.toLowerCase()
|
|
155
|
+
}, option.value))
|
|
156
|
+
]
|
|
157
|
+
}),
|
|
158
|
+
/*#__PURE__*/ _jsx("span", {
|
|
159
|
+
"aria-hidden": "true",
|
|
160
|
+
className: styles.arrow,
|
|
161
|
+
children: "→"
|
|
162
|
+
}),
|
|
163
|
+
/*#__PURE__*/ _jsx("span", {
|
|
164
|
+
"aria-label": `Target locale: ${locale.code} (the locale you're editing)`,
|
|
165
|
+
className: styles.current,
|
|
166
|
+
children: locale.code.toLowerCase()
|
|
167
|
+
})
|
|
168
|
+
]
|
|
169
|
+
}),
|
|
170
|
+
/*#__PURE__*/ _jsx(Tooltip, {
|
|
171
|
+
content: "Translate",
|
|
172
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
173
|
+
type: "button",
|
|
174
|
+
$variant: "filled",
|
|
175
|
+
$size: "sm",
|
|
176
|
+
$isIconButton: true,
|
|
177
|
+
$isLoading: isPending,
|
|
178
|
+
disabled: !canTranslate,
|
|
179
|
+
"aria-label": `Translate from ${sourceLng || "the selected locale"} into ${locale.code}`,
|
|
180
|
+
onClick: handleTranslate,
|
|
181
|
+
children: /*#__PURE__*/ _jsx(SendIcon, {})
|
|
182
|
+
})
|
|
183
|
+
}),
|
|
184
|
+
undo && /*#__PURE__*/ _jsx(Tooltip, {
|
|
185
|
+
content: "Undo translation",
|
|
186
|
+
children: /*#__PURE__*/ _jsx(Button, {
|
|
187
|
+
type: "button",
|
|
188
|
+
$variant: "light",
|
|
189
|
+
$size: "sm",
|
|
190
|
+
$isIconButton: true,
|
|
191
|
+
"aria-label": "Undo translation",
|
|
192
|
+
disabled: isPending,
|
|
193
|
+
onClick: handleUndo,
|
|
194
|
+
children: /*#__PURE__*/ _jsx(ReloadIcon, {})
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
]
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
});
|
|
201
|
+
};
|
|
202
|
+
export default TranslateFieldControl;
|
|
203
|
+
|
|
204
|
+
//# sourceMappingURL=TranslateFieldControl.js.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
.popup {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: 0.375rem;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.row {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: 0.375rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* The from → to direction group; lays the source select, arrow and target chip out inline. */
|
|
14
|
+
.direction {
|
|
15
|
+
display: flex;
|
|
16
|
+
align-items: center;
|
|
17
|
+
gap: 0.375rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.arrow {
|
|
21
|
+
display: flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
color: var(--theme-elevation-400);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* The fixed target locale — framed like the source Select (same border/radius/height/font) for
|
|
27
|
+
a consistent two-box "source → target" look, but with a muted bg to read as read-only. */
|
|
28
|
+
.current {
|
|
29
|
+
display: inline-flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
box-sizing: border-box;
|
|
32
|
+
height: 24px;
|
|
33
|
+
padding-inline: 0.5rem;
|
|
34
|
+
border: 1px solid var(--theme-elevation-150);
|
|
35
|
+
border-radius: var(--style-radius-s);
|
|
36
|
+
background: var(--theme-elevation-50);
|
|
37
|
+
font-size: 0.875rem;
|
|
38
|
+
font-weight: 500;
|
|
39
|
+
color: var(--theme-elevation-800);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.trigger {
|
|
43
|
+
/* inline-flex so the wrapper hugs the icon button — a block/flex div stretches full width,
|
|
44
|
+
which drags the popover anchor across the field. */
|
|
45
|
+
display: inline-flex;
|
|
46
|
+
width: fit-content;
|
|
47
|
+
margin-bottom: 0.25rem;
|
|
48
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Field } from "payload";
|
|
2
|
+
import type { RawPayloadComponentExport } from "./client/shared/types/PayloadComponentExport";
|
|
3
|
+
/**
|
|
4
|
+
* Strategy for placing a per-field control into a field's admin config — the seam
|
|
5
|
+
* between *what* to render (a control component) and *where* it goes (positioning).
|
|
6
|
+
*
|
|
7
|
+
* Today the only implementation is {@link beforeInputPositioner}, built on Payload's
|
|
8
|
+
* native `admin.components.beforeInput` slot. A future field-slots plugin can supply its
|
|
9
|
+
* own positioner (e.g. placing the trigger in the field label, next to the locale) by
|
|
10
|
+
* implementing this same interface — the control component and the wiring in
|
|
11
|
+
* `withFieldTranslation` stay untouched. This is the connector we expose later; for now
|
|
12
|
+
* it lives internal, with the default below as the built-in fallback.
|
|
13
|
+
*
|
|
14
|
+
* @template T - The concrete field type, preserved through positioning.
|
|
15
|
+
* @param field - The field to attach the control to.
|
|
16
|
+
* @param control - Import-map reference to the control component.
|
|
17
|
+
* @returns The field with the control positioned.
|
|
18
|
+
*/
|
|
19
|
+
export type FieldControlPositioner = <T extends Field>(field: T, control: RawPayloadComponentExport) => T;
|
|
20
|
+
/**
|
|
21
|
+
* Default positioner: append the control to the field's `admin.components.beforeInput`,
|
|
22
|
+
* rendering it just above the input. `beforeInput` is an array slot, so we append —
|
|
23
|
+
* preserving any components already declared on the field.
|
|
24
|
+
*/
|
|
25
|
+
export declare const beforeInputPositioner: FieldControlPositioner;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default positioner: append the control to the field's `admin.components.beforeInput`,
|
|
3
|
+
* rendering it just above the input. `beforeInput` is an array slot, so we append —
|
|
4
|
+
* preserving any components already declared on the field.
|
|
5
|
+
*/ export const beforeInputPositioner = (field, control)=>{
|
|
6
|
+
const withAdmin = field;
|
|
7
|
+
const existing = withAdmin.admin?.components?.beforeInput ?? [];
|
|
8
|
+
return {
|
|
9
|
+
...field,
|
|
10
|
+
admin: {
|
|
11
|
+
...withAdmin.admin,
|
|
12
|
+
components: {
|
|
13
|
+
...withAdmin.admin?.components,
|
|
14
|
+
beforeInput: [
|
|
15
|
+
...existing,
|
|
16
|
+
control
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=field-actions.js.map
|
package/dist/field-config.d.ts
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import type { Field } from
|
|
2
|
-
import type { FieldTranslationConfig } from
|
|
1
|
+
import type { Field, RichTextField, TextareaField, TextField } from "payload";
|
|
2
|
+
import type { FieldTranslationConfig } from "./server/shared/field-config";
|
|
3
3
|
export type { FieldTranslationConfig };
|
|
4
|
+
/**
|
|
5
|
+
* Field types eligible for the per-field translate control — the same text-bearing
|
|
6
|
+
* leaves the server's `TranslatableField` handles. `richText` writes back by re-mounting
|
|
7
|
+
* the Lexical editor (see the control); `text` / `textarea` write straight to form state.
|
|
8
|
+
*/
|
|
9
|
+
type FieldControlCandidate = TextField | TextareaField | RichTextField;
|
|
4
10
|
/**
|
|
5
11
|
* @deprecated Use `FieldTranslationConfig` instead
|
|
6
12
|
*/
|
|
7
13
|
export type { FieldTranslationConfig as TranslateKitFieldConfig };
|
|
8
14
|
/**
|
|
9
|
-
*
|
|
15
|
+
* Configure a Payload field for translation.
|
|
10
16
|
*
|
|
11
|
-
*
|
|
17
|
+
* - **Add a translate control** — wrap a `text` / `textarea` field with no second
|
|
18
|
+
* argument. A per-field Translate control is positioned on the field (today, just
|
|
19
|
+
* above the input). Requires `fieldLevel()` in the plugin's `levels` to register the
|
|
20
|
+
* endpoint. Control-injection behavior is `@since 0.6.0`; field exclusion predates it.
|
|
21
|
+
* - **Exclude from translation** — pass `{ exclude: true }` (any field type). The field
|
|
22
|
+
* value is never sent to the translation provider and no control is added.
|
|
12
23
|
*
|
|
13
24
|
* @param field - The Payload field to configure
|
|
14
|
-
* @param config -
|
|
15
|
-
* @returns
|
|
25
|
+
* @param config - Pass `{ exclude: true }` to exclude; omit to add a control
|
|
26
|
+
* @returns A new field with the configuration applied (the input is not mutated)
|
|
16
27
|
*
|
|
17
28
|
* @example
|
|
18
29
|
* ```ts
|
|
19
30
|
* import { withFieldTranslation } from '@focus-reactive/payload-plugin-translator'
|
|
20
31
|
*
|
|
21
|
-
* //
|
|
22
|
-
* withFieldTranslation(
|
|
23
|
-
* { name: 'sku', type: 'text', localized: true },
|
|
24
|
-
* { exclude: true }
|
|
25
|
-
* )
|
|
26
|
-
* ```
|
|
32
|
+
* // Add a per-field translate control
|
|
33
|
+
* withFieldTranslation({ name: 'title', type: 'text', localized: true })
|
|
27
34
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* // In a collection definition
|
|
31
|
-
* const Products: CollectionConfig = {
|
|
32
|
-
* slug: 'products',
|
|
33
|
-
* fields: [
|
|
34
|
-
* { name: 'title', type: 'text', localized: true },
|
|
35
|
-
* withFieldTranslation(
|
|
36
|
-
* { name: 'sku', type: 'text', localized: true },
|
|
37
|
-
* { exclude: true }
|
|
38
|
-
* ),
|
|
39
|
-
* ],
|
|
40
|
-
* }
|
|
35
|
+
* // Exclude a field from translation
|
|
36
|
+
* withFieldTranslation({ name: 'sku', type: 'text', localized: true }, { exclude: true })
|
|
41
37
|
* ```
|
|
42
38
|
*/
|
|
43
|
-
export declare function withFieldTranslation<T extends Field>(field: T,
|
|
39
|
+
export declare function withFieldTranslation<T extends Field>(field: T, ...rest: T extends FieldControlCandidate ? [config?: {
|
|
40
|
+
exclude?: boolean;
|
|
41
|
+
}] : [config: {
|
|
42
|
+
exclude: true;
|
|
43
|
+
}]): T;
|
|
44
44
|
/**
|
|
45
45
|
* @deprecated Use `withFieldTranslation` instead
|
|
46
46
|
*/
|
package/dist/field-config.js
CHANGED
|
@@ -1,46 +1,23 @@
|
|
|
1
|
-
import { TRANSLATE_KIT_CUSTOM_KEY } from
|
|
1
|
+
import { TRANSLATE_KIT_CUSTOM_KEY } from "./server/shared/field-config";
|
|
2
|
+
import { TranslateFieldControlExport } from "./client/widgets/translate-field-control/ui/TranslateFieldControl.export";
|
|
3
|
+
import { beforeInputPositioner } from "./field-actions";
|
|
2
4
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```ts
|
|
13
|
-
* import { withFieldTranslation } from '@focus-reactive/payload-plugin-translator'
|
|
14
|
-
*
|
|
15
|
-
* // Exclude a field from translation
|
|
16
|
-
* withFieldTranslation(
|
|
17
|
-
* { name: 'sku', type: 'text', localized: true },
|
|
18
|
-
* { exclude: true }
|
|
19
|
-
* )
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* @example
|
|
23
|
-
* ```ts
|
|
24
|
-
* // In a collection definition
|
|
25
|
-
* const Products: CollectionConfig = {
|
|
26
|
-
* slug: 'products',
|
|
27
|
-
* fields: [
|
|
28
|
-
* { name: 'title', type: 'text', localized: true },
|
|
29
|
-
* withFieldTranslation(
|
|
30
|
-
* { name: 'sku', type: 'text', localized: true },
|
|
31
|
-
* { exclude: true }
|
|
32
|
-
* ),
|
|
33
|
-
* ],
|
|
34
|
-
* }
|
|
35
|
-
* ```
|
|
36
|
-
*/ export function withFieldTranslation(field, config) {
|
|
37
|
-
return {
|
|
5
|
+
* Where the translate control is placed. The single swap point: when a dedicated
|
|
6
|
+
* field-slots plugin lands, point this at its positioner (or take one as config) and
|
|
7
|
+
* the control moves — e.g. into the field label — without touching anything else.
|
|
8
|
+
* @see FieldControlPositioner
|
|
9
|
+
*/ const activeFieldControlPositioner = beforeInputPositioner;
|
|
10
|
+
export function withFieldTranslation(field, config) {
|
|
11
|
+
const configured = {
|
|
38
12
|
...field,
|
|
39
13
|
custom: {
|
|
40
14
|
...field.custom ?? {},
|
|
41
|
-
[TRANSLATE_KIT_CUSTOM_KEY]: config
|
|
15
|
+
[TRANSLATE_KIT_CUSTOM_KEY]: config ?? {}
|
|
42
16
|
}
|
|
43
17
|
};
|
|
18
|
+
// No `exclude` = control intent: position the control via the active positioner.
|
|
19
|
+
// `exclude: true` = no control (the field is left out of translation entirely).
|
|
20
|
+
return config?.exclude ? configured : activeFieldControlPositioner(configured, new TranslateFieldControlExport());
|
|
44
21
|
}
|
|
45
22
|
/**
|
|
46
23
|
* @deprecated Use `withFieldTranslation` instead
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { createOpenAIProvider } from "./server/modules/translation-providers";
|
|
|
5
5
|
export type { TranslationProvider, TranslationInput, TranslationOutput, OpenAIProviderConfig, DryRunConfig } from "./server/modules/translation-providers";
|
|
6
6
|
export { createPayloadJobsRunner, createSyncRunner } from "./server/modules/task-runner";
|
|
7
7
|
export type { TaskRunnerProvider, PayloadJobsRunnerOptions } from "./server/modules/task-runner";
|
|
8
|
-
export { documentLevel, collectionLevel } from "./server/modules/translation-levels";
|
|
8
|
+
export { documentLevel, collectionLevel, fieldLevel } from "./server/modules/translation-levels";
|
|
9
9
|
export type { TranslationLevel } from "./server/modules/translation-levels";
|
|
10
10
|
export { withFieldTranslation } from "./field-config";
|
|
11
11
|
export type { FieldTranslationConfig } from "./field-config";
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { createOpenAIProvider } from "./server/modules/translation-providers";
|
|
|
5
5
|
// Task runners
|
|
6
6
|
export { createPayloadJobsRunner, createSyncRunner } from "./server/modules/task-runner";
|
|
7
7
|
// Translation levels
|
|
8
|
-
export { documentLevel, collectionLevel } from "./server/modules/translation-levels";
|
|
8
|
+
export { documentLevel, collectionLevel, fieldLevel } from "./server/modules/translation-levels";
|
|
9
9
|
// Field config
|
|
10
10
|
export { withFieldTranslation } from "./field-config";
|
|
11
11
|
// Deprecated exports (for backwards compatibility)
|
package/dist/plugin.d.ts
CHANGED
|
@@ -35,7 +35,7 @@ export type TranslatorPluginConfig = {
|
|
|
35
35
|
basePath?: string;
|
|
36
36
|
/**
|
|
37
37
|
* Which translation surfaces to enable, as level factories — `documentLevel()`,
|
|
38
|
-
* `collectionLevel()
|
|
38
|
+
* `collectionLevel()`, `fieldLevel()`. Omit for the
|
|
39
39
|
* default `[documentLevel(), collectionLevel()]`, which is exactly today's behaviour.
|
|
40
40
|
* Each level should appear at most once: endpoints are deduplicated, but admin
|
|
41
41
|
* components are not, so a duplicated level renders a duplicated control.
|
package/dist/plugin.js
CHANGED
|
@@ -54,7 +54,9 @@ import { normalizePath } from "./server/shared";
|
|
|
54
54
|
collections,
|
|
55
55
|
basePath,
|
|
56
56
|
access,
|
|
57
|
-
taskRunnerFactory
|
|
57
|
+
taskRunnerFactory,
|
|
58
|
+
schemaMap,
|
|
59
|
+
translationProvider
|
|
58
60
|
});
|
|
59
61
|
for (const level of activeLevels)level.extend(builder);
|
|
60
62
|
// Plugin-level contributions, routed through the same single config-writer:
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PayloadRequest } from "payload";
|
|
2
|
+
import type { FieldTranslationConfig } from "./model";
|
|
3
|
+
/**
|
|
4
|
+
* Synchronous single-field translation: read the field's value from the saved document in the
|
|
5
|
+
* chosen source locale, resolve the declared field path to its schema subtree, run
|
|
6
|
+
* `translateContent`, and return the translated value. No persistence — the result is written to
|
|
7
|
+
* form state by the caller.
|
|
8
|
+
*
|
|
9
|
+
* From-locale only: `source_lng` + `doc_id` are required (validated by the schema), so there is
|
|
10
|
+
* always exactly one DB read. Reserves HTTP errors for genuine errors — "nothing to translate"
|
|
11
|
+
* and "couldn't resolve the block" come back as a 200 `noop` with a notice.
|
|
12
|
+
*/
|
|
13
|
+
export declare class TranslateFieldHandler {
|
|
14
|
+
private readonly config;
|
|
15
|
+
constructor(config: FieldTranslationConfig);
|
|
16
|
+
handle(req: PayloadRequest): Promise<Response>;
|
|
17
|
+
}
|