i18ntk 1.10.1 → 2.0.2

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 (110) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +141 -1185
  3. package/main/i18ntk-analyze.js +149 -133
  4. package/main/i18ntk-backup-class.js +420 -0
  5. package/main/i18ntk-backup.js +4 -4
  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 +76 -25
  11. package/main/i18ntk-java.js +27 -32
  12. package/main/i18ntk-js.js +70 -68
  13. package/main/i18ntk-manage.js +128 -29
  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 +10 -396
  18. package/main/i18ntk-sizing.js +46 -40
  19. package/main/i18ntk-summary.js +21 -18
  20. package/main/i18ntk-ui.js +11 -10
  21. package/main/i18ntk-usage.js +55 -19
  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 -30
  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 +13 -5
  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 +23 -15
  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 +23 -20
  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 +152 -103
  86. package/utils/config-manager.js +204 -164
  87. package/utils/config.js +5 -4
  88. package/utils/env-manager.js +256 -0
  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/logger.js +6 -2
  94. package/utils/mini-commander.js +179 -0
  95. package/utils/missing-key-validator.js +5 -5
  96. package/utils/plugin-loader.js +29 -11
  97. package/utils/prompt.js +14 -44
  98. package/utils/safe-json.js +40 -0
  99. package/utils/secure-errors.js +3 -3
  100. package/utils/security-check-improved.js +390 -0
  101. package/utils/security-config.js +5 -5
  102. package/utils/security-fixed.js +607 -0
  103. package/utils/security.js +462 -248
  104. package/utils/setup-enforcer.js +136 -44
  105. package/utils/setup-validator.js +33 -32
  106. package/utils/terminal-icons.js +1 -1
  107. package/utils/ultra-performance-optimizer.js +11 -9
  108. package/utils/watch-locales.js +2 -1
  109. package/utils/prompt-fixed.js +0 -55
  110. package/utils/security-check.js +0 -450
