@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.
Files changed (34) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +405 -0
  3. package/dist/collections/translationExclusions.d.ts +2 -0
  4. package/dist/collections/translationExclusions.js +78 -0
  5. package/dist/collections/translationExclusions.js.map +1 -0
  6. package/dist/components/TranslationControl.css +92 -0
  7. package/dist/components/TranslationControl.d.ts +18 -0
  8. package/dist/components/TranslationControl.js +274 -0
  9. package/dist/components/TranslationControl.js.map +1 -0
  10. package/dist/exports/client.d.ts +5 -0
  11. package/dist/exports/client.js +5 -0
  12. package/dist/exports/client.js.map +1 -0
  13. package/dist/exports/rsc.d.ts +5 -0
  14. package/dist/exports/rsc.js +5 -0
  15. package/dist/exports/rsc.js.map +1 -0
  16. package/dist/globals/translationSettings.d.ts +2 -0
  17. package/dist/globals/translationSettings.js +78 -0
  18. package/dist/globals/translationSettings.js.map +1 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +254 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/services/translationService.d.ts +60 -0
  23. package/dist/services/translationService.js +533 -0
  24. package/dist/services/translationService.js.map +1 -0
  25. package/dist/types/index.d.ts +103 -0
  26. package/dist/types/index.js +3 -0
  27. package/dist/types/index.js.map +1 -0
  28. package/dist/utilities/fieldHelpers.d.ts +34 -0
  29. package/dist/utilities/fieldHelpers.js +180 -0
  30. package/dist/utilities/fieldHelpers.js.map +1 -0
  31. package/dist/utilities/injectTranslationControls.d.ts +5 -0
  32. package/dist/utilities/injectTranslationControls.js +92 -0
  33. package/dist/utilities/injectTranslationControls.js.map +1 -0
  34. 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,2 @@
1
+ import type { CollectionConfig } from 'payload';
2
+ export declare const getTranslationExclusionsCollection: (slug?: string) => CollectionConfig;
@@ -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 {};