@umituz/web-localization 1.0.5 → 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 +5 -4
- package/dist/infrastructure/services/google-translate.service.d.ts +2 -2
- package/dist/infrastructure/services/google-translate.service.d.ts.map +1 -1
- package/dist/infrastructure/services/google-translate.service.js +63 -25
- package/dist/infrastructure/utils/file.util.d.ts.map +1 -1
- package/dist/infrastructure/utils/text-validator.util.js +3 -4
- package/dist/scripts/cli.js +1 -1
- package/package.json +7 -6
- package/src/infrastructure/services/cli.service.ts +10 -7
- package/src/infrastructure/services/google-translate.service.ts +73 -30
- package/src/infrastructure/utils/file.util.ts +0 -1
- package/src/infrastructure/utils/text-validator.util.ts +2 -2
- package/src/scripts/cli.ts +1 -1
- package/src/domain/entities/translation.entity.d.ts +0 -30
- package/src/domain/entities/translation.entity.d.ts.map +0 -1
- package/src/domain/entities/translation.entity.js +0 -5
- package/src/domain/interfaces/translation-service.interface.d.ts +0 -21
- package/src/domain/interfaces/translation-service.interface.d.ts.map +0 -1
- package/src/domain/interfaces/translation-service.interface.js +0 -1
- package/src/index.d.ts +0 -11
- package/src/index.d.ts.map +0 -1
- package/src/index.js +0 -10
- package/src/infrastructure/constants/index.d.ts +0 -11
- package/src/infrastructure/constants/index.d.ts.map +0 -1
- package/src/infrastructure/constants/index.js +0 -10
- package/src/infrastructure/services/cli.service.d.ts +0 -11
- package/src/infrastructure/services/cli.service.d.ts.map +0 -1
- package/src/infrastructure/services/cli.service.js +0 -95
- package/src/infrastructure/services/google-translate.service.d.ts +0 -20
- package/src/infrastructure/services/google-translate.service.d.ts.map +0 -1
- package/src/infrastructure/services/google-translate.service.js +0 -202
- package/src/infrastructure/utils/file.util.d.ts +0 -10
- package/src/infrastructure/utils/file.util.d.ts.map +0 -1
- package/src/infrastructure/utils/file.util.js +0 -41
- package/src/infrastructure/utils/rate-limit.util.d.ts +0 -11
- package/src/infrastructure/utils/rate-limit.util.d.ts.map +0 -1
- package/src/infrastructure/utils/rate-limit.util.js +0 -20
- package/src/infrastructure/utils/text-validator.util.d.ts +0 -16
- package/src/infrastructure/utils/text-validator.util.d.ts.map +0 -1
- package/src/infrastructure/utils/text-validator.util.js +0 -34
- package/src/scripts/cli.d.ts +0 -6
- package/src/scripts/cli.d.ts.map +0 -1
- package/src/scripts/cli.js +0 -41
|
@@ -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"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { googleTranslateService } from "./google-translate.service";
|
|
5
|
-
import { parseTypeScriptFile, generateTypeScriptContent } from "../utils/file.util";
|
|
6
|
-
import { DEFAULT_LOCALES_DIR, DEFAULT_BASE_LANGUAGE } from "../constants";
|
|
4
|
+
import { googleTranslateService } from "./google-translate.service.js";
|
|
5
|
+
import { parseTypeScriptFile, generateTypeScriptContent } from "../utils/file.util.js";
|
|
6
|
+
import { DEFAULT_LOCALES_DIR, DEFAULT_BASE_LANGUAGE } from "../constants/index.js";
|
|
7
7
|
export class CLIService {
|
|
8
8
|
async sync(options = {}) {
|
|
9
9
|
const localesDir = path.resolve(process.cwd(), options.localesDir || DEFAULT_LOCALES_DIR);
|
|
@@ -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
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Google Translate Service
|
|
3
3
|
* @description Main translation service using Google Translate API
|
|
4
4
|
*/
|
|
5
|
-
import type { TranslationRequest, TranslationResponse, TranslationStats } from "../../domain/entities/translation.entity";
|
|
6
|
-
import type { ITranslationService, TranslationServiceConfig } from "../../domain/interfaces/translation-service.interface";
|
|
5
|
+
import type { TranslationRequest, TranslationResponse, TranslationStats } from "../../domain/entities/translation.entity.js";
|
|
6
|
+
import type { ITranslationService, TranslationServiceConfig } from "../../domain/interfaces/translation-service.interface.js";
|
|
7
7
|
declare class GoogleTranslateService implements ITranslationService {
|
|
8
8
|
private config;
|
|
9
9
|
private rateLimiter;
|
|
@@ -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,
|
|
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"}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Google Translate Service
|
|
3
3
|
* @description Main translation service using Google Translate API
|
|
4
4
|
*/
|
|
5
|
-
import { RateLimiter } from "../utils/rate-limit.util";
|
|
6
|
-
import { shouldSkipWord, needsTranslation, isValidText, } from "../utils/text-validator.util";
|
|
7
|
-
import { GOOGLE_TRANSLATE_API_URL, DEFAULT_MIN_DELAY, DEFAULT_TIMEOUT, } from "../constants";
|
|
5
|
+
import { RateLimiter } from "../utils/rate-limit.util.js";
|
|
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";
|
|
8
8
|
class GoogleTranslateService {
|
|
9
9
|
config = null;
|
|
10
10
|
rateLimiter = null;
|
|
@@ -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/dist/scripts/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* CLI Tool for @umituz/web-localization
|
|
4
4
|
*/
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
import { cliService } from "../infrastructure/services/cli.service";
|
|
6
|
+
import { cliService } from "../infrastructure/services/cli.service.js";
|
|
7
7
|
import chalk from "chalk";
|
|
8
8
|
const program = new Command();
|
|
9
9
|
program
|
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",
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import { googleTranslateService } from "./google-translate.service";
|
|
5
|
-
import { parseTypeScriptFile, generateTypeScriptContent } from "../utils/file.util";
|
|
4
|
+
import { googleTranslateService } from "./google-translate.service.js";
|
|
5
|
+
import { parseTypeScriptFile, generateTypeScriptContent } from "../utils/file.util.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_LOCALES_DIR,
|
|
8
|
-
DEFAULT_SOURCE_DIR,
|
|
9
8
|
DEFAULT_BASE_LANGUAGE
|
|
10
|
-
} from "../constants";
|
|
9
|
+
} from "../constants/index.js";
|
|
11
10
|
|
|
12
11
|
export interface SyncOptions {
|
|
13
12
|
localesDir?: string;
|
|
@@ -44,13 +43,17 @@ export class CLIService {
|
|
|
44
43
|
const langCode = file.replace(".ts", "");
|
|
45
44
|
|
|
46
45
|
// Deep merge with base data structure
|
|
47
|
-
const syncObject = (source:
|
|
46
|
+
const syncObject = (source: Record<string, unknown>, target: Record<string, unknown>): Record<string, unknown> => {
|
|
48
47
|
const result = { ...target };
|
|
49
48
|
for (const key in source) {
|
|
50
49
|
if (typeof source[key] === "object" && source[key] !== null) {
|
|
51
|
-
result[key] = syncObject(
|
|
50
|
+
result[key] = syncObject(
|
|
51
|
+
source[key] as Record<string, unknown>,
|
|
52
|
+
(target[key] as Record<string, unknown>) || {}
|
|
53
|
+
);
|
|
52
54
|
} else if (target[key] === undefined) {
|
|
53
|
-
|
|
55
|
+
// Let empty string indicate untranslated state
|
|
56
|
+
result[key] = typeof source[key] === "string" ? "" : source[key];
|
|
54
57
|
}
|
|
55
58
|
}
|
|
56
59
|
// Remove extra keys
|
|
@@ -7,22 +7,22 @@ import type {
|
|
|
7
7
|
TranslationRequest,
|
|
8
8
|
TranslationResponse,
|
|
9
9
|
TranslationStats,
|
|
10
|
-
} from "../../domain/entities/translation.entity";
|
|
10
|
+
} from "../../domain/entities/translation.entity.js";
|
|
11
11
|
import type {
|
|
12
12
|
ITranslationService,
|
|
13
13
|
TranslationServiceConfig,
|
|
14
|
-
} from "../../domain/interfaces/translation-service.interface";
|
|
15
|
-
import { RateLimiter } from "../utils/rate-limit.util";
|
|
14
|
+
} from "../../domain/interfaces/translation-service.interface.js";
|
|
15
|
+
import { RateLimiter } from "../utils/rate-limit.util.js";
|
|
16
16
|
import {
|
|
17
17
|
shouldSkipWord,
|
|
18
18
|
needsTranslation,
|
|
19
19
|
isValidText,
|
|
20
|
-
} from "../utils/text-validator.util";
|
|
20
|
+
} from "../utils/text-validator.util.js";
|
|
21
21
|
import {
|
|
22
22
|
GOOGLE_TRANSLATE_API_URL,
|
|
23
23
|
DEFAULT_MIN_DELAY,
|
|
24
24
|
DEFAULT_TIMEOUT,
|
|
25
|
-
} from "../constants";
|
|
25
|
+
} from "../constants/index.js";
|
|
26
26
|
|
|
27
27
|
class GoogleTranslateService implements ITranslationService {
|
|
28
28
|
private config: TranslationServiceConfig | null = null;
|
|
@@ -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
|
}
|
package/src/scripts/cli.ts
CHANGED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Translation Entity
|
|
3
|
-
* @description Data structures for translation requests and responses
|
|
4
|
-
*/
|
|
5
|
-
export interface TranslationRequest {
|
|
6
|
-
readonly text: string;
|
|
7
|
-
readonly targetLanguage: string;
|
|
8
|
-
readonly sourceLanguage?: string;
|
|
9
|
-
}
|
|
10
|
-
export interface TranslationResponse {
|
|
11
|
-
readonly originalText: string;
|
|
12
|
-
readonly translatedText: string;
|
|
13
|
-
readonly sourceLanguage: string;
|
|
14
|
-
readonly targetLanguage: string;
|
|
15
|
-
readonly success: boolean;
|
|
16
|
-
readonly error?: string;
|
|
17
|
-
}
|
|
18
|
-
export interface TranslationItem {
|
|
19
|
-
readonly key: string;
|
|
20
|
-
readonly from: string;
|
|
21
|
-
readonly to: string;
|
|
22
|
-
}
|
|
23
|
-
export interface TranslationStats {
|
|
24
|
-
totalCount: number;
|
|
25
|
-
successCount: number;
|
|
26
|
-
failureCount: number;
|
|
27
|
-
skippedCount: number;
|
|
28
|
-
translatedKeys: TranslationItem[];
|
|
29
|
-
}
|
|
30
|
-
//# sourceMappingURL=translation.entity.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"translation.entity.d.ts","sourceRoot":"","sources":["translation.entity.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,cAAc,EAAE,eAAe,EAAE,CAAC;CACnC"}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import type { TranslationRequest, TranslationResponse, TranslationStats } from "../entities/translation.entity";
|
|
2
|
-
/**
|
|
3
|
-
* Translation Service Config
|
|
4
|
-
*/
|
|
5
|
-
export interface TranslationServiceConfig {
|
|
6
|
-
minDelay?: number;
|
|
7
|
-
maxRetries?: number;
|
|
8
|
-
timeout?: number;
|
|
9
|
-
apiKey?: string;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Translation Service Interface
|
|
13
|
-
*/
|
|
14
|
-
export interface ITranslationService {
|
|
15
|
-
initialize(config: TranslationServiceConfig): void;
|
|
16
|
-
isInitialized(): boolean;
|
|
17
|
-
translate(request: TranslationRequest): Promise<TranslationResponse>;
|
|
18
|
-
translateBatch(requests: TranslationRequest[]): Promise<TranslationStats>;
|
|
19
|
-
translateObject(sourceObject: Record<string, unknown>, targetObject: Record<string, unknown>, targetLanguage: string, path?: string, stats?: TranslationStats, onTranslate?: (key: string, from: string, to: string) => void): Promise<void>;
|
|
20
|
-
}
|
|
21
|
-
//# sourceMappingURL=translation-service.interface.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"translation-service.interface.d.ts","sourceRoot":"","sources":["translation-service.interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,gCAAgC,CAAC;AAExC;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,UAAU,CAAC,MAAM,EAAE,wBAAwB,GAAG,IAAI,CAAC;IACnD,aAAa,IAAI,OAAO,CAAC;IACzB,SAAS,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACrE,cAAc,CAAC,QAAQ,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAC1E,eAAe,CACb,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACrC,cAAc,EAAE,MAAM,EACtB,IAAI,CAAC,EAAE,MAAM,EACb,KAAK,CAAC,EAAE,gBAAgB,EACxB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,GAC5D,OAAO,CAAC,IAAI,CAAC,CAAC;CAClB"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
package/src/index.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @umituz/web-localization
|
|
3
|
-
* Google Translate integrated localization package for web applications
|
|
4
|
-
*/
|
|
5
|
-
export * from "./domain/entities/translation.entity";
|
|
6
|
-
export * from "./domain/interfaces/translation-service.interface";
|
|
7
|
-
export * from "./infrastructure/services/google-translate.service";
|
|
8
|
-
export * from "./infrastructure/constants";
|
|
9
|
-
export * from "./infrastructure/utils/text-validator.util";
|
|
10
|
-
export * from "./infrastructure/utils/rate-limit.util";
|
|
11
|
-
//# sourceMappingURL=index.d.ts.map
|
package/src/index.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,sCAAsC,CAAC;AACrD,cAAc,mDAAmD,CAAC;AAClE,cAAc,oDAAoD,CAAC;AACnE,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4CAA4C,CAAC;AAC3D,cAAc,wCAAwC,CAAC"}
|
package/src/index.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @umituz/web-localization
|
|
3
|
-
* Google Translate integrated localization package for web applications
|
|
4
|
-
*/
|
|
5
|
-
export * from "./domain/entities/translation.entity";
|
|
6
|
-
export * from "./domain/interfaces/translation-service.interface";
|
|
7
|
-
export * from "./infrastructure/services/google-translate.service";
|
|
8
|
-
export * from "./infrastructure/constants";
|
|
9
|
-
export * from "./infrastructure/utils/text-validator.util";
|
|
10
|
-
export * from "./infrastructure/utils/rate-limit.util";
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Localization Constants
|
|
3
|
-
*/
|
|
4
|
-
export declare const GOOGLE_TRANSLATE_API_URL = "https://translate.googleapis.com/translate_a/single";
|
|
5
|
-
export declare const DEFAULT_MIN_DELAY = 100;
|
|
6
|
-
export declare const DEFAULT_MAX_RETRIES = 3;
|
|
7
|
-
export declare const DEFAULT_TIMEOUT = 10000;
|
|
8
|
-
export declare const DEFAULT_LOCALES_DIR = "src/locales";
|
|
9
|
-
export declare const DEFAULT_SOURCE_DIR = "src";
|
|
10
|
-
export declare const DEFAULT_BASE_LANGUAGE = "en-US";
|
|
11
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,eAAO,MAAM,wBAAwB,wDAAwD,CAAC;AAE9F,eAAO,MAAM,iBAAiB,MAAM,CAAC;AACrC,eAAO,MAAM,mBAAmB,IAAI,CAAC;AACrC,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC,eAAO,MAAM,mBAAmB,gBAAgB,CAAC;AACjD,eAAO,MAAM,kBAAkB,QAAQ,CAAC;AACxC,eAAO,MAAM,qBAAqB,UAAU,CAAC"}
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Localization Constants
|
|
3
|
-
*/
|
|
4
|
-
export const GOOGLE_TRANSLATE_API_URL = "https://translate.googleapis.com/translate_a/single";
|
|
5
|
-
export const DEFAULT_MIN_DELAY = 100;
|
|
6
|
-
export const DEFAULT_MAX_RETRIES = 3;
|
|
7
|
-
export const DEFAULT_TIMEOUT = 10000;
|
|
8
|
-
export const DEFAULT_LOCALES_DIR = "src/locales";
|
|
9
|
-
export const DEFAULT_SOURCE_DIR = "src";
|
|
10
|
-
export const DEFAULT_BASE_LANGUAGE = "en-US";
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export interface SyncOptions {
|
|
2
|
-
localesDir?: string;
|
|
3
|
-
sourceDir?: string;
|
|
4
|
-
baseLang?: string;
|
|
5
|
-
}
|
|
6
|
-
export declare class CLIService {
|
|
7
|
-
sync(options?: SyncOptions): Promise<void>;
|
|
8
|
-
translate(options?: SyncOptions): Promise<void>;
|
|
9
|
-
}
|
|
10
|
-
export declare const cliService: CLIService;
|
|
11
|
-
//# sourceMappingURL=cli.service.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli.service.d.ts","sourceRoot":"","sources":["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,95 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
import path from "path";
|
|
3
|
-
import chalk from "chalk";
|
|
4
|
-
import { googleTranslateService } from "./google-translate.service";
|
|
5
|
-
import { parseTypeScriptFile, generateTypeScriptContent } from "../utils/file.util";
|
|
6
|
-
import { DEFAULT_LOCALES_DIR, DEFAULT_BASE_LANGUAGE } from "../constants";
|
|
7
|
-
export class CLIService {
|
|
8
|
-
async sync(options = {}) {
|
|
9
|
-
const localesDir = path.resolve(process.cwd(), options.localesDir || DEFAULT_LOCALES_DIR);
|
|
10
|
-
const baseLang = options.baseLang || DEFAULT_BASE_LANGUAGE;
|
|
11
|
-
const baseLangPath = path.join(localesDir, `${baseLang}.ts`);
|
|
12
|
-
if (!fs.existsSync(localesDir)) {
|
|
13
|
-
console.error(chalk.red(`❌ Locales directory not found: ${localesDir}`));
|
|
14
|
-
return;
|
|
15
|
-
}
|
|
16
|
-
if (!fs.existsSync(baseLangPath)) {
|
|
17
|
-
console.error(chalk.red(`❌ Base language file not found: ${baseLangPath}`));
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
const baseData = parseTypeScriptFile(baseLangPath);
|
|
21
|
-
const files = fs.readdirSync(localesDir)
|
|
22
|
-
.filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/) && f !== `${baseLang}.ts`)
|
|
23
|
-
.sort();
|
|
24
|
-
console.log(chalk.blue(`📊 Found ${files.length} languages to sync with ${baseLang}.\n`));
|
|
25
|
-
for (const file of files) {
|
|
26
|
-
const targetPath = path.join(localesDir, file);
|
|
27
|
-
const targetData = parseTypeScriptFile(targetPath);
|
|
28
|
-
const langCode = file.replace(".ts", "");
|
|
29
|
-
// Deep merge with base data structure
|
|
30
|
-
const syncObject = (source, target) => {
|
|
31
|
-
const result = { ...target };
|
|
32
|
-
for (const key in source) {
|
|
33
|
-
if (typeof source[key] === "object" && source[key] !== null) {
|
|
34
|
-
result[key] = syncObject(source[key], target[key] || {});
|
|
35
|
-
}
|
|
36
|
-
else if (target[key] === undefined) {
|
|
37
|
-
result[key] = source[key];
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
// Remove extra keys
|
|
41
|
-
for (const key in target) {
|
|
42
|
-
if (source[key] === undefined) {
|
|
43
|
-
delete result[key];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return result;
|
|
47
|
-
};
|
|
48
|
-
const syncedData = syncObject(baseData, targetData);
|
|
49
|
-
fs.writeFileSync(targetPath, generateTypeScriptContent(syncedData, langCode));
|
|
50
|
-
console.log(chalk.green(` 🌍 ${langCode}: Synced structure.`));
|
|
51
|
-
}
|
|
52
|
-
console.log(chalk.bold.green("\n✅ Synchronization completed!"));
|
|
53
|
-
}
|
|
54
|
-
async translate(options = {}) {
|
|
55
|
-
const localesDir = path.resolve(process.cwd(), options.localesDir || DEFAULT_LOCALES_DIR);
|
|
56
|
-
const baseLang = options.baseLang || DEFAULT_BASE_LANGUAGE;
|
|
57
|
-
const baseLangPath = path.join(localesDir, `${baseLang}.ts`);
|
|
58
|
-
if (!fs.existsSync(baseLangPath)) {
|
|
59
|
-
console.error(chalk.red(`❌ Base language file not found: ${baseLangPath}`));
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
googleTranslateService.initialize({});
|
|
63
|
-
const baseData = parseTypeScriptFile(baseLangPath);
|
|
64
|
-
const files = fs.readdirSync(localesDir)
|
|
65
|
-
.filter(f => f.match(/^[a-z]{2}-[A-Z]{2}\.ts$/) && f !== `${baseLang}.ts`)
|
|
66
|
-
.sort();
|
|
67
|
-
console.log(chalk.blue.bold(`🚀 Starting automatic translation for ${files.length} languages...\n`));
|
|
68
|
-
for (const file of files) {
|
|
69
|
-
const targetPath = path.join(localesDir, file);
|
|
70
|
-
const targetData = parseTypeScriptFile(targetPath);
|
|
71
|
-
const langCode = file.replace(".ts", "");
|
|
72
|
-
const stats = {
|
|
73
|
-
totalCount: 0,
|
|
74
|
-
successCount: 0,
|
|
75
|
-
failureCount: 0,
|
|
76
|
-
skippedCount: 0,
|
|
77
|
-
translatedKeys: []
|
|
78
|
-
};
|
|
79
|
-
console.log(chalk.yellow(`🌍 Translating ${langCode}...`));
|
|
80
|
-
await googleTranslateService.translateObject(baseData, targetData, langCode.split("-")[0], // ISO 639-1
|
|
81
|
-
"", stats, (key, from, to) => {
|
|
82
|
-
process.stdout.write(chalk.gray(` • ${key}: ${from.substring(0, 15)}... → ${to.substring(0, 15)}...\r`));
|
|
83
|
-
});
|
|
84
|
-
if (stats.successCount > 0) {
|
|
85
|
-
fs.writeFileSync(targetPath, generateTypeScriptContent(targetData, langCode));
|
|
86
|
-
console.log(chalk.green(` ✅ Successfully translated ${stats.successCount} keys.`));
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
console.log(chalk.gray(" ✨ Already up to date."));
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
console.log(chalk.bold.green("\n✅ All translations completed!"));
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
export const cliService = new CLIService();
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Google Translate Service
|
|
3
|
-
* @description Main translation service using Google Translate API
|
|
4
|
-
*/
|
|
5
|
-
import type { TranslationRequest, TranslationResponse, TranslationStats } from "../../domain/entities/translation.entity";
|
|
6
|
-
import type { ITranslationService, TranslationServiceConfig } from "../../domain/interfaces/translation-service.interface";
|
|
7
|
-
declare class GoogleTranslateService implements ITranslationService {
|
|
8
|
-
private config;
|
|
9
|
-
private rateLimiter;
|
|
10
|
-
initialize(config: TranslationServiceConfig): void;
|
|
11
|
-
isInitialized(): boolean;
|
|
12
|
-
private ensureInitialized;
|
|
13
|
-
translate(request: TranslationRequest): Promise<TranslationResponse>;
|
|
14
|
-
translateBatch(requests: TranslationRequest[]): Promise<TranslationStats>;
|
|
15
|
-
translateObject(sourceObject: Record<string, unknown>, targetObject: Record<string, unknown>, targetLanguage: string, path?: string, stats?: TranslationStats, onTranslate?: (key: string, from: string, to: string) => void): Promise<void>;
|
|
16
|
-
private callTranslateAPI;
|
|
17
|
-
}
|
|
18
|
-
export declare const googleTranslateService: GoogleTranslateService;
|
|
19
|
-
export { GoogleTranslateService };
|
|
20
|
-
//# sourceMappingURL=google-translate.service.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"google-translate.service.d.ts","sourceRoot":"","sources":["google-translate.service.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EACV,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EACjB,MAAM,0CAA0C,CAAC;AAClD,OAAO,KAAK,EACV,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,uDAAuD,CAAC;AAa/D,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,202 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Google Translate Service
|
|
3
|
-
* @description Main translation service using Google Translate API
|
|
4
|
-
*/
|
|
5
|
-
import { RateLimiter } from "../utils/rate-limit.util";
|
|
6
|
-
import { shouldSkipWord, needsTranslation, isValidText, } from "../utils/text-validator.util";
|
|
7
|
-
import { GOOGLE_TRANSLATE_API_URL, DEFAULT_MIN_DELAY, DEFAULT_TIMEOUT, } from "../constants";
|
|
8
|
-
class GoogleTranslateService {
|
|
9
|
-
config = null;
|
|
10
|
-
rateLimiter = null;
|
|
11
|
-
initialize(config) {
|
|
12
|
-
this.config = {
|
|
13
|
-
minDelay: DEFAULT_MIN_DELAY,
|
|
14
|
-
timeout: DEFAULT_TIMEOUT,
|
|
15
|
-
...config,
|
|
16
|
-
};
|
|
17
|
-
this.rateLimiter = new RateLimiter(this.config.minDelay);
|
|
18
|
-
}
|
|
19
|
-
isInitialized() {
|
|
20
|
-
return this.config !== null && this.rateLimiter !== null;
|
|
21
|
-
}
|
|
22
|
-
ensureInitialized() {
|
|
23
|
-
if (!this.isInitialized()) {
|
|
24
|
-
throw new Error("GoogleTranslateService is not initialized. Call initialize() first.");
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
async translate(request) {
|
|
28
|
-
this.ensureInitialized();
|
|
29
|
-
const { text, targetLanguage, sourceLanguage = "en" } = request;
|
|
30
|
-
if (!isValidText(text) || shouldSkipWord(text)) {
|
|
31
|
-
return {
|
|
32
|
-
originalText: text,
|
|
33
|
-
translatedText: text,
|
|
34
|
-
sourceLanguage,
|
|
35
|
-
targetLanguage,
|
|
36
|
-
success: true,
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
if (!targetLanguage || targetLanguage.trim().length === 0) {
|
|
40
|
-
return {
|
|
41
|
-
originalText: text,
|
|
42
|
-
translatedText: text,
|
|
43
|
-
sourceLanguage,
|
|
44
|
-
targetLanguage,
|
|
45
|
-
success: false,
|
|
46
|
-
error: "Invalid target language",
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
await this.rateLimiter.waitForSlot();
|
|
50
|
-
try {
|
|
51
|
-
const translatedText = await this.callTranslateAPI(text, targetLanguage, sourceLanguage);
|
|
52
|
-
return {
|
|
53
|
-
originalText: text,
|
|
54
|
-
translatedText,
|
|
55
|
-
sourceLanguage,
|
|
56
|
-
targetLanguage,
|
|
57
|
-
success: true,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
catch (error) {
|
|
61
|
-
return {
|
|
62
|
-
originalText: text,
|
|
63
|
-
translatedText: text,
|
|
64
|
-
sourceLanguage,
|
|
65
|
-
targetLanguage,
|
|
66
|
-
success: false,
|
|
67
|
-
error: error instanceof Error ? error.message : "Unknown error",
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
async translateBatch(requests) {
|
|
72
|
-
this.ensureInitialized();
|
|
73
|
-
const stats = {
|
|
74
|
-
totalCount: requests.length,
|
|
75
|
-
successCount: 0,
|
|
76
|
-
failureCount: 0,
|
|
77
|
-
skippedCount: 0,
|
|
78
|
-
translatedKeys: [],
|
|
79
|
-
};
|
|
80
|
-
if (!Array.isArray(requests) || requests.length === 0) {
|
|
81
|
-
return stats;
|
|
82
|
-
}
|
|
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++;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return stats;
|
|
114
|
-
}
|
|
115
|
-
async translateObject(sourceObject, targetObject, targetLanguage, path = "", stats = {
|
|
116
|
-
totalCount: 0,
|
|
117
|
-
successCount: 0,
|
|
118
|
-
failureCount: 0,
|
|
119
|
-
skippedCount: 0,
|
|
120
|
-
translatedKeys: [],
|
|
121
|
-
}, onTranslate) {
|
|
122
|
-
if (!sourceObject || typeof sourceObject !== "object")
|
|
123
|
-
return;
|
|
124
|
-
if (!targetObject || typeof targetObject !== "object")
|
|
125
|
-
return;
|
|
126
|
-
if (!targetLanguage || targetLanguage.trim().length === 0)
|
|
127
|
-
return;
|
|
128
|
-
const keys = Object.keys(sourceObject);
|
|
129
|
-
const textsToTranslate = [];
|
|
130
|
-
for (const key of keys) {
|
|
131
|
-
const enValue = sourceObject[key];
|
|
132
|
-
const targetValue = targetObject[key];
|
|
133
|
-
const currentPath = path ? `${path}.${key}` : key;
|
|
134
|
-
if (typeof enValue === "object" && enValue !== null) {
|
|
135
|
-
if (!targetObject[key] || typeof targetObject[key] !== "object") {
|
|
136
|
-
targetObject[key] = {};
|
|
137
|
-
}
|
|
138
|
-
await this.translateObject(enValue, targetObject[key], targetLanguage, currentPath, stats, onTranslate);
|
|
139
|
-
}
|
|
140
|
-
else if (typeof enValue === "string") {
|
|
141
|
-
stats.totalCount++;
|
|
142
|
-
if (needsTranslation(targetValue, enValue)) {
|
|
143
|
-
textsToTranslate.push({ key, enValue, currentPath });
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
stats.skippedCount++;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
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 => ({
|
|
155
|
-
text: item.enValue,
|
|
156
|
-
targetLanguage,
|
|
157
|
-
})));
|
|
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
|
-
if (onTranslate)
|
|
165
|
-
onTranslate(currentPath, enValue, translatedItem.to);
|
|
166
|
-
resultIndex++;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
async callTranslateAPI(text, targetLanguage, sourceLanguage) {
|
|
173
|
-
const timeout = this.config?.timeout || DEFAULT_TIMEOUT;
|
|
174
|
-
const encodedText = encodeURIComponent(text);
|
|
175
|
-
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}`);
|
|
184
|
-
}
|
|
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];
|
|
193
|
-
}
|
|
194
|
-
return text;
|
|
195
|
-
}
|
|
196
|
-
finally {
|
|
197
|
-
clearTimeout(timeoutId);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
export const googleTranslateService = new GoogleTranslateService();
|
|
202
|
-
export { GoogleTranslateService };
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parses a TypeScript file containing an object export
|
|
3
|
-
* @description Simplistic parser for 'export default { ... }' or 'export const data = { ... }'
|
|
4
|
-
*/
|
|
5
|
-
export declare function parseTypeScriptFile(filePath: string): Record<string, unknown>;
|
|
6
|
-
/**
|
|
7
|
-
* Generates a TypeScript file content from an object
|
|
8
|
-
*/
|
|
9
|
-
export declare function generateTypeScriptContent(obj: Record<string, unknown>, langCode?: string): string;
|
|
10
|
-
//# sourceMappingURL=file.util.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"file.util.d.ts","sourceRoot":"","sources":["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,41 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
/**
|
|
3
|
-
* Parses a TypeScript file containing an object export
|
|
4
|
-
* @description Simplistic parser for 'export default { ... }' or 'export const data = { ... }'
|
|
5
|
-
*/
|
|
6
|
-
export function parseTypeScriptFile(filePath) {
|
|
7
|
-
if (!fs.existsSync(filePath))
|
|
8
|
-
return {};
|
|
9
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
10
|
-
// Extract the object part
|
|
11
|
-
// This is a naive implementation, but matches the pattern used in the project
|
|
12
|
-
const match = content.match(/export (default|const [^=]+ =) (\{[\s\S]*\});?\s*$/);
|
|
13
|
-
if (!match)
|
|
14
|
-
return {};
|
|
15
|
-
try {
|
|
16
|
-
// Evaluate the object string to a real JS object
|
|
17
|
-
// Using Function instead of eval for slightly better safety
|
|
18
|
-
const objStr = match[2];
|
|
19
|
-
const obj = new Function(`return ${objStr}`)();
|
|
20
|
-
return obj;
|
|
21
|
-
}
|
|
22
|
-
catch (err) {
|
|
23
|
-
console.error(`Error parsing ${filePath}:`, err);
|
|
24
|
-
return {};
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Generates a TypeScript file content from an object
|
|
29
|
-
*/
|
|
30
|
-
export function generateTypeScriptContent(obj, langCode) {
|
|
31
|
-
const jsonStr = JSON.stringify(obj, null, 2);
|
|
32
|
-
// Clean up keys (remove quotes if possible, though JSON.stringify adds them)
|
|
33
|
-
// For simplicity, we'll keep them as valid JS objects
|
|
34
|
-
return `/**
|
|
35
|
-
* Localization: ${langCode || "unknown"}
|
|
36
|
-
* Generated by @umituz/web-localization
|
|
37
|
-
*/
|
|
38
|
-
|
|
39
|
-
export default ${jsonStr};
|
|
40
|
-
`;
|
|
41
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple Rate Limiter
|
|
3
|
-
* @description Controls the frequency of API requests
|
|
4
|
-
*/
|
|
5
|
-
export declare class RateLimiter {
|
|
6
|
-
private lastRequestTime;
|
|
7
|
-
private minDelay;
|
|
8
|
-
constructor(minDelay?: number);
|
|
9
|
-
waitForSlot(): Promise<void>;
|
|
10
|
-
}
|
|
11
|
-
//# sourceMappingURL=rate-limit.util.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.util.d.ts","sourceRoot":"","sources":["rate-limit.util.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,qBAAa,WAAW;IACtB,OAAO,CAAC,eAAe,CAAK;IAC5B,OAAO,CAAC,QAAQ,CAAS;gBAEb,QAAQ,SAAM;IAIpB,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;CAWnC"}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Simple Rate Limiter
|
|
3
|
-
* @description Controls the frequency of API requests
|
|
4
|
-
*/
|
|
5
|
-
export class RateLimiter {
|
|
6
|
-
lastRequestTime = 0;
|
|
7
|
-
minDelay;
|
|
8
|
-
constructor(minDelay = 100) {
|
|
9
|
-
this.minDelay = minDelay;
|
|
10
|
-
}
|
|
11
|
-
async waitForSlot() {
|
|
12
|
-
const now = Date.now();
|
|
13
|
-
const elapsedTime = now - this.lastRequestTime;
|
|
14
|
-
if (elapsedTime < this.minDelay) {
|
|
15
|
-
const waitTime = this.minDelay - elapsedTime;
|
|
16
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
17
|
-
}
|
|
18
|
-
this.lastRequestTime = Date.now();
|
|
19
|
-
}
|
|
20
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Text Validation Utilities
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Validates if the text is suitable for translation
|
|
6
|
-
*/
|
|
7
|
-
export declare function isValidText(text: unknown): text is string;
|
|
8
|
-
/**
|
|
9
|
-
* Checks if a word should be skipped (e.g., proper nouns, symbols)
|
|
10
|
-
*/
|
|
11
|
-
export declare function shouldSkipWord(text: string): boolean;
|
|
12
|
-
/**
|
|
13
|
-
* Determines if a key needs translation
|
|
14
|
-
*/
|
|
15
|
-
export declare function needsTranslation(targetValue: unknown, sourceValue: string): boolean;
|
|
16
|
-
//# sourceMappingURL=text-validator.util.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"text-validator.util.d.ts","sourceRoot":"","sources":["text-validator.util.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;GAEG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,IAAI,MAAM,CAKzD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAGpD;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAKnF"}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Text Validation Utilities
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Validates if the text is suitable for translation
|
|
6
|
-
*/
|
|
7
|
-
export function isValidText(text) {
|
|
8
|
-
if (typeof text !== "string")
|
|
9
|
-
return false;
|
|
10
|
-
if (text.trim().length === 0)
|
|
11
|
-
return false;
|
|
12
|
-
if (/^\d+$/.test(text))
|
|
13
|
-
return false; // Don't translate pure numbers
|
|
14
|
-
return true;
|
|
15
|
-
}
|
|
16
|
-
/**
|
|
17
|
-
* Checks if a word should be skipped (e.g., proper nouns, symbols)
|
|
18
|
-
*/
|
|
19
|
-
export function shouldSkipWord(text) {
|
|
20
|
-
const skiplist = ["@umituz"];
|
|
21
|
-
return skiplist.some(word => text.includes(word));
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Determines if a key needs translation
|
|
25
|
-
*/
|
|
26
|
-
export function needsTranslation(targetValue, sourceValue) {
|
|
27
|
-
if (typeof targetValue !== "string")
|
|
28
|
-
return true;
|
|
29
|
-
if (targetValue.trim().length === 0)
|
|
30
|
-
return true;
|
|
31
|
-
if (targetValue === sourceValue && !/^\d+$/.test(sourceValue))
|
|
32
|
-
return true;
|
|
33
|
-
return false;
|
|
34
|
-
}
|
package/src/scripts/cli.d.ts
DELETED
package/src/scripts/cli.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["cli.ts"],"names":[],"mappings":";AAEA;;GAEG"}
|
package/src/scripts/cli.js
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* CLI Tool for @umituz/web-localization
|
|
4
|
-
*/
|
|
5
|
-
import { Command } from "commander";
|
|
6
|
-
import { cliService } from "../infrastructure/services/cli.service";
|
|
7
|
-
import chalk from "chalk";
|
|
8
|
-
const program = new Command();
|
|
9
|
-
program
|
|
10
|
-
.name("web-loc")
|
|
11
|
-
.description("Localization CLI tool for web applications")
|
|
12
|
-
.version("1.0.0");
|
|
13
|
-
program
|
|
14
|
-
.command("sync")
|
|
15
|
-
.description("Synchronize missing keys from base language to other languages")
|
|
16
|
-
.option("-d, --locales-dir <dir>", "Directory containing locale files", "src/locales")
|
|
17
|
-
.option("-b, --base-lang <lang>", "Base language code", "en-US")
|
|
18
|
-
.action(async (options) => {
|
|
19
|
-
try {
|
|
20
|
-
await cliService.sync(options);
|
|
21
|
-
}
|
|
22
|
-
catch (error) {
|
|
23
|
-
console.error(chalk.red("❌ Sync failed:"), error instanceof Error ? error.message : error);
|
|
24
|
-
process.exit(1);
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
program
|
|
28
|
-
.command("translate")
|
|
29
|
-
.description("Automatically translate missing strings using Google Translate")
|
|
30
|
-
.option("-d, --locales-dir <dir>", "Directory containing locale files", "src/locales")
|
|
31
|
-
.option("-b, --base-lang <lang>", "Base language code", "en-US")
|
|
32
|
-
.action(async (options) => {
|
|
33
|
-
try {
|
|
34
|
-
await cliService.translate(options);
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
console.error(chalk.red("❌ Translation failed:"), error instanceof Error ? error.message : error);
|
|
38
|
-
process.exit(1);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
program.parse();
|