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,276 @@
|
|
|
1
|
+
import { parse } from '@babel/parser';
|
|
2
|
+
import traverse from '@babel/traverse';
|
|
3
|
+
import * as t from '@babel/types';
|
|
4
|
+
import { FileInfo } from './projectScanner';
|
|
5
|
+
|
|
6
|
+
export interface ExtractedString {
|
|
7
|
+
text: string;
|
|
8
|
+
key: string;
|
|
9
|
+
filePath: string;
|
|
10
|
+
line: number;
|
|
11
|
+
column: number;
|
|
12
|
+
type: 'jsx' | 'string' | 'template';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class StringExtractor {
|
|
16
|
+
private readonly minStringLength = 2;
|
|
17
|
+
private readonly excludePatterns = [
|
|
18
|
+
/^[a-zA-Z]+\.[a-zA-Z]+$/, // property access
|
|
19
|
+
/^[{}()\[\]]$/, // single brackets
|
|
20
|
+
/^\d+$/, // numbers only
|
|
21
|
+
/^[a-zA-Z]$/, // single letters
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
async extractStrings(files: FileInfo[]): Promise<ExtractedString[]> {
|
|
25
|
+
const extractedStrings: ExtractedString[] = [];
|
|
26
|
+
|
|
27
|
+
for (const file of files) {
|
|
28
|
+
const strings = await this.extractFromFile(file);
|
|
29
|
+
extractedStrings.push(...strings);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return this.deduplicateStrings(extractedStrings);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async extractFromFile(file: FileInfo): Promise<ExtractedString[]> {
|
|
36
|
+
const strings: ExtractedString[] = [];
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const ast = parse(file.content, {
|
|
40
|
+
sourceType: 'module',
|
|
41
|
+
plugins: [
|
|
42
|
+
'jsx',
|
|
43
|
+
'typescript',
|
|
44
|
+
'decorators-legacy',
|
|
45
|
+
'classProperties',
|
|
46
|
+
'objectRestSpread',
|
|
47
|
+
'asyncGenerators',
|
|
48
|
+
'functionBind',
|
|
49
|
+
'exportDefaultFrom',
|
|
50
|
+
'exportNamespaceFrom',
|
|
51
|
+
'dynamicImport',
|
|
52
|
+
'nullishCoalescingOperator',
|
|
53
|
+
'optionalChaining'
|
|
54
|
+
]
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const lines = file.content.split('\n');
|
|
58
|
+
|
|
59
|
+
traverse(ast, {
|
|
60
|
+
// JSX text content
|
|
61
|
+
JSXText: (path: any) => {
|
|
62
|
+
const text = path.node.value.trim();
|
|
63
|
+
if (this.isValidString(text)) {
|
|
64
|
+
const loc = path.node.loc;
|
|
65
|
+
if (loc) {
|
|
66
|
+
strings.push({
|
|
67
|
+
text,
|
|
68
|
+
key: this.generateKey(text),
|
|
69
|
+
filePath: file.path,
|
|
70
|
+
line: loc.start.line,
|
|
71
|
+
column: loc.start.column,
|
|
72
|
+
type: 'jsx'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
// String literals
|
|
79
|
+
StringLiteral: (path: any) => {
|
|
80
|
+
const text = path.node.value;
|
|
81
|
+
if (this.isValidString(text) && this.isUIText(path)) {
|
|
82
|
+
const loc = path.node.loc;
|
|
83
|
+
if (loc) {
|
|
84
|
+
strings.push({
|
|
85
|
+
text,
|
|
86
|
+
key: this.generateKey(text),
|
|
87
|
+
filePath: file.path,
|
|
88
|
+
line: loc.start.line,
|
|
89
|
+
column: loc.start.column,
|
|
90
|
+
type: 'string'
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
// Template literals
|
|
97
|
+
TemplateLiteral: (path: any) => {
|
|
98
|
+
if (path.node.expressions.length === 0) {
|
|
99
|
+
const text = path.node.quasis[0]?.value.raw;
|
|
100
|
+
if (text && this.isValidString(text)) {
|
|
101
|
+
const loc = path.node.loc;
|
|
102
|
+
if (loc) {
|
|
103
|
+
strings.push({
|
|
104
|
+
text,
|
|
105
|
+
key: this.generateKey(text),
|
|
106
|
+
filePath: file.path,
|
|
107
|
+
line: loc.start.line,
|
|
108
|
+
column: loc.start.column,
|
|
109
|
+
type: 'template'
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.warn(`Warning: Could not parse file ${file.path}:`, error);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return strings;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private isValidString(text: string): boolean {
|
|
125
|
+
if (!text || text.length < this.minStringLength) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Check if it's likely UI text (contains letters and spaces)
|
|
130
|
+
const hasLetters = /[a-zA-Z]/.test(text);
|
|
131
|
+
const hasSpaces = /\s/.test(text);
|
|
132
|
+
const isSentence = /[.!?]$/.test(text);
|
|
133
|
+
const isPhrase = text.split(' ').length >= 2;
|
|
134
|
+
|
|
135
|
+
// Must have letters and either spaces or be a complete sentence
|
|
136
|
+
if (!hasLetters) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Accept if it has spaces (likely a phrase) or is a complete sentence
|
|
141
|
+
if (hasSpaces || isSentence || isPhrase) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// For single words, only accept if they're longer and look like UI text
|
|
146
|
+
if (text.length >= 4 && /^[A-Z][a-z]+$/.test(text)) {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Reject technical strings, single words, or very short strings
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private isUIText(path: any): boolean {
|
|
155
|
+
const parent = path.parent;
|
|
156
|
+
const text = path.node.value;
|
|
157
|
+
|
|
158
|
+
// Exclude technical strings that should never be localized
|
|
159
|
+
if (this.isTechnicalString(text)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Exclude keys in objects
|
|
164
|
+
if (t.isObjectProperty(parent) && parent.key === path.node) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Exclude import statements
|
|
169
|
+
if (t.isImportDeclaration(parent)) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Exclude import() calls (dynamic imports)
|
|
174
|
+
if (t.isCallExpression(parent) &&
|
|
175
|
+
parent.callee.type === 'Import') {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Exclude require calls
|
|
180
|
+
if (t.isCallExpression(parent) &&
|
|
181
|
+
t.isIdentifier(parent.callee) &&
|
|
182
|
+
parent.callee.name === 'require') {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Exclude document.getElementById calls
|
|
187
|
+
if (t.isCallExpression(parent) &&
|
|
188
|
+
t.isMemberExpression(parent.callee) &&
|
|
189
|
+
t.isIdentifier(parent.callee.object) &&
|
|
190
|
+
parent.callee.object.name === 'document' &&
|
|
191
|
+
t.isIdentifier(parent.callee.property) &&
|
|
192
|
+
parent.callee.property.name === 'getElementById') {
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Exclude variable declarations that look like constants
|
|
197
|
+
if (t.isVariableDeclarator(parent) &&
|
|
198
|
+
t.isIdentifier(parent.id) &&
|
|
199
|
+
/^[A-Z_]+$/.test(parent.id.name)) {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Exclude test function names
|
|
204
|
+
if (t.isCallExpression(parent) &&
|
|
205
|
+
t.isIdentifier(parent.callee) &&
|
|
206
|
+
parent.callee.name === 'test') {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private isTechnicalString(text: string): boolean {
|
|
214
|
+
// DOM IDs that should never be localized
|
|
215
|
+
const domIds = ['root', 'app', 'main', 'header', 'footer', 'nav', 'sidebar', 'content'];
|
|
216
|
+
|
|
217
|
+
// Package names that should never be localized
|
|
218
|
+
const packageNames = ['web-vitals', 'react', 'react-dom', 'react-scripts'];
|
|
219
|
+
|
|
220
|
+
// File extensions and paths
|
|
221
|
+
const filePatterns = /\.(js|ts|tsx|jsx|json|css|html|svg|png|jpg|jpeg|gif|ico)$/;
|
|
222
|
+
|
|
223
|
+
// URLs and protocols
|
|
224
|
+
const urlPatterns = /^(https?:\/\/|ftp:\/\/|mailto:|tel:)/;
|
|
225
|
+
|
|
226
|
+
// Environment variables and config keys
|
|
227
|
+
const envPatterns = /^[A-Z_]+$/;
|
|
228
|
+
|
|
229
|
+
// Technical identifiers (short, lowercase with underscores/hyphens)
|
|
230
|
+
const technicalPatterns = /^[a-z][a-z0-9_-]*$/;
|
|
231
|
+
|
|
232
|
+
// Additional patterns for React-specific technical strings
|
|
233
|
+
const reactPatterns = /^(app|src|component|element|container|wrapper|header|footer|nav|sidebar|content|main|root)([A-Z][a-z0-9]*)*$/;
|
|
234
|
+
|
|
235
|
+
// Check if it's a CSS class or ID pattern
|
|
236
|
+
const cssPatterns = /^[a-z][a-z0-9-]*-[a-z][a-z0-9-]*$/;
|
|
237
|
+
|
|
238
|
+
// Check if it's a file path or component name
|
|
239
|
+
const filePathPatterns = /^[a-z][a-z0-9]*\.[a-z]+$/;
|
|
240
|
+
|
|
241
|
+
// Check against all patterns
|
|
242
|
+
return domIds.includes(text) ||
|
|
243
|
+
packageNames.includes(text) ||
|
|
244
|
+
filePatterns.test(text) ||
|
|
245
|
+
urlPatterns.test(text) ||
|
|
246
|
+
envPatterns.test(text) ||
|
|
247
|
+
(technicalPatterns.test(text) && text.length < 20) ||
|
|
248
|
+
reactPatterns.test(text) ||
|
|
249
|
+
cssPatterns.test(text) ||
|
|
250
|
+
filePathPatterns.test(text) ||
|
|
251
|
+
text.includes('.') && text.includes('/') ||
|
|
252
|
+
text.includes('_') && text.length < 15;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private generateKey(text: string): string {
|
|
256
|
+
return text
|
|
257
|
+
.toLowerCase()
|
|
258
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
259
|
+
.replace(/\s+/g, '_')
|
|
260
|
+
.replace(/^[^a-z]/, '')
|
|
261
|
+
.substring(0, 50);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private deduplicateStrings(strings: ExtractedString[]): ExtractedString[] {
|
|
265
|
+
const seen = new Map<string, ExtractedString>();
|
|
266
|
+
|
|
267
|
+
for (const str of strings) {
|
|
268
|
+
const existing = seen.get(str.text);
|
|
269
|
+
if (!existing || str.type === 'jsx') {
|
|
270
|
+
seen.set(str.text, str);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return Array.from(seen.values());
|
|
275
|
+
}
|
|
276
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { ProjectScanner } from './core/projectScanner';
|
|
2
|
+
export { StringExtractor } from './core/stringExtractor';
|
|
3
|
+
export { I18nGenerator } from './core/i18nGenerator';
|
|
4
|
+
|
|
5
|
+
export type { FileInfo } from './core/projectScanner';
|
|
6
|
+
export type { ExtractedString } from './core/stringExtractor';
|
|
7
|
+
export type { I18nConfig } from './core/i18nGenerator';
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"moduleResolution": "node",
|
|
6
|
+
"lib": ["ES2020"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|