i18ntk 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,8 +3,57 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
-
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [4.1.0] - 2026-05-21
9
+
10
+ ### Fixed
11
+ - Runtime: stale manifest entries (deleted files after manifest construction) no longer cause unhandled exceptions; loadedFiles set before load with try/catch guard.
12
+ - Runtime: `refresh()` now correctly clears the key manifest for the refreshed language, preventing stale file references.
13
+ - Runtime: null `baseDir` guard prevents cascading `validatePath(null)` errors in `loadKeyManifestFromDir`.
14
+ - Backup: `handleVerify` hash-chain verification rewritten for incremental backups — rebuilds full state oldest→newest before per-entry hash comparison; no longer reports false "Missing file" errors for unchanged files.
15
+ - Backup: `cleanupOldBackups` and `handleCleanup` now preserve parent backups of incremental chains by scanning kept backups for `_meta.parent` references before deletion.
16
+ - Backup: `--incremental=false` is now correctly parsed as falsy (string-to-boolean conversion fixed).
17
+ - Backup: `buildRestoreChain` detects circular parent references via visited-set traversal.
18
+ - Sizing: `this.adminAuth` undefined crash in `run()` fixed — changed to correct `adminAuth` local variable.
19
+ - Sizing: duplicate `analyze()` method removed; format-based display logic (`--format=json` vs table) restored to the surviving method.
20
+ - Sizing: setup-check IIFE now guarded with `require.main === module`, preventing unexpected `process.exit()` on `require()`.
21
+ - Scanner: `frameworks.vanilla` key added to `getFrameworkSpecific` — no longer returns `undefined` for vanilla projects.
22
+ - Scanner: `--source-language` CLI flag now correctly propagates through config chain to `scanFile` and `generateSuggestion` (camelCase vs hyphen key mismatch fixed).
23
+ - Scanner: `isTextInLanguage` now always validates character ratio unconditionally even for no-stopword language profiles.
24
+ - Watch: `{ onChange: fn }` object-format callbacks are now properly subscribed to change/add/unlink events.
25
+ - Watch: debounce `setTimeout` timers are now stored per-watcher and cleared on `emitter.stop()`, preventing memory leaks and spurious I/O after stop.
26
+ - Watch: `'unlink'` events are now subscribed for backward-compatible plain-function callback users.
27
+ - Usage: duplicate `require.main === module` block removed (caused `TypeError: Identifier 'main' has already been declared` at execution).
28
+ - Usage: `_keyInSourceComments` optimized from O(n*m) to O(n+m) by pre-computing a `Set` of all comment strings once before the dead key loop.
29
+ - Usage: `--cleanup=false` and `--dry-run-delete=false` now correctly parse as falsy via `toBool()` helper.
30
+ - Usage: broken `detectFrameworkPatterns()` call with `undefined` arguments removed.
31
+ - Usage: dead `return;` in `analyze()` removed so the result object is now actually returned.
32
+ - Validator: missing `try` block in `run()` added so errors are caught by the existing `catch(error)` handler.
33
+ - Validator: `--enforce-key-style=true/false` now correctly parsed (previously silently ignored due to `!includes('=')` guard).
34
+ - Validator: `flat` style no longer produces false positives for nested keys — validates only the leaf segment (last `.`-delimited part).
35
+ - Validator: `suggestKeyFix` for `flat` style now returns `segments.map(s => s.toLowerCase()).join('')` instead of camelCase.
36
+ - Validator: `getLanguageFiles` no longer crashes when `excludeFiles` config property is undefined.
37
+ - Validator: `enforceKeyStyle` now correctly propagated from CLI args to `this.config.enforceKeyStyle` in `run()`.
38
+ - Protection: `standalone` mode boundary check now handles opening/closing punctuation (`(`, `[`, `{`, `"`, `'`, `-`, `–`, `—`) and CJK marks (`。`, `、`, `」`).
39
+ - Protection: `\b` word-boundary assertions replaced with Unicode-aware `(^|[\s\p{P}])` / `([\s\p{P}]|$)` patterns with `u` flag for non-ASCII language support.
40
+ - Protection: `surrounded` context rule parser now uses `indexOf(',')` split instead of greedy `(.+,.+)` regex to correctly parse multi-word right-side expressions.
41
+ - Protection: `hasProtectionRules` no longer throws `TypeError` when `terms` property is undefined.
42
+ - Protection: `shouldPreserveWholeValue` now respects context rules by only matching `type === 'global'` entries.
43
+ - Manager option 7 ("Fix placeholder translations") now interpolates fixer status values correctly instead of printing raw `{languages}`, `{sourceDir}`, `{skipped}`, or `{totalIssues}` placeholders.
44
+ - Delete Reports settings now include cache cleanup targets.
45
+ - Public package metadata updated.
46
+
47
+ ### Security
48
+ - Watch module: debounce timers properly cleaned up on stop and callback subscriptions corrected for object-format and unlink handlers.
49
+ - Runtime: loadedFiles lock-before-load pattern prevents duplicate I/O and stale manifest crash.
50
+ - Backup: circular parent reference detection; `--incremental=false` string truthy bypass closed.
51
+ - Sizing: adminAuth variable reference corrected; require()-time `process.exit()` guarded.
52
+ - Scanner: vanilla framework key prevents `undefined` return; stopword-less validRatio enforced.
53
+ - Usage: O(n+m) comment scanning prevents DoS via large codebase with many dead keys; `toBool()` prevents flag injection.
54
+ - Validator: try/catch pairing restored; `flat` leaf-segment prevents false-positive flood.
55
+ - Protection: Unicode-aware punctuation boundaries for CJK/Cyrillic/Arabic; standalone boundaries include the expanded punctuation set.
56
+
8
57
  ## [4.0.0] - 2026-05-21
9
58
 
10
59
  ### Added
@@ -17,32 +66,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
66
  - **Runtime Lazy Loading**: `runtime/index.js` adds `lazy: true` option to `initRuntime()`. Defers locale file loading until the first key access, guided by an auto-generated key-to-file manifest. Falls back to eager loading if manifest is missing. Manifest capped at 100KB with path containment validation.
18
67
  - **Protection Context-Aware Rules**: `utils/translate/protection.js` extends the protection config schema to support context rules (`after:word`, `before:word`, `standalone`, `surrounded:left,right`). Plain string terms remain fully backward compatible. Total context rules capped at 100.
19
68
 
20
- ### Fixed
21
- - `i18ntk/runtime` `initRuntime()` now returns independent runtime instances with separate language, fallback language, base directory, and cache state. Later `initRuntime()` calls no longer overwrite earlier returned runtimes or the module-level compatibility singleton.
22
- - Manager option 7 ("Fix placeholder translations") now interpolates fixer status values correctly instead of printing raw `{languages}`, `{sourceDir}`, `{skipped}`, or `{totalIssues}` placeholders.
69
+ ### Fixed
70
+ - `i18ntk/runtime` `initRuntime()` now returns independent runtime instances with separate language, fallback language, base directory, and cache state. Later `initRuntime()` calls no longer overwrite earlier returned runtimes or the module-level compatibility singleton.
23
71
 
24
- ### Changed
25
- - `watchLocales()` now returns a callable watcher object with EventEmitter methods instead of only a bare `stop` function. Existing `const stop = watchLocales(...); stop();` usage remains supported. The returned object fires `change`, `add`, `unlink`, `error` events. If a callback function is passed as the second argument, it is auto-subscribed to `change` and `add` for backward compatibility.
72
+ ### Changed
73
+ - `watchLocales()` now returns a callable watcher object with EventEmitter methods instead of only a bare `stop` function. Existing `const stop = watchLocales(...); stop();` usage remains supported. The returned object fires `change`, `add`, `unlink`, `error` events. If a callback function is passed as the second argument, it is auto-subscribed to `change` and `add` for backward compatibility.
26
74
  - **BREAKING**: `i18ntk-sizing` JSON reports now include `expansionPredictions` at the top level when `--predict-expansion` is used. This field is additive — existing report fields are preserved.
