@umituz/web-localization 1.1.6 ā 1.1.8
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/package.json +1 -1
- package/src/domain/entities/translation.entity.ts +1 -0
- package/src/infrastructure/services/cli.service.ts +152 -50
- package/src/infrastructure/services/google-translate.service.ts +383 -120
- package/src/infrastructure/utils/file.util.ts +181 -19
- package/src/infrastructure/utils/rate-limit.util.ts +125 -13
- package/src/integrations/i18n.setup.ts +160 -4
package/package.json
CHANGED
|
@@ -6,7 +6,8 @@ import { googleTranslateService } from "./google-translate.service.js";
|
|
|
6
6
|
import { parseTypeScriptFile, generateTypeScriptContent } from "../utils/file.util.js";
|
|
7
7
|
import {
|
|
8
8
|
DEFAULT_LOCALES_DIR,
|
|
9
|
-
DEFAULT_BASE_LANGUAGE
|
|
9
|
+
DEFAULT_BASE_LANGUAGE,
|
|
10
|
+
TRANSLATION_CONCURRENCY_LIMIT,
|
|
10
11
|
} from "../constants/index.js";
|
|
11
12
|
|
|
12
13
|
export interface SyncOptions {
|
|
@@ -14,6 +15,48 @@ export interface SyncOptions {
|
|
|
14
15
|
sourceDir?: string;
|
|
15
16
|
baseLang?: string;
|
|
16
17
|
force?: boolean;
|
|
18
|
+
concurrency?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Semaphore for controlling concurrent operations
|
|
23
|
+
*/
|
|
24
|
+
class Semaphore {
|
|
25
|
+
private permits: number;
|
|
26
|
+
private queue: Array<() => void> = [];
|
|
27
|
+
|
|
28
|
+
constructor(permits: number) {
|
|
29
|
+
this.permits = permits;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async acquire(): Promise<void> {
|
|
33
|
+
if (this.permits > 0) {
|
|
34
|
+
this.permits--;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return new Promise<void>((resolve) => {
|
|
39
|
+
this.queue.push(resolve);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
release(): void {
|
|
44
|
+
this.permits++;
|
|
45
|
+
const next = this.queue.shift();
|
|
46
|
+
if (next) {
|
|
47
|
+
this.permits--;
|
|
48
|
+
next();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async run<T>(fn: () => Promise<T>): Promise<T> {
|
|
53
|
+
await this.acquire();
|
|
54
|
+
try {
|
|
55
|
+
return await fn();
|
|
56
|
+
} finally {
|
|
57
|
+
this.release();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
17
60
|
}
|
|
18
61
|
|
|
19
62
|
// Extracted outside loop for better performance
|
|
@@ -59,21 +102,34 @@ export class CLIService {
|
|
|
59
102
|
}
|
|
60
103
|
|
|
61
104
|
const baseData = parseTypeScriptFile(baseLangPath);
|
|
105
|
+
|
|
106
|
+
// Pre-compile regex for better performance
|
|
107
|
+
const localeFileRegex = /^[a-z]{2}(-[A-Z]{2})?\.ts$/;
|
|
62
108
|
const files = fs.readdirSync(localesDir)
|
|
63
|
-
.filter(f =>
|
|
109
|
+
.filter(f => localeFileRegex.test(f) && f !== `${baseLang}.ts`)
|
|
64
110
|
.sort();
|
|
65
111
|
|
|
66
112
|
console.log(chalk.blue(`š Found ${files.length} languages to sync with ${baseLang}.\n`));
|
|
67
113
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const langCode = path.basename(file, ".ts");
|
|
114
|
+
// Process files in parallel with controlled concurrency
|
|
115
|
+
const concurrency = options.concurrency || TRANSLATION_CONCURRENCY_LIMIT;
|
|
116
|
+
const semaphore = new Semaphore(concurrency);
|
|
72
117
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
118
|
+
const syncPromises = files.map(async (file) => {
|
|
119
|
+
return semaphore.run(async () => {
|
|
120
|
+
const targetPath = path.join(localesDir, file);
|
|
121
|
+
const targetData = parseTypeScriptFile(targetPath);
|
|
122
|
+
const langCode = path.basename(file, ".ts");
|
|
123
|
+
|
|
124
|
+
const syncedData = syncObject(baseData, targetData);
|
|
125
|
+
fs.writeFileSync(targetPath, generateTypeScriptContent(syncedData, langCode));
|
|
126
|
+
|
|
127
|
+
// Non-blocking progress update
|
|
128
|
+
process.stdout.write(chalk.green(` š ${langCode}: Synced structure.\n`));
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await Promise.all(syncPromises);
|
|
77
133
|
|
|
78
134
|
console.log(chalk.bold.green("\nā
Synchronization completed!"));
|
|
79
135
|
}
|
|
@@ -90,53 +146,99 @@ export class CLIService {
|
|
|
90
146
|
|
|
91
147
|
googleTranslateService.initialize({});
|
|
92
148
|
const baseData = parseTypeScriptFile(baseLangPath);
|
|
149
|
+
|
|
150
|
+
// Pre-compile regex for better performance
|
|
151
|
+
const localeFileRegex = /^[a-z]{2}(-[A-Z]{2})?\.ts$/;
|
|
93
152
|
const files = fs.readdirSync(localesDir)
|
|
94
|
-
.filter(f =>
|
|
153
|
+
.filter(f => localeFileRegex.test(f) && f !== `${baseLang}.ts`)
|
|
95
154
|
.sort();
|
|
96
155
|
|
|
97
156
|
console.log(chalk.blue.bold(`š Starting automatic translation for ${files.length} languages...\n`));
|
|
98
157
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
158
|
+
// Process languages in parallel with controlled concurrency
|
|
159
|
+
const concurrency = options.concurrency || TRANSLATION_CONCURRENCY_LIMIT;
|
|
160
|
+
const semaphore = new Semaphore(concurrency);
|
|
161
|
+
|
|
162
|
+
// Shared statistics tracking
|
|
163
|
+
const totalStats = {
|
|
164
|
+
totalLanguages: files.length,
|
|
165
|
+
completedLanguages: 0,
|
|
166
|
+
totalSuccess: 0,
|
|
167
|
+
totalFailure: 0,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const translatePromises = files.map(async (file) => {
|
|
171
|
+
return semaphore.run(async () => {
|
|
172
|
+
const targetPath = path.join(localesDir, file);
|
|
173
|
+
const targetData = parseTypeScriptFile(targetPath);
|
|
174
|
+
const langCode = path.basename(file, ".ts");
|
|
175
|
+
|
|
176
|
+
const stats: TranslationStats = {
|
|
177
|
+
totalCount: 0,
|
|
178
|
+
successCount: 0,
|
|
179
|
+
failureCount: 0,
|
|
180
|
+
skippedCount: 0,
|
|
181
|
+
translatedKeys: []
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Non-blocking progress update with language name
|
|
185
|
+
const langName = langCode.padEnd(6);
|
|
186
|
+
process.stdout.write(chalk.yellow(`š Translating ${langName}...\n`));
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
// Extract ISO 639-1 language code (e.g., "en" from "en-US")
|
|
190
|
+
const targetLang = langCode.includes("-") ? langCode.split("-")[0] : langCode;
|
|
191
|
+
|
|
192
|
+
await googleTranslateService.translateObject(
|
|
193
|
+
baseData,
|
|
194
|
+
targetData,
|
|
195
|
+
targetLang,
|
|
196
|
+
"",
|
|
197
|
+
stats,
|
|
198
|
+
(_key, _from, _to) => {
|
|
199
|
+
// Non-blocking per-key progress (only in verbose mode or for debugging)
|
|
200
|
+
// Commented out to reduce console spam
|
|
201
|
+
// process.stdout.write(chalk.gray(` ⢠${key}: ${from.substring(0, 15)}... ā ${to.substring(0, 15)}...\r`));
|
|
202
|
+
},
|
|
203
|
+
options.force
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// Write translated content
|
|
207
|
+
if (stats.successCount > 0) {
|
|
208
|
+
fs.writeFileSync(targetPath, generateTypeScriptContent(targetData, langCode));
|
|
209
|
+
totalStats.totalSuccess += stats.successCount;
|
|
210
|
+
process.stdout.write(chalk.green(` ā
${langName} Successfully translated ${stats.successCount} keys.\n`));
|
|
211
|
+
} else if (stats.failureCount > 0) {
|
|
212
|
+
totalStats.totalFailure += stats.failureCount;
|
|
213
|
+
process.stdout.write(chalk.red(` ā ${langName} Failed to translate ${stats.failureCount} keys.\n`));
|
|
214
|
+
} else {
|
|
215
|
+
process.stdout.write(chalk.gray(` ⨠${langName} Already up to date.\n`));
|
|
216
|
+
}
|
|
217
|
+
} catch (error) {
|
|
218
|
+
totalStats.totalFailure += stats.failureCount;
|
|
219
|
+
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
220
|
+
process.stdout.write(chalk.red(` ā ${langName} Error: ${errorMsg}\n`));
|
|
221
|
+
} finally {
|
|
222
|
+
totalStats.completedLanguages++;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
await Promise.all(translatePromises);
|
|
228
|
+
|
|
229
|
+
// Final summary
|
|
230
|
+
console.log(chalk.bold.green("\nā
All translations completed!"));
|
|
231
|
+
console.log(chalk.gray(` š Processed ${totalStats.completedLanguages}/${totalStats.totalLanguages} languages`));
|
|
232
|
+
if (totalStats.totalSuccess > 0) {
|
|
233
|
+
console.log(chalk.green(` ā
Total keys translated: ${totalStats.totalSuccess}`));
|
|
234
|
+
}
|
|
235
|
+
if (totalStats.totalFailure > 0) {
|
|
236
|
+
console.log(chalk.red(` ā Total keys failed: ${totalStats.totalFailure}`));
|
|
137
237
|
}
|
|
138
238
|
|
|
139
|
-
|
|
239
|
+
// Display cache statistics
|
|
240
|
+
const cacheStats = googleTranslateService.getCacheStats();
|
|
241
|
+
console.log(chalk.gray(` š¾ Cache hit rate: ${cacheStats.size}/${cacheStats.maxSize}`));
|
|
140
242
|
}
|
|
141
243
|
}
|
|
142
244
|
|