Compare commits

...

10 Commits

Author SHA1 Message Date
Kim Biesbjerg
567a0586dc Update deps 2021-04-14 10:08:21 +02:00
Jabi
17dfbbed84 enable piped argument on function calls (#233) 2021-04-14 10:00:44 +02:00
Kim Biesbjerg
acdffe0121 Merge branch 'master' of https://github.com/biesbjerg/ngx-translate-extract into master 2020-09-29 11:56:31 +02:00
Kim Biesbjerg
116133ba32 Bump version 7.0.3 2020-09-29 11:56:03 +02:00
Kim Biesbjerg
50b2ca6f4a (chore) Update deps, allow newer version of typescript and angular compiler 2020-09-29 11:55:17 +02:00
Kim Biesbjerg
b46a914756 Update README.md 2020-08-05 12:59:00 +02:00
Kim Biesbjerg
bc3e5fbe2f fix typo 2020-06-25 10:52:24 +02:00
Kim Biesbjerg
ea990d6f9d update deps, bump version 2020-06-25 10:43:59 +02:00
Kim Biesbjerg
c60705d5fa run prettier on code 2020-05-28 19:36:21 +02:00
Kim Biesbjerg
85cd1e4a46 fix(directive-parser) add support for bound attributes 2020-05-28 19:35:26 +02:00
8 changed files with 813 additions and 1039 deletions

View File

@@ -101,7 +101,8 @@ Examples:
ngx-translate-extract -i './src/**/*.{ts,tsx,html}' -o strings.json Extract from ts, tsx and html ngx-translate-extract -i './src/**/*.{ts,tsx,html}' -o strings.json Extract from ts, tsx and html
ngx-translate-extract -i './src/**/!(*.spec).{ts,html}' -o Extract from ts, html, excluding files with ".spec" ngx-translate-extract -i './src/**/!(*.spec).{ts,html}' -o Extract from ts, html, excluding files with ".spec"
strings.json strings.json
```
## Note for GetText users ## Note for GetText users
Please pay attention of which version of `gettext-parser` you actually use in your project. For instance, `gettext-parser:1.2.2` does not support HTML tags in translation keys. Please pay attention of which version of `gettext-parser` you actually use in your project. For instance, `gettext-parser:1.2.2` does not support HTML tags in translation keys.

1624
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "@biesbjerg/ngx-translate-extract", "name": "@biesbjerg/ngx-translate-extract",
"version": "7.0.0", "version": "7.0.3",
"description": "Extract strings from projects using ngx-translate", "description": "Extract strings from projects using ngx-translate",
"main": "dist/index.js", "main": "dist/index.js",
"typings": "dist/index.d.ts", "typings": "dist/index.d.ts",
@@ -61,44 +61,44 @@
}, },
"config": {}, "config": {},
"devDependencies": { "devDependencies": {
"@angular/compiler": "^9.1.9", "@angular/compiler": "^11.2.9",
"@types/braces": "^3.0.0", "@types/braces": "^3.0.0",
"@types/chai": "^4.2.11", "@types/chai": "^4.2.16",
"@types/flat": "^5.0.1", "@types/flat": "^5.0.1",
"@types/gettext-parser": "4.0.0", "@types/gettext-parser": "4.0.0",
"@types/glob": "^7.1.1", "@types/glob": "^7.1.3",
"@types/mkdirp": "^1.0.0", "@types/mkdirp": "^1.0.1",
"@types/mocha": "^7.0.2", "@types/mocha": "^8.2.2",
"@types/node": "^12.12.42", "@types/node": "^14.14.37",
"@types/yargs": "^15.0.5", "@types/yargs": "^16.0.1",
"braces": "^3.0.2", "braces": "^3.0.2",
"chai": "^4.2.0", "chai": "^4.3.4",
"husky": "^4.2.5", "husky": "^6.0.0",
"lint-staged": "^10.2.6", "lint-staged": "^10.5.4",
"mocha": "^7.2.0", "mocha": "^8.3.2",
"prettier": "^2.0.5", "prettier": "^2.2.1",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ts-node": "^8.10.1", "ts-node": "^9.1.1",
"tslint": "^6.1.2", "tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0", "tslint-config-prettier": "^1.18.0",
"tslint-eslint-rules": "^5.4.0", "tslint-eslint-rules": "^5.4.0",
"tslint-etc": "^1.10.1", "tslint-etc": "^1.13.9",
"typescript": "^3.9.3" "typescript": "^4.2.4"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/compiler": "^8.0.0 || ^9.0.0", "@angular/compiler": ">=8.0.0",
"typescript": "^3.0.0" "typescript": ">=3.0.0"
}, },
"dependencies": { "dependencies": {
"@phenomnomnominal/tsquery": "^4.1.0", "@phenomnomnominal/tsquery": "^4.1.1",
"boxen": "^4.2.0", "boxen": "^5.0.1",
"colorette": "^1.2.0", "colorette": "^1.2.2",
"flat": "^5.0.0", "flat": "^5.0.2",
"gettext-parser": "^4.0.3", "gettext-parser": "^4.0.4",
"glob": "^7.1.6", "glob": "^7.1.6",
"mkdirp": "^1.0.4", "mkdirp": "^1.0.4",
"path": "^0.12.7", "path": "^0.12.7",
"terminal-link": "^2.1.1", "terminal-link": "^2.1.1",
"yargs": "^15.3.1" "yargs": "^16.2.0"
} }
} }

View File

@@ -74,7 +74,7 @@ export class ExtractTask implements TaskInterface {
try { try {
let event = 'CREATED'; let event = 'CREATED';
if (fs.existsSync(outputPath)) { if (fs.existsSync(outputPath)) {
this.options.replace ? event = 'REPLACED' : event = 'MERGED'; this.options.replace ? (event = 'REPLACED') : (event = 'MERGED');
} }
this.save(outputPath, final); this.save(outputPath, final);
this.out(`%s %s`, dim(`- ${outputPath}`), green(`[${event}]`)); this.out(`%s %s`, dim(`- ${outputPath}`), green(`[${event}]`));

View File

@@ -3,7 +3,18 @@ import {
TmplAstNode as Node, TmplAstNode as Node,
TmplAstElement as Element, TmplAstElement as Element,
TmplAstText as Text, TmplAstText as Text,
TmplAstTemplate as Template TmplAstTemplate as Template,
TmplAstTextAttribute as TextAttribute,
TmplAstBoundAttribute as BoundAttribute,
AST,
ASTWithSource,
LiteralPrimitive,
Conditional,
Binary,
BindingPipe,
Interpolation,
LiteralArray,
LiteralMap
} from '@angular/compiler'; } from '@angular/compiler';
import { ParserInterface } from './parser.interface'; import { ParserInterface } from './parser.interface';
@@ -24,13 +35,22 @@ export class DirectiveParser implements ParserInterface {
const elements: ElementLike[] = this.getElementsWithTranslateAttribute(nodes); const elements: ElementLike[] = this.getElementsWithTranslateAttribute(nodes);
elements.forEach((element) => { elements.forEach((element) => {
const attr = this.getAttribute(element, TRANSLATE_ATTR_NAME); const attribute = this.getAttribute(element, TRANSLATE_ATTR_NAME);
if (attr) { if (attribute?.value) {
collection = collection.add(attr); collection = collection.add(attribute.value);
return; return;
} }
const boundAttribute = this.getBoundAttribute(element, TRANSLATE_ATTR_NAME);
if (boundAttribute?.value) {
this.getLiteralPrimitives(boundAttribute.value).forEach((literalPrimitive) => {
collection = collection.add(literalPrimitive.value);
});
return;
}
const textNodes = this.getTextNodes(element); const textNodes = this.getTextNodes(element);
textNodes.forEach((textNode: Text) => { textNodes.forEach((textNode) => {
collection = collection.add(textNode.value.trim()); collection = collection.add(textNode.value.trim());
}); });
}); });
@@ -43,16 +63,18 @@ export class DirectiveParser implements ParserInterface {
*/ */
protected getElementsWithTranslateAttribute(nodes: Node[]): ElementLike[] { protected getElementsWithTranslateAttribute(nodes: Node[]): ElementLike[] {
let elements: ElementLike[] = []; let elements: ElementLike[] = [];
nodes.filter(this.isElementLike) nodes.filter(this.isElementLike).forEach((element) => {
.forEach((element) => { if (this.hasAttribute(element, TRANSLATE_ATTR_NAME)) {
if (this.hasAttribute(element, TRANSLATE_ATTR_NAME)) { elements = [...elements, element];
elements = [...elements, element]; }
} if (this.hasBoundAttribute(element, TRANSLATE_ATTR_NAME)) {
const childElements = this.getElementsWithTranslateAttribute(element.children); elements = [...elements, element];
if (childElements.length) { }
elements = [...elements, ...childElements]; const childElements = this.getElementsWithTranslateAttribute(element.children);
} if (childElements.length) {
}); elements = [...elements, ...childElements];
}
});
return elements; return elements;
} }
@@ -76,8 +98,59 @@ export class DirectiveParser implements ParserInterface {
* Get attribute value if present on element * Get attribute value if present on element
* @param element * @param element
*/ */
protected getAttribute(element: ElementLike, name: string): string | undefined { protected getAttribute(element: ElementLike, name: string): TextAttribute {
return element.attributes.find((attribute) => attribute.name === name)?.value; return element.attributes.find((attribute) => attribute.name === name);
}
/**
* Check if bound attribute is present on element
* @param element
* @param name
*/
protected hasBoundAttribute(element: ElementLike, name: string): boolean {
return this.getBoundAttribute(element, name) !== undefined;
}
/**
* Get bound attribute if present on element
* @param element
* @param name
*/
protected getBoundAttribute(element: ElementLike, name: string): BoundAttribute {
return element.inputs.find((input) => input.name === name);
}
/**
* Get literal primitives from expression
* @param exp
*/
protected getLiteralPrimitives(exp: AST): LiteralPrimitive[] {
if (exp instanceof LiteralPrimitive) {
return [exp];
}
let visit: AST[] = [];
if (exp instanceof Interpolation) {
visit = exp.expressions;
} else if (exp instanceof LiteralArray) {
visit = exp.expressions;
} else if (exp instanceof LiteralMap) {
visit = exp.values;
} else if (exp instanceof BindingPipe) {
visit = [exp.exp];
} else if (exp instanceof Conditional) {
visit = [exp.trueExp, exp.falseExp];
} else if (exp instanceof Binary) {
visit = [exp.left, exp.right];
} else if (exp instanceof ASTWithSource) {
visit = [exp.ast];
}
let results: LiteralPrimitive[] = [];
visit.forEach((child) => {
results = [...results, ...this.getLiteralPrimitives(child)];
});
return results;
} }
/** /**
@@ -104,5 +177,4 @@ export class DirectiveParser implements ParserInterface {
protected parseTemplate(template: string, path: string): Node[] { protected parseTemplate(template: string, path: string): Node[] {
return parseTemplate(template, path).nodes; return parseTemplate(template, path).nodes;
} }
} }

View File

@@ -9,7 +9,8 @@ import {
Binary, Binary,
LiteralMap, LiteralMap,
LiteralArray, LiteralArray,
Interpolation Interpolation,
MethodCall
} from '@angular/compiler'; } from '@angular/compiler';
import { ParserInterface } from './parser.interface'; import { ParserInterface } from './parser.interface';
@@ -131,11 +132,15 @@ export class PipeParser implements ParserInterface {
return this.getTranslatablesFromAsts(ast.expressions); return this.getTranslatablesFromAsts(ast.expressions);
} }
if (ast instanceof MethodCall) {
return this.getTranslatablesFromAsts(ast.args);
}
return []; return [];
} }
protected getTranslatablesFromAsts(asts: AST[]): BindingPipe[] { protected getTranslatablesFromAsts(asts: AST[]): BindingPipe[] {
return this.flatten(asts.map(ast => this.getTranslatablesFromAst(ast))); return this.flatten(asts.map((ast) => this.getTranslatablesFromAst(ast)));
} }
protected flatten<T extends AST>(array: T[][]): T[] { protected flatten<T extends AST>(array: T[][]): T[] {

View File

@@ -12,6 +12,55 @@ describe('DirectiveParser', () => {
parser = new DirectiveParser(); parser = new DirectiveParser();
}); });
it('should extract keys when using literal map in bound attribute', () => {
const contents = `<div [translate]="{ key1: 'value1' | translate, key2: 'value2' | translate }"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['value1', 'value2']);
});
it('should extract keys when using literal arrays in bound attribute', () => {
const contents = `<div [translate]="[ 'value1' | translate, 'value2' | translate ]"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['value1', 'value2']);
});
it('should extract keys when using binding pipe in bound attribute', () => {
const contents = `<div [translate]="'KEY1' | withPipe"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY1']);
});
it('should extract keys when using binary expression in bound attribute', () => {
const contents = `<div [translate]="keyVar || 'KEY1'"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY1']);
});
it('should extract keys when using literal primitive in bound attribute', () => {
const contents = `<div [translate]="'KEY1'"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY1']);
});
it('should extract keys when using conditional in bound attribute', () => {
const contents = `<div [translate]="condition ? 'KEY1' : 'KEY2'"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY1', 'KEY2']);
});
it('should extract keys when using nested conditionals in bound attribute', () => {
const contents = `<div [translate]="isSunny ? (isWarm ? 'Sunny and warm' : 'Sunny but cold') : 'Not sunny'"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Sunny and warm', 'Sunny but cold', 'Not sunny']);
});
it('should extract keys when using interpolation', () => {
const contents = `<div translate="{{ 'KEY1' + key2 + 'KEY3' }}"></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY1', 'KEY3']);
});
it('should extract keys keeping proper whitespace', () => { it('should extract keys keeping proper whitespace', () => {
const contents = ` const contents = `
<div translate> <div translate>
@@ -120,4 +169,5 @@ describe('DirectiveParser', () => {
const keys = parser.extract(contents, templateFilename).keys(); const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['this is an example']); expect(keys).to.deep.equal(['this is an example']);
}); });
}); });

View File

@@ -195,4 +195,10 @@ describe('PipeParser', () => {
const keys = parser.extract(contents, templateFilename).keys(); const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([]); expect(keys).to.deep.equal([]);
}); });
it('should extract strings from piped arguments inside a function calls on templates', () => {
const contents = `{{ callMe('Hello' | translate, 'World' | translate ) }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([`Hello`, `World`]);
});
}); });