Repository: i18next/i18next-parser
Pull request #854 (2023-07-11 14:33:34)

Improve namespace extraction from hook/HOC
### Why am I submitting this PR
While working on localizing our project, we used some code along the lines of the following:
```ts
export default withTranslation(undefined, { withRef: true })(
  ...
);
```

This resulted in the related strings being extracted under the "undefined" namespace, rather than using the default which is expected. Similarly, using a variable causes the strings to be extracted to a namespace that matches the name of that variable, rather than a warning being emitted.

### Does it fix an existing ticket?
No.

### 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))
- [ ] documentation is changed or added



Patch:
diff --git a/src/lexers/javascript-lexer.js b/src/lexers/javascript-lexer.js
index 22740ae..65cf44b 100644
--- a/src/lexers/javascript-lexer.js
+++ b/src/lexers/javascript-lexer.js
@@ -165,28 +165,45 @@ export default class JavascriptLexer extends BaseLexer {
       this.namespaceFunctions.includes(node.expression.escapedText) &&
       node.arguments.length
     ) {
-      const { text, elements } = node.arguments[0]
-
-      // useTranslation
-      if (text) {
-        this.defaultNamespace = text
-        const optionsArgument = node.arguments[1]
-
-        if (
-          optionsArgument &&
-          optionsArgument.kind === ts.SyntaxKind.ObjectLiteralExpression
-        ) {
-          const node = optionsArgument.properties.find(
-            (p) => p.name.escapedText === 'keyPrefix'
-          )
-          if (node != null) {
-            const keyPrefixValue = node.initializer.text
-            this.keyPrefix = keyPrefixValue
-          }
+      const namespaceArgument = node.arguments[0]
+      const optionsArgument = node.arguments[1]
+      // The namespace argument can be either an array of namespaces or a single namespace,
+      // so we convert it to an array in the case of a single namespace so that we can use
+      // the same code in both cases
+      const namespaces = namespaceArgument.elements || [namespaceArgument]
+
+      // Find the first namespace that is a string literal, or is `undefined`. In the case
+      // of `undefined`, we do nothing (see below), leaving the default namespace unchanged
+      const namespace = namespaces.find(
+        (ns) =>
+          ns.kind === ts.SyntaxKind.StringLiteral ||
+          (ns.kind === ts.SyntaxKind.Identifier && ns.text === 'undefined')
+      )
+
+      if (!namespace) {
+        // We know that the namespace argument was provided, so if we're unable to find a
+        // namespace, emit a warning since this will likely cause issues for the user
+        this.emit(
+          'warning',
+          namespaceArgument.kind === ts.SyntaxKind.Identifier
+            ? `Namespace is not a string literal nor an array containing a string literal: ${namespaceArgument.text}`
+            : 'Namespace is not a string literal nor an array containing a string literal'
+        )
+      } 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 (
+        optionsArgument &&
+        optionsArgument.kind === ts.SyntaxKind.ObjectLiteralExpression
+      ) {
+        const keyPrefixNode = optionsArgument.properties.find(
+          (p) => p.name.escapedText === 'keyPrefix'
+        )
+        if (keyPrefixNode != null) {
+          this.keyPrefix = keyPrefixNode.initializer.text
         }
-        // withTranslation
-      } else if (elements && elements.length) {
-        this.defaultNamespace = elements[0].text
       }
     }
 


Test patch:
diff --git a/test/lexers/javascript-lexer.test.js b/test/lexers/javascript-lexer.test.js
index 508ddf4..98b16cd 100644
--- a/test/lexers/javascript-lexer.test.js
+++ b/test/lexers/javascript-lexer.test.js
@@ -217,15 +217,41 @@ describe('JavascriptLexer', () => {
       ])
     })
 
-    it('extracts first namespace when it is an array', () => {
+    it('extracts the first valid namespace when it is an array', () => {
       const Lexer = new JavascriptLexer()
       const content =
-        'const ExtendedComponent = useTranslation(["foo", "baz"]); t("bar");'
+        'const ExtendedComponent = useTranslation([someVariable, "baz"]); t("bar");'
       assert.deepEqual(Lexer.extract(content), [
-        { namespace: 'foo', key: 'bar' },
+        { namespace: 'baz', key: 'bar' },
       ])
     })
 
+    it('emits a `warning` event if the extracted namespace is not a string literal or undefined', (done) => {
+      const Lexer = new JavascriptLexer()
+      const content = 'const {t} = useTranslation(someVariable); t("bar");'
+      Lexer.on('warning', (message) => {
+        assert.equal(
+          message,
+          'Namespace is not a string literal nor an array containing a string literal: someVariable'
+        )
+        done()
+      })
+      assert.deepEqual(Lexer.extract(content), [{ key: 'bar' }])
+    })
+
+    it('leaves the default namespace unchanged if `undefined` is passed', () => {
+      const Lexer = new JavascriptLexer()
+      const content = 'const {t} = useTranslation(undefined); t("bar");'
+      assert.deepEqual(Lexer.extract(content), [{ key: 'bar' }])
+    })
+
+    it('leaves the default namespace unchanged if `undefined` is passed in an array', () => {
+      const Lexer = new JavascriptLexer()
+      const content =
+        'const {t} = useTranslation([someVariable, undefined]); t("bar");'
+      assert.deepEqual(Lexer.extract(content), [{ key: 'bar' }])
+    })
+
     it('uses namespace from t function with priority', () => {
       const Lexer = new JavascriptLexer()
       const content =
@@ -256,15 +282,43 @@ describe('JavascriptLexer', () => {
       ])
     })
 
-    it('extracts first namespace when it is an array', () => {
+    it('extracts first valid namespace when it is an array', () => {
       const Lexer = new JavascriptLexer()
       const content =
-        'const ExtendedComponent = withTranslation(["foo", "baz"])(MyComponent); t("bar");'
+        'const ExtendedComponent = withTranslation([someVariable, "baz"])(MyComponent); t("bar");'
       assert.deepEqual(Lexer.extract(content), [
-        { namespace: 'foo', key: 'bar' },
+        { namespace: 'baz', key: 'bar' },
       ])
     })
 
+    it('emits a `warning` event if the extracted namespace is not a string literal or undefined', (done) => {
+      const Lexer = new JavascriptLexer()
+      const content =
+        'const ExtendedComponent = withTranslation(someVariable)(MyComponent); t("bar");'
+      Lexer.on('warning', (message) => {
+        assert.equal(
+          message,
+          'Namespace is not a string literal nor an array containing a string literal: someVariable'
+        )
+        done()
+      })
+      assert.deepEqual(Lexer.extract(content), [{ key: 'bar' }])
+    })
+
+    it('leaves the default namespace unchanged if `undefined` is passed', () => {
+      const Lexer = new JavascriptLexer()
+      const content =
+        'const ExtendedComponent = withTranslation(undefined)(MyComponent); t("bar");'
+      assert.deepEqual(Lexer.extract(content), [{ key: 'bar' }])
+    })
+
+    it('leaves the default namespace unchanged if `undefined` is passed in an array', () => {
+      const Lexer = new JavascriptLexer()
+      const content =
+        'const ExtendedComponent = withTranslation([someVariable, undefined])(MyComponent); t("bar");'
+      assert.deepEqual(Lexer.extract(content), [{ key: 'bar' }])
+    })
+
     it('uses namespace from t function with priority', () => {
       const Lexer = new JavascriptLexer()
       const content =