Repository: i18next/i18next-parser (js)

Issue:
The keyPrefix argument is ignored in JavascriptLexer, and not in JsxLexer
## To Reproduce

Name this file with .ts extension (configured with JavascriptLexer) and the keyPrefix is ignored, if you use JsxLexer instead, it's works
```typescript
export default function useArticlesFilter() {
  const { t } = useTranslation('filters', { keyPrefix: 'articles' });

  return useMemo(() => {
    return {
      menuTitle: t('menu'),
      badge: {
        Component: ArticlesBadge,
      },
      panel: {
        Component: ArticlesPanel,
        query: ArticlesPanelQuery,
      },
    };
  }, [t]);
}
```

## Expected behavior

Keyprefix should not be ignored

Pull request description:
[Fix] Allow to use multiple translation functions with different namespaces and keyPrefixes
### Why am I submitting this PR

Using multiple translation functions is currently a pain point for me.

### Does it fix an existing ticket?

Yes #494 
Also provides a workaround for #973 
Should also fix #737 

### 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))
- [x] documentation is changed or added (not relevant IMHO)


Patch:
diff --git a/src/lexers/javascript-lexer.js b/src/lexers/javascript-lexer.js
index 0e6a4d4..8f44c03 100644
--- a/src/lexers/javascript-lexer.js
+++ b/src/lexers/javascript-lexer.js
@@ -14,6 +14,7 @@ export default class JavascriptLexer extends BaseLexer {
     this.attr = options.attr || 'i18nKey'
     this.parseGenerics = options.parseGenerics || false
     this.typeMap = options.typeMap || {}
+    this.translationFunctionsWithArgs = {}
   }
 
   createCommentNodeParser() {
@@ -43,25 +44,49 @@ export default class JavascriptLexer extends BaseLexer {
   }
 
   setNamespaces(keys) {
-    if (this.defaultNamespace) {
-      return keys.map((entry) => ({
-        ...entry,
-        namespace: entry.namespace || this.defaultNamespace,
-      }))
-    }
-
-    return keys
+    return keys.map((entry) => {
+      const namespace =
+        entry.ns ??
+        this.translationFunctionsWithArgs?.[entry.functionName]?.ns ??
+        this.defaultNamespace
+      return namespace
+        ? {
+            ...entry,
+            namespace,
+          }
+        : entry
+    })
   }
 
   setKeyPrefixes(keys) {
-    if (this.keyPrefix) {
-      return keys.map((key) => ({
-        ...key,
-        keyPrefix: this.keyPrefix,
-      }))
-    }
+    return keys.map(({ functionName, ...key }) => {
+      const keyPrefix =
+        this.translationFunctionsWithArgs?.[functionName]?.keyPrefix ??
+        this.keyPrefix
+      return keyPrefix
+        ? {
+            ...key,
+            keyPrefix,
+          }
+        : key
+    })
+  }
 
-    return keys
+  variableDeclarationExtractor(node) {
+    const firstDeconstructedProp = node.name.elements?.[0]
+    if (
+      (firstDeconstructedProp?.propertyName ?? firstDeconstructedProp?.name)
+        ?.escapedText === 't' &&
+      this.functions.includes(firstDeconstructedProp?.name?.escapedText) &&
+      this.namespaceFunctions.includes(node.initializer.expression?.escapedText)
+    ) {
+      this.translationFunctionsWithArgs[
+        firstDeconstructedProp.name.escapedText
+      ] = {
+        pos: node.initializer.pos,
+        storeGlobally: !firstDeconstructedProp.propertyName?.escapedText,
+      }
+    }
   }
 
   extract(content, filename = '__default.js') {
@@ -72,6 +97,9 @@ export default class JavascriptLexer extends BaseLexer {
     const parseTree = (node) => {
       parseCommentNode(keys, node, content)
 
+      if (node.kind === ts.SyntaxKind.VariableDeclaration) {
+        this.variableDeclarationExtractor.call(this, node)
+      }
       if (
         node.kind === ts.SyntaxKind.ArrowFunction ||
         node.kind === ts.SyntaxKind.FunctionDeclaration
@@ -103,7 +131,7 @@ export default class JavascriptLexer extends BaseLexer {
     )
     parseTree(sourceFile)
 
-    return this.setNamespaces(keys)
+    return this.setKeyPrefixes(this.setNamespaces(keys))
   }
 
   /** @param {ts.FunctionLikeDeclaration} node */
@@ -161,12 +189,16 @@ export default class JavascriptLexer extends BaseLexer {
   expressionExtractor(node) {
     const entries = [{}]
 
-    const isNamespaceFunction =
-      this.namespaceFunctions.includes(node.expression.escapedText) ||
-      // Support matching the namespace as well, i.e. match `i18n.useTranslation('ns')`
-      this.namespaceFunctions.includes(this.expressionToName(node.expression))
+    const functionDefinition = Object.entries(
+      this.translationFunctionsWithArgs
+    ).find(([name, translationFunc]) => translationFunc?.pos === node.pos)
+    let storeGlobally = functionDefinition?.[1].storeGlobally ?? true
 
-    if (isNamespaceFunction && node.arguments.length) {
+    if (
+      this.namespaceFunctions.includes(node.expression.escapedText) &&
+      node.arguments.length
+    ) {
+      storeGlobally |= node.expression.escapedText === 'withTranslation'
       const namespaceArgument = node.arguments[0]
       const optionsArgument = node.arguments[1]
       // The namespace argument can be either an array of namespaces or a single namespace,
@@ -193,7 +225,10 @@ export default class JavascriptLexer extends BaseLexer {
         )
       } else if (namespace.kind === ts.SyntaxKind.StringLiteral) {
         // We found a string literal namespace, so we'll use this instead of the default
-        this.defaultNamespace = namespace.text
+        if (storeGlobally) {
+          this.defaultNamespace = namespace.text
+        }
+        entries[0].ns = namespace.text
       }
 
       if (
@@ -204,7 +239,10 @@ export default class JavascriptLexer extends BaseLexer {
           (p) => p.name.escapedText === 'keyPrefix'
         )
         if (keyPrefixNode != null) {
-          this.keyPrefix = keyPrefixNode.initializer.text
+          if (storeGlobally) {
+            this.keyPrefix = keyPrefixNode.initializer.text
+          }
+          entries[0].keyPrefix = keyPrefixNode.initializer.text
         }
       }
     }
@@ -344,10 +382,17 @@ export default class JavascriptLexer extends BaseLexer {
           entries[0].namespace = entries[0].ns[0]
         }
       }
+      entries[0].functionName = node.expression.escapedText
 
       return entries
     }
 
+    const isTranslationFunctionCreation =
+      node.expression.escapedText &&
+      this.namespaceFunctions.includes(node.expression.escapedText)
+    if (isTranslationFunctionCreation) {
+      this.translationFunctionsWithArgs[functionDefinition?.[0]] = entries[0]
+    }
     return null
   }
 
diff --git a/src/lexers/jsx-lexer.js b/src/lexers/jsx-lexer.js
index f4950af..e8eabf7 100644
--- a/src/lexers/jsx-lexer.js
+++ b/src/lexers/jsx-lexer.js
@@ -31,6 +31,9 @@ export default class JsxLexer extends JavascriptLexer {
       parseCommentNode(keys, node, content)
 
       switch (node.kind) {
+        case ts.SyntaxKind.VariableDeclaration:
+          this.variableDeclarationExtractor.call(this, node)
+          break
         case ts.SyntaxKind.CallExpression:
           const entries = this.expressionExtractor.call(this, node)
           if (entries) {


Test patch:
diff --git a/test/lexers/javascript-lexer.test.js b/test/lexers/javascript-lexer.test.js
index 7ee8bf1..d80b1b7 100644
--- a/test/lexers/javascript-lexer.test.js
+++ b/test/lexers/javascript-lexer.test.js
@@ -233,7 +233,7 @@ describe('JavascriptLexer', () => {
     it('extracts the first valid namespace when it is an array', () => {
       const Lexer = new JavascriptLexer()
       const content =
-        'const ExtendedComponent = useTranslation([someVariable, "baz"]); t("bar");'
+        'const {t} = useTranslation([someVariable, "baz"]); t("bar");'
       assert.deepEqual(Lexer.extract(content), [
         { namespace: 'baz', key: 'bar' },
       ])
diff --git a/test/parser.test.js b/test/parser.test.js
index df6e90e..c1ef272 100644
--- a/test/parser.test.js
+++ b/test/parser.test.js
@@ -442,6 +442,50 @@ describe('parser', () => {
     i18nextParser.end(fakeFile)
   })
 
+  it('applies namespaces to the corresponding translation function', (done) => {
+    let result
+    let resultCommon
+    const i18nextParser = new i18nTransform({
+      lexers: {
+        tsx: [
+          {
+            lexer: 'JsxLexer',
+            functions: ['t', 'tCommon', 'tPrefix'],
+          },
+        ],
+      },
+    })
+    const fakeFile = new Vinyl({
+      contents: fs.readFileSync(
+        path.resolve(__dirname, 'templating/multiple-translation-keys.tsx')
+      ),
+      path: 'file.tsx',
+    })
+    const expectedCommon = {
+      foo: '',
+      random: { stuff: '' },
+    }
+    const expected = {
+      bar: '',
+    }
+
+    i18nextParser.on('data', (file) => {
+      if (file.relative.endsWith(path.normalize('en/test.json'))) {
+        result = JSON.parse(file.contents)
+      }
+      if (file.relative.endsWith(path.normalize('en/common.json'))) {
+        resultCommon = JSON.parse(file.contents)
+      }
+    })
+    i18nextParser.on('end', () => {
+      assert.deepEqual(result, expected)
+      assert.deepEqual(resultCommon, expectedCommon)
+      done()
+    })
+
+    i18nextParser.end(fakeFile)
+  })
+
   it('handles escaped single and double quotes', (done) => {
     let result
     const i18nextParser = new i18nTransform()
diff --git a/test/templating/multiple-translation-keys.tsx b/test/templating/multiple-translation-keys.tsx
new file mode 100644
index 0000000..fb6c643
--- /dev/null
+++ b/test/templating/multiple-translation-keys.tsx
@@ -0,0 +1,17 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+
+function TestComponent() {
+  const { t: tCommon } = useTranslation('common')
+  const { t: tPrefix } = useTranslation('common', {keyPrefix: "random"})
+  const { t } = useTranslation('test')
+  return (
+    <>
+      <h1>{t('bar')}</h1>
+      <h2>{tCommon('foo')}</h2>
+      <h3>{tPrefix('stuff')}</h3>
+    </>
+  )
+}
+
+export default TestComponent