Repository: i18next/i18next-parser
Pull request #194 (2019-12-17 15:59:38)

add vuejs component lexer
vuejs [have ](https://vuejs.org/v2/guide/syntax.html) template part in it's *.vue components.
this PRs add parsing vue component format. tests included ; )

thanx

vincaslt: I think `dist` build should be generated, no?

Patch:
diff --git a/dist/lexers/javascript-lexer.js b/dist/lexers/javascript-lexer.js
index c971dbb..4f10526 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 _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 _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 _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);
@@ -74,9 +74,23 @@ JavascriptLexer = function (_BaseLexer) {_inherits(JavascriptLexer, _BaseLexer);
             }} catch (err) {_didIteratorError = true;_iteratorError = err;} finally {try {if (!_iteratorNormalCompletion && _iterator.return) {_iterator.return();}} finally {if (_didIteratorError) {throw _iteratorError;}}}
         }
 
+        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];
+          }
+        } else if (this.defaultNamespace) {
+          entry.namespace = this.defaultNamespace;
+        }
+
         return entry;
       }
 
+      if (node.expression.escapedText === 'useTranslation' && node.arguments.length) {
+        this.defaultNamespace = node.arguments[0].text;
+      }
+
       return null;
     } }, { key: 'concatenateString', value: function concatenateString(
 
diff --git a/dist/lexers/jsx-lexer.js b/dist/lexers/jsx-lexer.js
index bae6e86..4658e3c 100644
--- a/dist/lexers/jsx-lexer.js
+++ b/dist/lexers/jsx-lexer.js
@@ -43,11 +43,13 @@ JsxLexer = function (_JavascriptLexer) {_inherits(JsxLexer, _JavascriptLexer);
     node, sourceText) {var _this3 = this;
       var tagNode = node.openingElement || node;
 
-      var getKey = function getKey(node) {
-        var attribute = node.attributes.properties.find(function (attr) {return attr.name.text === _this3.attr;});
+      var getPropValue = function getPropValue(node, tagName) {
+        var attribute = node.attributes.properties.find(function (attr) {return attr.name.text === tagName;});
         return attribute && attribute.initializer.text;
       };
 
+      var getKey = function getKey(node) {return getPropValue(node, _this3.attr);};
+
       if (tagNode.tagName.text === "Trans") {
         var entry = {};
         entry.key = getKey(tagNode);
@@ -62,6 +64,11 @@ JsxLexer = function (_JavascriptLexer) {_inherits(JsxLexer, _JavascriptLexer);
           }
         }
 
+        var namespace = getPropValue(tagNode, 'ns');
+        if (namespace) {
+          entry.namespace = namespace;
+        }
+
         return entry.key ? entry : null;
       } else
       if (tagNode.tagName.text === "Interpolate") {
diff --git a/package.json b/package.json
index 1ff4d79..318e444 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "author": "Karel Ledru",
   "description": "Command Line tool for i18next",
   "name": "i18next-parser",
-  "version": "1.0.0-beta37",
+  "version": "1.0.0-beta38",
   "license": "MIT",
   "main": "dist/index.js",
   "bin": {
@@ -31,6 +31,7 @@
     "typescript": "^3.6.4",
     "vinyl": "~2.0.1",
     "vinyl-fs": "^3.0.2",
+    "vue-template-compiler": "^2.6.11",
     "yamljs": "^0.3.0"
   },
   "optionalDependencies": {},
diff --git a/src/lexers/javascript-lexer.js b/src/lexers/javascript-lexer.js
index 5e6b7e8..9177f7d 100644
--- a/src/lexers/javascript-lexer.js
+++ b/src/lexers/javascript-lexer.js
@@ -74,9 +74,23 @@ export default class JavascriptLexer extends BaseLexer {
         }
       }
 
+      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]
+        }
+      } else if (this.defaultNamespace) {
+        entry.namespace = this.defaultNamespace
+      }
+
       return entry
     }
 
+    if(node.expression.escapedText === 'useTranslation' && node.arguments.length) {
+      this.defaultNamespace = node.arguments[0].text
+    }
+
     return null
   }
 
