Repository: gilbsgilbs/babel-plugin-i18next-extract (ts)

Issue:
Support i18next JSON v4's format
**Is your feature request related to a problem? Please describe.**

The current JSON format is v3 as mentioned in the README, but since late september i18next has gone into v21, and [now uses a new v4 format](https://www.i18next.com/misc/json-format#i-18-next-json-v4) (for pluralization, among other things, see [their migration guide](https://www.i18next.com/misc/migration-guide#v-20-x-x-to-v-21-0-0)).

As long as the output format of this extractor will be based on v3 instead of the new v4 JSON format, updating i18next in v21 is not possible.

**Describe the solution you'd like**

Support the new v4 JSON format that comes with i18next v21+.

**Additional context**

Thanks for your work on this lib, it's greatly appreciated! I hope you'll find the time to perform the upgrade 💪

Pull request description:
feat: add support for JSONv4
Also force using JSONv4 for ICU as it makes the plural categories more
correct. This is technically a breaking change, but as the feature was
explicitely marked as expermiental, I wouldn't worry too much.

I have yet to decide if JSONv4 should be made the default now (which
would require a major version increase). We surely want new users to use
JSONv4 by default, but a major version increase might be an opportunity
to include other changes.

closes #203

Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index ca85d3e..fc867f9 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -112,8 +112,8 @@ informed that you're working on it.
 - `exporters/`: contains code related to exporting extracted translations to files.
     - `commons.ts`: Types and exceptions definitions for extractors.
     - `index.ts`: Entrypoint for exporters. Calls and uses the proper extractor implementation
-      (only JSONv3 at the moment).
-    - `jsonv3.ts`: [JSONv3](https://www.i18next.com/misc/json-format#i-18-next-json-v3) extractor
+      (only JSONv3 and JSONv4 at the moment).
+    - `json.ts`: [JSONv4](https://www.i18next.com/misc/json-format#i18next-json-v4) extractor
       implementation.
 - `comments.ts` parses the comment hints.
 - `config.ts` parses babel configuration options.
diff --git a/README.md b/README.md
index 07986a4..65e9de4 100644
--- a/README.md
+++ b/README.md
@@ -19,7 +19,7 @@ traverse your Javascript/Typescript code in order to find i18next translation ke
 
 ## Features
 
-- ✅ Keys extraction in [JSON v3 format](https://www.i18next.com/misc/json-format).
+- ✅ Keys extraction in [JSONv4 format](https://www.i18next.com/misc/json-format).
 - ✅ Detection of `i18next.t()` function calls.
 - ✅ Full [react-i18next](https://react.i18next.com/) support.
 - ✅ Plurals support.
diff --git a/docs/templates/configuration.md.yml b/docs/templates/configuration.md.yml
index 323a678..16ad11b 100644
--- a/docs/templates/configuration.md.yml
+++ b/docs/templates/configuration.md.yml
@@ -42,6 +42,13 @@ configOptions:
       See [react-i18next Trans Component](
       https://react.i18next.com/latest/trans-component#additional-options-on-i-18-next-init).
     defaultValue: "['br', 'strong', 'i', 'p']"
+  - name: compatibilityJSON
+    type: '"v3" | "v4"'
+    description: |
+      Compatibility mode of the output JSON file. This option only changes the way plurals are exported.
+
+      See [JSONv* format documentation](https://www.i18next.com/misc/json-format#i18next-json-v4) and [why this option exists](https://www.i18next.com/misc/migration-guide#json-format-v4-pluralization).
+    defaultValue: '"v3"'
   - name: i18nextInstanceNames
     type: "string[]"
     description: |
@@ -330,7 +337,7 @@ configOptions:
     type: "boolean"
     description: |
       Enable experimental support for [ICU message syntax](
-      http://userguide.icu-project.org/formatparse/messages). Only plurals are supported for now.
+      http://userguide.icu-project.org/formatparse/messages). Only plurals and JSONv4 are supported for now.
 
       ☠️  *This feature is incomplete and might change or move into an third-party module in the
       future. Stay tuned.* ☠️
diff --git a/src/config.ts b/src/config.ts
index 301b8e9..914d476 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -9,6 +9,7 @@ export interface Config {
   keySeparator: string | null;
   nsSeparator: string | null;
   transKeepBasicHtmlNodesFor: string[];
+  compatibilityJSON: 'v3' | 'v4';
 
   // plugin-specific options
   i18nextInstanceNames: string[];
@@ -71,6 +72,8 @@ export function parseConfig(opts: Partial<Config>): Config {
       'i',
       'p',
     ]),
+    // FIXME we need to default to v4 in the next major release.
+    compatibilityJSON: coalesce(opts.compatibilityJSON, 'v3'),
 
     i18nextInstanceNames: coalesce(opts.i18nextInstanceNames, [
       'i18next',
diff --git a/src/exporters/index.ts b/src/exporters/index.ts
index 4ace6b4..66f1926 100644
--- a/src/exporters/index.ts
+++ b/src/exporters/index.ts
@@ -7,7 +7,7 @@ import { Config } from '../config';
 import { TranslationKey } from '../keys';
 
 import { ConflictError, Exporter, ExportError } from './commons';
-import jsonv3Exporter from './jsonv3';
+import jsonExporter from './json';
 
 export { ConflictError, ExportError };
 
@@ -119,7 +119,7 @@ export default function exportTranslationKeys(
 ): void {
   const keysPerFilepath: { [path: string]: TranslationKey[] } = {};
 
-  const exporter = jsonv3Exporter;
+  const exporter = jsonExporter;
 
   for (const key of keys) {
     // Figure out in which path each key should go.
diff --git a/src/exporters/jsonv3.ts b/src/exporters/json.ts
similarity index 75%
rename from src/exporters/jsonv3.ts
rename to src/exporters/json.ts
index e0386f1..612e7e0 100644
--- a/src/exporters/jsonv3.ts
+++ b/src/exporters/json.ts
@@ -3,49 +3,49 @@ import stringify from 'json-stable-stringify';
 import { Exporter, ConflictError } from './commons';
 
 /**
- * JSONv3 values can be any valid value for JSON file.
+ * JSONv* values can be any valid value for JSON file.
  *
  * See i18next's "returnObjects" option.
  */
-type JsonV3Value =
+type JsonVxValue =
   | string
   | number
   | null
-  | JsonV3Value[]
-  | { [k: string]: JsonV3Value };
+  | JsonVxValue[]
+  | { [k: string]: JsonVxValue };
 
 /**
- * Content of a JSON v3 file.
+ * Content of a JSONv* file.
  */
-interface JsonV3File {
+interface JsonVxFile {
   whitespacesBefore: string;
   whitespacesAfter: string;
-  content: { [k: string]: JsonV3Value };
+  content: { [k: string]: JsonVxValue };
 }
 
 /**
- * Check whether a JsonV3Value is a plain object.
+ * Check whether a JsonVxValue is a plain object.
  */
-function jsonV3ValueIsObject(
-  val: JsonV3Value,
-): val is { [k: string]: JsonV3Value } {
+function jsonVxValueIsObject(
+  val: JsonVxValue,
+): val is { [k: string]: JsonVxValue } {
   return typeof val === 'object' && val !== null && !Array.isArray(val);
 }
 
 /**
- * Add a key recursively to a JSONv3 file.
+ * Add a key recursively to a JSONv4 file.
  *
- * @param fileContent JSONv3 file content
+ * @param fileContent JSONv* file content
  * @param keyPath keyPath of the key to add
  * @param cleanKey key without path
  * @param value Value to set for the key.
  */
 function recursiveAddKey(
-  fileContent: JsonV3File['content'],
+  fileContent: JsonVxFile['content'],
   keyPath: string[],
   cleanKey: string,
-  value: JsonV3Value,
-): JsonV3File['content'] {
+  value: JsonVxValue,
+): JsonVxFile['content'] {
   if (keyPath.length === 0) {
     return {
       ...fileContent,
@@ -57,7 +57,7 @@ function recursiveAddKey(
   let current = fileContent[currentKeyPath];
   if (current === undefined) {
     current = {};
-  } else if (!jsonV3ValueIsObject(current)) {
+  } else if (!jsonVxValueIsObject(current)) {
     throw new ConflictError();
   }
 
@@ -72,7 +72,7 @@ function recursiveAddKey(
   };
 }
 
-const jsonv3Exporter: Exporter<JsonV3File, JsonV3Value> = {
+const exporter: Exporter<JsonVxFile, JsonVxValue> = {
   init: () => {
     return {
       whitespacesBefore: '',
@@ -104,7 +104,7 @@ const jsonv3Exporter: Exporter<JsonV3File, JsonV3Value> = {
       const val = current[p];
       if (val === undefined) {
         return undefined;
-      } else if (!jsonV3ValueIsObject(val)) {
+      } else if (!jsonVxValueIsObject(val)) {
         throw new ConflictError();
       }
       current = val;
@@ -121,4 +121,4 @@ const jsonv3Exporter: Exporter<JsonV3File, JsonV3Value> = {
   },
 };
 
-export default jsonv3Exporter;
+export default exporter;
diff --git a/src/index.ts b/src/index.ts
index 3262a32..d0883fe 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,10 +1,3 @@
-import { init as i18nextInit } from 'i18next';
-
 import plugin from './plugin';
 
-i18nextInit({
-  // FIXME https://github.com/gilbsgilbs/babel-plugin-i18next-extract/issues/203
-  compatibilityJSON: 'v3',
-});
-
 export default plugin;
diff --git a/src/keys.ts b/src/keys.ts
index 0f8e909..94d026e 100644
--- a/src/keys.ts
+++ b/src/keys.ts
@@ -1,5 +1,5 @@
 import * as BabelTypes from '@babel/types';
-import i18next from 'i18next';
+import * as i18next from 'i18next';
 
 import { Config } from './config';
 
@@ -84,6 +84,13 @@ function parseExtractedKey(key: ExtractedKey, config: Config): TranslationKey {
  *   ({'bar', {contexts: ['male', 'female'], hasCount: true}}, 'en')
  *     => ['foo_male', 'foo_male_plural', 'foo_female', 'foo_female_plural']
  *
+ * FIXME: some of the work of this function should be delegated to the exporter.
+ * This function should just put derivation metadata (each plural for the current
+ * locale and each context) into an attribute of the key. The exporter should then
+ * produce the actual value of the key using the metadata from the TranslationKey.
+ * This would remove the need to specify JSONvX-specific code into this
+ * function, but it will be easier to do when we drop support for JSONv3.
+ *
  * @param extractedKey key that was extracted with an extractor.
  * @param locale locale code
  * @returns All derived keys that could be found from TranslationKey for
@@ -116,19 +123,57 @@ export function computeDerivedKeys(
   }
 
   if (parsedOptions.hasCount) {
+    const i18n = i18next.createInstance({
+      pluralSeparator: config.pluralSeparator,
+      // i18next doesn't allow passing "v4" as it's not "compat" mode.
+      compatibilityJSON: config.compatibilityJSON === 'v3' ? 'v3' : undefined,
+    });
+    // We need to init the i18n instance to have all the services initialized.
+    i18n.init();
+
+    const unknownLocaleError = new Error(`Unknown locale '${locale}'.`);
+    let pluralCategories: string[];
+
     // See https://www.i18next.com/translation-function/plurals#how-to-find-the-correct-plural-suffix
-    const pluralRule = i18next.services.pluralResolver.getRule(locale);
+
+    // - i18next v21+ w/ JSONv4+: Intl.PluralRules is *always* returned even
+    //   if the locale is unknown.
+    // - JSONv3- (independently of i18next version): some custom untyped object
+    //   is returned, or undefined if the local is unknown.
+    const pluralRule:
+      | Intl.PluralRules
+      | { numbers: Array<number> }
+      | undefined = i18n.services.pluralResolver.getRule(
+      locale,
+      // TODO: a comment hint should allow to override cardinality/ordinality.
+      // It defaults to cardinal, but this is not correct.
+      { ordinal: false },
+    );
     if (pluralRule === undefined) {
-      throw new Error(`Locale '${locale}' does not exist.`);
+      throw unknownLocaleError;
+    } else if (pluralRule instanceof Intl.PluralRules) {
+      const pluralRulesOptions = pluralRule.resolvedOptions();
+      if (pluralRulesOptions.locale !== locale) {
+        throw unknownLocaleError;
+      }
+      pluralCategories = pluralRulesOptions.pluralCategories;
+    } else {
+      const pluralsCount = pluralRule.numbers.length;
+      if (pluralsCount === 2) {
+        pluralCategories = ['', 'plural'];
+      } else {
+        pluralCategories = Array(pluralsCount)
+          .fill(null)
+          .map((_, idx) => idx.toString());
+      }
     }
-    const numberOfPlurals = pluralRule.numbers.length;
 
     if (config.enableExperimentalIcu) {
-      const pluralNumbersAsText = Array.from<string>(
-        new Set(pluralRule.numbers.map(pluralNumberToText)),
-      );
+      if (config.compatibilityJSON === 'v3') {
+        throw new Error('ICU format is only supported with JSONv4.');
+      }
 
-      const icuPlurals = pluralNumbersAsText
+      const icuPlurals = pluralCategories
         .map(
           (numAsText: string) =>
             `${numAsText} {${icuPluralValue(
@@ -139,59 +184,32 @@ export function computeDerivedKeys(
 
       extractedKey.parsedOptions.defaultValue = `{count, plural, ${icuPlurals}}`;
     } else {
-      if (numberOfPlurals === 1) {
-        keys = keys.map((k) => ({
-          ...k,
-          cleanKey: k.cleanKey + config.pluralSeparator + '0',
-          isDerivedKey: true,
-        }));
-      } else if (numberOfPlurals === 2) {
-        keys = keys.reduce(
-          (accumulator, k) => [
-            ...accumulator,
-            k,
-            {
-              ...k,
-              cleanKey: k.cleanKey + config.pluralSeparator + 'plural',
-              isDerivedKey: true,
-            },
-          ],
-          Array<TranslationKey>(),
-        );
-      } else {
-        keys = keys.reduce(
-          (accumulator, k) => [
-            ...accumulator,
-            ...Array(numberOfPlurals)
-              .fill(null)
-              .map((_, idx) => ({
-                ...k,
-                cleanKey: k.cleanKey + config.pluralSeparator + idx,
-                isDerivedKey: true,
-              })),
-          ],
-          Array<TranslationKey>(),
-        );
-      }
+      const pluralSuffixes = pluralCategories.map((cat) =>
+        cat.length === 0 ? '' : config.pluralSeparator + cat,
+      );
+      keys = keys.reduce(
+        (accumulator, k) => [
+          ...accumulator,
+          ...pluralSuffixes.map((suffix) => ({
+            ...k,
+            cleanKey: k.cleanKey + suffix,
+            // Let's not consider singular a derived key. This is useful if one
+            // want to use default values for singular.
+            isDerivedKey:
+              k.isDerivedKey ||
+              !['', `${config.pluralSeparator}_one`].includes(suffix),
+          })),
+        ],
+        Array<TranslationKey>(),
+      );
     }
   }
 
   return keys;
 }
 
-function pluralNumberToText(number: number): string {
-  switch (number) {
-    case 0:
-      return 'zero';
-    case 1:
-      return 'one';
-    default:
-      return 'other';
-  }
-}
-
 function icuPluralValue(defaultValue: string | null): string {
-  const oldVal = defaultValue || '';
+  const oldVal = defaultValue ?? '';
   const withIcuSingleCurlyBrace = oldVal
     .replace(/{{/g, '{')
     .replace(/}}/g, '}');


Test patch:
diff --git a/tests/__fixtures__/icu/icuPlurals.json b/tests/__fixtures__/icu/icuPlurals.json
index 85da4de..8879962 100644
--- a/tests/__fixtures__/icu/icuPlurals.json
+++ b/tests/__fixtures__/icu/icuPlurals.json
@@ -2,7 +2,8 @@
   "description": "ICU plurals test",
   "comment": "Can extract plurals into ICU format",
   "pluginOptions": {
-    "enableExperimentalIcu": true
+    "enableExperimentalIcu": true,
+    "compatibilityJSON": "v4"
   },
   "expectValues": [
     [
diff --git a/tests/__fixtures__/icu/jsonv3.js b/tests/__fixtures__/icu/jsonv3.js
new file mode 100644
index 0000000..f524ac2
--- /dev/null
+++ b/tests/__fixtures__/icu/jsonv3.js
@@ -0,0 +1,2 @@
+const count = 1;
+i18next.t("foo", {count, defaultValue: "The {{count}} item"});
diff --git a/tests/__fixtures__/icu/jsonv3.json b/tests/__fixtures__/icu/jsonv3.json
new file mode 100644
index 0000000..1c0b58d
--- /dev/null
+++ b/tests/__fixtures__/icu/jsonv3.json
@@ -0,0 +1,9 @@
+{
+  "description": "test icu plugin throws with JSONv3",
+  "pluginOptions": {
+    "enableExperimentalIcu": true,
+    "compatibilityJSON": "v3"
+  },
+  "expectValues": [],
+  "errorMessageRegexp": "ICU format is only supported with JSONv4\\.$"
+}
diff --git a/tests/__fixtures__/testLocale/plurals.json b/tests/__fixtures__/testLocale/plurals.json
index cf6ac06..c88a80b 100644
--- a/tests/__fixtures__/testLocale/plurals.json
+++ b/tests/__fixtures__/testLocale/plurals.json
@@ -1,14 +1,18 @@
 {
   "description": "plural tests in different languages",
-  "comment": "ms has 1 plural, fr has 2 plurals, ar has 6 plurals",
+  "comment": "ms has 1 plural, fr has 2 plurals (in JSONv3 only), ca has 2 plurals, ar has 6 plurals",
   "pluginOptions": {
-    "locales": ["fr", "ar", "ms"]
+    "locales": ["fr", "ca", "ar", "ms"]
   },
   "expectValues": [
     [
       {"myKey": "", "myKey_plural": ""},
       {"locale": "fr"}
     ],
+    [
+      {"myKey": "", "myKey_plural": ""},
+      {"locale": "ca"}
+    ],
     [
       {"myKey_0": "", "myKey_1": "", "myKey_2": "", "myKey_3": "", "myKey_4": "", "myKey_5": ""},
       {"locale": "ar"}
diff --git a/tests/__fixtures__/testLocale/pluralsJSONv4.js b/tests/__fixtures__/testLocale/pluralsJSONv4.js
new file mode 100644
index 0000000..287be0f
--- /dev/null
+++ b/tests/__fixtures__/testLocale/pluralsJSONv4.js
@@ -0,0 +1,1 @@
+i18next.t('myKey', {count: 22});
diff --git a/tests/__fixtures__/testLocale/pluralsJSONv4.json b/tests/__fixtures__/testLocale/pluralsJSONv4.json
new file mode 100644
index 0000000..228ae81
--- /dev/null
+++ b/tests/__fixtures__/testLocale/pluralsJSONv4.json
@@ -0,0 +1,22 @@
+{
+  "description": "plural tests in different languages",
+  "comment": "ms has 1 plural, ca has 2 plurals, ar has 6 plurals",
+  "pluginOptions": {
+    "locales": ["ca", "ar", "ms"],
+    "compatibilityJSON": "v4"
+  },
+  "expectValues": [
+    [
+      {"myKey_one": "", "myKey_other": ""},
+      {"locale": "ca"}
+    ],
+    [
+      {"myKey_zero": "", "myKey_one": "", "myKey_two": "", "myKey_few": "", "myKey_many": "", "myKey_other": ""},
+      {"locale": "ar"}
+    ],
+    [
+      {"myKey_other": ""},
+      {"locale": "ms"}
+    ]
+  ]
+}
diff --git a/tests/__fixtures__/testLocale/unknown.json b/tests/__fixtures__/testLocale/unknown.json
index ea8fcab..bb04601 100644
--- a/tests/__fixtures__/testLocale/unknown.json
+++ b/tests/__fixtures__/testLocale/unknown.json
@@ -4,5 +4,5 @@
     "locales": ["wtf"]
   },
   "expectValues": [],
-  "errorMessageRegexp": "Locale 'wtf' does not exist\\.$"
+  "errorMessageRegexp": "Unknown locale 'wtf'\\.$"
 }
diff --git a/tests/__fixtures__/testLocale/unknownJSONv4.js b/tests/__fixtures__/testLocale/unknownJSONv4.js
new file mode 100644
index 0000000..287be0f
--- /dev/null
+++ b/tests/__fixtures__/testLocale/unknownJSONv4.js
@@ -0,0 +1,1 @@
+i18next.t('myKey', {count: 22});
diff --git a/tests/__fixtures__/testLocale/unknownJSONv4.json b/tests/__fixtures__/testLocale/unknownJSONv4.json
new file mode 100644
index 0000000..0022061
--- /dev/null
+++ b/tests/__fixtures__/testLocale/unknownJSONv4.json
@@ -0,0 +1,9 @@
+{
+  "description": "test unknown locale",
+  "pluginOptions": {
+    "locales": ["wtf"],
+    "compatibilityJSON": "v4"
+  },
+  "expectValues": [],
+  "errorMessageRegexp": "Unknown locale 'wtf'\\.$"
+}
diff --git a/tests/exporters/jsonv3.test.ts b/tests/exporters/json.test.ts
similarity index 88%
rename from tests/exporters/jsonv3.test.ts
rename to tests/exporters/json.test.ts
index 4312023..8992d1f 100644
--- a/tests/exporters/jsonv3.test.ts
+++ b/tests/exporters/json.test.ts
@@ -1,13 +1,13 @@
 import { parseConfig } from '../../src/config';
 import { ConflictError } from '../../src/exporters';
-import jsonv3Exporter from '../../src/exporters/jsonv3';
+import exporter from '../../src/exporters/json';
 import { createTranslationKey } from '../helpers';
 
-describe('Test JSONv3 exporter', () => {
+describe('Test JSON exporter', () => {
   const config = parseConfig({ jsonSpace: 0 });
 
   it('can init', () => {
-    expect(jsonv3Exporter.init({ config })).toEqual({
+    expect(exporter.init({ config })).toEqual({
       whitespacesBefore: '',
       whitespacesAfter: '\n',
       content: {},
@@ -16,7 +16,7 @@ describe('Test JSONv3 exporter', () => {
 
   it('can parse', () => {
     expect(
-      jsonv3Exporter.parse({
+      exporter.parse({
         config,
         content: '\n\n\t\r\n  {"hello": "world"}\n\t  \r\n',
       }),
@@ -29,7 +29,7 @@ describe('Test JSONv3 exporter', () => {
 
   it('can stringify', () => {
     expect(
-      jsonv3Exporter.stringify({
+      exporter.stringify({
         config,
         file: {
           whitespacesBefore: '\n\n\t\r\n  ',
@@ -43,7 +43,7 @@ describe('Test JSONv3 exporter', () => {
   it('can stringify with custom spacing', () => {
     const config = parseConfig({ jsonSpace: 2 });
     expect(
-      jsonv3Exporter.stringify({
+      exporter.stringify({
         config,
         file: {
           whitespacesBefore: '',
@@ -56,7 +56,7 @@ describe('Test JSONv3 exporter', () => {
 
   it('can get simple key', () => {
     expect(
-      jsonv3Exporter.getKey({
+      exporter.getKey({
         config,
         keyPath: [],
         cleanKey: 'hello',
@@ -71,7 +71,7 @@ describe('Test JSONv3 exporter', () => {
 
   it('can get nested keys', () => {
     expect(
-      jsonv3Exporter.getKey({
+      exporter.getKey({
         config,
         keyPath: ['hello'],
         cleanKey: 'new',
@@ -86,7 +86,7 @@ describe('Test JSONv3 exporter', () => {
 
   it('can get undefined key', () => {
     expect(
-      jsonv3Exporter.getKey({
+      exporter.getKey({
         config,
         keyPath: ['what'],
         cleanKey: 'new',
@@ -101,7 +101,7 @@ describe('Test JSONv3 exporter', () => {
 
   it('throws ConflictError when path contains non JSON object', () => {
     expect(() =>
-      jsonv3Exporter.getKey({
+      exporter.getKey({
         config,
         keyPath: ['hello'],
         cleanKey: 'world',
@@ -114,7 +114,7 @@ describe('Test JSONv3 exporter', () => {
     ).toThrow(ConflictError);
 
     expect(() =>
-      jsonv3Exporter.getKey({
+      exporter.getKey({
         config,
         keyPath: ['hello'],
         cleanKey: 'world',
@@ -129,7 +129,7 @@ describe('Test JSONv3 exporter', () => {
 
   it('cannot get conflicting key', () => {
     expect(() =>
-      jsonv3Exporter.getKey({
+      exporter.getKey({
         config,
         keyPath: ['hello'],
         cleanKey: 'world',
@@ -144,7 +144,7 @@ describe('Test JSONv3 exporter', () => {
 
   it('can add key', () => {
     expect(
-      jsonv3Exporter.addKey({
+      exporter.addKey({
         config,
         file: { whitespacesBefore: '', whitespacesAfter: '', content: {} },
         key: createTranslationKey('hello'),
@@ -155,7 +155,7 @@ describe('Test JSONv3 exporter', () => {
 
   it('can add deep key', () => {
     expect(
-      jsonv3Exporter.addKey({
+      exporter.addKey({
         config,
         file: { whitespacesBefore: '', whitespacesAfter: '', content: {} },
         key: createTranslationKey('new', ['hello', 'brave']),
@@ -166,7 +166,7 @@ describe('Test JSONv3 exporter', () => {
 
   it('cannot add key in case of conflict', () => {
     expect(() =>
-      jsonv3Exporter.addKey({
+      exporter.addKey({
         config,
         file: {
           whitespacesBefore: '',