Repository: i18next/i18next-parser
Pull request #233 (2020-07-28 14:37:50)

Fix namespace parsing
Addresses the same issue as this PR: #232 (issue #161) only in a different way.

Lexers already kinda extracted namespaces if they found function calls to `withTranslation` or `useTranslation`, but failed to work correctly because the call order mattered. `transform.js` then made a second pass and extracted namespaces using regex, which was outdated and the original PR addressed that.

However, I'd say that lexers should be used to get the namespace. I fix them by doing a second pass on extracted keys to set the namespaces with the one that lexer found during extraction. `transform.js` then only sets the namespaces with default if there was no other namespace identified by lexer.

This would allow to completely remove `reactNamespace` @karellm : https://github.com/i18next/i18next-parser/issues/161#issuecomment-624729169

I didn't date to do it with this PR as it's a breaking change it seems - react `.js` files will stop being extracted without explicit lexer override, but it essentially serves no other purpose now: 
```
lexers: {
  js: ['JsxLexer']
}
```  


Patch:
diff --git a/dist/lexers/javascript-lexer.js b/dist/lexers/javascript-lexer.js
index b03f4b3..3360877 100644
--- a/dist/lexers/javascript-lexer.js
+++ b/dist/lexers/javascript-lexer.js
@@ -1,4 +1,4 @@
-'use strict';Object.defineProperty(exports, "__esModule", { value: true });var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {return typeof obj;} : function (obj) {return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;};var _createClass = function () {function defineProperties(target, props) {for (var i = 0; i < props.length; i++) {var descriptor = props[i];descriptor.enumerable = descriptor.enumerable || false;descriptor.configurable = true;if ("value" in descriptor) descriptor.writable = true;Object.defineProperty(target, descriptor.key, descriptor);}}return function (Constructor, protoProps, staticProps) {if (protoProps) defineProperties(Constructor.prototype, protoProps);if (staticProps) defineProperties(Constructor, staticProps);return Constructor;};}();var _baseLexer = require('./base-lexer');var _baseLexer2 = _interopRequireDefault(_baseLexer);
+'use strict';Object.defineProperty(exports, "__esModule", { value: true });var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {return typeof obj;} : function (obj) {return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;};var _extends = Object.assign || function (target) {for (var i = 1; i < arguments.length; i++) {var source = arguments[i];for (var key in source) {if (Object.prototype.hasOwnProperty.call(source, key)) {target[key] = source[key];}}}return target;};var _createClass = function () {function defineProperties(target, props) {for (var i = 0; i < props.length; i++) {var descriptor = props[i];descriptor.enumerable = descriptor.enumerable || false;descriptor.configurable = true;if ("value" in descriptor) descriptor.writable = true;Object.defineProperty(target, descriptor.key, descriptor);}}return function (Constructor, protoProps, staticProps) {if (protoProps) defineProperties(Constructor.prototype, protoProps);if (staticProps) defineProperties(Constructor, staticProps);return Constructor;};}();var _baseLexer = require('./base-lexer');var _baseLexer2 = _interopRequireDefault(_baseLexer);
 var _typescript = require('typescript');var ts = _interopRequireWildcard(_typescript);function _interopRequireWildcard(obj) {if (obj && obj.__esModule) {return obj;} else {var newObj = {};if (obj != null) {for (var key in obj) {if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key];}}newObj.default = obj;return newObj;}}function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj };}function _toConsumableArray(arr) {if (Array.isArray(arr)) {for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) {arr2[i] = arr[i];}return arr2;} else {return Array.from(arr);}}function _classCallCheck(instance, Constructor) {if (!(instance instanceof Constructor)) {throw new TypeError("Cannot call a class as a function");}}function _possibleConstructorReturn(self, call) {if (!self) {throw new ReferenceError("this hasn't been initialised - super() hasn't been called");}return call && (typeof call === "object" || typeof call === "function") ? call : self;}function _inherits(subClass, superClass) {if (typeof superClass !== "function" && superClass !== null) {throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);}subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;}var
 
 JavascriptLexer = function (_BaseLexer) {_inherits(JavascriptLexer, _BaseLexer);
@@ -34,9 +34,20 @@ JavascriptLexer = function (_BaseLexer) {_inherits(JavascriptLexer, _BaseLexer);
         });
 
       };
