i18ntk 2.0.4 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +38 -60
  2. package/main/i18ntk-analyze.js +49 -44
  3. package/main/i18ntk-complete.js +75 -74
  4. package/main/i18ntk-fixer.js +3 -3
  5. package/main/i18ntk-init.js +5 -5
  6. package/main/i18ntk-scanner.js +2 -2
  7. package/main/i18ntk-sizing.js +35 -35
  8. package/main/i18ntk-summary.js +4 -4
  9. package/main/i18ntk-ui.js +54 -8
  10. package/main/i18ntk-usage.js +14 -14
  11. package/main/i18ntk-validate.js +6 -5
  12. package/main/manage/commands/AnalyzeCommand.js +40 -35
  13. package/main/manage/commands/FixerCommand.js +2 -2
  14. package/main/manage/commands/ScannerCommand.js +2 -2
  15. package/main/manage/commands/ValidateCommand.js +9 -9
  16. package/main/manage/index.js +147 -75
  17. package/main/manage/managers/LanguageMenu.js +7 -2
  18. package/main/manage/services/UsageService.js +7 -7
  19. package/package.json +269 -290
  20. package/settings/settings-cli.js +3 -3
  21. package/ui-locales/de.json +161 -166
  22. package/ui-locales/en.json +13 -18
  23. package/ui-locales/es.json +171 -184
  24. package/ui-locales/fr.json +155 -161
  25. package/ui-locales/ja.json +192 -243
  26. package/ui-locales/ru.json +145 -196
  27. package/ui-locales/zh.json +179 -185
  28. package/utils/cli-helper.js +26 -98
  29. package/utils/extractors/regex.js +39 -12
  30. package/utils/i18n-helper.js +88 -40
  31. package/{scripts → utils}/locale-optimizer.js +61 -60
  32. package/utils/security-check-improved.js +16 -13
  33. package/utils/security.js +6 -4
  34. package/main/i18ntk-go.js +0 -283
  35. package/main/i18ntk-java.js +0 -380
  36. package/main/i18ntk-js.js +0 -512
  37. package/main/i18ntk-manage.js +0 -1694
  38. package/main/i18ntk-php.js +0 -462
  39. package/main/i18ntk-py.js +0 -379
  40. package/main/i18ntk-settings.js +0 -23
  41. package/main/manage/index-fixed.js +0 -1447
  42. package/main/manage/services/ConfigurationService-fixed.js +0 -449
  43. package/scripts/build-lite.js +0 -279
  44. package/scripts/deprecate-versions.js +0 -317
  45. package/scripts/export-translations.js +0 -84
  46. package/scripts/fix-all-i18n.js +0 -215
  47. package/scripts/fix-and-purify-i18n.js +0 -213
  48. package/scripts/fix-locale-control-chars.js +0 -110
  49. package/scripts/lint-locales.js +0 -80
  50. package/scripts/prepublish.js +0 -348
  51. package/scripts/security-check.js +0 -117
  52. package/scripts/sync-translations.js +0 -151
  53. package/scripts/sync-ui-locales.js +0 -20
  54. package/scripts/validate-all-translations.js +0 -139
  55. package/scripts/verify-deprecations.js +0 -157
  56. package/scripts/verify-translations.js +0 -63
  57. package/utils/security-fixed.js +0 -607
@@ -124,106 +124,34 @@ class CliHelper {
124
124
  }
125
125
 
126
126
  // Utility: Show framework suggestion warning only once per process
