@umituz/web-localization 1.1.7 → 1.1.9

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.
@@ -1,29 +1,127 @@
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
  import { RateLimiter } from "../utils/rate-limit.util.js";
6
6
  import { shouldSkipWord, needsTranslation, isValidText, } from "../utils/text-validator.util.js";
7
- import { GOOGLE_TRANSLATE_API_URL, DEFAULT_MIN_DELAY, DEFAULT_TIMEOUT, } from "../constants/index.js";
7
+ import { GOOGLE_TRANSLATE_API_URL, DEFAULT_MIN_DELAY, DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES, TRANSLATION_BATCH_SIZE, TRANSLATION_CONCURRENCY_LIMIT, } from "../constants/index.js";
8
+ /**
9
+ * Object pool for reusing Map instances
10
+ */
11
+ class MapPool {
12
+ pool = [];
13
+ maxPoolSize = 10;
14
+ acquire() {
15
+ return this.pool.pop() || new Map();
16
+ }
17
+ release(map) {
18
+ if (map.size === 0 && this.pool.length < this.maxPoolSize) {
19
+ this.pool.push(map);
20
+ }
21
+ }
22
+ clear() {
23
+ this.pool.length = 0;
24
+ }
25
+ }
8
26
  class GoogleTranslateService {
9
27
  config = null;
10
- rateLimiter = null;
28
+ _rateLimiter = null;
29
+ // Translation cache with LRU-style eviction
30
+ translationCache = new Map();
31
+ maxCacheSize = 1000;
32
+ cacheTTL = 1000 * 60 * 60; // 1 hour
33
+ // Object pools
34
+ mapPool = new MapPool();
35
+ // Performance tracking
36
+ activeRequests = 0;
37
+ maxConcurrentRequests = 20;
11
38
  initialize(config) {
12
39
  this.config = {
13
40
  minDelay: DEFAULT_MIN_DELAY,
14
41
  timeout: DEFAULT_TIMEOUT,
15
42
  ...config,
16
43
  };
17
- this.rateLimiter = new RateLimiter(this.config.minDelay);
44
+ this._rateLimiter = new RateLimiter(this.config.minDelay);
18
45
  }
19
46
  isInitialized() {
20
- return this.config !== null && this.rateLimiter !== null;
47
+ return this.config !== null && this._rateLimiter !== null;
48
+ }
49
+ get rateLimiter() {
50
+ if (!this._rateLimiter) {
51
+ throw new Error("RateLimiter not initialized");
52
+ }
53
+ return this._rateLimiter;
21
54
  }
22
55
  ensureInitialized() {
23
56
  if (!this.isInitialized()) {
24
57
  throw new Error("GoogleTranslateService is not initialized. Call initialize() first.");
25
58
  }
26
59
  }
60
+ /**
61
+ * Generate cache key for translation
62
+ */
63
+ getCacheKey(text, targetLang, sourceLang) {
64
+ return `${sourceLang}|${targetLang}|${text}`;
65
+ }
66
+ /**
67
+ * Get translation from cache
68
+ */
69
+ getFromCache(text, targetLang, sourceLang) {
70
+ const key = this.getCacheKey(text, targetLang, sourceLang);
71
+ const entry = this.translationCache.get(key);
72
+ if (!entry)
73
+ return null;
74
+ // Check if entry has expired
75
+ const now = Date.now();
76
+ if (now - entry.timestamp > this.cacheTTL) {
77
+ this.translationCache.delete(key);
78
+ return null;
79
+ }
80
+ // Update access count and timestamp for LRU
81
+ entry.accessCount++;
82
+ entry.timestamp = now;
83
+ return entry.translation;
84
+ }
85
+ /**
86
+ * Store translation in cache with automatic eviction
87
+ */
88
+ storeInCache(text, targetLang, sourceLang, translation) {
89
+ // If cache is full, remove least recently used entries
90
+ if (this.translationCache.size >= this.maxCacheSize) {
91
+ let oldestKey = null;
92
+ let oldestTime = Infinity;
93
+ for (const [key, entry] of this.translationCache) {
94
+ if (entry.timestamp < oldestTime) {
95
+ oldestTime = entry.timestamp;
96
+ oldestKey = key;
97
+ }
98
+ }
99
+ if (oldestKey) {
100
+ this.translationCache.delete(oldestKey);
101
+ }
102
+ }
103
+ const key = this.getCacheKey(text, targetLang, sourceLang);
104
+ this.translationCache.set(key, {
105
+ translation,
106
+ timestamp: Date.now(),
107
+ accessCount: 1,
108
+ });
109
+ }
110
+ /**
111
+ * Clear translation cache
112
+ */
113
+ clearCache() {
114
+ this.translationCache.clear();
115
+ }
116
+ /**
117
+ * Get cache statistics
118
+ */
119
+ getCacheStats() {
120
+ return {
121
+ size: this.translationCache.size,
122
+ maxSize: this.maxCacheSize,
123
+ };
124
+ }
27
125
  async translate(request) {
28
126
  this.ensureInitialized();
29
127
  const { text, targetLanguage, sourceLanguage = "en" } = request;
@@ -46,9 +144,28 @@ class GoogleTranslateService {
46
144
  error: "Invalid target language",
47
145
  };
48
146
  }
49
- await this.rateLimiter.waitForSlot();
147
+ // Check cache first
148
+ const cachedTranslation = this.getFromCache(text, targetLanguage, sourceLanguage);
149
+ if (cachedTranslation !== null) {
150
+ return {
151
+ originalText: text,
152
+ translatedText: cachedTranslation,
153
+ sourceLanguage,
154
+ targetLanguage,
155
+ success: true,
156
+ cached: true,
157
+ };
158
+ }
159
+ // Wait for rate limit slot with normal priority
160
+ await this.rateLimiter.waitForSlot(5);
50
161
  try {
162
+ const startTime = Date.now();
51
163
  const translatedText = await this.callTranslateAPI(text, targetLanguage, sourceLanguage);
164
+ const responseTime = Date.now() - startTime;
165
+ // Record response time for dynamic rate adjustment
166
+ this.rateLimiter.recordResponseTime(responseTime);
167
+ // Store in cache
168
+ this.storeInCache(text, targetLanguage, sourceLanguage, translatedText);
52
169
  return {
53
170
  originalText: text,
54
171
  translatedText,
@@ -80,33 +197,65 @@ class GoogleTranslateService {
80
197
  if (!Array.isArray(requests) || requests.length === 0) {
81
198
  return stats;
82
199
  }
83
- // Process requests concurrently with controlled parallelism
84
- const concurrencyLimit = 10;
85
- const chunks = [];
86
- for (let i = 0; i < requests.length; i += concurrencyLimit) {
87
- chunks.push(requests.slice(i, i + concurrencyLimit));
88
- }
89
- for (const chunk of chunks) {
90
- const results = await Promise.all(chunk.map(async (request) => {
91
- await this.rateLimiter.waitForSlot();
92
- return this.callTranslateAPI(request.text, request.targetLanguage, request.sourceLanguage || "en");
93
- }));
94
- for (let i = 0; i < chunk.length; i++) {
95
- const request = chunk[i];
96
- const translatedText = results[i];
97
- if (translatedText && translatedText !== request.text) {
98
- stats.successCount++;
99
- stats.translatedKeys.push({
100
- key: request.text,
101
- from: request.text,
102
- to: translatedText,
103
- });
104
- }
105
- else if (!translatedText) {
106
- stats.failureCount++;
107
- }
108
- else {
109
- stats.skippedCount++;
200
+ // Filter out requests that can be served from cache
201
+ const uncachedRequests = [];
202
+ const cacheIndexMap = new Map(); // Maps original index to cached translation
203
+ for (let i = 0; i < requests.length; i++) {
204
+ const request = requests[i];
205
+ const cachedTranslation = this.getFromCache(request.text, request.targetLanguage, request.sourceLanguage || "en");
206
+ if (cachedTranslation !== null) {
207
+ // Serve from cache
208
+ cacheIndexMap.set(i, cachedTranslation);
209
+ stats.successCount++;
210
+ stats.translatedKeys.push({
211
+ key: request.text,
212
+ from: request.text,
213
+ to: cachedTranslation,
214
+ });
215
+ }
216
+ else {
217
+ uncachedRequests.push(request);
218
+ }
219
+ }
220
+ stats.skippedCount = cacheIndexMap.size;
221
+ // Process uncached requests with controlled parallelism
222
+ if (uncachedRequests.length > 0) {
223
+ const chunks = [];
224
+ for (let i = 0; i < uncachedRequests.length; i += TRANSLATION_CONCURRENCY_LIMIT) {
225
+ chunks.push(uncachedRequests.slice(i, i + TRANSLATION_CONCURRENCY_LIMIT));
226
+ }
227
+ for (const chunk of chunks) {
228
+ const results = await Promise.allSettled(chunk.map(async (request) => {
229
+ await this.rateLimiter.waitForSlot(5);
230
+ const startTime = Date.now();
231
+ const result = await this.callTranslateAPI(request.text, request.targetLanguage, request.sourceLanguage || "en");
232
+ const responseTime = Date.now() - startTime;
233
+ // Record response time for dynamic rate adjustment
234
+ this.rateLimiter.recordResponseTime(responseTime);
235
+ return result;
236
+ }));
237
+ for (let i = 0; i < chunk.length; i++) {
238
+ const request = chunk[i];
239
+ const result = results[i];
240
+ if (result.status === "fulfilled") {
241
+ const translatedText = result.value;
242
+ if (translatedText && translatedText !== request.text) {
243
+ stats.successCount++;
244
+ stats.translatedKeys.push({
245
+ key: request.text,
246
+ from: request.text,
247
+ to: translatedText,
248
+ });
249
+ // Cache the successful translation
250
+ this.storeInCache(request.text, request.targetLanguage, request.sourceLanguage || "en", translatedText);
251
+ }
252
+ else {
253
+ stats.skippedCount++;
254
+ }
255
+ }
256
+ else {
257
+ stats.failureCount++;
258
+ }
110
259
  }
111
260
  }
112
261
  }
@@ -125,21 +274,30 @@ class GoogleTranslateService {
125
274
  return;
126
275
  if (!targetLanguage || targetLanguage.trim().length === 0)
127
276
  return;
128
- const keys = Object.keys(sourceObject);
277
+ // First pass: collect all texts to translate (flattens nested structure)
129
278
  const textsToTranslate = [];
279
+ const nestedObjects = [];
280
+ // Collect texts and nested objects
281
+ const keys = Object.keys(sourceObject);
130
282
  for (const key of keys) {
131
283
  const enValue = sourceObject[key];
132
284
  const targetValue = targetObject[key];
133
285
  const currentPath = path ? `${path}.${key}` : key;
134
286
  if (typeof enValue === "object" && enValue !== null) {
287
+ // Prepare nested object for processing
135
288
  if (!targetObject[key] || typeof targetObject[key] !== "object") {
136
289
  targetObject[key] = {};
137
290
  }
138
- await this.translateObject(enValue, targetObject[key], targetLanguage, currentPath, stats, onTranslate, force);
291
+ nestedObjects.push({
292
+ key,
293
+ sourceObj: enValue,
294
+ targetObj: targetObject[key],
295
+ currentPath,
296
+ });
139
297
  }
140
298
  else if (typeof enValue === "string") {
141
299
  stats.totalCount++;
142
- if (force || needsTranslation(targetValue, enValue)) {
300
+ if (force || needsTranslation(targetValue)) {
143
301
  textsToTranslate.push({ key, enValue, currentPath });
144
302
  }
145
303
  else {
@@ -147,97 +305,155 @@ class GoogleTranslateService {
147
305
  }
148
306
  }
149
307
  }
308
+ // Process nested objects recursively
309
+ for (const nested of nestedObjects) {
310
+ await this.translateObject(nested.sourceObj, nested.targetObj, targetLanguage, nested.currentPath, stats, onTranslate, force);
311
+ }
312
+ // Process texts in batches
150
313
  if (textsToTranslate.length > 0) {
151
- const batchSize = 50;
152
- for (let i = 0; i < textsToTranslate.length; i += batchSize) {
153
- const batch = textsToTranslate.slice(i, i + batchSize);
154
- const results = await this.translateBatch(batch.map(item => ({
314
+ for (let i = 0; i < textsToTranslate.length; i += TRANSLATION_BATCH_SIZE) {
315
+ const batch = textsToTranslate.slice(i, i + TRANSLATION_BATCH_SIZE);
316
+ const results = await this.translateBatch(batch.map((item) => ({
155
317
  text: item.enValue,
156
318
  targetLanguage,
157
319
  })));
158
- let resultIndex = 0;
159
- for (let j = 0; j < batch.length; j++) {
160
- const { key, enValue, currentPath } = batch[j];
161
- const translatedItem = results.translatedKeys[resultIndex];
162
- if (translatedItem && translatedItem.from === enValue && translatedItem.to !== enValue) {
163
- targetObject[key] = translatedItem.to;
164
- stats.successCount++;
165
- if (onTranslate)
166
- onTranslate(currentPath, enValue, translatedItem.to);
167
- resultIndex++;
320
+ // Use object pool for map
321
+ const translationMap = this.mapPool.acquire();
322
+ try {
323
+ // Create a map for quick lookup of translations
324
+ for (const item of results.translatedKeys) {
325
+ translationMap.set(item.from, item.to);
168
326
  }
169
- else {
170
- stats.failureCount++;
327
+ for (let j = 0; j < batch.length; j++) {
328
+ const { key, enValue, currentPath } = batch[j];
329
+ const translatedText = translationMap.get(enValue);
330
+ if (translatedText && translatedText !== enValue) {
331
+ targetObject[key] = translatedText;
332
+ stats.successCount++;
333
+ if (onTranslate)
334
+ onTranslate(currentPath, enValue, translatedText);
335
+ }
336
+ else {
337
+ stats.failureCount++;
338
+ }
171
339
  }
172
340
  }
341
+ finally {
342
+ // Clear and release map back to pool
343
+ translationMap.clear();
344
+ this.mapPool.release(translationMap);
345
+ }
173
346
  }
174
347
  }
175
348
  }
176
- async callTranslateAPI(text, targetLanguage, sourceLanguage, retries = 3, backoffMs = 2000) {
177
- // 1. Variable Protection (Extract {{variables}})
178
- const varMap = new Map();
179
- let counter = 0;
180
- // Find all {{something}} patterns
181
- let safeText = text.replace(/\{\{([^}]+)\}\}/g, (match) => {
182
- const placeholder = `_VAR${counter}_`; // Using a simple token less likely to be split
183
- varMap.set(placeholder, match);
184
- counter++;
185
- return placeholder;
186
- });
187
- const timeout = this.config?.timeout || DEFAULT_TIMEOUT;
188
- const encodedText = encodeURIComponent(safeText);
189
- const url = `${GOOGLE_TRANSLATE_API_URL}?client=gtx&sl=${sourceLanguage}&tl=${targetLanguage}&dt=t&q=${encodedText}`;
190
- for (let attempt = 0; attempt <= retries; attempt++) {
191
- const controller = new AbortController();
192
- const timeoutId = setTimeout(() => controller.abort(), timeout);
349
+ async callTranslateAPI(text, targetLanguage, sourceLanguage, retries = DEFAULT_MAX_RETRIES, backoffMs = 2000) {
350
+ // Increment active requests counter
351
+ this.activeRequests++;
352
+ try {
353
+ // 1. Variable Protection (Extract {{variables}})
354
+ // Use object pool for map to reduce GC pressure
355
+ const varMap = this.mapPool.acquire();
356
+ let counter = 0;
193
357
  try {
194
- const response = await fetch(url, {
195
- signal: controller.signal,
358
+ // Find all {{something}} patterns
359
+ // Use more specific pattern to avoid false matches
360
+ const safeText = text.replace(/\{\{([^}]+)\}\}/g, (match) => {
361
+ const placeholder = `__VAR${counter}__`;
362
+ varMap.set(placeholder, match);
363
+ counter++;
364
+ return placeholder;
196
365
  });
197
- if (!response.ok) {
198
- if (response.status === 429 || response.status >= 500) {
199
- if (attempt < retries) {
200
- clearTimeout(timeoutId);
201
- // Exponential backoff
202
- const delay = backoffMs * Math.pow(2, attempt);
203
- await new Promise(resolve => setTimeout(resolve, delay));
204
- continue;
366
+ const timeout = this.config?.timeout || DEFAULT_TIMEOUT;
367
+ const encodedText = encodeURIComponent(safeText);
368
+ const url = `${GOOGLE_TRANSLATE_API_URL}?client=gtx&sl=${sourceLanguage}&tl=${targetLanguage}&dt=t&q=${encodedText}`;
369
+ for (let attempt = 0; attempt < retries; attempt++) {
370
+ const controller = new AbortController();
371
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
372
+ try {
373
+ const response = await fetch(url, {
374
+ signal: controller.signal,
375
+ });
376
+ if (!response.ok) {
377
+ if (response.status === 429 || response.status >= 500) {
378
+ if (attempt < retries - 1) {
379
+ clearTimeout(timeoutId);
380
+ // Exponential backoff
381
+ const delay = backoffMs * Math.pow(2, attempt);
382
+ await this.sleep(delay);
383
+ continue;
384
+ }
385
+ }
386
+ throw new Error(`API request failed: ${response.status}`);
387
+ }
388
+ const data = await response.json();
389
+ let translatedStr = safeText;
390
+ if (Array.isArray(data) &&
391
+ data.length > 0 &&
392
+ Array.isArray(data[0]) &&
393
+ data[0].length > 0 &&
394
+ typeof data[0][0][0] === "string") {
395
+ translatedStr = data[0].map((item) => item[0]).join('');
205
396
  }
397
+ // 2. Re-inject Variables
398
+ if (varMap.size > 0) {
399
+ // Sometimes Google adds spaces, like __VAR0__ -> __ VAR0 __
400
+ // Use pre-compiled regex patterns for better performance
401
+ for (const [placeholder, originalVar] of varMap.entries()) {
402
+ // Escape special regex characters in placeholder
403
+ const escapedPlaceholder = placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
404
+ // Allow optional spaces between each character
405
+ const regex = new RegExp(escapedPlaceholder.split('').join('\\s*'), 'g');
406
+ translatedStr = translatedStr.replace(regex, originalVar);
407
+ }
408
+ }
409
+ return translatedStr;
206
410
  }
207
- throw new Error(`API request failed: ${response.status}`);
208
- }
209
- const data = await response.json();
210
- let translatedStr = safeText;
211
- if (Array.isArray(data) &&
212
- data.length > 0 &&
213
- Array.isArray(data[0]) &&
214
- data[0].length > 0 &&
215
- typeof data[0][0][0] === "string") {
216
- translatedStr = data[0].map((item) => item[0]).join('');
217
- }
218
- // 2. Re-inject Variables
219
- if (varMap.size > 0) {
220
- // Sometimes Google adds spaces, like _VAR0_ -> _ VAR0 _
221
- for (const [placeholder, originalVar] of varMap.entries()) {
222
- const regex = new RegExp(placeholder.split('').join('\\s*'), 'g');
223
- translatedStr = translatedStr.replace(regex, originalVar);
411
+ catch (error) {
412
+ clearTimeout(timeoutId);
413
+ if (attempt === retries - 1) {
414
+ throw error;
415
+ }
416
+ const delay = backoffMs * Math.pow(2, attempt);
417
+ await this.sleep(delay);
418
+ }
419
+ finally {
420
+ clearTimeout(timeoutId);
224
421
  }
225
422
  }
226
- return translatedStr;
227
- }
228
- catch (error) {
229
- clearTimeout(timeoutId);
230
- if (attempt === retries) {
231
- throw error;
232
- }
233
- const delay = backoffMs * Math.pow(2, attempt);
234
- await new Promise(resolve => setTimeout(resolve, delay));
423
+ return text;
235
424
  }
236
425
  finally {
237
- clearTimeout(timeoutId);
426
+ // Clear and release map back to pool
427
+ varMap.clear();
428
+ this.mapPool.release(varMap);
238
429
  }
239
430
  }
240
- return text;
431
+ finally {
432
+ // Decrement active requests counter
433
+ this.activeRequests--;
434
+ }
435
+ }
436
+ /**
437
+ * Optimized sleep function
438
+ */
439
+ sleep(ms) {
440
+ return new Promise((resolve) => setTimeout(resolve, ms));
441
+ }
442
+ /**
443
+ * Get number of active requests
444
+ */
445
+ getActiveRequestCount() {
446
+ return this.activeRequests;
447
+ }
448
+ /**
449
+ * Clean up resources
450
+ */
451
+ dispose() {
452
+ this.clearCache();
453
+ this.mapPool.clear();
454
+ if (this._rateLimiter) {
455
+ this._rateLimiter.clear();
456
+ }
241
457
  }
242
458
  }
243
459
  export const googleTranslateService = new GoogleTranslateService();
@@ -1,9 +1,35 @@
1
1
  /**
2
2
  * Parses a TypeScript file containing an object export
3
- * @description Simplistic parser for 'export default { ... }' or 'export const data = { ... }'
3
+ * @description Improved parser with caching and better error handling
4
+ * @security Note: Uses Function constructor - only use with trusted local files
4
5
  */
5
6
  export declare function parseTypeScriptFile(filePath: string): Record<string, unknown>;
6
7
  /**
7
8
  * Generates a TypeScript file content from an object
9
+ * @description Optimized content generation with proper escaping
8
10
  */
9
11
  export declare function generateTypeScriptContent(obj: Record<string, unknown>, langCode?: string): string;
12
+ /**
13
+ * Clear file content cache
14
+ * @description Call this when you know files have been modified externally
15
+ */
16
+ export declare function clearFileCache(): void;
17
+ /**
18
+ * Get cache statistics
19
+ */
20
+ export declare function getFileCacheStats(): {
21
+ size: number;
22
+ maxSize: number;
23
+ };
24
+ /**
25
+ * Check if a file exists (async version for better performance)
26
+ */
27
+ export declare function fileExists(filePath: string): Promise<boolean>;
28
+ /**
29
+ * Read file content asynchronously (for better performance in non-blocking scenarios)
30
+ */
31
+ export declare function readFileAsync(filePath: string): Promise<string | null>;
32
+ /**
33
+ * Write file content asynchronously (for better performance in non-blocking scenarios)
34
+ */
35
+ export declare function writeFileAsync(filePath: string, content: string): Promise<void>;