+    } }, { key: 'setNamespaces', value: function setNamespaces(
+
+    keys) {var _this3 = this;
+      if (this.defaultNamespace) {
+        return keys.map(function (entry) {return _extends({},
+          entry, {
+            namespace: entry.namespace || _this3.defaultNamespace });});
+
+      }
+
+      return keys;
     } }, { key: 'extract', value: function extract(
 
-    content) {var _this3 = this;var filename = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '__default.js';
+    content) {var _this4 = this;var filename = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '__default.js';
       var keys = [];
 
       var parseCommentNode = this.createCommentNodeParser();
@@ -47,7 +58,7 @@ JavascriptLexer = function (_BaseLexer) {_inherits(JavascriptLexer, _BaseLexer);
         parseCommentNode(keys, node, content);
 
         if (node.kind === ts.SyntaxKind.CallExpression) {
-          entry = _this3.expressionExtractor.call(_this3, node);
+          entry = _this4.expressionExtractor.call(_this4, node);
         }
 
         if (entry) {
@@ -64,12 +75,31 @@ JavascriptLexer = function (_BaseLexer) {_inherits(JavascriptLexer, _BaseLexer);
 
       parseTree(sourceFile);
 
-      return keys;
+      return this.setNamespaces(keys);
     } }, { key: 'expressionExtractor', value: function expressionExtractor(
 
     node) {
       var entry = {};
 
+      if (
+      node.expression.escapedText === 'useTranslation' &&
+      node.arguments.length)
+      {
+        this.defaultNamespace = node.arguments[0].text;
+      }
+
+      if (
+      node.expression.escapedText === 'withTranslation' &&
+      node.arguments.length)
+      {var _node$arguments$ =
+        node.arguments[0],text = _node$arguments$.text,elements = _node$arguments$.elements;
+        if (text) {
+          this.defaultNamespace = text;
+        } else if (elements && elements.length) {
+          this.defaultNamespace = elements[0].text;
+        }
+      }
+
       var isTranslationFunction =
       node.expression.text && this.functions.includes(node.expression.text) ||
       node.expression.name &&
@@ -129,36 +159,15 @@ JavascriptLexer = function (_BaseLexer) {_inherits(JavascriptLexer, _BaseLexer);
           } else if (_typeof(entry.ns) === 'object' && entry.ns.length) {
             entry.namespace = entry.ns[0];
           }
-        } else if (this.defaultNamespace) {
-          entry.namespace = this.defaultNamespace;
         }
 
         return entry;
       }
 
-      if (
-      node.expression.escapedText === 'useTranslation' &&
-      node.arguments.length)
-      {
-        this.defaultNamespace = node.arguments[0].text;
-      }
-
-      if (
-      node.expression.escapedText === 'withTranslation' &&
-      node.arguments.length)
-      {var _node$arguments$ =
-        node.arguments[0],text = _node$arguments$.text,elements = _node$arguments$.elements;
-        if (text) {
-          this.defaultNamespace = text;
-        } else if (elements && elements.length) {
-          this.defaultNamespace = elements[0].text;
-        }
-      }
-
       return null;
     } }, { key: 'commentExtractor', value: function commentExtractor(
 
-    commentText) {var _this4 = this;
+    commentText) {var _this5 = this;
       var regexp = new RegExp(this.callPattern, 'g');
       var expressions = commentText.match(regexp);
 
@@ -168,7 +177,7 @@ JavascriptLexer = function (_BaseLexer) {_inherits(JavascriptLexer, _BaseLexer);
 
       var keys = [];
       expressions.forEach(function (expression) {
-        var expressionKeys = _this4.extract(expression);
+        var expressionKeys = _this5.extract(expression);
         if (expressionKeys) {
           keys.push.apply(keys, _toConsumableArray(expressionKeys));
         }
diff --git a/dist/lexers/jsx-lexer.js b/dist/lexers/jsx-lexer.js
index 764c105..15c7b93 100644
--- a/dist/lexers/jsx-lexer.js
+++ b/dist/lexers/jsx-lexer.js
@@ -51,7 +51,7 @@ JsxLexer = function (_JavascriptLexer) {_inherits(JsxLexer, _JavascriptLexer);
 
       parseTree(sourceFile);
 
-      return keys;
+      return this.setNamespaces(keys);
     } }, { key: 'jsxExtractor', value: function jsxExtractor(
 
     node, sourceText) {var _this3 = this;
diff --git a/dist/transform.js b/dist/transform.js
index 4ae5c32..696d034 100644
--- a/dist/transform.js
+++ b/dist/transform.js
@@ -6,7 +6,6 @@ var _parser = require('./parser');var _parser2 = _interopRequireDefault(_parser)
 var _path = require('path');var _path2 = _interopRequireDefault(_path);
 var _vinyl = require('vinyl');var _vinyl2 = _interopRequireDefault(_vinyl);
 var _yamljs = require('yamljs');var _yamljs2 = _interopRequireDefault(_yamljs);
-var _baseLexer = require('./lexers/base-lexer');var _baseLexer2 = _interopRequireDefault(_baseLexer);
 var _i18next = require('i18next');var _i18next2 = _interopRequireDefault(_i18next);function _interopRequireDefault(obj) {return obj && obj.__esModule ? obj : { default: obj };}function _classCallCheck(instance, Constructor) {if (!(instance instanceof Constructor)) {throw new TypeError("Cannot call a class as a function");}}function _possibleConstructorReturn(self, call) {if (!self) {throw new ReferenceError("this hasn't been initialised - super() hasn't been called");}return call && (typeof call === "object" || typeof call === "function") ? call : self;}function _inherits(subClass, superClass) {if (typeof superClass !== "function" && superClass !== null) {throw new TypeError("Super expression must either be null or a function, not " + typeof superClass);}subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } });if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;}
 
 function getPluralSuffix(numberOfPluralForms, nthForm) {
@@ -91,8 +90,7 @@ i18nTransform = function (_Transform) {_inherits(i18nTransform, _Transform);
       }
 
       var filename = _path2.default.basename(file.path);
-      var entries = this.parser.parse(content, filename);
-      var extension = _path2.default.extname(filename).substr(1);var _iteratorNormalCompletion = true;var _didIteratorError = false;var _iteratorError = undefined;try {
+      var entries = this.parser.parse(content, filename);var _iteratorNormalCompletion = true;var _didIteratorError = false;var _iteratorError = undefined;try {
 
         for (var _iterator = entries[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {var entry = _step.value;
           var key = entry.key;
@@ -101,8 +99,6 @@ i18nTransform = function (_Transform) {_inherits(i18nTransform, _Transform);
           // make sure we're not pulling a 'namespace' out of a default value
           if (parts.length > 1 && key !== entry.defaultValue) {
             entry.namespace = parts.shift();
-          } else if (extension === 'jsx' || this.options.reactNamespace) {
-            entry.namespace = this.grabReactNamespace(content);
           }
           entry.namespace = entry.namespace || this.options.defaultNamespace;
 
@@ -295,14 +291,4 @@ i18nTransform = function (_Transform) {_inherits(i18nTransform, _Transform);
         contents: Buffer.from(text) });
 
       this.push(file);
-    } }, { key: 'grabReactNamespace', value: function grabReactNamespace(
-
-    content) {
-      var reactTranslateRegex = new RegExp(
-      'translate\\((?:\\s*\\[?\\s*)(' + _baseLexer2.default.stringPattern + ')');
-
-      var translateMatches = content.match(reactTranslateRegex);
-      if (translateMatches) {
-        return translateMatches[1].slice(1, -1);
-      }
     } }]);return i18nTransform;}(_stream.Transform);exports.default = i18nTransform;module.exports = exports['default'];
\ No newline at end of file
diff --git a/src/lexers/javascript-lexer.js b/src/lexers/javascript-lexer.js
index 1a7595a..740fc26 100644
--- a/src/lexers/javascript-lexer.js
+++ b/src/lexers/javascript-lexer.js
@@ -36,6 +36,17 @@ export default class JavascriptLexer extends BaseLexer {
     }
   }
 
+  setNamespaces(keys) {
+    if (this.defaultNamespace) {
+      return keys.map((entry) => ({
+        ...entry,
+        namespace: entry.namespace || this.defaultNamespace,
+      }))
+    }
+
+    return keys
+  }
+
   extract(content, filename = '__default.js') {
     const keys = []
 
@@ -64,12 +75,31 @@ export default class JavascriptLexer extends BaseLexer {
     )
     parseTree(sourceFile)
 
-    return keys
+    return this.setNamespaces(keys)
   }
 
   expressionExtractor(node) {
     const entry = {}
 
+    if (
+      node.expression.escapedText === 'useTranslation' &&
+      node.arguments.length
+    ) {
+      this.defaultNamespace = node.arguments[0].text
+    }
+
+    if (
+      node.expression.escapedText === 'withTranslation' &&
+      node.arguments.length
+    ) {
+      const { text, elements } = node.arguments[0]
+      if (text) {
+        this.defaultNamespace = text
+      } else if (elements && elements.length) {
+        this.defaultNamespace = elements[0].text
+      }
+    }
+
     const isTranslationFunction =
       (node.expression.text && this.functions.includes(node.expression.text)) ||
       (node.expression.name &&
@@ -129,32 +159,11 @@ export default class JavascriptLexer extends BaseLexer {
         } else if (typeof entry.ns === 'object' && entry.ns.length) {
           entry.namespace = entry.ns[0]
         }
-      } else if (this.defaultNamespace) {
-        entry.namespace = this.defaultNamespace
       }
 
       return entry
     }
 
-    if (
-      node.expression.escapedText === 'useTranslation' &&
-      node.arguments.length
-    ) {
-      this.defaultNamespace = node.arguments[0].text
-    }
-
-    if (
-      node.expression.escapedText === 'withTranslation' &&
-      node.arguments.length
-    ) {
-      const { text, elements } = node.arguments[0]
-      if (text) {
-        this.defaultNamespace = text
-      } else if (elements && elements.length) {
-        this.defaultNamespace = elements[0].text
-      }
-    }
-
     return null
   }
 
diff --git a/src/lexers/jsx-lexer.js b/src/lexers/jsx-lexer.js
index 62bedf9..a535211 100644
--- a/src/lexers/jsx-lexer.js
+++ b/src/lexers/jsx-lexer.js
@@ -51,7 +51,7 @@ export default class JsxLexer extends JavascriptLexer {
     )
     parseTree(sourceFile)
 
-    return keys
+    return this.setNamespaces(keys)
   }
 
   jsxExtractor(node, sourceText) {
diff --git a/src/transform.js b/src/transform.js
index e48eea9..d042d93 100644
--- a/src/transform.js
+++ b/src/transform.js
@@ -6,7 +6,6 @@ import Parser from './parser'
 import path from 'path'
 import VirtualFile from 'vinyl'
 import YAML from 'yamljs'
-import BaseLexer from './lexers/base-lexer'
 import i18next from 'i18next'
 
 function getPluralSuffix(numberOfPluralForms, nthForm) {
@@ -92,7 +91,6 @@ export default class i18nTransform extends Transform {
 
     const filename = path.basename(file.path)
     const entries = this.parser.parse(content, filename)
-    const extension = path.extname(filename).substr(1)
 
     for (const entry of entries) {
       let key = entry.key
@@ -101,8 +99,6 @@ export default class i18nTransform extends Transform {
       // make sure we're not pulling a 'namespace' out of a default value
       if (parts.length > 1 && key !== entry.defaultValue) {
         entry.namespace = parts.shift()
-      } else if (extension === 'jsx' || this.options.reactNamespace) {
-        entry.namespace = this.grabReactNamespace(content)
       }
       entry.namespace = entry.namespace || this.options.defaultNamespace
 
@@ -296,14 +292,4 @@ export default class i18nTransform extends Transform {
     })
     this.push(file)
   }
-
-  grabReactNamespace(content) {
-    const reactTranslateRegex = new RegExp(
-      'translate\\((?:\\s*\\[?\\s*)(' + BaseLexer.stringPattern + ')'
-    )
-    const translateMatches = content.match(reactTranslateRegex)
-    if (translateMatches) {
-      return translateMatches[1].slice(1, -1)
-    }
-  }
 }


Test patch:
diff --git a/test/parser.test.js b/test/parser.test.js
index 70c7400..375b9ca 100644
--- a/test/parser.test.js
+++ b/test/parser.test.js
@@ -240,7 +240,7 @@ describe('parser', () => {
     }
 
     i18nextParser.on('data', (file) => {
-      if (file.relative.endsWith(enLibraryPath)) {
+      if (file.relative.endsWith(path.normalize('en/react.json'))) {
         result = JSON.parse(file.contents)
       }
     })
@@ -291,6 +291,60 @@ describe('parser', () => {
     i18nextParser.end(fakeFile)
   })
 
+  it('applies withTranslation namespace globally', (done) => {
+    let result
+    const i18nextParser = new i18nTransform()
+    const fakeFile = new Vinyl({
+      contents: fs.readFileSync(
+        path.resolve(__dirname, 'templating/namespace-hoc.jsx')
+      ),
+      path: 'functional.jsx',
+    })
+    const expected = {
+      'test-1': '',
+      'test-2': '',
+    }
+
+    i18nextParser.on('data', (file) => {
+      if (file.relative.endsWith(path.normalize('en/test-namespace.json'))) {
+        result = JSON.parse(file.contents)
+      }
+    })
+    i18nextParser.on('end', () => {
+      assert.deepEqual(result, expected)
+      done()
+    })
+
+    i18nextParser.end(fakeFile)
+  })
+
+  it('applies useTranslation namespace globally', (done) => {
+    let result
+    const i18nextParser = new i18nTransform()
+    const fakeFile = new Vinyl({
+      contents: fs.readFileSync(
+        path.resolve(__dirname, 'templating/namespace-hook.jsx')
+      ),
+      path: 'functional.jsx',
+    })
+    const expected = {
+      'test-1': '',
+      'test-2': '',
+    }
+
+    i18nextParser.on('data', (file) => {
+      if (file.relative.endsWith(path.normalize('en/test-namespace.json'))) {
+        result = JSON.parse(file.contents)
+      }
+    })
+    i18nextParser.on('end', () => {
+      assert.deepEqual(result, expected)
+      done()
+    })
+
+    i18nextParser.end(fakeFile)
+  })
+
   it('handles escaped single and double quotes', (done) => {
     let result
     const i18nextParser = new i18nTransform()
diff --git a/test/templating/namespace-hoc.jsx b/test/templating/namespace-hoc.jsx
new file mode 100644
index 0000000..f7fbb23
--- /dev/null
+++ b/test/templating/namespace-hoc.jsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import { withTranslation, Trans } from 'react-i18next'
+
+// This will have test-namespace even though it comes before withTranslation during parsing
+const Component = () => {
+  return <Trans i18nKey="test-1"></Trans>
+}
+
+function TestComponent({ t, i18n }) {
+  return (
+    <>
+      <Component />
+      <h1>{t('test-2')}</h1>
+    </>
+  )
+}
+
+export default withTranslation('test-namespace')(TestComponent)
diff --git a/test/templating/namespace-hook.jsx b/test/templating/namespace-hook.jsx
new file mode 100644
index 0000000..4d18609
--- /dev/null
+++ b/test/templating/namespace-hook.jsx
@@ -0,0 +1,19 @@
+import React from 'react'
+import { useTranslation, Trans } from 'react-i18next'
+
+// This will have test-namespace even though it comes before useTranslation during parsing
+const Component = () => {
+  return <Trans i18nKey="test-1"></Trans>
+}
+
+function TestComponent() {
+  const { t } = useTranslation('test-namespace')
+  return (
+    <>
+      <Component />
+      <h1>{t('test-2')}</h1>
+    </>
+  )
+}
+
+export default TestComponent
diff --git a/test/templating/react.jsx b/test/templating/react.jsx
index 61c3ffc..803c15f 100644
--- a/test/templating/react.jsx
+++ b/test/templating/react.jsx
@@ -1,6 +1,7 @@
 import React from 'react'
-import { translate, Trans, Interpolate } from 'react-i18next'
+import { withTranslation, Trans, Interpolate } from 'react-i18next'
 
+// These will have namespace "react" even though it comes before withtranslation during parsing
 const bar = () => (
   <div>
     <span><Trans i18nKey="bar"></Trans></span>
@@ -42,4 +43,4 @@ class Test extends React.Component {
   }
 }
 
-export default translate('react')(Test)
+export default withTranslation('react')(Test)
diff --git a/test/templating/typescript.tsx b/test/templating/typescript.tsx
index ca0bfd3..f44363f 100644
--- a/test/templating/typescript.tsx
+++ b/test/templating/typescript.tsx
@@ -1,6 +1,8 @@
 import React from 'react'
-import { translate, Trans, Interpolate } from 'react-i18next'
+import { withTranslation, Trans, Interpolate } from 'react-i18next'
 
+
+// These will have namespace "react" even though it comes before withtranslation during parsing
 const bar = (): React.ReactElement<any> => (
   <div>
     <span><Trans i18nKey="bar"></Trans></span>
@@ -42,4 +44,4 @@ class Test extends React.Component<{ t: any }, {}> {
   }
 }
 
-export default translate('react')(Test)
+export default withTranslation('react')(Test)