127
- let frameworkWarningShown = false;
128
- function showFrameworkWarningOnce(ui) {
129
- if (frameworkWarningShown) return;
130
- frameworkWarningShown = true;
131
-
132
- // Try to use the proper translation system first
133
- let t;
134
- if (ui && typeof ui.t === 'function') {
135
- t = ui.t.bind(ui);
136
- } else {
137
- // Fallback to loading the UI i18n system properly
138
- try {
139
- const UIi18n = require('../main/i18ntk-ui');
140
- const fallbackUI = new UIi18n();
141
- fallbackUI.loadLanguage('en'); // Load English as fallback
142
- t = fallbackUI.t.bind(fallbackUI);
143
- } catch (error) {
144
- // Last resort: use locale files directly
145
- try {
146
- const path = require('path');
147
- const fs = require('fs');
148
- const localePath = path.join(__dirname, '..', 'resources', 'i18n', 'ui-locales', 'en.json');
149
- if (SecurityUtils.safeExistsSync(localePath)) {
150
- const translations = JSON.parse(SecurityUtils.safeReadFileSync(localePath, path.dirname(localePath), 'utf8'));
151
- t = (key) => {
152
- const keys = key.split('.');
153
- let result = translations;
154
- for (const k of keys) {
155
- result = result && result[k];
156
- }
157
- return result || key;
158
- };
159
- } else {
160
- throw new Error('Locale file not found');
161
- }
162
- } catch (fallbackError) {
163
- // Final fallback: load from current UI locale or English
164
- try {
165
- const path = require('path');
166
- const fs = require('fs');
167
-
168
- // Try to determine current language from settings
169
- const settingsManager = require('../settings/settings-manager');
170
- const settings = settingsManager.loadSettings();
171
- const currentLang = settings.uiLanguage || 'en';
172
-
173
- const localePath = path.join(__dirname, '..', 'resources', 'i18n', 'ui-locales', `${currentLang}.json`);
174
- if (SecurityUtils.safeExistsSync(localePath)) {
175
- const translations = JSON.parse(SecurityUtils.safeReadFileSync(localePath, path.dirname(localePath), 'utf8'));
176
- t = (key) => {
177
- const keys = key.split('.');
178
- let result = translations;
179
- for (const k of keys) {
180
- result = result && result[k];
181
- }
182
- return result || key;
183
- };
184
- } else {
185
- // Fallback to English
186
- const enLocalePath = path.join(__dirname, '..', 'resources', 'i18n', 'ui-locales', 'en.json');
187
- if (SecurityUtils.safeExistsSync(enLocalePath)) {
188
- const translations = JSON.parse(SecurityUtils.safeReadFileSync(enLocalePath, path.dirname(enLocalePath), 'utf8'));
189
- t = (key) => {
190
- const keys = key.split('.');
191
- let result = translations;
192
- for (const k of keys) {
193
- result = result && result[k];
194
- }
195
- return result || key;
196
- };
197
- } else {
198
- throw new Error('No locale files found');
199
- }
200
- }
201
- } catch (finalError) {
202
- // Absolute last resort: basic hardcoded fallback
203
- const messages = {
204
- 'init.suggestions.noFramework': 'No i18n framework detected. Consider using one of the following:',
205
- 'init.frameworks.react': ' - React i18next (react-i18next)',
206
- 'init.frameworks.vue': ' - Vue i18n (vue-i18next)',
207
- 'init.frameworks.i18next': ' - i18next (i18next)',
208
- 'init.frameworks.nuxt': ' - Nuxt i18n (@nuxtjs/i18next)',
209
- 'init.frameworks.svelte': ' - Svelte i18n (svelte-i18next)'
210
- };
211
- t = (key) => messages[key] || key;
212
- }
213
- }
214
- }
215
- }
216
-
217
- console.log(t('init.suggestions.noFramework'));
218
- console.log(t('init.frameworks.react'));
219
- console.log(t('init.frameworks.vue'));
220
- console.log(t('init.frameworks.i18next'));
221
- console.log(t('init.frameworks.nuxt'));
222
- console.log(t('init.frameworks.svelte'));
223
- }
127
+ let frameworkWarningShown = false;
128
+ function showFrameworkWarningOnce(ui) {
129
+ if (frameworkWarningShown) return;
130
+ frameworkWarningShown = true;
131
+
132
+ const translate = ui && typeof ui.t === 'function'
133
+ ? ui.t.bind(ui)
134
+ : require('./i18n-helper').t;
135
+
136
+ const fallback = {
137
+ noFramework: 'No i18n framework detected. Consider using one of the following:',
138
+ react: ' - React i18next (react-i18next)',
139
+ vue: ' - Vue i18n (vue-i18next)',
140
+ i18next: ' - i18next (i18next)',
141
+ nuxt: ' - Nuxt i18n (@nuxtjs/i18n)',
142
+ svelte: ' - Svelte i18n (svelte-i18n)'
143
+ };
144
+
145
+ console.log(translate('init.suggestions.noFramework') || fallback.noFramework);
146
+ console.log(translate('init.frameworks.react') || fallback.react);
147
+ console.log(translate('init.frameworks.vue') || fallback.vue);
148
+ console.log(translate('init.frameworks.i18next') || fallback.i18next);
149
+ console.log(translate('init.frameworks.nuxt') || fallback.nuxt);
150
+ console.log(translate('init.frameworks.svelte') || fallback.svelte);
151
+ }
224
152
 
225
153
  // Export a singleton instance and attach utility for backward compatibility
