i18ntk 1.10.2 → 2.0.3

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 (108) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +141 -1191
  3. package/main/i18ntk-analyze.js +65 -84
  4. package/main/i18ntk-backup-class.js +420 -0
  5. package/main/i18ntk-backup.js +3 -3
  6. package/main/i18ntk-complete.js +90 -65
  7. package/main/i18ntk-doctor.js +123 -103
  8. package/main/i18ntk-fixer.js +61 -725
  9. package/main/i18ntk-go.js +14 -15
  10. package/main/i18ntk-init.js +77 -26
  11. package/main/i18ntk-java.js +27 -32
  12. package/main/i18ntk-js.js +70 -68
  13. package/main/i18ntk-manage.js +129 -30
  14. package/main/i18ntk-php.js +75 -75
  15. package/main/i18ntk-py.js +55 -56
  16. package/main/i18ntk-scanner.js +59 -57
  17. package/main/i18ntk-setup.js +9 -404
  18. package/main/i18ntk-sizing.js +6 -6
  19. package/main/i18ntk-summary.js +21 -18
  20. package/main/i18ntk-ui.js +11 -10
  21. package/main/i18ntk-usage.js +54 -18
  22. package/main/i18ntk-validate.js +13 -13
  23. package/main/manage/commands/AnalyzeCommand.js +1124 -0
  24. package/main/manage/commands/BackupCommand.js +62 -0
  25. package/main/manage/commands/CommandRouter.js +295 -0
  26. package/main/manage/commands/CompleteCommand.js +61 -0
  27. package/main/manage/commands/DoctorCommand.js +60 -0
  28. package/main/manage/commands/FixerCommand.js +624 -0
  29. package/main/manage/commands/InitCommand.js +62 -0
  30. package/main/manage/commands/ScannerCommand.js +654 -0
  31. package/main/manage/commands/SizingCommand.js +60 -0
  32. package/main/manage/commands/SummaryCommand.js +61 -0
  33. package/main/manage/commands/UsageCommand.js +60 -0
  34. package/main/manage/commands/ValidateCommand.js +978 -0
  35. package/main/manage/index-fixed.js +1447 -0
  36. package/main/manage/index.js +1462 -0
  37. package/main/manage/managers/DebugMenu.js +140 -0
  38. package/main/manage/managers/InteractiveMenu.js +177 -0
  39. package/main/manage/managers/LanguageMenu.js +62 -0
  40. package/main/manage/managers/SettingsMenu.js +53 -0
  41. package/main/manage/services/AuthenticationService.js +263 -0
  42. package/main/manage/services/ConfigurationService-fixed.js +449 -0
  43. package/main/manage/services/ConfigurationService.js +449 -0
  44. package/main/manage/services/FileManagementService.js +368 -0
  45. package/main/manage/services/FrameworkDetectionService.js +458 -0
  46. package/main/manage/services/InitService.js +1051 -0
  47. package/main/manage/services/SetupService.js +462 -0
  48. package/main/manage/services/SummaryService.js +450 -0
  49. package/main/manage/services/UsageService.js +1502 -0
  50. package/package.json +32 -29
  51. package/runtime/enhanced.d.ts +221 -221
  52. package/runtime/index.d.ts +29 -29
  53. package/runtime/index.full.d.ts +331 -331
  54. package/runtime/index.js +7 -6
  55. package/scripts/build-lite.js +17 -17
  56. package/scripts/deprecate-versions.js +23 -6
  57. package/scripts/export-translations.js +5 -5
  58. package/scripts/fix-all-i18n.js +3 -3
  59. package/scripts/fix-and-purify-i18n.js +3 -2
  60. package/scripts/fix-locale-control-chars.js +110 -0
  61. package/scripts/lint-locales.js +80 -0
  62. package/scripts/locale-optimizer.js +8 -8
  63. package/scripts/prepublish.js +21 -21
  64. package/scripts/security-check.js +117 -117
  65. package/scripts/sync-translations.js +4 -4
  66. package/scripts/sync-ui-locales.js +9 -8
  67. package/scripts/validate-all-translations.js +8 -7
  68. package/scripts/verify-deprecations.js +157 -161
  69. package/scripts/verify-translations.js +6 -5
  70. package/settings/i18ntk-config.json +282 -282
  71. package/settings/language-config.json +5 -5
  72. package/settings/settings-cli.js +9 -9
  73. package/settings/settings-manager.js +18 -18
  74. package/ui-locales/de.json +2417 -2348
  75. package/ui-locales/en.json +2415 -2352
  76. package/ui-locales/es.json +2425 -2353
  77. package/ui-locales/fr.json +2418 -2348
  78. package/ui-locales/ja.json +2463 -2361
  79. package/ui-locales/ru.json +2463 -2359
  80. package/ui-locales/zh.json +2418 -2351
  81. package/utils/admin-auth.js +2 -2
  82. package/utils/admin-cli.js +297 -297
  83. package/utils/admin-pin.js +9 -9
  84. package/utils/cli-helper.js +9 -9
  85. package/utils/config-helper.js +73 -104
  86. package/utils/config-manager.js +204 -171
  87. package/utils/config.js +5 -4
  88. package/utils/env-manager.js +249 -263
  89. package/utils/framework-detector.js +27 -24
  90. package/utils/i18n-helper.js +85 -41
  91. package/utils/init-helper.js +152 -94
  92. package/utils/json-output.js +98 -98
  93. package/utils/mini-commander.js +179 -0
  94. package/utils/missing-key-validator.js +5 -5
  95. package/utils/plugin-loader.js +40 -29
  96. package/utils/prompt.js +14 -44
  97. package/utils/safe-json.js +40 -0
  98. package/utils/secure-errors.js +3 -3
  99. package/utils/security-check-improved.js +390 -0
  100. package/utils/security-config.js +5 -5
  101. package/utils/security-fixed.js +607 -0
  102. package/utils/security.js +652 -602
  103. package/utils/setup-enforcer.js +136 -44
  104. package/utils/setup-validator.js +33 -32
  105. package/utils/ultra-performance-optimizer.js +11 -9
  106. package/utils/watch-locales.js +2 -1
  107. package/utils/prompt-fixed.js +0 -55
  108. package/utils/security-check.js +0 -454