@@ -0,0 +1,256 @@
1
+ /**
2
+ * Centralized Environment Variable Manager
3
+ *
4
+ * This module provides secure access to a fixed allowlist of environment variables.
5
+ * Only explicitly defined environment variables are accessible, all others are ignored.
6
+ * No secrets or sensitive data should ever be stored in environment variables.
7
+ */
8
+
9
+ const ALLOWED_ENV_VARS = {
10
+ // Logging and output
11
+ 'I18NTK_LOG_LEVEL': {
12
+ default: 'error',
13
+ validate: (value) => ['error', 'warn', 'info', 'debug', 'silent'].includes(value.toLowerCase()),
14
+ transform: (value) => value.toLowerCase()
15
+ },
16
+
17
+ 'I18NTK_OUTDIR': {
18
+ default: './i18ntk-reports',
19
+ validate: (value) => typeof value === 'string' && value.length > 0,
20
+ transform: (value) => value.trim()
21
+ },
22
+
23
+ // UI and interaction
24
+ 'I18NTK_UI_LANGUAGE': {
25
+ default: 'en',
26
+ validate: (value) => ['en', 'de', 'es', 'fr', 'ru', 'ja', 'zh'].includes(value.toLowerCase()),
27
+ transform: (value) => value.toLowerCase()
28
+ },
29
+
30
+ 'I18NTK_SILENT': {
31
+ default: 'false',
32
+ validate: (value) => ['true', 'false', '1', '0', 'yes', 'no'].includes(value.toLowerCase()),
33
+ transform: (value) => {
34
+ const lower = value.toLowerCase();
35
+ return lower === 'true' || lower === '1' || lower === 'yes' ? 'true' : 'false';
36
+ }
37
+ },
38
+
39
+ // Debug and development
40
+ 'I18NTK_DEBUG_LOCALES': {
41
+ default: '0',
42
+ validate: (value) => ['0', '1', 'true', 'false'].includes(value.toLowerCase()),
43
+ transform: (value) => {
44
+ const lower = value.toLowerCase();
45
+ return lower === '1' || lower === 'true' ? '1' : '0';
46
+ }
47
+ },
48
+
49
+ // Runtime configuration
50
+ 'I18NTK_RUNTIME_DIR': {
51
+ default: null,
52
+ validate: (value) => typeof value === 'string',
53
+ transform: (value) => value.trim() || null
54
+ },
55
+
56
+ 'I18NTK_I18N_DIR': {
57
+ default: './locales',
58
+ validate: (value) => typeof value === 'string' && value.length > 0,
59
+ transform: (value) => value.trim()
60
+ },
61
+
62
+ 'I18NTK_SOURCE_DIR': {
63
+ default: './locales',
64
+ validate: (value) => typeof value === 'string' && value.length > 0,
65
+ transform: (value) => value.trim()
66
+ },
67
+
68
+ 'I18NTK_PROJECT_ROOT': {
69
+ default: '.',
70
+ validate: (value) => typeof value === 'string' && value.length > 0,
71
+ transform: (value) => value.trim()
72
+ },
73
+
74
+ // Framework detection
75
+ 'I18NTK_FRAMEWORK_PREFERENCE': {
76
+ default: 'auto',
77
+ validate: (value) => ['auto', 'vanilla', 'react', 'vue', 'angular', 'svelte', 'i18next', 'nuxt', 'next', 'django', 'flask', 'fastapi', 'spring-boot', 'laravel'].includes(value.toLowerCase()),
78
+ transform: (value) => value.toLowerCase()
79
+ },
80
+
81
+ 'I18NTK_FRAMEWORK_FALLBACK': {
82
+ default: 'vanilla',
83
+ validate: (value) => ['vanilla', 'react', 'vue', 'angular', 'svelte', 'i18next', 'nuxt', 'next', 'django', 'flask', 'fastapi', 'spring-boot', 'laravel'].includes(value.toLowerCase()),
84
+ transform: (value) => value.toLowerCase()
85
+ },
86
+
87
+ 'I18NTK_FRAMEWORK_DETECT': {
88
+ default: 'true',
89
+ validate: (value) => ['true', 'false', '1', '0', 'yes', 'no'].includes(value.toLowerCase()),
90
+ transform: (value) => {
91
+ const lower = value.toLowerCase();
92
+ return lower === 'true' || lower === '1' || lower === 'yes' ? 'true' : 'false';
93
+ }
94
+ }
95
+ };
96
+
97
+ // Security: Block access to sensitive environment variables
98
+ const BLOCKED_PATTERNS = [
99
+ /^SECRET/i,
100
+ /^PASSWORD/i,
101
+ /^KEY/i,
102
+ /^TOKEN/i,
103
+ /^API_KEY/i,
104
+ /^PRIVATE/i,
105
+ /^AUTH/i,
106
+ /^CREDENTIAL/i,
107
+ /^AWS_/i,
108
+ /^GITHUB_/i,
109
+ /^NPM_/i,
110
+ /^NODE_/i,
111
+ /^PATH$/,
112
+ /^HOME$/,
113
+ /^USER$/,
114
+ /^USERNAME$/,
115
+ /^SHELL$/,
116
+ /^TERM$/,
117
+ /^DISPLAY$/,
118
+ /^LANG$/,
119
+ /^LC_/i
120
+ ];
121
+
122
+ class EnvironmentManager {
123
+ constructor() {
124
+ this._cache = new Map();
125
+ this._validated = new Set();
126
+ }
127
+
128
+ /**
129
+ * Get a validated environment variable value
130
+ * @param {string} name - Environment variable name
131
+ * @returns {string|null} - Validated value or null if not allowed
132
+ */
133
+ get(name) {
134
+ // Only allow explicitly defined variables
135
+ if (!ALLOWED_ENV_VARS[name]) {
136
+ return null;
137
+ }
138
+
139
+ // Check cache first
140
+ if (this._cache.has(name)) {
141
+ return this._cache.get(name);
142
+ }
143
+
144
+ const definition = ALLOWED_ENV_VARS[name];
145
+ const rawValue = process.env[name];
146
+
147
+ // Use default if not set
148
+ if (rawValue === undefined || rawValue === null || rawValue === '') {
149
+ this._cache.set(name, definition.default);
150
+ return definition.default;
151
+ }
152
+
153
+ // Validate and transform
154
+ try {
155
+ const transformed = definition.transform(rawValue);
156
+
157
+ if (!definition.validate(transformed)) {
158
+ console.warn(`[i18ntk] Invalid value for ${name}: "${rawValue}". Using default: ${definition.default}`);
159
+ this._cache.set(name, definition.default);
160
+ return definition.default;
161
+ }
162
+
163
+ this._cache.set(name, transformed);
164
+ this._validated.add(name);
165
+ return transformed;
166
+ } catch (error) {
167
+ console.warn(`[i18ntk] Error processing ${name}: ${error.message}. Using default: ${definition.default}`);
168
+ this._cache.set(name, definition.default);
169
+ return definition.default;
170
+ }
171
+ }
172
+
173
+ /**
174
+ * Check if an environment variable is allowed
175
+ * @param {string} name - Environment variable name
176
+ * @returns {boolean} - True if allowed
177
+ */
178
+ isAllowed(name) {
179
+ return !!ALLOWED_ENV_VARS[name];
180
+ }
181
+
182
+ /**
183
+ * Get all allowed environment variables with their current values
184
+ * @returns {Object} - Object with variable names as keys and values
185
+ */
186
+ getAll() {
187
+ const result = {};
188
+ for (const name of Object.keys(ALLOWED_ENV_VARS)) {
189
+ result[name] = this.get(name);
190
+ }
191
+ return result;
192
+ }
193
+
194
+ /**
195
+ * Get documentation for all allowed environment variables
196
+ * @returns {Array} - Array of documentation objects
197
+ */
198
+ getDocumentation() {
199
+ return Object.entries(ALLOWED_ENV_VARS).map(([name, definition]) => ({
200
+ name,
201
+ default: definition.default,
202
+ description: this._getDescription(name)
203
+ }));
204
+ }
205
+
206
+ /**
207
+ * Clear the cache (for testing)
208
+ */
209
+ clearCache() {
210
+ this._cache.clear();
211
+ this._validated.clear();
212
+ }
213
+
214
+ /**
215
+ * Check if a variable name matches blocked patterns
216
+ * @param {string} name - Variable name to check
217
+ * @returns {boolean} - True if blocked
218
+ */
219
+ isBlocked(name) {
220
+ return BLOCKED_PATTERNS.some(pattern => pattern.test(name));
221
+ }
222
+
223
+ /**
224
+ * Get human-readable description for an environment variable
225
+ * @param {string} name - Environment variable name
226
+ * @returns {string} - Description
227
+ */
228
+ _getDescription(name) {
229
+ const descriptions = {
230
+ 'I18NTK_LOG_LEVEL': 'Logging level (error, warn, info, debug, silent)',
231
+ 'I18NTK_OUTDIR': 'Output directory for reports and generated files',
232
+ 'I18NTK_UI_LANGUAGE': 'UI language (en, de, es, fr, ru, ja, zh)',
233
+ 'I18NTK_SILENT': 'Run in silent mode without interactive prompts',
234
+ 'I18NTK_DEBUG_LOCALES': 'Enable debug logging for locale loading',
235
+ 'I18NTK_RUNTIME_DIR': 'Custom runtime directory path',
236
+ 'I18NTK_I18N_DIR': 'Directory containing i18n/locale files',
237
+ 'I18NTK_SOURCE_DIR': 'Source directory for scanning',
238
+ 'I18NTK_PROJECT_ROOT': 'Project root directory',
239
+ 'I18NTK_FRAMEWORK_PREFERENCE': 'Preferred framework (auto, react, vue, etc.)',
240
+ 'I18NTK_FRAMEWORK_FALLBACK': 'Fallback framework when auto-detection fails',
241
+ 'I18NTK_FRAMEWORK_DETECT': 'Enable automatic framework detection'
242
+ };
243
+
244
+ return descriptions[name] || 'Configuration option';
245
+ }
246
+ }
247
+
248
+ // Create singleton instance
249
+ const envManager = new EnvironmentManager();
250
+
251
+ module.exports = {
252
+ EnvironmentManager,
253
+ envManager,
254
+ ALLOWED_ENV_VARS,
255
+ BLOCKED_PATTERNS
256
+ };
@@ -1,6 +1,6 @@
1
- const fs = require('fs');
2
1
  const path = require('path');
