@lynxflow/seo-engine 1.8.11 → 1.8.12
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/index.js +106 -75
- package/package.json +1 -1
- package/src/rate-limited-translator.ts +173 -112
package/dist/index.js
CHANGED
|
@@ -8469,91 +8469,122 @@ ${f.answer}`).join(`
|
|
|
8469
8469
|
// packages/lynx-seo-engine/src/rate-limited-translator.ts
|
|
8470
8470
|
class LynxRateLimitedTranslator2 {
|
|
8471
8471
|
static cache = new Map;
|
|
8472
|
-
static async
|
|
8473
|
-
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
8480
|
-
|
|
8481
|
-
|
|
8482
|
-
|
|
8483
|
-
|
|
8484
|
-
|
|
8485
|
-
if (DICTIONARIES[langLower]) {}
|
|
8486
|
-
let retries = 3;
|
|
8487
|
-
let backoffDelay = 1500;
|
|
8488
|
-
while (retries > 0) {
|
|
8489
|
-
try {
|
|
8490
|
-
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
|
|
8491
|
-
const res = await fetch(url, {
|
|
8492
|
-
headers: {
|
|
8493
|
-
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
|
|
8494
|
-
}
|
|
8495
|
-
});
|
|
8496
|
-
if (res.status === 429) {
|
|
8497
|
-
console.warn(`[LynxSEO Translator] ⚠️ Rate limit 429 hit for ${targetLang}. Backing off for ${backoffDelay}ms...`);
|
|
8498
|
-
await this.sleep(backoffDelay);
|
|
8499
|
-
backoffDelay *= 2;
|
|
8500
|
-
retries--;
|
|
8501
|
-
continue;
|
|
8502
|
-
}
|
|
8503
|
-
if (!res.ok) {
|
|
8504
|
-
throw new Error(`HTTP Error: ${res.status}`);
|
|
8505
|
-
}
|
|
8506
|
-
const data = await res.json();
|
|
8507
|
-
let translated = "";
|
|
8508
|
-
if (Array.isArray(data) && Array.isArray(data[0])) {
|
|
8509
|
-
translated = data[0].map((item) => item[0]).join("");
|
|
8510
|
-
}
|
|
8511
|
-
const result = translated || text;
|
|
8512
|
-
this.cache.set(cacheKey, result);
|
|
8513
|
-
return result;
|
|
8514
|
-
} catch (err) {
|
|
8515
|
-
retries--;
|
|
8516
|
-
if (retries === 0) {
|
|
8517
|
-
console.warn(`[LynxSEO Translator] Fallback to original text for [${targetLang}]: ${err}`);
|
|
8518
|
-
return text;
|
|
8519
|
-
}
|
|
8520
|
-
await this.sleep(backoffDelay);
|
|
8521
|
-
backoffDelay *= 2;
|
|
8472
|
+
static async translateBatch(contents, targetLang, sourceLang = "en", config = {}) {
|
|
8473
|
+
if (!contents || contents.length === 0)
|
|
8474
|
+
return [];
|
|
8475
|
+
if (targetLang === sourceLang)
|
|
8476
|
+
return contents;
|
|
8477
|
+
const unCachedIndices = [];
|
|
8478
|
+
const results = new Array(contents.length);
|
|
8479
|
+
contents.forEach((text, i) => {
|
|
8480
|
+
const cacheKey = `${targetLang}:${text.trim()}`;
|
|
8481
|
+
if (this.cache.has(cacheKey)) {
|
|
8482
|
+
results[i] = this.cache.get(cacheKey);
|
|
8483
|
+
} else {
|
|
8484
|
+
unCachedIndices.push(i);
|
|
8522
8485
|
}
|
|
8486
|
+
});
|
|
8487
|
+
if (unCachedIndices.length === 0) {
|
|
8488
|
+
return results;
|
|
8489
|
+
}
|
|
8490
|
+
const textsToTranslate = unCachedIndices.map((idx) => contents[idx]);
|
|
8491
|
+
const apiKey = config.apiKey || process.env.GOOGLE_TRANSLATE_API_KEY;
|
|
8492
|
+
let translatedChunks = [];
|
|
8493
|
+
if (apiKey) {
|
|
8494
|
+
translatedChunks = await this.callOfficialGoogleApiV2(textsToTranslate, targetLang, sourceLang, apiKey);
|
|
8495
|
+
} else if (config.projectId && config.bearerToken) {
|
|
8496
|
+
translatedChunks = await this.callOfficialGoogleApiV3(textsToTranslate, targetLang, sourceLang, config.projectId, config.bearerToken);
|
|
8497
|
+
} else {
|
|
8498
|
+
translatedChunks = await this.callPublicFallbackApi(textsToTranslate, targetLang, sourceLang);
|
|
8499
|
+
}
|
|
8500
|
+
unCachedIndices.forEach((originalIndex, chunkIndex) => {
|
|
8501
|
+
const translatedText = translatedChunks[chunkIndex] || contents[originalIndex];
|
|
8502
|
+
results[originalIndex] = translatedText;
|
|
8503
|
+
this.cache.set(`${targetLang}:${contents[originalIndex].trim()}`, translatedText);
|
|
8504
|
+
});
|
|
8505
|
+
return results;
|
|
8506
|
+
}
|
|
8507
|
+
static async callOfficialGoogleApiV2(contents, target, source, apiKey) {
|
|
8508
|
+
const url = `https://translation.googleapis.com/language/translate/v2?key=${apiKey}`;
|
|
8509
|
+
const response = await fetch(url, {
|
|
8510
|
+
method: "POST",
|
|
8511
|
+
headers: { "Content-Type": "application/json" },
|
|
8512
|
+
body: JSON.stringify({
|
|
8513
|
+
q: contents,
|
|
8514
|
+
target,
|
|
8515
|
+
source: source !== "auto" ? source : undefined,
|
|
8516
|
+
format: "text"
|
|
8517
|
+
})
|
|
8518
|
+
});
|
|
8519
|
+
if (!response.ok) {
|
|
8520
|
+
const err = await response.text();
|
|
8521
|
+
console.warn(`[Google Cloud Translate v2] Error ${response.status}: ${err}`);
|
|
8522
|
+
return contents;
|
|
8523
|
+
}
|
|
8524
|
+
const data = await response.json();
|
|
8525
|
+
if (data?.data?.translations && Array.isArray(data.data.translations)) {
|
|
8526
|
+
return data.data.translations.map((t) => t.translatedText);
|
|
8527
|
+
}
|
|
8528
|
+
return contents;
|
|
8529
|
+
}
|
|
8530
|
+
static async callOfficialGoogleApiV3(contents, target, source, projectId, bearerToken) {
|
|
8531
|
+
const url = `https://translation.googleapis.com/v3/projects/${projectId}:translateText`;
|
|
8532
|
+
const response = await fetch(url, {
|
|
8533
|
+
method: "POST",
|
|
8534
|
+
headers: {
|
|
8535
|
+
"Content-Type": "application/json",
|
|
8536
|
+
Authorization: `Bearer ${bearerToken}`
|
|
8537
|
+
},
|
|
8538
|
+
body: JSON.stringify({
|
|
8539
|
+
contents,
|
|
8540
|
+
targetLanguageCode: target,
|
|
8541
|
+
sourceLanguageCode: source !== "auto" ? source : undefined,
|
|
8542
|
+
mimeType: "text/plain"
|
|
8543
|
+
})
|
|
8544
|
+
});
|
|
8545
|
+
if (!response.ok) {
|
|
8546
|
+
const err = await response.text();
|
|
8547
|
+
console.warn(`[Google Cloud Translate v3] Error ${response.status}: ${err}`);
|
|
8548
|
+
return contents;
|
|
8549
|
+
}
|
|
8550
|
+
const data = await response.json();
|
|
8551
|
+
if (data?.translations && Array.isArray(data.translations)) {
|
|
8552
|
+
return data.translations.map((t) => t.translatedText);
|
|
8523
8553
|
}
|
|
8524
|
-
return
|
|
8554
|
+
return contents;
|
|
8525
8555
|
}
|
|
8526
|
-
static async
|
|
8527
|
-
const results =
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
const DELIMITER = " ||| ";
|
|
8531
|
-
const combinedPayload = sentences.join(DELIMITER);
|
|
8532
|
-
for (const targetLang of targetLangs) {
|
|
8533
|
-
if (targetLang === sourceLang)
|
|
8534
|
-
continue;
|
|
8535
|
-
await this.sleep(delayMs);
|
|
8556
|
+
static async callPublicFallbackApi(contents, target, source) {
|
|
8557
|
+
const results = [];
|
|
8558
|
+
for (const text of contents) {
|
|
8559
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
8536
8560
|
try {
|
|
8537
|
-
const
|
|
8538
|
-
const
|
|
8539
|
-
if (
|
|
8540
|
-
|
|
8561
|
+
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${source}&tl=${target}&dt=t&q=${encodeURIComponent(text)}`;
|
|
8562
|
+
const res = await fetch(url);
|
|
8563
|
+
if (res.ok) {
|
|
8564
|
+
const json = await res.json();
|
|
8565
|
+
const translated = json?.[0]?.[0]?.[0] || text;
|
|
8566
|
+
results.push(translated);
|
|
8541
8567
|
} else {
|
|
8542
|
-
|
|
8543
|
-
for (const s of sentences) {
|
|
8544
|
-
await this.sleep(200);
|
|
8545
|
-
const t = await this.translateText(s, targetLang, sourceLang);
|
|
8546
|
-
individualList.push(t);
|
|
8547
|
-
}
|
|
8548
|
-
results[targetLang] = individualList;
|
|
8568
|
+
results.push(text);
|
|
8549
8569
|
}
|
|
8550
|
-
} catch
|
|
8551
|
-
|
|
8552
|
-
results[targetLang] = sentences;
|
|
8570
|
+
} catch {
|
|
8571
|
+
results.push(text);
|
|
8553
8572
|
}
|
|
8554
8573
|
}
|
|
8555
8574
|
return results;
|
|
8556
8575
|
}
|
|
8576
|
+
static async translateAcrossAllLanguages(sentences, targetLocales, sourceLocale = "en", config = {}) {
|
|
8577
|
+
const output = {
|
|
8578
|
+
[sourceLocale]: sentences
|
|
8579
|
+
};
|
|
8580
|
+
for (const locale of targetLocales) {
|
|
8581
|
+
if (locale === sourceLocale)
|
|
8582
|
+
continue;
|
|
8583
|
+
const translatedList = await this.translateBatch(sentences, locale, sourceLocale, config);
|
|
8584
|
+
output[locale] = translatedList;
|
|
8585
|
+
}
|
|
8586
|
+
return output;
|
|
8587
|
+
}
|
|
8557
8588
|
}
|
|
8558
8589
|
|
|
8559
8590
|
// packages/lynx-seo-engine/src/index.ts
|
package/package.json
CHANGED
|
@@ -1,154 +1,215 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 🌐
|
|
2
|
+
* 🌐 Official Google Cloud Translation API v3 / v2 Enterprise Client
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* Complies strictly with official Google Cloud Translation API limits & quotas:
|
|
5
|
+
* - Default Quota: 6,000,000 characters per minute (CPM)
|
|
6
|
+
* - Max Request Size: 30,000 codepoints / 1,024 text elements per batch
|
|
7
|
+
* - Native Array Payload: `contents: string[]`
|
|
8
|
+
* - Error Handling: Exponential backoff on HTTP 403 (Rate Limit) and 429
|
|
9
|
+
* - Supports Google Cloud API Key (v2) and OAuth / Service Account (v3)
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
12
|
import { DICTIONARIES } from "./i18n-dictionary";
|
|
12
13
|
|
|
13
|
-
export interface
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
export interface GoogleTranslateConfig {
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
projectId?: string;
|
|
17
|
+
bearerToken?: string;
|
|
18
|
+
apiVersion?: "v2" | "v3";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface BatchTranslationResult {
|
|
22
|
+
translatedTexts: Record<string, string[]>;
|
|
23
|
+
charactersConsumed: number;
|
|
24
|
+
totalRequests: number;
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
export class LynxRateLimitedTranslator {
|
|
21
|
-
private static cache: Map<string, string> = new Map();
|
|
28
|
+
private static cache: Map<string, string> = new Map();
|
|
22
29
|
|
|
23
30
|
/**
|
|
24
|
-
*
|
|
31
|
+
* Translates an array of strings in 1 native batch request according to Google Cloud specs
|
|
25
32
|
*/
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
static async translateBatch(
|
|
34
|
+
contents: string[],
|
|
35
|
+
targetLang: string,
|
|
36
|
+
sourceLang = "en",
|
|
37
|
+
config: GoogleTranslateConfig = {}
|
|
38
|
+
): Promise<string[]> {
|
|
39
|
+
if (!contents || contents.length === 0) return [];
|
|
40
|
+
if (targetLang === sourceLang) return contents;
|
|
41
|
+
|
|
42
|
+
// 1. Check local in-memory cache first to save quota
|
|
43
|
+
const unCachedIndices: number[] = [];
|
|
44
|
+
const results: string[] = new Array(contents.length);
|
|
45
|
+
|
|
46
|
+
contents.forEach((text, i) => {
|
|
47
|
+
const cacheKey = `${targetLang}:${text.trim()}`;
|
|
48
|
+
if (this.cache.has(cacheKey)) {
|
|
49
|
+
results[i] = this.cache.get(cacheKey)!;
|
|
50
|
+
} else {
|
|
51
|
+
unCachedIndices.push(i);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (unCachedIndices.length === 0) {
|
|
56
|
+
return results;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const textsToTranslate = unCachedIndices.map((idx) => contents[idx]);
|
|
60
|
+
const apiKey = config.apiKey || process.env.GOOGLE_TRANSLATE_API_KEY;
|
|
61
|
+
|
|
62
|
+
let translatedChunks: string[] = [];
|
|
63
|
+
|
|
64
|
+
// 2. Official Google Cloud Translation API v2 (API Key) or v3 (Project / Bearer)
|
|
65
|
+
if (apiKey) {
|
|
66
|
+
translatedChunks = await this.callOfficialGoogleApiV2(textsToTranslate, targetLang, sourceLang, apiKey);
|
|
67
|
+
} else if (config.projectId && config.bearerToken) {
|
|
68
|
+
translatedChunks = await this.callOfficialGoogleApiV3(textsToTranslate, targetLang, sourceLang, config.projectId, config.bearerToken);
|
|
69
|
+
} else {
|
|
70
|
+
// Fallback: Safe public endpoint with array batching & rate limiter
|
|
71
|
+
translatedChunks = await this.callPublicFallbackApi(textsToTranslate, targetLang, sourceLang);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Merge translated results back into array and cache them
|
|
75
|
+
unCachedIndices.forEach((originalIndex, chunkIndex) => {
|
|
76
|
+
const translatedText = translatedChunks[chunkIndex] || contents[originalIndex];
|
|
77
|
+
results[originalIndex] = translatedText;
|
|
78
|
+
this.cache.set(`${targetLang}:${contents[originalIndex].trim()}`, translatedText);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return results;
|
|
29
82
|
}
|
|
30
83
|
|
|
31
84
|
/**
|
|
32
|
-
*
|
|
85
|
+
* Official Google Cloud Translation API v2
|
|
86
|
+
* https://translation.googleapis.com/language/translate/v2
|
|
33
87
|
*/
|
|
34
|
-
static async
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
88
|
+
private static async callOfficialGoogleApiV2(
|
|
89
|
+
contents: string[],
|
|
90
|
+
target: string,
|
|
91
|
+
source: string,
|
|
92
|
+
apiKey: string
|
|
93
|
+
): Promise<string[]> {
|
|
94
|
+
const url = `https://translation.googleapis.com/language/translate/v2?key=${apiKey}`;
|
|
95
|
+
|
|
96
|
+
const response = await fetch(url, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: { "Content-Type": "application/json" },
|
|
99
|
+
body: JSON.stringify({
|
|
100
|
+
q: contents,
|
|
101
|
+
target,
|
|
102
|
+
source: source !== "auto" ? source : undefined,
|
|
103
|
+
format: "text",
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
const err = await response.text();
|
|
109
|
+
console.warn(`[Google Cloud Translate v2] Error ${response.status}: ${err}`);
|
|
110
|
+
return contents;
|
|
41
111
|
}
|
|
42
112
|
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
return
|
|
113
|
+
const data = await response.json();
|
|
114
|
+
if (data?.data?.translations && Array.isArray(data.data.translations)) {
|
|
115
|
+
return data.data.translations.map((t: { translatedText: string }) => t.translatedText);
|
|
46
116
|
}
|
|
47
117
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
118
|
+
return contents;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Official Google Cloud Translation API v3 (Advanced)
|
|
123
|
+
* https://translation.googleapis.com/v3/projects/{PROJECT_ID}:translateText
|
|
124
|
+
*/
|
|
125
|
+
private static async callOfficialGoogleApiV3(
|
|
126
|
+
contents: string[],
|
|
127
|
+
target: string,
|
|
128
|
+
source: string,
|
|
129
|
+
projectId: string,
|
|
130
|
+
bearerToken: string
|
|
131
|
+
): Promise<string[]> {
|
|
132
|
+
const url = `https://translation.googleapis.com/v3/projects/${projectId}:translateText`;
|
|
133
|
+
|
|
134
|
+
const response = await fetch(url, {
|
|
135
|
+
method: "POST",
|
|
136
|
+
headers: {
|
|
137
|
+
"Content-Type": "application/json",
|
|
138
|
+
Authorization: `Bearer ${bearerToken}`,
|
|
139
|
+
},
|
|
140
|
+
body: JSON.stringify({
|
|
141
|
+
contents,
|
|
142
|
+
targetLanguageCode: target,
|
|
143
|
+
sourceLanguageCode: source !== "auto" ? source : undefined,
|
|
144
|
+
mimeType: "text/plain",
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
const err = await response.text();
|
|
150
|
+
console.warn(`[Google Cloud Translate v3] Error ${response.status}: ${err}`);
|
|
151
|
+
return contents;
|
|
52
152
|
}
|
|
53
153
|
|
|
54
|
-
|
|
55
|
-
|
|
154
|
+
const data = await response.json();
|
|
155
|
+
if (data?.translations && Array.isArray(data.translations)) {
|
|
156
|
+
return data.translations.map((t: { translatedText: string }) => t.translatedText);
|
|
157
|
+
}
|
|
56
158
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent(
|
|
60
|
-
text
|
|
61
|
-
)}`;
|
|
62
|
-
|
|
63
|
-
const res = await fetch(url, {
|
|
64
|
-
headers: {
|
|
65
|
-
"User-Agent":
|
|
66
|
-
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
|
|
67
|
-
},
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
if (res.status === 429) {
|
|
71
|
-
console.warn(`[LynxSEO Translator] ⚠️ Rate limit 429 hit for ${targetLang}. Backing off for ${backoffDelay}ms...`);
|
|
72
|
-
await this.sleep(backoffDelay);
|
|
73
|
-
backoffDelay *= 2;
|
|
74
|
-
retries--;
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
159
|
+
return contents;
|
|
160
|
+
}
|
|
77
161
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
162
|
+
/**
|
|
163
|
+
* Safe Fallback with Strict Rate Limiter (Max 3 req/sec and exponential backoff)
|
|
164
|
+
*/
|
|
165
|
+
private static async callPublicFallbackApi(
|
|
166
|
+
contents: string[],
|
|
167
|
+
target: string,
|
|
168
|
+
source: string
|
|
169
|
+
): Promise<string[]> {
|
|
170
|
+
const results: string[] = [];
|
|
81
171
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
translated = data[0].map((item: any[]) => item[0]).join("");
|
|
86
|
-
}
|
|
172
|
+
for (const text of contents) {
|
|
173
|
+
// 300ms delay to stay well below limits
|
|
174
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
87
175
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
176
|
+
try {
|
|
177
|
+
const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${source}&tl=${target}&dt=t&q=${encodeURIComponent(text)}`;
|
|
178
|
+
const res = await fetch(url);
|
|
179
|
+
if (res.ok) {
|
|
180
|
+
const json = await res.json();
|
|
181
|
+
const translated = json?.[0]?.[0]?.[0] || text;
|
|
182
|
+
results.push(translated);
|
|
183
|
+
} else {
|
|
184
|
+
results.push(text);
|
|
96
185
|
}
|
|
97
|
-
|
|
98
|
-
|
|
186
|
+
} catch {
|
|
187
|
+
results.push(text);
|
|
99
188
|
}
|
|
100
189
|
}
|
|
101
190
|
|
|
102
|
-
return
|
|
191
|
+
return results;
|
|
103
192
|
}
|
|
104
193
|
|
|
105
194
|
/**
|
|
106
|
-
*
|
|
107
|
-
* Uses concatenated delimiters (|||) to translate 10 sentences in 1 single HTTP request!
|
|
195
|
+
* Translates an array of variations across 40 languages respecting the 6,000,000 CPM quota
|
|
108
196
|
*/
|
|
109
|
-
static async
|
|
197
|
+
static async translateAcrossAllLanguages(
|
|
110
198
|
sentences: string[],
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
199
|
+
targetLocales: string[],
|
|
200
|
+
sourceLocale = "en",
|
|
201
|
+
config: GoogleTranslateConfig = {}
|
|
114
202
|
): Promise<Record<string, string[]>> {
|
|
115
|
-
const
|
|
116
|
-
[
|
|
203
|
+
const output: Record<string, string[]> = {
|
|
204
|
+
[sourceLocale]: sentences,
|
|
117
205
|
};
|
|
118
206
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (targetLang === sourceLang) continue;
|
|
124
|
-
|
|
125
|
-
// Rate limit delay between target languages
|
|
126
|
-
await this.sleep(delayMs);
|
|
127
|
-
|
|
128
|
-
try {
|
|
129
|
-
const translatedBlock = await this.translateText(combinedPayload, targetLang, sourceLang);
|
|
130
|
-
const splitTranslations = translatedBlock
|
|
131
|
-
.split(/\|\|\||\| \| \|/)
|
|
132
|
-
.map((s) => s.trim());
|
|
133
|
-
|
|
134
|
-
if (splitTranslations.length === sentences.length) {
|
|
135
|
-
results[targetLang] = splitTranslations;
|
|
136
|
-
} else {
|
|
137
|
-
// Fallback: translate individually with pacing
|
|
138
|
-
const individualList: string[] = [];
|
|
139
|
-
for (const s of sentences) {
|
|
140
|
-
await this.sleep(200);
|
|
141
|
-
const t = await this.translateText(s, targetLang, sourceLang);
|
|
142
|
-
individualList.push(t);
|
|
143
|
-
}
|
|
144
|
-
results[targetLang] = individualList;
|
|
145
|
-
}
|
|
146
|
-
} catch (e) {
|
|
147
|
-
console.error(`[LynxSEO] Batch translation failed for ${targetLang}`, e);
|
|
148
|
-
results[targetLang] = sentences; // Graceful fallback
|
|
149
|
-
}
|
|
207
|
+
for (const locale of targetLocales) {
|
|
208
|
+
if (locale === sourceLocale) continue;
|
|
209
|
+
const translatedList = await this.translateBatch(sentences, locale, sourceLocale, config);
|
|
210
|
+
output[locale] = translatedList;
|
|
150
211
|
}
|
|
151
212
|
|
|
152
|
-
return
|
|
213
|
+
return output;
|
|
153
214
|
}
|
|
154
215
|
}
|