Compare commits
	
		
			8 Commits
		
	
	
		
			refactor-p
			...
			chore-upda
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 567a0586dc | ||
|  | 17dfbbed84 | ||
|  | acdffe0121 | ||
|  | 116133ba32 | ||
|  | 50b2ca6f4a | ||
|  | b46a914756 | ||
|  | bc3e5fbe2f | ||
|  | ea990d6f9d | 
| @@ -101,7 +101,8 @@ Examples: | |||||||
|   ngx-translate-extract -i './src/**/*.{ts,tsx,html}' -o strings.json     Extract from ts, tsx and html |   ngx-translate-extract -i './src/**/*.{ts,tsx,html}' -o strings.json     Extract from ts, tsx and html | ||||||
|   ngx-translate-extract -i './src/**/!(*.spec).{ts,html}' -o              Extract from ts, html, excluding files with ".spec" |   ngx-translate-extract -i './src/**/!(*.spec).{ts,html}' -o              Extract from ts, html, excluding files with ".spec" | ||||||
|   strings.json |   strings.json | ||||||
|  | ``` | ||||||
|  |  | ||||||
| ## Note for GetText users | ## Note for GetText users | ||||||
|  |  | ||||||
| Please pay attention of which version of `gettext-parser` you actually use in your project. For instance, `gettext-parser:1.2.2` does not support HTML tags in translation keys. | Please pay attention of which version of `gettext-parser` you actually use in your project. For instance, `gettext-parser:1.2.2` does not support HTML tags in translation keys. | ||||||
|   | |||||||
							
								
								
									
										1624
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1624
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										50
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										50
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "@biesbjerg/ngx-translate-extract", |   "name": "@biesbjerg/ngx-translate-extract", | ||||||
|   "version": "7.0.0", |   "version": "7.0.3", | ||||||
|   "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", | ||||||
| @@ -61,44 +61,44 @@ | |||||||
|   }, |   }, | ||||||
|   "config": {}, |   "config": {}, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@angular/compiler": "^9.1.9", |     "@angular/compiler": "^11.2.9", | ||||||
|     "@types/braces": "^3.0.0", |     "@types/braces": "^3.0.0", | ||||||
|     "@types/chai": "^4.2.11", |     "@types/chai": "^4.2.16", | ||||||
|     "@types/flat": "^5.0.1", |     "@types/flat": "^5.0.1", | ||||||
|     "@types/gettext-parser": "4.0.0", |     "@types/gettext-parser": "4.0.0", | ||||||
|     "@types/glob": "^7.1.1", |     "@types/glob": "^7.1.3", | ||||||
|     "@types/mkdirp": "^1.0.0", |     "@types/mkdirp": "^1.0.1", | ||||||
|     "@types/mocha": "^7.0.2", |     "@types/mocha": "^8.2.2", | ||||||
|     "@types/node": "^12.12.42", |     "@types/node": "^14.14.37", | ||||||
|     "@types/yargs": "^15.0.5", |     "@types/yargs": "^16.0.1", | ||||||
|     "braces": "^3.0.2", |     "braces": "^3.0.2", | ||||||
|     "chai": "^4.2.0", |     "chai": "^4.3.4", | ||||||
|     "husky": "^4.2.5", |     "husky": "^6.0.0", | ||||||
|     "lint-staged": "^10.2.6", |     "lint-staged": "^10.5.4", | ||||||
|     "mocha": "^7.2.0", |     "mocha": "^8.3.2", | ||||||
|     "prettier": "^2.0.5", |     "prettier": "^2.2.1", | ||||||
|     "rimraf": "^3.0.2", |     "rimraf": "^3.0.2", | ||||||
|     "ts-node": "^8.10.1", |     "ts-node": "^9.1.1", | ||||||
|     "tslint": "^6.1.2", |     "tslint": "^6.1.3", | ||||||
|     "tslint-config-prettier": "^1.18.0", |     "tslint-config-prettier": "^1.18.0", | ||||||
|     "tslint-eslint-rules": "^5.4.0", |     "tslint-eslint-rules": "^5.4.0", | ||||||
|     "tslint-etc": "^1.10.1", |     "tslint-etc": "^1.13.9", | ||||||
|     "typescript": "^3.9.3" |     "typescript": "^4.2.4" | ||||||
|   }, |   }, | ||||||
|   "peerDependencies": { |   "peerDependencies": { | ||||||
|     "@angular/compiler": "^8.0.0 || ^9.0.0", |     "@angular/compiler": ">=8.0.0", | ||||||
|     "typescript": "^3.0.0" |     "typescript": ">=3.0.0" | ||||||
|   }, |   }, | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@phenomnomnominal/tsquery": "^4.1.0", |     "@phenomnomnominal/tsquery": "^4.1.1", | ||||||
|     "boxen": "^4.2.0", |     "boxen": "^5.0.1", | ||||||
|     "colorette": "^1.2.0", |     "colorette": "^1.2.2", | ||||||
|     "flat": "^5.0.0", |     "flat": "^5.0.2", | ||||||
|     "gettext-parser": "^4.0.3", |     "gettext-parser": "^4.0.4", | ||||||
|     "glob": "^7.1.6", |     "glob": "^7.1.6", | ||||||
|     "mkdirp": "^1.0.4", |     "mkdirp": "^1.0.4", | ||||||
|     "path": "^0.12.7", |     "path": "^0.12.7", | ||||||
|     "terminal-link": "^2.1.1", |     "terminal-link": "^2.1.1", | ||||||
|     "yargs": "^15.3.1" |     "yargs": "^16.2.0" | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,20 +1,16 @@ | |||||||
| import { | import { | ||||||
| 	AST, | 	AST, | ||||||
| 	ASTWithSource, | 	TmplAstNode, | ||||||
| 	TmplAstNode as Node, |  | ||||||
| 	TmplAstBoundText as BoundText, |  | ||||||
| 	TmplAstElement as Element, |  | ||||||
| 	TmplAstTemplate as Template, |  | ||||||
| 	TmplAstBoundAttribute as BoundAttribute, |  | ||||||
| 	TmplAstBoundEvent as BoundEvent, |  | ||||||
| 	parseTemplate, | 	parseTemplate, | ||||||
| 	BindingPipe, | 	BindingPipe, | ||||||
| 	LiteralPrimitive, | 	LiteralPrimitive, | ||||||
| 	Conditional, | 	Conditional, | ||||||
|  | 	TmplAstTextAttribute, | ||||||
| 	Binary, | 	Binary, | ||||||
| 	LiteralMap, | 	LiteralMap, | ||||||
| 	LiteralArray, | 	LiteralArray, | ||||||
| 	Interpolation | 	Interpolation, | ||||||
|  | 	MethodCall | ||||||
| } from '@angular/compiler'; | } from '@angular/compiler'; | ||||||
|  |  | ||||||
| import { ParserInterface } from './parser.interface'; | import { ParserInterface } from './parser.interface'; | ||||||
| @@ -22,99 +18,146 @@ import { TranslationCollection } from '../utils/translation.collection'; | |||||||
| import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils'; | import { isPathAngularComponent, extractComponentInlineTemplate } from '../utils/utils'; | ||||||
|  |  | ||||||
| const TRANSLATE_PIPE_NAME = 'translate'; | const TRANSLATE_PIPE_NAME = 'translate'; | ||||||
| type ElementLike = Element | Template; |  | ||||||
|  |  | ||||||
| export class PipeParser implements ParserInterface { | export class PipeParser implements ParserInterface { | ||||||
| 	public extract(source: string, filePath: string): TranslationCollection | null { | 	public extract(source: string, filePath: string): TranslationCollection | null { | ||||||
| 		let collection: TranslationCollection = new TranslationCollection(); |  | ||||||
|  |  | ||||||
| 		if (filePath && isPathAngularComponent(filePath)) { | 		if (filePath && isPathAngularComponent(filePath)) { | ||||||
| 			source = extractComponentInlineTemplate(source); | 			source = extractComponentInlineTemplate(source); | ||||||
| 		} | 		} | ||||||
| 		const nodes: Node[] = this.parseTemplate(source, filePath); |  | ||||||
| 		const pipes = this.getBindingPipes(nodes, TRANSLATE_PIPE_NAME).filter((pipe) => !this.pipeHasConcatenatedString(pipe)); |  | ||||||
|  |  | ||||||
|  | 		let collection: TranslationCollection = new TranslationCollection(); | ||||||
|  | 		const nodes: TmplAstNode[] = this.parseTemplate(source, filePath); | ||||||
|  | 		const pipes: BindingPipe[] = nodes.map((node) => this.findPipesInNode(node)).flat(); | ||||||
| 		pipes.forEach((pipe) => { | 		pipes.forEach((pipe) => { | ||||||
| 			this.visitEachChild(pipe, (child) => { | 			this.parseTranslationKeysFromPipe(pipe).forEach((key: string) => { | ||||||
| 				if (child instanceof LiteralPrimitive) { | 				collection = collection.add(key); | ||||||
| 					collection = collection.add(child.value); |  | ||||||
| 				} |  | ||||||
| 			}); | 			}); | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		return collection; | 		return collection; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	protected getBindingPipes(nodes: any[], name: string): BindingPipe[] { | 	protected findPipesInNode(node: any): BindingPipe[] { | ||||||
| 		let pipes: BindingPipe[] = []; | 		let ret: BindingPipe[] = []; | ||||||
| 		nodes.forEach((node) => { |  | ||||||
| 			if (this.isElementLike(node)) { |  | ||||||
| 				pipes = [ |  | ||||||
| 					...pipes, |  | ||||||
| 					...this.getBindingPipes([ |  | ||||||
| 						...node.inputs, |  | ||||||
| 						...node.children |  | ||||||
| 					], name) |  | ||||||
| 				]; |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			this.visitEachChild(node, (exp) => { | 		if (node?.children) { | ||||||
| 				if (exp instanceof BindingPipe && exp.name === name) { | 			ret = node.children.reduce( | ||||||
| 					pipes = [...pipes, exp]; | 				(result: BindingPipe[], childNode: TmplAstNode) => { | ||||||
| 				} | 					const children = this.findPipesInNode(childNode); | ||||||
| 			}); | 					return result.concat(children); | ||||||
| 		}); | 				}, | ||||||
| 		return pipes; | 				[ret] | ||||||
| 	} | 			); | ||||||
|  |  | ||||||
| 	protected visitEachChild(exp: AST, visitor: (child: AST) => void): void { |  | ||||||
| 		visitor(exp); |  | ||||||
|  |  | ||||||
| 		let children: AST[] = []; |  | ||||||
| 		if (exp instanceof BoundText) { |  | ||||||
| 			children = [exp.value]; |  | ||||||
| 		} else if (exp instanceof BoundAttribute) { |  | ||||||
| 			children = [exp.value]; |  | ||||||
| 		} else if (exp instanceof BoundEvent) { |  | ||||||
| 			children = [exp.handler]; |  | ||||||
| 		} else if (exp instanceof Interpolation) { |  | ||||||
| 			children = exp.expressions; |  | ||||||
| 		} else if (exp instanceof LiteralArray) { |  | ||||||
| 			children = exp.expressions; |  | ||||||
| 		} else if (exp instanceof LiteralMap) { |  | ||||||
| 			children = exp.values; |  | ||||||
| 		} else if (exp instanceof BindingPipe) { |  | ||||||
| 			children = [exp.exp, ...exp.args]; |  | ||||||
| 		} else if (exp instanceof Conditional) { |  | ||||||
| 			children = [exp.trueExp, exp.falseExp]; |  | ||||||
| 		} else if (exp instanceof Binary) { |  | ||||||
| 			children = [exp.left, exp.right]; |  | ||||||
| 		} else if (exp instanceof ASTWithSource) { |  | ||||||
| 			children = [exp.ast]; |  | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		children.forEach((child) => { | 		if (node?.value?.ast) { | ||||||
| 			this.visitEachChild(child, visitor); | 			ret.push(...this.getTranslatablesFromAst(node.value.ast)); | ||||||
| 		}); | 		} | ||||||
|  |  | ||||||
|  | 		if (node?.attributes) { | ||||||
|  | 			const translateableAttributes = node.attributes.filter((attr: TmplAstTextAttribute) => { | ||||||
|  | 				return attr.name === TRANSLATE_PIPE_NAME; | ||||||
|  | 			}); | ||||||
|  | 			ret = [...ret, ...translateableAttributes]; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if (node?.inputs) { | ||||||
|  | 			node.inputs.forEach((input: any) => { | ||||||
|  | 				// <element [attrib]="'identifier' | translate"> | ||||||
|  | 				if (input?.value?.ast) { | ||||||
|  | 					ret.push(...this.getTranslatablesFromAst(input.value.ast)); | ||||||
|  | 				} | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return ret; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/** | 	protected parseTranslationKeysFromPipe(pipeContent: BindingPipe | LiteralPrimitive | Conditional): string[] { | ||||||
| 	 * Check if node type is ElementLike | 		const ret: string[] = []; | ||||||
| 	 * @param node | 		if (pipeContent instanceof LiteralPrimitive) { | ||||||
| 	 */ | 			ret.push(pipeContent.value); | ||||||
| 	protected isElementLike(node: Node): node is ElementLike { | 		} else if (pipeContent instanceof Conditional) { | ||||||
| 		return node instanceof Element || node instanceof Template; | 			const trueExp: LiteralPrimitive | Conditional = pipeContent.trueExp as any; | ||||||
|  | 			ret.push(...this.parseTranslationKeysFromPipe(trueExp)); | ||||||
|  | 			const falseExp: LiteralPrimitive | Conditional = pipeContent.falseExp as any; | ||||||
|  | 			ret.push(...this.parseTranslationKeysFromPipe(falseExp)); | ||||||
|  | 		} else if (pipeContent instanceof BindingPipe) { | ||||||
|  | 			ret.push(...this.parseTranslationKeysFromPipe(pipeContent.exp as any)); | ||||||
|  | 		} | ||||||
|  | 		return ret; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	/** | 	protected getTranslatablesFromAst(ast: AST): BindingPipe[] { | ||||||
| 	 * Check if pipe concatenates string (in that case we don't want to extract it) | 		// the entire expression is the translate pipe, e.g.: | ||||||
| 	 * @param pipe | 		// - 'foo' | translate | ||||||
| 	 */ | 		// - (condition ? 'foo' : 'bar') | translate | ||||||
| 	protected pipeHasConcatenatedString(pipe: BindingPipe): boolean { | 		if (this.expressionIsOrHasBindingPipe(ast)) { | ||||||
| 		return pipe?.exp instanceof Binary && pipe.exp.operation === '+'; | 			return [ast]; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// angular double curly bracket interpolation, e.g.: | ||||||
|  | 		// - {{ expressions }} | ||||||
|  | 		if (ast instanceof Interpolation) { | ||||||
|  | 			return this.getTranslatablesFromAsts(ast.expressions); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// ternary operator, e.g.: | ||||||
|  | 		// - condition ? null : ('foo' | translate) | ||||||
|  | 		// - condition ? ('foo' | translate) : null | ||||||
|  | 		if (ast instanceof Conditional) { | ||||||
|  | 			return this.getTranslatablesFromAsts([ast.trueExp, ast.falseExp]); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// string concatenation, e.g.: | ||||||
|  | 		// - 'foo' + 'bar' + ('baz' | translate) | ||||||
|  | 		if (ast instanceof Binary) { | ||||||
|  | 			return this.getTranslatablesFromAsts([ast.left, ast.right]); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// a pipe on the outer expression, but not the translate pipe - ignore the pipe, visit the expression, e.g.: | ||||||
|  | 		// - { foo: 'Hello' | translate } | json | ||||||
|  | 		if (ast instanceof BindingPipe) { | ||||||
|  | 			return this.getTranslatablesFromAst(ast.exp); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// object - ignore the keys, visit all values, e.g.: | ||||||
|  | 		// - { key1: 'value1' | translate, key2: 'value2' | translate } | ||||||
|  | 		if (ast instanceof LiteralMap) { | ||||||
|  | 			return this.getTranslatablesFromAsts(ast.values); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// array - visit all its values, e.g.: | ||||||
|  | 		// - [ 'value1' | translate, 'value2' | translate ] | ||||||
|  | 		if (ast instanceof LiteralArray) { | ||||||
|  | 			return this.getTranslatablesFromAsts(ast.expressions); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if (ast instanceof MethodCall) { | ||||||
|  | 			return this.getTranslatablesFromAsts(ast.args); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return []; | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	protected parseTemplate(template: string, path: string): Node[] { | 	protected getTranslatablesFromAsts(asts: AST[]): BindingPipe[] { | ||||||
|  | 		return this.flatten(asts.map((ast) => this.getTranslatablesFromAst(ast))); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	protected flatten<T extends AST>(array: T[][]): T[] { | ||||||
|  | 		return [].concat(...array); | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	protected expressionIsOrHasBindingPipe(exp: any): exp is BindingPipe { | ||||||
|  | 		if (exp.name && exp.name === TRANSLATE_PIPE_NAME) { | ||||||
|  | 			return true; | ||||||
|  | 		} | ||||||
|  | 		if (exp.exp && exp.exp instanceof BindingPipe) { | ||||||
|  | 			return this.expressionIsOrHasBindingPipe(exp.exp); | ||||||
|  | 		} | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	protected parseTemplate(template: string, path: string): TmplAstNode[] { | ||||||
| 		return parseTemplate(template, path).nodes; | 		return parseTemplate(template, path).nodes; | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -195,4 +195,10 @@ describe('PipeParser', () => { | |||||||
| 		const keys = parser.extract(contents, templateFilename).keys(); | 		const keys = parser.extract(contents, templateFilename).keys(); | ||||||
| 		expect(keys).to.deep.equal([]); | 		expect(keys).to.deep.equal([]); | ||||||
| 	}); | 	}); | ||||||
|  |  | ||||||
|  | 	it('should extract strings from piped arguments inside a function calls on templates', () => { | ||||||
|  | 		const contents = `{{ callMe('Hello' | translate, 'World' | translate ) }}`; | ||||||
|  | 		const keys = parser.extract(contents, templateFilename).keys(); | ||||||
|  | 		expect(keys).to.deep.equal([`Hello`, `World`]); | ||||||
|  | 	}); | ||||||
| }); | }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user