Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/payload/src/collections/endpoints/duplicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export const duplicateHandler: PayloadHandler = async (req) => {
const depth = searchParams.get('depth')
// draft defaults to true, unless explicitly set requested as false to prevent the newly duplicated document from being published
const draft = searchParams.get('draft') !== 'false'
const selectedLocales = (searchParams.get('selectedLocales') || '')
.replace(/^\[|\]$/g, '')
.split(',')
.map((s) => s.trim())

const doc = await duplicateOperation({
id,
Expand All @@ -26,6 +30,7 @@ export const duplicateHandler: PayloadHandler = async (req) => {
populate: sanitizePopulateParam(req.query.populate),
req,
select: sanitizeSelectParam(req.query.select),
selectedLocales,
})

const message = req.t('general:successfullyDuplicated', {
Expand Down
3 changes: 3 additions & 0 deletions packages/payload/src/collections/operations/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type Arguments<TSlug extends CollectionSlug> = {
publishSpecificLocale?: string
req: PayloadRequest
select?: SelectType
selectedLocales?: string[]
showHiddenFields?: boolean
}

Expand Down Expand Up @@ -113,6 +114,7 @@ export const createOperation = async <
},
req,
select: incomingSelect,
selectedLocales,
showHiddenFields,
} = args

Expand All @@ -130,6 +132,7 @@ export const createOperation = async <
draftArg: shouldSaveDraft,
overrideAccess,
req,
selectedLocales,
shouldSaveDraft,
})

Expand Down
2 changes: 2 additions & 0 deletions packages/payload/src/collections/operations/duplicate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { type Arguments as CreateArguments, createOperation } from './create.js'
export type Arguments<TSlug extends CollectionSlug> = {
data?: DeepPartial<RequiredDataFromCollectionSlug<TSlug>>
id: number | string
selectedLocales?: string[]
} & Omit<CreateArguments<TSlug>, 'data' | 'duplicateFromID'>

