i18ntk 4.1.0 → 4.2.1

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.
Files changed (49) hide show
  1. package/CHANGELOG.md +64 -5
  2. package/README.md +73 -17
  3. package/SECURITY.md +10 -4
  4. package/main/i18ntk-analyze.js +10 -20
  5. package/main/i18ntk-backup.js +106 -44
  6. package/main/i18ntk-init.js +153 -157
  7. package/main/i18ntk-setup.js +36 -13
  8. package/main/i18ntk-sizing.js +44 -27
  9. package/main/i18ntk-translate.js +311 -41
  10. package/main/i18ntk-usage.js +272 -103
  11. package/main/i18ntk-validate.js +38 -31
  12. package/main/manage/commands/AnalyzeCommand.js +7 -17
  13. package/main/manage/commands/CommandRouter.js +6 -6
  14. package/main/manage/commands/SizingCommand.js +5 -2
  15. package/main/manage/commands/TranslateCommand.js +73 -56
  16. package/main/manage/commands/ValidateCommand.js +58 -26
  17. package/main/manage/index.js +11 -42
  18. package/main/manage/managers/InteractiveMenu.js +11 -40
  19. package/main/manage/services/InitService.js +114 -118
  20. package/main/manage/services/UsageService.js +247 -96
  21. package/package.json +19 -14
  22. package/runtime/enhanced.d.ts +5 -5
  23. package/runtime/enhanced.js +49 -25
  24. package/runtime/i18ntk.d.ts +30 -7
  25. package/runtime/index.d.ts +48 -19
  26. package/runtime/index.js +175 -90
  27. package/settings/settings-cli.js +115 -38
  28. package/settings/settings-manager.js +24 -6
  29. package/ui-locales/de.json +192 -11
  30. package/ui-locales/en.json +182 -8
  31. package/ui-locales/es.json +193 -12
  32. package/ui-locales/fr.json +189 -8
  33. package/ui-locales/ja.json +190 -8
  34. package/ui-locales/ru.json +191 -9
  35. package/ui-locales/zh.json +194 -9
  36. package/utils/cli-helper.js +8 -12
  37. package/utils/config-helper.js +1 -1
  38. package/utils/config-manager.js +8 -6
  39. package/utils/localized-confirm.js +55 -0
  40. package/utils/menu-layout.js +41 -0
  41. package/utils/report-writer.js +110 -0
  42. package/utils/security.js +15 -22
  43. package/utils/translate/api.js +31 -3
  44. package/utils/translate/placeholder.js +42 -1
  45. package/utils/translate/report.js +32 -4
  46. package/utils/translate/safe-network.js +24 -4
  47. package/utils/usage-insights.js +435 -0
  48. package/utils/usage-source.js +50 -0
  49. package/utils/watch-locales.js +1 -8
