@umituz/web-localization 1.0.6 → 1.1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"cli.service.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/services/cli.service.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,UAAU;IACf,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAsD9C,SAAS,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;CAsD1D;AAED,eAAO,MAAM,UAAU,YAAmB,CAAC"}
1
+ {"version":3,"file":"cli.service.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/services/cli.service.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,UAAU;IACf,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IA0D9C,SAAS,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;CAsD1D;AAED,eAAO,MAAM,UAAU,YAAmB,CAAC"}
@@ -34,7 +34,8 @@ export class CLIService {
34
34
  result[key] = syncObject(source[key], target[key] || {});
35
35
  }
36
36
  else if (target[key] === undefined) {
37
- result[key] = source[key];
37
+ // Let empty string indicate untranslated state
38
+ result[key] = typeof source[key] === "string" ? "" : source[key];
38
39
  }
39
40
  }
40
41
  // Remove extra keys
@@ -1 +1 @@
1
- {"version":3,"file":"google-translate.service.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/services/google-translate.service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EACV,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,0DAA0D,CAAC;AAalE,cAAM,sBAAuB,YAAW,mBAAmB;IACzD,OAAO,CAAC,MAAM,CAAyC;IACvD,OAAO,CAAC,WAAW,CAA4B;IAE/C,UAAU,CAAC,MAAM,EAAE,wBAAwB,GAAG,IAAI;IASlD,aAAa,IAAI,OAAO;IAIxB,OAAO,CAAC,iBAAiB;IAQnB,SAAS,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAsDpE,cAAc,CAAC,QAAQ,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAyDzE,eAAe,CACnB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,cAAc,EAAE,MAAM,EACtB,IAAI,SAAK,EACT,KAAK,GAAE,gBAMN,EACD,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,GAC5D,OAAO,CAAC,IAAI,CAAC;YA6DF,gBAAgB;CAuC/B;AAED,eAAO,MAAM,sBAAsB,wBAA+B,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
1
+ {"version":3,"file":"google-translate.service.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/services/google-translate.service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,6CAA6C,CAAC;AACrD,OAAO,KAAK,EACV,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,0DAA0D,CAAC;AAalE,cAAM,sBAAuB,YAAW,mBAAmB;IACzD,OAAO,CAAC,MAAM,CAAyC;IACvD,OAAO,CAAC,WAAW,CAA4B;IAE/C,UAAU,CAAC,MAAM,EAAE,wBAAwB,GAAG,IAAI;IASlD,aAAa,IAAI,OAAO;IAIxB,OAAO,CAAC,iBAAiB;IAQnB,SAAS,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAsDpE,cAAc,CAAC,QAAQ,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAyDzE,eAAe,CACnB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,cAAc,EAAE,MAAM,EACtB,IAAI,SAAK,EACT,KAAK,GAAE,gBAMN,EACD,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,GAC5D,OAAO,CAAC,IAAI,CAAC;YA6DF,gBAAgB;CAkF/B;AAED,eAAO,MAAM,sBAAsB,wBAA+B,CAAC;AACnE,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
@@ -169,33 +169,71 @@ class GoogleTranslateService {
169
169
  }
170
170
  }
171
171
  }
