i18ntk 4.5.3 → 4.6.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.
@@ -43,8 +43,9 @@ const configManager = require('../utils/config-manager');
43
43
  const SecurityUtils = require('../utils/security');
44
44
  const AdminCLI = require('../utils/admin-cli');
45
45
  const watchLocales = require('../utils/watch-locales');
46
- const { getGlobalReadline, closeGlobalReadline } = require('../utils/cli');
47
- const { getUnifiedConfig, parseCommonArgs, displayHelp, validateSourceDir, displayPaths } = require('../utils/config-helper');
46
+ const { getGlobalReadline, closeGlobalReadline } = require('../utils/cli');
47
+ const { getUnifiedConfig, parseCommonArgs, displayHelp, validateSourceDir, displayPaths } = require('../utils/config-helper');
48
+ const { isInteractive } = require('../utils/prompt-helper');
48
49
  const I18nInitializer = require('./i18ntk-init');
49
50
  const JsonOutput = require('../utils/json-output');
50
51
  const ExitCodes = require('../utils/exit-codes');
@@ -105,14 +106,21 @@ class I18nValidator {
105
106
  );
106
107
 
107
108
  // Use the i18n directory for language files
108
- this.sourceDir = this.config.i18nDir || this.config.sourceDir;
109
- this.i18nDir = this.config.i18nDir || this.config.sourceDir;
109
+ this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
110
+ this.i18nDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
110
111
  this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
111
112
 
112
- try {
113
- validateSourceDir(this.sourceDir, 'i18ntk-validate');
114
- } catch (err) {
115
- console.log(t('init.requiredTitle'));
113
+ try {
114
+ if (!SecurityUtils.safeExistsSync(this.sourceDir, process.cwd())) {
115
+ const err = new Error(`Locale directory not found: ${this.sourceDir}`);
116
+ err.exitCode = 2;
117
+ throw err;
118
+ }
119
+ } catch (err) {
120
+ if (!isInteractive(args)) {
121
+ throw err;
122
+ }
123
+ console.log(t('init.requiredTitle'));
116
124
  console.log(t('init.requiredBody'));
117
125
  const answer = await this.prompt(t('init.promptRunNow'));
118
126
  if (answer.trim().toLowerCase() === 'y') {
@@ -827,11 +835,13 @@ class I18nValidator {
827
835
  if (args.uiLanguage) {
828
836
  loadTranslations(args.uiLanguage, path.resolve(__dirname, '..', 'ui-locales'));}
829
837
 
830
- if (args.sourceDir) {
831
- this.config.sourceDir = args.sourceDir;
832
- this.sourceDir = path.resolve(this.config.sourceDir);
833
- this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
834
- }
838
+ const localeDirArg = args.i18nDir || (!args.i18nDir && args.sourceDir ? args.sourceDir : null);
839
+ if (localeDirArg) {
840
+ this.config.i18nDir = localeDirArg;
841
+ this.sourceDir = path.resolve(localeDirArg);
842
+ this.i18nDir = this.sourceDir;
843
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
844
+ }
835
845
  if (args.strictMode) {
836
846
  this.config.strictMode = true;
837
847
  }
@@ -1131,14 +1141,18 @@ class I18nValidator {
1131
1141
  { message: 'Starting validation run' }
1132
1142
  );
1133
1143
 
1134
- const result = await this.validate();
1135
-
1136
- console.log(t('validate.validationProcessCompletedSuccessfully'));
1137
- SecurityUtils.logSecurityEvent(
1138
- t('validate.runCompleted'),
1139
- 'info',
1140
- { message: 'Validation run completed successfully' }
1141
- );
1144
+ const result = await this.validate();
1145
+
1146
+ if (result.success) {
1147
+ console.log(t('validate.validationProcessCompletedSuccessfully'));
1148
+ } else {
1149
+ console.log('❌ Validation failed.');
1150
+ }
1151
+ SecurityUtils.logSecurityEvent(
1152
+ result.success ? t('validate.runCompleted') : t('validate.runError'),
1153
+ result.success ? 'info' : 'error',
1154
+ { message: result.success ? 'Validation run completed successfully' : 'Validation run failed' }
1155
+ );
1142
1156
  return result;
1143
1157
  };
1144
1158
 
@@ -1234,8 +1248,8 @@ if (require.main === module) {
1234
1248
  timestamp: new Date().toISOString()
1235
1249
  });
1236
1250
 
1237
- process.exit(result.success ? ExitCodes.SUCCESS : ExitCodes.VALIDATION_FAILED);
1238
- }
1251
+ process.exit(result.success ? ExitCodes.SUCCESS : ExitCodes.VALIDATION_FAILED);
1252
+ }
1239
1253
  } catch (error) {
1240
1254
  console.error(t('validate.fatalValidationError', { error: error.message }));
1241
1255
  console.error(t('validate.stackTrace', { stack: error.stack }));
@@ -1245,7 +1259,7 @@ if (require.main === module) {
1245
1259
  timestamp: new Date().toISOString()
1246
1260
  });
