Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add functionality that you can lint your translation files (feature) #205

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as yargs from 'yargs';
import { red, green } from 'colorette';

import { ExtractTask } from './tasks/extract.task';
import { LintTask } from './tasks/lint.task';
import { ParserInterface } from '../parsers/parser.interface';
import { PipeParser } from '../parsers/pipe.parser';
import { DirectiveParser } from '../parsers/directive.parser';
Expand Down Expand Up @@ -58,6 +59,12 @@ export const cli = y
const paths = normalizePaths(output, parsed.patterns);
return paths;
})
.option('lint', {
alias: 'lt',
describe: 'Tests if given output files containing all keys',
default: false,
type: 'boolean'
})
.option('format', {
alias: 'f',
describe: 'Format',
Expand Down Expand Up @@ -98,6 +105,12 @@ export const cli = y
type: 'boolean',
conflicts: ['key-as-default-value', 'string-as-default-value']
})
.option('lint', {
alias: 'lt',
describe: 'Tests if given output files containing all keys',
default: false,
type: 'boolean'
})
.option('string-as-default-value', {
alias: 'd',
describe: 'Use string as default value',
Expand All @@ -117,13 +130,13 @@ export const cli = y
.exitProcess(true)
.parse(process.argv);

const extractTask = new ExtractTask(cli.input, cli.output, {
const task = cli.lint ? new LintTask(cli.input, cli.output) : new ExtractTask(cli.input, cli.output, {
replace: cli.replace
});

// Parsers
const parsers: ParserInterface[] = [new PipeParser(), new DirectiveParser(), new ServiceParser(), new MarkerParser()];
extractTask.setParsers(parsers);
task.setParsers(parsers);

// Post processors
const postProcessors: PostProcessorInterface[] = [];
Expand All @@ -141,17 +154,17 @@ if (cli.keyAsDefaultValue) {
if (cli.sort) {
postProcessors.push(new SortByKeyPostProcessor());
}
extractTask.setPostProcessors(postProcessors);
task.setPostProcessors(postProcessors);

// Compiler
const compiler: CompilerInterface = CompilerFactory.create(cli.format, {
indentation: cli.formatIndentation
});
extractTask.setCompiler(compiler);
task.setCompiler(compiler);

// Run task
try {
extractTask.execute();
task.execute();
console.log(green('\nDone.\n'));
console.log(donateMessage);
process.exit(0);
Expand Down
136 changes: 136 additions & 0 deletions src/cli/tasks/core.task.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { TranslationCollection } from '../../utils/translation.collection';
import { TaskInterface } from './task.interface';
import { ParserInterface } from '../../parsers/parser.interface';
import { PostProcessorInterface } from '../../post-processors/post-processor.interface';
import { CompilerInterface } from '../../compilers/compiler.interface';

import { bold, cyan, dim, green, red } from 'colorette';
import * as glob from 'glob';
import * as fs from 'fs';
import * as path from 'path';

export abstract class CoreTask implements TaskInterface {

protected parsers: ParserInterface[] = [];
protected postProcessors: PostProcessorInterface[] = [];
protected compiler: CompilerInterface;

protected constructor(protected inputs: string[], protected outputs: string[]) {
this.inputs = inputs.map((input) => path.resolve(input));
this.outputs = outputs.map((output) => path.resolve(output));
}

public execute(): void {
if (!this.compiler) {
throw new Error('No compiler configured');
}

this.printEnabledParsers();
this.printEnabledPostProcessors();
this.printEnabledCompiler();

this.out(bold('Extracting:'));
const extracted = this.extract();
this.out(green(`\nFound %d strings.\n`), extracted.count());

this.executeTask(extracted);
}

protected abstract executeTask(extracted: TranslationCollection): void;

public setParsers(parsers: ParserInterface[]): this {
this.parsers = parsers;
return this;
}

public setPostProcessors(postProcessors: PostProcessorInterface[]): this {
this.postProcessors = postProcessors;
return this;
}

public setCompiler(compiler: CompilerInterface): this {
this.compiler = compiler;
return this;
}

/**
* Extract strings from specified input dirs using configured parsers
*/
protected extract(): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
this.inputs.forEach((pattern) => {
this.getFiles(pattern).forEach((filePath) => {
this.out(dim('- %s'), filePath);
const contents: string = fs.readFileSync(filePath, 'utf-8');
this.parsers.forEach((parser) => {
const extracted = parser.extract(contents, filePath);
if (extracted instanceof TranslationCollection) {
collection = collection.union(extracted);
}
});
});
});
return collection;
}

/**
* Get all files matching pattern
*/
protected getFiles(pattern: string): string[] {
return glob.sync(pattern).filter((filePath) => fs.statSync(filePath).isFile());
}

protected out(...args: any[]): void {
console.log.apply(this, arguments);
}

protected printEnabledParsers(): void {
this.out(cyan('Enabled parsers:'));
if (this.parsers.length) {
this.out(cyan(dim(this.parsers.map((parser) => `- ${parser.constructor.name}`).join('\n'))));
} else {
this.out(cyan(dim('(none)')));
}
this.out();
}

protected printEnabledPostProcessors(): void {
this.out(cyan('Enabled post processors:'));
if (this.postProcessors.length) {
this.out(cyan(dim(this.postProcessors.map((postProcessor) => `- ${postProcessor.constructor.name}`).join('\n'))));
} else {
this.out(cyan(dim('(none)')));
}
this.out();
}

protected printEnabledCompiler(): void {
this.out(cyan('Compiler:'));
this.out(cyan(dim(`- ${this.compiler.constructor.name}`)));
this.out();
}

protected createOutputPath(output: string): string {
let dir: string = output;
let filename: string = `strings.${this.compiler.extension}`;
if (!fs.existsSync(output) || !fs.statSync(output).isDirectory()) {
dir = path.dirname(output);
filename = path.basename(output);
}

return path.join(dir, filename);
}

protected getExistingTranslationCollection(outputPath: string): TranslationCollection {
let existing: TranslationCollection = new TranslationCollection();
if (fs.existsSync(outputPath)) {
try {
existing = this.compiler.parse(fs.readFileSync(outputPath, 'utf-8'));
} catch (e) {
this.out(`%s %s`, dim(`- ${outputPath}`), red(`[ERROR]`));
throw e;
}
}
return existing;
}
}
121 changes: 12 additions & 109 deletions src/cli/tasks/extract.task.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { TranslationCollection } from '../../utils/translation.collection';
import { TaskInterface } from './task.interface';
import { CoreTask } from './core.task';
import { ParserInterface } from '../../parsers/parser.interface';
import { PostProcessorInterface } from '../../post-processors/post-processor.interface';
import { CompilerInterface } from '../../compilers/compiler.interface';

import { cyan, green, bold, dim, red } from 'colorette';
import * as glob from 'glob';
import { bold, dim, green, red } from 'colorette';
import * as fs from 'fs';
import * as path from 'path';
import * as mkdirp from 'mkdirp';
Expand All @@ -14,7 +13,7 @@ export interface ExtractTaskOptionsInterface {
replace?: boolean;
}

export class ExtractTask implements TaskInterface {
export class ExtractTask extends CoreTask {
protected options: ExtractTaskOptionsInterface = {
replace: false
};
Expand All @@ -24,45 +23,20 @@ export class ExtractTask implements TaskInterface {
protected compiler: CompilerInterface;

public constructor(protected inputs: string[], protected outputs: string[], options?: ExtractTaskOptionsInterface) {
this.inputs = inputs.map((input) => path.resolve(input));
this.outputs = outputs.map((output) => path.resolve(output));
this.options = { ...this.options, ...options };
super(inputs, outputs);
this.options = {...this.options, ...options};
}

public execute(): void {
if (!this.compiler) {
throw new Error('No compiler configured');
}

this.printEnabledParsers();
this.printEnabledPostProcessors();
this.printEnabledCompiler();

this.out(bold('Extracting:'));
const extracted = this.extract();
this.out(green(`\nFound %d strings.\n`), extracted.count());

/**
* Saves extracted translation keys in file(s)
*/
protected executeTask(extracted: TranslationCollection): void {
this.out(bold('Saving:'));

this.outputs.forEach((output) => {
let dir: string = output;
let filename: string = `strings.${this.compiler.extension}`;
if (!fs.existsSync(output) || !fs.statSync(output).isDirectory()) {
dir = path.dirname(output);
filename = path.basename(output);
}
const outputPath = this.createOutputPath(output);

const outputPath: string = path.join(dir, filename);

let existing: TranslationCollection = new TranslationCollection();
if (!this.options.replace && fs.existsSync(outputPath)) {
try {
existing = this.compiler.parse(fs.readFileSync(outputPath, 'utf-8'));
} catch (e) {
this.out(`%s %s`, dim(`- ${outputPath}`), red(`[ERROR]`));
throw e;
}
}
const existing = this.options.replace ? new TranslationCollection() : this.getExistingTranslationCollection(outputPath);

// merge extracted strings with existing
const draft = extracted.union(existing);
Expand All @@ -85,41 +59,6 @@ export class ExtractTask implements TaskInterface {
});
}

public setParsers(parsers: ParserInterface[]): this {
this.parsers = parsers;
return this;
}

public setPostProcessors(postProcessors: PostProcessorInterface[]): this {
this.postProcessors = postProcessors;
return this;
}

public setCompiler(compiler: CompilerInterface): this {
this.compiler = compiler;
return this;
}

/**
* Extract strings from specified input dirs using configured parsers
*/
protected extract(): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
this.inputs.forEach((pattern) => {
this.getFiles(pattern).forEach((filePath) => {
this.out(dim('- %s'), filePath);
const contents: string = fs.readFileSync(filePath, 'utf-8');
this.parsers.forEach((parser) => {
const extracted = parser.extract(contents, filePath);
if (extracted instanceof TranslationCollection) {
collection = collection.union(extracted);
}
});
});
});
return collection;
}

/**
* Run strings through configured post processors
*/
Expand All @@ -132,6 +71,7 @@ export class ExtractTask implements TaskInterface {

/**
* Compile and save translations
* @param output
* @param collection
*/
protected save(output: string, collection: TranslationCollection): void {
Expand All @@ -141,41 +81,4 @@ export class ExtractTask implements TaskInterface {
}
fs.writeFileSync(output, this.compiler.compile(collection));
}

/**
* Get all files matching pattern
*/
protected getFiles(pattern: string): string[] {
return glob.sync(pattern).filter((filePath) => fs.statSync(filePath).isFile());
}

protected out(...args: any[]): void {
console.log.apply(this, arguments);
}

protected printEnabledParsers(): void {
this.out(cyan('Enabled parsers:'));
if (this.parsers.length) {
this.out(cyan(dim(this.parsers.map((parser) => `- ${parser.constructor.name}`).join('\n'))));
} else {
this.out(cyan(dim('(none)')));
}
this.out();
}

protected printEnabledPostProcessors(): void {
this.out(cyan('Enabled post processors:'));
if (this.postProcessors.length) {
this.out(cyan(dim(this.postProcessors.map((postProcessor) => `- ${postProcessor.constructor.name}`).join('\n'))));
} else {
this.out(cyan(dim('(none)')));
}
this.out();
}

protected printEnabledCompiler(): void {
this.out(cyan('Compiler:'));
this.out(cyan(dim(`- ${this.compiler.constructor.name}`)));
this.out();
}
}
Loading