i18ntk 3.1.1 → 3.2.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 +64 -15
- package/README.md +14 -16
- package/main/manage/commands/TranslateCommand.js +52 -10
- package/main/manage/index.js +0 -2
- package/main/manage/managers/InteractiveMenu.js +4 -0
- package/main/manage/services/FileManagementService.js +14 -11
- package/package.json +6 -3
- package/runtime/enhanced.d.ts +1 -1
- package/runtime/i18ntk.d.ts +2 -11
- package/ui-locales/de.json +1 -1
- package/ui-locales/en.json +1 -1
- package/ui-locales/es.json +1 -1
- package/ui-locales/fr.json +1 -1
- package/ui-locales/ja.json +1 -1
- package/ui-locales/ru.json +1 -1
- package/ui-locales/zh.json +1 -1
- package/utils/admin-auth.js +6 -6
- package/utils/config-manager.js +13 -6
- package/utils/translate/api.js +17 -9
- package/utils/translate/traverse.js +14 -25
package/CHANGELOG.md
CHANGED
|
@@ -5,21 +5,70 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [3.
|
|
9
|
-
|
|
10
|
-
###
|
|
11
|
-
- **
|
|
12
|
-
- **
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
8
|
+
## [3.2.0] - 2026-05-16
|
|
9
|
+
|
|
10
|
+
### Security
|
|
11
|
+
- **CRITICAL**: Fixed invalid `crypto.createCipherGCM`/`createDecipherGCM` API calls in `admin-pin.js` — replaced with `crypto.createCipheriv`/`createDecipheriv`.
|
|
12
|
+
- **CRITICAL**: Fixed missing `SecurityUtils` imports in `admin-pin.js`, `security-config.js`, and `scripts/security-check.js` causing `ReferenceError` at runtime.
|
|
13
|
+
- **CRITICAL**: Removed encryption key stored alongside ciphertext in `admin-pin.js`. The AES key was stored in the same JSON file as the encrypted PIN, providing zero cryptographic protection. Encryption key is now derived via HKDF from the scrypt hash.
|
|
14
|
+
- Enforced HTTPS-only for Google Translate API requests in `utils/translate/api.js`; dropped `http` protocol support.
|
|
15
|
+
- Fixed `http.get` timeout for Node.js <16.14 compatibility by using `req.setTimeout()` instead of the options-based `{ timeout }` parameter.
|
|
16
|
+
- Added `SecurityUtils.validatePath` checks to `secure-backup.js` `restoreBackup` and `verifyBackup` methods.
|
|
17
|
+
- Added `backupDir` traversal validation in `secure-backup.js` constructor.
|
|
18
|
+
- Fixed `FileManagementService` `isAuthRequiredForScript`/`verifyPin` stubs — previously returned hardcoded `false`/`true`, disabling PIN protection. Now delegates to proper `AdminAuth` module.
|
|
19
|
+
- Fixed `admin-auth.js` `logSecurityEvent` signature mismatches — 6 calls passed raw strings instead of structured objects.
|
|
20
|
+
- Fixed `admin-pin.js` `getPinDisplay` to use stored `pinLength` instead of decrypting the raw PIN into memory.
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
- `admin-pin.js` lockout now uses timestamp-based expiry (`lockedUntil`) instead of `setTimeout`, ensuring lockout state survives process restarts.
|
|
24
|
+
- `translate/traverse.js` `setLeaf` now correctly creates `[]` for numeric array indices (was creating `{}`).
|
|
25
|
+
- `translate/traverse.js` extracted shared `parseKeyPath` function — `setLeaf` and `getLeaf` had duplicate path-parsing logic.
|
|
26
|
+
- `translate/traverse.js` `deepClone` now handles `null`, `undefined`, and circular references gracefully.
|
|
27
|
+
- `translate/api.js` retry logic now retries `TimeoutError` and `NetworkError` with exponential backoff (previously only retried rate-limit errors).
|
|
28
|
+
- `translate/api.js` added `User-Agent` header to Google Translate API requests.
|
|
29
|
+
- `main/manage/index.js` `startupTimeout` no longer cleared before `createPrompt` and other blocking initialization steps.
|
|
30
|
+
- `main/manage/index.js` removed silent no-op `t('init.autoDetectedI18nDirectory', ...)` whose return value was never used.
|
|
31
|
+
- `ultra-performance-optimizer.js` removed dead `preallocateMemory` pools (`stringPool`, `objectPool`, `arrayPool`) — ~1MB wasted allocation.
|
|
32
|
+
- `ultra-performance-optimizer.js` `getCacheKey` now uses async `fs.stat` instead of blocking `fs.statSync`.
|
|
33
|
+
- `ultra-performance-optimizer.js` GC timer now enforces minimum 5-second interval and warns when `--expose-gc` is missing.
|
|
34
|
+
- `ultra-performance-optimizer.js` `readFileUltra` now handles files >64KB with chunked reads.
|
|
35
|
+
- `ultra-performance-optimizer.js` `createUltraCache` replaced per-entry `setTimeout` (timer leak) with unified cleanup interval.
|
|
36
|
+
- `ultra-performance-optimizer.js` benchmark now uses real benchmark datasets instead of non-existent mock files.
|
|
37
|
+
- `config-manager.js` now exports `loadSettings`/`saveSettings` aliases — resolves 20+ phantom API fallback calls across the codebase.
|
|
38
|
+
- `config-manager.js` `updateConfig` now clones before deep-merging to prevent in-place cache corruption.
|
|
39
|
+
- `admin-pin.js` scrypt→pbkdf2 fallback now emits a console warning instead of failing silently.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
- Updated all documentation to v3.2.0: README, CHANGELOG, docs/README, getting-started, runtime, auto-translate, environment-variables, scanner-guide, API_REFERENCE, COMPONENTS, and CONFIGURATION.
|
|
43
|
+
- Updated `package.json` version, `versionInfo`, `majorChanges`, and `nextVersion` for v3.2.0.
|
|
44
|
+
- Socket badge URL updated to v3.2.0.
|
|
45
|
+
|
|
46
|
+
## [3.1.2] - 2026-05-07
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
- Auto Translate now resolves locale roots such as `./locales` to the selected source-language folder such as `./locales/en` when JSON files are stored under language folders.
|
|
50
|
+
- Public package staging now verifies root `package.json` and `package.public.json` release metadata are synchronized before pack or publish.
|
|
51
|
+
- Added a safe `publish:public:dry-run` path for validating the exact staged npm publish flow.
|
|
52
|
+
|
|
53
|
+
### Changed
|
|
54
|
+
- Updated release docs, npm README metadata, and package manifests for v3.1.2.
|
|
55
|
+
- Kept generated backups, temporary benchmark datasets, local setup state, and debug repair files out of future public repo commits through `.gitignore`.
|
|
56
|
+
|
|
57
|
+
## [3.1.1] - 2026-05-07
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
- **Auto Translate protection file workflow**: Added user-editable `i18ntk-auto-translate.json` support for protected terms, key paths, exact values, and regex patterns.
|
|
61
|
+
- **Public package README guard**: Public package staging now verifies `README.md` is included and non-empty before publish.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
- Updated README and release documentation for the current Auto Translate protection workflow and public package contents.
|
|
65
|
+
- Removed project-specific hardcoded validation examples so users configure their own brand and domain terms.
|
|
66
|
+
|
|
67
|
+
### Fixed
|
|
68
|
+
- Removed provider-shaped fake secret fixtures from tests to avoid GitHub push protection false positives.
|
|
69
|
+
- Ensured public package metadata includes `readmeFilename: "README.md"` so npm can render the package README.
|
|
70
|
+
|
|
71
|
+
## [3.1.0] - 2026-05-07
|
|
23
72
|
|
|
24
73
|
### Added
|
|
25
74
|
- **Placeholder-preserve translation mode**: Translates text segments around dynamic placeholders and reinserts the original tokens exactly.
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# i18ntk v3.
|
|
1
|
+
# i18ntk v3.2.0
|
|
2
2
|
|
|
3
3
|
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 @@ Zero-dependency internationalization toolkit for setup, scanning, analysis, vali
|
|
|
9
9
|
[](https://nodejs.org)
|
|
10
10
|
[](https://www.npmjs.com/package/i18ntk)
|
|
11
11
|
[](LICENSE)
|
|
12
|
-
[](https://socket.dev/npm/package/i18ntk/overview/3.2.0)
|
|
13
13
|
|
|
14
14
|
## Install
|
|
15
15
|
|
|
@@ -30,19 +30,14 @@ Requirements:
|
|
|
30
30
|
- npm `>=8.0.0`
|
|
31
31
|
- No runtime dependencies
|
|
32
32
|
|
|
33
|
-
## What's New in 3.
|
|
33
|
+
## What's New in 3.2.0
|
|
34
34
|
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
39
|
-
- Source-directory prompts are clearer and accept absolute paths or project-relative paths.
|
|
40
|
-
- Validation warnings now distinguish URLs, email addresses, secret-like values, and likely untranslated English content.
|
|
41
|
-
- Sizing reports now include per-language file counts, file-set mismatches, and per-file key/character statistics.
|
|
42
|
-
- Internal UI locale coverage is enforced against the English UI locale.
|
|
43
|
-
- Public package staging verifies `README.md` is present before publish.
|
|
35
|
+
- **SECURITY**: Fixed 4 critical runtime-crash bugs (invalid crypto APIs, missing imports) across admin-pin.js, security-config.js, and scripts/security-check.js.
|
|
36
|
+
- **SECURITY**: Removed encryption key stored alongside ciphertext in admin-pin.js; encryption key is now derived via HKDF.
|
|
37
|
+
- **SECURITY**: Enforced HTTPS-only for Google Translate API requests; fixed http.get timeout for Node.js <16.14 compatibility.
|
|
38
|
+
- **SECURITY**: Added path validation to backup restore/verify operations; locked down FileManagementService PIN verification stubs.
|
|
44
39
|
|
|
45
|
-
See [CHANGELOG.md](./CHANGELOG.md) and [docs/migration-guide-v3.
|
|
40
|
+
See [CHANGELOG.md](./CHANGELOG.md) and [docs/migration-guide-v3.2.0.md](./docs/migration-guide-v3.2.0.md) for release details.
|
|
46
41
|
|
|
47
42
|
## Quick Start
|
|
48
43
|
|
|
@@ -157,11 +152,13 @@ i18ntk-translate locales/en es --source-dir locales/en --files "*.json" --no-con
|
|
|
157
152
|
|
|
158
153
|
The manager flow asks for:
|
|
159
154
|
|
|
160
|
-
- source locale directory
|
|
155
|
+
- source locale directory, either the folder with JSON files or a locale root such as `./locales`
|
|
161
156
|
- source language code
|
|
162
157
|
- one or more target languages, or `all`
|
|
163
158
|
- one JSON file or all JSON files in the source directory
|
|
164
159
|
|
|
160
|
+
If 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.
|
|
161
|
+
|
|
165
162
|
Before writing files, the manager can run a dry-run preview. After confirmation it writes translated files under sibling target-language folders, for example:
|
|
166
163
|
|
|
167
164
|
```text
|
|
@@ -228,7 +225,7 @@ See [docs/auto-translate.md](./docs/auto-translate.md) for the full Auto Transla
|
|
|
228
225
|
|
|
229
226
|
Validation checks locale structure, completeness, placeholders, and content risks.
|
|
230
227
|
|
|
231
|
-
In 3.1.
|
|
228
|
+
In 3.1.2, warning types are more specific:
|
|
232
229
|
|
|
233
230
|
- `Potential risky content`: URL, email address, or secret-like value
|
|
234
231
|
- `Possible untranslated English content`: target-language value appears to contain too much English
|
|
@@ -292,7 +289,7 @@ Example:
|
|
|
292
289
|
|
|
293
290
|
```json
|
|
294
291
|
{
|
|
295
|
-
"version": "3.
|
|
292
|
+
"version": "3.2.0",
|
|
296
293
|
"sourceDir": "./locales",
|
|
297
294
|
"i18nDir": "./locales",
|
|
298
295
|
"outputDir": "./i18ntk-reports",
|
|
@@ -350,6 +347,7 @@ The public package manifest includes `readmeFilename: "README.md"`, and the rele
|
|
|
350
347
|
- [Auto Translate Guide](./docs/auto-translate.md)
|
|
351
348
|
- [Scanner Guide](./docs/scanner-guide.md)
|
|
352
349
|
- [Environment Variables](./docs/environment-variables.md)
|
|
350
|
+
- [Migration Guide v3.2.0](./docs/migration-guide-v3.2.0.md)
|
|
353
351
|
- [Migration Guide v3.1.1](./docs/migration-guide-v3.1.1.md)
|
|
354
352
|
- [Migration Guide v3.0.0](./docs/migration-guide-v3.0.0.md)
|
|
355
353
|
- [Migration Guide v2.6.0](./docs/migration-guide-v2.6.0.md)
|
|
@@ -71,7 +71,14 @@ class TranslateCommand {
|
|
|
71
71
|
console.error(`Source locale directory not found: ${this.sourceDir}`);
|
|
72
72
|
return { success: false, error: 'Source directory not found' };
|
|
73
73
|
}
|
|
74
|
-
const
|
|
74
|
+
const resolvedSource = this.resolveSourceDirectoryForLanguage(this.sourceDir, this.sourceLang);
|
|
75
|
+
if (!resolvedSource.ok) {
|
|
76
|
+
console.error(resolvedSource.message);
|
|
77
|
+
return { success: false, error: 'No source files found' };
|
|
78
|
+
}
|
|
79
|
+
this.sourceDir = resolvedSource.sourceDir;
|
|
80
|
+
this.configuredTargetLangs = this.getConfiguredTargetLanguages(unified, this.sourceDir);
|
|
81
|
+
const jsonFiles = resolvedSource.jsonFiles;
|
|
75
82
|
return await this.nonInteractiveFlow(jsonFiles);
|
|
76
83
|
}
|
|
77
84
|
|
|
@@ -84,18 +91,16 @@ class TranslateCommand {
|
|
|
84
91
|
// Step 2: Choose source language
|
|
85
92
|
this.sourceLang = await this.promptSourceLang(ask);
|
|
86
93
|
if (!this.sourceLang) return { success: false, error: 'No source language selected' };
|
|
87
|
-
this.configuredTargetLangs = this.getConfiguredTargetLanguages(unified, this.sourceDir);
|
|
88
|
-
|
|
89
|
-
const jsonFiles = SecurityUtils.safeReaddirSync(this.sourceDir, path.dirname(this.sourceDir))
|
|
90
|
-
.filter(f => f.endsWith('.json'))
|
|
91
|
-
.sort();
|
|
92
94
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
const resolvedSource = this.resolveSourceDirectoryForLanguage(this.sourceDir, this.sourceLang);
|
|
96
|
+
if (!resolvedSource.ok) {
|
|
97
|
+
console.error(resolvedSource.message);
|
|
95
98
|
return { success: false, error: 'No source files found' };
|
|
96
99
|
}
|
|
100
|
+
this.sourceDir = resolvedSource.sourceDir;
|
|
101
|
+
this.configuredTargetLangs = this.getConfiguredTargetLanguages(unified, this.sourceDir);
|
|
97
102
|
|
|
98
|
-
return await this.interactiveFlow(jsonFiles, ask);
|
|
103
|
+
return await this.interactiveFlow(resolvedSource.jsonFiles, ask);
|
|
99
104
|
}
|
|
100
105
|
|
|
101
106
|
async promptSourceDir(ask, defaultDir) {
|
|
@@ -106,8 +111,9 @@ class TranslateCommand {
|
|
|
106
111
|
console.log(' Accepted: an absolute path, or a path relative to the current project.');
|
|
107
112
|
console.log(' Examples:');
|
|
108
113
|
console.log(' ./locales/en');
|
|
114
|
+
console.log(' ./locales (then choose source language: en)');
|
|
109
115
|
console.log(` ${defaultDir}`);
|
|
110
|
-
console.log(' The folder
|
|
116
|
+
console.log(' The folder can contain JSON files directly, or language folders such as ./locales/en.');
|
|
111
117
|
console.log(' Press Enter to use the default.');
|
|
112
118
|
const input = await ask(' > ');
|
|
113
119
|
|
|
@@ -251,6 +257,42 @@ class TranslateCommand {
|
|
|
251
257
|
return { success: false, error: 'Non-interactive mode not supported from menu' };
|
|
252
258
|
}
|
|
253
259
|
|
|
260
|
+
getJsonFiles(sourceDir) {
|
|
261
|
+
return SecurityUtils.safeReaddirSync(sourceDir, path.dirname(sourceDir))
|
|
262
|
+
.filter(f => f.endsWith('.json'))
|
|
263
|
+
.sort();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
resolveSourceDirectoryForLanguage(selectedDir, sourceLang, options = {}) {
|
|
267
|
+
const shouldLog = options.log !== false;
|
|
268
|
+
const jsonFiles = this.getJsonFiles(selectedDir);
|
|
269
|
+
if (jsonFiles.length > 0) {
|
|
270
|
+
return { ok: true, sourceDir: selectedDir, jsonFiles };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const cleanLang = String(sourceLang || '').trim().toLowerCase();
|
|
274
|
+
if (cleanLang) {
|
|
275
|
+
const languageDir = path.join(selectedDir, cleanLang);
|
|
276
|
+
const languageStats = SecurityUtils.safeStatSync(languageDir, path.dirname(languageDir));
|
|
277
|
+
if (languageStats && languageStats.isDirectory()) {
|
|
278
|
+
const languageJsonFiles = this.getJsonFiles(languageDir);
|
|
279
|
+
if (languageJsonFiles.length > 0) {
|
|
280
|
+
if (shouldLog) {
|
|
281
|
+
console.log(` No JSON files found directly in: ${selectedDir}`);
|
|
282
|
+
console.log(` Using source language folder: ${languageDir}`);
|
|
283
|
+
}
|
|
284
|
+
return { ok: true, sourceDir: languageDir, jsonFiles: languageJsonFiles };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const checkedLanguageDir = cleanLang ? path.join(selectedDir, cleanLang) : null;
|
|
290
|
+
const message = checkedLanguageDir
|
|
291
|
+
? `No JSON files found in: ${selectedDir}\nAlso checked source language folder: ${checkedLanguageDir}`
|
|
292
|
+
: `No JSON files found in: ${selectedDir}`;
|
|
293
|
+
return { ok: false, sourceDir: selectedDir, jsonFiles: [], message };
|
|
294
|
+
}
|
|
295
|
+
|
|
254
296
|
getConfiguredTargetLanguages(config = {}, sourceDir = this.sourceDir) {
|
|
255
297
|
const candidates = []
|
|
256
298
|
.concat(config.defaultLanguages || [])
|
package/main/manage/index.js
CHANGED
|
@@ -146,7 +146,6 @@ class I18nManager {
|
|
|
146
146
|
|
|
147
147
|
if (hasLanguageDirs) {
|
|
148
148
|
this.config.sourceDir = possiblePath;
|
|
149
|
-
t('init.autoDetectedI18nDirectory', { path: possiblePath });
|
|
150
149
|
break;
|
|
151
150
|
}
|
|
152
151
|
} catch (error) {
|
|
@@ -347,7 +346,6 @@ class I18nManager {
|
|
|
347
346
|
if (!shouldSkipInitCheck) {
|
|
348
347
|
await SetupEnforcer.checkSetupCompleteAsync();
|
|
349
348
|
}
|
|
350
|
-
clearStartupTimeout();
|
|
351
349
|
|
|
352
350
|
prompt = createPrompt({ noPrompt: args.noPrompt });
|
|
353
351
|
const interactive = isInteractive({ noPrompt: args.noPrompt });
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Interactive Menu Manager
|
|
3
3
|
* @module managers/InteractiveMenu
|
|
4
|
+
* @deprecated This module is superseded by the I18nManager.showInteractiveMenu
|
|
5
|
+
* implementation in main/manage/index.js. This file is retained only for
|
|
6
|
+
* backward compatibility with existing test references and should not be
|
|
7
|
+
* used in new code.
|
|
4
8
|
*/
|
|
5
9
|
|
|
6
10
|
const { t } = require('../../../utils/i18n-helper');
|
|
@@ -350,19 +350,22 @@ module.exports = class FileManagementService {
|
|
|
350
350
|
* @returns {Promise<boolean>} True if authentication is required
|
|
351
351
|
*/
|
|
352
352
|
async isAuthRequiredForScript(scriptName) {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
353
|
+
try {
|
|
354
|
+
const SecurityUtils = require('../../../utils/security');
|
|
355
|
+
const configPath = path.join(process.cwd(), 'settings', 'admin-pin.json');
|
|
356
|
+
return SecurityUtils.safeExistsSync(configPath);
|
|
357
|
+
} catch {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
356
360
|
}
|
|
357
361
|
|
|
358
|
-
/**
|
|
359
|
-
* Verify PIN for authentication
|
|
360
|
-
* @param {string} pin - PIN to verify
|
|
361
|
-
* @returns {Promise<boolean>} True if PIN is valid
|
|
362
|
-
*/
|
|
363
362
|
async verifyPin(pin) {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
363
|
+
try {
|
|
364
|
+
const AdminAuth = require('../../../utils/admin-auth');
|
|
365
|
+
const auth = new AdminAuth();
|
|
366
|
+
return await auth.verifyPin(pin);
|
|
367
|
+
} catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
367
370
|
}
|
|
368
371
|
};
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "i18ntk",
|
|
3
|
-
"version": "3.
|
|
4
|
-
"description": "Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, auto translation, fixing, reporting, and runtime translation loading.",
|
|
3
|
+
"version": "3.2.0",
|
|
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
|
+
"i18ntk",
|
|
8
|
+
"i18n-toolkit",
|
|
7
9
|
"i18n",
|
|
8
10
|
"internationalization",
|
|
9
11
|
"localization",
|
|
@@ -158,5 +160,6 @@
|
|
|
158
160
|
"publishConfig": {
|
|
159
161
|
"access": "public"
|
|
160
162
|
},
|
|
161
|
-
"preferGlobal": true
|
|
163
|
+
"preferGlobal": true,
|
|
164
|
+
"readme": "# i18ntk v3.2.0\n\nZero-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/3.2.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 3.2.0\n\n- **SECURITY**: Fixed 4 critical runtime-crash bugs (invalid crypto APIs, missing imports) across admin-pin.js, security-config.js, and scripts/security-check.js.\n- **SECURITY**: Removed encryption key stored alongside ciphertext in admin-pin.js; encryption key is now derived via HKDF.\n- **SECURITY**: Enforced HTTPS-only for Google Translate API requests; fixed http.get timeout for Node.js <16.14 compatibility.\n- **SECURITY**: Added path validation to backup restore/verify operations; locked down FileManagementService PIN verification stubs.\n\nSee [CHANGELOG.md](./CHANGELOG.md) and [docs/migration-guide-v3.2.0.md](./docs/migration-guide-v3.2.0.md) for release details.\n\n## Quick Start\n\nInitialize a project:\n\n```bash\ni18ntk\n# or\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\nNote: manager route `i18ntk --command=backup` is disabled in current builds. Use `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\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\": [\"BrandName\", \"PRODUCT_CODE\", \"API\"],\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- `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## 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\nruntime.initRuntime({\n baseDir: './locales',\n language: 'en',\n fallbackLanguage: 'en',\n keySeparator: '.',\n preload: true\n});\n\nconsole.log(runtime.t('common.hello'));\nruntime.setLanguage('fr');\nconsole.log(runtime.getLanguage());\nconsole.log(runtime.getAvailableLanguages());\nruntime.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\": \"3.2.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- [Migration Guide v2.6.0](./docs/migration-guide-v2.6.0.md)\n- [Migration Guide v2.5.1](./docs/migration-guide-v2.5.1.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"
|
|
162
165
|
}
|
package/runtime/enhanced.d.ts
CHANGED
package/runtime/i18ntk.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// runtime/i18ntk.d.ts
|
|
2
2
|
// Complete TypeScript definitions for i18ntk internationalization framework
|
|
3
|
-
// Version
|
|
3
|
+
// Version 3.2.0 - Full TypeScript support with AES-256-GCM encryption
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Core translation parameters interface
|
|
@@ -495,20 +495,11 @@ export declare function initRuntime(options: {
|
|
|
495
495
|
}): BasicI18nRuntime;
|
|
496
496
|
|
|
497
497
|
/**
|
|
498
|
-
* Type guards
|
|
498
|
+
* Type guards (runtime checks, not exported by actual code)
|
|
499
499
|
*/
|
|
500
500
|
export declare function isI18nRuntime(obj: any): obj is I18nRuntime;
|
|
501
501
|
export declare function isBasicI18nRuntime(obj: any): obj is BasicI18nRuntime;
|
|
502
502
|
|
|
503
|
-
/**
|
|
504
|
-
* Utility functions
|
|
505
|
-
*/
|
|
506
|
-
export declare function validateConfig(config: any): boolean;
|
|
507
|
-
export declare function sanitizeKey(key: string): string;
|
|
508
|
-
export declare function formatNumber(value: number, locale?: string): string;
|
|
509
|
-
export declare function formatDate(date: Date, locale?: string): string;
|
|
510
|
-
export declare function formatCurrency(value: number, currency?: string, locale?: string): string;
|
|
511
|
-
|
|
512
503
|
/**
|
|
513
504
|
* Error types
|
|
514
505
|
*/
|
package/ui-locales/de.json
CHANGED
package/ui-locales/en.json
CHANGED
package/ui-locales/es.json
CHANGED
package/ui-locales/fr.json
CHANGED
package/ui-locales/ja.json
CHANGED
package/ui-locales/ru.json
CHANGED
package/ui-locales/zh.json
CHANGED
package/utils/admin-auth.js
CHANGED
|
@@ -544,13 +544,13 @@ class AdminAuth {
|
|
|
544
544
|
config.lastModified = new Date().toISOString();
|
|
545
545
|
const success = await this.saveConfig(config);
|
|
546
546
|
if (success) {
|
|
547
|
-
SecurityUtils.logSecurityEvent('pin_protection_disabled', 'info', 'PIN protection disabled (PIN retained)');
|
|
547
|
+
SecurityUtils.logSecurityEvent('pin_protection_disabled', 'info', { message: 'PIN protection disabled (PIN retained)' });
|
|
548
548
|
}
|
|
549
549
|
return success;
|
|
550
550
|
}
|
|
551
551
|
return true;
|
|
552
552
|
} catch (error) {
|
|
553
|
-
SecurityUtils.logSecurityEvent('pin_protection_disable_error', 'error', `Failed to disable PIN protection: ${error.message}`);
|
|
553
|
+
SecurityUtils.logSecurityEvent('pin_protection_disable_error', 'error', { message: `Failed to disable PIN protection: ${error.message}` });
|
|
554
554
|
return false;
|
|
555
555
|
}
|
|
556
556
|
}
|
|
@@ -566,13 +566,13 @@ class AdminAuth {
|
|
|
566
566
|
config.lastModified = new Date().toISOString();
|
|
567
567
|
const success = await this.saveConfig(config);
|
|
568
568
|
if (success) {
|
|
569
|
-
SecurityUtils.logSecurityEvent('pin_protection_enabled', 'info', 'PIN protection enabled');
|
|
569
|
+
SecurityUtils.logSecurityEvent('pin_protection_enabled', 'info', { message: 'PIN protection enabled' });
|
|
570
570
|
}
|
|
571
571
|
return success;
|
|
572
572
|
}
|
|
573
573
|
return false;
|
|
574
574
|
} catch (error) {
|
|
575
|
-
SecurityUtils.logSecurityEvent('pin_protection_enable_error', 'error', `Failed to enable PIN protection: ${error.message}`);
|
|
575
|
+
SecurityUtils.logSecurityEvent('pin_protection_enable_error', 'error', { message: `Failed to enable PIN protection: ${error.message}` });
|
|
576
576
|
return false;
|
|
577
577
|
}
|
|
578
578
|
}
|
|
@@ -618,7 +618,7 @@ class AdminAuth {
|
|
|
618
618
|
destroySession(sessionId) {
|
|
619
619
|
const deleted = this.activeSessions.delete(sessionId);
|
|
620
620
|
if (deleted) {
|
|
621
|
-
SecurityUtils.logSecurityEvent('admin_session_destroyed', 'info', 'Admin session destroyed');
|
|
621
|
+
SecurityUtils.logSecurityEvent('admin_session_destroyed', 'info', { message: 'Admin session destroyed' });
|
|
622
622
|
}
|
|
623
623
|
return deleted;
|
|
624
624
|
}
|
|
@@ -643,7 +643,7 @@ class AdminAuth {
|
|
|
643
643
|
}
|
|
644
644
|
|
|
645
645
|
if (cleaned > 0) {
|
|
646
|
-
SecurityUtils.logSecurityEvent('admin_sessions_cleaned', 'info', `Cleaned up ${cleaned} expired sessions`);
|
|
646
|
+
SecurityUtils.logSecurityEvent('admin_sessions_cleaned', 'info', { message: `Cleaned up ${cleaned} expired sessions` });
|
|
647
647
|
}
|
|
648
648
|
}
|
|
649
649
|
|
package/utils/config-manager.js
CHANGED
|
@@ -380,9 +380,13 @@ async function acquireConfigLock(timeoutMs = CONFIG_LOCK_TIMEOUT_MS) {
|
|
|
380
380
|
};
|
|
381
381
|
}
|
|
382
382
|
|
|
383
|
-
function isAutosaveDisabled() {
|
|
384
|
-
|
|
385
|
-
|
|
383
|
+
function isAutosaveDisabled() {
|
|
384
|
+
const raw = process.env.I18NTK_DISABLE_AUTOSAVE;
|
|
385
|
+
if (raw !== undefined && raw !== null && raw !== '') {
|
|
386
|
+
return ['true', '1', 'yes'].includes(String(raw).trim().toLowerCase());
|
|
387
|
+
}
|
|
388
|
+
return envManager.getBoolean('I18NTK_DISABLE_AUTOSAVE');
|
|
389
|
+
}
|
|
386
390
|
|
|
387
391
|
function clone(obj) {
|
|
388
392
|
return JSON.parse(JSON.stringify(obj));
|
|
@@ -716,9 +720,10 @@ async function setConfig(cfg) {
|
|
|
716
720
|
|
|
717
721
|
async function updateConfig(patch) {
|
|
718
722
|
const cfg = loadConfig();
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
723
|
+
const cloned = clone(cfg);
|
|
724
|
+
deepMerge(cloned, patch);
|
|
725
|
+
currentConfig = cloned;
|
|
726
|
+
return cloned;
|
|
722
727
|
}
|
|
723
728
|
|
|
724
729
|
async function resetToDefaults() {
|
|
@@ -760,6 +765,8 @@ module.exports = {
|
|
|
760
765
|
DEFAULT_CONFIG,
|
|
761
766
|
loadConfig,
|
|
762
767
|
saveConfig,
|
|
768
|
+
loadSettings: loadConfig,
|
|
769
|
+
saveSettings: saveConfig,
|
|
763
770
|
getConfig,
|
|
764
771
|
updateConfig,
|
|
765
772
|
setConfig,
|
package/utils/translate/api.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const https = require('https');
|
|
2
|
-
const http = require('http');
|
|
3
2
|
const { URL } = require('url');
|
|
4
3
|
|
|
5
4
|
const DEFAULT_CONCURRENCY = 3;
|
|
@@ -10,8 +9,11 @@ const MAX_BACKOFF_DELAY = 30000;
|
|
|
10
9
|
function httpGet(urlString, timeout = 15000) {
|
|
11
10
|
return new Promise((resolve) => {
|
|
12
11
|
const url = new URL(urlString);
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
if (url.protocol !== 'https:') {
|
|
13
|
+
resolve({ ok: false, error: 'ProtocolError', message: 'Only HTTPS requests are supported' });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const req = https.get(urlString, { headers: { 'User-Agent': 'i18ntk/3.2.0' } }, (res) => {
|
|
15
17
|
let data = '';
|
|
16
18
|
res.on('data', (chunk) => { data += chunk; });
|
|
17
19
|
res.on('end', () => {
|
|
@@ -23,13 +25,13 @@ function httpGet(urlString, timeout = 15000) {
|
|
|
23
25
|
}
|
|
24
26
|
});
|
|
25
27
|
});
|
|
26
|
-
req.
|
|
27
|
-
resolve({ ok: false, error: e.code || 'NetworkError', message: e.message });
|
|
28
|
-
});
|
|
29
|
-
req.on('timeout', () => {
|
|
28
|
+
req.setTimeout(timeout, () => {
|
|
30
29
|
req.destroy();
|
|
31
30
|
resolve({ ok: false, error: 'TimeoutError', message: 'Request timed out' });
|
|
32
31
|
});
|
|
32
|
+
req.on('error', (e) => {
|
|
33
|
+
resolve({ ok: false, error: e.code || 'NetworkError', message: e.message });
|
|
34
|
+
});
|
|
33
35
|
});
|
|
34
36
|
}
|
|
35
37
|
|
|
@@ -98,7 +100,7 @@ async function translateText(text, targetLang, options = {}) {
|
|
|
98
100
|
if (translated === text) {
|
|
99
101
|
return { ok: true, translated: text };
|
|
100
102
|
}
|
|
101
|
-
if (result.status === 429) {
|
|
103
|
+
if (result.status === 429 || (translated === null && result.status >= 400)) {
|
|
102
104
|
lastError = { error: 'RateLimited', message: 'Google Translate rate limit hit' };
|
|
103
105
|
continue;
|
|
104
106
|
}
|
|
@@ -109,6 +111,11 @@ async function translateText(text, targetLang, options = {}) {
|
|
|
109
111
|
continue;
|
|
110
112
|
}
|
|
111
113
|
|
|
114
|
+
if (result.error === 'TimeoutError' || result.error === 'NetworkError') {
|
|
115
|
+
lastError = { error: result.error, message: result.message || 'Network request failed, retrying' };
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
112
119
|
lastError = { error: result.error || 'UnknownError', message: result.message || 'Request failed' };
|
|
113
120
|
}
|
|
114
121
|
|
|
@@ -149,8 +156,9 @@ async function translateBatch(batch, targetLang, options = {}) {
|
|
|
149
156
|
}
|
|
150
157
|
}
|
|
151
158
|
|
|
152
|
-
const workerCount = Math.min(concurrency, batch.length);
|
|
159
|
+
const workerCount = Math.min(concurrency > 0 ? concurrency : DEFAULT_CONCURRENCY, batch.length);
|
|
153
160
|
const workers = Array.from({ length: workerCount }, () => worker());
|
|
161
|
+
|
|
154
162
|
await Promise.all(workers);
|
|
155
163
|
|
|
156
164
|
return results;
|
|
@@ -54,7 +54,7 @@ function collectLeaves(obj, prefix = '') {
|
|
|
54
54
|
return entries;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function
|
|
57
|
+
function parseKeyPath(keyPath) {
|
|
58
58
|
const parts = [];
|
|
59
59
|
let current = '';
|
|
60
60
|
for (let i = 0; i < keyPath.length; i++) {
|
|
@@ -77,13 +77,18 @@ function setLeaf(obj, keyPath, value) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
if (current) parts.push(current);
|
|
80
|
+
return parts;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function setLeaf(obj, keyPath, value) {
|
|
84
|
+
const parts = parseKeyPath(keyPath);
|
|
80
85
|
|
|
81
86
|
let target = obj;
|
|
82
87
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
83
88
|
const part = parts[i];
|
|
84
89
|
if (part.startsWith('[') && part.endsWith(']')) {
|
|
85
90
|
const idx = parseInt(part.slice(1, -1), 10);
|
|
86
|
-
if (!(idx in target)) target[idx] =
|
|
91
|
+
if (!(idx in target)) target[idx] = [];
|
|
87
92
|
target = target[idx];
|
|
88
93
|
} else {
|
|
89
94
|
if (!(part in target)) target[part] = {};
|
|
@@ -100,28 +105,7 @@ function setLeaf(obj, keyPath, value) {
|
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
function getLeaf(obj, keyPath) {
|
|
103
|
-
const parts =
|
|
104
|
-
let current = '';
|
|
105
|
-
for (let i = 0; i < keyPath.length; i++) {
|
|
106
|
-
const ch = keyPath[i];
|
|
107
|
-
if (ch === '.' && keyPath[i - 1] !== '\\') {
|
|
108
|
-
if (current) parts.push(current);
|
|
109
|
-
current = '';
|
|
110
|
-
} else if (ch === '[') {
|
|
111
|
-
if (current) parts.push(current);
|
|
112
|
-
current = '';
|
|
113
|
-
i++;
|
|
114
|
-
while (i < keyPath.length && keyPath[i] !== ']') {
|
|
115
|
-
current += keyPath[i];
|
|
116
|
-
i++;
|
|
117
|
-
}
|
|
118
|
-
parts.push(`[${current}]`);
|
|
119
|
-
current = '';
|
|
120
|
-
} else {
|
|
121
|
-
current += ch;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
if (current) parts.push(current);
|
|
108
|
+
const parts = parseKeyPath(keyPath);
|
|
125
109
|
|
|
126
110
|
let target = obj;
|
|
127
111
|
for (const part of parts) {
|
|
@@ -136,7 +120,12 @@ function getLeaf(obj, keyPath) {
|
|
|
136
120
|
}
|
|
137
121
|
|
|
138
122
|
function deepClone(obj) {
|
|
139
|
-
|
|
123
|
+
if (obj === null || obj === undefined) return obj;
|
|
124
|
+
try {
|
|
125
|
+
return JSON.parse(JSON.stringify(obj));
|
|
126
|
+
} catch (e) {
|
|
127
|
+
return obj;
|
|
128
|
+
}
|
|
140
129
|
}
|
|
141
130
|
|
|
142
131
|
module.exports = {
|