package/CHANGELOG.md CHANGED
@@ -3,11 +3,70 @@
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
-
8
- ## [4.1.0] - 2026-05-21
9
-
10
- ### Fixed
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [4.2.1] - 2026-05-31
9
+
10
+ ### Changed
11
+ - Auto Translate now treats uppercase target-language placeholders such as `[AR] What We Offer` as untranslated target values when the bracketed code matches the target language, so target-aware mode sends the source text for translation instead of keeping the placeholder copy.
12
+ - Auto Translate now performs a final pre-write leftover check and retries values that still look like placeholder-prefixed untranslated text, untranslated markers, source-language copies, or broken output.
13
+ - Auto Translate reports leftover values in the post-translation report and exits with validation failure when leftovers remain after the final retry, instead of reporting a clean completion.
14
+
15
+ ### Fixed
16
+ - Usage analysis no longer writes its inferred app source fallback, such as `src`, back into the shared locale configuration when `sourceDir` and `i18nDir` are both the locale directory.
17
+ - Manager sizing now reads the configured i18n directory unless `--source-dir` is explicitly provided, so running sizing after usage no longer silently analyzes the wrong directory.
18
+ - Manager sizing now treats a failed sizing analysis as a command failure instead of printing a generic operation success.
19
+ - Validation summary reports now include warning and error details, including content-risk warning payloads, instead of only totals.
20
+
21
+ ## [4.2.0] - 2026-05-30
22
+
23
+ ### Security
24
+ - Shared path validation no longer permits artifact-like filenames such as `.lock` or `.temp-config.json` to bypass base-directory containment.
25
+ - Shared path validation now rejects Windows cross-drive escape cases where `path.relative()` returns an absolute path.
26
+ - Custom `I18NTK_INTERNAL_PATH_PREFIXES` entries can no longer mark arbitrary outside directories as internal roots.
27
+ - Backup restore now rejects backup entry names containing path separators, absolute paths, traversal, or non-JSON names before writing restored files.
28
+ - Runtime locale loading now validates language identifiers before resolving single-file or directory locale paths, blocking `../` language names from reading JSON outside `baseDir`.
29
+ - Auto Translate provider URL validation now blocks IPv4-mapped IPv6 loopback/private hosts.
30
+
31
+ ### Changed
32
+ - Main runtime now includes production-safe features from the enhanced runtime surface: per-call language overrides, synchronous `translateBatch()`, and `clearCache()` / `getCacheInfo()` helpers.
33
+ - `i18ntk/runtime/enhanced` remains available as a legacy public subpath for compatibility, while new production integrations should prefer the lightweight `i18ntk/runtime` API.
34
+ - Usage analysis now indexes known translation keys back to source files, including direct i18n calls and literal key references that were previously missed.
35
+ - Usage analysis now expands simple dynamic templates backed by literal constants, bounded literal arrays, object maps, and ternaries to exact available keys before falling back to unresolved dynamic-expression reporting.
36
+ - Usage reports now list unresolved dynamic key expressions separately instead of treating broad wildcard prefixes as proof that every matching key is used.
37
+ - Usage reports now include namespace/file naming recommendations such as preferring `shop.*` keys and `shop.json` for `/shop` page or route files.
38
+ - Usage reports now list likely hardcoded user-facing text with suggested translation keys, and prefer an existing source key when the inline text matches a source translation value.
39
+ - Translation analysis and init reports now default to Markdown for readable output, with `reports.format` supporting `markdown`, `json`, or `text` through settings and config.
40
+ - Init default target languages now include English (`en`) before `de`, `es`, `fr`, and `ru` when the UI is running in another language.
41
+ - Confirmation prompts now accept localized native yes/no input for supported UI languages while retaining English fallback tokens.
42
+ - Auto Translate has moved out of beta in menus and documentation, and its settings are exposed with localized labels.
43
+ - Auto Translate now keeps existing translated target values by default and only translates missing, marker, source-copy, or likely English target strings; use `--translate-all` to force a full re-translation.
44
+ - Auto Translate now treats visibly corrupt target strings such as `?????`, Unicode replacement characters, and common mojibake as needing retranslation from the source language.
45
+ - Auto Translate now defaults to 12 concurrent provider requests and allows Google concurrency up to 100 instead of the old 25-request cap; DeepL and LibreTranslate remain capped lower to avoid provider/account throttling.
46
+ - Auto Translate progress output now separates string translation from placeholder-safe text-segment translation and shows the active key path during progress updates.
47
+ - Placeholder detection now covers ICU plural/select blocks, i18next nested `$t(...)` references, and wider named printf formats such as `%(total).2f`.
48
+ - Manager menu output is now grouped with clearer spacing and aligned option numbers.
49
+ - Documentation now consolidates migration guidance around `4.2.0` and removes stale old per-version migration guides from the working docs tree.
50
+ - Removed stale duplicate development artifacts `main/manage/index-fixed.js` and `utils/security-fixed.js` to reduce audit drift and prevent accidental reuse.
51
+ - Updated public, root, and development package metadata for the 4.2.0 release line.
52
+
53
+ ### Fixed
54
+ - Runtime JSON loading now preserves valid translation strings containing comment-like text such as `/* token */` by parsing valid JSON before using the comment-stripping fallback.
55
+ - Enhanced runtime now exports the top-level `translateBatch()`, `translateBatchEncrypted()`, and `tTyped()` helpers declared by its TypeScript definitions, and those declarations now reflect async return values.
56
+ - Usage analysis no longer scans the project root when `sourceDir` and `i18nDir` both point at the locale directory; it now uses a detected app source directory or disables usage scanning with a clear warning.
57
+ - Init backup prompts, completion summaries, report prompts, and report status text now use bundled UI locale keys instead of hard-coded English.
58
+ - Bundled UI locales were regenerated from `ui-locales/en.json` for newly added, source-copy, and corrupt target strings.
59
+ - JSON report output is now pretty-printed object JSON instead of a single JSON string containing escaped newlines.
60
+ - The managed Auto Translate command no longer forces UI translations back to English after the user has selected another UI language.
61
+ - Manager validation output no longer prints duplicate source/i18n/output directory blocks before the validator summary.
62
+ - `i18ntk-setup --help` now exits after printing help instead of running setup and writing project files.
63
+ - `npm run languages:list` and `npm run languages:status` now produce non-interactive output instead of opening the settings menu.
64
+ - `i18ntk-backup create locales` now recursively backs up modular locale layouts such as `locales/en/common.json`, and restore safely recreates nested JSON paths without allowing traversal.
65
+ - Removed a stale bundled `locales/es/navigation.json` fixture that made `i18ntk-doctor` report a dangling namespace after setup/init tests.
66
+
67
+ ## [4.1.0] - 2026-05-21
68
+
69
+ ### Fixed
11
70
  - Runtime: stale manifest entries (deleted files after manifest construction) no longer cause unhandled exceptions; loadedFiles set before load with try/catch guard.
