Add StringCollection to make it easier to work with strings

This commit is contained in:
Kim Biesbjerg 2016-12-09 05:18:04 +01:00
parent befd841457
commit 73801a9cc5
17 changed files with 204 additions and 111 deletions

View File

@ -34,10 +34,10 @@ var destination = path.join(options.output, filename);
try { try {
var extractor = new Extractor(serializer); var extractor = new Extractor(serializer);
var messages = extractor.extract(options.dir); var collection = extractor.process(options.dir);
if (messages.length > 0) { if (collection.count() > 0) {
extractor.save(destination); extractor.save(destination);
cli.ok(`Extracted ${messages.length} strings: '${destination}'`); cli.ok(`Extracted ${collection.count()} strings: '${destination}'`);
} else { } else {
cli.info(`Found no extractable strings in the supplied directory path: '${options.dir}'`); cli.info(`Found no extractable strings in the supplied directory path: '${options.dir}'`);
} }

View File

@ -46,7 +46,6 @@
"@types/chai": "^3.4.34", "@types/chai": "^3.4.34",
"@types/cheerio": "^0.17.31", "@types/cheerio": "^0.17.31",
"@types/glob": "^5.0.30", "@types/glob": "^5.0.30",
"@types/lodash": "^4.14.41",
"@types/mocha": "^2.2.33", "@types/mocha": "^2.2.33",
"chai": "^3.5.0", "chai": "^3.5.0",
"mocha": "^3.2.0", "mocha": "^3.2.0",
@ -60,7 +59,6 @@
"cli": "^1.0.1", "cli": "^1.0.1",
"fs": "0.0.1-security", "fs": "0.0.1-security",
"glob": "^7.1.1", "glob": "^7.1.1",
"lodash": "^4.17.2",
"path": "^0.12.7" "path": "^0.12.7"
} }
} }

View File

@ -1,17 +1,17 @@
import { Extractor } from './extractor'; import { Extractor } from './extractor';
import { JsonSerializer } from './serializers/json.serializer'; import { JsonSerializer } from './serializers/json.serializer';
import { StringCollection } from './utils/string.collection';
const serializer = new JsonSerializer(); const serializer = new JsonSerializer();
// Or const serializer = new PotSerializer();
const extractor = new Extractor(serializer); const extractor = new Extractor(serializer);
const src = '/your/project'; const src = '/your/project';
const dest = '/your/project/template.json'; const dest = '/your/project/template.json';
try { try {
const messages: string[] = extractor.extract(src); const collection: StringCollection = extractor.process(src);
const output: string = extractor.save(dest); const output: string = extractor.save(dest);
console.log({ messages, output }); console.log({ strings: collection.keys(), output: output });
} catch (e) { } catch (e) {
console.log(`Something went wrong: ${e.toString()}`); console.log(`Something went wrong: ${e.toString()}`);
} }

View File

