UNPKG

dynamicsmobile

Version:

Allows development of off-line mobile and web business apps over the Dynamics Mobile platform. More info on https://www.dynamicsmobile.com

739 lines (628 loc) 29.9 kB
const fs = require('fs'); const path = require('path'); const chalk = require('chalk'); function mergeLanguageFile(globalArray, filePath, type, fileMustExists) { if (!globalArray) throw 'globalArray is required'; if (!filePath) throw 'filePath is required' let languageFile; if (fileMustExists) { if (!fs.existsSync(filePath)) { throw new Error(`File ${filePath} does not exists!`); } } if (fs.existsSync(filePath)) languageFile = JSON.parse(fs.readFileSync(filePath, 'utf8')); if (languageFile) { for(const key in languageFile){ const existing = globalArray.find(res2 =>{ return res2.name == key }); if (!existing) { if (type == 'from') { globalArray.push({ name: key, from: languageFile[key], to: '' }); } else { globalArray.push({ name: key, from: '', to: languageFile[key] }); } } else { if (type == 'from') { existing.from = languageFile[key] } else { existing.to = languageFile[key] } } }; } } function readFilesRecursive(srcPath, patternsArray, __allFiles) { const files = fs.readdirSync(srcPath); files.forEach(file => { const stat = fs.lstatSync(path.join(srcPath, file)); if (stat.isDirectory()) { readFilesRecursive(path.join(srcPath, file), patternsArray, __allFiles); } else { patternsArray.forEach(p => { if (file.indexOf(p) >= 0) { __allFiles.push(path.join(srcPath, file)); } }) } }); } const yarg = require("yargs") .usage('Usage: i18n -- <command> [options]') .example('export', 'npm run i18n export -- --from en --to fr --file french.csv') .example('extract', 'npm run i18n extract -- --to fr') .command('export', 'Exports language translation from json to csv', yarg => { return yarg .options("from", { demandOption: true, description: ' source language - the laguage to include as a reference' }) .options("to", { demandOption: true, type: 'string', description: ' target language - language supposed to be translated' }) .options("file", { demandOption: true, type: 'string', description: 'target file path - e.g. the path to the resulting CSV file' }) }, argv => { const outputPath = `./${argv.file}`; const arr = [{ id: "$$$header", name: 'Item', from: argv.from, to: argv.to }]; mergeLanguageFile(arr, `./src/locales/${argv.from}.json`, 'from', true); mergeLanguageFile(arr, `./src/locales/${argv.to}.json`, 'to', false); mergeLanguageFile(arr, `./ext/locales/${argv.from}.json`, 'from', false); mergeLanguageFile(arr, `./ext/locales/${argv.to}.json`, 'to', false); fs.writeFileSync(outputPath, arr.map(element => { return `${element.name},"${element.from}","${element.to}"` }).join('\n'), 'utf8'); }) .command('translate', 'Checks for no translated srings and replaces them with translation resources', yarg => { return yarg.option('save', { default: false, type: 'boolean' }) }, argv => { if (!fs.existsSync('./ext')) { fixNonTranslatedFiles(argv, './src'); } else { fixNonTranslatedFiles(argv, './ext'); fixNonTranslatedFiles(argv, './src'); } }) .command('extract', 'Extracts translation resources from source code and updates the target language file', yarg => { return yarg.options("to", { demandOption: true, type: 'string' }) .option('override', { default: false, type: 'boolean' }) }, argv => { console.log(chalk.white.bgBlue.bold(' DMS '), 'Dynamics Mobile i18n is working...'); if (!fs.existsSync('./ext')) { console.log(chalk.white.bgBlue.bold(' DMS '), chalk.white('Extraction of translation resources from ' + chalk.white.bold('./src/') + ' started...')); extractTranslatableResourceFromExtensionApp(argv, './ext'); extractTranslatableResourceFromExtensionApp(argv, './src'); } else { console.log(chalk.white.bgBlue.bold(' DMS '), chalk.white('Extraction of translation resources from ' + chalk.white.bold('./ext/') + ' started...')); extractTranslatableResourceFromExtensionApp(argv, './ext'); extractTranslatableResourceFromExtensionApp(argv, './src'); } }) .command('import', 'Import language translation from csv', yarg => { return yarg .options("to", { demandOption: true, type: 'string', description: ' target language - language where the translations will be imported' }) .options("file", { demandOption: true, type: 'string', description: 'source file path - e.g. the path to the souce CSV file' }) .options("col", { demandOption: true, type: 'string', description: 'the number of the column, which contains the translation - 0 is the first column' }) }, argv => { importLanguageFile(argv); }) .command('boextract', 'Extracts translation resources from business objects and updates the target language file', yarg => { return yarg.options("to", { demandOption: true, type: 'string' }) .option('override', { default: false, type: 'boolean' }) }, argv => { console.log(chalk.white.bgBlue.bold(' DMS '), 'Dynamics Mobile i18n is working...'); if (!fs.existsSync('./ext')) { console.log(chalk.white.bgBlue.bold(' DMS '), chalk.white('Extraction of BO translation resources from ' + chalk.white.bold('./src/') + ' started...')); extractBusinessObjectTranslatableResourceFromExtensionApp(argv, './ext'); extractBusinessObjectTranslatableResourceFromExtensionApp(argv, './src'); } else { console.log(chalk.white.bgBlue.bold(' DMS '), chalk.white('Extraction of BO translation resources from ' + chalk.white.bold('./ext/') + ' started...')); extractBusinessObjectTranslatableResourceFromExtensionApp(argv, './ext'); extractBusinessObjectTranslatableResourceFromExtensionApp(argv, './src'); } }) .command('menuextract', 'Extracts translation resources from menu and updates the target language file', yarg => { return yarg.options("to", { demandOption: true, type: 'string' }) .option('override', { default: false, type: 'boolean' }) }, argv => { console.log(chalk.white.bgBlue.bold(' DMS '), 'Dynamics Mobile i18n is working...'); if (!fs.existsSync('./ext')) { console.log(chalk.white.bgBlue.bold(' DMS '), chalk.white('Extraction of menu translation resources from ' + chalk.white.bold('./src/') + ' started...')); extractMenuTranslatableResourceFromExtensionApp(argv, './ext'); extractMenuTranslatableResourceFromExtensionApp(argv, './src'); } else { console.log(chalk.white.bgBlue.bold(' DMS '), chalk.white('Extraction of menu translation resources from ' + chalk.white.bold('./ext/') + ' started...')); extractMenuTranslatableResourceFromExtensionApp(argv, './ext'); extractMenuTranslatableResourceFromExtensionApp(argv, './src'); } }) .command('fromlegacy', 'Extracts legacy translation(./Translations) to new translations (./locales) ', yarg => { }, argv => { console.log(chalk.white.bgBlue.bold(' DMS '), 'Dynamics Mobile i18n is working...'); var languages = {}; //move translations into locales //get all translations from the src folder const translations = fs.readdirSync(path.resolve('./src/Translations')); for (const translation of translations) { const translationFilePath = path.resolve('./src/Translations', translation); const translationFile = fs.readFileSync(translationFilePath); const translationContent = JSON.parse(translationFile); const localeCode = translation.split('.')[0]; if (!languages[localeCode]) { languages[localeCode] = {}; } console.log('************', translationContent.resources); translationContent.resources.forEach(element => { languages[localeCode][element.name] = element.text; }); const p = path.resolve(path.resolve('./src/locales'), `${localeCode}.json`); console.log('************', p); fs.writeFileSync(p, JSON.stringify(languages[localeCode])) } console.log(chalk.white.bgBlue.bold(' DMS '), 'Dynamics Mobile i18n work completed...'); }) .demandCommand() .recommendCommands() .showHelpOnFail() .command({ command: '*', handler() { console.log(chalk.white.bgRed(' DMS '), chalk.red('ERROR: Please provide a valid command')); } }) .argv; function importLanguageFile(argv) { const sourceCSVFilePath = argv.file.trim().replace(/"/g, ''); let targetLangFilePath = `./src/locales/${argv.to}.json`; const col = parseInt(argv.col); console.log(chalk.white.bgBlue.bold(' DMS '), chalk.white(`Importing "${sourceCSVFilePath}" into language file "${targetLangFilePath}"`)); if (!fs.existsSync(sourceCSVFilePath)) { console.log(chalk.white.bgRed.bold(' DMS '), chalk.white(`Source CSV file "${sourceCSVFilePath}" does not exists!"`)); process.exit(1); } if (fs.existsSync('./ext/locales')) targetLangFilePath = `./ext/locales/${argv.to}.json`; if (!col || col <= 0) { console.log(chalk.white.bgRed.bold(' DMS '), chalk.white(`Argument "col" must be integer greater than 0`)); process.exit(1); } //load target language, if any let targetLanguageContent = {}; if (fs.existsSync(targetLangFilePath)) targetLanguageContent = JSON.parse(fs.readFileSync(targetLangFilePath)); //load csv const csvText = fs.readFileSync(sourceCSVFilePath, 'utf8'); const csvLines = csvText.split('\n'); let newResourcesCount = 0; let existingResourcesCount = 0; csvLines.forEach((line, lineIndex) => { if (lineIndex == 0) return; const columns = line.split(','); let resourceId; let translated; columns.forEach((column, colIndex) => { const colText = column.trim().replace(/"/g, ''); if (colIndex == 0) { resourceId = colText; } if (colIndex == col) { translated = colText; } }); if (resourceId && translated) { const targetItem = targetLanguageContent[resourceId]; if (targetItem) { targetItem = translated; existingResourcesCount++; } else { newResourcesCount++ targetLanguageContent[resourceId]= translated; } } }); console.log(chalk.white.bgBlue.bold(' DMS '), chalk.white(`Importing completed to lang "${argv.to}". Added resources: ${newResourcesCount}, Updated resources: ${existingResourcesCount}`)); if (argv.dump) { console.log('Dumping resources:'); console.log(`#\tid\t\t\t${argv.to}`); console.log(`----------------------------------`); for(const key in targetLanguageContent){ console.log(`${key}\t${targetLanguageContent[key]}`); } } fs.writeFileSync(targetLangFilePath, JSON.stringify(targetLanguageContent)); } //find non translated strings in all HTML files and shows them in the console //optionally replaces them in the files function fixNonTranslatedFiles(argv, srcPath) { if (!fs.existsSync(srcPath)) return; const files = []; readFilesRecursive(srcPath, ['.ts', '.html'], files); //const regex2 = new RegExp('>[a-zA-Z0-9]+</[^i]', 'g'); // ([a-zA-Z\u0020]*>)([a-zA-Z0-9\u0020.!-#$&=|]+)(?=<\cls/[^i]{1}) const regex2 = new RegExp('<([\\w]+)[^>]*>([0-9a-zA-Z._\\u0020-!? :]+)</\\1>', 'g'); let detectedFiles = 0; let detectedResources = 0; files.forEach(file => { let content = fs.readFileSync(file, 'utf8'); const fileReplacements = []; if (file.indexOf('.html') > 0) { //test for strings which needs to be made as translation resources while ((m2 = regex2.exec(content)) !== null) { // This is necessary to avoid infinite loops with zero-width matches if (m2.index === regex2.lastIndex) { regex2.lastIndex++; } const match = m2[2]; const tag = m2[1].trim(); if (tag.length = 0 || match.length == 0 || match == '.' || match == '..' || match == '...' || tag == 'i') continue; if (match.trim() == '') continue; const id = "txt" + match.replace(/[\u0020.-]/g, '').replace(':', '').replace(' ', ''); const obj = { id: id, text: `>${match}</`, toReplace: `>{#${id}#}</` }; fileReplacements.push(obj); detectedResources++ } if (fileReplacements.length) { detectedFiles++; console.log(''); console.log(chalk.white.bgBlue(' DMS '), chalk.white.bgBlack(`Non-translated strings in: ${file}`)); fileReplacements.forEach(obj => { console.log(` ${obj.text.replace(/[<>/]/g, '')} => ${obj.toReplace.replace(/[<>/]/g, '')}`); }); } //console.log(''); if (argv.save) { fileReplacements.forEach(element => { content = content.replace(element.text, element.toReplace); }); //console.log('saving ', file); fs.writeFileSync(file, content, 'utf8'); } } }); console.log(chalk.white.bgBlack(' DMS '), chalk.white.bgBlack(`Detected ${detectedResources} non-translated strings in ${detectedFiles} files`)); if (argv.save) { } else { console.log(chalk.white.bgBlack(' DMS '), chalk.white.bgBlack('Changes were not saved. Use --save command line option to force saving')); } } // function extractTranslatableResourceFromSourceApp(argv) { // const srcPath = './src' // if (!fs.existsSync(srcPath)) // return; // const files = []; // readFilesRecursive(srcPath,['.ts','.html'], files); // const allTranslationResourcesFromSourceCode = []; // const regex = new RegExp('\{#[a-zA-Z0-9]*#\}', 'g'); // files.forEach(file => { // const content = fs.readFileSync(file, 'utf8'); // //test for existing translation resources // while ((m1 = regex.exec(content)) !== null) { // // This is necessary to avoid infinite loops with zero-width matches // if (m1.index === regex.lastIndex) { // regex.lastIndex++; // } // // The result can be accessed through the `m`-variable. // m1.forEach((match, groupIndex) => { // //console.log(`Found match, group ${groupIndex}: ${match}`); // const index = allTranslationResourcesFromSourceCode.indexOf(match); // if (index < 0) // allTranslationResourcesFromSourceCode.push(match); // }); // } // }); // const targetLanguageFilePath = path.join(srcPath, 'Translations', `${argv.to}.lang.json`); // let targetLanguageContent; // if (fs.existsSync(targetLanguageFilePath) && !argv.override) { // targetLanguageContent = JSON.parse(fs.readFileSync(targetLanguageFilePath)); // } // else { // targetLanguageContent = { resources: [] }; // } // let resourcesTranslatedCount = 0; // const actualOutput = targetLanguageContent; // allTranslationResourcesFromSourceCode.forEach(r => { // const found = actualOutput.resources.find(rt => { return rt.name == (r.replace('{#', '').replace('#}', '')) }); // if (!found) { // actualOutput.resources.push({ name: r.replace('{#', '').replace('#}', ''), text: "" }); // resourcesTranslatedCount++; // } // }); // fs.writeFileSync(targetLanguageFilePath, JSON.stringify(actualOutput), 'utf8'); // console.log(chalk.white.bgBlue.bold(' DMS '), chalk.white(`Extraction to language file "${targetLanguageFilePath}" was created successfully with ${resourcesTranslatedCount} new resources and total of ${actualOutput.resources.length} resources`)); // } function extractTranslatableResourceFromExtensionApp(argv, srcPath) { if (!fs.existsSync(srcPath)) return; const files = []; readFilesRecursive(srcPath, ['.ts', '.html'], files); const allTranslationResourcesFromSourceCode = []; const regex = new RegExp('\{#[a-zA-Z0-9 ]*#\}', 'g'); //console.log('files: ', files); files.forEach(file => { const content = fs.readFileSync(file, 'utf8'); //test for existing translation resources while ((m1 = regex.exec(content)) !== null) { // This is necessary to avoid infinite loops with zero-width matches if (m1.index === regex.lastIndex) { regex.lastIndex++; } // The result can be accessed through the `m`-variable. m1.forEach((match, groupIndex) => { //console.log(`Found match, group ${groupIndex}: ${match}`); const index = allTranslationResourcesFromSourceCode.indexOf(match); if (index < 0) allTranslationResourcesFromSourceCode.push(match); }); } }); const targetLanguageFilePath = path.join(srcPath, 'locales', `${argv.to}.json`); let alreadyTranslateResources; //load existing resources if (fs.existsSync(targetLanguageFilePath) && !argv.override) { alreadyTranslateResources = JSON.parse(fs.readFileSync(targetLanguageFilePath)); } else { alreadyTranslateResources = { }; } const actualOutput = {}; for(const key in alreadyTranslateResources){ actualOutput[key] = alreadyTranslateResources[key]; } let resourcesTranslatedCount = 0; allTranslationResourcesFromSourceCode.forEach(r => { const found = alreadyTranslateResources[r.replace('{#', '').replace('#}', '')]; if (!found) { actualOutput[r.replace('{#', '').replace('#}', '')] = ""; resourcesTranslatedCount++; } }); fs.writeFileSync(targetLanguageFilePath, JSON.stringify(actualOutput), 'utf8'); console.log(chalk.white.bgBlue.bold(' DMS '), chalk.white(`Extraction to language file "${targetLanguageFilePath}" was created successfully with ${resourcesTranslatedCount} new resources and total of ${actualOutput.resources.length} resources`)); } function escapeTranslationResName(name) { if (!name) return "[unknown]"; return name.replace(/-/g, '_').replace(/ /g, '_').replace(/\./g, '').replace(/%/g, '').replace(/#/g, ''); } function applyResourceOnEntity(bo, itemType, isPlural, name, label, allTranslationResourcesFromSourceCode, allTranslationLabelsFromSourceCode) { //console.log(bo,'=>', itemType); let hasChanges = false; let newLabel; if (label && label.indexOf('{#') < 0) { //there is non-translated label hasChanges = true; if (itemType == 'entity') { newLabel = `{#e${bo.name}${isPlural ? '_plural' : ''}#}` } else if (itemType == 'menu') { newLabel = `{#menu${escapeTranslationResName(label)}#}` } else { //field newLabel = `{#ef${bo.name}_${escapeTranslationResName(label)}#}` } } else if (!label) { //no label at all hasChanges = true; if (itemType == 'entity') { newLabel = `{#e${bo.name}${isPlural ? '_plural' : ''}#}` } else if (itemType == 'menu') { newLabel = `{#menu${escapeTranslationResName(name)}#}` } else { //field newLabel = `{#ef${bo.name}_${escapeTranslationResName(name)}#}` } } else { //there is translated label hasChanges = false; newLabel = label; } //console.log(originalLabel,',',newLabel); const index = allTranslationResourcesFromSourceCode.indexOf(newLabel); if (index < 0) { let x = label ? label : name; if (x && x.indexOf('{#') >= 0) { //handle view groups x = x.replace('{#', '').replace('#}', ''); } if (!x || x == 'label' || x.indexOf('{#') >= 0) { console.log(bo.name, '=>', itemType, ' ', x, ' ', label, ' ', name) throw new Error('oops') } allTranslationResourcesFromSourceCode.push(newLabel); allTranslationLabelsFromSourceCode.push(x) } //console.log({ hasChanges: hasChanges, label: newLabel }) return { hasChanges: hasChanges, label: newLabel }; } function extractBusinessObjectTranslatableResourceFromExtensionApp(argv, srcPath) { if (!fs.existsSync(srcPath)) return; const files = []; readFilesRecursive(srcPath, ['.bo.json'], files); const allTranslationResourcesFromSourceCode = []; const allTranslationLabelsFromSourceCode = []; let hasChanges = false; files.forEach(file => { const content = fs.readFileSync(file, 'utf8'); const entityName = path.basename(file).split('.')[0]; const bo = JSON.parse(content)[entityName]; //console.log('***', entityName, '***') //entity label var changes = applyResourceOnEntity(bo, 'entity', false, bo.name, bo.label, allTranslationResourcesFromSourceCode, allTranslationLabelsFromSourceCode); if (changes.hasChanges) { bo.label = changes.label; hasChanges = true; } //entity plural label changes = applyResourceOnEntity(bo, 'entity', true, bo.name, bo.pluralLabel, allTranslationResourcesFromSourceCode, allTranslationLabelsFromSourceCode); if (changes.hasChanges) { bo.pluralLabel = changes.label; hasChanges = true; } //entity properties for (const propName in bo.properties) { changes = applyResourceOnEntity(bo, 'entityfield', false, propName, bo.properties[propName].label, allTranslationResourcesFromSourceCode, allTranslationLabelsFromSourceCode); if (changes.hasChanges) { bo.properties[propName].label = changes.label; hasChanges = true; } } //view props and groups for (const viewName in bo.views) { const v = bo.views[viewName]; v.properties.forEach(p => { if (p.type == 'group') { changes = applyResourceOnEntity(bo, 'group', false, p.name, p.name, allTranslationResourcesFromSourceCode, allTranslationLabelsFromSourceCode); if (changes.hasChanges) { p.name = changes.label; hasChanges = true; } } else { changes = applyResourceOnEntity(bo, 'viewfield', false, p.name, p.label, allTranslationResourcesFromSourceCode, allTranslationLabelsFromSourceCode); if (changes.hasChanges) { p.label = changes.label; hasChanges = true; } } }) } if (hasChanges) { console.log('Writing ', file) const newBO = {}; newBO[entityName] = bo; fs.writeFileSync(file, JSON.stringify(newBO)); } }); const targetLanguageFilePath = path.join(srcPath, 'locales', `${argv.to}.json`); let alreadyTranslateResources; //load ext if (fs.existsSync(targetLanguageFilePath) && !argv.override) { alreadyTranslateResources = JSON.parse(fs.readFileSync(targetLanguageFilePath)); } else { alreadyTranslateResources = { }; } const actualOutput = alreadyTranslateResources; //load and add existing ./src const content = JSON.parse(fs.readFileSync(path.join(srcPath, 'locales', `${argv.to}.json`))); for(const srcKey in content){ const found = alreadyTranslateResources[srcKey]; if (!found) { alreadyTranslateResources[srcKey] = '' } else { } }; throw new Error('the code is not finished yet for the new locales feature....') // let resourcesTranslatedCount = 0; // for(const srcKey in allTranslationResourcesFromSourceCode){ // const found = alreadyTranslateResources.resources.find(rt => { return rt.name == (r.replace('{#', '').replace('#}', '')) }); // if (!found) { // actualOutput.resources.push({ name: r.replace('{#', '').replace('#}', ''), text: allTranslationLabelsFromSourceCode[index] }); // resourcesTranslatedCount++; // } // }; // fs.writeFileSync(targetLanguageFilePath, JSON.stringify(actualOutput), 'utf8'); // console.log(chalk.white.bgBlue.bold(' DMS '), chalk.white(`Extraction to language file "${targetLanguageFilePath}" was created successfully with ${resourcesTranslatedCount} new resources and total of ${actualOutput.resources.length} resources`)); } var globalHashChanges = false; function processMenuItem(menuItem, hasChanges, allTranslationResourcesFromSourceCode, allTranslationLabelsFromSourceCode) { var changes = applyResourceOnEntity(menuItem, 'menu', false, menuItem.taskId, menuItem.label, allTranslationResourcesFromSourceCode, allTranslationLabelsFromSourceCode); if (changes.hasChanges) { menuItem.label = changes.label; globalHashChanges = true; } if (menuItem.items) { menuItem.items.forEach(i => { var ch = processMenuItem(i, true, allTranslationResourcesFromSourceCode, allTranslationLabelsFromSourceCode); }); } return true; } function extractMenuTranslatableResourceFromExtensionApp(argv, srcPath) { if (!fs.existsSync(srcPath)) return; const files = []; readFilesRecursive(srcPath, ['root-menu.json'], files); const allTranslationResourcesFromSourceCode = []; const allTranslationLabelsFromSourceCode = []; let hasChanges = true; files.forEach(file => { const content = fs.readFileSync(file, 'utf8'); const menu = JSON.parse(content); menu.items.forEach( i => { var changes = processMenuItem(i, true, allTranslationResourcesFromSourceCode, allTranslationLabelsFromSourceCode); hasChanges = hasChanges & changes; } ); menu.quickItems.forEach( i => { var changes = processMenuItem(i, true, allTranslationResourcesFromSourceCode, allTranslationLabelsFromSourceCode); hasChanges = hasChanges & changes; } ); if (globalHashChanges) { console.log('Writing ', file) fs.writeFileSync(file, JSON.stringify(menu)); } }); const targetLanguageFilePath = path.join(srcPath, 'locales', `${argv.to}.json`); let alreadyTranslateResources; //load ext if (fs.existsSync(targetLanguageFilePath) && !argv.override) { alreadyTranslateResources = JSON.parse(fs.readFileSync(targetLanguageFilePath)); } else { alreadyTranslateResources = { }; } const actualOutput = alreadyTranslateResources; //load and add existing ./src const content = JSON.parse(fs.readFileSync(path.join(srcPath, 'locales', `${argv.to}.json`))); for(const sourceKey in content){ const found = alreadyTranslateResources[sourceKey]; if (!found) { alreadyTranslateResources[sourceKey]=''; } else { } }; let resourcesTranslatedCount = 0; allTranslationResourcesFromSourceCode.forEach((r, index) => { const found = alreadyTranslateResources[r.replace('{#', '').replace('#}', '')]; if (!found) { actualOutput[r.replace('{#', '').replace('#}', '')]=allTranslationLabelsFromSourceCode[index]; resourcesTranslatedCount++; } }); fs.writeFileSync(targetLanguageFilePath, JSON.stringify(actualOutput), 'utf8'); console.log(chalk.white.bgBlue.bold(' DMS '), chalk.white(`Extraction to language file "${targetLanguageFilePath}" was created successfully with ${resourcesTranslatedCount} new resources and total of ${actualOutput.resources.length} resources`)); }