@@ -99,6 +99,36 @@ const mapLocales = ({ masterLocale, locale, locales, isNull = false }: any) => {
9999 }
100100}
101101
102+ function resolveEntryFieldKey ( entry : Record < string , unknown > , baseKey : string ) : string | undefined {
103+ if ( baseKey in entry ) return baseKey ;
104+ const snake = baseKey . replace ( / ( [ A - Z ] ) / g, ( m ) => `_${ m . toLowerCase ( ) } ` ) ;
105+ if ( snake in entry ) return snake ;
106+ return undefined ;
107+ }
108+
109+ /**
110+ * Maps Contentful content type id → field id → whether that field is localized in the export schema.
111+ * Used so we only fan out values for fields with `localized: false`, not for localized fields that
112+ * happen to have a single locale in the entry (missing translations).
113+ */
114+ function buildContentfulFieldLocalizedByContentType (
115+ contentTypesFromPackage : any [ ]
116+ ) : Map < string , Map < string , boolean > > {
117+ const byCt = new Map < string , Map < string , boolean > > ( ) ;
118+ for ( const ct of contentTypesFromPackage ?? [ ] ) {
119+ const ctId = ct ?. sys ?. id ;
120+ if ( ! ctId ) continue ;
121+ const byField = new Map < string , boolean > ( ) ;
122+ for ( const f of ct ?. fields ?? [ ] ) {
123+ if ( f ?. id != null ) {
124+ byField . set ( f . id , f . localized === true ) ;
125+ }
126+ }
127+ byCt . set ( ctId , byField ) ;
128+ }
129+ return byCt ;
130+ }
131+
102132const transformCloudinaryObject = ( input : any ) => {
103133 const result : any = [ ] ;
104134 if ( ! Array . isArray ( input ) ) {
@@ -777,6 +807,7 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje
777807 const data = await fs . promises . readFile ( packagePath , "utf8" ) ;
778808 const entries = JSON . parse ( data ) ?. entries ;
779809 const content = JSON . parse ( data ) ?. contentTypes ;
810+ const cfFieldLocalizedByCt = buildContentfulFieldLocalizedByContentType ( content ) ;
780811 const LocaleMapper = { masterLocale : project ?. master_locale ?? LOCALE_MAPPER ?. masterLocale , ...project ?. locales ?? { } } ;
781812 if ( entries && entries . length > 0 ) {
782813 const assetId = await readFile ( assetsSave , ASSETS_SCHEMA_FILE ) ?? [ ] ;
@@ -814,7 +845,7 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje
814845 entryData [ name ] [ lang ] ??= { } ;
815846 entryData [ name ] [ lang ] [ id ] ??= { } ;
816847 locales . push ( lang ) ;
817- const fieldData = currentCT ?. fieldMapping ?. find ?.( ( item : any ) => ( key === item ?. uid ) && ( ! [ "text" , "url" ] ?. includes ?. ( item ?. backupFieldType ) ) ) ;
848+ const fieldData = currentCT ?. fieldMapping ?. find ?.( ( item : any ) => key === item ?. uid ) ;
818849 const newId = fieldData ?. contentstackFieldUid ?? `${ key } ` ?. replace ?.( / [ ^ a - z A - Z 0 - 9 ] + / g, "_" ) ;
819850 entryData [ name ] [ lang ] [ id ] [ newId ] = processField (
820851 langValue ,
@@ -860,18 +891,73 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje
860891 ) ;
861892 } ) ;
862893 } ) ;
894+
895+ // Non-localized Contentful fields (`localized: false` in the content type) only appear under
896+ // one locale in exports. Copy them to every other locale branch so each slice is complete.
897+ // Do not infer non-localized-ness from a single locale key — localized fields can legitimately
898+ // have only one locale when translations are missing.
899+ const entryLocaleKeys = new Set < string > ( ) ;
900+ for ( const [ , v ] of Object ?. entries ?.( fields ) ) {
901+ for ( const lang of Object . keys ( v as object ) ) {
902+ entryLocaleKeys . add ( lang ) ;
903+ }
904+ }
905+ const ct = contentTypes ?. find ( ( c : any ) => c ?. otherCmsUid === name ) ;
906+ for ( const [ key , value ] of Object ?. entries ?.( fields ) ) {
907+ const langs = Object ?. keys ( value as object ) ;
908+ if ( langs ?. length !== 1 ) continue ;
909+ const fd = ct ?. fieldMapping ?. find ?.( ( item : any ) => key === item ?. uid ) ;
910+ const localizedInCf = cfFieldLocalizedByCt . get ( name ) ?. get ( key ) ;
911+ const explicitlyNonLocalized =
912+ localizedInCf === false ||
913+ ( localizedInCf === undefined && fd ?. advanced ?. nonLocalizable === true ) ;
914+ if ( ! explicitlyNonLocalized ) continue ;
915+ const srcLang = langs [ 0 ] ;
916+ const newId = fd ?. contentstackFieldUid ?? `${ key } ` ?. replace ?.( / [ ^ a - z A - Z 0 - 9 ] + / g, "_" ) ;
917+ const srcEntry = entryData [ name ] [ srcLang ] ?. [ id ] as Record < string , unknown > | undefined ;
918+ if ( ! srcEntry ) continue ;
919+ const fk = resolveEntryFieldKey ( srcEntry , newId ) ;
920+ if ( fk === undefined ) continue ;
921+ for ( const tgtLang of entryLocaleKeys ) {
922+ if ( tgtLang === srcLang ) continue ;
923+ entryData [ name ] [ tgtLang ] ??= { } ;
924+ entryData [ name ] [ tgtLang ] [ id ] ??= { } ;
925+ const tgt = entryData [ name ] [ tgtLang ] [ id ] as Record < string , unknown > ;
926+ if ( tgt [ fk ] === undefined ) {
927+ tgt [ fk ] = srcEntry [ fk ] ;
928+ }
929+ }
930+ }
931+
863932 return entryData ;
864933 } ,
865934 { }
866935 ) ;
867936 for await ( const [ newKey , values ] of Object . entries ( result ) ) {
868937 const currentCT = contentTypes ?. find ( ( ct : any ) => ct ?. otherCmsUid === newKey ) ;
869938 const ctName = currentCT ?. contentstackUid in mapperKeys ?
870- mapperKeys ?. [ currentCT ?. contentstackUid ] : ( currentCT ?. contentstackUid ?? newKey . replace ( / ( [ A - Z ] ) / g, "_$1" ) . toLowerCase ( ) ) ;
871- for await ( const [ localeKey , localeValues ] of Object . entries (
872- values as { [ key : string ] : any }
873- ) ) {
874- const localeCode = mapLocales ( { masterLocale : master_locale , locale : localeKey , locales : LocaleMapper , isNull : true } ) ;
939+ mapperKeys ?. [ currentCT ?. contentstackUid ] : ( currentCT ?. contentstackUid ?? newKey ?. replace ?.( / ( [ A - Z ] ) / g, "_$1" ) ?. toLowerCase ?.( ) ) ;
940+ const valuesByCfLocale = values as { [ key : string ] : { [ uid : string ] : Record < string , unknown > } } ;
941+ const mergedByDestinationLocale : { [ localeCode : string ] : { [ uid : string ] : Record < string , unknown > } } = { } ;
942+ for ( const localeKey of Object . keys ( valuesByCfLocale ) ) {
943+ const localeValues = valuesByCfLocale [ localeKey ] ;
944+ if ( ! localeValues ) continue ;
945+ const localeCode = mapLocales ( {
946+ masterLocale : master_locale ,
947+ locale : localeKey ,
948+ locales : LocaleMapper ,
949+ isNull : true ,
950+ } ) ;
951+ if ( ! localeCode ) continue ;
952+ mergedByDestinationLocale [ localeCode ] ??= { } ;
953+ for ( const [ uid , entry ] of Object . entries ( localeValues ) ) {
954+ mergedByDestinationLocale [ localeCode ] [ uid ] = {
955+ ...( mergedByDestinationLocale [ localeCode ] [ uid ] ?? { } ) ,
956+ ...( entry ?? { } ) ,
957+ } ;
958+ }
959+ }
960+ for await ( const [ localeCode , localeValues ] of Object . entries ( mergedByDestinationLocale ) ) {
875961 const chunks = makeChunks ( localeValues ) ;
876962 for ( const [ entryKey , entryValue ] of Object . entries ( localeValues ) ) {
877963 const message = getLogMessage (
@@ -883,18 +969,12 @@ const createEntry = async (packagePath: any, destination_stack_id: string, proje
883969 }
884970 const refs : { [ key : string ] : any } = { } ;
885971 let chunkIndex = 1 ;
886- if ( localeCode ) {
887- const filePath = path . join (
888- entriesSave ,
889- ctName ,
890- localeCode
891- ) ;
892- for await ( const [ chunkId , chunkData ] of Object . entries ( chunks ) ) {
893- refs [ chunkIndex ++ ] = `${ chunkId } -entries.json` ;
894- await writeFile ( filePath , `${ chunkId } -entries.json` , chunkData ) ;
895- }
896- await writeFile ( filePath , ENTRIES_MASTER_FILE , refs ) ;
972+ const filePath = path . join ( entriesSave , ctName , localeCode ) ;
973+ for await ( const [ chunkId , chunkData ] of Object . entries ( chunks ) ) {
974+ refs [ chunkIndex ++ ] = `${ chunkId } -entries.json` ;
975+ await writeFile ( filePath , `${ chunkId } -entries.json` , chunkData ) ;
897976 }
977+ await writeFile ( filePath , ENTRIES_MASTER_FILE , refs ) ;
898978 }
899979 }
900980 } else {
0 commit comments