Compare commits

..

26 Commits

Author SHA1 Message Date
Kim Biesbjerg
64ebb5e6e8 Bump version 2017-11-08 14:02:41 +01:00
Kim Biesbjerg
40051f4144 Bump version 2017-11-07 15:17:38 +01:00
Sean G. Wright
14eb09f947 feat(cli): add verbose (vb) flag that can control output of all file … (#74)
* feat(cli): add verbose (vb) flag that can control output of all file paths to console

* docs(README): add -vb description
2017-11-07 15:14:31 +01:00
Tiago Dionesto Willrich da Silva
8d1e2c5a2f Change typescript to be a dependency (#75)
Typescript is being imported by the parser files, so it should be a direct dependency instead of just a development one.

Without that change, it's impossible to use the extractor programatically or with npx.
2017-11-07 15:13:35 +01:00
Dominik Herbst
4892ea5146 Configured cheerio to work with non-HTML standard elements to fix issues with custom component tags. (#79) 2017-11-07 15:13:01 +01:00
Kim Biesbjerg
ee28fe2a64 Create LICENSE 2017-07-08 14:10:54 +02:00
Kim Biesbjerg
7c06b66974 Bump version 2017-07-05 15:46:05 +02:00
Kim Biesbjerg
b2ae17697d Replace all occurences of escaped quotes. 2017-07-05 15:43:54 +02:00
Kim Biesbjerg
5259da8fe3 Add support for TranslateService's stream method. Closes #60 2017-07-05 15:18:41 +02:00
Kim Biesbjerg
2d73f056ff Update dependencies 2017-07-05 15:16:17 +02:00
Kim Biesbjerg
fde5245731 Bump version 2017-05-10 14:10:36 +02:00
Kim Biesbjerg
4ee7258a31 Fix potential bug when extracting strings from file containing multiple classes 2017-05-10 14:10:13 +02:00
Kim Biesbjerg
a6c7af0630 Update dependencies 2017-05-10 14:07:45 +02:00
Kim Biesbjerg
0949bf765b Bump version 2017-05-09 20:09:03 +02:00
Kim Biesbjerg
d416c6b9fd Add support for extracting strings from multiple classes per file. Closes #46 2017-05-09 20:08:44 +02:00
Kim Biesbjerg
4e351405fb Bump version 2017-05-09 14:40:06 +02:00
Kim Biesbjerg
39a335638b Add support for parsing NamespacedJson in Json compiler. Closes #44 2017-05-09 14:39:39 +02:00
Kim Biesbjerg
3b9561916b Fix crash when constructor parameter has no type. Closes #38 2017-05-05 11:31:30 +02:00
Kim Biesbjerg
5cef383f3b Forgot to build v2.2.2 before publishing to npm 2017-05-05 11:18:51 +02:00
Kim Biesbjerg
677d2a35ca Update dependencies. Bump version 2017-04-06 08:52:22 +02:00
cvaliere
262a89206d fix parser regexp (#31)
- fix template parser regexp (Closes #15)
2017-04-06 08:50:23 +02:00
Kim Biesbjerg
bcb4a9c069 Fix bug where obsolete strings were not removed when --clean was used. Closes #29 2017-03-31 08:31:14 +02:00
Kim Biesbjerg
5ad1fe6a18 Remove unused import 2017-03-31 08:21:28 +02:00
Kim Biesbjerg
bc5ce7e80d Add marker argument to readme 2017-03-30 14:42:03 +02:00
Kim Biesbjerg
030ab145d6 Add return types 2017-03-30 14:40:51 +02:00
Kim Biesbjerg
daaebede6f Add support for marker functions, to be able to extract strings not directly passed to TranslateService. Closes #10 2017-03-30 14:37:30 +02:00
21 changed files with 1689 additions and 147 deletions

4
.gitignore vendored
View File

@@ -10,8 +10,8 @@ npm-debug.log*
dist
# Extracted strings
template.json
template.pot
strings.json
strings.pot
# Dependency directory
node_modules

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2017 Kim Biesbjerg
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -50,6 +50,18 @@ If you want to use spaces instead, you can do the following:
`ngx-translate-extract -i ./src -o ./src/i18n/en.json --format-indentation ' '`
## Mark strings for extraction using a marker function
If, for some reason, you want to extract strings not passed directly to TranslateService, you can wrap them in a custom marker function.
```ts
import { _ } from '@biesbjerg/ngx-translate-extract';
_('Extract me');
```
Add the `marker` argument when running the extract script:
`ngx-translate-extract ... -m _`
Modify the scripts arguments as required.
@@ -70,6 +82,8 @@ Options:
--output, -o Paths where you would like to save extracted
strings. You can use path expansion, glob patterns
and multiple paths [array] [required]
--marker, -m Extract strings passed to a marker function
[string] [default: false]
--format, -f Output format
[string] [choices: "json", "namespaced-json", "pot"] [default: "json"]
--format-indentation, --fi Output format indentation [string] [default: "\t"]
@@ -79,3 +93,5 @@ Options:
[boolean] [default: false]
--clean, -c Remove obsolete strings when merging
[boolean] [default: false]
--verbose, -vb If true, prints all processed file paths to console
[boolean] [default: true]

1278
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@biesbjerg/ngx-translate-extract",
"version": "2.1.0",
"version": "2.3.4",
"description": "Extract strings from projects using ngx-translate",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
@@ -47,30 +47,30 @@
},
"config": {},
"devDependencies": {
"@types/chai": "3.4.35",
"@types/chai": "4.0.1",
"@types/glob": "5.0.30",
"@types/mocha": "2.2.40",
"@types/mocha": "2.2.41",
"@types/cheerio": "0.22.1",
"@types/chalk": "0.4.31",
"@types/flat": "0.0.28",
"@types/yargs": "6.6.0",
"@types/yargs": "8.0.0",
"@types/mkdirp": "0.3.29",
"chai": "3.5.0",
"mocha": "3.2.0",
"ts-node": "3.0.2",
"tslint": "4.5.1",
"tslint-eslint-rules": "3.5.1",
"typescript": "2.2.2"
"chai": "4.0.2",
"mocha": "3.4.2",
"ts-node": "3.1.0",
"tslint": "5.4.3",
"tslint-eslint-rules": "4.1.1"
},
"dependencies": {
"chalk": "1.1.3",
"yargs": "7.0.2",
"cheerio": "0.22.0",
"chalk": "2.0.1",
"yargs": "8.0.2",
"cheerio": "1.0.0-rc.2",
"fs": "0.0.1-security",
"gettext-parser": "1.2.2",
"glob": "7.1.1",
"glob": "7.1.2",
"path": "0.12.7",
"mkdirp": "0.5.1",
"flat": "2.0.1"
"flat": "2.0.1",
"typescript": "2.4.1"
}
}

View File

@@ -3,6 +3,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 { FunctionParser } from '../parsers/function.parser';
import { CompilerInterface } from '../compilers/compiler.interface';
import { CompilerFactory } from '../compilers/compiler.factory';
@@ -44,6 +45,12 @@ export const cli = yargs
normalize: true,
required: true
})
.option('marker', {
alias: 'm',
describe: 'Extract strings passed to a marker function',
default: false,
type: 'string'
})
.option('format', {
alias: 'f',
describe: 'Output format',
@@ -75,25 +82,37 @@ export const cli = yargs
default: false,
type: 'boolean'
})
.option('verbose', {
alias: 'vb',
describe: 'Log all output to console',
default: true,
type: 'boolean'
})
.exitProcess(true)
.parse(process.argv);
const parsers: ParserInterface[] = [
new ServiceParser(),
new PipeParser(),
new DirectiveParser()
];
const compiler: CompilerInterface = CompilerFactory.create(cli.format, {
indentation: cli.formatIndentation
});
new ExtractTask(cli.input, cli.output, {
const extract = new ExtractTask(cli.input, cli.output, {
replace: cli.replace,
sort: cli.sort,
clean: cli.clean,
patterns: cli.patterns
})
.setParsers(parsers)
.setCompiler(compiler)
.execute();
});
const compiler: CompilerInterface = CompilerFactory.create(cli.format, {
indentation: cli.formatIndentation
});
extract.setCompiler(compiler);
const parsers: ParserInterface[] = [
new PipeParser(),
new DirectiveParser(),
new ServiceParser()
];
if (cli.marker) {
parsers.push(new FunctionParser({
identifier: cli.marker
}));
}
extract.setParsers(parsers);
extract.execute();

View File

@@ -14,6 +14,7 @@ export interface ExtractTaskOptionsInterface {
sort?: boolean;
clean?: boolean;
patterns?: string[];
verbose?: boolean;
}
export class ExtractTask implements TaskInterface {
@@ -22,7 +23,8 @@ export class ExtractTask implements TaskInterface {
replace: false,
sort: false,
clean: false,
patterns: []
patterns: [],
verbose: true
};
protected _parsers: ParserInterface[] = [];
@@ -41,11 +43,6 @@ export class ExtractTask implements TaskInterface {
}
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);
}
@@ -69,7 +66,7 @@ export class ExtractTask implements TaskInterface {
let collection: TranslationCollection = new TranslationCollection();
this._input.forEach(dir => {
this._readDir(dir, this._options.patterns).forEach(path => {
this._out(chalk.gray('- %s'), path);
this._options.verbose && 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));
@@ -109,7 +106,7 @@ export class ExtractTask implements TaskInterface {
if (this._options.clean) {
const collectionCount = processedCollection.count();
processedCollection = processedCollection.intersect(processedCollection);
processedCollection = processedCollection.intersect(collection);
const removeCount = collectionCount - processedCollection.count();
if (removeCount > 0) {
this._out(chalk.dim('- removed %d obsolete strings'), removeCount);

View File

@@ -1,6 +1,8 @@
import { CompilerInterface } from './compiler.interface';
import { TranslationCollection } from '../utils/translation.collection';
import * as flat from 'flat';
export class JsonCompiler implements CompilerInterface {
public indentation: string = '\t';
@@ -18,7 +20,15 @@ export class JsonCompiler implements CompilerInterface {
}
public parse(contents: string): TranslationCollection {
return new TranslationCollection(JSON.parse(contents));
let values: any = JSON.parse(contents);
if (this._isNamespacedJsonFormat(values)) {
values = flat.flatten(values);
}
return new TranslationCollection(values);
}
protected _isNamespacedJsonFormat(values: any): boolean {
return Object.keys(values).some(key => typeof values[key] === 'object');
}
}

View File

@@ -1,5 +1,5 @@
export * from './utils/translation.collection';
export * from './utils/ast-utils';
export * from './utils/utils';
export * from './cli/cli';
export * from './cli/tasks/task.interface';
@@ -7,9 +7,11 @@ export * from './cli/tasks/extract.task';
export * from './parsers/parser.interface';
export * from './parsers/abstract-template.parser';
export * from './parsers/abstract-ast.parser';
export * from './parsers/directive.parser';
export * from './parsers/pipe.parser';
export * from './parsers/service.parser';
export * from './parsers/function.parser';
export * from './compilers/compiler.interface';
export * from './compilers/compiler.factory';

View File

@@ -0,0 +1,65 @@
import * as ts from 'typescript';
export abstract class AbstractAstParser {
protected _sourceFile: ts.SourceFile;
protected _createSourceFile(path: string, contents: string): ts.SourceFile {
return ts.createSourceFile(path, contents, null, /*setParentNodes */ false);
}
/**
* Get strings from function call's first argument
*/
protected _getCallArgStrings(callNode: ts.CallExpression): string[] {
if (!callNode.arguments.length) {
return;
}
const firstArg = callNode.arguments[0];
switch (firstArg.kind) {
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.FirstTemplateToken:
return [(firstArg as ts.StringLiteral).text];
case ts.SyntaxKind.ArrayLiteralExpression:
return (firstArg as ts.ArrayLiteralExpression).elements
.map((element: ts.StringLiteral) => element.text);
case ts.SyntaxKind.Identifier:
console.log('WARNING: We cannot extract variable values passed to TranslateService (yet)');
break;
default:
console.log(`SKIP: Unknown argument type: '${this._syntaxKindToName(firstArg.kind)}'`, firstArg);
}
}
/**
* Find all child nodes of a kind
*/
protected _findNodes(node: ts.Node, kind: ts.SyntaxKind): ts.Node[] {
const childrenNodes: ts.Node[] = node.getChildren(this._sourceFile);
const initialValue: ts.Node[] = node.kind === kind ? [node] : [];
return childrenNodes.reduce((result: ts.Node[], childNode: ts.Node) => {
return result.concat(this._findNodes(childNode, kind));
}, initialValue);
}
protected _syntaxKindToName(kind: ts.SyntaxKind): string {
return ts.SyntaxKind[kind];
}
protected _printAllChildren(sourceFile: ts.SourceFile, node: ts.Node, depth = 0): void {
console.log(
new Array(depth + 1).join('----'),
`[${node.kind}]`,
this._syntaxKindToName(node.kind),
`[pos: ${node.pos}-${node.end}]`,
':\t\t\t',
node.getFullText(sourceFile).trim()
);
depth++;
node.getChildren(sourceFile).forEach(childNode => this._printAllChildren(sourceFile, childNode, depth));
}
}

View File

@@ -2,7 +2,9 @@ import { ParserInterface } from './parser.interface';
import { AbstractTemplateParser } from './abstract-template.parser';
import { TranslationCollection } from '../utils/translation.collection';
import * as $ from 'cheerio';
import * as cheerio from 'cheerio';
const $ = cheerio.load('', {xmlMode: true});
export class DirectiveParser extends AbstractTemplateParser implements ParserInterface {

View File

@@ -0,0 +1,61 @@
import { ParserInterface } from './parser.interface';
import { AbstractAstParser } from './abstract-ast.parser';
import { TranslationCollection } from '../utils/translation.collection';
import * as ts from 'typescript';
export class FunctionParser extends AbstractAstParser implements ParserInterface {
protected _functionIdentifier: string = '_';
public constructor(options?: any) {
super();
if (options && typeof options.identifier !== 'undefined') {
this._functionIdentifier = options.identifier;
}
}
public extract(contents: string, path?: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
this._sourceFile = this._createSourceFile(path, contents);
const callNodes = this._findCallNodes();
callNodes.forEach(callNode => {
const keys: string[] = this._getCallArgStrings(callNode);
if (keys && keys.length) {
collection = collection.addKeys(keys);
}
});
return collection;
}
/**
* Find all calls to marker function
*/
protected _findCallNodes(node?: ts.Node): ts.CallExpression[] {
if (!node) {
node = this._sourceFile;
}
let callNodes = this._findNodes(node, ts.SyntaxKind.CallExpression) as ts.CallExpression[];
callNodes = callNodes
.filter(callNode => {
// Only call expressions with arguments
if (callNode.arguments.length < 1) {
return false;
}
const identifier = (callNode.getChildAt(0) as ts.Identifier).text;
if (identifier !== this._functionIdentifier) {
return false;
}
return true;
});
return callNodes;
}
}

View File

@@ -15,10 +15,10 @@ export class PipeParser extends AbstractTemplateParser implements ParserInterfac
protected _parseTemplate(template: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
const regExp: RegExp = /(['"`])([^>\1\r\n]*?)\1\s*\|\s*translate/g;
const regExp: RegExp = /(['"`])((?:(?!\1).|\\\1)+)\1\s*\|\s*translate/g;
let matches: RegExpExecArray;
while (matches = regExp.exec(template)) {
collection = collection.add(matches[2]);
collection = collection.add(matches[2].split('\\\'').join('\''));
}
return collection;

View File

@@ -1,48 +1,46 @@
import { ParserInterface } from './parser.interface';
import { AbstractAstParser } from './abstract-ast.parser';
import { TranslationCollection } from '../utils/translation.collection';
import { syntaxKindToName } from '../utils/ast-utils';
import * as ts from 'typescript';
export class ServiceParser implements ParserInterface {
export class ServiceParser extends AbstractAstParser implements ParserInterface {
protected _sourceFile: ts.SourceFile;
protected _instancePropertyName: any;
protected _serviceClassName: string = 'TranslateService';
protected _serviceMethodNames: string[] = ['get', 'instant'];
public extract(contents: string, path?: string): TranslationCollection {
let collection: TranslationCollection = new TranslationCollection();
this._sourceFile = this._createSourceFile(path, contents);
this._instancePropertyName = this._getInstancePropertyName();
if (!this._instancePropertyName) {
return collection;
}
const callNodes = this._findCallNodes();
callNodes.forEach(callNode => {
const keys: string[] = this._getCallArgStrings(callNode);
if (keys && keys.length) {
collection = collection.addKeys(keys);
const classNodes = this._findClassNodes(this._sourceFile);
classNodes.forEach(classNode => {
const constructorNode = this._findConstructorNode(classNode);
if (!constructorNode) {
return;
}
const propertyName: string = this._findTranslateServicePropertyName(constructorNode);
if (!propertyName) {
return;
}
const callNodes = this._findCallNodes(classNode, propertyName);
callNodes.forEach(callNode => {
const keys: string[] = this._getCallArgStrings(callNode);
if (keys && keys.length) {
collection = collection.addKeys(keys);
}
});
});
return collection;
}
protected _createSourceFile(path: string, contents: string): ts.SourceFile {
return ts.createSourceFile(path, contents, null, /*setParentNodes */ false);
}
/**
* Detect what the TranslateService instance property
* is called by inspecting constructor params
* is called by inspecting constructor arguments
*/
protected _getInstancePropertyName(): string {
const constructorNode = this._findConstructorNode();
protected _findTranslateServicePropertyName(constructorNode: ts.ConstructorDeclaration): string {
if (!constructorNode) {
return null;
}
@@ -53,13 +51,18 @@ export class ServiceParser implements ParserInterface {
return false;
}
// Parameter has no type
if (!parameter.type) {
return false;
}
// Make sure className is of the correct type
const parameterType: ts.Identifier = (parameter.type as ts.TypeReferenceNode).typeName as ts.Identifier;
if (!parameterType) {
return false;
}
const className: string = parameterType.text;
if (className !== this._serviceClassName) {
if (className !== 'TranslateService') {
return false;
}
@@ -72,29 +75,34 @@ export class ServiceParser implements ParserInterface {
}
/**
* Find first constructor
* Find class nodes
*/
protected _findConstructorNode(): ts.ConstructorDeclaration {
const constructors = this._findNodes(this._sourceFile, ts.SyntaxKind.Constructor, true) as ts.ConstructorDeclaration[];
if (constructors.length) {
return constructors[0];
protected _findClassNodes(node: ts.Node): ts.ClassDeclaration[] {
return this._findNodes(node, ts.SyntaxKind.ClassDeclaration) as ts.ClassDeclaration[];
}
/**
* Find constructor
*/
protected _findConstructorNode(node: ts.ClassDeclaration): ts.ConstructorDeclaration {
const constructorNodes = this._findNodes(node, ts.SyntaxKind.Constructor) as ts.ConstructorDeclaration[];
if (constructorNodes) {
return constructorNodes[0];
}
}
/**
* Find all calls to TranslateService methods
*/
protected _findCallNodes(node?: ts.Node): ts.CallExpression[] {
if (!node) {
node = this._sourceFile;
}
protected _findCallNodes(node: ts.Node, propertyIdentifier: string): ts.CallExpression[] {
let callNodes = this._findNodes(node, ts.SyntaxKind.CallExpression) as ts.CallExpression[];
callNodes = callNodes
// Only call expressions with arguments
.filter(callNode => callNode.arguments.length > 0)
// More filters
.filter(callNode => {
// Only call expressions with arguments
if (callNode.arguments.length < 1) {
return false;
}
const propAccess = callNode.getChildAt(0).getChildAt(0) as ts.PropertyAccessExpression;
if (!propAccess || propAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) {
return false;
@@ -102,7 +110,7 @@ export class ServiceParser implements ParserInterface {
if (!propAccess.getFirstToken() || propAccess.getFirstToken().kind !== ts.SyntaxKind.ThisKeyword) {
return false;
}
if (propAccess.name.text !== this._instancePropertyName) {
if (propAccess.name.text !== propertyIdentifier) {
return false;
}
@@ -110,7 +118,7 @@ export class ServiceParser implements ParserInterface {
if (!methodAccess || methodAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) {
return false;
}
if (!methodAccess.name || this._serviceMethodNames.indexOf(methodAccess.name.text) === -1) {
if (!methodAccess.name || (methodAccess.name.text !== 'get' && methodAccess.name.text !== 'instant' && methodAccess.name.text !== 'stream')) {
return false;
}
@@ -120,44 +128,4 @@ export class ServiceParser implements ParserInterface {
return callNodes;
}
/**
* Get strings from function call's first argument
*/
protected _getCallArgStrings(callNode: ts.CallExpression): string[] {
if (!callNode.arguments.length) {
return;
}
const firstArg = callNode.arguments[0];
switch (firstArg.kind) {
case ts.SyntaxKind.StringLiteral:
case ts.SyntaxKind.FirstTemplateToken:
return [(firstArg as ts.StringLiteral).text];
case ts.SyntaxKind.ArrayLiteralExpression:
return (firstArg as ts.ArrayLiteralExpression).elements
.map((element: ts.StringLiteral) => element.text);
case ts.SyntaxKind.Identifier:
console.log('WARNING: We cannot extract variable values passed to TranslateService (yet)');
break;
default:
console.log(`SKIP: Unknown argument type: '${syntaxKindToName(firstArg.kind)}'`, firstArg);
}
}
/**
* Find all child nodes of a kind
*/
protected _findNodes(node: ts.Node, kind: ts.SyntaxKind, onlyOne: boolean = false): ts.Node[] {
if (node.kind === kind && onlyOne) {
return [node];
}
const childrenNodes: ts.Node[] = node.getChildren(this._sourceFile);
const initialValue: ts.Node[] = node.kind === kind ? [node] : [];
return childrenNodes.reduce((result: ts.Node[], childNode: ts.Node) => {
return result.concat(this._findNodes(childNode, kind));
}, initialValue);
}
}

View File

@@ -1,19 +0,0 @@
import * as ts from 'typescript';
export function printAllChildren(sourceFile: ts.SourceFile, node: ts.Node, depth = 0) {
console.log(
new Array(depth + 1).join('----'),
`[${node.kind}]`,
syntaxKindToName(node.kind),
`[pos: ${node.pos}-${node.end}]`,
':\t\t\t',
node.getFullText(sourceFile).trim()
);
depth++;
node.getChildren(sourceFile).forEach(childNode => printAllChildren(sourceFile, childNode, depth));
}
export function syntaxKindToName(kind: ts.SyntaxKind) {
return ts.SyntaxKind[kind];
}

View File

@@ -1,6 +1,6 @@
export interface TranslationType {
[key: string]: string
};
}
export class TranslationCollection {

3
src/utils/utils.ts Normal file
View File

@@ -0,0 +1,3 @@
export function _(key: string | string[]): string | string[] {
return key;
}

View File

@@ -118,4 +118,10 @@ describe('DirectiveParser', () => {
expect(template).to.equal('<p translate="KEY">Hello World</p>');
});
it('should extract contents from within custom tags', () => {
const contents = `<custom-table><tbody><tr><td translate>Hello World</td></tr></tbody></custom-table>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
});

View File

@@ -0,0 +1,27 @@
import { expect } from 'chai';
import { FunctionParser } from '../../src/parsers/function.parser';
describe('FunctionParser', () => {
const componentFilename: string = 'test.component.ts';
let parser: FunctionParser;
beforeEach(() => {
parser = new FunctionParser();
});
it('should extract strings using marker function', () => {
const contents = `
import { _ } from '@biesbjerg/ngx-translate-extract';
_('Hello world');
_(['I', 'am', 'extracted']);
otherFunction('But I am not');
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello world', 'I', 'am', 'extracted']);
});
});

View File

@@ -18,6 +18,12 @@ describe('PipeParser', () => {
expect(keys).to.deep.equal(['SomeKey_NotWorking']);
});
it('should extract string using pipe, but between quotes only', () => {
const contents = `<input class="form-control" type="text" placeholder="{{'user.settings.form.phone.placeholder' | translate}}" [formControl]="settingsForm.controls['phone']">`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['user.settings.form.phone.placeholder']);
});
it('should extract interpolated strings using translate pipe', () => {
const contents = `Hello {{ 'World' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
@@ -25,11 +31,17 @@ describe('PipeParser', () => {
});
it('should extract strings with escaped quotes', () => {
const contents = `Hello {{ 'World\'s largest potato' | translate }}`;
const contents = `Hello {{ 'World\\'s largest potato' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([`World's largest potato`]);
});
it('should extract strings with multiple escaped quotes', () => {
const contents = `{{ 'C\\'est ok. C\\'est ok' | translate }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([`C'est ok. C'est ok`]);
});
it('should extract interpolated strings using translate pipe in attributes', () => {
const contents = `<span attr="{{ 'Hello World' | translate }}"></span>`;
const keys = parser.extract(contents, templateFilename).keys();

View File

@@ -60,6 +60,19 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract strings in TranslateService\'s stream() method', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
this._translateService.stream('Hello World');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should extract array of strings in TranslateService\'s get() method', () => {
const contents = `
@Component({ })
@@ -86,7 +99,20 @@ describe('ServiceParser', () => {
expect(key).to.deep.equal(['Hello', 'World']);
});
it('should not extract strings in get()/instant() methods of other services', () => {
it('should extract array of strings in TranslateService\'s stream() method', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService: TranslateService) { }
public test() {
this._translateService.stream(['Hello', 'World']);
}
`;
const key = parser.extract(contents, componentFilename).keys();
expect(key).to.deep.equal(['Hello', 'World']);
});
it('should not extract strings in get()/instant()/stream() methods of other services', () => {
const contents = `
@Component({ })
export class AppComponent {
@@ -97,6 +123,7 @@ describe('ServiceParser', () => {
public test() {
this._otherService.get('Hello World');
this._otherService.instant('Hi there');
this._otherService.stream('Hi there');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
@@ -151,4 +178,51 @@ describe('ServiceParser', () => {
expect(keys).to.deep.equal(['You are expected at {{time}}']);
});
it('should not crash when constructor parameter has no type', () => {
const contents = `
@Component({ })
export class AppComponent {
public constructor(protected _translateService) { }
public test() {
this._translateService.instant('Hello World');
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal([]);
});
it('should extract strings from all classes in the file', () => {
const contents = `
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
export class Stuff {
thing: string;
translate: any;
constructor(thing: string) {
this.translate.get('Not me');
this.thing = thing;
}
}
@Injectable()
export class MyComponent {
constructor(public translate: TranslateService) {
this.translate.instant("Extract me!");
}
}
export class OtherClass {
constructor(thing: string, _translate: TranslateService) {
this._translate.get("Do not extract me");
}
}
@Injectable()
export class AuthService {
constructor(public translate: TranslateService) {
this.translate.instant("Hello!");
}
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Extract me!', 'Hello!']);
});
});