diff --git a/packages/payload/src/collections/endpoints/duplicate.ts b/packages/payload/src/collections/endpoints/duplicate.ts index 5b0c3456f1b..c35243dfdbc 100644 --- a/packages/payload/src/collections/endpoints/duplicate.ts +++ b/packages/payload/src/collections/endpoints/duplicate.ts @@ -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, @@ -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', { diff --git a/packages/payload/src/collections/operations/create.ts b/packages/payload/src/collections/operations/create.ts index ac6c91d7a18..f9aa2ae121d 100644 --- a/packages/payload/src/collections/operations/create.ts +++ b/packages/payload/src/collections/operations/create.ts @@ -50,6 +50,7 @@ export type Arguments = { publishSpecificLocale?: string req: PayloadRequest select?: SelectType + selectedLocales?: string[] showHiddenFields?: boolean } @@ -113,6 +114,7 @@ export const createOperation = async < }, req, select: incomingSelect, + selectedLocales, showHiddenFields, } = args @@ -130,6 +132,7 @@ export const createOperation = async < draftArg: shouldSaveDraft, overrideAccess, req, + selectedLocales, shouldSaveDraft, }) diff --git a/packages/payload/src/collections/operations/duplicate.ts b/packages/payload/src/collections/operations/duplicate.ts index 888bea0a6f1..a4f47891b13 100644 --- a/packages/payload/src/collections/operations/duplicate.ts +++ b/packages/payload/src/collections/operations/duplicate.ts @@ -9,6 +9,7 @@ import { type Arguments as CreateArguments, createOperation } from './create.js' export type Arguments = { data?: DeepPartial> id: number | string + selectedLocales?: string[] } & Omit, 'data' | 'duplicateFromID'> export const duplicateOperation = async < @@ -22,5 +23,6 @@ export const duplicateOperation = async < ...args, data: incomingArgs?.data || {}, duplicateFromID: id, + selectedLocales: incomingArgs.selectedLocales, }) } diff --git a/packages/payload/src/duplicateDocument/index.ts b/packages/payload/src/duplicateDocument/index.ts index 7d6860de855..c5e1d15139d 100644 --- a/packages/payload/src/duplicateDocument/index.ts +++ b/packages/payload/src/duplicateDocument/index.ts @@ -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 = { @@ -18,6 +19,7 @@ type GetDuplicateDocumentArgs = { id: number | string overrideAccess?: boolean req: PayloadRequest + selectedLocales?: string[] shouldSaveDraft?: boolean } export const getDuplicateDocumentData = async ({ @@ -26,6 +28,7 @@ export const getDuplicateDocumentData = async ({ draftArg, overrideAccess, req, + selectedLocales, shouldSaveDraft, }: GetDuplicateDocumentArgs): Promise<{ duplicatedFromDoc: JsonObject @@ -59,6 +62,11 @@ export const getDuplicateDocumentData = async ({ req, }) + if (selectedLocales && selectedLocales.length > 0 && duplicatedFromDocWithLocales) { + const filteredDoc = filterLocales(duplicatedFromDocWithLocales, selectedLocales) + duplicatedFromDocWithLocales = filteredDoc as typeof duplicatedFromDocWithLocales + } + if (!duplicatedFromDocWithLocales && !hasWherePolicy) { throw new NotFound(req.t) } diff --git a/packages/payload/src/utilities/filterLocalizedData.ts b/packages/payload/src/utilities/filterLocalizedData.ts new file mode 100644 index 00000000000..2fd09f26a72 --- /dev/null +++ b/packages/payload/src/utilities/filterLocalizedData.ts @@ -0,0 +1,46 @@ +export function filterLocales( + obj: any, + selectedLocales: string[], + keepEmptyObjects = false, +): unknown { + if (Array.isArray(obj)) { + return obj.map((item) => filterLocales(item, selectedLocales, keepEmptyObjects)) + } + + if (obj && typeof obj === 'object') { + const result: Record = {} + + for (const [key, value] of Object.entries(obj as Record)) { + if (value && typeof value === 'object' && !Array.isArray(value)) { + const valueKeys = Object.keys(value) + + const allKeysLookLikeLocales = + valueKeys.length >= 2 && + valueKeys.length <= 5 && + valueKeys.every( + (k) => typeof k === 'string' && /^[a-z]{2}(?:[-_][A-Za-z0-9]+)?$/.test(k), + ) && + valueKeys.some((k) => selectedLocales.includes(k)) + + if (allKeysLookLikeLocales) { + const filtered = Object.fromEntries( + Object.entries(value).filter(([locale]) => selectedLocales.includes(locale)), + ) + + if (Object.keys(filtered).length > 0 || keepEmptyObjects) { + result[key] = filtered + } else { + // return empty object + } + + continue + } + } + + result[key] = filterLocales(value, selectedLocales, keepEmptyObjects) + } + + return result + } + return obj +} diff --git a/packages/translations/src/clientKeys.ts b/packages/translations/src/clientKeys.ts index 66ac0954fd9..9eaec286937 100644 --- a/packages/translations/src/clientKeys.ts +++ b/packages/translations/src/clientKeys.ts @@ -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', diff --git a/packages/translations/src/languages/ar.ts b/packages/translations/src/languages/ar.ts index 3836edb8f1e..1396eff5e28 100644 --- a/packages/translations/src/languages/ar.ts +++ b/packages/translations/src/languages/ar.ts @@ -445,7 +445,9 @@ export const arTranslations: DefaultTranslationsObject = { copyTo: 'انسخ إلى', copyToLocale: 'نسخ إلى الموقع المحلي', localeToPublish: 'الموقع للنشر', + selectedLocales: 'المواقع المختارة', selectLocaleToCopy: 'حدد الموقع المحلي للنسخ', + selectLocaleToDuplicate: 'اختر المواقع للتكرار', }, operators: { contains: 'يحتوي', diff --git a/packages/translations/src/languages/az.ts b/packages/translations/src/languages/az.ts index c1136be3bc9..3b0c5c1de62 100644 --- a/packages/translations/src/languages/az.ts +++ b/packages/translations/src/languages/az.ts @@ -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ə', diff --git a/packages/translations/src/languages/bg.ts b/packages/translations/src/languages/bg.ts index 009431d9a35..c7f2cbeae25 100644 --- a/packages/translations/src/languages/bg.ts +++ b/packages/translations/src/languages/bg.ts @@ -454,7 +454,9 @@ export const bgTranslations: DefaultTranslationsObject = { copyTo: 'Копирай в', copyToLocale: 'Копирайте в местното', localeToPublish: 'Местоположение за публикуване', + selectedLocales: 'Избрани локали', selectLocaleToCopy: 'Изберете място за копиране', + selectLocaleToDuplicate: 'Изберете локации за дублиране', }, operators: { contains: 'съдържа', diff --git a/packages/translations/src/languages/bnBd.ts b/packages/translations/src/languages/bnBd.ts index 9c17f5c460d..a6066ac3b27 100644 --- a/packages/translations/src/languages/bnBd.ts +++ b/packages/translations/src/languages/bnBd.ts @@ -462,7 +462,9 @@ export const bnBdTranslations: DefaultTranslationsObject = { copyTo: 'কপি করুন', copyToLocale: 'লোকেলে কপি করুন', localeToPublish: 'প্রকাশ করার লোকেল', + selectedLocales: 'নির্বাচিত ভাষা অথবা এলাকা', selectLocaleToCopy: 'কপি করার জন্য লোকেল নির্বাচন করুন', + selectLocaleToDuplicate: 'নির্বাচনকৃত লোকেলগুলি প্রতিলিপি করুন', }, operators: { contains: 'ধারণ করে', diff --git a/packages/translations/src/languages/bnIn.ts b/packages/translations/src/languages/bnIn.ts index 53932927e63..5840462c0ae 100644 --- a/packages/translations/src/languages/bnIn.ts +++ b/packages/translations/src/languages/bnIn.ts @@ -461,7 +461,9 @@ export const bnInTranslations: DefaultTranslationsObject = { copyTo: 'কপি করুন', copyToLocale: 'লোকেলে কপি করুন', localeToPublish: 'প্রকাশ করার লোকেল', + selectedLocales: 'নির্বাচিত ভাষা বা অঞ্চল', selectLocaleToCopy: 'কপি করার জন্য লোকেল নির্বাচন করুন', + selectLocaleToDuplicate: 'নকল করার জন্য লোকেলস নির্বাচন করুন', }, operators: { contains: 'ধারণ করে', diff --git a/packages/translations/src/languages/ca.ts b/packages/translations/src/languages/ca.ts index 2a52ae5b8d4..1d845ed64f3 100644 --- a/packages/translations/src/languages/ca.ts +++ b/packages/translations/src/languages/ca.ts @@ -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é', diff --git a/packages/translations/src/languages/cs.ts b/packages/translations/src/languages/cs.ts index c9ad698b7b1..d909975fbc6 100644 --- a/packages/translations/src/languages/cs.ts +++ b/packages/translations/src/languages/cs.ts @@ -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', diff --git a/packages/translations/src/languages/da.ts b/packages/translations/src/languages/da.ts index 0f501abe7d9..f711b0ae37e 100644 --- a/packages/translations/src/languages/da.ts +++ b/packages/translations/src/languages/da.ts @@ -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', diff --git a/packages/translations/src/languages/de.ts b/packages/translations/src/languages/de.ts index 7278f0484ae..92873067d3b 100644 --- a/packages/translations/src/languages/de.ts +++ b/packages/translations/src/languages/de.ts @@ -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', diff --git a/packages/translations/src/languages/en.ts b/packages/translations/src/languages/en.ts index 57e0f169e62..509f4700893 100644 --- a/packages/translations/src/languages/en.ts +++ b/packages/translations/src/languages/en.ts @@ -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', diff --git a/packages/translations/src/languages/es.ts b/packages/translations/src/languages/es.ts index b247074e981..636f5d946a5 100644 --- a/packages/translations/src/languages/es.ts +++ b/packages/translations/src/languages/es.ts @@ -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', diff --git a/packages/translations/src/languages/et.ts b/packages/translations/src/languages/et.ts index c83f5f8df93..3a6231b1982 100644 --- a/packages/translations/src/languages/et.ts +++ b/packages/translations/src/languages/et.ts @@ -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', diff --git a/packages/translations/src/languages/fa.ts b/packages/translations/src/languages/fa.ts index 8d881d4ad54..6e258875ab3 100644 --- a/packages/translations/src/languages/fa.ts +++ b/packages/translations/src/languages/fa.ts @@ -451,7 +451,9 @@ export const faTranslations: DefaultTranslationsObject = { copyTo: 'کپی کنید به', copyToLocale: 'کپی به محلی', localeToPublish: 'محل انتشار', + selectedLocales: 'انتخاب مناطق', selectLocaleToCopy: 'انتخاب مکان برای کپی کردن', + selectLocaleToDuplicate: 'انتخاب مکان‌ها برای تکثیر', }, operators: { contains: 'شامل', diff --git a/packages/translations/src/languages/fr.ts b/packages/translations/src/languages/fr.ts index da84806762f..a46a8f4dd62 100644 --- a/packages/translations/src/languages/fr.ts +++ b/packages/translations/src/languages/fr.ts @@ -467,7 +467,9 @@ export const frTranslations: DefaultTranslationsObject = { copyTo: 'Copier à', copyToLocale: 'Copier vers le lieu', localeToPublish: 'Locale à publier', + selectedLocales: 'Langues sélectionnées', selectLocaleToCopy: 'Sélectionnez la locale à copier', + selectLocaleToDuplicate: 'Sélectionnez les paramètres régionaux à dupliquer', }, operators: { contains: 'contient', diff --git a/packages/translations/src/languages/he.ts b/packages/translations/src/languages/he.ts index 7d866883cb8..bf2f8f9304e 100644 --- a/packages/translations/src/languages/he.ts +++ b/packages/translations/src/languages/he.ts @@ -443,7 +443,9 @@ export const heTranslations: DefaultTranslationsObject = { copyTo: 'העתק אל', copyToLocale: 'העתק למקום', localeToPublish: 'מיקום לפרסום', + selectedLocales: 'אזורים נבחרים', selectLocaleToCopy: 'בחר מיקום להעתקה', + selectLocaleToDuplicate: 'בחר שפות לשכפול', }, operators: { contains: 'מכיל', diff --git a/packages/translations/src/languages/hr.ts b/packages/translations/src/languages/hr.ts index b74c4af9d6f..889ef17a1c0 100644 --- a/packages/translations/src/languages/hr.ts +++ b/packages/translations/src/languages/hr.ts @@ -455,7 +455,9 @@ export const hrTranslations: DefaultTranslationsObject = { copyTo: 'Kopiraj na', copyToLocale: 'Kopiraj na lokaciju', localeToPublish: 'Lokacija za objavu', + selectedLocales: 'Odabrane lokalizacije', selectLocaleToCopy: 'Odaberite mjesto za kopiranje', + selectLocaleToDuplicate: 'Odaberite lokacije za duplikaciju', }, operators: { contains: 'sadrži', diff --git a/packages/translations/src/languages/hu.ts b/packages/translations/src/languages/hu.ts index 903502596ac..e38851a5a22 100644 --- a/packages/translations/src/languages/hu.ts +++ b/packages/translations/src/languages/hu.ts @@ -459,7 +459,9 @@ export const huTranslations: DefaultTranslationsObject = { copyTo: 'Másolja ide', copyToLocale: 'Másolás a helyi verzióba', localeToPublish: 'Közzététel helye', + selectedLocales: 'Kiválasztott helyi beállítások', selectLocaleToCopy: 'Válassza ki a másolni kívánt területet.', + selectLocaleToDuplicate: 'Válassza ki a másolandó helyszínekent.', }, operators: { contains: 'tartalmaz', diff --git a/packages/translations/src/languages/hy.ts b/packages/translations/src/languages/hy.ts index 2f9bce9549b..43f6837b1a7 100644 --- a/packages/translations/src/languages/hy.ts +++ b/packages/translations/src/languages/hy.ts @@ -459,7 +459,9 @@ export const hyTranslations: DefaultTranslationsObject = { copyTo: 'Պատճենել դեպի', copyToLocale: 'Պատճենել լոկալին', localeToPublish: 'Հրապարակման լոկալ', + selectedLocales: 'Ընտրված տեղադրություններ', selectLocaleToCopy: 'Ընտրեք լոկալ՝ պատճենելու համար', + selectLocaleToDuplicate: 'Ընտրեք տեղայնացվածությունները կրկնօրինակելու համար', }, operators: { contains: 'պարունակում է', diff --git a/packages/translations/src/languages/id.ts b/packages/translations/src/languages/id.ts index 0e31622af72..da3b08a4575 100644 --- a/packages/translations/src/languages/id.ts +++ b/packages/translations/src/languages/id.ts @@ -458,7 +458,9 @@ export const idTranslations: DefaultTranslationsObject = { copyTo: 'Salin ke', copyToLocale: 'Salin ke lokal', localeToPublish: 'Lokal untuk dipublikasikan', + selectedLocales: 'Lokasi yang Dipilih', selectLocaleToCopy: 'Pilih lokal untuk disalin', + selectLocaleToDuplicate: 'Pilih bahasa lokal untuk duplikat', }, operators: { contains: 'mengandung', diff --git a/packages/translations/src/languages/is.ts b/packages/translations/src/languages/is.ts index 7ee92561a89..62b4d5e56bb 100644 --- a/packages/translations/src/languages/is.ts +++ b/packages/translations/src/languages/is.ts @@ -453,7 +453,9 @@ export const isTranslations: DefaultTranslationsObject = { copyTo: 'Afrita til', copyToLocale: 'Afrita í staðfærslu', localeToPublish: 'Staðfærsla til að gefa út', + selectedLocales: 'Valdar svæði', selectLocaleToCopy: 'Veldu staðfærslu til að afrita', + selectLocaleToDuplicate: 'Veldu staðföng til að afrita', }, operators: { contains: 'inniheldur', diff --git a/packages/translations/src/languages/it.ts b/packages/translations/src/languages/it.ts index b0c4121c50f..a7a6bd9d76d 100644 --- a/packages/translations/src/languages/it.ts +++ b/packages/translations/src/languages/it.ts @@ -459,7 +459,9 @@ export const itTranslations: DefaultTranslationsObject = { copyTo: 'Copia per', copyToLocale: 'Copia in locale', localeToPublish: 'Località da pubblicare', + selectedLocales: 'Località Selezionate', selectLocaleToCopy: 'Seleziona la località da copiare', + selectLocaleToDuplicate: 'Seleziona le località da duplicare', }, operators: { contains: 'contiene', diff --git a/packages/translations/src/languages/ja.ts b/packages/translations/src/languages/ja.ts index a71c271f23b..e3c4c864d9c 100644 --- a/packages/translations/src/languages/ja.ts +++ b/packages/translations/src/languages/ja.ts @@ -457,7 +457,9 @@ export const jaTranslations: DefaultTranslationsObject = { copyTo: 'コピー先', copyToLocale: 'ロケールにコピー', localeToPublish: '公開する場所', + selectedLocales: '選択されたロケール', selectLocaleToCopy: 'コピーするロケールを選択してください', + selectLocaleToDuplicate: '重複するロケールを選択してください', }, operators: { contains: '含む', diff --git a/packages/translations/src/languages/ko.ts b/packages/translations/src/languages/ko.ts index 750fd868fd9..c994f8f4c10 100644 --- a/packages/translations/src/languages/ko.ts +++ b/packages/translations/src/languages/ko.ts @@ -453,7 +453,9 @@ export const koTranslations: DefaultTranslationsObject = { copyTo: '복사하기', copyToLocale: '로케일로 복사', localeToPublish: '발행할 장소', + selectedLocales: '선택된 로케일들', selectLocaleToCopy: '복사할 지역을 선택하십시오.', + selectLocaleToDuplicate: '로케일을 복제할 선택하세요', }, operators: { contains: '포함', diff --git a/packages/translations/src/languages/lt.ts b/packages/translations/src/languages/lt.ts index 91824191351..e0ce61cbfbc 100644 --- a/packages/translations/src/languages/lt.ts +++ b/packages/translations/src/languages/lt.ts @@ -457,7 +457,9 @@ export const ltTranslations: DefaultTranslationsObject = { copyTo: 'Kopijuoti į', copyToLocale: 'Kopijuoti į vietovę', localeToPublish: 'Publikuoti lokacijoje', + selectedLocales: 'Pasirinktos lokalės', selectLocaleToCopy: 'Pasirinkite lokalės kopijavimui', + selectLocaleToDuplicate: 'Pasirinkite vietoves, kurias norite dubliuoti', }, operators: { contains: 'yra', diff --git a/packages/translations/src/languages/lv.ts b/packages/translations/src/languages/lv.ts index 8fbd69266d3..d06fd491832 100644 --- a/packages/translations/src/languages/lv.ts +++ b/packages/translations/src/languages/lv.ts @@ -455,7 +455,9 @@ export const lvTranslations: DefaultTranslationsObject = { copyTo: 'Kopēt uz', copyToLocale: 'Kopēt uz lokalizāciju', localeToPublish: 'Lokalizācija publicēšanai', + selectedLocales: 'Izvēlētās lokalizācijas', selectLocaleToCopy: 'Izvēlieties lokalizāciju, no kuras kopēt', + selectLocaleToDuplicate: 'Izvēlieties lokalizācijas, kuras dublēt', }, operators: { contains: 'satur', diff --git a/packages/translations/src/languages/my.ts b/packages/translations/src/languages/my.ts index 0941d1c730c..2b59352bf2e 100644 --- a/packages/translations/src/languages/my.ts +++ b/packages/translations/src/languages/my.ts @@ -461,7 +461,9 @@ export const myTranslations: DefaultTranslationsObject = { copyTo: 'Salin ke', copyToLocale: 'Salin ke tempat setempat', localeToPublish: 'Untuk menerbitkan di lokasi', + selectedLocales: 'Pilihan Locale', selectLocaleToCopy: 'Pilih tempatan untuk menyalin', + selectLocaleToDuplicate: 'Pilih bahasa untuk dipadankan', }, operators: { contains: 'ပါဝင်သည်', diff --git a/packages/translations/src/languages/nb.ts b/packages/translations/src/languages/nb.ts index 9fb5e1472a1..1d1b8483e7a 100644 --- a/packages/translations/src/languages/nb.ts +++ b/packages/translations/src/languages/nb.ts @@ -456,7 +456,9 @@ export const nbTranslations: DefaultTranslationsObject = { copyTo: 'Kopier til', copyToLocale: 'Kopiere til språk', localeToPublish: 'Språk å publisere', + selectedLocales: 'Valgte lokaliteter', selectLocaleToCopy: 'Velg språk for å kopiere', + selectLocaleToDuplicate: 'Velg lokalinnstillinger for å duplisere', }, operators: { contains: 'inneholder', diff --git a/packages/translations/src/languages/nl.ts b/packages/translations/src/languages/nl.ts index b2cb1d1f876..e6f671844ce 100644 --- a/packages/translations/src/languages/nl.ts +++ b/packages/translations/src/languages/nl.ts @@ -463,7 +463,9 @@ export const nlTranslations: DefaultTranslationsObject = { copyTo: 'Kopiëren naar', copyToLocale: 'Kopieer naar taal', localeToPublish: 'Te publiceren taal', + selectedLocales: 'Geselecteerde Locales', selectLocaleToCopy: 'Selecteer taal om te kopiëren', + selectLocaleToDuplicate: 'Selecteer locales om te dupliceren', }, operators: { contains: 'bevat', diff --git a/packages/translations/src/languages/pl.ts b/packages/translations/src/languages/pl.ts index 2f6a35efb64..abe5317a707 100644 --- a/packages/translations/src/languages/pl.ts +++ b/packages/translations/src/languages/pl.ts @@ -453,7 +453,9 @@ export const plTranslations: DefaultTranslationsObject = { copyTo: 'Kopiuj do', copyToLocale: 'Kopiuj do lokalizacji', localeToPublish: 'Publikować lokalnie', + selectedLocales: 'Wybrane ustawienia regionalne', selectLocaleToCopy: 'Wybierz lokalizację do skopiowania', + selectLocaleToDuplicate: 'Wybierz regiony do skopiowania', }, operators: { contains: 'zawiera', diff --git a/packages/translations/src/languages/pt.ts b/packages/translations/src/languages/pt.ts index 5aa78d11b71..3cb0be8787e 100644 --- a/packages/translations/src/languages/pt.ts +++ b/packages/translations/src/languages/pt.ts @@ -457,7 +457,9 @@ export const ptTranslations: DefaultTranslationsObject = { copyTo: 'Copiar para', copyToLocale: 'Copiar para localidade', localeToPublish: 'Local para publicar', + selectedLocales: 'Locais selecionados', selectLocaleToCopy: 'Selecione o local para copiar', + selectLocaleToDuplicate: 'Selecione locais para duplicar', }, operators: { contains: 'contém', diff --git a/packages/translations/src/languages/ro.ts b/packages/translations/src/languages/ro.ts index 99de2fd2dbf..3541618cf0e 100644 --- a/packages/translations/src/languages/ro.ts +++ b/packages/translations/src/languages/ro.ts @@ -460,7 +460,9 @@ export const roTranslations: DefaultTranslationsObject = { copyTo: 'Copiați în', copyToLocale: 'Copiați în localizare', localeToPublish: 'Localizare pentru publicare', + selectedLocales: 'Locații selectate', selectLocaleToCopy: 'Selectați localizarea pentru copiere', + selectLocaleToDuplicate: 'Selectați localizările pentru duplicare', }, operators: { contains: 'conține', diff --git a/packages/translations/src/languages/rs.ts b/packages/translations/src/languages/rs.ts index aed84d4cbed..7ee2cd8e95f 100644 --- a/packages/translations/src/languages/rs.ts +++ b/packages/translations/src/languages/rs.ts @@ -455,7 +455,9 @@ export const rsTranslations: DefaultTranslationsObject = { copyTo: 'Kopiraj na', copyToLocale: 'Kopiraj na lokaciju', localeToPublish: 'Lokalitet za objavljivanje', + selectedLocales: 'Izabrane lokalne postavke', selectLocaleToCopy: 'Izaberite lokalitet za kopiranje', + selectLocaleToDuplicate: 'Izaberite lokacije za dupliciranje', }, operators: { contains: 'садржи', diff --git a/packages/translations/src/languages/rsLatin.ts b/packages/translations/src/languages/rsLatin.ts index fd2fe176508..064bb80f0db 100644 --- a/packages/translations/src/languages/rsLatin.ts +++ b/packages/translations/src/languages/rsLatin.ts @@ -456,7 +456,9 @@ export const rsLatinTranslations: DefaultTranslationsObject = { copyTo: 'Kopiraj u', copyToLocale: 'Kopiraj na lokaciju', localeToPublish: 'Lokal za objavljivanje', + selectedLocales: 'Izabrane regionalne postavke', selectLocaleToCopy: 'Izaberite lokalitet za kopiranje', + selectLocaleToDuplicate: 'Izaberite lokacije za kopiranje', }, operators: { contains: 'sadrži', diff --git a/packages/translations/src/languages/ru.ts b/packages/translations/src/languages/ru.ts index 254333ca680..a4d233d126b 100644 --- a/packages/translations/src/languages/ru.ts +++ b/packages/translations/src/languages/ru.ts @@ -458,7 +458,9 @@ export const ruTranslations: DefaultTranslationsObject = { copyTo: 'Копировать в', copyToLocale: 'Копировать в локаль', localeToPublish: 'Локаль для публикации', + selectedLocales: 'Выбранные локали', selectLocaleToCopy: 'Выберите локаль для копирования', + selectLocaleToDuplicate: 'Выберите локали для дублирования', }, operators: { contains: 'содержит', diff --git a/packages/translations/src/languages/sk.ts b/packages/translations/src/languages/sk.ts index 148fa2b6f22..ef0528212dc 100644 --- a/packages/translations/src/languages/sk.ts +++ b/packages/translations/src/languages/sk.ts @@ -454,7 +454,9 @@ export const skTranslations: DefaultTranslationsObject = { copyTo: 'Kopírovať do', copyToLocale: 'Kopírovať do lokalizácie', localeToPublish: 'Miesto na publikovanie', + selectedLocales: 'Vybrané miestne nastavenia', selectLocaleToCopy: 'Vyberte miestny systém na kopírovanie', + selectLocaleToDuplicate: 'Vyberte miestne nastavenia na duplikáciu', }, operators: { contains: 'obsahuje', diff --git a/packages/translations/src/languages/sl.ts b/packages/translations/src/languages/sl.ts index 49895148f20..c39a680e535 100644 --- a/packages/translations/src/languages/sl.ts +++ b/packages/translations/src/languages/sl.ts @@ -454,7 +454,9 @@ export const slTranslations: DefaultTranslationsObject = { copyTo: 'Kopiraj v', copyToLocale: 'Kopiraj v jezik', localeToPublish: 'Lokalno za objavo', + selectedLocales: 'Izbrane regionalne nastavitve', selectLocaleToCopy: 'Izberite jezik za kopiranje', + selectLocaleToDuplicate: 'Izberite jezikovne nastavitve za podvojitev', }, operators: { contains: 'vsebuje', diff --git a/packages/translations/src/languages/sv.ts b/packages/translations/src/languages/sv.ts index 13ea0e1bdad..893567e69e3 100644 --- a/packages/translations/src/languages/sv.ts +++ b/packages/translations/src/languages/sv.ts @@ -456,7 +456,9 @@ export const svTranslations: DefaultTranslationsObject = { copyTo: 'Kopiera till', copyToLocale: 'Kopiera till språk', localeToPublish: 'Publicera språk', + selectedLocales: 'Valda språkinställningar', selectLocaleToCopy: 'Välj språk att kopiera', + selectLocaleToDuplicate: 'Välj platser att duplicera', }, operators: { contains: 'innehåller', diff --git a/packages/translations/src/languages/ta.ts b/packages/translations/src/languages/ta.ts index 5a7a76a66bd..10bc4c1a357 100644 --- a/packages/translations/src/languages/ta.ts +++ b/packages/translations/src/languages/ta.ts @@ -454,7 +454,9 @@ export const taTranslations: DefaultTranslationsObject = { copyTo: 'இதற்கு நகலெடு', copyToLocale: 'மொழி அமைவுக்கு நகலெடு', localeToPublish: 'வெளியிட வேண்டிய மொழி அமைவு', + selectedLocales: 'தேர்வு செய்யப்பட்ட மொழிபெயர்ப்புகள்', selectLocaleToCopy: 'நகலெடுக்க வேண்டிய மொழி அமைவைத் தேர்ந்தெடுக்கவும்', + selectLocaleToDuplicate: 'மறுநகலெடுக்க உள்ளடக்க வடவிட்டஈ', }, operators: { contains: 'உள்ளடக்குகிறது', diff --git a/packages/translations/src/languages/th.ts b/packages/translations/src/languages/th.ts index 7fe52fa8a6a..bb13de7556d 100644 --- a/packages/translations/src/languages/th.ts +++ b/packages/translations/src/languages/th.ts @@ -447,7 +447,9 @@ export const thTranslations: DefaultTranslationsObject = { copyTo: 'คัดลอกไปที่', copyToLocale: 'คัดลอกไปยังสถานที่', localeToPublish: 'เผยแพร่ในสถานที่', + selectedLocales: 'ตำแหน่งที่ตั้งที่เลือก', selectLocaleToCopy: 'เลือกสถานที่ท้องถิ่นเพื่อคัดลอก', + selectLocaleToDuplicate: 'เลือกที่ตั้งเพื่อทำซ้ำ', }, operators: { contains: 'มี', diff --git a/packages/translations/src/languages/tr.ts b/packages/translations/src/languages/tr.ts index 8ece021f966..a114b291486 100644 --- a/packages/translations/src/languages/tr.ts +++ b/packages/translations/src/languages/tr.ts @@ -460,7 +460,9 @@ export const trTranslations: DefaultTranslationsObject = { copyTo: 'Kopyala', copyToLocale: 'Yerel hafızaya kopyala', localeToPublish: 'Yayınlanacak yerel', + selectedLocales: 'Seçilen Yerel Ayarlar', selectLocaleToCopy: 'Kopyalamak için yerel seçimi yapın', + selectLocaleToDuplicate: 'Çoğaltmak için yerelleri seçin', }, operators: { contains: 'içerir', diff --git a/packages/translations/src/languages/uk.ts b/packages/translations/src/languages/uk.ts index cf5fe3bcb8f..b9d33f02934 100644 --- a/packages/translations/src/languages/uk.ts +++ b/packages/translations/src/languages/uk.ts @@ -453,7 +453,9 @@ export const ukTranslations: DefaultTranslationsObject = { copyTo: 'Копіювати в', copyToLocale: 'Копіювати до локалізації', localeToPublish: 'Місце публікації', + selectedLocales: 'Вибрані локалі', selectLocaleToCopy: 'Виберіть локалізацію для копіювання', + selectLocaleToDuplicate: 'Виберіть локалі для дублювання', }, operators: { contains: 'містить', diff --git a/packages/translations/src/languages/vi.ts b/packages/translations/src/languages/vi.ts index c31022cfaf7..e9163254cec 100644 --- a/packages/translations/src/languages/vi.ts +++ b/packages/translations/src/languages/vi.ts @@ -454,7 +454,9 @@ export const viTranslations: DefaultTranslationsObject = { copyTo: 'Sao chép đến', copyToLocale: 'Sao chép vào địa phương', localeToPublish: 'Ngôn ngữ để xuất bản', + selectedLocales: 'Địa phương đã chọn', selectLocaleToCopy: 'Chọn địa phương để sao chép', + selectLocaleToDuplicate: 'Chọn ngôn ngữ để sao chép', }, operators: { contains: 'có chứa', diff --git a/packages/translations/src/languages/zh.ts b/packages/translations/src/languages/zh.ts index 3525f200255..29821acc81a 100644 --- a/packages/translations/src/languages/zh.ts +++ b/packages/translations/src/languages/zh.ts @@ -436,7 +436,9 @@ export const zhTranslations: DefaultTranslationsObject = { copyTo: '复制到', copyToLocale: '复制到指定语言环境', localeToPublish: '需发布的语言环境', + selectedLocales: '已选语言设置', selectLocaleToCopy: '选择要复制的语言环境', + selectLocaleToDuplicate: '选择要复制的区域设置', }, operators: { contains: '包含', diff --git a/packages/translations/src/languages/zhTw.ts b/packages/translations/src/languages/zhTw.ts index d146f2b8d8a..8bdbb112beb 100644 --- a/packages/translations/src/languages/zhTw.ts +++ b/packages/translations/src/languages/zhTw.ts @@ -434,7 +434,9 @@ export const zhTwTranslations: DefaultTranslationsObject = { copyTo: '複製到', copyToLocale: '複製至語言地區', localeToPublish: '要發佈的語言地區', + selectedLocales: '選擇的地區設定', selectLocaleToCopy: '選取要複製的語言地區', + selectLocaleToDuplicate: '選擇要複製的地區設定', }, operators: { contains: '包含', diff --git a/packages/ui/src/elements/DocumentControls/index.tsx b/packages/ui/src/elements/DocumentControls/index.tsx index 08f463263af..4d2b0890ff4 100644 --- a/packages/ui/src/elements/DocumentControls/index.tsx +++ b/packages/ui/src/elements/DocumentControls/index.tsx @@ -276,11 +276,11 @@ export const DocumentControls: React.FC<{ {(unsavedDraftWithValidations || !autosaveEnabled || (autosaveEnabled && showSaveDraftButton)) && ( - } - /> - )} + } + /> + )} } @@ -365,13 +365,25 @@ export const DocumentControls: React.FC<{ )} {collectionConfig.disableDuplicate !== true && isEditing && ( - + <> + + {localization && + + } + )} )} diff --git a/packages/ui/src/elements/DuplicateDocument/index.scss b/packages/ui/src/elements/DuplicateDocument/index.scss new file mode 100644 index 00000000000..b9193daeb1d --- /dev/null +++ b/packages/ui/src/elements/DuplicateDocument/index.scss @@ -0,0 +1,24 @@ +@import '../../scss/styles.scss'; + +.duplicate-selected-locales { + &__sub-header { + padding: 0 var(--gutter-h); + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid var(--theme-border-color); + } + + &__content { + padding: calc(var(--base) * 1.5) var(--gutter-h); + display: flex; + flex-direction: column; + gap: calc(var(--base)); + } + + &__item { + display: flex; + flex-direction: row; + gap: calc(var(--base) * 0.5); + } +} diff --git a/packages/ui/src/elements/DuplicateDocument/index.tsx b/packages/ui/src/elements/DuplicateDocument/index.tsx index 65e9710c306..56318101ed1 100644 --- a/packages/ui/src/elements/DuplicateDocument/index.tsx +++ b/packages/ui/src/elements/DuplicateDocument/index.tsx @@ -11,19 +11,26 @@ import { toast } from 'sonner' import type { DocumentDrawerContextType } from '../DocumentDrawer/Provider.js' +import { CheckboxInput } from '../../fields/Checkbox/index.js' import { useForm, useFormModified } from '../../forms/Form/context.js' import { useConfig } from '../../providers/Config/index.js' import { useLocale } from '../../providers/Locale/index.js' import { useRouteTransition } from '../../providers/RouteTransition/index.js' import { useTranslation } from '../../providers/Translation/index.js' import { requests } from '../../utilities/api.js' +import { traverseForLocalizedFields } from '../../utilities/traverseForLocalizedFields.js' +import { DrawerHeader } from '../BulkUpload/Header/index.js' +import { Button } from '../Button/index.js' import { ConfirmationModal } from '../ConfirmationModal/index.js' +import { Drawer } from '../Drawer/index.js' import { PopupList } from '../Popup/index.js' +import './index.scss' export type Props = { readonly id: string readonly onDuplicate?: DocumentDrawerContextType['onDuplicate'] readonly redirectAfterDuplicate?: boolean + readonly selectLocales?: boolean readonly singularLabel: SanitizedCollectionConfig['labels']['singular'] readonly slug: string } @@ -33,17 +40,19 @@ export const DuplicateDocument: React.FC = ({ slug, onDuplicate, redirectAfterDuplicate = true, + selectLocales, singularLabel, }) => { const router = useRouter() const modified = useFormModified() - const { openModal } = useModal() - const locale = useLocale() + const { openModal, toggleModal } = useModal() + const { code: localeCode } = useLocale() const { setModified } = useForm() const { startRouteTransition } = useRouteTransition() const { config: { + localization, routes: { admin: adminRoute, api: apiRoute }, serverURL, }, @@ -53,23 +62,55 @@ export const DuplicateDocument: React.FC = ({ const collectionConfig = getEntityConfig({ collectionSlug: slug }) const [renderModal, setRenderModal] = React.useState(false) + const [selectedLocales, setSelectedLocales] = React.useState([]) const { i18n, t } = useTranslation() const modalSlug = `duplicate-${id}` + const drawerSlug = `duplicate-locales-${id}` + const popupID = `action-duplicate${selectLocales ? `-locales` : ''}` + const baseClass = 'duplicate-selected-locales' - const duplicate = useCallback(async () => { + const [hasLocalizedFields, setHasLocalizedFields] = React.useState(false) + + React.useEffect(() => { + const hasLocalizedField = traverseForLocalizedFields(collectionConfig?.fields) + setHasLocalizedFields(hasLocalizedField) + }, [collectionConfig?.fields]) + + const localeOptions = + (localization && + localization.locales.map((locale) => (typeof locale === 'string' ? { label: locale, value: locale } : { label: locale.label, value: locale.code }))) || + [] + + const allLocales = localeOptions.map((locale) => locale.value) + + const arraysEqual = (a = [], b = []) => { + if (a.length !== b.length) { + return false + } + const sa = [...a].slice().sort() + const sb = [...b].slice().sort() + return sa.every((v, i) => v === sb[i]) + } + + const allLocalesSelected = arraysEqual(selectedLocales, allLocales) + + const duplicate = useCallback(async (selectLocales: boolean = false) => { setRenderModal(true) + const url = `${serverURL}${apiRoute}/${slug}/${id}/duplicate${localeCode ? `?locale=${localeCode}` : ''}${(selectLocales && !allLocalesSelected) ? `&selectedLocales=[${selectedLocales.join(',')}]` : ''}` + const headers = { + 'Accept-Language': i18n.language, + 'Content-Type': 'application/json', + credentials: 'include', + } + await requests .post( - `${serverURL}${apiRoute}/${slug}/${id}/duplicate${locale?.code ? `?locale=${locale.code}` : ''}`, + url, { body: JSON.stringify({}), - headers: { - 'Accept-Language': i18n.language, - 'Content-Type': 'application/json', - credentials: 'include', - }, + headers, }, ) .then(async (res) => { @@ -78,7 +119,7 @@ export const DuplicateDocument: React.FC = ({ if (res.status < 400) { toast.success( message || - t('general:successfullyDuplicated', { label: getTranslation(singularLabel, i18n) }), + t('general:successfullyDuplicated', { label: getTranslation(singularLabel, i18n) }), ) setModified(false) @@ -88,7 +129,7 @@ export const DuplicateDocument: React.FC = ({ router.push( formatAdminURL({ adminRoute, - path: `/collections/${slug}/${doc.id}${locale?.code ? `?locale=${locale.code}` : ''}`, + path: `/collections/${slug}/${doc.id}${localeCode ? `?locale=${localeCode}` : ''}`, }), ), ) @@ -100,13 +141,13 @@ export const DuplicateDocument: React.FC = ({ } else { toast.error( errors?.[0].message || - message || - t('error:unspecific', { label: getTranslation(singularLabel, i18n) }), + message || + t('error:unspecific', { label: getTranslation(singularLabel, i18n) }), ) } }) }, [ - locale, + localeCode, serverURL, apiRoute, slug, @@ -121,38 +162,118 @@ export const DuplicateDocument: React.FC = ({ adminRoute, collectionConfig, startRouteTransition, + selectedLocales, + allLocalesSelected, ]) const onConfirm = useCallback(async () => { setRenderModal(false) - await duplicate() - }, [duplicate]) - - return ( - - { - if (modified) { - setRenderModal(true) - return openModal(modalSlug) - } + if (selectLocales) { + openModal(drawerSlug) + return + } else { + await duplicate() + } + }, [duplicate, drawerSlug, selectLocales, openModal]) - return duplicate() - }} - > - {t('general:duplicate')} - - {renderModal && ( - - )} - - ) + if ( + !selectLocales || selectLocales && hasLocalizedFields + ) { + return ( + + { + if (modified) { + setRenderModal(true) + return openModal(modalSlug) + } else if (selectLocales && !modified) { + return openModal(drawerSlug) + } + + return duplicate() + }} + > + {t('general:duplicate')} {selectLocales && ' ' + t('localization:selectedLocales')} + + {renderModal && ( + + )} + {/* Select locales to duplicate drawer */} + { + selectLocales && + { + toggleModal(drawerSlug) + }} + title={t('general:duplicate') + ' ' + t('localization:selectedLocales')} + /> + } + slug={drawerSlug} + > +
+ + {t('localization:selectLocaleToDuplicate')} + + +
+
+
+ { + setSelectedLocales(allLocalesSelected ? [] : [...allLocales]) + }} + + /> + {t('general:selectAll', { count: allLocales.length, label: t('general:locales') })} +
+ {localeOptions.map((locale) => ( +
+ { + setSelectedLocales(prev => + !selectedLocales.includes(locale.value) + ? [...prev, locale.value] + : prev.filter(l => l !== locale.value) + ) + }} + /> + {typeof locale.label === 'string' ? locale.label : JSON.stringify(locale.label)} +
+ ))} +
+ +
+ } +
+ ) + } } diff --git a/test/localization/e2e.spec.ts b/test/localization/e2e.spec.ts index b2468ceeccc..d146f0b41e0 100644 --- a/test/localization/e2e.spec.ts +++ b/test/localization/e2e.spec.ts @@ -681,6 +681,53 @@ describe('Localization', () => { await page.goto(noLocalizedFieldsURL.create) await expect(page.locator('#publish-locale')).toHaveCount(0) }) + + describe('duplicate selected locales', () => { + test('should duplicate only selected locales', async () => { + await page.goto(urlPostsWithDrafts.create) + await changeLocale(page, defaultLocale) + await fillValues({ title: 'English Title' }) + await saveDocAndAssert(page) + const id = await page.locator('.id-label').innerText() + + await changeLocale(page, spanishLocale) + await fillValues({ title: 'Spanish Title' }) + await saveDocAndAssert(page) + + await changeLocale(page, 'pt') + await fillValues({ title: 'Portuguese Title' }) + await saveDocAndAssert(page) + + await openDocControls(page) + await page.locator('#action-duplicate-locales').click() + + await expect(page.locator('.duplicate-selected-locales__content')).toBeVisible() + await page + .locator('.duplicate-selected-locales__item', { hasText: 'English' }) + .locator('input') + .click() + await page + .locator('.duplicate-selected-locales__item', { hasText: 'Portuguese' }) + .locator('input') + .click() + const confirmButton = page.locator('#\\#action-duplicate-confirm') + await expect(confirmButton).toBeEnabled() + await confirmButton.click() + await expect(page.locator('.payload-toast-container')).toContainText( + 'successfully duplicated', + ) + + await expect.poll(() => page.url()).not.toContain(id) + await page.waitForURL((url) => !url.toString().includes(id)) + + await changeLocale(page, defaultLocale) + await expect(page.locator('#field-title')).toHaveValue('English Title') + await changeLocale(page, spanishLocale) + await expect(page.locator('#field-title')).toBeEmpty() + await changeLocale(page, 'pt') + await expect(page.locator('#field-title')).toHaveValue('Portuguese Title') + }) + }) }) async function createLocalizedArrayItem(page: Page, url: AdminUrlUtil) {