1async function asyncMapStrict(arr, fn) { 2 const result = []; 3 // console.time("Elapsed time :"); 4 for (let idx = 0; idx < arr.length; idx += 1) { 5 const cur = arr[idx]; 6 await new Promise((resolve) => setTimeout(resolve, 0.0001)); 7 8 result.push(await fn(cur, idx, arr)); 9 } 10 // console.timeEnd("Elapsed time :") 11 return result; 12} 13 14function sanitize(text) { 15 return text 16 .replace(/[^\p{L}\p{M}'ââ¿-]/gu, "") 17 .replaceAll("â", "'") 18 .normalize("NFKC"); 19} 20 21/** 22 * Memoizes the given function in local storage. 23 * @param {Function} fn - The function to memoize. 24 * @param {Object} options - Memoization options. 25 * @param {number} options.ttl - Time to live for cached results in milliseconds. 26 * @param {boolean} options.backgroundRefresh - Whether to refresh the cache in the background. 27 * @throws {Error} Throws an error if the function is anonymous. 28 * @returns {Function} Returns the memoized function. 29 */ 30/** 31 * Memoizes both sync and async functions in localStorage with TTL support 32 * @param {Function} fn - The function to memoize 33 * @param {Object} options - Memoization options 34 * @param {number} options.ttl - Time to live in milliseconds 35 * @param {boolean} options.backgroundRefresh - Whether to refresh cache in background 36 */ 37function memoizeLocalStorage( 38 fn, 39 options = { ttl: 100, backgroundRefresh: false }, 40) { 41 if (!fn.name) { 42 console.warn("Warning: memoizeLocalStorage called with anonymous function"); 43 throw new Error("memoizeLocalStorage only accepts non-anonymous functions"); 44 } 45 46 let cache = JSON.parse(localStorage.getItem(fn.name) || "{}"); 47 const debug = options.debug || false; 48 49 function log(...args) { 50 if (debug) console.log(`[Memoize:${fn.name}]`, ...args); 51 } 52 53 async function executeAndCache(args, argsKey) { 54 log("Executing function with args:", args); 55 try { 56 const result = fn(...args); 57 const isPromise = result instanceof Promise; 58 59 const finalResult = isPromise ? await result : result; 60 const cacheEntry = { 61 expiration: Date.now() + options.ttl, 62 result: finalResult, 63 isPromise, 64 }; 65 66 cache[fn.name] = { 67 ...cache[fn.name], 68 [argsKey]: cacheEntry, 69 }; 70 71 try { 72 localStorage.setItem(fn.name, JSON.stringify(cache)); 73 log("Cached result:", finalResult); 74 } catch (e) { 75 console.warn(`Cache storage failed for ${fn.name}:`, e); 76 // Clear old entries if storage fails 77 cache = { [fn.name]: { [argsKey]: cacheEntry } }; 78 localStorage.setItem(fn.name, JSON.stringify(cache)); 79 } 80 81 return finalResult; 82 } catch (error) { 83 console.error(`Execution failed for ${fn.name}:`, error); 84 throw error; 85 } 86 } 87 88 return function memoized(...args) { 89 const argsKey = JSON.stringify(args); 90 const now = Date.now(); 91 const currentCache = cache[fn.name]?.[argsKey]; 92 93 if (currentCache && currentCache.expiration > now) { 94 log("Cache hit"); 95 96 // Background refresh near expiration 97 if ( 98 options.backgroundRefresh && 99 currentCache.expiration - now < options.ttl * 0.2 100 ) { 101 log("Starting background refresh"); 102 executeAndCache(args, argsKey).catch((error) => 103 console.error(`Background refresh failed for ${fn.name}:`, error), 104 ); 105 } 106 107 // Return cached result, wrapping in Promise if original was async 108 return currentCache.isPromise 109 ? Promise.resolve(currentCache.result) 110 : currentCache.result; 111 } 112 113 log("Cache miss"); 114 return executeAndCache(args, argsKey); 115 }; 116} 117 118// Utility functions for cache management 119memoizeLocalStorage.clearCache = function (fnName) { 120 if (fnName) { 121 localStorage.removeItem(fnName); 122 } else { 123 Object.keys(localStorage) 124 .filter((key) => localStorage.getItem(key).includes('"expiration":')) 125 .forEach((key) => localStorage.removeItem(key)); 126 } 127}; 128 129memoizeLocalStorage.getCacheStats = function (fnName) { 130 const cache = JSON.parse(localStorage.getItem(fnName) || "{}"); 131 const now = Date.now(); 132 133 return {
134 totalEntries: Object.keys(cache[fnName] || {}).length, 135 validEntries: Object.values(cache[fnName] || {}).filter( 136 (entry) => entry.expiration > now, 137 ).length, 138 oldestEntry: Math.min( 139 ...Object.values(cache[fnName] || {}).map((entry) => entry.expiration), 140 ), 141 newestEntry: Math.max( 142 ...Object.values(cache[fnName] || {}).map((entry) => entry.expiration), 143 ), 144 }; 145}; 146 147async function wait(ms = 1) { 148 await new Promise((resolve) => setTimeout(resolve, ms)); 149} 150 151function clearStorage() { 152 // const cache = JSON.parse(localStorage.getItem("get_ipa_no_cache") || "{}"); 153 // cache["get_ipa_no_cache"] = ""; 154 // localStorage.setItem("get_ipa_no_cache", JSON.stringify(cache)); 155 localStorage.clear(); 156 localforage.clear(); 157} 158 159function updateLoadingText(filePath, fileExtension, rawText) { 160 const loadingBar = document.getElementById("loading_text"); 161 rawText = rawText || ""; 162 163 if (!filePath && !fileExtension && !rawText) { 164 loadingBar.innerHTML = ""; 165 loadingBar.style.display = "none"; 166 } else if (filePath && fileExtension) { 167 loadingBar.innerHTML = `Loading ${filePath}.${fileExtension}`; 168 loadingBar.style.display = "block"; 169 } else if (rawText) { 170 loadingBar.innerHTML = rawText; 171 loadingBar.style.display = "block"; 172 } 173} 174 175/** 176 * Helper function to encapsulate the repeated lexicon lookup logic. 177 * @param {string} lexiconName - The name of the lexicon (e.g., "German"). 178 * @param {string} text - The text to look up. 179 * @returns {string|null} The dictionary record or null if not found. 180 */ 181function lookupInLexicon(lexiconName, text) { 182 if (!globalThis.lexicon || !globalThis.lexicon[lexiconName]) { 183 return null; 184 } 185 const lexicon = globalThis.lexicon[lexiconName]; 186 const cleanedForLookup = text.replace(/[^\p{Letter}\p{Mark}-]+/gu, ""); 187 188 let dictRecord = lexicon.get(cleanedForLookup); 189 if (!dictRecord) { 190 dictRecord = lexicon.get(cleanedForLookup.toLowerCase()); 191 } 192 193 console.log(`Lexicon lookup for "${text}" in ${lexiconName}:`, dictRecord); 194 return dictRecord; 195} 196 197/** 198 * A map of language-specific handlers to replace the main switch statement. 199 * Each handler receives a context object and returns the calculated IPA string. 200 */ 201const ipaHandlers = { 202 // --- Group 1: Unique/Complex Handlers --- 203 204 Latin: ({ cleanText, langStyle, langForm }) => { 205 const isPhonetic = langForm === "Phonetic"; 206 const options = { 207 Ecclesiastical: { ecclesiastical: true, vulgar: false }, 208 Classical: { ecclesiastical: false, vulgar: false }, 209 Vulgar: { ecclesiastical: false, vulgar: true }, 210 }; 211 const styleOptions = options[langStyle]; 212 if (!styleOptions) return null; 213
214 return window.la_ipa.convert_words( 215 cleanText, 216 isPhonetic, 217 styleOptions.ecclesiastical, 218 styleOptions.vulgar, 219 ); 220 }, 221 222 Portuguese: ({ cleanText, langStyle, langForm }) => { 223 const region = langStyle === "Brazil" ? "rio" : "pt"; 224 const result = window.pt_ipa.IPA(cleanText, region); 225 return result?.[0]?.[langForm.toLowerCase()]; 226 }, 227 228 Spanish: ({ cleanText, langStyle, langForm }) => { 229 const dialect = 230 langStyle === "Castilian" ? "distincion-yeismo" : "seseo-yeismo"; 231 const isPhonetic = langForm === "Phonetic"; 232 const result = window.es_ipa.IPA(cleanText, dialect, isPhonetic); 233 return result?.text; 234 }, 235 236 Ukrainian: ({ cleanText, langForm }) => { 237 if (langForm !== "Phonetic") return null; 238 239 // --- Stage 1: Check for a definitive version in the dictionary --- 240 if (globalThis.lexicon?.["Ukrainian"] && cleanText.trim().length > 0) { 241 const dictRecord = 242 globalThis.lexicon["Ukrainian"].get( 243 cleanText.replace(/[^\p{Letter}\p{Mark}-]+/gu, ""), 244 ) || 245 globalThis.lexicon["Ukrainian"].get( 246 cleanText.replace(/[^\p{Letter}\p{Mark}-]+/gu, "").toLowerCase(), 247 ); 248 if (dictRecord) { 249 console.log("Ukrainian: Found in dictionary:", dictRecord); 250 251 // 1. Split by comma in case there are multiple forms (e.g., "замоÌк,заÌмок"). 252 // 2. Trim whitespace from each resulting word. 253 const stressedForms = dictRecord.split(",").map((word) => word.trim()); 254 255 // Generate an IPA for each stressed form provided by the dictionary. 256 const ipas = stressedForms 257 .map((form) => window.uk_ipa.pronunciation(form, true)) 258 .filter((ipa) => ipa) // Filter out any unsuccessful IPA generations 259 .map((ipa) => `/${ipa}/`) // Format each successful IPA 260 .join(","); // Join into the final string, e.g., "/ipa1/,/ipa2/" 261 262 return ipas || null; 263 } 264 } 265 266 // --- Stage 2: If not in dictionary, analyze and process --- 267 const VOWEL_REGEX = /[аеиÑоÑÑÑÑÑ]/i; 268 const STRESS_MARK = "\u0301"; 269 const applyStress = (vowel) => vowel + STRESS_MARK; 270 271 const syllables = cleanText.match(new RegExp(VOWEL_REGEX.source, "gi")); 272 const syllableCount = syllables ? syllables.length : 0; 273 274 // Case A: 0 or 1 syllables. Stress it and return a single IPA. 275 if (syllableCount <= 1) { 276 const stressedText = 277 syllableCount === 1 278 ? cleanText.replace(/[аеиÑоÑÑÑÑÑÐÐÐÐÐУÐЮЯÐ]/, applyStress) 279 : cleanText; 280 return window.uk_ipa.pronunciation(stressedText, true); 281 } 282 283 // Case B (Concise): Multi-syllable. Generate all variants using a functional chain. 284 const possibleIPAs = [...cleanText] 285 .map((char, index) => (VOWEL_REGEX.test(char) ? index : -1)) // Get indices of all vowels 286 .filter((index) => index !== -1) // Filter out non-vowel markers 287 .map((index) => { 288 // For each vowel index, create a stressed word and get its IPA 289 const stressedVowel = applyStress(cleanText[index]); 290 const stressedWord = 291 cleanText.slice(0, index) + 292 stressedVowel + 293 cleanText.slice(index + 1); 294 return window.uk_ipa.pronunciation(stressedWord, true); 295 }) 296 .filter((ipa) => ipa) // Filter out any unsuccessful IPA generations 297 .map((ipa) => `/${ipa}/`) // Format each successful IPA 298 .join(","); // Join into the final string 299 300 return possibleIPAs || null; // Return the generated string or null if empty 301 }, 302 Russian: ({ cleanText, langForm }) => { 303 if (langForm !== "Phonetic") return null; 304 305 // --- Stage 1: Check for definitive versions in the dictionary (Unchanged) --- 306 if (globalThis.lexicon?.["Russian"] && cleanText.trim().length > 0) { 307 const cleanedKey = cleanText.replace(/[^\p{Letter}\p{Mark}-]+/gu, ""); 308 309 const dictRecord = 310 globalThis.lexicon["Russian"].get(cleanedKey) || 311 globalThis.lexicon["Russian"].get(cleanedKey.toLowerCase()); 312 313 function prioritizeStressedIpa(ipaString) { 314 return ( 315 ipaString && 316 Object.values(
317 // Use a Set to efficiently handle identical duplicates from the start 318 Array.from(new Set(ipaString.split(","))) 319 .map((ipa) => ipa.trim().replace(/^\/|\/$/g, "")) // Clean up each IPA string 320 .filter(Boolean) // Remove any empty strings 321 .reduce((best, ipa) => { 322 // Reduce the array to an object of the best versions 323 const base = ipa.replace(/Ë/g, ""); // The key for grouping is the IPA without stress 324 const existing = best[base]; 325 // The new `ipa` is better if no version exists yet, or if the new one is stressed and the old one isn't. 326 if ( 327 !existing || 328 (ipa.includes("Ë") && !existing.includes("Ë")) 329 ) { 330 best[base] = ipa; 331 } 332 return best; 333 }, {}), 334 ) 335 .map((ipa) => `/${ipa}/`) 336 .join(",") 337 ); // Format the final values back into a string 338 } 339 340 if (dictRecord) { 341 console.log("Russian: Found in dictionary:", dictRecord); 342 const stressedForms = dictRecord.split(",").map((word) => word.trim()); 343 const ipas = stressedForms 344 .map((form) => window.ru_ipa.ipa_string(form)) 345 .filter((ipa) => ipa) 346 .map((ipa) => `/${ipa}/`) 347 .join(","); 348 return prioritizeStressedIpa(ipas) || null; 349 } 350 } 351 352 // --- Stage 2: Fallback logic for words not in the dictionary --- 353 const VOWEL_REGEX = /[аÑиÑеÑÑÑоÑ]/i; // Russian vowels 354 const STRESS_MARK = "\u0301"; 355 const applyStress = (vowel) => vowel + STRESS_MARK; 356 357 const syllables = cleanText.match(new RegExp(VOWEL_REGEX.source, "gi")); 358 const syllableCount = syllables ? syllables.length : 0; 359 360 // Case A: 0 or 1 syllables. Stress it and return a single IPA. 361 if (syllableCount <= 1) { 362 const stressedText = 363 syllableCount === 1 364 ? cleanText.replace(/[аÑиÑеÑÑÑоÐÐÐУÐЮЯÐÐЫ]/, applyStress) // Russian vowel set 365 : cleanText; 366 const singleIpa = window.ru_ipa.ipa_string(stressedText); // Russian IPA function 367 return singleIpa ? `/${singleIpa}/` : null; 368 } 369 370 // Case B: Multi-syllable. Generate all variants using a functional chain. 371 const possibleIPAs = [...cleanText] 372 .map((char, index) => (VOWEL_REGEX.test(char) ? index : -1)) 373 .filter((index) => index !== -1) 374 .map((index) => { 375 const stressedVowel = applyStress(cleanText[index]); 376 const stressedWord = 377 cleanText.slice(0, index) + 378 stressedVowel + 379 cleanText.slice(index + 1); 380 return window.ru_ipa.ipa_string(stressedWord); // Russian IPA function 381 }) 382 .filter((ipa) => ipa) 383 .map((ipa) => `/${ipa}/`) 384 .join(","); 385 386 return possibleIPAs || null; 387 }, 388 389 Greek: ({ cleanText, langStyle }) => { 390 const styleMap = { 391 "5th BCE Attic": "cla", 392 "1st CE Egyptian": "koi1", 393 "4th CE Koine": "koi2", 394 "10th CE Byzantine": "byz1", 395 "15th CE Constantinopolitan": "byz2", 396 }; 397 const styleCode = styleMap[langStyle]; 398 if (!styleCode) return null; 399 const result = window.grc_ipa.IPA(cleanText, styleCode); 400 return result?.[styleCode]?.IPA; 401 }, 402 403 Armenian: ({ cleanText, langStyle, langForm }) => { 404 const system = langStyle === "Western" ? "west" : "east"; 405 const methodMap = { Phonemic: "phonemic_IPA", Phonetic: "phonetic_IPA" }; 406 const method = methodMap[langForm]; 407 return method ? window.hy_ipa[method](cleanText, system) : null; 408 }, 409 410 Italian: ({ cleanText, langForm }) => { 411 if (langForm === "Phonemic") { 412 const result = window.it_ipa.to_phonemic(cleanText, "TEST"); 413 return result?.phonemic; 414 } 415 return null; 416 }, 417 418 // --- Group 2: Direct Generation for Phonemic form (No Lexicon) --- 419 ...Object.fromEntries( 420 ["Belorussian", "Bulgarian", "Polish", "Mongolian"].map((lang) => [ 421 lang, 422 ({ cleanText, langForm }) => { 423 if (langForm !== "Phonemic") return null; 424 const generators = { 425 Belorussian: () => window.be_ipa.toIPA(cleanText), 426 Bulgarian: () => window.bg_ipa.toIPA(cleanText), 427 Polish: () => window.pl_ipa.convert_to_IPA(cleanText), 428 Mongolian: () => window.mn_ipa.to_IPA(cleanText), 429 }; 430 return generators[lang](); 431 }, 432 ]), 433 ), 434 435 // --- Irish G2P: 17-pass pipeline, no Wiktionary Lua pron module exists --- 436 Irish: ({ cleanText, langStyle }) => { 437 const dialectMap = { 438 Connacht: "connacht", 439 Munster: "munster", 440 Ulster: "ulster", 441 }; 442 const dialect = dialectMap[langStyle] || "connacht"; 443 return window.ga_ipa.transcribe(cleanText, dialect); 444 }, 445 446 // --- Group 3: Lexicon Lookup with Generation Fallback --- 447 ...Object.fromEntries( 448 ["German", "French", "Czech", "Lithuanian", "Icelandic"].map((lang) => [ 449 lang, 450 ({ cleanText, langForm, langStyle }) => { 451 // langStyle needed for French post-processing 452 let ipa = null; 453 454 // Step 1: Attempt lexicon lookup for Phonemic form 455 if (langForm === "Phonemic") { 456 const dictRecord = lookupInLexicon(lang, cleanText); 457 if (dictRecord) { 458 ipa = dictRecord; 459 } 460 } 461 462 // Step 2: If no record found, use a generator 463 if (ipa === null) { 464 const generators = { 465 German: { 466 Phonemic: () =>
466 window.de_ipa.phonemic(cleanText), 467 Phonetic: () => window.de_ipa.phonetic(cleanText), 468 }, 469 French: { 470 Phonemic: () => window.fr_ipa.show(cleanText)?.[0], 471 }, 472 Czech: { 473 Phonemic: () => window.cs_ipa.toIPA(cleanText), 474 }, 475 Lithuanian: { 476 Phonemic: () => window.lt_ipa.toIPA(cleanText, true), 477 }, 478 Icelandic: { 479 Phonemic: () => window.is_ipa.toIPA(cleanText, "", ""), 480 }, 481 }; 482 const langGenerator = generators[lang]?.[langForm]; 483 if (langGenerator) { 484 ipa = langGenerator(); 485 } 486 } 487 488 // Step 3: Apply language-specific post-processing 489 if ( 490 lang === "French" && 491 langStyle === "Parisian (experimental)" && 492 ipa 493 ) { 494 ipa = ipa 495 .replace(/ÉÌÌÌÌÌÌ|ÉÌ/g, "õ") 496 .replace(/ÉÌ/g, "ÉÌ") 497 .replace(/ÅÌ|ÉÌ/g, "ÉÌ"); 498 } 499 500 return ipa; 501 }, 502 ]), 503 ), 504}; 505 506/** 507 * Retrieves the IPA transcription for a given text without using a cache. 508 */ 509function get_ipa_no_cache(text, args) { 510 const cleanText = sanitize(text); 511 console.log("doing actual IPA", text, cleanText, args); 512 513 const [lang, langStyle, langForm] = args.split(";"); 514 515 let ipa = ""; 516 try { 517 const handler = ipaHandlers[lang]; 518 if (handler) { 519 ipa = handler({ cleanText, lang, langStyle, langForm }) || ""; 520 console.log(`Handler for "${lang}" returned:`, ipa); 521 } else { 522 console.log(`No handler found for language: "${lang}"`); 523 } 524 } catch (err) { 525 ipa = ""; // Ensure ipa is reset on error 526 console.error("Error during IPA conversion:", err); 527 } 528 529 if (!ipa) { 530 return { value: text, status: "error" }; 531 } 532 533 // The French post-processing has been moved into its specific handler. 534 // The main function is now cleaner. 535 console.log("final ipa ", ipa); 536 return { value: ipa, status: "success" }; 537} 538function createElementFromHTML(htmlString) { 539 var div = document.createElement("div"); 540 div.innerHTML = htmlString.trim(); 541 542 // Change this to div.childNodes to support multiple top-level nodes. 543 return div.firstChild; 544} 545 546async function loadFileFromZipOrPath(zipPathOrBlob, filename) { 547 return new Promise((resolve, reject) => { 548 loadJs("./scripts/lib/jszip.min.js", async () => { 549 await loadJs("./scripts/lib/jszip-utils.min.js", async () => { 550 let data; 551 if (!(typeof zipPathOrBlob === "string")) { 552 console.log("blob", zipPathOrBlob); 553 data = zipPathOrBlob; 554 } else { 555 console.log("path", zipPathOrBlob); 556 const response = await fetchWithCache(zipPathOrBlob); 557 data = await response.blob(); 558 } 559 JSZip.loadAsync(data) 560 .then(async (zip) => { 561 const fileData = await zip.file(filename).async("string"); 562 resolve(fileData); 563 }) 564 .catch((error) => { 565 console.error(error); 566 reject(error); // or handle the error as needed 567 }); 568 }); 569 }); 570 }); 571} 572 573async function sendParallelRequests(urls) { 574 const promises = urls.map((url) => fetch(url)); 575 576 try { 577 const results = await Promise.any(promises); 578 return results; 579 } catch (error) { 580 return null; // Handle errors if no promises resolve successfully 581 } 582} 583 584async function fetchWithCacheMultiple(urls) { 585 const url = urls[0].split("/").slice(-2).join("/"); 586 587 console.log(" reading cache", url); 588 const cachedResponse = await localforage.getItem(url); 589 if (cachedResponse) { 590 console.log(" reading from cache", url); 591 if (cachedResponse instanceof Blob) { 592 const response = new Response(cachedResponse); 593 response.headers.set("X-From-Cache", "true"); 594 console.log("Returned cached blob ", url); 595 return response; 596 } 597 const response = new Response(JSON.parse(cachedResponse)); 598 response.headers.set("X-From-Cache", "true"); 599 console.log(" Returned cached string ", url); 600 return response; 601 } 602 console.log(" caching ", url); 603 604 // const response = await fetch(url); 605 const response = await sendParallelRequests(urls); 606 607 const contentType = response.headers.get("content-type"); 608 let responseContent; 609 let responseWithHeaders; 610 611 if (contentType == "application/zip") { 612 responseContent = await response.blob(); 613 try { 614 await localforage.setItem(url, responseContent); 615 } catch (err) { 616 console.log(err); 617 } 618 } else { 619 responseContent = await response.text(); 620 try { 621 await localforage.setItem(url, JSON.stringify(responseContent)); 622 } catch (err) { 623 console.log(err); 624 } 625 } 626 responseWithHeaders = new Response(responseContent, response); 627 return responseWithHeaders; 628} 629 630async function fetchWithCache( 631 url, 632 onProgress = (progress) => console.log(`Progress: ${progress.toFixed(2)}%`), 633) { 634 console.log(" reading cache", url); 635 let cachedResponse = await localforage.getItem(url); 636 if (cachedResponse) { 637 console.log(" reading from cache", url); 638 if (cachedResponse instanceof Blob) { 639 const response = new Response(cachedResponse); 640 response.headers.set("X-From-Cache", "true"); 641 console.log(" Returned cached blob ", url); 642 return response; 643 } 644 const response = new Response(JSON.parse(cachedResponse)); 645 response.headers.set("X-From-Cache", "true"); 646 console.log(" Returned cached string ", url); 647 return response; 648 } 649 console.log(" caching ", url); 650 const response = await fetch(url); 651 652 const contentLength = response.headers.get("content-length"); 653 const contentEncoding = response.headers.get("content-encoding"); 654 const isCompressed = 655 contentEncoding === "gzip" || 656 contentEncoding === "br" || 657 contentEncoding === "deflate"; 658 659 // If the content is compressed, we'll estimate the uncompressed size 660 // Average compression ratio for text/json is roughly 4:1, for binary files about 1.5:1 661 const contentType = response.headers.get("content-type"); 662 const compressionRatio = 663 contentType?.includes("text") || contentType?.includes("json") ? 4 : 1.5; 664 const total = 665 isCompressed && contentLength 666 ? parseInt(contentLength, 10) * compressionRatio 667 : parseInt(contentLength, 10); 668 669 let loaded = 0; 670 671 const reader = response.body.getReader(); 672 const stream = new ReadableStream({ 673 start(controller) { 674 function read() { 675 reader 676 .read() 677 .then(({ done, value }) => { 678 if (done) { 679 controller.close(); 680 return; 681 } 682 loaded += value.length; 683 if (onProgress && total) { 684 onProgress((loaded / total) * 100); 685 } 686 controller.enqueue(value); 687 read(); 688 }) 689 .catch((error) => { 690 console.error(" Stream reading error:", error); 691 controller.error(error); 692 }); 693 } 694 695 read(); 696 }, 697 }); 698 699 const newResponse = new Response(stream, { 700 headers: response.headers, 701 }); 702 703 let responseContent; 704 705 if (contentType && contentType.includes("zip")) { 706 responseContent = await newResponse.blob(); 707 try { 708 await localforage.setItem(url, responseContent); 709 } catch (err) { 710 console.log(err); 711 } 712 } else { 713 responseContent = await newResponse.text(); 714 try { 715 await localforage.setItem(url, JSON.stringify(responseContent)); 716 } catch (err) { 717 console.log(err); 718 } 719 } 720 721 return new Response(responseContent, { 722 headers: newResponse.headers, 723 }); 724} 725 726const loadedScripts = {}; 727 728async function loadJs(url, code) { 729 console.log(loadedScripts); 730 if (loadedScripts[url]) { 731 return new Promise((resolve) => { 732 setTimeout(() => { 733 code(); 734 resolve(); 735 }, 0); 736 }); 737 } 738 loadedScripts[url] = true; 739 740 return new Promise((resolve, reject) => { 741 const script = document.createElement("script"); 742 743 script.onload = () => { 744 code(); 745 resolve(); 746 }; 747 748 script.onerror = reject; 749 script.src = url; 750 751 document.head.appendChild(script); 752 }); 753} 754 755/** 756 * Disables all form elements on the page 757 */ 758function disableAll(include_elements = []) { 759 // Select all the forms on the page 760 const forms = Array.from(document.querySelectorAll("form")); 761 762 // Iterate through each form and disable all its elements
763 forms.forEach((form) => { 764 Array.from(form.elements) 765 .concat(include_elements) 766 .forEach((element) => { 767 element.disabled = true; 768 }); 769 }); 770} 771 772function enableAll(include_elements = []) { 773 // Get all the form elements on the page 774 const forms = Array.from(document.querySelectorAll("form")); 775 forms.forEach((form) => { 776 // Enable all elements in the form 777 Array.from(form.elements) 778 .concat(include_elements) 779 .forEach((element) => { 780 element.disabled = false; 781 }); 782 }); 783} 784 785async function translateWithFallback(text, sourceLang = "auto", targetLang) { 786 if (!text.match(/\p{L}/u)) { 787 return { 788 text: text, // Return original text 789 source: "no-translation-needed", 790 }; 791 } 792 // First attempt: GTX API 793 try { 794 await wait(1); // Rate limiting 795 const gtxUrl = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=${sourceLang}&tl=${targetLang}&dt=t&q=${encodeURIComponent( 796 text, 797 )}`; 798 const gtxResponse = await fetch(gtxUrl); 799 800 if (gtxResponse.ok) { 801 const gtxData = await gtxResponse.json(); 802 return { 803 text: gtxData[0][0][0], 804 source: "google-gtx", 805 }; 806 } 807 } catch (error) { 808 console.warn("GTX API failed:", error); 809 } 810 811 // Second attempt: Clients5 API 812 try { 813 await wait(1); // Rate limiting 814 const clients5Url = `https://clients5.google.com/translate_a/single?dj=1&dt=t&dt=sp&dt=ld&dt=bd&client=dict-chrome-ex&sl=${sourceLang}&tl=${targetLang}&q=${encodeURIComponent( 815 text, 816 )}`; 817 const clients5Response = await fetch(clients5Url); 818 819 if (clients5Response.ok) { 820 const clients5Data = await clients5Response.json(); 821 return { 822 text: clients5Data.sentences[0].trans, 823 source: "google-clients5", 824 }; 825 } 826 } catch (error) { 827 console.warn("Clients5 API failed:", error); 828 } 829 830 throw new Error("All translation attempts failed"); 831} 832 833const translateWithFallbackWrapper = function (...args) { 834 return translateWithFallback(...args) 835 .then((result) => result) 836 .catch((error) => { 837 console.error("Translation failed:", error); 838 throw error; 839 }); 840}; 841 842// Now memoize the wrapper function 843const translateWithFallbackCached = memoizeLocalStorage( 844 translateWithFallbackWrapper, 845 { 846 ttl: 24 * 60 * 60 * 1000, // 24 hours 847 backgroundRefresh: true, 848 }, 849); 850export { 851 asyncMapStrict, 852 sanitize, 853 clearStorage, 854 wait, 855 get_ipa_no_cache, 856 memoizeLocalStorage, 857 loadJs, 858 loadFileFromZipOrPath, 859 createElementFromHTML, 860 fetchWithCache, 861 fetchWithCacheMultiple, 862 disableAll, 863 enableAll, 864 updateLoadingText, 865 translateWithFallbackCached, 866};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.