Repository: i18next/i18next-parser
Pull request #362 (2021-06-22 13:22:46)

Support unicode escape sequence in json
### Why am I submitting this PR

When using unicode escape sequences, the parser converts them to utf8. This PR should allow to preserve them.

Be aware that this PR will also convert all non-ASCII characters to its unicode escape sequences as well. Personally, I find it best to make these characters explicit, but let me know if you'd like to see this behavior as an option.
Moreover, note that the case is not preserved inside the unicode escape sequences, it will always be lowercased (`\u00A0` will become `\u00a0`).


### Does it fix an existing ticket?

Yes #361 

### Checklist

- [x] only relevant code is changed (make a diff before you submit the PR)
- [x] tests are included and pass (see [details here](https://github.com/i18next/i18next-parser/blob/master/docs/development.md#tests))
- [ ] documentation is changed or added
- [x] I ran `yarn build` to compile and prettify the code



Patch:
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 4140e53..079782e 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -13,7 +13,7 @@ jobs:
     steps:
     - uses: actions/checkout@v2
     - name: Use Node.js ${{ matrix.node-version }}
-      uses: actions/setup-node@v2.1.5
+      uses: actions/setup-node@v2.3.0
       with:
         node-version: ${{ matrix.node-version }}
     - name: Cache Node.js modules
@@ -27,7 +27,7 @@ jobs:
     - run: npm install
     - run: npm run build
     - run: npm run coverage
-    - uses: codecov/codecov-action@v1
+    - uses: codecov/codecov-action@v2.0.2
       with: 
         name: node-${{ matrix.node-version }}
         fail_ci_if_error: false
diff --git a/dist/lexers/javascript-lexer.js b/dist/lexers/javascript-lexer.js
index 4c98bf0..919f9c9 100644
--- a/dist/lexers/javascript-lexer.js
+++ b/dist/lexers/javascript-lexer.js
@@ -57,6 +57,10 @@ JavascriptLexer = /*#__PURE__*/function (_BaseLexer) {(0, _inherits2["default"])
 
         parseCommentNode(keys, node, content);
 
+        if (node.kind === ts.SyntaxKind.TaggedTemplateExpression) {
+          entry = _this4.taggedTemplateExpressionExtractor.call(_this4, node);
+        }
+
         if (node.kind === ts.SyntaxKind.CallExpression) {
           entry = _this4.expressionExtractor.call(_this4, node);
         }
@@ -76,6 +80,30 @@ JavascriptLexer = /*#__PURE__*/function (_BaseLexer) {(0, _inherits2["default"])
       parseTree(sourceFile);
 
       return this.setNamespaces(keys);
+    } }, { key: "taggedTemplateExpressionExtractor", value:
+
+    function taggedTemplateExpressionExtractor(node) {
+      var entry = {};
+
+      var tag = node.tag,template = node.template;
+
+      var isTranslationFunction =
+      tag.text && this.functions.includes(tag.text) ||
+      tag.name && this.functions.includes(tag.name.text);
+
+      if (!isTranslationFunction) return null;
+
+      if (template.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
+        entry.key = template.text;
+      } else if (template.kind === ts.SyntaxKind.TemplateExpression) {
+        this.emit(
+        'warning',
+        'A key that is a template string must not have any interpolations.');
+
+        return null;
+      }
+
+      return entry;
     } }, { key: "expressionExtractor", value:
 
     function expressionExtractor(node) {
diff --git a/dist/lexers/jsx-lexer.js b/dist/lexers/jsx-lexer.js
index 6fd8fda..56b9973 100644
--- a/dist/lexers/jsx-lexer.js
+++ b/dist/lexers/jsx-lexer.js
@@ -30,6 +30,9 @@ JsxLexer = /*#__PURE__*/function (_JavascriptLexer) {(0, _inherits2["default"])(
           case ts.SyntaxKind.CallExpression:
             entry = _this2.expressionExtractor.call(_this2, node);
             break;
+          case ts.SyntaxKind.TaggedTemplateExpression:
+            entry = _this2.taggedTemplateExpressionExtractor(node);
+            break;
           case ts.SyntaxKind.JsxElement:
             entry = _this2.jsxExtractor.call(_this2, node, content);
             break;
diff --git a/dist/transform.js b/dist/transform.js
index c07e754..44f857f 100644
--- a/dist/transform.js
+++ b/dist/transform.js
@@ -294,6 +294,11 @@ i18nTransform = /*#__PURE__*/function (_Transform) {(0, _inherits2["default"])(i
 
       } else {
         text = JSON.stringify(contents, null, this.options.indentation) + '\n';
+        // Convert non-ASCII characters to unicode escape sequence
+        text = text.replace(
+        /[\u007F-\uFFFF]/g,
+        function (chr) {return "\\u".concat("0000".concat(chr.charCodeAt(0).toString(16)).substr(-4));});
+
       }
 
       if (this.options.lineEnding === 'auto') {
diff --git a/package.json b/package.json
index 152f988..72dbce8 100644
--- a/package.json
+++ b/package.json
@@ -29,7 +29,7 @@
     "broccoli-plugin": "^4.0.5",
     "cheerio": "^1.0.0-rc.2",
     "colors": "~1.4.0",
-    "commander": "~7.2.0",
+    "commander": "~8.1.0",
     "concat-stream": "~2.0.0",
     "eol": "^0.9.1",
     "fs-extra": "^10.0.0",
@@ -63,7 +63,7 @@
     "gulp": "^4.0.0",
     "mocha": "^9.0.0",
     "nyc": "^15.1.0",
-    "prettier": "2.3.1",
+    "prettier": "2.3.2",
     "sinon": "^11.1.1"
   },
   "keywords": [
diff --git a/src/lexers/javascript-lexer.js b/src/lexers/javascript-lexer.js
index cfef1f2..480e2de 100644
--- a/src/lexers/javascript-lexer.js
+++ b/src/lexers/javascript-lexer.js
@@ -57,6 +57,10 @@ export default class JavascriptLexer extends BaseLexer {
 
       parseCommentNode(keys, node, content)
 
+      if (node.kind === ts.SyntaxKind.TaggedTemplateExpression) {
+        entry = this.taggedTemplateExpressionExtractor.call(this, node)
+      }
+
       if (node.kind === ts.SyntaxKind.CallExpression) {
         entry = this.expressionExtractor.call(this, node)
       }
@@ -78,6 +82,30 @@ export default class JavascriptLexer extends BaseLexer {
     return this.setNamespaces(keys)
   }
 
+  taggedTemplateExpressionExtractor(node) {
+    const entry = {}
+
+    const { tag, template } = node
+
+    const isTranslationFunction =
+      (tag.text && this.functions.includes(tag.text)) ||
+      (tag.name && this.functions.includes(tag.name.text))
+
+    if (!isTranslationFunction) return null
+
+    if (template.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral) {
+      entry.key = template.text
+    } else if (template.kind === ts.SyntaxKind.TemplateExpression) {
+      this.emit(
+        'warning',
+        'A key that is a template string must not have any interpolations.'
+      )
+      return null
+    }
+
+    return entry
+  }
+
   expressionExtractor(node) {
     const entry = {}
 
diff --git a/src/lexers/jsx-lexer.js b/src/lexers/jsx-lexer.js
index 9d19ef0..e5c6933 100644
--- a/src/lexers/jsx-lexer.js
+++ b/src/lexers/jsx-lexer.js
@@ -30,6 +30,9 @@ export default class JsxLexer extends JavascriptLexer {
         case ts.SyntaxKind.CallExpression:
           entry = this.expressionExtractor.call(this, node)
           break
+        case ts.SyntaxKind.TaggedTemplateExpression:
+          entry = this.taggedTemplateExpressionExtractor.call(this, node)
+          break
         case ts.SyntaxKind.JsxElement:
           entry = this.jsxExtractor.call(this, node, content)
           break
diff --git a/src/transform.js b/src/transform.js
index 7436d14..39140c5 100644
--- a/src/transform.js
+++ b/src/transform.js
@@ -294,6 +294,11 @@ export default class i18nTransform extends Transform {
       })
     } else {
       text = JSON.stringify(contents, null, this.options.indentation) + '\n'
+      // Convert non-ASCII characters to unicode escape sequence
+      text = text.replace(
+        /[\u007F-\uFFFF]/g,
+        (chr) => `\\u${`0000${chr.charCodeAt(0).toString(16)}`.substr(-4)}`
+      )
     }
 
     if (this.options.lineEnding === 'auto') {
diff --git a/yarn.lock b/yarn.lock
index 5f33348..8b1d9f3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1157,7 +1157,7 @@ anymatch@^2.0.0:
     micromatch "^3.1.4"
     normalize-path "^2.1.1"
 
-anymatch@~3.1.1:
+anymatch@~3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716"
   integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==
@@ -1668,20 +1668,20 @@ cheerio@^1.0.0-rc.2:
     parse5-htmlparser2-tree-adapter "^6.0.1"
     tslib "^2.2.0"
 
-chokidar@3.5.1, chokidar@^3.4.0:
-  version "3.5.1"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a"
-  integrity sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==
+chokidar@3.5.2, chokidar@^3.4.0:
+  version "3.5.2"
+  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.2.tgz#dba3976fcadb016f66fd365021d91600d01c1e75"
+  integrity sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==
   dependencies:
-    anymatch "~3.1.1"
+    anymatch "~3.1.2"
     braces "~3.0.2"
-    glob-parent "~5.1.0"
+    glob-parent "~5.1.2"
     is-binary-path "~2.1.0"
     is-glob "~4.0.1"
     normalize-path "~3.0.0"
-    readdirp "~3.5.0"
+    readdirp "~3.6.0"
   optionalDependencies:
-    fsevents "~2.3.1"
+    fsevents "~2.3.2"
 
 chokidar@^2.0.0:
   version "2.1.8"
@@ -1870,10 +1870,10 @@ commander@~2.20.3:
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
   integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
 
-commander@~7.2.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
-  integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+commander@~8.1.0:
+  version "8.1.0"
+  resolved "https://registry.yarnpkg.com/commander/-/commander-8.1.0.tgz#db36e3e66edf24ff591d639862c6ab2c52664362"
+  integrity sha512-mf45ldcuHSYShkplHHGKWb4TrmwQadxOn7v4WuhDJy0ZVoY5JFajaRDKD0PNe5qXzBX0rhovjTnP6Kz9LETcuA==
 
 commondir@^1.0.1:
   version "1.0.1"
@@ -2664,7 +2664,7 @@ fsevents@^1.2.7:
     bindings "^1.5.0"
     nan "^2.12.1"
 
-fsevents@~2.3.1:
+fsevents@~2.3.2:
   version "2.3.2"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a"
   integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
@@ -2728,7 +2728,7 @@ glob-parent@^3.1.0:
     is-glob "^3.1.0"
     path-dirname "^1.0.0"
 
-glob-parent@^5.1.2, glob-parent@~5.1.0:
+glob-parent@^5.1.2, glob-parent@~5.1.2:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4"
   integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
@@ -3001,9 +3001,9 @@ https@^1.0.0:
   integrity sha1-PDfHrhqO65ZpBKKtHpdaGUt+06Q=
 
 i18next@^20.2.1:
-  version "20.3.2"
-  resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.3.2.tgz#5195e76b9e0848a1c198001bf6c7fc72995a55f1"
-  integrity sha512-e8CML2R9Ng2sSQOM80wb/PrM2j8mDm84o/T4Amzn9ArVyNX5/ENWxxAXkRpZdTQNDaxKImF93Wep4mAoozFrKw==
+  version "20.3.5"
+  resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.3.5.tgz#14308b79a3f1cafb24fdcd8e182d3673baf1e979"
+  integrity sha512-//MGeU6n4TencJmCgG+TCrpdgAD/NDEU/KfKQekYbJX6QV7sD/NjWQdVdBi+bkT0snegnSoB7QhjSeatrk3a0w==
   dependencies:
     "@babel/runtime" "^7.12.0"
 
@@ -3738,14 +3738,14 @@ mktemp@~0.4.0:
   integrity sha1-bQUVYRyKjITkhKogABKbmOmB/ws=
 
 mocha@^9.0.0:
-  version "9.0.1"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.0.1.tgz#01e66b7af0012330c0a38c4b6eaa6d92b8a81bf9"
-  integrity sha512-9zwsavlRO+5csZu6iRtl3GHImAbhERoDsZwdRkdJ/bE+eVplmoxNKE901ZJ9LdSchYBjSCPbjKc5XvcAri2ylw==
+  version "9.0.3"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.0.3.tgz#128cd6bbd3ee0adcdaef715f357f76ec1e6227c7"
+  integrity sha512-hnYFrSefHxYS2XFGtN01x8un0EwNu2bzKvhpRFhgoybIvMaOkkL60IVPmkb5h6XDmUl4IMSB+rT5cIO4/4bJgg==
   dependencies:
     "@ungap/promise-all-settled" "1.1.2"
     ansi-colors "4.1.1"
     browser-stdout "1.3.1"
-    chokidar "3.5.1"
+    chokidar "3.5.2"
     debug "4.3.1"
     diff "5.0.0"
     escape-string-regexp "4.0.0"
@@ -3758,12 +3758,12 @@ mocha@^9.0.0:
     minimatch "3.0.4"
     ms "2.1.3"
     nanoid "3.1.23"
-    serialize-javascript "5.0.1"
+    serialize-javascript "6.0.0"
     strip-json-comments "3.1.1"
     supports-color "8.1.1"
     which "2.0.2"
     wide-align "1.1.3"
-    workerpool "6.1.4"
+    workerpool "6.1.5"
     yargs "16.2.0"
     yargs-parser "20.2.4"
     yargs-unparser "2.0.0"
@@ -4313,10 +4313,10 @@ posix-character-classes@^0.1.0:
   resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
   integrity sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=
 
-prettier@2.3.1:
-  version "2.3.1"
-  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.1.tgz#76903c3f8c4449bc9ac597acefa24dc5ad4cbea6"
-  integrity sha512-p+vNbgpLjif/+D+DwAZAbndtRrR0md0MwfmOVN9N+2RgyACMT+7tfaRnT+WDPkqnuVwleyuBIG2XBxKDme3hPA==
+prettier@2.3.2:
+  version "2.3.2"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.2.tgz#ef280a05ec253712e486233db5c6f23441e7342d"
+  integrity sha512-lnJzDfJ66zkMy58OL5/NY5zp70S7Nz6KqcKkXYzn2tMVrNxvbqaBpg7H3qHaLxCJ5lNMsGuM8+ohS7cZrthdLQ==
 
 pretty-hrtime@^1.0.0:
   version "1.0.3"
@@ -4429,10 +4429,10 @@ readdirp@^2.2.1:
     micromatch "^3.1.10"
     readable-stream "^2.0.2"
 
-readdirp@~3.5.0:
-  version "3.5.0"
-  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.5.0.tgz#9ba74c019b15d365278d2e91bb8c48d7b4d42c9e"
-  integrity sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==
+readdirp@~3.6.0:
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7"
+  integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
   dependencies:
     picomatch "^2.2.1"
 
@@ -4723,10 +4723,10 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
   integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
 
-serialize-javascript@5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-5.0.1.tgz#7886ec848049a462467a97d3d918ebb2aaf934f4"
-  integrity sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==
+serialize-javascript@6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8"
+  integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==
   dependencies:
     randombytes "^2.1.0"
 
@@ -5261,9 +5261,9 @@ typedarray@^0.0.6:
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
 typescript@^4.2.4:
-  version "4.3.4"
-  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.4.tgz#3f85b986945bcf31071decdd96cf8bfa65f9dcbc"
-  integrity sha512-uauPG7XZn9F/mo+7MrsRjyvbxFpzemRjKEZXS4AK83oP2KKOJPvb+9cO/gmnv8arWZvhnjVOXz7B49m1l0e9Ew==
+  version "4.3.5"
+  resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4"
+  integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA==
 
 uglify-js@^3.1.4:
   version "3.9.1"
@@ -5553,10 +5553,10 @@ wordwrap@^1.0.0:
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
   integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=
 
-workerpool@6.1.4:
-  version "6.1.4"
-  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.4.tgz#6a972b6df82e38d50248ee2820aa98e2d0ad3090"
-  integrity sha512-jGWPzsUqzkow8HoAvqaPWTUPCrlPJaJ5tY8Iz7n1uCz3tTp6s3CDG0FF1NsX42WNlkRSW6Mr+CDZGnNoSsKa7g==
+workerpool@6.1.5:
+  version "6.1.5"
+  resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.1.5.tgz#0f7cf076b6215fd7e1da903ff6f22ddd1886b581"
+  integrity sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==
 
 wrap-ansi@^2.0.0:
   version "2.1.0"


Test patch:
diff --git a/test/lexers/javascript-lexer.test.js b/test/lexers/javascript-lexer.test.js
index 1f2ec02..057c34f 100644
--- a/test/lexers/javascript-lexer.test.js
+++ b/test/lexers/javascript-lexer.test.js
@@ -259,4 +259,34 @@ describe('JavascriptLexer', () => {
     ])
     assert.strictEqual(warningCount, 2)
   })
+
+  it('extracts non-interpolated tagged templates', () => {
+    const Lexer = new JavascriptLexer()
+    const content = 'i18n.t`some-key`'
+    assert.deepEqual(Lexer.extract(content), [
+      {
+        key: 'some-key',
+      },
+    ])
+  })
+
+  it('emits warnings on interpolated tagged templates', () => {
+    const Lexer = new JavascriptLexer()
+    const content = 'i18n.t`some-key${someVar}keykey`'
+
+    let warningCount = 0
+    Lexer.on('warning', (warning) => {
+      if (
+        warning.indexOf(
+          'A key that is a template string must not have any interpolations.'
+        ) === 0
+      ) {
+        warningCount++
+      }
+    })
+
+    Lexer.extract(content)
+
+    assert.equal(warningCount, 1)
+  })
 })
diff --git a/test/parser.test.js b/test/parser.test.js
index af268fa..947b126 100644
--- a/test/parser.test.js
+++ b/test/parser.test.js
@@ -990,6 +990,31 @@ describe('parser', () => {
       i18nextParser.end(fakeFile)
     })
 
+    it('writes non-breaking space output to json', (done) => {
+      let result
+      const expected = '{\n  "nbsp": "One\\u00a0Two\\u00a0Three"\n}\n'
+
+      const i18nextParser = new i18nTransform({
+        output: 'locales/$LOCALE/$NAMESPACE.json',
+      })
+      const fakeFile = new Vinyl({
+        contents: Buffer.from("t('nbsp', 'One\\u00A0Two\\u00a0Three')"),
+        path: 'file.js',
+      })
+
+      i18nextParser.on('data', (file) => {
+        if (file.relative.endsWith(path.normalize('en/translation.json'))) {
+          result = file.contents.toString('utf8')
+        }
+      })
+      i18nextParser.once('end', () => {
+        assert.equal(result, expected)
+        done()
+      })
+
+      i18nextParser.end(fakeFile)
+    })
+
     it('supports outputing to yml', (done) => {
       let result
       const i18nextParser = new i18nTransform({