payload-translate 1.0.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 +62 -0
- package/dist/components/TranslateButton/index.d.ts +3 -0
- package/dist/components/TranslateButton/index.js +150 -0
- package/dist/components/TranslateButton/index.js.map +1 -0
- package/dist/components/TranslateButton/index.scss +44 -0
- package/dist/endpoints/translateHandler.d.ts +2 -0
- package/dist/endpoints/translateHandler.js +114 -0
- package/dist/endpoints/translateHandler.js.map +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +3 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/rsc.d.ts +1 -0
- package/dist/exports/rsc.js +4 -0
- package/dist/exports/rsc.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/services/gemini.d.ts +6 -0
- package/dist/services/gemini.js +84 -0
- package/dist/services/gemini.js.map +1 -0
- package/dist/types.d.ts +42 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/applyTranslations.d.ts +2 -0
- package/dist/utils/applyTranslations.js +80 -0
- package/dist/utils/applyTranslations.js.map +1 -0
- package/dist/utils/extractTranslatableFields.d.ts +3 -0
- package/dist/utils/extractTranslatableFields.js +149 -0
- package/dist/utils/extractTranslatableFields.js.map +1 -0
- package/package.json +141 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# payload-translate
|
|
2
|
+
|
|
3
|
+
AI-powered translation plugin for Payload CMS using Google Gemini.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add payload-translate
|
|
9
|
+
# or
|
|
10
|
+
npm install payload-translate
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { buildConfig } from 'payload'
|
|
17
|
+
import { payloadTranslate } from 'payload-translate'
|
|
18
|
+
|
|
19
|
+
export default buildConfig({
|
|
20
|
+
plugins: [
|
|
21
|
+
payloadTranslate({
|
|
22
|
+
apiKey: process.env.GEMINI_API_KEY,
|
|
23
|
+
collections: ['posts', 'pages'],
|
|
24
|
+
}),
|
|
25
|
+
],
|
|
26
|
+
localization: {
|
|
27
|
+
defaultLocale: 'en',
|
|
28
|
+
locales: ['en', 'es', 'fr'],
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Options
|
|
34
|
+
|
|
35
|
+
| Option | Type | Required | Description |
|
|
36
|
+
|--------|------|----------|-------------|
|
|
37
|
+
| `apiKey` | `string` | Yes | Google Gemini API key |
|
|
38
|
+
| `collections` | `string[]` | Yes | Collection slugs to enable translation |
|
|
39
|
+
| `disabled` | `boolean` | No | Disable the plugin |
|
|
40
|
+
|
|
41
|
+
## How It Works
|
|
42
|
+
|
|
43
|
+
1. Open any document in an enabled collection
|
|
44
|
+
2. Click the **Translate** button
|
|
45
|
+
3. Select target locale
|
|
46
|
+
4. Content is translated via Gemini and saved
|
|
47
|
+
|
|
48
|
+
## Supported Fields
|
|
49
|
+
|
|
50
|
+
- Text fields
|
|
51
|
+
- Textarea fields
|
|
52
|
+
- Rich text (Lexical) - preserves formatting
|
|
53
|
+
|
|
54
|
+
## Requirements
|
|
55
|
+
|
|
56
|
+
- Payload 3.x
|
|
57
|
+
- Localization enabled in Payload config
|
|
58
|
+
- Google Gemini API key
|
|
59
|
+
|
|
60
|
+
## License
|
|
61
|
+
|
|
62
|
+
MIT
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Button, Modal, ReactSelect, useConfig, useDocumentInfo, useLocale, useModal } from '@payloadcms/ui';
|
|
4
|
+
import React, { useCallback, useState } from 'react';
|
|
5
|
+
import { toast } from 'sonner';
|
|
6
|
+
import './index.scss';
|
|
7
|
+
const MODAL_SLUG = 'translate-document-modal';
|
|
8
|
+
export const TranslateButton = ()=>{
|
|
9
|
+
const { config } = useConfig();
|
|
10
|
+
const { id, collectionSlug } = useDocumentInfo();
|
|
11
|
+
const locale = useLocale();
|
|
12
|
+
const { closeModal, openModal } = useModal();
|
|
13
|
+
const [isTranslating, setIsTranslating] = useState(false);
|
|
14
|
+
const [selectedLocale, setSelectedLocale] = useState(null);
|
|
15
|
+
// Get available locales (exclude current locale)
|
|
16
|
+
const localization = config.localization;
|
|
17
|
+
const locales = localization ? localization.locales : [];
|
|
18
|
+
const availableTargetLocales = locales.filter((l)=>{
|
|
19
|
+
const code = typeof l === 'string' ? l : l.code;
|
|
20
|
+
return code !== locale?.code;
|
|
21
|
+
});
|
|
22
|
+
const handleTranslate = useCallback(async ()=>{
|
|
23
|
+
if (!collectionSlug || !id || !locale?.code || !selectedLocale) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
setIsTranslating(true);
|
|
27
|
+
closeModal(MODAL_SLUG);
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(`${config.serverURL}${config.routes.api}/translate`, {
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
collection: collectionSlug,
|
|
32
|
+
documentId: id,
|
|
33
|
+
sourceLocale: locale.code,
|
|
34
|
+
targetLocale: selectedLocale.value
|
|
35
|
+
}),
|
|
36
|
+
credentials: 'include',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json'
|
|
39
|
+
},
|
|
40
|
+
method: 'POST'
|
|
41
|
+
});
|
|
42
|
+
const result = await response.json();
|
|
43
|
+
if (result.success) {
|
|
44
|
+
toast.success(result.message || 'Translation complete');
|
|
45
|
+
} else {
|
|
46
|
+
toast.error(result.error || 'Translation failed');
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
toast.error('Translation request failed');
|
|
50
|
+
console.error('Translation error:', error);
|
|
51
|
+
} finally{
|
|
52
|
+
setIsTranslating(false);
|
|
53
|
+
setSelectedLocale(null);
|
|
54
|
+
}
|
|
55
|
+
}, [
|
|
56
|
+
closeModal,
|
|
57
|
+
collectionSlug,
|
|
58
|
+
config,
|
|
59
|
+
id,
|
|
60
|
+
locale,
|
|
61
|
+
selectedLocale
|
|
62
|
+
]);
|
|
63
|
+
const handleCancel = useCallback(()=>{
|
|
64
|
+
setSelectedLocale(null);
|
|
65
|
+
closeModal(MODAL_SLUG);
|
|
66
|
+
}, [
|
|
67
|
+
closeModal
|
|
68
|
+
]);
|
|
69
|
+
// Don't render if no localization, no other locales, or document not saved yet
|
|
70
|
+
if (!config.localization || availableTargetLocales.length === 0 || !id) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const localeOptions = availableTargetLocales.map((l)=>{
|
|
74
|
+
if (typeof l === 'string') {
|
|
75
|
+
return {
|
|
76
|
+
label: l.toUpperCase(),
|
|
77
|
+
value: l
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
label: typeof l.label === 'string' ? l.label : l.code.toUpperCase(),
|
|
82
|
+
value: l.code
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
return /*#__PURE__*/ _jsxs(_Fragment, {
|
|
86
|
+
children: [
|
|
87
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
88
|
+
buttonStyle: "secondary",
|
|
89
|
+
disabled: isTranslating,
|
|
90
|
+
onClick: ()=>openModal(MODAL_SLUG),
|
|
91
|
+
children: isTranslating ? 'Translating...' : 'Translate'
|
|
92
|
+
}),
|
|
93
|
+
/*#__PURE__*/ _jsx(Modal, {
|
|
94
|
+
className: "translate-modal",
|
|
95
|
+
slug: MODAL_SLUG,
|
|
96
|
+
children: /*#__PURE__*/ _jsxs("div", {
|
|
97
|
+
className: "translate-modal__wrapper",
|
|
98
|
+
children: [
|
|
99
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
100
|
+
className: "translate-modal__content",
|
|
101
|
+
children: [
|
|
102
|
+
/*#__PURE__*/ _jsx("h3", {
|
|
103
|
+
children: "Translate Document"
|
|
104
|
+
}),
|
|
105
|
+
/*#__PURE__*/ _jsxs("p", {
|
|
106
|
+
children: [
|
|
107
|
+
"Translate content from ",
|
|
108
|
+
/*#__PURE__*/ _jsx("strong", {
|
|
109
|
+
children: locale?.code?.toUpperCase()
|
|
110
|
+
}),
|
|
111
|
+
" to:"
|
|
112
|
+
]
|
|
113
|
+
}),
|
|
114
|
+
/*#__PURE__*/ _jsx("div", {
|
|
115
|
+
className: "translate-modal__select",
|
|
116
|
+
children: /*#__PURE__*/ _jsx(ReactSelect, {
|
|
117
|
+
isClearable: true,
|
|
118
|
+
onChange: (option)=>setSelectedLocale(option),
|
|
119
|
+
options: localeOptions,
|
|
120
|
+
placeholder: "Select target locale...",
|
|
121
|
+
value: selectedLocale ?? undefined
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
]
|
|
125
|
+
}),
|
|
126
|
+
/*#__PURE__*/ _jsxs("div", {
|
|
127
|
+
className: "translate-modal__controls",
|
|
128
|
+
children: [
|
|
129
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
130
|
+
buttonStyle: "secondary",
|
|
131
|
+
onClick: handleCancel,
|
|
132
|
+
size: "medium",
|
|
133
|
+
children: "Cancel"
|
|
134
|
+
}),
|
|
135
|
+
/*#__PURE__*/ _jsx(Button, {
|
|
136
|
+
disabled: !selectedLocale,
|
|
137
|
+
onClick: handleTranslate,
|
|
138
|
+
size: "medium",
|
|
139
|
+
children: "Translate"
|
|
140
|
+
})
|
|
141
|
+
]
|
|
142
|
+
})
|
|
143
|
+
]
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
]
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/components/TranslateButton/index.tsx"],"sourcesContent":["'use client'\n\nimport type { Locale } from 'payload'\n\nimport {\n Button,\n Modal,\n ReactSelect,\n type ReactSelectOption,\n useConfig,\n useDocumentInfo,\n useLocale,\n useModal,\n} from '@payloadcms/ui'\nimport React, { useCallback, useState } from 'react'\nimport { toast } from 'sonner'\n\nimport './index.scss'\n\nconst MODAL_SLUG = 'translate-document-modal'\n\nexport const TranslateButton: React.FC = () => {\n const { config } = useConfig()\n const { id, collectionSlug } = useDocumentInfo()\n const locale = useLocale()\n const { closeModal, openModal } = useModal()\n const [isTranslating, setIsTranslating] = useState(false)\n const [selectedLocale, setSelectedLocale] = useState<null | ReactSelectOption>(null)\n\n // Get available locales (exclude current locale)\n const localization = config.localization\n const locales = localization ? localization.locales : []\n const availableTargetLocales = locales.filter((l: Locale | string) => {\n const code = typeof l === 'string' ? l : l.code\n return code !== locale?.code\n })\n\n const handleTranslate = useCallback(async () => {\n if (!collectionSlug || !id || !locale?.code || !selectedLocale) {\n return\n }\n\n setIsTranslating(true)\n closeModal(MODAL_SLUG)\n\n try {\n const response = await fetch(`${config.serverURL}${config.routes.api}/translate`, {\n body: JSON.stringify({\n collection: collectionSlug,\n documentId: id,\n sourceLocale: locale.code,\n targetLocale: selectedLocale.value,\n }),\n credentials: 'include',\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n\n const result = await response.json()\n\n if (result.success) {\n toast.success(result.message || 'Translation complete')\n } else {\n toast.error(result.error || 'Translation failed')\n }\n } catch (error) {\n toast.error('Translation request failed')\n console.error('Translation error:', error)\n } finally {\n setIsTranslating(false)\n setSelectedLocale(null)\n }\n }, [closeModal, collectionSlug, config, id, locale, selectedLocale])\n\n const handleCancel = useCallback(() => {\n setSelectedLocale(null)\n closeModal(MODAL_SLUG)\n }, [closeModal])\n\n // Don't render if no localization, no other locales, or document not saved yet\n if (!config.localization || availableTargetLocales.length === 0 || !id) {\n return null\n }\n\n const localeOptions: ReactSelectOption[] = availableTargetLocales.map((l: Locale | string) => {\n if (typeof l === 'string') {\n return { label: l.toUpperCase(), value: l }\n }\n return {\n label: typeof l.label === 'string' ? l.label : l.code.toUpperCase(),\n value: l.code,\n }\n })\n\n return (\n <>\n <Button\n buttonStyle=\"secondary\"\n disabled={isTranslating}\n onClick={() => openModal(MODAL_SLUG)}\n >\n {isTranslating ? 'Translating...' : 'Translate'}\n </Button>\n <Modal className=\"translate-modal\" slug={MODAL_SLUG}>\n <div className=\"translate-modal__wrapper\">\n <div className=\"translate-modal__content\">\n <h3>Translate Document</h3>\n <p>\n Translate content from <strong>{locale?.code?.toUpperCase()}</strong> to:\n </p>\n <div className=\"translate-modal__select\">\n <ReactSelect\n isClearable\n onChange={(option) => setSelectedLocale(option as ReactSelectOption)}\n options={localeOptions}\n placeholder=\"Select target locale...\"\n value={selectedLocale ?? undefined}\n />\n </div>\n </div>\n <div className=\"translate-modal__controls\">\n <Button buttonStyle=\"secondary\" onClick={handleCancel} size=\"medium\">\n Cancel\n </Button>\n <Button disabled={!selectedLocale} onClick={handleTranslate} size=\"medium\">\n Translate\n </Button>\n </div>\n </div>\n </Modal>\n </>\n )\n}\n"],"names":["Button","Modal","ReactSelect","useConfig","useDocumentInfo","useLocale","useModal","React","useCallback","useState","toast","MODAL_SLUG","TranslateButton","config","id","collectionSlug","locale","closeModal","openModal","isTranslating","setIsTranslating","selectedLocale","setSelectedLocale","localization","locales","availableTargetLocales","filter","l","code","handleTranslate","response","fetch","serverURL","routes","api","body","JSON","stringify","collection","documentId","sourceLocale","targetLocale","value","credentials","headers","method","result","json","success","message","error","console","handleCancel","length","localeOptions","map","label","toUpperCase","buttonStyle","disabled","onClick","className","slug","div","h3","p","strong","isClearable","onChange","option","options","placeholder","undefined","size"],"mappings":"AAAA;;AAIA,SACEA,MAAM,EACNC,KAAK,EACLC,WAAW,EAEXC,SAAS,EACTC,eAAe,EACfC,SAAS,EACTC,QAAQ,QACH,iBAAgB;AACvB,OAAOC,SAASC,WAAW,EAAEC,QAAQ,QAAQ,QAAO;AACpD,SAASC,KAAK,QAAQ,SAAQ;AAE9B,OAAO,eAAc;AAErB,MAAMC,aAAa;AAEnB,OAAO,MAAMC,kBAA4B;IACvC,MAAM,EAAEC,MAAM,EAAE,GAAGV;IACnB,MAAM,EAAEW,EAAE,EAAEC,cAAc,EAAE,GAAGX;IAC/B,MAAMY,SAASX;IACf,MAAM,EAAEY,UAAU,EAAEC,SAAS,EAAE,GAAGZ;IAClC,MAAM,CAACa,eAAeC,iBAAiB,GAAGX,SAAS;IACnD,MAAM,CAACY,gBAAgBC,kBAAkB,GAAGb,SAAmC;IAE/E,iDAAiD;IACjD,MAAMc,eAAeV,OAAOU,YAAY;IACxC,MAAMC,UAAUD,eAAeA,aAAaC,OAAO,GAAG,EAAE;IACxD,MAAMC,yBAAyBD,QAAQE,MAAM,CAAC,CAACC;QAC7C,MAAMC,OAAO,OAAOD,MAAM,WAAWA,IAAIA,EAAEC,IAAI;QAC/C,OAAOA,SAASZ,QAAQY;IAC1B;IAEA,MAAMC,kBAAkBrB,YAAY;QAClC,IAAI,CAACO,kBAAkB,CAACD,MAAM,CAACE,QAAQY,QAAQ,CAACP,gBAAgB;YAC9D;QACF;QAEAD,iBAAiB;QACjBH,WAAWN;QAEX,IAAI;YACF,MAAMmB,WAAW,MAAMC,MAAM,GAAGlB,OAAOmB,SAAS,GAAGnB,OAAOoB,MAAM,CAACC,GAAG,CAAC,UAAU,CAAC,EAAE;gBAChFC,MAAMC,KAAKC,SAAS,CAAC;oBACnBC,YAAYvB;oBACZwB,YAAYzB;oBACZ0B,cAAcxB,OAAOY,IAAI;oBACzBa,cAAcpB,eAAeqB,KAAK;gBACpC;gBACAC,aAAa;gBACbC,SAAS;oBACP,gBAAgB;gBAClB;gBACAC,QAAQ;YACV;YAEA,MAAMC,SAAS,MAAMhB,SAASiB,IAAI;YAElC,IAAID,OAAOE,OAAO,EAAE;gBAClBtC,MAAMsC,OAAO,CAACF,OAAOG,OAAO,IAAI;YAClC,OAAO;gBACLvC,MAAMwC,KAAK,CAACJ,OAAOI,KAAK,IAAI;YAC9B;QACF,EAAE,OAAOA,OAAO;YACdxC,MAAMwC,KAAK,CAAC;YACZC,QAAQD,KAAK,CAAC,sBAAsBA;QACtC,SAAU;YACR9B,iBAAiB;YACjBE,kBAAkB;QACpB;IACF,GAAG;QAACL;QAAYF;QAAgBF;QAAQC;QAAIE;QAAQK;KAAe;IAEnE,MAAM+B,eAAe5C,YAAY;QAC/Bc,kBAAkB;QAClBL,WAAWN;IACb,GAAG;QAACM;KAAW;IAEf,+EAA+E;IAC/E,IAAI,CAACJ,OAAOU,YAAY,IAAIE,uBAAuB4B,MAAM,KAAK,KAAK,CAACvC,IAAI;QACtE,OAAO;IACT;IAEA,MAAMwC,gBAAqC7B,uBAAuB8B,GAAG,CAAC,CAAC5B;QACrE,IAAI,OAAOA,MAAM,UAAU;YACzB,OAAO;gBAAE6B,OAAO7B,EAAE8B,WAAW;gBAAIf,OAAOf;YAAE;QAC5C;QACA,OAAO;YACL6B,OAAO,OAAO7B,EAAE6B,KAAK,KAAK,WAAW7B,EAAE6B,KAAK,GAAG7B,EAAEC,IAAI,CAAC6B,WAAW;YACjEf,OAAOf,EAAEC,IAAI;QACf;IACF;IAEA,qBACE;;0BACE,KAAC5B;gBACC0D,aAAY;gBACZC,UAAUxC;gBACVyC,SAAS,IAAM1C,UAAUP;0BAExBQ,gBAAgB,mBAAmB;;0BAEtC,KAAClB;gBAAM4D,WAAU;gBAAkBC,MAAMnD;0BACvC,cAAA,MAACoD;oBAAIF,WAAU;;sCACb,MAACE;4BAAIF,WAAU;;8CACb,KAACG;8CAAG;;8CACJ,MAACC;;wCAAE;sDACsB,KAACC;sDAAQlD,QAAQY,MAAM6B;;wCAAuB;;;8CAEvE,KAACM;oCAAIF,WAAU;8CACb,cAAA,KAAC3D;wCACCiE,WAAW;wCACXC,UAAU,CAACC,SAAW/C,kBAAkB+C;wCACxCC,SAAShB;wCACTiB,aAAY;wCACZ7B,OAAOrB,kBAAkBmD;;;;;sCAI/B,MAACT;4BAAIF,WAAU;;8CACb,KAAC7D;oCAAO0D,aAAY;oCAAYE,SAASR;oCAAcqB,MAAK;8CAAS;;8CAGrE,KAACzE;oCAAO2D,UAAU,CAACtC;oCAAgBuC,SAAS/B;oCAAiB4C,MAAK;8CAAS;;;;;;;;;AAQvF,EAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
.translate-modal {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
position: fixed;
|
|
6
|
+
inset: 0;
|
|
7
|
+
z-index: 100;
|
|
8
|
+
background: rgba(0, 0, 0, 0.5);
|
|
9
|
+
backdrop-filter: blur(4px);
|
|
10
|
+
|
|
11
|
+
&__wrapper {
|
|
12
|
+
background: var(--theme-bg);
|
|
13
|
+
border-radius: 8px;
|
|
14
|
+
padding: 24px;
|
|
15
|
+
max-width: 400px;
|
|
16
|
+
width: 90%;
|
|
17
|
+
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&__content {
|
|
21
|
+
margin-bottom: 24px;
|
|
22
|
+
|
|
23
|
+
h3 {
|
|
24
|
+
margin: 0 0 8px 0;
|
|
25
|
+
font-size: 1.25rem;
|
|
26
|
+
font-weight: 600;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
p {
|
|
30
|
+
margin: 0 0 16px 0;
|
|
31
|
+
color: var(--theme-elevation-600);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
&__select {
|
|
36
|
+
margin-top: 8px;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&__controls {
|
|
40
|
+
display: flex;
|
|
41
|
+
justify-content: flex-end;
|
|
42
|
+
gap: 12px;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { translateWithGemini } from '../services/gemini.js';
|
|
2
|
+
import { applyTranslations } from '../utils/applyTranslations.js';
|
|
3
|
+
import { extractTranslatableFields } from '../utils/extractTranslatableFields.js';
|
|
4
|
+
export const translateHandler = async (req)=>{
|
|
5
|
+
try {
|
|
6
|
+
const { payload, user } = req;
|
|
7
|
+
// Check authentication
|
|
8
|
+
if (!user) {
|
|
9
|
+
return Response.json({
|
|
10
|
+
error: 'Unauthorized',
|
|
11
|
+
success: false
|
|
12
|
+
}, {
|
|
13
|
+
status: 401
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
// Parse request body
|
|
17
|
+
const body = await req.json?.();
|
|
18
|
+
if (!body) {
|
|
19
|
+
return Response.json({
|
|
20
|
+
error: 'Invalid request body',
|
|
21
|
+
success: false
|
|
22
|
+
}, {
|
|
23
|
+
status: 400
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const { collection, documentId, sourceLocale, targetLocale } = body;
|
|
27
|
+
// Validate request
|
|
28
|
+
if (!collection || !documentId || !sourceLocale || !targetLocale) {
|
|
29
|
+
return Response.json({
|
|
30
|
+
error: 'Missing required fields',
|
|
31
|
+
success: false
|
|
32
|
+
}, {
|
|
33
|
+
status: 400
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
// Get API key from config
|
|
37
|
+
const apiKey = payload.config.custom?.translateApiKey;
|
|
38
|
+
if (!apiKey) {
|
|
39
|
+
return Response.json({
|
|
40
|
+
error: 'Translation API key not configured',
|
|
41
|
+
success: false
|
|
42
|
+
}, {
|
|
43
|
+
status: 500
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
// Fetch document in source locale
|
|
47
|
+
const document = await payload.findByID({
|
|
48
|
+
id: documentId,
|
|
49
|
+
collection,
|
|
50
|
+
depth: 0,
|
|
51
|
+
locale: sourceLocale
|
|
52
|
+
});
|
|
53
|
+
if (!document) {
|
|
54
|
+
return Response.json({
|
|
55
|
+
error: 'Document not found',
|
|
56
|
+
success: false
|
|
57
|
+
}, {
|
|
58
|
+
status: 404
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Get collection config to find localized fields
|
|
62
|
+
const collectionConfig = payload.collections[collection]?.config;
|
|
63
|
+
if (!collectionConfig) {
|
|
64
|
+
return Response.json({
|
|
65
|
+
error: 'Collection not found',
|
|
66
|
+
success: false
|
|
67
|
+
}, {
|
|
68
|
+
status: 404
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// Extract translatable fields
|
|
72
|
+
const translatableFields = extractTranslatableFields(document, collectionConfig.fields);
|
|
73
|
+
if (translatableFields.length === 0) {
|
|
74
|
+
return Response.json({
|
|
75
|
+
message: 'No translatable fields found',
|
|
76
|
+
success: true,
|
|
77
|
+
translatedFields: 0
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
// Prepare texts for translation
|
|
81
|
+
const texts = translatableFields.map((f)=>f.value);
|
|
82
|
+
// Translate with Gemini
|
|
83
|
+
const translations = await translateWithGemini({
|
|
84
|
+
apiKey,
|
|
85
|
+
sourceLocale,
|
|
86
|
+
targetLocale,
|
|
87
|
+
texts
|
|
88
|
+
});
|
|
89
|
+
// Apply translations to document
|
|
90
|
+
const updatedData = applyTranslations(document, translatableFields, translations);
|
|
91
|
+
// Update document in target locale
|
|
92
|
+
await payload.update({
|
|
93
|
+
id: documentId,
|
|
94
|
+
collection,
|
|
95
|
+
data: updatedData,
|
|
96
|
+
locale: targetLocale
|
|
97
|
+
});
|
|
98
|
+
return Response.json({
|
|
99
|
+
message: `Successfully translated ${translatableFields.length} field(s)`,
|
|
100
|
+
success: true,
|
|
101
|
+
translatedFields: translatableFields.length
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Translation error:', error);
|
|
105
|
+
return Response.json({
|
|
106
|
+
error: error instanceof Error ? error.message : 'Translation failed',
|
|
107
|
+
success: false
|
|
108
|
+
}, {
|
|
109
|
+
status: 500
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=translateHandler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/translateHandler.ts"],"sourcesContent":["import type { PayloadHandler } from 'payload'\n\nimport type { TranslateRequestBody, TranslateResponse } from '../types.js'\n\nimport { translateWithGemini } from '../services/gemini.js'\nimport { applyTranslations } from '../utils/applyTranslations.js'\nimport { extractTranslatableFields } from '../utils/extractTranslatableFields.js'\n\nexport const translateHandler: PayloadHandler = async (req) => {\n try {\n const { payload, user } = req\n\n // Check authentication\n if (!user) {\n return Response.json({ error: 'Unauthorized', success: false } as TranslateResponse, {\n status: 401,\n })\n }\n\n // Parse request body\n const body = (await req.json?.()) as TranslateRequestBody | undefined\n if (!body) {\n return Response.json(\n { error: 'Invalid request body', success: false } as TranslateResponse,\n { status: 400 },\n )\n }\n const { collection, documentId, sourceLocale, targetLocale } = body\n\n // Validate request\n if (!collection || !documentId || !sourceLocale || !targetLocale) {\n return Response.json(\n { error: 'Missing required fields', success: false } as TranslateResponse,\n { status: 400 },\n )\n }\n\n // Get API key from config\n const apiKey = (payload.config.custom as Record<string, unknown>)?.translateApiKey as\n | string\n | undefined\n if (!apiKey) {\n return Response.json(\n { error: 'Translation API key not configured', success: false } as TranslateResponse,\n { status: 500 },\n )\n }\n\n // Fetch document in source locale\n const document = await payload.findByID({\n id: documentId,\n collection,\n depth: 0,\n locale: sourceLocale,\n })\n\n if (!document) {\n return Response.json({ error: 'Document not found', success: false } as TranslateResponse, {\n status: 404,\n })\n }\n\n // Get collection config to find localized fields\n const collectionConfig = payload.collections[collection]?.config\n if (!collectionConfig) {\n return Response.json({ error: 'Collection not found', success: false } as TranslateResponse, {\n status: 404,\n })\n }\n\n // Extract translatable fields\n const translatableFields = extractTranslatableFields(\n document as Record<string, unknown>,\n collectionConfig.fields,\n )\n\n if (translatableFields.length === 0) {\n return Response.json({\n message: 'No translatable fields found',\n success: true,\n translatedFields: 0,\n } as TranslateResponse)\n }\n\n // Prepare texts for translation\n const texts = translatableFields.map((f) => f.value)\n\n // Translate with Gemini\n const translations = await translateWithGemini({\n apiKey,\n sourceLocale,\n targetLocale,\n texts,\n })\n\n // Apply translations to document\n const updatedData = applyTranslations(\n document as Record<string, unknown>,\n translatableFields,\n translations,\n )\n\n // Update document in target locale\n await payload.update({\n id: documentId,\n collection,\n data: updatedData,\n locale: targetLocale,\n })\n\n return Response.json({\n message: `Successfully translated ${translatableFields.length} field(s)`,\n success: true,\n translatedFields: translatableFields.length,\n } as TranslateResponse)\n } catch (error) {\n console.error('Translation error:', error)\n return Response.json(\n {\n error: error instanceof Error ? error.message : 'Translation failed',\n success: false,\n } as TranslateResponse,\n { status: 500 },\n )\n }\n}\n"],"names":["translateWithGemini","applyTranslations","extractTranslatableFields","translateHandler","req","payload","user","Response","json","error","success","status","body","collection","documentId","sourceLocale","targetLocale","apiKey","config","custom","translateApiKey","document","findByID","id","depth","locale","collectionConfig","collections","translatableFields","fields","length","message","translatedFields","texts","map","f","value","translations","updatedData","update","data","console","Error"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { TranslateButton } from '../components/TranslateButton/index.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { TranslateButton } from '../components/TranslateButton/index.js'\n"],"names":["TranslateButton"],"mappings":"AAAA,SAASA,eAAe,QAAQ,yCAAwC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/exports/rsc.ts"],"sourcesContent":["// RSC exports - currently not used by this plugin\nexport {}\n"],"names":[],"mappings":"AAAA,kDAAkD;AAClD,WAAS"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { translateHandler } from './endpoints/translateHandler.js';
|
|
2
|
+
export const payloadTranslate = (pluginOptions)=>(config)=>{
|
|
3
|
+
if (pluginOptions.disabled) {
|
|
4
|
+
return config;
|
|
5
|
+
}
|
|
6
|
+
// Store API key in custom config for endpoint access
|
|
7
|
+
if (!config.custom) {
|
|
8
|
+
config.custom = {};
|
|
9
|
+
}
|
|
10
|
+
;
|
|
11
|
+
config.custom.translateApiKey = pluginOptions.apiKey;
|
|
12
|
+
// Add TranslateButton to specified collections
|
|
13
|
+
if (!config.collections) {
|
|
14
|
+
config.collections = [];
|
|
15
|
+
}
|
|
16
|
+
for (const collectionSlug of pluginOptions.collections){
|
|
17
|
+
const collection = config.collections.find((c)=>c.slug === collectionSlug);
|
|
18
|
+
if (collection) {
|
|
19
|
+
if (!collection.admin) {
|
|
20
|
+
collection.admin = {};
|
|
21
|
+
}
|
|
22
|
+
if (!collection.admin.components) {
|
|
23
|
+
collection.admin.components = {};
|
|
24
|
+
}
|
|
25
|
+
if (!collection.admin.components.edit) {
|
|
26
|
+
collection.admin.components.edit = {};
|
|
27
|
+
}
|
|
28
|
+
if (!collection.admin.components.edit.beforeDocumentControls) {
|
|
29
|
+
collection.admin.components.edit.beforeDocumentControls = [];
|
|
30
|
+
}
|
|
31
|
+
collection.admin.components.edit.beforeDocumentControls.push('payload-translate/client#TranslateButton');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
// Register translation endpoint
|
|
35
|
+
if (!config.endpoints) {
|
|
36
|
+
config.endpoints = [];
|
|
37
|
+
}
|
|
38
|
+
config.endpoints.push({
|
|
39
|
+
handler: translateHandler,
|
|
40
|
+
method: 'post',
|
|
41
|
+
path: '/translate'
|
|
42
|
+
});
|
|
43
|
+
return config;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload'\n\nimport type { PayloadTranslateConfig } from './types.js'\n\nimport { translateHandler } from './endpoints/translateHandler.js'\n\nexport type { PayloadTranslateConfig }\n\nexport const payloadTranslate =\n (pluginOptions: PayloadTranslateConfig) =>\n (config: Config): Config => {\n if (pluginOptions.disabled) {\n return config\n }\n\n // Store API key in custom config for endpoint access\n if (!config.custom) {\n config.custom = {}\n }\n ;(config.custom as Record<string, unknown>).translateApiKey = pluginOptions.apiKey\n\n // Add TranslateButton to specified collections\n if (!config.collections) {\n config.collections = []\n }\n\n for (const collectionSlug of pluginOptions.collections) {\n const collection = config.collections.find((c) => c.slug === collectionSlug)\n\n if (collection) {\n if (!collection.admin) {\n collection.admin = {}\n }\n if (!collection.admin.components) {\n collection.admin.components = {}\n }\n if (!collection.admin.components.edit) {\n collection.admin.components.edit = {}\n }\n if (!collection.admin.components.edit.beforeDocumentControls) {\n collection.admin.components.edit.beforeDocumentControls = []\n }\n\n collection.admin.components.edit.beforeDocumentControls.push(\n 'payload-translate/client#TranslateButton',\n )\n }\n }\n\n // Register translation endpoint\n if (!config.endpoints) {\n config.endpoints = []\n }\n\n config.endpoints.push({\n handler: translateHandler,\n method: 'post',\n path: '/translate',\n })\n\n return config\n }\n"],"names":["translateHandler","payloadTranslate","pluginOptions","config","disabled","custom","translateApiKey","apiKey","collections","collectionSlug","collection","find","c","slug","admin","components","edit","beforeDocumentControls","push","endpoints","handler","method","path"],"mappings":"AAIA,SAASA,gBAAgB,QAAQ,kCAAiC;AAIlE,OAAO,MAAMC,mBACX,CAACC,gBACD,CAACC;QACC,IAAID,cAAcE,QAAQ,EAAE;YAC1B,OAAOD;QACT;QAEA,qDAAqD;QACrD,IAAI,CAACA,OAAOE,MAAM,EAAE;YAClBF,OAAOE,MAAM,GAAG,CAAC;QACnB;;QACEF,OAAOE,MAAM,CAA6BC,eAAe,GAAGJ,cAAcK,MAAM;QAElF,+CAA+C;QAC/C,IAAI,CAACJ,OAAOK,WAAW,EAAE;YACvBL,OAAOK,WAAW,GAAG,EAAE;QACzB;QAEA,KAAK,MAAMC,kBAAkBP,cAAcM,WAAW,CAAE;YACtD,MAAME,aAAaP,OAAOK,WAAW,CAACG,IAAI,CAAC,CAACC,IAAMA,EAAEC,IAAI,KAAKJ;YAE7D,IAAIC,YAAY;gBACd,IAAI,CAACA,WAAWI,KAAK,EAAE;oBACrBJ,WAAWI,KAAK,GAAG,CAAC;gBACtB;gBACA,IAAI,CAACJ,WAAWI,KAAK,CAACC,UAAU,EAAE;oBAChCL,WAAWI,KAAK,CAACC,UAAU,GAAG,CAAC;gBACjC;gBACA,IAAI,CAACL,WAAWI,KAAK,CAACC,UAAU,CAACC,IAAI,EAAE;oBACrCN,WAAWI,KAAK,CAACC,UAAU,CAACC,IAAI,GAAG,CAAC;gBACtC;gBACA,IAAI,CAACN,WAAWI,KAAK,CAACC,UAAU,CAACC,IAAI,CAACC,sBAAsB,EAAE;oBAC5DP,WAAWI,KAAK,CAACC,UAAU,CAACC,IAAI,CAACC,sBAAsB,GAAG,EAAE;gBAC9D;gBAEAP,WAAWI,KAAK,CAACC,UAAU,CAACC,IAAI,CAACC,sBAAsB,CAACC,IAAI,CAC1D;YAEJ;QACF;QAEA,gCAAgC;QAChC,IAAI,CAACf,OAAOgB,SAAS,EAAE;YACrBhB,OAAOgB,SAAS,GAAG,EAAE;QACvB;QAEAhB,OAAOgB,SAAS,CAACD,IAAI,CAAC;YACpBE,SAASpB;YACTqB,QAAQ;YACRC,MAAM;QACR;QAEA,OAAOnB;IACT,EAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { GeminiTranslationRequest } from '../types.js';
|
|
2
|
+
type TranslateWithGeminiArgs = {
|
|
3
|
+
apiKey: string;
|
|
4
|
+
} & GeminiTranslationRequest;
|
|
5
|
+
export declare function translateWithGemini({ apiKey, sourceLocale, targetLocale, texts, }: TranslateWithGeminiArgs): Promise<string[]>;
|
|
6
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent';
|
|
2
|
+
export async function translateWithGemini({ apiKey, sourceLocale, targetLocale, texts }) {
|
|
3
|
+
if (texts.length === 0) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
const prompt = buildTranslationPrompt(texts, sourceLocale, targetLocale);
|
|
7
|
+
const response = await fetch(`${GEMINI_API_URL}?key=${apiKey}`, {
|
|
8
|
+
body: JSON.stringify({
|
|
9
|
+
contents: [
|
|
10
|
+
{
|
|
11
|
+
parts: [
|
|
12
|
+
{
|
|
13
|
+
text: prompt
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
],
|
|
18
|
+
generationConfig: {
|
|
19
|
+
maxOutputTokens: 8192,
|
|
20
|
+
temperature: 0.1,
|
|
21
|
+
thinkingConfig: {
|
|
22
|
+
thinkingBudget: 0
|
|
23
|
+
},
|
|
24
|
+
topP: 0.95
|
|
25
|
+
}
|
|
26
|
+
}),
|
|
27
|
+
headers: {
|
|
28
|
+
'Content-Type': 'application/json'
|
|
29
|
+
},
|
|
30
|
+
method: 'POST'
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const error = await response.text();
|
|
34
|
+
throw new Error(`Gemini API error: ${error}`);
|
|
35
|
+
}
|
|
36
|
+
const result = await response.json();
|
|
37
|
+
// Handle response - find the text part
|
|
38
|
+
const parts = result.candidates?.[0]?.content?.parts || [];
|
|
39
|
+
const textPart = parts.find((p)=>p.text !== undefined);
|
|
40
|
+
const generatedText = textPart?.text;
|
|
41
|
+
if (!generatedText) {
|
|
42
|
+
throw new Error(`No translation returned from Gemini. Response: ${JSON.stringify(result)}`);
|
|
43
|
+
}
|
|
44
|
+
return parseTranslationResponse(generatedText, texts.length);
|
|
45
|
+
}
|
|
46
|
+
function buildTranslationPrompt(texts, sourceLocale, targetLocale) {
|
|
47
|
+
const textsJson = JSON.stringify(texts);
|
|
48
|
+
return `You are a professional translator. Translate the following texts from ${sourceLocale} to ${targetLocale}.
|
|
49
|
+
|
|
50
|
+
IMPORTANT RULES:
|
|
51
|
+
1. Maintain the exact same formatting, including HTML tags, markdown, and special characters
|
|
52
|
+
2. Do not translate proper nouns, brand names, or code/technical terms unless they have standard translations
|
|
53
|
+
3. Preserve any placeholder variables like {{name}} or {0}
|
|
54
|
+
4. Return ONLY a valid JSON array with the translations in the same order
|
|
55
|
+
5. Each translation should correspond to the input at the same index
|
|
56
|
+
|
|
57
|
+
Input texts (JSON array):
|
|
58
|
+
${textsJson}
|
|
59
|
+
|
|
60
|
+
Return ONLY the JSON array of translations, nothing else. Example format:
|
|
61
|
+
["translated text 1", "translated text 2"]`;
|
|
62
|
+
}
|
|
63
|
+
function parseTranslationResponse(response, expectedCount) {
|
|
64
|
+
let jsonStr = response.trim();
|
|
65
|
+
// Handle code blocks if present
|
|
66
|
+
if (jsonStr.startsWith('```')) {
|
|
67
|
+
jsonStr = jsonStr.replace(/```json?\n?/g, '').replace(/```/g, '').trim();
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const translations = JSON.parse(jsonStr);
|
|
71
|
+
if (!Array.isArray(translations)) {
|
|
72
|
+
throw new Error('Response is not an array');
|
|
73
|
+
}
|
|
74
|
+
if (translations.length !== expectedCount) {
|
|
75
|
+
console.warn(`Expected ${expectedCount} translations, got ${translations.length}`);
|
|
76
|
+
}
|
|
77
|
+
return translations;
|
|
78
|
+
} catch (_error) {
|
|
79
|
+
console.error('Failed to parse translation response:', response);
|
|
80
|
+
throw new Error('Failed to parse translation response from Gemini');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
//# sourceMappingURL=gemini.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/services/gemini.ts"],"sourcesContent":["import type { GeminiTranslationRequest } from '../types.js'\n\ntype TranslateWithGeminiArgs = {\n apiKey: string\n} & GeminiTranslationRequest\n\nconst GEMINI_API_URL =\n 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent'\n\nexport async function translateWithGemini({\n apiKey,\n sourceLocale,\n targetLocale,\n texts,\n}: TranslateWithGeminiArgs): Promise<string[]> {\n if (texts.length === 0) {\n return []\n }\n\n const prompt = buildTranslationPrompt(texts, sourceLocale, targetLocale)\n\n const response = await fetch(`${GEMINI_API_URL}?key=${apiKey}`, {\n body: JSON.stringify({\n contents: [\n {\n parts: [{ text: prompt }],\n },\n ],\n generationConfig: {\n maxOutputTokens: 8192,\n temperature: 0.1,\n thinkingConfig: {\n thinkingBudget: 0,\n },\n topP: 0.95,\n },\n }),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n\n if (!response.ok) {\n const error = await response.text()\n throw new Error(`Gemini API error: ${error}`)\n }\n\n const result = await response.json()\n\n // Handle response - find the text part\n const parts = result.candidates?.[0]?.content?.parts || []\n const textPart = parts.find((p: { text?: string }) => p.text !== undefined)\n const generatedText = textPart?.text\n\n if (!generatedText) {\n throw new Error(`No translation returned from Gemini. Response: ${JSON.stringify(result)}`)\n }\n\n return parseTranslationResponse(generatedText, texts.length)\n}\n\nfunction buildTranslationPrompt(\n texts: string[],\n sourceLocale: string,\n targetLocale: string,\n): string {\n const textsJson = JSON.stringify(texts)\n\n return `You are a professional translator. Translate the following texts from ${sourceLocale} to ${targetLocale}.\n\nIMPORTANT RULES:\n1. Maintain the exact same formatting, including HTML tags, markdown, and special characters\n2. Do not translate proper nouns, brand names, or code/technical terms unless they have standard translations\n3. Preserve any placeholder variables like {{name}} or {0}\n4. Return ONLY a valid JSON array with the translations in the same order\n5. Each translation should correspond to the input at the same index\n\nInput texts (JSON array):\n${textsJson}\n\nReturn ONLY the JSON array of translations, nothing else. Example format:\n[\"translated text 1\", \"translated text 2\"]`\n}\n\nfunction parseTranslationResponse(response: string, expectedCount: number): string[] {\n let jsonStr = response.trim()\n\n // Handle code blocks if present\n if (jsonStr.startsWith('```')) {\n jsonStr = jsonStr.replace(/```json?\\n?/g, '').replace(/```/g, '').trim()\n }\n\n try {\n const translations = JSON.parse(jsonStr)\n\n if (!Array.isArray(translations)) {\n throw new Error('Response is not an array')\n }\n\n if (translations.length !== expectedCount) {\n console.warn(`Expected ${expectedCount} translations, got ${translations.length}`)\n }\n\n return translations\n } catch (_error) {\n console.error('Failed to parse translation response:', response)\n throw new Error('Failed to parse translation response from Gemini')\n }\n}\n"],"names":["GEMINI_API_URL","translateWithGemini","apiKey","sourceLocale","targetLocale","texts","length","prompt","buildTranslationPrompt","response","fetch","body","JSON","stringify","contents","parts","text","generationConfig","maxOutputTokens","temperature","thinkingConfig","thinkingBudget","topP","headers","method","ok","error","Error","result","json","candidates","content","textPart","find","p","undefined","generatedText","parseTranslationResponse","textsJson","expectedCount","jsonStr","trim","startsWith","replace","translations","parse","Array","isArray","console","warn","_error"],"mappings":"AAMA,MAAMA,iBACJ;AAEF,OAAO,eAAeC,oBAAoB,EACxCC,MAAM,EACNC,YAAY,EACZC,YAAY,EACZC,KAAK,EACmB;IACxB,IAAIA,MAAMC,MAAM,KAAK,GAAG;QACtB,OAAO,EAAE;IACX;IAEA,MAAMC,SAASC,uBAAuBH,OAAOF,cAAcC;IAE3D,MAAMK,WAAW,MAAMC,MAAM,GAAGV,eAAe,KAAK,EAAEE,QAAQ,EAAE;QAC9DS,MAAMC,KAAKC,SAAS,CAAC;YACnBC,UAAU;gBACR;oBACEC,OAAO;wBAAC;4BAAEC,MAAMT;wBAAO;qBAAE;gBAC3B;aACD;YACDU,kBAAkB;gBAChBC,iBAAiB;gBACjBC,aAAa;gBACbC,gBAAgB;oBACdC,gBAAgB;gBAClB;gBACAC,MAAM;YACR;QACF;QACAC,SAAS;YACP,gBAAgB;QAClB;QACAC,QAAQ;IACV;IAEA,IAAI,CAACf,SAASgB,EAAE,EAAE;QAChB,MAAMC,QAAQ,MAAMjB,SAASO,IAAI;QACjC,MAAM,IAAIW,MAAM,CAAC,kBAAkB,EAAED,OAAO;IAC9C;IAEA,MAAME,SAAS,MAAMnB,SAASoB,IAAI;IAElC,uCAAuC;IACvC,MAAMd,QAAQa,OAAOE,UAAU,EAAE,CAAC,EAAE,EAAEC,SAAShB,SAAS,EAAE;IAC1D,MAAMiB,WAAWjB,MAAMkB,IAAI,CAAC,CAACC,IAAyBA,EAAElB,IAAI,KAAKmB;IACjE,MAAMC,gBAAgBJ,UAAUhB;IAEhC,IAAI,CAACoB,eAAe;QAClB,MAAM,IAAIT,MAAM,CAAC,+CAA+C,EAAEf,KAAKC,SAAS,CAACe,SAAS;IAC5F;IAEA,OAAOS,yBAAyBD,eAAe/B,MAAMC,MAAM;AAC7D;AAEA,SAASE,uBACPH,KAAe,EACfF,YAAoB,EACpBC,YAAoB;IAEpB,MAAMkC,YAAY1B,KAAKC,SAAS,CAACR;IAEjC,OAAO,CAAC,sEAAsE,EAAEF,aAAa,IAAI,EAAEC,aAAa;;;;;;;;;;AAUlH,EAAEkC,UAAU;;;0CAG8B,CAAC;AAC3C;AAEA,SAASD,yBAAyB5B,QAAgB,EAAE8B,aAAqB;IACvE,IAAIC,UAAU/B,SAASgC,IAAI;IAE3B,gCAAgC;IAChC,IAAID,QAAQE,UAAU,CAAC,QAAQ;QAC7BF,UAAUA,QAAQG,OAAO,CAAC,gBAAgB,IAAIA,OAAO,CAAC,QAAQ,IAAIF,IAAI;IACxE;IAEA,IAAI;QACF,MAAMG,eAAehC,KAAKiC,KAAK,CAACL;QAEhC,IAAI,CAACM,MAAMC,OAAO,CAACH,eAAe;YAChC,MAAM,IAAIjB,MAAM;QAClB;QAEA,IAAIiB,aAAatC,MAAM,KAAKiC,eAAe;YACzCS,QAAQC,IAAI,CAAC,CAAC,SAAS,EAAEV,cAAc,mBAAmB,EAAEK,aAAatC,MAAM,EAAE;QACnF;QAEA,OAAOsC;IACT,EAAE,OAAOM,QAAQ;QACfF,QAAQtB,KAAK,CAAC,yCAAyCjB;QACvD,MAAM,IAAIkB,MAAM;IAClB;AACF"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CollectionSlug } from 'payload';
|
|
2
|
+
export type PayloadTranslateConfig = {
|
|
3
|
+
/**
|
|
4
|
+
* Google Gemini API key for translations
|
|
5
|
+
*/
|
|
6
|
+
apiKey: string;
|
|
7
|
+
/**
|
|
8
|
+
* List of collection slugs to enable translation for
|
|
9
|
+
*/
|
|
10
|
+
collections: CollectionSlug[];
|
|
11
|
+
/**
|
|
12
|
+
* Whether the plugin is disabled
|
|
13
|
+
*/
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export type TranslateRequestBody = {
|
|
17
|
+
collection: string;
|
|
18
|
+
documentId: number | string;
|
|
19
|
+
sourceLocale: string;
|
|
20
|
+
targetLocale: string;
|
|
21
|
+
};
|
|
22
|
+
export type TranslateResponse = {
|
|
23
|
+
error?: string;
|
|
24
|
+
message?: string;
|
|
25
|
+
success: boolean;
|
|
26
|
+
translatedFields?: number;
|
|
27
|
+
};
|
|
28
|
+
export type TranslatableField = {
|
|
29
|
+
/**
|
|
30
|
+
* For richText fields, the path within the Lexical structure to the text node
|
|
31
|
+
* e.g., "root.children.0.children.1"
|
|
32
|
+
*/
|
|
33
|
+
lexicalPath?: string;
|
|
34
|
+
path: string;
|
|
35
|
+
type: 'richText' | 'text' | 'textarea';
|
|
36
|
+
value: string;
|
|
37
|
+
};
|
|
38
|
+
export type GeminiTranslationRequest = {
|
|
39
|
+
sourceLocale: string;
|
|
40
|
+
targetLocale: string;
|
|
41
|
+
texts: string[];
|
|
42
|
+
};
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/types.ts"],"sourcesContent":["import type { CollectionSlug } from 'payload'\n\nexport type PayloadTranslateConfig = {\n /**\n * Google Gemini API key for translations\n */\n apiKey: string\n /**\n * List of collection slugs to enable translation for\n */\n collections: CollectionSlug[]\n /**\n * Whether the plugin is disabled\n */\n disabled?: boolean\n}\n\nexport type TranslateRequestBody = {\n collection: string\n documentId: number | string\n sourceLocale: string\n targetLocale: string\n}\n\nexport type TranslateResponse = {\n error?: string\n message?: string\n success: boolean\n translatedFields?: number\n}\n\nexport type TranslatableField = {\n /**\n * For richText fields, the path within the Lexical structure to the text node\n * e.g., \"root.children.0.children.1\"\n */\n lexicalPath?: string\n path: string\n type: 'richText' | 'text' | 'textarea'\n value: string\n}\n\nexport type GeminiTranslationRequest = {\n sourceLocale: string\n targetLocale: string\n texts: string[]\n}\n"],"names":[],"mappings":"AA0CA,WAIC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export function applyTranslations(originalData, fields, translations) {
|
|
2
|
+
// Deep clone the original data
|
|
3
|
+
const result = JSON.parse(JSON.stringify(originalData));
|
|
4
|
+
fields.forEach((field, index)=>{
|
|
5
|
+
const translation = translations[index];
|
|
6
|
+
if (translation === undefined) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
if (field.type === 'richText' && field.lexicalPath) {
|
|
10
|
+
// For rich text, apply translation to the specific text node in the Lexical structure
|
|
11
|
+
applyLexicalTranslation(result, field.path, field.lexicalPath, translation);
|
|
12
|
+
} else {
|
|
13
|
+
// For text/textarea, directly set the translation
|
|
14
|
+
setNestedValue(result, field.path, translation);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
return result;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Apply a translation to a specific text node within a Lexical rich text field.
|
|
21
|
+
* @param data - The document data object
|
|
22
|
+
* @param fieldPath - Path to the rich text field (e.g., "content")
|
|
23
|
+
* @param lexicalPath - Path within the Lexical structure (e.g., "root.children.0.children.1")
|
|
24
|
+
* @param translation - The translated text
|
|
25
|
+
*/ function applyLexicalTranslation(data, fieldPath, lexicalPath, translation) {
|
|
26
|
+
// Get the Lexical editor state object
|
|
27
|
+
const lexicalState = getNestedValue(data, fieldPath);
|
|
28
|
+
if (!lexicalState || typeof lexicalState !== 'object') {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
// Navigate to the text node and update its text property
|
|
32
|
+
const pathParts = lexicalPath.split('.');
|
|
33
|
+
let current = lexicalState;
|
|
34
|
+
for(let i = 0; i < pathParts.length; i++){
|
|
35
|
+
const key = pathParts[i];
|
|
36
|
+
if (current && typeof current === 'object' && key in current) {
|
|
37
|
+
if (i === pathParts.length - 1) {
|
|
38
|
+
// We're at the text node, update the text property
|
|
39
|
+
;
|
|
40
|
+
current[key] = current[key];
|
|
41
|
+
}
|
|
42
|
+
current = current[key];
|
|
43
|
+
} else {
|
|
44
|
+
return; // Path not found
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// current should now be the text node
|
|
48
|
+
if (current && typeof current === 'object' && 'text' in current) {
|
|
49
|
+
;
|
|
50
|
+
current.text = translation;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function getNestedValue(obj, path) {
|
|
54
|
+
const keys = path.split('.');
|
|
55
|
+
let current = obj;
|
|
56
|
+
for (const key of keys){
|
|
57
|
+
if (current && typeof current === 'object' && key in current) {
|
|
58
|
+
current = current[key];
|
|
59
|
+
} else {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return current;
|
|
64
|
+
}
|
|
65
|
+
function setNestedValue(obj, path, value) {
|
|
66
|
+
const keys = path.split('.');
|
|
67
|
+
let current = obj;
|
|
68
|
+
for(let i = 0; i < keys.length - 1; i++){
|
|
69
|
+
const key = keys[i];
|
|
70
|
+
const nextKey = keys[i + 1];
|
|
71
|
+
if (!(key in current)) {
|
|
72
|
+
// Create array or object based on next key
|
|
73
|
+
current[key] = isNaN(Number(nextKey)) ? {} : [];
|
|
74
|
+
}
|
|
75
|
+
current = current[key];
|
|
76
|
+
}
|
|
77
|
+
current[keys[keys.length - 1]] = value;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
//# sourceMappingURL=applyTranslations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/applyTranslations.ts"],"sourcesContent":["import type { TranslatableField } from '../types.js'\n\nexport function applyTranslations(\n originalData: Record<string, unknown>,\n fields: TranslatableField[],\n translations: string[],\n): Record<string, unknown> {\n // Deep clone the original data\n const result = JSON.parse(JSON.stringify(originalData)) as Record<string, unknown>\n\n fields.forEach((field, index) => {\n const translation = translations[index]\n if (translation === undefined) {\n return\n }\n\n if (field.type === 'richText' && field.lexicalPath) {\n // For rich text, apply translation to the specific text node in the Lexical structure\n applyLexicalTranslation(result, field.path, field.lexicalPath, translation)\n } else {\n // For text/textarea, directly set the translation\n setNestedValue(result, field.path, translation)\n }\n })\n\n return result\n}\n\n/**\n * Apply a translation to a specific text node within a Lexical rich text field.\n * @param data - The document data object\n * @param fieldPath - Path to the rich text field (e.g., \"content\")\n * @param lexicalPath - Path within the Lexical structure (e.g., \"root.children.0.children.1\")\n * @param translation - The translated text\n */\nfunction applyLexicalTranslation(\n data: Record<string, unknown>,\n fieldPath: string,\n lexicalPath: string,\n translation: string,\n): void {\n // Get the Lexical editor state object\n const lexicalState = getNestedValue(data, fieldPath)\n if (!lexicalState || typeof lexicalState !== 'object') {\n return\n }\n\n // Navigate to the text node and update its text property\n const pathParts = lexicalPath.split('.')\n let current: unknown = lexicalState\n\n for (let i = 0; i < pathParts.length; i++) {\n const key = pathParts[i]\n if (current && typeof current === 'object' && key in current) {\n if (i === pathParts.length - 1) {\n // We're at the text node, update the text property\n ;(current as Record<string, unknown>)[key] = current[key as keyof typeof current]\n }\n current = (current as Record<string, unknown>)[key]\n } else {\n return // Path not found\n }\n }\n\n // current should now be the text node\n if (current && typeof current === 'object' && 'text' in current) {\n ;(current as Record<string, unknown>).text = translation\n }\n}\n\nfunction getNestedValue(obj: Record<string, unknown>, path: string): unknown {\n const keys = path.split('.')\n let current: unknown = obj\n\n for (const key of keys) {\n if (current && typeof current === 'object' && key in current) {\n current = (current as Record<string, unknown>)[key]\n } else {\n return undefined\n }\n }\n\n return current\n}\n\nfunction setNestedValue(obj: Record<string, unknown>, path: string, value: unknown): void {\n const keys = path.split('.')\n let current: Record<string, unknown> = obj\n\n for (let i = 0; i < keys.length - 1; i++) {\n const key = keys[i]\n const nextKey = keys[i + 1]\n\n if (!(key in current)) {\n // Create array or object based on next key\n current[key] = isNaN(Number(nextKey)) ? {} : []\n }\n\n current = current[key] as Record<string, unknown>\n }\n\n current[keys[keys.length - 1]] = value\n}\n"],"names":["applyTranslations","originalData","fields","translations","result","JSON","parse","stringify","forEach","field","index","translation","undefined","type","lexicalPath","applyLexicalTranslation","path","setNestedValue","data","fieldPath","lexicalState","getNestedValue","pathParts","split","current","i","length","key","text","obj","keys","value","nextKey","isNaN","Number"],"mappings":"AAEA,OAAO,SAASA,kBACdC,YAAqC,EACrCC,MAA2B,EAC3BC,YAAsB;IAEtB,+BAA+B;IAC/B,MAAMC,SAASC,KAAKC,KAAK,CAACD,KAAKE,SAAS,CAACN;IAEzCC,OAAOM,OAAO,CAAC,CAACC,OAAOC;QACrB,MAAMC,cAAcR,YAAY,CAACO,MAAM;QACvC,IAAIC,gBAAgBC,WAAW;YAC7B;QACF;QAEA,IAAIH,MAAMI,IAAI,KAAK,cAAcJ,MAAMK,WAAW,EAAE;YAClD,sFAAsF;YACtFC,wBAAwBX,QAAQK,MAAMO,IAAI,EAAEP,MAAMK,WAAW,EAAEH;QACjE,OAAO;YACL,kDAAkD;YAClDM,eAAeb,QAAQK,MAAMO,IAAI,EAAEL;QACrC;IACF;IAEA,OAAOP;AACT;AAEA;;;;;;CAMC,GACD,SAASW,wBACPG,IAA6B,EAC7BC,SAAiB,EACjBL,WAAmB,EACnBH,WAAmB;IAEnB,sCAAsC;IACtC,MAAMS,eAAeC,eAAeH,MAAMC;IAC1C,IAAI,CAACC,gBAAgB,OAAOA,iBAAiB,UAAU;QACrD;IACF;IAEA,yDAAyD;IACzD,MAAME,YAAYR,YAAYS,KAAK,CAAC;IACpC,IAAIC,UAAmBJ;IAEvB,IAAK,IAAIK,IAAI,GAAGA,IAAIH,UAAUI,MAAM,EAAED,IAAK;QACzC,MAAME,MAAML,SAAS,CAACG,EAAE;QACxB,IAAID,WAAW,OAAOA,YAAY,YAAYG,OAAOH,SAAS;YAC5D,IAAIC,MAAMH,UAAUI,MAAM,GAAG,GAAG;gBAC9B,mDAAmD;;gBACjDF,OAAmC,CAACG,IAAI,GAAGH,OAAO,CAACG,IAA4B;YACnF;YACAH,UAAU,AAACA,OAAmC,CAACG,IAAI;QACrD,OAAO;YACL,QAAO,iBAAiB;QAC1B;IACF;IAEA,sCAAsC;IACtC,IAAIH,WAAW,OAAOA,YAAY,YAAY,UAAUA,SAAS;;QAC7DA,QAAoCI,IAAI,GAAGjB;IAC/C;AACF;AAEA,SAASU,eAAeQ,GAA4B,EAAEb,IAAY;IAChE,MAAMc,OAAOd,KAAKO,KAAK,CAAC;IACxB,IAAIC,UAAmBK;IAEvB,KAAK,MAAMF,OAAOG,KAAM;QACtB,IAAIN,WAAW,OAAOA,YAAY,YAAYG,OAAOH,SAAS;YAC5DA,UAAU,AAACA,OAAmC,CAACG,IAAI;QACrD,OAAO;YACL,OAAOf;QACT;IACF;IAEA,OAAOY;AACT;AAEA,SAASP,eAAeY,GAA4B,EAAEb,IAAY,EAAEe,KAAc;IAChF,MAAMD,OAAOd,KAAKO,KAAK,CAAC;IACxB,IAAIC,UAAmCK;IAEvC,IAAK,IAAIJ,IAAI,GAAGA,IAAIK,KAAKJ,MAAM,GAAG,GAAGD,IAAK;QACxC,MAAME,MAAMG,IAAI,CAACL,EAAE;QACnB,MAAMO,UAAUF,IAAI,CAACL,IAAI,EAAE;QAE3B,IAAI,CAAEE,CAAAA,OAAOH,OAAM,GAAI;YACrB,2CAA2C;YAC3CA,OAAO,CAACG,IAAI,GAAGM,MAAMC,OAAOF,YAAY,CAAC,IAAI,EAAE;QACjD;QAEAR,UAAUA,OAAO,CAACG,IAAI;IACxB;IAEAH,OAAO,CAACM,IAAI,CAACA,KAAKJ,MAAM,GAAG,EAAE,CAAC,GAAGK;AACnC"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export function extractTranslatableFields(data, fields, basePath = '') {
|
|
2
|
+
const translatableFields = [];
|
|
3
|
+
for (const field of fields){
|
|
4
|
+
// Skip fields without names (like row, collapsible without name)
|
|
5
|
+
if (!('name' in field)) {
|
|
6
|
+
// Handle layout fields that contain nested fields
|
|
7
|
+
if ('fields' in field) {
|
|
8
|
+
const nestedFields = extractTranslatableFields(data, field.fields, basePath);
|
|
9
|
+
translatableFields.push(...nestedFields);
|
|
10
|
+
}
|
|
11
|
+
// Handle tabs
|
|
12
|
+
if ('tabs' in field) {
|
|
13
|
+
for (const tab of field.tabs){
|
|
14
|
+
if ('fields' in tab) {
|
|
15
|
+
const tabFields = extractTranslatableFields(data, tab.fields, basePath);
|
|
16
|
+
translatableFields.push(...tabFields);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const fieldPath = basePath ? `${basePath}.${field.name}` : field.name;
|
|
23
|
+
const value = data[field.name];
|
|
24
|
+
// Skip if no value
|
|
25
|
+
if (value === undefined || value === null) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
// Only process localized fields (UIField doesn't have localized property)
|
|
29
|
+
const isLocalized = 'localized' in field && field.localized;
|
|
30
|
+
if (!isLocalized) {
|
|
31
|
+
// But still recurse into nested structures for localized children
|
|
32
|
+
if (field.type === 'group' && 'fields' in field && typeof value === 'object') {
|
|
33
|
+
const groupFields = extractTranslatableFields(value, field.fields, fieldPath);
|
|
34
|
+
translatableFields.push(...groupFields);
|
|
35
|
+
}
|
|
36
|
+
if (field.type === 'array' && 'fields' in field && Array.isArray(value)) {
|
|
37
|
+
value.forEach((item, index)=>{
|
|
38
|
+
if (typeof item === 'object' && item !== null) {
|
|
39
|
+
const itemFields = extractTranslatableFields(item, field.fields, `${fieldPath}.${index}`);
|
|
40
|
+
translatableFields.push(...itemFields);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
if (field.type === 'blocks' && 'blocks' in field && Array.isArray(value)) {
|
|
45
|
+
value.forEach((block, index)=>{
|
|
46
|
+
if (typeof block === 'object' && block !== null && 'blockType' in block) {
|
|
47
|
+
const blockConfig = field.blocks.find((b)=>b.slug === block.blockType);
|
|
48
|
+
if (blockConfig) {
|
|
49
|
+
const blockFields = extractTranslatableFields(block, blockConfig.fields, `${fieldPath}.${index}`);
|
|
50
|
+
translatableFields.push(...blockFields);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
// Process localized fields
|
|
58
|
+
switch(field.type){
|
|
59
|
+
case 'array':
|
|
60
|
+
if ('fields' in field && Array.isArray(value)) {
|
|
61
|
+
value.forEach((item, index)=>{
|
|
62
|
+
if (typeof item === 'object' && item !== null) {
|
|
63
|
+
const itemFields = extractTranslatableFields(item, field.fields, `${fieldPath}.${index}`);
|
|
64
|
+
translatableFields.push(...itemFields);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
case 'blocks':
|
|
70
|
+
if ('blocks' in field && Array.isArray(value)) {
|
|
71
|
+
value.forEach((block, index)=>{
|
|
72
|
+
if (typeof block === 'object' && block !== null && 'blockType' in block) {
|
|
73
|
+
const blockConfig = field.blocks.find((b)=>b.slug === block.blockType);
|
|
74
|
+
if (blockConfig) {
|
|
75
|
+
const blockFields = extractTranslatableFields(block, blockConfig.fields, `${fieldPath}.${index}`);
|
|
76
|
+
translatableFields.push(...blockFields);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
break;
|
|
82
|
+
case 'group':
|
|
83
|
+
if ('fields' in field && typeof value === 'object') {
|
|
84
|
+
const groupFields = extractTranslatableFields(value, field.fields, fieldPath);
|
|
85
|
+
translatableFields.push(...groupFields);
|
|
86
|
+
}
|
|
87
|
+
break;
|
|
88
|
+
case 'richText':
|
|
89
|
+
if (value && typeof value === 'object') {
|
|
90
|
+
// Extract each text node individually with its path in the Lexical tree
|
|
91
|
+
const textNodes = extractLexicalTextNodes(value);
|
|
92
|
+
for (const textNode of textNodes){
|
|
93
|
+
if (textNode.text.trim()) {
|
|
94
|
+
translatableFields.push({
|
|
95
|
+
type: 'richText',
|
|
96
|
+
lexicalPath: textNode.path,
|
|
97
|
+
path: fieldPath,
|
|
98
|
+
value: textNode.text
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
break;
|
|
104
|
+
case 'text':
|
|
105
|
+
// falls through
|
|
106
|
+
case 'textarea':
|
|
107
|
+
if (typeof value === 'string' && value.trim()) {
|
|
108
|
+
translatableFields.push({
|
|
109
|
+
type: field.type,
|
|
110
|
+
path: fieldPath,
|
|
111
|
+
value
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return translatableFields;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Extracts all text nodes from a Lexical editor state with their paths.
|
|
121
|
+
* This allows us to translate each text node individually and apply
|
|
122
|
+
* translations back to the exact same location.
|
|
123
|
+
*/ function extractLexicalTextNodes(editorState) {
|
|
124
|
+
const textNodes = [];
|
|
125
|
+
if (!editorState || typeof editorState !== 'object') {
|
|
126
|
+
return textNodes;
|
|
127
|
+
}
|
|
128
|
+
const state = editorState;
|
|
129
|
+
if (!state.root) {
|
|
130
|
+
return textNodes;
|
|
131
|
+
}
|
|
132
|
+
function traverse(node, currentPath) {
|
|
133
|
+
if (node.type === 'text' && typeof node.text === 'string') {
|
|
134
|
+
textNodes.push({
|
|
135
|
+
path: currentPath,
|
|
136
|
+
text: node.text
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
if (node.children && Array.isArray(node.children)) {
|
|
140
|
+
node.children.forEach((child, index)=>{
|
|
141
|
+
traverse(child, `${currentPath}.children.${index}`);
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
traverse(state.root, 'root');
|
|
146
|
+
return textNodes;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=extractTranslatableFields.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/extractTranslatableFields.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nimport type { TranslatableField } from '../types.js'\n\nexport function extractTranslatableFields(\n data: Record<string, unknown>,\n fields: Field[],\n basePath: string = '',\n): TranslatableField[] {\n const translatableFields: TranslatableField[] = []\n\n for (const field of fields) {\n // Skip fields without names (like row, collapsible without name)\n if (!('name' in field)) {\n // Handle layout fields that contain nested fields\n if ('fields' in field) {\n const nestedFields = extractTranslatableFields(data, field.fields, basePath)\n translatableFields.push(...nestedFields)\n }\n // Handle tabs\n if ('tabs' in field) {\n for (const tab of field.tabs) {\n if ('fields' in tab) {\n const tabFields = extractTranslatableFields(data, tab.fields, basePath)\n translatableFields.push(...tabFields)\n }\n }\n }\n continue\n }\n\n const fieldPath = basePath ? `${basePath}.${field.name}` : field.name\n const value = data[field.name]\n\n // Skip if no value\n if (value === undefined || value === null) {continue}\n\n // Only process localized fields (UIField doesn't have localized property)\n const isLocalized = 'localized' in field && field.localized\n if (!isLocalized) {\n // But still recurse into nested structures for localized children\n if (field.type === 'group' && 'fields' in field && typeof value === 'object') {\n const groupFields = extractTranslatableFields(\n value as Record<string, unknown>,\n field.fields,\n fieldPath,\n )\n translatableFields.push(...groupFields)\n }\n if (field.type === 'array' && 'fields' in field && Array.isArray(value)) {\n value.forEach((item, index) => {\n if (typeof item === 'object' && item !== null) {\n const itemFields = extractTranslatableFields(\n item as Record<string, unknown>,\n field.fields,\n `${fieldPath}.${index}`,\n )\n translatableFields.push(...itemFields)\n }\n })\n }\n if (field.type === 'blocks' && 'blocks' in field && Array.isArray(value)) {\n value.forEach((block, index) => {\n if (typeof block === 'object' && block !== null && 'blockType' in block) {\n const blockConfig = field.blocks.find((b) => b.slug === block.blockType)\n if (blockConfig) {\n const blockFields = extractTranslatableFields(\n block as Record<string, unknown>,\n blockConfig.fields,\n `${fieldPath}.${index}`,\n )\n translatableFields.push(...blockFields)\n }\n }\n })\n }\n continue\n }\n\n // Process localized fields\n switch (field.type) {\n case 'array':\n if ('fields' in field && Array.isArray(value)) {\n value.forEach((item, index) => {\n if (typeof item === 'object' && item !== null) {\n const itemFields = extractTranslatableFields(\n item as Record<string, unknown>,\n field.fields,\n `${fieldPath}.${index}`,\n )\n translatableFields.push(...itemFields)\n }\n })\n }\n break\n case 'blocks':\n if ('blocks' in field && Array.isArray(value)) {\n value.forEach((block, index) => {\n if (typeof block === 'object' && block !== null && 'blockType' in block) {\n const blockConfig = field.blocks.find((b) => b.slug === block.blockType)\n if (blockConfig) {\n const blockFields = extractTranslatableFields(\n block as Record<string, unknown>,\n blockConfig.fields,\n `${fieldPath}.${index}`,\n )\n translatableFields.push(...blockFields)\n }\n }\n })\n }\n break\n\n case 'group':\n if ('fields' in field && typeof value === 'object') {\n const groupFields = extractTranslatableFields(\n value as Record<string, unknown>,\n field.fields,\n fieldPath,\n )\n translatableFields.push(...groupFields)\n }\n break\n\n case 'richText':\n if (value && typeof value === 'object') {\n // Extract each text node individually with its path in the Lexical tree\n const textNodes = extractLexicalTextNodes(value)\n for (const textNode of textNodes) {\n if (textNode.text.trim()) {\n translatableFields.push({\n type: 'richText',\n lexicalPath: textNode.path,\n path: fieldPath,\n value: textNode.text,\n })\n }\n }\n }\n break\n\n case 'text':\n // falls through\n case 'textarea':\n if (typeof value === 'string' && value.trim()) {\n translatableFields.push({\n type: field.type,\n path: fieldPath,\n value,\n })\n }\n break\n }\n }\n\n return translatableFields\n}\n\ninterface LexicalNode {\n [key: string]: unknown\n children?: LexicalNode[]\n text?: string\n type?: string\n}\n\ninterface LexicalTextNode {\n path: string\n text: string\n}\n\n/**\n * Extracts all text nodes from a Lexical editor state with their paths.\n * This allows us to translate each text node individually and apply\n * translations back to the exact same location.\n */\nfunction extractLexicalTextNodes(editorState: unknown): LexicalTextNode[] {\n const textNodes: LexicalTextNode[] = []\n\n if (!editorState || typeof editorState !== 'object') {\n return textNodes\n }\n\n const state = editorState as { root?: LexicalNode }\n if (!state.root) {\n return textNodes\n }\n\n function traverse(node: LexicalNode, currentPath: string): void {\n if (node.type === 'text' && typeof node.text === 'string') {\n textNodes.push({\n path: currentPath,\n text: node.text,\n })\n }\n if (node.children && Array.isArray(node.children)) {\n node.children.forEach((child, index) => {\n traverse(child, `${currentPath}.children.${index}`)\n })\n }\n }\n\n traverse(state.root, 'root')\n return textNodes\n}\n"],"names":["extractTranslatableFields","data","fields","basePath","translatableFields","field","nestedFields","push","tab","tabs","tabFields","fieldPath","name","value","undefined","isLocalized","localized","type","groupFields","Array","isArray","forEach","item","index","itemFields","block","blockConfig","blocks","find","b","slug","blockType","blockFields","textNodes","extractLexicalTextNodes","textNode","text","trim","lexicalPath","path","editorState","state","root","traverse","node","currentPath","children","child"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "payload-translate",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered translation plugin for Payload",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Bridger Tower",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/brijr/payload-translate.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/brijr/payload-translate#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/brijr/payload-translate/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"payload",
|
|
17
|
+
"payloadcms",
|
|
18
|
+
"plugin",
|
|
19
|
+
"translation",
|
|
20
|
+
"i18n",
|
|
21
|
+
"localization",
|
|
22
|
+
"gemini",
|
|
23
|
+
"ai"
|
|
24
|
+
],
|
|
25
|
+
"type": "module",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"import": "./src/index.ts",
|
|
29
|
+
"types": "./src/index.ts",
|
|
30
|
+
"default": "./src/index.ts"
|
|
31
|
+
},
|
|
32
|
+
"./client": {
|
|
33
|
+
"import": "./src/exports/client.ts",
|
|
34
|
+
"types": "./src/exports/client.ts",
|
|
35
|
+
"default": "./src/exports/client.ts"
|
|
36
|
+
},
|
|
37
|
+
"./rsc": {
|
|
38
|
+
"import": "./src/exports/rsc.ts",
|
|
39
|
+
"types": "./src/exports/rsc.ts",
|
|
40
|
+
"default": "./src/exports/rsc.ts"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"main": "./src/index.ts",
|
|
44
|
+
"types": "./src/index.ts",
|
|
45
|
+
"files": [
|
|
46
|
+
"dist"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"build": "pnpm copyfiles && pnpm build:types && pnpm build:swc",
|
|
50
|
+
"build:swc": "swc ./src -d ./dist --config-file .swcrc --strip-leading-paths",
|
|
51
|
+
"build:types": "tsc --outDir dist --rootDir ./src",
|
|
52
|
+
"clean": "rimraf {dist,*.tsbuildinfo}",
|
|
53
|
+
"copyfiles": "copyfiles -u 1 \"src/**/*.{html,css,scss,ttf,woff,woff2,eot,svg,jpg,png,json}\" dist/",
|
|
54
|
+
"dev": "next dev dev --turbo",
|
|
55
|
+
"dev:generate-importmap": "pnpm dev:payload generate:importmap",
|
|
56
|
+
"dev:generate-types": "pnpm dev:payload generate:types",
|
|
57
|
+
"dev:payload": "cross-env PAYLOAD_CONFIG_PATH=./dev/payload.config.ts payload",
|
|
58
|
+
"generate:importmap": "pnpm dev:generate-importmap",
|
|
59
|
+
"generate:types": "pnpm dev:generate-types",
|
|
60
|
+
"lint": "eslint",
|
|
61
|
+
"lint:fix": "eslint ./src --fix",
|
|
62
|
+
"prepublishOnly": "pnpm clean && pnpm build",
|
|
63
|
+
"test": "pnpm test:int && pnpm test:e2e",
|
|
64
|
+
"test:e2e": "playwright test",
|
|
65
|
+
"test:int": "vitest"
|
|
66
|
+
},
|
|
67
|
+
"devDependencies": {
|
|
68
|
+
"@eslint/eslintrc": "^3.2.0",
|
|
69
|
+
"@payloadcms/db-mongodb": "3.37.0",
|
|
70
|
+
"@payloadcms/db-postgres": "3.37.0",
|
|
71
|
+
"@payloadcms/db-sqlite": "3.37.0",
|
|
72
|
+
"@payloadcms/eslint-config": "3.9.0",
|
|
73
|
+
"@payloadcms/next": "3.37.0",
|
|
74
|
+
"@payloadcms/richtext-lexical": "3.37.0",
|
|
75
|
+
"@payloadcms/ui": "3.37.0",
|
|
76
|
+
"@playwright/test": "1.56.1",
|
|
77
|
+
"@swc-node/register": "1.10.9",
|
|
78
|
+
"@swc/cli": "0.6.0",
|
|
79
|
+
"@types/node": "^22.5.4",
|
|
80
|
+
"@types/react": "19.2.1",
|
|
81
|
+
"@types/react-dom": "19.2.1",
|
|
82
|
+
"copyfiles": "2.4.1",
|
|
83
|
+
"cross-env": "^7.0.3",
|
|
84
|
+
"eslint": "^9.23.0",
|
|
85
|
+
"eslint-config-next": "15.4.7",
|
|
86
|
+
"graphql": "^16.8.1",
|
|
87
|
+
"mongodb-memory-server": "10.1.4",
|
|
88
|
+
"next": "15.5.7",
|
|
89
|
+
"open": "^10.1.0",
|
|
90
|
+
"payload": "3.37.0",
|
|
91
|
+
"prettier": "^3.4.2",
|
|
92
|
+
"qs-esm": "7.0.2",
|
|
93
|
+
"react": "19.2.1",
|
|
94
|
+
"react-dom": "19.2.1",
|
|
95
|
+
"rimraf": "3.0.2",
|
|
96
|
+
"sharp": "0.34.2",
|
|
97
|
+
"sort-package-json": "^2.10.0",
|
|
98
|
+
"typescript": "5.7.3",
|
|
99
|
+
"vite-tsconfig-paths": "^5.1.4",
|
|
100
|
+
"vitest": "^3.1.2"
|
|
101
|
+
},
|
|
102
|
+
"peerDependencies": {
|
|
103
|
+
"payload": "^3.37.0"
|
|
104
|
+
},
|
|
105
|
+
"engines": {
|
|
106
|
+
"node": "^18.20.2 || >=20.9.0",
|
|
107
|
+
"pnpm": "^9 || ^10"
|
|
108
|
+
},
|
|
109
|
+
"publishConfig": {
|
|
110
|
+
"exports": {
|
|
111
|
+
".": {
|
|
112
|
+
"import": "./dist/index.js",
|
|
113
|
+
"types": "./dist/index.d.ts",
|
|
114
|
+
"default": "./dist/index.js"
|
|
115
|
+
},
|
|
116
|
+
"./client": {
|
|
117
|
+
"import": "./dist/exports/client.js",
|
|
118
|
+
"types": "./dist/exports/client.d.ts",
|
|
119
|
+
"default": "./dist/exports/client.js"
|
|
120
|
+
},
|
|
121
|
+
"./rsc": {
|
|
122
|
+
"import": "./dist/exports/rsc.js",
|
|
123
|
+
"types": "./dist/exports/rsc.d.ts",
|
|
124
|
+
"default": "./dist/exports/rsc.js"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"main": "./dist/index.js",
|
|
128
|
+
"types": "./dist/index.d.ts"
|
|
129
|
+
},
|
|
130
|
+
"pnpm": {
|
|
131
|
+
"onlyBuiltDependencies": [
|
|
132
|
+
"sharp",
|
|
133
|
+
"esbuild",
|
|
134
|
+
"unrs-resolver"
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
"registry": "https://registry.npmjs.org/",
|
|
138
|
+
"dependencies": {
|
|
139
|
+
"sonner": "^2.0.7"
|
|
140
|
+
}
|
|
141
|
+
}
|