export const duplicateOperation = async <
Expand All @@ -22,5 +23,6 @@ export const duplicateOperation = async <
...args,
data: incomingArgs?.data || {},
duplicateFromID: id,
selectedLocales: incomingArgs.selectedLocales,
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ export type Options<TSlug extends CollectionSlug, TSelect extends SelectType> =
* Specify [select](https://payloadcms.com/docs/queries/select) to control which fields to include to the result.
*/
select?: TSelect
/**
* Specifies which locales to include when duplicating localized fields. Non-localized data is always duplicated.
* By default, all locales are duplicated.
*/
selectedLocales?: string[]
/**
* Opt-in to receiving hidden fields. By default, they are hidden from returned documents in accordance to your config.
* @default false
Expand Down Expand Up @@ -107,6 +112,7 @@ export async function duplicateLocal<
overrideAccess = true,
populate,
select,
selectedLocales,
showHiddenFields,
} = options

Expand Down Expand Up @@ -138,6 +144,7 @@ export async function duplicateLocal<
populate,
req,
select,
selectedLocales,
showHiddenFields,
})
}
8 changes: 8 additions & 0 deletions packages/payload/src/duplicateDocument/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { NotFound } from '../errors/NotFound.js'
import { afterRead } from '../fields/hooks/afterRead/index.js'
import { beforeDuplicate } from '../fields/hooks/beforeDuplicate/index.js'
import { deepCopyObjectSimple } from '../utilities/deepCopyObject.js'
import { filterLocales } from '../utilities/filterLocalizedData.js'
import { getLatestCollectionVersion } from '../versions/getLatestCollectionVersion.js'

type GetDuplicateDocumentArgs = {
Expand All @@ -18,6 +19,7 @@ type GetDuplicateDocumentArgs = {
id: number | string
overrideAccess?: boolean
req: PayloadRequest
selectedLocales?: string[]
shouldSaveDraft?: boolean
}
export const getDuplicateDocumentData = async ({
Expand All @@ -26,6 +28,7 @@ export const getDuplicateDocumentData = async ({
draftArg,
overrideAccess,
req,
selectedLocales,
shouldSaveDraft,
}: GetDuplicateDocumentArgs): Promise<{
duplicatedFromDoc: JsonObject
Expand Down Expand Up @@ -59,6 +62,11 @@ export const getDuplicateDocumentData = async ({
req,
})

if (selectedLocales && selectedLocales.length > 0 && duplicatedFromDocWithLocales) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should actually loop over the collection fields and make sure that we are only trimming data on fields that are localized.

Copy link
Member Author

@jessrynkar jessrynkar Oct 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make sure that we are only trimming data on fields that are localized.

This is the current behavior, it is being handled in filterLocales() where we return any data that does not match a localized pattern i.e. title: 'hello' would be returned, title: {en: 'hello', es: 'hola'} would be formatted to only the selected locales before returning. I will add tests for this

const filteredDoc = filterLocales(duplicatedFromDocWithLocales, selectedLocales)
duplicatedFromDocWithLocales = filteredDoc as typeof duplicatedFromDocWithLocales
}

if (!duplicatedFromDocWithLocales && !hasWherePolicy) {
throw new NotFound(req.t)
}
Expand Down
42 changes: 42 additions & 0 deletions packages/payload/src/utilities/filterLocalizedData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
function isLocalizedField(obj: any, selectedLocales: string[]): boolean {
const keys = Object.keys(obj)
const allKeysAreLocales = keys.every((k) => /^[a-z]{2}(?:[-_][A-Za-z0-9]+)?$/.test(k))
const hasSelectedLocale = keys.some((k) => selectedLocales.includes(k))
return allKeysAreLocales && hasSelectedLocale
}

export function filterLocales(
obj: any,
selectedLocales: string[],
keepEmptyObjects = false,
): unknown {
if (!obj || typeof obj !== 'object') {
return obj
}

if (Array.isArray(obj)) {
return obj.map((item) => filterLocales(item, selectedLocales, keepEmptyObjects))
}

const result: Record<string, unknown> = {}

for (const [key, value] of Object.entries(obj)) {
if (
value &&
typeof value === 'object' &&
!Array.isArray(value) &&
isLocalizedField(value, selectedLocales)
) {
const filtered = Object.fromEntries(
Object.entries(value).filter(([locale]) => selectedLocales.includes(locale)),
)
if (Object.keys(filtered).length > 0 || keepEmptyObjects) {
result[key] = filtered
}
} else {
result[key] = filterLocales(value, selectedLocales, keepEmptyObjects)
}
}

return result
}
2 changes: 2 additions & 0 deletions packages/translations/src/clientKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,9 @@ export const clientTranslationKeys = createClientTranslationKeys([
'localization:localeToPublish',
'localization:copyToLocale',
'localization:copyFromTo',
'localization:selectedLocales',
'localization:selectLocaleToCopy',
'localization:selectLocaleToDuplicate',
'localization:cannotCopySameLocale',
'localization:copyFrom',
'localization:copyTo',
Expand Down
2 changes: 2 additions & 0 deletions packages/translations/src/languages/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,9 @@ export const arTranslations: DefaultTranslationsObject = {
copyTo: 'انسخ إلى',
copyToLocale: 'نسخ إلى الموقع المحلي',
localeToPublish: 'الموقع للنشر',
selectedLocales: 'المواقع المختارة',
selectLocaleToCopy: 'حدد الموقع المحلي للنسخ',
selectLocaleToDuplicate: 'اختر المواقع للتكرار',
},
operators: {
contains: 'يحتوي',
Expand Down
2 changes: 2 additions & 0 deletions packages/translations/src/languages/az.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,9 @@ export const azTranslations: DefaultTranslationsObject = {
copyTo: 'Köçür',
copyToLocale: 'Yerliyə köçürün',
localeToPublish: 'Yayımlamaq üçün yerləşdirin',
selectedLocales: 'Seçilmiş Dillər',
selectLocaleToCopy: 'Köçürmək üçün yerli seçin',
selectLocaleToDuplicate: 'Dublikat üçün məkanları seçin',
},
operators: {
contains: 'daxilində',
Expand Down
2 changes: 2 additions & 0 deletions packages/translations/src/languages/bg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,9 @@ export const bgTranslations: DefaultTranslationsObject = {
copyTo: 'Копирай в',
copyToLocale: 'Копирайте в местното',
localeToPublish: 'Местоположение за публикуване',
selectedLocales: 'Избрани локали',
selectLocaleToCopy: 'Изберете място за копиране',
selectLocaleToDuplicate: 'Изберете локации за дублиране',
},
operators: {
contains: 'съдържа',
Expand Down
2 changes: 2 additions & 0 deletions packages/translations/src/languages/bnBd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,9 @@ export const bnBdTranslations: DefaultTranslationsObject = {
copyTo: 'কপি করুন',
copyToLocale: 'লোকেলে কপি করুন',
localeToPublish: 'প্রকাশ করার লোকেল',
selectedLocales: 'নির্বাচিত ভাষা অথবা এলাকা',
selectLocaleToCopy: 'কপি করার জন্য লোকেল নির্বাচন করুন',
selectLocaleToDuplicate: 'নির্বাচনকৃত লোকেলগুলি প্রতিলিপি করুন',
},
operators: {
contains: 'ধারণ করে',
Expand Down
2 changes: 2 additions & 0 deletions packages/translations/src/languages/bnIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,9 @@ export const bnInTranslations: DefaultTranslationsObject = {
copyTo: 'কপি করুন',
copyToLocale: 'লোকেলে কপি করুন',
localeToPublish: 'প্রকাশ করার লোকেল',
selectedLocales: 'নির্বাচিত ভাষা বা অঞ্চল',
selectLocaleToCopy: 'কপি করার জন্য লোকেল নির্বাচন করুন',
selectLocaleToDuplicate: 'নকল করার জন্য লোকেলস নির্বাচন করুন',
},
operators: {
contains: 'ধারণ করে',
Expand Down
2 changes: 2 additions & 0 deletions packages/translations/src/languages/ca.ts
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,9 @@ export const caTranslations: DefaultTranslationsObject = {
copyTo: 'Copiar a',
copyToLocale: 'Copiar a idioma',
localeToPublish: 'Idioma per publicar',
selectedLocales: 'Idiomes seleccionats',
selectLocaleToCopy: "Selecciona l'idioma per copiar",
selectLocaleToDuplicate: 'Selecciona les configuracions regionals per duplicar',
},
operators: {
contains: 'conté',
Expand Down
2 changes: 2 additions & 0 deletions packages/translations/src/languages/cs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,9 @@ export const csTranslations: DefaultTranslationsObject = {
copyTo: 'Kopírovat do',
copyToLocale: 'Kopírovat do lokalizace',
localeToPublish: 'Místo k publikování',
selectedLocales: 'Vybrané jazykové verze',
selectLocaleToCopy: 'Vyberte lokalitu ke kopírování',
selectLocaleToDuplicate: 'Vyberte národní prostředí k duplikaci',
},
operators: {
contains: 'obsahuje',
Expand Down
2 changes: 2 additions & 0 deletions packages/translations/src/languages/da.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,9 @@ export const daTranslations: DefaultTranslationsObject = {
copyTo: 'Kopier til',
copyToLocale: 'Kopier til lokal',
localeToPublish: 'Offentliggør på lokalitet',
selectedLocales: 'Valgte sprogområder',
selectLocaleToCopy: 'Vælg lokalitet til kopiering',
selectLocaleToDuplicate: 'Vælg lokaliteter til at duplikere',
},
operators: {
contains: 'Indeholder',
Expand Down
2 changes: 2 additions & 0 deletions packages/translations/src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,9 @@ export const deTranslations: DefaultTranslationsObject = {
copyTo: 'Kopieren nach',
copyToLocale: 'Erstelle Kopie für Sprach-Variante',
localeToPublish: 'Zu veröffentlichende Sprache',
selectedLocales: 'Ausgewählte Gebietsschemata',
selectLocaleToCopy: 'Wähle den Ort zum Kopieren aus',
selectLocaleToDuplicate: 'Wählen Sie die Gebietsschemata zum Duplizieren aus',
},
operators: {
contains: 'enthält',
Expand Down
2 changes: 2 additions & 0 deletions packages/translations/src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,9 @@ export const enTranslations = {
copyTo: 'Copy to',
copyToLocale: 'Copy to locale',
localeToPublish: 'Locale to publish',
selectedLocales: 'Selected Locales',
selectLocaleToCopy: 'Select locale to copy',
selectLocaleToDuplicate: 'Select locales to duplicate',
},
operators: {
contains: 'contains',
Expand Down
2 changes: 2 additions & 0 deletions packages/translations/src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,9 @@ export const esTranslations: DefaultTranslationsObject = {
copyTo: 'Copiar a',
copyToLocale: 'Copiar a idioma',
localeToPublish: 'Idioma para publicar',
selectedLocales: 'Idiomas seleccionados',
selectLocaleToCopy: 'Selecciona el idioma a copiar',
selectLocaleToDuplicate: 'Seleccione los idiomas para duplicar',
},
operators: {
contains: 'contiene',
Expand Down
2 changes: 2 additions & 0 deletions packages/translations/src/languages/et.ts
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,9 @@ export const etTranslations: DefaultTranslationsObject = {
copyTo: 'Kopeeri keelde',
copyToLocale: 'Kopeeri keelde',
localeToPublish: 'Lokaal avaldamiseks',
selectedLocales: 'Valitud lokaadid',
selectLocaleToCopy: 'Vali keel kopeerimiseks',
selectLocaleToDuplicate: 'Valige kohad, mida dubleerida',
},
operators: {
contains: 'sisaldab',
Expand Down
32 changes: 15 additions & 17 deletions packages/translations/src/languages/fa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ export const faTranslations: DefaultTranslationsObject = {
lockUntil: 'قفل تا تاریخ',
logBackIn: 'دوباره وارد شوید',
loggedIn: 'برای ورود با یک حساب دیگر، ابتدا باید <0>خارج</0> شوید.',
loggedInChangePassword:
'برای تغییر رمز عبور، به صفحه <0>حساب کاربری</0> خود مراجعه کنید.',
loggedInChangePassword: 'برای تغییر رمز عبور، به صفحه <0>حساب کاربری</0> خود مراجعه کنید.',
loggedOutInactivity: 'به دلیل عدم فعالیت، از حساب خود خارج شدید.',
loggedOutSuccessfully: 'شما با موفقیت خارج شدید.',
loggingOut: 'در حال خروج...',
Expand Down Expand Up @@ -144,8 +143,7 @@ export const faTranslations: DefaultTranslationsObject = {
block: 'بلاک',
blocks: 'بلاک‌ها',
blockType: 'نوع بلاک',
chooseBetweenCustomTextOrDocument:
'یک URL سفارشی وارد کنید یا به یک صفحه دیگر لینک دهید.',
chooseBetweenCustomTextOrDocument: 'یک URL سفارشی وارد کنید یا به یک صفحه دیگر لینک دهید.',
chooseDocumentToLink: 'یک صفحه را برای لینک دادن انتخاب کنید',
chooseFromExisting: 'انتخاب از موارد موجود',
chooseLabel: 'انتخاب {{label}}',
Expand Down Expand Up @@ -198,8 +196,7 @@ export const faTranslations: DefaultTranslationsObject = {
'آیا از انتقال <1>{{count}} {{label}}</1> به پوشه اصلی مطمئن هستید؟',
moveItemToFolderConfirmation:
'آیا از انتقال <1>{{title}}</1> به پوشه <2>{{toFolder}}</2> مطمئن هستید؟',
moveItemToRootConfirmation:
'آیا از انتقال <1>{{title}}</1> به پوشه اصلی مطمئن هستید؟',
moveItemToRootConfirmation: 'آیا از انتقال <1>{{title}}</1> به پوشه اصلی مطمئن هستید؟',
movingFromFolder: 'انتقال "{{title}}" از پوشه {{fromFolder}}',
newFolder: 'پوشه جدید',
noFolder: 'بدون پوشه',
Expand All @@ -213,12 +210,12 @@ export const faTranslations: DefaultTranslationsObject = {
aboutToDeleteCount_many: 'آیا از حذف {{count}} {{label}} مطمئن هستید؟',
aboutToDeleteCount_one: 'آیا از حذف {{count}} {{label}} مطمئن هستید؟',
aboutToDeleteCount_other: 'آیا از حذف {{count}} {{label}} مطمئن هستید؟',
aboutToPermanentlyDelete: 'شما در حال حذف دائمی {{label}} "{{title}}" هستید. این عمل قابل بازگشت نیست. آیا مطمئن هستید؟',
aboutToPermanentlyDelete:
'شما در حال حذف دائمی {{label}} "{{title}}" هستید. این عمل قابل بازگشت نیست. آیا مطمئن هستید؟',
aboutToPermanentlyDeleteTrash:
'آیا از حذف دائمی <0>{{count}}</0> <1>{{label}}</1> از سطل زباله مطمئن هستید؟ این عمل قابل بازگشت نیست.',
aboutToRestore: 'آیا از بازیابی {{label}} "{{title}}" مطمئن هستید؟',
aboutToRestoreAsDraft:
'آیا می‌خواهید {{label}} "{{title}}" به عنوان پیش‌نویس بازیابی شود؟',
aboutToRestoreAsDraft: 'آیا می‌خواهید {{label}} "{{title}}" به عنوان پیش‌نویس بازیابی شود؟',
aboutToRestoreAsDraftCount: 'آیا می‌خواهید {{count}} {{label}} به عنوان پیش‌نویس بازیابی شوند؟',
aboutToRestoreCount: 'آیا از بازیابی {{count}} {{label}} مطمئن هستید؟',
aboutToTrash: 'آیا می‌خواهید {{label}} "{{title}}" به سطل زباله منتقل شود؟',
Expand Down Expand Up @@ -336,8 +333,7 @@ export const faTranslations: DefaultTranslationsObject = {
menu: 'منو',
moreOptions: 'گزینه‌های بیشتر',
move: 'انتقال',
moveConfirm:
'آیا از انتقال {{count}} {{label}} به <1>{{destination}}</1> مطمئن هستید؟',
moveConfirm: 'آیا از انتقال {{count}} {{label}} به <1>{{destination}}</1> مطمئن هستید؟',
moveCount: 'انتقال {{count}} {{label}}',
moveDown: 'انتقال به پایین',
moveUp: 'انتقال به بالا',
Expand Down Expand Up @@ -423,7 +419,8 @@ export const faTranslations: DefaultTranslationsObject = {
true: 'بله',
unauthorized: 'غیرمجاز',
unsavedChanges: 'تغییرات ذخیره نشده‌ای دارید. قبل از ادامه، آن‌ها را ذخیره یا لغو کنید.',
unsavedChangesDuplicate: 'شما تغییرات ذخیره نشده‌ای دارید. آیا می‌خواهید بدون ذخیره، کپی ایجاد کنید؟',
unsavedChangesDuplicate:
'شما تغییرات ذخیره نشده‌ای دارید. آیا می‌خواهید بدون ذخیره، کپی ایجاد کنید؟',
untitled: 'بدون عنوان',
upcomingEvents: 'رویدادهای آینده',
updatedAt: 'تاریخ به‌روزرسانی',
Expand All @@ -450,7 +447,9 @@ export const faTranslations: DefaultTranslationsObject = {
copyTo: 'کپی به',
copyToLocale: 'کپی به زبان دیگر',
localeToPublish: 'زبان مورد نظر برای انتشار',
selectedLocales: 'انتخاب مناطق',
selectLocaleToCopy: 'یک زبان برای کپی کردن اطلاعات انتخاب کنید',
selectLocaleToDuplicate: 'انتخاب مکان‌ها برای تکثیر',
},
operators: {
contains: 'شامل می‌شود',
Expand Down Expand Up @@ -485,8 +484,7 @@ export const faTranslations: DefaultTranslationsObject = {
filesToUpload: 'فایل‌ها برای آپلود',
fileToUpload: 'فایل برای آپلود',
focalPoint: 'نقطه کانونی',
focalPointDescription:
'نقطه کانونی را مستقیماً روی تصویر بکشید یا مقادیر زیر را تنظیم کنید.',
focalPointDescription: 'نقطه کانونی را مستقیماً روی تصویر بکشید یا مقادیر زیر را تنظیم کنید.',
height: 'ارتفاع',
lessInfo: 'اطلاعات کمتر',
moreInfo: 'اطلاعات بیشتر',
Expand Down Expand Up @@ -528,11 +526,11 @@ export const faTranslations: DefaultTranslationsObject = {
},
version: {
type: 'نوع',
aboutToPublishSelection:
'آیا از انتشار تمام {{label}} انتخاب شده مطمئن هستید؟',
aboutToPublishSelection: 'آیا از انتشار تمام {{label}} انتخاب شده مطمئن هستید؟',
aboutToRestore:
'شما در حال بازگردانی این {{label}} به نسخه‌ای هستید که در تاریخ {{versionDate}} ذخیره شده است. آیا ادامه می‌دهید؟',
aboutToRestoreGlobal: 'شما در حال بازگردانی {{label}} به نسخه مربوط به تاریخ {{versionDate}} هستید. آیا ادامه می‌دهید؟',
aboutToRestoreGlobal:
'شما در حال بازگردانی {{label}} به نسخه مربوط به تاریخ {{versionDate}} هستید. آیا ادامه می‌دهید؟',
aboutToRevertToPublished:
'شما در حال بازگرداندن این صفحه به آخرین نسخه منتشر شده آن هستید. آیا مطمئن هستید؟',
aboutToUnpublish: 'آیا از لغو انتشار این صفحه مطمئن هستید؟',
Expand Down
Loading
Loading