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 '  '` | `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. | Modify the scripts arguments as required. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "@biesbjerg/ngx-translate-extract", |   "name": "@biesbjerg/ngx-translate-extract", | ||||||
|   "version": "2.1.0", |   "version": "2.2.0", | ||||||
|   "description": "Extract strings from projects using ngx-translate", |   "description": "Extract strings from projects using ngx-translate", | ||||||
|   "main": "dist/index.js", |   "main": "dist/index.js", | ||||||
|   "typings": "dist/index.d.ts", |   "typings": "dist/index.d.ts", | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import { ParserInterface } from '../parsers/parser.interface'; | |||||||
| import { PipeParser } from '../parsers/pipe.parser'; | import { PipeParser } from '../parsers/pipe.parser'; | ||||||
| import { DirectiveParser } from '../parsers/directive.parser'; | import { DirectiveParser } from '../parsers/directive.parser'; | ||||||
| import { ServiceParser } from '../parsers/service.parser'; | import { ServiceParser } from '../parsers/service.parser'; | ||||||
|  | import { FunctionParser } from '../parsers/function.parser'; | ||||||
| import { CompilerInterface } from '../compilers/compiler.interface'; | import { CompilerInterface } from '../compilers/compiler.interface'; | ||||||
| import { CompilerFactory } from '../compilers/compiler.factory'; | import { CompilerFactory } from '../compilers/compiler.factory'; | ||||||
|  |  | ||||||
| @@ -44,6 +45,12 @@ export const cli = yargs | |||||||
| 		normalize: true, | 		normalize: true, | ||||||
| 		required: true | 		required: true | ||||||
| 	}) | 	}) | ||||||
|  | 	.option('marker', { | ||||||
|  | 		alias: 'm', | ||||||
|  | 		describe: 'Extract strings passed to a marker function', | ||||||
|  | 		default: false, | ||||||
|  | 		type: 'string' | ||||||
|  | 	}) | ||||||
| 	.option('format', { | 	.option('format', { | ||||||
| 		alias: 'f', | 		alias: 'f', | ||||||
| 		describe: 'Output format', | 		describe: 'Output format', | ||||||
| @@ -78,22 +85,28 @@ export const cli = yargs | |||||||
| 	.exitProcess(true) | 	.exitProcess(true) | ||||||
| 	.parse(process.argv); | 	.parse(process.argv); | ||||||
|  |  | ||||||
| const parsers: ParserInterface[] = [ | const extract = new ExtractTask(cli.input, cli.output, { | ||||||
| 	new ServiceParser(), |  | ||||||
| 	new PipeParser(), |  | ||||||
| 	new DirectiveParser() |  | ||||||
| ]; |  | ||||||
|  |  | ||||||
| const compiler: CompilerInterface = CompilerFactory.create(cli.format, { |  | ||||||
| 	indentation: cli.formatIndentation |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| new ExtractTask(cli.input, cli.output, { |  | ||||||
| 	replace: cli.replace, | 	replace: cli.replace, | ||||||
| 	sort: cli.sort, | 	sort: cli.sort, | ||||||
| 	clean: cli.clean, | 	clean: cli.clean, | ||||||
| 	patterns: cli.patterns | 	patterns: cli.patterns | ||||||
| }) | }); | ||||||
| .setParsers(parsers) |  | ||||||
| .setCompiler(compiler) | const compiler: CompilerInterface = CompilerFactory.create(cli.format, { | ||||||
| .execute(); | 	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(); | 		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._out(chalk.green('Extracted %d strings\n'), collection.count()); | ||||||
| 		this._save(collection); | 		this._save(collection); | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| export * from './utils/translation.collection'; | export * from './utils/translation.collection'; | ||||||
| export * from './utils/ast-utils'; | export * from './utils/utils'; | ||||||
|  |  | ||||||
| export * from './cli/cli'; | export * from './cli/cli'; | ||||||
| export * from './cli/tasks/task.interface'; | export * from './cli/tasks/task.interface'; | ||||||
| @@ -7,9 +7,11 @@ export * from './cli/tasks/extract.task'; | |||||||
|  |  | ||||||
| export * from './parsers/parser.interface'; | export * from './parsers/parser.interface'; | ||||||
| export * from './parsers/abstract-template.parser'; | export * from './parsers/abstract-template.parser'; | ||||||
|  | export * from './parsers/abstract-ast.parser'; | ||||||
| export * from './parsers/directive.parser'; | export * from './parsers/directive.parser'; | ||||||
| export * from './parsers/pipe.parser'; | export * from './parsers/pipe.parser'; | ||||||
| export * from './parsers/service.parser'; | export * from './parsers/service.parser'; | ||||||
|  | export * from './parsers/function.parser'; | ||||||
|  |  | ||||||
| export * from './compilers/compiler.interface'; | export * from './compilers/compiler.interface'; | ||||||
| export * from './compilers/compiler.factory'; | 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 { ParserInterface } from './parser.interface'; | ||||||
|  | import { AbstractAstParser } from './abstract-ast.parser'; | ||||||
| import { TranslationCollection } from '../utils/translation.collection'; | import { TranslationCollection } from '../utils/translation.collection'; | ||||||
| import { syntaxKindToName } from '../utils/ast-utils'; |  | ||||||
|  |  | ||||||
| import * as ts from 'typescript'; | import * as ts from 'typescript'; | ||||||
|  |  | ||||||
| export class ServiceParser implements ParserInterface { | export class ServiceParser extends AbstractAstParser implements ParserInterface { | ||||||
|  |  | ||||||
| 	protected _sourceFile: ts.SourceFile; | 	protected _sourceFile: ts.SourceFile; | ||||||
|  |  | ||||||
| @@ -33,10 +33,6 @@ export class ServiceParser implements ParserInterface { | |||||||
| 		return collection; | 		return collection; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	protected _createSourceFile(path: string, contents: string): ts.SourceFile { |  | ||||||
| 		return ts.createSourceFile(path, contents, null, /*setParentNodes */ false); |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	/** | 	/** | ||||||
| 	 * Detect what the TranslateService instance property | 	 * Detect what the TranslateService instance property | ||||||
| 	 * is called by inspecting constructor params | 	 * 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[]; | 		let callNodes = this._findNodes(node, ts.SyntaxKind.CallExpression) as ts.CallExpression[]; | ||||||
| 		callNodes = callNodes | 		callNodes = callNodes | ||||||
| 			// Only call expressions with arguments |  | ||||||
| 			.filter(callNode => callNode.arguments.length > 0) |  | ||||||
| 			// More filters |  | ||||||
| 			.filter(callNode => { | 			.filter(callNode => { | ||||||
|  | 				// Only call expressions with arguments | ||||||
|  | 				if (callNode.arguments.length < 1) { | ||||||
|  | 					return false; | ||||||
|  | 				} | ||||||
|  |  | ||||||
| 				const propAccess = callNode.getChildAt(0).getChildAt(0) as ts.PropertyAccessExpression; | 				const propAccess = callNode.getChildAt(0).getChildAt(0) as ts.PropertyAccessExpression; | ||||||
| 				if (!propAccess || propAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) { | 				if (!propAccess || propAccess.kind !== ts.SyntaxKind.PropertyAccessExpression) { | ||||||
| 					return false; | 					return false; | ||||||
| @@ -120,44 +118,4 @@ export class ServiceParser implements ParserInterface { | |||||||
| 		return callNodes; | 		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