Compare commits

..

30 Commits

Author SHA1 Message Date
dependabot[bot]
0a7c2dd5f0 Bump minimatch and mocha
Bumps [minimatch](https://github.com/isaacs/minimatch) to 3.1.2 and updates ancestor dependency [mocha](https://github.com/mochajs/mocha). These dependencies need to be updated together.


Updates `minimatch` from 3.0.4 to 3.1.2
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

Updates `mocha` from 8.3.2 to 10.1.0
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v8.3.2...v10.1.0)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
- dependency-name: mocha
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-11-11 12:29:34 +00:00
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
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
Kim Biesbjerg
8afbb2f3a9 bump version 2020-05-28 00:29:39 +02:00
Kim Biesbjerg
329c24d962 update deps 2020-05-28 00:29:02 +02:00
Kim Biesbjerg
a30a6f9215 clean up some tests 2020-05-28 00:28:13 +02:00
Kim Biesbjerg
2adec54c00 fix(directive-parser) refactor + correct handling of whitespace 2020-05-28 00:09:53 +02:00
Alexander von Weiss
619b3c56ea fix(pipe-parser): add support for more sophisticated expressions that worked in version 4.2.0 with the regex based parser (#185)
* fix(pipe-parser): add support for more sophisticated expressions
2020-05-20 15:18:31 +02:00
Kim Biesbjerg
5e0da552b0 Add username to GitHub sponsors 2020-04-16 15:00:34 +02:00
Kim Biesbjerg
5f2eb2a7a0 Update package.json example scripts 2020-04-16 14:57:51 +02:00
Kim Biesbjerg
90b59793a7 Bump version to 6.0.4 2020-04-16 09:07:02 +02:00
Kim Biesbjerg
deb6b2373b Remove noUnusedLocals since they are removed by prettier on commit anyway 2020-04-16 09:06:40 +02:00
Esteban Gehring
17dec7deb8 fix(npm-package): move typescript and angular to peer-dependencies (#183)
fix(npm-package): move typescript and angular to peer-dependencies. Fixes #173
2020-04-16 08:58:51 +02:00
Lorent Lempereur
71f4f42b33 Tests about support of HTML tags in translations keys with GetText (#172)
- Verify that html tags are supported in translation keys
- Add typed support of gettext-parser
2020-04-01 12:19:30 +02:00
Kim Biesbjerg
73f39d625c add single quote rule 2020-04-01 10:21:11 +02:00
Kim Biesbjerg
3bf2aaca4e Trim leading/trailing whitespace. Closes #175 2020-03-31 12:24:57 +02:00
15 changed files with 1288 additions and 1487 deletions

2
.github/FUNDING.yml vendored
View File

@@ -1,2 +1,2 @@
github: #biesbjerg
github: biesbjerg
custom: https://donate.biesbjerg.com

View File

@@ -15,11 +15,12 @@ Add a script to your project's `package.json`:
```json
...
"scripts": {
"extract-i18n": "ngx-translate-extract --input ./src --output ./src/assets/i18n/strings.json --key-as-default-value --clean --sort --format namespaced-json"
"i18n:init": "ngx-translate-extract --input ./src --output ./src/assets/i18n/template.json --key-as-default-value --replace --format json",
"i18n:extract": "ngx-translate-extract --input ./src --output ./src/assets/i18n/{en,da,de,fi,nb,nl,sv}.json --clean --format 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
@@ -99,4 +100,9 @@ Examples:
using brace expansion
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"
strings.json
strings.json
```
## 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.

2156
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",
"version": "6.0.3",
"version": "7.0.4",
"description": "Extract strings from projects using ngx-translate",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
@@ -61,39 +61,44 @@
},
"config": {},
"devDependencies": {
"@angular/compiler": "^11.2.9",
"@types/braces": "^3.0.0",
"@types/chai": "^4.2.11",
"@types/flat": "^5.0.0",
"@types/glob": "^7.1.1",
"@types/mkdirp": "^1.0.0",
"@types/mocha": "^7.0.2",
"@types/node": "^12.12.31",
"@types/yargs": "^15.0.4",
"@types/chai": "^4.2.16",
"@types/flat": "^5.0.1",
"@types/gettext-parser": "4.0.0",
"@types/glob": "^7.1.3",
"@types/mkdirp": "^1.0.1",
"@types/mocha": "^8.2.2",
"@types/node": "^14.14.37",
"@types/yargs": "^16.0.1",
"braces": "^3.0.2",
"chai": "^4.2.0",
"husky": "^4.2.3",
"lint-staged": "^10.0.9",
"mocha": "^7.1.1",
"prettier": "^2.0.2",
"chai": "^4.3.4",
"husky": "^6.0.0",
"lint-staged": "^10.5.4",
"mocha": "^10.1.0",
"prettier": "^2.2.1",
"rimraf": "^3.0.2",
"ts-node": "^8.8.1",
"tslint": "^6.1.0",
"ts-node": "^9.1.1",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"tslint-eslint-rules": "^5.4.0",
"tslint-etc": "^1.10.1"
"tslint-etc": "^1.13.9",
"typescript": "^4.2.4"
},
"peerDependencies": {
"@angular/compiler": ">=8.0.0",
"typescript": ">=3.0.0"
},
"dependencies": {
"@angular/compiler": "^9.0.7",
"@phenomnomnominal/tsquery": "^4.0.0",
"boxen": "^4.2.0",
"colorette": "^1.1.0",
"flat": "^5.0.0",
"gettext-parser": "^4.0.3",
"@phenomnomnominal/tsquery": "^4.1.1",
"boxen": "^5.0.1",
"colorette": "^1.2.2",
"flat": "^5.0.2",
"gettext-parser": "^4.0.4",
"glob": "^7.1.6",
"mkdirp": "^1.0.3",
"mkdirp": "^1.0.4",
"path": "^0.12.7",
"terminal-link": "^2.1.1",
"typescript": "^3.8.3",
"yargs": "^15.3.1"
"yargs": "^16.2.0"
}
}
}