1247
1261
 
1248
- process.exit(ExitCodes.CONFIG_ERROR);
1262
+ process.exit(error.exitCode || ExitCodes.CONFIG_ERROR);
1249
1263
  }
1250
1264
  })();
1251
1265
  }
@@ -80,10 +80,17 @@ class AnalyzeCommand {
80
80
  const uiLanguage = (this.config && this.config.uiLanguage) || 'en';
81
81
  loadTranslations(uiLanguage, path.resolve(__dirname, '../../../ui-locales'));
82
82
 
83
- this.sourceDir = this.config.sourceDir;
83
+ this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
84
84
  this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
85
85
  this.outputDir = this.config.outputDir;
86
86
 
87
+ const explicitLocaleDir = args.i18nDir || (!args.i18nDir && args.sourceDir ? args.sourceDir : null);
88
+ if (explicitLocaleDir && !SecurityUtils.safeExistsSync(this.sourceDir, process.cwd())) {
89
+ const err = new Error(`Locale directory not found: ${this.sourceDir}`);
90
+ err.exitCode = 2;
91
+ throw err;
92
+ }
93
+
87
94
  // Validate source directory exists and is readable/writable
88
95
  this.validateSourceDirWithFallback(this.sourceDir);
89
96
 
@@ -766,12 +773,34 @@ class AnalyzeCommand {
766
773
  }
767
774
  }
768
775
 
769
- // Show help message
770
- showHelp() {
771
- console.log(t('analyze.help_message'));
772
- }
773
-
774
- // Main analyze method
776
+ // Show help message
777
+ showHelp() {
778
+ console.log(t('analyze.help_message'));
779
+ }
780
+
781
+ provideSetupGuidance() {
782
+ return [
783
+ 'Setup guidance:',
784
+ `- Ensure the locale root exists: ${this.sourceDir}`,
785
+ `- Ensure the source locale exists: ${path.join(this.sourceDir, this.config.sourceLanguage)}`,
786
+ '- Add at least one target locale, for example de/common.json or de.json.',
787
+ '- Prefer --locales-dir for locale files. Use --code-dir for application source files.'
788
+ ].join('\n');
789
+ }
790
+
791
+ detectStructureType() {
792
+ const sourceLocaleDir = path.join(this.sourceDir, this.config.sourceLanguage);
793
+ const sourceLocaleFile = path.join(this.sourceDir, `${this.config.sourceLanguage}.json`);
794
+ if (SecurityUtils.safeExistsSync(sourceLocaleFile, this.sourceDir)) {
795
+ return { type: 'monolith' };
796
+ }
797
+ if (SecurityUtils.safeExistsSync(sourceLocaleDir, this.sourceDir)) {
798
+ return { type: 'directory' };
799
+ }
800
+ return { type: 'unknown' };
801
+ }
802
+
803
+ // Main analyze method
775
804
  async analyze() {
776
805
  try {
777
806
  const results = [];
@@ -806,11 +835,11 @@ class AnalyzeCommand {
806
835
  });
807
836
  console.log(JSON.stringify(jsonOutput.data, null, args.indent || 2));
808
837
  return;
809
- }
810
- console.log(error);
811
- console.log('\n' + guidance);
812
- return;
813
- }
838
+ }
839
+ console.log(error);
840
+ console.log('\n' + guidance);
841
+ return { success: true, warnings: 1, message: error };
842
+ }
814
843
 
