From 56675a2ccbfc2714044b17c6e284a65b39520b47 Mon Sep 17 00:00:00 2001 From: yocarbo <38982184+yocarbo@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:53:28 +0200 Subject: [PATCH 1/4] feat(parser): Add exception filter for removing unused keys --- README.md | 23 +++++++++++++++++++++++ src/parser.js | 34 ++++++++++++++++++++++++++++++++-- 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 48242ce..dd50936 100755 --- a/README.md +++ b/README.md @@ -503,6 +503,7 @@ Below are the configuration options with their default values: compatibilityJSON: 'v3', // One of: 'v1', 'v2', 'v3', 'v4 debug: false, removeUnusedKeys: false, + filterUnusedKeys: false, sort: false, attr: { list: ['data-i18n'], @@ -574,6 +575,28 @@ Type: `Boolean` Default: `false` Set to `true` to remove unused translation keys from i18n resource files. +#### filterUnusedKeys + +Type: `Object` Default: `false` + +If an `Object` is supplied and `removeUnusedKeys` is set to `true`, to leave specific keys you can specify a function like so: +```js +filterUnusedKeys: ({ lng, ns, unusedKey }) => { + if (ns === 'resource') { + const exceptionKeys = ['word', 'key.word', ...]; + if (exceptionKeys.includes(unusedKey)) { + // leave key + return true; + } + } + if (ns === 'other_resource') { + // leave key + return true; + } + // remove key + return false; +} + #### sort Type: `Boolean` Default: `false` diff --git a/src/parser.js b/src/parser.js index d243d19..d850735 100644 --- a/src/parser.js +++ b/src/parser.js @@ -796,10 +796,40 @@ class Parser { const resStoreKeys = flattenObjectKeys(_.get(this.resStore, [lng, ns], {})); const resScanKeys = flattenObjectKeys(_.get(this.resScan, [lng, ns], {})); const unusedKeys = _.differenceWith(resStoreKeys, resScanKeys, _.isEqual); + let filteredKey; for (let i = 0; i < unusedKeys.length; ++i) { - _.unset(resMerged[lng][ns], unusedKeys[i]); - this.log(`Removed an unused translation key { ${chalk.red(JSON.stringify(unusedKeys[i]))} from ${chalk.red(JSON.stringify(this.formatResourceLoadPath(lng, ns)))}`); + filteredKey = false; + + if ( + this.options.filterUnusedKeys && + typeof this.options.filterUnusedKeys === 'function' + ) { + filteredKey = this.options.filterUnusedKeys({ + lng, + ns, + unusedKey: unusedKeys[i].toString().replaceAll(',', '.'), + }); + } + + if (!filteredKey) { + _.unset(resMerged[lng][ns], unusedKeys[i]); + this.log( + `Removed an unused translation key { ${chalk.red( + JSON.stringify(unusedKeys[i]) + )} } from ${chalk.red( + JSON.stringify(this.formatResourceLoadPath(lng, ns)) + )}` + ); + continue; + } + this.log( + `Skipped removing an unused translation key { ${chalk.yellow( + JSON.stringify(unusedKeys[i]) + )} } from ${chalk.yellow( + JSON.stringify(this.formatResourceLoadPath(lng, ns)) + )}` + ); } // Omit empty object From 0c23fa86b8f4f422b6fcf1a28a161240c025cc4f Mon Sep 17 00:00:00 2001 From: cheton Date: Mon, 14 Oct 2024 22:23:03 +0800 Subject: [PATCH 2/4] refactor: allow the `removeUnusedKeys` function to be passed in to determine if an unused key should be removed --- README.md | 42 ++++++++++++++++++++---------------------- src/parser.js | 41 +++++++++-------------------------------- 2 files changed, 29 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index dd50936..c4c3b5b 100755 --- a/README.md +++ b/README.md @@ -503,7 +503,6 @@ Below are the configuration options with their default values: compatibilityJSON: 'v3', // One of: 'v1', 'v2', 'v3', 'v4 debug: false, removeUnusedKeys: false, - filterUnusedKeys: false, sort: false, attr: { list: ['data-i18n'], @@ -571,31 +570,30 @@ Set to `true` to turn on debug output. #### removeUnusedKeys -Type: `Boolean` Default: `false` - -Set to `true` to remove unused translation keys from i18n resource files. - -#### filterUnusedKeys +Type: `Boolean` or `Function` Default: `false` -Type: `Object` Default: `false` +Set to `true` to remove unused translation keys from i18n resource files. By default, this is set to `false`. +```js +{ // Default + removeUnusedKeys: false, +} +``` -If an `Object` is supplied and `removeUnusedKeys` is set to `true`, to leave specific keys you can specify a function like so: +If a function is provided, it will be used to decide whether an unused translation key should be removed. ```js -filterUnusedKeys: ({ lng, ns, unusedKey }) => { +// Available since 4.6.0 +// +// @param {string} lng The language of the unused translation key. +// @param {string} ns The namespace of the unused translation key. +// @param {array} key The translation key in its array form. +// @return {boolean} Returns true if the unused translation key should be removed. +removeUnusedKeys: function(lng, ns, key) { if (ns === 'resource') { - const exceptionKeys = ['word', 'key.word', ...]; - if (exceptionKeys.includes(unusedKey)) { - // leave key - return true; - } - } - if (ns === 'other_resource') { - // leave key return true; } - // remove key return false; } +``` #### sort @@ -617,7 +615,7 @@ If an `Object` is supplied, you can either specify a list of attributes and exte } ``` -You can set attr to `false` to disable parsing attribute as below: +You can set `attr` to `false` to disable parsing attribute as below: ```js { attr: false @@ -638,7 +636,7 @@ If an `Object` is supplied, you can either specify a list of translation functio } ``` -You can set func to `false` to disable parsing translation function as below: +You can set `func` to `false` to disable parsing translation function as below: ```js { func: false @@ -672,14 +670,14 @@ If an `Object` is supplied, you can specify a list of extensions, or override th } ``` -You can set trans to `false` to disable parsing Trans component as below: +You can set `trans` to `false` to disable parsing Trans component as below: ```js { trans: false } ``` -The fallbackKey can either be a boolean value, or a function like so: +The `fallbackKey` can either be a boolean value, or a function like so: ```js fallbackKey: function(ns, value) { // Returns a hash value as the fallback key diff --git a/src/parser.js b/src/parser.js index d850735..a1a5e51 100644 --- a/src/parser.js +++ b/src/parser.js @@ -786,7 +786,7 @@ class Parser { } let resStore = {}; - if (this.options.removeUnusedKeys) { + if (!!this.options.removeUnusedKeys) { // Merge two objects `resStore` and `resScan` deeply, returning a new merged object with the elements from both `resStore` and `resScan`. const overwriteMerge = (destinationArray, sourceArray, options) => sourceArray; const resMerged = deepMerge(this.resStore, this.resScan, { arrayMerge: overwriteMerge }); @@ -796,40 +796,17 @@ class Parser { const resStoreKeys = flattenObjectKeys(_.get(this.resStore, [lng, ns], {})); const resScanKeys = flattenObjectKeys(_.get(this.resScan, [lng, ns], {})); const unusedKeys = _.differenceWith(resStoreKeys, resScanKeys, _.isEqual); - let filteredKey; for (let i = 0; i < unusedKeys.length; ++i) { - filteredKey = false; - - if ( - this.options.filterUnusedKeys && - typeof this.options.filterUnusedKeys === 'function' - ) { - filteredKey = this.options.filterUnusedKeys({ - lng, - ns, - unusedKey: unusedKeys[i].toString().replaceAll(',', '.'), - }); - } - - if (!filteredKey) { - _.unset(resMerged[lng][ns], unusedKeys[i]); - this.log( - `Removed an unused translation key { ${chalk.red( - JSON.stringify(unusedKeys[i]) - )} } from ${chalk.red( - JSON.stringify(this.formatResourceLoadPath(lng, ns)) - )}` - ); - continue; + const unusedKey = unusedKeys[i]; + const isRemovable = (typeof this.options.removeUnusedKeys === 'function') + ? this.options.removeUnusedKeys(lng, ns, unusedKey) + : !!this.options.removeUnusedKeys; + + if (isRemovable) { + _.unset(resMerged[lng][ns], unusedKey); + this.log(`Removed an unused translation key { ${chalk.red(JSON.stringify(unusedKey))} } from ${chalk.red(JSON.stringify(this.formatResourceLoadPath(lng, ns)))}`); } - this.log( - `Skipped removing an unused translation key { ${chalk.yellow( - JSON.stringify(unusedKeys[i]) - )} } from ${chalk.yellow( - JSON.stringify(this.formatResourceLoadPath(lng, ns)) - )}` - ); } // Omit empty object From 3784b23cfbf916e84f8f8f53d137f26bbc2d0d5a Mon Sep 17 00:00:00 2001 From: cheton Date: Mon, 14 Oct 2024 22:27:36 +0800 Subject: [PATCH 3/4] chore: bump to 4.6.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f2a92cb..08b7397 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "i18next-scanner", - "version": "4.5.0", + "version": "4.6.0", "description": "Scan your code, extract translation keys/values, and merge them into i18n resource files.", "homepage": "https://github.com/i18next/i18next-scanner", "author": "Cheton Wu ", From 01a3f13309634081dbc00aeb5e1b5f3359eafd43 Mon Sep 17 00:00:00 2001 From: cheton Date: Mon, 14 Oct 2024 22:36:48 +0800 Subject: [PATCH 4/4] test: enhance test case for the `removeUnusedKeys` option --- test/transform-stream.test.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/test/transform-stream.test.js b/test/transform-stream.test.js index ac5aeb9..e7fa172 100644 --- a/test/transform-stream.test.js +++ b/test/transform-stream.test.js @@ -402,7 +402,12 @@ test('Keep old translations', done => { // https://github.com/i18next/i18next-scanner/issues/30 test('Remove old translation keys which are already removed from code', done => { const options = _.merge({}, defaults, { - removeUnusedKeys: true, + removeUnusedKeys: function(lng, ns, key) { + if (ns === 'resource') { + return true; + } + return false; + }, resource: { loadPath: 'test/fixtures/i18n/{{lng}}/{{ns}}.json', savePath: 'i18n/{{lng}}/{{ns}}.json' @@ -420,7 +425,11 @@ test('Remove old translation keys which are already removed from code', done => // English - locale.json if (file.path === 'i18n/en/locale.json') { const found = JSON.parse(contents); - const wanted = {}; + const wanted = { + 'language': { + 'en-US': 'English', + }, + }; expect(found).toEqual(wanted); } @@ -441,7 +450,11 @@ test('Remove old translation keys which are already removed from code', done => // German - locale.json if (file.path === 'i18n/de/locale.json') { const found = JSON.parse(contents); - const wanted = {}; + const wanted = { + 'language': { + 'de-DE': 'German', + }, + }; expect(found).toEqual(wanted); }