Compare commits

..

2 Commits

Author SHA1 Message Date
Kim Biesbjerg
0d74fb4f55 wip 2020-05-31 14:46:41 +02:00
Kim Biesbjerg
b928ca7555 wip 2020-05-31 14:31:55 +02:00
5 changed files with 1116 additions and 806 deletions

View File

@ -20,7 +20,7 @@ Add a script to your project's `package.json`:
} }
... ...
``` ```
You can now run `npm run i18n:extract` and it will extract strings from your project. You can now run `npm run extract-i18n` and it will extract strings from your project.
## Usage ## Usage
@ -101,7 +101,6 @@ 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

1658
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.4", "version": "7.0.0",
"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": "^11.2.9", "@angular/compiler": "^9.1.9",
"@types/braces": "^3.0.0", "@types/braces": "^3.0.0",
"@types/chai": "^4.2.16", "@types/chai": "^4.2.11",
"@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.3", "@types/glob": "^7.1.1",
"@types/mkdirp": "^1.0.1", "@types/mkdirp": "^1.0.0",
"@types/mocha": "^8.2.2", "@types/mocha": "^7.0.2",
"@types/node": "^14.14.37", "@types/node": "^12.12.42",
"@types/yargs": "^16.0.1", "@types/yargs": "^15.0.5",
"braces": "^3.0.2", "braces": "^3.0.2",
"chai": "^4.3.4", "chai": "^4.2.0",
"husky": "^6.0.0", "husky": "^4.2.5",
"lint-staged": "^10.5.4", "lint-staged": "^10.2.6",
"mocha": "^8.3.2", "mocha": "^7.2.0",
"prettier": "^2.2.1", "prettier": "^2.0.5",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"ts-node": "^9.1.1", "ts-node": "^8.10.1",
"tslint": "^6.1.3", "tslint": "^6.1.2",
"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.13.9", "tslint-etc": "^1.10.1",
"typescript": "^4.2.4" "typescript": "^3.9.3"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/compiler": ">=8.0.0", "@angular/compiler": "^8.0.0 || ^9.0.0",
"typescript": ">=3.0.0" "typescript": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"@phenomnomnominal/tsquery": "^4.1.1", "@phenomnomnominal/tsquery": "^4.1.0",
"boxen": "^5.0.1", "boxen": "^4.2.0",
"colorette": "^1.2.2", "colorette": "^1.2.0",
"flat": "^5.0.2", "flat": "^5.0.0",
"gettext-parser": "^4.0.4", "gettext-parser": "^4.0.3",
"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": "^16.2.0" "yargs": "^15.3.1"
} }
} }

View File

