Initial commit

This commit is contained in:
Kim Biesbjerg 2016-12-03 15:09:39 +01:00
parent 916ba888aa
commit b5e1125efb
12 changed files with 342 additions and 1 deletions

19
.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
# IDE
.vscode
# Logs and other files
npm-debug.log*
.DS_Store
# Compiled files
dist
# Extracted strings
*.json
*.pot
# Dependency directory
node_modules
# Source maps for JS builds
*.js.map

View File

@ -1 +1,14 @@
# ng2-translate-extract
# ng2-translate-extract
Extract strings from projects using ng2-translate to json or pot files.
## Install
1. `git clone https://github.com/biesbjerg/ng2-translate-extract.git`
2. `cd ng2-translate-extract`
3. `npm install`
## Try it out
1. Run `npm run watch` in a separate terminal
2. Edit `src/test.ts` to your likings
3. `node dist/test.js` to run it
**THIS IS STILL VERY MUCH A WORK IN PROGRESS**

37
package.json Normal file
View File

@ -0,0 +1,37 @@
{
"name": "ng2-translate-extract",
"version": "0.1.0",
"description": "Extract strings",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"postinstall": "npm run build"
},
"repository": {
"type": "git",
"url": "https://github.com/biesbjerg/ng2-translate-extract.git"
},
"keywords": [],
"author": "Kim Biesbjerg <kim@biesbjerg.com>",
"license": "MIT",
"bugs": {
"url": "https://github.com/biesbjerg/ng2-translate-extract/issues"
},
"homepage": "https://github.com/biesbjerg/ng2-translate-extract",
"engines": {
"node": ">=4.1.1"
},
"devDependencies": {
"typescript": "~2.0.10"
},
"config": {},
"dependencies": {
"@types/glob": "^5.0.30",
"@types/lodash": "^4.14.41",
"fs": "0.0.1-security",
"glob": "^7.1.1",
"lodash": "^4.17.2"
}
}

74
src/extractor.ts Normal file
View File

@ -0,0 +1,74 @@
import { ParserInterface } from './parsers/parser.interface';
import { HtmlParser } from './parsers/html.parser';
import { TypescriptParser } from './parsers/typescript.parser';
import { SerializerInterface } from './serializers/serializer.interface';
import { PotSerializer } from './serializers/pot.serializer';
import { uniq as arrayUnique } from 'lodash';
import { sync as readDir } from 'glob';
import { readFileSync as readFile, writeFileSync as writeFile } from 'fs';
export interface TypeParserMap {
[ext: string]: ParserInterface
}
export class Extractor {
public messages: string[] = [];
public parsers: TypeParserMap = {
html: new HtmlParser(),
ts: new TypescriptParser()
};
public constructor(public serializer: SerializerInterface) { }
/**
* Extracts messages from paths
*/
public extract(paths: string[]): string[] {
let messages = [];
paths.forEach(path => {
const filePaths = readDir(path);
filePaths.forEach(filePath => {
const result = this._extractMessages(filePath);
messages = [...messages, ...result];
});
});
return this.messages = arrayUnique(messages);
}
/**
* Serialize and return output
*/
public serialize(): string {
return this.serializer.serialize(this.messages);
}
/**
* Serialize and save to destination
*/
public save(destination: string): string {
const data = this.serialize();
writeFile(destination, data);
return data;
}
/**
* Extract messages from file using specialized parser
*/
protected _extractMessages(filePath: string): string[] {
const ext: string = filePath.split('.').pop();
if (!this.parsers.hasOwnProperty(ext)) {
return [];
}
const contents: string = readFile(filePath).toString();
const parser: ParserInterface = this.parsers[ext];
return parser.process(contents);
}
}

View File

