@focus-reactive/payload-plugin-translator 0.5.1 → 0.6.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 +191 -287
- 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 +2 -1
- package/dist/client/shared/ui/Button/Button.js +1 -1
- package/dist/client/shared/ui/Button/styles.module.scss +26 -3
- 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 +15 -7
- package/dist/server/features/translate-field/resolveFieldSubtree.js +14 -8
- 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.js +10 -4
- 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 +5 -0
- package/dist/server/modules/translation-pipeline/stages/field-collector/FieldChunkCollector.js +13 -4
- 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 +25 -10
- package/dist/server/shared/field-traversal/findFieldByPath.js +59 -14
- package/dist/server/shared/field-traversal/index.d.ts +1 -1
- package/dist/server/shared/field-traversal/index.js +1 -1
- package/dist/server/shared/field-traversal/kernel.d.ts +22 -0
- package/dist/server/shared/field-traversal/kernel.js +27 -0
- package/package.json +2 -2
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { FieldTranslationResult } from "../../../../../server/features/translate-field";
|
|
2
|
+
type Variables = {
|
|
3
|
+
collectionSlug: string;
|
|
4
|
+
fieldPath: string;
|
|
5
|
+
targetLng: string;
|
|
6
|
+
/** Source locale to translate from — the server reads its saved value. */
|
|
7
|
+
sourceLng: string;
|
|
8
|
+
/** Saved document id the source value is read from. */
|
|
9
|
+
docId: string | number;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Calls the synchronous field-translation endpoint (POST {basePath}/field).
|
|
13
|
+
* Returns the discriminated result; no cache invalidation — the caller writes
|
|
14
|
+
* the value straight to form state, nothing is persisted.
|
|
15
|
+
*
|
|
16
|
+
* From-locale only: the server reads the field's value from the saved document (`docId`) in
|
|
17
|
+
* `sourceLng` and translates it into the active locale.
|
|
18
|
+
*/
|
|
19
|
+
export declare function useTranslateField(): import("@tanstack/react-query").UseMutationResult<{
|
|
20
|
+
data: FieldTranslationResult;
|
|
21
|
+
}, Error, Variables, unknown>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { useMutation } from "@tanstack/react-query";
|
|
2
|
+
import { ofetch } from "ofetch";
|
|
3
|
+
import { useTranslateKitConfig } from "../../../../app/config";
|
|
4
|
+
import { handleNextApiError } from "../../../../shared/lib/errors/handleApiError";
|
|
5
|
+
/**
|
|
6
|
+
* Calls the synchronous field-translation endpoint (POST {basePath}/field).
|
|
7
|
+
* Returns the discriminated result; no cache invalidation — the caller writes
|
|
8
|
+
* the value straight to form state, nothing is persisted.
|
|
9
|
+
*
|
|
10
|
+
* From-locale only: the server reads the field's value from the saved document (`docId`) in
|
|
11
|
+
* `sourceLng` and translates it into the active locale.
|
|
12
|
+
*/ export function useTranslateField() {
|
|
13
|
+
const { basePath } = useTranslateKitConfig();
|
|
14
|
+
return useMutation({
|
|
15
|
+
mutationKey: [
|
|
16
|
+
"translate-field"
|
|
17
|
+
],
|
|
18
|
+
mutationFn: (variables)=>handleNextApiError(()=>ofetch(`/api${basePath}/field`, {
|
|
19
|
+
method: "post",
|
|
20
|
+
body: {
|
|
21
|
+
collection_slug: variables.collectionSlug,
|
|
22
|
+
field_path: variables.fieldPath,
|
|
23
|
+
target_lng: variables.targetLng,
|
|
24
|
+
source_lng: variables.sourceLng,
|
|
25
|
+
doc_id: variables.docId
|
|
26
|
+
}
|
|
27
|
+
}))
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=useTranslateField.js.map
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { UseFormReturn } from
|
|
2
|
-
import type { FormValues } from
|
|
1
|
+
import type { UseFormReturn } from "react-hook-form";
|
|
2
|
+
import type { FormValues } from "../model/schema";
|
|
3
3
|
type CollectionTranslationFormProps = {
|
|
4
4
|
form: UseFormReturn<FormValues>;
|
|
5
5
|
onSubmit: (values: FormValues) => Promise<void>;
|
|
6
6
|
selectedCount: number;
|
|
7
7
|
hasDrafts: boolean;
|
|
8
8
|
};
|
|
9
|
-
export declare function CollectionTranslationForm({ form, onSubmit, selectedCount, hasDrafts
|
|
9
|
+
export declare function CollectionTranslationForm({ form, onSubmit, selectedCount, hasDrafts }: CollectionTranslationFormProps): import("react/jsx-runtime").JSX.Element;
|
|
10
10
|
export {};
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { FormProvider } from
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
2
|
+
import { FormProvider } from "react-hook-form";
|
|
3
|
+
import { SendIcon } from "../../../shared/lib/assets/icons/SendIcon";
|
|
4
|
+
import Button from "../../../shared/ui/Button";
|
|
5
|
+
import FormSelectLocale from "../../../shared/ui/form/FormSelectLocale";
|
|
6
|
+
import { FormSelectStrategy } from "../../../shared/ui/form/FormSelectStrategy";
|
|
7
|
+
import { FormCheckboxPublish } from "../../../shared/ui/form/FormCheckboxPublish";
|
|
8
|
+
import { FORM_FIELDS } from "../model/constants";
|
|
9
|
+
import styles from "./styles.module.scss";
|
|
9
10
|
export function CollectionTranslationForm({ form, onSubmit, selectedCount, hasDrafts }) {
|
|
10
11
|
return /*#__PURE__*/ _jsx(FormProvider, {
|
|
11
12
|
...form,
|
|
@@ -16,24 +17,24 @@ export function CollectionTranslationForm({ form, onSubmit, selectedCount, hasDr
|
|
|
16
17
|
className: styles.row,
|
|
17
18
|
children: [
|
|
18
19
|
/*#__PURE__*/ _jsxs("div", {
|
|
19
|
-
className: styles[
|
|
20
|
+
className: styles["locale-group"],
|
|
20
21
|
children: [
|
|
21
22
|
/*#__PURE__*/ _jsx(FormSelectLocale, {
|
|
22
23
|
label: "From",
|
|
23
|
-
className: styles[
|
|
24
|
+
className: styles["select-locale-field"],
|
|
24
25
|
size: "md",
|
|
25
26
|
name: FORM_FIELDS.SOURCE_LNG
|
|
26
27
|
}),
|
|
27
28
|
/*#__PURE__*/ _jsx(FormSelectLocale, {
|
|
28
29
|
label: "To",
|
|
29
|
-
className: styles[
|
|
30
|
+
className: styles["select-locale-field"],
|
|
30
31
|
size: "md",
|
|
31
32
|
name: FORM_FIELDS.TARGET_LNG
|
|
32
33
|
})
|
|
33
34
|
]
|
|
34
35
|
}),
|
|
35
36
|
/*#__PURE__*/ _jsx(FormSelectStrategy, {
|
|
36
|
-
className: styles[
|
|
37
|
+
className: styles["strategy-field"],
|
|
37
38
|
name: FORM_FIELDS.STRATEGY,
|
|
38
39
|
size: "md"
|
|
39
40
|
})
|
|
@@ -49,12 +50,13 @@ export function CollectionTranslationForm({ form, onSubmit, selectedCount, hasDr
|
|
|
49
50
|
/*#__PURE__*/ _jsx(Button, {
|
|
50
51
|
$variant: "filled",
|
|
51
52
|
disabled: form.formState.isSubmitting || selectedCount === 0,
|
|
52
|
-
className: styles[
|
|
53
|
+
className: styles["submit-button"],
|
|
53
54
|
onClick: form.handleSubmit(onSubmit),
|
|
54
55
|
type: "submit",
|
|
55
56
|
$size: "md",
|
|
56
57
|
$isLoading: form.formState.isSubmitting,
|
|
57
|
-
|
|
58
|
+
$startContent: /*#__PURE__*/ _jsx(SendIcon, {}),
|
|
59
|
+
children: selectedCount ? `Queue ${selectedCount} translations` : "Queue translations"
|
|
58
60
|
})
|
|
59
61
|
]
|
|
60
62
|
})
|
package/dist/client/features/collection-translation-popup/ui/CollectionTranslationPopup.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { PropsWithChildren } from
|
|
1
|
+
import type { PropsWithChildren } from "react";
|
|
2
2
|
type CollectionTranslationPopupProps = PropsWithChildren<{
|
|
3
3
|
translationInProgress: boolean;
|
|
4
4
|
selectedCount: number;
|
|
5
5
|
}>;
|
|
6
|
-
declare function CollectionTranslationPopup({ children, translationInProgress, selectedCount
|
|
6
|
+
declare function CollectionTranslationPopup({ children, translationInProgress, selectedCount }: CollectionTranslationPopupProps): import("react/jsx-runtime").JSX.Element;
|
|
7
7
|
export default CollectionTranslationPopup;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { LanguageTranslateIcon } from
|
|
4
|
-
import { useToggle } from
|
|
5
|
-
import Button from
|
|
6
|
-
import ColorIndicator from
|
|
7
|
-
import Popup from
|
|
8
|
-
import styles from
|
|
3
|
+
import { LanguageTranslateIcon } from "../../../shared/lib/assets/icons/LanguageTranslateIcon";
|
|
4
|
+
import { useToggle } from "../../../shared/lib/utils/react/useToggle";
|
|
5
|
+
import Button from "../../../shared/ui/Button";
|
|
6
|
+
import ColorIndicator from "../../../shared/ui/ColorIndicator";
|
|
7
|
+
import Popup from "../../../shared/ui/Popup";
|
|
8
|
+
import styles from "./styles.module.scss";
|
|
9
9
|
function CollectionTranslationPopup({ children, translationInProgress, selectedCount }) {
|
|
10
10
|
const [isPopupOpen, popupOpen] = useToggle();
|
|
11
11
|
return /*#__PURE__*/ _jsx(Popup, {
|
|
@@ -13,8 +13,8 @@ function CollectionTranslationPopup({ children, translationInProgress, selectedC
|
|
|
13
13
|
onOpenChange: popupOpen.setValue,
|
|
14
14
|
$trigger: /*#__PURE__*/ _jsxs(Button, {
|
|
15
15
|
$size: "md",
|
|
16
|
-
$variant: "
|
|
17
|
-
className: styles[
|
|
16
|
+
$variant: "outlined-light",
|
|
17
|
+
className: styles["popup-trigger-button"],
|
|
18
18
|
"aria-label": "Open translation options",
|
|
19
19
|
onClick: popupOpen.setTrue,
|
|
20
20
|
children: [
|
|
@@ -38,7 +38,7 @@ function CollectionTranslationPopup({ children, translationInProgress, selectedC
|
|
|
38
38
|
}),
|
|
39
39
|
open: isPopupOpen,
|
|
40
40
|
children: /*#__PURE__*/ _jsx("div", {
|
|
41
|
-
className: styles[
|
|
41
|
+
className: styles["popup-content"],
|
|
42
42
|
children: children
|
|
43
43
|
})
|
|
44
44
|
});
|
package/dist/client/features/open-document-translation-popup/ui/OpenDocumentTranslationPopup.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { LanguageTranslateIcon } from
|
|
4
|
-
import { useToggle } from
|
|
5
|
-
import Button from
|
|
6
|
-
import Popup from
|
|
7
|
-
import styles from
|
|
3
|
+
import { LanguageTranslateIcon } from "../../../shared/lib/assets/icons/LanguageTranslateIcon";
|
|
4
|
+
import { useToggle } from "../../../shared/lib/utils/react/useToggle";
|
|
5
|
+
import Button from "../../../shared/ui/Button";
|
|
6
|
+
import Popup from "../../../shared/ui/Popup";
|
|
7
|
+
import styles from "./styles.module.scss";
|
|
8
8
|
function OpenDocumentTranslationPopup({ children, isLoading }) {
|
|
9
9
|
const [isPopupOpen, popupOpen] = useToggle();
|
|
10
10
|
return /*#__PURE__*/ _jsx(Popup, {
|
|
@@ -12,9 +12,9 @@ function OpenDocumentTranslationPopup({ children, isLoading }) {
|
|
|
12
12
|
onOpenChange: popupOpen.setValue,
|
|
13
13
|
$trigger: /*#__PURE__*/ _jsx(Button, {
|
|
14
14
|
$size: "md",
|
|
15
|
-
$variant: "
|
|
15
|
+
$variant: "outlined-light",
|
|
16
16
|
$isIconButton: true,
|
|
17
|
-
className: styles[
|
|
17
|
+
className: styles["popup-trigger-button"],
|
|
18
18
|
"aria-label": "Open translation options",
|
|
19
19
|
onClick: popupOpen.setTrue,
|
|
20
20
|
disabled: isLoading,
|
|
@@ -23,7 +23,7 @@ function OpenDocumentTranslationPopup({ children, isLoading }) {
|
|
|
23
23
|
}),
|
|
24
24
|
open: isPopupOpen,
|
|
25
25
|
children: /*#__PURE__*/ _jsx("div", {
|
|
26
|
-
className: styles[
|
|
26
|
+
className: styles["popup-content"],
|
|
27
27
|
children: children({
|
|
28
28
|
close: popupOpen.setFalse
|
|
29
29
|
})
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { UseFormReturn } from
|
|
2
|
-
import type { FormValues } from
|
|
1
|
+
import type { UseFormReturn } from "react-hook-form";
|
|
2
|
+
import type { FormValues } from "../model/schema";
|
|
3
3
|
type DocumentTranslationFormProps = {
|
|
4
4
|
form: UseFormReturn<FormValues>;
|
|
5
5
|
onSubmit: (values: FormValues) => Promise<void>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { FormProvider } from
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
2
|
+
import { FormProvider } from "react-hook-form";
|
|
3
|
+
import { SendIcon } from "../../../shared/lib/assets/icons/SendIcon";
|
|
4
|
+
import Button from "../../../shared/ui/Button";
|
|
5
|
+
import FormSelectLocale from "../../../shared/ui/form/FormSelectLocale";
|
|
6
|
+
import { FormSelectStrategy } from "../../../shared/ui/form/FormSelectStrategy";
|
|
7
|
+
import { FormCheckboxPublish } from "../../../shared/ui/form/FormCheckboxPublish";
|
|
8
|
+
import { FORM_FIELDS } from "../model/constants";
|
|
9
|
+
import styles from "./styles.module.scss";
|
|
9
10
|
export function DocumentTranslationForm({ form, onSubmit, hasDrafts }) {
|
|
10
11
|
return /*#__PURE__*/ _jsx(FormProvider, {
|
|
11
12
|
...form,
|
|
@@ -42,11 +43,12 @@ export function DocumentTranslationForm({ form, onSubmit, hasDrafts }) {
|
|
|
42
43
|
/*#__PURE__*/ _jsx(Button, {
|
|
43
44
|
$variant: "filled",
|
|
44
45
|
disabled: form.formState.isSubmitting,
|
|
45
|
-
className: styles[
|
|
46
|
+
className: styles["submit-button"],
|
|
46
47
|
onClick: form.handleSubmit(onSubmit),
|
|
47
48
|
type: "submit",
|
|
48
49
|
$size: "lg",
|
|
49
50
|
$isLoading: form.formState.isSubmitting,
|
|
51
|
+
$startContent: /*#__PURE__*/ _jsx(SendIcon, {}),
|
|
50
52
|
children: "Queue Translation"
|
|
51
53
|
})
|
|
52
54
|
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SendIcon: () => import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export const SendIcon = ()=>/*#__PURE__*/ _jsx("svg", {
|
|
3
|
+
width: "1em",
|
|
4
|
+
height: "1em",
|
|
5
|
+
viewBox: "0 0 24 24",
|
|
6
|
+
fill: "none",
|
|
7
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
8
|
+
children: /*#__PURE__*/ _jsx("path", {
|
|
9
|
+
d: "M10.3009 13.6949L20.102 3.89742M10.5795 14.1355L12.8019 18.5804C13.339 19.6545 13.6075 20.1916 13.9458 20.3356C14.2394 20.4606 14.575 20.4379 14.8492 20.2747C15.1651 20.0866 15.3591 19.5183 15.7472 18.3818L19.9463 6.08434C20.2845 5.09409 20.4535 4.59896 20.3378 4.27142C20.2371 3.98648 20.013 3.76234 19.7281 3.66167C19.4005 3.54595 18.9054 3.71502 17.9151 4.05315L5.61763 8.2523C4.48114 8.64037 3.91289 8.83441 3.72478 9.15032C3.56153 9.42447 3.53891 9.76007 3.66389 10.0536C3.80791 10.3919 4.34498 10.6605 5.41912 11.1975L9.86397 13.42C10.041 13.5085 10.1295 13.5527 10.2061 13.6118C10.2742 13.6643 10.3352 13.7253 10.3876 13.7933C10.4468 13.87 10.491 13.9585 10.5795 14.1355Z",
|
|
10
|
+
stroke: "currentColor",
|
|
11
|
+
strokeWidth: "2",
|
|
12
|
+
strokeLinecap: "round",
|
|
13
|
+
strokeLinejoin: "round"
|
|
14
|
+
})
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=SendIcon.js.map
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ComponentPropsWithRef, ReactNode } from "react";
|
|
2
2
|
type ButtonProps = ComponentPropsWithRef<"button"> & {
|
|
3
3
|
$fullWidth?: boolean;
|
|
4
|
-
$variant?: "outlined" | "filled" | "unstyled" | "transparent" | "light";
|
|
4
|
+
$variant?: "outlined" | "outlined-light" | "filled" | "unstyled" | "transparent" | "light";
|
|
5
|
+
/** Leading content (e.g. an icon) before children. Hidden while `$isLoading` — the spinner replaces it. */
|
|
5
6
|
$startContent?: ReactNode;
|
|
6
7
|
$size?: "sm" | "md" | "lg";
|
|
7
8
|
$isLoading?: boolean;
|
|
@@ -32,7 +32,7 @@ const Button = /*#__PURE__*/ forwardRef(function Button({ className = "", childr
|
|
|
32
32
|
ref: ref,
|
|
33
33
|
className: buttonClassName,
|
|
34
34
|
children: [
|
|
35
|
-
$startContent && /*#__PURE__*/ _jsx("div", {
|
|
35
|
+
!$isLoading && $startContent && /*#__PURE__*/ _jsx("div", {
|
|
36
36
|
className: styles["start-content"],
|
|
37
37
|
children: $startContent
|
|
38
38
|
}),
|
|
@@ -26,18 +26,41 @@
|
|
|
26
26
|
margin-right: calc(var(--base) / 2);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/* Mirrors Payload's `.btn--style-secondary` (@payloadcms/ui Button): transparent fill, a
|
|
30
|
+
theme-elevation-800 border (faint + low-contrast in the dark admin theme), text in theme-text. */
|
|
29
31
|
.outlined {
|
|
32
|
+
color: var(--theme-text);
|
|
33
|
+
border: 1px solid var(--theme-elevation-800);
|
|
34
|
+
|
|
35
|
+
&:hover:not(:disabled),
|
|
36
|
+
&:focus {
|
|
37
|
+
color: var(--theme-elevation-600);
|
|
38
|
+
border-color: var(--theme-elevation-400);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&:disabled {
|
|
42
|
+
cursor: not-allowed;
|
|
43
|
+
color: var(--theme-elevation-200);
|
|
44
|
+
border-color: var(--theme-elevation-200);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/* A muted outlined: transparent fill + a faint elev-200 border (Payload's subtle/dashed border
|
|
49
|
+
weight) instead of secondary's strong elev-800. For low-emphasis triggers that should sit
|
|
50
|
+
quietly next to other plugins' admin buttons. */
|
|
51
|
+
.outlined-light {
|
|
52
|
+
color: var(--theme-text);
|
|
30
53
|
border: 1px solid var(--theme-elevation-200);
|
|
31
54
|
|
|
32
55
|
&:hover:not(:disabled),
|
|
33
56
|
&:focus {
|
|
34
|
-
border-color: var(--theme-elevation-
|
|
57
|
+
border-color: var(--theme-elevation-300);
|
|
35
58
|
}
|
|
36
59
|
|
|
37
60
|
&:disabled {
|
|
38
61
|
cursor: not-allowed;
|
|
39
|
-
|
|
40
|
-
color: var(--theme-elevation-
|
|
62
|
+
color: var(--theme-elevation-200);
|
|
63
|
+
border-color: var(--theme-elevation-150);
|
|
41
64
|
}
|
|
42
65
|
}
|
|
43
66
|
|
|
@@ -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
|