Skip to content

Commit 410912c

Browse files
authored
fix(plugin-import-export): fix imports with locales in a different column order than exported (#15808)
Fixes #15804 There was an issue where if the localized columns were not in the same order as the data was exported it would import the data into the wrong locale as it wasn't reading the "defaultLocale" config and just using the first locale in the config.
1 parent d931894 commit 410912c

2 files changed

Lines changed: 108 additions & 9 deletions

File tree

packages/plugin-import-export/src/import/batchProcessor.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,18 @@ export interface ImportProcessOptions {
5858
* Separates multi-locale data from a document for sequential locale updates.
5959
*
6060
* When a field has locale-keyed values (e.g., { title: { en: 'Hello', es: 'Hola' } }),
61-
* this extracts the first locale's data for initial create/update, and stores
61+
* this extracts the default locale's data for initial create/update, and stores
6262
* remaining locales for subsequent update calls.
6363
*
6464
* @returns
65-
* - flatData: Document with first locale values extracted (for initial operation)
65+
* - flatData: Document with default locale values extracted (for initial operation)
6666
* - hasMultiLocale: Whether any multi-locale fields were found
6767
* - localeUpdates: Map of locale -> field data for follow-up updates
6868
*/
6969
function extractMultiLocaleData(
7070
data: Record<string, unknown>,
7171
configuredLocales?: string[],
72+
defaultLocale?: string,
7273
): {
7374
flatData: Record<string, unknown>
7475
hasMultiLocale: boolean
@@ -91,11 +92,12 @@ function extractMultiLocaleData(
9192

9293
if (localeKeys.length > 0) {
9394
hasMultiLocale = true
94-
const firstLocale = localeKeys[0]
95-
if (firstLocale) {
96-
flatData[key] = valueObj[firstLocale]
95+
const baseLocale =
96+
defaultLocale && localeKeys.includes(defaultLocale) ? defaultLocale : localeKeys[0]
97+
if (baseLocale) {
98+
flatData[key] = valueObj[baseLocale]
9799
for (const locale of localeKeys) {
98-
if (locale !== firstLocale) {
100+
if (locale !== baseLocale) {
99101
if (!localeUpdates[locale]) {
100102
localeUpdates[locale] = {}
101103
}
@@ -168,6 +170,10 @@ async function processImportBatch({
168170
? req.payload.config.localization.localeCodes
169171
: undefined
170172

173+
const defaultLocale = req.payload.config.localization
174+
? req.payload.config.localization.defaultLocale
175+
: undefined
176+
171177
const startingRowNumber = batchIndex * options.batchSize
172178

173179
for (let i = 0; i < batch.length; i++) {
@@ -217,16 +223,18 @@ async function processImportBatch({
217223
const { flatData, hasMultiLocale, localeUpdates } = extractMultiLocaleData(
218224
createData,
219225
configuredLocales,
226+
defaultLocale,
220227
)
221228

222229
if (hasMultiLocale) {
223230
// Create with default locale data
231+
const defaultLocaleReq = defaultLocale ? { ...req, locale: defaultLocale } : req
224232
savedDocument = await req.payload.create({
225233
collection: collectionSlug,
226234
data: flatData,
227235
draft: draftOption,
228236
overrideAccess: false,
229-
req,
237+
req: defaultLocaleReq,
230238
user,
231239
})
232240

@@ -343,6 +351,7 @@ async function processImportBatch({
343351
const { flatData, hasMultiLocale, localeUpdates } = extractMultiLocaleData(
344352
updateData,
345353
configuredLocales,
354+
defaultLocale,
346355
)
347356

348357
if (req.payload.config.debug) {
@@ -365,14 +374,15 @@ async function processImportBatch({
365374

366375
if (hasMultiLocale) {
367376
// Update with default locale data
377+
const defaultLocaleReq = defaultLocale ? { ...req, locale: defaultLocale } : req
368378
savedDocument = await req.payload.update({
369379
id: existingDoc.id as number | string,
370380
collection: collectionSlug,
371381
data: flatData,
372382
depth: 0,
373383
// Don't specify draft - this creates a new draft for versioned collections
374384
overrideAccess: false,
375-
req,
385+
req: defaultLocaleReq,
376386
user,
377387
})
378388

@@ -474,16 +484,18 @@ async function processImportBatch({
474484
const { flatData, hasMultiLocale, localeUpdates } = extractMultiLocaleData(
475485
createData,
476486
configuredLocales,
487+
defaultLocale,
477488
)
478489

479490
if (hasMultiLocale) {
480491
// Create with default locale data
492+
const defaultLocaleReq = defaultLocale ? { ...req, locale: defaultLocale } : req
481493
savedDocument = await req.payload.create({
482494
collection: collectionSlug,
483495
data: flatData,
484496
draft: draftOption,
485497
overrideAccess: false,
486-
req,
498+
req: defaultLocaleReq,
487499
user,
488500
})
489501

test/plugin-import-export/int.spec.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3281,6 +3281,93 @@ describe('@payloadcms/plugin-import-export', () => {
32813281
expect(importedPagesEs.docs[0]?.localized).toBe('Spanish text 1')
32823282
})
32833283

3284+
it('should import localized fields correctly regardless of CSV column order', async () => {
3285+
// CSV columns intentionally put 'de' before 'en' (the defaultLocale)
3286+
// to verify the import uses defaultLocale, not CSV column order
3287+
const csvContent =
3288+
'title,localized_de,localized_en,localized_es\n' +
3289+
'"Locale Order Test 1","German text 1","English text 1","Spanish text 1"\n' +
3290+
'"Locale Order Test 2","German text 2","English text 2","Spanish text 2"'
3291+
3292+
const csvBuffer = Buffer.from(csvContent)
3293+
3294+
let importDoc = await payload.create({
3295+
collection: 'imports',
3296+
user,
3297+
data: {
3298+
collectionSlug: 'pages',
3299+
importMode: 'create',
3300+
},
3301+
file: {
3302+
data: csvBuffer,
3303+
mimetype: 'text/csv',
3304+
name: 'locale-order-test.csv',
3305+
size: csvBuffer.length,
3306+
},
3307+
})
3308+
3309+
await payload.jobs.run()
3310+
3311+
importDoc = await payload.findByID({
3312+
collection: 'imports',
3313+
id: importDoc.id,
3314+
})
3315+
3316+
expect(importDoc.status).toBe('completed')
3317+
expect(importDoc.summary?.imported).toBe(2)
3318+
expect(importDoc.summary?.issues).toBe(0)
3319+
3320+
// Verify English (defaultLocale) has the correct English values, not German
3321+
const importedPagesEn = await payload.find({
3322+
collection: 'pages',
3323+
where: {
3324+
title: { contains: 'Locale Order Test ' },
3325+
},
3326+
locale: 'en',
3327+
sort: 'title',
3328+
})
3329+
3330+
expect(importedPagesEn.docs).toHaveLength(2)
3331+
expect(importedPagesEn.docs[0]?.localized).toBe('English text 1')
3332+
expect(importedPagesEn.docs[1]?.localized).toBe('English text 2')
3333+
3334+
// Verify German has the correct German values, not missing
3335+
const importedPagesDe = await payload.find({
3336+
collection: 'pages',
3337+
where: {
3338+
title: { contains: 'Locale Order Test ' },
3339+
},
3340+
locale: 'de',
3341+
sort: 'title',
3342+
})
3343+
3344+
expect(importedPagesDe.docs).toHaveLength(2)
3345+
expect(importedPagesDe.docs[0]?.localized).toBe('German text 1')
3346+
expect(importedPagesDe.docs[1]?.localized).toBe('German text 2')
3347+
3348+
// Verify Spanish has the correct Spanish values
3349+
const importedPagesEs = await payload.find({
3350+
collection: 'pages',
3351+
where: {
3352+
title: { contains: 'Locale Order Test ' },
3353+
},
3354+
locale: 'es',
3355+
sort: 'title',
3356+
})
3357+
3358+
expect(importedPagesEs.docs).toHaveLength(2)
3359+
expect(importedPagesEs.docs[0]?.localized).toBe('Spanish text 1')
3360+
expect(importedPagesEs.docs[1]?.localized).toBe('Spanish text 2')
3361+
3362+
// Cleanup
3363+
await payload.delete({
3364+
collection: 'pages',
3365+
where: {
3366+
title: { contains: 'Locale Order Test ' },
3367+
},
3368+
})
3369+
})
3370+
32843371
it('should import array fields from CSV', async () => {
32853372
const csvContent =
32863373
'title,array_0_field1,array_0_field2,array_1_field1,array_1_field2\n' +

0 commit comments

Comments
 (0)