@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/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Team Pigment
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
# Payload CMS Auto-Translate Plugin
|
|
2
|
+
|
|
3
|
+
A powerful auto-translation plugin for [Payload CMS](https://payloadcms.com) that automatically translates content from your default language to secondary languages with field-level translation control.
|
|
4
|
+
|
|
5
|
+
## ✨ Features
|
|
6
|
+
|
|
7
|
+
- 🌍 **One-Way Auto-Translation**: Automatically translates content from default language to secondary languages
|
|
8
|
+
- 🔒 **Field-Level Exclusion**: Toggle "do not translate" on specific fields in secondary languages
|
|
9
|
+
- 🎯 **Smart Translation**: Preserves excluded fields when updating default language content
|
|
10
|
+
- 🔧 **Flexible Configuration**: Configure per-collection settings and global exclusions
|
|
11
|
+
- 🤖 **OpenAI Integration**: Uses GPT-4o for high-quality translations (with custom provider support)
|
|
12
|
+
- 📦 **Zero UI Overhead**: Seamlessly integrates with Payload's admin panel
|
|
13
|
+
- ⚡ **Performance Optimized**: 10-15x faster translation with smart extraction and deduplication
|
|
14
|
+
|
|
15
|
+
## 📋 How It Works
|
|
16
|
+
|
|
17
|
+
1. Create or edit a post in your **default language** (e.g., Swedish)
|
|
18
|
+
2. Save the document - it automatically translates to all secondary languages (e.g., English)
|
|
19
|
+
3. Switch to a secondary language and mark specific fields as "translation locked" 🔒
|
|
20
|
+
4. Future updates to those fields in the default language won't overwrite your custom translations
|
|
21
|
+
|
|
22
|
+
## 🚀 Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install auto-translate
|
|
26
|
+
# or
|
|
27
|
+
pnpm add auto-translate
|
|
28
|
+
# or
|
|
29
|
+
yarn add auto-translate
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## ⚙️ Configuration
|
|
33
|
+
|
|
34
|
+
### Basic Setup
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
import { autoTranslate } from 'auto-translate'
|
|
38
|
+
import { buildConfig } from 'payload'
|
|
39
|
+
|
|
40
|
+
export default buildConfig({
|
|
41
|
+
// Your localization config is required
|
|
42
|
+
localization: {
|
|
43
|
+
defaultLocale: 'sv',
|
|
44
|
+
locales: ['sv', 'en', 'de', 'fr'],
|
|
45
|
+
fallback: true,
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
collections: [
|
|
49
|
+
// Your collections...
|
|
50
|
+
],
|
|
51
|
+
|
|
52
|
+
plugins: [
|
|
53
|
+
autoTranslate({
|
|
54
|
+
collections: {
|
|
55
|
+
posts: true,
|
|
56
|
+
pages: true,
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
],
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Environment Variables
|
|
64
|
+
|
|
65
|
+
Create a `.env` file in your project root:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Required for OpenAI translation
|
|
69
|
+
OPENAI_API_KEY=your-openai-api-key
|
|
70
|
+
|
|
71
|
+
# Optional: Custom OpenAI endpoint
|
|
72
|
+
OPENAI_BASE_URL=https://api.openai.com/v1
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
> **⚠️ Important:** Restart your server after updating `.env` or plugin settings.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 🔧 Advanced Configuration
|
|
80
|
+
|
|
81
|
+
### Full Configuration Options
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { autoTranslate } from 'auto-translate'
|
|
85
|
+
|
|
86
|
+
export default buildConfig({
|
|
87
|
+
plugins: [
|
|
88
|
+
autoTranslate({
|
|
89
|
+
// Enable auto-translate for specific collections
|
|
90
|
+
collections: {
|
|
91
|
+
posts: true,
|
|
92
|
+
pages: {
|
|
93
|
+
enabled: true,
|
|
94
|
+
// Exclude specific fields from translation in this collection
|
|
95
|
+
excludeFields: ['slug', 'author'],
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
// Optional: Show debug logs
|
|
100
|
+
debugging: false,
|
|
101
|
+
|
|
102
|
+
// Optional: Translation provider settings
|
|
103
|
+
provider: {
|
|
104
|
+
type: 'openai', // or 'custom'
|
|
105
|
+
model: 'gpt-4o', // OpenAI model to use
|
|
106
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
107
|
+
baseURL: process.env.OPENAI_BASE_URL,
|
|
108
|
+
|
|
109
|
+
// Optional: Custom translation function
|
|
110
|
+
customTranslate: async ({ data, fromLocale, toLocale }) => {
|
|
111
|
+
// Your custom translation logic
|
|
112
|
+
return translatedData
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Optional: Global field exclusions (across all collections)
|
|
117
|
+
excludeFields: ['slug', 'id', 'createdAt', 'updatedAt'],
|
|
118
|
+
|
|
119
|
+
// Optional: Enable translation sync by default
|
|
120
|
+
enableTranslationSyncByDefault: true,
|
|
121
|
+
|
|
122
|
+
// Optional: Custom collection slug for metadata
|
|
123
|
+
translationExclusionsSlug: 'translation-exclusions',
|
|
124
|
+
|
|
125
|
+
// Optional: Disable field-level exclusions entirely (default: true)
|
|
126
|
+
// When disabled: no exclusion UI, no exclusion collection
|
|
127
|
+
enableExclusions: true,
|
|
128
|
+
}),
|
|
129
|
+
],
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Per-Collection Configuration
|
|
134
|
+
|
|
135
|
+
You can configure translation behavior per collection:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
collections: {
|
|
139
|
+
// Simple enable
|
|
140
|
+
posts: true,
|
|
141
|
+
|
|
142
|
+
// Disable translation
|
|
143
|
+
drafts: false,
|
|
144
|
+
|
|
145
|
+
// Advanced configuration
|
|
146
|
+
pages: {
|
|
147
|
+
enabled: true,
|
|
148
|
+
excludeFields: ['slug', 'author', 'seo.keywords'],
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Disabling Field-Level Exclusions
|
|
154
|
+
|
|
155
|
+
For simpler setups, you can disable the field-level exclusion system entirely:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
autoTranslate({
|
|
159
|
+
collections: {
|
|
160
|
+
posts: true,
|
|
161
|
+
pages: true,
|
|
162
|
+
},
|
|
163
|
+
enableExclusions: false, // Disable field-level locking
|
|
164
|
+
excludeFields: ['slug'], // Global exclusions still work
|
|
165
|
+
})
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**When disabled**:
|
|
169
|
+
- ❌ No translation exclusions collection
|
|
170
|
+
- ❌ No 🌐/🔒 buttons on fields
|
|
171
|
+
- ✅ All localized fields are always translated
|
|
172
|
+
- ✅ Global/collection `excludeFields` still work
|
|
173
|
+
|
|
174
|
+
### Custom Translation Provider
|
|
175
|
+
|
|
176
|
+
If you don't want to use OpenAI, you can provide your own translation function:
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
provider: {
|
|
180
|
+
type: 'custom',
|
|
181
|
+
customTranslate: async ({ data, fromLocale, toLocale, payload, collection }) => {
|
|
182
|
+
// Use Google Translate, DeepL, or any other service
|
|
183
|
+
const translated = await yourTranslationService.translate(data, {
|
|
184
|
+
from: fromLocale,
|
|
185
|
+
to: toLocale,
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
return translated
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 📚 Usage Guide
|
|
196
|
+
|
|
197
|
+
### 1. Creating Content
|
|
198
|
+
|
|
199
|
+
1. Create a new document in your **default language**
|
|
200
|
+
2. Fill in all the fields with content
|
|
201
|
+
3. Save the document
|
|
202
|
+
4. ✨ The plugin automatically translates and saves the content in all secondary languages
|
|
203
|
+
|
|
204
|
+
### 2. Locking Fields from Translation
|
|
205
|
+
|
|
206
|
+
Sometimes you want to customize translations in secondary languages without them being overwritten:
|
|
207
|
+
|
|
208
|
+
1. Switch to a **secondary language** (e.g., English)
|
|
209
|
+
2. Find the field you want to customize
|
|
210
|
+
3. Click the **🔒 Translation lock** button (or **🌐 Auto-translate** to unlock)
|
|
211
|
+
4. Edit the field with your custom translation
|
|
212
|
+
5. Save the document
|
|
213
|
+
|
|
214
|
+
Now when you update that field in the default language, it won't overwrite your custom translation in the secondary language!
|
|
215
|
+
|
|
216
|
+
### 3. Managing Translation Sync
|
|
217
|
+
|
|
218
|
+
Each document has a **"Enable Auto-Translation"** checkbox in the sidebar:
|
|
219
|
+
|
|
220
|
+
- ✅ **Enabled** (default): Changes in default language automatically translate
|
|
221
|
+
- ❌ **Disabled**: No automatic translation occurs
|
|
222
|
+
|
|
223
|
+
### 4. Field-Level Control
|
|
224
|
+
|
|
225
|
+
The plugin tracks field exclusions per document, per locale, per field path. This means you can:
|
|
226
|
+
|
|
227
|
+
- Lock translation for `title` in English but keep it unlocked in German
|
|
228
|
+
- Lock a specific block item (e.g., `content.0.description`) in one language
|
|
229
|
+
- Lock nested fields like `seo.meta.description`
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 🏗️ How Translation Works
|
|
234
|
+
|
|
235
|
+
### Translation Flow
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
1. User edits document in DEFAULT language (e.g., Swedish)
|
|
239
|
+
↓
|
|
240
|
+
2. User saves/publishes document
|
|
241
|
+
↓
|
|
242
|
+
3. Plugin checks if translationSync is enabled
|
|
243
|
+
↓
|
|
244
|
+
4. Plugin checks if document is published (skips drafts/autosaves)
|
|
245
|
+
↓
|
|
246
|
+
5. For each SECONDARY language (e.g., English, German):
|
|
247
|
+
a. Fetch field-level exclusions for that language
|
|
248
|
+
b. Remove excluded fields from translation payload
|
|
249
|
+
c. Translate remaining fields using OpenAI
|
|
250
|
+
d. Merge translated data with existing, preserving excluded fields
|
|
251
|
+
e. Save translated document in that language
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
> **📝 Note**: When using Payload's drafts feature with autosave enabled, translations only trigger when you explicitly **publish** the document, not during autosave operations. This prevents unnecessary translation costs and API calls.
|
|
255
|
+
|
|
256
|
+
### Field Exclusion Logic
|
|
257
|
+
|
|
258
|
+
- **Global exclusions**: Applied to all collections (configured in plugin options)
|
|
259
|
+
- **Collection exclusions**: Applied to specific collections (configured per collection)
|
|
260
|
+
- **Field-level exclusions**: Set by users via UI (stored in `translation-exclusions` collection)
|
|
261
|
+
- **Auto-excluded**: `id`, `_id`, `createdAt`, `updatedAt`, `translationSync`, `__v`
|
|
262
|
+
|
|
263
|
+
### Nested Field Support
|
|
264
|
+
|
|
265
|
+
The plugin fully supports nested and complex field structures:
|
|
266
|
+
|
|
267
|
+
- **Objects**: `meta.description` ✅
|
|
268
|
+
- **Arrays**: `content.0.title` ✅
|
|
269
|
+
- **Blocks**: `layout.0.heading` ✅
|
|
270
|
+
- **Rich Text**: Translates rich text content ✅
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## 🎨 UI Components
|
|
275
|
+
|
|
276
|
+
The plugin adds minimal UI elements to your admin panel:
|
|
277
|
+
|
|
278
|
+
### Translation Sync Toggle
|
|
279
|
+
|
|
280
|
+
- **Location**: Document sidebar
|
|
281
|
+
- **Purpose**: Enable/disable auto-translation for the entire document
|
|
282
|
+
|
|
283
|
+
### Translation Control Button (per field)
|
|
284
|
+
|
|
285
|
+
- **Location**: Near each translatable field (in secondary languages only)
|
|
286
|
+
- **Purpose**: Lock/unlock translation for specific fields
|
|
287
|
+
- **States**:
|
|
288
|
+
- 🌐 **Auto-translate**: Field will be updated from default language
|
|
289
|
+
- 🔒 **Translation locked**: Field won't be overwritten from default language
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## 🔍 Debugging
|
|
294
|
+
|
|
295
|
+
Enable debugging to see detailed logs:
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
autoTranslate({
|
|
299
|
+
debugging: true,
|
|
300
|
+
// ... other options
|
|
301
|
+
})
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
This will log:
|
|
305
|
+
|
|
306
|
+
- Configuration on startup
|
|
307
|
+
- Translation triggers
|
|
308
|
+
- Excluded paths for each translation
|
|
309
|
+
- Translation success/failure messages
|
|
310
|
+
- API calls and responses
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## 📊 Architecture
|
|
315
|
+
|
|
316
|
+
The plugin is built with a clean, modular architecture inspired by [payload-ai](https://github.com/ashbuilds/payload-ai):
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
src/
|
|
320
|
+
├── index.ts # Main plugin entry point
|
|
321
|
+
├── types/ # TypeScript type definitions
|
|
322
|
+
├── collections/ # Translation metadata collection
|
|
323
|
+
├── services/ # Translation service layer
|
|
324
|
+
│ └── translationService.ts # Handles translation logic
|
|
325
|
+
├── utilities/ # Helper functions
|
|
326
|
+
│ └── fieldHelpers.ts # Field traversal & merging
|
|
327
|
+
├── components/ # UI components
|
|
328
|
+
│ └── TranslationControl.tsx # Field-level control button
|
|
329
|
+
└── endpoints/ # Custom API endpoints
|
|
330
|
+
└── translationExclusionsEndpoint.ts
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
---
|
|
334
|
+
|
|
335
|
+
## ⚡ Performance
|
|
336
|
+
|
|
337
|
+
The plugin includes intelligent optimizations that make translation 10-15x faster for large documents:
|
|
338
|
+
|
|
339
|
+
- **Smart String Extraction**: Only translatable content is sent to the API (80-95% size reduction)
|
|
340
|
+
- **Deduplication**: Identical strings are translated once and reused
|
|
341
|
+
- **Lexical Editor Support**: Special handling for rich text fields
|
|
342
|
+
|
|
343
|
+
For large documents with Lexical editor content, translation time can be reduced from 30-120 seconds to just 3-10 seconds.
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## 🧪 Testing
|
|
348
|
+
|
|
349
|
+
Run the test suite:
|
|
350
|
+
|
|
351
|
+
```bash
|
|
352
|
+
pnpm test # Run all tests
|
|
353
|
+
pnpm test:int # Run integration tests
|
|
354
|
+
pnpm test:e2e # Run end-to-end tests
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## 🔒 Access Control
|
|
360
|
+
|
|
361
|
+
The plugin respects Payload's access control. Translation operations run with the same permissions as the user making the update.
|
|
362
|
+
|
|
363
|
+
If you need custom access control for translation features, you can add hooks or modify the endpoints.
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## 🚧 Limitations
|
|
368
|
+
|
|
369
|
+
- **One-way translation only**: Default language → Secondary languages (not vice versa)
|
|
370
|
+
- **No real-time translation**: Translation happens on save, not as you type
|
|
371
|
+
- **OpenAI dependency**: Requires OpenAI API key (unless using custom provider)
|
|
372
|
+
- **Cost considerations**: Each translation uses OpenAI API credits
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## 🤝 Contributing
|
|
377
|
+
|
|
378
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## 📝 License
|
|
383
|
+
|
|
384
|
+
MIT
|
|
385
|
+
|
|
386
|
+
---
|
|
387
|
+
|
|
388
|
+
## 🙏 Acknowledgments
|
|
389
|
+
|
|
390
|
+
Inspired by the excellent [payload-ai plugin](https://github.com/ashbuilds/payload-ai) by [@ashbuilds](https://github.com/ashbuilds).
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## 📞 Support
|
|
395
|
+
|
|
396
|
+
If you have questions or need help:
|
|
397
|
+
|
|
398
|
+
1. Check the [Payload CMS documentation](https://payloadcms.com/docs)
|
|
399
|
+
2. Open an issue on [GitHub](https://github.com/pigment-se/auto-translate/issues)
|
|
400
|
+
3. Join the [Payload Discord](https://discord.gg/payload)
|
|
401
|
+
4. Visit [pigment.se](https://pigment.se)
|
|
402
|
+
|
|
403
|
+
---
|
|
404
|
+
|
|
405
|
+
**Made with ❤️ by Team Pigment for the Payload CMS community**
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export const getTranslationExclusionsCollection = (slug = 'translation-exclusions')=>({
|
|
2
|
+
slug,
|
|
3
|
+
access: {
|
|
4
|
+
create: ()=>true,
|
|
5
|
+
delete: ()=>true,
|
|
6
|
+
read: ()=>true,
|
|
7
|
+
update: ()=>true
|
|
8
|
+
},
|
|
9
|
+
admin: {
|
|
10
|
+
defaultColumns: [
|
|
11
|
+
'collection',
|
|
12
|
+
'documentId',
|
|
13
|
+
'locale',
|
|
14
|
+
'excludedPaths'
|
|
15
|
+
],
|
|
16
|
+
description: 'Stores field-level translation exclusions per document and locale. Each locale can have its own set of excluded fields. There should only be ONE record per (collection, documentId, locale) combination.',
|
|
17
|
+
group: 'Settings',
|
|
18
|
+
useAsTitle: 'collection'
|
|
19
|
+
},
|
|
20
|
+
fields: [
|
|
21
|
+
{
|
|
22
|
+
name: 'collection',
|
|
23
|
+
type: 'text',
|
|
24
|
+
admin: {
|
|
25
|
+
description: 'The collection this exclusion belongs to',
|
|
26
|
+
position: 'sidebar',
|
|
27
|
+
readOnly: true
|
|
28
|
+
},
|
|
29
|
+
index: true,
|
|
30
|
+
required: true
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'documentId',
|
|
34
|
+
type: 'text',
|
|
35
|
+
admin: {
|
|
36
|
+
description: 'The ID of the document',
|
|
37
|
+
position: 'sidebar',
|
|
38
|
+
readOnly: true
|
|
39
|
+
},
|
|
40
|
+
index: true,
|
|
41
|
+
required: true
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: 'locale',
|
|
45
|
+
type: 'text',
|
|
46
|
+
admin: {
|
|
47
|
+
description: 'The locale these exclusions apply to (e.g., "en", "de", "fr")',
|
|
48
|
+
position: 'sidebar',
|
|
49
|
+
readOnly: true
|
|
50
|
+
},
|
|
51
|
+
index: true,
|
|
52
|
+
label: 'Locale',
|
|
53
|
+
required: true
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'excludedPaths',
|
|
57
|
+
type: 'array',
|
|
58
|
+
admin: {
|
|
59
|
+
description: 'Fields that should NOT be auto-translated in this specific locale. Each locale has its own independent set of exclusions.'
|
|
60
|
+
},
|
|
61
|
+
fields: [
|
|
62
|
+
{
|
|
63
|
+
name: 'path',
|
|
64
|
+
type: 'text',
|
|
65
|
+
admin: {
|
|
66
|
+
description: 'Field path (e.g., "title", "content.0.description")'
|
|
67
|
+
},
|
|
68
|
+
label: 'Field Path',
|
|
69
|
+
required: true
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
label: `Excluded Fields for this Locale`,
|
|
73
|
+
required: true
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
//# sourceMappingURL=translationExclusions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/collections/translationExclusions.ts"],"sourcesContent":["import type { CollectionConfig } from 'payload'\n\nexport const getTranslationExclusionsCollection = (\n slug: string = 'translation-exclusions',\n): CollectionConfig => ({\n slug,\n access: {\n create: () => true,\n delete: () => true,\n read: () => true,\n update: () => true,\n },\n admin: {\n defaultColumns: ['collection', 'documentId', 'locale', 'excludedPaths'],\n description:\n 'Stores field-level translation exclusions per document and locale. Each locale can have its own set of excluded fields. There should only be ONE record per (collection, documentId, locale) combination.',\n group: 'Settings',\n useAsTitle: 'collection',\n },\n fields: [\n {\n name: 'collection',\n type: 'text',\n admin: {\n description: 'The collection this exclusion belongs to',\n position: 'sidebar',\n readOnly: true,\n },\n index: true,\n required: true,\n },\n {\n name: 'documentId',\n type: 'text',\n admin: {\n description: 'The ID of the document',\n position: 'sidebar',\n readOnly: true,\n },\n index: true,\n required: true,\n },\n {\n name: 'locale',\n type: 'text',\n admin: {\n description: 'The locale these exclusions apply to (e.g., \"en\", \"de\", \"fr\")',\n position: 'sidebar',\n readOnly: true,\n },\n index: true,\n label: 'Locale',\n required: true,\n },\n {\n name: 'excludedPaths',\n type: 'array',\n admin: {\n description:\n 'Fields that should NOT be auto-translated in this specific locale. Each locale has its own independent set of exclusions.',\n },\n fields: [\n {\n name: 'path',\n type: 'text',\n admin: {\n description: 'Field path (e.g., \"title\", \"content.0.description\")',\n },\n label: 'Field Path',\n required: true,\n },\n ],\n label: `Excluded Fields for this Locale`,\n required: true,\n },\n ],\n})\n"],"names":["getTranslationExclusionsCollection","slug","access","create","delete","read","update","admin","defaultColumns","description","group","useAsTitle","fields","name","type","position","readOnly","index","required","label"],"mappings":"AAEA,OAAO,MAAMA,qCAAqC,CAChDC,OAAe,wBAAwB,GACjB,CAAA;QACtBA;QACAC,QAAQ;YACNC,QAAQ,IAAM;YACdC,QAAQ,IAAM;YACdC,MAAM,IAAM;YACZC,QAAQ,IAAM;QAChB;QACAC,OAAO;YACLC,gBAAgB;gBAAC;gBAAc;gBAAc;gBAAU;aAAgB;YACvEC,aACE;YACFC,OAAO;YACPC,YAAY;QACd;QACAC,QAAQ;YACN;gBACEC,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLE,aAAa;oBACbM,UAAU;oBACVC,UAAU;gBACZ;gBACAC,OAAO;gBACPC,UAAU;YACZ;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLE,aAAa;oBACbM,UAAU;oBACVC,UAAU;gBACZ;gBACAC,OAAO;gBACPC,UAAU;YACZ;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLE,aAAa;oBACbM,UAAU;oBACVC,UAAU;gBACZ;gBACAC,OAAO;gBACPE,OAAO;gBACPD,UAAU;YACZ;YACA;gBACEL,MAAM;gBACNC,MAAM;gBACNP,OAAO;oBACLE,aACE;gBACJ;gBACAG,QAAQ;oBACN;wBACEC,MAAM;wBACNC,MAAM;wBACNP,OAAO;4BACLE,aAAa;wBACf;wBACAU,OAAO;wBACPD,UAAU;oBACZ;iBACD;gBACDC,OAAO,CAAC,+BAA+B,CAAC;gBACxCD,UAAU;YACZ;SACD;IACH,CAAA,EAAE"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
.translation-control {
|
|
2
|
+
margin-top: 0.75rem;
|
|
3
|
+
opacity: 0;
|
|
4
|
+
transform: translateY(-4px);
|
|
5
|
+
transition:
|
|
6
|
+
opacity 0.2s ease-in-out,
|
|
7
|
+
transform 0.2s ease-in-out;
|
|
8
|
+
pointer-events: none;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/* Show control when parent field is focused or hovered */
|
|
12
|
+
.field-type:focus-within .translation-control,
|
|
13
|
+
.field-type:hover .translation-control,
|
|
14
|
+
.translation-control.is-excluded {
|
|
15
|
+
opacity: 1;
|
|
16
|
+
transform: translateY(0);
|
|
17
|
+
pointer-events: auto;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* Keep excluded/locked fields always slightly visible for better awareness */
|
|
21
|
+
.translation-control.is-excluded {
|
|
22
|
+
opacity: 0.7;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.translation-control.is-excluded:hover {
|
|
26
|
+
opacity: 1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.translation-control__button {
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
gap: 0.5rem;
|
|
33
|
+
padding: 0.375rem 0.75rem;
|
|
34
|
+
background: var(--theme-elevation-0, #ffffff);
|
|
35
|
+
border: 1px solid var(--theme-elevation-300, #d1d5db);
|
|
36
|
+
border-radius: 6px;
|
|
37
|
+
cursor: pointer;
|
|
38
|
+
font-size: 0.813rem;
|
|
39
|
+
font-weight: 500;
|
|
40
|
+
color: var(--theme-text, #374151);
|
|
41
|
+
transition: all 0.15s ease;
|
|
42
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.translation-control__button:hover:not(:disabled) {
|
|
46
|
+
background: var(--theme-elevation-50, #f9fafb);
|
|
47
|
+
border-color: var(--theme-primary-500, #3b82f6);
|
|
48
|
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
49
|
+
transform: translateY(-1px);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.translation-control__button:active:not(:disabled) {
|
|
53
|
+
transform: translateY(0);
|
|
54
|
+
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.translation-control__button:disabled {
|
|
58
|
+
opacity: 0.5;
|
|
59
|
+
cursor: not-allowed;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* Excluded/Locked state styling */
|
|
63
|
+
.translation-control.is-excluded .translation-control__button {
|
|
64
|
+
background: var(--theme-warning-50, #fef3c7);
|
|
65
|
+
border-color: var(--theme-warning-400, #f59e0b);
|
|
66
|
+
color: var(--theme-warning-900, #92400e);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.translation-control.is-excluded .translation-control__button:hover:not(:disabled) {
|
|
70
|
+
background: var(--theme-warning-100, #fde68a);
|
|
71
|
+
border-color: var(--theme-warning-500, #f59e0b);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.translation-control__icon {
|
|
75
|
+
font-size: 1rem;
|
|
76
|
+
line-height: 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.translation-control__label {
|
|
80
|
+
line-height: 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.translation-control__status {
|
|
84
|
+
display: inline-block;
|
|
85
|
+
margin-left: 0.5rem;
|
|
86
|
+
padding: 0.25rem 0.5rem;
|
|
87
|
+
background: var(--theme-warning-50, #fef3c7);
|
|
88
|
+
border-radius: 4px;
|
|
89
|
+
font-size: 0.75rem;
|
|
90
|
+
color: var(--theme-warning-900, #92400e);
|
|
91
|
+
line-height: 1.4;
|
|
92
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import './TranslationControl.css';
|
|
3
|
+
type TranslationControlProps = {
|
|
4
|
+
collectionSlug?: string;
|
|
5
|
+
defaultLocale: string;
|
|
6
|
+
fieldPath?: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* UI component that allows users to toggle "do not translate" for specific fields
|
|
11
|
+
* Only shows on secondary locales (not the default locale)
|
|
12
|
+
*
|
|
13
|
+
* The component can receive the field path in two ways:
|
|
14
|
+
* 1. From Payload's `path` prop (preferred - includes runtime array/block indices)
|
|
15
|
+
* 2. From the `fieldPath` clientProp (fallback - static path from field definition)
|
|
16
|
+
*/
|
|
17
|
+
export declare const TranslationControl: React.FC<TranslationControlProps>;
|
|
18
|
+
export {};
|