@pigment/auto-translate 1.3.4 → 1.5.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 +48 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +237 -44
- package/dist/index.js.map +1 -1
- package/dist/services/translationService.d.ts +6 -0
- package/dist/services/translationService.js +37 -5
- package/dist/services/translationService.js.map +1 -1
- package/dist/types/index.d.ts +33 -0
- package/dist/types/index.js.map +1 -1
- package/dist/utilities/fieldHelpers.d.ts +26 -0
- package/dist/utilities/fieldHelpers.js +119 -0
- package/dist/utilities/fieldHelpers.js.map +1 -1
- package/package.json +29 -22
package/README.md
CHANGED
|
@@ -401,6 +401,54 @@ For more detailed information, check out these guides:
|
|
|
401
401
|
|
|
402
402
|
---
|
|
403
403
|
|
|
404
|
+
## 🌐 Edge Middleware (locale detection)
|
|
405
|
+
|
|
406
|
+
This plugin's translation logic runs inside Payload `afterOperation` hooks on the **Node.js server** and is fully edge-compatible on the _plugin side_ — no changes are needed in the plugin itself.
|
|
407
|
+
|
|
408
|
+
If your **Next.js app** needs locale-based routing (detect locale from `Accept-Language` and redirect), you can add middleware.
|
|
409
|
+
|
|
410
|
+
### Next.js 16 — middleware vs proxy
|
|
411
|
+
|
|
412
|
+
| Convention | Runtime | Next.js version |
|
|
413
|
+
|---|---|---|
|
|
414
|
+
| `middleware.ts` (legacy name, still supported) | **Edge** (default) | 15 + 16 |
|
|
415
|
+
| `proxy.ts` (new name in v16) | **Node.js only** | 16 only |
|
|
416
|
+
|
|
417
|
+
In Next.js 16 the `proxy` convention (Node.js runtime) is recommended, but **Edge runtime requires keeping the `middleware.ts` filename**. A ready-to-use example for both approaches is provided in `dev/middleware.ts`.
|
|
418
|
+
|
|
419
|
+
### Quick example (Edge, locale detection)
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
// middleware.ts (at the root of your Next.js project)
|
|
423
|
+
import type { NextRequest } from 'next/server'
|
|
424
|
+
import { NextResponse } from 'next/server'
|
|
425
|
+
|
|
426
|
+
const LOCALES = ['sv', 'en', 'de']
|
|
427
|
+
const DEFAULT_LOCALE = 'sv'
|
|
428
|
+
|
|
429
|
+
export function middleware(request: NextRequest) {
|
|
430
|
+
const { pathname } = request.nextUrl
|
|
431
|
+
const hasLocale = LOCALES.some(
|
|
432
|
+
(l) => pathname.startsWith(`/${l}/`) || pathname === `/${l}`,
|
|
433
|
+
)
|
|
434
|
+
if (hasLocale) return NextResponse.next()
|
|
435
|
+
|
|
436
|
+
const accept = request.headers.get('accept-language') ?? ''
|
|
437
|
+
const locale = LOCALES.find((l) => accept.toLowerCase().includes(l)) ?? DEFAULT_LOCALE
|
|
438
|
+
const url = request.nextUrl.clone()
|
|
439
|
+
url.pathname = `/${locale}${pathname}`
|
|
440
|
+
return NextResponse.redirect(url)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export const config = {
|
|
444
|
+
matcher: ['/((?!payload|api|_next|favicon\\.ico).*)'],
|
|
445
|
+
// NOTE: do NOT add runtime: 'edge' here — it's for Route Segments, not middleware.
|
|
446
|
+
// Edge is the implicit default for middleware.ts in Next.js 16.
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
---
|
|
451
|
+
|
|
404
452
|
## 🤝 Contributing
|
|
405
453
|
|
|
406
454
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
package/dist/index.d.ts
CHANGED
|
@@ -4,4 +4,4 @@ export { getTranslationExclusionsCollection } from './collections/translationExc
|
|
|
4
4
|
export { getTranslationSettingsGlobal } from './globals/translationSettings.js';
|
|
5
5
|
export { TranslationService } from './services/translationService.js';
|
|
6
6
|
export * from './types/index.js';
|
|
7
|
-
export declare const autoTranslate: (pluginOptions: AutoTranslateConfig) => (
|
|
7
|
+
export declare const autoTranslate: (pluginOptions: AutoTranslateConfig) => (incomingConfig: Config) => Config;
|
package/dist/index.js
CHANGED
|
@@ -6,7 +6,63 @@ export { getTranslationExclusionsCollection } from './collections/translationExc
|
|
|
6
6
|
export { getTranslationSettingsGlobal } from './globals/translationSettings.js';
|
|
7
7
|
export { TranslationService } from './services/translationService.js';
|
|
8
8
|
export * from './types/index.js';
|
|
9
|
-
|
|
9
|
+
// Fields that must never be passed as data to payload.update / payload.create
|
|
10
|
+
// (Postgres/drizzle rejects them; MongoDB silently ignores them)
|
|
11
|
+
const SYSTEM_FIELDS = new Set([
|
|
12
|
+
'id',
|
|
13
|
+
'createdAt',
|
|
14
|
+
'updatedAt',
|
|
15
|
+
'_status',
|
|
16
|
+
'__v',
|
|
17
|
+
'globalType',
|
|
18
|
+
'updatedBy'
|
|
19
|
+
]);
|
|
20
|
+
function stripSystemFields(data) {
|
|
21
|
+
const result = {};
|
|
22
|
+
for (const [key, value] of Object.entries(data)){
|
|
23
|
+
if (!SYSTEM_FIELDS.has(key)) {
|
|
24
|
+
result[key] = value;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Strips `id` from objects that are direct elements of arrays, recursively
|
|
31
|
+
* through the data tree. This prevents Postgres unique-constraint violations
|
|
32
|
+
* when inserting locale-specific rows into array tables (e.g. posts_content)
|
|
33
|
+
* that share a single PRIMARY KEY on `id` across all locales.
|
|
34
|
+
*
|
|
35
|
+
* Relationship objects (plain objects that are NOT direct array items) keep
|
|
36
|
+
* their `id` so that Payload can still resolve them correctly.
|
|
37
|
+
*/ function stripArrayItemIds(data) {
|
|
38
|
+
if (Array.isArray(data)) {
|
|
39
|
+
return data.map((item)=>{
|
|
40
|
+
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
41
|
+
// Direct array item — strip its Payload-internal `id`
|
|
42
|
+
const { id: _id, ...rest } = item;
|
|
43
|
+
const processed = {};
|
|
44
|
+
for (const [key, value] of Object.entries(rest)){
|
|
45
|
+
processed[key] = stripArrayItemIds(value);
|
|
46
|
+
}
|
|
47
|
+
return processed;
|
|
48
|
+
}
|
|
49
|
+
return stripArrayItemIds(item);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (data && typeof data === 'object') {
|
|
53
|
+
const result = {};
|
|
54
|
+
for (const [key, value] of Object.entries(data)){
|
|
55
|
+
result[key] = stripArrayItemIds(value);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
return data;
|
|
60
|
+
}
|
|
61
|
+
export const autoTranslate = (pluginOptions)=>(incomingConfig)=>{
|
|
62
|
+
// Create a shallow copy so we never mutate the caller's config object
|
|
63
|
+
const config = {
|
|
64
|
+
...incomingConfig
|
|
65
|
+
};
|
|
10
66
|
// If the plugin is disabled, return config immediately without any modifications
|
|
11
67
|
if (pluginOptions.disabled) {
|
|
12
68
|
if (pluginOptions.debugging) {
|
|
@@ -14,10 +70,6 @@ export const autoTranslate = (pluginOptions)=>(config)=>{
|
|
|
14
70
|
}
|
|
15
71
|
return config;
|
|
16
72
|
}
|
|
17
|
-
// Validate configuration
|
|
18
|
-
if (!config.collections) {
|
|
19
|
-
config.collections = [];
|
|
20
|
-
}
|
|
21
73
|
if (!config.localization) {
|
|
22
74
|
console.warn('[Auto-Translate Plugin] No localization config found. Plugin will not function properly.');
|
|
23
75
|
return config;
|
|
@@ -35,21 +87,32 @@ export const autoTranslate = (pluginOptions)=>(config)=>{
|
|
|
35
87
|
console.log('- Exclusions enabled:', enableExclusions);
|
|
36
88
|
}
|
|
37
89
|
// Add translation exclusions collection (only if exclusions are enabled)
|
|
90
|
+
// Use spread to avoid mutating the original array
|
|
38
91
|
if (enableExclusions) {
|
|
39
92
|
const exclusionsSlug = pluginOptions.translationExclusionsSlug || 'translation-exclusions';
|
|
40
|
-
config.collections
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
93
|
+
config.collections = [
|
|
94
|
+
...config.collections || [],
|
|
95
|
+
getTranslationExclusionsCollection(exclusionsSlug)
|
|
96
|
+
];
|
|
97
|
+
} else {
|
|
98
|
+
config.collections = [
|
|
99
|
+
...config.collections || []
|
|
100
|
+
];
|
|
45
101
|
}
|
|
102
|
+
// Add translation settings global using spread
|
|
46
103
|
const settingsSlug = pluginOptions.translationSettingsSlug || 'translation-settings';
|
|
47
|
-
config.globals
|
|
104
|
+
config.globals = [
|
|
105
|
+
...config.globals || [],
|
|
106
|
+
getTranslationSettingsGlobal(settingsSlug)
|
|
107
|
+
];
|
|
48
108
|
// Initialize translation service
|
|
49
109
|
const translationService = new TranslationService(pluginOptions);
|
|
50
110
|
// Configure collections with auto-translate
|
|
51
111
|
if (pluginOptions.collections) {
|
|
52
|
-
for(const
|
|
112
|
+
for(const rawSlug in pluginOptions.collections){
|
|
113
|
+
// Payload 3.85+ requires CollectionSlug (strict union), but for...in
|
|
114
|
+
// yields string. Cast once here and use collectionSlug throughout.
|
|
115
|
+
const collectionSlug = rawSlug;
|
|
53
116
|
const collectionConfig = pluginOptions.collections[collectionSlug];
|
|
54
117
|
// Skip if disabled
|
|
55
118
|
if (collectionConfig === false || typeof collectionConfig === 'object' && collectionConfig.enabled === false) {
|
|
@@ -61,16 +124,19 @@ export const autoTranslate = (pluginOptions)=>(config)=>{
|
|
|
61
124
|
continue;
|
|
62
125
|
}
|
|
63
126
|
// Add translationSync field to collection
|
|
64
|
-
collection.fields
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
127
|
+
collection.fields = [
|
|
128
|
+
...collection.fields,
|
|
129
|
+
{
|
|
130
|
+
name: 'translationSync',
|
|
131
|
+
type: 'checkbox',
|
|
132
|
+
admin: {
|
|
133
|
+
description: 'When enabled, changes in the default language will automatically translate to other languages',
|
|
134
|
+
position: 'sidebar'
|
|
135
|
+
},
|
|
136
|
+
defaultValue: pluginOptions.enableTranslationSyncByDefault ?? true,
|
|
137
|
+
label: 'Enable Auto-Translation'
|
|
138
|
+
}
|
|
139
|
+
];
|
|
74
140
|
// Auto-inject TranslationControl component into all localized fields
|
|
75
141
|
// Only inject if exclusions are enabled (otherwise there's nothing to control)
|
|
76
142
|
if (enableExclusions && pluginOptions.autoInjectUI !== false) {
|
|
@@ -86,16 +152,64 @@ export const autoTranslate = (pluginOptions)=>(config)=>{
|
|
|
86
152
|
if (!collection.hooks.afterOperation) {
|
|
87
153
|
collection.hooks.afterOperation = [];
|
|
88
154
|
}
|
|
155
|
+
// ---------------------------------------------------------------
|
|
156
|
+
// Nested-docs compatibility
|
|
157
|
+
// ---------------------------------------------------------------
|
|
158
|
+
// Resolve nested-docs field slugs once, shared by both the
|
|
159
|
+
// beforeChange guard and the afterOperation translation hook below.
|
|
160
|
+
const nestedDocsFieldSlugs = resolveNestedDocsFieldSlugs(pluginOptions);
|
|
161
|
+
// Determine whether this collection actually has a breadcrumbs array
|
|
162
|
+
// field (added by nestedDocsPlugin or manually).
|
|
163
|
+
const hasBreadcrumbsField = nestedDocsFieldSlugs !== null && collection.fields.some((f)=>'name' in f && f.name === nestedDocsFieldSlugs.breadcrumbsSlug && f.type === 'array');
|
|
164
|
+
if (hasBreadcrumbsField && nestedDocsFieldSlugs) {
|
|
165
|
+
const { breadcrumbsSlug } = nestedDocsFieldSlugs;
|
|
166
|
+
// Guard: strip `id` from breadcrumb array items on non-default-locale writes.
|
|
167
|
+
//
|
|
168
|
+
// Root cause: nested-docs' `resaveChildren` afterChange hook re-saves each
|
|
169
|
+
// child document when a parent is updated. For locales where the child has no
|
|
170
|
+
// row yet, `payload.find(child, locale)` falls back to the default locale,
|
|
171
|
+
// returning breadcrumbs that carry the default-locale array-item `id`s.
|
|
172
|
+
// `formatBreadcrumb` preserves those ids via `{ ...breadcrumb, doc, label, url }`.
|
|
173
|
+
// When Payload then writes the child in the secondary locale, Drizzle attempts
|
|
174
|
+
// an INSERT with the same `id` — colliding on the `breadcrumbs.id` PRIMARY KEY
|
|
175
|
+
// (shared across locales) and producing `ValidationError: Value must be unique: id`.
|
|
176
|
+
//
|
|
177
|
+
// Fix: remove `id` from every breadcrumb item in incoming data for any
|
|
178
|
+
// non-default-locale write. Payload will assign fresh per-locale ids on INSERT.
|
|
179
|
+
// This hook fires AFTER nested-docs' `populateBreadcrumbsBeforeChange` (because
|
|
180
|
+
// autoTranslate is registered later), so breadcrumbs are already fully populated
|
|
181
|
+
// before we strip the stale ids.
|
|
182
|
+
if (!collection.hooks.beforeChange) {
|
|
183
|
+
collection.hooks.beforeChange = [];
|
|
184
|
+
}
|
|
185
|
+
collection.hooks.beforeChange.push(async ({ data, req })=>{
|
|
186
|
+
if (!req.locale || req.locale === defaultLocale) return data;
|
|
187
|
+
if (!data[breadcrumbsSlug] || !Array.isArray(data[breadcrumbsSlug])) return data;
|
|
188
|
+
return {
|
|
189
|
+
...data,
|
|
190
|
+
[breadcrumbsSlug]: data[breadcrumbsSlug].map((item)=>{
|
|
191
|
+
if (item && typeof item === 'object') {
|
|
192
|
+
const { id: _id, ...rest } = item;
|
|
193
|
+
return rest;
|
|
194
|
+
}
|
|
195
|
+
return item;
|
|
196
|
+
})
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
if (pluginOptions.debugging) {
|
|
200
|
+
console.log(`[Auto-Translate Plugin] Nested-docs beforeChange guard added for: ${collectionSlug}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
89
203
|
// Main translation hook
|
|
90
|
-
|
|
91
|
-
// Only process create and
|
|
204
|
+
const translationHook = async ({ operation, req, result })=>{
|
|
205
|
+
// Only process create and updateByID operations
|
|
92
206
|
if (operation !== 'create' && operation !== 'updateByID') {
|
|
93
207
|
if (pluginOptions.debugging) {
|
|
94
208
|
req.payload.logger.error(`[Auto-Translate Plugin] Skipping translation - not create or update operation: ${operation}`);
|
|
95
209
|
}
|
|
96
210
|
return result;
|
|
97
211
|
}
|
|
98
|
-
// For create/update operations, result should have
|
|
212
|
+
// For create/update operations, result should have an id property
|
|
99
213
|
if (!result || typeof result !== 'object' || !('id' in result)) {
|
|
100
214
|
if (pluginOptions.debugging) {
|
|
101
215
|
req.payload.logger.error(`[Auto-Translate Plugin] No document found in result: ${JSON.stringify(result)}`);
|
|
@@ -143,9 +257,20 @@ export const autoTranslate = (pluginOptions)=>(config)=>{
|
|
|
143
257
|
}
|
|
144
258
|
// Get global/collection-level excluded fields
|
|
145
259
|
const configExcludedFields = translationService.getConfigExcludedFields(collectionSlug);
|
|
260
|
+
// Exclude nested-docs-managed fields from the AI translation payload.
|
|
261
|
+
// `parent` is locale-invariant (the same relationship across all locales)
|
|
262
|
+
// and must never be overwritten with an AI-translated value.
|
|
263
|
+
// `breadcrumbs` are computed and managed entirely by nested-docs; sending
|
|
264
|
+
// them through the AI would produce garbled data and would be overwritten
|
|
265
|
+
// by nested-docs anyway.
|
|
266
|
+
const nestedDocsExcludedFields = nestedDocsFieldSlugs ? [
|
|
267
|
+
nestedDocsFieldSlugs.parentSlug,
|
|
268
|
+
nestedDocsFieldSlugs.breadcrumbsSlug
|
|
269
|
+
] : [];
|
|
146
270
|
const allExcludedPaths = [
|
|
147
271
|
...excludedPaths,
|
|
148
|
-
...configExcludedFields
|
|
272
|
+
...configExcludedFields,
|
|
273
|
+
...nestedDocsExcludedFields
|
|
149
274
|
];
|
|
150
275
|
if (pluginOptions.debugging && allExcludedPaths.length > 0) {
|
|
151
276
|
req.payload.logger.info(`[Auto-Translate Plugin] Excluded paths for ${targetLocale}: ${allExcludedPaths.join(', ')}`);
|
|
@@ -179,7 +304,9 @@ export const autoTranslate = (pluginOptions)=>(config)=>{
|
|
|
179
304
|
toLocale: targetLocale
|
|
180
305
|
});
|
|
181
306
|
// Merge translated data with existing, preserving excluded fields
|
|
182
|
-
|
|
307
|
+
let finalData = {
|
|
308
|
+
...translatedData
|
|
309
|
+
};
|
|
183
310
|
if (existingDoc && allExcludedPaths.length > 0) {
|
|
184
311
|
// Preserve excluded fields from existing document
|
|
185
312
|
for (const excludedPath of allExcludedPaths){
|
|
@@ -189,11 +316,29 @@ export const autoTranslate = (pluginOptions)=>(config)=>{
|
|
|
189
316
|
}
|
|
190
317
|
}
|
|
191
318
|
}
|
|
319
|
+
// Strip system/internal fields before updating so Postgres adapter
|
|
320
|
+
// does not receive `id`, `createdAt`, `updatedAt`, etc. as data fields.
|
|
321
|
+
// MongoDB is lenient with extra fields; Postgres/drizzle raises
|
|
322
|
+
// ValidationError: The following field is invalid: id
|
|
323
|
+
//
|
|
324
|
+
// Also strip `id` from nested array items: Payload's array tables
|
|
325
|
+
// (e.g. posts_content) have a shared PRIMARY KEY on `id` across all
|
|
326
|
+
// locales, so reusing source-locale item IDs for a target locale causes
|
|
327
|
+
// a Postgres 23505 unique-constraint violation.
|
|
328
|
+
const strippedArrayIds = stripArrayItemIds(finalData);
|
|
329
|
+
const updateData = stripSystemFields(strippedArrayIds);
|
|
330
|
+
// Remove nested-docs-managed fields from the update payload entirely.
|
|
331
|
+
// They were already excluded from translation, but defensively delete them
|
|
332
|
+
// here too so a future refactor cannot accidentally re-introduce them.
|
|
333
|
+
if (nestedDocsFieldSlugs) {
|
|
334
|
+
delete updateData[nestedDocsFieldSlugs.parentSlug];
|
|
335
|
+
delete updateData[nestedDocsFieldSlugs.breadcrumbsSlug];
|
|
336
|
+
}
|
|
192
337
|
// Update the document in the target locale
|
|
193
338
|
await req.payload.update({
|
|
194
339
|
id: doc.id,
|
|
195
340
|
collection: collectionSlug,
|
|
196
|
-
data:
|
|
341
|
+
data: updateData,
|
|
197
342
|
locale: targetLocale,
|
|
198
343
|
// Prevent infinite loop - don't trigger hooks
|
|
199
344
|
context: {
|
|
@@ -205,17 +350,24 @@ export const autoTranslate = (pluginOptions)=>(config)=>{
|
|
|
205
350
|
req.payload.logger.info(`[Auto-Translate Plugin] Successfully translated ${collectionSlug}:${doc.id} to ${targetLocale}`);
|
|
206
351
|
}
|
|
207
352
|
} catch (error) {
|
|
353
|
+
// When @payloadcms/plugin-nested-docs `resaveChildren` re-saves a child
|
|
354
|
+
// document that has already been translated, Drizzle's locale-table upsert
|
|
355
|
+
// uses `ON CONFLICT (id)` as the conflict target. Because we pass a freshly
|
|
356
|
+
// generated UUID for `id`, there is no conflict on `id` — but the existing
|
|
357
|
+
// row's `(_parent_id, _locale)` unique constraint fires instead. Postgres
|
|
358
|
+
// surfaces this as a unique-constraint violation, and Payload/Drizzle maps
|
|
359
|
+
// it to a ValidationError with path "id". In this case the locale row that
|
|
360
|
+
// already exists is valid (it was written by an earlier translation pass),
|
|
361
|
+
// so we skip the write and continue rather than surfacing a false failure.
|
|
362
|
+
if (isLocaleRowAlreadyExistsError(error)) {
|
|
363
|
+
if (pluginOptions.debugging) {
|
|
364
|
+
req.payload.logger.info(`[Auto-Translate Plugin] Skipping ${collectionSlug}:${doc.id} → ${targetLocale}: locale row already exists (Drizzle upsert conflict on _parent_id/_locale). Existing translation is kept.`);
|
|
365
|
+
}
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
208
368
|
// Log detailed error information
|
|
209
369
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
210
370
|
const errorStack = error instanceof Error ? error.stack : undefined;
|
|
211
|
-
const errorDetails = {
|
|
212
|
-
collection: collectionSlug,
|
|
213
|
-
documentId: doc.id,
|
|
214
|
-
fromLocale: defaultLocale,
|
|
215
|
-
message: errorMessage,
|
|
216
|
-
stack: errorStack,
|
|
217
|
-
toLocale: targetLocale
|
|
218
|
-
};
|
|
219
371
|
req.payload.logger.error(`[Auto-Translate Plugin] Error translating ${collectionSlug}:${doc.id} to ${targetLocale}:`);
|
|
220
372
|
req.payload.logger.error(errorMessage);
|
|
221
373
|
if (pluginOptions.debugging && errorStack) {
|
|
@@ -231,23 +383,24 @@ export const autoTranslate = (pluginOptions)=>(config)=>{
|
|
|
231
383
|
}
|
|
232
384
|
}
|
|
233
385
|
return result;
|
|
234
|
-
}
|
|
386
|
+
};
|
|
235
387
|
// Prevent infinite loops - skip translation if triggered by our own update
|
|
236
|
-
|
|
237
|
-
|
|
388
|
+
// Wrap ALL afterOperation hooks so the skipAutoTranslate context is checked first
|
|
389
|
+
const existingHooks = [
|
|
390
|
+
...collection.hooks.afterOperation || [],
|
|
391
|
+
translationHook
|
|
238
392
|
];
|
|
239
393
|
collection.hooks.afterOperation = [
|
|
240
394
|
async (args)=>{
|
|
241
395
|
// Skip if this update was triggered by auto-translate
|
|
242
|
-
// Context might not be available on all operations
|
|
243
396
|
if ('req' in args && args.req?.context?.skipAutoTranslate) {
|
|
244
397
|
return args.result;
|
|
245
398
|
}
|
|
246
399
|
// Run all hooks including translation
|
|
247
|
-
for (const hook of
|
|
248
|
-
const
|
|
249
|
-
if (
|
|
250
|
-
args.result =
|
|
400
|
+
for (const hook of existingHooks){
|
|
401
|
+
const hookResult = await hook(args);
|
|
402
|
+
if (hookResult !== undefined) {
|
|
403
|
+
args.result = hookResult;
|
|
251
404
|
}
|
|
252
405
|
}
|
|
253
406
|
return args.result;
|
|
@@ -260,6 +413,46 @@ export const autoTranslate = (pluginOptions)=>(config)=>{
|
|
|
260
413
|
}
|
|
261
414
|
return config;
|
|
262
415
|
};
|
|
416
|
+
/**
|
|
417
|
+
* Detects the specific error pattern produced when Drizzle's locale-table upsert
|
|
418
|
+
* encounters an already-existing row for (_parent_id, _locale).
|
|
419
|
+
*
|
|
420
|
+
* Root cause: Drizzle issues `INSERT … ON CONFLICT (id) DO UPDATE`, generating a
|
|
421
|
+
* fresh UUID for `id`. Because that UUID is new there is no conflict on `id`, but
|
|
422
|
+
* Postgres fires the unique constraint on `(_parent_id, _locale)` instead. Payload
|
|
423
|
+
* maps this constraint violation to a ValidationError with `{ path: "id", message:
|
|
424
|
+
* "Value must be unique" }`.
|
|
425
|
+
*
|
|
426
|
+
* This happens when a plugin such as `@payloadcms/plugin-nested-docs` re-saves child
|
|
427
|
+
* documents (via its `resaveChildren` afterChange hook) that were already translated
|
|
428
|
+
* in an earlier pass. The existing locale data is valid, so we can safely skip the
|
|
429
|
+
* redundant write.
|
|
430
|
+
*/ function isLocaleRowAlreadyExistsError(error) {
|
|
431
|
+
if (!error || typeof error !== 'object') return false;
|
|
432
|
+
const err = error;
|
|
433
|
+
if (err['name'] !== 'ValidationError') return false;
|
|
434
|
+
const data = err['data'];
|
|
435
|
+
if (!data || !Array.isArray(data['errors'])) return false;
|
|
436
|
+
return data['errors'].some((e)=>e['path'] === 'id' && e['message'] === 'Value must be unique');
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Resolves the breadcrumbs/parent field slugs used by @payloadcms/plugin-nested-docs.
|
|
440
|
+
*
|
|
441
|
+
* Returns null when nested-docs compat is explicitly disabled (`nestedDocs: false`).
|
|
442
|
+
* Otherwise returns the configured or default slugs so the caller can:
|
|
443
|
+
* 1. Exclude those fields from the AI translation payload.
|
|
444
|
+
* 2. Strip stale default-locale ids from breadcrumb array items before non-default
|
|
445
|
+
* locale writes (preventing the "Value must be unique: id" Postgres PK collision
|
|
446
|
+
* caused by nested-docs' resaveChildren hook).
|
|
447
|
+
*/ function resolveNestedDocsFieldSlugs(pluginOptions) {
|
|
448
|
+
const opt = pluginOptions.nestedDocs;
|
|
449
|
+
// Explicit opt-out
|
|
450
|
+
if (opt === false) return null;
|
|
451
|
+
return {
|
|
452
|
+
breadcrumbsSlug: typeof opt === 'object' && opt.breadcrumbsFieldSlug ? opt.breadcrumbsFieldSlug : 'breadcrumbs',
|
|
453
|
+
parentSlug: typeof opt === 'object' && opt.parentFieldSlug ? opt.parentFieldSlug : 'parent'
|
|
454
|
+
};
|
|
455
|
+
}
|
|
263
456
|
/**
|
|
264
457
|
* Helper function to get nested value from object using dot notation
|
|
265
458
|
*/ function getNestedValue(obj, path) {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload'\n\nimport type { AutoTranslateConfig } from './types/index.js'\n\nimport { getTranslationExclusionsCollection } from './collections/translationExclusions.js'\nimport { getTranslationSettingsGlobal } from './globals/translationSettings.js'\nimport { TranslationService } from './services/translationService.js'\nimport { injectTranslationControls } from './utilities/injectTranslationControls.js'\n\nexport { getTranslationExclusionsCollection } from './collections/translationExclusions.js'\nexport { getTranslationSettingsGlobal } from './globals/translationSettings.js'\nexport { TranslationService } from './services/translationService.js'\nexport * from './types/index.js'\n\nexport const autoTranslate =\n (pluginOptions: AutoTranslateConfig) =>\n (config: Config): Config => {\n // If the plugin is disabled, return config immediately without any modifications\n if (pluginOptions.disabled) {\n if (pluginOptions.debugging) {\n console.log('[Auto-Translate Plugin] Plugin is disabled, skipping all modifications')\n }\n return config\n }\n\n // Validate configuration\n if (!config.collections) {\n config.collections = []\n }\n\n if (!config.localization) {\n console.warn(\n '[Auto-Translate Plugin] No localization config found. Plugin will not function properly.',\n )\n return config\n }\n\n const localizationConfig = config.localization\n const defaultLocale = localizationConfig.defaultLocale\n const allLocales = Array.isArray(localizationConfig.locales)\n ? localizationConfig.locales.map((l) => (typeof l === 'string' ? l : l.code))\n : []\n\n // Default enableExclusions to true for backward compatibility\n const enableExclusions = pluginOptions.enableExclusions !== false\n\n if (pluginOptions.debugging) {\n console.log('[Auto-Translate Plugin] Configuration:')\n console.log('- Default locale:', defaultLocale)\n console.log('- All locales:', allLocales)\n console.log('- Enabled collections:', Object.keys(pluginOptions.collections || {}))\n console.log('- Exclusions enabled:', enableExclusions)\n }\n\n // Add translation exclusions collection (only if exclusions are enabled)\n if (enableExclusions) {\n const exclusionsSlug = pluginOptions.translationExclusionsSlug || 'translation-exclusions'\n config.collections.push(getTranslationExclusionsCollection(exclusionsSlug))\n }\n\n // Add translation settings global\n if (!config.globals) {\n config.globals = []\n }\n const settingsSlug = pluginOptions.translationSettingsSlug || 'translation-settings'\n config.globals.push(getTranslationSettingsGlobal(settingsSlug))\n\n // Initialize translation service\n const translationService = new TranslationService(pluginOptions)\n\n // Configure collections with auto-translate\n if (pluginOptions.collections) {\n for (const collectionSlug in pluginOptions.collections) {\n const collectionConfig = pluginOptions.collections[collectionSlug]\n\n // Skip if disabled\n if (\n collectionConfig === false ||\n (typeof collectionConfig === 'object' && collectionConfig.enabled === false)\n ) {\n continue\n }\n\n const collection = config.collections.find((c) => c.slug === collectionSlug)\n\n if (!collection) {\n console.warn(`[Auto-Translate Plugin] Collection \"${collectionSlug}\" not found in config`)\n continue\n }\n\n // Add translationSync field to collection\n collection.fields.push({\n name: 'translationSync',\n type: 'checkbox',\n admin: {\n description:\n 'When enabled, changes in the default language will automatically translate to other languages',\n position: 'sidebar',\n },\n defaultValue: pluginOptions.enableTranslationSyncByDefault ?? true,\n label: 'Enable Auto-Translation',\n })\n\n // Auto-inject TranslationControl component into all localized fields\n // Only inject if exclusions are enabled (otherwise there's nothing to control)\n if (enableExclusions && pluginOptions.autoInjectUI !== false) {\n collection.fields = injectTranslationControls(collection.fields, defaultLocale)\n\n if (pluginOptions.debugging) {\n console.log(`[Auto-Translate Plugin] Auto-injected UI controls for: ${collectionSlug}`)\n }\n }\n\n // Add hooks for translation\n if (!collection.hooks) {\n collection.hooks = {}\n }\n\n if (!collection.hooks.afterOperation) {\n collection.hooks.afterOperation = []\n }\n\n // Main translation hook\n collection.hooks.afterOperation.push(async ({ operation, req, result }) => {\n // Only process create and update operations\n if (operation !== 'create' && operation !== 'updateByID') {\n if (pluginOptions.debugging) {\n req.payload.logger.error(\n `[Auto-Translate Plugin] Skipping translation - not create or update operation: ${operation}`,\n )\n }\n return result\n }\n\n // For create/update operations, result should have a id property\n if (!result || typeof result !== 'object' || !('id' in result)) {\n if (pluginOptions.debugging) {\n req.payload.logger.error(\n `[Auto-Translate Plugin] No document found in result: ${JSON.stringify(result)}`,\n )\n }\n return result\n }\n\n const doc = result\n\n // Only translate if editing from default locale\n if (req.locale !== defaultLocale) {\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Skipping translation - not default locale (current: ${req.locale}, default: ${defaultLocale})`,\n )\n }\n return result\n }\n\n // Skip translation for drafts when autosave is enabled\n // Only translate when document is published\n if (doc._status && doc._status !== 'published') {\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Skipping translation - document is a draft (status: ${doc._status})`,\n )\n }\n return result\n }\n\n // Check if translation sync is enabled\n if (!doc.translationSync) {\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Skipping translation - translationSync disabled for ${collectionSlug}:${doc.id}`,\n )\n }\n return result\n }\n\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Processing ${collectionSlug} document ${operation}: ${doc.id}`,\n )\n }\n\n // Get secondary locales (all locales except default)\n const secondaryLocales = allLocales.filter((locale) => locale !== defaultLocale)\n\n // Translate to each secondary locale\n for (const targetLocale of secondaryLocales) {\n try {\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Translating ${collectionSlug}:${doc.id} from ${defaultLocale} to ${targetLocale}`,\n )\n }\n\n // Get field-level exclusions for this locale (only if exclusions are enabled)\n let excludedPaths: string[] = []\n if (enableExclusions) {\n excludedPaths = await translationService.getExclusions(\n req.payload,\n collectionSlug,\n doc.id.toString(),\n targetLocale,\n )\n }\n\n // Get global/collection-level excluded fields\n const configExcludedFields =\n translationService.getConfigExcludedFields(collectionSlug)\n const allExcludedPaths = [...excludedPaths, ...configExcludedFields]\n\n if (pluginOptions.debugging && allExcludedPaths.length > 0) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Excluded paths for ${targetLocale}: ${allExcludedPaths.join(', ')}`,\n )\n }\n\n // Get existing document in target locale to preserve excluded fields\n // Only needed if exclusions are enabled\n let existingDoc: any = null\n if (enableExclusions && allExcludedPaths.length > 0) {\n try {\n const existingResult = await req.payload.findByID({\n id: doc.id,\n collection: collectionSlug,\n fallbackLocale: false,\n locale: targetLocale,\n })\n existingDoc = existingResult\n } catch (error) {\n // Document doesn't exist in this locale yet, that's okay\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] No existing document for ${targetLocale}, will create new`,\n )\n }\n }\n }\n\n // Translate the document\n const translatedData = await translationService.translate({\n collection: collectionSlug,\n data: doc,\n excludedPaths: allExcludedPaths,\n fromLocale: defaultLocale,\n payload: req.payload,\n toLocale: targetLocale,\n })\n\n // Merge translated data with existing, preserving excluded fields\n const finalData = translatedData\n if (existingDoc && allExcludedPaths.length > 0) {\n // Preserve excluded fields from existing document\n for (const excludedPath of allExcludedPaths) {\n const existingValue = getNestedValue(existingDoc, excludedPath)\n if (existingValue !== undefined) {\n setNestedValue(finalData, excludedPath, existingValue)\n }\n }\n }\n\n // Update the document in the target locale\n await req.payload.update({\n id: doc.id,\n collection: collectionSlug,\n data: finalData,\n locale: targetLocale,\n // Prevent infinite loop - don't trigger hooks\n context: {\n skipAutoTranslate: true,\n },\n req,\n })\n\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Successfully translated ${collectionSlug}:${doc.id} to ${targetLocale}`,\n )\n }\n } catch (error) {\n // Log detailed error information\n const errorMessage = error instanceof Error ? error.message : String(error)\n const errorStack = error instanceof Error ? error.stack : undefined\n const errorDetails = {\n collection: collectionSlug,\n documentId: doc.id,\n fromLocale: defaultLocale,\n message: errorMessage,\n stack: errorStack,\n toLocale: targetLocale,\n }\n\n req.payload.logger.error(\n `[Auto-Translate Plugin] Error translating ${collectionSlug}:${doc.id} to ${targetLocale}:`,\n )\n req.payload.logger.error(errorMessage)\n\n if (pluginOptions.debugging && errorStack) {\n req.payload.logger.error('Stack trace:')\n req.payload.logger.error(errorStack)\n }\n\n // Log additional context if it's an OpenAI error\n if (error && typeof error === 'object' && 'error' in error) {\n req.payload.logger.error('OpenAI error details:')\n req.payload.logger.error(JSON.stringify(error, null, 2))\n }\n\n // Continue with other locales even if one fails\n }\n }\n\n return result\n })\n\n // Prevent infinite loops - skip translation if triggered by our own update\n const originalAfterOperationHooks = [...(collection.hooks.afterOperation || [])]\n collection.hooks.afterOperation = [\n async (args) => {\n // Skip if this update was triggered by auto-translate\n // Context might not be available on all operations\n if ('req' in args && args.req?.context?.skipAutoTranslate) {\n return args.result\n }\n\n // Run all hooks including translation\n for (const hook of originalAfterOperationHooks) {\n const result = await hook(args)\n if (result !== undefined) {\n args.result = result\n }\n }\n\n return args.result\n },\n ]\n\n if (pluginOptions.debugging) {\n console.log(`[Auto-Translate Plugin] Configured collection: ${collectionSlug}`)\n }\n }\n }\n\n return config\n }\n\n/**\n * Helper function to get nested value from object using dot notation\n */\nfunction getNestedValue(obj: any, path: string): any {\n return path.split('.').reduce((current, part) => {\n if (current === null || current === undefined) {\n return undefined\n }\n return current[part]\n }, obj)\n}\n\n/**\n * Helper function to set nested value in object using dot notation\n */\nfunction setNestedValue(obj: any, path: string, value: any): void {\n const parts = path.split('.')\n let current = obj\n\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n if (!(part in current) || current[part] === null || typeof current[part] !== 'object') {\n // Check if next part is a number (array index)\n const nextPart = parts[i + 1]\n current[part] = /^\\d+$/.test(nextPart) ? [] : {}\n }\n current = current[part]\n }\n\n current[parts[parts.length - 1]] = value\n}\n"],"names":["getTranslationExclusionsCollection","getTranslationSettingsGlobal","TranslationService","injectTranslationControls","autoTranslate","pluginOptions","config","disabled","debugging","console","log","collections","localization","warn","localizationConfig","defaultLocale","allLocales","Array","isArray","locales","map","l","code","enableExclusions","Object","keys","exclusionsSlug","translationExclusionsSlug","push","globals","settingsSlug","translationSettingsSlug","translationService","collectionSlug","collectionConfig","enabled","collection","find","c","slug","fields","name","type","admin","description","position","defaultValue","enableTranslationSyncByDefault","label","autoInjectUI","hooks","afterOperation","operation","req","result","payload","logger","error","JSON","stringify","doc","locale","info","_status","translationSync","id","secondaryLocales","filter","targetLocale","excludedPaths","getExclusions","toString","configExcludedFields","getConfigExcludedFields","allExcludedPaths","length","join","existingDoc","existingResult","findByID","fallbackLocale","translatedData","translate","data","fromLocale","toLocale","finalData","excludedPath","existingValue","getNestedValue","undefined","setNestedValue","update","context","skipAutoTranslate","errorMessage","Error","message","String","errorStack","stack","errorDetails","documentId","originalAfterOperationHooks","args","hook","obj","path","split","reduce","current","part","value","parts","i","nextPart","test"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import type { Config } from 'payload'\n\nimport type { AutoTranslateConfig } from './types/index.js'\n\nimport { getTranslationExclusionsCollection } from './collections/translationExclusions.js'\nimport { getTranslationSettingsGlobal } from './globals/translationSettings.js'\nimport { TranslationService } from './services/translationService.js'\nimport { injectTranslationControls } from './utilities/injectTranslationControls.js'\n\nexport { getTranslationExclusionsCollection } from './collections/translationExclusions.js'\nexport { getTranslationSettingsGlobal } from './globals/translationSettings.js'\nexport { TranslationService } from './services/translationService.js'\nexport * from './types/index.js'\n\n// Fields that must never be passed as data to payload.update / payload.create\n// (Postgres/drizzle rejects them; MongoDB silently ignores them)\nconst SYSTEM_FIELDS = new Set([\n 'id',\n 'createdAt',\n 'updatedAt',\n '_status',\n '__v',\n 'globalType',\n 'updatedBy',\n])\n\nfunction stripSystemFields(data: Record<string, unknown>): Record<string, unknown> {\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(data)) {\n if (!SYSTEM_FIELDS.has(key)) {\n result[key] = value\n }\n }\n return result\n}\n\n/**\n * Strips `id` from objects that are direct elements of arrays, recursively\n * through the data tree. This prevents Postgres unique-constraint violations\n * when inserting locale-specific rows into array tables (e.g. posts_content)\n * that share a single PRIMARY KEY on `id` across all locales.\n *\n * Relationship objects (plain objects that are NOT direct array items) keep\n * their `id` so that Payload can still resolve them correctly.\n */\nfunction stripArrayItemIds(data: unknown): unknown {\n if (Array.isArray(data)) {\n return data.map((item) => {\n if (item && typeof item === 'object' && !Array.isArray(item)) {\n // Direct array item — strip its Payload-internal `id`\n const { id: _id, ...rest } = item as Record<string, unknown>\n const processed: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(rest)) {\n processed[key] = stripArrayItemIds(value)\n }\n return processed\n }\n return stripArrayItemIds(item)\n })\n }\n\n if (data && typeof data === 'object') {\n const result: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(data as Record<string, unknown>)) {\n result[key] = stripArrayItemIds(value)\n }\n return result\n }\n\n return data\n}\n\nexport const autoTranslate =\n (pluginOptions: AutoTranslateConfig) =>\n (incomingConfig: Config): Config => {\n // Create a shallow copy so we never mutate the caller's config object\n const config: Config = { ...incomingConfig }\n\n // If the plugin is disabled, return config immediately without any modifications\n if (pluginOptions.disabled) {\n if (pluginOptions.debugging) {\n console.log('[Auto-Translate Plugin] Plugin is disabled, skipping all modifications')\n }\n return config\n }\n\n if (!config.localization) {\n console.warn(\n '[Auto-Translate Plugin] No localization config found. Plugin will not function properly.',\n )\n return config\n }\n\n const localizationConfig = config.localization\n const defaultLocale = localizationConfig.defaultLocale\n const allLocales = Array.isArray(localizationConfig.locales)\n ? localizationConfig.locales.map((l) => (typeof l === 'string' ? l : l.code))\n : []\n\n // Default enableExclusions to true for backward compatibility\n const enableExclusions = pluginOptions.enableExclusions !== false\n\n if (pluginOptions.debugging) {\n console.log('[Auto-Translate Plugin] Configuration:')\n console.log('- Default locale:', defaultLocale)\n console.log('- All locales:', allLocales)\n console.log('- Enabled collections:', Object.keys(pluginOptions.collections || {}))\n console.log('- Exclusions enabled:', enableExclusions)\n }\n\n // Add translation exclusions collection (only if exclusions are enabled)\n // Use spread to avoid mutating the original array\n if (enableExclusions) {\n const exclusionsSlug = pluginOptions.translationExclusionsSlug || 'translation-exclusions'\n config.collections = [\n ...(config.collections || []),\n getTranslationExclusionsCollection(exclusionsSlug),\n ]\n } else {\n config.collections = [...(config.collections || [])]\n }\n\n // Add translation settings global using spread\n const settingsSlug = pluginOptions.translationSettingsSlug || 'translation-settings'\n config.globals = [...(config.globals || []), getTranslationSettingsGlobal(settingsSlug)]\n\n // Initialize translation service\n const translationService = new TranslationService(pluginOptions)\n\n // Configure collections with auto-translate\n if (pluginOptions.collections) {\n for (const rawSlug in pluginOptions.collections) {\n // Payload 3.85+ requires CollectionSlug (strict union), but for...in\n // yields string. Cast once here and use collectionSlug throughout.\n const collectionSlug = rawSlug as import('payload').CollectionSlug\n const collectionConfig =\n pluginOptions.collections[collectionSlug as keyof typeof pluginOptions.collections]\n\n // Skip if disabled\n if (\n collectionConfig === false ||\n (typeof collectionConfig === 'object' && collectionConfig.enabled === false)\n ) {\n continue\n }\n\n const collection = config.collections.find((c) => c.slug === collectionSlug)\n\n if (!collection) {\n console.warn(`[Auto-Translate Plugin] Collection \"${collectionSlug}\" not found in config`)\n continue\n }\n\n // Add translationSync field to collection\n collection.fields = [\n ...collection.fields,\n {\n name: 'translationSync',\n type: 'checkbox',\n admin: {\n description:\n 'When enabled, changes in the default language will automatically translate to other languages',\n position: 'sidebar',\n },\n defaultValue: pluginOptions.enableTranslationSyncByDefault ?? true,\n label: 'Enable Auto-Translation',\n },\n ]\n\n // Auto-inject TranslationControl component into all localized fields\n // Only inject if exclusions are enabled (otherwise there's nothing to control)\n if (enableExclusions && pluginOptions.autoInjectUI !== false) {\n collection.fields = injectTranslationControls(collection.fields, defaultLocale)\n\n if (pluginOptions.debugging) {\n console.log(`[Auto-Translate Plugin] Auto-injected UI controls for: ${collectionSlug}`)\n }\n }\n\n // Add hooks for translation\n if (!collection.hooks) {\n collection.hooks = {}\n }\n\n if (!collection.hooks.afterOperation) {\n collection.hooks.afterOperation = []\n }\n\n // ---------------------------------------------------------------\n // Nested-docs compatibility\n // ---------------------------------------------------------------\n // Resolve nested-docs field slugs once, shared by both the\n // beforeChange guard and the afterOperation translation hook below.\n const nestedDocsFieldSlugs = resolveNestedDocsFieldSlugs(pluginOptions)\n\n // Determine whether this collection actually has a breadcrumbs array\n // field (added by nestedDocsPlugin or manually).\n const hasBreadcrumbsField =\n nestedDocsFieldSlugs !== null &&\n collection.fields.some(\n (f) =>\n 'name' in f &&\n f.name === nestedDocsFieldSlugs.breadcrumbsSlug &&\n f.type === 'array',\n )\n\n if (hasBreadcrumbsField && nestedDocsFieldSlugs) {\n const { breadcrumbsSlug } = nestedDocsFieldSlugs\n\n // Guard: strip `id` from breadcrumb array items on non-default-locale writes.\n //\n // Root cause: nested-docs' `resaveChildren` afterChange hook re-saves each\n // child document when a parent is updated. For locales where the child has no\n // row yet, `payload.find(child, locale)` falls back to the default locale,\n // returning breadcrumbs that carry the default-locale array-item `id`s.\n // `formatBreadcrumb` preserves those ids via `{ ...breadcrumb, doc, label, url }`.\n // When Payload then writes the child in the secondary locale, Drizzle attempts\n // an INSERT with the same `id` — colliding on the `breadcrumbs.id` PRIMARY KEY\n // (shared across locales) and producing `ValidationError: Value must be unique: id`.\n //\n // Fix: remove `id` from every breadcrumb item in incoming data for any\n // non-default-locale write. Payload will assign fresh per-locale ids on INSERT.\n // This hook fires AFTER nested-docs' `populateBreadcrumbsBeforeChange` (because\n // autoTranslate is registered later), so breadcrumbs are already fully populated\n // before we strip the stale ids.\n if (!collection.hooks.beforeChange) {\n collection.hooks.beforeChange = []\n }\n collection.hooks.beforeChange.push(async ({ data, req }: any) => {\n if (!req.locale || req.locale === defaultLocale) return data\n if (!data[breadcrumbsSlug] || !Array.isArray(data[breadcrumbsSlug])) return data\n return {\n ...data,\n [breadcrumbsSlug]: data[breadcrumbsSlug].map((item: any) => {\n if (item && typeof item === 'object') {\n const { id: _id, ...rest } = item as Record<string, unknown>\n return rest\n }\n return item\n }),\n }\n })\n\n if (pluginOptions.debugging) {\n console.log(\n `[Auto-Translate Plugin] Nested-docs beforeChange guard added for: ${collectionSlug}`,\n )\n }\n }\n\n // Main translation hook\n const translationHook = async ({ operation, req, result }: any) => {\n // Only process create and updateByID operations\n if (operation !== 'create' && operation !== 'updateByID') {\n if (pluginOptions.debugging) {\n req.payload.logger.error(\n `[Auto-Translate Plugin] Skipping translation - not create or update operation: ${operation}`,\n )\n }\n return result\n }\n\n // For create/update operations, result should have an id property\n if (!result || typeof result !== 'object' || !('id' in result)) {\n if (pluginOptions.debugging) {\n req.payload.logger.error(\n `[Auto-Translate Plugin] No document found in result: ${JSON.stringify(result)}`,\n )\n }\n return result\n }\n\n const doc = result\n\n // Only translate if editing from default locale\n if (req.locale !== defaultLocale) {\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Skipping translation - not default locale (current: ${req.locale}, default: ${defaultLocale})`,\n )\n }\n return result\n }\n\n // Skip translation for drafts when autosave is enabled\n // Only translate when document is published\n if (doc._status && doc._status !== 'published') {\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Skipping translation - document is a draft (status: ${doc._status})`,\n )\n }\n return result\n }\n\n // Check if translation sync is enabled\n if (!doc.translationSync) {\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Skipping translation - translationSync disabled for ${collectionSlug}:${doc.id}`,\n )\n }\n return result\n }\n\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Processing ${collectionSlug} document ${operation}: ${doc.id}`,\n )\n }\n\n // Get secondary locales (all locales except default)\n const secondaryLocales = allLocales.filter((locale) => locale !== defaultLocale)\n\n // Translate to each secondary locale\n for (const targetLocale of secondaryLocales) {\n try {\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Translating ${collectionSlug}:${doc.id} from ${defaultLocale} to ${targetLocale}`,\n )\n }\n\n // Get field-level exclusions for this locale (only if exclusions are enabled)\n let excludedPaths: string[] = []\n if (enableExclusions) {\n excludedPaths = await translationService.getExclusions(\n req.payload,\n collectionSlug,\n doc.id.toString(),\n targetLocale,\n )\n }\n\n // Get global/collection-level excluded fields\n const configExcludedFields =\n translationService.getConfigExcludedFields(collectionSlug)\n\n // Exclude nested-docs-managed fields from the AI translation payload.\n // `parent` is locale-invariant (the same relationship across all locales)\n // and must never be overwritten with an AI-translated value.\n // `breadcrumbs` are computed and managed entirely by nested-docs; sending\n // them through the AI would produce garbled data and would be overwritten\n // by nested-docs anyway.\n const nestedDocsExcludedFields = nestedDocsFieldSlugs\n ? [nestedDocsFieldSlugs.parentSlug, nestedDocsFieldSlugs.breadcrumbsSlug]\n : []\n\n const allExcludedPaths = [\n ...excludedPaths,\n ...configExcludedFields,\n ...nestedDocsExcludedFields,\n ]\n\n if (pluginOptions.debugging && allExcludedPaths.length > 0) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Excluded paths for ${targetLocale}: ${allExcludedPaths.join(', ')}`,\n )\n }\n\n // Get existing document in target locale to preserve excluded fields\n // Only needed if exclusions are enabled\n let existingDoc: any = null\n if (enableExclusions && allExcludedPaths.length > 0) {\n try {\n const existingResult = await req.payload.findByID({\n id: doc.id,\n collection: collectionSlug,\n fallbackLocale: false,\n locale: targetLocale,\n })\n existingDoc = existingResult\n } catch (error) {\n // Document doesn't exist in this locale yet, that's okay\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] No existing document for ${targetLocale}, will create new`,\n )\n }\n }\n }\n\n // Translate the document\n const translatedData = await translationService.translate({\n collection: collectionSlug,\n data: doc,\n excludedPaths: allExcludedPaths,\n fromLocale: defaultLocale,\n payload: req.payload,\n toLocale: targetLocale,\n })\n\n // Merge translated data with existing, preserving excluded fields\n let finalData = { ...translatedData }\n if (existingDoc && allExcludedPaths.length > 0) {\n // Preserve excluded fields from existing document\n for (const excludedPath of allExcludedPaths) {\n const existingValue = getNestedValue(existingDoc, excludedPath)\n if (existingValue !== undefined) {\n setNestedValue(finalData, excludedPath, existingValue)\n }\n }\n }\n\n // Strip system/internal fields before updating so Postgres adapter\n // does not receive `id`, `createdAt`, `updatedAt`, etc. as data fields.\n // MongoDB is lenient with extra fields; Postgres/drizzle raises\n // ValidationError: The following field is invalid: id\n //\n // Also strip `id` from nested array items: Payload's array tables\n // (e.g. posts_content) have a shared PRIMARY KEY on `id` across all\n // locales, so reusing source-locale item IDs for a target locale causes\n // a Postgres 23505 unique-constraint violation.\n const strippedArrayIds = stripArrayItemIds(finalData)\n const updateData = stripSystemFields(strippedArrayIds as Record<string, unknown>)\n\n // Remove nested-docs-managed fields from the update payload entirely.\n // They were already excluded from translation, but defensively delete them\n // here too so a future refactor cannot accidentally re-introduce them.\n if (nestedDocsFieldSlugs) {\n delete updateData[nestedDocsFieldSlugs.parentSlug]\n delete updateData[nestedDocsFieldSlugs.breadcrumbsSlug]\n }\n\n // Update the document in the target locale\n await req.payload.update({\n id: doc.id,\n collection: collectionSlug,\n data: updateData,\n locale: targetLocale,\n // Prevent infinite loop - don't trigger hooks\n context: {\n skipAutoTranslate: true,\n },\n req,\n })\n\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Successfully translated ${collectionSlug}:${doc.id} to ${targetLocale}`,\n )\n }\n } catch (error) {\n // When @payloadcms/plugin-nested-docs `resaveChildren` re-saves a child\n // document that has already been translated, Drizzle's locale-table upsert\n // uses `ON CONFLICT (id)` as the conflict target. Because we pass a freshly\n // generated UUID for `id`, there is no conflict on `id` — but the existing\n // row's `(_parent_id, _locale)` unique constraint fires instead. Postgres\n // surfaces this as a unique-constraint violation, and Payload/Drizzle maps\n // it to a ValidationError with path \"id\". In this case the locale row that\n // already exists is valid (it was written by an earlier translation pass),\n // so we skip the write and continue rather than surfacing a false failure.\n if (isLocaleRowAlreadyExistsError(error)) {\n if (pluginOptions.debugging) {\n req.payload.logger.info(\n `[Auto-Translate Plugin] Skipping ${collectionSlug}:${doc.id} → ${targetLocale}: locale row already exists (Drizzle upsert conflict on _parent_id/_locale). Existing translation is kept.`,\n )\n }\n continue\n }\n\n // Log detailed error information\n const errorMessage = error instanceof Error ? error.message : String(error)\n const errorStack = error instanceof Error ? error.stack : undefined\n\n req.payload.logger.error(\n `[Auto-Translate Plugin] Error translating ${collectionSlug}:${doc.id} to ${targetLocale}:`,\n )\n req.payload.logger.error(errorMessage)\n\n if (pluginOptions.debugging && errorStack) {\n req.payload.logger.error('Stack trace:')\n req.payload.logger.error(errorStack)\n }\n\n // Log additional context if it's an OpenAI error\n if (error && typeof error === 'object' && 'error' in error) {\n req.payload.logger.error('OpenAI error details:')\n req.payload.logger.error(JSON.stringify(error, null, 2))\n }\n\n // Continue with other locales even if one fails\n }\n }\n\n return result\n }\n\n // Prevent infinite loops - skip translation if triggered by our own update\n // Wrap ALL afterOperation hooks so the skipAutoTranslate context is checked first\n const existingHooks = [...(collection.hooks.afterOperation || []), translationHook]\n collection.hooks.afterOperation = [\n async (args: any) => {\n // Skip if this update was triggered by auto-translate\n if ('req' in args && args.req?.context?.skipAutoTranslate) {\n return args.result\n }\n\n // Run all hooks including translation\n for (const hook of existingHooks) {\n const hookResult = await hook(args)\n if (hookResult !== undefined) {\n args.result = hookResult\n }\n }\n\n return args.result\n },\n ]\n\n if (pluginOptions.debugging) {\n console.log(`[Auto-Translate Plugin] Configured collection: ${collectionSlug}`)\n }\n }\n }\n\n return config\n }\n\n/**\n * Detects the specific error pattern produced when Drizzle's locale-table upsert\n * encounters an already-existing row for (_parent_id, _locale).\n *\n * Root cause: Drizzle issues `INSERT … ON CONFLICT (id) DO UPDATE`, generating a\n * fresh UUID for `id`. Because that UUID is new there is no conflict on `id`, but\n * Postgres fires the unique constraint on `(_parent_id, _locale)` instead. Payload\n * maps this constraint violation to a ValidationError with `{ path: \"id\", message:\n * \"Value must be unique\" }`.\n *\n * This happens when a plugin such as `@payloadcms/plugin-nested-docs` re-saves child\n * documents (via its `resaveChildren` afterChange hook) that were already translated\n * in an earlier pass. The existing locale data is valid, so we can safely skip the\n * redundant write.\n */\nfunction isLocaleRowAlreadyExistsError(error: unknown): boolean {\n if (!error || typeof error !== 'object') return false\n const err = error as Record<string, unknown>\n if (err['name'] !== 'ValidationError') return false\n const data = err['data'] as Record<string, unknown> | undefined\n if (!data || !Array.isArray(data['errors'])) return false\n return (data['errors'] as Array<Record<string, unknown>>).some(\n (e) => e['path'] === 'id' && e['message'] === 'Value must be unique',\n )\n}\n\n/**\n * Resolves the breadcrumbs/parent field slugs used by @payloadcms/plugin-nested-docs.\n *\n * Returns null when nested-docs compat is explicitly disabled (`nestedDocs: false`).\n * Otherwise returns the configured or default slugs so the caller can:\n * 1. Exclude those fields from the AI translation payload.\n * 2. Strip stale default-locale ids from breadcrumb array items before non-default\n * locale writes (preventing the \"Value must be unique: id\" Postgres PK collision\n * caused by nested-docs' resaveChildren hook).\n */\nfunction resolveNestedDocsFieldSlugs(\n pluginOptions: AutoTranslateConfig,\n): { breadcrumbsSlug: string; parentSlug: string } | null {\n const opt = pluginOptions.nestedDocs\n // Explicit opt-out\n if (opt === false) return null\n return {\n breadcrumbsSlug:\n typeof opt === 'object' && opt.breadcrumbsFieldSlug ? opt.breadcrumbsFieldSlug : 'breadcrumbs',\n parentSlug:\n typeof opt === 'object' && opt.parentFieldSlug ? opt.parentFieldSlug : 'parent',\n }\n}\n\n/**\n * Helper function to get nested value from object using dot notation\n */\nfunction getNestedValue(obj: any, path: string): any {\n return path.split('.').reduce((current, part) => {\n if (current === null || current === undefined) {\n return undefined\n }\n return current[part]\n }, obj)\n}\n\n/**\n * Helper function to set nested value in object using dot notation\n */\nfunction setNestedValue(obj: any, path: string, value: any): void {\n const parts = path.split('.')\n let current = obj\n\n for (let i = 0; i < parts.length - 1; i++) {\n const part = parts[i]\n if (!(part in current) || current[part] === null || typeof current[part] !== 'object') {\n // Check if next part is a number (array index)\n const nextPart = parts[i + 1]\n current[part] = /^\\d+$/.test(nextPart) ? [] : {}\n }\n current = current[part]\n }\n\n current[parts[parts.length - 1]] = value\n}\n"],"names":["getTranslationExclusionsCollection","getTranslationSettingsGlobal","TranslationService","injectTranslationControls","SYSTEM_FIELDS","Set","stripSystemFields","data","result","key","value","Object","entries","has","stripArrayItemIds","Array","isArray","map","item","id","_id","rest","processed","autoTranslate","pluginOptions","incomingConfig","config","disabled","debugging","console","log","localization","warn","localizationConfig","defaultLocale","allLocales","locales","l","code","enableExclusions","keys","collections","exclusionsSlug","translationExclusionsSlug","settingsSlug","translationSettingsSlug","globals","translationService","rawSlug","collectionSlug","collectionConfig","enabled","collection","find","c","slug","fields","name","type","admin","description","position","defaultValue","enableTranslationSyncByDefault","label","autoInjectUI","hooks","afterOperation","nestedDocsFieldSlugs","resolveNestedDocsFieldSlugs","hasBreadcrumbsField","some","f","breadcrumbsSlug","beforeChange","push","req","locale","translationHook","operation","payload","logger","error","JSON","stringify","doc","info","_status","translationSync","secondaryLocales","filter","targetLocale","excludedPaths","getExclusions","toString","configExcludedFields","getConfigExcludedFields","nestedDocsExcludedFields","parentSlug","allExcludedPaths","length","join","existingDoc","existingResult","findByID","fallbackLocale","translatedData","translate","fromLocale","toLocale","finalData","excludedPath","existingValue","getNestedValue","undefined","setNestedValue","strippedArrayIds","updateData","update","context","skipAutoTranslate","isLocaleRowAlreadyExistsError","errorMessage","Error","message","String","errorStack","stack","existingHooks","args","hook","hookResult","err","e","opt","nestedDocs","breadcrumbsFieldSlug","parentFieldSlug","obj","path","split","reduce","current","part","parts","i","nextPart","test"],"mappings":"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"}
|
|
@@ -53,6 +53,12 @@ export declare class TranslationService {
|
|
|
53
53
|
* Main translation method
|
|
54
54
|
*/
|
|
55
55
|
translate(options: TranslateOptions): Promise<any>;
|
|
56
|
+
/**
|
|
57
|
+
* Resolves the field schema for a collection or global slug so translation can
|
|
58
|
+
* be made schema-aware (e.g. to avoid translating enum-backed select/radio
|
|
59
|
+
* field values).
|
|
60
|
+
*/
|
|
61
|
+
private getDocumentFields;
|
|
56
62
|
/**
|
|
57
63
|
* Translates using OpenAI API (optimized version)
|
|
58
64
|
* This method is now public and can be used directly in your application
|