Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@biesbjerg/ngx-translate-extract",
"version": "7.0.3",
"name": "@enilpajic/ngx-translate-extract",
"version": "7.0.4",
"description": "Extract strings from projects using ngx-translate",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down
47 changes: 41 additions & 6 deletions src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ export const cli = y
required: true
})
.coerce('input', (input: string[]) => {
const paths = normalizePaths(input, parsed.patterns);
return paths;
return normalizePaths(input, parsed.patterns);
})
.option('output', {
alias: 'o',
Expand All @@ -55,8 +54,7 @@ export const cli = y
required: true
})
.coerce('output', (output: string[]) => {
const paths = normalizePaths(output, parsed.patterns);
return paths;
return normalizePaths(output, parsed.patterns);
})
.option('format', {
alias: 'f',
Expand All @@ -67,7 +65,7 @@ export const cli = y
})
.option('format-indentation', {
alias: 'fi',
describe: 'Format indentation (JSON/Namedspaced JSON)',
describe: 'Format indentation (JSON/Namespaced JSON)',
default: '\t',
type: 'string'
})
Expand Down Expand Up @@ -104,10 +102,42 @@ export const cli = y
type: 'string',
conflicts: ['null-as-default-value', 'key-as-default-value']
})
.option('marker', {
alias: 'm',
describe: 'Use a default marker (overrides the default which looks for a direct import from @biesjberg/ng-translate-extract-marker)',
type: 'string'
})
.option('pipe', {
describe: 'Check for these pipe names when extracting',
type: 'array',
default: ['translate'],
normalize: true
})
.option('service-name', {
describe: 'Check for this service name when extracting',
default: 'TranslateService',
type: 'string'
})
.option('service-method-name', {
describe: 'Check for these service method names when extracting',
type: 'array',
default: ['get', 'instant', 'stream'],
normalize: true
})
.option('directive', {
describe: 'Check for these directive names when extracting.',
type: 'array',
default: ['translate'],
normalize: true
})
.group(['format', 'format-indentation', 'sort', 'clean', 'replace'], 'Output')
.group(['key-as-default-value', 'null-as-default-value', 'string-as-default-value'], 'Extracted key value (defaults to empty string)')
.conflicts('key-as-default-value', 'null-as-default-value')
.example(`$0 -i ./src-a/ -i ./src-b/ -o strings.json`, 'Extract (ts, html) from multiple paths')
.example(`$0 -i ./src-a/ -o strings.json -m i18n`, 'Extract (ts, html), using custom marker name "i18n"')
.example(`$0 -i ./src-a/ -o strings.json --pipe translate --pipe translateAdvanced`, 'Extract (ts, html), using custom names for all translate pipes ("translate" and "translateAdvanced")')
.example(`$0 -i ./src-a/ -o strings.json --directive translate --directive translateAdvanced`, 'Extract (ts, html), using custom names for all translate directives ("translate" and "translateAdvanced")')
.example(`$0 -i ./src-a/ -o strings.json --service-name AdvancedTranslateService --service-method-name get --service-method-name getAll --service-method-name observable`, 'Extract (ts, html), using custom service name "AdvancedTranslateService" and custom service method names: get, getAll, observable')
.example(`$0 -i './{src-a,src-b}/' -o strings.json`, 'Extract (ts, html) from multiple paths using brace expansion')
.example(`$0 -i ./src/ -o ./i18n/da.json -o ./i18n/en.json`, 'Extract (ts, html) and save to da.json and en.json')
.example(`$0 -i ./src/ -o './i18n/{en,da}.json'`, 'Extract (ts, html) and save to da.json and en.json using brace expansion')
Expand All @@ -122,7 +152,12 @@ const extractTask = new ExtractTask(cli.input, cli.output, {
});

// Parsers
const parsers: ParserInterface[] = [new PipeParser(), new DirectiveParser(), new ServiceParser(), new MarkerParser()];
const parsers: ParserInterface[] = [
new PipeParser(cli.pipe),
new DirectiveParser(cli.directive),
new ServiceParser(cli['service-name'], cli['service-method-name']),
new MarkerParser(cli.marker)
];
extractTask.setParsers(parsers);

// Post processors
Expand Down
50 changes: 30 additions & 20 deletions src/parsers/directive.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,14 @@ import { ParserInterface } from './parser.interface';
import { TranslationCollection } from '../utils/translation.collection';
import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils';

const TRANSLATE_ATTR_NAME = 'translate';
const TRANSLATE_ATTR_NAMES = ['translate'];
type ElementLike = Element | Template;

