Repository: i18next/i18next-parser
Pull request #423 (2021-09-20 16:57:55)

Add support for JSON v4
### Why am I submitting this PR

Adding support for [JSON v4](https://www.i18next.com/misc/json-format#i-18-next-json-v4)

### Does it fix an existing ticket?

No

### Checklist

- [x] only relevant code is changed (make a diff before you submit the PR)
- [x] tests are included and pass (see [details here](https://github.com/i18next/i18next-parser/blob/master/docs/development.md#tests))
- [x] documentation is changed or added
- [x] I ran `yarn build` to compile and prettify the code



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..e77882b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -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"
 


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 }))