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

Issue:
Cast with <> in Typescript breaks marker function parsing
Hi, thanks for your fork!

I recently migrated from https://github.com/bartholomej/ngx-translate-extract to your fork as part of an Angular17 migration. We have a relatively large Angular application with ~250 components and around 1500 strings in two languages - a good test case :) 

When testing the string extraction (with the marker func) I noticed that some strings were suddenly missing. I tracked down the problem and created a small repro. sample (see Angular component below). 

There is a problem with parsing this line from the example component below 

`const myNiceVar2 = <string>input;`

The cast interrupts the parsing of further markers in the current function.  

```typescript
import { Component } from '@angular/core';
import { marker } from '@colsen1991/ngx-translate-extract-marker';

marker('1');

@Component({
  standalone: true,
  imports: [],
  selector: 'app-root',
  template: '<h1>hello</h1>',
  styles: '',
})
export class AppComponent {
  constructor() {
    marker('2');
  }

  myFunc(): void {
    marker('3');
    const input: unknown = 'hello';    
    const myNiceVar1 = input as string;
    marker('4');
    
    // --> cast in the next line breaks the parsing in this block
    const myNiceVar2 = <string>input;
    marker('5_i_will_never_show');
  }

  myOtherFunc(): void {
    marker('6');
  }
}
```

leads to this output 

```JSON
{
	"1": "1",
	"2": "2",
	"3": "3",
	"4": "4",
	"6": "6"
}
```

We can easily get round this and there were only a few places in our code. But I think it should be fixed somehow, so I'm posting it here as an issue.

Pull request:
fix: Fix marker function parsing when used after bracket syntax casting expression
The issue is that the `tsquery.ast` defaults to parsing the source as TSX, which causes problems when bracket syntax casting (`<Type>`) is used. This is because the parser confuses the casting syntax with JSX. To resolve this, we need to explicitly set the ScriptKind parameter to TS.

In the `ast` function description is also suggested to use `ScriptKind.TS` if the code uses the `<Type>` syntax for casting.

> "If you are using the `<Type>` syntax for casting, you should use `ScriptKind.TS`."
https://github.com/phenomnomnominal/tsquery/blob/master/src/ast.ts#L12

This change ensures that the parser passes the correct `ScriptKind` to the `ast` function based on the source file extension, so that for .ts files it avoids the confusion with JSX and preventing parsing errors.

Fixes #43

Patch:
diff --git a/src/parsers/marker.parser.ts b/src/parsers/marker.parser.ts
index 1015be5..c9812da 100644
--- a/src/parsers/marker.parser.ts
+++ b/src/parsers/marker.parser.ts
@@ -1,4 +1,5 @@
-import { tsquery } from '@phenomnomnominal/tsquery';
+import { ScriptKind, tsquery } from '@phenomnomnominal/tsquery';
+import { extname } from 'path';
 
 import { ParserInterface } from './parser.interface.js';
 import { TranslationCollection } from '../utils/translation.collection.js';
