@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.
- package/dist/infrastructure/services/cli.service.d.ts.map +1 -1
- package/dist/infrastructure/services/cli.service.js +2 -1
- package/dist/infrastructure/services/google-translate.service.d.ts.map +1 -1
- package/dist/infrastructure/services/google-translate.service.js +60 -22
- package/dist/infrastructure/utils/file.util.d.ts.map +1 -1
- package/dist/infrastructure/utils/text-validator.util.js +3 -4
- package/package.json +7 -6
- package/src/infrastructure/services/cli.service.ts +2 -1
- package/src/infrastructure/services/google-translate.service.ts +68 -25
- package/src/infrastructure/utils/text-validator.util.ts +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.service.d.ts","sourceRoot":"","sources":["../../../src/infrastructure/services/cli.service.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
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;
|
|
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(
|
|
184
|
+
const encodedText = encodeURIComponent(safeText);
|
|
175
185
|
const url = `${GOOGLE_TRANSLATE_API_URL}?client=gtx&sl=${sourceLanguage}&tl=${targetLanguage}&dt=t&q=${encodedText}`;
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
186
|
-
|
|
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":"
|
|
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.
|
|
30
|
-
return true;
|
|
31
|
-
if
|
|
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
|
|
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
|
-
"
|
|
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": "^
|
|
45
|
-
"eslint": "^8.57.0",
|
|
45
|
+
"@typescript-eslint/eslint-plugin": "^7.7.0",
|
|
46
46
|
"@typescript-eslint/parser": "^7.7.0",
|
|
47
|
-
"
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
247
|
-
|
|
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
|
-
|
|
250
|
-
const response = await fetch(url, {
|
|
251
|
-
signal: controller.signal,
|
|
252
|
-
});
|
|
282
|
+
const data = await response.json();
|
|
253
283
|
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
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.
|
|
29
|
-
if
|
|
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
|
}
|