3
2
  const { gte } = require('./version-utils');
3
+ const SecurityUtils = require('./security');
4
4
 
5
5
  // Framework compatibility information
6
6
  const FRAMEWORK_COMPATIBILITY = {
@@ -38,9 +38,9 @@ const FRAMEWORKS = {
38
38
  configFilePatterns: [
39
39
  /i18n\.(js|ts)$/,
40
40
  /i18ntk\.config\.(js|ts)$/,
41
- /\.i18nrc(\.(js|json))?$/
41
+ /\.i18nrc(\.(js|json))?$/,
42
42
  ],
43
- setupGuide: 'https://github.com/vladnosiv/i18n-management-toolkit',
43
+ setupGuide: 'Refer to the official i18ntk documentation on GitHub for setup instructions.',
44
44
  priority: 100, // Higher priority to detect before other frameworks
45
45
  ignore: [
46
46
  'node_modules/**',
@@ -52,7 +52,7 @@ const FRAMEWORKS = {
52
52
  '**/*.test.{js,jsx,ts,tsx}'
53
53
  ]
54
54
  },
55
-
55
+
56
56
  // Vue i18n has highest specificity due to its unique syntax
57
57
  'vue-i18n': {
58
58
  name: 'vue-i18n',
@@ -66,7 +66,7 @@ const FRAMEWORKS = {
66
66
  ],
67
67
  ignore: ['node_modules/**']
68
68
  },
69
-
69
+
70
70
  // React i18next has medium specificity
71
71
  'react-i18next': {
72
72
  name: 'React i18next',
@@ -76,9 +76,9 @@ const FRAMEWORKS = {
76
76
  regex: /\b(?:useTranslation|withTranslation|Trans|I18n|i18n\.t|t\(?=\s*[`'"])/,
77
77
  configFile: 'i18n.js',
78
78
  configFilePatterns: [/i18n\.(js|ts)$/, /i18next\.config\.(js|ts)$/],
79
- setupGuide: 'https://react.i18next.com/'
79
+ setupGuide: 'Refer to the official react-i18next documentation for setup instructions.'
80
80
  },
81
-
81
+
82
82
  // Base i18next has lowest specificity
83
83
  'i18next': {
84
84
  name: 'i18next',
@@ -246,10 +246,10 @@ const FRAMEWORKS = {
246
246
  'test/tmp/**'
247
247
  ]
248
248
  },
249
- go: {
250
- name: 'go',
251
- deps: ['golang.org/x/text/language', 'github.com/nicksnyder/go-i18n/v2/i18n'],
252
- globs: ['**/*.go'],
249
+ go: {
250
+ name: 'go',
251
+ deps: ['go-i18n', 'x-text'],
252
+ globs: ['**/*.go'],
253
253
  patterns: [
254
254
  /i18n\.NewMessage\([^,]+,\s*["`]([^"`]+)["`]/g,
255
255
  /i18n\.NewLocalizer\([^)]+\)\.MustLocalize\([^,]+,\s*["`]([^"`]+)["`]/g,
@@ -277,11 +277,6 @@ const FRAMEWORKS = {
277
277
  }
278
278
  };
279
279
 
280
- /**
281
- * Detect the i18n framework being used in the project
282
- * @param {string} projectRoot - Path to the project root
283
- * @returns {Promise<Object>} Object containing framework info and detection confidence
284
- */
285
280
  /**
286
281
  * Detects the i18n framework being used in the project
287
282
  * @param {string} projectRoot - Path to the project root
@@ -294,23 +289,31 @@ async function detectFramework(projectRoot) {
294
289
 
295
290
  const packageJsonPath = path.join(projectRoot, 'package.json');
296
291
  const detectedFrameworks = [];
297
-
292
+
298
293
  // Only proceed if package.json exists
299
- if (!fs.existsSync(packageJsonPath)) {
294
+ if (!SecurityUtils.safeExistsSync(packageJsonPath, projectRoot)) {
300
295
  return null;
301
296
  }
302
297
 
303
298
  try {
304
299
  // Read and parse package.json
305
- const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
306
- const deps = {
307
- ...(packageJson.dependencies || {}),
300
+ const packageJsonContent = SecurityUtils.safeReadFileSync(packageJsonPath, projectRoot, 'utf8');
301
+ if (!packageJsonContent) {
302
+ return null;
303
+ }
304
+ const packageJson = SecurityUtils.safeParseJSON(packageJsonContent);
305
+ if (!packageJson) {
306
+ return null;
307
+ }
308
+
309
+ const deps = {
310
+ ...(packageJson.dependencies || {}),
308
311
  ...(packageJson.devDependencies || {}),
309
312
  ...(packageJson.peerDependencies || {})
310
313
  };
311
314
 
312
315
  // Sort frameworks by priority (highest first)
313
- const sortedFrameworks = Object.entries(FRAMEWORKS).sort((a, b) =>
316
+ const sortedFrameworks = Object.entries(FRAMEWORKS).sort((a, b) =>
314
317
  (b[1].priority || 0) - (a[1].priority || 0)
315
318
  );
316
319
 
@@ -350,7 +353,7 @@ async function detectFramework(projectRoot) {
350
353
  // First sort by confidence
351
354
  const confidenceDiff = b.confidence - a.confidence;
352
355
  if (confidenceDiff !== 0) return confidenceDiff;
353
-
356
+
354
357
  // If confidence is equal, sort by priority
355
358
  return (b.priority || 0) - (a.priority || 0);
356
359
  })[0];
@@ -363,4 +366,4 @@ async function detectFramework(projectRoot) {
363
366
  }
364
367
  }
365
368
 
366
- module.exports = { detectFramework, FRAMEWORKS };
369
+ module.exports = { detectFramework, FRAMEWORKS };
@@ -2,11 +2,47 @@
2
2
  const path = require('path');
3
3
  const fs = require('fs');
4
4
 
5
+ // Lazy load SecurityUtils to prevent circular dependencies
6
+ let securityUtils;
7
+ function getSecurityUtils() {
8
+ if (!securityUtils) {
9
+ try {
10
+ securityUtils = require('./security');
11
+ } catch (error) {
12
+ // Fallback: use basic fs operations if SecurityUtils is not available
13
+ return {
14
+ safeExistsSync: (path) => {
15
+ try {
16
+ return require('fs').existsSync(path);
17
+ } catch {
18
+ return false;
19
+ }
20
+ },
21
+ safeWriteFileSync: (path, data, encoding) => {
22
+ try {
23
+ return require('fs').writeFileSync(path, data, encoding);
24
+ } catch {
25
+ return null;
26
+ }
27
+ },
28
+ safeReadFileSync: (path, encoding) => {
29
+ try {
30
+ return require('fs').readFileSync(path, encoding);
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+ };
36
+ }
37
+ }
38
+ return securityUtils;
39
+ }
40
+
5
41
  // Helper functions for OS-agnostic path handling
6
42
  function toPosix(p) { return String(p).replace(/\\/g, '/'); }
7
- function isBundledPath(p) {
8
- const s = toPosix(p);
9
- return s.includes('/node_modules/i18ntk/') || s.includes('/i18ntk/ui-locales/');
43
+ function isBundledPath(p) {
44
+ const s = toPosix(p);
45
+ return s.includes('/node_modules/i18ntk/') || s.includes('/i18ntk/ui-locales/');
10
46
  }
11
47
 
12
48
  function safeRequireConfig() {
@@ -21,7 +57,8 @@ function stripBOMAndComments(s) {
21
57
  }
22
58
 
23
59
  function readJsonSafe(file) {
24
- const raw = fs.readFileSync(file, 'utf8');
60
+ const SecurityUtils = getSecurityUtils();
61
+ const raw = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8');
25
62
  return JSON.parse(stripBOMAndComments(raw));
26
63
  }
27
64
 
@@ -43,7 +80,8 @@ function resolveLocalesDirs() {
43
80
  try {
44
81
  const normalized = path.normalize(path.resolve(dir.trim()));
45
82
 
46
- if (fs.existsSync(normalized) && fs.statSync(normalized).isDirectory()) {
83
+ const SecurityUtils = getSecurityUtils();
84
+ if (SecurityUtils.safeExistsSync(normalized) && fs.statSync(normalized).isDirectory()) {
47
85
  dirs.push(normalized);
48
86
  }
49
87
  } catch {
@@ -54,7 +92,7 @@ function resolveLocalesDirs() {
54
92
 
55
93
  const pkgA = pkgUiLocalesDirViaThisFile();
56
94
  addDir(pkgA);
57
-
95
+
58
96
  const pkgB = pkgUiLocalesDirViaResolve();
59
97
  if (pkgB && pkgB !== pkgA) {
60
98
  addDir(pkgB);
@@ -78,28 +116,33 @@ function candidatesForLang(dir, lang) {
78
116
 
79
117
  function findLocaleFilesAllDirs(lang) {
80
118
  const dirs = resolveLocalesDirs();
81
-
119
+
82
120
  if (process.env.I18NTK_DEBUG_LOCALES === '1') {
83
121
  console.log('🔎 i18ntk locale search dirs:', dirs);
84
122
  }
85
-
123
+
86
124
  const files = [];
87
125
  const errors = [];
88
-
126
+
89
127
  for (const dir of dirs) {
90
128
  for (const candidate of candidatesForLang(dir, lang)) {
91
129
  try {
92
- if (fs.existsSync(candidate)) {
130
+ const SecurityUtils = getSecurityUtils();
131
+ if (SecurityUtils.safeExistsSync(candidate)) {
93
132
  const stats = fs.statSync(candidate);
94
133
  if (stats.isFile() && stats.size > 0) {
95
134
  // Validate file is readable and parseable
96
135
  fs.accessSync(candidate, fs.constants.R_OK);
97
136
  // Quick JSON validation
98
- const content = fs.readFileSync(candidate, 'utf8');
99
- if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
100
- files.push(candidate);
137
+ const content = SecurityUtils.safeReadFileSync(candidate, path.dirname(candidate), 'utf8');
138
+ if (content) {
139
+ if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
140
+ files.push(candidate);
141
+ } else {
142
+ errors.push({ file: candidate, error: 'Invalid JSON format' });
143
+ }
101
144
  } else {
102
- errors.push({ file: candidate, error: 'Invalid JSON format' });
145
+ errors.push({ file: candidate, error: 'Empty or unreadable file' });
103
146
  }
104
147
  }
105
148
  }
@@ -108,11 +151,11 @@ function findLocaleFilesAllDirs(lang) {
108
151
  }
109
152
  }
110
153
  }
111
-
154
+
112
155
  if (process.env.I18NTK_DEBUG_LOCALES === '1' && errors.length > 0) {
113
156
  console.warn(`⚠️ Locale resolution errors for ${lang}:`, errors);
114
157
  }
115
-
158
+
116
159
  return files;
117
160
  }
118
161
 
@@ -121,34 +164,34 @@ let currentLanguage = 'en';
121
164
  let isInitialized = false;
122
165
  const missingWarned = new Set();
123
166
 
124
- function loadTranslations(language) {
125
- const cfg = safeRequireConfig();
126
- const settings = cfg?.getConfig?.() || {};
127
- const configuredLanguage = settings.uiLanguage || settings.language || 'en';
128
-
129
- // Prioritize settings file language over environment variable
130
- const requested = (configuredLanguage || language || 'en').toString();
167
+ function loadTranslations(language) {
168
+ const cfg = safeRequireConfig();
169
+ const settings = cfg?.getConfig?.() || {};
170
+ const configuredLanguage = settings.uiLanguage || settings.language;
171
+
172
+ // Prioritize CLI argument, then UI language, then language fallback
173
+ const requested = (language || configuredLanguage || 'en').toString();
131
174
  const short = requested.split('-')[0].toLowerCase();
132
175
  const tryOrder = [requested, short, 'en'];
133
176
 
134
177
  const loadErrors = [];
135
-
178
+
136
179
  for (const lang of tryOrder) {
137
180
  const files = findLocaleFilesAllDirs(lang);
138
-
181
+
139
182
  // Prioritize bundled locales over project ones
140
183
  const prioritizedFiles = files.sort((a, b) => Number(isBundledPath(b)) - Number(isBundledPath(a)));
141
-
184
+
142
185
  for (const file of prioritizedFiles) {
143
186
  try {
144
187
  translations = readJsonSafe(file);
145
188
  currentLanguage = lang;
146
189
  isInitialized = true;
147
-
190
+
148
191
  if (process.env.I18NTK_DEBUG_LOCALES === '1') {
149
192
  console.log(`🗂 Loaded UI locale → ${file} (${lang})`);
150
193
  }
151
-
194
+
152
195
  // Validate translations object
153
196
  if (typeof translations === 'object' && translations !== null) {
154
197
  return translations;
@@ -199,11 +242,11 @@ function loadTranslations(language) {
199
242
  };
200
243
  currentLanguage = 'en';
201
244
  isInitialized = true;
202
-
245
+
203
246
  if (loadErrors.length > 0) {
204
247
  console.warn(`⚠️ No valid UI locale files found. Using built-in English strings.`);
205
248
  }
206
-
249
+
207
250
  return translations;
208
251
  }
209
252
 
@@ -219,7 +262,7 @@ function t(key, params = {}) {
219
262
  loadTranslations();
220
263
  isInitialized = true;
221
264
  }
222
-
265
+
223
266
  // Split the key into parts (e.g., 'module.subkey' -> ['module', 'subkey'])
224
267
  const keyParts = key.split('.');
225
268
  let value = translations;
@@ -258,12 +301,12 @@ function t(key, params = {}) {
258
301
  }
259
302
  return key;
260
303
  }
261
-
304
+
262
305
  // If we found a string, interpolate parameters
263
306
  if (typeof value === 'string') {
264
307
  return interpolateParams(value, params);
265
308
  }
266
-
309
+
267
310
  // Return the key if the final value is not a string
268
311
  console.warn(`Translation key does not resolve to a string: ${key}`);
269
312
  return key;
@@ -309,12 +352,13 @@ function getAvailableLanguages() {
309
352
  const langs = new Set();
310
353
  for (const d of dirs) {
311
354
  try {
312
- if (!fs.existsSync(d)) continue;
355
+ const SecurityUtils = getSecurityUtils();
356
+ if (!SecurityUtils.safeExistsSync(d)) continue;
313
357
  for (const f of fs.readdirSync(d)) {
314
358
  if (f.endsWith('.json')) langs.add(path.basename(f, '.json'));
315
359
  }
316
360
  for (const f of fs.readdirSync(d, { withFileTypes: true })) {
317
- if (f.isDirectory() && fs.existsSync(path.join(d, f.name, `${f.name}.json`))) {
361
+ if (f.isDirectory() && SecurityUtils.safeExistsSync(path.join(d, f.name, `${f.name}.json`))) {
318
362
  langs.add(f.name);
319
363
  }
320
364
  }
@@ -347,11 +391,11 @@ function deepMerge(target, source) {
347
391
  * Refresh language from settings manager
348
392
  * This ensures translations stay in sync with settings changes
349
393
  */
350
- function refreshLanguageFromSettings() {
351
- const cfg = safeRequireConfig();
352
- const settings = cfg?.getConfig?.() || {};
353
- const configuredLanguage = settings.language || settings.uiLanguage || 'en';
354
-
394
+ function refreshLanguageFromSettings() {
395
+ const cfg = safeRequireConfig();
396
+ const settings = cfg?.getConfig?.() || {};
397
+ const configuredLanguage = settings.uiLanguage || settings.language || 'en';
398
+
355
399
  if (configuredLanguage !== currentLanguage) {
356
400
  isInitialized = false;
357
401
  loadTranslations(configuredLanguage);
@@ -373,4 +417,4 @@ module.exports = {
373
417
  deepMerge,
374
418
  refreshTranslations,
375
419
  refreshLanguageFromSettings
376
- };
420
+ };