@pigment/auto-translate 1.2.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/LICENSE +22 -0
- package/README.md +405 -0
- package/dist/collections/translationExclusions.d.ts +2 -0
- package/dist/collections/translationExclusions.js +78 -0
- package/dist/collections/translationExclusions.js.map +1 -0
- package/dist/components/TranslationControl.css +92 -0
- package/dist/components/TranslationControl.d.ts +18 -0
- package/dist/components/TranslationControl.js +274 -0
- package/dist/components/TranslationControl.js.map +1 -0
- package/dist/exports/client.d.ts +5 -0
- package/dist/exports/client.js +5 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/rsc.d.ts +5 -0
- package/dist/exports/rsc.js +5 -0
- package/dist/exports/rsc.js.map +1 -0
- package/dist/globals/translationSettings.d.ts +2 -0
- package/dist/globals/translationSettings.js +78 -0
- package/dist/globals/translationSettings.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +254 -0
- package/dist/index.js.map +1 -0
- package/dist/services/translationService.d.ts +60 -0
- package/dist/services/translationService.js +533 -0
- package/dist/services/translationService.js.map +1 -0
- package/dist/types/index.d.ts +103 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utilities/fieldHelpers.d.ts +34 -0
- package/dist/utilities/fieldHelpers.js +180 -0
- package/dist/utilities/fieldHelpers.js.map +1 -0
- package/dist/utilities/injectTranslationControls.d.ts +5 -0
- package/dist/utilities/injectTranslationControls.js +92 -0
- package/dist/utilities/injectTranslationControls.js.map +1 -0
- package/package.json +114 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { getTranslationExclusionsCollection } from './collections/translationExclusions.js';
|
|
2
|
+
import { getTranslationSettingsGlobal } from './globals/translationSettings.js';
|
|
3
|
+
import { TranslationService } from './services/translationService.js';
|
|
4
|
+
import { injectTranslationControls } from './utilities/injectTranslationControls.js';
|
|
5
|
+
export * from './types/index.js';
|
|
6
|
+
export { getTranslationExclusionsCollection } from './collections/translationExclusions.js';
|
|
7
|
+
export { getTranslationSettingsGlobal } from './globals/translationSettings.js';
|
|
8
|
+
export const autoTranslate = (pluginOptions)=>(config)=>{
|
|
9
|
+
// Validate configuration
|
|
10
|
+
if (!config.collections) {
|
|
11
|
+
config.collections = [];
|
|
12
|
+
}
|
|
13
|
+
if (!config.localization) {
|
|
14
|
+
console.warn('[Auto-Translate Plugin] No localization config found. Plugin will not function properly.');
|
|
15
|
+
return config;
|
|
16
|
+
}
|
|
17
|
+
const localizationConfig = config.localization;
|
|
18
|
+
const defaultLocale = localizationConfig.defaultLocale;
|
|
19
|
+
const allLocales = Array.isArray(localizationConfig.locales) ? localizationConfig.locales.map((l)=>typeof l === 'string' ? l : l.code) : [];
|
|
20
|
+
// Default enableExclusions to true for backward compatibility
|
|
21
|
+
const enableExclusions = pluginOptions.enableExclusions !== false;
|
|
22
|
+
if (pluginOptions.debugging) {
|
|
23
|
+
console.log('[Auto-Translate Plugin] Configuration:');
|
|
24
|
+
console.log('- Default locale:', defaultLocale);
|
|
25
|
+
console.log('- All locales:', allLocales);
|
|
26
|
+
console.log('- Enabled collections:', Object.keys(pluginOptions.collections || {}));
|
|
27
|
+
console.log('- Exclusions enabled:', enableExclusions);
|
|
28
|
+
}
|
|
29
|
+
// Add translation exclusions collection (only if exclusions are enabled)
|
|
30
|
+
if (enableExclusions) {
|
|
31
|
+
const exclusionsSlug = pluginOptions.translationExclusionsSlug || 'translation-exclusions';
|
|
32
|
+
config.collections.push(getTranslationExclusionsCollection(exclusionsSlug));
|
|
33
|
+
}
|
|
34
|
+
// Add translation settings global
|
|
35
|
+
if (!config.globals) {
|
|
36
|
+
config.globals = [];
|
|
37
|
+
}
|
|
38
|
+
const settingsSlug = pluginOptions.translationSettingsSlug || 'translation-settings';
|
|
39
|
+
config.globals.push(getTranslationSettingsGlobal(settingsSlug));
|
|
40
|
+
// Initialize translation service
|
|
41
|
+
const translationService = new TranslationService(pluginOptions);
|
|
42
|
+
// Configure collections with auto-translate
|
|
43
|
+
if (pluginOptions.collections) {
|
|
44
|
+
for(const collectionSlug in pluginOptions.collections){
|
|
45
|
+
const collectionConfig = pluginOptions.collections[collectionSlug];
|
|
46
|
+
// Skip if disabled
|
|
47
|
+
if (collectionConfig === false || typeof collectionConfig === 'object' && collectionConfig.enabled === false) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const collection = config.collections.find((c)=>c.slug === collectionSlug);
|
|
51
|
+
if (!collection) {
|
|
52
|
+
console.warn(`[Auto-Translate Plugin] Collection "${collectionSlug}" not found in config`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// Add translationSync field to collection
|
|
56
|
+
collection.fields.push({
|
|
57
|
+
name: 'translationSync',
|
|
58
|
+
type: 'checkbox',
|
|
59
|
+
admin: {
|
|
60
|
+
description: 'When enabled, changes in the default language will automatically translate to other languages',
|
|
61
|
+
position: 'sidebar'
|
|
62
|
+
},
|
|
63
|
+
defaultValue: pluginOptions.enableTranslationSyncByDefault ?? true,
|
|
64
|
+
label: 'Enable Auto-Translation'
|
|
65
|
+
});
|
|
66
|
+
// Auto-inject TranslationControl component into all localized fields
|
|
67
|
+
// Only inject if exclusions are enabled (otherwise there's nothing to control)
|
|
68
|
+
if (enableExclusions && pluginOptions.autoInjectUI !== false) {
|
|
69
|
+
collection.fields = injectTranslationControls(collection.fields, defaultLocale);
|
|
70
|
+
if (pluginOptions.debugging) {
|
|
71
|
+
console.log(`[Auto-Translate Plugin] Auto-injected UI controls for: ${collectionSlug}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// Add hooks for translation
|
|
75
|
+
if (!collection.hooks) {
|
|
76
|
+
collection.hooks = {};
|
|
77
|
+
}
|
|
78
|
+
if (!collection.hooks.afterChange) {
|
|
79
|
+
collection.hooks.afterChange = [];
|
|
80
|
+
}
|
|
81
|
+
// Main translation hook
|
|
82
|
+
collection.hooks.afterChange.push(async ({ doc, operation, previousDoc, req })=>{
|
|
83
|
+
// Only process create and update operations
|
|
84
|
+
if (operation !== 'create' && operation !== 'update') {
|
|
85
|
+
return doc;
|
|
86
|
+
}
|
|
87
|
+
// Only translate if editing from default locale
|
|
88
|
+
if (req.locale !== defaultLocale) {
|
|
89
|
+
if (pluginOptions.debugging) {
|
|
90
|
+
req.payload.logger.info(`[Auto-Translate Plugin] Skipping translation - not default locale (current: ${req.locale}, default: ${defaultLocale})`);
|
|
91
|
+
}
|
|
92
|
+
return doc;
|
|
93
|
+
}
|
|
94
|
+
// Skip translation for drafts when autosave is enabled
|
|
95
|
+
// Only translate when document is published
|
|
96
|
+
if (doc._status && doc._status !== 'published') {
|
|
97
|
+
if (pluginOptions.debugging) {
|
|
98
|
+
req.payload.logger.info(`[Auto-Translate Plugin] Skipping translation - document is a draft (status: ${doc._status})`);
|
|
99
|
+
}
|
|
100
|
+
return doc;
|
|
101
|
+
}
|
|
102
|
+
// Check if translation sync is enabled
|
|
103
|
+
if (!doc.translationSync) {
|
|
104
|
+
if (pluginOptions.debugging) {
|
|
105
|
+
req.payload.logger.info(`[Auto-Translate Plugin] Skipping translation - translationSync disabled for ${collectionSlug}:${doc.id}`);
|
|
106
|
+
}
|
|
107
|
+
return doc;
|
|
108
|
+
}
|
|
109
|
+
if (pluginOptions.debugging) {
|
|
110
|
+
req.payload.logger.info(`[Auto-Translate Plugin] Processing ${collectionSlug} document ${operation}: ${doc.id}`);
|
|
111
|
+
}
|
|
112
|
+
// Get secondary locales (all locales except default)
|
|
113
|
+
const secondaryLocales = allLocales.filter((locale)=>locale !== defaultLocale);
|
|
114
|
+
// Translate to each secondary locale
|
|
115
|
+
for (const targetLocale of secondaryLocales){
|
|
116
|
+
try {
|
|
117
|
+
if (pluginOptions.debugging) {
|
|
118
|
+
req.payload.logger.info(`[Auto-Translate Plugin] Translating ${collectionSlug}:${doc.id} from ${defaultLocale} to ${targetLocale}`);
|
|
119
|
+
}
|
|
120
|
+
// Get field-level exclusions for this locale (only if exclusions are enabled)
|
|
121
|
+
let excludedPaths = [];
|
|
122
|
+
if (enableExclusions) {
|
|
123
|
+
excludedPaths = await translationService.getExclusions(req.payload, collectionSlug, doc.id, targetLocale);
|
|
124
|
+
}
|
|
125
|
+
// Get global/collection-level excluded fields
|
|
126
|
+
const configExcludedFields = translationService.getConfigExcludedFields(collectionSlug);
|
|
127
|
+
const allExcludedPaths = [
|
|
128
|
+
...excludedPaths,
|
|
129
|
+
...configExcludedFields
|
|
130
|
+
];
|
|
131
|
+
if (pluginOptions.debugging && allExcludedPaths.length > 0) {
|
|
132
|
+
req.payload.logger.info(`[Auto-Translate Plugin] Excluded paths for ${targetLocale}: ${allExcludedPaths.join(', ')}`);
|
|
133
|
+
}
|
|
134
|
+
// Get existing document in target locale to preserve excluded fields
|
|
135
|
+
// Only needed if exclusions are enabled
|
|
136
|
+
let existingDoc = null;
|
|
137
|
+
if (enableExclusions && allExcludedPaths.length > 0) {
|
|
138
|
+
try {
|
|
139
|
+
const existingResult = await req.payload.findByID({
|
|
140
|
+
id: doc.id,
|
|
141
|
+
collection: collectionSlug,
|
|
142
|
+
fallbackLocale: false,
|
|
143
|
+
locale: targetLocale
|
|
144
|
+
});
|
|
145
|
+
existingDoc = existingResult;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
// Document doesn't exist in this locale yet, that's okay
|
|
148
|
+
if (pluginOptions.debugging) {
|
|
149
|
+
req.payload.logger.info(`[Auto-Translate Plugin] No existing document for ${targetLocale}, will create new`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
// Translate the document
|
|
154
|
+
const translatedData = await translationService.translate({
|
|
155
|
+
collection: collectionSlug,
|
|
156
|
+
data: doc,
|
|
157
|
+
excludedPaths: allExcludedPaths,
|
|
158
|
+
fromLocale: defaultLocale,
|
|
159
|
+
payload: req.payload,
|
|
160
|
+
toLocale: targetLocale
|
|
161
|
+
});
|
|
162
|
+
// Merge translated data with existing, preserving excluded fields
|
|
163
|
+
const finalData = translatedData;
|
|
164
|
+
if (existingDoc && allExcludedPaths.length > 0) {
|
|
165
|
+
// Preserve excluded fields from existing document
|
|
166
|
+
for (const excludedPath of allExcludedPaths){
|
|
167
|
+
const existingValue = getNestedValue(existingDoc, excludedPath);
|
|
168
|
+
if (existingValue !== undefined) {
|
|
169
|
+
setNestedValue(finalData, excludedPath, existingValue);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Update the document in the target locale
|
|
174
|
+
await req.payload.update({
|
|
175
|
+
id: doc.id,
|
|
176
|
+
collection: collectionSlug,
|
|
177
|
+
data: finalData,
|
|
178
|
+
locale: targetLocale,
|
|
179
|
+
// Prevent infinite loop - don't trigger hooks
|
|
180
|
+
context: {
|
|
181
|
+
skipAutoTranslate: true
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
if (pluginOptions.debugging) {
|
|
185
|
+
req.payload.logger.info(`[Auto-Translate Plugin] Successfully translated ${collectionSlug}:${doc.id} to ${targetLocale}`);
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
req.payload.logger.error(`[Auto-Translate Plugin] Error translating to ${targetLocale}:`, error);
|
|
189
|
+
// Continue with other locales even if one fails
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return doc;
|
|
193
|
+
});
|
|
194
|
+
// Prevent infinite loops - skip translation if triggered by our own update
|
|
195
|
+
const originalAfterChangeHooks = [
|
|
196
|
+
...collection.hooks.afterChange || []
|
|
197
|
+
];
|
|
198
|
+
collection.hooks.afterChange = [
|
|
199
|
+
async (args)=>{
|
|
200
|
+
// Skip if this update was triggered by auto-translate
|
|
201
|
+
if (args.context?.skipAutoTranslate) {
|
|
202
|
+
return args.doc;
|
|
203
|
+
}
|
|
204
|
+
// Run all hooks including translation
|
|
205
|
+
for (const hook of originalAfterChangeHooks){
|
|
206
|
+
const result = await hook(args);
|
|
207
|
+
if (result !== undefined) {
|
|
208
|
+
args.doc = result;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return args.doc;
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
if (pluginOptions.debugging) {
|
|
215
|
+
console.log(`[Auto-Translate Plugin] Configured collection: ${collectionSlug}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* If the plugin is disabled, we still want to keep added collections/fields
|
|
221
|
+
* so the database schema is consistent which is important for migrations.
|
|
222
|
+
*/ if (pluginOptions.disabled) {
|
|
223
|
+
return config;
|
|
224
|
+
}
|
|
225
|
+
return config;
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Helper function to get nested value from object using dot notation
|
|
229
|
+
*/ function getNestedValue(obj, path) {
|
|
230
|
+
return path.split('.').reduce((current, part)=>{
|
|
231
|
+
if (current === null || current === undefined) {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
return current[part];
|
|
235
|
+
}, obj);
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Helper function to set nested value in object using dot notation
|
|
239
|
+
*/ function setNestedValue(obj, path, value) {
|
|
240
|
+
const parts = path.split('.');
|
|
241
|
+
let current = obj;
|
|
242
|
+
for(let i = 0; i < parts.length - 1; i++){
|
|
243
|
+
const part = parts[i];
|
|
244
|
+
if (!(part in current) || current[part] === null || typeof current[part] !== 'object') {
|
|
245
|
+
// Check if next part is a number (array index)
|
|
246
|
+
const nextPart = parts[i + 1];
|
|
247
|
+
current[part] = /^\d+$/.test(nextPart) ? [] : {};
|
|
248
|
+
}
|
|
249
|
+
current = current[part];
|
|
250
|
+
}
|
|
251
|
+
current[parts[parts.length - 1]] = value;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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 * from './types/index.js'\nexport { getTranslationExclusionsCollection } from './collections/translationExclusions.js'\nexport { getTranslationSettingsGlobal } from './globals/translationSettings.js'\n\nexport const autoTranslate =\n (pluginOptions: AutoTranslateConfig) =>\n (config: Config): Config => {\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.afterChange) {\n collection.hooks.afterChange = []\n }\n\n // Main translation hook\n collection.hooks.afterChange.push(async ({ doc, operation, previousDoc, req }) => {\n // Only process create and update operations\n if (operation !== 'create' && operation !== 'update') {\n return doc\n }\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 doc\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 doc\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 doc\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,\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 })\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 req.payload.logger.error(\n `[Auto-Translate Plugin] Error translating to ${targetLocale}:`,\n error,\n )\n // Continue with other locales even if one fails\n }\n }\n\n return doc\n })\n\n // Prevent infinite loops - skip translation if triggered by our own update\n const originalAfterChangeHooks = [...(collection.hooks.afterChange || [])]\n collection.hooks.afterChange = [\n async (args) => {\n // Skip if this update was triggered by auto-translate\n if (args.context?.skipAutoTranslate) {\n return args.doc\n }\n\n // Run all hooks including translation\n for (const hook of originalAfterChangeHooks) {\n const result = await hook(args)\n if (result !== undefined) {\n args.doc = result\n }\n }\n\n return args.doc\n },\n ]\n\n if (pluginOptions.debugging) {\n console.log(`[Auto-Translate Plugin] Configured collection: ${collectionSlug}`)\n }\n }\n }\n\n /**\n * If the plugin is disabled, we still want to keep added collections/fields\n * so the database schema is consistent which is important for migrations.\n */\n if (pluginOptions.disabled) {\n return config\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","collections","localization","console","warn","localizationConfig","defaultLocale","allLocales","Array","isArray","locales","map","l","code","enableExclusions","debugging","log","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","afterChange","doc","operation","previousDoc","req","locale","payload","logger","info","_status","translationSync","id","secondaryLocales","filter","targetLocale","excludedPaths","getExclusions","configExcludedFields","getConfigExcludedFields","allExcludedPaths","length","join","existingDoc","existingResult","findByID","fallbackLocale","error","translatedData","translate","data","fromLocale","toLocale","finalData","excludedPath","existingValue","getNestedValue","undefined","setNestedValue","update","context","skipAutoTranslate","originalAfterChangeHooks","args","hook","result","disabled","obj","path","split","reduce","current","part","value","parts","i","nextPart","test"],"mappings":"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"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Payload } from 'payload';
|
|
2
|
+
import type { AutoTranslateConfig, TranslateOptions } from '../types/index.js';
|
|
3
|
+
export declare class TranslationService {
|
|
4
|
+
private client?;
|
|
5
|
+
private config;
|
|
6
|
+
constructor(config: AutoTranslateConfig);
|
|
7
|
+
/**
|
|
8
|
+
* Extracts translatable text from lexical editor nodes
|
|
9
|
+
*/
|
|
10
|
+
private extractFromLexicalNode;
|
|
11
|
+
/**
|
|
12
|
+
* Extracts translatable strings from data structure
|
|
13
|
+
* Returns a map of paths to translatable values and metadata for reconstruction
|
|
14
|
+
*/
|
|
15
|
+
private extractTranslatableStrings;
|
|
16
|
+
/**
|
|
17
|
+
* Lazily initialize OpenAI client only when needed
|
|
18
|
+
*/
|
|
19
|
+
private getOpenAIClient;
|
|
20
|
+
/**
|
|
21
|
+
* Gets the original value at a path in metadata (helper for deduplication)
|
|
22
|
+
*/
|
|
23
|
+
private getOriginalValue;
|
|
24
|
+
/**
|
|
25
|
+
* Checks if an object is a lexical editor node
|
|
26
|
+
*/
|
|
27
|
+
private isLexicalEditorNode;
|
|
28
|
+
/**
|
|
29
|
+
* Reconstructs data with translated strings, applying deduplicated translations
|
|
30
|
+
*/
|
|
31
|
+
private reconstructWithTranslations;
|
|
32
|
+
/**
|
|
33
|
+
* Determines if a string should be skipped from translation
|
|
34
|
+
*/
|
|
35
|
+
private shouldSkipString;
|
|
36
|
+
/**
|
|
37
|
+
* Translates using OpenAI API (optimized version)
|
|
38
|
+
*/
|
|
39
|
+
private translateWithOpenAI;
|
|
40
|
+
/**
|
|
41
|
+
* Legacy translation method (sends entire structure)
|
|
42
|
+
*/
|
|
43
|
+
private translateWithOpenAILegacy;
|
|
44
|
+
/**
|
|
45
|
+
* Gets global and collection-specific excluded fields
|
|
46
|
+
*/
|
|
47
|
+
getConfigExcludedFields(collection: string): string[];
|
|
48
|
+
/**
|
|
49
|
+
* Gets translation exclusions for a document
|
|
50
|
+
*/
|
|
51
|
+
getExclusions(payload: Payload, collection: string, documentId: string, locale: string): Promise<string[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Main translation method
|
|
54
|
+
*/
|
|
55
|
+
translate(options: TranslateOptions): Promise<any>;
|
|
56
|
+
/**
|
|
57
|
+
* Updates translation exclusions for a document
|
|
58
|
+
*/
|
|
59
|
+
updateExclusions(payload: Payload, collection: string, documentId: string, locale: string, excludedPaths: string[]): Promise<void>;
|
|
60
|
+
}
|