@@ -9,7 +10,16 @@ const MARKER_IMPORT_NAME = 'marker';
 
 export class MarkerParser implements ParserInterface {
 	public extract(source: string, filePath: string): TranslationCollection | null {
-		const sourceFile = tsquery.ast(source, filePath);
+		const supportedScriptTypes: Record<string, ScriptKind> = {
+			'.js': ScriptKind.JS,
+			'.jsx': ScriptKind.JSX,
+			'.ts': ScriptKind.TS,
+			'.tsx': ScriptKind.TSX
+		};
+
+		const scriptKind = supportedScriptTypes[extname(filePath)] ?? ScriptKind.TS;
+
+		const sourceFile = tsquery.ast(source, filePath, scriptKind);
 
 		const markerImportName = getNamedImportAlias(sourceFile, MARKER_MODULE_NAME, MARKER_IMPORT_NAME);
 		if (!markerImportName) {
diff --git a/src/parsers/pipe.parser.ts b/src/parsers/pipe.parser.ts
index 5449287..5959b22 100644
--- a/src/parsers/pipe.parser.ts
+++ b/src/parsers/pipe.parser.ts
@@ -15,7 +15,8 @@ import {
 	TmplAstSwitchBlock,
 	TmplAstDeferredBlock,
 	TmplAstForLoopBlock,
-	TmplAstElement
+	TmplAstElement,
+	KeyedRead
 } from '@angular/compiler';
 
 import { ParserInterface } from './parser.interface.js';
@@ -195,6 +196,19 @@ export class PipeParser implements ParserInterface {
 			return this.getTranslatablesFromAsts(ast.args);
 		}
 
+		// immediately accessed static object or array - the angular parser bundles this as "KeyedRead", where:
+		// { 'a': 1, 'b': 2 }[ 'a' ];
+		//                     ^^^ <- keyedRead.key
+		// ^^^^^^^^^^^^^^^^^^ <- keyedRead.receiver
+		//
+		// html examples:
+		// - { key1: 'value1' | translate, key2: 'value2' | translate }[key]
+		// - [ 'value1' | translate, 'value2' | translate ][key]
+		// - [ 'foo', 'bar' ][ 'key' | translate ]
+		if (ast instanceof KeyedRead) {
+			return this.getTranslatablesFromAsts([ast.receiver, ast.key]);
+		}
+
 		return [];
 	}
 


Test patch:
diff --git a/tests/parsers/marker.parser.spec.ts b/tests/parsers/marker.parser.spec.ts
index db6f0e9..ec20cfb 100644
--- a/tests/parsers/marker.parser.spec.ts
+++ b/tests/parsers/marker.parser.spec.ts
@@ -71,4 +71,20 @@ describe('MarkerParser', () => {
 		const keys = parser.extract(contents, componentFilename).keys();
 		expect(keys).to.deep.equal(['Hello world']);
 	});
+
+	it('should not break after bracket syntax casting', () => {
+		const contents = `
+		import { marker } from '@colsen1991/ngx-translate-extract-marker';
+		
+		marker('hello');
+		const input: unknown = 'hello';    
+		const myNiceVar1 = input as string;
+		marker('hello.after.as.syntax');
+			
+		const myNiceVar2 = <string>input;
+		marker('hello.after.bracket.syntax');
+		`;
+		const keys = parser.extract(contents, componentFilename).keys();
+		expect(keys).to.deep.equal(['hello', 'hello.after.as.syntax', 'hello.after.bracket.syntax']);
+	});
 });
diff --git a/tests/parsers/pipe.parser.spec.ts b/tests/parsers/pipe.parser.spec.ts
index 9eca297..d126b26 100644
--- a/tests/parsers/pipe.parser.spec.ts
+++ b/tests/parsers/pipe.parser.spec.ts
@@ -73,6 +73,34 @@ describe('PipeParser', () => {
 				expect(keys).to.deep.equal(['Hello', 'World']);
 			});
 
+			it('should extract strings from object map', () => {
+				const contents = `{{ {
+					choice1: 'Hello' | ${translatePipeName},
+					choice2: 'World' | ${translatePipeName},
+				}[choice] }}`;
+				const keys = parser.extract(contents, templateFilename).keys();
+				expect(keys).to.deep.equal(['Hello', 'World']);
+			});
+
+			it('should extract strings from object map inside attribute', () => {
+				const contents = `
+					<span [attr]="{
+						choice1: 'Hello' | ${translatePipeName},
+						choice2: 'World' | ${translatePipeName}
+					}[choice]"></span>`;
+				const keys = parser.extract(contents, templateFilename).keys();
+				expect(keys).to.deep.equal(['Hello', 'World']);
+			});
+
+			it('should extract strings from KeyedRead.key', () => {
+				const contents = `{{ {
+					choice1: 'Foo',
+					choice2: 'Bar',
+				}[ 'choice' | ${translatePipeName} ] }}`;
+				const keys = parser.extract(contents, templateFilename).keys();
+				expect(keys).to.deep.equal(['choice']);
+			});
+
 			it('should extract strings from object', () => {
 				const contents = `{{ { foo: 'Hello' | ${translatePipeName}, bar: ['World' | ${translatePipeName}], deep: { nested: { baz: 'Yes' | ${translatePipeName} } } } | json }}`;
 				const keys = parser.extract(contents, templateFilename).keys();