@@ -2,32 +2,48 @@ import { join } from 'path';
22import omit from 'lodash/omit' ;
33import isEmpty from 'lodash/isEmpty' ;
44
5- import { log , fsUtil } from '../../utils' ;
5+ import { log , fsUtil , fileHelper } from '../../utils' ;
66import { ImportConfig , ModuleClassParams , TaxonomyQueryParams } from '../../types' ;
77import { sanitizePath } from '@contentstack/cli-utilities' ;
88
99export default class TaxonomiesImportSetup {
1010 private config : ImportConfig ;
1111 private taxonomiesFilePath : string ;
12+ private taxonomiesFolderPath : string ;
1213 private stackAPIClient : ModuleClassParams [ 'stackAPIClient' ] ;
1314 private dependencies : ModuleClassParams [ 'dependencies' ] ;
1415 private taxonomiesConfig : ImportConfig [ 'modules' ] [ 'taxonomies' ] ;
1516 private termsSuccessPath : string ;
1617 private taxSuccessPath : string ;
1718 private taxonomiesMapperDirPath : string ;
1819 private termsMapperDirPath : string ;
20+ private localesFilePath : string ;
21+ private isLocaleBasedStructure : boolean = false ;
1922 public taxonomiesMapper : Record < string , unknown > = { } ;
2023 public termsMapper : Record < string , unknown > = { } ;
24+ public masterLocaleFilePath : string ;
2125
2226 constructor ( { config, stackAPIClient } : ModuleClassParams ) {
2327 this . config = config ;
2428 this . stackAPIClient = stackAPIClient ;
25- this . taxonomiesFilePath = join ( sanitizePath ( this . config . contentDir ) , 'taxonomies' , 'taxonomies.json' ) ;
29+ this . taxonomiesFolderPath = join ( sanitizePath ( this . config . contentDir ) , 'taxonomies' ) ;
30+ this . taxonomiesFilePath = join ( this . taxonomiesFolderPath , 'taxonomies.json' ) ;
2631 this . taxonomiesConfig = config . modules . taxonomies ;
2732 this . taxonomiesMapperDirPath = join ( sanitizePath ( this . config . backupDir ) , 'mapper' , 'taxonomies' ) ;
2833 this . taxSuccessPath = join ( sanitizePath ( this . taxonomiesMapperDirPath ) , 'success.json' ) ;
2934 this . termsMapperDirPath = join ( sanitizePath ( this . taxonomiesMapperDirPath ) , 'terms' ) ;
3035 this . termsSuccessPath = join ( sanitizePath ( this . termsMapperDirPath ) , 'success.json' ) ;
36+ this . localesFilePath = join (
37+ sanitizePath ( this . config . contentDir ) ,
38+ config . modules . locales ?. dirName || 'locales' ,
39+ config . modules . locales ?. fileName || 'locales.json' ,
40+ ) ;
41+ this . masterLocaleFilePath = join (
42+ sanitizePath ( this . config . contentDir ) ,
43+ config . modules . locales ?. dirName || 'locales' ,
44+ 'master-locale.json' ,
45+ ) ;
46+
3147 this . taxonomiesMapper = { } ;
3248 this . termsMapper = { } ;
3349 }
@@ -41,21 +57,19 @@ export default class TaxonomiesImportSetup {
4157 try {
4258 const taxonomies : any = fsUtil . readFile ( this . taxonomiesFilePath ) ;
4359 if ( ! isEmpty ( taxonomies ) ) {
60+ // 1. Detect locale-based structure
61+ this . isLocaleBasedStructure = this . detectLocaleBasedStructure ( ) ;
62+
4463 // 2. Create mapper directory
4564 fsUtil . makeDirectory ( this . taxonomiesMapperDirPath ) ;
4665 fsUtil . makeDirectory ( this . termsMapperDirPath ) ;
4766
48- for ( const taxonomy of Object . values ( taxonomies ) as any ) {
49- let targetTaxonomy : any = await this . getTaxonomies ( taxonomy ) ;
50- if ( ! targetTaxonomy ) {
51- log ( this . config , `Taxonomies with uid '${ taxonomy . uid } ' not found in the stack!` , 'info' ) ;
52- continue ;
53- }
54- targetTaxonomy = this . sanitizeTaxonomyAttribs ( targetTaxonomy ) ;
55- this . taxonomiesMapper [ taxonomy . uid ] = targetTaxonomy ;
56- const terms = await this . getAllTermsOfTaxonomy ( targetTaxonomy ) ;
57- const sanitizedTerms = this . sanitizeTermsAttribs ( terms ) ;
58- this . termsMapper [ taxonomy . uid ] = sanitizedTerms ;
67+ if ( this . isLocaleBasedStructure ) {
68+ log ( this . config , 'Detected locale-based folder structure for taxonomies' , 'info' ) ;
69+ await this . setupTaxonomiesByLocale ( taxonomies ) ;
70+ } else {
71+ log ( this . config , 'Using legacy folder structure for taxonomies' , 'info' ) ;
72+ await this . setupTaxonomiesLegacy ( taxonomies ) ;
5973 }
6074
6175 if ( this . taxonomiesMapper !== undefined && ! isEmpty ( this . taxonomiesMapper ) ) {
@@ -74,18 +88,176 @@ export default class TaxonomiesImportSetup {
7488 }
7589 }
7690
91+ /**
92+ * Setup taxonomies using legacy format (root-level taxonomy files)
93+ */
94+ async setupTaxonomiesLegacy ( taxonomies : any ) : Promise < void > {
95+ for ( const taxonomy of Object . values ( taxonomies ) as any ) {
96+ let targetTaxonomy : any = await this . getTaxonomies ( taxonomy ) ;
97+ if ( ! targetTaxonomy ) {
98+ log ( this . config , `Taxonomies with uid '${ taxonomy . uid } ' not found in the stack!` , 'info' ) ;
99+ continue ;
100+ }
101+ targetTaxonomy = this . sanitizeTaxonomyAttribs ( targetTaxonomy ) ;
102+ this . taxonomiesMapper [ taxonomy . uid ] = targetTaxonomy ;
103+ const terms = await this . getAllTermsOfTaxonomy ( targetTaxonomy ) ;
104+ if ( Array . isArray ( terms ) && terms . length > 0 ) {
105+ log ( this . config , `Terms found for taxonomy '${ taxonomy . uid } ', processing...` , 'info' ) ;
106+ const sanitizedTerms = this . sanitizeTermsAttribs ( terms ) ;
107+ this . termsMapper [ taxonomy . uid ] = sanitizedTerms ;
108+ } else {
109+ log ( this . config , `No terms found for taxonomy '${ taxonomy . uid } ', skipping...` , 'info' ) ;
110+ }
111+ }
112+ }
113+
114+ /**
115+ * Setup taxonomies using locale-based format (taxonomies organized by locale)
116+ * For locale-based structure, we query the target stack for each taxonomy+locale combination
117+ */
118+ async setupTaxonomiesByLocale ( taxonomies : any ) : Promise < void > {
119+ const locales = this . loadAvailableLocales ( ) ;
120+
121+ for ( const localeCode of Object . keys ( locales ) ) {
122+ log ( this . config , `Processing taxonomies for locale: ${ localeCode } ` , 'info' ) ;
123+
124+ for ( const taxonomy of Object . values ( taxonomies ) as any ) {
125+ // Query target stack for this taxonomy in this locale
126+ let targetTaxonomy : any = await this . getTaxonomies ( taxonomy , localeCode ) ;
127+ if ( ! targetTaxonomy ) {
128+ log ( this . config , `Taxonomy '${ taxonomy . uid } ' not found in target stack for locale: ${ localeCode } ` , 'info' ) ;
129+ continue ;
130+ }
131+
132+ targetTaxonomy = this . sanitizeTaxonomyAttribs ( targetTaxonomy ) ;
133+
134+ // Store with composite key: taxonomyUID_locale
135+ // const mapperKey = `${taxonomy.uid}_${localeCode}`; // TODO: Unsure about this required or not
136+ this . taxonomiesMapper [ taxonomy . uid ] = targetTaxonomy ;
137+ const terms = await this . getAllTermsOfTaxonomy ( targetTaxonomy , localeCode ) ;
138+ if ( Array . isArray ( terms ) && terms . length > 0 ) {
139+ log (
140+ this . config ,
141+ `Terms found for taxonomy '${ taxonomy . uid } for locale: ${ localeCode } ', processing...` ,
142+ 'info' ,
143+ ) ;
144+ const sanitizedTerms = this . sanitizeTermsAttribs ( terms ) ;
145+ this . termsMapper [ taxonomy . uid ] = sanitizedTerms ;
146+ } else {
147+ log (
148+ this . config ,
149+ `No terms found for taxonomy '${ taxonomy . uid } for locale: ${ localeCode } ', skipping...` ,
150+ 'info' ,
151+ ) ;
152+ }
153+ }
154+ }
155+ }
156+
157+ /**
158+ * Detect if locale-based folder structure exists
159+ * @returns {boolean } true if locale-based structure detected, false otherwise
160+ */
161+ detectLocaleBasedStructure ( ) : boolean {
162+ const masterLocaleCode = this . getMasterLocaleCode ( ) ;
163+ const masterLocaleFolder = join ( this . taxonomiesFolderPath , masterLocaleCode ) ;
164+
165+ // Check if master locale folder exists (indicates new locale-based structure)
166+ if ( ! fileHelper . fileExistsSync ( masterLocaleFolder ) ) {
167+ log ( this . config , 'No locale-based folder structure detected' , 'info' ) ;
168+ return false ;
169+ }
170+
171+ log ( this . config , 'Locale-based folder structure detected' , 'info' ) ;
172+ return true ;
173+ }
174+
175+ /**
176+ * Get the master locale code
177+ * First tries to read from master-locale.json, then falls back to config, then 'en-us'
178+ * @returns {string } The master locale code
179+ */
180+ getMasterLocaleCode ( ) : string {
181+ // Try to read from master-locale.json file
182+ if ( fileHelper . fileExistsSync ( this . masterLocaleFilePath ) ) {
183+ try {
184+ const masterLocaleData = fsUtil . readFile ( this . masterLocaleFilePath , true ) as Record <
185+ string ,
186+ Record < string , any >
187+ > ;
188+ // The file contains an object with UID as key, extract the code
189+ const firstLocale = Object . values ( masterLocaleData ) [ 0 ] ;
190+ if ( firstLocale ?. code ) {
191+ log ( this . config , `Master locale loaded from file: ${ firstLocale . code } ` , 'info' ) ;
192+ return firstLocale . code ;
193+ }
194+ } catch ( error ) {
195+ log ( this . config , 'Error reading master-locale.json, using fallback' , 'warn' ) ;
196+ }
197+ }
198+
199+ // Fallback to config or default
200+ const fallbackCode = this . config . master_locale ?. code || 'en-us' ;
201+ log ( this . config , `Using fallback master locale: ${ fallbackCode } ` , 'info' ) ;
202+ return fallbackCode ;
203+ }
204+
205+ /**
206+ * Load available locales from locales file
207+ * @returns {Record<string, string> } Map of locale codes
208+ */
209+ loadAvailableLocales ( ) : Record < string , string > {
210+ const locales : Record < string , string > = { } ;
211+
212+ // First, get the master locale
213+ const masterLocaleCode = this . getMasterLocaleCode ( ) ;
214+ locales [ masterLocaleCode ] = masterLocaleCode ;
215+
216+ // Then load additional locales from locales.json if it exists
217+ if ( ! fileHelper . fileExistsSync ( this . localesFilePath ) ) {
218+ log ( this . config , 'No locales file found, using only master locale' , 'info' ) ;
219+ return locales ;
220+ }
221+
222+ try {
223+ const localesData = fsUtil . readFile ( this . localesFilePath , true ) as Record < string , Record < string , any > > ;
224+
225+ for ( const [ uid , locale ] of Object . entries ( localesData ) ) {
226+ if ( locale ?. code ) {
227+ locales [ locale . code ] = locale . code ;
228+ }
229+ }
230+
231+ log (
232+ this . config ,
233+ `Loaded ${ Object . keys ( locales ) . length } locales (1 master + ${ Object . keys ( locales ) . length - 1 } additional)` ,
234+ 'info' ,
235+ ) ;
236+ return locales ;
237+ } catch ( error ) {
238+ log ( this . config , 'Error loading locales file, using only master locale' , 'error' ) ;
239+ return locales ;
240+ }
241+ }
242+
77243 /**
78244 * Retrieves the taxonomies based on the provided taxonomy UID.
79245 *
80246 * @param taxonomy - The UID of the taxonomy to retrieve.
247+ * @param locale - Optional locale code to query taxonomy in specific locale
81248 * @returns A promise that resolves to the retrieved taxonomies.
82249 */
83- async getTaxonomies ( taxonomy : any ) : Promise < any > {
250+ async getTaxonomies ( taxonomy : any , locale ?: string ) : Promise < any > {
251+ const query : any = { } ;
252+ if ( locale ) {
253+ query . locale = locale ;
254+ }
255+
84256 return await this . stackAPIClient
85257 . taxonomy ( taxonomy . uid )
86- . fetch ( )
258+ . fetch ( query )
87259 . then ( ( data : any ) => data )
88- . catch ( ( err : any ) => this . handleTaxonomyErrorMsg ( err ) ) ;
260+ . catch ( ( err : any ) => this . handleTaxonomyErrorMsg ( err , taxonomy . uid , locale ) ) ;
89261 }
90262
91263 /**
@@ -102,19 +274,22 @@ export default class TaxonomiesImportSetup {
102274 * Retrieves all terms of a taxonomy.
103275 *
104276 * @param taxonomy - The taxonomy object.
277+ * @param locale - Optional locale code to query terms in specific locale
105278 * @param skip - The number of terms to skip (default: 0).
106279 * @param terms - An array to store the retrieved terms (default: []).
107280 * @returns A promise that resolves to an array of terms.
108281 */
109- async getAllTermsOfTaxonomy ( taxonomy : any , skip = 0 , terms : any [ ] = [ ] ) : Promise < any > {
282+ async getAllTermsOfTaxonomy ( taxonomy : any , locale ?: string , skip = 0 , terms : any [ ] = [ ] ) : Promise < any > {
110283 const queryParams : TaxonomyQueryParams = {
111284 include_count : true ,
112285 limit : 100 ,
113286 skip,
287+ depth : 0 ,
114288 } ;
115289
116- if ( skip >= 0 ) queryParams [ 'skip' ] = skip || 0 ;
117- queryParams [ 'depth' ] = 0 ;
290+ if ( locale ) {
291+ queryParams . locale = locale ;
292+ }
118293
119294 await this . stackAPIClient
120295 . taxonomy ( taxonomy . uid )
@@ -124,10 +299,10 @@ export default class TaxonomiesImportSetup {
124299 . then ( ( data : any ) => {
125300 terms = terms . concat ( data . items ) ;
126301 if ( data . count >= skip + queryParams . limit ) {
127- return this . getAllTermsOfTaxonomy ( taxonomy , skip + 100 , terms ) ;
302+ return this . getAllTermsOfTaxonomy ( taxonomy , locale , skip + 100 , terms ) ;
128303 }
129304 } )
130- . catch ( ( err : any ) => this . handleTaxonomyErrorMsg ( err ) ) ;
305+ . catch ( ( err : any ) => this . handleTaxonomyErrorMsg ( err , taxonomy . uid , locale ) ) ;
131306 return terms ;
132307 }
133308
@@ -144,12 +319,15 @@ export default class TaxonomiesImportSetup {
144319 return terms ;
145320 }
146321
147- handleTaxonomyErrorMsg ( err : any ) {
322+ handleTaxonomyErrorMsg ( err : any , taxonomyUid ?: string , locale ?: string ) {
323+ const context = locale ? ` for locale: ${ locale } ` : '' ;
324+ const taxInfo = taxonomyUid ? ` (${ taxonomyUid } ${ context } )` : '' ;
325+
148326 if ( err ?. errorMessage || err ?. message ) {
149327 const errorMsg = err ?. errorMessage || err ?. errors ?. taxonomy || err ?. errors ?. term || err ?. message ;
150- log ( this . config , errorMsg , 'error' ) ;
328+ log ( this . config , ` ${ errorMsg } ${ taxInfo } ` , 'error' ) ;
151329 } else {
152- log ( this . config , ' Error fetching taxonomy data!' , 'error' ) ;
330+ log ( this . config , ` Error fetching taxonomy data${ taxInfo } !` , 'error' ) ;
153331 log ( this . config , err , 'error' ) ;
154332 }
155333 }
0 commit comments