i18ntk 4.5.4 → 4.6.1
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.
- package/CHANGELOG.md +45 -2
- package/README.md +124 -536
- package/main/i18ntk-init.js +56 -25
- package/main/i18ntk-scanner.js +148 -29
- package/main/i18ntk-translate.js +6 -3
- package/main/manage/commands/ScannerCommand.js +145 -28
- package/main/manage/services/FrameworkDetectionService.js +67 -9
- package/main/manage/services/InitService.js +56 -25
- package/package.json +27 -43
- package/utils/config-helper.js +2 -2
- package/utils/framework-detector.js +317 -6
- package/utils/report-model.js +17 -4
- package/utils/usage-source.js +2 -1
- package/utils/validation-risk.js +22 -13
|
@@ -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');
|
|
@@ -207,11 +208,24 @@ class ScannerCommand {
|
|
|
207
208
|
const packageJson = SecurityUtils.safeParseJSON(packageJsonContent);
|
|
208
209
|
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
209
210
|
|
|
211
|
+
if (deps.next || deps['next-intl'] || deps['next-i18next']) return 'next';
|
|
210
212
|
if (deps.react || deps['react-dom']) return 'react';
|
|
211
213
|
if (deps.vue || deps['vue-router']) return 'vue';
|
|
212
214
|
if (deps['@angular/core'] || deps.angular) return 'angular';
|
|
213
|
-
if (deps.next) return 'next';
|
|
214
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';
|
|
215
229
|
|
|
216
230
|
return 'vanilla';
|
|
217
231
|
} catch (error) {
|
|
@@ -237,57 +251,110 @@ class ScannerCommand {
|
|
|
237
251
|
|
|
238
252
|
const frameworkSpecific = {
|
|
239
253
|
react: [
|
|
240
|
-
// React specific patterns - enhanced for i18next detection
|
|
241
254
|
/children:\s*["']([^"']{2,99})["']/g,
|
|
242
255
|
/dangerouslySetInnerHTML={{\s*__html:\s*["']([^"']{2,99})["']/g,
|
|
243
|
-
// JSX text content without translation
|
|
244
256
|
/>([^<{][^<>{]*[^}>])</g,
|
|
245
|
-
// Button text
|
|
246
257
|
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
247
|
-
|
|
248
|
-
/<
|
|
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
|
|
249
261
|
],
|
|
250
262
|
vue: [
|
|
251
|
-
// Vue specific patterns - enhanced for vue-i18n detection
|
|
252
263
|
/v-text=["']([^"']{2,99})["']/g,
|
|
253
264
|
/v-html=["']([^"']{2,99})["']/g,
|
|
254
|
-
// Vue template text
|
|
255
265
|
/>([^<{][^<>{]*[^}>])</g,
|
|
256
|
-
// Button text
|
|
257
266
|
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
258
|
-
|
|
259
|
-
|
|
267
|
+
/<span[^>]*>([^<]{2,99})<\/span>/g,
|
|
268
|
+
/\$t\(["']([^"']{2,99})["']\)/g,
|
|
269
|
+
/v-t=["']([^"']{2,99})["']/g
|
|
260
270
|
],
|
|
261
271
|
angular: [
|
|
262
|
-
// Angular specific patterns - enhanced for ngx-translate detection
|
|
263
272
|
/\[innerHTML\]=["']([^"']{2,99})["']/g,
|
|
264
273
|
/\[textContent\]=["']([^"']{2,99})["']/g,
|
|
265
|
-
// Angular template text
|
|
266
274
|
/>([^<{][^<>{]*[^}>])</g,
|
|
267
|
-
// Button text
|
|
268
275
|
/<button[^>]*>([^<]{2,99})<\/button>/g,
|
|
269
|
-
|
|
270
|
-
|
|
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
|
|
271
325
|
],
|
|
272
326
|
django: [
|
|
273
|
-
// Django template patterns
|
|
274
327
|
/\{\%\s*trans\s+["']([^"']{2,99})["']\s*%\}/g,
|
|
275
328
|
/\{\%\s*blocktrans\s*%\}([^%]{2,99})\{\%\s*endblocktrans\s*%\}/g,
|
|
276
329
|
/{{\s*_["']([^"']{2,99})["']\s*}}/g,
|
|
277
330
|
/{{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g
|
|
278
331
|
],
|
|
279
332
|
flask: [
|
|
280
|
-
// Flask/Jinja2 template patterns
|
|
281
333
|
/\{\{\s*_["']([^"']{2,99})["']\s*}}/g,
|
|
282
334
|
/\{\{\s*gettext\(["']([^"']{2,99})["']\)\s*}}/g,
|
|
283
335
|
/\{\{\s*lazy_gettext\(["']([^"']{2,99})["']\)\s*}}/g
|
|
284
336
|
],
|
|
285
337
|
python: [
|
|
286
|
-
// Python source patterns
|
|
287
338
|
/gettext\(["']([^"']{2,99})["']\)/g,
|
|
288
339
|
/_\(["']([^"']{2,99})["']\)/g,
|
|
289
340
|
/gettext_lazy\(["']([^"']{2,99})["']\)/g,
|
|
290
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
|
|
291
358
|
]
|
|
292
359
|
};
|
|
293
360
|
|
|
@@ -380,24 +447,63 @@ class ScannerCommand {
|
|
|
380
447
|
key: `ui.${key}`,
|
|
381
448
|
original: text,
|
|
382
449
|
translationKey: `t('ui.${key}')`,
|
|
383
|
-
frameworkSpecific: this.
|
|
450
|
+
frameworkSpecific: getFrameworkSuggestions(this.framework, text)
|
|
384
451
|
};
|
|
385
452
|
}
|
|
386
453
|
|
|
387
454
|
getFrameworkSpecific(text) {
|
|
455
|
+
const keySnippet = (str) => str.toLowerCase()
|
|
456
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
457
|
+
.replace(/\s+/g, '_')
|
|
458
|
+
.substring(0, 40);
|
|
388
459
|
const frameworks = {
|
|
389
460
|
react: {
|
|
390
461
|
hook: `const { t } = useTranslation();`,
|
|
391
|
-
usage: `{t('ui.${text
|
|
392
|
-
component: `<Trans i18nKey="ui.${text
|
|
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>`
|
|
393
469
|
},
|
|
394
470
|
vue: {
|
|
395
|
-
directive: `{{ $t('ui.${text
|
|
396
|
-
method: `this.$t('ui.${text
|
|
471
|
+
directive: `{{ $t('ui.${keySnippet(text)}') }}`,
|
|
472
|
+
method: `this.$t('ui.${keySnippet(text)}')`
|
|
397
473
|
},
|
|
398
474
|
angular: {
|
|
399
475
|
pipe: `{{ '${text}' | translate }}`,
|
|
400
|
-
service: `this.translateService.instant('ui.${text
|
|
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'`
|
|
401
507
|
},
|
|
402
508
|
django: {
|
|
403
509
|
template: `{% trans '${text}' %}`,
|
|
@@ -413,6 +519,17 @@ class ScannerCommand {
|
|
|
413
519
|
gettext: `import gettext\ngettext.gettext('${text}')`,
|
|
414
520
|
underscore: `from gettext import gettext as _\n_('${text}')`,
|
|
415
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)}')`
|
|
416
533
|
}
|
|
417
534
|
};
|
|
418
535
|
|
|
@@ -433,7 +550,7 @@ class ScannerCommand {
|
|
|
433
550
|
}
|
|
434
551
|
|
|
435
552
|
const allResults = [];
|
|
436
|
-
const extensions = [
|
|
553
|
+
const extensions = [...SCANNER_EXTENSIONS];
|
|
437
554
|
|
|
438
555
|
const scanRecursive = (currentDir) => {
|
|
439
556
|
const items = SecurityUtils.safeReaddirSync(currentDir, path.dirname(currentDir), { withFileTypes: true });
|
|
@@ -586,7 +703,7 @@ class ScannerCommand {
|
|
|
586
703
|
} else if (fwPref && fwPref !== 'auto') {
|
|
587
704
|
this.framework = fwPref;
|
|
588
705
|
} else if (fwDetectEnabled) {
|
|
589
|
-
const detected =
|
|
706
|
+
const detected = detectProjectFramework(process.cwd());
|
|
590
707
|
this.framework = detected || fwFallback;
|
|
591
708
|
} else {
|
|
592
709
|
this.framework = fwFallback;
|
|
@@ -612,7 +729,7 @@ class ScannerCommand {
|
|
|
612
729
|
console.log(this.t('scanner.starting', { framework: this.framework }));
|
|
613
730
|
console.log(this.t('scanner.sourceDirectory', { sourceDir: this.sourceDir }));
|
|
614
731
|
|
|
615
|
-
const patterns =
|
|
732
|
+
const patterns = getFrameworkPatterns(this.framework);
|
|
616
733
|
const exclusions = this.config.exclude || ['node_modules', '.git', 'dist', 'build'];
|
|
617
734
|
const minLength = this.config.minLength || 3;
|
|
618
735
|
const maxLength = this.config.maxLength || 100;
|
|
@@ -40,6 +40,7 @@ module.exports = class FrameworkDetectionService {
|
|
|
40
40
|
const goModPath = path.join(process.cwd(), 'go.mod');
|
|
41
41
|
const pomPath = path.join(process.cwd(), 'pom.xml');
|
|
42
42
|
const composerPath = path.join(process.cwd(), 'composer.json');
|
|
43
|
+
const cargoTomlPath = path.join(process.cwd(), 'Cargo.toml');
|
|
43
44
|
|
|
44
45
|
let detectedLanguage = 'generic';
|
|
45
46
|
let detectedFramework = 'generic';
|
|
@@ -89,12 +90,21 @@ module.exports = class FrameworkDetectionService {
|
|
|
89
90
|
|
|
90
91
|
// Only check other frameworks if i18ntk-runtime wasn't detected
|
|
91
92
|
if (detectedFramework !== 'i18ntk-runtime') {
|
|
92
|
-
if (deps.
|
|
93
|
-
else if (deps.
|
|
93
|
+
if (deps.next || deps['next-intl'] || deps['next-i18next']) detectedFramework = 'nextjs';
|
|
94
|
+
else if (deps.react || deps['react-dom']) detectedFramework = 'react';
|
|
94
95
|
else if (deps['@angular/core']) detectedFramework = 'angular';
|
|
95
|
-
else if (deps.
|
|
96
|
-
else if (deps.nuxt) detectedFramework = 'nuxt';
|
|
96
|
+
else if (deps.vue || deps['vue-router']) detectedFramework = 'vue';
|
|
97
|
+
else if (deps.nuxt || deps['@nuxtjs/i18n']) detectedFramework = 'nuxt';
|
|
97
98
|
else if (deps.svelte) detectedFramework = 'svelte';
|
|
99
|
+
else if (deps.astro || deps['astro-i18next'] || deps['@astrojs/i18n']) detectedFramework = 'astro';
|
|
100
|
+
else if (deps['@builder.io/qwik'] || deps['qwik-speak'] || deps['qwik-i18n']) detectedFramework = 'qwik';
|
|
101
|
+
else if (deps.gatsby || deps['gatsby-plugin-react-i18next'] || deps['gatsby-plugin-intl']) detectedFramework = 'gatsby';
|
|
102
|
+
else if (deps['@remix-run/react'] || deps['remix-i18next'] || deps['i18next-remix']) detectedFramework = 'remix';
|
|
103
|
+
else if (deps['solid-js'] || deps['@solid-primitives/i18n']) detectedFramework = 'solid';
|
|
104
|
+
else if (deps['ember-source'] || deps['ember-intl']) detectedFramework = 'ember';
|
|
105
|
+
else if (deps['react-native'] || deps['react-native-localize']) detectedFramework = 'react-native';
|
|
106
|
+
else if (deps['expo-localization'] || deps.expo) detectedFramework = 'expo';
|
|
107
|
+
else if (deps['@ionic/angular'] || deps['ionic-react'] || deps['@ionic/vue']) detectedFramework = 'ionic';
|
|
98
108
|
else detectedFramework = 'generic';
|
|
99
109
|
}
|
|
100
110
|
} catch (error) {
|
|
@@ -140,7 +150,17 @@ module.exports = class FrameworkDetectionService {
|
|
|
140
150
|
} catch (error) {
|
|
141
151
|
detectedFramework = 'generic';
|
|
142
152
|
}
|
|
143
|
-
}
|
|
153
|
+
} else if (SecurityUtils.safeExistsSync(cargoTomlPath)) {
|
|
154
|
+
detectedLanguage = 'rust';
|
|
155
|
+
try {
|
|
156
|
+
const cargoContent = SecurityUtils.safeReadFileSync(cargoTomlPath, path.dirname(cargoTomlPath), 'utf8');
|
|
157
|
+
if (cargoContent.includes('fluent') || cargoContent.includes('fluent-rs')) detectedFramework = 'fluent';
|
|
158
|
+
else if (cargoContent.includes('gettext')) detectedFramework = 'gettext-rs';
|
|
159
|
+
else detectedFramework = 'generic';
|
|
160
|
+
} catch (error) {
|
|
161
|
+
detectedFramework = 'generic';
|
|
162
|
+
}
|
|
163
|
+
}
|
|
144
164
|
|
|
145
165
|
return { detectedLanguage, detectedFramework };
|
|
146
166
|
}
|
|
@@ -155,13 +175,27 @@ module.exports = class FrameworkDetectionService {
|
|
|
155
175
|
javascript: [
|
|
156
176
|
{ name: 'i18next', description: 'Feature-rich i18n framework for JavaScript' },
|
|
157
177
|
{ name: 'react-i18next', description: 'React integration for i18next' },
|
|
178
|
+
{ name: 'next-intl', description: 'Next.js i18n integration' },
|
|
179
|
+
{ name: 'remix-i18next', description: 'Remix i18n integration' },
|
|
180
|
+
{ name: 'gatsby-plugin-react-i18next', description: 'Gatsby i18n integration' },
|
|
158
181
|
{ name: 'vue-i18n', description: 'Vue.js i18n plugin' },
|
|
159
|
-
{ name: 'Angular i18n', description: 'Built-in Angular i18n' }
|
|
182
|
+
{ name: 'Angular i18n', description: 'Built-in Angular i18n' },
|
|
183
|
+
{ name: 'svelte-i18n', description: 'Svelte i18n library' },
|
|
184
|
+
{ name: 'astro-i18next', description: 'Astro i18n integration' },
|
|
185
|
+
{ name: 'qwik-speak', description: 'Qwik i18n library' },
|
|
186
|
+
{ name: 'solid-i18n', description: 'SolidJS i18n library' },
|
|
187
|
+
{ name: 'ember-intl', description: 'Ember i18n library' },
|
|
188
|
+
{ name: 'react-native-localize', description: 'React Native localization' },
|
|
189
|
+
{ name: 'ionic-angular', description: 'Ionic i18n support' }
|
|
160
190
|
],
|
|
161
191
|
typescript: [
|
|
162
192
|
{ name: 'i18next', description: 'TypeScript-first i18n framework' },
|
|
163
193
|
{ name: 'react-i18next', description: 'React + TypeScript integration' },
|
|
164
|
-
{ name: '
|
|
194
|
+
{ name: 'next-intl', description: 'Next.js + TypeScript i18n' },
|
|
195
|
+
{ name: 'vue-i18n', description: 'Vue.js i18n with TypeScript support' },
|
|
196
|
+
{ name: 'angular i18n', description: 'Angular i18n with TypeScript' },
|
|
197
|
+
{ name: 'astro-i18next', description: 'Astro i18n with TypeScript' },
|
|
198
|
+
{ name: 'qwik-speak', description: 'Qwik i18n with TypeScript' }
|
|
165
199
|
],
|
|
166
200
|
python: [
|
|
167
201
|
{ name: 'Django i18n', description: 'Built-in Django internationalization' },
|
|
@@ -177,6 +211,11 @@ module.exports = class FrameworkDetectionService {
|
|
|
177
211
|
{ name: 'go-i18n', description: 'Go i18n library with pluralization' },
|
|
178
212
|
{ name: 'nicksnyder/go-i18n', description: 'Feature-rich Go i18n' }
|
|
179
213
|
],
|
|
214
|
+
rust: [
|
|
215
|
+
{ name: 'fluent', description: 'Project Fluent localization for Rust' },
|
|
216
|
+
{ name: 'fluent-rs', description: 'Rust implementation of Project Fluent' },
|
|
217
|
+
{ name: 'gettext-rs', description: 'GNU gettext bindings for Rust' }
|
|
218
|
+
],
|
|
180
219
|
php: [
|
|
181
220
|
{ name: 'Laravel i18n', description: 'Built-in Laravel localization' },
|
|
182
221
|
{ name: 'Symfony Translation', description: 'Symfony translation component' },
|
|
@@ -335,13 +374,32 @@ module.exports = class FrameworkDetectionService {
|
|
|
335
374
|
|
|
336
375
|
const i18nFrameworks = [
|
|
337
376
|
'react-i18next',
|
|
377
|
+
'next-intl',
|
|
378
|
+
'next-i18next',
|
|
379
|
+
'remix-i18next',
|
|
380
|
+
'i18next-remix',
|
|
381
|
+
'gatsby-plugin-react-i18next',
|
|
382
|
+
'gatsby-plugin-intl',
|
|
338
383
|
'vue-i18n',
|
|
339
384
|
'angular-i18n',
|
|
340
385
|
'i18next',
|
|
341
|
-
'next-i18next',
|
|
342
386
|
'svelte-i18n',
|
|
387
|
+
'sveltekit-i18n',
|
|
388
|
+
'astro-i18next',
|
|
389
|
+
'@astrojs/i18n',
|
|
390
|
+
'qwik-speak',
|
|
391
|
+
'qwik-i18n',
|
|
392
|
+
'@solid-primitives/i18n',
|
|
393
|
+
'ember-intl',
|
|
394
|
+
'react-native-localize',
|
|
395
|
+
'expo-localization',
|
|
396
|
+
'@ngx-translate/core',
|
|
397
|
+
'@ionic/angular',
|
|
343
398
|
'@nuxtjs/i18n',
|
|
344
|
-
'
|
|
399
|
+
'formatjs',
|
|
400
|
+
'@lingui/core',
|
|
401
|
+
'i18ntk-runtime',
|
|
402
|
+
'i18ntk/runtime'
|
|
345
403
|
];
|
|
346
404
|
|
|
347
405
|
const installedFrameworks = i18nFrameworks.filter(framework => dependencies[framework]);
|
|
@@ -18,29 +18,56 @@ const AdminAuth = require('../../../utils/admin-auth');
|
|
|
18
18
|
const { parseConfirmation } = require('../../../utils/localized-confirm');
|
|
19
19
|
const { normalizeReportFormat, writeReportFile } = require('../../../utils/report-writer');
|
|
20
20
|
|
|
21
|
-
// Language configurations with native names
|
|
22
|
-
const LANGUAGE_CONFIG = {
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
'
|
|
43
|
-
}
|
|
21
|
+
// Language configurations with native names — comprehensive ISO 639-1 coverage
|
|
22
|
+
const LANGUAGE_CONFIG = {
|
|
23
|
+
'af': { name: 'Afrikaans', nativeName: 'Afrikaans' }, 'sq': { name: 'Albanian', nativeName: 'Shqip' },
|
|
24
|
+
'ar': { name: 'Arabic', nativeName: '\u0627\u0644\u0639\u0631\u0628\u064a\u0629' }, 'hy': { name: 'Armenian', nativeName: '\u0540\u0561\u0575\u0565\u0580\u0565\u0576' },
|
|
25
|
+
'az': { name: 'Azerbaijani', nativeName: 'Az\u0259rbaycan' }, 'eu': { name: 'Basque', nativeName: 'Euskara' },
|
|
26
|
+
'be': { name: 'Belarusian', nativeName: '\u0411\u0435\u043b\u0430\u0440\u0443\u0441\u043a\u0430\u044f' },
|
|
27
|
+
'bn': { name: 'Bengali', nativeName: '\u09ac\u09be\u0982\u09b2\u09be' },
|
|
28
|
+
'bs': { name: 'Bosnian', nativeName: 'Bosanski' }, 'bg': { name: 'Bulgarian', nativeName: '\u0411\u044a\u043b\u0433\u0430\u0440\u0441\u043a\u0438' },
|
|
29
|
+
'ca': { name: 'Catalan', nativeName: 'Catal\u00e0' }, 'zh': { name: 'Chinese', nativeName: '\u4e2d\u6587' },
|
|
30
|
+
'hr': { name: 'Croatian', nativeName: 'Hrvatski' }, 'cs': { name: 'Czech', nativeName: '\u010ce\u0161tina' },
|
|
31
|
+
'da': { name: 'Danish', nativeName: 'Dansk' }, 'nl': { name: 'Dutch', nativeName: 'Nederlands' },
|
|
32
|
+
'en': { name: 'English', nativeName: 'English' }, 'et': { name: 'Estonian', nativeName: 'Eesti' },
|
|
33
|
+
'fi': { name: 'Finnish', nativeName: 'Suomi' }, 'fr': { name: 'French', nativeName: 'Fran\u00e7ais' },
|
|
34
|
+
'ka': { name: 'Georgian', nativeName: '\u10e5\u10d0\u10e0\u10d7\u10e3\u10da\u10d8' },
|
|
35
|
+
'de': { name: 'German', nativeName: 'Deutsch' }, 'el': { name: 'Greek', nativeName: '\u0395\u03bb\u03bb\u03b7\u03bd\u03b9\u03ba\u03ac' },
|
|
36
|
+
'gu': { name: 'Gujarati', nativeName: '\u0a97\u0ac1\u0a9c\u0ab0\u0abe\u0aa4\u0ac0' },
|
|
37
|
+
'he': { name: 'Hebrew', nativeName: '\u05e2\u05d1\u05e8\u05d9\u05ea' },
|
|
38
|
+
'hi': { name: 'Hindi', nativeName: '\u0939\u093f\u0928\u094d\u0926\u0940' },
|
|
39
|
+
'hu': { name: 'Hungarian', nativeName: 'Magyar' }, 'is': { name: 'Icelandic', nativeName: '\u00cdslenska' },
|
|
40
|
+
'id': { name: 'Indonesian', nativeName: 'Bahasa Indonesia' },
|
|
41
|
+
'ga': { name: 'Irish', nativeName: 'Gaeilge' }, 'it': { name: 'Italian', nativeName: 'Italiano' },
|
|
42
|
+
'ja': { name: 'Japanese', nativeName: '\u65e5\u672c\u8a9e' },
|
|
43
|
+
'kk': { name: 'Kazakh', nativeName: '\u049a\u0430\u0437\u0430\u049b' },
|
|
44
|
+
'ko': { name: 'Korean', nativeName: '\ud55c\uad6d\uc5b4' },
|
|
45
|
+
'lv': { name: 'Latvian', nativeName: 'Latvie\u0161u' }, 'lt': { name: 'Lithuanian', nativeName: 'Lietuvi\u0173' },
|
|
46
|
+
'mk': { name: 'Macedonian', nativeName: '\u041c\u0430\u043a\u0435\u0434\u043e\u043d\u0441\u043a\u0438' },
|
|
47
|
+
'ms': { name: 'Malay', nativeName: 'Bahasa Melayu' },
|
|
48
|
+
'ml': { name: 'Malayalam', nativeName: '\u0d2e\u0d32\u0d2f\u0d3e\u0d33\u0d02' },
|
|
49
|
+
'mr': { name: 'Marathi', nativeName: '\u092e\u0930\u093e\u0920\u0940' },
|
|
50
|
+
'mn': { name: 'Mongolian', nativeName: '\u041c\u043e\u043d\u0433\u043e\u043b' },
|
|
51
|
+
'no': { name: 'Norwegian', nativeName: 'Norsk' },
|
|
52
|
+
'fa': { name: 'Persian', nativeName: '\u0641\u0627\u0631\u0633\u06cc' },
|
|
53
|
+
'pl': { name: 'Polish', nativeName: 'Polski' }, 'pt': { name: 'Portuguese', nativeName: 'Portugu\u00eas' },
|
|
54
|
+
'pa': { name: 'Punjabi', nativeName: '\u0a2a\u0a70\u0a1c\u0a3e\u0a2c\u0a40' },
|
|
55
|
+
'ro': { name: 'Romanian', nativeName: 'Rom\u00e2n\u0103' },
|
|
56
|
+
'ru': { name: 'Russian', nativeName: '\u0420\u0443\u0441\u0441\u043a\u0438\u0439' },
|
|
57
|
+
'sr': { name: 'Serbian', nativeName: '\u0421\u0440\u043f\u0441\u043a\u0438' },
|
|
58
|
+
'sk': { name: 'Slovak', nativeName: 'Sloven\u010dina' },
|
|
59
|
+
'sl': { name: 'Slovenian', nativeName: 'Sloven\u0161\u010dina' },
|
|
60
|
+
'es': { name: 'Spanish', nativeName: 'Espa\u00f1ol' },
|
|
61
|
+
'sw': { name: 'Swahili', nativeName: 'Kiswahili' }, 'sv': { name: 'Swedish', nativeName: 'Svenska' },
|
|
62
|
+
'tl': { name: 'Filipino', nativeName: 'Filipino' },
|
|
63
|
+
'ta': { name: 'Tamil', nativeName: '\u0ba4\u0bae\u0bbf\u0bb4\u0bcd' },
|
|
64
|
+
'te': { name: 'Telugu', nativeName: '\u0c24\u0c46\u0c32\u0c41\u0c17\u0c41' },
|
|
65
|
+
'th': { name: 'Thai', nativeName: '\u0e44\u0e17\u0e22' },
|
|
66
|
+
'tr': { name: 'Turkish', nativeName: 'T\u00fcrk\u00e7e' },
|
|
67
|
+
'uk': { name: 'Ukrainian', nativeName: '\u0423\u043a\u0440\u0430\u0457\u043d\u0441\u044c\u043a\u0430' },
|
|
68
|
+
'ur': { name: 'Urdu', nativeName: '\u0627\u0631\u062f\u0648' },
|
|
69
|
+
'vi': { name: 'Vietnamese', nativeName: 'Ti\u1ebfng Vi\u1ec7t' },
|
|
70
|
+
};
|
|
44
71
|
|
|
45
72
|
class InitService {
|
|
46
73
|
constructor(config = {}) {
|
|
@@ -56,7 +83,9 @@ class InitService {
|
|
|
56
83
|
};
|
|
57
84
|
|
|
58
85
|
this.format = getFormatAdapter(this.config.format);
|
|
59
|
-
this.config.supportedExtensions
|
|
86
|
+
if (!this.config.supportedExtensions || !this.config.supportedExtensions.length) {
|
|
87
|
+
this.config.supportedExtensions = [this.format.extension];
|
|
88
|
+
}
|
|
60
89
|
this.detectedFramework = detectFramework(process.cwd());
|
|
61
90
|
if (this.detectedFramework && !this.config.translationPatterns) {
|
|
62
91
|
this.config.translationPatterns = this.detectedFramework.patterns;
|
|
@@ -68,7 +97,9 @@ class InitService {
|
|
|
68
97
|
: path.join(this.sourceDir, this.config.sourceLanguage);
|
|
69
98
|
|
|
70
99
|
// Ensure defaultLanguages is properly initialized from config
|
|
71
|
-
this.config.defaultLanguages
|
|
100
|
+
if (!this.config.defaultLanguages || !Array.isArray(this.config.defaultLanguages) || this.config.defaultLanguages.length === 0) {
|
|
101
|
+
this.config.defaultLanguages = ['en', 'de', 'es', 'fr', 'ru'];
|
|
102
|
+
}
|
|
72
103
|
}
|
|
73
104
|
|
|
74
105
|
// Check i18n dependencies
|