export class DirectiveParser implements ParserInterface {
constructor(private readonly attrNames?: string[]) {
this.attrNames = attrNames && attrNames.length ? attrNames : TRANSLATE_ATTR_NAMES;
}

public extract(source: string, filePath: string): TranslationCollection | null {
let collection: TranslationCollection = new TranslationCollection();

Expand All @@ -35,16 +39,19 @@ export class DirectiveParser implements ParserInterface {
const elements: ElementLike[] = this.getElementsWithTranslateAttribute(nodes);

elements.forEach((element) => {
const attribute = this.getAttribute(element, TRANSLATE_ATTR_NAME);
if (attribute?.value) {
collection = collection.add(attribute.value);
const attributes = this.getAttributes(element, this.attrNames);
if (attributes?.length && attributes.filter(a => a?.value)?.length) {
attributes.filter(a => a?.value)
.forEach(attribute => collection = collection.add(attribute.value));
return;
}

const boundAttribute = this.getBoundAttribute(element, TRANSLATE_ATTR_NAME);
if (boundAttribute?.value) {
this.getLiteralPrimitives(boundAttribute.value).forEach((literalPrimitive) => {
collection = collection.add(literalPrimitive.value);
const boundAttributes = this.getBoundAttributes(element, this.attrNames);
if (boundAttributes?.length && boundAttributes.filter(a => a?.value)?.length) {
boundAttributes.filter(a => a?.value).forEach(boundAttribute => {
this.getLiteralPrimitives(boundAttribute.value).forEach((literalPrimitive) => {
collection = collection.add(literalPrimitive.value);
});
});
return;
}
Expand All @@ -64,12 +71,13 @@ export class DirectiveParser implements ParserInterface {
protected getElementsWithTranslateAttribute(nodes: Node[]): ElementLike[] {
let elements: ElementLike[] = [];
nodes.filter(this.isElementLike).forEach((element) => {
if (this.hasAttribute(element, TRANSLATE_ATTR_NAME)) {
if (this.hasAttribute(element, this.attrNames)) {
elements = [...elements, element];
}
if (this.hasBoundAttribute(element, TRANSLATE_ATTR_NAME)) {
if (this.hasBoundAttributes(element, this.attrNames)) {
elements = [...elements, element];
}

const childElements = this.getElementsWithTranslateAttribute(element.children);
if (childElements.length) {
elements = [...elements, ...childElements];
Expand All @@ -89,35 +97,37 @@ export class DirectiveParser implements ParserInterface {
/**
* Check if attribute is present on element
* @param element
* @param names
*/
protected hasAttribute(element: ElementLike, name: string): boolean {
return this.getAttribute(element, name) !== undefined;
protected hasAttribute(element: ElementLike, names: string[]): boolean {
return this.getAttributes(element, names)?.length > 0 || false;
}

/**
* Get attribute value if present on element
* @param element
* @param names
*/
protected getAttribute(element: ElementLike, name: string): TextAttribute {
return element.attributes.find((attribute) => attribute.name === name);
protected getAttributes(element: ElementLike, names: string[]): TextAttribute[] {
return element.attributes.filter((attribute) => names.includes(attribute.name));
}

/**
* Check if bound attribute is present on element
* @param element
* @param name
* @param names
*/
protected hasBoundAttribute(element: ElementLike, name: string): boolean {
return this.getBoundAttribute(element, name) !== undefined;
protected hasBoundAttributes(element: ElementLike, names: string[]): boolean {
return this.getBoundAttributes(element, names)?.length > 0 || false;
}

/**
* Get bound attribute if present on element
* @param element
* @param name
* @param names
*/
protected getBoundAttribute(element: ElementLike, name: string): BoundAttribute {
return element.inputs.find((input) => input.name === name);
protected getBoundAttributes(element: ElementLike, names: string[]): BoundAttribute[] {
return element.inputs.filter((input) => names.includes(input.name));
}

/**
Expand Down
3 changes: 2 additions & 1 deletion src/parsers/marker.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ const MARKER_MODULE_NAME = '@biesbjerg/ngx-translate-extract-marker';
const MARKER_IMPORT_NAME = 'marker';

export class MarkerParser implements ParserInterface {
constructor(private marker?: string) {}
public extract(source: string, filePath: string): TranslationCollection | null {
const sourceFile = tsquery.ast(source, filePath);

const markerImportName = getNamedImportAlias(sourceFile, MARKER_MODULE_NAME, MARKER_IMPORT_NAME);
const markerImportName = this.marker || getNamedImportAlias(sourceFile, MARKER_MODULE_NAME, MARKER_IMPORT_NAME);
if (!markerImportName) {
return null;
}
Expand Down
10 changes: 7 additions & 3 deletions src/parsers/pipe.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import { ParserInterface } from './parser.interface';
import { TranslationCollection } from '../utils/translation.collection';
import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils';

const TRANSLATE_PIPE_NAME = 'translate';
const DEFAULT_TRANSLATE_PIPE_NAMES = ['translate'];

export class PipeParser implements ParserInterface {
constructor(private readonly pipes?: string[]) {
this.pipes = pipes && pipes.length ? pipes : DEFAULT_TRANSLATE_PIPE_NAMES;
}

public extract(source: string, filePath: string): TranslationCollection | null {
if (filePath && isPathAngularComponent(filePath)) {
source = extractComponentInlineTemplate(source);
Expand Down Expand Up @@ -54,7 +58,7 @@ export class PipeParser implements ParserInterface {

if (node?.attributes) {
const translateableAttributes = node.attributes.filter((attr: TmplAstTextAttribute) => {
return attr.name === TRANSLATE_PIPE_NAME;
return this.pipes.includes(attr.name);
});
ret = [...ret, ...translateableAttributes];
}
Expand Down Expand Up @@ -143,7 +147,7 @@ export class PipeParser implements ParserInterface {
}

protected expressionIsOrHasBindingPipe(exp: any): exp is BindingPipe {
if (exp.name && exp.name === TRANSLATE_PIPE_NAME) {
if (exp.name && this.pipes.includes(exp.name)) {
return true;
}
if (exp.exp && exp.exp instanceof BindingPipe) {
Expand Down
17 changes: 11 additions & 6 deletions src/parsers/service.parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ import {
findConstructorDeclaration
} from '../utils/ast-helpers';

const TRANSLATE_SERVICE_TYPE_REFERENCE = 'TranslateService';
const TRANSLATE_SERVICE_METHOD_NAMES = ['get', 'instant', 'stream'];
const DEFAULT_TRANSLATE_SERVICE_TYPE_REFERENCE = 'TranslateService';
const DEFAULT_TRANSLATE_SERVICE_METHOD_NAMES = ['get', 'instant', 'stream'];

export class ServiceParser implements ParserInterface {
constructor(private readonly serviceName?: string, private readonly serviceMethodNames?: string[]) {
this.serviceName = serviceName || DEFAULT_TRANSLATE_SERVICE_TYPE_REFERENCE;
this.serviceMethodNames = serviceMethodNames && serviceMethodNames.length ? serviceMethodNames : DEFAULT_TRANSLATE_SERVICE_METHOD_NAMES;
}

public extract(source: string, filePath: string): TranslationCollection | null {
const sourceFile = tsquery.ast(source, filePath);

Expand Down Expand Up @@ -50,15 +55,15 @@ export class ServiceParser implements ParserInterface {
if (!constructorDeclaration) {
return [];
}
const paramName = findMethodParameterByType(constructorDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE);
return findMethodCallExpressions(constructorDeclaration, paramName, TRANSLATE_SERVICE_METHOD_NAMES);
const paramName = findMethodParameterByType(constructorDeclaration, this.serviceName);
return findMethodCallExpressions(constructorDeclaration, paramName, this.serviceMethodNames);
}

protected findPropertyCallExpressions(classDeclaration: ClassDeclaration): CallExpression[] {
const propName: string = findClassPropertyByType(classDeclaration, TRANSLATE_SERVICE_TYPE_REFERENCE);
const propName: string = findClassPropertyByType(classDeclaration, this.serviceName);
if (!propName) {
return [];
}
return findPropertyCallExpressions(classDeclaration, propName, TRANSLATE_SERVICE_METHOD_NAMES);
return findPropertyCallExpressions(classDeclaration, propName, this.serviceMethodNames);
}
}
33 changes: 33 additions & 0 deletions tests/parsers/directive.parser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ describe('DirectiveParser', () => {
expect(keys).to.deep.equal(['value1', 'value2']);
});

it('should extract keys when using literal arrays in bound attribute using custom directive names', () => {
const contents = `
<div [translate]="[ 'value1' | translate, 'value2' | translate2 ]"></div>
<div [translate2]="[ 'value3' | translate2, 'value4' | translate ]"></div>
`;
const keys = new DirectiveParser(['translate', 'translate2']).extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['value1', 'value2', 'value3', 'value4']);
});

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();
Expand All @@ -43,6 +52,12 @@ describe('DirectiveParser', () => {
expect(keys).to.deep.equal(['KEY1']);
});

it('should extract keys when using custom directive names in bound attribute', () => {
const contents = `<div [translate]="'KEY1'"></div><div [translateX]="'KEY2'"></div>`;
const keys = new DirectiveParser(['translate', 'translateX']).extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['KEY1', 'KEY2']);
});

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();
Expand Down Expand Up @@ -85,6 +100,12 @@ describe('DirectiveParser', () => {
expect(keys).to.deep.equal(['MY_KEY']);
});

it('should use translate attribute value as key when present with custom directive name', () => {
const contents = '<div myTranslate="MY_KEY">Hello World<div>';
const keys = new DirectiveParser(['myTranslate']).extract(contents, templateFilename).keys();
expect(keys).to.deep.equal(['MY_KEY']);
});

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();
Expand All @@ -109,6 +130,18 @@ describe('DirectiveParser', () => {
expect(keys).to.deep.equal(['Hello World']);
});

it('should extract and parse inline template with custom directive name', () => {
const contents = `
@Component({
selector: 'test',
template: '<p myTranslate>Hello World</p>'
})
export class TestComponent { }
`;
const keys = new DirectiveParser(['myTranslate']).extract(contents, componentFilename).keys();
expect(keys).to.deep.equal(['Hello World']);
});

it('should extract contents when no translate attribute value is provided', () => {
const contents = '<div translate>Hello World</div>';
const keys = parser.extract(contents, templateFilename).keys();
Expand Down
Loading