diff --git a/src/cli/cli.ts b/src/cli/cli.ts index cd6d77d0..98aa7665 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -7,6 +7,7 @@ import { PipeParser } from '../parsers/pipe.parser.js'; import { DirectiveParser } from '../parsers/directive.parser.js'; import { ServiceParser } from '../parsers/service.parser.js'; import { MarkerParser } from '../parsers/marker.parser.js'; +import { FunctionParser } from '../parsers/function.parser.js'; import { PostProcessorInterface } from '../post-processors/post-processor.interface.js'; import { SortByKeyPostProcessor } from '../post-processors/sort-by-key.post-processor.js'; import { KeyAsDefaultValuePostProcessor } from '../post-processors/key-as-default-value.post-processor.js'; @@ -79,6 +80,12 @@ export const cli: any = y // temporary any describe: 'Remove obsolete strings after merge', type: 'boolean' }) + .option('marker', { + alias: 'm', + describe: 'Name of a custom marker function for extracting strings', + type: 'string', + default: undefined + }) .option('key-as-default-value', { alias: 'k', describe: 'Use key as default value', @@ -115,7 +122,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(), new DirectiveParser(), new ServiceParser()]; +if (cli.marker) { + parsers.push(new FunctionParser(cli.marker)) +} else { + parsers.push(new MarkerParser()) +} extractTask.setParsers(parsers); // Post processors diff --git a/src/parsers/function.parser.ts b/src/parsers/function.parser.ts new file mode 100644 index 00000000..a05ec4cc --- /dev/null +++ b/src/parsers/function.parser.ts @@ -0,0 +1,33 @@ +import { tsquery } from '@phenomnomnominal/tsquery'; + +import { ParserInterface } from './parser.interface.js'; +import { TranslationCollection } from '../utils/translation.collection.js'; +import { getStringsFromExpression, findSimpleCallExpressions } from '../utils/ast-helpers.js'; +import pkg from 'typescript'; +const { isIdentifier } = pkg; + +export class FunctionParser implements ParserInterface { + constructor(private fnName: string) {} + + public extract(source: string, filePath: string): TranslationCollection | null { + const sourceFile = tsquery.ast(source, filePath); + + let collection: TranslationCollection = new TranslationCollection(); + + const callExpressions = findSimpleCallExpressions(sourceFile, this.fnName); + callExpressions.forEach((callExpression) => { + if (!isIdentifier(callExpression.expression) + || callExpression.expression.escapedText != this.fnName) { + return + } + + const [firstArg] = callExpression.arguments; + if (!firstArg) { + return; + } + const strings = getStringsFromExpression(firstArg); + collection = collection.addKeys(strings); + }); + return collection; + } +} diff --git a/src/utils/ast-helpers.ts b/src/utils/ast-helpers.ts index ca298cc4..879019dd 100644 --- a/src/utils/ast-helpers.ts +++ b/src/utils/ast-helpers.ts @@ -83,6 +83,14 @@ export function findFunctionCallExpressions(node: Node, fnName: string | string[ return tsquery(node, query); } +export function findSimpleCallExpressions(node: Node, fnName: string) { + if (Array.isArray(fnName)) { + fnName = fnName.join('|'); + } + const query = `CallExpression:has(Identifier[name="${fnName}"])`; + return tsquery(node, query); +} + export function findPropertyCallExpressions(node: Node, prop: string, fnName: string | string[]): CallExpression[] { if (Array.isArray(fnName)) { fnName = fnName.join('|'); diff --git a/tests/parsers/function.parser.spec.ts b/tests/parsers/function.parser.spec.ts new file mode 100644 index 00000000..42a59466 --- /dev/null +++ b/tests/parsers/function.parser.spec.ts @@ -0,0 +1,61 @@ +import { expect } from 'chai'; + +import { FunctionParser } from '../../src/parsers/function.parser.js'; + +describe('FunctionParser', () => { + const componentFilename: string = 'test.component.ts'; + + let parser: FunctionParser; + + beforeEach(() => { + parser = new FunctionParser('MK'); + }); + + it('should extract strings using marker function', () => { + const contents = ` + MK('Hello world'); + MK(['I', 'am', 'extracted']); + otherFunction('But I am not'); + MK(message || 'binary expression'); + MK(message ? message : 'conditional operator'); + MK('FOO.bar'); + `; + const keys = parser.extract(contents, componentFilename).keys(); + expect(keys).to.deep.equal(['Hello world', 'I', 'am', 'extracted', 'binary expression', 'conditional operator', 'FOO.bar']); + }); + + it('should extract split strings', () => { + const contents = ` + MK('Hello ' + 'world'); + MK('This is a ' + 'very ' + 'very ' + 'very ' + 'very ' + 'long line.'); + MK('Mix ' + \`of \` + 'different ' + \`types\`); + `; + const keys = parser.extract(contents, componentFilename).keys(); + 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 = ` + MK('Hello ' + 'world'); + MK('This is a ' + 'very ' + 'very ' + 'very ' + 'very ' + 'long line.'); + MK('Mix ' + \`of \` + 'different ' + \`types\`); + `; + const keys = parser.extract(contents, componentFilename).keys(); + expect(keys).to.deep.equal(['Hello world', 'This is a very very very very long line.', 'Mix of different types']); + }); + + it('should extract the strings', () => { + const contents = ` + + export class AppModule { + constructor() { + MK('DYNAMIC_TRAD.val1'); + MK('DYNAMIC_TRAD.val2'); + } + } + `; + const keys = parser.extract(contents, componentFilename).keys(); + expect(keys).to.deep.equal(['DYNAMIC_TRAD.val1', 'DYNAMIC_TRAD.val2']); + }); + +});