Repository: i18next/i18next-parser
Pull request #425 (2021-09-21 04:32:17)

Bump tmpl from 1.0.4 to 1.0.5
Bumps [tmpl](https://github.com/daaku/nodejs-tmpl) from 1.0.4 to 1.0.5.
<details>
<summary>Commits</summary>
<ul>
<li>See full diff in <a href="https://github.com/daaku/nodejs-tmpl/commits/v1.0.5">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=tmpl&package-manager=npm_and_yarn&previous-version=1.0.4&new-version=1.0.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/i18next/i18next-parser/network/alerts).

</details>


Patch:
diff --git a/.gitignore b/.gitignore
index 761e45c..1efc282 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,7 @@ npm-debug.log
 # manual test files
 test/manual/en
 test/manual/fr
+
+# coverage
+.nyc_output
+coverage
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0bc8282..409da14 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,8 @@
 # Changelog
 
+# 5.0.0 - unreleased
+- BREAKING: Format Json output conforming to i18next JSON v4 (see: https://www.i18next.com/misc/json-format)
+
 # 4.8.0 - latest
 
 - Add template literal support for defaultValue as a second argument #419 #420
@@ -43,7 +46,7 @@
 
 # 3.11.0
 
-- Add a pluralSeparator option #300 #302 
+- Add a pluralSeparator option #300 #302
 
 # 3.10.0
 
diff --git a/README.md b/README.md
index 3041ee7..6fcce13 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ Finally, if you want to make this process even less painful, I invite you to che
 - Parses comments for static keys to support dynamic key translations.
 - Supports i18next features:
   - **Context**: keys of the form `key_context`
-  - **Plural**: keys of the form `key_plural` and `key_0`, `key_1` as described [here](https://www.i18next.com/translation-function/plurals)
+  - **Plural**: keys of the form `key_zero`, `key_one`, `key_two`, `key_few`, `key_many` and `key_other` as described [here](https://www.i18next.com/translation-function/plurals)
 - Tested on Node 10+. If you need support for 6 and 8, look at the `1.0.x` versions.
 
 ## Versions
diff --git a/dist/helpers.js b/dist/helpers.js
index 0777c21..c1472d7 100644
--- a/dist/helpers.js
+++ b/dist/helpers.js
@@ -1,4 +1,4 @@
-"use strict";var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(exports, "__esModule", { value: true });exports.dotPathToHash = dotPathToHash;exports.mergeHashes = mergeHashes;exports.transferValues = transferValues;var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof")); /**
+"use strict";var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");Object.defineProperty(exports, "__esModule", { value: true });exports.dotPathToHash = dotPathToHash;exports.mergeHashes = mergeHashes;exports.transferValues = transferValues;exports.hasRelatedPluralKey = hasRelatedPluralKey;var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof")); /**
  * Take an entry for the Parser and turn it into a hash,
  * turning the key path 'foo.bar' into an hash {foo: {bar: ""}}
  * The generated hash can be merged with an optional `target`.
@@ -15,8 +15,8 @@ function dotPathToHash(entry) {var target = arguments.length > 1 && arguments[1]
   var conflict = false;
   var duplicate = false;
   var path = entry.keyWithNamespace;
-  if (options.suffix || options.suffix === 0) {
-    path += "".concat(options.pluralSeparator).concat(options.suffix);
+  if (options.suffix) {
+    path += options.suffix;
   }
 
   var separator = options.separator || '.';
@@ -48,7 +48,7 @@ function dotPathToHash(entry) {var target = arguments.length > 1 && arguments[1]
   options.useKeysAsDefaultValue;
 
   var newValue =
-  entry["defaultValue_".concat(options.suffix)] ||
+  entry["defaultValue".concat(options.suffix)] ||
   entry.defaultValue ||
   defaultValue ||
   '';
@@ -117,31 +117,35 @@ function dotPathToHash(entry) {var target = arguments.length > 1 && arguments[1]
 /**
  * Takes a `source` hash and makes sure its value
  * is pasted in the `target` hash, if the target
- * hash has the corresponding key (or if `keepRemoved` is true).
+ * hash has the corresponding key (or if `options.keepRemoved` is true).
  * @returns An `{ old, new, mergeCount, pullCount, oldCount }` object.
  * `old` is a hash of values that have not been merged into `target`.
  * `new` is `target`. `mergeCount` is the number of keys merged into
  * `new`, `pullCount` is the number of context and plural keys added to
  * `new` and `oldCount` is the number of keys that were either added to `old` or
- * `new` (if `keepRemoved` is true and `target` didn't have the corresponding
+ * `new` (if `options.keepRemoved` is true and `target` didn't have the corresponding
  * key).
  */
-function mergeHashes(source, target) {var keepRemoved = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
+function mergeHashes(source, target) {var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
   var old = {};
   var mergeCount = 0;
   var pullCount = 0;
   var oldCount = 0;
+
+  var keepRemoved = options.keepRemoved || false;
+  var pluralSeparator = options.pluralSeparator || '_';
+
   for (var key in source) {
     var hasNestedEntries =
     (0, _typeof2["default"])(target[key]) === 'object' && !Array.isArray(target[key]);
 
     if (hasNestedEntries) {
-      var nested = mergeHashes(source[key], target[key], keepRemoved);
+      var nested = mergeHashes(source[key], target[key], options);
       mergeCount += nested.mergeCount;
       pullCount += nested.pullCount;
+      oldCount += nested.oldCount;
       if (Object.keys(nested.old).length) {
         old[key] = nested.old;
-        oldCount += nested.oldCount;
       }
     } else {
       if (target[key] !== undefined) {
@@ -154,7 +158,9 @@ function mergeHashes(source, target) {var keepRemoved = arguments.length > 2 &&
         }
       } else {
         // support for plural in keys
-        var pluralRegex = /(_plural)|(_\d+)$/;
+        var pluralRegex = new RegExp("(".concat(
+        pluralSeparator, "(?:zero|one|two|few|many|other))$"));
+
         var pluralMatch = pluralRegex.test(key);
         var singularKey = key.replace(pluralRegex, '');
 
@@ -165,7 +171,8 @@ function mergeHashes(source, target) {var keepRemoved = arguments.length > 2 &&
 
         if (
         contextMatch && target[rawKey] !== undefined ||
-        pluralMatch && target[singularKey] !== undefined)
+        pluralMatch &&
+        hasRelatedPluralKey("".concat(singularKey).concat(pluralSeparator), target))
         {
           target[key] = source[key];
           pullCount += 1;
@@ -201,4 +208,16 @@ function transferValues(source, target) {
       target[key] = sourceValue;
     }
   }
+}
+
+function hasRelatedPluralKey(rawKey, source) {
+  var suffixes = ['zero', 'one', 'two', 'few', 'many', 'other'];
+
+  for (var _i = 0, _suffixes = suffixes; _i < _suffixes.length; _i++) {var suffix = _suffixes[_i];
+    if (source["".concat(rawKey).concat(suffix)] !== undefined) {
+      return true;
+    }
+  }
+
+  return false;
 }
\ No newline at end of file
diff --git a/dist/lexers/handlebars-lexer.js b/dist/lexers/handlebars-lexer.js
index a5f3cb9..6b46a71 100644
--- a/dist/lexers/handlebars-lexer.js
+++ b/dist/lexers/handlebars-lexer.js
@@ -32,7 +32,14 @@ HandlebarsLexer = /*#__PURE__*/function (_BaseLexer) {(0, _inherits2["default"])
         var parts = arg.split('=');
         result.arguments.push(arg);
         if (parts.length === 2 && this.validateString(parts[1])) {
-          result.options[parts[0]] = parts[1].slice(1, -1);
+          var value = parts[1].slice(1, -1);
+          if (value === 'true') {
+            result.options[parts[0]] = true;
+          } else if (value === 'false') {
+            result.options[parts[0]] = false;
+          } else {
+            result.options[parts[0]] = value;
+          }
         }
       }
       return result;
diff --git a/dist/lexers/javascript-lexer.js b/dist/lexers/javascript-lexer.js
index 0c5d87e..ed8ab28 100644
--- a/dist/lexers/javascript-lexer.js
+++ b/dist/lexers/javascript-lexer.js
@@ -181,8 +181,16 @@ JavascriptLexer = /*#__PURE__*/function (_BaseLexer) {(0, _inherits2["default"])
                 'warning', "Options argument is a spread operator : ".concat(
                 p.expression.text));
 
+              } else if (p.initializer) {
+                if (p.initializer.kind === ts.SyntaxKind.TrueKeyword) {
+                  entry[p.name.text] = true;
+                } else if (p.initializer.kind === ts.SyntaxKind.FalseKeyword) {
+                  entry[p.name.text] = false;
+                } else {
+                  entry[p.name.text] = p.initializer.text || '';
+                }
               } else {
-                entry[p.name.text] = p.initializer && p.initializer.text || '';
+                entry[p.name.text] = '';
               }
             }} catch (err) {_iterator.e(err);} finally {_iterator.f();}
         }
diff --git a/dist/lexers/jsx-lexer.js b/dist/lexers/jsx-lexer.js
index bc1c867..90d8336 100644
--- a/dist/lexers/jsx-lexer.js
+++ b/dist/lexers/jsx-lexer.js
@@ -101,13 +101,26 @@ JsxLexer = /*#__PURE__*/function (_JavascriptLexer) {(0, _inherits2["default"])(
             return;
           }
 
-          if (property.initializer.expression) {
-            entry[
-            property.name.text] = "{".concat(
-            property.initializer.expression.text, "}");
-          } else {
-            entry[property.name.text] = property.initializer.text;
-          }
+          if (property.initializer) {
+            if (property.initializer.expression) {
+              if (
+              property.initializer.expression.kind === ts.SyntaxKind.TrueKeyword)
+              {
+                entry[property.name.text] = true;
+              } else if (
+              property.initializer.expression.kind ===
+              ts.SyntaxKind.FalseKeyword)
+              {
+                entry[property.name.text] = false;
+              } else {
+                entry[
+                property.name.text] = "{".concat(
+                property.initializer.expression.text, "}");
+              }
+            } else {
+              entry[property.name.text] = property.initializer.text;
+            }
+          } else entry[property.name.text] = true;
         });
 
         return entry.key ? entry : null;
