diff --git a/.gitignore b/.gitignore index 7ff9da7..5979473 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +.idea/ + # Runtime data pids *.pid @@ -66,4 +68,4 @@ typings/ # dotenv environment variables file .env -.env.test \ No newline at end of file +.env.test diff --git a/src/index.ts b/src/index.ts index d28c90a..37c507c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,7 +17,7 @@ import { evaluateFilePath, FileType, DirectoryStructure, - TranslatableFile, + TranslatableFile, ensureDirectoryExists, } from './util/file-system'; import { matcherMap } from './matchers'; @@ -102,6 +102,10 @@ commander '-o, --overwrite', 'overwrite existing translations instead of skipping them', ) + .option( + '-r, --recursive', + 'recursively load translations from subdirectories', + ) .parse(process.argv); const translate = async ( @@ -122,6 +126,7 @@ const translate = async ( appName?: string, context?: string, overwrite: boolean = false, + recursive: boolean = false, ) => { const workingDir = path.resolve(process.cwd(), inputDir); const resolvedCacheDir = path.resolve(process.cwd(), cacheDir); @@ -158,6 +163,7 @@ const translate = async ( exclude, fileType, withArrays, + recursive ); if (templateFiles.length === 0) { @@ -176,7 +182,7 @@ const translate = async ( console.log(`🏭 Loading source files...`); for (const file of templateFiles) { - console.log(chalk`├── ${String(file.name)} (${file.type})`); + console.log(chalk`├── ${String(file.relativePath)} (${file.type})`); } console.log(chalk`└── {green.bold Done}`); console.log(); @@ -208,9 +214,9 @@ const translate = async ( ); if (inconsistentKeys.length > 0) { - inconsistentFiles.push(file.name); + inconsistentFiles.push(file.relativePath); console.log( - chalk`├── {yellow.bold ${file.name} contains} {red.bold ${String( + chalk`├── {yellow.bold ${file.relativePath} contains} {red.bold ${String( inconsistentKeys.length, )}} {yellow.bold inconsistent key(s)}`, ); @@ -231,6 +237,10 @@ const translate = async ( fixSourceInconsistencies( templateFilePath, evaluateFilePath(resolvedCacheDir, dirStructure, sourceLang), + exclude, + fileType, + withArrays, + recursive, ); console.log(chalk`└── {green.bold Fixed all inconsistencies.}`); } else { @@ -252,9 +262,9 @@ const translate = async ( ); if (invalidKeys.length > 0) { - invalidFiles.push(file.name); + invalidFiles.push(file.relativePath); console.log( - chalk`├── {yellow.bold ${file.name} contains} {red.bold ${String( + chalk`├── {yellow.bold ${file.relativePath} contains} {red.bold ${String( invalidKeys.length, )}} {yellow.bold invalid key(s)}`, ); @@ -318,29 +328,30 @@ const translate = async ( exclude, fileType, withArrays, + recursive ); if (deleteUnusedStrings) { - const templateFileNames = templateFiles.map((t) => t.name); + const templateFileNames = templateFiles.map((t) => t.relativePath); const deletableFiles = existingFiles.filter( - (f) => !templateFileNames.includes(f.name), + (f) => !templateFileNames.includes(f.relativePath), ); for (const file of deletableFiles) { console.log( - chalk`├── {red.bold ${file.name} is no longer used and will be deleted.}`, + chalk`├── {red.bold ${file.relativePath} is no longer used and will be deleted.}`, ); fs.unlinkSync( path.resolve( evaluateFilePath(workingDir, dirStructure, language), - file.name, + file.relativePath, ), ); const cacheFile = path.resolve( evaluateFilePath(workingDir, dirStructure, language), - file.name, + file.relativePath, ); if (fs.existsSync(cacheFile)) { fs.unlinkSync(cacheFile); @@ -349,12 +360,12 @@ const translate = async ( } for (const templateFile of templateFiles) { - process.stdout.write(`├── Translating ${templateFile.name}`); + process.stdout.write(`├── Translating ${templateFile.relativePath}`); const [addedTranslations, removedTranslations] = await translateContent( templateFile, - existingFiles.find((f) => f.name === templateFile.name), + existingFiles.find((f) => f.relativePath === templateFile.relativePath), ); totalAddedTranslations += addedTranslations; @@ -364,14 +375,14 @@ const translate = async ( case 'ngx-translate': const sourceFile = templateFiles.find( - (f) => f.name === `${sourceLang}.json`, + (f) => f.relativePath === `${sourceLang}.json`, ); if (!sourceFile) { throw new Error('Could not find source file. This is a bug.'); } const [addedTranslations, removedTranslations] = await translateContent( sourceFile, - templateFiles.find((f) => f.name === `${language}.json`), + templateFiles.find((f) => f.relativePath === `${language}.json`), ); totalAddedTranslations += addedTranslations; @@ -441,6 +452,7 @@ translate( commander.appName, commander.context, commander.overwrite, + commander.recursive ).catch((e: Error) => { console.log(); console.log(chalk.bgRed('An error has occurred:')); @@ -460,7 +472,7 @@ function createTranslator( dirStructure: DirectoryStructure, deleteUnusedStrings: boolean, withArrays: boolean, - overwrite, + overwrite: boolean, ) { return async ( sourceFile: TranslatableFile, @@ -468,7 +480,7 @@ function createTranslator( ) => { const cachePath = path.resolve( evaluateFilePath(cacheDir, dirStructure, sourceLang), - sourceFile ? sourceFile.name : '', + sourceFile ? sourceFile.relativePath : '', ); let cacheDiff: string[] = []; if (fs.existsSync(cachePath) && !fs.statSync(cachePath).isDirectory()) { @@ -535,11 +547,14 @@ function createTranslator( 2, ) + `\n`; + const writePath = path.resolve( + evaluateFilePath(workingDir, dirStructure, targetLang), + destinationFile?.relativePath ?? sourceFile.relativePath, + ) + ensureDirectoryExists(writePath); + fs.writeFileSync( - path.resolve( - evaluateFilePath(workingDir, dirStructure, targetLang), - destinationFile?.name ?? sourceFile.name, - ), + writePath, newContent, ); @@ -548,14 +563,15 @@ function createTranslator( dirStructure, targetLang, ); - if (!fs.existsSync(languageCachePath)) { - fs.mkdirSync(languageCachePath); - } + + const langCacheFilePath = path.resolve( + languageCachePath, + destinationFile?.relativePath ?? sourceFile.relativePath, + ); + ensureDirectoryExists(langCacheFilePath) + fs.writeFileSync( - path.resolve( - languageCachePath, - destinationFile?.name ?? sourceFile.name, - ), + langCacheFilePath, JSON.stringify(translatedFile, null, 2) + '\n', ); } diff --git a/src/util/file-system.ts b/src/util/file-system.ts index 3256963..fe4b127 100644 --- a/src/util/file-system.ts +++ b/src/util/file-system.ts @@ -8,7 +8,7 @@ export type FileType = 'key-based' | 'natural' | 'auto'; export type DirectoryStructure = 'default' | 'ngx-translate'; export interface TranslatableFile { - name: string; + relativePath: string; originalContent: string; type: FileType; content: object; @@ -44,16 +44,17 @@ export const detectFileType = (json: any): FileType => { export const loadTranslations = ( directory: string, - exclude?: string, + exclude: string | undefined, fileType: FileType = 'auto', withArrays = false, + recursive = false ) => - globSync(`${directory}/*.json`, { ignore: exclude }).map((f) => { + globSync(`${directory}${recursive ? '/**' : ''}/*.json`, { ignore: exclude }).map((f) => { const json = require(path.resolve(directory, f)); const type = fileType === 'auto' ? detectFileType(json) : fileType; return { - name: path.basename(f), + relativePath: recursive ? path.relative(directory, f) : path.basename(f), originalContent: json, type, content: @@ -65,11 +66,28 @@ export const loadTranslations = ( } as TranslatableFile; }); +export const ensureDirectoryExists = (filePath: string) => { + const dirname = path.dirname(filePath); + if (!fs.existsSync(dirname)) { + fs.mkdirSync(dirname, { recursive:true }); + } +} + export const fixSourceInconsistencies = ( directory: string, cacheDir: string, + exclude: string | undefined, + fileType: FileType = 'auto', + withArrays = false, + recursive = false ) => { - const files = loadTranslations(directory).filter((f) => f.type === 'natural'); + const files = loadTranslations( + directory, + exclude, + fileType, + withArrays, + recursive + ).filter((f) => f.type === 'natural'); for (const file of files) { const fixedContent = Object.keys(file.content).reduce( @@ -77,13 +95,18 @@ export const fixSourceInconsistencies = ( {} as { [k: string]: string }, ); + const outPath = path.resolve(directory, file.relativePath); + const cachePath = path.resolve(cacheDir, file.relativePath); + ensureDirectoryExists(outPath) + ensureDirectoryExists(cachePath) + fs.writeFileSync( - path.resolve(directory, file.name), - JSON.stringify(fixedContent, null, 2) + '\n', + outPath, + JSON.stringify(fixedContent, null, 2) + '\n' ); fs.writeFileSync( - path.resolve(cacheDir, file.name), + cachePath, JSON.stringify(fixedContent, null, 2) + '\n', ); }