diff --git a/package-lock.json b/package-lock.json index 61e5f6c1..b960064e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "@biesbjerg/ngx-translate-extract", - "version": "7.0.2", - "lockfileVersion": 1, + "version": "7.0.3", + "lockfileVersion": 2, "requires": true, "dependencies": { "@angular/compiler": { @@ -1952,6 +1952,14 @@ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, "string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", @@ -2013,14 +2021,6 @@ "es-abstract": "^1.17.5" } }, - "string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "requires": { - "safe-buffer": "~5.2.0" - } - }, "stringify-object": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", diff --git a/package.json b/package.json index d34e2174..33f7e1a9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/cli/cli.ts b/src/cli/cli.ts index d3df4b37..5e00c5e2 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -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', @@ -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', @@ -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' }) @@ -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') @@ -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 diff --git a/src/parsers/directive.parser.ts b/src/parsers/directive.parser.ts index d910b728..090e8637 100644 --- a/src/parsers/directive.parser.ts +++ b/src/parsers/directive.parser.ts @@ -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(); @@ -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; } @@ -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]; @@ -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)); } /** diff --git a/src/parsers/marker.parser.ts b/src/parsers/marker.parser.ts index 32c498c1..ff9cbdfc 100644 --- a/src/parsers/marker.parser.ts +++ b/src/parsers/marker.parser.ts @@ -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; } diff --git a/src/parsers/pipe.parser.ts b/src/parsers/pipe.parser.ts index 73a2c89d..f48ea813 100644 --- a/src/parsers/pipe.parser.ts +++ b/src/parsers/pipe.parser.ts @@ -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); @@ -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]; } @@ -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) { diff --git a/src/parsers/service.parser.ts b/src/parsers/service.parser.ts index d444a0f3..01334795 100644 --- a/src/parsers/service.parser.ts +++ b/src/parsers/service.parser.ts @@ -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); @@ -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); } } diff --git a/tests/parsers/directive.parser.spec.ts b/tests/parsers/directive.parser.spec.ts index 7d7b11d0..40c63168 100644 --- a/tests/parsers/directive.parser.spec.ts +++ b/tests/parsers/directive.parser.spec.ts @@ -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 = ` +
+
+ `; + 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 = `
`; const keys = parser.extract(contents, templateFilename).keys(); @@ -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 = `
`; + 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 = `
`; const keys = parser.extract(contents, templateFilename).keys(); @@ -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 = '
Hello World
'; + 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 = `
Hello World
`; const keys = parser.extract(contents, templateFilename).keys(); @@ -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: '

Hello World

' + }) + 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 = '
Hello World
'; const keys = parser.extract(contents, templateFilename).keys(); diff --git a/tests/parsers/pipe.parser.spec.ts b/tests/parsers/pipe.parser.spec.ts index ba5e3ab2..f64b3e1c 100644 --- a/tests/parsers/pipe.parser.spec.ts +++ b/tests/parsers/pipe.parser.spec.ts @@ -17,6 +17,15 @@ describe('PipeParser', () => { expect(keys).to.deep.equal(['SomeKey_NotWorking']); }); + it('should only extract string using pipe with custom pipe names', () => { + const contents = ` + + + `; + const keys = new PipeParser(['translate', 'translate2']).extract(contents, templateFilename).keys(); + expect(keys).to.deep.equal(['SomeKey_NotWorking', 'SomeKey2']); + }); + it('should extract string using pipe, but between quotes only', () => { const contents = ``; const keys = parser.extract(contents, templateFilename).keys(); diff --git a/tests/parsers/service.parser.spec.ts b/tests/parsers/service.parser.spec.ts index 7eba5ffb..cccfe04d 100644 --- a/tests/parsers/service.parser.spec.ts +++ b/tests/parsers/service.parser.spec.ts @@ -23,6 +23,33 @@ describe('ServiceParser', () => { expect(keys).to.deep.equal(['It works!']); }); + it('should extract strings when TranslateService is accessed directly via constructor parameter, when custom service name is used', () => { + const contents = ` + @Component({ }) + export class MyComponent { + public constructor(protected translateService: AdvancedTranslateService) { + translateService.get('It works!'); + } + `; + const keys = new ServiceParser('AdvancedTranslateService').extract(contents, componentFilename).keys(); + expect(keys).to.deep.equal(['It works!']); + }); + + it('should extract strings when TranslateService is accessed directly via constructor parameter, when custom service name is used, with custom service method names', () => { + const contents = ` + @Component({ }) + export class MyComponent { + public constructor(protected translateService: AdvancedTranslateService) { + translateService.get('It works!'); + translateService.instant('It works 2!'); + translateService.someBrandNewMethod('It works 3!'); + } + `; + const keys = new ServiceParser('AdvancedTranslateService', ['get', 'someBrandNewMethod', 'instant']) + .extract(contents, componentFilename).keys(); + expect(keys).to.deep.equal(['It works!', 'It works 2!', 'It works 3!']); + }); + it('should support extracting binary expressions', () => { const contents = ` @Component({ }) @@ -51,7 +78,7 @@ describe('ServiceParser', () => { expect(keys).to.deep.equal(['Fallback message']); }); - it("should extract strings in TranslateService's get() method", () => { + it('should extract strings in TranslateService\'s get() method', () => { const contents = ` @Component({ }) export class AppComponent {