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 +72 -31
- package/README.md +7 -4
- package/SECURITY.md +6 -4
- package/main/i18ntk-backup.js +121 -67
- package/main/i18ntk-scanner.js +9 -7
- package/main/i18ntk-sizing.js +18 -50
- package/main/i18ntk-usage.js +26 -51
- package/main/i18ntk-validate.js +11 -6
- package/package.json +46 -2
- package/runtime/index.js +13 -7
- package/utils/translate/protection.js +17 -12
- package/utils/watch-locales.js +12 -1
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
|
-
|
|
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.
|
|
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
|
[](https://nodejs.org)
|
|
10
10
|
[](https://www.npmjs.com/package/i18ntk)
|
|
11
11
|
[](LICENSE)
|
|
12
|
-
[](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.
|
|
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.
|
|
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.
|
|
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.
|
package/main/i18ntk-backup.js
CHANGED
|
@@ -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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
492
|
-
|
|
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
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
logger.info(
|
|
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:');
|
package/main/i18ntk-scanner.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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;
|
package/main/i18ntk-sizing.js
CHANGED
|
@@ -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
|
-
(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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();
|
package/main/i18ntk-usage.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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;
|
package/main/i18ntk-validate.js
CHANGED
|
@@ -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('--')
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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\n\n[](https://www.npmjs.com/package/i18ntk)\n[](https://www.npmjs.com/package/i18ntk)\n[](https://nodejs.org)\n[](https://www.npmjs.com/package/i18ntk)\n[](LICENSE)\n[](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\n\n[](https://www.npmjs.com/package/i18ntk)\n[](https://www.npmjs.com/package/i18ntk)\n[](https://nodejs.org)\n[](https://www.npmjs.com/package/i18ntk)\n[](LICENSE)\n[](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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
79
|
-
if (
|
|
80
|
-
const
|
|
81
|
-
const
|
|
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(
|
|
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})\\
|
|
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 === '' ||
|
|
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(
|
|
284
|
-
const rightRegex = new RegExp(`^\\s+(${rightAlternation})\\
|
|
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
|
package/utils/watch-locales.js
CHANGED
|
@@ -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;
|