diff --git a/src/cli/cli-options.interface.ts b/src/cli/cli-options.interface.ts deleted file mode 100644 index 928bdd4..0000000 --- a/src/cli/cli-options.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface CliOptionsInterface { - input: string[]; - output: string[]; - format: 'json' | 'namespaced-json' | 'pot'; - replace: boolean; - sort: boolean; - clean: boolean; - help: boolean; -} diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 274cca3..ed69e8b 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -1,33 +1,26 @@ -import { Extractor } from '../utils/extractor'; -import { CliOptionsInterface } from './cli-options.interface'; -import { TranslationCollection } from '../utils/translation.collection'; -import { ParserInterface } from '../parsers/parser.interface'; +import { ExtractTask } from './tasks/extract.task'; import { PipeParser } from '../parsers/pipe.parser'; import { DirectiveParser } from '../parsers/directive.parser'; import { ServiceParser } from '../parsers/service.parser'; -import { CompilerInterface } from '../compilers/compiler.interface'; -import { JsonCompiler } from '../compilers/json.compiler'; -import { NamespacedJsonCompiler } from '../compilers/namespaced-json.compiler'; -import { PoCompiler } from '../compilers/po.compiler'; import * as fs from 'fs'; -import * as path from 'path'; -import * as mkdirp from 'mkdirp'; -import * as chalk from 'chalk'; import * as yargs from 'yargs'; -const options: CliOptionsInterface = yargs +export const cli = yargs .usage('Extract strings from files for translation.\nUsage: $0 [options]') - .help('h') - .alias('h', 'help') + .version('2.0.0') // TODO: Read from package.json + .alias('version', 'v') + .help('help') + .alias('help', 'h') .option('input', { - alias: ['i', 'dir', 'd'], + alias: 'i', describe: 'Paths you would like to extract strings from. You can use path expansion, glob patterns and multiple paths', default: process.env.PWD, type: 'array', + normalize: true }) - .check((options: CliOptionsInterface) => { - options.input.forEach(dir => { + .check(options => { + options.input.forEach((dir: string) => { if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { throw new Error(`The path you supplied was not found: '${dir}'`) } @@ -35,10 +28,17 @@ const options: CliOptionsInterface = yargs }); return true; }) + .option('patterns', { + alias: 'p', + describe: 'Input file patterns to parse', + type: 'array', + default: ['/**/*.html', '/**/*.ts'] + }) .option('output', { alias: 'o', describe: 'Paths where you would like to save extracted strings. You can use path expansion, glob patterns and multiple paths', type: 'array', + normalize: true, required: true }) .option('format', { @@ -66,88 +66,22 @@ const options: CliOptionsInterface = yargs default: false, type: 'boolean' }) - .argv; + .exitProcess(true) + .parse(process.argv); -const patterns: string[] = [ - '/**/*.html', - '/**/*.ts' -]; -const parsers: ParserInterface[] = [ - new ServiceParser(), - new PipeParser(), - new DirectiveParser() -]; - -let compiler: CompilerInterface; -let ext: string; -switch (options.format) { - case 'pot': - compiler = new PoCompiler(); - ext = 'pot'; - break; - case 'json': - compiler = new JsonCompiler(); - ext = 'json'; - break; - case 'namespaced-json': - compiler = new NamespacedJsonCompiler(); - ext = 'json'; - break; -} - -const extractor: Extractor = new Extractor(parsers, patterns); - -let extractedStrings: TranslationCollection = new TranslationCollection(); - -// Extract strings from paths -console.log(chalk.bold('Extracting strings from...')); -options.input.forEach(dir => { - const normalizedDir: string = path.resolve(dir); - console.log(chalk.gray('- %s'), normalizedDir); - extractedStrings = extractedStrings.union(extractor.process(normalizedDir)); +const extractTask = new ExtractTask(cli.input, cli.output, { + replace: cli.replace, + sort: cli.sort, + clean: cli.clean, + patterns: cli.patterns }); -console.log(chalk.green('Extracted %d strings\n'), extractedStrings.count()); -// Save extracted strings to output paths -options.output.forEach(output => { - const normalizedOutput: string = path.resolve(output); +extractTask + .setParsers([ + new ServiceParser(), + new PipeParser(), + new DirectiveParser() + ]) + .setCompiler(cli.format) + .execute(); - let outputDir: string = normalizedOutput; - let outputFilename: string = `template.${ext}`; - if (!fs.existsSync(normalizedOutput) || !fs.statSync(normalizedOutput).isDirectory()) { - outputDir = path.dirname(normalizedOutput); - outputFilename = path.basename(normalizedOutput); - } - const outputPath: string = path.join(outputDir, outputFilename); - - console.log(chalk.bold('Saving to: %s'), outputPath); - if (!fs.existsSync(outputDir)) { - console.log(chalk.dim('- Created output dir: %s'), outputDir); - mkdirp.sync(outputDir); - } - - let processedStrings: TranslationCollection = extractedStrings; - - if (fs.existsSync(outputPath) && !options.replace) { - const existingStrings: TranslationCollection = compiler.parse(fs.readFileSync(outputPath, 'utf-8')); - if (existingStrings.count() > 0) { - processedStrings = processedStrings.union(existingStrings); - console.log(chalk.dim('- Merged with %d existing strings'), existingStrings.count()); - } - - if (options.clean) { - const collectionCount = processedStrings.count(); - processedStrings = processedStrings.intersect(processedStrings); - const removeCount = collectionCount - processedStrings.count(); - console.log(chalk.dim('- Removed %d obsolete strings'), removeCount); - } - } - - if (options.sort) { - processedStrings = processedStrings.sort(); - console.log(chalk.dim('- Sorted strings')); - } - - fs.writeFileSync(outputPath, compiler.compile(processedStrings)); - console.log(chalk.green('OK!\n')); -}); diff --git a/src/cli/tasks/extract.task.ts b/src/cli/tasks/extract.task.ts new file mode 100644 index 0000000..17fcc8d --- /dev/null +++ b/src/cli/tasks/extract.task.ts @@ -0,0 +1,156 @@ +import { TranslationCollection } from '../../utils/translation.collection'; +import { TaskInterface } from './task.interface'; +import { ParserInterface } from '../../parsers/parser.interface'; +import { CompilerInterface } from '../../compilers/compiler.interface'; +import { CompilerFactory } from '../../compilers/compiler.factory'; + +import * as chalk from 'chalk'; +import * as glob from 'glob'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as mkdirp from 'mkdirp'; + +export interface ExtractTaskOptionsInterface { + replace?: boolean; + sort?: boolean; + clean?: boolean; + patterns?: string[]; +} + +export class ExtractTask implements TaskInterface { + + protected _options: ExtractTaskOptionsInterface = { + replace: false, + sort: false, + clean: false, + patterns: [] + }; + + protected _parsers: ParserInterface[] = []; + protected _compiler: CompilerInterface; + + public constructor(protected _input: string[], protected _output: string[], options?: ExtractTaskOptionsInterface) { + this._options = Object.assign({}, this._options, options); + } + + public execute(): void { + if (!this._parsers) { + throw new Error('No parsers configured'); + } + if (!this._compiler) { + throw new Error('No compiler configured'); + } + + const collection = this._extract(); + if (collection.isEmpty()) { + this._out(chalk.yellow('Did not find any extractable strings\n')); + return; + } + + this._out(chalk.green('Extracted %d strings\n'), collection.count()); + this._save(collection); + } + + public setParsers(parsers: ParserInterface[]): this { + this._parsers = parsers; + return this; + } + + public setCompiler(compiler: CompilerInterface | string): this { + if (typeof compiler === 'string') { + this._compiler = CompilerFactory.create(compiler) + } else { + this._compiler = compiler; + } + + return this; + } + + /** + * Extract strings from input dirs using configured parsers + */ + protected _extract(): TranslationCollection { + let collection: TranslationCollection = new TranslationCollection(); + + this._out(chalk.bold('Extracting strings...')); + + this._input.forEach(dir => { + this._readDir(dir, this._options.patterns).forEach(path => { + this._out(chalk.gray('- %s'), path); + const contents: string = fs.readFileSync(path, 'utf-8'); + this._parsers.forEach((parser: ParserInterface) => { + collection = collection.union(parser.extract(contents, path)); + }); + }); + }); + return collection; + } + + /** + * Process collection according to options (merge, clean, sort), compile and save + * @param collection + */ + protected _save(collection: TranslationCollection): void { + this._output.forEach(output => { + const normalizedOutput: string = path.resolve(output); + + let dir: string = normalizedOutput; + let filename: string = `template.${this._compiler.extension}`; + if (!fs.existsSync(normalizedOutput) || !fs.statSync(normalizedOutput).isDirectory()) { + dir = path.dirname(normalizedOutput); + filename = path.basename(normalizedOutput); + } + + const outputPath: string = path.join(dir, filename); + let processedCollection: TranslationCollection = collection; + + this._out(chalk.bold('\nSaving: %s'), outputPath); + + if (fs.existsSync(outputPath) && !this._options.replace) { + const existingCollection: TranslationCollection = this._compiler.parse(fs.readFileSync(outputPath, 'utf-8')); + if (!existingCollection.isEmpty()) { + processedCollection = processedCollection.union(existingCollection); + this._out(chalk.dim('- merged with %d existing strings'), existingCollection.count()); + } + + if (this._options.clean) { + const collectionCount = processedCollection.count(); + processedCollection = processedCollection.intersect(processedCollection); + const removeCount = collectionCount - processedCollection.count(); + if (removeCount > 0) { + this._out(chalk.dim('- removed %d obsolete strings'), removeCount); + } + } + } + + if (this._options.sort) { + processedCollection = processedCollection.sort(); + this._out(chalk.dim('- sorted strings')); + } + + if (!fs.existsSync(dir)) { + mkdirp.sync(dir); + this._out(chalk.dim('- created dir: %s'), dir); + } + fs.writeFileSync(outputPath, this._compiler.compile(processedCollection)); + + this._out(chalk.green('Done!')); + }); + } + + /** + * Get all files in dir matching patterns + */ + protected _readDir(dir: string, patterns: string[]): string[] { + return patterns.reduce((results, pattern) => { + return glob.sync(dir + pattern) + .filter(path => fs.statSync(path).isFile()) + .concat(results); + }, []); + } + + protected _out(...args: any[]): void { + console.log.apply(this, arguments); + } + +} diff --git a/src/cli/tasks/task.interface.ts b/src/cli/tasks/task.interface.ts new file mode 100644 index 0000000..42c079f --- /dev/null +++ b/src/cli/tasks/task.interface.ts @@ -0,0 +1,3 @@ +export interface TaskInterface { + execute(): void; +} diff --git a/src/compilers/compiler.factory.ts b/src/compilers/compiler.factory.ts new file mode 100644 index 0000000..e6ba41e --- /dev/null +++ b/src/compilers/compiler.factory.ts @@ -0,0 +1,17 @@ +import { CompilerInterface } from '../compilers/compiler.interface'; +import { JsonCompiler } from '../compilers/json.compiler'; +import { NamespacedJsonCompiler } from '../compilers/namespaced-json.compiler'; +import { PoCompiler } from '../compilers/po.compiler'; + +export class CompilerFactory { + + public static create(format: string): CompilerInterface { + switch (format) { + case 'pot': return new PoCompiler(); + case 'json': return new JsonCompiler(); + case 'namespaced-json': return new NamespacedJsonCompiler(); + default: throw new Error(`Unknown format: ${format}`); + } + } + +} diff --git a/src/compilers/compiler.interface.ts b/src/compilers/compiler.interface.ts index 1d085bc..83b0575 100644 --- a/src/compilers/compiler.interface.ts +++ b/src/compilers/compiler.interface.ts @@ -2,6 +2,8 @@ import { TranslationCollection } from '../utils/translation.collection'; export interface CompilerInterface { + extension: string; + compile(collection: TranslationCollection): string; parse(contents: string): TranslationCollection; diff --git a/src/compilers/json.compiler.ts b/src/compilers/json.compiler.ts index 7484ab0..3d0b601 100644 --- a/src/compilers/json.compiler.ts +++ b/src/compilers/json.compiler.ts @@ -3,6 +3,8 @@ import { TranslationCollection } from '../utils/translation.collection'; export class JsonCompiler implements CompilerInterface { + public extension = 'json'; + public compile(collection: TranslationCollection): string { return JSON.stringify(collection.values, null, '\t'); } diff --git a/src/compilers/namespaced-json.compiler.ts b/src/compilers/namespaced-json.compiler.ts index cdde435..4b48c98 100644 --- a/src/compilers/namespaced-json.compiler.ts +++ b/src/compilers/namespaced-json.compiler.ts @@ -5,6 +5,8 @@ import * as flat from 'flat'; export class NamespacedJsonCompiler implements CompilerInterface { + public extension = 'json'; + public compile(collection: TranslationCollection): string { const values: {} = flat.unflatten(collection.values); return JSON.stringify(values, null, '\t'); diff --git a/src/compilers/po.compiler.ts b/src/compilers/po.compiler.ts index dc43fd6..141ff1f 100644 --- a/src/compilers/po.compiler.ts +++ b/src/compilers/po.compiler.ts @@ -5,6 +5,8 @@ import * as gettext from 'gettext-parser'; export class PoCompiler implements CompilerInterface { + public extension = 'po'; + /** * Translation domain */ diff --git a/src/utils/extractor.ts b/src/utils/extractor.ts deleted file mode 100644 index 4bd5b0e..0000000 --- a/src/utils/extractor.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { ParserInterface } from '../parsers/parser.interface'; -import { TranslationCollection } from './translation.collection'; - -import * as glob from 'glob'; -import * as fs from 'fs'; - -export class Extractor { - - public constructor(public parsers: ParserInterface[], public patterns: string[]) { } - - /** - * Extract strings from dir - */ - public process(dir: string): TranslationCollection { - let collection: TranslationCollection = new TranslationCollection(); - - this._readDir(dir, this.patterns).forEach(path => { - const contents: string = fs.readFileSync(path, 'utf-8'); - this.parsers.forEach((parser: ParserInterface) => { - collection = collection.union(parser.extract(contents, path)); - }); - }); - - return collection; - } - - /** - * Get all files in dir matching patterns - */ - protected _readDir(dir: string, patterns: string[]): string[] { - return patterns.reduce((results, pattern) => { - return glob.sync(dir + pattern) - .filter(path => fs.statSync(path).isFile()) - .concat(results); - }, []); - } - -}