@umituz/web-localization 1.1.6 → 1.1.7
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Google Translate Service
|
|
3
|
-
* @description Main translation service using Google Translate API
|
|
2
|
+
* Google Translate Service with Performance Optimizations
|
|
3
|
+
* @description Main translation service using Google Translate API with caching and pooling
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type {
|
|
@@ -27,10 +27,53 @@ import {
|
|
|
27
27
|
TRANSLATION_CONCURRENCY_LIMIT,
|
|
28
28
|
} from "../constants/index.js";
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Translation cache entry with TTL support
|
|
32
|
+
*/
|
|
33
|
+
interface CacheEntry {
|
|
34
|
+
translation: string;
|
|
35
|
+
timestamp: number;
|
|
36
|
+
accessCount: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Object pool for reusing Map instances
|
|
41
|
+
*/
|
|
42
|
+
class MapPool {
|
|
43
|
+
private pool: Map<string, string>[] = [];
|
|
44
|
+
private readonly maxPoolSize = 10;
|
|
45
|
+
|
|
46
|
+
acquire(): Map<string, string> {
|
|
47
|
+
return this.pool.pop() || new Map<string, string>();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
release(map: Map<string, string>): void {
|
|
51
|
+
if (map.size === 0 && this.pool.length < this.maxPoolSize) {
|
|
52
|
+
this.pool.push(map);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
clear(): void {
|
|
57
|
+
this.pool.length = 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
30
61
|
class GoogleTranslateService implements ITranslationService {
|
|
31
62
|
private config: TranslationServiceConfig | null = null;
|
|
32
63
|
private _rateLimiter: RateLimiter | null = null;
|
|
33
64
|
|
|
65
|
+
// Translation cache with LRU-style eviction
|
|
66
|
+
private translationCache = new Map<string, CacheEntry>();
|
|
67
|
+
private readonly maxCacheSize = 1000;
|
|
68
|
+
private readonly cacheTTL = 1000 * 60 * 60; // 1 hour
|
|
69
|
+
|
|
70
|
+
// Object pools
|
|
71
|
+
private readonly mapPool = new MapPool();
|
|
72
|
+
|
|
73
|
+
// Performance tracking
|
|
74
|
+
private activeRequests = 0;
|
|
75
|
+
private readonly maxConcurrentRequests = 20;
|
|
76
|
+
|
|
34
77
|
initialize(config: TranslationServiceConfig): void {
|
|
35
78
|
this.config = {
|
|
36
79
|
minDelay: DEFAULT_MIN_DELAY,
|
|
@@ -59,6 +102,82 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
59
102
|
}
|
|
60
103
|
}
|
|
61
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Generate cache key for translation
|
|
107
|
+
*/
|
|
108
|
+
private getCacheKey(text: string, targetLang: string, sourceLang: string): string {
|
|
109
|
+
return `${sourceLang}|${targetLang}|${text}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get translation from cache
|
|
114
|
+
*/
|
|
115
|
+
private getFromCache(text: string, targetLang: string, sourceLang: string): string | null {
|
|
116
|
+
const key = this.getCacheKey(text, targetLang, sourceLang);
|
|
117
|
+
const entry = this.translationCache.get(key);
|
|
118
|
+
|
|
119
|
+
if (!entry) return null;
|
|
120
|
+
|
|
121
|
+
// Check if entry has expired
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
if (now - entry.timestamp > this.cacheTTL) {
|
|
124
|
+
this.translationCache.delete(key);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Update access count and timestamp for LRU
|
|
129
|
+
entry.accessCount++;
|
|
130
|
+
entry.timestamp = now;
|
|
131
|
+
|
|
132
|
+
return entry.translation;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Store translation in cache with automatic eviction
|
|
137
|
+
*/
|
|
138
|
+
private storeInCache(text: string, targetLang: string, sourceLang: string, translation: string): void {
|
|
139
|
+
// If cache is full, remove least recently used entries
|
|
140
|
+
if (this.translationCache.size >= this.maxCacheSize) {
|
|
141
|
+
let oldestKey: string | null = null;
|
|
142
|
+
let oldestTime = Infinity;
|
|
143
|
+
|
|
144
|
+
for (const [key, entry] of this.translationCache) {
|
|
145
|
+
if (entry.timestamp < oldestTime) {
|
|
146
|
+
oldestTime = entry.timestamp;
|
|
147
|
+
oldestKey = key;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (oldestKey) {
|
|
152
|
+
this.translationCache.delete(oldestKey);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const key = this.getCacheKey(text, targetLang, sourceLang);
|
|
157
|
+
this.translationCache.set(key, {
|
|
158
|
+
translation,
|
|
159
|
+
timestamp: Date.now(),
|
|
160
|
+
accessCount: 1,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Clear translation cache
|
|
166
|
+
*/
|
|
167
|
+
clearCache(): void {
|
|
168
|
+
this.translationCache.clear();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get cache statistics
|
|
173
|
+
*/
|
|
174
|
+
getCacheStats(): { size: number; maxSize: number } {
|
|
175
|
+
return {
|
|
176
|
+
size: this.translationCache.size,
|
|
177
|
+
maxSize: this.maxCacheSize,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
62
181
|
async translate(request: TranslationRequest): Promise<TranslationResponse> {
|
|
63
182
|
this.ensureInitialized();
|
|
64
183
|
|
|
@@ -85,14 +204,36 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
85
204
|
};
|
|
86
205
|
}
|
|
87
206
|
|
|
88
|
-
|
|
207
|
+
// Check cache first
|
|
208
|
+
const cachedTranslation = this.getFromCache(text, targetLanguage, sourceLanguage);
|
|
209
|
+
if (cachedTranslation !== null) {
|
|
210
|
+
return {
|
|
211
|
+
originalText: text,
|
|
212
|
+
translatedText: cachedTranslation,
|
|
213
|
+
sourceLanguage,
|
|
214
|
+
targetLanguage,
|
|
215
|
+
success: true,
|
|
216
|
+
cached: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Wait for rate limit slot with normal priority
|
|
221
|
+
await this.rateLimiter.waitForSlot(5);
|
|
89
222
|
|
|
90
223
|
try {
|
|
224
|
+
const startTime = Date.now();
|
|
91
225
|
const translatedText = await this.callTranslateAPI(
|
|
92
226
|
text,
|
|
93
227
|
targetLanguage,
|
|
94
228
|
sourceLanguage
|
|
95
229
|
);
|
|
230
|
+
const responseTime = Date.now() - startTime;
|
|
231
|
+
|
|
232
|
+
// Record response time for dynamic rate adjustment
|
|
233
|
+
this.rateLimiter.recordResponseTime(responseTime);
|
|
234
|
+
|
|
235
|
+
// Store in cache
|
|
236
|
+
this.storeInCache(text, targetLanguage, sourceLanguage, translatedText);
|
|
96
237
|
|
|
97
238
|
return {
|
|
98
239
|
originalText: text,
|
|
@@ -128,43 +269,88 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
128
269
|
return stats;
|
|
129
270
|
}
|
|
130
271
|
|
|
131
|
-
//
|
|
132
|
-
const
|
|
272
|
+
// Filter out requests that can be served from cache
|
|
273
|
+
const uncachedRequests: TranslationRequest[] = [];
|
|
274
|
+
const cacheIndexMap = new Map<number, string>(); // Maps original index to cached translation
|
|
275
|
+
|
|
276
|
+
for (let i = 0; i < requests.length; i++) {
|
|
277
|
+
const request = requests[i];
|
|
278
|
+
const cachedTranslation = this.getFromCache(
|
|
279
|
+
request.text,
|
|
280
|
+
request.targetLanguage,
|
|
281
|
+
request.sourceLanguage || "en"
|
|
282
|
+
);
|
|
133
283
|
|
|
134
|
-
|
|
135
|
-
|
|
284
|
+
if (cachedTranslation !== null) {
|
|
285
|
+
// Serve from cache
|
|
286
|
+
cacheIndexMap.set(i, cachedTranslation);
|
|
287
|
+
stats.successCount++;
|
|
288
|
+
stats.translatedKeys.push({
|
|
289
|
+
key: request.text,
|
|
290
|
+
from: request.text,
|
|
291
|
+
to: cachedTranslation,
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
uncachedRequests.push(request);
|
|
295
|
+
}
|
|
136
296
|
}
|
|
137
297
|
|
|
138
|
-
|
|
139
|
-
const results = await Promise.allSettled(
|
|
140
|
-
chunk.map(async (request) => {
|
|
141
|
-
await this.rateLimiter.waitForSlot();
|
|
142
|
-
return this.callTranslateAPI(
|
|
143
|
-
request.text,
|
|
144
|
-
request.targetLanguage,
|
|
145
|
-
request.sourceLanguage || "en"
|
|
146
|
-
);
|
|
147
|
-
})
|
|
148
|
-
);
|
|
298
|
+
stats.skippedCount = cacheIndexMap.size;
|
|
149
299
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
300
|
+
// Process uncached requests with controlled parallelism
|
|
301
|
+
if (uncachedRequests.length > 0) {
|
|
302
|
+
const chunks: TranslationRequest[][] = [];
|
|
303
|
+
|
|
304
|
+
for (let i = 0; i < uncachedRequests.length; i += TRANSLATION_CONCURRENCY_LIMIT) {
|
|
305
|
+
chunks.push(uncachedRequests.slice(i, i + TRANSLATION_CONCURRENCY_LIMIT));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
for (const chunk of chunks) {
|
|
309
|
+
const results = await Promise.allSettled(
|
|
310
|
+
chunk.map(async (request) => {
|
|
311
|
+
await this.rateLimiter.waitForSlot(5);
|
|
312
|
+
const startTime = Date.now();
|
|
313
|
+
const result = await this.callTranslateAPI(
|
|
314
|
+
request.text,
|
|
315
|
+
request.targetLanguage,
|
|
316
|
+
request.sourceLanguage || "en"
|
|
317
|
+
);
|
|
318
|
+
const responseTime = Date.now() - startTime;
|
|
319
|
+
|
|
320
|
+
// Record response time for dynamic rate adjustment
|
|
321
|
+
this.rateLimiter.recordResponseTime(responseTime);
|
|
322
|
+
|
|
323
|
+
return result;
|
|
324
|
+
})
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
328
|
+
const request = chunk[i];
|
|
329
|
+
const result = results[i];
|
|
330
|
+
|
|
331
|
+
if (result.status === "fulfilled") {
|
|
332
|
+
const translatedText = result.value;
|
|
333
|
+
if (translatedText && translatedText !== request.text) {
|
|
334
|
+
stats.successCount++;
|
|
335
|
+
stats.translatedKeys.push({
|
|
336
|
+
key: request.text,
|
|
337
|
+
from: request.text,
|
|
338
|
+
to: translatedText,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Cache the successful translation
|
|
342
|
+
this.storeInCache(
|
|
343
|
+
request.text,
|
|
344
|
+
request.targetLanguage,
|
|
345
|
+
request.sourceLanguage || "en",
|
|
346
|
+
translatedText
|
|
347
|
+
);
|
|
348
|
+
} else {
|
|
349
|
+
stats.skippedCount++;
|
|
350
|
+
}
|
|
163
351
|
} else {
|
|
164
|
-
stats.
|
|
352
|
+
stats.failureCount++;
|
|
165
353
|
}
|
|
166
|
-
} else {
|
|
167
|
-
stats.failureCount++;
|
|
168
354
|
}
|
|
169
355
|
}
|
|
170
356
|
}
|
|
@@ -191,64 +377,96 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
191
377
|
if (!targetObject || typeof targetObject !== "object") return;
|
|
192
378
|
if (!targetLanguage || targetLanguage.trim().length === 0) return;
|
|
193
379
|
|
|
380
|
+
// First pass: collect all texts to translate (flattens nested structure)
|
|
381
|
+
const textsToTranslate: Array<{
|
|
382
|
+
key: string;
|
|
383
|
+
enValue: string;
|
|
384
|
+
currentPath: string;
|
|
385
|
+
}> = [];
|
|
386
|
+
|
|
387
|
+
const nestedObjects: Array<{
|
|
388
|
+
key: string;
|
|
389
|
+
sourceObj: Record<string, unknown>;
|
|
390
|
+
targetObj: Record<string, unknown>;
|
|
391
|
+
currentPath: string;
|
|
392
|
+
}> = [];
|
|
393
|
+
|
|
394
|
+
// Collect texts and nested objects
|
|
194
395
|
const keys = Object.keys(sourceObject);
|
|
195
|
-
const textsToTranslate: Array<{key: string; enValue: string; currentPath: string}> = [];
|
|
196
|
-
|
|
197
396
|
for (const key of keys) {
|
|
198
397
|
const enValue = sourceObject[key];
|
|
199
398
|
const targetValue = targetObject[key];
|
|
200
399
|
const currentPath = path ? `${path}.${key}` : key;
|
|
201
400
|
|
|
202
401
|
if (typeof enValue === "object" && enValue !== null) {
|
|
402
|
+
// Prepare nested object for processing
|
|
203
403
|
if (!targetObject[key] || typeof targetObject[key] !== "object") {
|
|
204
404
|
targetObject[key] = {};
|
|
205
405
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
406
|
+
nestedObjects.push({
|
|
407
|
+
key,
|
|
408
|
+
sourceObj: enValue as Record<string, unknown>,
|
|
409
|
+
targetObj: targetObject[key] as Record<string, unknown>,
|
|
210
410
|
currentPath,
|
|
211
|
-
|
|
212
|
-
onTranslate,
|
|
213
|
-
force
|
|
214
|
-
);
|
|
411
|
+
});
|
|
215
412
|
} else if (typeof enValue === "string") {
|
|
216
413
|
stats.totalCount++;
|
|
217
414
|
if (force || needsTranslation(targetValue)) {
|
|
218
|
-
textsToTranslate.push({key, enValue, currentPath});
|
|
415
|
+
textsToTranslate.push({ key, enValue, currentPath });
|
|
219
416
|
} else {
|
|
220
417
|
stats.skippedCount++;
|
|
221
418
|
}
|
|
222
419
|
}
|
|
223
420
|
}
|
|
224
421
|
|
|
422
|
+
// Process nested objects recursively
|
|
423
|
+
for (const nested of nestedObjects) {
|
|
424
|
+
await this.translateObject(
|
|
425
|
+
nested.sourceObj,
|
|
426
|
+
nested.targetObj,
|
|
427
|
+
targetLanguage,
|
|
428
|
+
nested.currentPath,
|
|
429
|
+
stats,
|
|
430
|
+
onTranslate,
|
|
431
|
+
force
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Process texts in batches
|
|
225
436
|
if (textsToTranslate.length > 0) {
|
|
226
437
|
for (let i = 0; i < textsToTranslate.length; i += TRANSLATION_BATCH_SIZE) {
|
|
227
438
|
const batch = textsToTranslate.slice(i, i + TRANSLATION_BATCH_SIZE);
|
|
228
439
|
const results = await this.translateBatch(
|
|
229
|
-
batch.map(item => ({
|
|
440
|
+
batch.map((item) => ({
|
|
230
441
|
text: item.enValue,
|
|
231
442
|
targetLanguage,
|
|
232
443
|
}))
|
|
233
444
|
);
|
|
234
445
|
|
|
235
|
-
//
|
|
236
|
-
const translationMap =
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
446
|
+
// Use object pool for map
|
|
447
|
+
const translationMap = this.mapPool.acquire();
|
|
448
|
+
try {
|
|
449
|
+
// Create a map for quick lookup of translations
|
|
450
|
+
for (const item of results.translatedKeys) {
|
|
451
|
+
translationMap.set(item.from, item.to);
|
|
452
|
+
}
|
|
240
453
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
454
|
+
for (let j = 0; j < batch.length; j++) {
|
|
455
|
+
const { key, enValue, currentPath } = batch[j];
|
|
456
|
+
const translatedText = translationMap.get(enValue);
|
|
244
457
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
458
|
+
if (translatedText && translatedText !== enValue) {
|
|
459
|
+
targetObject[key] = translatedText;
|
|
460
|
+
stats.successCount++;
|
|
461
|
+
if (onTranslate) onTranslate(currentPath, enValue, translatedText);
|
|
462
|
+
} else {
|
|
463
|
+
stats.failureCount++;
|
|
464
|
+
}
|
|
251
465
|
}
|
|
466
|
+
} finally {
|
|
467
|
+
// Clear and release map back to pool
|
|
468
|
+
translationMap.clear();
|
|
469
|
+
this.mapPool.release(translationMap);
|
|
252
470
|
}
|
|
253
471
|
}
|
|
254
472
|
}
|
|
@@ -261,80 +479,125 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
261
479
|
retries = DEFAULT_MAX_RETRIES,
|
|
262
480
|
backoffMs = 2000
|
|
263
481
|
): Promise<string> {
|
|
264
|
-
//
|
|
265
|
-
|
|
266
|
-
let counter = 0;
|
|
267
|
-
|
|
268
|
-
// Find all {{something}} patterns
|
|
269
|
-
const safeText = text.replace(/\{\{([^}]+)\}\}/g, (match) => {
|
|
270
|
-
const placeholder = `_VAR${counter}_`; // Using a simple token less likely to be split
|
|
271
|
-
varMap.set(placeholder, match);
|
|
272
|
-
counter++;
|
|
273
|
-
return placeholder;
|
|
274
|
-
});
|
|
482
|
+
// Increment active requests counter
|
|
483
|
+
this.activeRequests++;
|
|
275
484
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
const controller = new AbortController();
|
|
282
|
-
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
485
|
+
try {
|
|
486
|
+
// 1. Variable Protection (Extract {{variables}})
|
|
487
|
+
// Use object pool for map to reduce GC pressure
|
|
488
|
+
const varMap = this.mapPool.acquire();
|
|
489
|
+
let counter = 0;
|
|
283
490
|
|
|
284
491
|
try {
|
|
285
|
-
|
|
286
|
-
|
|
492
|
+
// Find all {{something}} patterns
|
|
493
|
+
// Use more specific pattern to avoid false matches
|
|
494
|
+
const safeText = text.replace(/\{\{([^}]+)\}\}/g, (match) => {
|
|
495
|
+
const placeholder = `__VAR${counter}__`;
|
|
496
|
+
varMap.set(placeholder, match);
|
|
497
|
+
counter++;
|
|
498
|
+
return placeholder;
|
|
287
499
|
});
|
|
288
500
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
501
|
+
const timeout = this.config?.timeout || DEFAULT_TIMEOUT;
|
|
502
|
+
const encodedText = encodeURIComponent(safeText);
|
|
503
|
+
const url = `${GOOGLE_TRANSLATE_API_URL}?client=gtx&sl=${sourceLanguage}&tl=${targetLanguage}&dt=t&q=${encodedText}`;
|
|
504
|
+
|
|
505
|
+
for (let attempt = 0; attempt < retries; attempt++) {
|
|
506
|
+
const controller = new AbortController();
|
|
507
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
508
|
+
|
|
509
|
+
try {
|
|
510
|
+
const response = await fetch(url, {
|
|
511
|
+
signal: controller.signal,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
if (!response.ok) {
|
|
515
|
+
if (response.status === 429 || response.status >= 500) {
|
|
516
|
+
if (attempt < retries - 1) {
|
|
517
|
+
clearTimeout(timeoutId);
|
|
518
|
+
// Exponential backoff
|
|
519
|
+
const delay = backoffMs * Math.pow(2, attempt);
|
|
520
|
+
await this.sleep(delay);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
throw new Error(`API request failed: ${response.status}`);
|
|
297
525
|
}
|
|
298
|
-
}
|
|
299
|
-
throw new Error(`API request failed: ${response.status}`);
|
|
300
|
-
}
|
|
301
526
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
527
|
+
const data = await response.json();
|
|
528
|
+
|
|
529
|
+
let translatedStr = safeText;
|
|
530
|
+
if (
|
|
531
|
+
Array.isArray(data) &&
|
|
532
|
+
data.length > 0 &&
|
|
533
|
+
Array.isArray(data[0]) &&
|
|
534
|
+
data[0].length > 0 &&
|
|
535
|
+
typeof data[0][0][0] === "string"
|
|
536
|
+
) {
|
|
537
|
+
translatedStr = data[0].map((item: unknown[]) => item[0] as string).join('');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// 2. Re-inject Variables
|
|
541
|
+
if (varMap.size > 0) {
|
|
542
|
+
// Sometimes Google adds spaces, like __VAR0__ -> __ VAR0 __
|
|
543
|
+
// Use pre-compiled regex patterns for better performance
|
|
544
|
+
for (const [placeholder, originalVar] of varMap.entries()) {
|
|
545
|
+
// Escape special regex characters in placeholder
|
|
546
|
+
const escapedPlaceholder = placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
547
|
+
// Allow optional spaces between each character
|
|
548
|
+
const regex = new RegExp(escapedPlaceholder.split('').join('\\s*'), 'g');
|
|
549
|
+
translatedStr = translatedStr.replace(regex, originalVar);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
314
552
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
553
|
+
return translatedStr;
|
|
554
|
+
} catch (error) {
|
|
555
|
+
clearTimeout(timeoutId);
|
|
556
|
+
if (attempt === retries - 1) {
|
|
557
|
+
throw error;
|
|
558
|
+
}
|
|
559
|
+
const delay = backoffMs * Math.pow(2, attempt);
|
|
560
|
+
await this.sleep(delay);
|
|
561
|
+
} finally {
|
|
562
|
+
clearTimeout(timeoutId);
|
|
321
563
|
}
|
|
322
564
|
}
|
|
323
565
|
|
|
324
|
-
return
|
|
325
|
-
} catch (error) {
|
|
326
|
-
clearTimeout(timeoutId);
|
|
327
|
-
if (attempt === retries - 1) {
|
|
328
|
-
throw error;
|
|
329
|
-
}
|
|
330
|
-
const delay = backoffMs * Math.pow(2, attempt);
|
|
331
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
566
|
+
return text;
|
|
332
567
|
} finally {
|
|
333
|
-
|
|
568
|
+
// Clear and release map back to pool
|
|
569
|
+
varMap.clear();
|
|
570
|
+
this.mapPool.release(varMap);
|
|
334
571
|
}
|
|
572
|
+
} finally {
|
|
573
|
+
// Decrement active requests counter
|
|
574
|
+
this.activeRequests--;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Optimized sleep function
|
|
580
|
+
*/
|
|
581
|
+
private sleep(ms: number): Promise<void> {
|
|
582
|
+
return new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Get number of active requests
|
|
587
|
+
*/
|
|
588
|
+
getActiveRequestCount(): number {
|
|
589
|
+
return this.activeRequests;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Clean up resources
|
|
594
|
+
*/
|
|
595
|
+
dispose(): void {
|
|
596
|
+
this.clearCache();
|
|
597
|
+
this.mapPool.clear();
|
|
598
|
+
if (this._rateLimiter) {
|
|
599
|
+
this._rateLimiter.clear();
|
|
335
600
|
}
|
|
336
|
-
|
|
337
|
-
return text;
|
|
338
601
|
}
|
|
339
602
|
}
|
|
340
603
|
|