@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 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 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;
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 text;
8554
+ return contents;
8525
8555
  }
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);
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 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;
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
- 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;
8568
+ results.push(text);
8549
8569
  }
8550
- } catch (e) {
8551
- console.error(`[LynxSEO] Batch translation failed for ${targetLang}`, e);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@lynxflow/seo-engine",
3
- "version": "1.8.11",
3
+ "version": "1.8.12",
4
4
  "description": "High-Performance Multilingual Programmatic SEO & AI Search Engine SDK",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
@@ -1,154 +1,215 @@
1
1
  /**
2
- * 🌐 LynxRateLimitedTranslator (Safe Anti-Ban Batch Translation Engine)
2
+ * 🌐 Official Google Cloud Translation API v3 / v2 Enterprise Client
3
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)
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 TranslationBatchOptions {
14
- sourceLang?: string;
15
- targetLangs: string[];
16
- maxBatchSize?: number;
17
- delayBetweenRequestsMs?: number;
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(); // Key: `${targetLang}:${text}`
28
+ private static cache: Map<string, string> = new Map();
22
29
 
23
30
  /**
24
- * Helper: Sleep with random jitter to avoid burst patterns
31
+ * Translates an array of strings in 1 native batch request according to Google Cloud specs
25
32
  */
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)));
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
- * Translate a single text safely with retry and backoff
85
+ * Official Google Cloud Translation API v2
86
+ * https://translation.googleapis.com/language/translate/v2
33
87
  */
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;
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 cacheKey = `${targetLang}:${text.trim()}`;
44
- if (this.cache.has(cacheKey)) {
45
- return this.cache.get(cacheKey)!;
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
- // Check if target language exists in built-in dictionary
49
- const langLower = targetLang.toLowerCase();
50
- if (DICTIONARIES[langLower]) {
51
- // Fast path: dictionary available
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
- let retries = 3;
55
- let backoffDelay = 1500;
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
- 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
- }
159
+ return contents;
160
+ }
77
161
 
78
- if (!res.ok) {
79
- throw new Error(`HTTP Error: ${res.status}`);
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
- 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
- }
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
- 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;
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
- await this.sleep(backoffDelay);
98
- backoffDelay *= 2;
186
+ } catch {
187
+ results.push(text);
99
188
  }
100
189
  }
101
190
 
102
- return text;
191
+ return results;
103
192
  }
104
193
 
105
194
  /**
106
- * Batch translates an array of texts across multiple target languages
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 batchTranslateSentences(
197
+ static async translateAcrossAllLanguages(
110
198
  sentences: string[],
111
- targetLangs: string[],
112
- sourceLang = "en",
113
- delayMs = 350
199
+ targetLocales: string[],
200
+ sourceLocale = "en",
201
+ config: GoogleTranslateConfig = {}
114
202
  ): Promise<Record<string, string[]>> {
115
- const results: Record<string, string[]> = {
116
- [sourceLang]: sentences,
203
+ const output: Record<string, string[]> = {
204
+ [sourceLocale]: sentences,
117
205
  };
118
206
 
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
- }
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 results;
213
+ return output;
153
214
  }
154
215
  }