@ -0,0 +1,25 @@
import { ParserInterface } from './parser.interface';
export class HtmlParser implements ParserInterface {
public patterns = {
PipeSingleQuote: '{{\\s*\'((?:\\\\.|[^\'\\\\])*)\'\\s*\\|\\s*translate(:.*?)?\\s*}}',
PipeDoubleQuote: '{{\\s*"((?:\\\\.|[^"\\\\])*)"\\s*\\|\\s*translate(:.*?)?\\s*}}'
}
public process(contents: string): string[] {
let results: string[] = [];
for (let patternName in this.patterns) {
const regExp = new RegExp(this.patterns[patternName], 'g');
let matches;
while (matches = regExp.exec(contents)) {
results.push(matches[1]);
}
}
return results;
}
}

View File

@ -0,0 +1,5 @@
export interface ParserInterface {
process(contents: string): string[];
}

View File

@ -0,0 +1,60 @@
import { ParserInterface } from './parser.interface';
export class TypescriptParser implements ParserInterface {
public patterns = {
TranslateServiceMethodsSingleQuote: '{{TRANSLATE_SERVICE}}\.(?:get|instant)\\s*\\\(\\s*\'((?:\\\\.|[^\'\\\\])*)\\s*\'',
TranslateServiceMethodsDoubleQuote: '{{TRANSLATE_SERVICE}}\.(?:get|instant)\\s*\\\(\\s*"((?:\\\\.|[^"\\\\])*)\\s*"',
}
public process(contents: string): string[] {
let results: string[] = [];
const translateServiceVar = this._extractTranslateServiceVar(contents);
if (!translateServiceVar) {
return [];
}
for (let patternName in this.patterns) {
const regExp = this._createRegExp(patternName, {
TRANSLATE_SERVICE: translateServiceVar
});
let matches;
while (matches = regExp.exec(contents)) {
results.push(matches[1]);
}
}
return results;
}
/**
* Create regular expression, replacing placeholders with real values
*/
protected _createRegExp(patternName: string, replaceVars: {} = {}): RegExp {
if (!this.patterns.hasOwnProperty(patternName)) {
throw new Error('Invalid pattern name');
}
let pattern = this.patterns[patternName];
Object.keys(replaceVars).forEach(key => {
pattern = pattern.replace('{{' + key + '}}', replaceVars[key]);
});
return new RegExp(pattern, 'gi');
}
/**
* Extract name of TranslateService variable for use in patterns
*/
protected _extractTranslateServiceVar(contents: string): string {
const matches = contents.match(/([a-z0-9_]+)\s*:\s*TranslateService/i)
if (matches === null) {
return '';
}
return matches[1];
}
}

View File

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

View File

@ -0,0 +1,46 @@
import { SerializerInterface } from './serializer.interface';
export class PotSerializer implements SerializerInterface {
protected _headers = {
'Content-Type': 'text/plain; charset=utf-8',
'Content-Transfer-Encoding': '8bit'
};
protected _buffer: string[] = [];
public serialize(messages: string[]): string {
this._reset();
this._addHeader(this._headers);
this._addMessages(messages);
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(messages: string[]): void {
messages.forEach(message => {
this._add('msgid', message);
this._add('msgstr', '');
});
}
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\\"');
}
}

View File

@ -0,0 +1,5 @@
export interface SerializerInterface {
serialize(messages: string[]): string;
}

26
src/test.ts Normal file
View File

@ -0,0 +1,26 @@
import { Extractor } from './extractor';
import { JsonSerializer } from './serializers/json.serializer';
import { PotSerializer } from './serializers/pot.serializer';
const root = '/Users/kim/ionic/mindly-app/master/src';
const paths = [
root + '/**/*.html',
root + '/**/*.ts'
];
const destination = 'template.pot';
// const serializer = new JsonSerializer();
const serializer = new PotSerializer();
const extractor = new Extractor(serializer);
try {
extractor.extract(paths);
const output = extractor.save(destination);
console.log(`Extracted strings to "${destination}"`);
console.log();
console.log('OUTPUT:');
console.log(output);
} catch (e) {
console.log(`Error extracting strings to "${destination}"`);
throw e;
}

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"target": "ES6",
"module": "commonjs",
"outDir": "./dist/",
"sourceMap": true,
"declaration": true,
"removeComments": true,
"forceConsistentCasingInFileNames": true
},
"exclude": [
"dist",
"node_modules"
]
}