diff --git a/dist/transform.js b/dist/transform.js
index 0c19c5f..9d4eaa0 100644
--- a/dist/transform.js
+++ b/dist/transform.js
@@ -6,16 +6,7 @@ var _parser = _interopRequireDefault(require("./parser"));
 var _path = _interopRequireDefault(require("path"));
 var _vinyl = _interopRequireDefault(require("vinyl"));
 var _jsYaml = _interopRequireDefault(require("js-yaml"));
-var _i18next = _interopRequireDefault(require("i18next"));function _createForOfIteratorHelper(o, allowArrayLike) {var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];if (!it) {if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {if (it) o = it;var i = 0;var F = function F() {};return { s: F, n: function n() {if (i >= o.length) return { done: true };return { done: false, value: o[i++] };}, e: function e(_e) {throw _e;}, f: F };}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");}var normalCompletion = true,didErr = false,err;return { s: function s() {it = it.call(o);}, n: function n() {var step = it.next();normalCompletion = step.done;return step;}, e: function e(_e2) {didErr = true;err = _e2;}, f: function f() {try {if (!normalCompletion && it["return"] != null) it["return"]();} finally {if (didErr) throw err;}} };}function _unsupportedIterableToArray(o, minLen) {if (!o) return;if (typeof o === "string") return _arrayLikeToArray(o, minLen);var n = Object.prototype.toString.call(o).slice(8, -1);if (n === "Object" && o.constructor) n = o.constructor.name;if (n === "Map" || n === "Set") return Array.from(o);if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);}function _arrayLikeToArray(arr, len) {if (len == null || len > arr.length) len = arr.length;for (var i = 0, arr2 = new Array(len); i < len; i++) {arr2[i] = arr[i];}return arr2;}function ownKeys(object, enumerableOnly) {var keys = Object.keys(object);if (Object.getOwnPropertySymbols) {var symbols = Object.getOwnPropertySymbols(object);if (enumerableOnly) {symbols = symbols.filter(function (sym) {return Object.getOwnPropertyDescriptor(object, sym).enumerable;});}keys.push.apply(keys, symbols);}return keys;}function _objectSpread(target) {for (var i = 1; i < arguments.length; i++) {var source = arguments[i] != null ? arguments[i] : {};if (i % 2) {ownKeys(Object(source), true).forEach(function (key) {(0, _defineProperty2["default"])(target, key, source[key]);});} else if (Object.getOwnPropertyDescriptors) {Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));} else {ownKeys(Object(source)).forEach(function (key) {Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));});}}return target;}function _createSuper(Derived) {var hasNativeReflectConstruct = _isNativeReflectConstruct();return function _createSuperInternal() {var Super = (0, _getPrototypeOf2["default"])(Derived),result;if (hasNativeReflectConstruct) {var NewTarget = (0, _getPrototypeOf2["default"])(this).constructor;result = Reflect.construct(Super, arguments, NewTarget);} else {result = Super.apply(this, arguments);}return (0, _possibleConstructorReturn2["default"])(this, result);};}function _isNativeReflectConstruct() {if (typeof Reflect === "undefined" || !Reflect.construct) return false;if (Reflect.construct.sham) return false;if (typeof Proxy === "function") return true;try {Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));return true;} catch (e) {return false;}}
-
-function getPluralSuffix(numberOfPluralForms, nthForm) {
-  if (numberOfPluralForms.length > 2) {
-    return nthForm; // key_0, key_1, etc.
-  } else if (nthForm === 1) {
-    return 'plural';
-  }
-  return '';
-}var
+var _i18next = _interopRequireDefault(require("i18next"));function _createForOfIteratorHelper(o, allowArrayLike) {var it = typeof Symbol !== "undefined" && o[Symbol.iterator] || o["@@iterator"];if (!it) {if (Array.isArray(o) || (it = _unsupportedIterableToArray(o)) || allowArrayLike && o && typeof o.length === "number") {if (it) o = it;var i = 0;var F = function F() {};return { s: F, n: function n() {if (i >= o.length) return { done: true };return { done: false, value: o[i++] };}, e: function e(_e) {throw _e;}, f: F };}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");}var normalCompletion = true,didErr = false,err;return { s: function s() {it = it.call(o);}, n: function n() {var step = it.next();normalCompletion = step.done;return step;}, e: function e(_e2) {didErr = true;err = _e2;}, f: function f() {try {if (!normalCompletion && it["return"] != null) it["return"]();} finally {if (didErr) throw err;}} };}function _unsupportedIterableToArray(o, minLen) {if (!o) return;if (typeof o === "string") return _arrayLikeToArray(o, minLen);var n = Object.prototype.toString.call(o).slice(8, -1);if (n === "Object" && o.constructor) n = o.constructor.name;if (n === "Map" || n === "Set") return Array.from(o);if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);}function _arrayLikeToArray(arr, len) {if (len == null || len > arr.length) len = arr.length;for (var i = 0, arr2 = new Array(len); i < len; i++) {arr2[i] = arr[i];}return arr2;}function ownKeys(object, enumerableOnly) {var keys = Object.keys(object);if (Object.getOwnPropertySymbols) {var symbols = Object.getOwnPropertySymbols(object);if (enumerableOnly) {symbols = symbols.filter(function (sym) {return Object.getOwnPropertyDescriptor(object, sym).enumerable;});}keys.push.apply(keys, symbols);}return keys;}function _objectSpread(target) {for (var i = 1; i < arguments.length; i++) {var source = arguments[i] != null ? arguments[i] : {};if (i % 2) {ownKeys(Object(source), true).forEach(function (key) {(0, _defineProperty2["default"])(target, key, source[key]);});} else if (Object.getOwnPropertyDescriptors) {Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));} else {ownKeys(Object(source)).forEach(function (key) {Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));});}}return target;}function _createSuper(Derived) {var hasNativeReflectConstruct = _isNativeReflectConstruct();return function _createSuperInternal() {var Super = (0, _getPrototypeOf2["default"])(Derived),result;if (hasNativeReflectConstruct) {var NewTarget = (0, _getPrototypeOf2["default"])(this).constructor;result = Reflect.construct(Super, arguments, NewTarget);} else {result = Super.apply(this, arguments);}return (0, _possibleConstructorReturn2["default"])(this, result);};}function _isNativeReflectConstruct() {if (typeof Reflect === "undefined" || !Reflect.construct) return false;if (Reflect.construct.sham) return false;if (typeof Proxy === "function") return true;try {Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));return true;} catch (e) {return false;}}var
 
 i18nTransform = /*#__PURE__*/function (_Transform) {(0, _inherits2["default"])(i18nTransform, _Transform);var _super = _createSuper(i18nTransform);
   function i18nTransform() {var _this;var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};(0, _classCallCheck2["default"])(this, i18nTransform);
@@ -61,7 +52,9 @@ i18nTransform = /*#__PURE__*/function (_Transform) {(0, _inherits2["default"])(i
     _this.localeRegex = /\$LOCALE/g;
     _this.namespaceRegex = /\$NAMESPACE/g;
 
-    _i18next["default"].init();return _this;
+    _i18next["default"].init({
+      pluralSeparator: _this.options.pluralSeparator });return _this;
+
   }(0, _createClass2["default"])(i18nTransform, [{ key: "error", value:
 
     function error(_error) {
@@ -135,8 +128,6 @@ i18nTransform = /*#__PURE__*/function (_Transform) {(0, _inherits2["default"])(i
 
       this.options.locales),_step2;try {var _loop = function _loop() {var locale = _step2.value;
           var catalog = {};
-          var pluralRule = _i18next["default"].services.pluralResolver.getRule(locale);
-          var numbers = pluralRule && pluralRule.numbers || [1, 2];
 
           var countWithPlurals = 0;
           var uniqueCount = _this2.entries.length;
@@ -167,11 +158,13 @@ i18nTransform = /*#__PURE__*/function (_Transform) {(0, _inherits2["default"])(i
             }
           };
 
-          // generates plurals according to i18next rules: key, key_plural, key_0, key_1, etc.
+          // generates plurals according to i18next rules: key_zero, key_one, key_two, key_few, key_many and key_other
           var _iterator3 = _createForOfIteratorHelper(_this2.entries),_step3;try {var _loop2 = function _loop2() {var entry = _step3.value;
               if (entry.count !== undefined) {
-                numbers.forEach(function (_, i) {
-                  transformEntry(entry, getPluralSuffix(numbers, i));
+                _i18next["default"].services.pluralResolver.
+                getSuffixes(locale, { ordinal: entry.ordinal }).
+                forEach(function (suffix) {
+                  transformEntry(entry, suffix);
                 });
               } else {
                 transformEntry(entry);
@@ -201,16 +194,13 @@ i18nTransform = /*#__PURE__*/function (_Transform) {(0, _inherits2["default"])(i
 
 
 
-            (0, _helpers.mergeHashes)(
-            existingCatalog,
-            catalog[namespace],
-            _this2.options.keepRemoved),newCatalog = _mergeHashes["new"],oldKeys = _mergeHashes.old,mergeCount = _mergeHashes.mergeCount,oldCount = _mergeHashes.oldCount;
-
+            (0, _helpers.mergeHashes)(existingCatalog, catalog[namespace], _this2.options),newCatalog = _mergeHashes["new"],oldKeys = _mergeHashes.old,mergeCount = _mergeHashes.mergeCount,oldCount = _mergeHashes.oldCount;
 
             // restore old translations
             var _mergeHashes2 = (0, _helpers.mergeHashes)(
             existingOldCatalog,
-            newCatalog),oldCatalog = _mergeHashes2.old,restoreCount = _mergeHashes2.mergeCount;
+            newCatalog, _objectSpread(_objectSpread({},
+            _this2.options), {}, { keepRemoved: false })),oldCatalog = _mergeHashes2.old,restoreCount = _mergeHashes2.mergeCount;
 
 
             // backup unused translations
diff --git a/docs/migration.md b/docs/migration.md
index 33ebe52..3abbeb4 100644
--- a/docs/migration.md
+++ b/docs/migration.md
@@ -1,3 +1,11 @@
+# Migrating from `4.x` to `5.x`
+
+## Breaking change
+
+- The output format is now in [JSON v4](https://www.i18next.com/misc/json-format). To convert your existing translations to the new v4 format, have a look at [i18next-v4-format-converter](https://github.com/i18next/i18next-v4-format-converter) or [this web tool](https://i18next.github.io/i18next-v4-format-converter-web/).
+
+---
+
 # Migrating from `2.x` to `3.x`
 
 ## Breaking change
diff --git a/package.json b/package.json
index ef0ae57..4fc74c7 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "author": "Karel Ledru",
   "description": "Command Line tool for i18next",
   "name": "i18next-parser",
-  "version": "4.8.0",
+  "version": "5.0.0",
   "license": "MIT",
   "main": "dist/index.js",
   "bin": {
@@ -34,7 +34,7 @@
     "eol": "^0.9.1",
     "fs-extra": "^10.0.0",
     "gulp-sort": "^2.0.0",
-    "i18next": "^20.2.1",
+    "i18next": "^21.2.0",
     "js-yaml": "4.1.0",
     "rsvp": "^4.8.2",
     "through2": "~4.0.2",
diff --git a/src/helpers.js b/src/helpers.js
index 2743120..5670881 100644
--- a/src/helpers.js
+++ b/src/helpers.js
@@ -15,8 +15,8 @@ function dotPathToHash(entry, target = {}, options = {}) {
   let conflict = false
   let duplicate = false
   let path = entry.keyWithNamespace
-  if (options.suffix || options.suffix === 0) {
-    path += `${options.pluralSeparator}${options.suffix}`
+  if (options.suffix) {
+    path += options.suffix
   }
 
   const separator = options.separator || '.'
@@ -48,7 +48,7 @@ function dotPathToHash(entry, target = {}, options = {}) {
       : options.useKeysAsDefaultValue
 
   let newValue =
-    entry[`defaultValue_${options.suffix}`] ||
+    entry[`defaultValue${options.suffix}`] ||
     entry.defaultValue ||
     defaultValue ||
     ''
@@ -117,31 +117,35 @@ function dotPathToHash(entry, target = {}, options = {}) {
 /**
  * Takes a `source` hash and makes sure its value
  * is pasted in the `target` hash, if the target
- * hash has the corresponding key (or if `keepRemoved` is true).
+ * hash has the corresponding key (or if `options.keepRemoved` is true).
  * @returns An `{ old, new, mergeCount, pullCount, oldCount }` object.
  * `old` is a hash of values that have not been merged into `target`.
  * `new` is `target`. `mergeCount` is the number of keys merged into
  * `new`, `pullCount` is the number of context and plural keys added to
  * `new` and `oldCount` is the number of keys that were either added to `old` or
- * `new` (if `keepRemoved` is true and `target` didn't have the corresponding
+ * `new` (if `options.keepRemoved` is true and `target` didn't have the corresponding
  * key).
  */
-function mergeHashes(source, target, keepRemoved = false) {
+function mergeHashes(source, target, options = {}) {
   let old = {}
   let mergeCount = 0
   let pullCount = 0
   let oldCount = 0
+
+  const keepRemoved = options.keepRemoved || false
+  const pluralSeparator = options.pluralSeparator || '_'
+
   for (const key in source) {
     const hasNestedEntries =
       typeof target[key] === 'object' && !Array.isArray(target[key])
 
     if (hasNestedEntries) {
-      const nested = mergeHashes(source[key], target[key], keepRemoved)
+      const nested = mergeHashes(source[key], target[key], options)
       mergeCount += nested.mergeCount
       pullCount += nested.pullCount
+      oldCount += nested.oldCount
       if (Object.keys(nested.old).length) {
         old[key] = nested.old
-        oldCount += nested.oldCount
       }
     } else {
       if (target[key] !== undefined) {
@@ -154,7 +158,9 @@ function mergeHashes(source, target, keepRemoved = false) {
         }
       } else {
         // support for plural in keys
-        const pluralRegex = /(_plural)|(_\d+)$/
+        const pluralRegex = new RegExp(
+          `(${pluralSeparator}(?:zero|one|two|few|many|other))$`
+        )
         const pluralMatch = pluralRegex.test(key)
         const singularKey = key.replace(pluralRegex, '')
 
@@ -165,7 +171,8 @@ function mergeHashes(source, target, keepRemoved = false) {
 
         if (
           (contextMatch && target[rawKey] !== undefined) ||
-          (pluralMatch && target[singularKey] !== undefined)
+          (pluralMatch &&
+            hasRelatedPluralKey(`${singularKey}${pluralSeparator}`, target))
         ) {
           target[key] = source[key]
           pullCount += 1
@@ -203,4 +210,16 @@ function transferValues(source, target) {
   }
 }
 
-export { dotPathToHash, mergeHashes, transferValues }
+function hasRelatedPluralKey(rawKey, source) {
+  const suffixes = ['zero', 'one', 'two', 'few', 'many', 'other']
+
+  for (const suffix of suffixes) {
+    if (source[`${rawKey}${suffix}`] !== undefined) {
+      return true
+    }
+  }
+
+  return false
+}
+
+export { dotPathToHash, mergeHashes, transferValues, hasRelatedPluralKey }
diff --git a/src/lexers/handlebars-lexer.js b/src/lexers/handlebars-lexer.js
index 33e1627..a4d5266 100644
--- a/src/lexers/handlebars-lexer.js
+++ b/src/lexers/handlebars-lexer.js
@@ -32,7 +32,14 @@ export default class HandlebarsLexer extends BaseLexer {
       const parts = arg.split('=')
       result.arguments.push(arg)
       if (parts.length === 2 && this.validateString(parts[1])) {
-        result.options[parts[0]] = parts[1].slice(1, -1)
+        const value = parts[1].slice(1, -1)
+        if (value === 'true') {
+          result.options[parts[0]] = true
+        } else if (value === 'false') {
+          result.options[parts[0]] = false
+        } else {
+          result.options[parts[0]] = value
+        }
       }
     }
     return result
diff --git a/src/lexers/javascript-lexer.js b/src/lexers/javascript-lexer.js
index 357bab8..1cf7773 100644
--- a/src/lexers/javascript-lexer.js
+++ b/src/lexers/javascript-lexer.js
@@ -181,8 +181,16 @@ export default class JavascriptLexer extends BaseLexer {
               'warning',
               `Options argument is a spread operator : ${p.expression.text}`
             )
+          } else if (p.initializer) {
+            if (p.initializer.kind === ts.SyntaxKind.TrueKeyword) {
+              entry[p.name.text] = true
+            } else if (p.initializer.kind === ts.SyntaxKind.FalseKeyword) {
+              entry[p.name.text] = false
+            } else {
+              entry[p.name.text] = p.initializer.text || ''
+            }
           } else {
-            entry[p.name.text] = (p.initializer && p.initializer.text) || ''
+            entry[p.name.text] = ''
           }
         }
       }
diff --git a/src/lexers/jsx-lexer.js b/src/lexers/jsx-lexer.js
index e5c6933..d9ace4e 100644
--- a/src/lexers/jsx-lexer.js
+++ b/src/lexers/jsx-lexer.js
@@ -101,13 +101,26 @@ export default class JsxLexer extends JavascriptLexer {
           return
         }
 
-        if (property.initializer.expression) {
-          entry[
-            property.name.text
-          ] = `{${property.initializer.expression.text}}`
-        } else {
-          entry[property.name.text] = property.initializer.text
-        }
+        if (property.initializer) {
+          if (property.initializer.expression) {
+            if (
+              property.initializer.expression.kind === ts.SyntaxKind.TrueKeyword
+            ) {
+              entry[property.name.text] = true
+            } else if (
+              property.initializer.expression.kind ===
+              ts.SyntaxKind.FalseKeyword
+            ) {
+              entry[property.name.text] = false
+            } else {
+              entry[
+                property.name.text
+              ] = `{${property.initializer.expression.text}}`
+            }
+          } else {
+            entry[property.name.text] = property.initializer.text
+          }
+        } else entry[property.name.text] = true
       })
 
       return entry.key ? entry : null
diff --git a/src/transform.js b/src/transform.js
index cc7ef2b..df597bc 100644
--- a/src/transform.js
+++ b/src/transform.js
@@ -8,15 +8,6 @@ import VirtualFile from 'vinyl'
 import yaml from 'js-yaml'
 import i18next from 'i18next'
 
-function getPluralSuffix(numberOfPluralForms, nthForm) {
-  if (numberOfPluralForms.length > 2) {
-    return nthForm // key_0, key_1, etc.
-  } else if (nthForm === 1) {
-    return 'plural'
-  }
-  return ''
-}
-
 export default class i18nTransform extends Transform {
   constructor(options = {}) {
     options.objectMode = true
@@ -61,7 +52,9 @@ export default class i18nTransform extends Transform {
     this.localeRegex = /\$LOCALE/g
     this.namespaceRegex = /\$NAMESPACE/g
 
-    i18next.init()
+    i18next.init({
+      pluralSeparator: this.options.pluralSeparator,
+    })
   }
 
   error(error) {
@@ -135,8 +128,6 @@ export default class i18nTransform extends Transform {
 
     for (const locale of this.options.locales) {
       const catalog = {}
-      const pluralRule = i18next.services.pluralResolver.getRule(locale)
-      const numbers = (pluralRule && pluralRule.numbers) || [1, 2]
 
       let countWithPlurals = 0
       let uniqueCount = this.entries.length
@@ -167,12 +158,14 @@ export default class i18nTransform extends Transform {
         }
       }
 
-      // generates plurals according to i18next rules: key, key_plural, key_0, key_1, etc.
+      // generates plurals according to i18next rules: key_zero, key_one, key_two, key_few, key_many and key_other
       for (const entry of this.entries) {
         if (entry.count !== undefined) {
-          numbers.forEach((_, i) => {
-            transformEntry(entry, getPluralSuffix(numbers, i))
-          })
+          i18next.services.pluralResolver
+            .getSuffixes(locale, { ordinal: entry.ordinal })
+            .forEach((suffix) => {
+              transformEntry(entry, suffix)
+            })
         } else {
           transformEntry(entry)
         }
@@ -201,16 +194,13 @@ export default class i18nTransform extends Transform {
           old: oldKeys,
           mergeCount,
           oldCount,
-        } = mergeHashes(
-          existingCatalog,
-          catalog[namespace],
-          this.options.keepRemoved
-        )
+        } = mergeHashes(existingCatalog, catalog[namespace], this.options)
 
         // restore old translations
         const { old: oldCatalog, mergeCount: restoreCount } = mergeHashes(
           existingOldCatalog,
-          newCatalog
+          newCatalog,
+          { ...this.options, keepRemoved: false }
         )
 
         // backup unused translations
diff --git a/yarn.lock b/yarn.lock
index c8f5eda..bb2646c 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2129,10 +2129,10 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
   dependencies:
     ms "2.0.0"
 
-debug@4.3.1, debug@^4.1.0, debug@^4.1.1:
-  version "4.3.1"
-  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.1.tgz#f0d229c505e0c6d8c49ac553d1b13dc183f6b2ee"
-  integrity sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
+debug@4.3.2, debug@^4.1.0, debug@^4.1.1:
+  version "4.3.2"
+  resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
+  integrity sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==
   dependencies:
     ms "2.1.2"
 
@@ -3104,10 +3104,10 @@ https@^1.0.0:
   resolved "https://registry.yarnpkg.com/https/-/https-1.0.0.tgz#3c37c7ae1a8eeb966904a2ad1e975a194b7ed3a4"
   integrity sha1-PDfHrhqO65ZpBKKtHpdaGUt+06Q=
 
-i18next@^20.2.1:
-  version "20.6.1"
-  resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.6.1.tgz#535e5f6e5baeb685c7d25df70db63bf3cc0aa345"
-  integrity sha512-yCMYTMEJ9ihCwEQQ3phLo7I/Pwycf8uAx+sRHwwk5U9Aui/IZYgQRyMqXafQOw5QQ7DM1Z+WyEXWIqSuJHhG2A==
+i18next@^21.2.0:
+  version "21.2.0"
+  resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.2.0.tgz#07346948d8c1e7be91518897839f8158c50dd76d"
+  integrity sha512-I2BLM/YNyEjCzRak1kL3uYYawwkfduQbNramMx7uMjjbos3zGZB1ag6dgbdoTR0baR34fpG4X4oWKaQExNo8wg==
   dependencies:
     "@babel/runtime" "^7.12.0"
 
@@ -3842,15 +3842,15 @@ mktemp@~0.4.0:
   integrity sha1-bQUVYRyKjITkhKogABKbmOmB/ws=
 
 mocha@^9.0.0:
-  version "9.1.1"
-  resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.1.1.tgz#33df2eb9c6262434630510c5f4283b36efda9b61"
-  integrity sha512-0wE74YMgOkCgBUj8VyIDwmLUjTsS13WV1Pg7l0SHea2qzZzlq7MDnfbPsHKcELBRk3+izEVkRofjmClpycudCA==
+  version "9.1.2"
+  resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.1.2.tgz#93f53175b0f0dc4014bd2d612218fccfcf3534d3"
+  integrity sha512-ta3LtJ+63RIBP03VBjMGtSqbe6cWXRejF9SyM9Zyli1CKZJZ+vfCTj3oW24V7wAphMJdpOFLoMI3hjJ1LWbs0w==
   dependencies:
     "@ungap/promise-all-settled" "1.1.2"
     ansi-colors "4.1.1"
     browser-stdout "1.3.1"
     chokidar "3.5.2"
-    debug "4.3.1"
+    debug "4.3.2"
     diff "5.0.0"
     escape-string-regexp "4.0.0"
     find-up "5.0.0"
@@ -3861,12 +3861,11 @@ mocha@^9.0.0:
     log-symbols "4.1.0"
     minimatch "3.0.4"
     ms "2.1.3"
-    nanoid "3.1.23"
+    nanoid "3.1.25"
     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.5"
     yargs "16.2.0"
     yargs-parser "20.2.4"
@@ -3902,10 +3901,10 @@ nan@^2.12.1:
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01"
   integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==
 
-nanoid@3.1.23:
-  version "3.1.23"
-  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.23.tgz#f744086ce7c2bc47ee0a8472574d5c78e4183a81"
-  integrity sha512-FiB0kzdP0FFVGDKlRLEQ1BgDzU87dy5NnzjeW9YZNt+/c3+q82EQDUwniSAUxp/F0gFNI1ZhKU1FqYsMuqZVnw==
+nanoid@3.1.25:
+  version "3.1.25"
+  resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.25.tgz#09ca32747c0e543f0e1814b7d3793477f9c8e152"
+  integrity sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==
 
 nanomatch@^1.2.9:
   version "1.2.13"
@@ -5075,7 +5074,7 @@ string-width@^1.0.1, string-width@^1.0.2:
     is-fullwidth-code-point "^1.0.0"
     strip-ansi "^3.0.0"
 
-"string-width@^1.0.2 || 2", string-width@^2.1.0:
+string-width@^2.1.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e"
   integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==
@@ -5255,9 +5254,9 @@ tmp@^0.1.0:
     rimraf "^2.6.3"
 
 tmpl@1.0.x:
-  version "1.0.4"
-  resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1"
-  integrity sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc"
+  integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==
 
 to-absolute-glob@^2.0.0:
   version "2.0.2"
@@ -5645,13 +5644,6 @@ which@^1.2.14, which@^1.2.9:
   dependencies:
     isexe "^2.0.0"
 
-wide-align@1.1.3:
-  version "1.1.3"
-  resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"
-  integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==
-  dependencies:
-    string-width "^1.0.2 || 2"
-
 wordwrap@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"


Test patch:
diff --git a/test/helpers/hasRelatedPluralKey.test.js b/test/helpers/hasRelatedPluralKey.test.js
new file mode 100644
index 0000000..4793e22
--- /dev/null
+++ b/test/helpers/hasRelatedPluralKey.test.js
@@ -0,0 +1,47 @@
+import { assert } from 'chai'
+import { hasRelatedPluralKey } from '../../src/helpers'
+
+describe('hasRelatedPluralKey helper function', () => {
+  it('return false when `source` does not contain valid plural form of `rawKey`', (done) => {
+    const rawKey = 'key1_'
+    const source = {
+      key1: '',
+      key1_: '',
+      key1_0: '',
+      key1_1: '',
+      key1_three: '',
+    }
+    const res = hasRelatedPluralKey(rawKey, source)
+
+    assert.strictEqual(res, false)
+    done()
+  })
+
+  it('return true when `source` contains any valid plural form of `rawKey`', (done) => {
+    const rawKey = 'key1_'
+    const sources = [
+      { key1_zero: '' },
+      { key1_one: '' },
+      { key1_two: '' },
+      { key1_few: '' },
+      { key1_many: '' },
+      { key1_other: '' },
+      {
+        key1_zero: '',
+        key1_one: '',
+        key1_two: '',
+        key1_few: '',
+        key1_many: '',
+        key1_other: '',
+      },
+    ]
+
+    sources.forEach((source) => {
+      const res = hasRelatedPluralKey(rawKey, source)
+
+      assert.strictEqual(res, true)
+    })
+
+    done()
+  })
+})
diff --git a/test/helpers/mergeHashes.test.js b/test/helpers/mergeHashes.test.js
index 113fd00..6829717 100644
--- a/test/helpers/mergeHashes.test.js
+++ b/test/helpers/mergeHashes.test.js
@@ -61,7 +61,7 @@ describe('mergeHashes helper function', () => {
       key4: { key41: 'value41' },
     }
     const target = { key1: '', key3: '' }
-    const res = mergeHashes(source, target, true)
+    const res = mergeHashes(source, target, { keepRemoved: true })
 
     assert.deepEqual(res.new, {
       key1: 'value1',
@@ -76,12 +76,34 @@ describe('mergeHashes helper function', () => {
     done()
   })
 
+  it('copies `source` nested keys to `target` regardless of presence when `keepRemoved` is enabled', (done) => {
+    const source = {
+      key1: 'value1',
+      key2: 'value2',
+      key4: { key41: 'value41' },
+    }
+    const target = { key1: '', key3: '', key4: { key42: '' } }
+    const res = mergeHashes(source, target, { keepRemoved: true })
+
+    assert.deepEqual(res.new, {
+      key1: 'value1',
+      key2: 'value2',
+      key3: '',
+      key4: { key41: 'value41', key42: '' },
+    })
+    assert.deepEqual(res.old, {})
+    assert.strictEqual(res.mergeCount, 1)
+    assert.strictEqual(res.pullCount, 0)
+    assert.strictEqual(res.oldCount, 2)
+    done()
+  })
+
   it('restores plural keys when the singular one exists', (done) => {
-    const source = { key1: '', key1_plural: 'value1' }
-    const target = { key1: '' }
+    const source = { key1_one: '', key1_other: 'value1' }
+    const target = { key1_one: '' }
     const res = mergeHashes(source, target)
 
-    assert.deepEqual(res.new, { key1: '', key1_plural: 'value1' })
+    assert.deepEqual(res.new, { key1_one: '', key1_other: 'value1' })
     assert.deepEqual(res.old, {})
     assert.strictEqual(res.mergeCount, 1)
     assert.strictEqual(res.pullCount, 1)
@@ -90,12 +112,12 @@ describe('mergeHashes helper function', () => {
   })
 
   it('does not restore plural keys when the singular one does not', (done) => {
-    const source = { key1: '', key1_plural: 'value1' }
+    const source = { key1_one: '', key1_other: 'value1' }
     const target = { key2: '' }
     const res = mergeHashes(source, target)
 
     assert.deepEqual(res.new, { key2: '' })
-    assert.deepEqual(res.old, { key1: '', key1_plural: 'value1' })
+    assert.deepEqual(res.old, { key1_one: '', key1_other: 'value1' })
     assert.strictEqual(res.mergeCount, 0)
     assert.strictEqual(res.pullCount, 0)
     assert.strictEqual(res.oldCount, 2)
diff --git a/test/lexers/handlebars-lexer.test.js b/test/lexers/handlebars-lexer.test.js
index d2bd796..752f2f6 100644
--- a/test/lexers/handlebars-lexer.test.js
+++ b/test/lexers/handlebars-lexer.test.js
@@ -70,6 +70,15 @@ describe('HandlebarsLexer', () => {
     done()
   })
 
+  it('extracts boolean options', (done) => {
+    const Lexer = new HandlebarsLexer()
+    const content = '<p>{{t "first" ordinal="true" custom="false"}}</p>'
+    assert.deepEqual(Lexer.extract(content), [
+      { key: 'first', ordinal: true, custom: false },
+    ])
+    done()
+  })
+
   describe('parseArguments()', () => {
     it('matches string arguments', (done) => {
       const Lexer = new HandlebarsLexer()
diff --git a/test/lexers/html-lexer.test.js b/test/lexers/html-lexer.test.js
index da06929..6336053 100644
--- a/test/lexers/html-lexer.test.js
+++ b/test/lexers/html-lexer.test.js
@@ -88,4 +88,14 @@ describe('HTMLLexer', () => {
     ])
     done()
   })
+
+  it('extracts boolean options', (done) => {
+    const Lexer = new HTMLLexer()
+    const content =
+      '<p data-i18n="first" data-i18n-options=\'{"ordinal": true, "custom": false}\'>first</p>'
+    assert.deepEqual(Lexer.extract(content), [
+      { key: 'first', ordinal: true, custom: false },
+    ])
+    done()
+  })
 })
diff --git a/test/lexers/javascript-lexer.test.js b/test/lexers/javascript-lexer.test.js
index 1f84c38..9ba148e 100644
--- a/test/lexers/javascript-lexer.test.js
+++ b/test/lexers/javascript-lexer.test.js
@@ -258,6 +258,19 @@ describe('JavascriptLexer', () => {
     ])
   })
 
+  it('extracts boolean options', () => {
+    const Lexer = new JavascriptLexer()
+
+    const content = 'i18n.t("headline", {ordinal: true, custom: false});'
+    assert.deepEqual(Lexer.extract(content), [
+      {
+        key: 'headline',
+        ordinal: true,
+        custom: false,
+      },
+    ])
+  })
+
   it('emits warnings on dynamic keys', () => {
     const Lexer = new JavascriptLexer()
     const content =
diff --git a/test/lexers/jsx-lexer.test.js b/test/lexers/jsx-lexer.test.js
index 4224e60..9a33313 100644
--- a/test/lexers/jsx-lexer.test.js
+++ b/test/lexers/jsx-lexer.test.js
@@ -77,6 +77,22 @@ describe('JsxLexer', () => {
       done()
     })
 
+    it('extracts boolean attributes', (done) => {
+      const Lexer = new JsxLexer()
+      const content =
+        '<Trans ordinal customTrue={true} customFalse={false}>Yo</Trans>'
+      assert.deepEqual(Lexer.extract(content), [
+        {
+          key: 'Yo',
+          defaultValue: 'Yo',
+          ordinal: true,
+          customTrue: true,
+          customFalse: false,
+        },
+      ])
+      done()
+    })
+
     it('extracts keys from Trans elements without an i18nKey', (done) => {
       const Lexer = new JsxLexer()
       const content = '<Trans count={count}>Yo</Trans>'
diff --git a/test/lexers/vue-lexer.test.js b/test/lexers/vue-lexer.test.js
index 2a5fdbe..09da2f3 100644
--- a/test/lexers/vue-lexer.test.js
+++ b/test/lexers/vue-lexer.test.js
@@ -62,4 +62,16 @@ describe('VueLexer', () => {
     ])
     done()
   })
+
+  it('extracts boolean options', (done) => {
+    const Lexer = new VueLexer()
+    const content =
+      "<template><p>{{ $t('first', {ordinal: true, custom: false}) }}</p><template><script>export default " +
+      "{ mounted() { this.$i18n.t('second'); } }</script>"
+    assert.deepEqual(Lexer.extract(content), [
+      { key: 'second' },
+      { key: 'first', ordinal: true, custom: false },
+    ])
+    done()
+  })
 })
diff --git a/test/locales/en/test_context_plural.json b/test/locales/en/test_context_plural.json
index 7d3586a..cd66a39 100644
--- a/test/locales/en/test_context_plural.json
+++ b/test/locales/en/test_context_plural.json
@@ -1,5 +1,5 @@
 {
   "first": "first",
-  "first_context1_plural": "first context1 plural",
-  "first_context2_plural_2": "first context2 plural 2"
+  "first_context1_other": "first context1 plural",
+  "first_context2_two": "first context2 plural 2"
 }
diff --git a/test/locales/en/test_plural.json b/test/locales/en/test_plural.json
index fc410e8..6600b48 100644
--- a/test/locales/en/test_plural.json
+++ b/test/locales/en/test_plural.json
@@ -1,7 +1,7 @@
 {
-  "first": "first",
-  "first_plural": "first plural",
-  "second": "second",
-  "second_0": "second plural 0",
-  "second_12": "second plural 12"
-}
\ No newline at end of file
+  "first_one": "first",
+  "first_other": "first plural",
+  "second_one": "second",
+  "second_zero": "second plural zero",
+  "second_other": "second plural other"
+}
diff --git a/test/parser.test.js b/test/parser.test.js
index 2b2a66e..09b9f4a 100644
--- a/test/parser.test.js
+++ b/test/parser.test.js
@@ -178,16 +178,16 @@ describe('parser', () => {
       first: '',
       second: '',
       third: {
-        first:
+        first_one:
           'Hello <1>{{name}}</1>, you have {{count}} unread message. <5>Go to messages</5>.',
-        first_plural:
+        first_other:
           'Hello <1>{{name}}</1>, you have {{count}} unread message. <5>Go to messages</5>.',
         second: " <1>Hello,</1> this shouldn't be trimmed.",
         third: "<0>Hello,</0>this should be trimmed.<2> and this shoudln't</2>",
       },
       fourth: '',
-      fifth: '',
-      fifth_plural: '',
+      fifth_one: '',
+      fifth_other: '',
       bar: '',
       foo: '',
       'This should be part of the value and the key':
@@ -223,16 +223,16 @@ describe('parser', () => {
       first: '',
       second: '',
       third: {
-        first:
+        first_one:
           'Hello <1>{{name}}</1>, you have {{count}} unread message. <5>Go to messages</5>.',
-        first_plural:
+        first_other:
           'Hello <1>{{name}}</1>, you have {{count}} unread message. <5>Go to messages</5>.',
         second: " <1>Hello,</1> this shouldn't be trimmed.",
         third: "<0>Hello,</0>this should be trimmed.<2> and this shoudln't</2>",
       },
       fourth: '',
-      fifth: '',
-      fifth_plural: '',
+      fifth_one: '',
+      fifth_other: '',
       bar: '',
       foo: '',
       'This should be part of the value and the key':
@@ -602,16 +602,18 @@ describe('parser', () => {
       output: 'test/locales/$LOCALE/$NAMESPACE.json',
     })
     const fakeFile = new Vinyl({
-      contents: Buffer.from("t('test_plural:first'); t('test_plural:second')"),
+      contents: Buffer.from(
+        "t('test_plural:first_one'); t('test_plural:second_one')"
+      ),
       path: 'file.js',
     })
 
     const expectedResult = {
-      first: 'first',
-      first_plural: 'first plural',
-      second: 'second',
-      second_0: 'second plural 0',
-      second_12: 'second plural 12',
+      first_one: 'first',
+      first_other: 'first plural',
+      second_one: 'second',
+      second_zero: 'second plural zero',
+      second_other: 'second plural other',
     }
 
     i18nextParser.on('data', (file) => {
@@ -639,7 +641,8 @@ describe('parser', () => {
 
     const expectedResult = {
       first: 'first',
-      first_context1_plural: 'first context1 plural',
+      first_context1_other: 'first context1 plural',
+      first_context2_two: 'first context2 plural 2',
     }
 
     i18nextParser.on('data', (file) => {
@@ -901,17 +904,17 @@ describe('parser', () => {
         first: '',
         second: '',
         third: {
-          first:
+          first_one:
             'Hello <1>{{name}}</1>, you have {{count}} unread message. <5>Go to messages</5>.',
-          first_plural:
+          first_other:
             'Hello <1>{{name}}</1>, you have {{count}} unread message. <5>Go to messages</5>.',
           second: " <1>Hello,</1> this shouldn't be trimmed.",
           third:
             "<0>Hello,</0>this should be trimmed.<2> and this shoudln't</2>",
         },
         fourth: '',
-        fifth: '',
-        fifth_plural: '',
+        fifth_one: '',
+        fifth_other: '',
         bar: '',
         foo: '',
         'This should be part of the value and the key':
@@ -957,17 +960,17 @@ describe('parser', () => {
         first: '',
         second: '',
         third: {
-          first:
+          first_one:
             'Hello <1>{{name}}</1>, you have {{count}} unread message. <5>Go to messages</5>.',
-          first_plural:
+          first_other:
             'Hello <1>{{name}}</1>, you have {{count}} unread message. <5>Go to messages</5>.',
           second: " <b>Hello,</b> this shouldn't be trimmed.",
           third:
             "<b>Hello,</b>this should be trimmed.<2> and this shoudln't</2>",
         },
         fourth: '',
-        fifth: '',
-        fifth_plural: '',
+        fifth_one: '',
+        fifth_other: '',
         bar: '',
         foo: '',
         'This should be part of the value and the key':
@@ -1218,8 +1221,8 @@ describe('parser', () => {
       })
       i18nextParser.once('end', () => {
         assert.deepEqual(result, {
-          'test {{count}}': '',
-          'test {{count}}_plural': '',
+          'test {{count}}_one': '',
+          'test {{count}}_other': '',
         })
         done()
       })
@@ -1244,8 +1247,8 @@ describe('parser', () => {
       })
       i18nextParser.once('end', () => {
         assert.deepEqual(result, {
-          'test {{count}}': '',
-          'test {{count}}~~~plural': '',
+          'test {{count}}~~~one': '',
+          'test {{count}}~~~other': '',
         })
         done()
       })
@@ -1270,8 +1273,8 @@ describe('parser', () => {
       })
       i18nextParser.once('end', () => {
         assert.deepEqual(result, {
-          'test {{count}}': '',
-          'test {{count}}_plural': '',
+          'test {{count}}_one': '',
+          'test {{count}}_other': '',
         })
         done()
       })
@@ -1294,12 +1297,12 @@ describe('parser', () => {
       })
       i18nextParser.once('end', () => {
         assert.deepEqual(result, {
-          'test {{count}}_0': '',
-          'test {{count}}_1': '',
-          'test {{count}}_2': '',
-          'test {{count}}_3': '',
-          'test {{count}}_4': '',
-          'test {{count}}_5': '',
+          'test {{count}}_zero': '',
+          'test {{count}}_one': '',
+          'test {{count}}_two': '',
+          'test {{count}}_few': '',
+          'test {{count}}_many': '',
+          'test {{count}}_other': '',
         })
         done()
       })
@@ -1324,8 +1327,8 @@ describe('parser', () => {
       })
       i18nextParser.once('end', () => {
         assert.deepEqual(result, {
-          'test {{count}}': 'test {{count}}',
-          'test {{count}}_plural': 'test {{count}}',
+          'test {{count}}_one': 'test {{count}}',
+          'test {{count}}_other': 'test {{count}}',
         })
         done()
       })
@@ -1348,8 +1351,8 @@ describe('parser', () => {
       })
       i18nextParser.once('end', () => {
         assert.deepEqual(result, {
-          key: 'test {{count}}',
-          key_plural: 'test {{count}}',
+          key_one: 'test {{count}}',
+          key_other: 'test {{count}}',
         })
         done()
       })
@@ -1362,7 +1365,7 @@ describe('parser', () => {
       const i18nextParser = new i18nTransform()
       const fakeFile = new Vinyl({
         contents: Buffer.from(
-          "t('key', { count, defaultValue: 'singular {{count}}', defaultValue_plural: 'plural {{count}}' })"
+          "t('key', { count, defaultValue: 'singular {{count}}', defaultValue_other: 'plural {{count}}' })"
         ),
         path: 'file.js',
       })
@@ -1374,8 +1377,8 @@ describe('parser', () => {
       })
       i18nextParser.once('end', () => {
         assert.deepEqual(result, {
-          key: 'singular {{count}}',
-          key_plural: 'plural {{count}}',
+          key_one: 'singular {{count}}',
+          key_other: 'plural {{count}}',
         })
         done()
       })
@@ -1393,10 +1396,10 @@ describe('parser', () => {
           `t('key', {
             count,
             defaultValue: 'default for missing',
-            defaultValue_0: 'zero {{count}}',
-            defaultValue_1: 'one {{count}}',
-            defaultValue_2: 'two {{count}}',
-            defaultValue_3: 'three-ten {{count}}',
+            defaultValue_zero: 'zero {{count}}',
+            defaultValue_one: 'one {{count}}',
+            defaultValue_two: 'two {{count}}',
+            defaultValue_few: 'three-ten {{count}}',
           })`
         ),
         path: 'file.js',
@@ -1409,13 +1412,13 @@ describe('parser', () => {
       })
       i18nextParser.once('end', () => {
         assert.deepEqual(result, {
-          key_0: 'zero {{count}}',
-          key_1: 'one {{count}}',
-          key_2: 'two {{count}}',
-          key_3: 'three-ten {{count}}',
+          key_zero: 'zero {{count}}',
+          key_one: 'one {{count}}',
+          key_two: 'two {{count}}',
+          key_few: 'three-ten {{count}}',
           // Missing forms fall back on default
-          key_4: 'default for missing',
-          key_5: 'default for missing',
+          key_many: 'default for missing',
+          key_other: 'default for missing',
         })
         done()
       })
@@ -1441,12 +1444,40 @@ describe('parser', () => {
       })
       i18nextParser.once('end', () => {
         assert.deepEqual(result, {
-          'test {{count}}_0': 'test {{count}}',
-          'test {{count}}_1': 'test {{count}}',
-          'test {{count}}_2': 'test {{count}}',
-          'test {{count}}_3': 'test {{count}}',
-          'test {{count}}_4': 'test {{count}}',
-          'test {{count}}_5': 'test {{count}}',
+          'test {{count}}_zero': 'test {{count}}',
+          'test {{count}}_one': 'test {{count}}',
+          'test {{count}}_two': 'test {{count}}',
+          'test {{count}}_few': 'test {{count}}',
+          'test {{count}}_many': 'test {{count}}',
+          'test {{count}}_other': 'test {{count}}',
+        })
+        done()
+      })
+
+      i18nextParser.end(fakeFile)
+    })
+
+    it('generates ordinal', (done) => {
+      let result
+      const i18nextParser = new i18nTransform()
+      const fakeFile = new Vinyl({
+        contents: Buffer.from(
+          "t('test {{count}}', { count: 1, ordinal: true })"
+        ),
+        path: 'file.js',
+      })
+
+      i18nextParser.on('data', (file) => {
+        if (file.relative.endsWith(enLibraryPath)) {
+          result = JSON.parse(file.contents)
+        }
+      })
+      i18nextParser.once('end', () => {
+        assert.deepEqual(result, {
+          'test {{count}}_one': '',
+          'test {{count}}_two': '',
+          'test {{count}}_few': '',
+          'test {{count}}_other': '',
         })
         done()
       })
@@ -1528,8 +1559,37 @@ describe('parser', () => {
       i18nextParser.end(fakeFile)
     })
 
+    it('supports keepRemoved option', (done) => {
+      let result, resultOld
+      const i18nextParser = new i18nTransform({
+        keepRemoved: true,
+        output: 'test/locales/$LOCALE/$NAMESPACE.json',
+      })
+      const fakeFile = new Vinyl({
+        contents: Buffer.from("t('test_merge:first'); t('test_merge:second')"),
+        path: 'file.js',
+      })
+
+      i18nextParser.on('data', (file) => {
+        if (file.relative.endsWith(path.normalize('en/test_merge.json'))) {
+          result = JSON.parse(file.contents)
+        } else if (
+          file.relative.endsWith(path.normalize('en/test_merge_old.json'))
+        ) {
+          resultOld = JSON.parse(file.contents)
+        }
+      })
+      i18nextParser.once('end', () => {
+        assert.deepEqual(result, { first: 'first', second: '', third: 'third' })
+        assert.deepEqual(resultOld, undefined)
+        done()
+      })
+
+      i18nextParser.end(fakeFile)
+    })
+
     describe('lexers', () => {
-      it('support custom lexers options', (done) => {
+      it('supports custom lexers options', (done) => {
         let result
         const i18nextParser = new i18nTransform({
           lexers: {
@@ -1559,7 +1619,7 @@ describe('parser', () => {
         i18nextParser.end(fakeFile)
       })
 
-      it('support function as custom lexer', (done) => {
+      it('supports function as custom lexer', (done) => {
         class CustomLexer {
           extract(content) {
             return content.split(';').map((key) => ({ key }))