27
75
 
28
- ### Security
29
- - All new features reuse existing `SecurityUtils.safe*` wrappers for file I/O, path validation, and input sanitization.
30
- - Watch module validates all directory paths against project root with containment checks and caps at 50 directories.
31
- - Runtime lazy-loading manifest entries validated for path containment and size-limited to 100KB.
32
- - Protection context rules parsed from constrained DSL — never accepts raw user-controlled regex from config.
33
- - Backup incremental chain depth capped at 10 to prevent resource exhaustion; hash chain verified before restore.
34
-
35
- ## [3.3.0] - 2026-05-20
76
+ ## [3.3.0] - 2026-05-20
36
77
 
37
- ### Changed
38
- - Auto Translate now supports `--provider google|deepl|libretranslate`; DeepL uses `DEEPL_API_KEY`, while LibreTranslate supports `LIBRETRANSLATE_URL` and optional `LIBRETRANSLATE_API_KEY`.
39
- - Auto Translate provider networking now keeps HTTPS, host allowlist, response-size, private-network, and redacted security logging protections in place for additional providers.
40
-
41
- ### Fixed
42
- - `i18ntk-complete` now fills missing target-language keys from the English source value with a language prefix such as `[DE] Home` instead of writing `NOT_TRANSLATED`; this works for both `locales/en/*.json` and monolith `locales/en.json` layouts.
43
-
44
- ### Security
45
- - Eliminated all 21 dynamic `require()` calls flagged by Socket.dev: 20 `require(path.join(__dirname, ...))` patterns in `i18ntk-js.js`, `i18ntk-py.js`, `i18ntk-java.js`, `i18ntk-php.js`, and `i18ntk-go.js` converted to static string literal requires.
78
+ ### Changed
79
+ - Auto Translate now supports `--provider google|deepl|libretranslate`; DeepL uses `DEEPL_API_KEY`, while LibreTranslate supports `LIBRETRANSLATE_URL` and optional `LIBRETRANSLATE_API_KEY`.
80
+ - Auto Translate provider networking now keeps HTTPS, host allowlist, response-size, private-network, and redacted security logging protections in place for additional providers.
81
+
82
+ ### Fixed
83
+ - `i18ntk-complete` now fills missing target-language keys from the English source value with a language prefix such as `[DE] Home` instead of writing `NOT_TRANSLATED`; this works for both `locales/en/*.json` and monolith `locales/en.json` layouts.
84
+
85
+ ### Security
86
+ - Eliminated all 21 dynamic `require()` calls flagged by Socket.dev: 20 `require(path.join(__dirname, ...))` patterns in `i18ntk-js.js`, `i18ntk-py.js`, `i18ntk-java.js`, `i18ntk-php.js`, and `i18ntk-go.js` converted to static string literal requires.
46
87
  - Added `SecurityUtils.validatePath()` gate around the remaining dynamic `require()` in `i18ntk-translate.js` `loadCustomTranslateFn`.
47
88
  - Created `utils/translate/safe-network.js` — a secure HTTPS wrapper with URL host/path allowlist validation, response size limits (100KB), suspicious query parameter detection, and security event logging. All outbound network access now flows through this validated layer.
48
89
  - Replaced direct `https.get` call in `utils/translate/api.js` with `safeHttpGet` from the safe-network wrapper.
@@ -54,18 +95,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
54
95
 
55
96
  ### Socket.dev Analysis Disclaimer
56
97
 
57
- This package is a developer CLI and runtime helper that performs file I/O, network access (translation provider APIs on user request), and environment variable access. As such, Socket.dev will flag the following alerts that are **expected and by design**:
98
+ This package is a developer CLI and runtime helper that performs file I/O, network access (translation provider APIs on user request), and environment variable access. As such, Socket.dev will flag the following alerts that are **expected and by design**:
58
99
 
59
100
  | Alert | Why it's expected |
60
101
  |---|---|
61
- | Network access | Only contacts configured translation providers via HTTPS when user invokes auto-translate. All outbound calls flow through `safe-network.js` with host/path allowlist validation, response size limits, private-network blocking, and redacted security event logging. No telemetry, no unexpected outbound calls. |
102
+ | Network access | Only contacts configured translation providers via HTTPS when user invokes auto-translate. All outbound calls flow through `safe-network.js` with host/path allowlist validation, response size limits, private-network blocking, and redacted security event logging. No telemetry, no unexpected outbound calls. |
62
103
  | Environment variable access | Centralized through `env-manager.js` with a strict allowlist. Blocks `SECRET`, `PASSWORD`, `KEY`, `TOKEN`, `AWS_*`, `NPM_*`, and 15+ other patterns. |
63
104
  | Filesystem access | Reads/writes only project locale files and reports within validated paths. All FS operations gated by `SecurityUtils.validatePath`. |
64
- | URL strings | Hardcoded default provider URLs for Google, DeepL, and LibreTranslate used only for auto-translation. No external resource loading. |
65
-
66
- The v3.3.0 release resolves the actionable dynamic-require alert by eliminating all 21 instances.
67
-
68
- ## [3.2.0] - 2026-05-16
105
+ | URL strings | Hardcoded default provider URLs for Google, DeepL, and LibreTranslate used only for auto-translation. No external resource loading. |
106
+
107
+ The v3.3.0 release resolves the actionable dynamic-require alert by eliminating all 21 instances.
108
+
109
+ ## [3.2.0] - 2026-05-16
69
110
 
70
111
  ### Security
71
112
  - **CRITICAL**: Fixed invalid `crypto.createCipherGCM`/`createDecipherGCM` API calls in `admin-pin.js` — replaced with `crypto.createCipheriv`/`createDecipheriv`.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # i18ntk v4.0.0
1
+ # i18ntk v4.1.0
2
2
 
3
3
  A i18n toolkit - A zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, translation completion, automatic JSON locale translation, reporting, and runtime translation loading.
4
4
 
