Compare commits

..

14 Commits

Author SHA1 Message Date
dependabot[bot]
a108eb776c
Bump ansi-regex from 3.0.0 to 3.0.1 (#3)
Bumps [ansi-regex](https://github.com/chalk/ansi-regex) from 3.0.0 to 3.0.1.
- [Release notes](https://github.com/chalk/ansi-regex/releases)
- [Commits](https://github.com/chalk/ansi-regex/compare/v3.0.0...v3.0.1)

---
updated-dependencies:
- dependency-name: ansi-regex
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-22 09:13:58 +03:00
dependabot[bot]
a60ea65325
Bump minimist from 1.2.5 to 1.2.6 (#2)
Bumps [minimist](https://github.com/substack/minimist) from 1.2.5 to 1.2.6.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.5...1.2.6)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-04-21 23:49:50 +03:00
dd31950151
Merge pull request #1 from unistack-org/dependabot/npm_and_yarn/path-parse-1.0.7
Bump path-parse from 1.0.6 to 1.0.7
2022-01-12 17:36:04 +03:00
dependabot[bot]
f0cc5a2d9f
Bump path-parse from 1.0.6 to 1.0.7
Bumps [path-parse](https://github.com/jbgutierrez/path-parse) from 1.0.6 to 1.0.7.
- [Release notes](https://github.com/jbgutierrez/path-parse/releases)
- [Commits](https://github.com/jbgutierrez/path-parse/commits/v1.0.7)

---
updated-dependencies:
- dependency-name: path-parse
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-13 01:30:55 +00:00
Kim Biesbjerg
82eb652e4b Bump version 2021-04-14 10:10:21 +02:00
Kim Biesbjerg
4e91eb5fc5
Update deps (#234) 2021-04-14 10:09:26 +02:00
riot
0809e065ec
fixes example call to generate i18n extracts (#225) 2021-04-14 10:03:06 +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
5 changed files with 806 additions and 1116 deletions

View File

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

@ -1,20 +1,16 @@
import { import {
AST, AST,
ASTWithSource, TmplAstNode,
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';
@ -22,99 +18,146 @@ 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.visitEachChild(pipe, (child) => { this.parseTranslationKeysFromPipe(pipe).forEach((key: string) => {
if (child instanceof LiteralPrimitive) { collection = collection.add(key);
collection = collection.add(child.value);
}
}); });
}); });
return collection; return collection;
} }
protected getBindingPipes(nodes: any[], name: string): BindingPipe[] { protected findPipesInNode(node: any): BindingPipe[] {
let pipes: BindingPipe[] = []; let ret: BindingPipe[] = [];
nodes.forEach((node) => {
if (this.isElementLike(node)) {
pipes = [
...pipes,
...this.getBindingPipes([
...node.inputs,
...node.children
], name)
];
}
this.visitEachChild(node, (exp) => { if (node?.children) {
if (exp instanceof BindingPipe && exp.name === name) { ret = node.children.reduce(
pipes = [...pipes, exp]; (result: BindingPipe[], childNode: TmplAstNode) => {
} const children = this.findPipesInNode(childNode);
}); return result.concat(children);
}); },
return pipes; [ret]
} );
protected visitEachChild(exp: AST, visitor: (child: AST) => void): void {
visitor(exp);
let children: 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) => { if (node?.value?.ast) {
this.visitEachChild(child, visitor); ret.push(...this.getTranslatablesFromAst(node.value.ast));
}); }
if (node?.attributes) {
const translateableAttributes = node.attributes.filter((attr: TmplAstTextAttribute) => {
return attr.name === TRANSLATE_PIPE_NAME;
});
ret = [...ret, ...translateableAttributes];
}
if (node?.inputs) {
node.inputs.forEach((input: any) => {
// <element [attrib]="'identifier' | translate">
if (input?.value?.ast) {
ret.push(...this.getTranslatablesFromAst(input.value.ast));
}
});
}
return ret;
} }
/** protected parseTranslationKeysFromPipe(pipeContent: BindingPipe | LiteralPrimitive | Conditional): string[] {
* Check if node type is ElementLike const ret: string[] = [];
* @param node if (pipeContent instanceof LiteralPrimitive) {
*/ ret.push(pipeContent.value);
protected isElementLike(node: Node): node is ElementLike { } else if (pipeContent instanceof Conditional) {
return node instanceof Element || node instanceof Template; const trueExp: LiteralPrimitive | Conditional = pipeContent.trueExp as any;
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[] {
* Check if pipe concatenates string (in that case we don't want to extract it) // the entire expression is the translate pipe, e.g.:
* @param pipe // - 'foo' | translate
*/ // - (condition ? 'foo' : 'bar') | translate
protected pipeHasConcatenatedString(pipe: BindingPipe): boolean { if (this.expressionIsOrHasBindingPipe(ast)) {
return pipe?.exp instanceof Binary && pipe.exp.operation === '+'; 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 parseTemplate(template: string, path: string): Node[] { 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,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`]);
});
}); });