diff --git a/README.md b/README.md index 1e0ca14..0e24a5b 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,18 @@ If you want to use spaces instead, you can do the following: `ngx-translate-extract -i ./src -o ./src/i18n/en.json --format-indentation ' '` +## Mark strings for extraction using a marker function +If, for some reason, you want to extract strings not passed directly to TranslateService, you can wrap them in a custom marker function. + +```ts +import { _ } from '@biesbjerg/ngx-translate-extract'; + +_('Extract me'); +``` + +Add the `marker` argument when running the extract script: + +`ngx-translate-extract ... -m _` Modify the scripts arguments as required. diff --git a/package.json b/package.json index 91b2a10..66463dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@biesbjerg/ngx-translate-extract", - "version": "2.1.0", + "version": "2.2.0", "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 e8a62e4..17be4d4 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -3,6 +3,7 @@ import { ParserInterface } from '../parsers/parser.interface'; import { PipeParser } from '../parsers/pipe.parser'; import { DirectiveParser } from '../parsers/directive.parser'; import { ServiceParser } from '../parsers/service.parser'; +import { FunctionParser } from '../parsers/function.parser'; import { CompilerInterface } from '../compilers/compiler.interface'; import { CompilerFactory } from '../compilers/compiler.factory'; @@ -44,6 +45,12 @@ export const cli = yargs normalize: true, required: true }) + .option('marker', { + alias: 'm', + describe: 'Extract strings passed to a marker function', + default: false, + type: 'string' + }) .option('format', { alias: 'f', describe: 'Output format', @@ -78,22 +85,28 @@ export const cli = yargs .exitProcess(true) .parse(process.argv); -const parsers: ParserInterface[] = [ - new ServiceParser(), - new PipeParser(), - new DirectiveParser() -]; - -const compiler: CompilerInterface = CompilerFactory.create(cli.format, { - indentation: cli.formatIndentation -}); - -new ExtractTask(cli.input, cli.output, { +const extract = new ExtractTask(cli.input, cli.output, { replace: cli.replace, sort: cli.sort, clean: cli.clean, patterns: cli.patterns -}) -.setParsers(parsers) -.setCompiler(compiler) -.execute(); +}); + +const compiler: CompilerInterface = CompilerFactory.create(cli.format, { + indentation: cli.formatIndentation +}); +extract.setCompiler(compiler); + +const parsers: ParserInterface[] = [ + new PipeParser(), + new DirectiveParser(), + new ServiceParser() +]; +if (cli.marker) { + parsers.push(new FunctionParser({ + identifier: cli.marker + })); +} +extract.setParsers(parsers); + +extract.execute(); diff --git a/src/cli/tasks/extract.task.ts b/src/cli/tasks/extract.task.ts index 8f628af..ff378cb 100644 --- a/src/cli/tasks/extract.task.ts +++ b/src/cli/tasks/extract.task.ts @@ -41,11 +41,6 @@ export class ExtractTask implements TaskInterface { } const collection = this._extract(); - if (collection.isEmpty()) { - this._out(chalk.yellow('Did not find any extractable strings\n')); - return; - } - this._out(chalk.green('Extracted %d strings\n'), collection.count()); this._save(collection); } diff --git a/src/index.ts b/src/index.ts index 8b97519..72c4fe5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ export * from './utils/translation.collection'; -export * from './utils/ast-utils'; +export * from './utils/utils'; export * from './cli/cli'; export * from './cli/tasks/task.interface'; @@ -7,9 +7,11 @@ export * from './cli/tasks/extract.task'; export * from './parsers/parser.interface'; export * from './parsers/abstract-template.parser'; +export * from './parsers/abstract-ast.parser'; export * from './parsers/directive.parser'; export * from './parsers/pipe.parser'; export * from './parsers/service.parser'; +export * from './parsers/function.parser'; export * from './compilers/compiler.interface'; export * from './compilers/compiler.factory'; diff --git a/src/parsers/abstract-ast.parser.ts b/src/parsers/abstract-ast.parser.ts new file mode 100644 index 0000000..3d60359 --- /dev/null +++ b/src/parsers/abstract-ast.parser.ts @@ -0,0 +1,69 @@ +import * as ts from 'typescript'; + +export abstract class AbstractAstParser { + + protected _sourceFile: ts.SourceFile; + + protected _createSourceFile(path: string, contents: string): ts.SourceFile { + return ts.createSourceFile(path, contents, null, /*setParentNodes */ false); + } + + /** + * Get strings from function call's first argument + */ + protected _getCallArgStrings(callNode: ts.CallExpression): string[] { + if (!callNode.arguments.length) { + return; + } + + const firstArg = callNode.arguments[0]; + switch (firstArg.kind) { + case ts.SyntaxKind.StringLiteral: + case ts.SyntaxKind.FirstTemplateToken: + return [(firstArg as ts.StringLiteral).text]; + case ts.SyntaxKind.ArrayLiteralExpression: + return (firstArg as ts.ArrayLiteralExpression).elements + .map((element: ts.StringLiteral) => element.text); + case ts.SyntaxKind.Identifier: + console.log('WARNING: We cannot extract variable values passed to TranslateService (yet)'); + break; + default: + console.log(`SKIP: Unknown argument type: '${this._syntaxKindToName(firstArg.kind)}'`, firstArg); + } + } + + /** + * Find all child nodes of a kind + */ + protected _findNodes(node: ts.Node, kind: ts.SyntaxKind, onlyOne: boolean = false): ts.Node[] { + if (node.kind === kind && onlyOne) { + return [node]; + } + + const childrenNodes: ts.Node[] = node.getChildren(this._sourceFile); + const initialValue: ts.Node[] = node.kind === kind ? [node] : []; + + return childrenNodes.reduce((result: ts.Node[], childNode: ts.Node) => { + return result.concat(this._findNodes(childNode, kind)); + }, initialValue); + } + + protected _printAllChildren(sourceFile: ts.SourceFile, node: ts.Node, depth = 0) { + console.log( + new Array(depth + 1).join('----'), + `[${node.kind}]`, + this._syntaxKindToName(node.kind), + `[pos: ${node.pos}-${node.end}]`, + ':\t\t\t', + node.getFullText(sourceFile).trim() + ); + + depth++; + node.getChildren(sourceFile).forEach(childNode => this._printAllChildren(sourceFile, childNode, depth)); + } + + protected _syntaxKindToName(kind: ts.SyntaxKind) { + return ts.SyntaxKind[kind]; + } + +} diff --git a/src/parsers/function.parser.ts b/src/parsers/function.parser.ts new file mode 100644 index 0000000..c3609bb --- /dev/null +++ b/src/parsers/function.parser.ts @@ -0,0 +1,61 @@ +import { ParserInterface } from './parser.interface'; +import { AbstractAstParser } from './abstract-ast.parser'; +import { TranslationCollection } from '../utils/translation.collection'; + +import * as ts from 'typescript'; + +export class FunctionParser extends AbstractAstParser implements ParserInterface { + + protected _functionIdentifier: string = '_'; + + public constructor(options?: any) { + super(); + if (options && typeof options.identifier !== 'undefined') { + this._functionIdentifier = options.identifier; + } + } + + public extract(contents: string, path?: string): TranslationCollection { + let collection: TranslationCollection = new TranslationCollection(); + + this._sourceFile = this._createSourceFile(path, contents); + + const callNodes = this._findCallNodes(); + callNodes.forEach(callNode => { + const keys: string[] = this._getCallArgStrings(callNode); + if (keys && keys.length) { + collection = collection.addKeys(keys); + } + }); + + return collection; + } + + /** + * Find all calls to marker function + */ + protected _findCallNodes(node?: ts.Node): ts.CallExpression[] { + if (!node) { + node = this._sourceFile; + } + + let callNodes = this._findNodes(node, ts.SyntaxKind.CallExpression) as ts.CallExpression[]; + callNodes = callNodes + .filter(callNode => { + // Only call expressions with arguments + if (callNode.arguments.length < 1) { + return false; + } + + const identifier = (callNode.getChildAt(0) as ts.Identifier).text; + if (identifier !== this._functionIdentifier) { + return false; + } + + return true; + }); + + return callNodes; + } + +} diff --git a/src/parsers/service.parser.ts b/src/parsers/service.parser.ts index 99cc2dc..23ec692 100644 --- a/src/parsers/service.parser.ts +++ b/src/parsers/service.parser.ts @@ -1,10 +1,10 @@ import { ParserInterface } from './parser.interface'; +import { AbstractAstParser } from './abstract-ast.parser'; import { TranslationCollection } from '../utils/translation.collection'; -import { syntaxKindToName } from '../utils/ast-utils'; import * as ts from 'typescript'; -export class ServiceParser implements ParserInterface { +export class ServiceParser extends AbstractAstParser implements ParserInterface { protected _sourceFile: ts.SourceFile; @@ -33,10 +33,6 @@ export class ServiceParser implements ParserInterface { return collection; } - protected _createSourceFile(path: string, contents: string): ts.SourceFile { - return ts.createSourceFile(path, contents, null, /*setParentNodes */ false); - } - /** * Detect what the TranslateService instance property * is called by inspecting constructor params @@ -91,10 +87,12 @@ export class ServiceParser implements ParserInterface { let callNodes = this._findNodes(node, ts.SyntaxKind.CallExpression) as ts.CallExpression[]; callNodes = callNodes - // Only call expressions with arguments - .filter(callNode => callNode.arguments.length > 0) - // More filters .filter(callNode => { + // Only call expressions with arguments + if (callNode.arguments.length < 1) { + return false; + } + const propAccess = callNode.getChildAt(0).getChildAt(0) as ts.PropertyAccessExpression; if (!propAccess || propAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) { return false; @@ -120,44 +118,4 @@ export class ServiceParser implements ParserInterface { return callNodes; } - /** - * Get strings from function call's first argument - */ - protected _getCallArgStrings(callNode: ts.CallExpression): string[] { - if (!callNode.arguments.length) { - return; - } - - const firstArg = callNode.arguments[0]; - switch (firstArg.kind) { - case ts.SyntaxKind.StringLiteral: - case ts.SyntaxKind.FirstTemplateToken: - return [(firstArg as ts.StringLiteral).text]; - case ts.SyntaxKind.ArrayLiteralExpression: - return (firstArg as ts.ArrayLiteralExpression).elements - .map((element: ts.StringLiteral) => element.text); - case ts.SyntaxKind.Identifier: - console.log('WARNING: We cannot extract variable values passed to TranslateService (yet)'); - break; - default: - console.log(`SKIP: Unknown argument type: '${syntaxKindToName(firstArg.kind)}'`, firstArg); - } - } - - /** - * Find all child nodes of a kind - */ - protected _findNodes(node: ts.Node, kind: ts.SyntaxKind, onlyOne: boolean = false): ts.Node[] { - if (node.kind === kind && onlyOne) { - return [node]; - } - - const childrenNodes: ts.Node[] = node.getChildren(this._sourceFile); - const initialValue: ts.Node[] = node.kind === kind ? [node] : []; - - return childrenNodes.reduce((result: ts.Node[], childNode: ts.Node) => { - return result.concat(this._findNodes(childNode, kind)); - }, initialValue); - } - } diff --git a/src/utils/ast-utils.ts b/src/utils/ast-utils.ts deleted file mode 100644 index 0b8cae1..0000000 --- a/src/utils/ast-utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as ts from 'typescript'; - -export function printAllChildren(sourceFile: ts.SourceFile, node: ts.Node, depth = 0) { - console.log( - new Array(depth + 1).join('----'), - `[${node.kind}]`, - syntaxKindToName(node.kind), - `[pos: ${node.pos}-${node.end}]`, - ':\t\t\t', - node.getFullText(sourceFile).trim() - ); - - depth++; - node.getChildren(sourceFile).forEach(childNode => printAllChildren(sourceFile, childNode, depth)); -} - -export function syntaxKindToName(kind: ts.SyntaxKind) { - return ts.SyntaxKind[kind]; -} diff --git a/src/utils/utils.ts b/src/utils/utils.ts new file mode 100644 index 0000000..dea0067 --- /dev/null +++ b/src/utils/utils.ts @@ -0,0 +1,5 @@ +import * as ts from 'typescript'; + +export function _(key: string | string[]): string | string[] { + return key; +} diff --git a/tests/parsers/function.parser.spec.ts b/tests/parsers/function.parser.spec.ts new file mode 100644 index 0000000..bc5319d --- /dev/null +++ b/tests/parsers/function.parser.spec.ts @@ -0,0 +1,27 @@ +import { expect } from 'chai'; + +import { FunctionParser } from '../../src/parsers/function.parser'; + +describe('FunctionParser', () => { + + const componentFilename: string = 'test.component.ts'; + + let parser: FunctionParser; + + beforeEach(() => { + parser = new FunctionParser(); + }); + + + it('should extract strings using marker function', () => { + const contents = ` + import { _ } from '@biesbjerg/ngx-translate-extract'; + _('Hello world'); + _(['I', 'am', 'extracted']); + otherFunction('But I am not'); + `; + const keys = parser.extract(contents, componentFilename).keys(); + expect(keys).to.deep.equal(['Hello world', 'I', 'am', 'extracted']); + }); + +});