815
844
  if (!args.json) {
816
845
  console.log(t('analyze.foundLanguages', { count: languages.length, languages: languages.join(', ') }) || `📋 Found ${languages.length} languages to analyze: ${languages.join(', ')}`);
@@ -829,8 +858,11 @@ class AnalyzeCommand {
829
858
  const report = this.generateLanguageReport(analysis);
830
859
 
831
860
  // Save report
832
- const reportPath = await this.saveReport(language, report);
833
- const processedCount = results.length + 1;
861
+ const reportPath = await this.saveReport(language, report);
862
+ if (!reportPath) {
863
+ throw new Error(`Failed to save analysis report for ${language}`);
864
+ }
865
+ const processedCount = results.length + 1;
834
866
 
835
867
  if (!args.json) {
836
868
  console.log(t('analyze.completed', { language }) || `✅ Analysis completed for ${language}`);
@@ -898,9 +930,9 @@ class AnalyzeCommand {
898
930
  console.log(t('analyze.finished') || '\n✅ Analysis completed successfully!');
899
931
 
900
932
  // Only prompt for input if running standalone and not in no-prompt mode
901
- if (require.main === module && !this.noPrompt) {
902
- await this.prompt('\nPress Enter to continue...');
903
- }
933
+ if (require.main === module && !this.noPrompt && process.env.CI !== 'true' && process.stdin.isTTY && process.stdout.isTTY) {
934
+ await this.prompt('\nPress Enter to continue...');
935
+ }
904
936
  this.closeReadline();
905
937
 
906
938
  return results;
@@ -937,7 +969,7 @@ class AnalyzeCommand {
937
969
  const uiLanguage = this.config.uiLanguage || 'en';
938
970
  loadTranslations(uiLanguage, path.resolve(__dirname, '../../../ui-locales'));
939
971
 
940
- this.sourceDir = this.config.sourceDir;
972
+ this.sourceDir = this.config.localesDir || this.config.i18nDir || this.config.sourceDir;
941
973
  this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
942
974
  this.outputDir = this.config.outputDir;
943
975
  }
@@ -975,19 +1007,20 @@ class AnalyzeCommand {
975
1007
  loadTranslations(args.uiLanguage, path.resolve(__dirname, '../../../ui-locales'));}
976
1008
 
977
1009
  // Update config if source directory is provided
978
- if (args.sourceDir) {
979
- this.config.sourceDir = args.sourceDir;
980
- this.sourceDir = path.resolve(PROJECT_ROOT, this.config.sourceDir);
981
- this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
982
- }
1010
+ const localeDirArg = args.i18nDir || (!args.i18nDir && args.sourceDir ? args.sourceDir : null);
1011
+ if (localeDirArg) {
1012
+ this.config.i18nDir = localeDirArg;
1013
+ this.sourceDir = path.resolve(PROJECT_ROOT, localeDirArg);
1014
+ this.sourceLanguageDir = path.join(this.sourceDir, this.config.sourceLanguage);
1015
+ }
983
1016
 
984
1017
  if (args.outputDir) {
985
1018
  this.config.outputDir = args.outputDir;
986
1019
  this.outputDir = path.resolve(this.config.outputDir);
987
1020
  }
988
- const execute = async () => {
989
- await this.analyze();
990
- };
1021
+ const execute = async () => {
1022
+ return await this.analyze();
1023
+ };
991
1024
 
992
1025
  if (args.watch) {
993
1026
  await execute();
@@ -1003,19 +1036,21 @@ class AnalyzeCommand {
1003
1036
  });
1004
1037
  console.log('��� Watching for translation changes. Press Ctrl+C to exit.');
1005
1038
  } else {
1006
- await execute();
1007
- if (!fromMenu && require.main === module) {
1008
- process.exit(0);
1009
- }
1010
- }
1039
+ const result = await execute();
1040
+ if (!fromMenu && require.main === module) {
1041
+ process.exit(0);
1042
+ }
1043
+ return result;
1044
+ }
1011
1045
  } catch (error) {
1012
1046
  console.error(t('analyze.error') || '❌ Analysis failed:', error.message);
1013
1047
  this.closeReadline();
1014
- if (!fromMenu && require.main === module) {
1015
- process.exit(1);
1016
- }
1017
- }
1018
- }
1048
+ if (!fromMenu && require.main === module) {
1049
+ process.exit(1);
1050
+ }
1051
+ throw error;
1052
+ }
1053
+ }
1019
1054
 