172
- async callTranslateAPI(text, targetLanguage, sourceLanguage) {
172
+ async callTranslateAPI(text, targetLanguage, sourceLanguage, retries = 3, backoffMs = 2000) {
173
+ // 1. Variable Protection (Extract {{variables}})
174
+ const varMap = new Map();
175
+ let counter = 0;
176
+ // Find all {{something}} patterns
177
+ let safeText = text.replace(/\{\{([^}]+)\}\}/g, (match) => {
178
+ const placeholder = `_VAR${counter}_`; // Using a simple token less likely to be split
179
+ varMap.set(placeholder, match);
180
+ counter++;
181
+ return placeholder;
182
+ });
173
183
  const timeout = this.config?.timeout || DEFAULT_TIMEOUT;
174
- const encodedText = encodeURIComponent(text);
184
+ const encodedText = encodeURIComponent(safeText);
175
185
  const url = `${GOOGLE_TRANSLATE_API_URL}?client=gtx&sl=${sourceLanguage}&tl=${targetLanguage}&dt=t&q=${encodedText}`;
176
- const controller = new AbortController();
177
- const timeoutId = setTimeout(() => controller.abort(), timeout);
178
- try {
179
- const response = await fetch(url, {
180
- signal: controller.signal,
181
- });
182
- if (!response.ok) {
183
- throw new Error(`API request failed: ${response.status}`);
186
+ for (let attempt = 0; attempt <= retries; attempt++) {
187
+ const controller = new AbortController();
188
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
189
+ try {
190
+ const response = await fetch(url, {
191
+ signal: controller.signal,
192
+ });
193
+ if (!response.ok) {
194
+ if (response.status === 429 || response.status >= 500) {
195
+ if (attempt < retries) {
196
+ clearTimeout(timeoutId);
197
+ // Exponential backoff
198
+ const delay = backoffMs * Math.pow(2, attempt);
199
+ await new Promise(resolve => setTimeout(resolve, delay));
200
+ continue;
201
+ }
202
+ }
203
+ throw new Error(`API request failed: ${response.status}`);
204
+ }
205
+ const data = await response.json();
206
+ let translatedStr = safeText;
207
+ if (Array.isArray(data) &&
208
+ data.length > 0 &&
209
+ Array.isArray(data[0]) &&
210
+ data[0].length > 0 &&
211
+ typeof data[0][0][0] === "string") {
212
+ translatedStr = data[0].map((item) => item[0]).join('');
213
+ }
214
+ // 2. Re-inject Variables
215
+ if (varMap.size > 0) {
216
+ // Sometimes Google adds spaces, like _VAR0_ -> _ VAR0 _
217
+ for (const [placeholder, originalVar] of varMap.entries()) {
218
+ const regex = new RegExp(placeholder.split('').join('\\s*'), 'g');
219
+ translatedStr = translatedStr.replace(regex, originalVar);
220
+ }
221
+ }
222
+ return translatedStr;
223
+ }
224
+ catch (error) {
225
+ clearTimeout(timeoutId);
226
+ if (attempt === retries) {
227
+ throw error;
228
+ }
229
+ const delay = backoffMs * Math.pow(2, attempt);
230
+ await new Promise(resolve => setTimeout(resolve, delay));
184
231
  }
185
- const data = await response.json();
186
- if (Array.isArray(data) &&
187
- data.length > 0 &&
188
- Array.isArray(data[0]) &&
189
- data[0].length > 0 &&
190
- Array.isArray(data[0][0]) &&
191
- typeof data[0][0][0] === "string") {
192
- return data[0][0][0];
232
+ finally {
233
+ clearTimeout(timeoutId);
193
234
  }
194
- return text;
195
- }
196
- finally {
197
- clearTimeout(timeoutId);
198
235
  }
236
+ return text;
199
237
  }
200
238
  }
201
239
  export const googleTranslateService = new GoogleTranslateService();
@@ -1 +1 @@
1
- {"version":3,"file":"file.util.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/utils/file.util.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoB7E;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAajG"}
1
+ {"version":3,"file":"file.util.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/utils/file.util.ts"],"names":[],"mappings":"AAEA;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoB7E;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAajG"}
@@ -26,9 +26,8 @@ export function shouldSkipWord(text) {
26
26
  export function needsTranslation(targetValue, sourceValue) {
27
27
  if (typeof targetValue !== "string")
28
28
  return true;
29
- if (targetValue.trim().length === 0)
30
- return true;
31
- if (targetValue === sourceValue && !/^\d+$/.test(sourceValue))
32
- return true;
29
+ if (targetValue.length === 0)
30
+ return true; // Empty string means untranslated
31
+ // Do NOT return true if target === source anymore, to avoid infinite translations for words that are identical in both languages
33
32
  return false;
34
33
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/web-localization",
3
- "version": "1.0.6",
3
+ "version": "1.1.0",
4
4
  "description": "Google Translate integrated localization package for web applications",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -35,16 +35,17 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "commander": "^12.0.0",
39
38
  "chalk": "^5.3.0",
40
- "dotenv": "^16.4.5"
39
+ "commander": "^12.0.0",
40
+ "dotenv": "^16.4.5",
41
+ "ts-morph": "^27.0.2"
41
42
  },
42
43
  "devDependencies": {
43
44
  "@types/node": "^20.12.7",
44
- "typescript": "^5.4.5",
45
- "eslint": "^8.57.0",
45
+ "@typescript-eslint/eslint-plugin": "^7.7.0",
46
46
  "@typescript-eslint/parser": "^7.7.0",
47
- "@typescript-eslint/eslint-plugin": "^7.7.0"
47
+ "eslint": "^8.57.0",
48
+ "typescript": "^5.4.5"
48
49
  },
49
50
  "files": [
50
51
  "src",
@@ -52,7 +52,8 @@ export class CLIService {
52
52
  (target[key] as Record<string, unknown>) || {}
53
53
  );
54
54
  } else if (target[key] === undefined) {
55
- result[key] = source[key];
55
+ // Let empty string indicate untranslated state
56
+ result[key] = typeof source[key] === "string" ? "" : source[key];
56
57
  }
57
58
  }
58
59
  // Remove extra keys
@@ -237,41 +237,84 @@ class GoogleTranslateService implements ITranslationService {
237
237
  private async callTranslateAPI(
238
238
  text: string,
239
239
  targetLanguage: string,
240
- sourceLanguage: string
240
+ sourceLanguage: string,
241
+ retries = 3,
242
+ backoffMs = 2000
241
243
  ): Promise<string> {
244
+ // 1. Variable Protection (Extract {{variables}})
245
+ const varMap = new Map<string, string>();
246
+ let counter = 0;
247
+
248
+ // Find all {{something}} patterns
249
+ let safeText = text.replace(/\{\{([^}]+)\}\}/g, (match) => {
250
+ const placeholder = `_VAR${counter}_`; // Using a simple token less likely to be split
251
+ varMap.set(placeholder, match);
252
+ counter++;
253
+ return placeholder;
254
+ });
255
+
242
256
  const timeout = this.config?.timeout || DEFAULT_TIMEOUT;
243
- const encodedText = encodeURIComponent(text);
257
+ const encodedText = encodeURIComponent(safeText);
244
258
  const url = `${GOOGLE_TRANSLATE_API_URL}?client=gtx&sl=${sourceLanguage}&tl=${targetLanguage}&dt=t&q=${encodedText}`;
245
259
 
246
- const controller = new AbortController();
247
- const timeoutId = setTimeout(() => controller.abort(), timeout);
260
+ for (let attempt = 0; attempt <= retries; attempt++) {
261
+ const controller = new AbortController();
262
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
263
+
264
+ try {
265
+ const response = await fetch(url, {
266
+ signal: controller.signal,
267
+ });
268
+
269
+ if (!response.ok) {
270
+ if (response.status === 429 || response.status >= 500) {
271
+ if (attempt < retries) {
272
+ clearTimeout(timeoutId);
273
+ // Exponential backoff
274
+ const delay = backoffMs * Math.pow(2, attempt);
275
+ await new Promise(resolve => setTimeout(resolve, delay));
276
+ continue;
277
+ }
278
+ }
279
+ throw new Error(`API request failed: ${response.status}`);
280
+ }
248
281
 
249
- try {
250
- const response = await fetch(url, {
251
- signal: controller.signal,
252
- });
282
+ const data = await response.json();
253
283
 
254
- if (!response.ok) {
255
- throw new Error(`API request failed: ${response.status}`);
256
- }
284
+ let translatedStr = safeText;
285
+ if (
286
+ Array.isArray(data) &&
287
+ data.length > 0 &&
288
+ Array.isArray(data[0]) &&
289
+ data[0].length > 0 &&
290
+ typeof data[0][0][0] === "string"
291
+ ) {
292
+ translatedStr = data[0].map((item: any) => item[0]).join('');
293
+ }
257
294
 
258
- const data = await response.json();
295
+ // 2. Re-inject Variables
296
+ if (varMap.size > 0) {
297
+ // Sometimes Google adds spaces, like _VAR0_ -> _ VAR0 _
298
+ for (const [placeholder, originalVar] of varMap.entries()) {
299
+ const regex = new RegExp(placeholder.split('').join('\\s*'), 'g');
300
+ translatedStr = translatedStr.replace(regex, originalVar);
301
+ }
302
+ }
259
303
 
260
- if (
261
- Array.isArray(data) &&
262
- data.length > 0 &&
263
- Array.isArray(data[0]) &&
264
- data[0].length > 0 &&
265
- Array.isArray(data[0][0]) &&
266
- typeof data[0][0][0] === "string"
267
- ) {
268
- return data[0][0][0];
304
+ return translatedStr;
305
+ } catch (error) {
306
+ clearTimeout(timeoutId);
307
+ if (attempt === retries) {
308
+ throw error;
309
+ }
310
+ const delay = backoffMs * Math.pow(2, attempt);
311
+ await new Promise(resolve => setTimeout(resolve, delay));
312
+ } finally {
313
+ clearTimeout(timeoutId);
269
314
  }
270
-
271
- return text;
272
- } finally {
273
- clearTimeout(timeoutId);
274
315
  }
316
+
317
+ return text;
275
318
  }
276
319
  }
277
320
 
@@ -25,7 +25,7 @@ export function shouldSkipWord(text: string): boolean {
25
25
  */
26
26
  export function needsTranslation(targetValue: unknown, sourceValue: string): boolean {
27
27
  if (typeof targetValue !== "string") return true;
28
- if (targetValue.trim().length === 0) return true;
29
- if (targetValue === sourceValue && !/^\d+$/.test(sourceValue)) return true;
28
+ if (targetValue.length === 0) return true; // Empty string means untranslated
29
+ // Do NOT return true if target === source anymore, to avoid infinite translations for words that are identical in both languages
30
30
  return false;
31
31
  }