Last active
October 28, 2025 21:19
-
-
Save elliott-w/9e9ed3df05e12f8626e33c88477252b5 to your computer and use it in GitHub Desktop.
Payload CMS Require Translations Plugin - Prevents publishing until all translations have been added through draft autosaves
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { | |
| traverseFields, | |
| type CollectionConfig, | |
| type CollectionSlug, | |
| type GlobalConfig, | |
| type GlobalSlug, | |
| type Plugin, | |
| type TraverseFieldsCallback, | |
| type Validate, | |
| } from 'payload' | |
| import { hasText } from '@payloadcms/richtext-lexical/shared' | |
| const getValueByPath = (obj: any, path: (string | number)[]) => { | |
| return path.reduce((current, key) => current?.[key], obj) | |
| } | |
| export const validateAllLocales = | |
| (originalValidate?: Validate): Validate => | |
| async (_, options) => { | |
| if (originalValidate) { | |
| const result = await originalValidate(_, options) | |
| if (typeof result === 'string') { | |
| return result | |
| } | |
| } | |
| const { req, data, path, collectionSlug, required } = options | |
| const type = 'type' in options ? (options.type as string) : undefined | |
| const localization = req.payload.config.localization | |
| if (data && localization && collectionSlug && required) { | |
| if (data._status === 'published') { | |
| if (!req.context.draftDocPromise) { | |
| req.context.draftDocPromise = req.payload.findByID({ | |
| id: data.id, | |
| collection: collectionSlug as CollectionSlug, | |
| draft: true, | |
| locale: 'all', | |
| }) | |
| } | |
| const doc = (await req.context.draftDocPromise) as any | |
| const draftValue = getValueByPath(doc, path) | |
| if (typeof draftValue === 'object' && draftValue !== null) { | |
| const missingLocales = localization.locales.filter(locale => { | |
| if (type === 'richText') { | |
| return !(locale.code in draftValue && hasText(draftValue[locale.code])) | |
| } | |
| if (!(locale.code in draftValue)) { | |
| return true | |
| } | |
| const value = draftValue[locale.code] | |
| return value === null || value === '' || value === undefined | |
| }) | |
| if (missingLocales.length > 0) { | |
| return `This field is required in ${missingLocales.map(locale => locale.label).join(', ')}` | |
| } | |
| } else if (!draftValue) { | |
| return 'This field is required' | |
| } | |
| } | |
| } | |
| return true | |
| } | |
| interface RequireTranslationsPluginArgs { | |
| /** | |
| * Whether to enable the plugin. | |
| * @default true | |
| */ | |
| enable?: boolean | |
| collectionSlugs?: CollectionSlug[] | |
| globalSlugs?: GlobalSlug[] | |
| } | |
| // Main plugin function | |
| export const requireTranslationsPlugin = ({ | |
| enable = true, | |
| collectionSlugs = [], | |
| globalSlugs = [], | |
| }: RequireTranslationsPluginArgs = {}): Plugin => { | |
| return config => { | |
| if (!enable) { | |
| return config | |
| } | |
| const addValidateFunctionsCallback: TraverseFieldsCallback = ({ field }) => { | |
| switch (field.type) { | |
| case 'richText': | |
| case 'text': | |
| case 'textarea': | |
| case 'number': | |
| case 'select': | |
| if (field.localized) { | |
| const originalValidate = field.validate | |
| field.validate = validateAllLocales(originalValidate as Validate) | |
| } | |
| break | |
| default: | |
| break | |
| } | |
| } | |
| // Process collections to add field-level hooks | |
| ;(config.collections || []).forEach((collection: CollectionConfig) => { | |
| if (collectionSlugs.includes(collection.slug as CollectionSlug)) { | |
| traverseFields({ | |
| fields: collection.fields, | |
| callback: addValidateFunctionsCallback, | |
| }) | |
| } | |
| }) | |
| // Process globals to add field-level hooks | |
| config.globals = (config.globals || []).map((global: GlobalConfig) => { | |
| if (globalSlugs.includes(global.slug as GlobalSlug)) { | |
| traverseFields({ | |
| fields: global.fields, | |
| callback: addValidateFunctionsCallback, | |
| }) | |
| } | |
| return global | |
| }) | |
| return config | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment