transcripto-cli 1.0.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.
- package/README.md +576 -0
- package/dist/cli/generate.d.ts +2 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +416 -0
- package/dist/cli/generate.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +43 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +2 -0
- package/dist/cli/init.d.ts.map +1 -0
- package/dist/cli/init.js +81 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/report.d.ts +2 -0
- package/dist/cli/report.d.ts.map +1 -0
- package/dist/cli/report.js +137 -0
- package/dist/cli/report.js.map +1 -0
- package/dist/cli/scan.d.ts +2 -0
- package/dist/cli/scan.d.ts.map +1 -0
- package/dist/cli/scan.js +62 -0
- package/dist/cli/scan.js.map +1 -0
- package/dist/cli/watch-i18n.d.ts +2 -0
- package/dist/cli/watch-i18n.d.ts.map +1 -0
- package/dist/cli/watch-i18n.js +73 -0
- package/dist/cli/watch-i18n.js.map +1 -0
- package/dist/cli/watch.d.ts +2 -0
- package/dist/cli/watch.d.ts.map +1 -0
- package/dist/cli/watch.js +147 -0
- package/dist/cli/watch.js.map +1 -0
- package/dist/core/i18nGenerator.d.ts +16 -0
- package/dist/core/i18nGenerator.d.ts.map +1 -0
- package/dist/core/i18nGenerator.js +139 -0
- package/dist/core/i18nGenerator.js.map +1 -0
- package/dist/core/projectScanner.d.ts +12 -0
- package/dist/core/projectScanner.d.ts.map +1 -0
- package/dist/core/projectScanner.js +53 -0
- package/dist/core/projectScanner.js.map +1 -0
- package/dist/core/stringExtractor.d.ts +21 -0
- package/dist/core/stringExtractor.d.ts.map +1 -0
- package/dist/core/stringExtractor.js +268 -0
- package/dist/core/stringExtractor.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
- package/src/cli/generate.ts +422 -0
- package/src/cli/index.ts +50 -0
- package/src/cli/init.ts +96 -0
- package/src/cli/report.ts +160 -0
- package/src/cli/scan.ts +69 -0
- package/src/cli/watch-i18n.ts +77 -0
- package/src/cli/watch.ts +129 -0
- package/src/core/i18nGenerator.ts +127 -0
- package/src/core/projectScanner.ts +62 -0
- package/src/core/stringExtractor.ts +276 -0
- package/src/index.ts +7 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { ExtractedString } from '../core/stringExtractor';
|
|
4
|
+
|
|
5
|
+
interface ReportData {
|
|
6
|
+
totalStrings: number;
|
|
7
|
+
filesScanned: number;
|
|
8
|
+
languages: string[];
|
|
9
|
+
coverage: number;
|
|
10
|
+
missingTranslations: string[];
|
|
11
|
+
stringsByFile: Record<string, number>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function reportCommand(): Promise<void> {
|
|
15
|
+
console.log(chalk.blue('📊 Generating localization report...'));
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const report = await generateReport();
|
|
19
|
+
displayReport(report);
|
|
20
|
+
|
|
21
|
+
} catch (error) {
|
|
22
|
+
console.error(chalk.red('❌ Report generation failed:'), error);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function generateReport(): Promise<ReportData> {
|
|
28
|
+
// Load extracted strings
|
|
29
|
+
const strings = await loadExtractedStrings();
|
|
30
|
+
|
|
31
|
+
// Load config
|
|
32
|
+
const config = await loadConfig();
|
|
33
|
+
|
|
34
|
+
// Load existing translations
|
|
35
|
+
const translations = await loadTranslations(config.i18n.languages);
|
|
36
|
+
|
|
37
|
+
// Calculate metrics
|
|
38
|
+
const filesScanned = new Set(strings.map(s => s.filePath)).size;
|
|
39
|
+
const stringsByFile: Record<string, number> = {};
|
|
40
|
+
|
|
41
|
+
strings.forEach(str => {
|
|
42
|
+
const fileName = str.filePath.split('/').pop() || str.filePath;
|
|
43
|
+
stringsByFile[fileName] = (stringsByFile[fileName] || 0) + 1;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Check missing translations
|
|
47
|
+
const missingTranslations: string[] = [];
|
|
48
|
+
for (const lang of config.i18n.languages) {
|
|
49
|
+
if (lang !== 'en' && (!translations[lang] || Object.keys(translations[lang]).length < strings.length)) {
|
|
50
|
+
missingTranslations.push(lang);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const coverage = strings.length > 0
|
|
55
|
+
? Math.round((translations.en ? Object.keys(translations.en).length : 0) / strings.length * 100)
|
|
56
|
+
: 0;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
totalStrings: strings.length,
|
|
60
|
+
filesScanned,
|
|
61
|
+
languages: config.i18n.languages,
|
|
62
|
+
coverage,
|
|
63
|
+
missingTranslations,
|
|
64
|
+
stringsByFile
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function displayReport(report: ReportData): void {
|
|
69
|
+
console.log(chalk.green('\n📈 Transcripto Localization Report'));
|
|
70
|
+
console.log(chalk.gray('='.repeat(40)));
|
|
71
|
+
|
|
72
|
+
console.log(chalk.white(`\n📝 Total Strings: ${report.totalStrings}`));
|
|
73
|
+
console.log(chalk.white(`📁 Files Scanned: ${report.filesScanned}`));
|
|
74
|
+
console.log(chalk.white(`🌐 Languages: ${report.languages.join(', ')}`));
|
|
75
|
+
|
|
76
|
+
// Coverage with color coding
|
|
77
|
+
let coverageColor = chalk.red;
|
|
78
|
+
if (report.coverage >= 80) coverageColor = chalk.green;
|
|
79
|
+
else if (report.coverage >= 50) coverageColor = chalk.yellow;
|
|
80
|
+
|
|
81
|
+
console.log(coverageColor(`📊 Coverage: ${report.coverage}%`));
|
|
82
|
+
|
|
83
|
+
// Missing translations
|
|
84
|
+
if (report.missingTranslations.length > 0) {
|
|
85
|
+
console.log(chalk.red(`\n⚠️ Missing Translations: ${report.missingTranslations.join(', ')}`));
|
|
86
|
+
} else {
|
|
87
|
+
console.log(chalk.green('\n✅ All languages have complete translations'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Strings by file
|
|
91
|
+
if (Object.keys(report.stringsByFile).length > 0) {
|
|
92
|
+
console.log(chalk.white('\n📋 Strings by File:'));
|
|
93
|
+
const sortedFiles = Object.entries(report.stringsByFile)
|
|
94
|
+
.sort(([,a], [,b]) => b - a)
|
|
95
|
+
.slice(0, 10);
|
|
96
|
+
|
|
97
|
+
sortedFiles.forEach(([file, count]) => {
|
|
98
|
+
console.log(chalk.gray(` ${file}: ${count} strings`));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (Object.keys(report.stringsByFile).length > 10) {
|
|
102
|
+
console.log(chalk.gray(` ... and ${Object.keys(report.stringsByFile).length - 10} more files`));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Recommendations
|
|
107
|
+
console.log(chalk.white('\n💡 Recommendations:'));
|
|
108
|
+
|
|
109
|
+
if (report.totalStrings === 0) {
|
|
110
|
+
console.log(chalk.yellow(' • Run "transcripto scan" to extract UI strings'));
|
|
111
|
+
} else if (report.coverage < 100) {
|
|
112
|
+
console.log(chalk.yellow(' • Run "transcripto generate" to update translation files'));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (report.missingTranslations.length > 0) {
|
|
116
|
+
console.log(chalk.yellow(' • Run "npx lingo.dev@latest run" to generate missing translations'));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (report.totalStrings > 0 && report.coverage === 100) {
|
|
120
|
+
console.log(chalk.green(' • Localization is complete! 🎉'));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function loadExtractedStrings(): Promise<ExtractedString[]> {
|
|
125
|
+
try {
|
|
126
|
+
const content = await fs.readFile('.transcripto/extracted-strings.json', 'utf-8');
|
|
127
|
+
return JSON.parse(content);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function loadConfig() {
|
|
134
|
+
try {
|
|
135
|
+
const configContent = await fs.readFile('.transcripto/config.json', 'utf-8');
|
|
136
|
+
return JSON.parse(configContent);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
i18n: {
|
|
140
|
+
languages: ['en', 'hi', 'kn'],
|
|
141
|
+
outputDir: './src/i18n'
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function loadTranslations(languages: string[]): Promise<Record<string, Record<string, string>>> {
|
|
148
|
+
const translations: Record<string, Record<string, string>> = {};
|
|
149
|
+
|
|
150
|
+
for (const lang of languages) {
|
|
151
|
+
try {
|
|
152
|
+
const content = await fs.readFile(`./src/i18n/${lang}.json`, 'utf-8');
|
|
153
|
+
translations[lang] = JSON.parse(content);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
translations[lang] = {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return translations;
|
|
160
|
+
}
|
package/src/cli/scan.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { ProjectScanner } from '../core/projectScanner';
|
|
4
|
+
import { StringExtractor } from '../core/stringExtractor';
|
|
5
|
+
|
|
6
|
+
export async function scanCommand(): Promise<void> {
|
|
7
|
+
console.log(chalk.blue('🔍 Scanning project for UI text strings...'));
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
// Load config
|
|
11
|
+
const config = await loadConfig();
|
|
12
|
+
|
|
13
|
+
// Initialize scanner and extractor
|
|
14
|
+
const scanner = new ProjectScanner();
|
|
15
|
+
const extractor = new StringExtractor();
|
|
16
|
+
|
|
17
|
+
// Scan project
|
|
18
|
+
const files = await scanner.scanProject();
|
|
19
|
+
console.log(chalk.gray(`📁 Found ${files.length} files to scan`));
|
|
20
|
+
|
|
21
|
+
// Extract strings
|
|
22
|
+
const strings = await extractor.extractStrings(files);
|
|
23
|
+
|
|
24
|
+
// Save results
|
|
25
|
+
await fs.writeFile(
|
|
26
|
+
'.transcripto/extracted-strings.json',
|
|
27
|
+
JSON.stringify(strings, null, 2),
|
|
28
|
+
'utf-8'
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
console.log(chalk.green(`✅ Extracted ${strings.length} UI text strings`));
|
|
32
|
+
|
|
33
|
+
if (strings.length > 0) {
|
|
34
|
+
console.log(chalk.yellow('\n📝 Sample extracted strings:'));
|
|
35
|
+
strings.slice(0, 5).forEach((str, index) => {
|
|
36
|
+
console.log(chalk.white(` ${index + 1}. "${str.text}" → ${str.key}`));
|
|
37
|
+
console.log(chalk.gray(` from ${str.filePath}:${str.line}`));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (strings.length > 5) {
|
|
41
|
+
console.log(chalk.gray(` ... and ${strings.length - 5} more`));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log(chalk.yellow('\n🎯 Next steps:'));
|
|
46
|
+
console.log(chalk.white(' transcripto generate - Generate i18n files from extracted strings'));
|
|
47
|
+
console.log(chalk.white(' transcripto replace - Replace inline text with constants'));
|
|
48
|
+
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error(chalk.red('❌ Scan failed:'), error);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function loadConfig() {
|
|
56
|
+
try {
|
|
57
|
+
const configContent = await fs.readFile('.transcripto/config.json', 'utf-8');
|
|
58
|
+
return JSON.parse(configContent);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.warn(chalk.yellow('⚠️ No .transcripto/config.json found, using defaults'));
|
|
61
|
+
return {
|
|
62
|
+
scan: {
|
|
63
|
+
extensions: ['.ts', '.tsx', '.js', '.jsx', '.html'],
|
|
64
|
+
exclude: ['node_modules', 'dist', 'build', '.git', 'coverage'],
|
|
65
|
+
minStringLength: 2
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import chokidar from 'chokidar';
|
|
4
|
+
import { ProjectScanner } from '../core/projectScanner';
|
|
5
|
+
import { StringExtractor } from '../core/stringExtractor';
|
|
6
|
+
import { I18nGenerator } from '../core/i18nGenerator';
|
|
7
|
+
|
|
8
|
+
export async function watchI18nCommand(): Promise<void> {
|
|
9
|
+
console.log(chalk.cyan('👁️ Starting i18n file watcher...'));
|
|
10
|
+
console.log(chalk.gray('📁 Monitoring: ./i18n/en.json'));
|
|
11
|
+
console.log(chalk.gray('🔄 Auto-translating on changes...\n'));
|
|
12
|
+
|
|
13
|
+
// Ensure i18n directory exists
|
|
14
|
+
try {
|
|
15
|
+
await fs.mkdir('./i18n', { recursive: true });
|
|
16
|
+
} catch {
|
|
17
|
+
// Directory already exists
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Initial translation if en.json exists
|
|
21
|
+
try {
|
|
22
|
+
const enContent = await fs.readFile('./i18n/en.json', 'utf-8');
|
|
23
|
+
const translations = JSON.parse(enContent);
|
|
24
|
+
const keys = Object.keys(translations);
|
|
25
|
+
|
|
26
|
+
if (keys.length > 0) {
|
|
27
|
+
console.log(chalk.yellow('📝 Found existing translations, running initial translation...'));
|
|
28
|
+
await runTranslation();
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
console.log(chalk.yellow('📝 No existing en.json found, waiting for file creation...'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Watch for changes in en.json
|
|
35
|
+
const watcher = chokidar.watch('./i18n/en.json', {
|
|
36
|
+
persistent: true,
|
|
37
|
+
ignoreInitial: true
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
watcher.on('change', async () => {
|
|
41
|
+
console.log(chalk.cyan('\n🔄 File changed: ./i18n/en.json'));
|
|
42
|
+
console.log(chalk.yellow('🌐 Running auto-translation...'));
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
await runTranslation();
|
|
46
|
+
console.log(chalk.green('✅ Auto-translation completed!'));
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(chalk.red('❌ Auto-translation failed:'), error);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
watcher.on('add', async () => {
|
|
53
|
+
console.log(chalk.cyan('\n📁 File created: ./i18n/en.json'));
|
|
54
|
+
console.log(chalk.yellow('🌐 Running initial translation...'));
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
await runTranslation();
|
|
58
|
+
console.log(chalk.green('✅ Initial translation completed!'));
|
|
59
|
+
} catch (error) {
|
|
60
|
+
console.error(chalk.red('❌ Initial translation failed:'), error);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
console.log(chalk.green('🎉 Watcher started successfully!'));
|
|
65
|
+
console.log(chalk.gray('💡 Add new keys to ./i18n/en.json and they will be auto-translated'));
|
|
66
|
+
console.log(chalk.gray('⏹️ Press Ctrl+C to stop watching\n'));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function runTranslation() {
|
|
70
|
+
const generator = new I18nGenerator();
|
|
71
|
+
|
|
72
|
+
// Generate lingo.dev config for root/i18n
|
|
73
|
+
await generator.generateLingoDevConfig();
|
|
74
|
+
|
|
75
|
+
// Run lingo.dev translation
|
|
76
|
+
await generator.runLingoDev();
|
|
77
|
+
}
|
package/src/cli/watch.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import * as chokidar from 'chokidar';
|
|
5
|
+
import { I18nGenerator } from '../core/i18nGenerator';
|
|
6
|
+
import { ProjectScanner } from '../core/projectScanner';
|
|
7
|
+
import { StringExtractor } from '../core/stringExtractor';
|
|
8
|
+
import { ExtractedString } from '../core/stringExtractor';
|
|
9
|
+
|
|
10
|
+
export async function watchCommand(options: any): Promise<void> {
|
|
11
|
+
console.log(chalk.blue('👁️ Starting Transcripto file watcher...'));
|
|
12
|
+
|
|
13
|
+
const yes = options.yes || false;
|
|
14
|
+
|
|
15
|
+
if (yes) {
|
|
16
|
+
console.log(chalk.yellow('🤖 Running in non-interactive mode (--yes)'));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Initial setup
|
|
20
|
+
await setupProject(yes);
|
|
21
|
+
|
|
22
|
+
// Watch for file changes
|
|
23
|
+
const watcher = chokidar.watch([
|
|
24
|
+
'src/**/*.{ts,tsx,js,jsx}',
|
|
25
|
+
'!src/i18n/**/*',
|
|
26
|
+
'!dist/**/*',
|
|
27
|
+
'!node_modules/**/*'
|
|
28
|
+
], {
|
|
29
|
+
ignored: /node_modules/,
|
|
30
|
+
persistent: true,
|
|
31
|
+
ignoreInitial: true
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
console.log(chalk.green('📁 Watching for changes in src/ directory...'));
|
|
35
|
+
console.log(chalk.gray('💡 Press Ctrl+C to stop watching'));
|
|
36
|
+
|
|
37
|
+
watcher.on('change', async (filePath: string) => {
|
|
38
|
+
console.log(chalk.yellow(`\n📝 File changed: ${filePath}`));
|
|
39
|
+
await handleFileChange(filePath, yes);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
watcher.on('add', async (filePath: string) => {
|
|
43
|
+
console.log(chalk.yellow(`\n➕ File added: ${filePath}`));
|
|
44
|
+
await handleFileChange(filePath, yes);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Handle process termination
|
|
48
|
+
process.on('SIGINT', () => {
|
|
49
|
+
console.log(chalk.cyan('\n👋 Stopping Transcripto watcher...'));
|
|
50
|
+
watcher.close();
|
|
51
|
+
process.exit(0);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function setupProject(yes: boolean): Promise<void> {
|
|
56
|
+
try {
|
|
57
|
+
// Check if project is initialized
|
|
58
|
+
try {
|
|
59
|
+
await fs.access('.transcripto');
|
|
60
|
+
console.log(chalk.green('✅ Transcripto project already initialized'));
|
|
61
|
+
} catch {
|
|
62
|
+
console.log(chalk.yellow('🔧 Initializing Transcripto project...'));
|
|
63
|
+
const { initCommand } = await import('./init');
|
|
64
|
+
await initCommand();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Initial scan and generation
|
|
68
|
+
console.log(chalk.blue('🔍 Performing initial scan...'));
|
|
69
|
+
await performFullWorkflow(yes);
|
|
70
|
+
|
|
71
|
+
} catch (error) {
|
|
72
|
+
console.error(chalk.red('❌ Setup failed:'), error);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function handleFileChange(filePath: string, yes: boolean): Promise<void> {
|
|
78
|
+
try {
|
|
79
|
+
console.log(chalk.cyan('🔄 Updating translations...'));
|
|
80
|
+
|
|
81
|
+
// Perform full workflow
|
|
82
|
+
await performFullWorkflow(yes);
|
|
83
|
+
|
|
84
|
+
console.log(chalk.green('✅ Translations updated successfully!'));
|
|
85
|
+
|
|
86
|
+
} catch (error) {
|
|
87
|
+
console.error(chalk.red('❌ Update failed:'), error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function performFullWorkflow(yes: boolean): Promise<void> {
|
|
92
|
+
// Scan for strings
|
|
93
|
+
const scanner = new ProjectScanner();
|
|
94
|
+
const files = await scanner.scanProject('./src');
|
|
95
|
+
|
|
96
|
+
const extractor = new StringExtractor();
|
|
97
|
+
const strings = await extractor.extractStrings(files);
|
|
98
|
+
|
|
99
|
+
if (strings.length === 0) {
|
|
100
|
+
console.log(chalk.yellow('⚠️ No strings found'));
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Generate i18n files using same config as generate command
|
|
105
|
+
const generator = new I18nGenerator();
|
|
106
|
+
const config = {
|
|
107
|
+
outputDir: './i18n', // Changed to root folder
|
|
108
|
+
languages: ['en'], // Start with just English, lingo.dev will add more
|
|
109
|
+
constantsFile: '', // No constants file needed
|
|
110
|
+
keyPrefix: ''
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
await generator.generateI18nFiles(strings, config);
|
|
114
|
+
await generator.generateLingoDevConfig(); // Let lingo.dev decide target languages
|
|
115
|
+
|
|
116
|
+
// Run lingo.dev automatically
|
|
117
|
+
try {
|
|
118
|
+
await generator.runLingoDev();
|
|
119
|
+
console.log(chalk.green('🌐 lingo.dev translations completed!'));
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.log(chalk.yellow('⚠️ lingo.dev failed, continuing...'));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Replace strings with constants
|
|
125
|
+
// const replacer = new TextReplacer();
|
|
126
|
+
// await replacer.replaceInFiles(strings);
|
|
127
|
+
|
|
128
|
+
console.log(chalk.green(`✅ Processed ${strings.length} strings`));
|
|
129
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { ExtractedString } from './stringExtractor';
|
|
4
|
+
|
|
5
|
+
export interface I18nConfig {
|
|
6
|
+
outputDir: string;
|
|
7
|
+
languages: string[];
|
|
8
|
+
constantsFile: string;
|
|
9
|
+
keyPrefix: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class I18nGenerator {
|
|
13
|
+
private readonly defaultConfig: I18nConfig = {
|
|
14
|
+
outputDir: './i18n', // Changed to root folder
|
|
15
|
+
languages: ['en', 'hi', 'kn'],
|
|
16
|
+
constantsFile: './src/i18n/constants.ts',
|
|
17
|
+
keyPrefix: ''
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
async generateI18nFiles(
|
|
21
|
+
strings: ExtractedString[],
|
|
22
|
+
config: Partial<I18nConfig> = {}
|
|
23
|
+
): Promise<void> {
|
|
24
|
+
const finalConfig = { ...this.defaultConfig, ...config };
|
|
25
|
+
|
|
26
|
+
// Create output directory
|
|
27
|
+
await fs.mkdir(finalConfig.outputDir, { recursive: true });
|
|
28
|
+
|
|
29
|
+
// Generate translation files only (no constants file)
|
|
30
|
+
for (const language of finalConfig.languages) {
|
|
31
|
+
await this.generateTranslationFile(strings, language, finalConfig);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async generateTranslationFile(
|
|
36
|
+
strings: ExtractedString[],
|
|
37
|
+
language: string,
|
|
38
|
+
config: I18nConfig
|
|
39
|
+
): Promise<void> {
|
|
40
|
+
const translations: Record<string, string> = {};
|
|
41
|
+
|
|
42
|
+
for (const str of strings) {
|
|
43
|
+
translations[str.key] = str.text;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const filePath = path.join(config.outputDir, `${language}.json`);
|
|
47
|
+
const content = JSON.stringify(translations, null, 2);
|
|
48
|
+
|
|
49
|
+
await fs.writeFile(filePath, content, 'utf-8');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async generateLingoDevConfig(): Promise<void> {
|
|
53
|
+
// First, delete any existing lingo.dev config to ensure clean setup
|
|
54
|
+
try {
|
|
55
|
+
await fs.unlink('.lingodev.json');
|
|
56
|
+
console.log('🗑️ Removed existing lingo.dev config');
|
|
57
|
+
} catch {
|
|
58
|
+
// Config doesn't exist, that's fine
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Let lingo.dev handle target languages automatically - don't specify any
|
|
62
|
+
const config = {
|
|
63
|
+
source: './i18n/en.json', // Changed to root folder
|
|
64
|
+
// target: [] - Let lingo.dev decide automatically
|
|
65
|
+
output: './i18n', // Changed to root folder
|
|
66
|
+
format: 'json'
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const content = JSON.stringify(config, null, 2);
|
|
70
|
+
await fs.writeFile('.lingodev.json', content, 'utf-8');
|
|
71
|
+
console.log('📁 Created lingo.dev config with automatic language selection');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async runLingoDev(): Promise<void> {
|
|
75
|
+
const { spawn } = await import('child_process');
|
|
76
|
+
|
|
77
|
+
// First, initialize lingo.dev if not already done
|
|
78
|
+
try {
|
|
79
|
+
await fs.access('i18n.json');
|
|
80
|
+
console.log('📁 lingo.dev already initialized');
|
|
81
|
+
} catch {
|
|
82
|
+
console.log('🔧 Initializing lingo.dev...');
|
|
83
|
+
await this.initializeLingoDev();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Then run lingo.dev
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
const process = spawn('npx', ['lingo.dev@latest', 'run'], {
|
|
89
|
+
stdio: 'inherit',
|
|
90
|
+
shell: true
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
process.on('close', (code) => {
|
|
94
|
+
if (code === 0) {
|
|
95
|
+
console.log('✅ lingo.dev translations completed successfully!');
|
|
96
|
+
resolve();
|
|
97
|
+
} else {
|
|
98
|
+
reject(new Error(`lingo.dev process exited with code ${code}`));
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
process.on('error', reject);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private async initializeLingoDev(): Promise<void> {
|
|
107
|
+
const { spawn } = await import('child_process');
|
|
108
|
+
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
const process = spawn('npx', ['lingo.dev@latest', 'init'], {
|
|
111
|
+
stdio: 'inherit',
|
|
112
|
+
shell: true
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
process.on('close', (code) => {
|
|
116
|
+
if (code === 0) {
|
|
117
|
+
console.log('✅ lingo.dev initialized successfully');
|
|
118
|
+
resolve();
|
|
119
|
+
} else {
|
|
120
|
+
reject(new Error(`lingo.dev init failed with code ${code}`));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
process.on('error', reject);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { glob } from 'glob';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export interface FileInfo {
|
|
6
|
+
path: string;
|
|
7
|
+
content: string;
|
|
8
|
+
size: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class ProjectScanner {
|
|
12
|
+
private readonly extensions = ['.ts', '.tsx', '.js', '.jsx', '.html'];
|
|
13
|
+
private readonly excludePatterns = [
|
|
14
|
+
'**/node_modules/**',
|
|
15
|
+
'**/dist/**',
|
|
16
|
+
'**/build/**',
|
|
17
|
+
'**/.git/**',
|
|
18
|
+
'**/coverage/**'
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
async scanProject(rootPath: string = process.cwd()): Promise<FileInfo[]> {
|
|
22
|
+
const pattern = `**/*.{${this.extensions.map(ext => ext.slice(1)).join(',')}}`;
|
|
23
|
+
|
|
24
|
+
const files = await glob(pattern, {
|
|
25
|
+
cwd: rootPath,
|
|
26
|
+
ignore: this.excludePatterns,
|
|
27
|
+
absolute: true
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const fileInfos: FileInfo[] = [];
|
|
31
|
+
|
|
32
|
+
for (const filePath of files) {
|
|
33
|
+
try {
|
|
34
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
35
|
+
const stats = await fs.stat(filePath);
|
|
36
|
+
|
|
37
|
+
fileInfos.push({
|
|
38
|
+
path: filePath,
|
|
39
|
+
content,
|
|
40
|
+
size: stats.size
|
|
41
|
+
});
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.warn(`Warning: Could not read file ${filePath}:`, error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return fileInfos;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async getProjectStructure(rootPath: string = process.cwd()): Promise<string[]> {
|
|
51
|
+
const pattern = '**/*';
|
|
52
|
+
|
|
53
|
+
const files = await glob(pattern, {
|
|
54
|
+
cwd: rootPath,
|
|
55
|
+
ignore: this.excludePatterns,
|
|
56
|
+
absolute: false,
|
|
57
|
+
nodir: true
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return files.sort();
|
|
61
|
+
}
|
|
62
|
+
}
|