1020
1055
  async runSetupWizard() {
1021
1056
  console.log('Translation Analysis Setup Wizard');
@@ -1132,14 +1167,14 @@ class AnalyzeCommand {
1132
1167
  /**
1133
1168
  * Execute the analyze command
1134
1169
  */
1135
- async execute(options = {}) {
1136
- try {
1137
- await this.initialize();
1138
- await this.run(options);
1139
- return { success: true, command: 'analyze' };
1140
- } catch (error) {
1141
- console.error(`Analyze command failed: ${error.message}`);
1142
- throw error;
1170
+ async execute(options = {}) {
1171
+ try {
1172
+ await this.initialize();
1173
+ const result = await this.run(options);
1174
+ return result && result.success === false ? result : { success: true, command: 'analyze', result };
1175
+ } catch (error) {
1176
+ console.error(`Analyze command failed: ${error.message}`);
1177
+ throw error;
1143
1178
  }
1144
1179
  }
1145
1180
 
@@ -9,6 +9,7 @@
9
9
 
10
10
  const fs = require('fs');
11
11
  const path = require('path');
12
+ const { detectProjectFramework, getFrameworkPatterns, getFrameworkSuggestions, SCANNER_EXTENSIONS, WRAPPER_SKIP_PATTERNS } = require('../../../utils/framework-detector');
12
13
  const { getUnifiedConfig, displayHelp } = require('../../../utils/config-helper');
13
14
  const { loadTranslations } = require('../../../utils/i18n-helper');
14
15
  const SecurityUtils = require('../../../utils/security');
@@ -122,9 +123,11 @@ class ScannerCommand {
122
123
  const value = valueParts.join('=');
123
124
 
124
125
  switch (key) {
125
- case 'source-dir':
126
- parsed.sourceDir = value || '';
127
- break;
126
+ case 'source-dir':
127
+ case 'code-dir':
128
+ case 'source-code-dir':
129
+ parsed.sourceDir = value || '';
130
+ break;
128
131
  case 'framework':
129
132
  parsed.framework = value || '';
130
133
  break;
@@ -146,9 +149,13 @@ class ScannerCommand {
146
149
  case 'output-report':
147
150
  parsed.outputReport = true;
148
151
  break;
149
- case 'include-tests':
150
- parsed.includeTests = true;
151
- break;
152
+ case 'include-tests':
153
+ parsed.includeTests = true;
154
+ break;
155
+ case 'source-language':
156
+ case 'source-locale':
157
+ parsed.sourceLanguage = value || '';
158
+ break;
152
159
  case 'help':
153
160
  case 'h':
154
161
  parsed.help = true;
@@ -201,11 +208,24 @@ class ScannerCommand {
201
208
  const packageJson = SecurityUtils.safeParseJSON(packageJsonContent);
202
209
  const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
203
210
 
211
+ if (deps.next || deps['next-intl'] || deps['next-i18next']) return 'next';
204
212
  if (deps.react || deps['react-dom']) return 'react';
205
213
  if (deps.vue || deps['vue-router']) return 'vue';
206
214
  if (deps['@angular/core'] || deps.angular) return 'angular';
207
- if (deps.next) return 'next';
208
215
  if (deps.svelte) return 'svelte';
216
+ if (deps.astro || deps['astro-i18next'] || deps['@astrojs/i18n']) return 'astro';
217
+ if (deps['@remix-run/react'] || deps['remix-i18next']) return 'remix';
218
+ if (deps.gatsby || deps['gatsby-plugin-react-i18next']) return 'gatsby';
219
+ if (deps['@builder.io/qwik'] || deps['qwik-speak']) return 'qwik';
220
+ if (deps['solid-js'] || deps['@solid-primitives/i18n']) return 'solid';
221
+ if (deps['ember-source'] || deps['ember-intl']) return 'ember';
222
+ if (deps.nuxt || deps['@nuxtjs/i18n']) return 'nuxt';
223
+
224
+ const cargoTomlPath = path.join(projectRoot, 'Cargo.toml');
225
+ if (SecurityUtils.safeExistsSync(cargoTomlPath, projectRoot)) return 'rust';
226
+
227
+ const goModPath = path.join(projectRoot, 'go.mod');
228
+ if (SecurityUtils.safeExistsSync(goModPath, projectRoot)) return 'go';
209
229
 
210
230
  return 'vanilla';
211
231
  } catch (error) {
@@ -231,57 +251,110 @@ class ScannerCommand {
231
251
 
232
252
  const frameworkSpecific = {
233
253
  react: [
234
- // React specific patterns - enhanced for i18next detection
235
254
  /children:\s*["']([^"']{2,99})["']/g,
236
255
  /dangerouslySetInnerHTML={{\s*__html:\s*["']([^"']{2,99})["']/g,
237
- // JSX text content without translation
238
256
  />([^<{][^<>{]*[^}>])</g,
239
- // Button text
240
257
  /<button[^>]*>([^<]{2,99})<\/button>/g,
241
- // Span text
242
- /<span[^>]*>([^<]{2,99})<\/span>/g
258
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
259
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g,
260
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*\{[']\s*([^'}]+\.[^'}]+)\s*[']/g
243
261
  ],
244
262
  vue: [
245
- // Vue specific patterns - enhanced for vue-i18n detection
246
263
  /v-text=["']([^"']{2,99})["']/g,
247
264
  /v-html=["']([^"']{2,99})["']/g,
248
- // Vue template text
249
265
  />([^<{][^<>{]*[^}>])</g,
250
- // Button text
251
266
  /<button[^>]*>([^<]{2,99})<\/button>/g,
252
- // Span text
253
- /<span[^>]*>([^<]{2,99})<\/span>/g
267
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
268
+ /\$t\(["']([^"']{2,99})["']\)/g,
269
+ /v-t=["']([^"']{2,99})["']/g
254
270
  ],
255
271
  angular: [
256
- // Angular specific patterns - enhanced for ngx-translate detection
257
272
  /\[innerHTML\]=["']([^"']{2,99})["']/g,
258
273
  /\[textContent\]=["']([^"']{2,99})["']/g,
259
- // Angular template text
260
274
  />([^<{][^<>{]*[^}>])</g,
261
- // Button text
262
275
  /<button[^>]*>([^<]{2,99})<\/button>/g,
263
- // Span text
264
- /<span[^>]*>([^<]{2,99})<\/span>/g
276
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
277
+ /i18n=["']([^"']{2,99})["']/g,
278
+ /\[attr\.title\]=["']([^"']{2,99})["']/g
279
+ ],
280
+ next: [
281
+ /children:\s*["']([^"']{2,99})["']/g,
282
+ />([^<{][^<>{]*[^}>])</g,
283
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
284
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
285
+ ],
286
+ svelte: [
287
+ />([^<{][^<>{]*[^}>])</g,
288
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
289
+ /\$_\(["']([^"']{2,99})["']\)/g,
290
+ /t\.set\(["']([^"']{2,99})["']/g,
291
+ /\{#if[^}]*\}([^<]{2,99})\{\/if\}/g
292
+ ],
293
+ astro: [
294
+ />([^<{][^<>{]*[^}>])</g,
295
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
296
+ /t\(["']([^"']{2,99})["']\)/g
297
+ ],
298
+ remix: [
299
+ /children:\s*["']([^"']{2,99})["']/g,
300
+ />([^<{][^<>{]*[^}>])</g,
301
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
302
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
303
+ ],
304
+ qwik: [
305
+ />([^<{][^<>{]*[^}>])</g,
306
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
307
+ /t\(["']([^"']{2,99})["']\)/g,
308
+ /useTranslate\(\)/g
309
+ ],
310
+ solid: [
311
+ />([^<{][^<>{]*[^}>])</g,
312
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
313
+ /t\(["']([^"']{2,99})["']\)/g,
314
+ /useI18n\(\)\.t\(["']([^"']{2,99})["']\)/g
315
+ ],
316
+ ember: [
317
+ />([^<{][^<>{]*[^}>])</g,
318
+ /\{\{t\s+["']([^"']{2,99})["']\s*\}\}/g,
319
+ /\{\{intl\.t\s+["']([^"']{2,99})["']\s*\}\}/g
320
+ ],
321
+ gatsby: [
322
+ /children:\s*["']([^"']{2,99})["']/g,
323
+ />([^<{][^<>{]*[^}>])</g,
324
+ /<(?:Trans)[\s\S]*?\bi18nKey\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
265
325
  ],
266
326
  django: [
267
- // Django template patterns
268
327
  /\{\%\s*trans\s+["']([^"']{2,99})["']\s*%\}/g,
269
328
  /\{\%\s*blocktrans\s*%\}([^%]{2,99})\{\%\s*endblocktrans\s*%\}/g,
270
329
  /{{\s*_["']([^"']{2,99})["']\s*}}/g,
271
330
  /{{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g
272
331
  ],
273
332
  flask: [
274
- // Flask/Jinja2 template patterns
275
333
  /\{\{\s*_["']([^"']{2,99})["']\s*}}/g,
276
334
  /\{\{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g,
277
335
  /\{\{\s*lazy_gettext\(["']([^"']{2,99})["']\)\s*}}/g
278
336
  ],
279
337
  python: [
280
- // Python source patterns
281
338
  /gettext\(["']([^"']{2,99})["']\)/g,
282
339
  /_\(["']([^"']{2,99})["']\)/g,
283
340
  /gettext_lazy\(["']([^"']{2,99})["']\)/g,
284
341
  /lazy_gettext\(["']([^"']{2,99})["']\)/g
342
+ ],
343
+ rust: [
344
+ /bundle\.get_message\(\s*["']([^"']{2,99})["']\)/g,
345
+ /\.get_message\s*\(\s*["']([^"']{2,99})["']\)/g,
346
+ /ts!\s*\(\s*["']([^"']{2,99})["']/g,
347
+ /fluent!\s*\(\s*["']([^"']{2,99})["']/g
348
+ ],
349
+ go: [
350
+ /i18n\.Translate\([^,]+,\s*["']([^"']{2,99})["']\)/g,
351
+ /i18n\.NewMessage\([^,]+,\s*["']([^"']{2,99})["']\)/g,
352
+ /t\.Get\([^,]+,\s*["']([^"']{2,99})["']\)/g
353
+ ],
354
+ vanilla: [
355
+ /t\(["']([^"']{2,99})["']\)/g,
356
+ /i18n\.t\(["']([^"']{2,99})["']\)/g,
357
+ /translate\(["']([^"']{2,99})["']\)/g
285
358
  ]
286
359
  };
287
360
 
@@ -374,24 +447,63 @@ class ScannerCommand {
374
447
  key: `ui.${key}`,
375
448
  original: text,
376
449
  translationKey: `t('ui.${key}')`,
377
- frameworkSpecific: this.getFrameworkSpecific(text)
450
+ frameworkSpecific: getFrameworkSuggestions(this.framework, text)
378
451
  };
379
452
  }
380
453
 
381
454
  getFrameworkSpecific(text) {
455
+ const keySnippet = (str) => str.toLowerCase()
456
+ .replace(/[^a-z0-9\s]/g, '')
457
+ .replace(/\s+/g, '_')
458
+ .substring(0, 40);
382
459
  const frameworks = {
383
460
  react: {
384
461
  hook: `const { t } = useTranslation();`,
385
- usage: `{t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')}`,
386
- component: `<Trans i18nKey="ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}">${text}</Trans>`
462
+ usage: `{t('ui.${keySnippet(text)}')}`,
463
+ component: `<Trans i18nKey="ui.${keySnippet(text)}">${text}</Trans>`
464
+ },
465
+ next: {
466
+ hook: `const t = useTranslations();`,
467
+ usage: `{t('ui.${keySnippet(text)}')}`,
468
+ component: `<Trans i18nKey="ui.${keySnippet(text)}">${text}</Trans>`
387
469
  },
388
470
  vue: {
389
- directive: `{{ $t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}') }}`,
390
- method: `this.$t('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')`
471
+ directive: `{{ $t('ui.${keySnippet(text)}') }}`,
472
+ method: `this.$t('ui.${keySnippet(text)}')`
391
473
  },
392
474
  angular: {
393
475
  pipe: `{{ '${text}' | translate }}`,
394
- service: `this.translateService.instant('ui.${text.toLowerCase().replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, '_')}')`
476
+ service: `this.translateService.instant('ui.${keySnippet(text)}')`
477
+ },
478
+ svelte: {
479
+ store: `$_(('ui.${keySnippet(text)}'))`,
480
+ method: `t.set('ui.${keySnippet(text)}', '${text}')`
481
+ },
482
+ astro: {
483
+ import: `import { t } from 'astro-i18next';`,
484
+ usage: `{t('ui.${keySnippet(text)}')}`
485
+ },
486
+ remix: {
487
+ hook: `const { t } = useTranslation();`,
488
+ usage: `{t('ui.${keySnippet(text)}')}`,
489
+ server: `export const handle = i18next.handle;`
490
+ },
491
+ qwik: {
492
+ hook: `const t = useTranslate();`,
493
+ usage: `{t('ui.${keySnippet(text)}')}`
494
+ },
495
+ solid: {
496
+ hook: `const [t] = useI18n();`,
497
+ usage: `{t('ui.${keySnippet(text)}')}`
498
+ },
499
+ ember: {
500
+ template: `{{t 'ui.${keySnippet(text)}'}}`,
501
+ helper: `this.intl.t('ui.${keySnippet(text)}')`
502
+ },
503
+ gatsby: {
504
+ hook: `const { t } = useTranslation();`,
505
+ usage: `{t('ui.${keySnippet(text)}')}`,
506
+ plugin: `'gatsby-plugin-react-i18next'`
395
507
  },
396
508
  django: {
397
509
  template: `{% trans '${text}' %}`,
@@ -407,6 +519,17 @@ class ScannerCommand {
407
519
  gettext: `import gettext\ngettext.gettext('${text}')`,
408
520
  underscore: `from gettext import gettext as _\n_('${text}')`,
409
521
  lazy: `from gettext import gettext_lazy as _\n_('${text}')`
522
+ },
523
+ rust: {
524
+ fluent: `bundle.get_message("ui_${keySnippet(text)}")`,
525
+ gettext: `gettext("ui_${keySnippet(text)}")`
526
+ },
527
+ go: {
528
+ translate: `i18n.Translate("ui_${keySnippet(text)}")`,
529
+ message: `i18n.NewMessage("ui_${keySnippet(text)}")`
530
+ },
531
+ vanilla: {
532
+ generic: `t('ui.${keySnippet(text)}')`
410
533
  }
411
534
  };
412
535
 
@@ -427,7 +550,7 @@ class ScannerCommand {
427
550
  }
428
551
 
429
552
  const allResults = [];
430
- const extensions = ['.js', '.jsx', '.ts', '.tsx', '.vue', '.html', '.svelte', '.py', '.pyx', '.pyi'];
553
+ const extensions = [...SCANNER_EXTENSIONS];
431
554
 
432
555
  const scanRecursive = (currentDir) => {
433
556
  const items = SecurityUtils.safeReaddirSync(currentDir, path.dirname(currentDir), { withFileTypes: true });
@@ -580,7 +703,7 @@ class ScannerCommand {
580
703
  } else if (fwPref && fwPref !== 'auto') {
581
704
  this.framework = fwPref;
582
705
  } else if (fwDetectEnabled) {
583
- const detected = this.detectFramework(process.cwd());
706
+ const detected = detectProjectFramework(process.cwd());
584
707
  this.framework = detected || fwFallback;
585
708
  } else {
586
709
  this.framework = fwFallback;
@@ -606,7 +729,7 @@ class ScannerCommand {
606
729
  console.log(this.t('scanner.starting', { framework: this.framework }));
607
730
  console.log(this.t('scanner.sourceDirectory', { sourceDir: this.sourceDir }));
608
731
 
609
- const patterns = this.getFrameworkPatterns(this.framework);
732
+ const patterns = getFrameworkPatterns(this.framework);
610
733
  const exclusions = this.config.exclude || ['node_modules', '.git', 'dist', 'build'];
611
734
  const minLength = this.config.minLength || 3;
612
735
  const maxLength = this.config.maxLength || 100;
@@ -669,4 +792,4 @@ class ScannerCommand {
669
792
  }
670
793
  }
671
794
 
672
- module.exports = ScannerCommand;
795
+ module.exports = ScannerCommand;