i18ntk 4.4.2 → 4.4.4

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/CHANGELOG.md CHANGED
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [4.4.4] - 2026-06-05
9
+
10
+ ### Fixed
11
+ - Likely-untranslated reporting now ignores placeholder-only and symbol/dynamic values such as `{file}`, `{path}`, and icon-prefixed labels instead of treating them as untranslated English.
12
+ - Dynamic values with translated surrounding copy and English placeholder tokens, such as `"command": "指示: {command}"`, are no longer flagged as untranslated.
13
+
14
+ ### Changed
15
+ - `.i18ntk-config` now accepts a top-level `extensions` object for VS Code Workbench and Lens settings. The CLI preserves this section during config validation and ignores unknown extension-owned nested keys.
16
+ - Documented shared config edge cases so editor extensions can sync workspace defaults without changing CLI behavior.
17
+
18
+ ## [4.4.3] - 2026-06-04
19
+
20
+ ### Fixed
21
+ - `package.public.json` now includes the `./report` export entry (`./utils/report-model.js`) that was missing, fixing the sync check during public package builds.
22
+
8
23
  ## [4.4.2] - 2026-06-02
9
24
 
10
25
  ### Fixed
package/README.md CHANGED
@@ -14,6 +14,14 @@ A zero-dependency internationalization toolkit for setup, scanning, analysis, va
14
14
  [![i18ntk Workbench](https://img.shields.io/badge/VS_Code-i18ntk_Workbench-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=VladNoskov.i18ntk-workbench)
15
15
  [![i18ntk Lens](https://img.shields.io/badge/VS_Code-i18ntk_Lens-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=VladNoskov.i18ntk-lens)
16
16
 
17
+ ## The i18ntk ecosystem
18
+
19
+ - i18ntk — CLI and runtime toolkit
20
+ - i18ntk Workbench — full VS Code dashboard and reports
21
+ - i18ntk Lens — inline hovers, CodeLens, and diagnostics
22
+
23
+ Use the CLI in CI, Workbench for project-level management, and Lens for day-to-day editor feedback.
24
+
17
25
  ## Install
18
26
 
19
27
  ```bash
@@ -56,6 +64,12 @@ i18next is mainly a runtime internationalization library. i18ntk is mainly workf
56
64
  | Validation reports | Yes | Limited |
57
65
  | Auto-translation workflow | Yes | External tooling |
58
66
 
67
+ ## What's New in 4.4.4
68
+
69
+ - **SHARED CONFIG FOR EXTENSIONS**: `.i18ntk-config` can now carry editor-owned settings under `extensions.workbench` and `extensions.lens`. The CLI preserves the top-level `extensions` object and ignores unknown nested extension keys.
70
+ - **DYNAMIC VALUE REPORTING**: Likely-untranslated reports now ignore placeholder-only and symbol/dynamic values such as `{file}`, `{path}`, and icon-prefixed labels.
71
+ - **PLACEHOLDER-AWARE TRANSLATION CHECKS**: Values with translated surrounding copy and English placeholder tokens, such as `"command": "指示: {command}"`, are no longer reported as untranslated.
72
+
59
73
  ## What's New in 4.4.2
60
74
 
61
75
  - **AUTO TRANSLATE RELATIVE PATHS**: Programmatic `processFile()` calls now accept project-relative source paths, matching CLI source resolution.
@@ -109,6 +123,7 @@ Run common checks:
109
123
  i18ntk --command=analyze
110
124
  i18ntk --command=validate
111
125
  i18ntk --command=usage
126
+ i18ntk report --json
112
127
  i18ntk --command=sizing
113
128
  i18ntk --command=summary
114
129
  ```
@@ -141,6 +156,7 @@ i18ntk --command=init
141
156
  i18ntk --command=analyze
142
157
  i18ntk --command=validate
143
158
  i18ntk --command=usage
159
+ i18ntk report --json --markdown --html --out ./i18ntk-reports
144
160
  i18ntk --command=scanner
145
161
  i18ntk --command=sizing
146
162
  i18ntk --command=complete
@@ -155,6 +171,7 @@ i18ntk-init
155
171
  i18ntk-analyze
156
172
  i18ntk-validate
157
173
  i18ntk-usage
174
+ i18ntk-report
158
175
  i18ntk-scanner
159
176
  i18ntk-sizing
160
177
  i18ntk-complete
@@ -176,6 +193,7 @@ Note: manager route `i18ntk --command=backup` is disabled in current builds. Use
176
193
  | `i18ntk --command=analyze` / `i18ntk-analyze` | Compares source and target translation coverage. | Missing keys, extra keys, untranslated markers, completion by language. | Markdown/JSON/text reports when report output is enabled. |
177
194
  | `i18ntk --command=validate` / `i18ntk-validate` | Validates structure and translation quality risks. | Placeholder mismatches, missing keys, risky URLs/emails/secrets, likely English target text. | Validation summary report. Does not edit locale files. |
178
195
  | `i18ntk --command=usage` / `i18ntk-usage` | Maps translation keys to source files and finds unused/missing keys. | Direct i18n calls, literal known-key references, bounded dynamic templates/object maps, unresolved dynamic expressions, hardcoded text candidates, namespace/file naming mismatches. | Usage report with key locations, namespace recommendations, unresolved dynamic expressions, hardcoded text suggestions, and optional dead-key report. Does not delete unless cleanup deletion is explicitly enabled. |
196
+ | `i18ntk report` / `i18ntk-report` | Generates the stable schemaVersion 1 report used by CLI automation and i18ntk Workbench. | Locale completeness, missing keys, unused keys with confidence, placeholders, likely untranslated values, expansion risk, and hardcoded text candidates. | JSON to stdout by default, plus JSON/Markdown/HTML files when `--out` is used. Does not edit locale files. |
179
197
  | `i18ntk --command=scanner` / `i18ntk-scanner` | Scans source for i18n issues and hardcoded user-facing text. | JSX/template text, common text attributes, i18n usage patterns, source-language text profiles. | Scanner report. Does not edit files. |
180
198
  | `i18ntk --command=complete` / `i18ntk-complete` | Adds missing keys to target language files for 100% key coverage. | Source-language keys missing from targets. | Target locale JSON files, using missing translation markers/prefixes. |
181
199
  | `i18ntk --command=translate` / `i18ntk-translate` | Auto-translates locale JSON using configured provider behavior. | Missing, empty, untranslated-marker, source-copy, likely-English, or visibly corrupt target values by default. | Target locale JSON files and translation reports. Existing translated values are kept unless `--translate-all` is used. If unresolved values remain after retry, writes `i18ntk-reports/auto-translate/latest.json` for targeted follow-up. |
@@ -540,7 +558,7 @@ Example:
540
558
 
541
559
  ```json
542
560
  {
543
- "version": "4.3.3",
561
+ "version": "4.4.4",
544
562
  "sourceDir": "./locales",
545
563
  "i18nDir": "./locales",
546
564
  "outputDir": "./i18ntk-reports",
@@ -570,6 +588,17 @@ Example:
570
588
  },
571
589
  "setup": {
572
590
  "completed": true
591
+ },
592
+ "extensions": {
593
+ "workbench": {
594
+ "localeDirectory": "./locales",
595
+ "sourceLocale": "en"
596
+ },
597
+ "lens": {
598
+ "localeDirectory": "./locales",
599
+ "sourceLocale": "en",
600
+ "keyFormats": ["dot", "snake"]
601
+ }
573
602
  }
574
603
  }
575
604
  ```
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const {
7
+ generateI18ntkReport,
8
+ renderReportAsMarkdown,
9
+ renderReportAsHtml,
10
+ } = require('../utils/report-model');
11
+
12
+ function parseArgs(argv = process.argv.slice(2)) {
13
+ const args = {
14
+ json: false,
15
+ markdown: false,
16
+ html: false,
17
+ out: null,
18
+ sourceDir: null,
19
+ i18nDir: null,
20
+ sourceLocale: 'en',
21
+ help: false,
22
+ };
23
+
24
+ for (const raw of argv) {
25
+ const arg = raw === 'report' ? '' : raw;
26
+ if (!arg) continue;
27
+ if (arg === '--help' || arg === '-h') args.help = true;
28
+ else if (arg === '--json') args.json = true;
29
+ else if (arg === '--markdown' || arg === '--md') args.markdown = true;
30
+ else if (arg === '--html') args.html = true;
31
+ else if (arg.startsWith('--out=')) args.out = arg.slice('--out='.length);
32
+ else if (arg === '--out') args.expectOut = true;
33
+ else if (args.expectOut) {
34
+ args.out = arg;
35
+ args.expectOut = false;
36
+ } else if (arg.startsWith('--source-dir=')) args.sourceDir = arg.slice('--source-dir='.length);
37
+ else if (arg.startsWith('--i18n-dir=')) args.i18nDir = arg.slice('--i18n-dir='.length);
38
+ else if (arg.startsWith('--locales-dir=')) args.i18nDir = arg.slice('--locales-dir='.length);
39
+ else if (arg.startsWith('--source-language=')) args.sourceLocale = arg.slice('--source-language='.length);
40
+ else if (arg.startsWith('--source-locale=')) args.sourceLocale = arg.slice('--source-locale='.length);
41
+ }
42
+
43
+ if (!args.json && !args.markdown && !args.html) {
44
+ args.json = true;
45
+ }
46
+ return args;
47
+ }
48
+
49
+ async function run(argv = process.argv.slice(2), env = process.env) {
50
+ const args = parseArgs(argv);
51
+ if (args.help) {
52
+ process.stdout.write(helpText());
53
+ return { exitCode: 0 };
54
+ }
55
+
56
+ const projectRoot = path.resolve(env.I18NTK_PROJECT_ROOT || process.cwd());
57
+ const report = generateI18ntkReport({
58
+ projectRoot,
59
+ sourceDir: args.sourceDir || env.I18NTK_SOURCE_DIR || './src',
60
+ localesDir: args.i18nDir || env.I18NTK_I18N_DIR || './locales',
61
+ sourceLocale: args.sourceLocale || env.I18NTK_SOURCE_LOCALE || 'en',
62
+ });
63
+
64
+ const exports = {};
65
+ if (args.out) {
66
+ const outDir = path.resolve(projectRoot, args.out);
67
+ ensureWithin(projectRoot, outDir);
68
+ fs.mkdirSync(outDir, { recursive: true });
69
+ if (args.json) {
70
+ exports.json = path.join(outDir, 'i18ntk-report.json');
71
+ fs.writeFileSync(exports.json, `${JSON.stringify({ ...report, exports: undefined }, null, 2)}\n`, 'utf8');
72
+ }
73
+ if (args.markdown) {
74
+ exports.markdown = path.join(outDir, 'i18ntk-report.md');
75
+ fs.writeFileSync(exports.markdown, renderReportAsMarkdown(report), 'utf8');
76
+ }
77
+ if (args.html) {
78
+ exports.html = path.join(outDir, 'i18ntk-report.html');
79
+ fs.writeFileSync(exports.html, renderReportAsHtml(report), 'utf8');
80
+ }
81
+ }
82
+
83
+ const response = Object.keys(exports).length ? { ...report, exports } : report;
84
+ if (args.markdown && !args.json && !args.html && !args.out) {
85
+ process.stdout.write(renderReportAsMarkdown(response));
86
+ } else if (args.html && !args.json && !args.markdown && !args.out) {
87
+ process.stdout.write(renderReportAsHtml(response));
88
+ } else {
89
+ process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
90
+ }
91
+ return { exitCode: 0, report: response };
92
+ }
93
+
94
+ function ensureWithin(root, target) {
95
+ const rel = path.relative(root, target);
96
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
97
+ throw new Error('Report output directory must be inside the project root.');
98
+ }
99
+ }
100
+
101
+ function helpText() {
102
+ return `Usage: i18ntk report [options]
103
+
104
+ Options:
105
+ --json Print/write stable JSON report output
106
+ --markdown Print/write Markdown report output
107
+ --html Print/write HTML report output
108
+ --out <dir> Write selected report formats into a directory
109
+ --source-dir=<dir> Source code directory to scan
110
+ --i18n-dir=<dir> Locale directory
111
+ --source-language=<code> Source locale code (default: en)
112
+ `;
113
+ }
114
+
115
+ if (require.main === module) {
116
+ run().catch(error => {
117
+ process.stderr.write(`i18ntk report failed: ${error.message}\n`);
118
+ process.exit(1);
119
+ });
120
+ }
121
+
122
+ module.exports = {
123
+ parseArgs,
124
+ run,
125
+ };
@@ -22,8 +22,9 @@ const UsageCommand = require('./UsageCommand');
22
22
  const BackupCommand = require('./BackupCommand');
23
23
  const DoctorCommand = require('./DoctorCommand');
24
24
  const FixerCommand = require('./FixerCommand');
25
- const ScannerCommand = require('./ScannerCommand');
26
- const TranslateCommand = require('./TranslateCommand');
25
+ const ScannerCommand = require('./ScannerCommand');
26
+ const TranslateCommand = require('./TranslateCommand');
27
+ const ReportCommand = require('./ReportCommand');
27
28
 
28
29
  class CommandRouter {
29
30
  constructor(config = {}, ui = null, adminAuth = null) {
@@ -46,8 +47,9 @@ class CommandRouter {
46
47
  'backup': new BackupCommand(config, ui),
47
48
  'doctor': new DoctorCommand(config, ui),
48
49
  'fix': new FixerCommand(config, ui),
49
- 'scanner': new ScannerCommand(config, ui),
50
- 'translate': new TranslateCommand(config, ui)
50
+ 'scanner': new ScannerCommand(config, ui),
51
+ 'translate': new TranslateCommand(config, ui),
52
+ 'report': new ReportCommand(config, ui)
51
53
  };
52
54
  }
53
55
 
@@ -237,8 +239,11 @@ class CommandRouter {
237
239
  case 'scanner':
238
240
  return await this.commandHandlers.scanner.execute(options);
239
241
 
240
- case 'translate':
241
- return await this.commandHandlers.translate.execute(options);
242
+ case 'translate':
243
+ return await this.commandHandlers.translate.execute(options);
244
+
245
+ case 'report':
246
+ return await this.commandHandlers.report.execute(options);
242
247
 
243
248
  case 'debug':
244
249
  console.log('Debug functionality is not available in this version.');
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ class ReportCommand {
5
+ constructor(config = {}, ui = null) {
6
+ this.config = config;
7
+ this.ui = ui;
8
+ }
9
+
10
+ async execute() {
11
+ const reportCli = require('../../i18ntk-report');
12
+ await reportCli.run(process.argv.slice(2));
13
+ return { success: true, command: 'report' };
14
+ }
15
+
16
+ getMetadata() {
17
+ return {
18
+ name: 'report',
19
+ description: 'Generate a stable i18ntk JSON, Markdown, or HTML report',
20
+ category: 'analysis',
21
+ aliases: [],
22
+ usage: 'report [--json] [--markdown] [--html] [--out <dir>]',
23
+ examples: [
24
+ 'report --json',
25
+ 'report --markdown --out=./i18ntk-reports',
26
+ 'report --json --markdown --html --out=./i18ntk-reports'
27
+ ]
28
+ };
29
+ }
30
+ }
31
+
32
+ module.exports = ReportCommand;
@@ -304,18 +304,26 @@ class I18nManager {
304
304
  // Parse args early so we can short-circuit help/version flows before setup
305
305
  const args = this.parseArgs();
306
306
  const rawArgs = process.argv.slice(2);
307
- const directCommands = [
308
- 'init', 'analyze', 'validate', 'usage', 'scanner', 'sizing', 'complete', 'fix', 'summary', 'debug', 'workflow'
309
- ];
307
+ const directCommands = [
308
+ 'init', 'analyze', 'validate', 'usage', 'scanner', 'sizing', 'complete', 'fix', 'summary', 'debug', 'workflow', 'report'
309
+ ];
310
310
  const commandFlagArg = rawArgs.find(arg => arg.startsWith('--command='));
311
311
  const requestedCommand = commandFlagArg
312
312
  ? commandFlagArg.split('=')[1]
313
313
  : (rawArgs.length > 0 && directCommands.includes(rawArgs[0]) ? rawArgs[0] : null);
314
- const shouldSkipInitCheck = requestedCommand === 'init';
314
+ const shouldSkipInitCheck = requestedCommand === 'init' || requestedCommand === 'report';
315
315
 
316
316
  // Show help immediately without any setup/auth (useful for CI/uninitialized projects)
317
- if (args.help) {
318
- this.showHelp();
317
+ if (args.help) {
318
+ this.showHelp();
319
+ return;
320
+ }
321
+
322
+ if (requestedCommand === 'report') {
323
+ const ReportCommand = require('./commands/ReportCommand');
324
+ const command = new ReportCommand(this.config, this.ui);
325
+ await command.execute(args);
326
+ this.safeClose();
319
327
  return;
320
328
  }
321
329
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18ntk",
3
- "version": "4.4.2",
3
+ "version": "4.4.4",
4
4
  "description": "i18n Tool Kit - Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.",
5
5
  "readmeFilename": "README.md",
6
6
  "keywords": [
@@ -65,6 +65,7 @@
65
65
  "default": "./runtime/enhanced.js"
66
66
  },
67
67
  "./runtime/*": "./runtime/*",
68
+ "./report": "./utils/report-model.js",
68
69
  "./ui-locales/*": "./ui-locales/*",
69
70
  "./package.json": "./package.json"
70
71
  },
@@ -80,6 +81,7 @@
80
81
  "i18ntk-sizing": "main/i18ntk-sizing.js",
81
82
  "i18ntk-summary": "main/i18ntk-summary.js",
82
83
  "i18ntk-doctor": "main/i18ntk-doctor.js",
84
+ "i18ntk-report": "main/i18ntk-report.js",
83
85
  "i18ntk-fixer": "main/i18ntk-fixer.js",
84
86
  "i18ntk-scanner": "main/i18ntk-scanner.js",
85
87
  "i18ntk-backup": "main/i18ntk-backup.js",
@@ -96,6 +98,7 @@
96
98
  "main/i18ntk-init.js",
97
99
  "main/i18ntk-scanner.js",
98
100
  "main/i18ntk-setup.js",
101
+ "main/i18ntk-report.js",
99
102
  "main/i18ntk-sizing.js",
100
103
  "main/i18ntk-summary.js",
101
104
  "main/i18ntk-ui.js",
@@ -148,6 +151,7 @@
148
151
  "utils/prompt-helper.js",
149
152
  "utils/prompt.js",
150
153
  "utils/report-writer.js",
154
+ "utils/report-model.js",
151
155
  "utils/secure-errors.js",
152
156
  "utils/security.js",
153
157
  "utils/setup-enforcer.js",
@@ -175,14 +179,17 @@
175
179
  },
176
180
  "preferGlobal": true,
177
181
  "versionInfo": {
178
- "version": "4.4.2",
179
- "releaseDate": "02/06/2026",
180
- "lastUpdated": "02/06/2026",
182
+ "version": "4.4.4",
183
+ "releaseDate": "05/06/2026",
184
+ "lastUpdated": "05/06/2026",
181
185
  "maintainer": "Vlad Noskov",
182
186
  "changelog": "./CHANGELOG.md",
183
187
  "documentation": "./README.md",
184
188
  "apiReference": "./docs/api/API_REFERENCE.md",
185
189
  "majorChanges": [
190
+ "CONFIG: Shared .i18ntk-config now accepts extension-owned sections under extensions.workbench and extensions.lens; the CLI preserves the top-level extensions object while ignoring unknown nested extension settings.",
191
+ "REPORTS: Likely-untranslated detection now ignores placeholder-only and symbol/dynamic values such as {file}, {path}, and emoji labels instead of treating them as untranslated English.",
192
+ "REPORTS: Dynamic values with translated surrounding copy and English placeholder tokens are no longer flagged as untranslated.",
186
193
  "TRANSLATE: processFile() accepts project-relative source paths, matching direct CLI resolution.",
187
194
  "TRANSLATE: only-missing mode keeps existing translations that intentionally preserve configured product terms.",
188
195
  "TRANSLATE: broken target value detection now catches mojibake, replacement characters, repeated question marks, and target-language prefix leftovers.",
@@ -208,7 +215,7 @@
208
215
  "i18ntk/runtime module-level helpers keep the first initialized runtime configuration for compatibility instead of being overwritten by later initRuntime() calls.",
209
216
  "utils/watch-locales.js returns a callable watcher object with EventEmitter methods and stop(); existing bare stop-function usage remains supported."
210
217
  ],
211
- "nextVersion": "4.4.3",
218
+ "nextVersion": "4.4.5",
212
219
  "supportedNodeVersions": ">=16.0.0",
213
220
  "supportedFrameworks": {
214
221
  "react-i18next": ">=11.0.0",
@@ -230,18 +237,18 @@
230
237
  "spring-boot": ">=2.5.0",
231
238
  "laravel": ">=8.0.0"
232
239
  },
233
- "supportPolicy": "Versions earlier than 4.4.1 may be unstable or insecure. Upgrade to 4.4.2 or newer.",
240
+ "supportPolicy": "Versions earlier than 4.4.1 may be unstable or insecure. Upgrade to 4.4.4 or newer.",
234
241
  "deprecations": [
235
242
  "4.3.0",
236
243
  "4.3.1",
237
244
  "4.3.2",
238
245
  "4.3.3"
239
246
  ],
240
- "deprecationMessage": "i18ntk 4.3.x and earlier have known security vulnerabilities (path traversal, JSON DoS). Upgrade to i18ntk@4.4.2 or newer: npm install -g i18ntk@latest",
247
+ "deprecationMessage": "i18ntk 4.3.x and earlier have known security vulnerabilities (path traversal, JSON DoS). Upgrade to i18ntk@4.4.4 or newer: npm install -g i18ntk@latest",
241
248
  "securityAdvisories": [
242
249
  "GHSA-i18ntk-4.3.x-path-traversal: Backup command accepted arbitrary paths without validation (fixed in 4.4.1)",
243
250
  "GHSA-i18ntk-4.3.x-json-dos: Deeply nested JSON files could cause denial of service (fixed in 4.4.1)"
244
251
  ]
245
252
  },
246
- "readme": "# i18ntk v4.4.2\n\nA zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, translation completion, automatic JSON locale translation, reporting, and runtime translation loading.\n\n![i18ntk Logo](https://raw.githubusercontent.com/vladnoskv/i18ntk/main/docs/screenshots/i18ntk-logo-public.PNG)\n\n[![npm version](https://img.shields.io/npm/v/i18ntk.svg?color=brightgreen)](https://www.npmjs.com/package/i18ntk)\n[![npm downloads](https://img.shields.io/npm/dt/i18ntk.svg)](https://www.npmjs.com/package/i18ntk)\n[![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)\n[![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)\n[![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)\n[![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.4.2)](https://socket.dev/npm/package/i18ntk/overview/4.4.2)\n\n[![i18ntk Workbench](https://img.shields.io/badge/VS_Code-i18ntk_Workbench-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=VladNoskov.i18ntk-workbench)\n[![i18ntk Lens](https://img.shields.io/badge/VS_Code-i18ntk_Lens-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=VladNoskov.i18ntk-lens)\n\n## Install\n\n```bash\n# global CLI use\nnpm install -g i18ntk\n\n# local project use\nnpm install --save-dev i18ntk\n\n# one-off execution\nnpx i18ntk --help\n```\n\n## i18ntk Summary\n\n**What it does**\n\n- Manages locale files from the command line.\n- Finds missing, unused, risky, and inconsistent translation keys.\n- Produces validation and summary reports.\n- Supports framework-aware i18n workflows.\n- Provides a lightweight runtime translation toolkit.\n\n**What it does not do**\n\n- It is not a translation management SaaS.\n- It does not replace human translation review.\n- It does not force you to replace i18next, react-i18next, vue-i18n, or another runtime.\n\n**Why not i18next?**\n\ni18next is mainly a runtime internationalization library. i18ntk is mainly workflow tooling around translation files. They can work together: i18next handles runtime translation, while i18ntk handles setup, scanning, validation, reporting, and maintenance.\n\n| Need | i18ntk | i18next |\n| --- | --- | --- |\n| Runtime translation | Basic toolkit | Mature runtime |\n| Locale file scanning | Yes | No |\n| Missing key detection | Yes | No |\n| Unused key detection | Yes | No |\n| Validation reports | Yes | Limited |\n| Auto-translation workflow | Yes | External tooling |\n\n## What's New in 4.4.2\n\n- **AUTO TRANSLATE RELATIVE PATHS**: Programmatic `processFile()` calls now accept project-relative source paths, matching CLI source resolution.\n- **PROTECTED TERMS IN ONLY-MISSING MODE**: Existing translations that intentionally keep protected product terms such as `i18ntk` are no longer treated as English leftovers.\n- **BROKEN TARGET RETRIES**: Auto Translate now detects and retries more visibly broken target values, including mojibake, replacement characters, repeated question marks, and target-language prefix leftovers.\n- **REGRESSION COVERAGE**: Added focused tests for relative paths, protected product terms, broken values, placeholder handling, and residual checks.\n\n## What's New in 4.4.1\n\n- **DEAD-KEY DETECTION**: Dead-key confidence now uses resolved dynamic key data from usage insights instead of crude text-overlap heuristics. Keys expanded from template literals or const arrays are properly tracked.\n- **LOCALE JSON IMPORT DETECTION**: `import en from '../../locales/en/foo.json'` is now detected and property accesses are tracked as key usages, closing the gap between CLI and VSCode scanners.\n- **CONFIDENCE-SPLIT REPORTS**: Unused keys are now reported by confidence tier — confirmed (≥80%), likely (40-80%), possibly used (<40%) — instead of a flat list.\n- **NEW CLI FLAGS**: `--strict-unused` (only high-confidence keys), `--json` (structured JSON output for CI), `--prune` / `--prune-keep` (stale report cleanup).\n- **MOJIBAKE DETECTION**: Replacement-character artifacts like `Abwicklungspr?fung` and `L?ser` are detected during translation analysis.\n- **CLIENT-BOUNDARY WARNINGS**: `\"use client\"` files importing locale JSON are flagged — this bypasses the runtime and increases bundle size.\n- **COPY-FORMATTER DETECTION**: Local `const tx = ...` functions that don't call a translation runtime are identified as likely copy formatters, reducing false positive key noise.\n- **CONFIG FIX**: `--source-dir` and `--i18n-dir` are no longer forced equal when both are explicitly passed via CLI.\n- **Next.js DETECTION**: App Router files with `\"use server\"` / `\"use client\"` directives are now detected and reported by component type.\n- **VSCode DIAGNOSTICS**: New `i18ntk.clearDiagnostics` command. Stale diagnostics are now cleared at scan start. New diagnostic codes: `i18ntk.clientBoundary`, `i18ntk.copyFormatter`.\n- **AUTO TRANSLATE RESUME REPORTS**: If a provider still returns untranslated values after the final targeted retry, Auto Translate writes `i18ntk-reports/auto-translate/latest.json` so tooling can identify and retry only unresolved keys.\n- **VS CODE RESIDUAL PICKUP**: i18ntk Workbench and Lens read Auto Translate residual reports, show the affected locale JSON key in the editor, and can add intentionally unchanged keys to Auto Translate protection.\n- **WRAPPER CONFIG**: `.i18ntk-config` now supports `usage.translationFunctions`, `usage.serverWrappers`, and `usage.copyFormatters` for fine-grained control.\n- **TELEMETRY/EVENT STRING FILTERING**: String literals inside `trackEvent()`, `emitDomainEvent()`, `analytics.track()` and similar calls are classified as telemetry literals and no longer falsely counted as translation usage.\n- **OBJECT-METHOD KEY DETECTION**: input.tx(\"key\"), helper.tx(\"key\"), and .tx(\\`dynamic.${var}\\`) patterns are now recognized as translation calls alongside standalone `tx()` calls.\n\n- **LOCAL WRAPPER RESOLUTION**: Functions like `const text = (key, fallback) => tx(key)` that internally call the translation runtime are detected, and their string-literal invocations are automatically resolved to actual keys.\n\nSee [CHANGELOG.md](./CHANGELOG.md) for more release details.\n\n## Security hardening in 4.4.1\n\n- **PATH TRAVERSAL HARDENED**: Backup, complete, and config-helper commands now validate all user-supplied paths through `SecurityUtils.validatePath()`, blocking writes outside project boundaries.\n- **JSON DoS PREVENTED**: `safeParseJSON` enforces maximum depth (1000) and maximum size (50 MB) before parsing, preventing denial-of-service via deeply nested or oversized JSON.\n- **INPUT SANITIZATION TIGHTENED**: `sanitizeInput` default whitelist no longer allows backslashes or curly braces that could enable path traversal or template injection.\n\n- **LIBRETRANSLATE URL GATED**: Custom LibreTranslate host now requires `I18NTK_ALLOW_CUSTOM_LIBRETRANSLATE_HOST=1` env flag (parity with DeepL).\n\n## Quick Start\n\nInitialize a project:\n\n```bash\ni18ntk\n# or with explicit command\ni18ntk --command=init\n```\n\nRun common checks:\n\n```bash\ni18ntk --command=analyze\ni18ntk --command=validate\ni18ntk --command=usage\ni18ntk --command=sizing\ni18ntk --command=summary\n```\n\nComplete or fix translation files:\n\n```bash\ni18ntk --command=complete\ni18ntk-fixer --help\n```\n\nAuto-translate locale JSON:\n\n```bash\ni18ntk --command=translate\n# or\ni18ntk-translate locales/en/common.json de --report-stdout\n```\n\nThe full onboarding guide is in [docs/getting-started.md](./docs/getting-started.md).\n\n## Main Commands\n\nPrimary CLI:\n\n```bash\ni18ntk\ni18ntk --help\ni18ntk --command=init\ni18ntk --command=analyze\ni18ntk --command=validate\ni18ntk --command=usage\ni18ntk --command=scanner\ni18ntk --command=sizing\ni18ntk --command=complete\ni18ntk --command=translate\ni18ntk --command=summary\n```\n\nStandalone executables:\n\n```bash\ni18ntk-init\ni18ntk-analyze\ni18ntk-validate\ni18ntk-usage\ni18ntk-scanner\ni18ntk-sizing\ni18ntk-complete\ni18ntk-summary\ni18ntk-doctor\ni18ntk-fixer\ni18ntk-backup\ni18ntk-translate\n```\n\nNote: manager route `i18ntk --command=backup` is disabled in current builds. Use `i18ntk-backup` directly for backup operations.\n\n## Command Reference\n\n| Command | What it does | Looks for | Writes or changes |\n| --- | --- | --- | --- |\n| `i18ntk` | Opens the interactive management menu. | Project config, setup state, available commands. | Only changes files after you choose a command that writes. |\n| `i18ntk --command=init` / `i18ntk-init` | Sets up locale folders and missing target-language files. | Source language files and selected target languages. | Locale JSON files, `.i18ntk-config`, optional reports/backups. |\n| `i18ntk --command=analyze` / `i18ntk-analyze` | Compares source and target translation coverage. | Missing keys, extra keys, untranslated markers, completion by language. | Markdown/JSON/text reports when report output is enabled. |\n| `i18ntk --command=validate` / `i18ntk-validate` | Validates structure and translation quality risks. | Placeholder mismatches, missing keys, risky URLs/emails/secrets, likely English target text. | Validation summary report. Does not edit locale files. |\n| `i18ntk --command=usage` / `i18ntk-usage` | Maps translation keys to source files and finds unused/missing keys. | Direct i18n calls, literal known-key references, bounded dynamic templates/object maps, unresolved dynamic expressions, hardcoded text candidates, namespace/file naming mismatches. | Usage report with key locations, namespace recommendations, unresolved dynamic expressions, hardcoded text suggestions, and optional dead-key report. Does not delete unless cleanup deletion is explicitly enabled. |\n| `i18ntk --command=scanner` / `i18ntk-scanner` | Scans source for i18n issues and hardcoded user-facing text. | JSX/template text, common text attributes, i18n usage patterns, source-language text profiles. | Scanner report. Does not edit files. |\n| `i18ntk --command=complete` / `i18ntk-complete` | Adds missing keys to target language files for 100% key coverage. | Source-language keys missing from targets. | Target locale JSON files, using missing translation markers/prefixes. |\n| `i18ntk --command=translate` / `i18ntk-translate` | Auto-translates locale JSON using configured provider behavior. | Missing, empty, untranslated-marker, source-copy, likely-English, or visibly corrupt target values by default. | Target locale JSON files and translation reports. Existing translated values are kept unless `--translate-all` is used. If unresolved values remain after retry, writes `i18ntk-reports/auto-translate/latest.json` for targeted follow-up. |\n| `i18ntk --command=sizing` / `i18ntk-sizing` | Estimates translated string length expansion and layout risk. | Text length, expansion ratios, placeholder-bearing strings. | Sizing report. Does not edit locale files. |\n| `i18ntk --command=summary` / `i18ntk-summary` | Shows project translation status. | Configured locales, reports, completeness status. | Console/report output only. |\n| `i18ntk-fixer` | Fixes placeholder and missing-marker issues, and can audit English source files with `--check-placeholders`. | Placeholder corruption, missing translation markers, configured language files, `[LANG] ...` leftovers in English locales. | Locale JSON files when fixes are applied. Use dry-run options where available before bulk edits. |\n| `i18ntk-backup` | Creates, verifies, restores, and cleans locale backups. | Locale JSON files and backup manifests. | Backup archives/manifests, or restored locale files when using restore. |\n\n## Common Options\n\nMany commands support:\n\n- `--source-dir <path>`\n- `--i18n-dir <path>`\n- `--output-dir <path>`\n- `--source-language <code>`\n- `--ui-language <code>`\n- `--no-prompt`\n- `--help`\n\nCommand-specific tools add their own flags such as `--dry-run`, `--output-report`, `--cleanup`, `--predict-expansion`, or Auto Translate provider options.\n\nExample:\n\n```bash\ni18ntk --command=analyze --source-dir=./src --i18n-dir=./locales --output-dir=./i18ntk-reports\n```\n\n## Auto Translate\n\nInteractive manager flow:\n\n```bash\ni18ntk\n# choose \"Auto Translate\"\n```\n\nDirect CLI examples:\n\n```bash\ni18ntk-translate locales/en/common.json de\ni18ntk-translate locales/en/common.json fr --dry-run --report-stdout\ni18ntk-translate locales/en es --source-dir locales/en --files \"*.json\" --no-confirm --preserve-placeholders\n```\n\nProvider examples:\n\n```bash\nexport DEEPL_API_KEY=\"your-deepl-api-key\"\ni18ntk-translate locales/en/common.json de --provider deepl --no-confirm --preserve-placeholders\n\nexport LIBRETRANSLATE_URL=\"https://libretranslate.com/translate\"\nexport LIBRETRANSLATE_API_KEY=\"optional-api-key\"\ni18ntk-translate locales/en/common.json es --provider libretranslate --no-confirm --preserve-placeholders\n```\n\n`google` remains the default provider. You can also set `I18NTK_TRANSLATE_PROVIDER=deepl` or `I18NTK_TRANSLATE_PROVIDER=libretranslate`.\n\nProvider requests are HTTPS-only and response-size limited, and security logs redact provider query strings and response bodies. DeepL is pinned to official DeepL hosts by default; set `I18NTK_ALLOW_CUSTOM_TRANSLATE_HOSTS=1` only for a trusted DeepL-compatible proxy. Custom LibreTranslate URLs are blocked for localhost/private IP ranges unless `I18NTK_ALLOW_PRIVATE_TRANSLATE_URLS=1` is set for trusted local testing. Keep provider API keys in environment variables or a secret manager.\n\nThe manager flow asks for:\n\n- source locale directory, either the folder with JSON files or a locale root such as `./locales`\n- source language code\n- one or more target languages, or `all`\n- one JSON file or all JSON files in the source directory\n\nIf you select a locale root such as `./locales` and choose source language `en`, the manager automatically uses `./locales/en` when that folder contains the source JSON files.\n\nBefore writing files, the manager can run a dry-run preview. After confirmation it writes translated files under sibling target-language folders, for example:\n\n```text\nlocales/en/common.json\nlocales/de/common.json\nlocales/fr/common.json\n```\n\nAuto Translate is target-aware by default. When a target file already exists, it keeps translated target values and only sends values that are missing, empty, marked as untranslated, still identical to the source, likely still English, or visibly corrupt from encoding damage such as `?????`, replacement characters, or common mojibake. Use `--translate-all` when you intentionally want to re-translate every source string.\n\n### Placeholder Handling\n\nAuto Translate detects common placeholders such as:\n\n- `{name}`\n- `{{count}}`\n- `%s`\n- `%d`\n- `:id`\n- `%{name}`\n- `${value}`\n- `{count, plural, one {# item} other {# items}}`\n- `$t(common.save)`\n- `%(total).2f`\n\nUseful flags:\n\n- `--preserve-placeholders`: translate text around placeholders and reinsert original tokens\n- `--skip-placeholders`: copy placeholder-bearing strings unchanged\n- `--send-placeholders`: send placeholder-bearing strings through translation after masking\n- `--custom-regex <regex>`: add project-specific placeholder detection\n- `--only-missing`: keep existing translated target values and translate only missing/source-copy/likely English values (default)\n- `--translate-all`: re-translate every source string\n\nProgress output is stage-aware for large files. Normal keys are reported as `Translating strings`, while preserve-mode placeholder work is reported as `Translating placeholder-safe text segments`; each progress update includes the current key path when available.\n\n### Protected Terms and Keys\n\nAuto Translate can create and use a project-local protection file:\n\n```bash\ni18ntk-translate locales/en/common.json de --create-protection-file --protection-file ./i18ntk-auto-translate.json\n```\n\nExample `i18ntk-auto-translate.json`:\n\n```json\n{\n \"version\": 1,\n \"terms\": [\n \"BrandName\",\n \"PRODUCT_CODE\",\n { \"value\": \"OK\", \"context\": \"after:Click|Press|Tap\" },\n { \"value\": \"API\", \"context\": \"standalone\" }\n ],\n \"keys\": [\"app.brandName\", \"legal.companyName\", \"product.*.symbol\"],\n \"values\": [\"BrandName Ltd\", \"support@example.com\"],\n \"patterns\": [\"[A-Z]{2,}-\\\\d+\"]\n}\n```\n\n- `terms` are masked before translation and restored exactly afterward.\n - **Plain strings**: masked everywhere (backward compatible).\n - **Context objects**: masked only in specific contexts (`after:word`, `before:word`, `standalone`, `surrounded:left,right`).\n- `keys` are exact key paths or `*` wildcard paths copied unchanged.\n- `values` are exact source values copied unchanged.\n- `patterns` are JavaScript regex strings for advanced protected substrings.\n\nUseful flags:\n\n- `--protection-file <path>`\n- `--create-protection-file`\n- `--no-protection`\n\nOpen Settings and choose `Auto Translate` to edit defaults for placeholder mode, translate-only-needed mode, concurrency, batch size, retry settings, report output, BOM output, protection file path, first-run setup prompt, and update prompt.\n\nSee [docs/auto-translate.md](./docs/auto-translate.md) for the full Auto Translate guide.\n\n## Validation\n\nValidation checks locale structure, completeness, placeholders, and content risks.\n\nValidation warning types are specific:\n\n- `Potential risky content`: URL, email address, or secret-like value\n- `Possible untranslated English content`: target-language value appears to contain too much English\n\nEnglish-content warnings include:\n\n- detected English percentage\n- configured threshold\n- matched word count\n- sample matched words\n\nTune warnings in `.i18ntk-config`:\n\n```json\n{\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"]\n}\n```\n\n## Sizing Analysis\n\n`i18ntk-sizing` reports translation file sizes, key counts, average value length, and file-set mismatches across language folders.\n\n```bash\ni18ntk-sizing --source-dir ./locales --format table\ni18ntk-sizing --source-dir ./locales --detailed --output-dir ./i18ntk-reports\n```\n\nUse `--detailed` to print per-file rows in the terminal.\n\n### Expansion Prediction (New in 4.0.0)\n\nPredict UI layout overflow risk by analyzing per-key character-count expansion across languages:\n\n```bash\ni18ntk-sizing --source-dir ./locales --predict-expansion --output-report\n```\n\nExpansion ratios are classified into risk tiers:\n\n- **Safe** (<30% expansion): no UI impact expected\n- **Warning** (30–50%): may overflow in tight layouts — test on target languages\n- **Critical** (>50%): high risk of truncation — review UI element sizing\n\nThe report includes a built-in language-pair expansion reference table (EN→DE +35%, EN→RU +50%, EN→JA −40%, etc.) and lists the top-30 most-expanded keys.\n\n## Scanner: Multi-Language Detection (New in 4.0.0)\n\n`i18ntk-scanner` now supports detecting hardcoded text in multiple source languages beyond English:\n\n```bash\ni18ntk-scanner --source-dir ./src --source-language de\ni18ntk-scanner --source-dir ./src --source-language ja --output-report\n```\n\nSupported language profiles (12+): English, German, French, Spanish, Japanese, Chinese, Russian, Korean, Arabic, Hindi, and more. Each profile includes language-specific character ranges, stopword lists for false-positive filtering, and transliteration rules for key generation.\n\n## Usage: Dead Key Detection (New in 4.0.0)\n\n`i18ntk-usage` can identify translation keys that are defined but never referenced in source code:\n\n```bash\ni18ntk-usage --source-dir ./src --i18n-dir ./locales --cleanup\ni18ntk-usage --source-dir ./src --i18n-dir ./locales --cleanup --dry-run-delete\n```\n\nEach dead key receives a confidence score (0.0–1.0) factoring:\n- Unresolved dynamic key patterns (e.g., `` t(`prefix.${dynamic}`) ``) — lower score and listed in the usage report; simple consts, bounded arrays, object maps, and ternaries are expanded to exact keys where possible\n- Key appears in source code comments or JSDoc — medium score\n- Parent file recently modified (<30 days) — medium score\n- No references found anywhere — high score (>0.8)\n\nThe `--dry-run-delete` flag writes a `.dead-keys.json` report for review before any destructive action.\n\n## Validator: Key Naming Conventions (New in 4.0.0)\n\nEnforce consistent translation key naming across your project:\n\n```bash\ni18ntk-validate --enforce-key-style\n```\n\nConfigure the expected style in `.i18ntk-config`:\n\n```json\n{\n \"keyStyle\": \"dot.notation\"\n}\n```\n\nSupported styles: `dot.notation`, `snake_case`, `camelCase`, `kebab-case`, `flat`. Violations are reported as warnings with suggested canonical forms.\n\n## Watch: Hot Reload (New in 4.0.0)\n\n`utils/watch-locales.js` now provides debounced file watching with EventEmitter support:\n\n```js\nconst watchLocales = require('i18ntk/utils/watch-locales');\nconst watcher = watchLocales('./locales');\n\nwatcher.on('change', (filePath) => {\n console.log('Locale changed:', filePath);\n});\n\nwatcher.on('add', (filePath) => {\n console.log('Locale added:', filePath);\n});\n\n// Later:\nwatcher.stop();\n```\n\nFeatures: 300ms debounce (configurable), SHA-256 hash tracking to skip no-change saves, and a maximum of 50 watched directories.\n\n### Migration\n\nThe `watchLocales` return value gained EventEmitter methods in v4.0.0. Existing stop-function usage still works:\n\n```js\nconst stop = watchLocales('./locales', onChange);\n```\n\nCan be updated to:\n\n```js\nconst watcher = watchLocales('./locales');\nwatcher.on('change', onChange);\nwatcher.stop();\n```\n\nPassing a callback as the second argument is still supported — it auto-subscribes to `change` and `add` events.\n\n## Backup: Incremental Mode (New in 4.0.0)\n\nCreate differential backups that only include changed files:\n\n```bash\ni18ntk-backup create ./locales --incremental\n```\n\nIncremental backups store SHA-256 hashes per file and a parent-chain reference. Restoring an incremental backup automatically chains from the oldest full backup through each incremental diff in order. Chain depth is capped at 10 increments. Use `verify` to validate the hash chain.\n\n## Runtime: Lazy Loading (New in 4.0.0)\n\nReduce memory usage by deferring locale file loads until first key access:\n\n```js\nconst runtime = require('i18ntk/runtime');\n\nconst i18n = runtime.initRuntime({\n baseDir: './locales',\n language: 'en',\n lazy: true\n});\n\nconsole.log(i18n.t('common.hello')); // loads common.json on first access\n```\n\nWhen `lazy: true`, the runtime builds a key-to-file manifest on first access and loads individual files on demand. Files are loaded once and cached. If the manifest is missing or incomplete, the runtime falls back to full eager loading for that language. Manifest size is capped at 100KB with path containment validation.\n\nProduction guidance:\n\n- Prefer the object returned from `initRuntime()` instead of module-level `runtime.t()` in apps with multiple tenants, projects, or locale roots.\n- Use `lazy: true` for large modular locale folders where lower steady-state memory matters more than a small first-key lookup cost.\n- Use `preload: true` without `lazy` for small locale sets or latency-sensitive startup paths.\n- Call `refresh(language)` after deploying or writing changed locale files so cached data and lazy manifests are rebuilt.\n- Use per-call language overrides when rendering one-off alternate-language strings: `i18n.t('common.hello', {}, { language: 'de' })`.\n- Use `translateBatch()` for small groups of labels and `clearCache()` / `getCacheInfo()` for cache maintenance and diagnostics.\n- `i18ntk/runtime/enhanced` remains available for compatibility with existing async/encryption users, but new production integrations should start with `i18ntk/runtime`.\n\n## Runtime API\n\nUse `i18ntk/runtime` when an application needs to read locale JSON files at runtime.\n\n```js\nconst runtime = require('i18ntk/runtime');\n\nconst i18n = runtime.initRuntime({\n baseDir: './locales',\n language: 'en',\n fallbackLanguage: 'en',\n keySeparator: '.',\n preload: true\n});\n\nconsole.log(i18n.t('common.hello'));\ni18n.setLanguage('fr');\nconsole.log(i18n.getLanguage());\nconsole.log(i18n.getAvailableLanguages());\ni18n.refresh('fr');\n```\n\nUseful production helpers:\n\n```js\ni18n.t('common.hello', {}, { language: 'de' }); // per-call language override\ni18n.translateBatch(['menu.home', 'menu.settings']);\ni18n.clearCache('fr');\nconsole.log(i18n.getCacheInfo());\n```\n\nSee [docs/runtime.md](./docs/runtime.md) for runtime details.\n\n## Configuration\n\ni18ntk uses a project-local `.i18ntk-config` file.\n\nExample:\n\n```json\n{\n \"version\": \"4.3.3\",\n \"sourceDir\": \"./locales\",\n \"i18nDir\": \"./locales\",\n \"outputDir\": \"./i18ntk-reports\",\n \"sourceLanguage\": \"en\",\n \"defaultLanguages\": [\"en\", \"de\", \"es\", \"fr\", \"ru\"],\n \"reports\": {\n \"format\": \"markdown\"\n },\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"],\n \"autoTranslate\": {\n \"placeholderMode\": \"preserve\",\n \"concurrency\": 12,\n \"batchSize\": 100,\n \"progressInterval\": 25,\n \"retryCount\": 3,\n \"retryDelay\": 1000,\n \"timeout\": 15000,\n \"dryRunFirst\": true,\n \"onlyMissingOrEnglish\": true,\n \"reportStdout\": true,\n \"bom\": false,\n \"protectionEnabled\": true,\n \"protectionFile\": \"./i18ntk-auto-translate.json\",\n \"promptProtectionSetup\": true,\n \"promptProtectionUpdate\": true\n },\n \"setup\": {\n \"completed\": true\n }\n}\n```\n\nSee [docs/api/CONFIGURATION.md](./docs/api/CONFIGURATION.md) for the full configuration model.\n\n## Public Package Contents\n\nThe public package intentionally ships runtime and CLI files only.\n\nThe package includes:\n\n- CLI entry points under `main/`\n- manager commands and services\n- runtime API files under `runtime/`\n- settings UI files required at runtime\n- bundled internal UI locales\n- shared utilities required by the shipped commands\n- `README.md`, `CHANGELOG.md`, `LICENSE`, and policy files\n\nThe public package manifest includes `readmeFilename: \"README.md\"`, and the release staging script fails if `README.md` is missing or empty.\n\n## Documentation\n\n- [Documentation Index](./docs/README.md)\n- [Getting Started](./docs/getting-started.md)\n- [API Reference](./docs/api/API_REFERENCE.md)\n- [Configuration Guide](./docs/api/CONFIGURATION.md)\n- [Runtime API Guide](./docs/runtime.md)\n- [Auto Translate Guide](./docs/auto-translate.md)\n- [Scanner Guide](./docs/scanner-guide.md)\n- [Environment Variables](./docs/environment-variables.md)\n- [Migration Guide v4.3.3](./docs/migration-guide-v4.3.3.md)\n\n## Security\n\n- No API key is required for the default Auto Translate flow.\n- Do not store secrets in locale files, `.i18ntk-config`, or protection files.\n- Project-specific brand/product terms should be configured by the user, not hardcoded into the package.\n- Report security issues using [SECURITY.md](./SECURITY.md).\n\n## Community\n\n- [Contributing](./CONTRIBUTING.md)\n- [Code of Conduct](./CODE_OF_CONDUCT.md)\n- [Funding](./FUNDING.md)\n\n## Related Tools\n\n| Tool | Purpose |\n|---|---|\n| **i18ntk** | Zero-dependency i18n toolkit for scanning, validation, translation, reports, and runtime loading. |\n| **i18ntk Workbench** | Full VS Code localization health dashboard powered by i18ntk. |\n| **i18ntk Lens** | Lightweight inline translation hovers, diagnostics, and key navigation. |\n| **PublishGuard** | Pre-publish safety scanner for npm packages and VS Code extensions. |\n| **ContextKit** | AI coding context manager for AGENTS.md, Claude, Cursor, Copilot, Roo, and Codex files. |\n\n## License\n\nMIT. See [LICENSE](./LICENSE).\n"
253
+ "readme": "# i18ntk v4.4.2\n\nA zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, translation completion, automatic JSON locale translation, reporting, and runtime translation loading.\n\n![i18ntk Logo](https://raw.githubusercontent.com/vladnoskv/i18ntk/main/docs/screenshots/i18ntk-logo-public.PNG)\n\n[![npm version](https://img.shields.io/npm/v/i18ntk.svg?color=brightgreen)](https://www.npmjs.com/package/i18ntk)\n[![npm downloads](https://img.shields.io/npm/dt/i18ntk.svg)](https://www.npmjs.com/package/i18ntk)\n[![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)\n[![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)\n[![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)\n[![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.4.2)](https://socket.dev/npm/package/i18ntk/overview/4.4.2)\n\n[![i18ntk Workbench](https://img.shields.io/badge/VS_Code-i18ntk_Workbench-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=VladNoskov.i18ntk-workbench)\n[![i18ntk Lens](https://img.shields.io/badge/VS_Code-i18ntk_Lens-007ACC?logo=visualstudiocode&logoColor=white)](https://marketplace.visualstudio.com/items?itemName=VladNoskov.i18ntk-lens)\n\n## The i18ntk ecosystem\n\n- i18ntk — CLI and runtime toolkit\n- i18ntk Workbench — full VS Code dashboard and reports\n- i18ntk Lens — inline hovers, CodeLens, and diagnostics\n\nUse the CLI in CI, Workbench for project-level management, and Lens for day-to-day editor feedback.\n\n## Install\n\n```bash\n# global CLI use\nnpm install -g i18ntk\n\n# local project use\nnpm install --save-dev i18ntk\n\n# one-off execution\nnpx i18ntk --help\n```\n\n## i18ntk Summary\n\n**What it does**\n\n- Manages locale files from the command line.\n- Finds missing, unused, risky, and inconsistent translation keys.\n- Produces validation and summary reports.\n- Supports framework-aware i18n workflows.\n- Provides a lightweight runtime translation toolkit.\n\n**What it does not do**\n\n- It is not a translation management SaaS.\n- It does not replace human translation review.\n- It does not force you to replace i18next, react-i18next, vue-i18n, or another runtime.\n\n**Why not i18next?**\n\ni18next is mainly a runtime internationalization library. i18ntk is mainly workflow tooling around translation files. They can work together: i18next handles runtime translation, while i18ntk handles setup, scanning, validation, reporting, and maintenance.\n\n| Need | i18ntk | i18next |\n| --- | --- | --- |\n| Runtime translation | Basic toolkit | Mature runtime |\n| Locale file scanning | Yes | No |\n| Missing key detection | Yes | No |\n| Unused key detection | Yes | No |\n| Validation reports | Yes | Limited |\n| Auto-translation workflow | Yes | External tooling |\n\n## What's New in 4.4.4\n\n- **SHARED CONFIG FOR EXTENSIONS**: `.i18ntk-config` can now carry editor-owned settings under `extensions.workbench` and `extensions.lens`. The CLI preserves the top-level `extensions` object and ignores unknown nested extension keys.\n- **DYNAMIC VALUE REPORTING**: Likely-untranslated reports now ignore placeholder-only and symbol/dynamic values such as `{file}`, `{path}`, and icon-prefixed labels.\n- **PLACEHOLDER-AWARE TRANSLATION CHECKS**: Values with translated surrounding copy and English placeholder tokens, such as `\"command\": \"指示: {command}\"`, are no longer reported as untranslated.\n\n## What's New in 4.4.2\n\n- **AUTO TRANSLATE RELATIVE PATHS**: Programmatic `processFile()` calls now accept project-relative source paths, matching CLI source resolution.\n- **PROTECTED TERMS IN ONLY-MISSING MODE**: Existing translations that intentionally keep protected product terms such as `i18ntk` are no longer treated as English leftovers.\n- **BROKEN TARGET RETRIES**: Auto Translate now detects and retries more visibly broken target values, including mojibake, replacement characters, repeated question marks, and target-language prefix leftovers.\n- **REGRESSION COVERAGE**: Added focused tests for relative paths, protected product terms, broken values, placeholder handling, and residual checks.\n\n## What's New in 4.4.1\n\n- **DEAD-KEY DETECTION**: Dead-key confidence now uses resolved dynamic key data from usage insights instead of crude text-overlap heuristics. Keys expanded from template literals or const arrays are properly tracked.\n- **LOCALE JSON IMPORT DETECTION**: `import en from '../../locales/en/foo.json'` is now detected and property accesses are tracked as key usages, closing the gap between CLI and VSCode scanners.\n- **CONFIDENCE-SPLIT REPORTS**: Unused keys are now reported by confidence tier — confirmed (≥80%), likely (40-80%), possibly used (<40%) — instead of a flat list.\n- **NEW CLI FLAGS**: `--strict-unused` (only high-confidence keys), `--json` (structured JSON output for CI), `--prune` / `--prune-keep` (stale report cleanup).\n- **MOJIBAKE DETECTION**: Replacement-character artifacts like `Abwicklungspr?fung` and `L?ser` are detected during translation analysis.\n- **CLIENT-BOUNDARY WARNINGS**: `\"use client\"` files importing locale JSON are flagged — this bypasses the runtime and increases bundle size.\n- **COPY-FORMATTER DETECTION**: Local `const tx = ...` functions that don't call a translation runtime are identified as likely copy formatters, reducing false positive key noise.\n- **CONFIG FIX**: `--source-dir` and `--i18n-dir` are no longer forced equal when both are explicitly passed via CLI.\n- **Next.js DETECTION**: App Router files with `\"use server\"` / `\"use client\"` directives are now detected and reported by component type.\n- **VSCode DIAGNOSTICS**: New `i18ntk.clearDiagnostics` command. Stale diagnostics are now cleared at scan start. New diagnostic codes: `i18ntk.clientBoundary`, `i18ntk.copyFormatter`.\n- **AUTO TRANSLATE RESUME REPORTS**: If a provider still returns untranslated values after the final targeted retry, Auto Translate writes `i18ntk-reports/auto-translate/latest.json` so tooling can identify and retry only unresolved keys.\n- **VS CODE RESIDUAL PICKUP**: i18ntk Workbench and Lens read Auto Translate residual reports, show the affected locale JSON key in the editor, and can add intentionally unchanged keys to Auto Translate protection.\n- **WRAPPER CONFIG**: `.i18ntk-config` now supports `usage.translationFunctions`, `usage.serverWrappers`, and `usage.copyFormatters` for fine-grained control.\n- **TELEMETRY/EVENT STRING FILTERING**: String literals inside `trackEvent()`, `emitDomainEvent()`, `analytics.track()` and similar calls are classified as telemetry literals and no longer falsely counted as translation usage.\n- **OBJECT-METHOD KEY DETECTION**: input.tx(\"key\"), helper.tx(\"key\"), and .tx(\\`dynamic.${var}\\`) patterns are now recognized as translation calls alongside standalone `tx()` calls.\n\n- **LOCAL WRAPPER RESOLUTION**: Functions like `const text = (key, fallback) => tx(key)` that internally call the translation runtime are detected, and their string-literal invocations are automatically resolved to actual keys.\n\nSee [CHANGELOG.md](./CHANGELOG.md) for more release details.\n\n## Security hardening in 4.4.1\n\n- **PATH TRAVERSAL HARDENED**: Backup, complete, and config-helper commands now validate all user-supplied paths through `SecurityUtils.validatePath()`, blocking writes outside project boundaries.\n- **JSON DoS PREVENTED**: `safeParseJSON` enforces maximum depth (1000) and maximum size (50 MB) before parsing, preventing denial-of-service via deeply nested or oversized JSON.\n- **INPUT SANITIZATION TIGHTENED**: `sanitizeInput` default whitelist no longer allows backslashes or curly braces that could enable path traversal or template injection.\n\n- **LIBRETRANSLATE URL GATED**: Custom LibreTranslate host now requires `I18NTK_ALLOW_CUSTOM_LIBRETRANSLATE_HOST=1` env flag (parity with DeepL).\n\n## Quick Start\n\nInitialize a project:\n\n```bash\ni18ntk\n# or with explicit command\ni18ntk --command=init\n```\n\nRun common checks:\n\n```bash\ni18ntk --command=analyze\ni18ntk --command=validate\ni18ntk --command=usage\ni18ntk report --json\ni18ntk --command=sizing\ni18ntk --command=summary\n```\n\nComplete or fix translation files:\n\n```bash\ni18ntk --command=complete\ni18ntk-fixer --help\n```\n\nAuto-translate locale JSON:\n\n```bash\ni18ntk --command=translate\n# or\ni18ntk-translate locales/en/common.json de --report-stdout\n```\n\nThe full onboarding guide is in [docs/getting-started.md](./docs/getting-started.md).\n\n## Main Commands\n\nPrimary CLI:\n\n```bash\ni18ntk\ni18ntk --help\ni18ntk --command=init\ni18ntk --command=analyze\ni18ntk --command=validate\ni18ntk --command=usage\ni18ntk report --json --markdown --html --out ./i18ntk-reports\ni18ntk --command=scanner\ni18ntk --command=sizing\ni18ntk --command=complete\ni18ntk --command=translate\ni18ntk --command=summary\n```\n\nStandalone executables:\n\n```bash\ni18ntk-init\ni18ntk-analyze\ni18ntk-validate\ni18ntk-usage\ni18ntk-report\ni18ntk-scanner\ni18ntk-sizing\ni18ntk-complete\ni18ntk-summary\ni18ntk-doctor\ni18ntk-fixer\ni18ntk-backup\ni18ntk-translate\n```\n\nNote: manager route `i18ntk --command=backup` is disabled in current builds. Use `i18ntk-backup` directly for backup operations.\n\n## Command Reference\n\n| Command | What it does | Looks for | Writes or changes |\n| --- | --- | --- | --- |\n| `i18ntk` | Opens the interactive management menu. | Project config, setup state, available commands. | Only changes files after you choose a command that writes. |\n| `i18ntk --command=init` / `i18ntk-init` | Sets up locale folders and missing target-language files. | Source language files and selected target languages. | Locale JSON files, `.i18ntk-config`, optional reports/backups. |\n| `i18ntk --command=analyze` / `i18ntk-analyze` | Compares source and target translation coverage. | Missing keys, extra keys, untranslated markers, completion by language. | Markdown/JSON/text reports when report output is enabled. |\n| `i18ntk --command=validate` / `i18ntk-validate` | Validates structure and translation quality risks. | Placeholder mismatches, missing keys, risky URLs/emails/secrets, likely English target text. | Validation summary report. Does not edit locale files. |\n| `i18ntk --command=usage` / `i18ntk-usage` | Maps translation keys to source files and finds unused/missing keys. | Direct i18n calls, literal known-key references, bounded dynamic templates/object maps, unresolved dynamic expressions, hardcoded text candidates, namespace/file naming mismatches. | Usage report with key locations, namespace recommendations, unresolved dynamic expressions, hardcoded text suggestions, and optional dead-key report. Does not delete unless cleanup deletion is explicitly enabled. |\n| `i18ntk report` / `i18ntk-report` | Generates the stable schemaVersion 1 report used by CLI automation and i18ntk Workbench. | Locale completeness, missing keys, unused keys with confidence, placeholders, likely untranslated values, expansion risk, and hardcoded text candidates. | JSON to stdout by default, plus JSON/Markdown/HTML files when `--out` is used. Does not edit locale files. |\n| `i18ntk --command=scanner` / `i18ntk-scanner` | Scans source for i18n issues and hardcoded user-facing text. | JSX/template text, common text attributes, i18n usage patterns, source-language text profiles. | Scanner report. Does not edit files. |\n| `i18ntk --command=complete` / `i18ntk-complete` | Adds missing keys to target language files for 100% key coverage. | Source-language keys missing from targets. | Target locale JSON files, using missing translation markers/prefixes. |\n| `i18ntk --command=translate` / `i18ntk-translate` | Auto-translates locale JSON using configured provider behavior. | Missing, empty, untranslated-marker, source-copy, likely-English, or visibly corrupt target values by default. | Target locale JSON files and translation reports. Existing translated values are kept unless `--translate-all` is used. If unresolved values remain after retry, writes `i18ntk-reports/auto-translate/latest.json` for targeted follow-up. |\n| `i18ntk --command=sizing` / `i18ntk-sizing` | Estimates translated string length expansion and layout risk. | Text length, expansion ratios, placeholder-bearing strings. | Sizing report. Does not edit locale files. |\n| `i18ntk --command=summary` / `i18ntk-summary` | Shows project translation status. | Configured locales, reports, completeness status. | Console/report output only. |\n| `i18ntk-fixer` | Fixes placeholder and missing-marker issues, and can audit English source files with `--check-placeholders`. | Placeholder corruption, missing translation markers, configured language files, `[LANG] ...` leftovers in English locales. | Locale JSON files when fixes are applied. Use dry-run options where available before bulk edits. |\n| `i18ntk-backup` | Creates, verifies, restores, and cleans locale backups. | Locale JSON files and backup manifests. | Backup archives/manifests, or restored locale files when using restore. |\n\n## Common Options\n\nMany commands support:\n\n- `--source-dir <path>`\n- `--i18n-dir <path>`\n- `--output-dir <path>`\n- `--source-language <code>`\n- `--ui-language <code>`\n- `--no-prompt`\n- `--help`\n\nCommand-specific tools add their own flags such as `--dry-run`, `--output-report`, `--cleanup`, `--predict-expansion`, or Auto Translate provider options.\n\nExample:\n\n```bash\ni18ntk --command=analyze --source-dir=./src --i18n-dir=./locales --output-dir=./i18ntk-reports\n```\n\n## Auto Translate\n\nInteractive manager flow:\n\n```bash\ni18ntk\n# choose \"Auto Translate\"\n```\n\nDirect CLI examples:\n\n```bash\ni18ntk-translate locales/en/common.json de\ni18ntk-translate locales/en/common.json fr --dry-run --report-stdout\ni18ntk-translate locales/en es --source-dir locales/en --files \"*.json\" --no-confirm --preserve-placeholders\n```\n\nProvider examples:\n\n```bash\nexport DEEPL_API_KEY=\"your-deepl-api-key\"\ni18ntk-translate locales/en/common.json de --provider deepl --no-confirm --preserve-placeholders\n\nexport LIBRETRANSLATE_URL=\"https://libretranslate.com/translate\"\nexport LIBRETRANSLATE_API_KEY=\"optional-api-key\"\ni18ntk-translate locales/en/common.json es --provider libretranslate --no-confirm --preserve-placeholders\n```\n\n`google` remains the default provider. You can also set `I18NTK_TRANSLATE_PROVIDER=deepl` or `I18NTK_TRANSLATE_PROVIDER=libretranslate`.\n\nProvider requests are HTTPS-only and response-size limited, and security logs redact provider query strings and response bodies. DeepL is pinned to official DeepL hosts by default; set `I18NTK_ALLOW_CUSTOM_TRANSLATE_HOSTS=1` only for a trusted DeepL-compatible proxy. Custom LibreTranslate URLs are blocked for localhost/private IP ranges unless `I18NTK_ALLOW_PRIVATE_TRANSLATE_URLS=1` is set for trusted local testing. Keep provider API keys in environment variables or a secret manager.\n\nThe manager flow asks for:\n\n- source locale directory, either the folder with JSON files or a locale root such as `./locales`\n- source language code\n- one or more target languages, or `all`\n- one JSON file or all JSON files in the source directory\n\nIf you select a locale root such as `./locales` and choose source language `en`, the manager automatically uses `./locales/en` when that folder contains the source JSON files.\n\nBefore writing files, the manager can run a dry-run preview. After confirmation it writes translated files under sibling target-language folders, for example:\n\n```text\nlocales/en/common.json\nlocales/de/common.json\nlocales/fr/common.json\n```\n\nAuto Translate is target-aware by default. When a target file already exists, it keeps translated target values and only sends values that are missing, empty, marked as untranslated, still identical to the source, likely still English, or visibly corrupt from encoding damage such as `?????`, replacement characters, or common mojibake. Use `--translate-all` when you intentionally want to re-translate every source string.\n\n### Placeholder Handling\n\nAuto Translate detects common placeholders such as:\n\n- `{name}`\n- `{{count}}`\n- `%s`\n- `%d`\n- `:id`\n- `%{name}`\n- `${value}`\n- `{count, plural, one {# item} other {# items}}`\n- `$t(common.save)`\n- `%(total).2f`\n\nUseful flags:\n\n- `--preserve-placeholders`: translate text around placeholders and reinsert original tokens\n- `--skip-placeholders`: copy placeholder-bearing strings unchanged\n- `--send-placeholders`: send placeholder-bearing strings through translation after masking\n- `--custom-regex <regex>`: add project-specific placeholder detection\n- `--only-missing`: keep existing translated target values and translate only missing/source-copy/likely English values (default)\n- `--translate-all`: re-translate every source string\n\nProgress output is stage-aware for large files. Normal keys are reported as `Translating strings`, while preserve-mode placeholder work is reported as `Translating placeholder-safe text segments`; each progress update includes the current key path when available.\n\n### Protected Terms and Keys\n\nAuto Translate can create and use a project-local protection file:\n\n```bash\ni18ntk-translate locales/en/common.json de --create-protection-file --protection-file ./i18ntk-auto-translate.json\n```\n\nExample `i18ntk-auto-translate.json`:\n\n```json\n{\n \"version\": 1,\n \"terms\": [\n \"BrandName\",\n \"PRODUCT_CODE\",\n { \"value\": \"OK\", \"context\": \"after:Click|Press|Tap\" },\n { \"value\": \"API\", \"context\": \"standalone\" }\n ],\n \"keys\": [\"app.brandName\", \"legal.companyName\", \"product.*.symbol\"],\n \"values\": [\"BrandName Ltd\", \"support@example.com\"],\n \"patterns\": [\"[A-Z]{2,}-\\\\d+\"]\n}\n```\n\n- `terms` are masked before translation and restored exactly afterward.\n - **Plain strings**: masked everywhere (backward compatible).\n - **Context objects**: masked only in specific contexts (`after:word`, `before:word`, `standalone`, `surrounded:left,right`).\n- `keys` are exact key paths or `*` wildcard paths copied unchanged.\n- `values` are exact source values copied unchanged.\n- `patterns` are JavaScript regex strings for advanced protected substrings.\n\nUseful flags:\n\n- `--protection-file <path>`\n- `--create-protection-file`\n- `--no-protection`\n\nOpen Settings and choose `Auto Translate` to edit defaults for placeholder mode, translate-only-needed mode, concurrency, batch size, retry settings, report output, BOM output, protection file path, first-run setup prompt, and update prompt.\n\nSee [docs/auto-translate.md](./docs/auto-translate.md) for the full Auto Translate guide.\n\n## Validation\n\nValidation checks locale structure, completeness, placeholders, and content risks.\n\nValidation warning types are specific:\n\n- `Potential risky content`: URL, email address, or secret-like value\n- `Possible untranslated English content`: target-language value appears to contain too much English\n\nEnglish-content warnings include:\n\n- detected English percentage\n- configured threshold\n- matched word count\n- sample matched words\n\nTune warnings in `.i18ntk-config`:\n\n```json\n{\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"]\n}\n```\n\n## Sizing Analysis\n\n`i18ntk-sizing` reports translation file sizes, key counts, average value length, and file-set mismatches across language folders.\n\n```bash\ni18ntk-sizing --source-dir ./locales --format table\ni18ntk-sizing --source-dir ./locales --detailed --output-dir ./i18ntk-reports\n```\n\nUse `--detailed` to print per-file rows in the terminal.\n\n### Expansion Prediction (New in 4.0.0)\n\nPredict UI layout overflow risk by analyzing per-key character-count expansion across languages:\n\n```bash\ni18ntk-sizing --source-dir ./locales --predict-expansion --output-report\n```\n\nExpansion ratios are classified into risk tiers:\n\n- **Safe** (<30% expansion): no UI impact expected\n- **Warning** (30–50%): may overflow in tight layouts — test on target languages\n- **Critical** (>50%): high risk of truncation — review UI element sizing\n\nThe report includes a built-in language-pair expansion reference table (EN→DE +35%, EN→RU +50%, EN→JA −40%, etc.) and lists the top-30 most-expanded keys.\n\n## Scanner: Multi-Language Detection (New in 4.0.0)\n\n`i18ntk-scanner` now supports detecting hardcoded text in multiple source languages beyond English:\n\n```bash\ni18ntk-scanner --source-dir ./src --source-language de\ni18ntk-scanner --source-dir ./src --source-language ja --output-report\n```\n\nSupported language profiles (12+): English, German, French, Spanish, Japanese, Chinese, Russian, Korean, Arabic, Hindi, and more. Each profile includes language-specific character ranges, stopword lists for false-positive filtering, and transliteration rules for key generation.\n\n## Usage: Dead Key Detection (New in 4.0.0)\n\n`i18ntk-usage` can identify translation keys that are defined but never referenced in source code:\n\n```bash\ni18ntk-usage --source-dir ./src --i18n-dir ./locales --cleanup\ni18ntk-usage --source-dir ./src --i18n-dir ./locales --cleanup --dry-run-delete\n```\n\nEach dead key receives a confidence score (0.0–1.0) factoring:\n- Unresolved dynamic key patterns (e.g., `` t(`prefix.${dynamic}`) ``) — lower score and listed in the usage report; simple consts, bounded arrays, object maps, and ternaries are expanded to exact keys where possible\n- Key appears in source code comments or JSDoc — medium score\n- Parent file recently modified (<30 days) — medium score\n- No references found anywhere — high score (>0.8)\n\nThe `--dry-run-delete` flag writes a `.dead-keys.json` report for review before any destructive action.\n\n## Validator: Key Naming Conventions (New in 4.0.0)\n\nEnforce consistent translation key naming across your project:\n\n```bash\ni18ntk-validate --enforce-key-style\n```\n\nConfigure the expected style in `.i18ntk-config`:\n\n```json\n{\n \"keyStyle\": \"dot.notation\"\n}\n```\n\nSupported styles: `dot.notation`, `snake_case`, `camelCase`, `kebab-case`, `flat`. Violations are reported as warnings with suggested canonical forms.\n\n## Watch: Hot Reload (New in 4.0.0)\n\n`utils/watch-locales.js` now provides debounced file watching with EventEmitter support:\n\n```js\nconst watchLocales = require('i18ntk/utils/watch-locales');\nconst watcher = watchLocales('./locales');\n\nwatcher.on('change', (filePath) => {\n console.log('Locale changed:', filePath);\n});\n\nwatcher.on('add', (filePath) => {\n console.log('Locale added:', filePath);\n});\n\n// Later:\nwatcher.stop();\n```\n\nFeatures: 300ms debounce (configurable), SHA-256 hash tracking to skip no-change saves, and a maximum of 50 watched directories.\n\n### Migration\n\nThe `watchLocales` return value gained EventEmitter methods in v4.0.0. Existing stop-function usage still works:\n\n```js\nconst stop = watchLocales('./locales', onChange);\n```\n\nCan be updated to:\n\n```js\nconst watcher = watchLocales('./locales');\nwatcher.on('change', onChange);\nwatcher.stop();\n```\n\nPassing a callback as the second argument is still supported — it auto-subscribes to `change` and `add` events.\n\n## Backup: Incremental Mode (New in 4.0.0)\n\nCreate differential backups that only include changed files:\n\n```bash\ni18ntk-backup create ./locales --incremental\n```\n\nIncremental backups store SHA-256 hashes per file and a parent-chain reference. Restoring an incremental backup automatically chains from the oldest full backup through each incremental diff in order. Chain depth is capped at 10 increments. Use `verify` to validate the hash chain.\n\n## Runtime: Lazy Loading (New in 4.0.0)\n\nReduce memory usage by deferring locale file loads until first key access:\n\n```js\nconst runtime = require('i18ntk/runtime');\n\nconst i18n = runtime.initRuntime({\n baseDir: './locales',\n language: 'en',\n lazy: true\n});\n\nconsole.log(i18n.t('common.hello')); // loads common.json on first access\n```\n\nWhen `lazy: true`, the runtime builds a key-to-file manifest on first access and loads individual files on demand. Files are loaded once and cached. If the manifest is missing or incomplete, the runtime falls back to full eager loading for that language. Manifest size is capped at 100KB with path containment validation.\n\nProduction guidance:\n\n- Prefer the object returned from `initRuntime()` instead of module-level `runtime.t()` in apps with multiple tenants, projects, or locale roots.\n- Use `lazy: true` for large modular locale folders where lower steady-state memory matters more than a small first-key lookup cost.\n- Use `preload: true` without `lazy` for small locale sets or latency-sensitive startup paths.\n- Call `refresh(language)` after deploying or writing changed locale files so cached data and lazy manifests are rebuilt.\n- Use per-call language overrides when rendering one-off alternate-language strings: `i18n.t('common.hello', {}, { language: 'de' })`.\n- Use `translateBatch()` for small groups of labels and `clearCache()` / `getCacheInfo()` for cache maintenance and diagnostics.\n- `i18ntk/runtime/enhanced` remains available for compatibility with existing async/encryption users, but new production integrations should start with `i18ntk/runtime`.\n\n## Runtime API\n\nUse `i18ntk/runtime` when an application needs to read locale JSON files at runtime.\n\n```js\nconst runtime = require('i18ntk/runtime');\n\nconst i18n = runtime.initRuntime({\n baseDir: './locales',\n language: 'en',\n fallbackLanguage: 'en',\n keySeparator: '.',\n preload: true\n});\n\nconsole.log(i18n.t('common.hello'));\ni18n.setLanguage('fr');\nconsole.log(i18n.getLanguage());\nconsole.log(i18n.getAvailableLanguages());\ni18n.refresh('fr');\n```\n\nUseful production helpers:\n\n```js\ni18n.t('common.hello', {}, { language: 'de' }); // per-call language override\ni18n.translateBatch(['menu.home', 'menu.settings']);\ni18n.clearCache('fr');\nconsole.log(i18n.getCacheInfo());\n```\n\nSee [docs/runtime.md](./docs/runtime.md) for runtime details.\n\n## Configuration\n\ni18ntk uses a project-local `.i18ntk-config` file.\n\nExample:\n\n```json\n{\n \"version\": \"4.4.4\",\n \"sourceDir\": \"./locales\",\n \"i18nDir\": \"./locales\",\n \"outputDir\": \"./i18ntk-reports\",\n \"sourceLanguage\": \"en\",\n \"defaultLanguages\": [\"en\", \"de\", \"es\", \"fr\", \"ru\"],\n \"reports\": {\n \"format\": \"markdown\"\n },\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"],\n \"autoTranslate\": {\n \"placeholderMode\": \"preserve\",\n \"concurrency\": 12,\n \"batchSize\": 100,\n \"progressInterval\": 25,\n \"retryCount\": 3,\n \"retryDelay\": 1000,\n \"timeout\": 15000,\n \"dryRunFirst\": true,\n \"onlyMissingOrEnglish\": true,\n \"reportStdout\": true,\n \"bom\": false,\n \"protectionEnabled\": true,\n \"protectionFile\": \"./i18ntk-auto-translate.json\",\n \"promptProtectionSetup\": true,\n \"promptProtectionUpdate\": true\n },\n \"setup\": {\n \"completed\": true\n },\n \"extensions\": {\n \"workbench\": {\n \"localeDirectory\": \"./locales\",\n \"sourceLocale\": \"en\"\n },\n \"lens\": {\n \"localeDirectory\": \"./locales\",\n \"sourceLocale\": \"en\",\n \"keyFormats\": [\"dot\", \"snake\"]\n }\n }\n}\n```\n\nSee [docs/api/CONFIGURATION.md](./docs/api/CONFIGURATION.md) for the full configuration model.\n\n## Public Package Contents\n\nThe public package intentionally ships runtime and CLI files only.\n\nThe package includes:\n\n- CLI entry points under `main/`\n- manager commands and services\n- runtime API files under `runtime/`\n- settings UI files required at runtime\n- bundled internal UI locales\n- shared utilities required by the shipped commands\n- `README.md`, `CHANGELOG.md`, `LICENSE`, and policy files\n\nThe public package manifest includes `readmeFilename: \"README.md\"`, and the release staging script fails if `README.md` is missing or empty.\n\n## Documentation\n\n- [Documentation Index](./docs/README.md)\n- [Getting Started](./docs/getting-started.md)\n- [API Reference](./docs/api/API_REFERENCE.md)\n- [Configuration Guide](./docs/api/CONFIGURATION.md)\n- [Runtime API Guide](./docs/runtime.md)\n- [Auto Translate Guide](./docs/auto-translate.md)\n- [Scanner Guide](./docs/scanner-guide.md)\n- [Environment Variables](./docs/environment-variables.md)\n- [Migration Guide v4.3.3](./docs/migration-guide-v4.3.3.md)\n\n## Security\n\n- No API key is required for the default Auto Translate flow.\n- Do not store secrets in locale files, `.i18ntk-config`, or protection files.\n- Project-specific brand/product terms should be configured by the user, not hardcoded into the package.\n- Report security issues using [SECURITY.md](./SECURITY.md).\n\n## Community\n\n- [Contributing](./CONTRIBUTING.md)\n- [Code of Conduct](./CODE_OF_CONDUCT.md)\n- [Funding](./FUNDING.md)\n\n## Related Tools\n\n| Tool | Purpose |\n|---|---|\n| **i18ntk** | Zero-dependency i18n toolkit for scanning, validation, translation, reports, and runtime loading. |\n| **i18ntk Workbench** | Full VS Code localization health dashboard powered by i18ntk. |\n| **i18ntk Lens** | Lightweight inline translation hovers, diagnostics, and key navigation. |\n| **PublishGuard** | Pre-publish safety scanner for npm packages and VS Code extensions. |\n| **ContextKit** | AI coding context manager for AGENTS.md, Claude, Cursor, Copilot, Roo, and Codex files. |\n\n## License\n\nMIT. See [LICENSE](./LICENSE).\n"
247
254
  }