diff --git a/src/lexers/jsx-lexer.js b/src/lexers/jsx-lexer.js
index 0ea0658..66671ed 100644
--- a/src/lexers/jsx-lexer.js
+++ b/src/lexers/jsx-lexer.js
@@ -43,11 +43,13 @@ export default class JsxLexer extends JavascriptLexer {
   jsxExtractor(node, sourceText) {
     const tagNode = node.openingElement || node
 
-    const getKey = (node) => {
-      const attribute = node.attributes.properties.find(attr => attr.name.text === this.attr)
+    const getPropValue = (node, tagName) => {
+      const attribute = node.attributes.properties.find(attr => attr.name.text === tagName)
       return attribute && attribute.initializer.text
     }
 
+    const getKey = (node) => getPropValue(node, this.attr)
+
     if (tagNode.tagName.text === "Trans") {
       const entry = {}
       entry.key = getKey(tagNode)
@@ -62,6 +64,11 @@ export default class JsxLexer extends JavascriptLexer {
         }
       }
 
+      const namespace = getPropValue(tagNode, 'ns')
+      if (namespace) {
+        entry.namespace = namespace
+      }
+
       return entry.key ? entry : null
     }
     else if (tagNode.tagName.text === "Interpolate") {
diff --git a/src/lexers/vue-lexer.js b/src/lexers/vue-lexer.js
new file mode 100644
index 0000000..de682ec
--- /dev/null
+++ b/src/lexers/vue-lexer.js
@@ -0,0 +1,25 @@
+import BaseLexer from './base-lexer'
+import JavascriptLexer from './javascript-lexer.js'
+
+export default class VueLexer extends BaseLexer {
+  constructor(options = {}) {
+    super(options)
+
+    this.functions = options.functions || ['$t']
+  }
+
+  extract(content, filename) {
+    let keys = []
+
+    const Lexer = new JavascriptLexer()
+    Lexer.on('warning', warning => this.emit('warning', warning))
+    keys = keys.concat(Lexer.extract(content))
+
+    const compiledTemplate = require('vue-template-compiler').compile(content).render
+    const Lexer2 = new JavascriptLexer({ functions: this.functions })
+    Lexer2.on('warning', warning => this.emit('warning', warning))
+    keys = keys.concat(Lexer2.extract(compiledTemplate))
+
+    return keys
+  }
+}
diff --git a/src/parser.js b/src/parser.js
index 4ac79cb..736fdb9 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -4,6 +4,7 @@ import HandlebarsLexer from './lexers/handlebars-lexer'
 import HTMLLexer from './lexers/html-lexer'
 import JavascriptLexer from './lexers/javascript-lexer'
 import JsxLexer from './lexers/jsx-lexer'
+import VueLexer from './lexers/vue-lexer'
 
 const lexers = {
   hbs: ['HandlebarsLexer'],
@@ -18,6 +19,8 @@ const lexers = {
   jsx: ['JsxLexer'],
   tsx: ['JsxLexer'],
 
+  vue: ['VueLexer'],
+
   default: ['JavascriptLexer']
 }
 
@@ -25,7 +28,8 @@ const lexersMap = {
   HandlebarsLexer,
   HTMLLexer,
   JavascriptLexer,
-  JsxLexer
+  JsxLexer,
+  VueLexer
 }
 
 export default class Parser extends EventEmitter {
diff --git a/yarn.lock b/yarn.lock
index 53df72e..b14c6ae 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1293,6 +1293,11 @@ d@1, d@^1.0.1:
     es5-ext "^0.10.50"
     type "^1.0.1"
 
+de-indent@^1.0.2:
+  version "1.0.2"
+  resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
+  integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=
+
 debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8, debug@^2.6.9:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@@ -2173,6 +2178,11 @@ he@1.1.1:
   resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
   integrity sha1-k0EP0hsAlzUVH4howvJx80J+I/0=
 
+he@^1.1.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
+  integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
+
 heimdalljs-logger@^0.1.7:
   version "0.1.10"
   resolved "https://registry.yarnpkg.com/heimdalljs-logger/-/heimdalljs-logger-0.1.10.tgz#90cad58aabb1590a3c7e640ddc6a4cd3a43faaf7"
@@ -4245,6 +4255,14 @@ vinyl@~2.0.1:
     remove-trailing-separator "^1.0.1"
     replace-ext "^1.0.0"
 
+vue-template-compiler@^2.6.11:
+  version "2.6.11"
+  resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.11.tgz#c04704ef8f498b153130018993e56309d4698080"
+  integrity sha512-KIq15bvQDrcCjpGjrAhx4mUlyyHfdmTaoNfeoATHLAiWB+MU3cx4lOzMwrnUh9cCxy0Lt1T11hAFY6TQgroUAA==
+  dependencies:
+    de-indent "^1.0.2"
+    he "^1.1.0"
+
 walk-sync@^0.3.1:
   version "0.3.4"
   resolved "https://registry.yarnpkg.com/walk-sync/-/walk-sync-0.3.4.tgz#cf78486cc567d3a96b5b2237c6108017a5ffb9a4"


Test patch:
diff --git a/test/lexers/javascript-lexer.test.js b/test/lexers/javascript-lexer.test.js
index 3e00f8b..bbb7ebf 100644
--- a/test/lexers/javascript-lexer.test.js
+++ b/test/lexers/javascript-lexer.test.js
@@ -123,4 +123,17 @@ describe('JavascriptLexer', () => {
     const content = 'i18n.t("first") as potato'
     assert.deepEqual(Lexer.extract(content), [{ key: 'first' }])
   })
+
+  it('extracts default namespace from useTranslation hook', () => {
+    const Lexer = new JavascriptLexer()
+    const content = 'const {t} = useTranslation("foo"); t("bar");'
+    assert.deepEqual(Lexer.extract(content), [{ namespace: 'foo', key: 'bar' }])
+  })
+
+  it('uses namespace from t function with priority', () => {
+    const Lexer = new JavascriptLexer()
+    const content = 'const {t} = useTranslation("foo"); t("bar", {ns: "baz"});'
+    assert.deepEqual(Lexer.extract(content), [{ namespace: 'baz', key: 'bar', ns: 'baz' }])
+  })
+
 })
diff --git a/test/lexers/jsx-lexer.test.js b/test/lexers/jsx-lexer.test.js
index 4e1dc6b..dd4dea7 100644
--- a/test/lexers/jsx-lexer.test.js
+++ b/test/lexers/jsx-lexer.test.js
@@ -119,6 +119,14 @@ describe('JsxLexer', () => {
       assert.equal(Lexer.extract(content)[0].defaultValue, 'Some Interpolated Content')
       done()
     })
+
+    it('uses the ns (namespace) prop', (done) => {
+      const Lexer = new JsxLexer()
+      const content = `<Trans ns="foo">bar</Trans>`
+      assert.deepEqual(Lexer.extract(content), [{ key: 'bar', defaultValue: 'bar', namespace: 'foo' }])
+      done()
+    })
+
   })
 
   describe('supports TypeScript', () => {
diff --git a/test/lexers/vue-lexer.test.js b/test/lexers/vue-lexer.test.js
new file mode 100644
index 0000000..9681f1e
--- /dev/null
+++ b/test/lexers/vue-lexer.test.js
@@ -0,0 +1,44 @@
+import { assert } from 'chai'
+import VueLexer from '../../src/lexers/vue-lexer'
+
+describe('VueLexer', () => {
+  it('extracts keys from template & js', (done) => {
+    const Lexer = new VueLexer()
+    const content =
+      "<template><p>{{ $t('first') }}</p><template><script>export default " +
+      "{ mounted() { this.$i18n.t('second'); } }</script>"
+    assert.deepEqual(Lexer.extract(content), [{ key: 'second' },{ key: 'first' }])
+    done()
+  })
+
+  it('extracts keys with interpolation from template & js', (done) => {
+    const Lexer = new VueLexer()
+    const content =
+      "<template><p>{{ $t('first {test}', {test: 'station'}) }}</p><template>" +
+      "<script>export default { mounted() { this.$i18n.t('second {test}', " +
+      "{test: 'interpol'}); } }</script>"
+    assert.deepEqual(Lexer.extract(content), [{
+      key: 'second {test}',
+      test: 'interpol'
+    }, {
+      key: 'first {test}',
+      test: 'station'
+    }])
+    done()
+  })
+
+  it('extracts keys with plural from template & js', (done) => {
+    const Lexer = new VueLexer()
+    const content =
+      "<template><p>{{ $t('first', {count: 5}) }}</p><template><script>export default " +
+      "{ mounted() { this.$i18n.t('second', {count: 2}); } }</script>"
+    assert.deepEqual(Lexer.extract(content), [{
+      key: 'second',
+      count: '2'
+    }, {
+      key: 'first',
+      count: '5'
+    }])
+    done()
+  })
+})