i18ntk 2.2.0 → 2.3.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # i18ntk v2.2.0
1
+ # i18ntk v2.3.0
2
2
 
3
- Zero-dependency i18n toolkit for initialization, scanning, analysis, validation, usage tracking, and translation completion.
3
+ Zero-dependency internationalization toolkit for setup, scanning, analysis, validation, usage tracking, and translation completion.
4
4
 
5
5
  ![i18ntk Logo](docs/screenshots/i18ntk-logo-public.PNG)
6
6
 
@@ -9,57 +9,76 @@ Zero-dependency i18n toolkit for initialization, scanning, analysis, validation,
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/2.2.0)](https://socket.dev/npm/package/i18ntk/overview/2.2.0)
12
+ [![socket](https://socket.dev/api/badge/npm/package/i18ntk/2.3.0)](https://socket.dev/npm/package/i18ntk/overview/2.3.0)
13
13
 
14
14
  ## Upgrade Notice
15
15
 
16
- Versions earlier than `2.2.0` may contain known stability and security issues.
17
- They are considered unsupported for production use. Upgrade to `2.2.0` or newer.
16
+ Versions earlier than `2.3.0` may contain known stability and security issues.
17
+ They are considered unsupported for production use. Upgrade to `2.3.0` or newer.
18
18
 
19
- ## Why i18ntk
19
+ ## What i18ntk Does
20
20
 
21
21
  - Zero runtime dependencies
22
- - Works across JS/TS, React, Vue, Angular, and generic projects
23
- - Supports non-interactive CI runs (`--no-prompt`)
24
- - Includes usage/coverage validation and missing-key completion
25
- - Ships with runtime translation helpers via `i18ntk/runtime`
22
+ - Interactive and non-interactive project setup
23
+ - Translation completeness analysis and usage tracking
24
+ - Validation, sizing, and summary reporting
25
+ - Missing-key completion and fixer workflows
26
+ - Runtime translation helpers for application code
27
+ - Support for JS/TS, React, Vue, Angular, and generic projects
28
+
29
+ ## Getting Started
30
+
31
+ 1. Install the package.
32
+ 2. Run `i18ntk` or `i18ntk --command=init` to initialize the project.
33
+ 3. Confirm the source language and locale directories.
34
+ 4. Run `i18ntk --command=analyze` or `i18ntk --command=validate` to inspect translation coverage.
35
+ 5. Use `i18ntk --command=complete` to fill missing keys when needed.
36
+
37
+ The full onboarding flow is documented in [docs/getting-started.md](docs/getting-started.md).
26
38
 
27
39
  ## Install
28
40
 
29
41
  ```bash
30
- # global (recommended for CLI use)
42
+ # global CLI use
31
43
  npm install -g i18ntk
32
44
 
33
- # local
45
+ # local project use
34
46
  npm install --save-dev i18ntk
35
47
 
36
- # one-off
48
+ # one-off execution
37
49
  npx i18ntk --help
38
50
  ```
39
51
 
40
- ## Quick Start
52
+ ## Setup
53
+
54
+ The toolkit stores project configuration in `.i18ntk-config` at the project root.
55
+
56
+ Recommended setup flow:
41
57
 
42
58
  ```bash
43
- # initialize locales/project settings
59
+ i18ntk
60
+ # or
44
61
  i18ntk --command=init
62
+ ```
45
63
 
46
- # analyze translation completeness
47
- i18ntk --command=analyze
64
+ During setup, you can define:
48
65
 
49
- # validate translation structure/content
50
- i18ntk --command=validate
66
+ - source directory
67
+ - source language
68
+ - UI language
69
+ - framework preference
70
+ - output directory
71
+ - backup behavior
51
72
 
52
- # complete missing keys
53
- i18ntk --command=complete
54
- ```
73
+ If you run in CI or a non-interactive shell, use:
55
74
 
56
- ## Command Model (v2)
75
+ ```bash
76
+ i18ntk --command=init --no-prompt
77
+ ```
57
78
 
58
- Primary CLI:
79
+ ## Daily Use
59
80
 
60
81
  ```bash
61
- i18ntk
62
- i18ntk --command=init
63
82
  i18ntk --command=analyze
64
83
  i18ntk --command=validate
65
84
  i18ntk --command=usage
@@ -67,10 +86,9 @@ i18ntk --command=scanner
67
86
  i18ntk --command=sizing
68
87
  i18ntk --command=complete
69
88
  i18ntk --command=summary
70
- i18ntk --command=debug
71
89
  ```
72
90
 
73
- Standalone executables:
91
+ Standalone commands are also available:
74
92
 
75
93
  ```bash
76
94
  i18ntk-init
@@ -97,15 +115,43 @@ i18ntk-backup
97
115
  - `--dry-run`
98
116
  - `--help`
99
117
 
100
- ## Configuration
118
+ Example:
101
119
 
102
- i18ntk reads project settings from `.i18ntk-config` in your project root.
120
+ ```bash
121
+ i18ntk --command=analyze --source-dir=./src --i18n-dir=./locales --output-dir=./i18ntk-reports
122
+ ```
103
123
 
104
- Example:
124
+ ## Runtime API
125
+
126
+ Use `i18ntk/runtime` when your application needs to read locale JSON files at runtime.
127
+
128
+ ```js
129
+ const runtime = require('i18ntk/runtime');
130
+
131
+ runtime.initRuntime({
132
+ baseDir: './locales',
133
+ language: 'en',
134
+ fallbackLanguage: 'en',
135
+ keySeparator: '.',
136
+ preload: true
137
+ });
138
+
139
+ console.log(runtime.t('common.hello'));
140
+ runtime.setLanguage('fr');
141
+ console.log(runtime.getLanguage());
142
+ console.log(runtime.getAvailableLanguages());
143
+ runtime.refresh('fr');
144
+ ```
145
+
146
+ For a deeper walkthrough, see [docs/runtime.md](docs/runtime.md).
147
+
148
+ ## Configuration
149
+
150
+ Example `.i18ntk-config`:
105
151
 
106
152
  ```json
107
153
  {
108
- "version": "2.2.0",
154
+ "version": "2.3.0",
109
155
  "sourceDir": "./locales",
110
156
  "i18nDir": "./locales",
111
157
  "outputDir": "./i18ntk-reports",
@@ -117,32 +163,19 @@ Example:
117
163
  }
118
164
  ```
119
165
 
120
- ## Runtime API
121
-
122
- ```ts
123
- import { initRuntime, t, setLanguage, getLanguage } from 'i18ntk/runtime';
124
-
125
- initRuntime({
126
- baseDir: './locales',
127
- language: 'en',
128
- fallbackLanguage: 'en',
129
- preload: true
130
- });
131
-
132
- console.log(t('common.hello'));
133
- setLanguage('fr');
134
- console.log(getLanguage());
135
- ```
166
+ See [docs/api/CONFIGURATION.md](docs/api/CONFIGURATION.md) for the full configuration model.
136
167
 
137
- ## Documentation
168
+ ## Docs
138
169
 
139
170
  - [Documentation Index](docs/README.md)
171
+ - [Getting Started](docs/getting-started.md)
140
172
  - [API Reference](docs/api/API_REFERENCE.md)
141
173
  - [Configuration Guide](docs/api/CONFIGURATION.md)
142
174
  - [Runtime API Guide](docs/runtime.md)
143
175
  - [Scanner Guide](docs/scanner-guide.md)
144
176
  - [Environment Variables](docs/environment-variables.md)
145
- - [Migration Guide v2.2.0](docs/migration-guide-v2.2.0.md)
177
+ - [Migration Guide v2.3.0](docs/migration-guide-v2.3.0.md)
178
+ - [Optimization Prompt](docs/development/package-optimization-prompt.md)
146
179
 
147
180
  ## License
148
181
 
@@ -2,12 +2,12 @@
2
2
 
3
3
  'use strict';
4
4
 
5
- const fs = require('fs/promises');
6
- const path = require('path');
7
- const { existsSync } = require('fs');
8
- const configManager = require('../utils/config-manager');
9
- const { logger } = require('../utils/logger');
10
- const { colors } = require('../utils/logger');
5
+ const fs = require('fs');
6
+ const fsp = fs.promises;
7
+ const path = require('path');
8
+ const configManager = require('../utils/config-manager');
9
+ const { logger } = require('../utils/logger');
10
+ const { colors } = require('../utils/logger');
11
11
  const SecurityUtils = require('../utils/security');
12
12
 
13
13
  /**
@@ -16,10 +16,10 @@ const SecurityUtils = require('../utils/security');
16
16
  * Class-based implementation of backup functionality for use with CommandRouter
17
17
  */
18
18
  class I18nBackup {
19
- constructor(config = {}) {
20
- this.config = config;
21
- this.backupDir = path.join(process.cwd(), 'i18n-backups');
22
- this.maxBackups = config.backup?.maxBackups || 10;
19
+ constructor(config = {}) {
20
+ this.config = config;
21
+ this.backupDir = path.join(process.cwd(), 'i18ntk-backups');
22
+ this.maxBackups = Math.min(Math.max(parseInt(config.backup?.maxBackups, 10) || 1, 1), 3);
23
23
  }
24
24
 
25
25
  /**
@@ -93,7 +93,7 @@ Options:
93
93
 
94
94
  // Create backup directory if it doesn't exist
95
95
  try {
96
- await fs.mkdir(outputDir, { recursive: true });
96
+ await fsp.mkdir(outputDir, { recursive: true });
97
97
  logger.debug(`Created backup directory: ${outputDir}`);
98
98
  } catch (err) {
99
99
  if (err.code !== 'EEXIST') {
@@ -106,7 +106,7 @@ Options:
106
106
  // Validate directory
107
107
  const sourceDir = path.resolve(dir);
108
108
  try {
109
- const stats = await fs.stat(sourceDir);
109
+ const stats = await fsp.stat(sourceDir);
110
110
  if (!stats.isDirectory()) {
111
111
  throw new Error(`Path exists but is not a directory: ${sourceDir}`);
112
112
  }
@@ -121,7 +121,7 @@ Options:
121
121
  logger.info('\nCreating backup...');
122
122
 
123
123
  // Read all files in the directory
124
- const files = (await fs.readdir(sourceDir, { withFileTypes: true }))
124
+ const files = (await fsp.readdir(sourceDir, { withFileTypes: true }))
125
125
  .filter(dirent => dirent.isFile() && dirent.name.endsWith('.json'))
126
126
  .map(dirent => dirent.name);
127
127
 
@@ -135,7 +135,7 @@ Options:
135
135
  for (const file of files) {
136
136
  const filePath = path.join(sourceDir, file);
137
137
  try {
138
- const content = JSON.parse(await fs.readFile(filePath, 'utf8'));
138
+ const content = JSON.parse(await fsp.readFile(filePath, 'utf8'));
139
139
  translations[file] = content;
140
140
  } catch (error) {
141
141
  logger.error(`Could not read file ${file}: ${error.message}`);
@@ -143,8 +143,8 @@ Options:
143
143
  }
144
144
 
145
145
  // Create the backup
146
- await fs.writeFile(backupPath, JSON.stringify(translations, null, 2));
147
- const stats = await fs.stat(backupPath);
146
+ await fsp.writeFile(backupPath, JSON.stringify(translations, null, 2));
147
+ const stats = await fsp.stat(backupPath);
148
148
 
149
149
  logger.success('Backup created successfully');
150
150
  logger.info(` Location: ${backupPath}`);
@@ -218,7 +218,7 @@ Options:
218
218
  try {
219
219
  // Ensure backup directory exists
220
220
  try {
221
- await fs.access(this.backupDir);
221
+ await fsp.access(this.backupDir);
222
222
  } catch (err) {
223
223
  if (err.code === 'ENOENT') {
224
224
  logger.warn('No backups found. The backup directory does not exist yet.');
@@ -228,14 +228,14 @@ Options:
228
228
  return { success: true, backups: [] };
229
229
  }
230
230
 
231
- const files = await fs.readdir(this.backupDir);
231
+ const files = await fsp.readdir(this.backupDir);
232
232
  const backups = [];
233
233
 
234
234
  for (const file of files) {
235
235
  if (file.startsWith('backup-') && file.endsWith('.json')) {
236
236
  try {
237
237
  const filePath = path.join(this.backupDir, file);
238
- const stats = await fs.stat(filePath);
238
+ const stats = await fsp.stat(filePath);
239
239
  backups.push({
240
240
  name: file,
241
241
  path: filePath,
@@ -298,14 +298,14 @@ Options:
298
298
  logger.info('\nVerifying backup...');
299
299
 
300
300
  try {
301
- const data = await fs.readFile(backupPath, 'utf8');
301
+ const data = await fsp.readFile(backupPath, 'utf8');
302
302
  const content = JSON.parse(data);
303
303
 
304
304
  if (typeof content === 'object' && content !== null) {
305
305
  const fileCount = Object.keys(content).length;
306
306
  logger.success('Backup is valid');
307
307
  logger.info(` Contains ${fileCount} translation files`);
308
- logger.info(` Last modified: ${(await fs.stat(backupPath)).mtime.toLocaleString()}`);
308
+ logger.info(` Last modified: ${(await fsp.stat(backupPath)).mtime.toLocaleString()}`);
309
309
 
310
310
  return {
311
311
  success: true,
@@ -334,14 +334,14 @@ Options:
334
334
  logger.info('\nCleaning up old backups...');
335
335
 
336
336
  try {
337
- const files = await fs.readdir(this.backupDir);
337
+ const files = await fsp.readdir(this.backupDir);
338
338
  const backupFiles = files
339
339
  .filter(file => file.startsWith('backup-') && file.endsWith('.json'))
340
340
  .map(file => ({
341
341
  name: file,
342
- path: path.join(this.backupDir, file),
343
- time: fs.statSync(path.join(this.backupDir, file)).mtime.getTime()
344
- }))
342
+ path: path.join(this.backupDir, file),
343
+ time: fs.statSync(path.join(this.backupDir, file)).mtime.getTime()
344
+ }))
345
345
  .sort((a, b) => b.time - a.time);
346
346
 
347
347
  // Keep only the most recent 'keep' files
@@ -355,7 +355,7 @@ Options:
355
355
  // Delete old backups
356
356
  for (const file of toDelete) {
357
357
  try {
358
- await fs.unlink(file.path);
358
+ await fsp.unlink(file.path);
359
359
  logger.info(` - Deleted: ${file.name}`);
360
360
  } catch (err) {
361
361
  logger.error(` - Failed to delete ${file.name}: ${err.message}`);
@@ -374,12 +374,10 @@ Options:
374
374
  } catch (error) {
375
375
  logger.error('Error cleaning up backups:');
376
376
  logger.error(` ${error.message}`);
377
- if (process.env.DEBUG) {
378
- console.error(error);
379
- }
380
- throw error;
381
- }
382
- }
377
+ logger.debug(error.stack || error.message);
378
+ throw error;
379
+ }
380
+ }
383
381
 
384
382
  async cleanupOldBackups(outputDir) {
385
383
  try {
@@ -408,13 +406,11 @@ Options:
408
406
  }
409
407
  }
410
408
 
411
- handleError(error) {
412
- logger.error('Backup operation failed:');
413
- logger.error(` ${error.message}`);
414
- if (process.env.DEBUG) {
415
- console.error(error);
416
- }
417
- }
418
- }
409
+ handleError(error) {
410
+ logger.error('Backup operation failed:');
411
+ logger.error(` ${error.message}`);
412
+ logger.debug(error.stack || error.message);
413
+ }
414
+ }
419
415
 
420
- module.exports = I18nBackup;
416
+ module.exports = I18nBackup;
@@ -2,7 +2,8 @@
2
2
 
3
3
  'use strict';
4
4
 
5
- const fs = require('fs/promises');
5
+ const fs = require('fs');
6
+ const fsp = fs.promises;
6
7
  const path = require('path');
7
8
 
8
9
  // Simple CLI argument parser
@@ -36,16 +37,15 @@ function parseArgs(args) {
36
37
 
37
38
  return result;
38
39
  }
39
- const { existsSync } = require('fs');
40
- const configManager = require('../utils/config-manager');
41
- const { logger } = require('../utils/logger');
42
- const { colors } = require('../utils/logger');
40
+ const configManager = require('../utils/config-manager');
41
+ const { logger } = require('../utils/logger');
42
+ const { colors } = require('../utils/logger');
43
43
  const prompt = require('../utils/prompt');
44
44
 
45
45
  // Backup configuration
46
46
  const config = configManager.getConfig();
47
- const backupDir = path.join(process.cwd(), 'i18n-backups');
48
- const maxBackups = config.backup?.maxBackups || 10;
47
+ const backupDir = path.join(process.cwd(), 'i18ntk-backups');
48
+ const maxBackups = Math.min(Math.max(parseInt(config.backup?.maxBackups, 10) || 1, 1), 3);
49
49
 
50
50
  // Main function to handle commands
51
51
  async function main() {
@@ -121,7 +121,7 @@ async function handleCreate(args) {
121
121
 
122
122
  // Create backup directory if it doesn't exist
123
123
  try {
124
- await fs.mkdir(outputDir, { recursive: true });
124
+ await fsp.mkdir(outputDir, { recursive: true });
125
125
  logger.debug(`Created backup directory: ${outputDir}`);
126
126
  } catch (err) {
127
127
  if (err.code !== 'EEXIST') {
@@ -134,7 +134,7 @@ async function handleCreate(args) {
134
134
  // Validate directory
135
135
  const sourceDir = path.resolve(dir);
136
136
  try {
137
- const stats = await fs.stat(sourceDir);
137
+ const stats = await fsp.stat(sourceDir);
138
138
  if (!stats.isDirectory()) {
139
139
  throw new Error(`Path exists but is not a directory: ${sourceDir}`);
140
140
  }
@@ -149,7 +149,7 @@ async function handleCreate(args) {
149
149
  logger.info('\nCreating backup...');
150
150
 
151
151
  // Read all files in the directory
152
- const files = (await fs.readdir(sourceDir, { withFileTypes: true }))
152
+ const files = (await fsp.readdir(sourceDir, { withFileTypes: true }))
153
153
  .filter(dirent => dirent.isFile() && dirent.name.endsWith('.json'))
154
154
  .map(dirent => dirent.name);
155
155
 
@@ -163,7 +163,7 @@ async function handleCreate(args) {
163
163
  for (const file of files) {
164
164
  const filePath = path.join(sourceDir, file);
165
165
  try {
166
- const content = JSON.parse(await fs.readFile(filePath, 'utf8'));
166
+ const content = JSON.parse(await fsp.readFile(filePath, 'utf8'));
167
167
  translations[file] = content;
168
168
  } catch (error) {
169
169
  logger.error(`Could not read file ${file}: ${error.message}`);
@@ -171,8 +171,8 @@ async function handleCreate(args) {
171
171
  }
172
172
 
173
173
  // Create the backup
174
- await fs.writeFile(backupPath, JSON.stringify(translations, null, 2));
175
- const stats = await fs.stat(backupPath);
174
+ await fsp.writeFile(backupPath, JSON.stringify(translations, null, 2));
175
+ const stats = await fsp.stat(backupPath);
176
176
 
177
177
  logger.success('Backup created successfully');
178
178
  logger.info(` Location: ${backupPath}`);
@@ -203,12 +203,12 @@ async function handleRestore(args) {
203
203
 
204
204
  try {
205
205
  // Read the backup file
206
- const backupData = await fs.readFile(backupPath, 'utf8');
206
+ const backupData = await fsp.readFile(backupPath, 'utf8');
207
207
  const translations = JSON.parse(backupData);
208
208
 
209
209
  // Create output directory if it doesn't exist
210
210
  try {
211
- await fs.mkdir(outputDir, { recursive: true });
211
+ await fsp.mkdir(outputDir, { recursive: true });
212
212
  } catch (err) {
213
213
  if (err.code !== 'EEXIST') throw err;
214
214
  }
@@ -216,7 +216,7 @@ async function handleRestore(args) {
216
216
  // Write the restored files
217
217
  for (const [file, content] of Object.entries(translations)) {
218
218
  const filePath = path.join(outputDir, file);
219
- await fs.writeFile(filePath, JSON.stringify(content, null, 2));
219
+ await fsp.writeFile(filePath, JSON.stringify(content, null, 2));
220
220
  }
221
221
 
222
222
  logger.success('Backup restored successfully');
@@ -230,7 +230,7 @@ async function handleList() {
230
230
  try {
231
231
  // Ensure backup directory exists
232
232
  try {
233
- await fs.access(backupDir);
233
+ await fsp.access(backupDir);
234
234
  } catch (err) {
235
235
  if (err.code === 'ENOENT') {
236
236
  logger.warn('No backups found. The backup directory does not exist yet.');
@@ -240,14 +240,14 @@ async function handleList() {
240
240
  return;
241
241
  }
242
242
 
243
- const files = await fs.readdir(backupDir);
243
+ const files = await fsp.readdir(backupDir);
244
244
  const backups = [];
245
245
 
246
246
  for (const file of files) {
247
247
  if (file.startsWith('backup-') && file.endsWith('.json')) {
248
248
  try {
249
249
  const filePath = path.join(backupDir, file);
250
- const stats = await fs.stat(filePath);
250
+ const stats = await fsp.stat(filePath);
251
251
  backups.push({
252
252
  name: file,
253
253
  path: filePath,
@@ -307,14 +307,14 @@ async function handleVerify(args) {
307
307
  logger.info('\nVerifying backup...');
308
308
 
309
309
  try {
310
- const data = await fs.readFile(backupPath, 'utf8');
310
+ const data = await fsp.readFile(backupPath, 'utf8');
311
311
  const content = JSON.parse(data);
312
312
 
313
313
  if (typeof content === 'object' && content !== null) {
314
314
  const fileCount = Object.keys(content).length;
315
315
  logger.success('Backup is valid');
316
316
  logger.info(` Contains ${fileCount} translation files`);
317
- logger.info(` Last modified: ${(await fs.stat(backupPath)).mtime.toLocaleString()}`);
317
+ logger.info(` Last modified: ${(await fsp.stat(backupPath)).mtime.toLocaleString()}`);
318
318
  } else {
319
319
  throw new Error('Invalid backup format');
320
320
  }
@@ -331,13 +331,13 @@ async function handleCleanup(args) {
331
331
  logger.info('\nCleaning up old backups...');
332
332
 
333
333
  try {
334
- const files = await fs.readdir(backupDir);
334
+ const files = await fsp.readdir(backupDir);
335
335
  const backupFiles = files
336
336
  .filter(file => file.startsWith('backup-') && file.endsWith('.json'))
337
337
  .map(file => ({
338
338
  name: file,
339
339
  path: path.join(backupDir, file),
340
- time: fs.statSync(path.join(backupDir, file)).mtime.getTime()
340
+ time: fs.statSync(path.join(backupDir, file)).mtime.getTime()
341
341
  }))
342
342
  .sort((a, b) => b.time - a.time);
343
343
 
@@ -352,7 +352,7 @@ async function handleCleanup(args) {
352
352
  // Delete old backups
353
353
  for (const file of toDelete) {
354
354
  try {
355
- await fs.unlink(file.path);
355
+ await fsp.unlink(file.path);
356
356
  logger.info(` - Deleted: ${file.name}`);
357
357
  } catch (err) {
358
358
  logger.error(` - Failed to delete ${file.name}: ${err.message}`);
@@ -365,12 +365,10 @@ async function handleCleanup(args) {
365
365
  } catch (error) {
366
366
  logger.error('Error cleaning up backups:');
367
367
  logger.error(` ${error.message}`);
368
- if (process.env.DEBUG) {
369
- console.error(error);
370
- }
371
- process.exit(1);
372
- }
373
- }
368
+ logger.debug(error.stack || error.message);
369
+ process.exit(1);
370
+ }
371
+ }
374
372
 
375
373
  // Start the application
376
374
  // Handle unhandled promise rejections
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- const SecurityUtils = require('../utils/security');
3
- const fs = require('fs');
4
- const path = require('path');
5
- const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../utils/config-helper');
2
+ const SecurityUtils = require('../utils/security');
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const packageJson = require('../package.json');
6
+ const { getUnifiedConfig, parseCommonArgs, displayHelp } = require('../utils/config-helper');
6
7
  const SetupEnforcer = require('../utils/setup-enforcer');
7
8
 
8
9
  // Ensure setup is complete before running (only for standalone execution)
@@ -97,7 +98,7 @@ class I18nDoctor {
97
98
  }
98
99
  }
99
100
 
100
- const pkgVersion = require('../package.json').version;
101
+ const pkgVersion = packageJson.version;
101
102
  if (config.version && config.version !== pkgVersion) {
102
103
  issues.push(`Config version mismatch: ${config.version} != ${pkgVersion}`);
103
104
  exitCode = Math.max(exitCode, ExitCodes.CONFIG_ERROR);
@@ -181,4 +182,4 @@ if (require.main === module) {
181
182
  doctor.run();
182
183
  }
183
184
 
184
- module.exports = I18nDoctor;
185
+ module.exports = I18nDoctor;
@@ -754,7 +754,7 @@ class I18nInitializer {
754
754
  }
755
755
 
756
756
  // Interactive admin PIN setup
757
- async promptAdminPinSetup() {
757
+ async promptAdminPinSetup() {
758
758
  const { ask, askHidden, flushStdout } = require('../utils/cli');
759
759
 
760
760
  console.log('\n' + t('init.adminPinSetupOptional'));
@@ -806,8 +806,31 @@ class I18nInitializer {
806
806
  }
807
807
  } else {
808
808
  console.log(t('init.skippingAdminPinSetup'));
809
- }
810
- }
809
+ }
810
+ }
811
+
812
+ async promptBackupConfiguration(skipPrompt = false) {
813
+ const defaultBackupConfig = { enabled: false, maxBackups: 1, location: './i18ntk-backups' };
814
+ if (skipPrompt || !isInteractive()) {
815
+ return null;
816
+ }
817
+
818
+ const { ask } = require('../utils/cli');
819
+ console.log('\nBackup Settings');
820
+ console.log('Backups are disabled by default to avoid backup recursion and repo pollution.');
821
+ const enableAnswer = await ask('Enable automatic backups? (y/N): ');
822
+ const enabled = ['y', 'yes'].includes(String(enableAnswer || '').trim().toLowerCase());
823
+
824
+ if (!enabled) {
825
+ return defaultBackupConfig;
826
+ }
827
+
828
+ const keepAnswer = await ask('How many backups should be kept automatically (1-3, default 1): ');
829
+ const parsedKeep = parseInt(String(keepAnswer || '').trim(), 10);
830
+ const maxBackups = Number.isInteger(parsedKeep) ? Math.min(Math.max(parsedKeep, 1), 3) : 1;
831
+
832
+ return { enabled: true, maxBackups, location: './i18ntk-backups' };
833
+ }
811
834
 
812
835
  // Interactive language selection
813
836
  async selectLanguages(skipPrompt = false) {
@@ -921,11 +944,24 @@ class I18nInitializer {
921
944
  // Prompt for admin PIN setup if not already configured
922
945
  const securitySettings = configManager.getConfig().security || {};
923
946
 
924
- if (!securitySettings.adminPinEnabled && securitySettings.adminPinPromptOnInit !== false && !args.noPrompt) {
925
- const { flushStdout } = require('../utils/cli');
926
- await flushStdout();
927
- await this.promptAdminPinSetup();
928
- }
947
+ if (!securitySettings.adminPinEnabled && securitySettings.adminPinPromptOnInit !== false && !args.noPrompt) {
948
+ const { flushStdout } = require('../utils/cli');
949
+ await flushStdout();
950
+ await this.promptAdminPinSetup();
951
+ }
952
+
953
+ const backupSettings = await this.promptBackupConfiguration(args.noPrompt);
954
+ if (backupSettings) {
955
+ await configManager.updateConfig({
956
+ backup: {
957
+ ...(this.config.backup || {}),
958
+ ...backupSettings
959
+ }
960
+ });
961
+ this.config.backup = { ...(this.config.backup || {}), ...backupSettings };
962
+ } else if (!this.config.backup) {
963
+ this.config.backup = { enabled: false, maxBackups: 1, location: './i18ntk-backups' };
964
+ }
929
965
 
930
966
  // Get target languages - use args.languages if provided
931
967
  let targetLanguages = args.languages || await this.selectLanguages(args.noPrompt);