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.
Files changed (58) hide show
  1. package/README.md +576 -0
  2. package/dist/cli/generate.d.ts +2 -0
  3. package/dist/cli/generate.d.ts.map +1 -0
  4. package/dist/cli/generate.js +416 -0
  5. package/dist/cli/generate.js.map +1 -0
  6. package/dist/cli/index.d.ts +3 -0
  7. package/dist/cli/index.d.ts.map +1 -0
  8. package/dist/cli/index.js +43 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/cli/init.d.ts +2 -0
  11. package/dist/cli/init.d.ts.map +1 -0
  12. package/dist/cli/init.js +81 -0
  13. package/dist/cli/init.js.map +1 -0
  14. package/dist/cli/report.d.ts +2 -0
  15. package/dist/cli/report.d.ts.map +1 -0
  16. package/dist/cli/report.js +137 -0
  17. package/dist/cli/report.js.map +1 -0
  18. package/dist/cli/scan.d.ts +2 -0
  19. package/dist/cli/scan.d.ts.map +1 -0
  20. package/dist/cli/scan.js +62 -0
  21. package/dist/cli/scan.js.map +1 -0
  22. package/dist/cli/watch-i18n.d.ts +2 -0
  23. package/dist/cli/watch-i18n.d.ts.map +1 -0
  24. package/dist/cli/watch-i18n.js +73 -0
  25. package/dist/cli/watch-i18n.js.map +1 -0
  26. package/dist/cli/watch.d.ts +2 -0
  27. package/dist/cli/watch.d.ts.map +1 -0
  28. package/dist/cli/watch.js +147 -0
  29. package/dist/cli/watch.js.map +1 -0
  30. package/dist/core/i18nGenerator.d.ts +16 -0
  31. package/dist/core/i18nGenerator.d.ts.map +1 -0
  32. package/dist/core/i18nGenerator.js +139 -0
  33. package/dist/core/i18nGenerator.js.map +1 -0
  34. package/dist/core/projectScanner.d.ts +12 -0
  35. package/dist/core/projectScanner.d.ts.map +1 -0
  36. package/dist/core/projectScanner.js +53 -0
  37. package/dist/core/projectScanner.js.map +1 -0
  38. package/dist/core/stringExtractor.d.ts +21 -0
  39. package/dist/core/stringExtractor.d.ts.map +1 -0
  40. package/dist/core/stringExtractor.js +268 -0
  41. package/dist/core/stringExtractor.js.map +1 -0
  42. package/dist/index.d.ts +7 -0
  43. package/dist/index.d.ts.map +1 -0
  44. package/dist/index.js +10 -0
  45. package/dist/index.js.map +1 -0
  46. package/package.json +44 -0
  47. package/src/cli/generate.ts +422 -0
  48. package/src/cli/index.ts +50 -0
  49. package/src/cli/init.ts +96 -0
  50. package/src/cli/report.ts +160 -0
  51. package/src/cli/scan.ts +69 -0
  52. package/src/cli/watch-i18n.ts +77 -0
  53. package/src/cli/watch.ts +129 -0
  54. package/src/core/i18nGenerator.ts +127 -0
  55. package/src/core/projectScanner.ts +62 -0
  56. package/src/core/stringExtractor.ts +276 -0
  57. package/src/index.ts +7 -0
  58. 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
+ }