226
154
  const cliHelper = new CliHelper();
227
155
  cliHelper.showFrameworkWarningOnce = showFrameworkWarningOnce;
228
156
 
229
- module.exports = cliHelper;
157
+ module.exports = cliHelper;
@@ -1,21 +1,48 @@
1
- function extract(content, patterns = []) {
2
- const keys = new Set();
3
- if (!Array.isArray(patterns)) return [];
4
- if (content === null || content === undefined) return [];
1
+ function normalizeKeyCandidate(rawKey) {
2
+ if (rawKey === null || rawKey === undefined) return null;
3
+
4
+ let key = String(rawKey).trim();
5
+ if (!key) return null;
6
+
7
+ // Normalize dynamic template segments to wildcard form.
8
+ key = key.replace(/\$\{[^}]+\}/g, '*');
9
+
10
+ // Reject multiline keys and obvious code fragments.
11
+ if (/[\r\n\t]/.test(key)) return null;
12
+ if (/\s/.test(key)) return null;
13
+ if (/(=>|\|\||&&|function\b|return\b|includes\()/i.test(key)) return null;
14
+
15
+ // Allow typical i18n key character sets only.
16
+ if (!/^[A-Za-z0-9_.:*-]+$/.test(key)) return null;
17
+
18
+ // Reject malformed key shapes.
19
+ if (key.startsWith('.') || key.endsWith('.')) return null;
20
+ if (key.includes('..')) return null;
21
+
22
+ return key;
23
+ }
24
+
25
+ function extract(content, patterns = []) {
26
+ const keys = new Set();
27
+ if (!Array.isArray(patterns)) return [];
28
+ if (content === null || content === undefined) return [];
5
29
  const contentStr = String(content);
6
30
  for (const pattern of patterns) {
7
31
  try {
8
- let regex = pattern instanceof RegExp ? new RegExp(pattern.source, 'g') : new RegExp(pattern, 'g');
9
- let match;
10
- while ((match = regex.exec(contentStr)) !== null) {
11
- if (match[1]) keys.add(match[1]);
12
- if (regex.lastIndex === 0) break;
13
- }
14
- } catch (e) {
32
+ let regex = pattern instanceof RegExp ? new RegExp(pattern.source, 'g') : new RegExp(pattern, 'g');
33
+ let match;
34
+ while ((match = regex.exec(contentStr)) !== null) {
35
+ if (match[1]) {
36
+ const normalized = normalizeKeyCandidate(match[1]);
37
+ if (normalized) keys.add(normalized);
38
+ }
39
+ if (regex.lastIndex === 0) break;
40
+ }
41
+ } catch (e) {
15
42
  // skip invalid patterns
16
43
  }
17
44
  }
18
45
  return Array.from(keys);
19
46
  }
20
47
 
21
- module.exports = { extract };
48
+ module.exports = { extract };
@@ -11,23 +11,24 @@ function getSecurityUtils() {
11
11
  } catch (error) {
12
12
  // Fallback: use basic fs operations if SecurityUtils is not available
13
13
  return {
14
- safeExistsSync: (path) => {
14
+ safeExistsSync: (targetPath) => {
15
15
  try {
16
- return require('fs').existsSync(path);
16
+ require('fs').accessSync(targetPath);
17
+ return true;
17
18
  } catch {
18
19
  return false;
19
20
  }
20
21
  },
21
- safeWriteFileSync: (path, data, encoding) => {
22
+ safeWriteFileSync: (targetPath, data, _basePath, encoding = 'utf8') => {
22
23
  try {
23
- return require('fs').writeFileSync(path, data, encoding);
24
+ return require('fs').writeFileSync(targetPath, data, encoding);
24
25
  } catch {
25
26
  return null;
26
27
  }
27
28
  },
28
- safeReadFileSync: (path, encoding) => {
29
+ safeReadFileSync: (targetPath, _basePath, encoding = 'utf8') => {
29
30
  try {
30
- return require('fs').readFileSync(path, encoding);
31
+ return require('fs').readFileSync(targetPath, encoding);
31
32
  } catch {
32
33
  return null;
33
34
  }
@@ -56,9 +57,42 @@ function stripBOMAndComments(s) {
56
57
  return s;
57
58
  }
58
59
 
59
- function readJsonSafe(file) {
60
+ function getValidationBase(targetPath) {
61
+ const fallbackBase = path.resolve(__dirname, '..');
62
+ if (!targetPath || typeof targetPath !== 'string') {
63
+ return fallbackBase;
64
+ }
65
+
66
+ let current = path.resolve(path.dirname(targetPath));
67
+ while (true) {
68
+ try {
69
+ if (fs.statSync(current).isDirectory()) {
70
+ return current;
71
+ }
72
+ } catch {
73
+ // Continue upward until we find an existing directory.
74
+ }
75
+
76
+ const parent = path.dirname(current);
77
+ if (parent === current) {
78
+ break;
79
+ }
80
+ current = parent;
81
+ }
82
+
83
+ return fallbackBase;
84
+ }
85
+
86
+ function safeReadFile(targetPath, encoding = 'utf8') {
60
87
  const SecurityUtils = getSecurityUtils();
61
- const raw = SecurityUtils.safeReadFileSync(file, path.dirname(file), 'utf8');
88
+ return SecurityUtils.safeReadFileSync(targetPath, getValidationBase(targetPath), encoding);
89
+ }
90
+
91
+ function readJsonSafe(file) {
92
+ const raw = safeReadFile(file, 'utf8');
93
+ if (raw === null) {
94
+ throw new Error(`Unable to read JSON file: ${file}`);
95
+ }
62
96
  return JSON.parse(stripBOMAndComments(raw));
63
97
  }
64
98
 
@@ -67,21 +101,30 @@ function pkgUiLocalesDirViaThisFile() {
67
101
  }
68
102
 
69
103
  function pkgUiLocalesDirViaResolve() {
70
- try {
71
- const enJson = require.resolve('i18ntk/ui-locales/en.json');
72
- return path.dirname(enJson);
73
- } catch { return null; }
74
- }
104
+ try {
105
+ // Try the new correct path first (ui-locales/en.json)
106
+ const enJson = require.resolve('i18ntk/ui-locales/en.json');
107
+ return path.dirname(enJson);
108
+ } catch {
109
+ try {
110
+ // Fallback to the old incorrect path for backward compatibility
111
+ const enJson = require.resolve('i18ntk/resources/i18n/ui-locales/en.json');
112
+ return path.dirname(enJson);
113
+ } catch { return null; }
114
+ }
115
+ }
116
+
117
+ // Removed legacyResourcesUiLocalesDir as resources/i18n/ui-locales is deprecated
75
118
 
76
- function resolveLocalesDirs() {
119
+ function resolveLocalesDirs(preferredDir) {
120
+ const SecurityUtils = getSecurityUtils();
77
121
  const dirs = [];
78
122
  const addDir = (dir) => {
79
123
  if (typeof dir === 'string' && dir.trim()) {
80
124
  try {
81
125
  const normalized = path.normalize(path.resolve(dir.trim()));
82
-
83
- const SecurityUtils = getSecurityUtils();
84
- if (SecurityUtils.safeExistsSync(normalized) && fs.statSync(normalized).isDirectory()) {
126
+ const stats = SecurityUtils.safeStatSync(normalized, getValidationBase(normalized));
127
+ if (stats && stats.isDirectory()) {
85
128
  dirs.push(normalized);
86
129
  }
87
130
  } catch {
@@ -90,6 +133,8 @@ function resolveLocalesDirs() {
90
133
  }
91
134
  };
92
135
 
136
+ addDir(preferredDir);
137
+
93
138
  const pkgA = pkgUiLocalesDirViaThisFile();
94
139
  addDir(pkgA);
95
140
 
@@ -98,6 +143,8 @@ function resolveLocalesDirs() {
98
143
  addDir(pkgB);
99
144
  }
100
145
 
146
+ // Removed legacy directory fallback
147
+
101
148
  // Deduplicate while preserving order
102
149
  const seen = new Set();
103
150
  return dirs.filter(dir => {
@@ -114,8 +161,9 @@ function candidatesForLang(dir, lang) {
114
161
  ];
115
162
  }
116
163
 
117
- function findLocaleFilesAllDirs(lang) {
118
- const dirs = resolveLocalesDirs();
164
+ function findLocaleFilesAllDirs(lang, preferredDir) {
165
+ const SecurityUtils = getSecurityUtils();
166
+ const dirs = resolveLocalesDirs(preferredDir);
119
167
 
120
168
  if (process.env.I18NTK_DEBUG_LOCALES === '1') {
121
169
  console.log('🔎 i18ntk locale search dirs:', dirs);
@@ -127,14 +175,12 @@ function findLocaleFilesAllDirs(lang) {
127
175
  for (const dir of dirs) {
128
176
  for (const candidate of candidatesForLang(dir, lang)) {
129
177
  try {
130
- const SecurityUtils = getSecurityUtils();
131
- if (SecurityUtils.safeExistsSync(candidate)) {
132
- const stats = fs.statSync(candidate);
133
- if (stats.isFile() && stats.size > 0) {
178
+ const stats = SecurityUtils.safeStatSync(candidate, getValidationBase(candidate));
179
+ if (stats && stats.isFile() && stats.size > 0) {
134
180
  // Validate file is readable and parseable
135
181
  fs.accessSync(candidate, fs.constants.R_OK);
136
182
  // Quick JSON validation
137
- const content = SecurityUtils.safeReadFileSync(candidate, path.dirname(candidate), 'utf8');
183
+ const content = safeReadFile(candidate, 'utf8');
138
184
  if (content) {
139
185
  if (content.trim().startsWith('{') || content.trim().startsWith('[')) {
140
186
  files.push(candidate);
@@ -144,7 +190,6 @@ function findLocaleFilesAllDirs(lang) {
144
190
  } else {
145
191
  errors.push({ file: candidate, error: 'Empty or unreadable file' });
146
192
  }
147
- }
148
193
  }
149
194
  } catch (error) {
150
195
  errors.push({ file: candidate, error: error.message });
@@ -164,20 +209,22 @@ let currentLanguage = 'en';
164
209
  let isInitialized = false;
165
210
  const missingWarned = new Set();
166
211
 
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();
212
+ function loadTranslations(language) {
213
+ const cfg = safeRequireConfig();
214
+ const settings = cfg?.getConfig?.() || {};
215
+ const configuredLanguage = settings.uiLanguage || settings.language;
216
+
217
+ // Prioritize CLI argument, then UI language, then language fallback
218
+ const requested = (language || configuredLanguage || 'en').toString();
174
219
  const short = requested.split('-')[0].toLowerCase();
175
220
  const tryOrder = [requested, short, 'en'];
176
221
 
177
222
  const loadErrors = [];
178
223
 
224
+ const preferredDir = arguments.length > 1 ? arguments[1] : null;
225
+
179
226
  for (const lang of tryOrder) {
180
- const files = findLocaleFilesAllDirs(lang);
227
+ const files = findLocaleFilesAllDirs(lang, preferredDir);
181
228
 
182
229
  // Prioritize bundled locales over project ones
183
230
  const prioritizedFiles = files.sort((a, b) => Number(isBundledPath(b)) - Number(isBundledPath(a)));
@@ -353,12 +400,13 @@ function getAvailableLanguages() {
353
400
  for (const d of dirs) {
354
401
  try {
355
402
  const SecurityUtils = getSecurityUtils();
356
- if (!SecurityUtils.safeExistsSync(d)) continue;
403
+ if (!SecurityUtils.safeExistsSync(d, getValidationBase(d))) continue;
357
404
  for (const f of fs.readdirSync(d)) {
358
405
  if (f.endsWith('.json')) langs.add(path.basename(f, '.json'));
359
406
  }
360
407
  for (const f of fs.readdirSync(d, { withFileTypes: true })) {
361
- if (f.isDirectory() && SecurityUtils.safeExistsSync(path.join(d, f.name, `${f.name}.json`))) {
408
+ const nestedPath = path.join(d, f.name, `${f.name}.json`);
409
+ if (f.isDirectory() && SecurityUtils.safeExistsSync(nestedPath, getValidationBase(nestedPath))) {
362
410
  langs.add(f.name);
363
411
  }
364
412
  }
@@ -391,10 +439,10 @@ function deepMerge(target, source) {
391
439
  * Refresh language from settings manager
392
440
  * This ensures translations stay in sync with settings changes
393
441
  */
394
- function refreshLanguageFromSettings() {
395
- const cfg = safeRequireConfig();
396
- const settings = cfg?.getConfig?.() || {};
397
- const configuredLanguage = settings.uiLanguage || settings.language || 'en';
442
+ function refreshLanguageFromSettings() {
443
+ const cfg = safeRequireConfig();
444
+ const settings = cfg?.getConfig?.() || {};
445
+ const configuredLanguage = settings.uiLanguage || settings.language || 'en';
398
446
 
399
447
  if (configuredLanguage !== currentLanguage) {
400
448
  isInitialized = false;
@@ -417,4 +465,4 @@ module.exports = {
417
465
  deepMerge,
418
466
  refreshTranslations,
419
467
  refreshLanguageFromSettings
420
- };
468
+ };