@umituz/web-localization 1.1.5 → 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/domain/interfaces/translation-service.interface.ts +0 -1
- package/src/infrastructure/constants/index.ts +3 -3
- package/src/infrastructure/services/cli.service.ts +152 -48
- package/src/infrastructure/services/google-translate.service.ts +388 -125
- package/src/infrastructure/utils/file.util.ts +181 -19
- package/src/infrastructure/utils/rate-limit.util.ts +127 -15
- package/src/infrastructure/utils/text-validator.util.ts +2 -4
- 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 {
|
|
@@ -23,12 +23,57 @@ import {
|
|
|
23
23
|
DEFAULT_MIN_DELAY,
|
|
24
24
|
DEFAULT_TIMEOUT,
|
|
25
25
|
DEFAULT_MAX_RETRIES,
|
|
26
|
+
TRANSLATION_BATCH_SIZE,
|
|
27
|
+
TRANSLATION_CONCURRENCY_LIMIT,
|
|
26
28
|
} from "../constants/index.js";
|
|
27
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
|
+
|
|
28
61
|
class GoogleTranslateService implements ITranslationService {
|
|
29
62
|
private config: TranslationServiceConfig | null = null;
|
|
30
63
|
private _rateLimiter: RateLimiter | null = null;
|
|
31
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
|
+
|
|
32
77
|
initialize(config: TranslationServiceConfig): void {
|
|
33
78
|
this.config = {
|
|
34
79
|
minDelay: DEFAULT_MIN_DELAY,
|
|
@@ -57,6 +102,82 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
57
102
|
}
|
|
58
103
|
}
|
|
59
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
|
+
|
|
60
181
|
async translate(request: TranslationRequest): Promise<TranslationResponse> {
|
|
61
182
|
this.ensureInitialized();
|
|
62
183
|
|
|
@@ -83,14 +204,36 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
83
204
|
};
|
|
84
205
|
}
|
|
85
206
|
|
|
86
|
-
|
|
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);
|
|
87
222
|
|
|
88
223
|
try {
|
|
224
|
+
const startTime = Date.now();
|
|
89
225
|
const translatedText = await this.callTranslateAPI(
|
|
90
226
|
text,
|
|
91
227
|
targetLanguage,
|
|
92
228
|
sourceLanguage
|
|
93
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);
|
|
94
237
|
|
|
95
238
|
return {
|
|
96
239
|
originalText: text,
|
|
@@ -126,44 +269,88 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
126
269
|
return stats;
|
|
127
270
|
}
|
|
128
271
|
|
|
129
|
-
//
|
|
130
|
-
const
|
|
131
|
-
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
|
+
);
|
|
132
283
|
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
}
|
|
135
296
|
}
|
|
136
297
|
|
|
137
|
-
|
|
138
|
-
const results = await Promise.allSettled(
|
|
139
|
-
chunk.map(async (request) => {
|
|
140
|
-
await this.rateLimiter.waitForSlot();
|
|
141
|
-
return this.callTranslateAPI(
|
|
142
|
-
request.text,
|
|
143
|
-
request.targetLanguage,
|
|
144
|
-
request.sourceLanguage || "en"
|
|
145
|
-
);
|
|
146
|
-
})
|
|
147
|
-
);
|
|
298
|
+
stats.skippedCount = cacheIndexMap.size;
|
|
148
299
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
+
}
|
|
162
351
|
} else {
|
|
163
|
-
stats.
|
|
352
|
+
stats.failureCount++;
|
|
164
353
|
}
|
|
165
|
-
} else {
|
|
166
|
-
stats.failureCount++;
|
|
167
354
|
}
|
|
168
355
|
}
|
|
169
356
|
}
|
|
@@ -190,65 +377,96 @@ class GoogleTranslateService implements ITranslationService {
|
|
|
190
377
|
if (!targetObject || typeof targetObject !== "object") return;
|
|
191
378
|
if (!targetLanguage || targetLanguage.trim().length === 0) return;
|
|
192
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
|
|
193
395
|
const keys = Object.keys(sourceObject);
|
|
194
|
-
const textsToTranslate: Array<{key: string; enValue: string; currentPath: string}> = [];
|
|
195
|
-
|
|
196
396
|
for (const key of keys) {
|
|
197
397
|
const enValue = sourceObject[key];
|
|
198
398
|
const targetValue = targetObject[key];
|
|
199
399
|
const currentPath = path ? `${path}.${key}` : key;
|
|
200
400
|
|
|
201
401
|
if (typeof enValue === "object" && enValue !== null) {
|
|
402
|
+
// Prepare nested object for processing
|
|
202
403
|
if (!targetObject[key] || typeof targetObject[key] !== "object") {
|
|
203
404
|
targetObject[key] = {};
|
|
204
405
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
406
|
+
nestedObjects.push({
|
|
407
|
+
key,
|
|
408
|
+
sourceObj: enValue as Record<string, unknown>,
|
|
409
|
+
targetObj: targetObject[key] as Record<string, unknown>,
|
|
209
410
|
currentPath,
|
|
210
|
-
|
|
211
|
-
onTranslate,
|
|
212
|
-
force
|
|
213
|
-
);
|
|
411
|
+
});
|
|
214
412
|
} else if (typeof enValue === "string") {
|
|
215
413
|
stats.totalCount++;
|
|
216
|
-
if (force || needsTranslation(targetValue
|
|
217
|
-
textsToTranslate.push({key, enValue, currentPath});
|
|
414
|
+
if (force || needsTranslation(targetValue)) {
|
|
415
|
+
textsToTranslate.push({ key, enValue, currentPath });
|
|
218
416
|
} else {
|
|
219
417
|
stats.skippedCount++;
|
|
220
418
|
}
|
|
221
419
|
}
|
|
222
420
|
}
|
|
223
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
|
|
224
436
|
if (textsToTranslate.length > 0) {
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const batch = textsToTranslate.slice(i, i + batchSize);
|
|
437
|
+
for (let i = 0; i < textsToTranslate.length; i += TRANSLATION_BATCH_SIZE) {
|
|
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) {
|
|
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
|
|