2020
2121/* eslint-env node */
2222const fs = require ( 'fs' ) ;
23+ const path = require ( 'path' ) ;
2324const CORE_AI_TRANSLATE_API_KEY = process . env . CORE_AI_TRANSLATE_API_KEY ;
2425
2526// A global accumulator object initialized to zero
@@ -83,7 +84,7 @@ function aggregateUtilizationMetrics(obj) {
8384const translationContext =
8485`This is a bunch of strings extracted from a JavaScript file used to develop our product with is a text editor.
8586Some strings may have HTML or templates(mustache library used).
86- The brand name “ Phoenix Pro” must remain in English and should never be translated.
87+ The brand name " Phoenix Pro" must remain in English and should never be translated.
8788Please translate these strings accurately.
8889` ;
8990
@@ -131,11 +132,11 @@ async function getTranslation(apiInput) {
131132 }
132133}
133134
134- function _getAllNLSFolders ( ) {
135- let names = fs . readdirSync ( 'src/nls' ) ;
135+ function _getAllNLSFolders ( nlsDir ) {
136+ let names = fs . readdirSync ( nlsDir ) ;
136137 let nlsFolders = [ ] ;
137138 for ( let name of names ) {
138- let stat = fs . statSync ( `src/nls/ ${ name } ` ) ;
139+ let stat = fs . statSync ( path . join ( nlsDir , name ) ) ;
139140 if ( stat . isDirectory ( ) ) {
140141 nlsFolders . push ( name ) ;
141142 }
@@ -180,18 +181,18 @@ const FILE_HEADER = `/*
180181 */
181182
182183define(` ,
183- FILE_FOOTER = ');' ;
184+ FILE_FOOTER = ');' ;
184185
185- function _isTranslatableKey ( key ) {
186+ function _isTranslatableKey ( key , sourceStrings ) {
186187 const doNotTranslateDirective = '_DO_NOT_TRANSLATE' ;
187188 const translationDisabledForKey = `${ key } ${ doNotTranslateDirective } ` ;
188- if ( key . endsWith ( doNotTranslateDirective ) || rootStrings [ translationDisabledForKey ] === 'true' ) {
189+ if ( key . endsWith ( doNotTranslateDirective ) || sourceStrings [ translationDisabledForKey ] === 'true' ) {
189190 return false ;
190191 }
191192 return true ;
192193}
193194
194- async function coreAiTranslate ( stringsToTranslate , lang ) {
195+ async function coreAiTranslate ( stringsToTranslate , lang , errorsFile ) {
195196 if ( ! Object . keys ( stringsToTranslate ) . length ) {
196197 return { } ;
197198 }
@@ -203,7 +204,7 @@ async function coreAiTranslate(stringsToTranslate, lang) {
203204 if ( translations . failedLanguages . length ) {
204205 const errorStr = `Error translating ${ lang } . it has failures ` ;
205206 console . error ( errorStr ) ;
206- fs . writeFileSync ( `src/nls/errors.txt` , errorStr ) ;
207+ fs . writeFileSync ( errorsFile , errorStr ) ;
207208 // this is oke to continue in case of partial translations.
208209 }
209210 let translationForLanguage = translations . translations [ lang ] ;
@@ -214,7 +215,7 @@ async function coreAiTranslate(stringsToTranslate, lang) {
214215 if ( ! translationForLanguage ) {
215216 const errorStr = `Error translating. AI response doesnt have the language ${ lang } translated!` ;
216217 console . error ( errorStr ) ;
217- fs . writeFileSync ( `src/nls/errors.txt` , errorStr ) ;
218+ fs . writeFileSync ( errorsFile , errorStr ) ;
218219 return { } ;
219220 }
220221 return translationForLanguage ;
@@ -272,24 +273,35 @@ function getSortedObject(obj) {
272273 *
273274 * Finally, we update all the autogenerated translations to disk.
274275 *
275- * @param lang
276+ * @param {string } lang - locale code
277+ * @param {object } config - { nlsDir, sourceStrings, format, errorsFile }
276278 * @return {Promise<void> }
277279 * @private
278280 */
279- async function _processLang ( lang ) {
281+ async function _processLang ( lang , config ) {
280282 if ( lang === 'root' ) {
281283 return ;
282284 }
283- const expertTranslations = _getJson ( `src/nls/${ lang } /expertTranslations.json` , 'utf8' ) ;
284- let lastTranslated = _getJson ( `src/nls/${ lang } /lastTranslated.json` , 'utf8' ) ;
285- require ( `../src/nls/${ lang } /strings` ) ;
286- let localeStringsJS = requireDefinedStrings ;
285+ const { nlsDir, sourceStrings, format, errorsFile } = config ;
286+ const langDir = path . join ( nlsDir , lang ) ;
287+
288+ const expertTranslations = _getJson ( path . join ( langDir , 'expertTranslations.json' ) ) ;
289+ let lastTranslated = _getJson ( path . join ( langDir , 'lastTranslated.json' ) ) ;
290+
291+ let localeStringsJS ;
292+ if ( format === 'json' ) {
293+ localeStringsJS = _getJson ( path . join ( langDir , 'strings.json' ) ) ;
294+ } else {
295+ require ( path . resolve ( langDir , 'strings' ) ) ;
296+ localeStringsJS = requireDefinedStrings ;
297+ }
298+
287299 let translations = { } , updatedLastTranslatedJSON = { } , pendingTranslate = { } ;
288- for ( let rootKey of Object . keys ( rootStrings ) ) {
289- if ( ! _isTranslatableKey ( rootKey ) ) {
300+ for ( let rootKey of Object . keys ( sourceStrings ) ) {
301+ if ( ! _isTranslatableKey ( rootKey , sourceStrings ) ) {
290302 continue ; // move on to next string
291303 }
292- let englishStringToTranslate = rootStrings [ rootKey ] ;
304+ let englishStringToTranslate = sourceStrings [ rootKey ] ;
293305 let lastTranslatedEnglishString = lastTranslated [ rootKey ] ;
294306 if ( englishStringToTranslate === lastTranslatedEnglishString ) {
295307 // we have already translated this in the last pass.
@@ -316,54 +328,169 @@ async function _processLang(lang) {
316328 }
317329 //let translatedText = await _translateString(englishStringToTranslate, lang);
318330 console . log ( `Translating ${ Object . keys ( pendingTranslate ) . length } strings to` , lang ) ;
319- const aiTranslations = await coreAiTranslate ( pendingTranslate , lang ) ;
320- const allRootKeys = new Set ( Object . keys ( rootStrings ) ) ;
331+ const aiTranslations = await coreAiTranslate ( pendingTranslate , lang , errorsFile ) ;
332+ const allRootKeys = new Set ( Object . keys ( sourceStrings ) ) ;
321333 for ( let rootKey of Object . keys ( pendingTranslate ) ) {
322334 if ( ! allRootKeys . has ( rootKey ) ) {
323335 // AI hallucinated a root key?
324336 const errorStr = `AI translated for a root key that doesnt exist!!! in ${ lang } : ${ rootKey } \nTranslation: ${ aiTranslations [ rootKey ] } ` ;
325337 console . error ( errorStr ) ;
326- fs . writeFileSync ( `src/nls/errors.txt` , errorStr ) ;
338+ fs . writeFileSync ( errorsFile , errorStr ) ;
327339 continue ;
328340 }
329- let englishStringToTranslate = rootStrings [ rootKey ] ;
341+ let englishStringToTranslate = sourceStrings [ rootKey ] ;
330342 const translatedText = aiTranslations [ rootKey ] ;
331343 if ( translatedText ) {
332344 translations [ rootKey ] = translatedText ;
333345 updatedLastTranslatedJSON [ rootKey ] = englishStringToTranslate ;
334346 }
335347 }
336348 // now detect any keys that has not yet been translated
337- const allKeys = Object . keys ( rootStrings ) . filter ( _isTranslatableKey ) ;
349+ const allKeys = Object . keys ( sourceStrings ) . filter ( k => _isTranslatableKey ( k , sourceStrings ) ) ;
338350 const translatedKeys = Object . keys ( translations ) ;
339351 const notTranslated = allKeys . filter ( key => ! translatedKeys . includes ( key ) ) ;
340352 if ( notTranslated . length ) {
341353 const errorStr = `Some strings not translated in ${ lang } \n${ notTranslated } ` ;
342354 console . error ( errorStr ) ;
343- fs . writeFileSync ( `src/nls/errors.txt` , errorStr ) ;
355+ fs . writeFileSync ( errorsFile , errorStr ) ;
344356 }
345357
346- let translatedStringsJSON = JSON . stringify ( translations , null , 2 ) ;
347- let fileToWrite = `${ FILE_HEADER } ${ translatedStringsJSON } ${ FILE_FOOTER } ` ;
348- if ( ! shallowEqual ( translations , localeStringsJS ) ) {
349- fs . writeFileSync ( `src/nls/${ lang } /strings.js` , fileToWrite ) ;
358+ if ( format === 'json' ) {
359+ // Write plain JSON
360+ if ( ! shallowEqual ( translations , localeStringsJS ) ) {
361+ fs . writeFileSync ( path . join ( langDir , 'strings.json' ) ,
362+ JSON . stringify ( translations , null , 2 ) ) ;
363+ }
364+ } else {
365+ // Write define()-wrapped JS
366+ let translatedStringsJSON = JSON . stringify ( translations , null , 2 ) ;
367+ let fileToWrite = `${ FILE_HEADER } ${ translatedStringsJSON } ${ FILE_FOOTER } ` ;
368+ if ( ! shallowEqual ( translations , localeStringsJS ) ) {
369+ fs . writeFileSync ( path . join ( langDir , 'strings.js' ) , fileToWrite ) ;
370+ }
350371 }
351372 if ( ! shallowEqual ( updatedLastTranslatedJSON , lastTranslated ) ) {
352373 const sortedList = getSortedObject ( updatedLastTranslatedJSON ) ;
353- fs . writeFileSync ( `src/nls/${ lang } /lastTranslated.json` , JSON . stringify ( sortedList , null , 2 ) ) ;
374+ fs . writeFileSync ( path . join ( langDir , 'lastTranslated.json' ) ,
375+ JSON . stringify ( sortedList , null , 2 ) ) ;
354376 }
355377}
356378
379+ // ---- Phoenix NLS translation ----
380+
357381async function translate ( ) {
358382 console . log ( "please make sure that core.ai lang translation service credentials are available as env vars." ) ;
359383 return new Promise ( async ( resolve ) => {
360- let langs = _getAllNLSFolders ( ) ;
384+ const nlsDir = 'src/nls' ;
385+ let langs = _getAllNLSFolders ( nlsDir ) ;
361386 console . log ( langs ) ;
387+ const config = {
388+ nlsDir,
389+ sourceStrings : rootStrings ,
390+ format : 'js' ,
391+ errorsFile : 'src/nls/errors.txt'
392+ } ;
362393 for ( let lang of langs ) {
363- await _processLang ( lang ) ;
394+ await _processLang ( lang , config ) ;
364395 }
365396 resolve ( ) ;
366397 } ) ;
367398}
368399
400+ // ---- mdviewer translation ----
401+
402+ /**
403+ * Flatten a nested object into dot-notation keys.
404+ * { toolbar: { done: "Done" } } → { "toolbar.done": "Done" }
405+ */
406+ function _flattenObject ( obj , prefix = '' ) {
407+ const result = { } ;
408+ for ( const key of Object . keys ( obj ) ) {
409+ const fullKey = prefix ? `${ prefix } .${ key } ` : key ;
410+ if ( typeof obj [ key ] === 'object' && obj [ key ] !== null && ! Array . isArray ( obj [ key ] ) ) {
411+ Object . assign ( result , _flattenObject ( obj [ key ] , fullKey ) ) ;
412+ } else {
413+ result [ fullKey ] = obj [ key ] ;
414+ }
415+ }
416+ return result ;
417+ }
418+
419+ /**
420+ * Unflatten dot-notation keys back into a nested object.
421+ * { "toolbar.done": "Done" } → { toolbar: { done: "Done" } }
422+ */
423+ function _unflattenObject ( obj ) {
424+ const result = { } ;
425+ for ( const flatKey of Object . keys ( obj ) ) {
426+ const parts = flatKey . split ( '.' ) ;
427+ let current = result ;
428+ for ( let i = 0 ; i < parts . length - 1 ; i ++ ) {
429+ if ( ! current [ parts [ i ] ] || typeof current [ parts [ i ] ] !== 'object' ) {
430+ current [ parts [ i ] ] = { } ;
431+ }
432+ current = current [ parts [ i ] ] ;
433+ }
434+ current [ parts [ parts . length - 1 ] ] = obj [ flatKey ] ;
435+ }
436+ return result ;
437+ }
438+
439+ async function translateMdviewer ( ) {
440+ const mdNlsDir = 'src-mdviewer/src/md-nls-autogenerated' ;
441+ const localesDir = 'src-mdviewer/src/locales' ;
442+ const enJsonPath = path . join ( localesDir , 'en.json' ) ;
443+ const rootStringsPath = path . join ( mdNlsDir , 'root' , 'strings.json' ) ;
444+
445+ if ( ! fs . existsSync ( enJsonPath ) ) {
446+ console . log ( "[mdviewer] en.json not found, skipping mdviewer translation." ) ;
447+ return ;
448+ }
449+
450+ // Flatten en.json → root/strings.json (source of truth → NLS format)
451+ const enNested = _getJson ( enJsonPath ) ;
452+ const enFlat = getSortedObject ( _flattenObject ( enNested ) ) ;
453+ if ( ! fs . existsSync ( path . join ( mdNlsDir , 'root' ) ) ) {
454+ fs . mkdirSync ( path . join ( mdNlsDir , 'root' ) , { recursive : true } ) ;
455+ }
456+ fs . writeFileSync ( rootStringsPath , JSON . stringify ( enFlat , null , 2 ) ) ;
457+ console . log ( `[mdviewer] Flattened en.json → root/strings.json (${ Object . keys ( enFlat ) . length } keys)` ) ;
458+
459+ const mdRootStrings = enFlat ;
460+
461+ console . log ( "[mdviewer] Starting mdviewer locale translation..." ) ;
462+ let langs = _getAllNLSFolders ( mdNlsDir ) ;
463+ console . log ( "[mdviewer] Locales:" , langs ) ;
464+
465+ const config = {
466+ nlsDir : mdNlsDir ,
467+ sourceStrings : mdRootStrings ,
468+ format : 'json' ,
469+ errorsFile : path . join ( mdNlsDir , 'errors.txt' )
470+ } ;
471+
472+ for ( let lang of langs ) {
473+ await _processLang ( lang , config ) ;
474+ }
475+
476+ // Copy translated flat JSON → nested locale files for mdviewer runtime
477+ console . log ( "[mdviewer] Copying translations to locales folder..." ) ;
478+ for ( let lang of langs ) {
479+ if ( lang === 'root' ) continue ;
480+ const flatStrings = _getJson ( path . join ( mdNlsDir , lang , 'strings.json' ) ) ;
481+ if ( ! Object . keys ( flatStrings ) . length ) continue ;
482+ const nested = _unflattenObject ( flatStrings ) ;
483+ fs . writeFileSync (
484+ path . join ( localesDir , `${ lang } .json` ) ,
485+ JSON . stringify ( nested , null , 2 ) + '\n'
486+ ) ;
487+ }
488+
489+ console . log ( "[mdviewer] mdviewer locale translation complete." ) ;
490+ }
491+
369492exports . translate = translate ;
493+ exports . translateMdviewer = translateMdviewer ;
494+ exports . coreAiTranslate = coreAiTranslate ;
495+ exports . shallowEqual = shallowEqual ;
496+ exports . getSortedObject = getSortedObject ;
0 commit comments