@pigment/auto-translate 1.5.0 → 1.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 +28 -10
- package/dist/collections/translationExclusions.js +101 -4
- package/dist/collections/translationExclusions.js.map +1 -1
- package/dist/components/LockTranslation/actions/lockTranslations.d.ts +1 -1
- package/dist/components/LockTranslation/actions/lockTranslations.js +2 -2
- package/dist/components/LockTranslation/actions/lockTranslations.js.map +1 -1
- package/dist/components/LockTranslation/index.js +7 -6
- package/dist/components/LockTranslation/index.js.map +1 -1
- package/dist/components/OpenAiModelField.d.ts +2 -0
- package/dist/components/OpenAiModelField.js +96 -0
- package/dist/components/OpenAiModelField.js.map +1 -0
- package/dist/components/TranslationControl.js +45 -51
- package/dist/components/TranslationControl.js.map +1 -1
- package/dist/endpoints/listOpenAiModels.d.ts +5 -0
- package/dist/endpoints/listOpenAiModels.js +93 -0
- package/dist/endpoints/listOpenAiModels.js.map +1 -0
- package/dist/exports/client.d.ts +1 -0
- package/dist/exports/client.js +1 -0
- package/dist/exports/client.js.map +1 -1
- package/dist/globals/translationSettings.js +24 -13
- package/dist/globals/translationSettings.js.map +1 -1
- package/dist/services/translationService.js +17 -17
- package/dist/services/translationService.js.map +1 -1
- package/dist/utilities/fieldHelpers.d.ts +0 -29
- package/dist/utilities/fieldHelpers.js +0 -152
- package/dist/utilities/fieldHelpers.js.map +1 -1
- package/dist/utilities/injectTranslationControls.d.ts +1 -1
- package/dist/utilities/injectTranslationControls.js +16 -11
- package/dist/utilities/injectTranslationControls.js.map +1 -1
- package/package.json +12 -12
- package/dist/components/TranslationSettingsLock.css +0 -87
- package/dist/components/TranslationSettingsLock.d.ts +0 -9
- package/dist/components/TranslationSettingsLock.js +0 -155
- package/dist/components/TranslationSettingsLock.js.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useDocumentInfo, useLocale } from '@payloadcms/ui';
|
|
3
|
+
import { useConfig, useDocumentInfo, useLocale } from '@payloadcms/ui';
|
|
4
4
|
import React, { useCallback, useEffect, useState } from 'react';
|
|
5
5
|
import './TranslationControl.css';
|
|
6
6
|
/**
|
|
@@ -16,23 +16,17 @@ import './TranslationControl.css';
|
|
|
16
16
|
const fieldPath = payloadPath || clientFieldPath;
|
|
17
17
|
const { id, collectionSlug: docCollectionSlug } = useDocumentInfo();
|
|
18
18
|
const { code: currentLocale } = useLocale();
|
|
19
|
+
const { config: { routes, serverURL } } = useConfig();
|
|
20
|
+
const exclusionsURL = `${serverURL}${routes.api}/translation-exclusions`;
|
|
19
21
|
const [isExcluded, setIsExcluded] = useState(false);
|
|
20
22
|
const [isLoading, setIsLoading] = useState(false);
|
|
21
23
|
// Use collectionSlug from props or from document context
|
|
22
24
|
const effectiveCollectionSlug = collectionSlug || docCollectionSlug;
|
|
23
|
-
// Don't show on default locale - you can only lock fields in secondary locales
|
|
24
|
-
if (currentLocale === defaultLocale) {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
// Don't show if we don't have a valid field path
|
|
28
|
-
if (!fieldPath) {
|
|
29
|
-
console.warn('[TranslationControl] No field path available');
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
25
|
// Load exclusion state on mount and when locale changes
|
|
33
26
|
useEffect(()=>{
|
|
34
|
-
// Reset state when switching documents
|
|
35
|
-
|
|
27
|
+
// Reset state when switching documents, on create (no ID), or on the default
|
|
28
|
+
// locale where the control is hidden anyway
|
|
29
|
+
if (!id || !effectiveCollectionSlug || currentLocale === defaultLocale) {
|
|
36
30
|
setIsExcluded(false); // Reset to default state
|
|
37
31
|
return;
|
|
38
32
|
}
|
|
@@ -48,13 +42,13 @@ import './TranslationControl.css';
|
|
|
48
42
|
const whereQuery = {
|
|
49
43
|
and: [
|
|
50
44
|
{
|
|
51
|
-
|
|
45
|
+
collectionSlug: {
|
|
52
46
|
equals: effectiveCollectionSlug
|
|
53
47
|
}
|
|
54
48
|
},
|
|
55
49
|
{
|
|
56
50
|
documentId: {
|
|
57
|
-
equals: id
|
|
51
|
+
equals: String(id)
|
|
58
52
|
}
|
|
59
53
|
},
|
|
60
54
|
{
|
|
@@ -68,7 +62,7 @@ import './TranslationControl.css';
|
|
|
68
62
|
limit: '1',
|
|
69
63
|
where: JSON.stringify(whereQuery)
|
|
70
64
|
}).toString();
|
|
71
|
-
const fullUrl =
|
|
65
|
+
const fullUrl = `${exclusionsURL}?${queryString}`;
|
|
72
66
|
console.log('[TranslationControl] Query URL:', fullUrl);
|
|
73
67
|
console.log('[TranslationControl] Where clause:', whereQuery);
|
|
74
68
|
const response = await fetch(fullUrl);
|
|
@@ -77,7 +71,7 @@ import './TranslationControl.css';
|
|
|
77
71
|
if (data.docs && data.docs.length > 0) {
|
|
78
72
|
const exclusion = data.docs[0];
|
|
79
73
|
// CRITICAL: Verify this exclusion belongs to THIS document AND locale
|
|
80
|
-
if (exclusion.locale === currentLocale && exclusion.documentId === id) {
|
|
74
|
+
if (exclusion.locale === currentLocale && exclusion.documentId === String(id)) {
|
|
81
75
|
const excludedPaths = exclusion.excludedPaths?.map((item)=>item.path) || [];
|
|
82
76
|
const isFieldExcluded = excludedPaths.includes(fieldPath);
|
|
83
77
|
console.log('[TranslationControl] Loaded exclusions for document', id, 'locale', currentLocale, ':', {
|
|
@@ -111,10 +105,12 @@ import './TranslationControl.css';
|
|
|
111
105
|
id,
|
|
112
106
|
effectiveCollectionSlug,
|
|
113
107
|
currentLocale,
|
|
114
|
-
|
|
108
|
+
defaultLocale,
|
|
109
|
+
fieldPath,
|
|
110
|
+
exclusionsURL
|
|
115
111
|
]);
|
|
116
112
|
const toggleExclusion = useCallback(async ()=>{
|
|
117
|
-
if (!id || !effectiveCollectionSlug) {
|
|
113
|
+
if (!id || !effectiveCollectionSlug || !fieldPath) {
|
|
118
114
|
return;
|
|
119
115
|
}
|
|
120
116
|
setIsLoading(true);
|
|
@@ -123,13 +119,13 @@ import './TranslationControl.css';
|
|
|
123
119
|
const whereQuery = {
|
|
124
120
|
and: [
|
|
125
121
|
{
|
|
126
|
-
|
|
122
|
+
collectionSlug: {
|
|
127
123
|
equals: effectiveCollectionSlug
|
|
128
124
|
}
|
|
129
125
|
},
|
|
130
126
|
{
|
|
131
127
|
documentId: {
|
|
132
|
-
equals: id
|
|
128
|
+
equals: String(id)
|
|
133
129
|
}
|
|
134
130
|
},
|
|
135
131
|
{
|
|
@@ -151,7 +147,7 @@ import './TranslationControl.css';
|
|
|
151
147
|
limit: '1',
|
|
152
148
|
where: JSON.stringify(whereQuery)
|
|
153
149
|
}).toString();
|
|
154
|
-
const fullUrl =
|
|
150
|
+
const fullUrl = `${exclusionsURL}?${queryString}`;
|
|
155
151
|
console.log('[TranslationControl] Toggle - Query URL:', fullUrl);
|
|
156
152
|
console.log('[TranslationControl] Toggle - Where clause:', whereQuery);
|
|
157
153
|
const findResponse = await fetch(fullUrl);
|
|
@@ -163,7 +159,7 @@ import './TranslationControl.css';
|
|
|
163
159
|
if (data.docs && data.docs.length > 0) {
|
|
164
160
|
const exclusion = data.docs[0];
|
|
165
161
|
// CRITICAL: Verify this exclusion belongs to THIS document AND locale
|
|
166
|
-
if (exclusion.locale === currentLocale && exclusion.documentId === id) {
|
|
162
|
+
if (exclusion.locale === currentLocale && exclusion.documentId === String(id)) {
|
|
167
163
|
existingId = exclusion.id;
|
|
168
164
|
currentExcludedPaths = exclusion.excludedPaths?.map((item)=>item.path) || [];
|
|
169
165
|
console.log('[TranslationControl] Current excluded paths for document', id, 'locale', currentLocale, ':', currentExcludedPaths);
|
|
@@ -192,8 +188,8 @@ import './TranslationControl.css';
|
|
|
192
188
|
}
|
|
193
189
|
// Create the exclusion data - ALWAYS include the current locale
|
|
194
190
|
const exclusionsData = {
|
|
195
|
-
|
|
196
|
-
documentId: id,
|
|
191
|
+
collectionSlug: effectiveCollectionSlug,
|
|
192
|
+
documentId: String(id),
|
|
197
193
|
excludedPaths: currentExcludedPaths.map((path)=>({
|
|
198
194
|
path
|
|
199
195
|
})),
|
|
@@ -201,31 +197,27 @@ import './TranslationControl.css';
|
|
|
201
197
|
};
|
|
202
198
|
console.log('[TranslationControl] Saving exclusions:', exclusionsData);
|
|
203
199
|
// Update or create record using Payload's REST API
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
'Content-Type': 'application/json'
|
|
221
|
-
},
|
|
222
|
-
method: 'POST'
|
|
223
|
-
});
|
|
224
|
-
if (createResponse.ok) {
|
|
225
|
-
const result = await createResponse.json();
|
|
226
|
-
console.log('[TranslationControl] Created exclusions:', result.doc);
|
|
227
|
-
}
|
|
200
|
+
const saveResponse = existingId ? await fetch(`${exclusionsURL}/${existingId}`, {
|
|
201
|
+
body: JSON.stringify(exclusionsData),
|
|
202
|
+
headers: {
|
|
203
|
+
'Content-Type': 'application/json'
|
|
204
|
+
},
|
|
205
|
+
method: 'PATCH'
|
|
206
|
+
}) : await fetch(exclusionsURL, {
|
|
207
|
+
body: JSON.stringify(exclusionsData),
|
|
208
|
+
headers: {
|
|
209
|
+
'Content-Type': 'application/json'
|
|
210
|
+
},
|
|
211
|
+
method: 'POST'
|
|
212
|
+
});
|
|
213
|
+
if (!saveResponse.ok) {
|
|
214
|
+
const errorBody = await saveResponse.text();
|
|
215
|
+
throw new Error(`Save failed (${saveResponse.status}): ${errorBody}`);
|
|
228
216
|
}
|
|
217
|
+
const result = await saveResponse.json();
|
|
218
|
+
console.log('[TranslationControl] Saved exclusions:', result.doc);
|
|
219
|
+
// Only flip the displayed state once the save is confirmed — otherwise
|
|
220
|
+
// the button would show "Locked" for a field that was never persisted.
|
|
229
221
|
setIsExcluded(!isExcluded);
|
|
230
222
|
} catch (error) {
|
|
231
223
|
console.error('[TranslationControl] Error toggling exclusion:', error);
|
|
@@ -237,10 +229,12 @@ import './TranslationControl.css';
|
|
|
237
229
|
effectiveCollectionSlug,
|
|
238
230
|
currentLocale,
|
|
239
231
|
fieldPath,
|
|
240
|
-
isExcluded
|
|
232
|
+
isExcluded,
|
|
233
|
+
exclusionsURL
|
|
241
234
|
]);
|
|
242
|
-
// Don't show on
|
|
243
|
-
|
|
235
|
+
// Don't show on default locale (you can only lock fields in secondary locales),
|
|
236
|
+
// without a valid field path, or on create (no id yet)
|
|
237
|
+
if (currentLocale === defaultLocale || !fieldPath || !id) {
|
|
244
238
|
return null;
|
|
245
239
|
}
|
|
246
240
|
return /*#__PURE__*/ _jsxs("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/TranslationControl.tsx"],"sourcesContent":["'use client'\n\nimport { useDocumentInfo, useLocale } from '@payloadcms/ui'\nimport React, { useCallback, useEffect, useState } from 'react'\n\nimport './TranslationControl.css'\n\ntype TranslationControlProps = {\n collectionSlug?: string\n defaultLocale: string\n fieldPath?: string // Optional - will be inferred from Payload's path prop if not provided\n path?: string // Payload provides this at runtime with array/block indices\n}\n\n/**\n * UI component that allows users to toggle \"do not translate\" for specific fields\n * Only shows on secondary locales (not the default locale)\n * \n * The component can receive the field path in two ways:\n * 1. From Payload's `path` prop (preferred - includes runtime array/block indices)\n * 2. From the `fieldPath` clientProp (fallback - static path from field definition)\n */\nexport const TranslationControl: React.FC<TranslationControlProps> = ({\n collectionSlug,\n defaultLocale,\n fieldPath: clientFieldPath,\n path: payloadPath,\n}) => {\n // Use Payload's runtime path if available (includes array/block indices like \"layout.0.heading\")\n // Otherwise fall back to the static path from clientProps\n const fieldPath = payloadPath || clientFieldPath\n const { id, collectionSlug: docCollectionSlug } = useDocumentInfo()\n const { code: currentLocale } = useLocale()\n const [isExcluded, setIsExcluded] = useState(false)\n const [isLoading, setIsLoading] = useState(false)\n\n // Use collectionSlug from props or from document context\n const effectiveCollectionSlug = collectionSlug || docCollectionSlug\n\n // Don't show on default locale - you can only lock fields in secondary locales\n if (currentLocale === defaultLocale) {\n return null\n }\n\n // Don't show if we don't have a valid field path\n if (!fieldPath) {\n console.warn('[TranslationControl] No field path available')\n return null\n }\n\n // Load exclusion state on mount and when locale changes\n useEffect(() => {\n // Reset state when switching documents or when there's no ID (new document)\n if (!id || !effectiveCollectionSlug) {\n setIsExcluded(false) // Reset to default state\n return\n }\n\n const loadExclusionState = async () => {\n try {\n console.log('[TranslationControl] Loading exclusion state for:', {\n collection: effectiveCollectionSlug,\n documentId: id,\n fieldPath,\n locale: currentLocale,\n })\n\n // Build query for this specific locale AND document ID\n const whereQuery = {\n and: [\n { collection: { equals: effectiveCollectionSlug } },\n { documentId: { equals: id } }, // This ensures we only get exclusions for THIS document\n { locale: { equals: currentLocale } },\n ],\n }\n\n const queryString = new URLSearchParams({\n limit: '1',\n where: JSON.stringify(whereQuery),\n }).toString()\n\n const fullUrl = `/api/translation-exclusions?${queryString}`\n console.log('[TranslationControl] Query URL:', fullUrl)\n console.log('[TranslationControl] Where clause:', whereQuery)\n\n const response = await fetch(fullUrl)\n\n if (response.ok) {\n const data = await response.json()\n\n if (data.docs && data.docs.length > 0) {\n const exclusion = data.docs[0]\n\n // CRITICAL: Verify this exclusion belongs to THIS document AND locale\n if (exclusion.locale === currentLocale && exclusion.documentId === id) {\n const excludedPaths = exclusion.excludedPaths?.map((item: any) => item.path) || []\n const isFieldExcluded = excludedPaths.includes(fieldPath)\n\n console.log('[TranslationControl] Loaded exclusions for document', id, 'locale', currentLocale, ':', {\n excludedPaths,\n fieldPath,\n isFieldExcluded,\n })\n\n setIsExcluded(isFieldExcluded)\n } else {\n console.warn('[TranslationControl] Document/Locale mismatch in loaded exclusion!', {\n expectedLocale: currentLocale,\n expectedDocId: id,\n gotLocale: exclusion.locale,\n gotDocId: exclusion.documentId,\n })\n // This exclusion is for a different document - ignore it\n setIsExcluded(false)\n }\n } else {\n // No exclusions found for this document/locale - that's fine\n console.log('[TranslationControl] No exclusions found for document', id, 'locale', currentLocale)\n setIsExcluded(false)\n }\n }\n } catch (error) {\n console.error('[TranslationControl] Failed to load exclusion state:', error)\n }\n }\n\n loadExclusionState()\n }, [id, effectiveCollectionSlug, currentLocale, fieldPath])\n\n const toggleExclusion = useCallback(async () => {\n if (!id || !effectiveCollectionSlug) {\n return\n }\n\n setIsLoading(true)\n try {\n // Build query parameters for Payload REST API\n const whereQuery = {\n and: [\n { collection: { equals: effectiveCollectionSlug } },\n { documentId: { equals: id } },\n { locale: { equals: currentLocale } },\n ],\n }\n\n // Debug: Log the query we're making\n console.log('[TranslationControl] Fetching exclusions for:', {\n collection: effectiveCollectionSlug,\n documentId: id,\n fieldPath,\n locale: currentLocale,\n })\n\n // Properly format the where clause for Payload's REST API\n const queryString = new URLSearchParams({\n limit: '1',\n where: JSON.stringify(whereQuery),\n }).toString()\n\n const fullUrl = `/api/translation-exclusions?${queryString}`\n console.log('[TranslationControl] Toggle - Query URL:', fullUrl)\n console.log('[TranslationControl] Toggle - Where clause:', whereQuery)\n\n const findResponse = await fetch(fullUrl)\n\n let currentExcludedPaths: string[] = []\n let existingId: null | string = null\n\n if (findResponse.ok) {\n const data = await findResponse.json()\n console.log('[TranslationControl] Found exclusions:', data.docs)\n\n if (data.docs && data.docs.length > 0) {\n const exclusion = data.docs[0]\n\n // CRITICAL: Verify this exclusion belongs to THIS document AND locale\n if (exclusion.locale === currentLocale && exclusion.documentId === id) {\n existingId = exclusion.id\n currentExcludedPaths = exclusion.excludedPaths?.map((item: any) => item.path) || []\n console.log(\n '[TranslationControl] Current excluded paths for document',\n id,\n 'locale',\n currentLocale,\n ':',\n currentExcludedPaths,\n )\n } else {\n console.warn('[TranslationControl] Found exclusion for wrong document/locale!', {\n expectedLocale: currentLocale,\n expectedDocId: id,\n gotLocale: exclusion.locale,\n gotDocId: exclusion.documentId,\n })\n // Don't use this record - it's for a different document\n existingId = null\n currentExcludedPaths = []\n }\n }\n }\n\n // Update excluded paths for THIS locale only\n if (!isExcluded) {\n // Add path if not already excluded\n if (!currentExcludedPaths.includes(fieldPath)) {\n currentExcludedPaths.push(fieldPath)\n }\n } else {\n // Remove path from exclusions\n currentExcludedPaths = currentExcludedPaths.filter((path) => path !== fieldPath)\n }\n\n // Create the exclusion data - ALWAYS include the current locale\n const exclusionsData = {\n collection: effectiveCollectionSlug,\n documentId: id,\n excludedPaths: currentExcludedPaths.map((path) => ({ path })),\n locale: currentLocale, // Ensure this is the CURRENT locale\n }\n\n console.log('[TranslationControl] Saving exclusions:', exclusionsData)\n\n // Update or create record using Payload's REST API\n if (existingId) {\n const updateResponse = await fetch(`/api/translation-exclusions/${existingId}`, {\n body: JSON.stringify(exclusionsData),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'PATCH',\n })\n\n if (updateResponse.ok) {\n const result = await updateResponse.json()\n console.log('[TranslationControl] Updated exclusions:', result.doc)\n }\n } else {\n const createResponse = await fetch('/api/translation-exclusions', {\n body: JSON.stringify(exclusionsData),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n\n if (createResponse.ok) {\n const result = await createResponse.json()\n console.log('[TranslationControl] Created exclusions:', result.doc)\n }\n }\n\n setIsExcluded(!isExcluded)\n } catch (error) {\n console.error('[TranslationControl] Error toggling exclusion:', error)\n } finally {\n setIsLoading(false)\n }\n }, [id, effectiveCollectionSlug, currentLocale, fieldPath, isExcluded])\n\n // Don't show on create (no id yet)\n if (!id) {\n return null\n }\n\n return (\n <div className={`translation-control ${isExcluded ? 'is-excluded' : ''}`}>\n <button\n className=\"translation-control__button\"\n disabled={isLoading}\n onClick={toggleExclusion}\n title={\n isExcluded\n ? 'This field is locked and will not be auto-translated from the default language'\n : 'Click to lock this field from auto-translation'\n }\n type=\"button\"\n >\n <span className=\"translation-control__icon\">{isExcluded ? '🔒' : '🌐'}</span>\n <span className=\"translation-control__label\">\n {isExcluded ? 'Locked' : 'Auto-translate'}\n </span>\n </button>\n {isExcluded && (\n <span className=\"translation-control__status\">\n This field will not be overwritten when the default language version is updated.\n </span>\n )}\n </div>\n )\n}\n"],"names":["useDocumentInfo","useLocale","React","useCallback","useEffect","useState","TranslationControl","collectionSlug","defaultLocale","fieldPath","clientFieldPath","path","payloadPath","id","docCollectionSlug","code","currentLocale","isExcluded","setIsExcluded","isLoading","setIsLoading","effectiveCollectionSlug","console","warn","loadExclusionState","log","collection","documentId","locale","whereQuery","and","equals","queryString","URLSearchParams","limit","where","JSON","stringify","toString","fullUrl","response","fetch","ok","data","json","docs","length","exclusion","excludedPaths","map","item","isFieldExcluded","includes","expectedLocale","expectedDocId","gotLocale","gotDocId","error","toggleExclusion","findResponse","currentExcludedPaths","existingId","push","filter","exclusionsData","updateResponse","body","headers","method","result","doc","createResponse","div","className","button","disabled","onClick","title","type","span"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/components/TranslationControl.tsx"],"sourcesContent":["'use client'\n\nimport { useConfig, useDocumentInfo, useLocale } from '@payloadcms/ui'\nimport React, { useCallback, useEffect, useState } from 'react'\n\nimport './TranslationControl.css'\n\ntype TranslationControlProps = {\n collectionSlug?: string\n defaultLocale: string\n fieldPath?: string // Optional - will be inferred from Payload's path prop if not provided\n path?: string // Payload provides this at runtime with array/block indices\n}\n\n/**\n * UI component that allows users to toggle \"do not translate\" for specific fields\n * Only shows on secondary locales (not the default locale)\n * \n * The component can receive the field path in two ways:\n * 1. From Payload's `path` prop (preferred - includes runtime array/block indices)\n * 2. From the `fieldPath` clientProp (fallback - static path from field definition)\n */\nexport const TranslationControl: React.FC<TranslationControlProps> = ({\n collectionSlug,\n defaultLocale,\n fieldPath: clientFieldPath,\n path: payloadPath,\n}) => {\n // Use Payload's runtime path if available (includes array/block indices like \"layout.0.heading\")\n // Otherwise fall back to the static path from clientProps\n const fieldPath = payloadPath || clientFieldPath\n const { id, collectionSlug: docCollectionSlug } = useDocumentInfo()\n const { code: currentLocale } = useLocale()\n const {\n config: { routes, serverURL },\n } = useConfig()\n const exclusionsURL = `${serverURL}${routes.api}/translation-exclusions`\n const [isExcluded, setIsExcluded] = useState(false)\n const [isLoading, setIsLoading] = useState(false)\n\n // Use collectionSlug from props or from document context\n const effectiveCollectionSlug = collectionSlug || docCollectionSlug\n\n // Load exclusion state on mount and when locale changes\n useEffect(() => {\n // Reset state when switching documents, on create (no ID), or on the default\n // locale where the control is hidden anyway\n if (!id || !effectiveCollectionSlug || currentLocale === defaultLocale) {\n setIsExcluded(false) // Reset to default state\n return\n }\n\n const loadExclusionState = async () => {\n try {\n console.log('[TranslationControl] Loading exclusion state for:', {\n collection: effectiveCollectionSlug,\n documentId: id,\n fieldPath,\n locale: currentLocale,\n })\n\n // Build query for this specific locale AND document ID\n const whereQuery = {\n and: [\n { collectionSlug: { equals: effectiveCollectionSlug } },\n { documentId: { equals: String(id) } }, // This ensures we only get exclusions for THIS document\n { locale: { equals: currentLocale } },\n ],\n }\n\n const queryString = new URLSearchParams({\n limit: '1',\n where: JSON.stringify(whereQuery),\n }).toString()\n\n const fullUrl = `${exclusionsURL}?${queryString}`\n console.log('[TranslationControl] Query URL:', fullUrl)\n console.log('[TranslationControl] Where clause:', whereQuery)\n\n const response = await fetch(fullUrl)\n\n if (response.ok) {\n const data = await response.json()\n\n if (data.docs && data.docs.length > 0) {\n const exclusion = data.docs[0]\n\n // CRITICAL: Verify this exclusion belongs to THIS document AND locale\n if (exclusion.locale === currentLocale && exclusion.documentId === String(id)) {\n const excludedPaths = exclusion.excludedPaths?.map((item: any) => item.path) || []\n const isFieldExcluded = excludedPaths.includes(fieldPath)\n\n console.log('[TranslationControl] Loaded exclusions for document', id, 'locale', currentLocale, ':', {\n excludedPaths,\n fieldPath,\n isFieldExcluded,\n })\n\n setIsExcluded(isFieldExcluded)\n } else {\n console.warn('[TranslationControl] Document/Locale mismatch in loaded exclusion!', {\n expectedLocale: currentLocale,\n expectedDocId: id,\n gotLocale: exclusion.locale,\n gotDocId: exclusion.documentId,\n })\n // This exclusion is for a different document - ignore it\n setIsExcluded(false)\n }\n } else {\n // No exclusions found for this document/locale - that's fine\n console.log('[TranslationControl] No exclusions found for document', id, 'locale', currentLocale)\n setIsExcluded(false)\n }\n }\n } catch (error) {\n console.error('[TranslationControl] Failed to load exclusion state:', error)\n }\n }\n\n loadExclusionState()\n }, [id, effectiveCollectionSlug, currentLocale, defaultLocale, fieldPath, exclusionsURL])\n\n const toggleExclusion = useCallback(async () => {\n if (!id || !effectiveCollectionSlug || !fieldPath) {\n return\n }\n\n setIsLoading(true)\n try {\n // Build query parameters for Payload REST API\n const whereQuery = {\n and: [\n { collectionSlug: { equals: effectiveCollectionSlug } },\n { documentId: { equals: String(id) } },\n { locale: { equals: currentLocale } },\n ],\n }\n\n // Debug: Log the query we're making\n console.log('[TranslationControl] Fetching exclusions for:', {\n collection: effectiveCollectionSlug,\n documentId: id,\n fieldPath,\n locale: currentLocale,\n })\n\n // Properly format the where clause for Payload's REST API\n const queryString = new URLSearchParams({\n limit: '1',\n where: JSON.stringify(whereQuery),\n }).toString()\n\n const fullUrl = `${exclusionsURL}?${queryString}`\n console.log('[TranslationControl] Toggle - Query URL:', fullUrl)\n console.log('[TranslationControl] Toggle - Where clause:', whereQuery)\n\n const findResponse = await fetch(fullUrl)\n\n let currentExcludedPaths: string[] = []\n let existingId: null | string = null\n\n if (findResponse.ok) {\n const data = await findResponse.json()\n console.log('[TranslationControl] Found exclusions:', data.docs)\n\n if (data.docs && data.docs.length > 0) {\n const exclusion = data.docs[0]\n\n // CRITICAL: Verify this exclusion belongs to THIS document AND locale\n if (exclusion.locale === currentLocale && exclusion.documentId === String(id)) {\n existingId = exclusion.id\n currentExcludedPaths = exclusion.excludedPaths?.map((item: any) => item.path) || []\n console.log(\n '[TranslationControl] Current excluded paths for document',\n id,\n 'locale',\n currentLocale,\n ':',\n currentExcludedPaths,\n )\n } else {\n console.warn('[TranslationControl] Found exclusion for wrong document/locale!', {\n expectedLocale: currentLocale,\n expectedDocId: id,\n gotLocale: exclusion.locale,\n gotDocId: exclusion.documentId,\n })\n // Don't use this record - it's for a different document\n existingId = null\n currentExcludedPaths = []\n }\n }\n }\n\n // Update excluded paths for THIS locale only\n if (!isExcluded) {\n // Add path if not already excluded\n if (!currentExcludedPaths.includes(fieldPath)) {\n currentExcludedPaths.push(fieldPath)\n }\n } else {\n // Remove path from exclusions\n currentExcludedPaths = currentExcludedPaths.filter((path) => path !== fieldPath)\n }\n\n // Create the exclusion data - ALWAYS include the current locale\n const exclusionsData = {\n collectionSlug: effectiveCollectionSlug,\n documentId: String(id),\n excludedPaths: currentExcludedPaths.map((path) => ({ path })),\n locale: currentLocale, // Ensure this is the CURRENT locale\n }\n\n console.log('[TranslationControl] Saving exclusions:', exclusionsData)\n\n // Update or create record using Payload's REST API\n const saveResponse = existingId\n ? await fetch(`${exclusionsURL}/${existingId}`, {\n body: JSON.stringify(exclusionsData),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'PATCH',\n })\n : await fetch(exclusionsURL, {\n body: JSON.stringify(exclusionsData),\n headers: {\n 'Content-Type': 'application/json',\n },\n method: 'POST',\n })\n\n if (!saveResponse.ok) {\n const errorBody = await saveResponse.text()\n throw new Error(`Save failed (${saveResponse.status}): ${errorBody}`)\n }\n\n const result = await saveResponse.json()\n console.log('[TranslationControl] Saved exclusions:', result.doc)\n\n // Only flip the displayed state once the save is confirmed — otherwise\n // the button would show \"Locked\" for a field that was never persisted.\n setIsExcluded(!isExcluded)\n } catch (error) {\n console.error('[TranslationControl] Error toggling exclusion:', error)\n } finally {\n setIsLoading(false)\n }\n }, [id, effectiveCollectionSlug, currentLocale, fieldPath, isExcluded, exclusionsURL])\n\n // Don't show on default locale (you can only lock fields in secondary locales),\n // without a valid field path, or on create (no id yet)\n if (currentLocale === defaultLocale || !fieldPath || !id) {\n return null\n }\n\n return (\n <div className={`translation-control ${isExcluded ? 'is-excluded' : ''}`}>\n <button\n className=\"translation-control__button\"\n disabled={isLoading}\n onClick={toggleExclusion}\n title={\n isExcluded\n ? 'This field is locked and will not be auto-translated from the default language'\n : 'Click to lock this field from auto-translation'\n }\n type=\"button\"\n >\n <span className=\"translation-control__icon\">{isExcluded ? '🔒' : '🌐'}</span>\n <span className=\"translation-control__label\">\n {isExcluded ? 'Locked' : 'Auto-translate'}\n </span>\n </button>\n {isExcluded && (\n <span className=\"translation-control__status\">\n This field will not be overwritten when the default language version is updated.\n </span>\n )}\n </div>\n )\n}\n"],"names":["useConfig","useDocumentInfo","useLocale","React","useCallback","useEffect","useState","TranslationControl","collectionSlug","defaultLocale","fieldPath","clientFieldPath","path","payloadPath","id","docCollectionSlug","code","currentLocale","config","routes","serverURL","exclusionsURL","api","isExcluded","setIsExcluded","isLoading","setIsLoading","effectiveCollectionSlug","loadExclusionState","console","log","collection","documentId","locale","whereQuery","and","equals","String","queryString","URLSearchParams","limit","where","JSON","stringify","toString","fullUrl","response","fetch","ok","data","json","docs","length","exclusion","excludedPaths","map","item","isFieldExcluded","includes","warn","expectedLocale","expectedDocId","gotLocale","gotDocId","error","toggleExclusion","findResponse","currentExcludedPaths","existingId","push","filter","exclusionsData","saveResponse","body","headers","method","errorBody","text","Error","status","result","doc","div","className","button","disabled","onClick","title","type","span"],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Endpoint } from 'payload';
|
|
2
|
+
export declare function isChatCapableModel(id: string): boolean;
|
|
3
|
+
/** GPT-5+ and o-series only accept the default temperature (1). */
|
|
4
|
+
export declare function supportsCustomTemperature(model: string): boolean;
|
|
5
|
+
export declare const listOpenAiModelsEndpoint: Endpoint;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const OPENAI_MODELS_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
2
|
+
const NON_CHAT_MODEL = /(embedding|whisper|tts|dall-e|realtime|audio|image|moderation|transcribe|search-preview|codex|sora|babbage|davinci|computer-use|deep-research|gpt-oss|omni-moderation)/i;
|
|
3
|
+
let modelsCache = null;
|
|
4
|
+
let modelsInflight = null;
|
|
5
|
+
export function isChatCapableModel(id) {
|
|
6
|
+
if (!id || NON_CHAT_MODEL.test(id)) return false;
|
|
7
|
+
return /^(gpt-|o[1-9]|chatgpt-)/i.test(id);
|
|
8
|
+
}
|
|
9
|
+
/** GPT-5+ and o-series only accept the default temperature (1). */ export function supportsCustomTemperature(model) {
|
|
10
|
+
if (!model) return true;
|
|
11
|
+
if (/^o[1-9]/i.test(model)) return false;
|
|
12
|
+
if (/^gpt-5/i.test(model)) return false;
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
async function fetchChatCapableModels(apiKey) {
|
|
16
|
+
const openaiRes = await fetch('https://api.openai.com/v1/models', {
|
|
17
|
+
headers: {
|
|
18
|
+
Authorization: `Bearer ${apiKey}`
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
if (!openaiRes.ok) {
|
|
22
|
+
const errText = await openaiRes.text();
|
|
23
|
+
throw new Error(`OpenAI API ${openaiRes.status}: ${errText.slice(0, 200)}`);
|
|
24
|
+
}
|
|
25
|
+
const body = await openaiRes.json();
|
|
26
|
+
return (body.data ?? []).filter((model)=>typeof model?.id === 'string' && isChatCapableModel(model.id)).sort((a, b)=>(b.created ?? 0) - (a.created ?? 0) || a.id.localeCompare(b.id)).map((model)=>({
|
|
27
|
+
label: model.id,
|
|
28
|
+
value: model.id
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
async function getCachedChatCapableModels(apiKey) {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (modelsCache && modelsCache.expiresAt > now) {
|
|
34
|
+
return modelsCache.models;
|
|
35
|
+
}
|
|
36
|
+
if (modelsInflight) return modelsInflight;
|
|
37
|
+
modelsInflight = fetchChatCapableModels(apiKey).then((models)=>{
|
|
38
|
+
modelsCache = {
|
|
39
|
+
models,
|
|
40
|
+
expiresAt: Date.now() + OPENAI_MODELS_CACHE_TTL_MS
|
|
41
|
+
};
|
|
42
|
+
return models;
|
|
43
|
+
}).finally(()=>{
|
|
44
|
+
modelsInflight = null;
|
|
45
|
+
});
|
|
46
|
+
return modelsInflight;
|
|
47
|
+
}
|
|
48
|
+
export const listOpenAiModelsEndpoint = {
|
|
49
|
+
path: '/openai-models',
|
|
50
|
+
method: 'get',
|
|
51
|
+
handler: async (req)=>{
|
|
52
|
+
if (!req.user) {
|
|
53
|
+
return Response.json({
|
|
54
|
+
error: 'Unauthorized'
|
|
55
|
+
}, {
|
|
56
|
+
status: 401
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
60
|
+
if (!apiKey) {
|
|
61
|
+
return Response.json({
|
|
62
|
+
error: 'OPENAI_API_KEY is not set'
|
|
63
|
+
}, {
|
|
64
|
+
status: 500
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const models = await getCachedChatCapableModels(apiKey);
|
|
69
|
+
return Response.json({
|
|
70
|
+
models
|
|
71
|
+
});
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
74
|
+
if (message.startsWith('OpenAI API ')) {
|
|
75
|
+
return Response.json({
|
|
76
|
+
error: message
|
|
77
|
+
}, {
|
|
78
|
+
status: 502
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
req.payload.logger.error({
|
|
82
|
+
err
|
|
83
|
+
}, '[auto-translate] Failed to list OpenAI models');
|
|
84
|
+
return Response.json({
|
|
85
|
+
error: message
|
|
86
|
+
}, {
|
|
87
|
+
status: 500
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=listOpenAiModels.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/endpoints/listOpenAiModels.ts"],"sourcesContent":["import type { Endpoint } from 'payload'\n\ntype OpenAIModel = {\n id: string\n created?: number\n object?: string\n owned_by?: string\n}\n\ntype ModelOption = { label: string; value: string }\n\nconst OPENAI_MODELS_CACHE_TTL_MS = 10 * 60 * 1000\n\nconst NON_CHAT_MODEL =\n /(embedding|whisper|tts|dall-e|realtime|audio|image|moderation|transcribe|search-preview|codex|sora|babbage|davinci|computer-use|deep-research|gpt-oss|omni-moderation)/i\n\nlet modelsCache: { models: ModelOption[]; expiresAt: number } | null = null\nlet modelsInflight: Promise<ModelOption[]> | null = null\n\nexport function isChatCapableModel(id: string): boolean {\n if (!id || NON_CHAT_MODEL.test(id)) return false\n return /^(gpt-|o[1-9]|chatgpt-)/i.test(id)\n}\n\n/** GPT-5+ and o-series only accept the default temperature (1). */\nexport function supportsCustomTemperature(model: string): boolean {\n if (!model) return true\n if (/^o[1-9]/i.test(model)) return false\n if (/^gpt-5/i.test(model)) return false\n return true\n}\n\nasync function fetchChatCapableModels(apiKey: string): Promise<ModelOption[]> {\n const openaiRes = await fetch('https://api.openai.com/v1/models', {\n headers: { Authorization: `Bearer ${apiKey}` },\n })\n\n if (!openaiRes.ok) {\n const errText = await openaiRes.text()\n throw new Error(`OpenAI API ${openaiRes.status}: ${errText.slice(0, 200)}`)\n }\n\n const body = (await openaiRes.json()) as { data?: OpenAIModel[] }\n return (body.data ?? [])\n .filter((model) => typeof model?.id === 'string' && isChatCapableModel(model.id))\n .sort((a, b) => (b.created ?? 0) - (a.created ?? 0) || a.id.localeCompare(b.id))\n .map((model) => ({\n label: model.id,\n value: model.id,\n }))\n}\n\nasync function getCachedChatCapableModels(apiKey: string): Promise<ModelOption[]> {\n const now = Date.now()\n if (modelsCache && modelsCache.expiresAt > now) {\n return modelsCache.models\n }\n\n if (modelsInflight) return modelsInflight\n\n modelsInflight = fetchChatCapableModels(apiKey)\n .then((models) => {\n modelsCache = { models, expiresAt: Date.now() + OPENAI_MODELS_CACHE_TTL_MS }\n return models\n })\n .finally(() => {\n modelsInflight = null\n })\n\n return modelsInflight\n}\n\nexport const listOpenAiModelsEndpoint: Endpoint = {\n path: '/openai-models',\n method: 'get',\n handler: async (req) => {\n if (!req.user) {\n return Response.json({ error: 'Unauthorized' }, { status: 401 })\n }\n\n const apiKey = process.env.OPENAI_API_KEY\n if (!apiKey) {\n return Response.json({ error: 'OPENAI_API_KEY is not set' }, { status: 500 })\n }\n\n try {\n const models = await getCachedChatCapableModels(apiKey)\n return Response.json({ models })\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err)\n if (message.startsWith('OpenAI API ')) {\n return Response.json({ error: message }, { status: 502 })\n }\n req.payload.logger.error({ err }, '[auto-translate] Failed to list OpenAI models')\n return Response.json({ error: message }, { status: 500 })\n }\n },\n}\n"],"names":["OPENAI_MODELS_CACHE_TTL_MS","NON_CHAT_MODEL","modelsCache","modelsInflight","isChatCapableModel","id","test","supportsCustomTemperature","model","fetchChatCapableModels","apiKey","openaiRes","fetch","headers","Authorization","ok","errText","text","Error","status","slice","body","json","data","filter","sort","a","b","created","localeCompare","map","label","value","getCachedChatCapableModels","now","Date","expiresAt","models","then","finally","listOpenAiModelsEndpoint","path","method","handler","req","user","Response","error","process","env","OPENAI_API_KEY","err","message","String","startsWith","payload","logger"],"mappings":"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"}
|
package/dist/exports/client.d.ts
CHANGED
package/dist/exports/client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { LockTranslation } from '../components/LockTranslation/index.js'\nexport { TranslationControl } from '../components/TranslationControl.js'\nexport type * from '../types/index.js'\n"],"names":["LockTranslation","TranslationControl"],"mappings":"AAAA,SAASA,eAAe,QAAQ,yCAAwC;AACxE,SAASC,kBAAkB,QAAQ,sCAAqC"}
|
|
1
|
+
{"version":3,"sources":["../../src/exports/client.ts"],"sourcesContent":["export { LockTranslation } from '../components/LockTranslation/index.js'\nexport { OpenAiModelField } from '../components/OpenAiModelField.js'\nexport { TranslationControl } from '../components/TranslationControl.js'\nexport type * from '../types/index.js'\n"],"names":["LockTranslation","OpenAiModelField","TranslationControl"],"mappings":"AAAA,SAASA,eAAe,QAAQ,yCAAwC;AACxE,SAASC,gBAAgB,QAAQ,oCAAmC;AACpE,SAASC,kBAAkB,QAAQ,sCAAqC"}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
+
import { listOpenAiModelsEndpoint, supportsCustomTemperature } from '../endpoints/listOpenAiModels.js';
|
|
1
2
|
export const getTranslationSettingsGlobal = (slug = 'translation-settings')=>({
|
|
2
3
|
slug,
|
|
3
4
|
admin: {
|
|
4
5
|
description: 'Configure translation settings including the system prompt and model parameters',
|
|
5
6
|
group: 'Auto-Translate Settings'
|
|
6
7
|
},
|
|
8
|
+
endpoints: [
|
|
9
|
+
listOpenAiModelsEndpoint
|
|
10
|
+
],
|
|
7
11
|
fields: [
|
|
8
12
|
{
|
|
9
13
|
name: 'settingsLock',
|
|
@@ -28,8 +32,8 @@ export const getTranslationSettingsGlobal = (slug = 'translation-settings')=>({
|
|
|
28
32
|
type: 'textarea',
|
|
29
33
|
access: {
|
|
30
34
|
read: ()=>true,
|
|
31
|
-
update: ({
|
|
32
|
-
return !
|
|
35
|
+
update: ({ doc })=>{
|
|
36
|
+
return !doc?.lockTranslationSettings;
|
|
33
37
|
}
|
|
34
38
|
},
|
|
35
39
|
admin: {
|
|
@@ -45,8 +49,8 @@ export const getTranslationSettingsGlobal = (slug = 'translation-settings')=>({
|
|
|
45
49
|
type: 'textarea',
|
|
46
50
|
access: {
|
|
47
51
|
read: ()=>true,
|
|
48
|
-
update: ({
|
|
49
|
-
return !
|
|
52
|
+
update: ({ doc })=>{
|
|
53
|
+
return !doc?.lockTranslationSettings;
|
|
50
54
|
}
|
|
51
55
|
},
|
|
52
56
|
admin: {
|
|
@@ -68,12 +72,15 @@ export const getTranslationSettingsGlobal = (slug = 'translation-settings')=>({
|
|
|
68
72
|
type: 'text',
|
|
69
73
|
access: {
|
|
70
74
|
read: ()=>true,
|
|
71
|
-
update: ({
|
|
72
|
-
return !
|
|
75
|
+
update: ({ doc })=>{
|
|
76
|
+
return !doc?.lockTranslationSettings;
|
|
73
77
|
}
|
|
74
78
|
},
|
|
75
79
|
admin: {
|
|
76
|
-
|
|
80
|
+
components: {
|
|
81
|
+
Field: '@pigment/auto-translate/client#OpenAiModelField'
|
|
82
|
+
},
|
|
83
|
+
description: 'The OpenAI model to use for translations'
|
|
77
84
|
},
|
|
78
85
|
defaultValue: 'gpt-4o',
|
|
79
86
|
label: 'Model',
|
|
@@ -84,13 +91,17 @@ export const getTranslationSettingsGlobal = (slug = 'translation-settings')=>({
|
|
|
84
91
|
type: 'number',
|
|
85
92
|
access: {
|
|
86
93
|
read: ()=>true,
|
|
87
|
-
update: ({
|
|
88
|
-
return !
|
|
94
|
+
update: ({ doc })=>{
|
|
95
|
+
return !doc?.lockTranslationSettings;
|
|
89
96
|
}
|
|
90
97
|
},
|
|
91
98
|
admin: {
|
|
92
|
-
description: 'Controls randomness in translation (0.0-2.0). Lower values are more deterministic.',
|
|
93
|
-
step: 0.1
|
|
99
|
+
description: 'Controls randomness in translation (0.0-2.0). Lower values are more deterministic. Not applied for GPT-5+ or o-series models.',
|
|
100
|
+
step: 0.1,
|
|
101
|
+
condition: (_data, siblingData)=>{
|
|
102
|
+
const model = typeof siblingData?.model === 'string' ? siblingData.model : '';
|
|
103
|
+
return supportsCustomTemperature(model);
|
|
104
|
+
}
|
|
94
105
|
},
|
|
95
106
|
defaultValue: 0.3,
|
|
96
107
|
label: 'Temperature',
|
|
@@ -103,8 +114,8 @@ export const getTranslationSettingsGlobal = (slug = 'translation-settings')=>({
|
|
|
103
114
|
type: 'number',
|
|
104
115
|
access: {
|
|
105
116
|
read: ()=>true,
|
|
106
|
-
update: ({
|
|
107
|
-
return !
|
|
117
|
+
update: ({ doc })=>{
|
|
118
|
+
return !doc?.lockTranslationSettings;
|
|
108
119
|
}
|
|
109
120
|
},
|
|
110
121
|
admin: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/globals/translationSettings.ts"],"sourcesContent":["import type { GlobalConfig } from 'payload'\n\nexport const getTranslationSettingsGlobal = (\n slug: string = 'translation-settings',\n): GlobalConfig => ({\n slug,\n admin: {\n description: 'Configure translation settings including the system prompt and model parameters',\n group: 'Auto-Translate Settings',\n },\n fields: [\n {\n name: 'settingsLock',\n type: 'ui',\n admin: {\n components: {\n Field: '@pigment/auto-translate/client#LockTranslation',\n },\n position: 'sidebar',\n },\n },\n {\n name: 'lockTranslationSettings',\n type: 'checkbox',\n admin: {\n hidden: true,\n },\n defaultValue: true,\n },\n {\n name: 'systemPrompt',\n type: 'textarea',\n access: {\n read: () => true,\n update: ({
|
|
1
|
+
{"version":3,"sources":["../../src/globals/translationSettings.ts"],"sourcesContent":["import type { GlobalConfig } from 'payload'\n\nimport { listOpenAiModelsEndpoint, supportsCustomTemperature } from '../endpoints/listOpenAiModels.js'\n\nexport const getTranslationSettingsGlobal = (\n slug: string = 'translation-settings',\n): GlobalConfig => ({\n slug,\n admin: {\n description: 'Configure translation settings including the system prompt and model parameters',\n group: 'Auto-Translate Settings',\n },\n endpoints: [listOpenAiModelsEndpoint],\n fields: [\n {\n name: 'settingsLock',\n type: 'ui',\n admin: {\n components: {\n Field: '@pigment/auto-translate/client#LockTranslation',\n },\n position: 'sidebar',\n },\n },\n {\n name: 'lockTranslationSettings',\n type: 'checkbox',\n admin: {\n hidden: true,\n },\n defaultValue: true,\n },\n {\n name: 'systemPrompt',\n type: 'textarea',\n access: {\n read: () => true,\n update: ({ doc }) => {\n return !doc?.lockTranslationSettings\n },\n },\n admin: {\n description:\n 'The main instruction for the AI translator. Use {fromLocale} and {toLocale} as placeholders.',\n rows: 3,\n },\n defaultValue: `You are a professional translator. Translate the JSON object values from {fromLocale} to {toLocale}.`,\n label: 'System Prompt',\n required: true,\n },\n {\n name: 'translationRules',\n type: 'textarea',\n access: {\n read: () => true,\n update: ({ doc }) => {\n return !doc?.lockTranslationSettings\n },\n },\n admin: {\n description:\n \"⚠️ Do not edit if you don't know what you are doing. These rules ensure proper JSON translation behavior.\",\n rows: 8,\n },\n defaultValue: `Rules:\n- Only translate the values, never the keys\n- Preserve the exact JSON structure\n- Do not translate field names like 'id', 'createdAt', 'updatedAt', etc.\n- Maintain formatting, HTML tags, and special characters\n- Return only valid JSON without any markdown formatting or code blocks\n- If a value is already in the target language or is a proper noun, keep it as is`,\n label: 'Translation Rules',\n required: true,\n },\n {\n name: 'model',\n type: 'text',\n access: {\n read: () => true,\n update: ({ doc }) => {\n return !doc?.lockTranslationSettings\n },\n },\n admin: {\n components: {\n Field: '@pigment/auto-translate/client#OpenAiModelField',\n },\n description: 'The OpenAI model to use for translations',\n },\n defaultValue: 'gpt-4o',\n label: 'Model',\n required: true,\n },\n {\n name: 'temperature',\n type: 'number',\n access: {\n read: () => true,\n update: ({ doc }) => {\n return !doc?.lockTranslationSettings\n },\n },\n admin: {\n description:\n 'Controls randomness in translation (0.0-2.0). Lower values are more deterministic. Not applied for GPT-5+ or o-series models.',\n step: 0.1,\n condition: (_data, siblingData) => {\n const model = typeof siblingData?.model === 'string' ? siblingData.model : ''\n return supportsCustomTemperature(model)\n },\n },\n defaultValue: 0.3,\n label: 'Temperature',\n max: 2,\n min: 0,\n required: true,\n },\n {\n name: 'maxTokens',\n type: 'number',\n access: {\n read: () => true,\n update: ({ doc }) => {\n return !doc?.lockTranslationSettings\n },\n },\n admin: {\n description: 'Maximum tokens for the response. Leave empty for automatic.',\n },\n label: 'Max Tokens',\n min: 1,\n },\n ],\n hooks: {\n afterChange: [\n async ({ data, req }) => {\n if (!data?.lockTranslationSettings) {\n const result = await req.payload.updateGlobal({\n slug: 'translation-settings',\n data: { lockTranslationSettings: true },\n req,\n })\n\n return result\n }\n },\n ],\n },\n label: 'Translation Settings',\n})\n"],"names":["listOpenAiModelsEndpoint","supportsCustomTemperature","getTranslationSettingsGlobal","slug","admin","description","group","endpoints","fields","name","type","components","Field","position","hidden","defaultValue","access","read","update","doc","lockTranslationSettings","rows","label","required","step","condition","_data","siblingData","model","max","min","hooks","afterChange","data","req","result","payload","updateGlobal"],"mappings":"AAEA,SAASA,wBAAwB,EAAEC,yBAAyB,QAAQ,mCAAkC;AAEtG,OAAO,MAAMC,+BAA+B,CAC1CC,OAAe,sBAAsB,GACnB,CAAA;QAClBA;QACAC,OAAO;YACLC,aAAa;YACbC,OAAO;QACT;QACAC,WAAW;YAACP;SAAyB;QACrCQ,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNN,OAAO;oBACLO,YAAY;wBACVC,OAAO;oBACT;oBACAC,UAAU;gBACZ;YACF;YACA;gBACEJ,MAAM;gBACNC,MAAM;gBACNN,OAAO;oBACLU,QAAQ;gBACV;gBACAC,cAAc;YAChB;YACA;gBACEN,MAAM;gBACNC,MAAM;gBACNM,QAAQ;oBACNC,MAAM,IAAM;oBACZC,QAAQ,CAAC,EAAEC,GAAG,EAAE;wBACd,OAAO,CAACA,KAAKC;oBACf;gBACF;gBACAhB,OAAO;oBACLC,aACE;oBACFgB,MAAM;gBACR;gBACAN,cAAc,CAAC,oGAAoG,CAAC;gBACpHO,OAAO;gBACPC,UAAU;YACZ;YACA;gBACEd,MAAM;gBACNC,MAAM;gBACNM,QAAQ;oBACNC,MAAM,IAAM;oBACZC,QAAQ,CAAC,EAAEC,GAAG,EAAE;wBACd,OAAO,CAACA,KAAKC;oBACf;gBACF;gBACAhB,OAAO;oBACLC,aACE;oBACFgB,MAAM;gBACR;gBACAN,cAAc,CAAC;;;;;;iFAM4D,CAAC;gBAC5EO,OAAO;gBACPC,UAAU;YACZ;YACA;gBACEd,MAAM;gBACNC,MAAM;gBACNM,QAAQ;oBACNC,MAAM,IAAM;oBACZC,QAAQ,CAAC,EAAEC,GAAG,EAAE;wBACd,OAAO,CAACA,KAAKC;oBACf;gBACF;gBACAhB,OAAO;oBACLO,YAAY;wBACVC,OAAO;oBACT;oBACAP,aAAa;gBACf;gBACAU,cAAc;gBACdO,OAAO;gBACPC,UAAU;YACZ;YACA;gBACEd,MAAM;gBACNC,MAAM;gBACNM,QAAQ;oBACNC,MAAM,IAAM;oBACZC,QAAQ,CAAC,EAAEC,GAAG,EAAE;wBACd,OAAO,CAACA,KAAKC;oBACf;gBACF;gBACAhB,OAAO;oBACLC,aACE;oBACFmB,MAAM;oBACNC,WAAW,CAACC,OAAOC;wBACjB,MAAMC,QAAQ,OAAOD,aAAaC,UAAU,WAAWD,YAAYC,KAAK,GAAG;wBAC3E,OAAO3B,0BAA0B2B;oBACnC;gBACF;gBACAb,cAAc;gBACdO,OAAO;gBACPO,KAAK;gBACLC,KAAK;gBACLP,UAAU;YACZ;YACA;gBACEd,MAAM;gBACNC,MAAM;gBACNM,QAAQ;oBACNC,MAAM,IAAM;oBACZC,QAAQ,CAAC,EAAEC,GAAG,EAAE;wBACd,OAAO,CAACA,KAAKC;oBACf;gBACF;gBACAhB,OAAO;oBACLC,aAAa;gBACf;gBACAiB,OAAO;gBACPQ,KAAK;YACP;SACD;QACDC,OAAO;YACLC,aAAa;gBACX,OAAO,EAAEC,IAAI,EAAEC,GAAG,EAAE;oBAClB,IAAI,CAACD,MAAMb,yBAAyB;wBAClC,MAAMe,SAAS,MAAMD,IAAIE,OAAO,CAACC,YAAY,CAAC;4BAC5ClC,MAAM;4BACN8B,MAAM;gCAAEb,yBAAyB;4BAAK;4BACtCc;wBACF;wBAEA,OAAOC;oBACT;gBACF;aACD;QACH;QACAb,OAAO;IACT,CAAA,EAAE"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import OpenAI from 'openai';
|
|
2
|
+
import { supportsCustomTemperature } from '../endpoints/listOpenAiModels.js';
|
|
2
3
|
import { filterExcludedPaths, overlayNonTranslatableValues } from '../utilities/fieldHelpers.js';
|
|
3
4
|
export class TranslationService {
|
|
4
5
|
client;
|
|
@@ -340,12 +341,13 @@ export class TranslationService {
|
|
|
340
341
|
response_format: {
|
|
341
342
|
type: 'json_object'
|
|
342
343
|
},
|
|
343
|
-
|
|
344
|
+
...supportsCustomTemperature(settings.model) ? {
|
|
345
|
+
temperature: settings.temperature
|
|
346
|
+
} : {},
|
|
347
|
+
...settings.maxTokens ? {
|
|
348
|
+
max_tokens: settings.maxTokens
|
|
349
|
+
} : {}
|
|
344
350
|
};
|
|
345
|
-
// Add maxTokens if specified
|
|
346
|
-
if (settings.maxTokens) {
|
|
347
|
-
requestParams.max_tokens = settings.maxTokens;
|
|
348
|
-
}
|
|
349
351
|
const response = await client.chat.completions.create(requestParams, {
|
|
350
352
|
timeout
|
|
351
353
|
});
|
|
@@ -406,10 +408,8 @@ export class TranslationService {
|
|
|
406
408
|
}
|
|
407
409
|
return [];
|
|
408
410
|
} catch (error) {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
}
|
|
412
|
-
return [];
|
|
411
|
+
payload.logger.error(`[Auto-Translate] Error fetching exclusions: ${error}`);
|
|
412
|
+
throw error;
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
415
|
/**
|
|
@@ -529,12 +529,13 @@ export class TranslationService {
|
|
|
529
529
|
response_format: {
|
|
530
530
|
type: 'json_object'
|
|
531
531
|
},
|
|
532
|
-
|
|
532
|
+
...supportsCustomTemperature(settings.model) ? {
|
|
533
|
+
temperature: settings.temperature
|
|
534
|
+
} : {},
|
|
535
|
+
...settings.maxTokens ? {
|
|
536
|
+
max_tokens: settings.maxTokens
|
|
537
|
+
} : {}
|
|
533
538
|
};
|
|
534
|
-
// Add maxTokens if specified
|
|
535
|
-
if (settings.maxTokens) {
|
|
536
|
-
requestParams.max_tokens = settings.maxTokens;
|
|
537
|
-
}
|
|
538
539
|
const response = await client.chat.completions.create(requestParams, {
|
|
539
540
|
timeout
|
|
540
541
|
});
|
|
@@ -635,9 +636,8 @@ export class TranslationService {
|
|
|
635
636
|
payload.logger.info(`[Auto-Translate] Updated exclusions for ${collection}:${documentId}:${locale}`);
|
|
636
637
|
}
|
|
637
638
|
} catch (error) {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
639
|
+
payload.logger.error(`[Auto-Translate] Error updating exclusions: ${error}`);
|
|
640
|
+
throw error;
|
|
641
641
|
}
|
|
642
642
|
}
|
|
643
643
|
}
|