Add option to merge extracted strings with existing translations (Thanks to @ocombe)
This commit is contained in:
9
src/compilers/compiler.interface.ts
Normal file
9
src/compilers/compiler.interface.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { TranslationCollection } from '../utils/translation.collection';
|
||||
|
||||
export interface CompilerInterface {
|
||||
|
||||
compile(collection: TranslationCollection): string;
|
||||
|
||||
parse(contents: string): TranslationCollection;
|
||||
|
||||
}
|
||||
14
src/compilers/json.compiler.ts
Normal file
14
src/compilers/json.compiler.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { CompilerInterface } from './compiler.interface';
|
||||
import { TranslationCollection } from '../utils/translation.collection';
|
||||
|
||||
export class JsonCompiler implements CompilerInterface {
|
||||
|
||||
public compile(collection: TranslationCollection): string {
|
||||
return JSON.stringify(collection.values, null, '\t');
|
||||
}
|
||||
|
||||
public parse(contents: string): TranslationCollection {
|
||||
return new TranslationCollection(JSON.parse(contents));
|
||||
}
|
||||
|
||||
}
|
||||
53
src/compilers/po.compiler.ts
Normal file
53
src/compilers/po.compiler.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { CompilerInterface } from './compiler.interface';
|
||||
import { TranslationCollection } from '../utils/translation.collection';
|
||||
|
||||
import * as gettext from 'gettext-parser';
|
||||
|
||||
export class PoCompiler implements CompilerInterface {
|
||||
|
||||
/**
|
||||
* Translation domain
|
||||
*/
|
||||
public domain = '';
|
||||
|
||||
public compile(collection: TranslationCollection): string {
|
||||
const data = {
|
||||
charset: 'utf-8',
|
||||
headers: {
|
||||
'mime-version': '1.0',
|
||||
'content-type': 'text/plain; charset=utf-8',
|
||||
'content-transfer-encoding': '8bit'
|
||||
},
|
||||
translations: {
|
||||
'default': Object.keys(collection.values).reduce((translations, key) => {
|
||||
translations[key] = {
|
||||
msgid: key,
|
||||
msgstr: collection.get(key)
|
||||
};
|
||||
return translations;
|
||||
}, {})
|
||||
}
|
||||
};
|
||||
|
||||
return gettext.po.compile(data, 'utf-8');
|
||||
}
|
||||
|
||||
public parse(contents: string): TranslationCollection {
|
||||
const collection = new TranslationCollection();
|
||||
|
||||
const po = gettext.po.parse(contents, 'utf-8');
|
||||
if (!po.translations.hasOwnProperty(this.domain)) {
|
||||
return collection;
|
||||
}
|
||||
|
||||
const values = Object.keys(po.translations[this.domain])
|
||||
.filter(key => key.length > 0)
|
||||
.reduce((values, key) => {
|
||||
values[key] = po.translations[this.domain][key].msgstr.pop();
|
||||
return values;
|
||||
}, {});
|
||||
|
||||
return new TranslationCollection(values);
|
||||
}
|
||||
|
||||
}
|
||||
1
src/declarations.d.ts
vendored
Normal file
1
src/declarations.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module '*';
|
||||
@@ -1,17 +1,16 @@
|
||||
import { Extractor } from './extractor';
|
||||
import { JsonSerializer } from './serializers/json.serializer';
|
||||
import { StringCollection } from './utils/string.collection';
|
||||
import { JsonCompiler } from './compilers/json.compiler';
|
||||
import { TranslationCollection } from './utils/translation.collection';
|
||||
|
||||
const serializer = new JsonSerializer();
|
||||
const extractor = new Extractor(serializer);
|
||||
const compiler = new JsonCompiler();
|
||||
const extractor = new Extractor();
|
||||
|
||||
const src = '/your/project';
|
||||
const dest = '/your/project/template.json';
|
||||
const dirPath = '/your/project';
|
||||
|
||||
try {
|
||||
const collection: StringCollection = extractor.process(src);
|
||||
const serialized: string = extractor.save(dest);
|
||||
console.log({ strings: collection.keys(), serialized });
|
||||
const collection: TranslationCollection = extractor.process(dirPath);
|
||||
const result: string = compiler.compile(collection);
|
||||
console.log(result);
|
||||
} catch (e) {
|
||||
console.log(`Something went wrong: ${e.toString()}`);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,7 @@ import { ParserInterface } from './parsers/parser.interface';
|
||||
import { PipeParser } from './parsers/pipe.parser';
|
||||
import { DirectiveParser } from './parsers/directive.parser';
|
||||
import { ServiceParser } from './parsers/service.parser';
|
||||
import { SerializerInterface } from './serializers/serializer.interface';
|
||||
import { StringCollection } from './utils/string.collection';
|
||||
import { TranslationCollection } from './utils/translation.collection';
|
||||
|
||||
import * as glob from 'glob';
|
||||
import * as fs from 'fs';
|
||||
@@ -22,42 +21,24 @@ export class Extractor {
|
||||
new ServiceParser()
|
||||
];
|
||||
|
||||
public collection: StringCollection = new StringCollection();
|
||||
|
||||
public constructor(public serializer: SerializerInterface) { }
|
||||
|
||||
/**
|
||||
* Process dir
|
||||
* Extract strings from dir
|
||||
*/
|
||||
public process(dir: string): StringCollection {
|
||||
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) => {
|
||||
this.collection.merge(parser.extract(contents, path));
|
||||
collection = collection.union(parser.extract(contents, path));
|
||||
});
|
||||
});
|
||||
|
||||
return this.collection;
|
||||
return collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize and return output
|
||||
*/
|
||||
public serialize(): string {
|
||||
return this.serializer.serialize(this.collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize and save to destination
|
||||
*/
|
||||
public save(destination: string): string {
|
||||
const data = this.serialize();
|
||||
fs.writeFileSync(destination, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all files in dir matching find patterns
|
||||
* Get all files in dir matching patterns
|
||||
*/
|
||||
protected _readDir(dir: string, patterns: string[]): string[] {
|
||||
return patterns.reduce((results, pattern) => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ParserInterface } from './parser.interface';
|
||||
import { AbstractTemplateParser } from './abstract-template.parser';
|
||||
import { StringCollection } from '../utils/string.collection';
|
||||
import { TranslationCollection } from '../utils/translation.collection';
|
||||
|
||||
import * as $ from 'cheerio';
|
||||
|
||||
export class DirectiveParser extends AbstractTemplateParser implements ParserInterface {
|
||||
|
||||
public extract(contents: string, path?: string): StringCollection {
|
||||
public extract(contents: string, path?: string): TranslationCollection {
|
||||
if (path && this._isAngularComponent(path)) {
|
||||
contents = this._extractInlineTemplate(contents);
|
||||
}
|
||||
@@ -14,8 +14,8 @@ export class DirectiveParser extends AbstractTemplateParser implements ParserInt
|
||||
return this._parseTemplate(contents);
|
||||
}
|
||||
|
||||
protected _parseTemplate(template: string): StringCollection {
|
||||
const collection = new StringCollection();
|
||||
protected _parseTemplate(template: string): TranslationCollection {
|
||||
let collection: TranslationCollection = new TranslationCollection();
|
||||
|
||||
template = this._normalizeTemplateAttributes(template);
|
||||
$(template)
|
||||
@@ -26,7 +26,7 @@ export class DirectiveParser extends AbstractTemplateParser implements ParserInt
|
||||
const attr = $element.attr('translate') || $element.attr('ng2-translate');
|
||||
|
||||
if (attr) {
|
||||
collection.add(attr);
|
||||
collection = collection.add(attr);
|
||||
} else {
|
||||
$element
|
||||
.contents()
|
||||
@@ -34,7 +34,7 @@ export class DirectiveParser extends AbstractTemplateParser implements ParserInt
|
||||
.filter(node => node.type === 'text')
|
||||
.map(node => node.nodeValue.trim())
|
||||
.filter(text => text.length > 0)
|
||||
.forEach(text => collection.add(text));
|
||||
.forEach(text => collection = collection.add(text));
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { StringCollection } from '../utils/string.collection';
|
||||
import { TranslationCollection } from '../utils/translation.collection';
|
||||
|
||||
export interface ParserInterface {
|
||||
|
||||
extract(contents: string, path?: string): StringCollection;
|
||||
extract(contents: string, path?: string): TranslationCollection;
|
||||
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ParserInterface } from './parser.interface';
|
||||
import { AbstractTemplateParser } from './abstract-template.parser';
|
||||
import { StringCollection } from '../utils/string.collection';
|
||||
import { TranslationCollection } from '../utils/translation.collection';
|
||||
|
||||
export class PipeParser extends AbstractTemplateParser implements ParserInterface {
|
||||
|
||||
public extract(contents: string, path?: string): StringCollection {
|
||||
public extract(contents: string, path?: string): TranslationCollection {
|
||||
if (path && this._isAngularComponent(path)) {
|
||||
contents = this._extractInlineTemplate(contents);
|
||||
}
|
||||
@@ -12,14 +12,14 @@ export class PipeParser extends AbstractTemplateParser implements ParserInterfac
|
||||
return this._parseTemplate(contents);
|
||||
}
|
||||
|
||||
protected _parseTemplate(template: string): StringCollection {
|
||||
const collection = new StringCollection();
|
||||
protected _parseTemplate(template: string): TranslationCollection {
|
||||
let collection: TranslationCollection = new TranslationCollection();
|
||||
|
||||
const regExp = new RegExp(/(['"`])([^\1\r\n]*)\1\s*\|\s*translate(:.*?)?/, 'g');
|
||||
|
||||
let matches;
|
||||
while (matches = regExp.exec(template)) {
|
||||
collection.add(matches[2]);
|
||||
collection = collection.add(matches[2]);
|
||||
}
|
||||
|
||||
return collection;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ParserInterface } from './parser.interface';
|
||||
import { StringCollection } from '../utils/string.collection';
|
||||
import { TranslationCollection } from '../utils/translation.collection';
|
||||
|
||||
export class ServiceParser implements ParserInterface {
|
||||
|
||||
public extract(contents: string, path?: string): StringCollection {
|
||||
const collection = new StringCollection();
|
||||
public extract(contents: string, path?: string): TranslationCollection {
|
||||
let collection: TranslationCollection = new TranslationCollection();
|
||||
|
||||
const translateServiceVar = this._extractTranslateServiceVar(contents);
|
||||
if (!translateServiceVar) {
|
||||
@@ -17,10 +17,9 @@ export class ServiceParser implements ParserInterface {
|
||||
let matches;
|
||||
while (matches = regExp.exec(contents)) {
|
||||
if (this._stringContainsArray(matches[1])) {
|
||||
const matchCollection = StringCollection.fromArray(this._stringToArray(matches[1]));
|
||||
collection.merge(matchCollection);
|
||||
collection = collection.addKeys(this._stringToArray(matches[1]));
|
||||
} else {
|
||||
collection.add(matches[3]);
|
||||
collection = collection.add(matches[3]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
import { SerializerInterface } from './serializer.interface';
|
||||
import { StringCollection } from '../utils/string.collection';
|
||||
|
||||
export class JsonSerializer implements SerializerInterface {
|
||||
|
||||
public serialize(collection: StringCollection): string {
|
||||
return JSON.stringify(collection.values, null, '\t');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { SerializerInterface } from './serializer.interface';
|
||||
import { StringCollection } from '../utils/string.collection';
|
||||
|
||||
export class PotSerializer implements SerializerInterface {
|
||||
|
||||
protected _headers = {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
'Content-Transfer-Encoding': '8bit'
|
||||
};
|
||||
|
||||
protected _buffer: string[] = [];
|
||||
|
||||
public serialize(collection: StringCollection): string {
|
||||
this._reset();
|
||||
this._addHeader(this._headers);
|
||||
this._addMessages(collection);
|
||||
|
||||
return this._buffer.join('\n');
|
||||
}
|
||||
|
||||
protected _addHeader(headers: {}): void {
|
||||
this._add('msgid', '');
|
||||
this._add('msgstr', '');
|
||||
Object.keys(headers).forEach(key => {
|
||||
this._buffer.push(`"${key}: ${headers[key]}\\n"`);
|
||||
});
|
||||
}
|
||||
|
||||
protected _addMessages(collection: StringCollection): void {
|
||||
Object.keys(collection.values).forEach(key => {
|
||||
this._add('msgid', key);
|
||||
this._add('msgstr', collection.get(key));
|
||||
});
|
||||
}
|
||||
|
||||
protected _add(key: string, val: string): void {
|
||||
this._buffer.push(`${key} "${this._escape(val)}"`);
|
||||
}
|
||||
|
||||
protected _reset(): void {
|
||||
this._buffer = [];
|
||||
}
|
||||
|
||||
protected _escape(message: string): string {
|
||||
return message.replace(/"([^"\\]+)*"/g, '\\"$1\\"');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
import { StringCollection } from '../utils/string.collection';
|
||||
|
||||
export interface SerializerInterface {
|
||||
|
||||
serialize(collection: StringCollection): string;
|
||||
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
69
src/utils/translation.collection.ts
Normal file
69
src/utils/translation.collection.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export interface TranslationType {
|
||||
[key: string]: string
|
||||
};
|
||||
|
||||
export class TranslationCollection {
|
||||
|
||||
public values: TranslationType = {};
|
||||
|
||||
public constructor(values: TranslationType = {}) {
|
||||
this.values = values;
|
||||
}
|
||||
|
||||
public add(key: string, val: string = ''): TranslationCollection {
|
||||
return new TranslationCollection(Object.assign({}, this.values, { [key]: val }));
|
||||
}
|
||||
|
||||
public addKeys(keys: string[]): TranslationCollection {
|
||||
const values = keys.reduce((results, key) => {
|
||||
results[key] = '';
|
||||
return results;
|
||||
}, {});
|
||||
return new TranslationCollection(Object.assign({}, this.values, values));
|
||||
}
|
||||
|
||||
public remove(key: string): TranslationCollection {
|
||||
let newCollection = new TranslationCollection();
|
||||
Object.keys(this.values).forEach(collectionKey => {
|
||||
if (key !== collectionKey) {
|
||||
newCollection = newCollection.add(key, this.values[key]);
|
||||
}
|
||||
});
|
||||
return newCollection;
|
||||
}
|
||||
|
||||
public union(collection: TranslationCollection): TranslationCollection {
|
||||
return new TranslationCollection(Object.assign({}, this.values, collection.values));
|
||||
}
|
||||
|
||||
public intersect(collection: TranslationCollection): TranslationCollection {
|
||||
let newCollection = new TranslationCollection();
|
||||
Object.keys(this.values).forEach(key => {
|
||||
if (collection.has(key)) {
|
||||
newCollection = newCollection.add(key, this.values[key]);
|
||||
}
|
||||
});
|
||||
return newCollection;
|
||||
}
|
||||
|
||||
public has(key: string): boolean {
|
||||
return this.values.hasOwnProperty(key);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public isEmpty(): boolean {
|
||||
return Object.keys(this.values).length === 0;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user