@ -3,8 +3,8 @@ import { PipeParser } from './parsers/pipe.parser';
import { DirectiveParser } from './parsers/directive.parser'; import { DirectiveParser } from './parsers/directive.parser';
import { ServiceParser } from './parsers/service.parser'; import { ServiceParser } from './parsers/service.parser';
import { SerializerInterface } from './serializers/serializer.interface'; import { SerializerInterface } from './serializers/serializer.interface';
import { StringCollection } from './utils/string.collection';
import * as lodash from 'lodash';
import * as glob from 'glob'; import * as glob from 'glob';
import * as fs from 'fs'; import * as fs from 'fs';
@ -16,35 +16,35 @@ export class Extractor {
new ServiceParser() new ServiceParser()
]; ];
public globPatterns: string[] = [ public find: string[] = [
'/**/*.html', '/**/*.html',
'/**/*.ts', '/**/*.ts',
'/**/*.js' '/**/*.js'
]; ];
public messages: string[] = []; public collection: StringCollection = new StringCollection();
public constructor(public serializer: SerializerInterface) { } public constructor(public serializer: SerializerInterface) { }
/** /**
* Extracts messages from paths * Process dir
*/ */
public extract(dir: string): string[] { public process(dir: string): StringCollection {
let messages = []; this._getFiles(dir).forEach(path => {
const contents: string = fs.readFileSync(path, 'utf-8');
this._getFiles(dir).forEach(filePath => { this.parsers.forEach((parser: ParserInterface) => {
const result = this._extractMessages(filePath); this.collection.merge(parser.extract(contents, path));
messages = [...messages, ...result]; });
}); });
return this.messages = lodash.uniq(messages); return this.collection;
} }
/** /**
* Serialize and return output * Serialize and return output
*/ */
public serialize(): string { public serialize(): string {
return this.serializer.serialize(this.messages); return this.serializer.serialize(this.collection);
} }
/** /**
@ -62,10 +62,10 @@ export class Extractor {
protected _getFiles(dir: string): string[] { protected _getFiles(dir: string): string[] {
let results: string[] = []; let results: string[] = [];
this.globPatterns.forEach(globPattern => { this.find.forEach(pattern => {
const files = glob const files = glob
.sync(dir + globPattern) .sync(dir + pattern)
.filter(filePath => fs.statSync(filePath).isFile()); .filter(path => fs.statSync(path).isFile());
results = [...results, ...files]; results = [...results, ...files];
}); });
@ -73,18 +73,4 @@ export class Extractor {
return results; return results;
} }
/**
* Extract messages from file using parser
*/
protected _extractMessages(filePath: string): string[] {
let results: string[] = [];
const contents: string = fs.readFileSync(filePath, 'utf-8');
this.parsers.forEach((parser: ParserInterface) => {
results = [...results, ...parser.process(filePath, contents)];
});
return results;
}
} }

View File

