Repository: i18next/i18next-parser (js)

Issue:
JS Lexer does not recognize keys in the options passed to `t` func
## 🐛 Bug Report
When using the built-in Javascript Lexer, `t` wrapped keys in the options that are passed to the `t` function are ignored and not parsed.
E.g. `t('foo {{ target }}', { target: t('bar') })`
- `foo {{ target }}` is extracted
- `bar` is not`

## To Reproduce
Run i18next-parser with the base JS lexer on any .js file containing the following line:
```js
 // t('foo {{ target }}', { target: t('bar') })
```

## Expected behavior

Translated keys within the options passed to already extracted keys should be caught as well.

```json
{ 
 "foo {{ target }}": "foo {{ target }}",
 "bar": "bar"
} 
```

## Your Environment

- *runtime version*: node v14 and node v18
- *i18next-parser*: 7.0.0
- *i18next*: 21.5.2
- *os*: Windows

Apologies if this has been reported before. We'd like to avoid extra comments and lines if possible and the included JS lexer does not appear to catch the below case. Assistance or a reference to what is considered best practices in this situation would be appreciated.

Pull request description:
Fix t func in options
### Why am I submitting this PR

Fixed a bug that prevented extraction of wrapped t functions.

Fixes #779

### Does it fix an existing ticket?

Yes #779

### Checklist

- [x] only relevant code is changed (make a diff before you submit the PR)
- [x] tests are included and pass: `yarn test` (see [details here](https://github.com/i18next/i18next-parser/blob/master/docs/development.md#tests))

Patch:
diff --git a/src/lexers/javascript-lexer.js b/src/lexers/javascript-lexer.js
index 2255552..d1f9d0f 100644
--- a/src/lexers/javascript-lexer.js
+++ b/src/lexers/javascript-lexer.js
@@ -70,8 +70,6 @@ export default class JavascriptLexer extends BaseLexer {
     const parseCommentNode = this.createCommentNodeParser()
 
     const parseTree = (node) => {
-      let entry
-
       parseCommentNode(keys, node, content)
 
       if (
@@ -82,15 +80,17 @@ export default class JavascriptLexer extends BaseLexer {
       }
 
       if (node.kind === ts.SyntaxKind.TaggedTemplateExpression) {
-        entry = this.taggedTemplateExpressionExtractor.call(this, node)
+        const entry = this.taggedTemplateExpressionExtractor.call(this, node)
+        if (entry) {
+          keys.push(entry)
+        }
       }
 
       if (node.kind === ts.SyntaxKind.CallExpression) {
-        entry = this.expressionExtractor.call(this, node)
-      }
-
-      if (entry) {
-        keys.push(entry)
+        const entries = this.expressionExtractor.call(this, node)
+        if (entries) {
+          keys.push(...entries)
+        }
       }
 
       node.forEachChild(parseTree)
@@ -159,7 +159,7 @@ export default class JavascriptLexer extends BaseLexer {
   }
 
   expressionExtractor(node) {
-    const entry = {}
+    const entries = [{}]
 
     if (
       this.namespaceFunctions.includes(node.expression.escapedText) &&
@@ -230,7 +230,7 @@ export default class JavascriptLexer extends BaseLexer {
         keyArgument.kind === ts.SyntaxKind.StringLiteral ||
         keyArgument.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral
       ) {
-        entry.key = keyArgument.text
+        entries[0].key = keyArgument.text
       } else if (keyArgument.kind === ts.SyntaxKind.BinaryExpression) {
         const concatenatedString = this.concatenateString(keyArgument)
         if (!concatenatedString) {
@@ -240,7 +240,7 @@ export default class JavascriptLexer extends BaseLexer {
           )
           return null
         }
-        entry.key = concatenatedString
+        entries[0].key = concatenatedString
       } else {
         this.emit(
           'warning',
@@ -260,7 +260,7 @@ export default class JavascriptLexer extends BaseLexer {
           }
           if (typeArg.kind === ts.SyntaxKind.TypeLiteral) {
             for (const member of typeArg.members) {
-              entry[member.name.text] = ''
+              entries[0][member.name.text] = ''
             }
           } else if (
             typeArg.kind === ts.SyntaxKind.TypeReference &&
@@ -268,7 +268,7 @@ export default class JavascriptLexer extends BaseLexer {
           ) {
             const typeName = typeArg.typeName.text
             if (typeName in this.typeMap) {
-              Object.assign(entry, this.typeMap[typeName])
+              Object.assign(entries[0], this.typeMap[typeName])
             }
           } else if (Array.isArray(typeArg.types)) {
             typeArgument.types.forEach((tp) => parseTypeArgument(tp))
@@ -286,7 +286,7 @@ export default class JavascriptLexer extends BaseLexer {
         (optionsArgument.kind === ts.SyntaxKind.StringLiteral ||
           optionsArgument.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral)
       ) {
-        entry.defaultValue = optionsArgument.text
+        entries[0].defaultValue = optionsArgument.text
         optionsArgument = node.arguments.shift()
       } else if (
         optionsArgument &&
@@ -300,7 +300,7 @@ export default class JavascriptLexer extends BaseLexer {
           )
           return null
         }
-        entry.defaultValue = concatenatedString
+        entries[0].defaultValue = concatenatedString
         optionsArgument = node.arguments.shift()
       }
 
@@ -316,27 +316,32 @@ export default class JavascriptLexer extends BaseLexer {
             )
           } else if (p.initializer) {
             if (p.initializer.kind === ts.SyntaxKind.TrueKeyword) {
-              entry[p.name.text] = true
+              entries[0][p.name.text] = true
             } else if (p.initializer.kind === ts.SyntaxKind.FalseKeyword) {
-              entry[p.name.text] = false
+              entries[0][p.name.text] = false
+            } else if (p.initializer.kind === ts.SyntaxKind.CallExpression) {
+              const nestedEntries = this.expressionExtractor(p.initializer)
+              if (nestedEntries) {
+                entries.push(...nestedEntries)
+              }
             } else {
-              entry[p.name.text] = p.initializer.text || ''
+              entries[0][p.name.text] = p.initializer.text || ''
             }
           } else {
-            entry[p.name.text] = ''
+            entries[0][p.name.text] = ''
           }
         }
       }
 
-      if (entry.ns) {
-        if (typeof entry.ns === 'string') {
-          entry.namespace = entry.ns
-        } else if (typeof entry.ns === 'object' && entry.ns.length) {
-          entry.namespace = entry.ns[0]
+      if (entries[0].ns) {
+        if (typeof entries[0].ns === 'string') {
+          entries[0].namespace = entries[0].ns
+        } else if (typeof entries.ns === 'object' && entries.ns.length) {
+          entries[0].namespace = entries[0].ns[0]
         }
       }
 
-      return entry
+      return entries
     }
 
     return null
diff --git a/src/lexers/jsx-lexer.js b/src/lexers/jsx-lexer.js
index 852411b..f4950af 100644
--- a/src/lexers/jsx-lexer.js
+++ b/src/lexers/jsx-lexer.js
@@ -32,7 +32,10 @@ export default class JsxLexer extends JavascriptLexer {
 
       switch (node.kind) {
         case ts.SyntaxKind.CallExpression:
-          entry = this.expressionExtractor.call(this, node)
+          const entries = this.expressionExtractor.call(this, node)
+          if (entries) {
+            keys.push(...entries)
+          }
           break
         case ts.SyntaxKind.TaggedTemplateExpression:
           entry = this.taggedTemplateExpressionExtractor.call(this, node)


Test patch:
diff --git a/test/lexers/javascript-lexer.test.js b/test/lexers/javascript-lexer.test.js
index 5fe9bdd..2fe09e1 100644
--- a/test/lexers/javascript-lexer.test.js
+++ b/test/lexers/javascript-lexer.test.js
@@ -209,6 +209,17 @@ describe('JavascriptLexer', () => {
     const content = 'i18n.t("first") as potato'
     assert.deepEqual(Lexer.extract(content), [{ key: 'first' }])
   })
+  it('supports for t function in options', () => {
+    const Lexer = new JavascriptLexer()
+    const content =
+      'i18n.t("first", {option: i18n.t("second",{option2: i18n.t("third")}), option3: i18n.t("fourth")})'
+    assert.deepEqual(Lexer.extract(content), [
+      { key: 'first' },
+      { key: 'second' },
+      { key: 'third' },
+      { key: 'fourth' },
+    ])
+  })
 
   describe('useTranslation', () => {
     it('extracts default namespace', () => {