Repository: vendure-ecommerce/ngx-translate-extract (ts)

Issue:
Service parser is not locating the TranslateService if it is a private field
The service parser fails to extract translation keys when the `TranslateService` is assigned to a private field using the JavaScript private field syntax (#). For example, from the code below the key 'hello.world' is not extracted:

```ts
import {Component, inject} from '@angular/core';
import {TranslateService} from '@ngx-translate/core';

@Component({
  selector: 'my-component',
  template: `<p>{{ translatedText }}</p>`
})
export class MyComponent {
  readonly #translate = inject(TranslateService);
  translatedText = '';

  constructor() {
    this.translatedText = this.#translate.instant('hello.world');
  }
}

```

Pull request:
fix: Fix parser not locating `TranslateService` in private fields using the `#` syntax
Fixes #54

Patch:
diff --git a/src/utils/ast-helpers.ts b/src/utils/ast-helpers.ts
index 0309f1f..0b859ec 100644
--- a/src/utils/ast-helpers.ts
+++ b/src/utils/ast-helpers.ts
@@ -8,7 +8,11 @@ import pkg, {
 	CallExpression,
 	Expression,
 	StringLiteral,
-	SourceFile
+	SourceFile,
+	PropertyDeclaration,
+	PropertyAccessExpression,
+	isPropertyAccessExpression,
+	isCallExpression
 } from 'typescript';
 const { SyntaxKind, isStringLiteralLike, isArrayLiteralExpression, isBinaryExpression, isConditionalExpression } = pkg;
 
@@ -142,15 +146,15 @@ export function findClassPropertiesConstructorParameterByType(node: ClassDeclara
 }
 
 export function findClassPropertiesDeclarationByType(node: ClassDeclaration, type: string): string[] {
-	const query = `PropertyDeclaration:has(TypeReference > Identifier[name="${type}"]) > Identifier`;
-	const result = tsquery<Identifier>(node, query);
-	return result.map((n) => n.text);
+	const query = `PropertyDeclaration:has(TypeReference > Identifier[name="${type}"])`;
+	const result = tsquery<PropertyDeclaration>(node, query);
+	return result.map((n) => n.name.getText());
 }
 
 export function findClassPropertiesDeclarationByInject(node: ClassDeclaration, type: string): string[] {
-	const query = `PropertyDeclaration:has(CallExpression > Identifier[name="inject"]):has(CallExpression > Identifier[name="${type}"]) > Identifier`;
-	const result = tsquery<Identifier>(node, query);
-	return result.map((n) => n.text);
+	const query = `PropertyDeclaration:has(CallExpression > Identifier[name="inject"]):has(CallExpression > Identifier[name="${type}"])`;
+	const result = tsquery<PropertyDeclaration>(node, query);
+	return result.map((n) => n.name.getText());
 }
 
 export function findClassPropertiesGetterByType(node: ClassDeclaration, type: string): string[] {
@@ -179,22 +183,23 @@ export function findPropertyCallExpressions(node: Node, prop: string, fnName: st
 	if (Array.isArray(fnName)) {
 		fnName = fnName.join('|');
 	}
-	const query = 'CallExpression > ' +
-        `PropertyAccessExpression:has(Identifier[name=/^(${fnName})$/]):has(PropertyAccessExpression:has(Identifier[name="${prop}"]):has(ThisKeyword)) > ` +
-        `PropertyAccessExpression:has(ThisKeyword) > Identifier[name="${prop}"]`;
-	const nodes = tsquery<Identifier>(node, query);
-	// Since the direct descendant operator (>) is not supported in :has statements, we need to
-	// check manually whether everything is correctly matched
-	const filtered = nodes.reduce<CallExpression[]>((result: CallExpression[], n: Node) => {
-		if (
-			tsquery(n.parent, 'PropertyAccessExpression > ThisKeyword').length > 0 &&
-			tsquery(n.parent.parent, `PropertyAccessExpression > Identifier[name=/^(${fnName})$/]`).length > 0
-		) {
-			result.push(n.parent.parent.parent as CallExpression);
+
+	const query = `CallExpression > PropertyAccessExpression:has(Identifier[name=/^(${fnName})$/]):has(PropertyAccessExpression:has(ThisKeyword))`;
+	const result = tsquery<PropertyAccessExpression>(node, query);
+
+	const nodes: CallExpression[] = [];
+	result.forEach((n) => {
+		const identifier = isPropertyAccessExpression(n.expression) ? n.expression.name : null;
+		const property = identifier?.parent;
+		const method = property?.parent;
+		const callExpression = method?.parent;
+
+		if (identifier?.getText() === prop && isCallExpression(callExpression)) {
+			nodes.push(callExpression);
 		}
-		return result;
-	}, []);
-	return filtered;
+	});
+
+	return nodes;
 }
 
 export function getStringsFromExpression(expression: Expression): string[] {


Test patch:
diff --git a/tests/parsers/service.parser.spec.ts b/tests/parsers/service.parser.spec.ts
index 2b16239..5bf5fb1 100644
--- a/tests/parsers/service.parser.spec.ts
+++ b/tests/parsers/service.parser.spec.ts
@@ -314,6 +314,22 @@ describe('ServiceParser', () => {
 		expect(keys).to.deep.equal(['Hello World']);
 	});
 
+	it('should extract strings when TranslateService is declared as a private property', () => {
+		const contents = `
+			export class MyComponent {
+				readonly #translate: TranslateService;
+				public constructor(translateService: TranslateService) {
+					this.#translate = translateService;
+				}
+				public test() {
+					this.#translate.instant('Hello World');
+				}
+			}
+		`;
+		const keys = parser.extract(contents, componentFilename)?.keys();
+		expect(keys).to.deep.equal(['Hello World']);
+	});
+
 	it('should extract strings when TranslateService is injected using the inject function ', () => {
 		const contents = `
 			export class MyComponent {
@@ -328,6 +344,23 @@ describe('ServiceParser', () => {
 		expect(keys).to.deep.equal(['Hello World']);
 	});
 
+	it('should locate TranslateService when it is a JavaScript native private property', () => {
+		const contents = `
+			@Component({})
+			export class MyComponent {
+			    readonly #translate = inject(TranslateService);
+				
+				public test() {
+					this.#translate.get('get.works');
+					this.#translate.stream('stream.works');
+					this.#translate.instant('instant.works');
+				}
+			}
+		`;
+		const keys = parser.extract(contents, componentFilename)?.keys();
+		expect(keys).to.deep.equal(['get.works', 'stream.works', 'instant.works']);
+	});
+
 	it('should extract strings passed to TranslateServices methods only', () => {
 		const contents = `
 			export class AppComponent implements OnInit {