@yxw007/translate 0.1.5 → 0.2.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.
@@ -1,8 +1,11 @@
1
- // translate v0.1.5 Copyright (c) 2024 Potter<aa4790139@gmail.com> and contributors
1
+ // translate v0.2.0 Copyright (c) 2025 Potter<aa4790139@gmail.com> and contributors
2
2
  'use strict';
3
3
 
4
4
  Object.defineProperty(exports, '__esModule', { value: true });
5
5
 
6
+ require('fs/promises');
7
+ require('fs');
8
+ require('path');
6
9
  var clientTranslate = require('@aws-sdk/client-translate');
7
10
 
8
11
  class TranslationError extends Error {
@@ -117,6 +120,9 @@ function useLogger(name = "") {
117
120
  };
118
121
  }
119
122
 
123
+ function sleep(ms) {
124
+ return new Promise((resolve) => setTimeout(resolve, ms));
125
+ }
120
126
  function getGapLine() {
121
127
  return "-".repeat(20);
122
128
  }
@@ -134,6 +140,66 @@ async function throwResponseError(name, res) {
134
140
  catch (e) { }
135
141
  return new TranslationError(name, `Translate fail ! ${res.status}: ${res.statusText} ${bodyRes?.message ?? ""}`);
136
142
  }
143
+ function splitText(text, maxCharacterNum) {
144
+ const SPLIT_PRIORITY = [
145
+ /\n\n+/, // 段落分隔(优先保留空行)
146
+ /[.。!??!\n]/, // 中日韩句子结束符+英文标点+换行
147
+ /[;;]/, // 分号(中英文)
148
+ /[,,]/g, // 逗号(中英文)
149
+ /\s/, // 空格(避免切分单词)
150
+ ];
151
+ const BEST_MATCH_RATIO = 0.7;
152
+ const chunks = [];
153
+ while (text.length > 0) {
154
+ const chunk = text.slice(0, maxCharacterNum);
155
+ // Scene 1:Prioritization of cases not subject to severance
156
+ if (text.length <= maxCharacterNum) {
157
+ chunks.push(text);
158
+ break;
159
+ }
160
+ // Scene 2:Finding Split Points by Priority
161
+ let splitPos = -1;
162
+ for (const delimiter of SPLIT_PRIORITY) {
163
+ const regex = new RegExp(delimiter.source + "(?=[^]*)", "g"); // back-to-front search
164
+ let m, longestMatch;
165
+ while ((m = regex.exec(chunk)) !== null) {
166
+ if (m.index === regex.lastIndex) {
167
+ regex.lastIndex++;
168
+ }
169
+ if (longestMatch != null) {
170
+ longestMatch = m.index > longestMatch.index ? m : longestMatch;
171
+ }
172
+ else {
173
+ longestMatch = m;
174
+ }
175
+ }
176
+ if (longestMatch?.index !== undefined && longestMatch.index >= maxCharacterNum * BEST_MATCH_RATIO) {
177
+ splitPos = longestMatch.index;
178
+ break; // Finding Quality Split Points
179
+ }
180
+ }
181
+ // Scene 3:Conservative splitting in the absence of a suitable separator
182
+ if (splitPos === -1) {
183
+ splitPos = chunk.lastIndexOf(" ", maxCharacterNum); // look for the space
184
+ splitPos = splitPos === -1 ? maxCharacterNum : splitPos; // forcible division
185
+ }
186
+ if (splitPos == 0) {
187
+ text = text.slice(splitPos + 1);
188
+ }
189
+ else {
190
+ chunks.push(text.slice(0, splitPos));
191
+ text = text.slice(splitPos);
192
+ }
193
+ }
194
+ return chunks;
195
+ }
196
+ function isOverMaxCharacterNum(text, max_character_num) {
197
+ if (!text || text.length <= 0) {
198
+ return false;
199
+ }
200
+ const total = text.reduce((pre, cur) => pre + cur.length, 0);
201
+ return total > max_character_num;
202
+ }
137
203
 