@ -4,8 +4,8 @@ export abstract class AbstractTemplateParser {
* Checks if file is of type javascript or typescript and * Checks if file is of type javascript or typescript and
* makes the assumption that it is an Angular Component * makes the assumption that it is an Angular Component
*/ */
protected _isAngularComponent(filePath: string): boolean { protected _isAngularComponent(path: string): boolean {
return new RegExp(/\.(ts|js)$/, 'i').test(filePath); return new RegExp(/\.(ts|js)$/, 'i').test(path);
} }
/** /**

View File

@ -1,20 +1,21 @@
import { ParserInterface } from './parser.interface'; import { ParserInterface } from './parser.interface';
import { AbstractTemplateParser } from './abstract-template.parser'; import { AbstractTemplateParser } from './abstract-template.parser';
import { StringCollection } from '../utils/string.collection';
import * as $ from 'cheerio'; import * as $ from 'cheerio';
export class DirectiveParser extends AbstractTemplateParser implements ParserInterface { export class DirectiveParser extends AbstractTemplateParser implements ParserInterface {
public process(filePath: string, contents: string): string[] { public extract(contents: string, path?: string): StringCollection {
if (this._isAngularComponent(filePath)) { if (path && this._isAngularComponent(path)) {
contents = this._extractInlineTemplate(contents); contents = this._extractInlineTemplate(contents);
} }
return this._parseTemplate(contents); return this._parseTemplate(contents);
} }
protected _parseTemplate(template: string): string[] { protected _parseTemplate(template: string): StringCollection {
let results: string[] = []; const collection = new StringCollection();
template = this._normalizeTemplateAttributes(template); template = this._normalizeTemplateAttributes(template);
$(template) $(template)
@ -25,7 +26,7 @@ export class DirectiveParser extends AbstractTemplateParser implements ParserInt
const attr = $element.attr('translate') || $element.attr('ng2-translate'); const attr = $element.attr('translate') || $element.attr('ng2-translate');
if (attr) { if (attr) {
results.push(attr); collection.add(attr);
} else { } else {
$element $element
.contents() .contents()
@ -33,11 +34,11 @@ export class DirectiveParser extends AbstractTemplateParser implements ParserInt
.filter(textNode => textNode.type === 'text') .filter(textNode => textNode.type === 'text')
.map(textNode => textNode.nodeValue.trim()) .map(textNode => textNode.nodeValue.trim())
.filter(text => text.length > 0) .filter(text => text.length > 0)
.forEach(text => results.push(text)); .forEach(text => collection.add(text));
} }
}); });
return results; return collection;
} }
} }

View File

@ -1,5 +1,7 @@
import { StringCollection } from '../utils/string.collection';
export interface ParserInterface { export interface ParserInterface {
process(filePath: string, contents: string): string[]; extract(contents: string, path?: string): StringCollection;
} }

View File

@ -1,27 +1,28 @@
import { ParserInterface } from './parser.interface'; import { ParserInterface } from './parser.interface';
import { AbstractTemplateParser } from './abstract-template.parser'; import { AbstractTemplateParser } from './abstract-template.parser';
import { StringCollection } from '../utils/string.collection';
export class PipeParser extends AbstractTemplateParser implements ParserInterface { export class PipeParser extends AbstractTemplateParser implements ParserInterface {
public process(filePath: string, contents: string): string[] { public extract(contents: string, path?: string): StringCollection {
if (this._isAngularComponent(filePath)) { if (path && this._isAngularComponent(path)) {
contents = this._extractInlineTemplate(contents); contents = this._extractInlineTemplate(contents);
} }
return this._parseTemplate(contents); return this._parseTemplate(contents);
} }
protected _parseTemplate(template: string): string[] { protected _parseTemplate(template: string): StringCollection {
let results: string[] = []; const collection = new StringCollection();
const regExp = new RegExp(/([\'"`])([^\1\r\n]*)\1\s+\|\s*translate(:.*?)?/, 'g'); const regExp = new RegExp(/([\'"`])([^\1\r\n]*)\1\s+\|\s*translate(:.*?)?/, 'g');
let matches; let matches;
while (matches = regExp.exec(template)) { while (matches = regExp.exec(template)) {
results.push(matches[2]); collection.add(matches[2]);
} }
return results; return collection;
} }
} }

View File

@ -1,13 +1,14 @@
import { ParserInterface } from './parser.interface'; import { ParserInterface } from './parser.interface';
import { StringCollection } from '../utils/string.collection';
export class ServiceParser implements ParserInterface { export class ServiceParser implements ParserInterface {
public process(filePath: string, contents: string): string[] { public extract(contents: string, path?: string): StringCollection {
let results: string[] = []; const collection = new StringCollection();
const translateServiceVar = this._extractTranslateServiceVar(contents); const translateServiceVar = this._extractTranslateServiceVar(contents);
if (!translateServiceVar) { if (!translateServiceVar) {
return results; return collection;
} }
const methodRegExp: RegExp = new RegExp(/(?:get|instant)\s*\(\s*(\[?([\'"`])([^\1\r\n]+)\2\]?)/); const methodRegExp: RegExp = new RegExp(/(?:get|instant)\s*\(\s*(\[?([\'"`])([^\1\r\n]+)\2\]?)/);
@ -16,13 +17,13 @@ export class ServiceParser implements ParserInterface {
let matches; let matches;
while (matches = regExp.exec(contents)) { while (matches = regExp.exec(contents)) {
if (this._stringContainsArray(matches[1])) { if (this._stringContainsArray(matches[1])) {
results.push(...this._stringToArray(matches[1])); collection.add(this._stringToArray(matches[1]));
} else { } else {
results.push(matches[3]); collection.add(matches[3]);
} }
} }
return results; return collection;
} }
/** /**

View File

@ -1,14 +1,10 @@
import { SerializerInterface } from './serializer.interface'; import { SerializerInterface } from './serializer.interface';
import { StringCollection } from '../utils/string.collection';
export class JsonSerializer implements SerializerInterface { export class JsonSerializer implements SerializerInterface {
public serialize(messages: string[]): string { public serialize(collection: StringCollection): string {
let result = {}; return JSON.stringify(collection.values, null, '\t');
messages.forEach(message => {
result[message] = '';
});
return JSON.stringify(result, null, '\t');
} }
} }

View File

@ -1,4 +1,5 @@
import { SerializerInterface } from './serializer.interface'; import { SerializerInterface } from './serializer.interface';
import { StringCollection } from '../utils/string.collection';
export class PotSerializer implements SerializerInterface { export class PotSerializer implements SerializerInterface {
@ -9,10 +10,10 @@ export class PotSerializer implements SerializerInterface {
protected _buffer: string[] = []; protected _buffer: string[] = [];
public serialize(messages: string[]): string { public serialize(collection: StringCollection): string {
this._reset(); this._reset();
this._addHeader(this._headers); this._addHeader(this._headers);
this._addMessages(messages); this._addMessages(collection);
return this._buffer.join('\n'); return this._buffer.join('\n');
} }
@ -25,10 +26,10 @@ export class PotSerializer implements SerializerInterface {
}); });
} }
protected _addMessages(messages: string[]): void { protected _addMessages(collection: StringCollection): void {
messages.forEach(message => { Object.keys(collection.values).forEach(key => {
this._add('msgid', message); this._add('msgid', key);
this._add('msgstr', ''); this._add('msgstr', collection.get(key));
}); });
} }

View File

@ -1,5 +1,7 @@
import { StringCollection } from '../utils/string.collection';
export interface SerializerInterface { export interface SerializerInterface {
serialize(messages: string[]): string; serialize(collection: StringCollection): string;
} }

View File

@ -0,0 +1,51 @@
export interface StringType {
[key: string]: string
};
export class StringCollection {
public values: StringType = {};
public static fromObject(values: StringType): StringCollection {
const collection = new StringCollection();
Object.keys(values).forEach(key => collection.add(key, values[key]));
return collection;
}
public static fromArray(keys: string[]): StringCollection {
const collection = new StringCollection();
keys.forEach(key => collection.add(key));
return collection;
}
public add(keys: string | string[], val: string = ''): StringCollection {
if (!Array.isArray(keys)) {
keys = [keys];
}
keys.forEach(key => this.values[key] = val);
return this;
}
public remove(key: string): StringCollection {
delete this.values[key];
return this;
}
public merge(collection: StringCollection): StringCollection {
this.values = Object.assign({}, this.values, collection.values);
return this;
}
public get(key: string): string {
return this.values[key];
}
public keys(): string[] {
return Object.keys(this.values);
}
public count(): number {
return Object.keys(this.values).length;
}
}

View File

@ -15,20 +15,20 @@ describe('DirectiveParser', () => {
it('should extract contents when no translate attribute value is provided', () => { it('should extract contents when no translate attribute value is provided', () => {
const contents = '<div translate>Hello World</div>'; const contents = '<div translate>Hello World</div>';
const messages = parser.process(templateFilename, contents); const keys = parser.extract(contents, templateFilename).keys();
expect(messages).to.deep.equal(['Hello World']); expect(keys).to.deep.equal(['Hello World']);
}); });
it('should extract translate attribute if provided', () => { it('should extract translate attribute if provided', () => {
const contents = '<div translate="KEY">Hello World<div>'; const contents = '<div translate="KEY">Hello World<div>';
const messages = parser.process(templateFilename, contents); const keys = parser.extract(contents, templateFilename).keys();
expect(messages).to.deep.equal(['KEY']); expect(keys).to.deep.equal(['KEY']);
}); });
it('should extract bound translate attribute as key if provided', () => { it('should extract bound translate attribute as key if provided', () => {
const contents = `<div [translate]="'KEY'">Hello World<div>`; const contents = `<div [translate]="'KEY'">Hello World<div>`;
const messages = parser.process(templateFilename, contents); const keys = parser.extract(contents, templateFilename).keys();
expect(messages).to.deep.equal(['KEY']); expect(keys).to.deep.equal(['KEY']);
}); });
it('should extract direct text nodes when no translate attribute value is provided', () => { it('should extract direct text nodes when no translate attribute value is provided', () => {
@ -39,8 +39,8 @@ describe('DirectiveParser', () => {
Hi <em>there</em> Hi <em>there</em>
</div> </div>
`; `;
const messages = parser.process(templateFilename, contents); const keys = parser.extract(contents, templateFilename).keys();
expect(messages).to.deep.equal(['Hello', 'Hi']); expect(keys).to.deep.equal(['Hello', 'Hi']);
}); });
it('should extract direct text nodes of tags with a translate attribute', () => { it('should extract direct text nodes of tags with a translate attribute', () => {
@ -51,8 +51,8 @@ describe('DirectiveParser', () => {
<div translate>Hi there</div> <div translate>Hi there</div>
</div> </div>
`; `;
const messages = parser.process(templateFilename, contents); const keys = parser.extract(contents, templateFilename).keys();
expect(messages).to.deep.equal(['Hello World', 'Hi there']); expect(keys).to.deep.equal(['Hello World', 'Hi there']);
}); });
it('should extract translate attribute if provided or direct text nodes if not', () => { it('should extract translate attribute if provided or direct text nodes if not', () => {
@ -64,8 +64,8 @@ describe('DirectiveParser', () => {
<p [translate]="'OTHER_KEY'">Lorem Ipsum</p> <p [translate]="'OTHER_KEY'">Lorem Ipsum</p>
</div> </div>
`; `;
const messages = parser.process(templateFilename, contents); const keys = parser.extract(contents, templateFilename).keys();
expect(messages).to.deep.equal(['KEY', 'Hi there', 'OTHER_KEY']); expect(keys).to.deep.equal(['KEY', 'Hi there', 'OTHER_KEY']);
}); });
it('should extract and parse inline template', () => { it('should extract and parse inline template', () => {
@ -76,26 +76,26 @@ describe('DirectiveParser', () => {
}) })
export class TestComponent { } export class TestComponent { }
`; `;
const messages = parser.process(componentFilename, contents); const keys = parser.extract(contents, componentFilename).keys();
expect(messages).to.deep.equal(['Hello World']); expect(keys).to.deep.equal(['Hello World']);
}); });
it('should extract contents when no ng2-translate attribute value is provided', () => { it('should extract contents when no ng2-translate attribute value is provided', () => {
const contents = '<div ng2-translate>Hello World</div>'; const contents = '<div ng2-translate>Hello World</div>';
const messages = parser.process(templateFilename, contents); const keys = parser.extract(contents, templateFilename).keys();
expect(messages).to.deep.equal(['Hello World']); expect(keys).to.deep.equal(['Hello World']);
}); });
it('should extract ng2-translate attribute if provided', () => { it('should extract ng2-translate attribute if provided', () => {
const contents = '<div ng2-translate="KEY">Hello World<div>'; const contents = '<div ng2-translate="KEY">Hello World<div>';
const messages = parser.process(templateFilename, contents); const keys = parser.extract(contents, templateFilename).keys();
expect(messages).to.deep.equal(['KEY']); expect(keys).to.deep.equal(['KEY']);
}); });
it('should extract bound ng2-translate attribute as key if provided', () => { it('should extract bound ng2-translate attribute as key if provided', () => {
const contents = `<div [ng2-translate]="'KEY'">Hello World<div>`; const contents = `<div [ng2-translate]="'KEY'">Hello World<div>`;
const messages = parser.process(templateFilename, contents); const keys = parser.extract(contents, templateFilename).keys();
expect(messages).to.deep.equal(['KEY']); expect(keys).to.deep.equal(['KEY']);
}); });
}); });

View File

@ -14,20 +14,20 @@ describe('PipeParser', () => {
it('should extract interpolated strings using translate pipe', () => { it('should extract interpolated strings using translate pipe', () => {
const contents = `Hello {{ 'World' | translate }}`; const contents = `Hello {{ 'World' | translate }}`;
const messages = parser.process(templateFilename, contents); const keys = parser.extract(contents, templateFilename).keys();
expect(messages).to.deep.equal(['World']); expect(keys).to.deep.equal(['World']);
}); });
it('should extract interpolated strings using translate pipe in attributes', () => { it('should extract interpolated strings using translate pipe in attributes', () => {
const contents = `<span attr="{{ 'Hello World' | translate }}"></span>`; const contents = `<span attr="{{ 'Hello World' | translate }}"></span>`;
const messages = parser.process(templateFilename, contents); const keys = parser.extract(contents, templateFilename).keys();
expect(messages).to.deep.equal(['Hello World']); expect(keys).to.deep.equal(['Hello World']);
}); });
it('should extract bound strings using translate pipe in attributes', () => { it('should extract bound strings using translate pipe in attributes', () => {
const contents = `<span [attr]="'Hello World' | translate"></span>`; const contents = `<span [attr]="'Hello World' | translate"></span>`;
const messages = parser.process(templateFilename, contents); const keys = parser.extract(contents, templateFilename).keys();
expect(messages).to.deep.equal(['Hello World']); expect(keys).to.deep.equal(['Hello World']);
}); });
}); });

View File

@ -30,8 +30,8 @@ describe('ServiceParser', () => {
protected _translateService: TranslateService protected _translateService: TranslateService
) { } ) { }
`; `;
const messages = parser.extractTranslateServiceVar(contents); const name = parser.extractTranslateServiceVar(contents);
expect(messages).to.equal('_translateService'); expect(name).to.equal('_translateService');
}); });
it('should extract strings in TranslateService\'s get() method', () => { it('should extract strings in TranslateService\'s get() method', () => {
@ -43,8 +43,8 @@ describe('ServiceParser', () => {
this._translateService.get('Hello World'); this._translateService.get('Hello World');
} }
`; `;
const messages = parser.process(componentFilename, contents); const keys = parser.extract(contents, componentFilename).keys();
expect(messages).to.deep.equal(['Hello World']); expect(keys).to.deep.equal(['Hello World']);
}); });
it('should extract strings in TranslateService\'s instant() method', () => { it('should extract strings in TranslateService\'s instant() method', () => {
@ -56,8 +56,8 @@ describe('ServiceParser', () => {
this._translateService.instant('Hello World'); this._translateService.instant('Hello World');
} }
`; `;
const messages = parser.process(componentFilename, contents); const keys = parser.extract(contents, componentFilename).keys();
expect(messages).to.deep.equal(['Hello World']); expect(keys).to.deep.equal(['Hello World']);
}); });
it('should extract array of strings in TranslateService\'s get() method', () => { it('should extract array of strings in TranslateService\'s get() method', () => {
@ -69,8 +69,8 @@ describe('ServiceParser', () => {
this._translateService.get(['Hello', 'World']); this._translateService.get(['Hello', 'World']);
} }
`; `;
const messages = parser.process(componentFilename, contents); const keys = parser.extract(contents, componentFilename).keys();
expect(messages).to.deep.equal(['Hello', 'World']); expect(keys).to.deep.equal(['Hello', 'World']);
}); });
it('should extract array of strings in TranslateService\'s instant() method', () => { it('should extract array of strings in TranslateService\'s instant() method', () => {
@ -82,8 +82,8 @@ describe('ServiceParser', () => {
this._translateService.instant(['Hello', 'World']); this._translateService.instant(['Hello', 'World']);
} }
`; `;
const messages = parser.process(componentFilename, contents); const key = parser.extract(contents, componentFilename).keys();
expect(messages).to.deep.equal(['Hello', 'World']); expect(key).to.deep.equal(['Hello', 'World']);
}); });
it('should not extract strings in get()/instant() methods of other services', () => { it('should not extract strings in get()/instant() methods of other services', () => {
@ -99,8 +99,8 @@ describe('ServiceParser', () => {
this._otherService.instant('Hi there'); this._otherService.instant('Hi there');
} }
`; `;
const messages = parser.process(componentFilename, contents); const keys = parser.extract(contents, componentFilename).keys();
expect(messages).to.deep.equal([]); expect(keys).to.deep.equal([]);
}); });
}); });

View File

@ -0,0 +1,54 @@
import { expect } from 'chai';
import { StringCollection } from '../../src/utils/string.collection';
describe('StringCollection', () => {
let collection: StringCollection;
beforeEach(() => {
collection = new StringCollection();
});
it('should add item with value', () => {
collection.add('key', 'translation');
expect(collection.get('key')).to.equal('translation');
});
it('should add item with default value', () => {
collection.add('key');
expect(collection.get('key')).to.equal('');
});
it('should add array of items with default values', () => {
collection.add(['key', 'key2']);
expect(collection.count()).to.equal(2);
});
it('should remove item', () => {
collection.add('key1').add('key2').remove('key1');
expect(collection.count()).to.equal(1);
});
it('should return number of items', () => {
collection.add('key1').add('key2');
expect(collection.count()).to.equal(2);
});
it('should initialize with array of keys', () => {
const newCollection = StringCollection.fromArray(['Hello', 'World']);
expect(newCollection.count()).to.equal(2);
});
it('should initialize with key/value pairs', () => {
const newCollection = StringCollection.fromObject({'key': 'translation'});
expect(newCollection.get('key')).to.equal('translation');
});
it('should merge with other collection', () => {
collection.add('Hello');
const newCollection = StringCollection.fromArray(['World']);
expect(collection.merge(newCollection).count()).to.equal(2);
});
});