diff --git a/packages/payload/src/fields/hooks/afterRead/promise.ts b/packages/payload/src/fields/hooks/afterRead/promise.ts index 5d2c35b3da7..508412bbf46 100644 --- a/packages/payload/src/fields/hooks/afterRead/promise.ts +++ b/packages/payload/src/fields/hooks/afterRead/promise.ts @@ -140,12 +140,17 @@ export const promise = async ({ } } + const shouldLocalizeField = fieldShouldBeLocalized({ + field, + parentIsLocalized: parentIsLocalized!, + }) + const shouldHoistLocalizedValue: boolean = Boolean( flattenLocales && fieldAffectsDataResult && typeof siblingDoc[field.name!] === 'object' && siblingDoc[field.name!] !== null && - fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) && + shouldLocalizeField && locale !== 'all' && req.payload.config.localization, ) @@ -258,7 +263,7 @@ export const promise = async ({ // => Object.entries(siblingDoc[field.name]) will be the value of a single locale, not all locales // => do not run the hook for each locale !shouldHoistLocalizedValue && - fieldShouldBeLocalized({ field, parentIsLocalized: parentIsLocalized! }) && + shouldLocalizeField && typeof siblingDoc[field.name] === 'object' if (fieldAffectsDataResult) { @@ -644,44 +649,76 @@ export const promise = async ({ case 'group': { if (fieldAffectsDataResult) { - let groupDoc = siblingDoc[field.name] as JsonObject + const groupSelect = + typeof select?.[field.name] === 'object' + ? (select?.[field.name] as SelectType) + : undefined - if (typeof siblingDoc[field.name] !== 'object') { - groupDoc = {} + if (shouldLocalizeField && !shouldHoistLocalizedValue) { + Object.values(siblingDoc[field.name] || {}).forEach((localizedData) => { + traverseFields({ + blockData, + collection, + context, + currentDepth, + depth, + doc, + draft, + fallbackLocale, + fieldPromises, + fields: field.fields, + findMany, + flattenLocales, + global, + locale, + overrideAccess, + parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, + parentPath: path, + parentSchemaPath: schemaPath, + populate, + populationPromises, + req, + select: groupSelect, + selectMode, + showHiddenFields, + siblingDoc: localizedData || {}, + triggerAccessControl, + triggerHooks, + }) + }) + } else { + traverseFields({ + blockData, + collection, + context, + currentDepth, + depth, + doc, + draft, + fallbackLocale, + fieldPromises, + fields: field.fields, + findMany, + flattenLocales, + global, + locale, + overrideAccess, + parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, + parentPath: path, + parentSchemaPath: schemaPath, + populate, + populationPromises, + req, + select: groupSelect, + selectMode, + showHiddenFields, + siblingDoc: typeof siblingDoc[field.name] !== 'object' ? {} : siblingDoc[field.name], + triggerAccessControl, + triggerHooks, + }) } - - const groupSelect = select?.[field.name] - - traverseFields({ - blockData, - collection, - context, - currentDepth, - depth, - doc, - draft, - fallbackLocale, - fieldPromises, - fields: field.fields, - findMany, - flattenLocales, - global, - locale, - overrideAccess, - parentIndexPath: '', - parentIsLocalized: parentIsLocalized || field.localized, - parentPath: path, - parentSchemaPath: schemaPath, - populate, - populationPromises, - req, - select: typeof groupSelect === 'object' ? groupSelect : undefined, - selectMode, - showHiddenFields, - siblingDoc: groupDoc, - triggerAccessControl, - triggerHooks, - }) } else { traverseFields({ blockData, @@ -814,56 +851,113 @@ export const promise = async ({ } case 'tab': { - let tabDoc = siblingDoc - let tabSelect: SelectType | undefined + const tabDoc = siblingDoc const isNamedTab = tabHasName(field) if (isNamedTab) { - tabDoc = siblingDoc[field.name] as JsonObject - - if (typeof siblingDoc[field.name] !== 'object') { - tabDoc = {} - } - - if (typeof select?.[field.name] === 'object') { - tabSelect = select?.[field.name] as SelectType + const tabSelect: SelectType | undefined = + typeof select?.[field.name] === 'object' + ? (select?.[field.name] as SelectType) + : undefined + if (shouldLocalizeField && !shouldHoistLocalizedValue) { + Object.values(siblingDoc[field.name] || {}).forEach((localizedData) => { + traverseFields({ + blockData, + collection, + context, + currentDepth, + depth, + doc, + draft, + fallbackLocale, + fieldPromises, + fields: field.fields, + findMany, + flattenLocales, + global, + locale, + overrideAccess, + parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, + parentPath: path, + parentSchemaPath: schemaPath, + populate, + populationPromises, + req, + select: tabSelect, + selectMode, + showHiddenFields, + siblingDoc: localizedData || {}, + triggerAccessControl, + triggerHooks, + }) + }) + } else { + traverseFields({ + blockData, + collection, + context, + currentDepth, + depth, + doc, + draft, + fallbackLocale, + fieldPromises, + fields: field.fields, + findMany, + flattenLocales, + global, + locale, + overrideAccess, + parentIndexPath: '', + parentIsLocalized: parentIsLocalized || field.localized, + parentPath: path, + parentSchemaPath: schemaPath, + populate, + populationPromises, + req, + select: tabSelect, + selectMode, + showHiddenFields, + siblingDoc: typeof siblingDoc[field.name] !== 'object' ? {} : siblingDoc[field.name], + triggerAccessControl, + triggerHooks, + }) } } else { - tabSelect = select + traverseFields({ + blockData, + collection, + context, + currentDepth, + depth, + doc, + draft, + fallbackLocale, + fieldPromises, + fields: field.fields, + findMany, + flattenLocales, + global, + locale, + overrideAccess, + parentIndexPath: isNamedTab ? '' : indexPath, + parentIsLocalized: parentIsLocalized || field.localized, + parentPath: isNamedTab ? path : parentPath, + parentSchemaPath: schemaPath, + populate, + populationPromises, + req, + select, + selectMode, + showHiddenFields, + siblingDoc: tabDoc, + triggerAccessControl, + triggerHooks, + }) } - traverseFields({ - blockData, - collection, - context, - currentDepth, - depth, - doc, - draft, - fallbackLocale, - fieldPromises, - fields: field.fields, - findMany, - flattenLocales, - global, - locale, - overrideAccess, - parentIndexPath: isNamedTab ? '' : indexPath, - parentIsLocalized: parentIsLocalized || field.localized, - parentPath: isNamedTab ? path : parentPath, - parentSchemaPath: schemaPath, - populate, - populationPromises, - req, - select: tabSelect, - selectMode, - showHiddenFields, - siblingDoc: tabDoc, - triggerAccessControl, - triggerHooks, - }) - break } diff --git a/test/localization/collections/AllFields/index.ts b/test/localization/collections/AllFields/index.ts index eb565b1bb06..9c58493231e 100644 --- a/test/localization/collections/AllFields/index.ts +++ b/test/localization/collections/AllFields/index.ts @@ -204,6 +204,36 @@ export const AllFieldsLocalized: CollectionConfig = { ], }, + // Deeply nested: localized tab + { + type: 'tabs', + tabs: [ + { + name: 't1', + label: 'Deeply Nested Tab', + localized: true, + fields: [ + { + type: 'tabs', + tabs: [ + { + name: 't2', + label: 'Nested Tab Level 2', + fields: [ + { + name: 'text', + type: 'text', + localized: true, + }, + ], + }, + ], + }, + ], + }, + ], + }, + // Deeply nested: localized group > non-localized group > localized array { name: 'g1', diff --git a/test/localization/int.spec.ts b/test/localization/int.spec.ts index 72a082ba371..9ec70651cfe 100644 --- a/test/localization/int.spec.ts +++ b/test/localization/int.spec.ts @@ -3459,6 +3459,11 @@ describe('Localization', () => { const doc = await payload.create({ collection: allFieldsLocalizedSlug, data: { + t1: { + t2: { + text: 'EN Deep Text', + }, + }, g1: { g2: { g2a1: [{ text: 'EN Deep 1' }, { text: 'EN Deep 2' }], @@ -3536,9 +3541,16 @@ describe('Localization', () => { // Verify deeply nested localization has locale keys only at topmost localized field expect((allLocalesDoc.g1 as any).en).toBeDefined() + expect((allLocalesDoc.g1 as any).g2).toBeUndefined() expect((allLocalesDoc.g1 as any).en.g2.g2a1).toHaveLength(2) expect((allLocalesDoc.g1 as any).en.g2.g2a1[0].text).toBe('EN Deep 1') expect((allLocalesDoc.g1 as any).es).toBeUndefined() + + // Verify deeply nested localization in tab has locale keys only at topmost localized field + expect((allLocalesDoc.t1 as any).en).toBeDefined() + expect((allLocalesDoc.t1 as any).t2).toBeUndefined() + expect((allLocalesDoc.t1 as any).en.t2.text).toBe('EN Deep Text') + expect((allLocalesDoc.t1 as any).es).toBeUndefined() }) }) }) diff --git a/test/localization/payload-types.ts b/test/localization/payload-types.ts index 2770a4b7949..e054f11b399 100644 --- a/test/localization/payload-types.ts +++ b/test/localization/payload-types.ts @@ -433,6 +433,11 @@ export interface AllFieldsLocalized { localizedInNonLocalizedTab?: string | null; }; unnamedTabLocalizedText?: string | null; + t1?: { + t2?: { + text?: string | null; + }; + }; g1?: { g2?: { g2a1?: @@ -1224,6 +1229,15 @@ export interface AllFieldsLocalizedSelect { localizedInNonLocalizedTab?: T; }; unnamedTabLocalizedText?: T; + t1?: + | T + | { + t2?: + | T + | { + text?: T; + }; + }; g1?: | T | { @@ -1736,6 +1750,6 @@ export interface Auth { declare module 'payload' { - // @ts-ignore + // @ts-ignore export interface GeneratedTypes extends Config {} -} \ No newline at end of file +}