package/runtime/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
 
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
+ const SecurityUtils = require('../utils/security');
8
9
 
9
10
  let configManager = null;
10
11
  try { configManager = require('../utils/config-manager'); } catch (_) { /* optional */ }
@@ -27,7 +28,7 @@ function stripBOMAndComments(s) {
27
28
  }
28
29
 
29
30
  function readJsonSafe(file) {
30
- const raw = fs.readFileSync(file, 'utf8');
31
+ const raw = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8');
31
32
  return JSON.parse(stripBOMAndComments(raw));
32
33
  }
33
34
 
@@ -85,7 +86,7 @@ function listJsonFilesRecursively(dir) {
85
86
  const stack = [dir];
86
87
  while (stack.length) {
87
88
  const d = stack.pop();
88
- if (!fs.existsSync(d)) continue;
89
+ if (!SecurityUtils.safeExistsSync(d)) continue;
89
90
  for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
90
91
  const full = path.join(d, entry.name);
91
92
  if (entry.isDirectory()) {
@@ -104,7 +105,7 @@ function readLanguageFromBase(baseDir, lang) {
104
105
  const langDir = path.join(baseDir, lang);
105
106
 
106
107
  // Prefer folder if exists, otherwise single file
107
- if (fs.existsSync(langDir) && fs.statSync(langDir).isDirectory()) {
108
+ if (SecurityUtils.safeExistsSync(langDir) && fs.statSync(langDir).isDirectory()) {
108
109
  const files = listJsonFilesRecursively(langDir);
109
110
  for (const file of files) {
110
111
  try {
@@ -114,7 +115,7 @@ function readLanguageFromBase(baseDir, lang) {
114
115
  // Skip unreadable/invalid files
115
116
  }
116
117
  }
117
- } else if (fs.existsSync(langFile) && fs.statSync(langFile).isFile()) {
118
+ } else if (SecurityUtils.safeExistsSync(langFile) && fs.statSync(langFile).isFile()) {
118
119
  try {
119
120
  const data = readJsonSafe(langFile);
120
121
  if (data && typeof data === 'object') deepMerge(merged, data);
@@ -203,7 +204,7 @@ function getLanguage() {
203
204
  function getAvailableLanguages() {
204
205
  const langs = new Set();
205
206
  if (!state.baseDir) state.baseDir = resolveBaseDir();
206
- if (!fs.existsSync(state.baseDir)) return ['en'];
207
+ if (!SecurityUtils.safeExistsSync(state.baseDir)) return ['en'];
207
208
  for (const entry of fs.readdirSync(state.baseDir, { withFileTypes: true })) {
208
209
  if (entry.isFile() && entry.name.toLowerCase().endsWith('.json')) {
209
210
  langs.add(entry.name.replace(/\.json$/i, ''));
@@ -211,7 +212,7 @@ function getAvailableLanguages() {
211
212
  // language folder convention
212
213
  const lang = entry.name;
213
214
  const idx = path.join(state.baseDir, lang, `${lang}.json`);
214
- if (fs.existsSync(idx)) langs.add(lang);
215
+ if (SecurityUtils.safeExistsSync(idx)) langs.add(lang);
215
216
  else langs.add(lang); // be permissive
216
217
  }
217
218
  }
@@ -54,7 +54,7 @@ class LiteBuild {
54
54
  }
55
55
 
56
56
  async cleanBuildDir() {
57
- if (fs.existsSync(this.buildDir)) {
57
+ if (SecurityUtils.safeExistsSync(this.buildDir)) {
58
58
  fs.rmSync(this.buildDir, { recursive: true, force: true });
59
59
  }
60
60
  fs.mkdirSync(this.buildDir, { recursive: true });
@@ -65,7 +65,7 @@ class LiteBuild {
65
65
  'main',
66
66
  'utils',
67
67
  'settings',
68
- 'ui-locales/en',
68
+ 'resources/i18n/ui-locales/en',
69
69
  'scripts'
70
70
  ];
71
71
 
@@ -77,7 +77,7 @@ class LiteBuild {
77
77
  async copyEssentialFiles() {
78
78
  const essentialFiles = [
79
79
  // Main CLI scripts
80
- { src: 'main/i18ntk-manage.js', dest: 'main/i18ntk-manage.js' },
80
+ { src: 'main/manage/index.js', dest: 'main/manage/index.js' },
81
81
  { src: 'main/i18ntk-init.js', dest: 'main/i18ntk-init.js' },
82
82
  { src: 'main/i18ntk-analyze.js', dest: 'main/i18ntk-analyze.js' },
83
83
  { src: 'main/i18ntk-validate.js', dest: 'main/i18ntk-validate.js' },
@@ -106,17 +106,17 @@ class LiteBuild {
106
106
  const srcPath = path.join(this.projectRoot, src);
107
107
  const destPath = path.join(this.buildDir, dest);
108
108
 
109
- if (fs.existsSync(srcPath)) {
109
+ if (SecurityUtils.safeExistsSync(srcPath)) {
110
110
  fs.copyFileSync(srcPath, destPath);
111
111
  }
112
112
  });
113
113
  }
114
114
 
115
115
  async copyEnglishLocale() {
116
- const enLocaleDir = path.join(this.projectRoot, 'ui-locales', 'en');
117
- const destDir = path.join(this.buildDir, 'ui-locales', 'en');
116
+ const enLocaleDir = path.join(this.projectRoot, 'resources', 'i18n', 'ui-locales', 'en');
117
+ const destDir = path.join(this.buildDir, 'resources', 'i18n', 'ui-locales', 'en');
118
118
 
119
- if (fs.existsSync(enLocaleDir)) {
119
+ if (SecurityUtils.safeExistsSync(enLocaleDir)) {
120
120
  const files = fs.readdirSync(enLocaleDir);
121
121
  files.forEach(file => {
122
122
  if (file.endsWith('.json')) {
@@ -137,7 +137,7 @@ class LiteBuild {
137
137
  main: this.packageJson.main,
138
138
  bin: this.packageJson.bin,
139
139
  scripts: {
140
- start: 'node main/i18ntk-manage.js',
140
+ start: 'node main/manage/index.js',
141
141
  analyze: 'node main/i18ntk-analyze.js',
142
142
  validate: 'node main/i18ntk-validate.js'
143
143
  },
@@ -150,7 +150,7 @@ class LiteBuild {
150
150
  homepage: this.packageJson.homepage
151
151
  };
152
152
 
153
- fs.writeFileSync(
153
+ SecurityUtils.safeWriteFileSync(
154
154
  path.join(this.buildDir, 'package.json'),
155
155
  JSON.stringify(litePackageJson, null, 2)
156
156
  );
@@ -167,12 +167,12 @@ docs/
167
167
  .i18ntk/
168
168
 
169
169
  # Non-English locales
170
- ui-locales/es/
171
- ui-locales/fr/
172
- ui-locales/de/
173
- ui-locales/ja/
174
- ui-locales/ru/
175
- ui-locales/zh/
170
+ resources/i18n/ui-locales/es/
171
+ resources/i18n/ui-locales/fr/
172
+ resources/i18n/ui-locales/de/
173
+ resources/i18n/ui-locales/ja/
174
+ resources/i18n/ui-locales/ru/
175
+ resources/i18n/ui-locales/zh/
176
176
 
177
177
  # Debug and test files
178
178
  *.test.js
@@ -205,7 +205,7 @@ Thumbs.db
205
205
  .DS_Store
206
206
  `;
207
207
 
208
- fs.writeFileSync(
208
+ SecurityUtils.safeWriteFileSync(
209
209
  path.join(this.buildDir, '.npmignore'),
210
210
  npmIgnoreContent.trim()
211
211
  );
@@ -225,7 +225,7 @@ Thumbs.db
225
225
  }
226
226
  };
227
227
 
228
- fs.writeFileSync(
228
+ SecurityUtils.safeWriteFileSync(
229
229
  path.join(this.buildDir, 'build-report.json'),
230
230
  JSON.stringify(report, null, 2)
231
231
  );
@@ -12,13 +12,25 @@
12
12
  */
13
13
 
14
14
  const { execSync } = require('child_process');
15
- const fs = require('fs');
16
15
  const path = require('path');
16
+ const SecurityUtils = require('../utils/security');
17
17
 
18
18
  // Configuration
19
19
  const CONFIG_FILE = path.join(__dirname, '..', 'deprecation-config.json');
20
20
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
21
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
21
+
22
+ // Securely read package.json
23
+ const packageJsonContent = SecurityUtils.safeReadFileSync(packageJsonPath, path.dirname(packageJsonPath), 'utf8');
24
+ if (!packageJsonContent) {
25
+ console.error('❌ Failed to read package.json');
26
+ process.exit(1);
27
+ }
28
+
29
+ const packageJson = SecurityUtils.safeParseJSON(packageJsonContent);
30
+ if (!packageJson) {
31
+ console.error('❌ Failed to parse package.json');
32
+ process.exit(1);
33
+ }
22
34
 
23
35
  const deprecationMessage = '⚠️ DEPRECATED: This version contains security vulnerabilities and missing features. Please upgrade to i18ntk@1.10.0 for: 🔒 Zero shell access security, 🚀 97% performance improvement, 🐍 Python framework support, and 🛡️ PIN protection. Run: npm install i18ntk@latest';
24
36
 
@@ -27,11 +39,16 @@ const defaultVersionsToDeprecate = packageJson.versionInfo.deprecations;
27
39
 
28
40
  // Load admin configuration if available
29
41
  function loadAdminConfig() {
30
- if (fs.existsSync(CONFIG_FILE)) {
42
+ if (SecurityUtils.safeExistsSync(CONFIG_FILE, path.dirname(CONFIG_FILE))) {
31
43
  try {
32
- const config = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8'));
33
- console.log('📋 Using admin configuration from deprecation-config.json');
34
- return config;
44
+ const configContent = SecurityUtils.safeReadFileSync(CONFIG_FILE, path.dirname(CONFIG_FILE), 'utf8');
45
+ if (configContent) {
46
+ const config = SecurityUtils.safeParseJSON(configContent);
47
+ if (config) {
48
+ console.log('📋 Using admin configuration from deprecation-config.json');
49
+ return config;
50
+ }
51
+ }
35
52
  } catch (error) {
36
53
  console.warn('⚠️ Could not read admin config, using defaults:', error.message);
37
54
  }
@@ -8,13 +8,13 @@ const { loadTranslations, t } = require('../utils/i18n-helper');
8
8
  loadTranslations('en');
9
9
 
10
10
  // Configuration
11
- const SOURCE_DIR = path.join(__dirname, '..', 'ui-locales');
11
+ const SOURCE_DIR = path.join(__dirname, '..', 'resources', 'i18n', 'ui-locales');
12
12
  const TARGET_LANGUAGES = ['de', 'fr', 'es', 'ru', 'ja', 'zh'];
13
13
 
14
14
  function createLanguageTemplate(lang) {
15
15
  const langDir = path.join(SOURCE_DIR, lang);
16
16
 
17
- if (!fs.existsSync(langDir)) {
17
+ if (!SecurityUtils.safeExistsSync(langDir)) {
18
18
  fs.mkdirSync(langDir, { recursive: true });
19
19
  console.log(t('exportTranslations.createdDirectory', { dir: langDir }));
20
20
  }
@@ -31,10 +31,10 @@ function createLanguageTemplate(lang) {
31
31
  const sourceFile = path.join(enDir, file);
32
32
  const targetFile = path.join(langDir, file);
33
33
 
34
- if (!fs.existsSync(targetFile)) {
35
- const sourceContent = JSON.parse(fs.readFileSync(sourceFile, 'utf8'));
34
+ if (!SecurityUtils.safeExistsSync(targetFile)) {
35
+ const sourceContent = JSON.parse(SecurityUtils.safeReadFileSync(sourceFile, path.dirname(sourceFile), 'utf8'));
36
36
  const templateContent = createTemplateFromEnglish(sourceContent);
37
- fs.writeFileSync(targetFile, JSON.stringify(templateContent, null, 2));
37
+ SecurityUtils.safeWriteFileSync(targetFile, JSON.stringify(templateContent, null, 2));
38
38
  console.log(t('exportTranslations.createdTemplate', { lang, file }));
39
39
  } else {
40
40
  console.log(t('exportTranslations.skippedExisting', { lang, file }));
@@ -19,7 +19,7 @@ const argv = Object.fromEntries(
19
19
  );
20
20
 
21
21
  const SOURCE_DIR = path.resolve(argv['source-dir'] || './');
22
- const I18N_DIR = path.resolve(argv['i18n-dir'] || './ui-locales');
22
+ const I18N_DIR = path.resolve(argv['i18n-dir'] || './resources/i18n/ui-locales');
23
23
  const LANGS = (argv.languages || 'en,de,es,fr,ru,ja,zh').split(',').map(s => s.trim());
24
24
  const WRITE = !!argv.write;
25
25
  const PRUNE_EXTRAS = !!argv['prune-extras'];
@@ -36,8 +36,8 @@ const KEY_PATTERNS = [
36
36
 
37
37
  const EXCLUDE_DIRS = new Set(['node_modules', '.git', path.basename(I18N_DIR)]);
38
38
 
39
- function readUTF8(p) { try { return fs.readFileSync(p, 'utf8'); } catch { return null; } }
40
- function writeJSON(p, obj) { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf8'); }
39
+ function readUTF8(p) { try { return SecurityUtils.safeReadFileSync(p, path.dirname(p), 'utf8'); } catch { return null; } }
40
+ function writeJSON(p, obj) { fs.mkdirSync(path.dirname(p), { recursive: true }); SecurityUtils.safeWriteFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf8'); }
41
41
  function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
42
42
  function isFile(p) { try { return fs.statSync(p).isFile(); } catch { return false; } }
43
43
 
@@ -15,6 +15,7 @@
15
15
 
16
16
  const fs = require('fs');
17
17
  const path = require('path');
18
+ const SecurityUtils = require('../utils/security');
18
19
 
19
20
  // ---------------- CLI ARGUMENTS ----------------
20
21
  const argv = Object.fromEntries(
@@ -45,11 +46,11 @@ const COUNTRY_CODES = { de: 'DE', es: 'ES', fr: 'FR', ru: 'RU', ja: 'JA', zh: 'Z
45
46
 
46
47
  // ---------------- HELPERS ----------------
47
48
  function readUTF8(p) {
48
- try { return fs.readFileSync(p, 'utf8'); } catch { return null; }
49
+ try { return SecurityUtils.safeReadFileSync(p, path.dirname(p), 'utf8'); } catch { return null; }
49
50
  }
50
51
  function writeJSON(p, obj) {
51
52
  fs.mkdirSync(path.dirname(p), { recursive: true });
52
- fs.writeFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf8');
53
+ SecurityUtils.safeWriteFileSync(p, JSON.stringify(obj, null, 2) + '\n', 'utf8');
53
54
  }
54
55
  function isDir(p) { try { return fs.statSync(p).isDirectory(); } catch { return false; } }
55
56
  function isFile(p) { try { return fs.statSync(p).isFile(); } catch { return false; } }
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fix control characters in locale files
4
+ * Removes problematic newlines and other control chars from JSON strings
5
+ */
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const SecurityUtils = require('../utils/security');
9
+
10
+ const uiLocalesDirs = [
11
+ path.join(__dirname, '..', 'ui-locales'),
12
+ path.join(__dirname, '..', 'resources', 'i18n', 'ui-locales')
13
+ ];
14
+
15
+ function sanitizeValue(value) {
16
+ if (typeof value === 'string') {
17
+ // Remove/escape control characters but keep the structure
18
+ // Replace literal newlines/tabs with spaces, but preserve intended structure
19
+ return value
20
+ .replace(/[\u0000-\u0008\u000B-\u000C\u000E-\u001F]/g, '') // Remove other control chars
21
+ .replace(/\u000A/g, ' ') // Replace newlines with space
22
+ .replace(/\u000D/g, '') // Remove carriage returns
23
+ .trim();
24
+ }
25
+ if (Array.isArray(value)) {
26
+ return value.map(sanitizeValue);
27
+ }
28
+ if (value && typeof value === 'object') {
29
+ const result = {};
30
+ for (const [k, v] of Object.entries(value)) {
31
+ result[k] = sanitizeValue(v);
32
+ }
33
+ return result;
34
+ }
35
+ return value;
36
+ }
37
+
38
+ function fixFile(filePath) {
39
+ try {
40
+ const baseDir = path.dirname(filePath);
41
+ const raw = SecurityUtils.safeReadFileSync(filePath, baseDir, 'utf8');
42
+ if (!raw) {
43
+ console.error(`✗ Error reading ${path.basename(filePath)}`);
44
+ return false;
45
+ }
46
+
47
+ const data = SecurityUtils.safeParseJSON(raw);
48
+ if (!data) {
49
+ console.error(`✗ Error parsing ${path.basename(filePath)}`);
50
+ return false;
51
+ }
52
+
53
+ const locale = path.basename(filePath, '.json');
54
+
55
+ // Sanitize all values
56
+ const sanitized = sanitizeValue(data);
57
+
58
+ // Write back with proper formatting using SecurityUtils
59
+ const success = SecurityUtils.safeWriteFileSync(
60
+ filePath,
61
+ JSON.stringify(sanitized, null, 2) + '\n',
62
+ baseDir,
63
+ 'utf8'
64
+ );
65
+
66
+ if (success) {
67
+ console.log(` ✓ Fixed ${locale}.json`);
68
+ return true;
69
+ } else {
70
+ console.error(` ✗ Error writing ${path.basename(filePath)}`);
71
+ return false;
72
+ }
73
+ } catch (error) {
74
+ console.error(`✗ Error fixing ${path.basename(filePath)}: ${error.message}`);
75
+ return false;
76
+ }
77
+ }
78
+
79
+ function main() {
80
+ console.log('Fixing control characters in locale files...\n');
81
+
82
+ let fixed = 0;
83
+ let failed = 0;
84
+
85
+ for (const uiLocalesDir of uiLocalesDirs) {
86
+ if (!SecurityUtils.safeExistsSync(uiLocalesDir)) {
87
+ console.log(`ℹ️ Skipping ${uiLocalesDir} (does not exist)\n`);
88
+ continue;
89
+ }
90
+
91
+ console.log(`Processing: ${uiLocalesDir}`);
92
+ const files = SecurityUtils.safeReaddirSync(uiLocalesDir, path.dirname(uiLocalesDir))
93
+ .filter(f => f.endsWith('.json'))
94
+ .map(f => path.join(uiLocalesDir, f));
95
+
96
+ for (const file of files) {
97
+ if (fixFile(file)) {
98
+ fixed++;
99
+ } else {
100
+ failed++;
101
+ }
102
+ }
103
+ console.log();
104
+ }
105
+
106
+ console.log(`\nFixed ${fixed} file(s)${failed ? `, ${failed} failed` : ''}`);
107
+ process.exit(failed > 0 ? 1 : 0);
108
+ }
109
+
110
+ main();
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Simple locale lint:
4
+ * - flags control characters in any locale string
5
+ * - en locale: ensures a small set of CLI strings stay plain ASCII (catches mojibake)
6
+ */
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+ const SecurityUtils = require('../utils/security');
10
+
11
+ const uiLocalesDir = path.join(__dirname, '..', 'ui-locales');
12
+ const asciiOnlyKeys = new Set([
13
+ 'ui.autoDetectedI18nDirectory',
14
+ 'ui.executingCommand',
15
+ 'ui.unknownCommand',
16
+ 'ui.errorExecutingCommand',
17
+ 'ui.errorLoadingTranslationFile',
18
+ 'ui.errorSavingLanguagePreference',
19
+ 'ui.noActiveReadlineInterface',
20
+ 'ui.uiLanguageUpdated',
21
+ 'menu.invalidChoice',
22
+ 'menu.returning',
23
+ 'menu.invalidOption',
24
+ 'menu.nonInteractiveModeWarning',
25
+ 'menu.useDirectExecution',
26
+ 'menu.useHelpForCommands',
27
+ 'menu.autoDetectedI18nDirectory'
28
+ ]);
29
+
30
+ function walk(value, pathParts, onString) {
31
+ if (typeof value === 'string') {
32
+ onString(value, pathParts.join('.'));
33
+ return;
34
+ }
35
+ if (Array.isArray(value)) {
36
+ value.forEach((item, idx) => walk(item, pathParts.concat(idx), onString));
37
+ return;
38
+ }
39
+ if (value && typeof value === 'object') {
40
+ for (const [k, v] of Object.entries(value)) {
41
+ walk(v, pathParts.concat(k), onString);
42
+ }
43
+ }
44
+ }
45
+
46
+ function lintFile(filePath) {
47
+ const raw = SecurityUtils.safeReadFileSync(filePath, path.dirname(filePath), 'utf8');
48
+ if (!raw) {
49
+ return [`${path.basename(filePath, '.json')}: unable to read locale file`];
50
+ }
51
+ const data = JSON.parse(raw);
52
+ const locale = path.basename(filePath, '.json');
53
+ const issues = [];
54
+
55
+ walk(data, [], (str, keyPath) => {
56
+ if (/[\u0000-\u001F]/.test(str)) {
57
+ issues.push(`${locale}: control char in ${keyPath}`);
58
+ }
59
+ if (locale === 'en' && asciiOnlyKeys.has(keyPath) && /[^\x20-\x7E]/.test(str)) {
60
+ issues.push(`${locale}: non-ASCII in ${keyPath} -> "${str}"`);
61
+ }
62
+ });
63
+
64
+ return issues;
65
+ }
66
+
67
+ function main() {
68
+ const files = fs.readdirSync(uiLocalesDir).filter(f => f.endsWith('.json'));
69
+ const allIssues = files.flatMap(f => lintFile(path.join(uiLocalesDir, f)));
70
+
71
+ if (allIssues.length) {
72
+ console.error('Locale lint failed:');
73
+ allIssues.forEach(msg => console.error(` - ${msg}`));
74
+ process.exit(1);
75
+ }
76
+
77
+ console.log('Locale lint passed.');
78
+ }
79
+
80
+ main();
@@ -21,7 +21,7 @@ const { getGlobalReadline } = require('../utils/cli');
21
21
 
22
22
  class LocaleOptimizer {
23
23
  constructor() {
24
- this.uiLocalesDir = path.join(__dirname, '..', 'ui-locales');
24
+ this.uiLocalesDir = path.join(__dirname, '..', 'resources', 'i18n', 'ui-locales');
25
25
  this.allLocales = ['en', 'de', 'es', 'fr', 'ja', 'ru', 'zh'];
26
26
  this.backupDir = path.join(__dirname, '..', 'backups', 'locales');
27
27
  this.rl = getGlobalReadline();
@@ -139,7 +139,7 @@ class LocaleOptimizer {
139
139
  console.log(` Keeping: ${keepList.join(', ').toUpperCase()}`);
140
140
 
141
141
  // Create backup directory
142
- if (!fs.existsSync(this.backupDir)) {
142
+ if (!SecurityUtils.safeExistsSync(this.backupDir)) {
143
143
  fs.mkdirSync(this.backupDir, { recursive: true });
144
144
  }
145
145
 
@@ -151,7 +151,7 @@ class LocaleOptimizer {
151
151
  const filePath = path.join(this.uiLocalesDir, `${locale}.json`);
152
152
  const backupPath = path.join(this.backupDir, `${locale}.json`);
153
153
 
154
- if (!keepList.includes(locale) && fs.existsSync(filePath)) {
154
+ if (!keepList.includes(locale) && SecurityUtils.safeExistsSync(filePath)) {
155
155
  // Backup the file
156
156
  fs.copyFileSync(filePath, backupPath);
157
157
 
@@ -173,7 +173,7 @@ class LocaleOptimizer {
173
173
 
174
174
  // Create warning file
175
175
  const warningPath = path.join(this.backupDir, 'REMOVED_LOCALES.txt');
176
- fs.writeFileSync(warningPath, `Removed locales: ${removedLocales.join(',')}\nRestore with: node scripts/locale-optimizer.js --restore`);
176
+ SecurityUtils.safeWriteFileSync(warningPath, `Removed locales: ${removedLocales.join(',')}\nRestore with: node scripts/locale-optimizer.js --restore`);
177
177
  }
178
178
 
179
179
  const savedKB = (savedSpace / 1024).toFixed(1);
@@ -189,7 +189,7 @@ class LocaleOptimizer {
189
189
  restoreLocales() {
190
190
  console.log('🔄 Restoring locales from backup...');
191
191
 
192
- if (!fs.existsSync(this.backupDir)) {
192
+ if (!SecurityUtils.safeExistsSync(this.backupDir)) {
193
193
  console.log(' ❌ No backup directory found');
194
194
  return;
195
195
  }
@@ -348,7 +348,7 @@ class LocaleOptimizer {
348
348
  getAvailableLocales() {
349
349
  return this.allLocales.filter(locale => {
350
350
  const filePath = path.join(this.uiLocalesDir, `${locale}.json`);
351
- return fs.existsSync(filePath);
351
+ return SecurityUtils.safeExistsSync(filePath);
352
352
  });
353
353
  }
354
354
 
@@ -357,7 +357,7 @@ class LocaleOptimizer {
357
357
  */
358
358
  getLocaleSize(locale) {
359
359
  const filePath = path.join(this.uiLocalesDir, `${locale}.json`);
360
- if (fs.existsSync(filePath)) {
360
+ if (SecurityUtils.safeExistsSync(filePath)) {
361
361
  return fs.statSync(filePath).size / 1024;
362
362
  }
363
363
  return 0;
@@ -380,7 +380,7 @@ class LocaleOptimizer {
380
380
 
381
381
  this.allLocales.forEach(locale => {
382
382
  const filePath = path.join(this.uiLocalesDir, `${locale}.json`);
383
- if (fs.existsSync(filePath)) {
383
+ if (SecurityUtils.safeExistsSync(filePath)) {
384
384
  activeLocales.push(locale);
385
385
  }
386
386
  });