@@ -0,0 +1,442 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const ISSUE_TYPES = new Set([
7
+ 'missing_key',
8
+ 'unused_key',
9
+ 'placeholder_mismatch',
10
+ 'likely_untranslated',
11
+ 'expansion_risk',
12
+ 'hardcoded_text',
13
+ ]);
14
+
15
+ function generateI18ntkReport(options = {}) {
16
+ const projectRoot = path.resolve(options.projectRoot || process.cwd());
17
+ const localesDir = path.resolve(projectRoot, options.localesDir || options.i18nDir || './locales');
18
+ const sourceDir = path.resolve(projectRoot, options.sourceDir || './src');
19
+ const sourceLocale = String(options.sourceLocale || 'en');
20
+
21
+ ensureWithin(projectRoot, localesDir, 'Locale directory');
22
+ ensureWithin(projectRoot, sourceDir, 'Source directory');
23
+ if (!fs.existsSync(localesDir)) {
24
+ throw new Error(`Locale directory not found: ${localesDir}`);
25
+ }
26
+
27
+ const localeFiles = discoverLocaleFiles(localesDir);
28
+ if (localeFiles.length === 0) {
29
+ throw new Error(`No locale JSON files found in ${localesDir}`);
30
+ }
31
+
32
+ const locales = [...new Set(localeFiles.map(file => file.locale))].sort();
33
+ if (!locales.includes(sourceLocale)) {
34
+ throw new Error(`Source locale "${sourceLocale}" was not found in ${localesDir}`);
35
+ }
36
+
37
+ const localeValues = {};
38
+ const keyLocations = new Map();
39
+ for (const item of localeFiles) {
40
+ const raw = fs.readFileSync(item.filePath, 'utf8');
41
+ let parsed;
42
+ try {
43
+ parsed = JSON.parse(raw);
44
+ } catch (error) {
45
+ const rel = path.relative(projectRoot, item.filePath);
46
+ throw new Error(`Invalid JSON in ${rel}: ${error.message}`);
47
+ }
48
+ const flat = flattenObject(parsed);
49
+ localeValues[item.locale] = { ...(localeValues[item.locale] || {}), ...flat };
50
+ for (const key of Object.keys(flat)) {
51
+ keyLocations.set(`${item.locale}:${key}`, {
52
+ file: item.filePath,
53
+ line: findLine(raw, key),
54
+ column: 1,
55
+ });
56
+ }
57
+ }
58
+
59
+ const sourceValues = localeValues[sourceLocale] || {};
60
+ const allKeys = new Set(Object.keys(sourceValues));
61
+ for (const values of Object.values(localeValues)) {
62
+ Object.keys(values).forEach(key => allKeys.add(key));
63
+ }
64
+
65
+ const sourceScan = scanSourceFiles(sourceDir, new Set(Object.keys(sourceValues)));
66
+ const usedKeys = sourceScan.usedKeys;
67
+ const issues = [];
68
+ const addIssue = issue => issues.push(normalizeIssue(issue, issues.length + 1, projectRoot));
69
+
70
+ for (const locale of locales) {
71
+ if (locale === sourceLocale) continue;
72
+ const values = localeValues[locale] || {};
73
+ for (const key of allKeys) {
74
+ if (sourceValues[key] === undefined) continue;
75
+ const targetValue = values[key];
76
+ const location = keyLocations.get(`${locale}:${key}`);
77
+ if (targetValue === undefined || targetValue === null || targetValue === '') {
78
+ const usage = sourceScan.usageLocations.get(key)?.[0];
79
+ addIssue({
80
+ type: 'missing_key',
81
+ severity: 'warning',
82
+ locale,
83
+ key,
84
+ file: usage?.file || location?.file,
85
+ line: usage?.line || location?.line,
86
+ column: usage?.column || location?.column,
87
+ message: `${locale} is missing translation key "${key}".`,
88
+ suggestion: 'Add this key to the locale file.',
89
+ });
90
+ continue;
91
+ }
92
+
93
+ const sourcePlaceholders = extractPlaceholders(sourceValues[key]);
94
+ const targetPlaceholders = extractPlaceholders(targetValue);
95
+ const missing = [...sourcePlaceholders].filter(name => !targetPlaceholders.has(name));
96
+ const extra = [...targetPlaceholders].filter(name => !sourcePlaceholders.has(name));
97
+ if (missing.length || extra.length) {
98
+ addIssue({
99
+ type: 'placeholder_mismatch',
100
+ severity: 'error',
101
+ locale,
102
+ key,
103
+ file: location?.file,
104
+ line: location?.line,
105
+ column: location?.column,
106
+ message: `${locale}.${key} has placeholder mismatch.`,
107
+ suggestion: `Missing: ${missing.join(', ') || 'none'}; extra: ${extra.join(', ') || 'none'}.`,
108
+ });
109
+ }
110
+
111
+ if (looksLikelyUntranslated(sourceValues[key], targetValue, locale)) {
112
+ addIssue({
113
+ type: 'likely_untranslated',
114
+ severity: 'warning',
115
+ locale,
116
+ key,
117
+ file: location?.file,
118
+ line: location?.line,
119
+ column: location?.column,
120
+ confidence: 0.85,
121
+ message: `${locale}.${key} looks untranslated.`,
122
+ suggestion: 'Review this value against the source locale.',
123
+ });
124
+ }
125
+
126
+ const expansionPercent = getExpansionPercent(sourceValues[key], targetValue);
127
+ if (expansionPercent >= Number(options.expansionThresholdPct || 30)) {
128
+ addIssue({
129
+ type: 'expansion_risk',
130
+ severity: 'info',
131
+ locale,
132
+ key,
133
+ file: location?.file,
134
+ line: location?.line,
135
+ column: location?.column,
136
+ confidence: 0.75,
137
+ message: `${locale}.${key} is ${expansionPercent}% longer than the source value.`,
138
+ suggestion: 'Check layouts with constrained space.',
139
+ });
140
+ }
141
+ }
142
+ }
143
+
144
+ for (const key of Object.keys(sourceValues)) {
145
+ if (!usedKeys.has(key)) {
146
+ const location = keyLocations.get(`${sourceLocale}:${key}`);
147
+ addIssue({
148
+ type: 'unused_key',
149
+ severity: 'info',
150
+ locale: sourceLocale,
151
+ key,
152
+ file: location?.file,
153
+ line: location?.line,
154
+ column: location?.column,
155
+ confidence: sourceScan.scannedFiles > 0 ? 0.8 : 0.4,
156
+ message: `Translation key "${key}" was not found in scanned source files.`,
157
+ suggestion: 'Review before deleting; dynamic key construction may hide usage.',
158
+ });
159
+ }
160
+ }
161
+
162
+ for (const item of sourceScan.hardcodedTexts) {
163
+ addIssue({
164
+ type: 'hardcoded_text',
165
+ severity: 'warning',
166
+ file: item.file,
167
+ line: item.line,
168
+ column: item.column,
169
+ confidence: item.existingKey ? 0.9 : 0.65,
170
+ message: item.existingKey
171
+ ? `Hardcoded text matches existing key "${item.existingKey}".`
172
+ : `Hardcoded user-facing text: "${item.text}".`,
173
+ suggestion: item.existingKey ? `Use t('${item.existingKey}') instead.` : 'Move this text into locale files.',
174
+ });
175
+ }
176
+
177
+ const localeReports = locales.map(locale => {
178
+ const values = localeValues[locale] || {};
179
+ const missingKeys = Object.keys(sourceValues).filter(key => values[key] === undefined || values[key] === null || values[key] === '').length;
180
+ const translatedKeys = Math.max(0, Object.keys(sourceValues).length - missingKeys);
181
+ const byLocale = issues.filter(issue => issue.locale === locale);
182
+ return {
183
+ locale,
184
+ totalKeys: Object.keys(sourceValues).length,
185
+ translatedKeys,
186
+ missingKeys,
187
+ completenessPct: pct(translatedKeys, Object.keys(sourceValues).length),
188
+ placeholderMismatchCount: byLocale.filter(issue => issue.type === 'placeholder_mismatch').length,
189
+ likelyUntranslatedCount: byLocale.filter(issue => issue.type === 'likely_untranslated').length,
190
+ expansionRiskCount: byLocale.filter(issue => issue.type === 'expansion_risk').length,
191
+ };
192
+ });
193
+
194
+ const averageCompletenessPct = localeReports.length
195
+ ? Math.round(localeReports.reduce((sum, locale) => sum + locale.completenessPct, 0) / localeReports.length)
196
+ : 0;
197
+
198
+ return {
199
+ schemaVersion: 1,
200
+ generatedAt: new Date().toISOString(),
201
+ projectRoot,
202
+ config: {
203
+ sourceLocale,
204
+ localesDir,
205
+ namespaces: [...new Set(localeFiles.map(file => file.namespace).filter(Boolean))].sort(),
206
+ },
207
+ summary: {
208
+ totalKeys: Object.keys(sourceValues).length,
209
+ localeCount: locales.length,
210
+ averageCompletenessPct,
211
+ issueCount: issues.length,
212
+ missingKeyCount: issues.filter(issue => issue.type === 'missing_key').length,
213
+ unusedKeyCount: issues.filter(issue => issue.type === 'unused_key').length,
214
+ placeholderMismatchCount: issues.filter(issue => issue.type === 'placeholder_mismatch').length,
215
+ likelyUntranslatedCount: issues.filter(issue => issue.type === 'likely_untranslated').length,
216
+ expansionRiskCount: issues.filter(issue => issue.type === 'expansion_risk').length,
217
+ hardcodedTextCount: issues.filter(issue => issue.type === 'hardcoded_text').length,
218
+ },
219
+ locales: localeReports,
220
+ issues,
221
+ };
222
+ }
223
+
224
+ function discoverLocaleFiles(localesDir) {
225
+ const files = [];
226
+ const entries = fs.readdirSync(localesDir, { withFileTypes: true });
227
+ for (const entry of entries) {
228
+ const entryPath = path.join(localesDir, entry.name);
229
+ if (entry.isDirectory()) {
230
+ for (const filePath of walk(entryPath, ['.json'])) {
231
+ files.push({
232
+ locale: entry.name,
233
+ namespace: path.basename(filePath, '.json'),
234
+ filePath,
235
+ });
236
+ }
237
+ } else if (entry.isFile() && entry.name.endsWith('.json')) {
238
+ files.push({
239
+ locale: path.basename(entry.name, '.json'),
240
+ namespace: 'default',
241
+ filePath: entryPath,
242
+ });
243
+ }
244
+ }
245
+ return files;
246
+ }
247
+
248
+ function walk(dir, extensions, acc = []) {
249
+ if (!fs.existsSync(dir)) return acc;
250
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
251
+ if (['node_modules', '.git', 'dist', 'build', 'coverage', 'out', 'tmp', 'temp', 'test', 'tests', 'test_env', 'i18ntk-reports'].includes(entry.name)) continue;
252
+ const entryPath = path.join(dir, entry.name);
253
+ if (entry.isDirectory()) walk(entryPath, extensions, acc);
254
+ else if (entry.isFile() && extensions.includes(path.extname(entry.name))) acc.push(entryPath);
255
+ }
256
+ return acc;
257
+ }
258
+
259
+ function flattenObject(value, prefix = '', out = {}) {
260
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
261
+ for (const [key, child] of Object.entries(value)) {
262
+ flattenObject(child, prefix ? `${prefix}.${key}` : key, out);
263
+ }
264
+ } else if (prefix) {
265
+ out[prefix] = value == null ? '' : String(value);
266
+ }
267
+ return out;
268
+ }
269
+
270
+ function scanSourceFiles(sourceDir, availableKeys) {
271
+ const usedKeys = new Set();
272
+ const usageLocations = new Map();
273
+ const hardcodedTexts = [];
274
+ const translationValueIndex = new Map();
275
+ const files = walk(sourceDir, ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte']);
276
+
277
+ for (const file of files) {
278
+ const content = fs.readFileSync(file, 'utf8');
279
+ const lines = content.split(/\r?\n/);
280
+ const keyPatterns = [
281
+ /\b(?:t|tx|__|_t)\s*\(\s*['"`]([^'"`]+)['"`]/g,
282
+ /\bi18n\.t\s*\(\s*['"`]([^'"`]+)['"`]/g,
283
+ /\bi18nKey\s*=\s*['"`]([^'"`]+)['"`]/g,
284
+ ];
285
+ for (const pattern of keyPatterns) {
286
+ let match;
287
+ while ((match = pattern.exec(content))) {
288
+ const key = match[1];
289
+ usedKeys.add(key);
290
+ const location = offsetToLocation(content, match.index);
291
+ const list = usageLocations.get(key) || [];
292
+ list.push({ file, line: location.line, column: location.column });
293
+ usageLocations.set(key, list);
294
+ }
295
+ }
296
+
297
+ lines.forEach((lineText, index) => {
298
+ const textPattern = />\s*([A-Z][A-Za-z0-9 ,.!?'-]{3,})\s*</g;
299
+ const attrPattern = /\b(?:aria-label|title|alt|placeholder)=["']([A-Z][^"']{3,})["']/g;
300
+ const assignmentPattern = /\b(?:label|title|text|message|placeholder)\s*=\s*["']([A-Z][^"']{3,})["']/g;
301
+ for (const pattern of [textPattern, attrPattern, assignmentPattern]) {
302
+ let match;
303
+ while ((match = pattern.exec(lineText))) {
304
+ const text = match[1].trim();
305
+ const existingKey = translationValueIndex.get(text) || findKeyByValue(text, availableKeys);
306
+ hardcodedTexts.push({ file, line: index + 1, column: match.index + 1, text, existingKey });
307
+ }
308
+ }
309
+ });
310
+ }
311
+
312
+ return { usedKeys, usageLocations, hardcodedTexts, scannedFiles: files.length };
313
+ }
314
+
315
+ function findKeyByValue(_text, _availableKeys) {
316
+ return undefined;
317
+ }
318
+
319
+ function extractPlaceholders(value) {
320
+ const placeholders = new Set();
321
+ const text = String(value || '');
322
+ const patterns = [/\{\{\s*([\w.-]+)\s*\}\}/g, /%\{([\w.-]+)\}/g, /\{([\w.-]+)\}/g];
323
+ for (const pattern of patterns) {
324
+ let match;
325
+ while ((match = pattern.exec(text))) placeholders.add(match[1]);
326
+ }
327
+ return placeholders;
328
+ }
329
+
330
+ function looksLikelyUntranslated(source, target, locale) {
331
+ if (!source || !target || locale === 'en') return false;
332
+ const normalizedSource = normalizeText(source);
333
+ const normalizedTarget = normalizeText(target);
334
+ if (!/[a-z]/i.test(normalizedSource)) return false;
335
+ return normalizedSource.length >= 4 && normalizedSource === normalizedTarget;
336
+ }
337
+
338
+ function normalizeText(value) {
339
+ return String(value || '').replace(/\{\{[^}]+\}\}|\{[^}]+\}|%\{[^}]+\}/g, '').trim().toLowerCase();
340
+ }
341
+
342
+ function getExpansionPercent(source, target) {
343
+ const sourceLength = String(source || '').length;
344
+ const targetLength = String(target || '').length;
345
+ if (sourceLength < 4 || targetLength <= sourceLength) return 0;
346
+ return Math.round(((targetLength - sourceLength) / sourceLength) * 100);
347
+ }
348
+
349
+ function pct(part, total) {
350
+ return total > 0 ? Math.round((part / total) * 100) : 0;
351
+ }
352
+
353
+ function normalizeIssue(issue, index, projectRoot) {
354
+ if (!ISSUE_TYPES.has(issue.type)) throw new Error(`Invalid report issue type: ${issue.type}`);
355
+ return {
356
+ id: `${issue.type}-${index}`,
357
+ type: issue.type,
358
+ severity: issue.severity || 'warning',
359
+ locale: issue.locale,
360
+ key: issue.key,
361
+ file: issue.file ? path.relative(projectRoot, issue.file).split(path.sep).join('/') : undefined,
362
+ line: issue.line,
363
+ column: issue.column,
364
+ confidence: issue.confidence,
365
+ message: issue.message,
366
+ suggestion: issue.suggestion,
367
+ };
368
+ }
369
+
370
+ function findLine(content, key) {
371
+ const escaped = key.split('.').pop().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
372
+ const lines = content.split(/\r?\n/);
373
+ const pattern = new RegExp(`"${escaped}"\\s*:`);
374
+ const index = lines.findIndex(line => pattern.test(line));
375
+ return index >= 0 ? index + 1 : undefined;
376
+ }
377
+
378
+ function offsetToLocation(content, offset) {
379
+ const before = content.slice(0, offset).split(/\r?\n/);
380
+ return { line: before.length, column: before[before.length - 1].length + 1 };
381
+ }
382
+
383
+ function ensureWithin(root, target, label) {
384
+ const rel = path.relative(root, target);
385
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
386
+ throw new Error(`${label} must be inside the project root.`);
387
+ }
388
+ }
389
+
390
+ function renderReportAsMarkdown(report) {
391
+ const lines = [
392
+ '# i18ntk Report',
393
+ '',
394
+ `Generated: ${report.generatedAt}`,
395
+ `Project: ${report.projectRoot}`,
396
+ '',
397
+ '## Summary',
398
+ '',
399
+ `- Total keys: ${report.summary.totalKeys}`,
400
+ `- Locales: ${report.summary.localeCount}`,
401
+ `- Average completeness: ${report.summary.averageCompletenessPct}%`,
402
+ `- Issues: ${report.summary.issueCount}`,
403
+ '',
404
+ '## Translation completeness',
405
+ '',
406
+ '| Locale | Translated | Missing | Complete | Placeholder | Untranslated | Expansion |',
407
+ '|---|---:|---:|---:|---:|---:|---:|',
408
+ ...report.locales.map(locale => `| ${md(locale.locale)} | ${locale.translatedKeys}/${locale.totalKeys} | ${locale.missingKeys} | ${locale.completenessPct}% | ${locale.placeholderMismatchCount} | ${locale.likelyUntranslatedCount} | ${locale.expansionRiskCount} |`),
409
+ '',
410
+ '## Issues',
411
+ '',
412
+ ...(report.issues.length
413
+ ? report.issues.map(issue => `- **${issue.severity} ${issue.type}** ${md(issue.locale || '')} ${issue.key ? `\`${md(issue.key)}\`` : ''} ${md(issue.message)}${issue.file ? ` (${md(issue.file)}${issue.line ? `:${issue.line}` : ''})` : ''}`)
414
+ : ['_No issues found._']),
415
+ '',
416
+ ];
417
+ return lines.join('\n');
418
+ }
419
+
420
+ function renderReportAsHtml(report) {
421
+ const issues = report.issues.map(issue => `<tr><td>${html(issue.type)}</td><td>${html(issue.severity)}</td><td>${html(issue.locale || '')}</td><td>${html(issue.key || '')}</td><td>${html(issue.message)}</td><td>${html(issue.file || '')}</td></tr>`).join('');
422
+ return `<!doctype html>
423
+ <html lang="en"><head><meta charset="utf-8"><title>i18ntk Report</title>
424
+ <style>body{font-family:system-ui,sans-serif;margin:24px;line-height:1.45}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:6px;text-align:left}.cards{display:flex;gap:12px;flex-wrap:wrap}.card{border:1px solid #ddd;padding:12px}</style>
425
+ </head><body><h1>i18ntk Report</h1><div class="cards"><div class="card">Keys: ${report.summary.totalKeys}</div><div class="card">Locales: ${report.summary.localeCount}</div><div class="card">Completeness: ${report.summary.averageCompletenessPct}%</div><div class="card">Issues: ${report.summary.issueCount}</div></div>
426
+ <h2>Translation completeness</h2><table><thead><tr><th>Locale</th><th>Translated</th><th>Missing</th><th>Complete</th></tr></thead><tbody>${report.locales.map(locale => `<tr><td>${html(locale.locale)}</td><td>${locale.translatedKeys}/${locale.totalKeys}</td><td>${locale.missingKeys}</td><td>${locale.completenessPct}%</td></tr>`).join('')}</tbody></table>
427
+ <h2>Issues</h2><table><thead><tr><th>Type</th><th>Severity</th><th>Locale</th><th>Key</th><th>Message</th><th>File</th></tr></thead><tbody>${issues || '<tr><td colspan="6">No issues found.</td></tr>'}</tbody></table></body></html>`;
428
+ }
429
+
430
+ function md(value) {
431
+ return String(value || '').replace(/[\\|`*_{}\[\]()#+\-.!]/g, '\\$&');
432
+ }
433
+
434
+ function html(value) {
435
+ return String(value || '').replace(/[&<>"']/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char]));
436
+ }
437
+
438
+ module.exports = {
439
+ generateI18ntkReport,
440
+ renderReportAsMarkdown,
441
+ renderReportAsHtml,
442
+ };
package/utils/security.js CHANGED
@@ -734,8 +734,10 @@ static _logging = false;
734
734
  'framework', 'processing', 'performance', 'advanced',
735
735
  // UI and theme settings
736
736
  'theme', 'ui', 'setup', 'reports', 'display', 'interface',
737
- // Security and settings
738
- 'security', 'settings', 'preferences', 'config', 'configuration',
737
+ // Security and settings
738
+ 'security', 'settings', 'preferences', 'config', 'configuration',
739
+ // Extension-owned config sections. The CLI preserves these but ignores unknown nested keys.
740
+ 'extensions',
739
741
  // Additional common properties
740
742
  'autoSave', 'autoBackup', 'validateOnSave', 'showWarnings', 'verbose',
741
743
  'timeout', 'retries', 'batchSize', 'maxConcurrency', 'cacheEnabled',