Repository: i18next/i18next-parser
Pull request #508 (2022-01-21 13:03:43)

6.0.0
- [x] Drop support for node 13 and 15
- [x] Add support for node 18
- [x] Update dependencies
- [x] Turn into pure ESM


Patch:
diff --git a/.babelrc b/.babelrc
index 5c4c5a0..af93977 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,6 +1,11 @@
 {
   "presets": [
-    "@babel/preset-env"
+    [
+      "@babel/preset-env",
+      {
+        "modules": false
+      }
+    ]
   ],
   "plugins": [
     "@babel/transform-runtime",
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 16c3da9..aa2f2f4 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -9,7 +9,7 @@ jobs:
     runs-on: ubuntu-latest
     strategy:
       matrix:
-        node-version: [12.x, 13.x, 14.x, 15.x, 16.x]
+        node-version: [12.x, 14.x, 16.x]
     steps:
     - uses: actions/checkout@v2
     - name: Use Node.js ${{ matrix.node-version }}
diff --git a/.lintstagedrc.js b/.lintstagedrc.js
index b5d308d..bf24669 100644
--- a/.lintstagedrc.js
+++ b/.lintstagedrc.js
@@ -1,3 +1,3 @@
-module.exports = {
+export default {
   '*.{js,css,md}': 'yarn prettify',
 }
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..6cc73f7
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,7 @@
+{
+  "trailingComma": "es5",
+  "semi": false,
+  "singleQuote": true,
+  "quoteProps": "as-needed",
+  "jsxSingleQuote": true
+}
diff --git a/.prettierrc.js b/.prettierrc.js
deleted file mode 100644
index 282779d..0000000
--- a/.prettierrc.js
+++ /dev/null
@@ -1,7 +0,0 @@
-module.exports = {
-  trailingComma: 'es5',
-  semi: false,
-  singleQuote: true,
-  quoteProps: 'as-needed',
-  jsxSingleQuote: true
-}
diff --git a/.vscode/launch.json b/.vscode/launch.json
index d5c5ebe..8c61659 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -17,7 +17,8 @@
         "--require", "@babel/register",
         "--require", "@babel/polyfill",
         "--plugins", "@babel/plugin-transform-runtime",
-        "${workspaceFolder}/test/**/*.test.js"
+        "${workspaceFolder}/test/**/*.test.js",
+        "${workspaceFolder}/test/*.test.js"
       ],
       "runtimeArgs": [
         "--nolazy"
diff --git a/README.md b/README.md
index 7bcbc52..3f0fc2e 100644
--- a/README.md
+++ b/README.md
@@ -65,16 +65,19 @@ npm install --save-dev i18next-parser
 [Gulp](http://gulpjs.com/) defines itself as the streaming build system. Put simply, it is like Grunt, but performant and elegant.
 
 ```javascript
-const i18nextParser = require('i18next-parser').gulp;
-
-gulp.task('i18next', function() {
-  gulp.src('app/**')
-    .pipe(new i18nextParser({
-      locales: ['en', 'de'],
-      output: 'locales/$LOCALE/$NAMESPACE.json'
-    }))
-    .pipe(gulp.dest('./'));
-});
+import { gulp as i18nextParser } from 'i18next-parser'
+
+gulp.task('i18next', function () {
+  gulp
+    .src('app/**')
+    .pipe(
+      new i18nextParser({
+        locales: ['en', 'de'],
+        output: 'locales/$LOCALE/$NAMESPACE.json',
+      })
+    )
+    .pipe(gulp.dest('./'))
+})
 ```
 
 **IMPORTANT**: `output` is required to know where to read the catalog from. You might think that `gulp.dest()` is enough though it does not inform the transform where to read the existing catalog from.
@@ -91,8 +94,8 @@ npm install --save-dev i18next-parser
 [Broccoli.js](https://github.com/broccolijs/broccoli) defines itself as a fast, reliable asset pipeline, supporting constant-time rebuilds and compact build definitions.
 
 ```javascript
-const Funnel = require('broccoli-funnel')
-const i18nextParser = require('i18next-parser').broccoli
+import Funnel from 'broccoli-funnel'
+import { broccoli as i18nextParser} from 'i18next-parser')
 
 const appRoot = 'broccoli'
 
@@ -105,7 +108,7 @@ i18n = new i18nextParser([i18n], {
   output: 'broccoli/locales/$LOCALE/$NAMESPACE.json'
 })
 
-module.exports = i18n
+export default i18n
 ```
 
 ## Options
@@ -115,7 +118,7 @@ Using a config file gives you fine-grained control over how i18next-parser treat
 ```js
 // i18next-parser.config.js
 
-module.exports = {
+export default {
   contextSeparator: '_',
   // Key separator used in your translation keys
 
@@ -153,7 +156,7 @@ module.exports = {
     jsx: ['JsxLexer'],
     tsx: ['JsxLexer'],
 
-    default: ['JavascriptLexer']
+    default: ['JavascriptLexer'],
   },
 
   lineEnding: 'auto',
@@ -217,8 +220,8 @@ module.exports = {
   // for that key across locales are reset to the default value, and existing translations are moved to
   // the `_old` file.
 
-  i18nextOptions: null
-  // If you wish to customize options in internally used i18next instance, you can define an object with any 
+  i18nextOptions: null,
+  // If you wish to customize options in internally used i18next instance, you can define an object with any
   // configuration property supported by i18next (https://www.i18next.com/overview/configuration-options).
   // { compatibilityJSON: 'v3' } can be used to generate v3 compatible plurals.
 }
@@ -288,8 +291,8 @@ Typescript is supported via Javascript and Jsx lexers. If you are using Javascri
   handlebars: [
     {
       lexer: 'HandlebarsLexer',
-      functions: ['t'] // Array of functions to match
-    }
+      functions: ['t'], // Array of functions to match
+    },
   ]
 }
 ```
@@ -312,7 +315,7 @@ Typescript is supported via Javascript and Jsx lexers. If you are using Javascri
 You can provide function instead of string as a custom lexer.
 
 ```js
-const CustomJsLexer = require('./CustomJsLexer');
+import CustomJsLexer from './CustomJsLexer';
 
 // ...
 {
@@ -369,7 +372,6 @@ Here is a list of the warnings:
 - **Found same keys with different values**: if you use different default values for the same key, you'll get this error. For example, having `t('key', {defaultValue: 'foo'})` and `t('key', {defaultValue: bar'})`. The parser will select the latest one.
 - **Found translation key already mapped to a map or parent of new key already mapped to a string**: happens in this kind of situation: `t('parent', {defaultValue: 'foo'})` and `t('parent.child', {defaultValue: 'bar'})`. `parent` is both a translation and an object for `child`.
 
-
 ## Contribute
 
 Any contribution is welcome. Please [read the guidelines](docs/development.md) first.
diff --git a/bin/cli.js b/bin/cli.js
index 57a4d7e..2ebee64 100755
--- a/bin/cli.js
+++ b/bin/cli.js
@@ -1,135 +1,166 @@
 #!/usr/bin/env node
 
-var colors        = require('colors')
-var fs            = require('fs')
-var i18nTransform = require('../dist/transform')['default']
-var path          = require('path')
-var pkg           = require('../package.json')
-var program       = require('commander')
-var sort          = require('gulp-sort')
-var vfs           = require('vinyl-fs')
+import { promises as fsp } from 'fs'
+import colors from 'colors'
+import path from 'path'
+import program from 'commander'
+import sort from 'gulp-sort'
+import vfs from 'vinyl-fs'
 
-program
-.version(pkg.version)
-.option('-c, --config <path>', 'Path to the config file (default: i18next-parser.config.js)', 'i18next-parser.config.js')
-.option('-o, --output <path>', 'Path to the output directory (default: locales/$LOCALE/$NAMESPACE.json)')
-.option('-s, --silent', 'Disable logging to stdout')
-.option('--fail-on-warnings', 'Exit with an exit code of 1 on warnings')
-.option('--fail-on-update', 'Exit with an exit code of 1 when translations are updated (for CI purpose)')
+import i18nTransform from '../dist/transform.js'
+;(async () => {
+  const pkg = JSON.parse(
+    await fsp.readFile(new URL('../package.json', import.meta.url), 'utf-8')
+  )
 
-program.on('--help', function() {
-  console.log('  Examples:')
-  console.log('')
-  console.log('    $ i18next "src/**/*.{js,jsx}"')
-  console.log('    $ i18next "/path/to/src/app.js" "/path/to/assets/index.html"')
-  console.log('    $ i18next --config i18next-parser.config.js --output locales/\$LOCALE/\$NAMESPACE.json')
-  console.log('')
-})
+  program
+    .version(pkg.version)
+    .option(
+      '-c, --config <path>',
+      'Path to the config file (default: i18next-parser.config.js)',
+      'i18next-parser.config.js'
+    )
+    .option(
+      '-o, --output <path>',
+      'Path to the output directory (default: locales/$LOCALE/$NAMESPACE.json)'
+    )
+    .option('-s, --silent', 'Disable logging to stdout')
+    .option('--fail-on-warnings', 'Exit with an exit code of 1 on warnings')
+    .option(
+      '--fail-on-update',
+      'Exit with an exit code of 1 when translations are updated (for CI purpose)'
+    )
 
-program.parse(process.argv)
+  program.on('--help', function () {
+    console.log('  Examples:')
+    console.log('')
+    console.log('    $ i18next "src/**/*.{js,jsx}"')
+    console.log(
+      '    $ i18next "/path/to/src/app.js" "/path/to/assets/index.html"'
+    )
+    console.log(
+      '    $ i18next --config i18next-parser.config.js --output locales/$LOCALE/$NAMESPACE.json'
+    )
+    console.log('')
+  })
 
-var config = {}
-try {
-  config = require(path.resolve(program.opts().config))
-} catch (err) {
-  if (err.code === 'MODULE_NOT_FOUND') {
-    console.log('  [error] '.red + 'Config file does not exist: ' + program.opts().config)
-  }
-  else {
-    throw err
-  }
-}
+  program.parse(process.argv)
 
-config.output = program.opts().output || config.output || 'locales/$LOCALE/$NAMESPACE.json'
-config.failOnWarnings = program.opts().failOnWarnings || config.failOnWarnings || false
-config.failOnUpdate = program.opts().failOnUpdate || config.failOnUpdate || false
+  let config = {}
+  try {
+    config = (await import(path.resolve(program.opts().config))).default
+  } catch (err) {
+    if (err.code === 'MODULE_NOT_FOUND') {
+      console.log(
+        '  [error] '.red +
+          'Config file does not exist: ' +
+          program.opts().config
+      )
+    } else {
+      throw err
+    }
+  }
 
-var args = program.args || []
-var globs
+  config.output =
+    program.opts().output || config.output || 'locales/$LOCALE/$NAMESPACE.json'
+  config.failOnWarnings =
+    program.opts().failOnWarnings || config.failOnWarnings || false
+  config.failOnUpdate =
+    program.opts().failOnUpdate || config.failOnUpdate || false
 
-// prefer globs specified in the cli
-if (args.length) {
-  globs = args.map(function (s) {
-    s = s.trim()
-    if (s.match(/(^'.*'$|^".*"$)/)) {
-      s = s.slice(1, -1)
-    }
-    return s
-  })
-}
+  let args = program.args || []
+  let globs
 
-// if config has an input parameter, try to use it
-else if (config.input) {
-  if (!Array.isArray(config.input)) {
-    if (typeof config.input === 'string') {
-      config.input = [config.input]
-    }
-    else {
-      console.log('  [error]   '.red + '`input` must be an array when specified in the config')
-      program.help()
-      program.exit(1)
-    }
+  // prefer globs specified in the cli
+  if (args.length) {
+    globs = args.map(function (s) {
+      s = s.trim()
+      if (s.match(/(^'.*'$|^".*"$)/)) {
+        s = s.slice(1, -1)
+      }
+      return s
+    })
   }
 
-  globs = config.input.map(function (s) {
-    var negate = ''
-    if (s.startsWith('!')) {
-      negate = '!'
-      s = s.substr(1)
+  // if config has an input parameter, try to use it
+  else if (config.input) {
+    if (!Array.isArray(config.input)) {
+      if (typeof config.input === 'string') {
+        config.input = [config.input]
+      } else {
+        console.log(
+          '  [error]   '.red +
+            '`input` must be an array when specified in the config'
+        )
+        program.help()
+        program.exit(1)
+      }
     }
-    return negate + path.resolve(path.dirname(path.resolve(program.opts().config)), s)
-  })
-}
 
-if (!globs.length) {
-  console.log('  [error] '.red + 'missing argument: ')
-  program.help()
-  return
-}
+    globs = config.input.map(function (s) {
+      var negate = ''
+      if (s.startsWith('!')) {
+        negate = '!'
+        s = s.substr(1)
+      }
+      return (
+        negate +
+        path.resolve(path.dirname(path.resolve(program.opts().config)), s)
+      )
+    })
+  }
+
+  if (!globs || !globs.length) {
+    console.log('  [error] '.red + 'missing argument: ')
+    program.help()
+    process.exit(0)
+  }
 
-// Welcome message
-if (!program.opts().silent) {
-  console.log()
-  console.log('  i18next Parser'.cyan)
-  console.log('  --------------'.cyan)
-  console.log('  Input:  '.cyan + args.join(', '))
-  console.log('  Output: '.cyan + config.output)
-  console.log()
-}
+  // Welcome message
+  if (!program.opts().silent) {
+    console.log()
+    console.log('  i18next Parser'.cyan)
+    console.log('  --------------'.cyan)
+    console.log('  Input:  '.cyan + args.join(', '))
+    console.log('  Output: '.cyan + config.output)
+    console.log()
+  }
 
-var count = 0
+  var count = 0
 
-vfs.src(globs)
-.pipe(sort())
-.pipe(
-  new i18nTransform(config)
-  .on('reading', function (file) {
-    if (!program.opts().silent) {
-      console.log('  [read]    '.green + file.path)
-    }
-    count++
-  })
-  .on('data', function (file) {
-    if (!program.opts().silent) {
-      console.log('  [write]   '.green + file.path)
-    }
-  })
-  .on('error', function (message, region) {
-    if (typeof region === 'string') {
-      message += ': ' + region.trim()
-    }
-    console.log('  [error]   '.red + message)
-  })
-  .on('warning', function (message) {
-    if (!program.opts().silent) {
-      console.log('  [warning] '.yellow + message)
-    }
-  })
-  .on('finish', function () {
-    if (!program.opts().silent) {
-      console.log()
-      console.log('  Stats:  '.cyan + count + ' files were parsed')
-    }
-  })
-)
-.pipe(vfs.dest(process.cwd()))
+  vfs
+    .src(globs)
+    .pipe(sort())
+    .pipe(
+      new i18nTransform(config)
+        .on('reading', function (file) {
+          if (!program.opts().silent) {
+            console.log('  [read]    '.green + file.path)
+          }
+          count++
+        })
+        .on('data', function (file) {
+          if (!program.opts().silent) {
+            console.log('  [write]   '.green + file.path)
+          }
+        })
+        .on('error', function (message, region) {
+          if (typeof region === 'string') {
+            message += ': ' + region.trim()
+          }
+          console.log('  [error]   '.red + message)
+        })
+        .on('warning', function (message) {
+          if (!program.opts().silent) {
+            console.log('  [warning] '.yellow + message)
+          }
+        })
+        .on('finish', function () {
+          if (!program.opts().silent) {
+            console.log()
+            console.log('  Stats:  '.cyan + count + ' files were parsed')
+          }
+        })
+    )
+    .pipe(vfs.dest(process.cwd()))
+})()
diff --git a/i18next-parser.config.js b/i18next-parser.config.js
index b1e40c8..77711cf 100644
--- a/i18next-parser.config.js
+++ b/i18next-parser.config.js
@@ -1,3 +1,3 @@
-module.exports = {
-  output: 'test/manual/$LOCALE/$NAMESPACE.json'
-};
+export default {
+  output: 'test/manual/$LOCALE/$NAMESPACE.json',
+}
diff --git a/package.json b/package.json
index 6539742..980b992 100644
--- a/package.json
+++ b/package.json
@@ -2,14 +2,15 @@
   "name": "i18next-parser",
   "description": "Command Line tool for i18next",
   "version": "5.4.0",
+  "type": "module",
   "license": "MIT",
   "author": "Karel Ledru",
-  "main": "dist/index.js",
+  "exports": "./dist/index.js",
   "bin": {
     "i18next": "./bin/cli.js"
   },
   "engines": {
-    "node": ">=12",
+    "node": "^12.20.0 || ^14.13.1 || >=16.0.0 || >=18.0.0",
     "npm": ">=6",
     "yarn": ">=1"
   },
@@ -36,7 +37,7 @@
     "i18next": "^21.2.0",
     "js-yaml": "4.1.0",
     "rsvp": "^4.8.2",
-    "sort-keys": "^4.2.0",
+    "sort-keys": "^5.0.0",
     "through2": "~4.0.2",
     "typescript": "^4.2.4",
     "vinyl": "~2.2.1",
diff --git a/src/broccoli.js b/src/broccoli.js
index 682948a..5afb21d 100644
--- a/src/broccoli.js
+++ b/src/broccoli.js
@@ -1,12 +1,11 @@
-import colors from 'colors'
 import fse from 'fs-extra'
-import path from 'path'
 import Plugin from 'broccoli-plugin'
-import i18nTransform from './transform'
 import rsvp from 'rsvp'
 import sort from 'gulp-sort'
 import vfs from 'vinyl-fs'
 
+import i18nTransform from './transform.js'
+
 const Promise = rsvp.Promise
 
 export default class i18nextParser extends Plugin {
diff --git a/src/index.js b/src/index.js
index c15101b..1f08cf8 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,4 +1,4 @@
-export { default as broccoli } from './broccoli'
-export { default as parser } from './parser'
-export { default as transform } from './transform'
-export { default as gulp } from './transform'
+export { default as broccoli } from './broccoli.js'
+export { default as parser } from './parser.js'
+export { default as transform } from './transform.js'
+export { default as gulp } from './transform.js'
diff --git a/src/lexers/handlebars-lexer.js b/src/lexers/handlebars-lexer.js
index a4d5266..8017835 100644
--- a/src/lexers/handlebars-lexer.js
+++ b/src/lexers/handlebars-lexer.js
@@ -1,4 +1,4 @@
-import BaseLexer from './base-lexer'
+import BaseLexer from './base-lexer.js'
 
 export default class HandlebarsLexer extends BaseLexer {
   constructor(options = {}) {
diff --git a/src/lexers/html-lexer.js b/src/lexers/html-lexer.js
index 9fed263..02677b0 100644
--- a/src/lexers/html-lexer.js
+++ b/src/lexers/html-lexer.js
@@ -1,4 +1,4 @@
-import BaseLexer from './base-lexer'
+import BaseLexer from './base-lexer.js'
 import cheerio from 'cheerio'
 
 export default class HTMLLexer extends BaseLexer {
diff --git a/src/lexers/javascript-lexer.js b/src/lexers/javascript-lexer.js
index a1d98db..723dc90 100644
--- a/src/lexers/javascript-lexer.js
+++ b/src/lexers/javascript-lexer.js
@@ -1,5 +1,5 @@
-import BaseLexer from './base-lexer'
-import * as ts from 'typescript'
+import BaseLexer from './base-lexer.js'
+import ts from 'typescript'
 
 export default class JavascriptLexer extends BaseLexer {
   constructor(options = {}) {
diff --git a/src/lexers/jsx-lexer.js b/src/lexers/jsx-lexer.js
index d9ace4e..8e23b25 100644
--- a/src/lexers/jsx-lexer.js
+++ b/src/lexers/jsx-lexer.js
@@ -1,5 +1,5 @@
-import JavascriptLexer from './javascript-lexer'
-import * as ts from 'typescript'
+import JavascriptLexer from './javascript-lexer.js'
+import ts from 'typescript'
 
 export default class JsxLexer extends JavascriptLexer {
   constructor(options = {}) {
diff --git a/src/lexers/vue-lexer.js b/src/lexers/vue-lexer.js
index 556330d..44accce 100644
--- a/src/lexers/vue-lexer.js
+++ b/src/lexers/vue-lexer.js
@@ -1,4 +1,5 @@
-import BaseLexer from './base-lexer'
+import VueTemplateCompiler from 'vue-template-compiler'
+import BaseLexer from './base-lexer.js'
 import JavascriptLexer from './javascript-lexer.js'
 
 export default class VueLexer extends BaseLexer {
@@ -15,9 +16,7 @@ export default class VueLexer extends BaseLexer {
     Lexer.on('warning', (warning) => this.emit('warning', warning))
     keys = keys.concat(Lexer.extract(content))
 
-    const compiledTemplate = require('vue-template-compiler').compile(
-      content
-    ).render
+    const compiledTemplate = VueTemplateCompiler.compile(content).render
     const Lexer2 = new JavascriptLexer({ functions: this.functions })
     Lexer2.on('warning', (warning) => this.emit('warning', warning))
     keys = keys.concat(Lexer2.extract(compiledTemplate))
diff --git a/src/parser.js b/src/parser.js
index bca6710..eabbd60 100644
--- a/src/parser.js
+++ b/src/parser.js
@@ -1,10 +1,10 @@
 import path from 'path'
 import EventEmitter from 'events'
-import HandlebarsLexer from './lexers/handlebars-lexer'
-import HTMLLexer from './lexers/html-lexer'
-import JavascriptLexer from './lexers/javascript-lexer'
-import JsxLexer from './lexers/jsx-lexer'
-import VueLexer from './lexers/vue-lexer'
+import HandlebarsLexer from './lexers/handlebars-lexer.js'
+import HTMLLexer from './lexers/html-lexer.js'
+import JavascriptLexer from './lexers/javascript-lexer.js'
+import JsxLexer from './lexers/jsx-lexer.js'
+import VueLexer from './lexers/vue-lexer.js'
 
 const lexers = {
   hbs: ['HandlebarsLexer'],
diff --git a/src/transform.js b/src/transform.js
index 16207d0..8b2090d 100644
--- a/src/transform.js
+++ b/src/transform.js
@@ -1,19 +1,20 @@
-import {
-  dotPathToHash,
-  mergeHashes,
-  transferValues,
-  makeDefaultSort,
-} from './helpers'
 import { Transform } from 'stream'
 import eol from 'eol'
 import fs from 'fs'
-import Parser from './parser'
 import path from 'path'
 import VirtualFile from 'vinyl'
 import yaml from 'js-yaml'
 import i18next from 'i18next'
 import sortKeys from 'sort-keys'
 
+import {
+  dotPathToHash,
+  mergeHashes,
+  transferValues,
+  makeDefaultSort,
+} from './helpers.js'
+import Parser from './parser.js'
+
 export default class i18nTransform extends Transform {
   constructor(options = {}) {
     options.objectMode = true


Test patch:
diff --git a/test/Intl.PluralRules.mock.js b/test/Intl.PluralRules.mock.js
index 4266781..ea88622 100644
--- a/test/Intl.PluralRules.mock.js
+++ b/test/Intl.PluralRules.mock.js
@@ -1,4 +1,4 @@
-class PluralRules {
+export default class PluralRules {
   constructor(locale) {
     this.locale = locale
   }
@@ -15,5 +15,3 @@ class PluralRules {
     }
   }
 }
-
-module.exports = PluralRules
diff --git a/test/broccoli/Brocfile.js b/test/broccoli/Brocfile.js
index 1222a3b..e2c9083 100644
--- a/test/broccoli/Brocfile.js
+++ b/test/broccoli/Brocfile.js
@@ -1,9 +1,11 @@
-const path = require('path')
-const Funnel = require('broccoli-funnel')
+import path from 'path'
+import { fileURLToPath } from 'url'
+import Funnel from 'broccoli-funnel'
 // In a real use case, it should be:
-// const i18next = require('i18next-parser').broccoli
-const i18nextParser = require('../../src/index').broccoli
+// import i18next from 'i18next-parser').broccol
+import { broccoli as i18nextParser } from '../../src/index.js'
 
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
 const appRoot = path.resolve(__dirname, 'src')
 
 const filesToParse = new Funnel(appRoot, {
@@ -17,4 +19,4 @@ const i18n = new i18nextParser([filesToParse], {
   silent: true,
 })
 
-module.exports = i18n
+export default i18n
diff --git a/test/broccoli/broccoli.test.js b/test/broccoli/broccoli.test.js
index 05b6673..111d8e1 100644
--- a/test/broccoli/broccoli.test.js
+++ b/test/broccoli/broccoli.test.js
@@ -1,11 +1,15 @@
-import path from 'path'
 import { assert } from 'chai'
-import sinon from 'sinon'
+import { fileURLToPath } from 'url'
+import broccoli from 'broccoli'
 import fs from 'fs-extra'
-import { Builder } from 'broccoli'
+import path from 'path'
+import sinon from 'sinon'
 import brocFile from './Brocfile.js'
 import PluralRulesMock from '../Intl.PluralRules.mock.js'
 
+const { Builder } = broccoli
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
 describe('broccoli plugin', function () {
   // test execution time depends on I/O
   this.timeout(0)
diff --git a/test/cli/cli.test.js b/test/cli/cli.test.js
index 3e4a594..cf18442 100644
--- a/test/cli/cli.test.js
+++ b/test/cli/cli.test.js
@@ -1,7 +1,10 @@
-import path from 'path'
 import { assert } from 'chai'
+import { fileURLToPath } from 'url'
 import execa from 'execa'
 import fs from 'fs-extra'
+import path from 'path'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
 
 describe('CLI', function () {
   // test execution time depends on I/O
diff --git a/test/cli/i18next-parser.config.js b/test/cli/i18next-parser.config.js
index 028fa54..16b0c4e 100644
--- a/test/cli/i18next-parser.config.js
+++ b/test/cli/i18next-parser.config.js
@@ -1,6 +1,9 @@
-const path = require('path')
+import path from 'path'
+import { fileURLToPath } from 'url'
 
-module.exports = {
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+export default {
   input: ['**/*.html'],
   output: path.resolve(__dirname, 'locales/$LOCALE/$NAMESPACE.json'),
   sort: true,
diff --git a/test/gulp/gulp.test.js b/test/gulp/gulp.test.js
index ee961b0..e44e2cd 100644
--- a/test/gulp/gulp.test.js
+++ b/test/gulp/gulp.test.js
@@ -1,12 +1,15 @@
-import path from 'path'
 import { assert } from 'chai'
-import sinon from 'sinon'
+import { fileURLToPath } from 'url'
 import fs from 'fs-extra'
-import pEvent from 'p-event'
 import gulp from 'gulp'
+import path from 'path'
+import pEvent from 'p-event'
+import sinon from 'sinon'
 import './gulpfile.js'
 import PluralRulesMock from '../Intl.PluralRules.mock.js'
 
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
 describe('gulp plugin', function () {
   // test execution time depends on I/O
   this.timeout(0)
diff --git a/test/gulp/gulpfile.js b/test/gulp/gulpfile.js
index 740abfe..d359a7d 100644
--- a/test/gulp/gulpfile.js
+++ b/test/gulp/gulpfile.js
@@ -1,8 +1,11 @@
-const path = require('path')
-const gulp = require('gulp')
+import { fileURLToPath } from 'url'
+import gulp from 'gulp'
+import path from 'path'
 // In a real use case, it should be:
-// const i18next = require('i18next-parser').gulp
-const i18next = require('../../src/index').gulp
+// import { gulp as i18next } from 'i18next-parser'
+import { gulp as i18next } from '../../src/index.js'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
 
 gulp.task('i18next', function () {
   return gulp
diff --git a/test/helpers/dotPathToHash.test.js b/test/helpers/dotPathToHash.test.js
index 1dc8620..0ed2a1f 100644
--- a/test/helpers/dotPathToHash.test.js
+++ b/test/helpers/dotPathToHash.test.js
@@ -1,5 +1,5 @@
 import { assert } from 'chai'
-import { dotPathToHash } from '../../src/helpers'
+import { dotPathToHash } from '../../src/helpers.js'
 
 describe('dotPathToHash helper function', () => {
   it('creates an object from a string path', (done) => {
diff --git a/test/helpers/getPluralSuffixPosition.test.js b/test/helpers/getPluralSuffixPosition.test.js
index df76860..96f6b63 100644
--- a/test/helpers/getPluralSuffixPosition.test.js
+++ b/test/helpers/getPluralSuffixPosition.test.js
@@ -1,5 +1,5 @@
 import { assert } from 'chai'
-import { getPluralSuffixPosition } from '../../src/helpers'
+import { getPluralSuffixPosition } from '../../src/helpers.js'
 
 describe('getPluralSuffixPosition helper function', () => {
   it('returns suffix position for all valid plural forms', (done) => {
diff --git a/test/helpers/getSingularForm.test.js b/test/helpers/getSingularForm.test.js
index 08a889c..ca9cce8 100644
--- a/test/helpers/getSingularForm.test.js
+++ b/test/helpers/getSingularForm.test.js
@@ -1,5 +1,5 @@
 import { assert } from 'chai'
-import { getSingularForm } from '../../src/helpers'
+import { getSingularForm } from '../../src/helpers.js'
 
 describe('getSingularForm helper function', () => {
   it('returns singular key for all valid plural forms', (done) => {
diff --git a/test/helpers/hasRelatedPluralKey.test.js b/test/helpers/hasRelatedPluralKey.test.js
index abd5827..959bcfd 100644
--- a/test/helpers/hasRelatedPluralKey.test.js
+++ b/test/helpers/hasRelatedPluralKey.test.js
@@ -1,5 +1,5 @@
 import { assert } from 'chai'
-import { hasRelatedPluralKey } from '../../src/helpers'
+import { hasRelatedPluralKey } from '../../src/helpers.js'
 
 describe('hasRelatedPluralKey helper function', () => {
   it('returns false when `source` does not contain valid plural form of `rawKey`', (done) => {
diff --git a/test/helpers/makeDefaultSort.test.js b/test/helpers/makeDefaultSort.test.js
index 354f367..7061e3f 100644
--- a/test/helpers/makeDefaultSort.test.js
+++ b/test/helpers/makeDefaultSort.test.js
@@ -1,5 +1,5 @@
 import { assert } from 'chai'
-import { makeDefaultSort } from '../../src/helpers'
+import { makeDefaultSort } from '../../src/helpers.js'
 
 describe('makeDefaultSort helper function', () => {
   it('sorts the keys alphanumerically', (done) => {
diff --git a/test/helpers/mergeHashes.test.js b/test/helpers/mergeHashes.test.js
index ecc1bbb..dd02969 100644
--- a/test/helpers/mergeHashes.test.js
+++ b/test/helpers/mergeHashes.test.js
@@ -1,5 +1,5 @@
 import { assert } from 'chai'
-import { mergeHashes } from '../../src/helpers'
+import { mergeHashes } from '../../src/helpers.js'
 
 describe('mergeHashes helper function', () => {
   it('replaces empty `target` keys with `source`', (done) => {
diff --git a/test/helpers/transferValues.test.js b/test/helpers/transferValues.test.js
index 5942b70..236bebd 100644
--- a/test/helpers/transferValues.test.js
+++ b/test/helpers/transferValues.test.js
@@ -1,5 +1,5 @@
 import { assert } from 'chai'
-import { transferValues } from '../../src/helpers'
+import { transferValues } from '../../src/helpers.js'
 
 describe('transferValues helper function', () => {
   it('sets undefined keys', (done) => {
diff --git a/test/lexers/base-lexer.test.js b/test/lexers/base-lexer.test.js
index 7476b6e..61afcf0 100644
--- a/test/lexers/base-lexer.test.js
+++ b/test/lexers/base-lexer.test.js
@@ -1,5 +1,5 @@
 import { assert } from 'chai'
-import BaseLexer from '../../src/lexers/base-lexer'
+import BaseLexer from '../../src/lexers/base-lexer.js'
 
 describe('BaseLexer', () => {
   it('functionPattern() return a regex pattern', (done) => {
diff --git a/test/lexers/handlebars-lexer.test.js b/test/lexers/handlebars-lexer.test.js
index 752f2f6..9116c8c 100644
--- a/test/lexers/handlebars-lexer.test.js
+++ b/test/lexers/handlebars-lexer.test.js
@@ -1,5 +1,5 @@
 import { assert } from 'chai'
-import HandlebarsLexer from '../../src/lexers/handlebars-lexer'
+import HandlebarsLexer from '../../src/lexers/handlebars-lexer.js'
 
 describe('HandlebarsLexer', () => {
   it('extracts keys from translation components', (done) => {
diff --git a/test/lexers/html-lexer.test.js b/test/lexers/html-lexer.test.js
index 6336053..2e68868 100644
--- a/test/lexers/html-lexer.test.js
+++ b/test/lexers/html-lexer.test.js
@@ -1,5 +1,5 @@
 import { assert } from 'chai'
-import HTMLLexer from '../../src/lexers/html-lexer'
+import HTMLLexer from '../../src/lexers/html-lexer.js'
 
 describe('HTMLLexer', () => {
   it('extracts keys from html attributes', (done) => {
diff --git a/test/lexers/javascript-lexer.test.js b/test/lexers/javascript-lexer.test.js
index ef09576..2d59f4c 100644
--- a/test/lexers/javascript-lexer.test.js
+++ b/test/lexers/javascript-lexer.test.js
@@ -1,6 +1,5 @@
 import { assert } from 'chai'
-
-import JavascriptLexer from '../../src/lexers/javascript-lexer'
+import JavascriptLexer from '../../src/lexers/javascript-lexer.js'
 
 describe('JavascriptLexer', () => {
   it('extracts keys from translation components', (done) => {
diff --git a/test/lexers/jsx-lexer.test.js b/test/lexers/jsx-lexer.test.js
index 9a33313..3925442 100644
--- a/test/lexers/jsx-lexer.test.js
+++ b/test/lexers/jsx-lexer.test.js
@@ -1,5 +1,5 @@
 import { assert } from 'chai'
-import JsxLexer from '../../src/lexers/jsx-lexer'
+import JsxLexer from '../../src/lexers/jsx-lexer.js'
 
 describe('JsxLexer', () => {
   describe('<Interpolate>', () => {
diff --git a/test/lexers/vue-lexer.test.js b/test/lexers/vue-lexer.test.js
index 09da2f3..1d8f2f2 100644
--- a/test/lexers/vue-lexer.test.js
+++ b/test/lexers/vue-lexer.test.js
@@ -1,5 +1,5 @@
 import { assert } from 'chai'
-import VueLexer from '../../src/lexers/vue-lexer'
+import VueLexer from '../../src/lexers/vue-lexer.js'
 
 describe('VueLexer', () => {
   it('extracts keys from template & js', (done) => {
diff --git a/test/parser.test.js b/test/parser.test.js
index a67bb63..f9c1765 100644
--- a/test/parser.test.js
+++ b/test/parser.test.js
@@ -1,12 +1,14 @@
 import { assert, expect } from 'chai'
-import Vinyl from 'vinyl'
+import { fileURLToPath } from 'url'
 import fs from 'fs'
-import i18nTransform from '../src/transform'
 import path from 'path'
 import sinon from 'sinon'
+import Vinyl from 'vinyl'
+import i18nTransform from '../src/transform.js'
 
 const enLibraryPath = path.normalize('en/translation.json')
 const arLibraryPath = path.normalize('ar/translation.json')
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
 
 describe('parser', () => {
   it('parses globally on multiple lines', (done) => {