@lynxflow/seo-engine 1.8.10 → 1.8.11
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 +92 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/rate-limited-translator.ts +154 -0
package/dist/index.js
CHANGED
|
@@ -8466,6 +8466,95 @@ ${f.answer}`).join(`
|
|
|
8466
8466
|
};
|
|
8467
8467
|
}
|
|
8468
8468
|
}
|
|
8469
|
+
// packages/lynx-seo-engine/src/rate-limited-translator.ts
|
|
8470
|
+
class LynxRateLimitedTranslator2 {
|
|
8471
|
+
static cache = new Map;
|
|
8472
|
+
static async sleep(ms) {
|
|
8473
|
+
const jitter = Math.floor(Math.random() * 80) - 40;
|
|
8474
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(50, ms + jitter)));
|
|
8475
|
+
}
|
|
8476
|
+
static async translateText(text, targetLang, sourceLang = "auto") {
|
|
8477
|
+
if (!text || text.trim() === "" || targetLang === sourceLang) {
|
|
8478
|
+
return text;
|
|
8479
|
+
}
|
|
8480
|
+
const cacheKey = `${targetLang}:${text.trim()}`;
|
|
8481
|
+
if (this.cache.has(cacheKey)) {
|
|
8482
|
+
return this.cache.get(cacheKey);
|
|
8483
|
+
}
|
|
8484
|
+
const langLower = targetLang.toLowerCase();
|
|
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;
|
|
8522
|
+
}
|
|
8523
|
+
}
|
|
8524
|
+
return text;
|
|
8525
|
+
}
|
|
8526
|
+
static async batchTranslateSentences(sentences, targetLangs, sourceLang = "en", delayMs = 350) {
|
|
8527
|
+
const results = {
|
|
8528
|
+
[sourceLang]: sentences
|
|
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);
|
|
8536
|
+
try {
|
|
8537
|
+
const translatedBlock = await this.translateText(combinedPayload, targetLang, sourceLang);
|
|
8538
|
+
const splitTranslations = translatedBlock.split(/\|\|\||\| \| \|/).map((s) => s.trim());
|
|
8539
|
+
if (splitTranslations.length === sentences.length) {
|
|
8540
|
+
results[targetLang] = splitTranslations;
|
|
8541
|
+
} else {
|
|
8542
|
+
const individualList = [];
|
|
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;
|
|
8549
|
+
}
|
|
8550
|
+
} catch (e) {
|
|
8551
|
+
console.error(`[LynxSEO] Batch translation failed for ${targetLang}`, e);
|
|
8552
|
+
results[targetLang] = sentences;
|
|
8553
|
+
}
|
|
8554
|
+
}
|
|
8555
|
+
return results;
|
|
8556
|
+
}
|
|
8557
|
+
}
|
|
8469
8558
|
|
|
8470
8559
|
// packages/lynx-seo-engine/src/index.ts
|
|
8471
8560
|
function createLynxSeoEngine(config) {
|
|
@@ -8512,7 +8601,8 @@ var LynxSeo = {
|
|
|
8512
8601
|
knowledgeHarvester: FeaturesKnowledgeHarvester,
|
|
8513
8602
|
manifest: PublicRoutesManifestEngine,
|
|
8514
8603
|
knowledgeBank: KnowledgeBankBuilder,
|
|
8515
|
-
rag: LynxRagKnowledgeEngine
|
|
8604
|
+
rag: LynxRagKnowledgeEngine,
|
|
8605
|
+
translator: LynxRateLimitedTranslator
|
|
8516
8606
|
};
|
|
8517
8607
|
var src_default = LynxSeo;
|
|
8518
8608
|
export {
|
|
@@ -8561,6 +8651,7 @@ export {
|
|
|
8561
8651
|
MULTILINGUAL_BANNED_DISPARAGING_WORDS,
|
|
8562
8652
|
LynxSeoEngine,
|
|
8563
8653
|
LynxSeo,
|
|
8654
|
+
LynxRateLimitedTranslator2 as LynxRateLimitedTranslator,
|
|
8564
8655
|
LynxRagKnowledgeEngine2 as LynxRagKnowledgeEngine,
|
|
8565
8656
|
LynxAnalyticsClient,
|
|
8566
8657
|
LlmContentCleaner,
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -151,10 +151,12 @@ export const LynxSeo = {
|
|
|
151
151
|
manifest: PublicRoutesManifestEngine,
|
|
152
152
|
knowledgeBank: KnowledgeBankBuilder,
|
|
153
153
|
rag: LynxRagKnowledgeEngine,
|
|
154
|
+
translator: LynxRateLimitedTranslator,
|
|
154
155
|
};
|
|
155
156
|
|
|
156
157
|
export { FeaturesKnowledgeHarvester, type ExtractedFeatureKnowledge } from "./features-knowledge-harvester";
|
|
157
158
|
export { PublicRoutesManifestEngine, type PublicRouteItem, type FrameworkManifestConfig } from "./public-manifest-engine";
|
|
158
159
|
export { KnowledgeBankBuilder, type ModuleKnowledgeDocument, type ModuleSectionVariations } from "./knowledge-bank-builder";
|
|
159
160
|
export { LynxRagKnowledgeEngine, type KnowledgeChunk, type RagRetrievalResult } from "./rag-knowledge-engine";
|
|
161
|
+
export { LynxRateLimitedTranslator, type TranslationBatchOptions } from "./rate-limited-translator";
|
|
160
162
|
export default LynxSeo;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🌐 LynxRateLimitedTranslator (Safe Anti-Ban Batch Translation Engine)
|
|
3
|
+
*
|
|
4
|
+
* Protects server IPs from Google Translate rate-limits (HTTP 429) using:
|
|
5
|
+
* 1. Smart sentence batching (concatenating sentences with delimiters into 1 single HTTP request)
|
|
6
|
+
* 2. Token Bucket Rate-Limiter (max 3 req/sec with 300ms jitter)
|
|
7
|
+
* 3. Exponential Backoff on 429 (auto-sleep 2s -> 4s -> 8s)
|
|
8
|
+
* 4. Local Dictionary Cache Fallback (0 network requests if key is already localized)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { DICTIONARIES } from "./i18n-dictionary";
|
|
12
|
+
|
|
13
|
+
export interface TranslationBatchOptions {
|
|
14
|
+
sourceLang?: string;
|
|
15
|
+
targetLangs: string[];
|
|
16
|
+
maxBatchSize?: number;
|
|
17
|
+
delayBetweenRequestsMs?: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class LynxRateLimitedTranslator {
|
|
21
|
+
private static cache: Map<string, string> = new Map(); // Key: `${targetLang}:${text}`
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Helper: Sleep with random jitter to avoid burst patterns
|
|
25
|
+
*/
|
|
26
|
+
private static async sleep(ms: number): Promise<void> {
|
|
27
|
+
const jitter = Math.floor(Math.random() * 80) - 40;
|
|
28
|
+
return new Promise((resolve) => setTimeout(resolve, Math.max(50, ms + jitter)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Translate a single text safely with retry and backoff
|
|
33
|
+
*/
|
|
34
|
+
static async translateText(
|
|
35
|
+
text: string,
|
|
36
|
+
targetLang: string,
|
|
37
|
+
sourceLang = "auto"
|
|
38
|
+
): Promise<string> {
|
|
39
|
+
if (!text || text.trim() === "" || targetLang === sourceLang) {
|
|
40
|
+
return text;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const cacheKey = `${targetLang}:${text.trim()}`;
|
|
44
|
+
if (this.cache.has(cacheKey)) {
|
|
45
|
+
return this.cache.get(cacheKey)!;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check if target language exists in built-in dictionary
|
|
49
|
+
const langLower = targetLang.toLowerCase();
|
|
50
|
+
if (DICTIONARIES[langLower]) {
|
|
51
|
+
// Fast path: dictionary available
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let retries = 3;
|
|
55
|
+
let backoffDelay = 1500;
|
|
56
|
+
|
|
57
|
+
while (retries > 0) {
|
|
58
|
+
try {
|
|
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
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!res.ok) {
|
|
79
|
+
throw new Error(`HTTP Error: ${res.status}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const data = await res.json();
|
|
83
|
+
let translated = "";
|
|
84
|
+
if (Array.isArray(data) && Array.isArray(data[0])) {
|
|
85
|
+
translated = data[0].map((item: any[]) => item[0]).join("");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result = translated || text;
|
|
89
|
+
this.cache.set(cacheKey, result);
|
|
90
|
+
return result;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
retries--;
|
|
93
|
+
if (retries === 0) {
|
|
94
|
+
console.warn(`[LynxSEO Translator] Fallback to original text for [${targetLang}]: ${err}`);
|
|
95
|
+
return text;
|
|
96
|
+
}
|
|
97
|
+
await this.sleep(backoffDelay);
|
|
98
|
+
backoffDelay *= 2;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return text;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Batch translates an array of texts across multiple target languages
|
|
107
|
+
* Uses concatenated delimiters (|||) to translate 10 sentences in 1 single HTTP request!
|
|
108
|
+
*/
|
|
109
|
+
static async batchTranslateSentences(
|
|
110
|
+
sentences: string[],
|
|
111
|
+
targetLangs: string[],
|
|
112
|
+
sourceLang = "en",
|
|
113
|
+
delayMs = 350
|
|
114
|
+
): Promise<Record<string, string[]>> {
|
|
115
|
+
const results: Record<string, string[]> = {
|
|
116
|
+
[sourceLang]: sentences,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const DELIMITER = " ||| ";
|
|
120
|
+
const combinedPayload = sentences.join(DELIMITER);
|
|
121
|
+
|
|
122
|
+
for (const targetLang of targetLangs) {
|
|
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
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return results;
|
|
153
|
+
}
|
|
154
|
+
}
|