138
204
  function google(options) {
139
205
  const base = "https://translate.googleapis.com/translate_a/single";
@@ -1450,25 +1516,29 @@ function deepl$2(options) {
1450
1516
  if (!Array.isArray(text)) {
1451
1517
  text = [text];
1452
1518
  }
1453
- const requestBody = JSON.stringify({
1454
- text,
1455
- source_lang: from === "auto" ? undefined : from,
1456
- target_lang: to,
1457
- });
1458
1519
  const res = await fetch(url, {
1459
1520
  method: "POST",
1460
1521
  headers: {
1461
- "Content-Type": "application/json; charset=UTF-8;",
1522
+ "Content-Type": "application/json; charset=UTF-8",
1462
1523
  Authorization: `DeepL-Auth-Key ${key}`,
1524
+ Accept: "*/*",
1525
+ Host: "api-free.deepl.com",
1463
1526
  Connection: "keep-alive",
1464
1527
  },
1465
- body: requestBody,
1528
+ body: JSON.stringify({
1529
+ text: text,
1530
+ source_lang: from === "auto" ? undefined : from,
1531
+ target_lang: to,
1532
+ }),
1466
1533
  });
1467
1534
  if (!res.ok) {
1468
1535
  throw await throwResponseError(this.name, res);
1469
1536
  }
1470
1537
  const bodyRes = await res.json();
1471
- const body = bodyRes?.translations;
1538
+ if (bodyRes.error) {
1539
+ throw new TranslationError(this.name, `Translate fail ! code: ${bodyRes.error.code}, message: ${bodyRes.error.message}`);
1540
+ }
1541
+ const body = bodyRes.translations;
1472
1542
  if (!body || body.length === 0) {
1473
1543
  throw new TranslationError(this.name, "Translate fail ! translate's result is null or empty");
1474
1544
  }
@@ -2375,15 +2445,20 @@ function getLanguage(engine) {
2375
2445
  }
2376
2446
 
2377
2447
  const appName = "Translate";
2448
+ const defaultMaxCharacterNum = 1000;
2378
2449
 
2379
2450
  const logger = useLogger();
2380
2451
  const cache = new Cache();
2381
2452
  class Translator {
2382
2453
  engines;
2383
2454
  cache_time;
2384
- constructor(cache_time = 60 * 1000) {
2455
+ concurrencyMax;
2456
+ concurrencyDelay;
2457
+ constructor(cache_time = 60 * 1000, concurrencyMax = 4, concurrencyDelay = 20) {
2385
2458
  this.engines = new Map();
2386
2459
  this.cache_time = cache_time;
2460
+ this.concurrencyMax = concurrencyMax;
2461
+ this.concurrencyDelay = concurrencyDelay;
2387
2462
  }
2388
2463
  /**
2389
2464
  * This method is obsolete, please use the addEngine method
@@ -2431,8 +2506,7 @@ class Translator {
2431
2506
  if (cache.get(key)) {
2432
2507
  return Promise.resolve(cache.get(key)?.value);
2433
2508
  }
2434
- return engineInstance
2435
- .translate(text, options)
2509
+ return this.concurrencyHandle(engineInstance, text, options)
2436
2510
  .then((translated) => {
2437
2511
  cache.set(key, translated, cache_time ?? this.cache_time);
2438
2512
  return translated;
@@ -2447,6 +2521,55 @@ class Translator {
2447
2521
  }
2448
2522
  });
2449
2523
  }
2524
+ async concurrencyHandle(engine, text, options) {
2525
+ const { max_character_num = defaultMaxCharacterNum } = options;
2526
+ const maxCharacterNum = max_character_num > 0 ? max_character_num : defaultMaxCharacterNum;
2527
+ if (Array.isArray(text)) {
2528
+ if (isOverMaxCharacterNum(text, max_character_num)) {
2529
+ throw new TranslationError(appName, "String arrays do not support automatic character splitting, and the total number of characters in a string array exceeds the limit on the number of translated characters.");
2530
+ }
2531
+ return engine.translate(text, options);
2532
+ }
2533
+ else {
2534
+ return this.concurrencyTranslate(engine, text, options, maxCharacterNum);
2535
+ }
2536
+ }
2537
+ async concurrencyTranslate(engine, text, options, maxCharacterMum) {
2538
+ const pendingTasks = splitText(text, maxCharacterMum).map((content, index) => ({ content, index }));
2539
+ const result = [];
2540
+ let activeTasks = 0;
2541
+ const concurrencyDelay = this.concurrencyDelay;
2542
+ const concurrencyMax = this.concurrencyMax;
2543
+ return new Promise((resolve, reject) => {
2544
+ function processTasks() {
2545
+ while (activeTasks < concurrencyMax && pendingTasks.length > 0) {
2546
+ const { content, index } = pendingTasks.shift();
2547
+ activeTasks++;
2548
+ engine
2549
+ .translate(content, options)
2550
+ .then((res) => {
2551
+ result.push({
2552
+ translated: res,
2553
+ index,
2554
+ });
2555
+ })
2556
+ .catch((error) => reject(error))
2557
+ .finally(async () => {
2558
+ activeTasks--;
2559
+ if (activeTasks === 0 && pendingTasks.length <= 0) {
2560
+ result.sort((a, b) => a.index - b.index);
2561
+ const arr = result.reduce((pre, cur) => pre.concat(cur.translated), []);
2562
+ return resolve([arr.join("")]);
2563
+ }
2564
+ await sleep(concurrencyDelay);
2565
+ processTasks();
2566
+ });
2567
+ }
2568
+ }
2569
+ processTasks();
2570
+ return result;
2571
+ });
2572
+ }
2450
2573
  }
2451
2574
  const translator = new Translator();
2452
2575
  var index = {