View File

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

View File

@@ -1,7 +1,7 @@
import { CompilerInterface } from './compiler.interface';
import { TranslationCollection, TranslationType } from '../utils/translation.collection';
import * as gettext from 'gettext-parser';
import { po } from 'gettext-parser';
export class PoCompiler implements CompilerInterface {
public extension: string = 'po';
@@ -34,23 +34,24 @@ export class PoCompiler implements CompilerInterface {
}
};
return gettext.po.compile(data);
return po.compile(data).toString('utf8');
}
public parse(contents: string): TranslationCollection {
const collection = new TranslationCollection();
const po = gettext.po.parse(contents, 'utf8');
if (!po.translations.hasOwnProperty(this.domain)) {
const parsedPo = po.parse(contents, 'utf8');
if (!parsedPo.translations.hasOwnProperty(this.domain)) {
return collection;
}
const values = Object.keys(po.translations[this.domain])
const values = Object.keys(parsedPo.translations[this.domain])
.filter((key) => key.length > 0)
.reduce((result, key) => {
return {
...result,
[key]: po.translations[this.domain][key].msgstr.pop()
[key]: parsedPo.translations[this.domain][key].msgstr.pop()
};
}, {} as TranslationType);

View File

@@ -1 +0,0 @@
declare module 'gettext-parser';

View File

@@ -1,87 +1,180 @@
import {
parseTemplate,
TmplAstNode as Node,
TmplAstElement as Element,
TmplAstText as Text,
TmplAstTemplate as Template,
TmplAstTextAttribute as TextAttribute,
TmplAstBoundAttribute as BoundAttribute,
AST,
ASTWithSource,
LiteralPrimitive,
Conditional,
Binary,
BindingPipe,
Interpolation,
LiteralArray,
LiteralMap
} from '@angular/compiler';
import { ParserInterface } from './parser.interface';
import { TranslationCollection } from '../utils/translation.collection';
import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils';
import { parseTemplate, TmplAstNode, TmplAstElement, TmplAstTextAttribute } from '@angular/compiler';
const TRANSLATE_ATTR_NAME = 'translate';
type ElementLike = Element | Template;
export class DirectiveParser implements ParserInterface {
public extract(source: string, filePath: string): TranslationCollection | null {
let collection: TranslationCollection = new TranslationCollection();
if (filePath && isPathAngularComponent(filePath)) {
source = extractComponentInlineTemplate(source);
}
const nodes: Node[] = this.parseTemplate(source, filePath);
const elements: ElementLike[] = this.getElementsWithTranslateAttribute(nodes);
let collection: TranslationCollection = new TranslationCollection();
elements.forEach((element) => {
const attribute = this.getAttribute(element, TRANSLATE_ATTR_NAME);
if (attribute?.value) {
collection = collection.add(attribute.value);
return;
}
const nodes: TmplAstNode[] = this.parseTemplate(source, filePath);
this.getTranslatableElements(nodes).forEach((element) => {
const key = this.getElementTranslateAttrValue(element) || this.getElementContent(element);
collection = collection.add(key);
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);
textNodes.forEach((textNode) => {
collection = collection.add(textNode.value.trim());
});
});
return collection;
}
protected getTranslatableElements(nodes: TmplAstNode[]): TmplAstElement[] {
return nodes
.filter((element) => this.isElement(element))
.reduce((result: TmplAstElement[], element: TmplAstElement) => {
return result.concat(this.findChildrenElements(element));
}, [])
.filter((element) => this.isTranslatable(element));
/**
* Find all ElementLike nodes with a translate attribute
* @param nodes
*/
protected getElementsWithTranslateAttribute(nodes: Node[]): ElementLike[] {
let elements: ElementLike[] = [];
nodes.filter(this.isElementLike).forEach((element) => {
if (this.hasAttribute(element, TRANSLATE_ATTR_NAME)) {
elements = [...elements, element];
}
if (this.hasBoundAttribute(element, TRANSLATE_ATTR_NAME)) {
elements = [...elements, element];
}
const childElements = this.getElementsWithTranslateAttribute(element.children);
if (childElements.length) {
elements = [...elements, ...childElements];
}
});
return elements;
}
protected findChildrenElements(node: TmplAstNode): TmplAstElement[] {
if (!this.isElement(node)) {
return [];
}
// If element has translate attribute all its content is translatable
// so we don't need to traverse any deeper
if (this.isTranslatable(node)) {
return [node];
}
return node.children.reduce(
(result: TmplAstElement[], childNode: TmplAstNode) => {
if (this.isElement(childNode)) {
const children = this.findChildrenElements(childNode);
return result.concat(children);
}
return result;
},
[node]
);
/**
* Get direct child nodes of type Text
* @param element
*/
protected getTextNodes(element: ElementLike): Text[] {
return element.children.filter(this.isText);
}
protected parseTemplate(template: string, path: string): TmplAstNode[] {
/**
* Check if attribute is present on element
* @param element
*/
protected hasAttribute(element: ElementLike, name: string): boolean {
return this.getAttribute(element, name) !== undefined;
}
/**
* Get attribute value if present on element
* @param element
*/
protected getAttribute(element: ElementLike, name: string): TextAttribute {
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;
}
/**
* Check if node type is ElementLike
* @param node
*/
protected isElementLike(node: Node): node is ElementLike {
return node instanceof Element || node instanceof Template;
}
/**
* Check if node type is Text
* @param node
*/
protected isText(node: Node): node is Text {
return node instanceof Text;
}
/**
* Parse a template into nodes
* @param template
* @param path
*/
protected parseTemplate(template: string, path: string): Node[] {
return parseTemplate(template, path).nodes;
}
protected isElement(node: any): node is TmplAstElement {
return node?.attributes && node?.children;
}
protected isTranslatable(node: TmplAstNode): boolean {
if (this.isElement(node) && node.attributes.some((attribute) => attribute.name === 'translate')) {
return true;
}
return false;
}
protected getElementTranslateAttrValue(element: TmplAstElement): string {
const attr: TmplAstTextAttribute = element.attributes.find((attribute) => attribute.name === 'translate');
return attr?.value ?? '';
}
protected getElementContent(element: TmplAstElement): string {
const content = element.sourceSpan.start.file.content;
const start = element.startSourceSpan.end.offset;
const end = element.endSourceSpan.start.offset;
const val = content.substring(start, end);
return this.cleanKey(val);
}
protected cleanKey(val: string): string {
return val.replace(/\r?\n|\r|\t/g, '');
}
}

View File

@@ -1,4 +1,17 @@
import { TmplAstNode, parseTemplate, BindingPipe, LiteralPrimitive, Conditional, TmplAstTextAttribute } from '@angular/compiler';
import {
AST,
TmplAstNode,
parseTemplate,
BindingPipe,
LiteralPrimitive,
Conditional,
TmplAstTextAttribute,
Binary,
LiteralMap,
LiteralArray,
Interpolation,
MethodCall
} from '@angular/compiler';
import { ParserInterface } from './parser.interface';
import { TranslationCollection } from '../utils/translation.collection';
@@ -36,9 +49,8 @@ export class PipeParser implements ParserInterface {
);
}
if (node?.value?.ast?.expressions) {
const translateables = node.value.ast.expressions.filter((exp: any) => this.expressionIsOrHasBindingPipe(exp));
ret.push(...translateables);
if (node?.value?.ast) {
ret.push(...this.getTranslatablesFromAst(node.value.ast));
}
if (node?.attributes) {
@@ -51,17 +63,8 @@ export class PipeParser implements ParserInterface {
if (node?.inputs) {
node.inputs.forEach((input: any) => {
// <element [attrib]="'identifier' | translate">
if (input?.value?.ast && this.expressionIsOrHasBindingPipe(input.value.ast)) {
ret.push(input.value.ast);
}
// <element attrib="{{'identifier' | translate}}>"
if (input?.value?.ast?.expressions) {
input.value.ast.expressions.forEach((exp: BindingPipe) => {
if (this.expressionIsOrHasBindingPipe(exp)) {
ret.push(exp);
}
});
if (input?.value?.ast) {
ret.push(...this.getTranslatablesFromAst(input.value.ast));
}
});
}
@@ -84,7 +87,67 @@ export class PipeParser implements ParserInterface {
return ret;
}
protected expressionIsOrHasBindingPipe(exp: any): boolean {
protected getTranslatablesFromAst(ast: AST): BindingPipe[] {
// 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;
}

View File

@@ -0,0 +1,24 @@
import { expect } from 'chai';
import { TranslationCollection } from '../../src/utils/translation.collection';
import { PoCompiler } from '../../src/compilers/po.compiler';
describe('PoCompiler', () => {
let compiler: PoCompiler;
beforeEach(() => {
compiler = new PoCompiler();
});
it('should still include html ', () => {
const collection = new TranslationCollection({
'A <strong>test</strong>': 'Un <strong>test</strong>',
'With a lot of <em>html</em> included': 'Avec beaucoup d\'<em>html</em> inclus'
});
const result: Buffer = Buffer.from(compiler.compile(collection));
expect(result.toString('utf8')).to.equal('msgid ""\nmsgstr ""\n"mime-version: 1.0\\n"\n"Content-Type: text/plain; charset=utf-8\\n"\n"Content-Transfer-Encoding: 8bit\\n"\n\nmsgid "A <strong>test</strong>"\nmsgstr "Un <strong>test</strong>"\n\nmsgid "With a lot of <em>html</em> included"\nmsgstr "Avec beaucoup d\'<em>html</em> inclus"');
});
});

View File

@@ -12,28 +12,89 @@ describe('DirectiveParser', () => {
parser = new DirectiveParser();
});
it('should not choke when no html is present in template', () => {
const contents = 'Hello World';
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([]);
expect(keys).to.deep.equal(['value1', 'value2']);
});
it('should use contents as key when there is no translate attribute value provided', () => {
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', () => {
const contents = `
<div translate>
Wubba
Lubba
Dub Dub
</div>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Wubba Lubba Dub Dub']);
});
it('should use element contents as key when no translate attribute value is present', () => {
const contents = '<div translate>Hello World</div>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});
it('should use translate attribute value as key when provided', () => {
it('should use translate attribute value as key when present', () => {
const contents = '<div translate="MY_KEY">Hello World<div>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['MY_KEY']);
});
it('should not process children when translate attribute is present', () => {
it('should extract keys from child elements when translate attribute is present', () => {
const contents = `<div translate>Hello <strong translate>World</strong></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello <strong translate>World</strong>']);
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should not extract keys from child elements when translate attribute is not present', () => {
const contents = `<div translate>Hello <strong>World</strong></div>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello']);
});
it('should extract and parse inline template', () => {
@@ -66,43 +127,16 @@ describe('DirectiveParser', () => {
expect(collection.values).to.deep.equal({});
});
it('should extract contents from within custom tags', () => {
it('should extract contents from custom elements', () => {
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']);
});
it('should not cause error when no html is present in template', () => {
it('should extract from template without leading/trailing whitespace', () => {
const contents = `
import { Component } from '@angular/core';
@Component({
template: '{{ variable }}'
})
export class MyComponent {
variable: string
}
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal([]);
});
it('should extract contents without line breaks', () => {
const contents = `
<p translate>
Please leave a message for your client letting them know why you
rejected the field and what they need to do to fix it.
</p>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal([
'Please leave a message for your client letting them know why you rejected the field and what they need to do to fix it.'
]);
});
it('should extract contents without indent spaces', () => {
const contents = `
<div *ngIf="!isLoading && studentsToGrid && studentsToGrid.length == 0" class="no-students" mt-rtl translate>There
are currently no students in this class. The good news is, adding students is really easy! Just use the options
<div *ngIf="!isLoading && studentsToGrid && studentsToGrid.length == 0" class="no-students" mt-rtl translate>There
are currently no students in this class. The good news is, adding students is really easy! Just use the options
at the top.
</div>
`;
@@ -112,9 +146,28 @@ describe('DirectiveParser', () => {
]);
});
it('should extract contents without indent spaces', () => {
const contents = `<button mat-button (click)="search()" translate>client.search.searchBtn</button>`;
it('should extract keys from element without leading/trailing whitespace', () => {
const contents = `
<div translate>
this is an example
of a long label
</div>
<div>
<p translate>
this is an example
of another a long label
</p>
</div>
`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['client.search.searchBtn']);
expect(keys).to.deep.equal(['this is an example of a long label', 'this is an example of another a long label']);
});
it('should collapse excessive whitespace', () => {
const contents = '<p translate>this is an example</p>';
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['this is an example']);
});
});

View File

@@ -36,6 +36,17 @@ describe('MarkerParser', () => {
expect(keys).to.deep.equal(['Hello world', 'This is a very very very very long line.', 'Mix of different types']);
});
it('should extract split strings while keeping html tags', () => {
const contents = `
import { marker as _ } from '@biesbjerg/ngx-translate-extract-marker';
_('Hello ' + 'world');
_('This <em>is</em> a ' + 'very ' + 'very ' + 'very ' + 'very ' + 'long line.');
_('Mix ' + \`of \` + 'different ' + \`types\`);
`;
const keys = parser.extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello world', 'This <em>is</em> a very very very very long line.', 'Mix of different types']);
});
it('should extract the strings', () => {
const contents = `
import { marker } from '@biesbjerg/ngx-translate-extract-marker';

View File

@@ -47,12 +47,48 @@ describe('PipeParser', () => {
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should extract strings from ternary operators right expression', () => {
const contents = `{{ condition ? null : ('World' | translate) }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});
it('should extract strings from ternary operators inside attribute bindings', () => {
const contents = `<span [attr]="condition ? null : ('World' | translate)"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});
it('should extract strings from ternary operators left expression', () => {
const contents = `{{ condition ? ('World' | translate) : null }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['World']);
});
it('should extract strings inside string concatenation', () => {
const contents = `{{ 'a' + ('Hello' | translate) + 'b' + 'c' + ('World' | translate) + 'd' }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should extract strings from object', () => {
const contents = `{{ { foo: 'Hello' | translate, bar: ['World' | translate], deep: { nested: { baz: 'Yes' | translate } } } | json }}`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World', 'Yes']);
});
it('should extract strings from ternary operators inside attribute bindings', () => {
const contents = `<span [attr]="(condition ? 'Hello' : 'World') | translate"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should extract strings from nested expressions', () => {
const contents = `<span [attr]="{ foo: ['a' + ((condition ? 'Hello' : 'World') | translate) + 'b'] }"></span>`;
const keys = parser.extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['Hello', 'World']);
});
it('should extract strings from nested ternary operators ', () => {
const contents = `<h3>{{ (condition ? 'Hello' : anotherCondition ? 'Nested' : 'World' ) | translate }}</h3>`;
const keys = parser.extract(contents, templateFilename).keys();
@@ -159,4 +195,10 @@ describe('PipeParser', () => {
const keys = parser.extract(contents, templateFilename).keys();
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`]);
});
});

View File

@@ -1,7 +1,6 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"noUnusedLocals": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,

View File

@@ -112,6 +112,7 @@
"check-operator",
"check-separator",
"check-type"
]
],
"quotemark": [true, "single"]
}
}