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.
@@ -15,7 +15,16 @@ const FRAMEWORK_COMPATIBILITY = {
15
15
  'python': { minVersion: '3.6.0' },
16
16
  'ruby': { minVersion: '2.7.0' },
17
17
  'ruby-on-rails': { minVersion: '6.0.0' },
18
- 'go': { minVersion: '1.16.0' }
18
+ 'go': { minVersion: '1.16.0' },
19
+ 'rust': { minVersion: '1.65.0' },
20
+ 'fluent': { minVersion: '0.16.0' },
21
+ 'remix': { minVersion: '1.0.0' },
22
+ 'gatsby': { minVersion: '4.0.0' },
23
+ 'astro': { minVersion: '2.0.0' },
24
+ 'qwik': { minVersion: '1.0.0' },
25
+ 'ember-intl': { minVersion: '5.0.0' },
26
+ 'react-native-localize': { minVersion: '2.0.0' },
27
+ 'ionic': { minVersion: '6.0.0' }
19
28
  };
20
29
 
21
30
  // Define framework detection in order of specificity
@@ -245,10 +254,10 @@ const FRAMEWORKS = {
245
254
  'test/tmp/**'
246
255
  ]
247
256
  },
248
- go: {
249
- name: 'go',
250
- deps: ['go-i18n', 'x-text'],
251
- globs: ['**/*.go'],
257
+ go: {
258
+ name: 'go',
259
+ deps: ['go-i18n', 'x-text'],
260
+ globs: ['**/*.go'],
252
261
  patterns: [
253
262
  /i18n\.NewMessage\([^,]+,\s*["`]([^"`]+)["`]/g,
254
263
  /i18n\.NewLocalizer\([^)]+\)\.MustLocalize\([^,]+,\s*["`]([^"`]+)["`]/g,
@@ -275,6 +284,27 @@ const FRAMEWORKS = {
275
284
  ]
276
285
  },
277
286
 
287
+ rust: {
288
+ name: 'rust',
289
+ deps: ['fluent', 'fluent-bundle', 'fluent-resmgr', 'fluent-rs', 'intl-memoizer', 'unic-langid'],
290
+ globs: ['**/*.rs'],
291
+ patterns: [
292
+ /\bLocalization::new\(\s*&\[\s*([^\]]+)\s*\],\s*&\[\s*([^\]]+)\s*\]\)/g,
293
+ /bundle\.get_message\(\s*["`]([^"`]+)["`]/g,
294
+ /\.get_message\s*\(\s*["`]([^"`]+)["`]/g,
295
+ /\bformat!\s*\(\s*["`]([^"`]+)["`]/g
296
+ ],
297
+ ignore: [
298
+ 'target/**',
299
+ '**/*_test.rs',
300
+ '**/test/**',
301
+ '**/tests/**',
302
+ '**/examples/**',
303
+ '**/benches/**',
304
+ '**/migrations/**'
305
+ ]
306
+ },
307
+
278
308
  'ngx-translate': {
279
309
  name: 'Angular ngx-translate',
280
310
  deps: ['@ngx-translate/core', '@ngx-translate/http-loader'],
@@ -322,9 +352,287 @@ const FRAMEWORKS = {
322
352
  /\buseI18n\(\s*\)\s*\.\s*t\(\s*['"`]([^'"`]+)['"`]/g,
323
353
  /\bt\(\s*['"`]([^'"`]+)['"`]/g
324
354
  ]
355
+ },
356
+
357
+ remix: {
358
+ name: 'Remix i18n',
359
+ deps: ['remix-i18next', 'i18next-remix', '@remix-run/react'],
360
+ globs: ['app/**/*.{js,jsx,ts,tsx}'],
361
+ patterns: [
362
+ /\buseTranslation\(\s*['"`][^'"`]*['"`]?\s*\)/g,
363
+ /\bt\(\s*['"`]([^'"`]+)['"`]/g,
364
+ /\bgetServerSideTranslations\(/g
365
+ ]
366
+ },
367
+
368
+ gatsby: {
369
+ name: 'Gatsby i18n',
370
+ deps: ['gatsby-plugin-react-i18next', '@herob191/gatsby-plugin-react-i18next', 'gatsby-plugin-intl', 'gatsby-theme-i18n'],
371
+ globs: ['src/**/*.{js,jsx,ts,tsx}'],
372
+ patterns: [
373
+ /\bt\(\s*['"`]([^'"`]+)['"`]/g,
374
+ /\buseTranslation\(\s*['"`][^'"`]*['"`]?\s*\)/g,
375
+ /\bgraphql\s*`[^`]*locales[^`]*`/g
376
+ ]
377
+ },
378
+
379
+ astro: {
380
+ name: 'Astro i18n',
381
+ deps: ['astro-i18next', '@astrojs/i18n', 'astro-i18n', '@inox-tools/astro-i18n'],
382
+ globs: ['src/**/*.{astro,js,jsx,ts,tsx}'],
383
+ patterns: [
384
+ /\bt\(\s*['"`]([^'"`]+)['"`]/g,
385
+ /\bAstro\.i18n\(\s*['"`]([^'"`]+)['"`]/g,
386
+ /\buseTranslations\(\s*['"`][^'"`]*['"`]?\s*\)/g
387
+ ]
388
+ },
389
+
390
+ qwik: {
391
+ name: 'Qwik i18n',
392
+ deps: ['qwik-speak', 'qwik-i18n', '@qwikdev/i18n'],
393
+ globs: ['src/**/*.{tsx,ts,jsx,js}'],
394
+ patterns: [
395
+ /\buseTranslate\(\s*\)/g,
396
+ /\bt\(\s*['"`]([^'"`]+)['"`]/g,
397
+ /\buseSpeak\(\s*\)/g
398
+ ]
399
+ },
400
+
401
+ 'ember-intl': {
402
+ name: 'Ember Intl',
403
+ deps: ['ember-intl', '@ember-intl/formatjs'],
404
+ globs: ['app/**/*.{js,ts,hbs}'],
405
+ patterns: [
406
+ /\bintl\.t\(\s*['"`]([^'"`]+)['"`]/g,
407
+ /\{\{\s*t\s+['"`]([^'"`]+)['"`]\s*\}\}/g,
408
+ /\{\{t\s+['"`]([^'"`]+)['"`]\s*\}\}/g
409
+ ]
410
+ },
411
+
412
+ 'react-native-localize': {
413
+ name: 'React Native Localize',
414
+ deps: ['react-native-localize', 'i18n-js', 'expo-localization'],
415
+ globs: ['**/*.{js,jsx,ts,tsx}'],
416
+ patterns: [
417
+ /\bi18n\.t\(\s*['"`]([^'"`]+)['"`]/g,
418
+ /\bt\(\s*['"`]([^'"`]+)['"`]/g,
419
+ /\btranslate\(\s*['"`]([^'"`]+)['"`]/g
420
+ ]
421
+ },
422
+
423
+ ionic: {
424
+ name: 'Ionic i18n',
425
+ deps: ['@ionic/angular', 'ionic-react', '@ionic/vue'],
426
+ globs: ['src/**/*.{ts,tsx,html}'],
427
+ patterns: [
428
+ /\btranslateService\.instant\(\s*['"`]([^'"`]+)['"`]/g,
429
+ /\btranslateService\.get\(\s*['"`]([^'"`]+)['"`]/g,
430
+ /\|\s*translate\s*[^}]*\}\}/g
431
+ ]
325
432
  }
326
433
  };
327
434
 
435
+ // ============================================================
436
+ // CENTRALIZED SHARED CONSTANTS — single source of truth
437
+ // All scanner/validator/extension code imports from here.
438
+ // ============================================================
439
+
440
+ const SOURCE_EXTENSIONS = new Set([
441
+ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts',
442
+ '.vue', '.svelte', '.astro', '.mdx', '.html', '.rs',
443
+ '.py', '.pyx', '.pyi', '.go', '.rb', '.java', '.php', '.hbs'
444
+ ]);
445
+
446
+ const SCANNER_EXTENSIONS = new Set([
447
+ '.js', '.jsx', '.ts', '.tsx', '.mjs', '.mts', '.cjs', '.cts',
448
+ '.vue', '.html', '.svelte', '.astro', '.mdx',
449
+ '.py', '.pyx', '.pyi', '.php', '.rb', '.go', '.rs', '.java'
450
+ ]);
451
+
452
+ const EXCLUDE_DIRS = new Set([
453
+ 'node_modules', '.git', '.hg', '.svn',
454
+ '.next', '.nuxt', '.output', '.astro', '.svelte-kit', '.cache', '__generated__',
455
+ 'dist', 'build', 'coverage', 'out', 'target',
456
+ 'vendor', 'tmp', 'temp', '.terraform'
457
+ ]);
458
+
459
+ const SOURCE_DIRS = [
460
+ 'src', 'app', 'lib', 'source', 'pages', 'routes', 'components',
461
+ 'layouts', 'api', 'content', 'modules', 'views'
462
+ ];
463
+
464
+ const FRAMEWORK_PATTERNS = {
465
+ react: [
466
+ /children:\s*["']([^"']{2,99})["']/g,
467
+ /dangerouslySetInnerHTML={{\s*__html:\s*["']([^"']{2,99})["']/g,
468
+ />([^<{][^<>{]*[^}>])</g,
469
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
470
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
471
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
472
+ ],
473
+ next: [
474
+ /children:\s*["']([^"']{2,99})["']/g,
475
+ />([^<{][^<>{]*[^}>])</g,
476
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
477
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
478
+ ],
479
+ vue: [
480
+ /v-text=["']([^"']{2,99})["']/g,
481
+ /v-html=["']([^"']{2,99})["']/g,
482
+ />([^<{][^<>{]*[^}>])</g,
483
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
484
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
485
+ /\$t\(["']([^"']{2,99})["']\)/g,
486
+ /v-t=["']([^"']{2,99})["']/g
487
+ ],
488
+ angular: [
489
+ /\[innerHTML\]=["']([^"']{2,99})["']/g,
490
+ /\[textContent\]=["']([^"']{2,99})["']/g,
491
+ />([^<{][^<>{]*[^}>])</g,
492
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
493
+ /<span[^>]*>([^<]{2,99})<\/span>/g,
494
+ /i18n=["']([^"']{2,99})["']/g,
495
+ /\[attr\.title\]=["']([^"']{2,99})["']/g
496
+ ],
497
+ svelte: [
498
+ />([^<{][^<>{]*[^}>])</g,
499
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
500
+ /\$_\(["']([^"']{2,99})["']\)/g,
501
+ /t\.set\(["']([^"']{2,99})["']/g,
502
+ /\{#if[^}]*\}([^<]{2,99})\{\/if\}/g
503
+ ],
504
+ astro: [
505
+ />([^<{][^<>{]*[^}>])</g,
506
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
507
+ /t\(["']([^"']{2,99})["']\)/g
508
+ ],
509
+ remix: [
510
+ /children:\s*["']([^"']{2,99})["']/g,
511
+ />([^<{][^<>{]*[^}>])</g,
512
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
513
+ /<(?:FormattedMessage|Trans)[\s\S]*?\b(?:id|defaultMessage|i18nKey)\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
514
+ ],
515
+ qwik: [
516
+ />([^<{][^<>{]*[^}>])</g,
517
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
518
+ /t\(["']([^"']{2,99})["']\)/g,
519
+ /useTranslate\(\)/g
520
+ ],
521
+ solid: [
522
+ />([^<{][^<>{]*[^}>])</g,
523
+ /<button[^>]*>([^<]{2,99})<\/button>/g,
524
+ /t\(["']([^"']{2,99})["']\)/g,
525
+ /useI18n\(\)\.t\(["']([^"']{2,99})["']\)/g
526
+ ],
527
+ ember: [
528
+ />([^<{][^<>{]*[^}>])</g,
529
+ /\{\{t\s+["']([^"']{2,99})["']\s*\}\}/g,
530
+ /\{\{intl\.t\s+["']([^"']{2,99})["']\s*\}\}/g
531
+ ],
532
+ gatsby: [
533
+ /children:\s*["']([^"']{2,99})["']/g,
534
+ />([^<{][^<>{]*[^}>])</g,
535
+ /<(?:Trans)[\s\S]*?\bi18nKey\s*=\s*(?:\{|)(['"`])([^'"`}]+)\1[^>]*>/g
536
+ ],
537
+ django: [
538
+ /\{\%\s*trans\s+["']([^"']{2,99})["']\s*%\}/g,
539
+ /\{\%\s*blocktrans\s*%\}([^%]{2,99})\{\%\s*endblocktrans\s*%\}/g,
540
+ /{{\s*_["']([^"']{2,99})["']\s*}}/g,
541
+ /{{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g
542
+ ],
543
+ flask: [
544
+ /\{\{\s*_["']([^"']{2,99})["']\s*}}/g,
545
+ /\{\{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g,
546
+ /\{\{\s*lazy_gettext\(["']([^"']{2,99})["']\)\s*}}/g
547
+ ],
548
+ python: [
549
+ /gettext\(["']([^"']{2,99})["']\)/g,
550
+ /_\(["']([^"']{2,99})["']\)/g,
551
+ /gettext_lazy\(["']([^"']{2,99})["']\)/g,
552
+ /lazy_gettext\(["']([^"']{2,99})["']\)/g
553
+ ],
554
+ rust: [
555
+ /bundle\.get_message\(\s*["']([^"']{2,99})["']\)/g,
556
+ /\.get_message\s*\(\s*["']([^"']{2,99})["']\)/g,
557
+ /ts!\s*\(\s*["']([^"']{2,99})["']/g,
558
+ /fluent!\s*\(\s*["']([^"']{2,99})["']/g
559
+ ],
560
+ go: [
561
+ /i18n\.Translate\([^,]+,\s*["']([^"']{2,99})["']\)/g,
562
+ /i18n\.NewMessage\([^,]+,\s*["']([^"']{2,99})["']\)/g,
563
+ /t\.Get\([^,]+,\s*["']([^"']{2,99})["']\)/g
564
+ ],
565
+ vanilla: [
566
+ /t\(["']([^"']{2,99})["']\)/g,
567
+ /i18n\.t\(["']([^"']{2,99})["']\)/g,
568
+ /translate\(["']([^"']{2,99})["']\)/g
569
+ ]
570
+ };
571
+
572
+ const FRAMEWORK_SUGGESTIONS = {
573
+ react: { hook: 'const { t } = useTranslation();', usage: "{t('ui.KEY')}", component: "<Trans i18nKey=\"ui.KEY\">text</Trans>" },
574
+ next: { hook: 'const t = useTranslations();', usage: "{t('ui.KEY')}", component: "<Trans i18nKey=\"ui.KEY\">text</Trans>" },
575
+ vue: { directive: "{{ $t('ui.KEY') }}", method: "this.$t('ui.KEY')" },
576
+ angular: { pipe: "{{ 'text' | translate }}", service: "this.translateService.instant('ui.KEY')" },
577
+ svelte: { store: "$_(('ui.KEY'))", method: "t.set('ui.KEY', 'text')" },
578
+ astro: { import: "import { t } from 'astro-i18next';", usage: "{t('ui.KEY')}" },
579
+ remix: { hook: 'const { t } = useTranslation();', usage: "{t('ui.KEY')}", server: 'export const handle = i18next.handle;' },
580
+ qwik: { hook: 'const t = useTranslate();', usage: "{t('ui.KEY')}" },
581
+ solid: { hook: 'const [t] = useI18n();', usage: "{t('ui.KEY')}" },
582
+ ember: { template: "{{t 'ui.KEY'}}", helper: "this.intl.t('ui.KEY')" },
583
+ gatsby: { hook: 'const { t } = useTranslation();', usage: "{t('ui.KEY')}", plugin: "'gatsby-plugin-react-i18next'" },
584
+ django: { template: "{% trans 'text' %}", python: "from django.utils.translation import gettext as _\n_('text')", model: "from django.utils.translation import gettext_lazy as _\n_('text')" },
585
+ flask: { template: "{{ _('text') }}", python: "from flask_babel import gettext as _\n_('text')", lazy: "from flask_babel import lazy_gettext as _\n_('text')" },
586
+ python: { gettext: "import gettext\ngettext.gettext('text')", underscore: "from gettext import gettext as _\n_('text')", lazy: "from gettext import gettext_lazy as _\n_('text')" },
587
+ rust: { fluent: 'bundle.get_message("ui_KEY")', gettext: 'gettext("ui_KEY")' },
588
+ go: { translate: 'i18n.Translate("ui_KEY")', message: 'i18n.NewMessage("ui_KEY")' },
589
+ vanilla: { generic: "t('ui.KEY')" }
590
+ };
591
+
592
+ const WRAPPER_SKIP_PATTERNS = [
593
+ 't(', 'tx(', 'i18n.t(', 'i18n.translate(', 'translate(',
594
+ 'useI18n(', 'useTranslation(', '__', '__t', '$_(', '$t(',
595
+ 'gettext(', 'gettext_lazy(', 'lazy_gettext(', 'pgettext(', 'ngettext('
596
+ ];
597
+
598
+ function _keySnippet(text) {
599
+ return String(text || '').toLowerCase()
600
+ .replace(/[^a-z0-9\s]/g, '')
601
+ .replace(/\s+/g, '_')
602
+ .substring(0, 40);
603
+ }
604
+
605
+ function getFrameworkPatterns(framework) {
606
+ const base = [
607
+ /(?<![\w])["'`]([^"'`]{2,99})["'`]/g,
608
+ /`([^`]{2,99})`/g,
609
+ />([^<]{2,99})</g,
610
+ /title=["']([^"']{2,99})["']/g,
611
+ /alt=["']([^"']{2,99})["']/g,
612
+ /placeholder=["']([^"']{2,99})["']/g
613
+ ];
614
+ return [...base, ...(FRAMEWORK_PATTERNS[framework] || FRAMEWORK_PATTERNS.vanilla || [])];
615
+ }
616
+
617
+ function getFrameworkSuggestions(framework, text) {
618
+ const template = FRAMEWORK_SUGGESTIONS[framework] || FRAMEWORK_SUGGESTIONS.vanilla || {};
619
+ const result = {};
620
+ for (const [k, v] of Object.entries(template)) {
621
+ result[k] = v.replace(/ui\.KEY/g, `ui.${_keySnippet(text)}`).replace(/\btext\b/g, text);
622
+ }
623
+ return result;
624
+ }
625
+
626
+ function getSourceExtensions() { return [...SOURCE_EXTENSIONS]; }
627
+
628
+ function getExcludeDirs() { return [...EXCLUDE_DIRS]; }
629
+
630
+ function detectProjectFramework(projectRoot) {
631
+ const detected = detectFramework(projectRoot);
632
+ if (detected && detected.id) return detected.id;
633
+ return 'vanilla';
634
+ }
635
+
328
636
  /**
329
637
  * Detects the i18n framework being used in the project
330
638
  * @param {string} projectRoot - Path to the project root
@@ -414,4 +722,7 @@ function detectFramework(projectRoot) {
414
722
  }
415
723
  }
416
724
 
417
- module.exports = { detectFramework, FRAMEWORKS };
725
+ module.exports = { detectFramework, FRAMEWORKS, FRAMEWORK_COMPATIBILITY,
726
+ SOURCE_EXTENSIONS, SCANNER_EXTENSIONS, EXCLUDE_DIRS, SOURCE_DIRS,
727
+ FRAMEWORK_PATTERNS, FRAMEWORK_SUGGESTIONS, WRAPPER_SKIP_PATTERNS,
728
+ getFrameworkPatterns, getFrameworkSuggestions, getSourceExtensions, getExcludeDirs, detectProjectFramework };
@@ -7,7 +7,7 @@ function isInteractive(opts={}) {
7
7
  envManager.get('npm_config_loglevel') === 'silent';
8
8
  if (opts.noPrompt) return false;
9
9
  if (envSilent) return false;
10
- return process.stdin.isTTY === true;
10
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
11
11
  }
12
12
 
13
13
  class NoopPrompt {
@@ -2,8 +2,12 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
+ const { getSourceExtensions, getExcludeDirs } = require('./framework-detector');
5
6
  const SecurityUtils = require('./security');
6
7
 
8
+ const EXCLUDE_DIRS = new Set(getExcludeDirs());
9
+ const SOURCE_EXTENSIONS = getSourceExtensions();
10
+
7
11
  const ISSUE_TYPES = new Set([
8
12
  'missing_key',
9
13
  'unused_key',
@@ -265,7 +269,7 @@ function discoverLocaleFiles(localesDir) {
265
269
  function walk(dir, extensions, acc = [], basePath = process.cwd()) {
266
270
  if (!SecurityUtils.safeExistsSync(dir, basePath)) return acc;
267
271
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
268
- if (['node_modules', '.git', 'dist', 'build', 'coverage', 'out', 'tmp', 'temp', 'test', 'tests', 'test_env', 'i18ntk-reports'].includes(entry.name)) continue;
272
+ if (EXCLUDE_DIRS.has(entry.name)) continue;
269
273
  const entryPath = path.join(dir, entry.name);
270
274
  if (entry.isDirectory()) walk(entryPath, extensions, acc, basePath);
271
275
  else if (entry.isFile() && extensions.includes(path.extname(entry.name))) acc.push(entryPath);
@@ -290,7 +294,7 @@ function scanSourceFiles(sourceDir, availableKeys) {
290
294
  const hardcodedTexts = [];
291
295
  const translationValueIndex = new Map();
292
296
  const basePath = path.resolve(sourceDir);
293
- const files = walk(sourceDir, ['.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte'], [], basePath);
297
+ const files = walk(sourceDir, SOURCE_EXTENSIONS, [], basePath);
294
298
 
295
299
  for (const file of files) {
296
300
  const content = SecurityUtils.safeReadFileSync(file, basePath, 'utf8');
@@ -339,7 +343,13 @@ function findKeyByValue(_text, _availableKeys) {
339
343
  function extractPlaceholders(value) {
340
344
  const placeholders = new Set();
341
345
  const text = String(value || '');
342
- const patterns = [/\{\{\s*([\w.-]+)\s*\}\}/g, /%\{([\w.-]+)\}/g, /\{([\w.-]+)\}/g];
346
+ const patterns = [
347
+ /\{\{\s*([\w.-]+)\s*\}\}/g,
348
+ /%\{([\w.-]+)\}/g,
349
+ /\{([\w.-]+)\}/g,
350
+ /\$\{([\w.-]+)\}/g,
351
+ /\$([a-zA-Z_][a-zA-Z0-9_-]+)/g
352
+ ];
343
353
  for (const pattern of patterns) {
344
354
  let match;
345
355
  while ((match = pattern.exec(text))) placeholders.add(match[1]);
@@ -356,7 +366,10 @@ function looksLikelyUntranslated(source, target, locale) {
356
366
  }
357
367
 
358
368
  function normalizeText(value) {
359
- return String(value || '').replace(/\{\{[^}]+\}\}|\{[^}]+\}|%\{[^}]+\}/g, '').trim().toLowerCase();
369
+ return String(value || '')
370
+ .replace(/\{\{[^}]+\}\}|\{[^}]+\}|%\{[^}]+\}/g, '')
371
+ .replace(/\$[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\{\s*[^}]+\})?/g, '')
372
+ .trim().toLowerCase();
360
373
  }
361
374
 
362
375
  function getExpansionPercent(source, target) {
@@ -2,8 +2,9 @@
2
2
 
3
3
  const path = require('path');
4
4
  const SecurityUtils = require('./security');
5
+ const { SOURCE_DIRS } = require('./framework-detector');
5
6
 
6
- const DEFAULT_SOURCE_DIRS = ['src', 'app', 'lib', 'source'];
7
+ const DEFAULT_SOURCE_DIRS = SOURCE_DIRS;
7
8
 
8
9
  function resolveUsageSourceDir(options = {}) {
9
10
  const projectRoot = path.resolve(options.projectRoot || process.cwd());