@@ -9,7 +9,7 @@ A i18n toolkit - A zero-dependency internationalization toolkit for setup, scann
9
9
  [![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)
10
10
  [![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)
11
11
  [![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
12
- [![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.0.0)](https://socket.dev/npm/package/i18ntk/overview/4.0.0)
12
+ [![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.1.0)](https://socket.dev/npm/package/i18ntk/overview/4.1.0)
13
13
 
14
14
  ## Install
15
15
 
@@ -30,6 +30,10 @@ Requirements:
30
30
  - npm `>=8.0.0`
31
31
  - No runtime dependencies
32
32
 
33
+ ## What's New in 4.1.0
34
+
35
+ - **FIX**: Critical and high-impact bugs resolved across the v4.0.0 feature set — runtime staleness crashes, backup hash-chain verification, sizing adminAuth crash, scanner `--source-language` propagation, watch callback subscriptions, dead key detection performance, validator key style enforcement, and protection Unicode boundary handling. See [CHANGELOG.md](./CHANGELOG.md) for complete details.
36
+
33
37
  ## What's New in 4.0.0
34
38
 
35
39
  - **SIZING**: `--predict-expansion` flag computes per-key expansion ratios across languages with Safe/Warning/Critical risk tiers for UI layout planning.
@@ -40,7 +44,6 @@ Requirements:
40
44
  - **BACKUP**: `--incremental` flag creates differential backups with SHA-256 hashing and chained restores.
41
45
  - **RUNTIME**: `lazy: true` option defers locale file loading until first key access for lower memory usage.
42
46
  - **PROTECTION**: Context-aware rules (`after:word`, `before:word`, `standalone`, `surrounded:left,right`) for precise term masking.
43
- - **FIX**: `initRuntime()` now returns independent instances with isolated language and cache state.
44
47
 
45
48
  See [CHANGELOG.md](./CHANGELOG.md) for more release details.
46
49
 
@@ -446,7 +449,7 @@ Example:
446
449
 
447
450
  ```json
448
451
  {
449
- "version": "4.0.0",
452
+ "version": "4.1.0",
450
453
  "sourceDir": "./locales",
451
454
  "i18nDir": "./locales",
452
455
  "outputDir": "./i18ntk-reports",
package/SECURITY.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  The supported production line is `4.x`.
6
6
 
7
- Versions earlier than `4.0.0` are not recommended for production use because later releases include Auto Translate provider hardening, dynamic-require elimination, path-validation hardening, lazy loading with manifest validation, and incremental backup hash-chain verification.
7
+ Versions earlier than `4.1.0` are not recommended for production use because later releases include Auto Translate provider hardening, dynamic-require elimination, path-validation hardening, lazy loading with manifest validation, incremental backup hash-chain verification, and post-4.0.0 critical bug fixes for runtime staleness, backup verification, and CLI flag parsing.
8
8
 
9
9
  ## Security Model
10
10
 
@@ -41,8 +41,10 @@ The v3.3.0 release **resolved** the previously actionable Socket.dev alert:
41
41
  The v4.0.0 release adds the following security hardening:
42
42
  - **Watch module**: all watched directories validated against project root with containment checks; capped at 50 directories.
43
43
  - **Runtime lazy loading**: key-to-file manifest entries validated for path containment; manifest size capped at 100KB.
44
- - **Incremental backups**: hash-chain verification before restore; chain depth capped at 10 increments.
45
- - **Protection context rules**: DSL-parsed context rules — never raw user-controlled regex from config; bounded at 200 chars per rule, 100 rules total.
44
+ - **Incremental backups**: hash-chain verification before restore; chain depth capped at 10 increments; circular parent references detected.
45
+ - **Protection context rules**: DSL-parsed context rules — never raw user-controlled regex from config; bounded at 200 chars per rule, 100 rules total; Unicode-aware `\p{P}` word boundaries for non-ASCII language support.
46
+ - **Scanner multi-language detection**: source-language propagation fixed; stopword-less language profiles now still enforce valid-character ratios.
47
+ - **Usage dead key detection**: optimized O(n+m) comment scanning instead of O(n*m); all CLI boolean flags validated with strict `toBool()` conversion.
46
48
 
47
49
  ## Reporting Vulnerabilities
48
50
 
@@ -66,7 +68,7 @@ Security reports are reviewed privately first. Confirmed issues should receive:
66
68
 
67
69
  ## User Guidance
68
70
 
69
- - Keep i18ntk updated to `4.0.0` or newer.
71
+ - Keep i18ntk updated to `4.1.0` or newer.
70
72
  - Do not commit `.i18ntk-config`, admin PIN files, backup directories, generated reports, logs, npm credentials, or secret material.
71
73
  - Run i18ntk only in project directories you trust.
72
74
  - Review generated translation changes before committing them.
@@ -98,18 +98,24 @@ function getParentHashes(parentData) {
98
98
  return hashes;
99
99
  }
100
100
 
101
- async function buildRestoreChain(startPath, startData) {
101
+ async function buildRestoreChain(startPath, startData) {
102
102
  const chain = [{ path: startPath, data: startData }];
103
- let current = startData;
104
- let currentPath = startPath;
105
-
106
- while (current._meta && current._meta.parent) {
107
- if (chain.length >= 11) {
108
- throw new Error('Chain broken: incremental backup chain exceeds the maximum depth of 10');
109
- }
110
- const parentName = current._meta.parent;
103
+ const visited = new Set();
104
+ visited.add(path.basename(startPath));
105
+ let current = startData;
106
+ let currentPath = startPath;
107
+
108
+ while (current._meta && current._meta.parent) {
109
+ if (chain.length >= 11) {
110
+ throw new Error('Chain broken: incremental backup chain exceeds the maximum depth of 10');
111
+ }
112
+ const parentName = current._meta.parent;
113
+ if (visited.has(parentName)) {
114
+ throw new Error('circular chain reference');
115
+ }
116
+ visited.add(parentName);
111
117
  const parentDir = path.dirname(currentPath);
112
- const parentPath = path.join(parentDir, parentName);
118
+ const parentPath = path.join(parentDir, parentName);
113
119
  if (!SecurityUtils.safeExistsSync(parentPath, parentDir)) {
114
120
  throw new Error(`Chain broken: parent backup '${parentName}' not found in ${parentDir}`);
115
121
  }
@@ -122,10 +128,61 @@ async function buildRestoreChain(startPath, startData) {
122
128
  chain.push({ path: currentPath, data: current });
123
129
  }
124
130
 
125
- chain.reverse();
126
- return chain;
127
- }
128
-
131
+ chain.reverse();
132
+ return chain;
133
+ }
134
+
135
+ function readBackupData(backupPath, baseDir) {
136
+ const raw = SecurityUtils.safeReadFileSync(backupPath, baseDir, 'utf8');
137
+ if (raw === null) {
138
+ throw new Error(`Unable to read backup: ${path.basename(backupPath)}`);
139
+ }
140
+ return JSON.parse(raw);
141
+ }
142
+
143
+ function collectProtectedChainNames(backupDirPath, keptFiles) {
144
+ const protectedNames = new Set();
145
+ const byName = new Map();
146
+ for (const file of keptFiles) {
147
+ byName.set(file.name, file);
148
+ }
149
+
150
+ const queue = [];
151
+ for (const file of keptFiles) {
152
+ try {
153
+ const data = readBackupData(file.path, backupDirPath);
154
+ if (data._meta && data._meta.parent) {
155
+ queue.push(data._meta.parent);
156
+ }
157
+ } catch {}
158
+ }
159
+
160
+ while (queue.length > 0) {
161
+ const name = queue.shift();
162
+ if (protectedNames.has(name)) {
163
+ continue;
164
+ }
165
+ protectedNames.add(name);
166
+
167
+ const file = byName.get(name) || {
168
+ name,
169
+ path: path.join(backupDirPath, name)
170
+ };
171
+ if (!SecurityUtils.safeExistsSync(file.path, backupDirPath)) {
172
+ continue;
173
+ }
174
+
175
+ try {
176
+ const data = readBackupData(file.path, backupDirPath);
177
+ if (data._meta && data._meta.parent) {
178
+ queue.push(data._meta.parent);
179
+ }
180
+ } catch {}
181
+ }
182
+
183
+ return protectedNames;
184
+ }
185
+
129
186
  const configManager = require('../utils/config-manager');
130
187
  const { logger } = require('../utils/logger');
131
188
  const { colors } = require('../utils/logger');
@@ -210,7 +267,15 @@ async function cleanupOldBackups(backupDirPath) {
210
267
  .sort((a, b) => b.time - a.time);
211
268
 
212
269
  const toDelete = backupFiles.slice(maxBackups);
213
- for (const file of toDelete) {
270
+ const kept = backupFiles.slice(0, maxBackups);
271
+
272
+ const protectedChainNames = collectProtectedChainNames(backupDirPath, kept);
273
+
274
+ for (const file of toDelete) {
275
+ if (protectedChainNames.has(file.name)) {
276
+ logger.info(` Keeping ${file.name} (parent of a kept incremental backup)`);
277
+ continue;
278
+ }
214
279
  try {
215
280
  await fsp.unlink(file.path);
216
281
  } catch (err) {
@@ -230,7 +295,7 @@ async function handleCreate(args) {
230
295
  const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
231
296
  const backupName = `backup-${timestamp}.json`;
232
297
  const backupPath = path.join(outputDir, backupName);
233
- const isIncremental = !!args.incremental;
298
+ const isIncremental = args.incremental !== 'false' && args.incremental !== false;
234
299
 
235
300
  logger.debug(`Source directory: ${dir}`);
236
301
  logger.debug(`Backup will be saved to: ${backupPath}`);
@@ -488,32 +553,11 @@ async function handleVerify(args) {
488
553
  if (data._meta && data._meta.hashes) {
489
554
  logger.info(' Performing hash chain verification...');
490
555
 
491
- const chain = [{ path: backupPath, data: data }];
492
- let current = data;
493
- let currentPath = backupPath;
494
- while (current._meta && current._meta.parent) {
495
- if (chain.length >= 11) {
496
- logger.warn(' Chain broken: maximum incremental depth of 10 exceeded');
497
- break;
498
- }
499
- const parentName = current._meta.parent;
500
- const parentDir = path.dirname(currentPath);
501
- const parentPath = path.join(parentDir, parentName);
502
- if (!SecurityUtils.safeExistsSync(parentPath, parentDir)) {
503
- logger.warn(` Chain broken: parent '${parentName}' not found`);
504
- break;
505
- }
506
- const parentRaw = SecurityUtils.safeReadFileSync(parentPath, parentDir, 'utf8');
507
- if (parentRaw === null) {
508
- logger.warn(` Chain broken: cannot read parent '${parentName}'`);
509
- break;
510
- }
511
- current = JSON.parse(parentRaw);
512
- currentPath = parentPath;
513
- chain.push({ path: currentPath, data: current });
514
- }
515
-
556
+ const chain = await buildRestoreChain(backupPath, data);
557
+
516
558
  let allValid = true;
559
+
560
+ // Rebuild full state oldest->newest and verify each manifest against it.
517
561
  const reconstructed = {};
518
562
  for (const entry of chain) {
519
563
  const entryMeta = entry.data._meta;
@@ -526,13 +570,13 @@ async function handleVerify(args) {
526
570
 
527
571
  if (!entryHashes) {
528
572
  logger.warn(` ${entryName}: no manifest hashes (legacy backup)`);
529
- continue;
530
- }
573
+ continue;
574
+ }
531
575
 
532
576
  let entryValid = true;
533
577
  for (const [file, expectedHash] of Object.entries(entryHashes)) {
534
578
  if (!Object.prototype.hasOwnProperty.call(reconstructed, file)) {
535
- logger.warn(` Missing file in manifest: ${file}`);
579
+ logger.warn(` Missing file in reconstructed backup state: ${file}`);
536
580
  entryValid = false;
537
581
  continue;
538
582
  }
@@ -542,19 +586,20 @@ async function handleVerify(args) {
542
586
  entryValid = false;
543
587
  }
544
588
  }
589
+
590
+ if (entryValid) {
591
+ logger.success(` ${entryName}: ${Object.keys(entryHashes).length} file(s) verified`);
592
+ } else {
593
+ allValid = false;
594
+ }
595
+ }
545
596
 
546
- if (entryValid) {
547
- logger.success(` ${entryName}: ${Object.keys(entryHashes).length} file(s) verified`);
548
- } else {
549
- allValid = false;
550
- }
551
- }
552
-
553
- if (allValid) {
554
- logger.success('\nBackup chain verification passed');
555
- } else {
556
- logger.error('\nBackup chain verification FAILED');
557
- }
597
+ if (allValid) {
598
+ logger.success('\nBackup chain verification passed');
599
+ } else {
600
+ logger.error('\nBackup chain verification FAILED');
601
+ process.exitCode = 1;
602
+ }
558
603
  } else {
559
604
  const fileCount = Object.keys(data).filter(k => k !== '_meta').length;
560
605
  logger.success('Backup is valid');
@@ -586,24 +631,33 @@ async function handleCleanup(args) {
586
631
 
587
632
  // Keep only the most recent 'keep' files
588
633
  const toDelete = backupFiles.slice(keep);
634
+ const kept = backupFiles.slice(0, keep);
589
635
 
590
636
  if (toDelete.length === 0) {
591
637
  logger.info('No old backups to delete.');
592
638
  return;
593
639
  }
594
640
 
595
- // Delete old backups
596
- for (const file of toDelete) {
597
- try {
641
+ const protectedChainNames = collectProtectedChainNames(backupDir, kept);
642
+ let deletedCount = 0;
643
+
644
+ // Delete old backups, skipping parents of kept backups
645
+ for (const file of toDelete) {
646
+ if (protectedChainNames.has(file.name)) {
647
+ logger.info(` Keeping ${file.name} (parent of a kept incremental backup)`);
648
+ continue;
649
+ }
650
+ try {
598
651
  await fsp.unlink(file.path);
599
- logger.info(` - Deleted: ${file.name}`);
600
- } catch (err) {
601
- logger.error(` - Failed to delete ${file.name}: ${err.message}`);
602
- }
603
- }
604
-
605
- logger.info(`\nRemoved ${toDelete.length} old backups`);
606
- logger.info(`Total backups kept: ${keep}`);
652
+ logger.info(` - Deleted: ${file.name}`);
653
+ deletedCount++;
654
+ } catch (err) {
655
+ logger.error(` - Failed to delete ${file.name}: ${err.message}`);
656
+ }
657
+ }
658
+
659
+ logger.info(`\nRemoved ${deletedCount} old backups`);
660
+ logger.info(`Total backups kept: ${keep}`);
607
661
 
608
662
  } catch (error) {
609
663
  logger.error('Error cleaning up backups:');
@@ -423,12 +423,11 @@ class I18nTextScanner {
423
423
  for (const word of words) {
424
424
  if (profile.stopwords.includes(word)) return true;
425
425
  }
426
- const validChars = trimmed.match(/[\p{L}\p{N}\s\-,.!?':"()\[\]{}]/gu) || [];
427
- const validRatio = validChars.length / trimmed.length;
428
- return validRatio >= 0.5;
429
426
  }
430
427
 
431
- return true;
428
+ const validChars = trimmed.match(/[\p{L}\p{N}\s\-,.!?':"()\[\]{}]/gu) || [];
429
+ const validRatio = validChars.length / trimmed.length;
430
+ return validRatio >= 0.5;
432
431
  }
433
432
 
434
433
  scanFile(filePath, patterns, minLength, maxLength) {
@@ -436,7 +435,7 @@ class I18nTextScanner {
436
435
  const content = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
437
436
  const lines = content.split('\n');
438
437
  const results = [];
439
- const sourceLang = this.config.sourceLanguage || 'en';
438
+ const sourceLang = this.sourceLanguage || 'en';
440
439
 
441
440
  patterns.forEach(pattern => {
442
441
  let match;
@@ -475,7 +474,7 @@ class I18nTextScanner {
475
474
  }
476
475
 
477
476
  generateSuggestion(text) {
478
- const sourceLang = this.config.sourceLanguage || 'en';
477
+ const sourceLang = this.sourceLanguage || 'en';
479
478
  const transliterations = {
480
479
  ja: { 'あ': 'a', 'い': 'i', 'う': 'u', 'え': 'e', 'お': 'o', 'か': 'ka', 'き': 'ki', 'く': 'ku', 'け': 'ke', 'こ': 'ko', 'さ': 'sa', 'し': 'shi', 'す': 'su', 'せ': 'se', 'そ': 'so', 'た': 'ta', 'ち': 'chi', 'つ': 'tsu', 'て': 'te', 'と': 'to', 'な': 'na', 'に': 'ni', 'ぬ': 'nu', 'ね': 'ne', 'の': 'no', 'は': 'ha', 'ひ': 'hi', 'ふ': 'fu', 'へ': 'he', 'ほ': 'ho', 'ま': 'ma', 'み': 'mi', 'む': 'mu', 'め': 'me', 'も': 'mo', 'や': 'ya', 'ゆ': 'yu', 'よ': 'yo', 'ら': 'ra', 'り': 'ri', 'る': 'ru', 'れ': 're', 'ろ': 'ro', 'わ': 'wa', 'を': 'wo', 'ん': 'n' },
481
480
  ru: { 'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo', 'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm', 'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u', 'ф': 'f', 'х': 'kh', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch', 'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya' },
@@ -533,6 +532,9 @@ class I18nTextScanner {
533
532
  gettext: `import gettext\ngettext.gettext('${text}')`,
534
533
  underscore: `from gettext import gettext as _\n_('${text}')`,
535
534
  lazy: `from gettext import gettext_lazy as _\n_('${text}')`
535
+ },
536
+ vanilla: {
537
+ generic: `t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')`
536
538
  }
537
539
  };
538
540
 
@@ -696,7 +698,7 @@ class I18nTextScanner {
696
698
  this.sourceDir = this.config.sourceDir || './src';
697
699
 
698
700
  // Source language for multi-language detection
699
- this.sourceLanguage = args['source-language'] || this.config.sourceLanguage || 'en';
701
+ this.sourceLanguage = args.sourceLanguage || this.config.sourceLanguage || 'en';
700
702
 
701
703
  // Resolve framework with precedence: CLI arg > config.framework.preference|string > auto-detect > fallback
702
704
  const cliFramework = args.framework;
@@ -41,15 +41,17 @@ const { logger } = require('../utils/logger');
41
41
  const { getGlobalReadline, closeGlobalReadline } = require('../utils/cli');
42
42
  const SetupEnforcer = require('../utils/setup-enforcer');
43
43
 
44
- // Ensure setup is complete before running
45
- (async () => {
46
- try {
47
- await SetupEnforcer.checkSetupCompleteAsync();
48
- } catch (error) {
49
- console.error('Setup check failed:', error.message);
50
- process.exit(1);
51
- }
52
- })();
44
+ // Ensure setup is complete before running (only when executed directly)
45
+ if (require.main === module) {
46
+ (async () => {
47
+ try {
48
+ await SetupEnforcer.checkSetupCompleteAsync();
49
+ } catch (error) {
50
+ console.error('Setup check failed:', error.message);
51
+ process.exit(1);
52
+ }
53
+ })();
54
+ }
53
55
 
54
56
  loadTranslations();
55
57
 
@@ -1100,45 +1102,7 @@ Generated: ${new Date().toISOString()}
1100
1102
  }
1101
1103
  }
1102
1104
 
1103
- // Main analysis method
1104
- async analyze() {
1105
- const startTime = Date.now();
1106
-
1107
- try {
1108
- logger.info(t("sizing.starting_i18n_sizing_analysis"));
1109
- logger.info(t("sizing.source_directory", { sourceDir: this.sourceDir }));
1110
-
1111
- const files = this.getLanguageFiles();
1112
-
1113
- if (files.length === 0) {
1114
- logger.warn(t("sizing.no_translation_files_found"));
1115
- return;
1116
- }
1117
-
1118
- logger.info(t("sizing.found_languages", { languages: files.map(f => f.language).join(', ') }));
1119
-
1120
- this.analyzeFileSizes(files);
1121
- this.analyzeTranslationContent(files);
1122
- this.generateSizeComparison();
1123
-
1124
- if (this.format === 'table') {
1125
- this.displayFolderResults();
1126
- } else if (this.format === 'json') {
1127
- logger.info(t("sizing.analysisStats", { stats: JSON.stringify(this.stats, null, 2) }));
1128
- }
1129
-
1130
- await this.generateHumanReadableReport();
1131
-
1132
- const endTime = Date.now();
1133
- logger.info(t("sizing.analysis_completed", { duration: ((endTime - startTime) / 1000).toFixed(2) }));
1134
-
1135
- } catch (error) {
1136
- logger.error(t("sizing.analysis_failed", { errorMessage: error.message }));
1137
- process.exit(1);
1138
- }
1139
- }
1140
-
1141
- // Parse command line arguments without yargs
1105
+ // Parse command line arguments without yargs
1142
1106
  parseArgs() {
1143
1107
  const args = process.argv.slice(2);
1144
1108
  const options = {
@@ -1335,7 +1299,7 @@ Options:
1335
1299
 
1336
1300
  const cliHelper = require('../utils/cli-helper');
1337
1301
  const pin = await cliHelper.promptPin(t('adminCli.enterPin'));
1338
- const isValid = await this.adminAuth.verifyPin(pin);
1302
+ const isValid = await adminAuth.verifyPin(pin);
1339
1303
 
1340
1304
  if (!isValid) {
1341
1305
  console.log(t('adminCli.invalidPin'));
@@ -1377,7 +1341,11 @@ Options:
1377
1341
  this.generateSizeComparison();
1378
1342
 
1379
1343
  // Display results
1380
- this.displayFolderResults();
1344
+ if (this.format === 'table') {
1345
+ this.displayFolderResults();
1346
+ } else if (this.format === 'json') {
1347
+ logger.info(t("sizing.analysisStats", { stats: JSON.stringify(this.stats, null, 2) }));
1348
+ }
1381
1349
 
1382
1350
  // Generate reports if requested
1383
1351
  await this.generateHumanReadableReport();
@@ -61,6 +61,10 @@ async function getConfig() {
61
61
  return await getUnifiedConfig('usage');
62
62
  }
63
63
 
64
+ function toBool(v) {
65
+ return v === true || v === 'true' || v === '1';
66
+ }
67
+
64
68
  class I18nUsageAnalyzer {
65
69
  constructor(config = {}) {
66
70
  this.config = config;
@@ -88,6 +92,7 @@ class I18nUsageAnalyzer {
88
92
  this.deadKeys = new Map();
89
93
  this.cleanupMode = false;
90
94
  this.dryRunDelete = false;
95
+ this._sourceCommentsSet = null;
91
96
 
92
97
  // Use global translation function
93
98
  this.rl = null;
@@ -383,10 +388,10 @@ class I18nUsageAnalyzer {
383
388
  console.log('🔍 Debug mode enabled');
384
389
  }
385
390
 
386
- if (args.cleanup) {
391
+ if (toBool(args.cleanup)) {
387
392
  this.cleanupMode = true;
388
393
  }
389
- if (args.dryRunDelete) {
394
+ if (toBool(args.dryRunDelete)) {
390
395
  this.dryRunDelete = true;
391
396
  }
392
397
 
@@ -533,9 +538,6 @@ class I18nUsageAnalyzer {
533
538
  // Load available translation keys first
534
539
  await this.loadAvailableKeys();
535
540
 
536
- // NEW: Detect framework patterns before analysis
537
- await this.detectFrameworkPatterns();
538
-
539
541
  // Perform usage analysis with enhanced features
540
542
  await this.analyzeUsage();
541
543
 
@@ -614,6 +616,7 @@ class I18nUsageAnalyzer {
614
616
  }
615
617
 
616
618
  if (this.cleanupMode) {
619
+ this._buildSourceCommentsSet();
617
620
  const deadKeys = this.findDeadKeys();
618
621
  console.log('\n' + t('usage.deadKeysDetectionTitle'));
619
622
  console.log(t('usage.deadKeysCount', { count: deadKeys.length }));
@@ -1242,11 +1245,13 @@ Analysis Features (v1.10.1):
1242
1245
  return false;
1243
1246
  }
1244
1247
 
1245
- _keyInSourceComments(key) {
1248
+ _buildSourceCommentsSet() {
1249
+ if (this._sourceCommentsSet !== null) return;
1250
+ this._sourceCommentsSet = new Set();
1251
+
1246
1252
  const commentPatterns = [
1247
1253
  /\/\/[^\n]*/g,
1248
- /\/\*[\s\S]*?\*\//g,
1249
- /\/\*\*[\s\S]*?\*\//g
1254
+ /\/\*[\s\S]*?\*\//g
1250
1255
  ];
1251
1256
 
1252
1257
  try {
@@ -1262,13 +1267,22 @@ Analysis Features (v1.10.1):
1262
1267
  const comments = content.match(pattern);
1263
1268
  if (comments) {
1264
1269
  for (const comment of comments) {
1265
- if (comment.includes(key)) {
1266
- return true;
1267
- }
1270
+ this._sourceCommentsSet.add(comment);
1268
1271
  }
1269
1272
  }
1270
1273
  }
1271
1274
  }
1275
+ } catch (e) {
1276
+ this._sourceCommentsSet = new Set();
1277
+ }
1278
+ }
1279
+
1280
+ _keyInSourceComments(key) {
1281
+ try {
1282
+ if (!this._sourceCommentsSet || this._sourceCommentsSet.size === 0) return false;
1283
+ for (const comment of this._sourceCommentsSet) {
1284
+ if (comment.includes(key)) return true;
1285
+ }
1272
1286
  } catch (e) {
1273
1287
  // Silently fail - comment detection is best-effort
1274
1288
  }
@@ -1936,9 +1950,6 @@ Analysis Features (v1.10.1):
1936
1950
  // Close readline interface to prevent hanging
1937
1951
  this.closeReadline();
1938
1952
 
1939
- // Return instead of force exit to allow proper cleanup
1940
- return;
1941
-
1942
1953
  return {
1943
1954
  success: true,
1944
1955
  stats: {
@@ -2013,40 +2024,4 @@ if (require.main === module) {
2013
2024
  }
2014
2025
  }
2015
2026
 
2016
- module.exports = I18nUsageAnalyzer;
2017
-
2018
- // Run if called directly
2019
- if (require.main === module) {
2020
- async function main() {
2021
- try {
2022
- const cliArgs = parseCommonArgs(process.argv.slice(2));
2023
-
2024
- if (cliArgs.help) {
2025
- displayHelp('usage');
2026
- process.exit(0);
2027
- }
2028
-
2029
- // Let run() handle full initialization to avoid duplicate setup output
2030
- const analyzer = new I18nUsageAnalyzer();
2031
- await analyzer.run();
2032
- } catch (error) {
2033
- console.error('Error:', error.message);
2034
- process.exit(1);
2035
- }
2036
- }
2037
-
2038
- // Check if we're being called from the menu system (stdin has data)
2039
- const hasStdinData = !process.stdin.isTTY;
2040
-
2041
- if (hasStdinData) {
2042
- // When called from menu, consume stdin data and run with defaults
2043
- process.stdin.resume();
2044
- process.stdin.on('data', () => {});
2045
- process.stdin.on('end', () => {
2046
- main();
2047
- });
2048
- } else {
2049
- // Normal direct execution
2050
- main();
2051
- }
2052
- }
2027
+ module.exports = I18nUsageAnalyzer;
@@ -164,12 +164,13 @@ class I18nValidator {
164
164
  const args = process.argv.slice(2);
165
165
  args.forEach(arg => {
166
166
  const sanitizedArg = SecurityUtils.sanitizeInput(arg);
167
- if (sanitizedArg.startsWith('--') && !sanitizedArg.includes('=')) {
167
+ if (sanitizedArg.startsWith('--enforce-key-style')) {
168
+ const val = arg.split('=')[1];
169
+ baseArgs.enforceKeyStyle = val === undefined ? true : val !== 'false';
170
+ } else if (sanitizedArg.startsWith('--') && !sanitizedArg.includes('=')) {
168
171
  const key = sanitizedArg.substring(2);
169
172
  if (['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(key)) {
170
173
  baseArgs.uiLanguage = key;
171
- } else if (key === 'enforce-key-style') {
172
- baseArgs.enforceKeyStyle = true;
173
174
  }
174
175
  }
175
176
  });
@@ -242,7 +243,7 @@ class I18nValidator {
242
243
  const files = items
243
244
  .filter(item => {
244
245
  return item.isFile() && item.name.endsWith('.json') &&
245
- !this.config.excludeFiles.includes(item.name);
246
+ (!Array.isArray(this.config.excludeFiles) || !this.config.excludeFiles.includes(item.name));
246
247
  }).map(item => item.name);
247
248
 
248
249
  return files;
@@ -691,7 +692,8 @@ class I18nValidator {
691
692
  const violations = [];
692
693
  for (const key of allKeys) {
693
694
  const sanitizedKey = SecurityUtils.sanitizeInput(key);
694
- if (!regex.test(sanitizedKey)) {
695
+ const testKey = keyStyle === 'flat' ? sanitizedKey.split('.').pop() : sanitizedKey;
696
+ if (!regex.test(testKey)) {
695
697
  violations.push({
696
698
  key: sanitizedKey,
697
699
  suggestedFix: this.suggestKeyFix(sanitizedKey, keyStyle),
@@ -729,7 +731,7 @@ class I18nValidator {
729
731
  case 'kebab-case':
730
732
  return segments.map(s => s.toLowerCase()).join('-');
731
733
  case 'flat':
732
- return segments.map((s, i) => i === 0 ? s.toLowerCase() : s.charAt(0).toUpperCase() + s.slice(1).toLowerCase()).join('');
734
+ return segments.map(s => s.toLowerCase()).join('');
733
735
  default:
734
736
  return sanitizedKey;
735
737
  }
@@ -1054,6 +1056,7 @@ class I18nValidator {
1054
1056
 
1055
1057
  const args = this.parseArgs();
1056
1058
 
1059
+ try {
1057
1060
  // Ensure config is always initialized
1058
1061
  if (!this.config) {
1059
1062
  this.config = {};
@@ -1071,6 +1074,7 @@ class I18nValidator {
1071
1074
  } else {
1072
1075
  await this.initialize();
1073
1076
  }
1077
+ this.config.enforceKeyStyle = args.enforceKeyStyle !== undefined ? args.enforceKeyStyle : this.config.enforceKeyStyle;
1074
1078
 
1075
1079
  // Skip admin authentication when called from menu
1076
1080
  if (!fromMenu) {
@@ -1146,6 +1150,7 @@ class I18nValidator {
1146
1150
  );
1147
1151
  throw error;
1148
1152
  }
1153
+ }
1149
1154
  }
1150
1155
 
1151
1156
 
package/package.json CHANGED
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "name": "i18ntk",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
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": [
7
7
  "i18ntk",
8
8
  "i18n",
9
+ "i18n toolkit",
10
+ "i18n tool kit",
9
11
  "internationalization",
10
12
  "localization",
11
13
  "translation",
@@ -161,5 +163,47 @@
161
163
  "access": "public"
162
164
  },
163
165
  "preferGlobal": true,
164
- "readme": "# i18ntk v4.0.0\n\nA i18n toolkit - A 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.0.0)](https://socket.dev/npm/package/i18ntk/overview/4.0.0)\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\nRequirements:\n\n- Node.js `>=16.0.0`\n- npm `>=8.0.0`\n- No runtime dependencies\n\n## What's New in 4.0.0\n\n- **SIZING**: `--predict-expansion` flag computes per-key expansion ratios across languages with Safe/Warning/Critical risk tiers for UI layout planning.\n- **WATCH**: `watchLocales()` now returns an EventEmitter-compatible watcher with debounced `change`/`add`/`unlink`/`error` events and SHA-256 hash tracking.\n- **USAGE**: `--cleanup` and `--dry-run-delete` flags identify dead translation keys with confidence scores.\n- **VALIDATOR**: `--enforce-key-style` enforces dot.notation, snake_case, camelCase, kebab-case, or flat naming conventions.\n- **SCANNER**: `--source-language` supports multi-language hardcoded text detection with 12+ language profiles.\n- **BACKUP**: `--incremental` flag creates differential backups with SHA-256 hashing and chained restores.\n- **RUNTIME**: `lazy: true` option defers locale file loading until first key access for lower memory usage.\n- **PROTECTION**: Context-aware rules (`after:word`, `before:word`, `standalone`, `surrounded:left,right`) for precise term masking.\n- **FIX**: `initRuntime()` now returns independent instances with isolated language and cache state.\n\nSee [CHANGELOG.md](./CHANGELOG.md) for more release details.\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\ni18ntk --command=debug\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`n\nNote: manager route `i18ntk --command=backup` is disabled in current builds. Use `i18ntk-backup` (or legacy `i18ntk-backup`) directly for backup operations.\n\n## Common Options\n\nMost 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- `--dry-run`\n- `--help`\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 (Beta)\"\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\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\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\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 Beta` to edit defaults for placeholder 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\nIn 3.1.2, warning types are more 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- Dynamic key patterns (e.g., `` t(`prefix.${dynamic}`) ``) — lower score\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\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\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.0.0\",\n \"sourceDir\": \"./locales\",\n \"i18nDir\": \"./locales\",\n \"outputDir\": \"./i18ntk-reports\",\n \"sourceLanguage\": \"en\",\n \"defaultLanguages\": [\"de\", \"es\", \"fr\", \"ru\"],\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"],\n \"autoTranslate\": {\n \"placeholderMode\": \"preserve\",\n \"concurrency\": 6,\n \"batchSize\": 100,\n \"progressInterval\": 25,\n \"retryCount\": 3,\n \"retryDelay\": 1000,\n \"timeout\": 15000,\n \"dryRunFirst\": 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. The publish staging script excludes development-only content such as tests, scripts, docs, release staging folders, local config files, and generated protection files.\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 v3.2.0](./docs/migration-guide-v3.2.0.md)\n- [Migration Guide v3.1.1](./docs/migration-guide-v3.1.1.md)\n- [Migration Guide v3.0.0](./docs/migration-guide-v3.0.0.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## License\n\nMIT. See [LICENSE](./LICENSE).\n"
166
+ "versionInfo": {
167
+ "version": "4.1.0",
168
+ "releaseDate": "21/05/2026",
169
+ "lastUpdated": "21/05/2026",
170
+ "maintainer": "Vlad Noskov",
171
+ "changelog": "./CHANGELOG.md",
172
+ "documentation": "./README.md",
173
+ "apiReference": "./docs/api/API_REFERENCE.md",
174
+ "majorChanges": [
175
+ "FIX: Runtime lazy loading now tolerates stale manifest entries, refreshes key manifests correctly, and guards null base directories.",
176
+ "FIX: Incremental backup verification, cleanup, boolean flag parsing, and circular parent detection are hardened.",
177
+ "FIX: Scanner, sizing, usage, validator, watcher, and protection edge cases from the 4.0.0 feature set are corrected.",
178
+ "FIX: Manager option 7 status output, Delete Reports cache cleanup, and public package version metadata are corrected."
179
+ ],
180
+ "breakingChanges": [
181
+ "i18ntk/runtime module-level helpers keep the first initialized runtime configuration for compatibility instead of being overwritten by later initRuntime() calls.",
182
+ "utils/watch-locales.js now returns a callable watcher object with EventEmitter methods and a stop() method; existing bare stop-function usage remains supported."
183
+ ],
184
+ "nextVersion": "4.1.1",
185
+ "supportedNodeVersions": ">=16.0.0",
186
+ "supportedFrameworks": {
187
+ "react-i18next": ">=11.0.0",
188
+ "vue-i18n": ">=9.0.0",
189
+ "angular-i18n": ">=12.0.0",
190
+ "next-i18next": ">=13.0.0",
191
+ "nuxt-i18n": ">=8.0.0",
192
+ "svelte-i18n": ">=3.0.0",
193
+ "sveltekit-i18n": ">=2.0.0",
194
+ "react-native-localize": ">=2.0.0",
195
+ "expo-localization": ">=14.0.0",
196
+ "ionic-angular": ">=6.0.0",
197
+ "ember-intl": ">=5.0.0",
198
+ "formatjs": ">=2.0.0",
199
+ "i18next": ">=21.0.0",
200
+ "django": ">=3.0.0",
201
+ "flask-babel": ">=2.0.0",
202
+ "fastapi": ">=0.70.0",
203
+ "spring-boot": ">=2.5.0",
204
+ "laravel": ">=8.0.0"
205
+ },
206
+ "supportPolicy": "Versions earlier than 4.1.0 may be unstable or insecure. Upgrade to 4.1.0 or newer."
207
+ },
208
+ "readme": "# i18ntk v4.1.0\n\nA i18n toolkit - A 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.1.0)](https://socket.dev/npm/package/i18ntk/overview/4.1.0)\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\nRequirements:\n\n- Node.js `>=16.0.0`\n- npm `>=8.0.0`\n- No runtime dependencies\n\n## What's New in 4.1.0\n\n- **FIX**: Critical and high-impact bugs resolved across the v4.0.0 feature set — runtime staleness crashes, backup hash-chain verification, sizing adminAuth crash, scanner `--source-language` propagation, watch callback subscriptions, dead key detection performance, validator key style enforcement, and protection Unicode boundary handling. See [CHANGELOG.md](./CHANGELOG.md) for complete details.\n\n## What's New in 4.0.0\n\n- **SIZING**: `--predict-expansion` flag computes per-key expansion ratios across languages with Safe/Warning/Critical risk tiers for UI layout planning.\n- **WATCH**: `watchLocales()` now returns an EventEmitter-compatible watcher with debounced `change`/`add`/`unlink`/`error` events and SHA-256 hash tracking.\n- **USAGE**: `--cleanup` and `--dry-run-delete` flags identify dead translation keys with confidence scores.\n- **VALIDATOR**: `--enforce-key-style` enforces dot.notation, snake_case, camelCase, kebab-case, or flat naming conventions.\n- **SCANNER**: `--source-language` supports multi-language hardcoded text detection with 12+ language profiles.\n- **BACKUP**: `--incremental` flag creates differential backups with SHA-256 hashing and chained restores.\n- **RUNTIME**: `lazy: true` option defers locale file loading until first key access for lower memory usage.\n- **PROTECTION**: Context-aware rules (`after:word`, `before:word`, `standalone`, `surrounded:left,right`) for precise term masking.\n\nSee [CHANGELOG.md](./CHANGELOG.md) for more release details.\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\ni18ntk --command=debug\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`n\nNote: manager route `i18ntk --command=backup` is disabled in current builds. Use `i18ntk-backup` (or legacy `i18ntk-backup`) directly for backup operations.\n\n## Common Options\n\nMost 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- `--dry-run`\n- `--help`\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 (Beta)\"\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\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\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\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 Beta` to edit defaults for placeholder 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\nIn 3.1.2, warning types are more 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- Dynamic key patterns (e.g., `` t(`prefix.${dynamic}`) ``) — lower score\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\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\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.1.0\",\n \"sourceDir\": \"./locales\",\n \"i18nDir\": \"./locales\",\n \"outputDir\": \"./i18ntk-reports\",\n \"sourceLanguage\": \"en\",\n \"defaultLanguages\": [\"de\", \"es\", \"fr\", \"ru\"],\n \"englishContentThresholdPercent\": 10,\n \"allowedEnglishTerms\": [\"BrandName\", \"PRODUCT_CODE\"],\n \"autoTranslate\": {\n \"placeholderMode\": \"preserve\",\n \"concurrency\": 6,\n \"batchSize\": 100,\n \"progressInterval\": 25,\n \"retryCount\": 3,\n \"retryDelay\": 1000,\n \"timeout\": 15000,\n \"dryRunFirst\": 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. The publish staging script excludes development-only content such as tests, scripts, docs, release staging folders, local config files, and generated protection files.\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 v3.2.0](./docs/migration-guide-v3.2.0.md)\n- [Migration Guide v3.1.1](./docs/migration-guide-v3.1.1.md)\n- [Migration Guide v3.0.0](./docs/migration-guide-v3.0.0.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## License\n\nMIT. See [LICENSE](./LICENSE).\n"
165
209
  }
package/runtime/index.js CHANGED
@@ -139,7 +139,8 @@ function listJsonFilesRecursively(dir, baseDir = dir) {
139
139
  return results;
140
140
  }
141
141
 
142
- function loadKeyManifestFromDir(baseDir) {
142
+ function loadKeyManifestFromDir(baseDir) {
143
+ if (!baseDir || typeof baseDir !== 'string') return new Map();
143
144
  const validatedBase = SecurityUtils.validatePath(baseDir, path.dirname(baseDir));
144
145
  const baseStat = SecurityUtils.safeStatSync(validatedBase, path.dirname(validatedBase));
145
146
  const baseRoot = baseStat && baseStat.isFile() ? path.dirname(validatedBase) : validatedBase;
@@ -288,11 +289,15 @@ function resolveKey(obj, key, sep = '.', runtimeState = null, lang = null) {
288
289
  const prefix = parts.slice(0, i).join(sep);
289
290
  const filePath = manifest.get(prefix);
290
291
  const loadedFileKey = `${lang}\0${filePath}`;
291
- if (filePath && !runtimeState.loadedFiles.has(loadedFileKey)) {
292
- loadFileLazy(runtimeState, filePath, lang);
293
- runtimeState.loadedFiles.add(loadedFileKey);
294
- const langData = runtimeState.cache.get(lang);
295
- return resolveKey(langData, key, sep, runtimeState, lang);
292
+ if (filePath && !runtimeState.loadedFiles.has(loadedFileKey)) {
293
+ runtimeState.loadedFiles.add(loadedFileKey);
294
+ try {
295
+ loadFileLazy(runtimeState, filePath, lang);
296
+ } catch (_) {
297
+ // stale manifest entry — already marked as loaded to prevent retry
298
+ }
299
+ const langData = runtimeState.cache.get(lang);
300
+ return resolveKey(langData, key, sep, runtimeState, lang);
296
301
  }
297
302
  }
298
303
  if (!runtimeState.eagerLoadedLanguages.has(lang)) {
@@ -433,7 +438,8 @@ function refresh(lang) {
433
438
 
434
439
  function refreshForState(runtimeState, lang = runtimeState.language) {
435
440
  if (runtimeState.cache.has(lang)) runtimeState.cache.delete(lang);
436
- if (runtimeState.eagerLoadedLanguages) runtimeState.eagerLoadedLanguages.delete(lang);
441
+ if (runtimeState.eagerLoadedLanguages) runtimeState.eagerLoadedLanguages.delete(lang);
442
+ if (runtimeState.keyManifest) runtimeState.keyManifest.delete(lang);
437
443
  if (runtimeState.loadedFiles) {
438
444
  for (const fileKey of Array.from(runtimeState.loadedFiles)) {
439
445
  if (fileKey.startsWith(`${lang}\0`)) runtimeState.loadedFiles.delete(fileKey);
@@ -75,10 +75,15 @@ function parseContextString(context) {
75
75
  return { mode: 'before', words };
76
76
  }
77
77
 
78
- const surroundedMatch = trimmed.match(/^surrounded:(.+),(.+)$/i);
79
- if (surroundedMatch) {
80
- const leftWords = surroundedMatch[1].split('|').map(w => w.trim()).filter(Boolean);
81
- const rightWords = surroundedMatch[2].split('|').map(w => w.trim()).filter(Boolean);
78
+ const prefix = trimmed.substring(0, 'surrounded:'.length);
79
+ if (prefix.toLowerCase() === 'surrounded:') {
80
+ const rest = trimmed.substring('surrounded:'.length);
81
+ const idx = rest.indexOf(',');
82
+ if (idx === -1) return null;
83
+ const left = rest.slice(0, idx).trim();
84
+ const right = rest.slice(idx + 1).trim();
85
+ const leftWords = left.split('|').map(w => w.trim()).filter(Boolean);
86
+ const rightWords = right.split('|').map(w => w.trim()).filter(Boolean);
82
87
  if (leftWords.length === 0 || rightWords.length === 0) return null;
83
88
  return { mode: 'surrounded', left: leftWords, right: rightWords };
84
89
  }
@@ -243,7 +248,7 @@ function shouldPreserveWholeValue(keyPath, value, protection) {
243
248
  if (protection.keys.some(rule => keyMatchesRule(keyPath, rule))) return true;
244
249
  const valueText = String(value);
245
250
  return protection.values.includes(valueText) ||
246
- (protection.normalizedTerms || []).some(rule => rule.value === valueText);
251
+ (protection.normalizedTerms || []).some(rule => rule.type === 'global' && rule.value === valueText);
247
252
  }
248
253
 
249
254
  function addReplacement(replacements, original) {
@@ -261,27 +266,27 @@ function shouldProtectInContext(value, rule, index, fullText) {
261
266
 
262
267
  if (context.mode === 'after') {
263
268
  const alternation = context.words.map(escapeRegExp).join('|');
264
- const regex = new RegExp(`\\b(${alternation})\\s+$`, 'i');
269
+ const regex = new RegExp(`(^|[\\s\\p{P}])(${alternation})\\s+$`, 'iu');
265
270
  return regex.test(before);
266
271
  }
267
272
 
268
273
  if (context.mode === 'before') {
269
274
  const alternation = context.words.map(escapeRegExp).join('|');
270
- const regex = new RegExp(`^\\s+(${alternation})\\b`, 'i');
275
+ const regex = new RegExp(`^\\s+(${alternation})([\\s\\p{P}]|$)`, 'iu');
271
276
  return regex.test(after);
272
277
  }
273
278
 
274
279
  if (context.mode === 'standalone') {
275
- const isBoundaryBefore = before === '' || /\s$/.test(before);
276
- const isBoundaryAfter = after === '' || /^[\s.,!?;:]/.test(after);
280
+ const isBoundaryBefore = before === '' || /(?:\s|\(|\[|\{|"|'|\-|–|—|。|、|」)$/.test(before);
281
+ const isBoundaryAfter = after === '' || /^[\s.,!?;:)\]}<>"'\-–—。、」]/.test(after);
277
282
  return isBoundaryBefore && isBoundaryAfter;
278
283
  }
279
284
 
280
285
  if (context.mode === 'surrounded') {
281
286
  const leftAlternation = context.left.map(escapeRegExp).join('|');
282
287
  const rightAlternation = context.right.map(escapeRegExp).join('|');
283
- const leftRegex = new RegExp(`\\b(${leftAlternation})\\s+$`, 'i');
284
- const rightRegex = new RegExp(`^\\s+(${rightAlternation})\\b`, 'i');
288
+ const leftRegex = new RegExp(`(^|[\\s\\p{P}])(${leftAlternation})\\s+$`, 'iu');
289
+ const rightRegex = new RegExp(`^\\s+(${rightAlternation})([\\s\\p{P}]|$)`, 'iu');
285
290
  return leftRegex.test(before) && rightRegex.test(after);
286
291
  }
287
292
 
@@ -361,7 +366,7 @@ function hasProtectionRules(protection) {
361
366
  protection &&
362
367
  (
363
368
  (protection.normalizedTerms && protection.normalizedTerms.length) ||
364
- protection.terms.length ||
369
+ (protection.terms && protection.terms.length) ||
365
370
  protection.keys.length ||
366
371
  protection.values.length ||
367
372
  protection.patterns.length
@@ -99,7 +99,7 @@ function watchDirectory(dir, emitter, watchers, options = {}) {
99
99
  return;
100
100
  }
101
101
 
102
- watchers.push({ watcher, path: dir });
102
+ watchers.push({ watcher, path: dir, debounceTimers });
103
103
  watchState.count++;
104
104
 
105
105
  try {
@@ -126,6 +126,11 @@ function watchLocales(dirs, onChange, options = {}) {
126
126
  if (typeof onChange === 'function') {
127
127
  emitter.on('change', onChange);
128
128
  emitter.on('add', onChange);
129
+ emitter.on('unlink', onChange);
130
+ } else if (typeof onChange === 'object' && onChange !== null && typeof onChange.onChange === 'function') {
131
+ emitter.on('change', onChange.onChange);
132
+ emitter.on('add', onChange.onChange);
133
+ emitter.on('unlink', onChange.onChange);
129
134
  }
130
135
 
131
136
  const {
@@ -160,6 +165,12 @@ function watchLocales(dirs, onChange, options = {}) {
160
165
 
161
166
  const stop = () => {
162
167
  for (const entry of watchers) {
168
+ if (entry.debounceTimers) {
169
+ for (const timer of entry.debounceTimers.values()) {
170
+ clearTimeout(timer);
171
+ }
172
+ entry.debounceTimers.clear();
173
+ }
163
174
  try { entry.watcher.close(); } catch (_) { /* ignore */ }
164
175
  }
165
176
  watchers.length = 0;