Add support for marker functions, to be able to extract strings not directly passed to TranslateService. Closes #10
This commit is contained in:
		
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								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. | ||||
|  | ||||
|   | ||||
| @@ -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", | ||||
|   | ||||
| @@ -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(); | ||||
|   | ||||
| @@ -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); | ||||
| 	} | ||||
|   | ||||
| @@ -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'; | ||||
|   | ||||
							
								
								
									
										69
									
								
								src/parsers/abstract-ast.parser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/parsers/abstract-ast.parser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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]; | ||||
| 	} | ||||
|  | ||||
| } | ||||
							
								
								
									
										61
									
								
								src/parsers/function.parser.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/parsers/function.parser.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||
| 	} | ||||
|  | ||||
| } | ||||
| @@ -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); | ||||
| 	} | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -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]; | ||||
| } | ||||
							
								
								
									
										5
									
								
								src/utils/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								src/utils/utils.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| import * as ts from 'typescript'; | ||||
|  | ||||
| export function _(key: string | string[]): string | string[] { | ||||
| 	return key; | ||||
| } | ||||
							
								
								
									
										27
									
								
								tests/parsers/function.parser.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								tests/parsers/function.parser.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -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']); | ||||
| 	}); | ||||
|  | ||||
| }); | ||||
		Reference in New Issue
	
	Block a user