@ -1,16 +1,20 @@
import { import {
AST, AST,
TmplAstNode, ASTWithSource,
TmplAstNode as Node,
TmplAstBoundText as BoundText,
TmplAstElement as Element,
TmplAstTemplate as Template,
TmplAstBoundAttribute as BoundAttribute,
TmplAstBoundEvent as BoundEvent,
parseTemplate, parseTemplate,
BindingPipe, BindingPipe,
LiteralPrimitive, LiteralPrimitive,
Conditional, Conditional,
TmplAstTextAttribute,
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';
@ -18,146 +22,99 @@ import { TranslationCollection } from '../utils/translation.collection';
import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils'; import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils';
const TRANSLATE_PIPE_NAME = 'translate'; const TRANSLATE_PIPE_NAME = 'translate';
type ElementLike = Element | Template;
export class PipeParser implements ParserInterface { export class PipeParser implements ParserInterface {
public extract(source: string, filePath: string): TranslationCollection | null { public extract(source: string, filePath: string): TranslationCollection | null {
let collection: TranslationCollection = new TranslationCollection();
if (filePath && isPathAngularComponent(filePath)) { if (filePath && isPathAngularComponent(filePath)) {
source = extractComponentInlineTemplate(source); source = extractComponentInlineTemplate(source);
} }
const nodes: Node[] = this.parseTemplate(source, filePath);
const pipes = this.getBindingPipes(nodes, TRANSLATE_PIPE_NAME).filter((pipe) => !this.pipeHasConcatenatedString(pipe));
let collection: TranslationCollection = new TranslationCollection();
const nodes: TmplAstNode[] = this.parseTemplate(source, filePath);
const pipes: BindingPipe[] = nodes.map((node) => this.findPipesInNode(node)).flat();
pipes.forEach((pipe) => { pipes.forEach((pipe) => {
this.parseTranslationKeysFromPipe(pipe).forEach((key: string) => { this.visitEachChild(pipe, (child) => {
collection = collection.add(key); if (child instanceof LiteralPrimitive) {
collection = collection.add(child.value);
}
}); });
}); });
return collection; return collection;
} }
protected findPipesInNode(node: any): BindingPipe[] { protected getBindingPipes(nodes: any[], name: string): BindingPipe[] {
let ret: BindingPipe[] = []; let pipes: BindingPipe[] = [];
nodes.forEach((node) => {
if (node?.children) { if (this.isElementLike(node)) {
ret = node.children.reduce( pipes = [
(result: BindingPipe[], childNode: TmplAstNode) => { ...pipes,
const children = this.findPipesInNode(childNode); ...this.getBindingPipes([
return result.concat(children); ...node.inputs,
}, ...node.children
[ret] ], name)
); ];
} }
if (node?.value?.ast) { this.visitEachChild(node, (exp) => {
ret.push(...this.getTranslatablesFromAst(node.value.ast)); if (exp instanceof BindingPipe && exp.name === name) {
pipes = [...pipes, exp];
} }
if (node?.attributes) {
const translateableAttributes = node.attributes.filter((attr: TmplAstTextAttribute) => {
return attr.name === TRANSLATE_PIPE_NAME;
}); });
ret = [...ret, ...translateableAttributes]; });
return pipes;
} }
if (node?.inputs) { protected visitEachChild(exp: AST, visitor: (child: AST) => void): void {
node.inputs.forEach((input: any) => { visitor(exp);
// <element [attrib]="'identifier' | translate">
if (input?.value?.ast) { let children: AST[] = [];
ret.push(...this.getTranslatablesFromAst(input.value.ast)); if (exp instanceof BoundText) {
children = [exp.value];
} else if (exp instanceof BoundAttribute) {
children = [exp.value];
} else if (exp instanceof BoundEvent) {
children = [exp.handler];
} else if (exp instanceof Interpolation) {
children = exp.expressions;
} else if (exp instanceof LiteralArray) {
children = exp.expressions;
} else if (exp instanceof LiteralMap) {
children = exp.values;
} else if (exp instanceof BindingPipe) {
children = [exp.exp, ...exp.args];
} else if (exp instanceof Conditional) {
children = [exp.trueExp, exp.falseExp];
} else if (exp instanceof Binary) {
children = [exp.left, exp.right];
} else if (exp instanceof ASTWithSource) {
children = [exp.ast];
} }
children.forEach((child) => {
this.visitEachChild(child, visitor);
}); });
} }
return ret; /**
* Check if node type is ElementLike
* @param node
*/
protected isElementLike(node: Node): node is ElementLike {
return node instanceof Element || node instanceof Template;
} }
protected parseTranslationKeysFromPipe(pipeContent: BindingPipe | LiteralPrimitive | Conditional): string[] { /**
const ret: string[] = []; * Check if pipe concatenates string (in that case we don't want to extract it)
if (pipeContent instanceof LiteralPrimitive) { * @param pipe
ret.push(pipeContent.value); */
} else if (pipeContent instanceof Conditional) { protected pipeHasConcatenatedString(pipe: BindingPipe): boolean {
const trueExp: LiteralPrimitive | Conditional = pipeContent.trueExp as any; return pipe?.exp instanceof Binary && pipe.exp.operation === '+';
ret.push(...this.parseTranslationKeysFromPipe(trueExp));
const falseExp: LiteralPrimitive | Conditional = pipeContent.falseExp as any;
ret.push(...this.parseTranslationKeysFromPipe(falseExp));
} else if (pipeContent instanceof BindingPipe) {
ret.push(...this.parseTranslationKeysFromPipe(pipeContent.exp as any));
}
return ret;
} }
protected getTranslatablesFromAst(ast: AST): BindingPipe[] { protected parseTemplate(template: string, path: string): Node[] {
// the entire expression is the translate pipe, e.g.:
// - 'foo' | translate
// - (condition ? 'foo' : 'bar') | translate
if (this.expressionIsOrHasBindingPipe(ast)) {
return [ast];
}
// angular double curly bracket interpolation, e.g.:
// - {{ expressions }}
if (ast instanceof Interpolation) {
return this.getTranslatablesFromAsts(ast.expressions);
}
// ternary operator, e.g.:
// - condition ? null : ('foo' | translate)
// - condition ? ('foo' | translate) : null
if (ast instanceof Conditional) {
return this.getTranslatablesFromAsts([ast.trueExp, ast.falseExp]);
}
// string concatenation, e.g.:
// - 'foo' + 'bar' + ('baz' | translate)
if (ast instanceof Binary) {
return this.getTranslatablesFromAsts([ast.left, ast.right]);
}
// a pipe on the outer expression, but not the translate pipe - ignore the pipe, visit the expression, e.g.:
// - { foo: 'Hello' | translate } | json
if (ast instanceof BindingPipe) {
return this.getTranslatablesFromAst(ast.exp);
}
// object - ignore the keys, visit all values, e.g.:
// - { key1: 'value1' | translate, key2: 'value2' | translate }
if (ast instanceof LiteralMap) {
return this.getTranslatablesFromAsts(ast.values);
}
// array - visit all its values, e.g.:
// - [ 'value1' | translate, 'value2' | translate ]
if (ast instanceof LiteralArray) {
return this.getTranslatablesFromAsts(ast.expressions);
}
if (ast instanceof MethodCall) {
return this.getTranslatablesFromAsts(ast.args);
}
return [];
}
protected getTranslatablesFromAsts(asts: AST[]): BindingPipe[] {
return this.flatten(asts.map((ast) => this.getTranslatablesFromAst(ast)));
}
protected flatten<T extends AST>(array: T[][]): T[] {
return [].concat(...array);
}
protected expressionIsOrHasBindingPipe(exp: any): exp is BindingPipe {
if (exp.name && exp.name === TRANSLATE_PIPE_NAME) {
return true;
}
if (exp.exp && exp.exp instanceof BindingPipe) {
return this.expressionIsOrHasBindingPipe(exp.exp);
}
return false;
}
protected parseTemplate(template: string, path: string): TmplAstNode[] {
return parseTemplate(template, path).nodes; return parseTemplate(template, path).nodes;
} }
} }

View File

@ -195,10 +195,4 @@ 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`]);
});
}); });