12
71
  - Runtime: `refresh()` now correctly clears the key manifest for the refreshed language, preventing stale file references.
13
72
  - Runtime: null `baseDir` guard prevents cascading `validatePath(null)` errors in `loadKeyManifestFromDir`.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # i18ntk v4.1.0
1
+ # i18ntk v4.2.1
2
2
 
3
3
  A i18n toolkit - A zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, translation completion, automatic JSON locale translation, reporting, and runtime translation loading.
4
4
 
@@ -9,7 +9,7 @@ A i18n toolkit - A zero-dependency internationalization toolkit for setup, scann
9
9
  [![node](https://img.shields.io/badge/node-%3E%3D16-339933)](https://nodejs.org)
10
10
  [![dependencies](https://img.shields.io/badge/dependencies-0-success)](https://www.npmjs.com/package/i18ntk)
11
11
  [![license](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
12
- [![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.1.0)](https://socket.dev/npm/package/i18ntk/overview/4.1.0)
12
+ [![socket](https://socket.dev/api/badge/npm/package/i18ntk/4.2.1)](https://socket.dev/npm/package/i18ntk/overview/4.2.1)
13
13
 
14
14
  ## Install
15
15
 
@@ -30,6 +30,15 @@ Requirements:
30
30
  - npm `>=8.0.0`
31
31
  - No runtime dependencies
32
32
 
33
+ ## What's New in 4.2.1
34
+
35
+ - **AUTO TRANSLATE**: Existing target values like `[AR] What We Offer` are now treated as untranslated placeholders for the matching target language and are translated from the source text.
36
+ - **AUTO TRANSLATE**: Before writing each output file, Auto Translate now performs a final leftover check and retries any placeholder-prefixed or source-copy values once.
37
+ - **AUTO TRANSLATE**: If leftovers remain after the final retry, the command warns, includes them in the report, recommends rerunning Auto Translate, and exits with validation failure instead of reporting a clean completion.
38
+ - **SIZING/USAGE**: Usage analysis no longer writes its inferred app source fallback back into the shared locale config, so running usage before sizing no longer makes sizing analyze the wrong directory.
39
+ - **VALIDATION REPORTS**: Validation summary files now include warning and error details, including English-content warning payloads, instead of only totals.
40
+ - **DOCS**: Versioned docs and migration guidance now reflect the current 4.2.1 command surface.
41
+
33
42
  ## What's New in 4.1.0
34
43
 
35
44
  - **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.
@@ -100,7 +109,6 @@ i18ntk --command=sizing
100
109
  i18ntk --command=complete
101
110
  i18ntk --command=translate
102
111
  i18ntk --command=summary
103
- i18ntk --command=debug
104
112
  ```
105
113
 
106
114
  Standalone executables:
@@ -119,12 +127,29 @@ i18ntk-fixer
119
127
  i18ntk-backup
120
128
  i18ntk-translate
121
129
  ```
122
- `n
123
- Note: manager route `i18ntk --command=backup` is disabled in current builds. Use `i18ntk-backup` (or legacy `i18ntk-backup`) directly for backup operations.
130
+
131
+ Note: manager route `i18ntk --command=backup` is disabled in current builds. Use `i18ntk-backup` directly for backup operations.
132
+
133
+ ## Command Reference
134
+
135
+ | Command | What it does | Looks for | Writes or changes |
136
+ | --- | --- | --- | --- |
137
+ | `i18ntk` | Opens the interactive management menu. | Project config, setup state, available commands. | Only changes files after you choose a command that writes. |
138
+ | `i18ntk --command=init` / `i18ntk-init` | Sets up locale folders and missing target-language files. | Source language files and selected target languages. | Locale JSON files, `.i18ntk-config`, optional reports/backups. |
139
+ | `i18ntk --command=analyze` / `i18ntk-analyze` | Compares source and target translation coverage. | Missing keys, extra keys, untranslated markers, completion by language. | Markdown/JSON/text reports when report output is enabled. |
140
+ | `i18ntk --command=validate` / `i18ntk-validate` | Validates structure and translation quality risks. | Placeholder mismatches, missing keys, risky URLs/emails/secrets, likely English target text. | Validation summary report. Does not edit locale files. |
141
+ | `i18ntk --command=usage` / `i18ntk-usage` | Maps translation keys to source files and finds unused/missing keys. | Direct i18n calls, literal known-key references, bounded dynamic templates/object maps, unresolved dynamic expressions, hardcoded text candidates, namespace/file naming mismatches. | Usage report with key locations, namespace recommendations, unresolved dynamic expressions, hardcoded text suggestions, and optional dead-key report. Does not delete unless cleanup deletion is explicitly enabled. |
142
+ | `i18ntk --command=scanner` / `i18ntk-scanner` | Scans source for i18n issues and hardcoded user-facing text. | JSX/template text, common text attributes, i18n usage patterns, source-language text profiles. | Scanner report. Does not edit files. |
143
+ | `i18ntk --command=complete` / `i18ntk-complete` | Adds missing keys to target language files for 100% key coverage. | Source-language keys missing from targets. | Target locale JSON files, using missing translation markers/prefixes. |
144
+ | `i18ntk --command=translate` / `i18ntk-translate` | Auto-translates locale JSON using configured provider behavior. | Missing, empty, untranslated-marker, source-copy, likely-English, or visibly corrupt target values by default. | Target locale JSON files and translation reports. Existing translated values are kept unless `--translate-all` is used. |
145
+ | `i18ntk --command=sizing` / `i18ntk-sizing` | Estimates translated string length expansion and layout risk. | Text length, expansion ratios, placeholder-bearing strings. | Sizing report. Does not edit locale files. |
146
+ | `i18ntk --command=summary` / `i18ntk-summary` | Shows project translation status. | Configured locales, reports, completeness status. | Console/report output only. |
147
+ | `i18ntk-fixer` | Fixes placeholder and missing-marker issues. | Placeholder corruption, missing translation markers, configured language files. | Locale JSON files when fixes are applied. Use dry-run options where available before bulk edits. |
148
+ | `i18ntk-backup` | Creates, verifies, restores, and cleans locale backups. | Locale JSON files and backup manifests. | Backup archives/manifests, or restored locale files when using restore. |
124
149
 
125
150
  ## Common Options
126
151
 
127
- Most commands support:
152
+ Many commands support:
128
153
 
129
154
  - `--source-dir <path>`
130
155
  - `--i18n-dir <path>`
@@ -132,9 +157,10 @@ Most commands support:
132
157
  - `--source-language <code>`
133
158
  - `--ui-language <code>`
134
159
  - `--no-prompt`
135
- - `--dry-run`
136
160
  - `--help`
137
161
 
162
+ Command-specific tools add their own flags such as `--dry-run`, `--output-report`, `--cleanup`, `--predict-expansion`, or Auto Translate provider options.
163
+
138
164
  Example:
139
165
 
140
166
  ```bash
@@ -147,7 +173,7 @@ Interactive manager flow:
147
173
 
148
174
  ```bash
149
175
  i18ntk
150
- # choose "Auto Translate (Beta)"
176
+ # choose "Auto Translate"
151
177
  ```
152
178
 
153
179
  Direct CLI examples:
@@ -190,6 +216,8 @@ locales/de/common.json
190
216
  locales/fr/common.json
191
217
  ```
192
218
 
219
+ Auto Translate is target-aware by default. When a target file already exists, it keeps translated target values and only sends values that are missing, empty, marked as untranslated, still identical to the source, likely still English, or visibly corrupt from encoding damage such as `?????`, replacement characters, or common mojibake. Use `--translate-all` when you intentionally want to re-translate every source string.
220
+
193
221
  ### Placeholder Handling
194
222
 
195
223
  Auto Translate detects common placeholders such as:
@@ -201,6 +229,9 @@ Auto Translate detects common placeholders such as:
201
229
  - `:id`
202
230
  - `%{name}`
203
231
  - `${value}`
232
+ - `{count, plural, one {# item} other {# items}}`
233
+ - `$t(common.save)`
234
+ - `%(total).2f`
204
235
 
205
236
  Useful flags:
206
237
 
@@ -208,6 +239,10 @@ Useful flags:
208
239
  - `--skip-placeholders`: copy placeholder-bearing strings unchanged
209
240
  - `--send-placeholders`: send placeholder-bearing strings through translation after masking
210
241
  - `--custom-regex <regex>`: add project-specific placeholder detection
242
+ - `--only-missing`: keep existing translated target values and translate only missing/source-copy/likely English values (default)
243
+ - `--translate-all`: re-translate every source string
244
+
245
+ Progress output is stage-aware for large files. Normal keys are reported as `Translating strings`, while preserve-mode placeholder work is reported as `Translating placeholder-safe text segments`; each progress update includes the current key path when available.
211
246
 
212
247
  ### Protected Terms and Keys
213
248
 
@@ -247,7 +282,7 @@ Useful flags:
247
282
  - `--create-protection-file`
248
283
  - `--no-protection`
249
284
 
250
- Open 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.
285
+ Open Settings and choose `Auto Translate` to edit defaults for placeholder mode, translate-only-needed mode, concurrency, batch size, retry settings, report output, BOM output, protection file path, first-run setup prompt, and update prompt.
251
286
 
252
287
  See [docs/auto-translate.md](./docs/auto-translate.md) for the full Auto Translate guide.
253
288
 
@@ -255,7 +290,7 @@ See [docs/auto-translate.md](./docs/auto-translate.md) for the full Auto Transla
255
290
 
256
291
  Validation checks locale structure, completeness, placeholders, and content risks.
257
292
 
258
- In 3.1.2, warning types are more specific:
293
+ Validation warning types are specific:
259
294
 
260
295
  - `Potential risky content`: URL, email address, or secret-like value
261
296
  - `Possible untranslated English content`: target-language value appears to contain too much English
@@ -324,7 +359,7 @@ i18ntk-usage --source-dir ./src --i18n-dir ./locales --cleanup --dry-run-delete
324
359
  ```
325
360
 
326
361
  Each dead key receives a confidence score (0.0–1.0) factoring:
327
- - Dynamic key patterns (e.g., `` t(`prefix.${dynamic}`) ``) — lower score
362
+ - Unresolved dynamic key patterns (e.g., `` t(`prefix.${dynamic}`) ``) — lower score and listed in the usage report; simple consts, bounded arrays, object maps, and ternaries are expanded to exact keys where possible
328
363
  - Key appears in source code comments or JSDoc — medium score
329
364
  - Parent file recently modified (<30 days) — medium score
330
365
  - No references found anywhere — high score (>0.8)
@@ -417,6 +452,16 @@ console.log(i18n.t('common.hello')); // loads common.json on first access
417
452
 
418
453
  When `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.
419
454
 
455
+ Production guidance:
456
+
457
+ - Prefer the object returned from `initRuntime()` instead of module-level `runtime.t()` in apps with multiple tenants, projects, or locale roots.
458
+ - Use `lazy: true` for large modular locale folders where lower steady-state memory matters more than a small first-key lookup cost.
459
+ - Use `preload: true` without `lazy` for small locale sets or latency-sensitive startup paths.
460
+ - Call `refresh(language)` after deploying or writing changed locale files so cached data and lazy manifests are rebuilt.
461
+ - Use per-call language overrides when rendering one-off alternate-language strings: `i18n.t('common.hello', {}, { language: 'de' })`.
462
+ - Use `translateBatch()` for small groups of labels and `clearCache()` / `getCacheInfo()` for cache maintenance and diagnostics.
463
+ - `i18ntk/runtime/enhanced` remains available for compatibility with existing async/encryption users, but new production integrations should start with `i18ntk/runtime`.
464
+
420
465
  ## Runtime API
421
466
 
422
467
  Use `i18ntk/runtime` when an application needs to read locale JSON files at runtime.
@@ -439,6 +484,15 @@ console.log(i18n.getAvailableLanguages());
439
484
  i18n.refresh('fr');
440
485
  ```
441
486
 
487
+ Useful production helpers:
488
+
489
+ ```js
490
+ i18n.t('common.hello', {}, { language: 'de' }); // per-call language override
491
+ i18n.translateBatch(['menu.home', 'menu.settings']);
492
+ i18n.clearCache('fr');
493
+ console.log(i18n.getCacheInfo());
494
+ ```
495
+
442
496
  See [docs/runtime.md](./docs/runtime.md) for runtime details.
443
497
 
444
498
  ## Configuration
@@ -449,23 +503,27 @@ Example:
449
503
 
450
504
  ```json
451
505
  {
452
- "version": "4.1.0",
506
+ "version": "4.2.1",
453
507
  "sourceDir": "./locales",
454
508
  "i18nDir": "./locales",
455
509
  "outputDir": "./i18ntk-reports",
456
510
  "sourceLanguage": "en",
457
- "defaultLanguages": ["de", "es", "fr", "ru"],
511
+ "defaultLanguages": ["en", "de", "es", "fr", "ru"],
512
+ "reports": {
513
+ "format": "markdown"
514
+ },
458
515
  "englishContentThresholdPercent": 10,
459
516
  "allowedEnglishTerms": ["BrandName", "PRODUCT_CODE"],
460
517
  "autoTranslate": {
461
518
  "placeholderMode": "preserve",
462
- "concurrency": 6,
519
+ "concurrency": 12,
463
520
  "batchSize": 100,
464
521
  "progressInterval": 25,
465
522
  "retryCount": 3,
466
523
  "retryDelay": 1000,
467
524
  "timeout": 15000,
468
525
  "dryRunFirst": true,
526
+ "onlyMissingOrEnglish": true,
469
527
  "reportStdout": true,
470
528
  "bom": false,
471
529
  "protectionEnabled": true,
@@ -507,9 +565,7 @@ The public package manifest includes `readmeFilename: "README.md"`, and the rele
507
565
  - [Auto Translate Guide](./docs/auto-translate.md)
508
566
  - [Scanner Guide](./docs/scanner-guide.md)
509
567
  - [Environment Variables](./docs/environment-variables.md)
510
- - [Migration Guide v3.2.0](./docs/migration-guide-v3.2.0.md)
511
- - [Migration Guide v3.1.1](./docs/migration-guide-v3.1.1.md)
512
- - [Migration Guide v3.0.0](./docs/migration-guide-v3.0.0.md)
568
+ - [Migration Guide v4.2.1](./docs/migration-guide-v4.2.1.md)
513
569
 
514
570
  ## Security
515
571
 
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.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.
7
+ Versions earlier than `4.2.0` are not recommended for production use because later releases include Auto Translate provider hardening, dynamic-require elimination, path-validation hardening, runtime language validation, 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
 
@@ -38,13 +38,19 @@ Socket.dev scans the published npm package and may flag the following alerts. Th
38
38
  The v3.3.0 release **resolved** the previously actionable Socket.dev alert:
39
39
  - **Dynamic require** — all 21 instances eliminated (20 converted to static string literals, 1 gated with `SecurityUtils.validatePath`).
40
40
 
41
- The v4.0.0 release adds the following security hardening:
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
44
  - **Incremental backups**: hash-chain verification before restore; chain depth capped at 10 increments; circular parent references detected.
45
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
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.
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.
48
+
49
+ The v4.2.0 release adds the following security hardening:
50
+ - **Shared path validation**: artifact-like filenames no longer bypass base containment; cross-drive absolute paths and environment-added internal prefixes are constrained.
51
+ - **Backup restore**: backup entry names must be plain `.json` filenames and are restored through stable output-directory containment.
52
+ - **Runtime locale loading**: language identifiers are validated before locale path resolution.
53
+ - **Auto Translate networking**: IPv4-mapped IPv6 loopback/private hosts are blocked by provider URL validation.
48
54
 
49
55
  ## Reporting Vulnerabilities
50
56
 
@@ -68,7 +74,7 @@ Security reports are reviewed privately first. Confirmed issues should receive:
68
74
 
69
75
  ## User Guidance
70
76
 
71
- - Keep i18ntk updated to `4.1.0` or newer.
77
+ - Keep i18ntk updated to `4.2.0` or newer.
72
78
  - Do not commit `.i18ntk-config`, admin PIN files, backup directories, generated reports, logs, npm credentials, or secret material.
73
79
  - Run i18ntk only in project directories you trust.
74
80
  - Review generated translation changes before committing them.
@@ -13,9 +13,11 @@ const { loadTranslations, t } = require('../utils/i18n-helper');
13
13
  const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../utils/config-helper');
14
14
  const SecurityUtils = require('../utils/security');
15
15
  const AdminCLI = require('../utils/admin-cli');
16
- const watchLocales = require('../utils/watch-locales');
17
- const JsonOutput = require('../utils/json-output');
18
- const SetupEnforcer = require('../utils/setup-enforcer');
16
+ const watchLocales = require('../utils/watch-locales');
17
+ const JsonOutput = require('../utils/json-output');
18
+ const SetupEnforcer = require('../utils/setup-enforcer');
19
+ const configManager = require('../utils/config-manager');
20
+ const { normalizeReportFormat, writeReportFile } = require('../utils/report-writer');
19
21
 
20
22
  // Ensure setup is complete before running
21
23
  (async () => {
@@ -696,23 +698,11 @@ try {
696
698
  return null;
697
699
  }
698
700
 
699
- // Create a safe filename
700
- const safeLanguage = language.replace(/[^\w-]/g, '_');
701
- const reportPath = path.resolve(validatedOutputDir, `translation-report-${safeLanguage}.json`);
702
-
703
- // Ensure the final path is still within the output directory
704
- if (!reportPath.startsWith(validatedOutputDir)) {
705
- console.error('Invalid report path detected, potential directory traversal attack');
706
- return null;
707
- }
708
-
709
- // Use safeWriteFile for secure file writing
710
- const success = await SecurityUtils.safeWriteFile(reportPath, JSON.stringify(report, null, 2), process.cwd(), 'utf8');
711
- if (!success) {
712
- throw new Error(t('analyze.failedToWriteReportFile') || 'Failed to write report file securely');
713
- }
714
-
715
- return reportPath;
701
+ const safeLanguage = language.replace(/[^\w-]/g, '_');
702
+ const settings = configManager.getConfig ? configManager.getConfig() : {};
703
+ const format = normalizeReportFormat(this.config?.reports?.format || settings.reports?.format || this.config?.reportFormat || 'markdown');
704
+ const reportPath = await writeReportFile(validatedOutputDir, `translation-report-${safeLanguage}`, report, { format, title: `Translation Report ${safeLanguage.toUpperCase()}` });
705
+ return reportPath;
716
706
 
717
707
  } catch (error) {
718
708
  console.error(`Failed to save report for ${language}:`, error.message);
@@ -57,14 +57,35 @@ function computeFileHash(filePath) {
57
57
  }
58
58
  }
59
59
 
60
- function computeContentHash(content) {
60
+ function computeContentHash(content) {
61
61
  if (typeof content === 'object' && content !== null) {
62
62
  const normalized = JSON.stringify(content);
63
63
  return crypto.createHash('sha256').update(normalized).digest('hex');
64
64
  }
65
65
  const str = String(content);
66
- return crypto.createHash('sha256').update(str).digest('hex');
67
- }
66
+ return crypto.createHash('sha256').update(str).digest('hex');
67
+ }
68
+
69
+ async function collectJsonFiles(rootDir, currentDir = rootDir) {
70
+ const entries = await fsp.readdir(currentDir, { withFileTypes: true });
71
+ const files = [];
72
+
73
+ for (const entry of entries) {
74
+ const fullPath = path.join(currentDir, entry.name);
75
+ if (entry.isDirectory()) {
76
+ files.push(...await collectJsonFiles(rootDir, fullPath));
77
+ continue;
78
+ }
79
+ if (!entry.isFile() || !entry.name.endsWith('.json')) {
80
+ continue;
81
+ }
82
+
83
+ const relativePath = path.relative(rootDir, fullPath).split(path.sep).join('/');
84
+ files.push({ relativePath, fullPath });
85
+ }
86
+
87
+ return files;
88
+ }
68
89
 
69
90
  async function findMostRecentBackup(backupDirPath) {
70
91
  try {
@@ -140,6 +161,51 @@ function readBackupData(backupPath, baseDir) {
140
161
  return JSON.parse(raw);
141
162
  }
142
163
 
164
+ function validateBackupEntryName(fileName) {
165
+ if (!fileName || typeof fileName !== 'string') {
166
+ throw new Error('Invalid backup entry name');
167
+ }
168
+ if (fileName === '_meta') {
169
+ return null;
170
+ }
171
+ if (fileName.includes('\0') || /^[a-zA-Z]:/.test(fileName)) {
172
+ throw new Error(`Unsafe backup entry name: ${fileName}`);
173
+ }
174
+
175
+ const normalizedSeparators = fileName.replace(/\\/g, '/');
176
+ const rawSegments = normalizedSeparators.split('/');
177
+ const normalized = path.posix.normalize(normalizedSeparators);
178
+ const segments = normalized.split('/');
179
+
180
+ if (
181
+ path.posix.isAbsolute(normalizedSeparators) ||
182
+ rawSegments.some(segment => !segment || segment === '.' || segment === '..') ||
183
+ normalized === '.' ||
184
+ normalized === '..' ||
185
+ normalized.startsWith('../') ||
186
+ !normalized.endsWith('.json') ||
187
+ segments.some(segment => !segment || segment === '.' || segment === '..')
188
+ ) {
189
+ throw new Error(`Unsafe backup entry name: ${fileName}`);
190
+ }
191
+
192
+ return normalized;
193
+ }
194
+
195
+ function restoreBackupEntry(outputDir, fileName, content) {
196
+ const safeName = validateBackupEntryName(fileName);
197
+ if (!safeName) return false;
198
+
199
+ const filePath = SecurityUtils.safeJoin(outputDir, safeName);
200
+ if (!filePath) {
201
+ throw new Error(`Backup entry escapes restore directory: ${fileName}`);
202
+ }
203
+ if (!SecurityUtils.safeWriteFileSync(filePath, JSON.stringify(content, null, 2), outputDir, 'utf8')) {
204
+ throw new Error(`Unable to restore backup entry: ${fileName}`);
205
+ }
206
+ return true;
207
+ }
208
+
143
209
  function collectProtectedChainNames(backupDirPath, keptFiles) {
144
210
  const protectedNames = new Set();
145
211
  const byName = new Map();
@@ -327,31 +393,29 @@ async function handleCreate(args) {
327
393
 
328
394
  logger.info('\nCreating backup...');
329
395
 
330
- const files = (await fsp.readdir(sourceDir, { withFileTypes: true }))
331
- .filter(dirent => dirent.isFile() && dirent.name.endsWith('.json'))
332
- .map(dirent => dirent.name);
333
-
334
- if (files.length === 0) {
335
- logger.warn('No JSON files found in the specified directory');
336
- process.exit(0);
337
- }
338
-
339
- const translations = {};
340
- const hashes = {};
341
- for (const file of files) {
342
- const filePath = path.join(sourceDir, file);
343
- try {
344
- const rawContent = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
345
- if (rawContent === null) {
346
- logger.error(`Could not read file ${file}`);
347
- continue;
348
- }
349
- translations[file] = JSON.parse(rawContent);
350
- hashes[file] = computeFileHash(filePath);
351
- } catch (error) {
352
- logger.error(`Could not read file ${file}: ${error.message}`);
353
- }
354
- }
396
+ const files = await collectJsonFiles(sourceDir);
397
+
398
+ if (files.length === 0) {
399
+ logger.warn('No JSON files found in the specified directory');
400
+ process.exit(0);
401
+ }
402
+
403
+ const translations = {};
404
+ const hashes = {};
405
+ for (const file of files) {
406
+ const filePath = file.fullPath;
407
+ try {
408
+ const rawContent = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
409
+ if (rawContent === null) {
410
+ logger.error(`Could not read file ${file.relativePath}`);
411
+ continue;
412
+ }
413
+ translations[file.relativePath] = JSON.parse(rawContent);
414
+ hashes[file.relativePath] = computeFileHash(filePath);
415
+ } catch (error) {
416
+ logger.error(`Could not read file ${file.relativePath}: ${error.message}`);
417
+ }
418
+ }
355
419
 
356
420
  let meta = {
357
421
  type: 'full',
@@ -437,28 +501,26 @@ async function handleRestore(args) {
437
501
  const chain = await buildRestoreChain(backupPath, backupData);
438
502
  await fsp.mkdir(outputDir, { recursive: true });
439
503
 
440
- const restoredFiles = new Set();
441
- for (const entry of chain) {
442
- for (const [file, content] of Object.entries(entry.data)) {
443
- if (file === '_meta') continue;
444
- const filePath = path.join(outputDir, file);
445
- SecurityUtils.safeWriteFileSync(filePath, JSON.stringify(content, null, 2), path.dirname(filePath), 'utf8');
446
- restoredFiles.add(file);
447
- }
448
- }
504
+ const restoredFiles = new Set();
505
+ for (const entry of chain) {
506
+ for (const [file, content] of Object.entries(entry.data)) {
507
+ if (restoreBackupEntry(outputDir, file, content)) {
508
+ restoredFiles.add(file);
509
+ }
510
+ }
511
+ }
449
512
 
450
513
  logger.success('Incremental backup restored successfully');
451
514
  logger.info(` ${restoredFiles.size} files restored across ${chain.length} backup(s) to: ${outputDir}`);
452
515
  } else {
453
516
  await fsp.mkdir(outputDir, { recursive: true });
454
517
 
455
- let count = 0;
456
- for (const [file, content] of Object.entries(backupData)) {
457
- if (file === '_meta') continue;
458
- const filePath = path.join(outputDir, file);
459
- SecurityUtils.safeWriteFileSync(filePath, JSON.stringify(content, null, 2), path.dirname(filePath), 'utf8');
460
- count++;
461
- }
518
+ let count = 0;
519
+ for (const [file, content] of Object.entries(backupData)) {
520
+ if (restoreBackupEntry(outputDir, file, content)) {
521
+ count++;
522
+ }
523
+ }
462
524
 
463
525
  logger.success('Backup restored successfully');
464
526
  logger.info(` Restored ${count} files to: ${outputDir}`);