i18next-cli 1.67.9 → 1.68.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -240,6 +240,7 @@ The primary language is checked too: any key used in your code but absent from t
240
240
  **Options:**
241
241
  - `--namespace <ns>, -n <ns>`: Filter the report by a specific namespace.
242
242
  - `--hide-translated`: Hide already translated keys in the detailed view, showing only missing translations.
243
+ - `--unused`: Report only unused translation keys — keys present in your translation files that are no longer used in your source code (i.e. what `extract` with `removeUnusedKeys` would delete). Never modifies any files and exits with a non-zero status code when unused keys are found, so it can serve as a dedicated CI check alongside the regular missing-translations check. Note that static analysis cannot detect dynamically constructed keys (e.g. ``t(`error.${code}`)``); to find keys that are truly unused at runtime, see [find unused translations with locize](https://www.locize.com/docs/guides/find-unused-translations).
243
244
 
244
245
  **Usage Examples:**
245
246
 
@@ -261,6 +262,12 @@ npx i18next-cli status de --hide-translated
261
262
 
262
263
  # Combine options to see only missing translations in a specific namespace
263
264
  npx i18next-cli status de --namespace common --hide-translated
265
+
266
+ # Report only unused keys across all locales (read-only, exits 1 when any are found)
267
+ npx i18next-cli status --unused
268
+
269
+ # Report only unused keys in the 'en' files — e.g. as a separate CI check
270
+ npx i18next-cli status en --unused
264
271
  ```
265
272
 
266
273
  The detailed view provides a rich, at-a-glance summary for each namespace, followed by a list of every key and its translation status.
package/dist/cjs/cli.js CHANGED
@@ -37,7 +37,7 @@ const program = new commander.Command();
37
37
  program
38
38
  .name('i18next-cli')
39
39
  .description('A unified, high-performance i18next CLI.')
40
- .version('1.67.9'); // This string is replaced with the actual version at build time by rollup
40
+ .version('1.68.0'); // This string is replaced with the actual version at build time by rollup
41
41
  // new: global config override option
42
42
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
43
43
  program
@@ -118,6 +118,7 @@ program
118
118
  .description('Display translation status. Provide a locale for a detailed key-by-key view.')
119
119
  .option('-n, --namespace <ns>', 'Filter the status report by a specific namespace')
120
120
  .option('--hide-translated', 'Hide already translated keys in the detailed view')
121
+ .option('--unused', 'Report only unused translation keys (read-only; exits 1 when any are found)')
121
122
  .action(async (locale, options) => {
122
123
  const cfgPath = program.opts().config;
123
124
  let config$1 = await config.loadConfig(cfgPath);
@@ -132,6 +133,10 @@ program
132
133
  console.log(node_util.styleText('green', 'Project structure detected successfully!'));
133
134
  config$1 = detected;
134
135
  }
136
+ if (options.unused) {
137
+ await status.runUnusedReport(config$1, { locale, namespace: options.namespace });
138
+ return;
139
+ }
135
140
  await status.runStatus(config$1, { detail: locale, namespace: options.namespace, hideTranslated: !!options.hideTranslated });
136
141
  });
137
142
  program
package/dist/cjs/index.js CHANGED
@@ -30,6 +30,7 @@ exports.recommendedAcceptedTags = linter.recommendedAcceptedTags;
30
30
  exports.runLinter = linter.runLinter;
31
31
  exports.runSyncer = syncer.runSyncer;
32
32
  exports.runStatus = status.runStatus;
33
+ exports.runUnusedReport = status.runUnusedReport;
33
34
  exports.runTypesGenerator = typesGenerator.runTypesGenerator;
34
35
  exports.runRenameKey = renameKey.runRenameKey;
35
36
  exports.runInstrumenter = instrumenter.runInstrumenter;
@@ -3,8 +3,7 @@
3
3
  var node_util = require('node:util');
4
4
  var ora = require('ora');
5
5
  var node_path = require('node:path');
6
- require('@swc/core');
7
- require('node:fs/promises');
6
+ var extractor = require('./extractor/core/extractor.js');
8
7
  var keyFinder = require('./extractor/core/key-finder.js');
9
8
  require('glob');
10
9
  var nestedObject = require('./utils/nested-object.js');
@@ -12,8 +11,8 @@ var fileUtils = require('./utils/file-utils.js');
12
11
  var pluralRules = require('./utils/plural-rules.js');
13
12
  var nesting = require('./utils/nesting.js');
14
13
  var contextVariants = require('./utils/context-variants.js');
15
- var funnelMsgTracker = require('./utils/funnel-msg-tracker.js');
16
14
  require('node:module');
15
+ var funnelMsgTracker = require('./utils/funnel-msg-tracker.js');
17
16
 
18
17
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
19
18
 
@@ -738,6 +737,87 @@ function generateProgressBarText(percentage) {
738
737
  const emptyBars = totalBars - filledBars;
739
738
  return `[${node_util.styleText('green', ''.padStart(filledBars, '■'))}${''.padStart(emptyBars, '□')}]`;
740
739
  }
740
+ /**
741
+ * Reports translation keys that exist in the translation files but are no
742
+ * longer used in the source code (see issue #281).
743
+ *
744
+ * "Unused" is defined as "what `extract` with `removeUnusedKeys` would delete":
745
+ * the report runs the extractor in dry-run mode and diffs the existing key set
746
+ * against the pruned result. This inherits all of extract's edge-case handling
747
+ * (plural variants, context variants, `preservePatterns`, `ignoreNamespaces`)
748
+ * instead of re-implementing usedness detection that could drift from it.
749
+ *
750
+ * The command never writes any files and exits with a non-zero status code
751
+ * when unused keys are found, so it can serve as a dedicated CI check
752
+ * alongside `status <locale>` (missing translations).
753
+ */
754
+ async function runUnusedReport(config, options = {}) {
755
+ if (options.locale && !config.locales.includes(options.locale)) {
756
+ console.error(node_util.styleText('red', `Error: Locale "${options.locale}" is not defined in your configuration.`));
757
+ process.exit(1);
758
+ return;
759
+ }
760
+ // Work on a copy with `removeUnusedKeys` forced on: the dry-run diff below
761
+ // derives "unused" from what extract would prune, which requires pruning to
762
+ // be active regardless of the user's config. The caller's config object
763
+ // stays untouched.
764
+ const cfg = { ...config, extract: { ...config.extract, removeUnusedKeys: true } };
765
+ const spinner = ora__default.default('Analyzing project for unused translation keys...\n').start();
766
+ let extraction;
767
+ try {
768
+ extraction = await extractor.runExtractor(cfg, { isDryRun: true, quiet: true });
769
+ spinner.succeed('Analysis complete.');
770
+ }
771
+ catch (error) {
772
+ spinner.fail('Failed to analyze unused translation keys.');
773
+ console.error(error);
774
+ process.exit(1);
775
+ return;
776
+ }
777
+ const { results, hasErrors } = extraction;
778
+ const rawSep = cfg.extract.keySeparator;
779
+ const keySeparator = rawSep === false ? false : (rawSep ?? '.');
780
+ let totalUnused = 0;
781
+ for (const result of results) {
782
+ if (options.locale && result.locale !== options.locale)
783
+ continue;
784
+ if (options.namespace && result.namespace && result.namespace !== options.namespace)
785
+ continue;
786
+ const keptKeys = new Set(nestedObject.getNestedKeys(result.newTranslations || {}, keySeparator));
787
+ const unusedKeys = nestedObject.getNestedKeys(result.existingTranslations || {}, keySeparator)
788
+ .filter(key => !keptKeys.has(key))
789
+ .sort();
790
+ if (unusedKeys.length === 0)
791
+ continue;
792
+ totalUnused += unusedKeys.length;
793
+ const label = result.namespace ? `${result.locale}/${result.namespace}` : result.locale;
794
+ console.log(node_util.styleText(['cyan', 'bold'], `\n[${label}] ${result.path}`));
795
+ for (const key of unusedKeys) {
796
+ console.log(` ${node_util.styleText('red', '✗')} ${key}`);
797
+ }
798
+ }
799
+ if (hasErrors) {
800
+ console.log(node_util.styleText(['yellow', 'bold'], '\n⚠ Some source files could not be parsed — keys used only in those files may be falsely reported as unused.'));
801
+ }
802
+ if (totalUnused > 0) {
803
+ console.log(node_util.styleText(['yellow', 'bold'], `\nSummary: Found ${totalUnused} unused key(s)${options.locale ? ` for "${options.locale}"` : ''}. No files were modified.`));
804
+ console.log(`Run ${node_util.styleText('cyan', 'npx i18next-cli extract')} to remove them.`);
805
+ }
806
+ else {
807
+ console.log(node_util.styleText(['green', 'bold'], '\nSummary: 🎉 No unused keys found.'));
808
+ }
809
+ // Static analysis has an inherent blind spot for dynamically constructed
810
+ // keys, so this link doubles as an accuracy disclaimer. Gated like the other
811
+ // funnel messages (never in CI/non-TTY, 24h cooldown).
812
+ if (await funnelMsgTracker.shouldShowFunnel('status-unused')) {
813
+ console.log(node_util.styleText('gray', "\nℹ Static analysis cannot detect dynamically constructed keys (e.g. t('error.' + code))."));
814
+ console.log(node_util.styleText('gray', ' To find keys that are truly unused at runtime, see https://www.locize.com/docs/guides/find-unused-translations'));
815
+ await funnelMsgTracker.recordFunnelShown('status-unused');
816
+ }
817
+ if (totalUnused > 0 || hasErrors) {
818
+ process.exit(1);
819
+ }
820
+ }
741
821
  async function printLocizeFunnel() {
742
822
  if (!(await funnelMsgTracker.shouldShowFunnel('status')))
743
823
  return;
@@ -748,3 +828,4 @@ async function printLocizeFunnel() {
748
828
  }
749
829
 
750
830
  exports.runStatus = runStatus;
831
+ exports.runUnusedReport = runUnusedReport;
package/dist/esm/cli.js CHANGED
@@ -19,7 +19,7 @@ import { runSyncer } from './syncer.js';
19
19
  import { runMigrator } from './migrator.js';
20
20
  import { runInit } from './init.js';
21
21
  import { runLinterCli } from './linter.js';
22
- import { runStatus } from './status.js';
22
+ import { runUnusedReport, runStatus } from './status.js';
23
23
  import { runLocizeSync, runLocizeDownload, runLocizeMigrate } from './locize.js';
24
24
  import { runRenameKey } from './rename-key.js';
25
25
  import { runInstrumenter } from './instrumenter/core/instrumenter.js';
@@ -31,7 +31,7 @@ const program = new Command();
31
31
  program
32
32
  .name('i18next-cli')
33
33
  .description('A unified, high-performance i18next CLI.')
34
- .version('1.67.9'); // This string is replaced with the actual version at build time by rollup
34
+ .version('1.68.0'); // This string is replaced with the actual version at build time by rollup
35
35
  // new: global config override option
36
36
  program.option('-c, --config <path>', 'Path to i18next-cli config file (overrides detection)');
37
37
  program
@@ -112,6 +112,7 @@ program
112
112
  .description('Display translation status. Provide a locale for a detailed key-by-key view.')
113
113
  .option('-n, --namespace <ns>', 'Filter the status report by a specific namespace')
114
114
  .option('--hide-translated', 'Hide already translated keys in the detailed view')
115
+ .option('--unused', 'Report only unused translation keys (read-only; exits 1 when any are found)')
115
116
  .action(async (locale, options) => {
116
117
  const cfgPath = program.opts().config;
117
118
  let config = await loadConfig(cfgPath);
@@ -126,6 +127,10 @@ program
126
127
  console.log(styleText('green', 'Project structure detected successfully!'));
127
128
  config = detected;
128
129
  }
130
+ if (options.unused) {
131
+ await runUnusedReport(config, { locale, namespace: options.namespace });
132
+ return;
133
+ }
129
134
  await runStatus(config, { detail: locale, namespace: options.namespace, hideTranslated: !!options.hideTranslated });
130
135
  });
131
136
  program
package/dist/esm/index.js CHANGED
@@ -5,7 +5,7 @@ export { getTranslations } from './extractor/core/translation-manager.js';
5
5
  import 'node:module';
6
6
  export { recommendedAcceptedAttributes, recommendedAcceptedTags, runLinter } from './linter.js';
7
7
  export { runSyncer } from './syncer.js';
8
- export { runStatus } from './status.js';
8
+ export { runStatus, runUnusedReport } from './status.js';
9
9
  export { runTypesGenerator } from './types-generator.js';
10
10
  export { runRenameKey } from './rename-key.js';
11
11
  export { runInstrumenter, writeExtractedKeys } from './instrumenter/core/instrumenter.js';
@@ -1,8 +1,7 @@
1
1
  import { styleText } from 'node:util';
2
2
  import ora from 'ora';
3
3
  import { resolve } from 'node:path';
4
- import '@swc/core';
5
- import 'node:fs/promises';
4
+ import { runExtractor } from './extractor/core/extractor.js';
6
5
  import { findKeys } from './extractor/core/key-finder.js';
7
6
  import 'glob';
8
7
  import { getNestedKeys, getNestedValue } from './utils/nested-object.js';
@@ -10,8 +9,8 @@ import { loadTranslationFile, getOutputPath } from './utils/file-utils.js';
10
9
  import { safePluralRules } from './utils/plural-rules.js';
11
10
  import { parseNestedReferences } from './utils/nesting.js';
12
11
  import { isContextVariantOfAcceptingKey } from './utils/context-variants.js';
13
- import { shouldShowFunnel, recordFunnelShown } from './utils/funnel-msg-tracker.js';
14
12
  import 'node:module';
13
+ import { shouldShowFunnel, recordFunnelShown } from './utils/funnel-msg-tracker.js';
15
14
 
16
15
  function classifyValue(value) {
17
16
  if (value === undefined || value === null)
@@ -732,6 +731,87 @@ function generateProgressBarText(percentage) {
732
731
  const emptyBars = totalBars - filledBars;
733
732
  return `[${styleText('green', ''.padStart(filledBars, '■'))}${''.padStart(emptyBars, '□')}]`;
734
733
  }
734
+ /**
735
+ * Reports translation keys that exist in the translation files but are no
736
+ * longer used in the source code (see issue #281).
737
+ *
738
+ * "Unused" is defined as "what `extract` with `removeUnusedKeys` would delete":
739
+ * the report runs the extractor in dry-run mode and diffs the existing key set
740
+ * against the pruned result. This inherits all of extract's edge-case handling
741
+ * (plural variants, context variants, `preservePatterns`, `ignoreNamespaces`)
742
+ * instead of re-implementing usedness detection that could drift from it.
743
+ *
744
+ * The command never writes any files and exits with a non-zero status code
745
+ * when unused keys are found, so it can serve as a dedicated CI check
746
+ * alongside `status <locale>` (missing translations).
747
+ */
748
+ async function runUnusedReport(config, options = {}) {
749
+ if (options.locale && !config.locales.includes(options.locale)) {
750
+ console.error(styleText('red', `Error: Locale "${options.locale}" is not defined in your configuration.`));
751
+ process.exit(1);
752
+ return;
753
+ }
754
+ // Work on a copy with `removeUnusedKeys` forced on: the dry-run diff below
755
+ // derives "unused" from what extract would prune, which requires pruning to
756
+ // be active regardless of the user's config. The caller's config object
757
+ // stays untouched.
758
+ const cfg = { ...config, extract: { ...config.extract, removeUnusedKeys: true } };
759
+ const spinner = ora('Analyzing project for unused translation keys...\n').start();
760
+ let extraction;
761
+ try {
762
+ extraction = await runExtractor(cfg, { isDryRun: true, quiet: true });
763
+ spinner.succeed('Analysis complete.');
764
+ }
765
+ catch (error) {
766
+ spinner.fail('Failed to analyze unused translation keys.');
767
+ console.error(error);
768
+ process.exit(1);
769
+ return;
770
+ }
771
+ const { results, hasErrors } = extraction;
772
+ const rawSep = cfg.extract.keySeparator;
773
+ const keySeparator = rawSep === false ? false : (rawSep ?? '.');
774
+ let totalUnused = 0;
775
+ for (const result of results) {
776
+ if (options.locale && result.locale !== options.locale)
777
+ continue;
778
+ if (options.namespace && result.namespace && result.namespace !== options.namespace)
779
+ continue;
780
+ const keptKeys = new Set(getNestedKeys(result.newTranslations || {}, keySeparator));
781
+ const unusedKeys = getNestedKeys(result.existingTranslations || {}, keySeparator)
782
+ .filter(key => !keptKeys.has(key))
783
+ .sort();
784
+ if (unusedKeys.length === 0)
785
+ continue;
786
+ totalUnused += unusedKeys.length;
787
+ const label = result.namespace ? `${result.locale}/${result.namespace}` : result.locale;
788
+ console.log(styleText(['cyan', 'bold'], `\n[${label}] ${result.path}`));
789
+ for (const key of unusedKeys) {
790
+ console.log(` ${styleText('red', '✗')} ${key}`);
791
+ }
792
+ }
793
+ if (hasErrors) {
794
+ console.log(styleText(['yellow', 'bold'], '\n⚠ Some source files could not be parsed — keys used only in those files may be falsely reported as unused.'));
795
+ }
796
+ if (totalUnused > 0) {
797
+ console.log(styleText(['yellow', 'bold'], `\nSummary: Found ${totalUnused} unused key(s)${options.locale ? ` for "${options.locale}"` : ''}. No files were modified.`));
798
+ console.log(`Run ${styleText('cyan', 'npx i18next-cli extract')} to remove them.`);
799
+ }
800
+ else {
801
+ console.log(styleText(['green', 'bold'], '\nSummary: 🎉 No unused keys found.'));
802
+ }
803
+ // Static analysis has an inherent blind spot for dynamically constructed
804
+ // keys, so this link doubles as an accuracy disclaimer. Gated like the other
805
+ // funnel messages (never in CI/non-TTY, 24h cooldown).
806
+ if (await shouldShowFunnel('status-unused')) {
807
+ console.log(styleText('gray', "\nℹ Static analysis cannot detect dynamically constructed keys (e.g. t('error.' + code))."));
808
+ console.log(styleText('gray', ' To find keys that are truly unused at runtime, see https://www.locize.com/docs/guides/find-unused-translations'));
809
+ await recordFunnelShown('status-unused');
810
+ }
811
+ if (totalUnused > 0 || hasErrors) {
812
+ process.exit(1);
813
+ }
814
+ }
735
815
  async function printLocizeFunnel() {
736
816
  if (!(await shouldShowFunnel('status')))
737
817
  return;
@@ -741,4 +821,4 @@ async function printLocizeFunnel() {
741
821
  return recordFunnelShown('status');
742
822
  }
743
823
 
744
- export { runStatus };
824
+ export { runStatus, runUnusedReport };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18next-cli",
3
- "version": "1.67.9",
3
+ "version": "1.68.0",
4
4
  "description": "A unified, high-performance i18next CLI.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAqBnC,QAAA,MAAM,OAAO,SAAgB,CAAA;AA4c7B,OAAO,EAAE,OAAO,EAAE,CAAA"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAqBnC,QAAA,MAAM,OAAO,SAAgB,CAAA;AAid7B,OAAO,EAAE,OAAO,EAAE,CAAA"}
package/types/index.d.ts CHANGED
@@ -3,7 +3,7 @@ export { defineConfig } from './config.js';
3
3
  export { extract, findKeys, getTranslations, runExtractor } from './extractor.js';
4
4
  export { runLinter, recommendedAcceptedTags, recommendedAcceptedAttributes } from './linter.js';
5
5
  export { runSyncer } from './syncer.js';
6
- export { runStatus } from './status.js';
6
+ export { runStatus, runUnusedReport } from './status.js';
7
7
  export { runTypesGenerator } from './types-generator.js';
8
8
  export { runRenameKey } from './rename-key.js';
9
9
  export { runInstrumenter, writeExtractedKeys } from './instrumenter/index.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,oBAAoB,EACpB,MAAM,EACN,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,SAAS,EACT,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,MAAM,EACN,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,yBAAyB,EACzB,sBAAsB,EACtB,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACxB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EACL,OAAO,EACP,QAAQ,EACR,eAAe,EACf,YAAY,EACb,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,SAAS,EAAE,uBAAuB,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAA;AAC/F,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC7E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,oBAAoB,EACpB,MAAM,EACN,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,SAAS,EACT,aAAa,EACb,YAAY,EACZ,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,MAAM,EACN,mBAAmB,EACnB,eAAe,EACf,eAAe,EACf,yBAAyB,EACzB,sBAAsB,EACtB,iBAAiB,EACjB,cAAc,EACd,qBAAqB,EACrB,uBAAuB,EACxB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EACL,OAAO,EACP,QAAQ,EACR,eAAe,EACf,YAAY,EACb,MAAM,gBAAgB,CAAA;AAEvB,OAAO,EAAE,SAAS,EAAE,uBAAuB,EAAE,6BAA6B,EAAE,MAAM,aAAa,CAAA;AAC/F,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AACxD,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAC7E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAA;AAC/D,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA"}
package/types/status.d.ts CHANGED
@@ -30,5 +30,29 @@ interface StatusOptions {
30
30
  * @throws {Error} When unable to extract keys or read translation files
31
31
  */
32
32
  export declare function runStatus(config: I18nextToolkitConfig, options?: StatusOptions): Promise<void>;
33
+ /**
34
+ * Options for the unused-keys report.
35
+ */
36
+ interface UnusedOptions {
37
+ /** Restrict the report to a single locale */
38
+ locale?: string;
39
+ /** Restrict the report to a single namespace */
40
+ namespace?: string;
41
+ }
42
+ /**
43
+ * Reports translation keys that exist in the translation files but are no
44
+ * longer used in the source code (see issue #281).
45
+ *
46
+ * "Unused" is defined as "what `extract` with `removeUnusedKeys` would delete":
47
+ * the report runs the extractor in dry-run mode and diffs the existing key set
48
+ * against the pruned result. This inherits all of extract's edge-case handling
49
+ * (plural variants, context variants, `preservePatterns`, `ignoreNamespaces`)
50
+ * instead of re-implementing usedness detection that could drift from it.
51
+ *
52
+ * The command never writes any files and exits with a non-zero status code
53
+ * when unused keys are found, so it can serve as a dedicated CI check
54
+ * alongside `status <locale>` (missing translations).
55
+ */
56
+ export declare function runUnusedReport(config: I18nextToolkitConfig, options?: UnusedOptions): Promise<void>;
33
57
  export {};
34
58
  //# sourceMappingURL=status.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAgB,MAAM,YAAY,CAAA;AAOpE;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAyHD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAoDzF"}
1
+ {"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../src/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,oBAAoB,EAAmC,MAAM,YAAY,CAAA;AAOvF;;GAEG;AACH,UAAU,aAAa;IACrB,0EAA0E;IAC1E,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wCAAwC;IACxC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAyHD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,SAAS,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAoDzF;AAsnBD;;GAEG;AACH,UAAU,aAAa;IACrB,6CAA6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,eAAe,CAAE,MAAM,EAAE,oBAAoB,EAAE,OAAO,GAAE,aAAkB,iBAuE/F"}