Repository: i18next/i18next-parser
Pull request #302 (2021-04-29 16:14:32)

Add the ability to configure plural separators
### Why am I submitting this PR

I would like to be able to configure the plural separator, just like the key, namespace, and context separator.

### Does it fix an existing ticket?

Closes https://github.com/i18next/i18next-parser/issues/300

### 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 f01319f..761e45c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,6 +19,7 @@ Thumbs.db
 *.swp
 /sftp-config.json
 /.vscode
+.history
 
 # node files
 node_modules/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 71d93cf..049188c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
 # Changelog
 
-# 3.9.0 - latest
+# 3.10.0 - latest
+
+- defaultValue, useKeysAsDefaultValue and skipDefaultValues options support function #224 #299 #301
+
+# 3.9.0
 
 - Update to babel 7 #298
 
diff --git a/README.md b/README.md
index 75dad2a..9b5085f 100644
--- a/README.md
+++ b/README.md
@@ -126,6 +126,7 @@ module.exports = {
 
   defaultValue: '',
   // Default value to give to empty keys
+  // You may also specify a function accepting the locale, namespace, and key as arguments
 
   indentation: 2,
   // Indentation of the catalog files
@@ -169,6 +170,10 @@ module.exports = {
   // Supports JSON (.json) and YAML (.yml) file formats
   // Where to write the locale files relative to process.cwd()
 
+  pluralSeparator: '_',
+  // Plural separator used in your translation keys
+  // If you want to use plain english keys, separators such as `_` might conflict. You might want to set `pluralSeparator` to a different string that does not occur in your keys.
+
   input: undefined,
   // An array of globs that describe where to look for source files
   // relative to the location of the configuration file
@@ -177,11 +182,13 @@ module.exports = {
   // Whether or not to sort the catalog
 
   skipDefaultValues: false,
-  // Whether to ignore default values.
+  // Whether to ignore default values
+  // You may also specify a function accepting the locale and namespace as arguments
 
   useKeysAsDefaultValue: false,
   // Whether to use the keys as the default value; ex. "Hello": "Hello", "World": "World"
   // This option takes precedence over the `defaultValue` and `skipDefaultValues` options
+  // You may also specify a function accepting the locale and namespace as arguments
 
   verbose: false,
   // Display info about the parsing including some stats
diff --git a/dist/helpers.js b/dist/helpers.js
index 7152d30..fabe25b 100644
--- a/dist/helpers.js
+++ b/dist/helpers.js
@@ -14,25 +14,43 @@
 function dotPathToHash(entry) {var target = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
   var path = entry.keyWithNamespace;
   if (options.suffix || options.suffix === 0) {
-    path += "_".concat(options.suffix);
+    path += "".concat(options.pluralSeparator).concat(options.suffix);
   }
 
   var separator = options.separator || '.';
+
+  var key = entry.keyWithNamespace.substring(
+  entry.keyWithNamespace.indexOf(separator) + separator.length,
+  entry.keyWithNamespace.length);
+
+
+  var defaultValue =
+  typeof options.value === 'function' ?
+  options.value(options.locale, entry.namespace, key) :
+  options.value;
+
+  var skipDefaultValues =
+  typeof options.skipDefaultValues === 'function' ?
+  options.skipDefaultValues(options.locale, entry.namespace) :
+  options.skipDefaultValues;
+
+  var useKeysAsDefaultValue =
+  typeof options.useKeysAsDefaultValue === 'function' ?
+  options.useKeysAsDefaultValue(options.locale, entry.namespace) :
+  options.useKeysAsDefaultValue;
+
   var newValue =
   entry["defaultValue_".concat(options.suffix)] ||
   entry.defaultValue ||
-  options.value ||
+  defaultValue ||
   '';
 
-  if (options.skipDefaultValues) {
+  if (skipDefaultValues) {
     newValue = '';
   }
 
-  if (options.useKeysAsDefaultValue) {
-    newValue = entry.keyWithNamespace.substring(
-    entry.keyWithNamespace.indexOf(separator) + separator.length,
-    entry.keyWithNamespace.length);
-
+  if (useKeysAsDefaultValue) {
+    newValue = key;
   }
 
   if (path.endsWith(separator)) {
diff --git a/dist/transform.js b/dist/transform.js
index 43f7317..1b6a6e5 100644
--- a/dist/transform.js
+++ b/dist/transform.js
@@ -34,6 +34,7 @@ i18nTransform = /*#__PURE__*/function (_Transform) {_inherits(i18nTransform, _Tr
       lineEnding: 'auto',
       locales: ['en', 'fr'],
       namespaceSeparator: ':',
+      pluralSeparator: '_',
       output: 'locales/$LOCALE/$NAMESPACE.json',
       sort: false,
       useKeysAsDefaultValue: false,
@@ -140,7 +141,9 @@ i18nTransform = /*#__PURE__*/function (_Transform) {_inherits(i18nTransform, _Tr
           var transformEntry = function transformEntry(entry, suffix) {
             var _dotPathToHash = (0, _helpers.dotPathToHash)(entry, catalog, {
               suffix: suffix,
+              locale: locale,
               separator: _this2.options.keySeparator,
+              pluralSeparator: _this2.options.pluralSeparator,
               value: _this2.options.defaultValue,
               useKeysAsDefaultValue: _this2.options.useKeysAsDefaultValue,
               skipDefaultValues: _this2.options.skipDefaultValues,
diff --git a/package.json b/package.json
index 2dfe4a9..62a10fd 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "author": "Karel Ledru",
   "description": "Command Line tool for i18next",
   "name": "i18next-parser",
-  "version": "3.9.0",
+  "version": "3.10.0",
   "license": "MIT",
   "main": "dist/index.js",
   "bin": {
diff --git a/src/helpers.js b/src/helpers.js
index 5a04803..864d72a 100644
--- a/src/helpers.js
+++ b/src/helpers.js
@@ -14,25 +14,43 @@
 function dotPathToHash(entry, target = {}, options = {}) {
   let path = entry.keyWithNamespace
   if (options.suffix || options.suffix === 0) {
-    path += `_${options.suffix}`
+    path += `${options.pluralSeparator}${options.suffix}`
   }
 
   const separator = options.separator || '.'
+
+  const key = entry.keyWithNamespace.substring(
+    entry.keyWithNamespace.indexOf(separator) + separator.length,
+    entry.keyWithNamespace.length
+  )
+
+  const defaultValue =
+    typeof options.value === 'function'
+      ? options.value(options.locale, entry.namespace, key)
+      : options.value
+
+  const skipDefaultValues =
+    typeof options.skipDefaultValues === 'function'
+      ? options.skipDefaultValues(options.locale, entry.namespace)
+      : options.skipDefaultValues
+
+  const useKeysAsDefaultValue =
+    typeof options.useKeysAsDefaultValue === 'function'
+      ? options.useKeysAsDefaultValue(options.locale, entry.namespace)
+      : options.useKeysAsDefaultValue
+
   let newValue =
     entry[`defaultValue_${options.suffix}`] ||
     entry.defaultValue ||
-    options.value ||
+    defaultValue ||
     ''
 
-  if (options.skipDefaultValues) {
+  if (skipDefaultValues) {
     newValue = ''
   }
 
-  if (options.useKeysAsDefaultValue) {
-    newValue = entry.keyWithNamespace.substring(
-      entry.keyWithNamespace.indexOf(separator) + separator.length,
-      entry.keyWithNamespace.length
-    )
+  if (useKeysAsDefaultValue) {
+    newValue = key
   }
 
   if (path.endsWith(separator)) {
diff --git a/src/transform.js b/src/transform.js
index e67531a..7436d14 100644
--- a/src/transform.js
+++ b/src/transform.js
@@ -34,6 +34,7 @@ export default class i18nTransform extends Transform {
       lineEnding: 'auto',
       locales: ['en', 'fr'],
       namespaceSeparator: ':',
+      pluralSeparator: '_',
       output: 'locales/$LOCALE/$NAMESPACE.json',
       sort: false,
       useKeysAsDefaultValue: false,
@@ -140,7 +141,9 @@ export default class i18nTransform extends Transform {
       const transformEntry = (entry, suffix) => {
         const { duplicate, conflict } = dotPathToHash(entry, catalog, {
           suffix,
+          locale,
           separator: this.options.keySeparator,
+          pluralSeparator: this.options.pluralSeparator,
           value: this.options.defaultValue,
           useKeysAsDefaultValue: this.options.useKeysAsDefaultValue,
           skipDefaultValues: this.options.skipDefaultValues,
diff --git a/yarn.lock b/yarn.lock
index 677da64..2443540 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -827,14 +827,7 @@
     pirates "^4.0.0"
     source-map-support "^0.5.16"
 
-"@babel/runtime@^7.12.0":
-  version "7.13.10"
-  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.10.tgz#47d42a57b6095f4468da440388fdbad8bebf0d7d"
-  integrity sha512-4QPkjJq6Ns3V/RgpEahRk+AGfL0eO6RHHtTWoNNr5mO49G6B5+X6d6THgWEAvTrznU5xYpbAlVKRYcsCgh/Akw==
-  dependencies:
-    regenerator-runtime "^0.13.4"
-
-"@babel/runtime@^7.8.4":
+"@babel/runtime@^7.12.0", "@babel/runtime@^7.8.4":
   version "7.13.17"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.13.17.tgz#8966d1fc9593bf848602f0662d6b4d0069e3a7ec"
   integrity sha512-NCdgJEelPTSh+FEFylhnP1ylq848l1z9t9N0j1Lfbcw0+KXGjsTvUmkxy+voLLXB5SOKMbLLx4jxYliGrYQseA==
@@ -2839,9 +2832,9 @@ https@^1.0.0:
   integrity sha1-PDfHrhqO65ZpBKKtHpdaGUt+06Q=
 
 i18next@^20.2.1:
-  version "20.2.1"
-  resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.2.1.tgz#8aedfdc596f7d54b9d3f6d87bcb89214467cb785"
-  integrity sha512-JLruWDEQ3T6tKT6P7u+DsNtToMHUwUcQIYOMRcnNBdUhSfKkoIDUKdVDKgGtmqr//LrirxjADUdr3d5Gwbow6g==
+  version "20.2.2"
+  resolved "https://registry.yarnpkg.com/i18next/-/i18next-20.2.2.tgz#175644829dcf35546ba8c174583acfe449e4ef1a"
+  integrity sha512-uWCv9LzKpe+OwvnKKrb8CbJwgAhasQofD58cB0PQ6bTPXEl5PlItl5C4esmY8HtriLu9nrjc2Hi0IfYv3Fy8BQ==
   dependencies:
     "@babel/runtime" "^7.12.0"
 


Test patch:
diff --git a/test/parser.test.js b/test/parser.test.js
index d2470ee..6e838e4 100644
--- a/test/parser.test.js
+++ b/test/parser.test.js
@@ -721,6 +721,86 @@ describe('parser', () => {
       i18nextParser.end(fakeFile)
     })
 
+    it('supports a defaultValue function', (done) => {
+      let result
+      const i18nextParser = new i18nTransform({
+        defaultValue: (locale, namespace, key) =>
+          `${locale}:${namespace}:${key}`,
+      })
+      const fakeFile = new Vinyl({
+        contents: Buffer.from("t('first')"),
+        path: 'file.js',
+      })
+
+      i18nextParser.on('data', (file) => {
+        if (file.relative.endsWith(enLibraryPath)) {
+          result = JSON.parse(file.contents)
+        }
+      })
+      i18nextParser.once('end', () => {
+        assert.deepEqual(result, { first: 'en:translation:first' })
+        done()
+      })
+
+      i18nextParser.end(fakeFile)
+    })
+
+    it('supports a useKeysAsDefaultValue function', (done) => {
+      let enResult
+      let arResult
+      const i18nextParser = new i18nTransform({
+        locales: ['en', 'ar'],
+        useKeysAsDefaultValue: (locale, namespace) => locale === 'en',
+      })
+      const fakeFile = new Vinyl({
+        contents: Buffer.from("t('first')"),
+        path: 'file.js',
+      })
+
+      i18nextParser.on('data', (file) => {
+        if (file.relative.endsWith(enLibraryPath)) {
+          enResult = JSON.parse(file.contents)
+        } else if (file.relative.endsWith(arLibraryPath)) {
+          arResult = JSON.parse(file.contents)
+        }
+      })
+      i18nextParser.once('end', () => {
+        assert.deepEqual(enResult, { first: 'first' })
+        assert.deepEqual(arResult, { first: '' })
+        done()
+      })
+
+      i18nextParser.end(fakeFile)
+    })
+
+    it('supports a skipDefaultValues function', (done) => {
+      let enResult
+      let arResult
+      const i18nextParser = new i18nTransform({
+        locales: ['en', 'ar'],
+        skipDefaultValues: (locale, namespace) => locale === 'en',
+      })
+      const fakeFile = new Vinyl({
+        contents: Buffer.from("t('first', { defaultValue: 'default' })"),
+        path: 'file.js',
+      })
+
+      i18nextParser.on('data', (file) => {
+        if (file.relative.endsWith(enLibraryPath)) {
+          enResult = JSON.parse(file.contents)
+        } else if (file.relative.endsWith(arLibraryPath)) {
+          arResult = JSON.parse(file.contents)
+        }
+      })
+      i18nextParser.once('end', () => {
+        assert.deepEqual(enResult, { first: '' })
+        assert.deepEqual(arResult, { first: 'default' })
+        done()
+      })
+
+      i18nextParser.end(fakeFile)
+    })
+
     it('supports a lineEnding', (done) => {
       let result
       const i18nextParser = new i18nTransform({
@@ -1089,6 +1169,32 @@ describe('parser', () => {
       i18nextParser.end(fakeFile)
     })
 
+    it('generates plurals with custom plural separator', (done) => {
+      let result
+      const i18nextParser = new i18nTransform({
+        pluralSeparator: '~~~',
+      })
+      const fakeFile = new Vinyl({
+        contents: Buffer.from("t('test {{count}}', { count: 1 })"),
+        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}}': '',
+          'test {{count}}~~~plural': '',
+        })
+        done()
+      })
+
+      i18nextParser.end(fakeFile)
+    })
+
     it('generates one plural key for unknow languages', (done) => {
       let result
       const i18nextParser = new i18nTransform({ locales: ['unknown'] })