@lynxflow/seo-engine 1.8.10 → 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 +123 -1
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/rate-limited-translator.ts +215 -0
package/dist/index.js
CHANGED
|
@@ -8466,6 +8466,126 @@ ${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 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);
|
|
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);
|
|
8553
|
+
}
|
|
8554
|
+
return contents;
|
|
8555
|
+
}
|
|
8556
|
+
static async callPublicFallbackApi(contents, target, source) {
|
|
8557
|
+
const results = [];
|
|
8558
|
+
for (const text of contents) {
|
|
8559
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
8560
|
+
try {
|
|
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);
|
|
8567
|
+
} else {
|
|
8568
|
+
results.push(text);
|
|
8569
|
+
}
|
|
8570
|
+
} catch {
|
|
8571
|
+
results.push(text);
|
|
8572
|
+
}
|
|
8573
|
+
}
|
|
8574
|
+
return results;
|
|
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
|
+
}
|
|
8588
|
+
}
|
|
8469
8589
|
|
|
8470
8590
|
// packages/lynx-seo-engine/src/index.ts
|
|
8471
8591
|
function createLynxSeoEngine(config) {
|
|
@@ -8512,7 +8632,8 @@ var LynxSeo = {
|
|
|
8512
8632
|
knowledgeHarvester: FeaturesKnowledgeHarvester,
|
|
8513
8633
|
manifest: PublicRoutesManifestEngine,
|
|
8514
8634
|
knowledgeBank: KnowledgeBankBuilder,
|
|
8515
|
-
rag: LynxRagKnowledgeEngine
|
|
8635
|
+
rag: LynxRagKnowledgeEngine,
|
|
8636
|
+
translator: LynxRateLimitedTranslator
|
|
8516
8637
|
};
|
|
8517
8638
|
var src_default = LynxSeo;
|
|
8518
8639
|
export {
|
|
@@ -8561,6 +8682,7 @@ export {
|
|
|
8561
8682
|
MULTILINGUAL_BANNED_DISPARAGING_WORDS,
|
|
8562
8683
|
LynxSeoEngine,
|
|
8563
8684
|
LynxSeo,
|
|
8685
|
+
LynxRateLimitedTranslator2 as LynxRateLimitedTranslator,
|
|
8564
8686
|
LynxRagKnowledgeEngine2 as LynxRagKnowledgeEngine,
|
|
8565
8687
|
LynxAnalyticsClient,
|
|
8566
8688
|
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,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🌐 Official Google Cloud Translation API v3 / v2 Enterprise Client
|
|
3
|
+
*
|
|
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)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { DICTIONARIES } from "./i18n-dictionary";
|
|
13
|
+
|
|
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;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class LynxRateLimitedTranslator {
|
|
28
|
+
private static cache: Map<string, string> = new Map();
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Translates an array of strings in 1 native batch request according to Google Cloud specs
|
|
32
|
+
*/
|
|
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;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Official Google Cloud Translation API v2
|
|
86
|
+
* https://translation.googleapis.com/language/translate/v2
|
|
87
|
+
*/
|
|
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;
|
|
111
|
+
}
|
|
112
|
+
|
|
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);
|
|
116
|
+
}
|
|
117
|
+
|
|
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;
|
|
152
|
+
}
|
|
153
|
+
|
|
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
|
+
}
|
|
158
|
+
|
|
159
|
+
return contents;
|
|
160
|
+
}
|
|
161
|
+
|
|
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[] = [];
|
|
171
|
+
|
|
172
|
+
for (const text of contents) {
|
|
173
|
+
// 300ms delay to stay well below limits
|
|
174
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
175
|
+
|
|
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);
|
|
185
|
+
}
|
|
186
|
+
} catch {
|
|
187
|
+
results.push(text);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return results;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Translates an array of variations across 40 languages respecting the 6,000,000 CPM quota
|
|
196
|
+
*/
|
|
197
|
+
static async translateAcrossAllLanguages(
|
|
198
|
+
sentences: string[],
|
|
199
|
+
targetLocales: string[],
|
|
200
|
+
sourceLocale = "en",
|
|
201
|
+
config: GoogleTranslateConfig = {}
|
|
202
|
+
): Promise<Record<string, string[]>> {
|
|
203
|
+
const output: Record<string, string[]> = {
|
|
204
|
+
[sourceLocale]: sentences,
|
|
205
|
+
};
|
|
206
|
+
|
|
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;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return output;
|
|
214
|
+
}
|
|
215
|
+
}
|