1(function () { 2 "use strict"; 3 const API_ENDPOINTS = { 4 CONFIG: "/translate/config", 5 TEXT_BASED: "/translate/text-based", 6 }; 7 const SELECTORS = { 8 ERROR_MESSAGE: "tl-error-message", 9 DROPDOWN_CONTAINER: "tl-dropdown-container", 10 DROPDOWN_STYLE: "tl-dropdown-style", 11 DROPDOWN: "tl-dropdown", 12 DRAG_HANDLE: "tl-drag-handle", 13 RESET_HANDLE: "tl-reset-handle", 14 POWERED_BY: "tl-powered-by", 15 }; 16 const ATTRIBUTES = { 17 TRANSLATED_TO: "data-tl-to", 18 SOURCE_TEXT: "data-tl-src", 19 SOURCE_ATTRIBUTE_PREFIX: "data-tl-src-", 20 TRANSLATION_STATE: "data-tl-state", 21 }; 22 const TRANSLATABLE_ATTRIBUTES = [ 23 "title", 24 "alt", 25 "placeholder", 26 "value", 27 "aria-label", 28 "aria-description", 29 "data-tooltip", 30 "data-tip", 31 "data-original-title", 32 "data-hover", 33 "data-after-content", 34 ]; 35 const TIMINGS = { 36 ERROR_MESSAGE_DURATION: 5e3, 37 SPA_NAVIGATION_DEBOUNCE: 150, 38 DYNAMIC_TRANSLATION_DEBOUNCE: 100, 39 // ms 40 }; 41 const LOCAL_STORAGE_KEYS = { 42 DROPDOWN_POSITION: "tl-dropdown-position", 43 SELECTED_LANGUAGE: "tl-selected-language", 44 SOURCE_CACHE: "tl-source-cache", 45 }; 46 const DROPDOWN_EXCLUDED_SELECTORS = [ 47 "[data-no-translate]", 48 '[data-translated="true"]', 49 ".notranslate", 50 // '[translate="no"]', 51 "script", 52 "style", 53 "noscript", 54 ]; 55 const HARD_CODED_RTL = /* @__PURE__ */ new Set([ 56 "ar", 57 // Arabic 58 "he", 59 // Hebrew 60 "fa", 61 // Persian 62 "ur", 63 // Urdu 64 "ps", 65 // Pashto 66 "sd", 67 // Sindhi 68 "ug", 69 // Uyghur 70 "yi", 71 // Yiddish 72 ]); 73 function isRtl(lang) { 74 const base = lang.split("-")[0].toLowerCase(); 75 if (HARD_CODED_RTL.has(base)) return true; 76 try { 77 const script = new Intl.Locale(lang).maximize().script; 78 return script === "Arab" || script === "Hebr"; 79 } catch { 80 return false; 81 } 82 } 83 function applyDirection(lang) { 84 const direction = isRtl(lang) ? "rtl" : "ltr"; 85 document.documentElement.dir = direction; 86 document.documentElement.lang = lang; 87 } 88 class ErrorHandler { 89 static showErrorMessage(type, details, retryCallback) { 90 const existing = document.getElementById(SELECTORS.ERROR_MESSAGE); 91 if (existing) existing.remove(); 92 const errorDiv = document.createElement("div"); 93 errorDiv.id = SELECTORS.ERROR_MESSAGE; 94 errorDiv.setAttribute("data-no-translate", "true"); 95 const errorDetail = this.getErrorDetail(type, details); 96 console.error( 97 `Translation Error [${type}]:`, 98 errorDetail.message, 99 details 100 ); 101 const errorContent = document.createElement("div"); 102 errorContent.style.cssText = 103 "display: flex; flex-direction: column; gap: 8px;"; 104 const messageDiv = document.createElement("div"); 105 messageDiv.textContent = errorDetail.message; 106 errorContent.appendChild(messageDiv); 107 if (errorDetail.retryable && retryCallback) { 108 const retryButton = document.createElement("button"); 109 retryButton.textContent = "Retry"; 110 retryButton.style.cssText = ` 111 background: white; color: #dc2626; border: none; 112 padding: 4px 12px; border-radius: 4px; cursor: pointer; 113 font-size: 12px; font-weight: 500; margin-top: 4px; 114 `; 115 retryButton.onclick = () => { 116 errorDiv.remove(); 117 retryCallback(); 118 }; 119 errorContent.appendChild(retryButton); 120 } 121 errorDiv.style.cssText = ` 122 position: fixed; bottom: 24px; right: 24px; z-index: 9999; 123 background: #dc2626; color: white; padding: 12px 16px; 124 border-radius: 8px; font: 14px/1.4 sans-serif; 125 max-width: 320px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); 126 `; 127 errorDiv.appendChild(errorContent); 128 document.body.appendChild(errorDiv); 129 const duration = 130 errorDetail.showDuration || TIMINGS.ERROR_MESSAGE_DURATION; 131 setTimeout(() => { 132 if (errorDiv.parentNode) { 133 errorDiv.remove(); 134 } 135 }, duration); 136 } 137 static getErrorDetail(type, customMessage) { 138 const errorMap = { 139 auth: { 140 type: "auth", 141 message: 142 customMessage || 143 "ð Authentication failed. Please check your API key.", 144 retryable: false, 145 showDuration: 8e3, 146 }, 147 server: { 148 type: "server", 149 message: 150 customMessage || 151 "â ï¸ Translation service temporarily unavailable. Please try again later.", 152 retryable: true, 153 showDuration: 8e3, 154 }, 155 network: { 156 type: "network", 157 message: 158 customMessage || 159 "ð Connection failed. Please check your internet connection.", 160 retryable: true, 161 showDuration: 8e3, 162 }, 163 "rate-limit": { 164 type: "rate-limit", 165 message: 166 customMessage || "â³ Too many requests. Please try again later.", 167 fallback: "Using cached translations where available.", 168 retryable: true, 169 showDuration: 1e4, 170 }, 171 "unsupported-language": { 172 type: "unsupported-language", 173 message: 174 customMessage || 175 "ð Language not supported. Using default language.", 176 retryable: false, 177 showDuration: 6e3, 178 }, 179 "invalid-url": {
180 type: "invalid-url", 181 message: 182 customMessage || 183 "â Invalid URL format. Please check the URL and try again.", 184 retryable: false, 185 showDuration: 8e3, 186 }, 187 "cache-unavailable": { 188 type: "cache-unavailable", 189 message: 190 customMessage || 191 "ð¾ Cache unavailable. Using direct translation service.", 192 fallback: "Translations may be slower than usual.", 193 retryable: false, 194 showDuration: 6e3, 195 }, 196 "config-invalid": { 197 type: "config-invalid", 198 message: 199 customMessage || 200 "âï¸ Invalid configuration. Please contact support.", 201 retryable: false, 202 showDuration: 1e4, 203 }, 204 offline: { 205 type: "offline", 206 message: 207 customMessage || 208 "ð¡ You are offline. Using cached translations where available.", 209 fallback: "New translations unavailable until connection restored.", 210 retryable: true, 211 showDuration: 6e3, 212 }, 213 }; 214 return errorMap[type] || errorMap["server"]; 215 } 216 static clearError() { 217 const existing = document.getElementById(SELECTORS.ERROR_MESSAGE); 218 if (existing) existing.remove(); 219 } 220 static isOffline() { 221 return !navigator.onLine; 222 } 223 } 224 function snakeToCamelString(str) { 225 return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); 226 } 227 function camelToSnakeString(str) { 228 return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); 229 } 230 function snakeToCamelObject(obj) { 231 return Object.fromEntries( 232 Object.entries(obj).map(([key, value]) => [ 233 snakeToCamelString(key), 234 value, 235 ]) 236 ); 237 } 238 function camelToSnakeObject(obj) { 239 return Object.fromEntries( 240 Object.entries(obj).map(([key, value]) => [ 241 camelToSnakeString(key), 242 value, 243 ]) 244 ); 245 } 246 class ApiService { 247 constructor(apiConfig, onCriticalError) { 248 this.maxRetries = 3; 249 const scriptTag = document.currentScript; 250 this.apiUrl = 251 (scriptTag == null ? void 0 : scriptTag.dataset.apiRoot) || 252 (typeof SCRIPT_CONFIG !== "undefined" 253 ? SCRIPT_CONFIG.apiRoot 254 : undefined); 255 this.onCriticalError = onCriticalError; 256 } 257 getApiUrl() { 258 return this.apiUrl; 259 } 260 setCriticalErrorHandler(handler) { 261 this.onCriticalError = handler; 262 } 263 async fetchConfig(apiKey, domain) { 264 try { 265 if (!apiKey || !domain) { 266 ErrorHandler.showErrorMessage( 267 "config-invalid", 268 "Invalid configuration: Missing API key or domain." 269 ); 270 throw new Error("INVALID_CONFIG"); 271 } 272 try { 273 new URL(this.apiUrl); 274 } catch { 275 ErrorHandler.showErrorMessage( 276 "invalid-url", 277 "Invalid API URL format." 278 ); 279 throw new Error("INVALID_URL"); 280 } 281 const response = await fetch(`${this.apiUrl}${API_ENDPOINTS.CONFIG}`, { 282 method: "GET", 283 credentials: "omit", 284 headers: { 285 Authorization: `Bearer ${apiKey}`, 286 Origin: window.location.origin, 287 "Content-Type": "application/json", 288 }, 289 }); 290 if (!response.ok) { 291 await this.handleConfigError(response); 292 throw new Error(`HTTP error! status: ${response.status}`); 293 } 294 const config = await response.json(); 295 const payload = config.payload || config; 296 if (!payload.default_language) { 297 ErrorHandler.showErrorMessage( 298 "config-invalid", 299 "Invalid configuration: Missing default language." 300 ); 301 throw new Error("INVALID_CONFIG"); 302 } 303 if (!payload.selected_languages) { 304 ErrorHandler.showErrorMessage( 305 "config-invalid", 306 "Invalid configuration: No target languages specified." 307 ); 308 throw new Error("INVALID_CONFIG"); 309 } 310 return { 311 domain, 312 defaultLang: payload.default_language, 313 languageLabels: payload.language_labels, 314 targetLanguages: payload.selected_languages, 315 websiteId: payload.website_id, 316 teamId: payload.team_id, 317 }; 318 } catch (e) { 319 console.error("â Failed to fetch configuration:", e); 320 if (e instanceof TypeError && e.message.includes("Failed to fetch")) { 321 if (ErrorHandler.isOffline()) { 322 ErrorHandler.showErrorMessage( 323 "offline", 324 "Cannot load translation configuration while offline." 325 ); 326 } else { 327 ErrorHandler.showErrorMessage( 328 "network", 329 "Failed to load translation configuration. Please check your connection." 330 ); 331 }
332 throw new Error("NETWORK_ERROR"); 333 } 334 if (e instanceof Error && e.message === "RATE_LIMIT") { 335 throw e; 336 } 337 throw e; 338 } 339 } 340 async handleConfigError(response) { 341 var _a; 342 const errorData = await response.json().catch(() => ({})); 343 if (response.status === 401) { 344 ErrorHandler.showErrorMessage( 345 "auth", 346 errorData.error === "invalid_api_key" ? "Invalid API key" : void 0 347 ); 348 } else if (response.status === 403) { 349 ErrorHandler.showErrorMessage( 350 "auth", 351 errorData.error === "origin_not_allowed" 352 ? "Domain not authorized for this API key" 353 : void 0 354 ); 355 } else if (response.status === 404) { 356 ErrorHandler.showErrorMessage( 357 "server", 358 errorData.detail.payload.error === "website_not_found" 359 ? errorData.detail.payload.message 360 : "Translation configuration not found." 361 ); 362 } else if (response.status === 429) { 363 ErrorHandler.showErrorMessage( 364 "rate-limit", 365 errorData.message || 366 "You're sending requests too quickly. Please wait and try again." 367 ); 368 throw new Error("RATE_LIMIT"); 369 } else if (response.status >= 500) { 370 if ( 371 errorData.detail && 372 errorData.detail.includes("Redis connection failed") 373 ) { 374 ErrorHandler.showErrorMessage( 375 "server", 376 "Translation service unavailable (Redis connection failed)" 377 ); 378 (_a = this.onCriticalError) == null ? void 0 : _a.call(this); 379 } else { 380 ErrorHandler.showErrorMessage("server"); 381 } 382 } else { 383 ErrorHandler.showErrorMessage("network", `HTTP ${response.status}`); 384 } 385 } 386 async translateTextBased({ 387 textNodes, 388 translateConfig, 389 apiConfig, 390 dropdownLabels, 391 pageContext, 392 abortSignal, 393 }) { 394 if (ErrorHandler.isOffline()) { 395 ErrorHandler.showErrorMessage( 396 "offline", 397 "You are currently offline. Using cached translations where available." 398 ); 399 }
400 const makeRequest = async (retryAttempt = 0) => { 401 try { 402 const response = await fetch( 403 `${this.apiUrl}${API_ENDPOINTS.TEXT_BASED}`, 404 { 405 method: "POST", 406 headers: { 407 "Content-Type": "application/json", 408 Accept: "application/json", 409 Authorization: `Bearer ${apiConfig.key}`, 410 "X-Domain": apiConfig.domain, 411 }, 412 body: JSON.stringify( 413 camelToSnakeObject({ 414 textNodes, 415 translateConfig, 416 dropdownLabels, 417 pageContext, 418 }) 419 ), 420 signal: abortSignal, 421 } 422 ); 423 if (!response.ok) { 424 await this.handleTranslationError(response); 425 throw new Error(`HTTP error! status: ${response.status}`); 426 } 427 const data = await response.json(); 428 if (data.redis_available === false) { 429 ErrorHandler.showErrorMessage( 430 "cache-unavailable", 431 "Cache service temporarily unavailable. Translations may be slower. Please refresh the browser." 432 ); 433 } 434 const responseBody = snakeToCamelObject(data); 435 return responseBody; 436 } catch (error) { 437 if (ApiService.isAbortError(error)) { 438 throw error; 439 } 440 if ( 441 error instanceof TypeError && 442 error.message.includes("Failed to fetch") 443 ) { 444 if (retryAttempt < this.maxRetries) { 445 await new Promise((resolve) => 446 setTimeout(resolve, Math.pow(2, retryAttempt) * 1e3) 447 ); 448 return makeRequest(retryAttempt + 1); 449 } 450 if (ErrorHandler.isOffline()) { 451 ErrorHandler.showErrorMessage("offline"); 452 } else { 453 ErrorHandler.showErrorMessage( 454 "network", 455 "Network connection failed. Please check your connection." 456 ); 457 } 458 throw new Error("NETWORK_ERROR"); 459 } 460 if (error instanceof Error && error.message === "RATE_LIMIT") { 461 if (retryAttempt < 2) { 462 await new Promise((resolve) => 463 setTimeout(resolve, 5e3 * (retryAttempt + 1)) 464 ); 465 return makeRequest(retryAttempt + 1); 466 } 467 } 468 throw error; 469 } 470 }; 471 return makeRequest(); 472 } 473 async handleTranslationError(response) { 474 const errorData = await response.json().catch(() => ({})); 475 const errorDetail = errorData.detail || {}; 476 const errorType = errorDetail.error || "unknown"; 477 let errorCode = `HTTP_${response.status}`; 478 switch (errorType) { 479 case "unsupported_language": 480 ErrorHandler.showErrorMessage( 481 "unsupported-language", 482 errorDetail.message 483 ); 484 errorCode = "UNSUPPORTED_LANGUAGE"; 485 break; 486 case "rate_limit": 487 const retryMsg = errorDetail.from_cache_only 488 ? "Rate limit reached. Serving cached translations." 489 : "Too many requests. Please try again later."; 490 ErrorHandler.showErrorMessage("rate-limit", retryMsg); 491 errorCode = "RATE_LIMIT"; 492 break; 493 case "invalid_api_key": 494 ErrorHandler.showErrorMessage( 495 "auth", 496 "Invalid API key. Please check your configuration." 497 ); 498 errorCode = "UNAUTHORIZED"; 499 break; 500 case "domain_mismatch": 501 case "origin_not_allowed": 502 ErrorHandler.showErrorMessage( 503 "auth", 504 "This domain is not authorized for the provided API key." 505 ); 506 errorCode = "FORBIDDEN"; 507 break; 508 default: 509 if (response.status === 401) { 510 ErrorHandler.showErrorMessage( 511 "auth", 512 errorDetail.message || "Authentication failed" 513 ); 514 errorCode = "UNAUTHORIZED"; 515 } else if (response.status === 403) { 516 ErrorHandler.showErrorMessage( 517 "auth", 518 errorDetail.message || "Access forbidden" 519 ); 520 errorCode = "FORBIDDEN"; 521 } else if (response.status === 404) { 522 ErrorHandler.showErrorMessage( 523 "server", 524 errorData.error === "website_not_found" 525 ? "Translation configuration not found." 526 : void 0 527 ); 528 } else if (response.status === 429) { 529 ErrorHandler.showErrorMessage( 530 "rate-limit", 531 errorDetail.message || 532 "You're sending requests too quickly. Please wait a moment and try again." 533 ); 534 errorCode = "RATE_LIMIT"; 535 } else if (response.status >= 500) { 536 if (errorDetail.message && errorDetail.message.includes("Redis")) { 537 ErrorHandler.showErrorMessage( 538 "server", 539 "Translation service experiencing issues. Some features may be limited." 540 ); 541 errorCode = "REDIS_UNAVAILABLE"; 542 } else { 543 ErrorHandler.showErrorMessage( 544 "server", 545 errorDetail.message || "Server error" 546 ); 547 errorCode = "SERVER_ERROR"; 548 } 549 } else { 550 ErrorHandler.showErrorMessage( 551 "network", 552 `HTTP ${response.status}: ${ 553 errorDetail.message || "Unknown error" 554 }` 555 );
556 errorCode = "NETWORK_ERROR"; 557 } 558 } 559 throw new Error(errorCode); 560 } 561 static isAbortError(error) { 562 return error.name === "AbortError"; 563 } 564 } 565 class StorageManager { 566 static saveDropdownPosition(position) { 567 try { 568 localStorage.setItem( 569 LOCAL_STORAGE_KEYS.DROPDOWN_POSITION, 570 JSON.stringify(position) 571 ); 572 } catch (e) { 573 console.warn("Failed to save dropdown position to localStorage:", e); 574 } 575 } 576 static loadDropdownPosition() { 577 try { 578 const saved = localStorage.getItem( 579 LOCAL_STORAGE_KEYS.DROPDOWN_POSITION 580 ); 581 if (saved) { 582 const position = JSON.parse(saved); 583 if ( 584 typeof position.x === "number" && 585 typeof position.y === "number" 586 ) { 587 return position; 588 } 589 } 590 } catch (e) { 591 console.warn("Failed to load dropdown position from localStorage:", e); 592 } 593 return null; 594 } 595 static saveSelectedLanguage(lang) { 596 try { 597 localStorage.setItem(LOCAL_STORAGE_KEYS.SELECTED_LANGUAGE, lang); 598 } catch (e) { 599 console.warn("Failed to save selected language to localStorage:", e); 600 } 601 } 602 static loadSelectedLanguage() { 603 try { 604 let value = localStorage.getItem(LOCAL_STORAGE_KEYS.SELECTED_LANGUAGE); 605 if (value && value[0] === '"' && value[value.length - 1] === '"') { 606 value = value.slice(1, -1); 607 } 608 return value; 609 } catch (e) { 610 console.warn("Failed to load selected language from localStorage:", e); 611 return null; 612 } 613 } 614 } 615 class DomUtils { 616 static getCurrentPath() { 617 return window.location.pathname + window.location.search; 618 } 619 static getScriptConfig() { 620 const tag = document.currentScript; 621 const rawDomain = 622 (tag == null ? void 0 : tag.dataset.domain) || window.location.host; 623 const domain = DomUtils.normalizeDomain(rawDomain); 624 const apiKey = tag?.dataset.apiKey; 625 const apiRoot = tag?.dataset.apiRoot; 626 return { apiKey, apiRoot, domain }; 627 } 628 static normalizeDomain(inputDomain) { 629 if (!inputDomain) return ""; 630 let domain = inputDomain.trim().toLowerCase(); 631 domain = domain.replace(/^(https?:\/\/)/, ""); 632 if (domain.startsWith("www.")) { 633 domain = domain.slice(4); 634 } 635 domain = domain.split("/")[0]; 636 // The translation API is registered for arabnews.com; when running on 637 // localhost/staging hosts, fall back to the registered domain 638 if (domain !== "arabnews.com") { 639 domain = "arabnews.com"; 640 } 641 return domain; 642 } 643 static shouldPreventTranslation(element) { 644 if (!element) return false; 645 let current = element; 646 while (current) { 647 // if (current.getAttribute('translate') === 'no') { 648 // return true; 649 // } 650 if (current.classList && current.classList.contains("notranslate")) { 651 return true; 652 } 653 // Check if any class name contains "no-translate" 654 if (current.classList && current.classList.length > 0) { 655 for (let i = 0; i < current.classList.length; i++) { 656 if (current.classList[i].includes("no-translate")) { 657 return true; 658 } 659 } 660 } 661 if (current.hasAttribute("data-no-translate")) { 662 return true; 663 } 664 current = current.parentElement; 665 } 666 return false; 667 } 668 static isNonContentElement(element) { 669 const tagName = element.tagName; 670 return ( 671 tagName === "SCRIPT" || tagName === "STYLE" || tagName === "NOSCRIPT" 672 ); 673 } 674 static escapeHtml(str) { 675 const htmlEscapes = { 676 "&": "&", 677 "<": "<", 678 ">": ">", 679 '"': """, 680 "'": "'", 681 "/": "/", 682 }; 683 return str.replace(/[&<>"'/]/g, (char) => htmlEscapes[char] || char); 684 } 685 static detectCSPViolations() { 686 try { 687 const testStyle = document.createElement("style"); 688 testStyle.textContent = ".csp-test { display: none; }"; 689 document.head.appendChild(testStyle); 690 document.head.removeChild(testStyle); 691 } catch (e) { 692 console.warn( 693 "â ï¸ CSP may block inline styles needed for translation dropdown" 694 ); 695 } 696 document.addEventListener("securitypolicyviolation", (e) => { 697 if ( 698 e.violatedDirective.includes("script-src") || 699 e.violatedDirective.includes("style-src") || 700 e.violatedDirective.includes("connect-src") 701 ) { 702 console.warn( 703 `â ï¸ CSP violation detected: ${e.violatedDirective} - Translation features may be limited` 704 ); 705 } 706 }); 707 } 708 709 static updateCanonicalUrl(targetLang, sourceLang) { 710 const canonical = document.querySelector('link[rel="canonical"]'); 711 if (!canonical) return;
712 try { 713 const url = new URL(canonical.href); 714 if (targetLang !== sourceLang) { 715 url.searchParams.set("lang", targetLang); 716 } else { 717 url.searchParams.delete("lang"); 718 } 719 canonical.href = url.toString(); 720 } catch (e) { 721 console.warn("Failed to update canonical URL:", e); 722 } 723 } 724 } 725 function waitForDomToSettle(root, quietMs = 250, maxWaitMs = 1e3) { 726 return new Promise((resolve) => { 727 let timer; 728 const obs = new MutationObserver(() => { 729 clearTimeout(timer); 730 timer = window.setTimeout(done, quietMs); 731 }); 732 function done() { 733 obs.disconnect(); 734 resolve(); 735 } 736 obs.observe(root, { 737 childList: true, 738 subtree: true, 739 characterData: true, 740 }); 741 const hardStop = window.setTimeout(() => { 742 obs.disconnect(); 743 resolve(); 744 }, maxWaitMs); 745 timer = window.setTimeout(() => { 746 clearTimeout(hardStop); 747 done(); 748 }, quietMs); 749 }); 750 } 751 class ConfigManager { 752 constructor(apiService, scriptConfig) { 753 this.apiService = apiService; 754 this.scriptConfig = scriptConfig; 755 this.state = { 756 translationConfig: null, 757 apiConfig: null, 758 currentLang: "en", 759 }; 760 } 761 getState() { 762 return { ...this.state }; 763 } 764 updateState(updates) { 765 this.state = { ...this.state, ...updates }; 766 } 767 async initializeConfig() { 768 const { apiKey, domain } = this.scriptConfig; 769 if (!apiKey) { 770 console.error("â No API key provided. Translation disabled."); 771 return null; 772 } 773 const maxRetries = 5; 774 let retryCount = 0; 775 let delay = 500; 776 while (retryCount < maxRetries) { 777 try { 778 const config = await this.apiService.fetchConfig(apiKey, domain); 779 const translationConfig = { 780 defaultLang: config.defaultLang, 781 languageLabels: config.languageLabels, 782 targetLanguages: config.targetLanguages, 783 domain: config.domain, 784 websiteId: config.websiteId, 785 teamId: config.teamId, 786 }; 787 const apiConfig = { 788 key: apiKey, 789 domain, 790 apiUrl: this.apiService.getApiUrl(), 791 }; 792 this.updateState({ 793 translationConfig, 794 apiConfig, 795 }); 796 let initialLang = translationConfig.defaultLang; 797 const savedLang = StorageManager.loadSelectedLanguage(); 798 if ( 799 savedLang && 800 (translationConfig.targetLanguages.includes(savedLang) || 801 savedLang === translationConfig.defaultLang) 802 ) { 803 initialLang = savedLang; 804 } 805 this.updateState({ currentLang: initialLang }); 806 return translationConfig; 807 } catch (e) { 808 retryCount++; 809 if (e instanceof Error && e.message === "RATE_LIMIT") { 810 console.error("â Rate limit exceeded. Stopping retries."); 811 break; 812 } 813 console.warn( 814 `â ï¸ Config fetch attempt ${retryCount} failed: ${e.message}. Retrying in ${delay}ms...` 815 ); 816 await new Promise((resolve) => setTimeout(resolve, delay)); 817 delay = Math.min(delay * 2, 4e3); 818 } 819 } 820 console.error( 821 "â Failed to initialize translation script after all retries" 822 ); 823 this.updateState({ 824 translationConfig: null, 825 apiConfig: null, 826 }); 827 return null; 828 } 829 getCurrentLanguage() { 830 return this.state.currentLang; 831 } 832 getTranslationConfig() { 833 return this.state.translationConfig; 834 } 835 getApiConfig() { 836 return this.state.apiConfig; 837 } 838 setCurrentLanguage(lang) { 839 this.updateState({ currentLang: lang }); 840 StorageManager.saveSelectedLanguage(lang); 841 } 842 isInDefaultLanguage() { 843 var _a; 844 return ( 845 this.state.currentLang === 846 ((_a = this.state.translationConfig) == null ? void 0 : _a.defaultLang) 847 ); 848 } 849 } 850 851 const queued = /* @__PURE__ */ new WeakSet(); 852 const translated = /* @__PURE__ */ new WeakSet(); 853 const translatedTextStore = /* @__PURE__ */ new WeakMap(); 854 const translatedAttributeStore = /* @__PURE__ */ new WeakMap(); 855 function setTranslatedText(node, translatedText) { 856 translatedTextStore.set(node, translatedText.trim()); 857 } 858 function getTranslatedText(node) { 859 return translatedTextStore.get(node); 860 } 861 function clearTranslatedText(node) { 862 translatedTextStore.delete(node); 863 } 864 function setTranslatedAttribute(element, attribute, translatedValue) { 865 const existing =
866 translatedAttributeStore.get(element) || /* @__PURE__ */ new Map(); 867 existing.set(attribute, translatedValue.trim()); 868 translatedAttributeStore.set(element, existing); 869 } 870 function getTranslatedAttribute(element, attribute) { 871 const map = translatedAttributeStore.get(element); 872 return map == null ? void 0 : map.get(attribute); 873 } 874 function clearTranslatedAttribute(element, attribute) { 875 if (!attribute) { 876 translatedAttributeStore.delete(element); 877 return; 878 } 879 const map = translatedAttributeStore.get(element); 880 if (!map) return; 881 map.delete(attribute); 882 if (map.size === 0) { 883 translatedAttributeStore.delete(element); 884 } 885 } 886 class TranslationObserver { 887 constructor(onUrlChange, onContentChange, configManager) { 888 this.onUrlChange = onUrlChange; 889 this.onContentChange = onContentChange; 890 this.configManager = configManager; 891 this.observer = null; 892 this.currentUrl = ""; 893 } 894 start() { 895 this.stop(); 896 this.currentUrl = DomUtils.getCurrentPath(); 897 this.observer = new MutationObserver((mutations) => { 898 if (window.__isTranslatingDOM) return; 899 const newUrl = DomUtils.getCurrentPath(); 900 if (newUrl !== this.currentUrl) { 901 this.currentUrl = newUrl; 902 this.onUrlChange(newUrl); 903 return; 904 } 905 this.processMutations(mutations); 906 }); 907 this.observer.observe(document.body, { 908 childList: true, 909 subtree: true, 910 characterData: true, 911 attributes: true, 912 attributeFilter: [...TRANSLATABLE_ATTRIBUTES], 913 }); 914 } 915 stop() { 916 var _a; 917 (_a = this.observer) == null ? void 0 : _a.disconnect(); 918 this.observer = null; 919 } 920 processMutations(mutations) { 921 var _a, _b; 922 for (const mutation of mutations) { 923 if (mutation.type === "childList") { 924 if ( 925 mutation.addedNodes.length > 0 && 926 mutation.removedNodes.length === 1 927 ) { 928 const twin = mutation.removedNodes[0]; 929 if ( 930 twin.nodeType === 1 && 931 twin.hasAttribute(ATTRIBUTES.SOURCE_TEXT) 932 ) { 933 mutation.addedNodes.forEach((node) => { 934 if (node.nodeType === 1) { 935 const el = node; 936 if (!el.hasAttribute(ATTRIBUTES.SOURCE_TEXT)) { 937 el.setAttribute( 938 ATTRIBUTES.SOURCE_TEXT, 939 twin.getAttribute(ATTRIBUTES.SOURCE_TEXT) 940 ); 941 const translatedTo = twin.getAttribute( 942 ATTRIBUTES.TRANSLATED_TO 943 ); 944 if (translatedTo) { 945 el.setAttribute(ATTRIBUTES.TRANSLATED_TO, translatedTo); 946 } 947 for (let i = 0; i < twin.attributes.length; i++) { 948 const attr = twin.attributes[i]; 949 if ( 950 attr.name.startsWith(ATTRIBUTES.SOURCE_ATTRIBUTE_PREFIX) 951 ) { 952 if (!el.hasAttribute(attr.name)) { 953 el.setAttribute(attr.name, attr.value); 954 } 955 } 956 } 957 } 958 } 959 }); 960 } 961 } 962 mutation.addedNodes.forEach((node) => { 963 if ( 964 this.isTranslatableElement(node) && 965 !queued.has(node) && 966 !translated.has(node) 967 ) { 968 this.onContentChange(node); 969 } 970 }); 971 } else if (mutation.type === "characterData") { 972 const textNode = mutation.target; 973 const parent = mutation.target.parentElement; 974 if (!parent) continue; 975 const translationState = parent.getAttribute( 976 ATTRIBUTES.TRANSLATION_STATE 977 ); 978 const translatedTo = parent.getAttribute(ATTRIBUTES.TRANSLATED_TO); 979 const currentLang = this.configManager.getCurrentLanguage(); 980 if ( 981 translationState === "translated" || 982 translatedTo === currentLang 983 ) { 984 const stored = getTranslatedText(textNode); 985 const current = 986 ((_a = mutation.target.data) == null ? void 0 : _a.trim()) || ""; 987 if (stored !== void 0 && stored !== current) { 988 parent.setAttribute( 989 ATTRIBUTES.SOURCE_TEXT, 990 mutation.target.data || "" 991 ); 992 clearTranslatedText(textNode); 993 translated.delete(textNode); 994 parent.removeAttribute(ATTRIBUTES.TRANSLATION_STATE); 995 parent.removeAttribute(ATTRIBUTES.TRANSLATED_TO); 996 if (!queued.has(parent)) { 997 this.onContentChange(parent); 998 } 999 continue; 1000 } 1001 } 1002 if ( 1003 this.isTranslatableElement(parent) && 1004 !queued.has(parent) && 1005 !translated.has(parent) 1006 ) { 1007 this.onContentChange(parent); 1008 } 1009 } else if (mutation.type === "attributes") { 1010 const element = mutation.target; 1011 const attrName = mutation.attributeName || ""; 1012 const translationState = element.getAttribute( 1013 ATTRIBUTES.TRANSLATION_STATE 1014 ); 1015 const translatedTo = element.getAttribute(ATTRIBUTES.TRANSLATED_TO); 1016 const currentLang = this.configManager.getCurrentLanguage(); 1017 if ( 1018 (translationState === "translated" || 1019 translatedTo === currentLang) && 1020 attrName 1021 ) { 1022 const storedAttr = getTranslatedAttribute(element, attrName); 1023 const currentVal = 1024 ((_b = element.getAttribute(attrName)) == null 1025 ? void 0 1026 : _b.trim()) || ""; 1027 if (storedAttr !== void 0 && storedAttr !== currentVal) { 1028 const sourceAttributeName = `${ATTRIBUTES.SOURCE_ATTRIBUTE_PREFIX}${attrName}`; 1029 element.setAttribute( 1030 sourceAttributeName, 1031 element.getAttribute(attrName) || "" 1032 ); 1033 clearTranslatedAttribute(element, attrName); 1034 translated.delete(element); 1035 element.removeAttribute(ATTRIBUTES.TRANSLATION_STATE); 1036 element.removeAttribute(ATTRIBUTES.TRANSLATED_TO); 1037 if (!queued.has(element)) { 1038 this.onContentChange(element); 1039 } 1040 continue; 1041 } 1042 } 1043 if ( 1044 element && 1045 this.isTranslatableElement(element) && 1046 !queued.has(element) && 1047 !translated.has(element) && 1048 !element.hasAttribute(ATTRIBUTES.TRANSLATION_STATE) 1049 ) { 1050 this.onContentChange(element); 1051 } 1052 } 1053 } 1054 } 1055 isTranslatableElement(node) { 1056 if (node.nodeType !== 1) return false;
1057 const element = node; 1058 if (element.tagName === "p") return true; 1059 if (element.tagName === "span") return true; 1060 const translationState = element.getAttribute( 1061 ATTRIBUTES.TRANSLATION_STATE 1062 ); 1063 if ( 1064 translationState === "translating" || 1065 translationState === "translated" 1066 ) { 1067 return false; 1068 } 1069 if (element.matches(DROPDOWN_EXCLUDED_SELECTORS.join(", "))) return false; 1070 if (DomUtils.shouldPreventTranslation(element)) return false; 1071 if (DomUtils.isNonContentElement(element)) return false; 1072 const currentLang = this.configManager.getCurrentLanguage(); 1073 if (element.getAttribute(ATTRIBUTES.TRANSLATED_TO) === currentLang) { 1074 element.removeAttribute(ATTRIBUTES.TRANSLATED_TO); 1075 } 1076 return true; 1077 } 1078 } 1079 class TextExtractor { 1080 static extractFromElement(root, shouldCheckUnwantedContent) { 1081 return this.extractWithLanguageFilter(root, shouldCheckUnwantedContent); 1082 } 1083 static extractFreshContentForLanguage( 1084 root, 1085 targetLanguage, 1086 shouldCheckUnwantedContent 1087 ) { 1088 return this.extractWithLanguageFilter( 1089 root, 1090 shouldCheckUnwantedContent, 1091 targetLanguage 1092 ); 1093 } 1094 static extractWithLanguageFilter( 1095 root, 1096 shouldCheckUnwantedContent, 1097 targetLanguage 1098 ) { 1099 const textNodes = []; 1100 const nodeMap = /* @__PURE__ */ new Map(); 1101 const attributeTexts = []; 1102 const attributeMap = /* @__PURE__ */ new Map(); 1103 let textIndex = 0; 1104 let attributeIndex = 0; 1105 const walker = document.createTreeWalker( 1106 root === document ? document.body : root, 1107 NodeFilter.SHOW_TEXT, 1108 { 1109 acceptNode: (node2) => { 1110 var _a; 1111 const text = (_a = node2.textContent) == null ? void 0 : _a.trim(); 1112 const parent = node2.parentNode; 1113 if (!text || text.length === 0) { 1114 return NodeFilter.FILTER_REJECT; 1115 } 1116 if (DomUtils.shouldPreventTranslation(parent)) { 1117 return NodeFilter.FILTER_REJECT; 1118 } 1119 if (parent && DomUtils.isNonContentElement(parent)) { 1120 return NodeFilter.FILTER_REJECT; 1121 } 1122 if (this.isNumberLike(text)) { 1123 return NodeFilter.FILTER_REJECT; 1124 } 1125 if (targetLanguage && parent) { 1126 const translatedTo = parent.getAttribute( 1127 ATTRIBUTES.TRANSLATED_TO 1128 ); 1129 if (translatedTo === targetLanguage) { 1130 return NodeFilter.FILTER_REJECT; 1131 } 1132 } 1133 return NodeFilter.FILTER_ACCEPT; 1134 }, 1135 } 1136 ); 1137 let node; 1138 while ((node = walker.nextNode())) { 1139 const parent = node.parentElement; 1140 if (parent && parent.hasAttribute(ATTRIBUTES.SOURCE_TEXT)) { 1141 textNodes.push(parent.getAttribute(ATTRIBUTES.SOURCE_TEXT)); 1142 } else { 1143 textNodes.push(node.textContent); 1144 } 1145 nodeMap.set(textIndex, node); 1146 textIndex++; 1147 } 1148 const translatableAttributeSelector = TRANSLATABLE_ATTRIBUTES.map( 1149 (attr) => `[${attr}]` 1150 ).join(","); 1151 const elementsWithAttributes = Array.from( 1152 (root === document ? document.body : root).querySelectorAll( 1153 translatableAttributeSelector 1154 ) 1155 ); 1156 elementsWithAttributes.forEach((element) => { 1157 if (DomUtils.shouldPreventTranslation(element)) { 1158 return; 1159 } 1160 if (DomUtils.isNonContentElement(element)) { 1161 return; 1162 } 1163 if ( 1164 targetLanguage && 1165 element.getAttribute(ATTRIBUTES.TRANSLATED_TO) === targetLanguage 1166 ) { 1167 return; 1168 } 1169 TRANSLATABLE_ATTRIBUTES.forEach((attr) => { 1170 const value = element.getAttribute(attr); 1171 if (value && value.trim()) { 1172 const sourceAttribute = `${ATTRIBUTES.SOURCE_ATTRIBUTE_PREFIX}${attr}`; 1173 if (element.hasAttribute(sourceAttribute)) { 1174 attributeTexts.push(element.getAttribute(sourceAttribute)); 1175 } else { 1176 attributeTexts.push(value); 1177 } 1178 attributeMap.set(attributeIndex, { 1179 element, 1180 attribute: attr, 1181 }); 1182 attributeIndex++; 1183 } 1184 }); 1185 }); 1186 return { textNodes, nodeMap, attributeTexts, attributeMap }; 1187 } 1188 static isJsonString(text) {
1189 try { 1190 JSON.parse(text); 1191 return true; 1192 } catch { 1193 return false; 1194 } 1195 } 1196 // eslint-disable-next-line @typescript-eslint/no-unused-vars 1197 static isUnwantedContent(text) { 1198 return false; 1199 } 1200 static isNumberLike(text) { 1201 const t = text.trim(); 1202 if (!t) return false; 1203 if (/[A-Za-z]/.test(t)) return false; 1204 const allowedCharsRegex = /[0-9\s.,+\-:;/()%$â¬Â£â¹]/g; 1205 const stripped = t.replace(allowedCharsRegex, ""); 1206 return /[0-9]/.test(t) && stripped.length === 0; 1207 } 1208 } 1209 class DebouncedSender { 1210 constructor(sendFn, delay = 70, concurrencyLimit = 4) { 1211 this.sendFn = sendFn; 1212 this.delay = delay; 1213 this.concurrencyLimit = concurrencyLimit; 1214 this.pending = /* @__PURE__ */ new Set(); 1215 this.requestQueue = []; 1216 this.activeRequests = 0; 1217 } 1218 enqueue(el) { 1219 if (queued.has(el) || translated.has(el)) { 1220 return; 1221 } 1222 this.pending.add(el); 1223 if (this.timerId) { 1224 clearTimeout(this.timerId); 1225 } 1226 this.timerId = window.setTimeout(() => this.flush(), this.delay); 1227 } 1228 flush() { 1229 if (this.pending.size === 0) return; 1230 const elems = Array.from(this.pending); 1231 this.pending.clear(); 1232 this.timerId = void 0; 1233 const payload = collectUniqueContent(elems); 1234 if (payload.texts.length === 0 && payload.attributeTexts.length === 0) 1235 return; 1236 this.requestQueue.push(payload); 1237 this.processQueue(); 1238 } 1239 processQueue() { 1240 if ( 1241 this.activeRequests >= this.concurrencyLimit || 1242 this.requestQueue.length === 0 1243 ) { 1244 return; 1245 } 1246 this.activeRequests++; 1247 const payload = this.requestQueue.shift(); 1248 payload.textMap.forEach((n) => queued.add(n)); 1249 payload.attributeMap.forEach(({ element }) => queued.add(element)); 1250 this.sendFn(payload) 1251 .catch((err) => { 1252 console.error("DebouncedSender sendFn error:", err); 1253 payload.textMap.forEach((n) => queued.delete(n)); 1254 payload.attributeMap.forEach(({ element }) => queued.delete(element)); 1255 }) 1256 .finally(() => { 1257 this.activeRequests--; 1258 this.processQueue(); 1259 }); 1260 } 1261 } 1262 function collectUniqueContent(elems) { 1263 const texts = []; 1264 const textMap = /* @__PURE__ */ new Map(); 1265 const attributeTexts = []; 1266 const attributeMap = /* @__PURE__ */ new Map(); 1267 let textIdx = 0; 1268 let attrIdx = 0; 1269 elems.forEach((el) => { 1270 const { 1271 textNodes, 1272 nodeMap, 1273 attributeTexts: elemAttrTexts, 1274 attributeMap: elemAttrMap, 1275 } = TextExtractor.extractFreshContentForLanguage( 1276 el, 1277 document.documentElement.lang, 1278 (text) => 1279 TextExtractor.isJsonString(text) || 1280 TextExtractor.isUnwantedContent(text) 1281 ); 1282 textNodes.forEach((txt, localIdx) => { 1283 const node = nodeMap.get(localIdx); 1284 if (!node) return; 1285 if (queued.has(node) || translated.has(node)) return; 1286 texts.push(txt); 1287 textMap.set(textIdx++, node); 1288 });
1289 elemAttrTexts.forEach((txt, localIdx) => { 1290 const attrInfo = elemAttrMap.get(localIdx); 1291 if (!attrInfo) return; 1292 if (queued.has(attrInfo.element) || translated.has(attrInfo.element)) 1293 return; 1294 attributeTexts.push(txt); 1295 attributeMap.set(attrIdx++, attrInfo); 1296 }); 1297 }); 1298 return { texts, textMap, attributeTexts, attributeMap }; 1299 } 1300 class NavigationService { 1301 constructor() { 1302 this.handler = null; 1303 this.debounceTimer = null; 1304 this.isListening = false; 1305 this.originalPushState = null; 1306 this.originalReplaceState = null; 1307 this.onRouteChange = () => { 1308 this.clearDebounce(); 1309 this.debounceTimer = window.setTimeout(() => { 1310 if (this.handler) { 1311 const currentUrl = 1312 window.location.pathname + window.location.search; 1313 this.handler(currentUrl); 1314 } 1315 }, TIMINGS.SPA_NAVIGATION_DEBOUNCE); 1316 }; 1317 } 1318 setHandler(handler) { 1319 this.handler = handler; 1320 } 1321 start() { 1322 if (this.isListening) return; 1323 this.isListening = true; 1324 this.hookHistoryAPI(); 1325 this.addPopstateListener(); 1326 } 1327 stop() { 1328 if (!this.isListening) return; 1329 this.isListening = false; 1330 this.unhookHistoryAPI(); 1331 this.removePopstateListener(); 1332 this.clearDebounce(); 1333 } 1334 hookHistoryAPI() { 1335 this.originalPushState = history.pushState.bind(history); 1336 this.originalReplaceState = history.replaceState.bind(history); 1337 const getPathSearch = () => 1338 window.location.pathname + window.location.search; 1339 const toPathSearch = (url) => { 1340 if (url == null) return getPathSearch(); 1341 try { 1342 const u = new URL(String(url), window.location.href); 1343 return u.pathname + u.search; 1344 } catch { 1345 return getPathSearch(); 1346 } 1347 }; 1348 history.pushState = (...args) => { 1349 const before = getPathSearch(); 1350 const target = toPathSearch(args[2]); 1351 const result = this.originalPushState.apply(history, args); 1352 if (target !== before) { 1353 this.onRouteChange(); 1354 } 1355 return result; 1356 }; 1357 history.replaceState = (...args) => { 1358 const before = getPathSearch(); 1359 const target = toPathSearch(args[2]); 1360 const result = this.originalReplaceState.apply(history, args); 1361 if (target !== before) { 1362 this.onRouteChange(); 1363 } 1364 return result; 1365 }; 1366 } 1367 unhookHistoryAPI() { 1368 if (this.originalPushState) { 1369 history.pushState = this.originalPushState; 1370 this.originalPushState = null; 1371 } 1372 if (this.originalReplaceState) { 1373 history.replaceState = this.originalReplaceState; 1374 this.originalReplaceState = null; 1375 } 1376 } 1377 addPopstateListener() { 1378 window.addEventListener("popstate", this.onRouteChange); 1379 } 1380 removePopstateListener() { 1381 window.removeEventListener("popstate", this.onRouteChange); 1382 } 1383 clearDebounce() { 1384 if (this.debounceTimer) { 1385 clearTimeout(this.debounceTimer); 1386 this.debounceTimer = null; 1387 } 1388 } 1389 } 1390 class PageContextCollector { 1391 static collectPageContext() { 1392 var _a, _b; 1393 const context = {}; 1394 const title = (_a = document.title) == null ? void 0 : _a.trim(); 1395 if (title && title.length > 0) { 1396 context.title = title; 1397 } 1398 const descriptionMeta = document.querySelector( 1399 'meta[name="description"]' 1400 ); 1401 const description = 1402 (_b = descriptionMeta == null ? void 0 : descriptionMeta.content) == 1403 null 1404 ? void 0 1405 : _b.trim(); 1406 if (description && description.length > 0) { 1407 context.description = description; 1408 } 1409 context.url = window.location.pathname + window.location.search; 1410 return context; 1411 } 1412 } 1413 class MetadataUtils { 1414 static collectMetadata() { 1415 var _a, _b; 1416 const list = []; 1417 const title = (_a = document.title) == null ? void 0 : _a.trim(); 1418 if (title) { 1419 list.push(["title", title]); 1420 } 1421 const desc = document.querySelector('meta[name="description"]'); 1422 const descContent = 1423 (_b = desc == null ? void 0 : desc.content) == null 1424 ? void 0 1425 : _b.trim(); 1426 if (descContent) { 1427 list.push(["description", descContent]); 1428 } 1429 return list; 1430 } 1431 static applyMetadata(translated2) { 1432 const html = document.documentElement; 1433 translated2.forEach(([key, val]) => { 1434 if (key === "title") { 1435 document.title = val; 1436 html.setAttribute( 1437 ATTRIBUTES.TRANSLATED_TO, 1438 document.documentElement.lang 1439 ); 1440 } else if (key === "description") { 1441 const meta = document.querySelector('meta[name="description"]'); 1442 if (meta) { 1443 meta.content = val; 1444 html.setAttribute( 1445 ATTRIBUTES.TRANSLATED_TO, 1446 document.documentElement.lang 1447 ); 1448 } 1449 } 1450 }); 1451 } 1452 } 1453 class SkeletonManager { 1454 static apply(node) { 1455 var _a; 1456 if ( 1457 node.nodeType !== Node.TEXT_NODE || 1458 !((_a = node.textContent) == null ? void 0 : _a.trim()) 1459 ) { 1460 return null; 1461 } 1462 const parent = node.parentElement; 1463 if ( 1464 !parent || 1465 DomUtils.isNonContentElement(parent) || 1466 DomUtils.shouldPreventTranslation(parent) 1467 ) { 1468 return null; 1469 } 1470 const wrapper = document.createElement("span"); 1471 wrapper.className = "tl-skeleton";
1472 const bgColor = this.findInheritedBackgroundColor(parent); 1473 const hexColor = this.rgbToHex(bgColor); 1474 const skeletonBgColor = this.adjustColorBrightness(hexColor, -5); 1475 const skeletonHighlightColor = this.adjustColorBrightness(hexColor, 10); 1476 wrapper.style.setProperty("--skeleton-bg-color", skeletonBgColor); 1477 wrapper.style.setProperty( 1478 "--skeleton-highlight-color", 1479 skeletonHighlightColor 1480 ); 1481 if (node.parentNode) { 1482 node.parentNode.insertBefore(wrapper, node); 1483 wrapper.appendChild(node); 1484 } 1485 return wrapper; 1486 } 1487 static remove(skeletonWrapper) { 1488 if (!skeletonWrapper || !skeletonWrapper.parentNode) return; 1489 const originalNode = skeletonWrapper.firstChild; 1490 if (originalNode && skeletonWrapper.parentNode) { 1491 skeletonWrapper.parentNode.insertBefore(originalNode, skeletonWrapper); 1492 } 1493 skeletonWrapper.remove(); 1494 } 1495 static findInheritedBackgroundColor(element) { 1496 let current = element; 1497 while (current) { 1498 const color = window.getComputedStyle(current).backgroundColor; 1499 if (color && color !== "rgba(0, 0, 0, 0)" && color !== "transparent") { 1500 return color; 1501 } 1502 if (current === document.body) break; 1503 current = current.parentElement; 1504 } 1505 return ( 1506 window.getComputedStyle(document.documentElement).backgroundColor || 1507 "#65737e" 1508 ); 1509 } 1510 static rgbToHex(rgb) { 1511 const result = rgb.match(/\d+/g); 1512 if (!result) return "#e0e0e0"; 1513 return ( 1514 "#" + 1515 result 1516 .slice(0, 3) 1517 .map((x) => ("0" + parseInt(x).toString(16)).slice(-2)) 1518 .join("") 1519 ); 1520 } 1521 static adjustColorBrightness(hex, percent) { 1522 const num = parseInt(hex.replace("#", ""), 16); 1523 const amt = Math.round(2.55 * percent); 1524 const R = (num >> 16) + amt; 1525 const G = ((num >> 8) & 255) + amt; 1526 const B = (num & 255) + amt; 1527 return ( 1528 "#" + 1529 ( 1530 16777216 + 1531 (this.clamp(R, 0, 255) << 16) + 1532 (this.clamp(G, 0, 255) << 8) + 1533 this.clamp(B, 0, 255) 1534 ) 1535 .toString(16) 1536 .slice(1) 1537 ); 1538 } 1539 static clamp(val, min, max) { 1540 return Math.min(Math.max(val, min), max); 1541 } 1542 } 1543 const BUILD_INFO = { 1544 version: "1.0.1", 1545 buildTime: /* @__PURE__ */ new Date().toISOString(), 1546 environment: "dev", 1547 }; 1548 if (typeof window !== "undefined") { 1549 window.__TRANSLATOR_BUILD_INFO = BUILD_INFO; 1550 } 1551 class TextTranslationEngine { 1552 constructor(apiService, configManager) { 1553 this.observer = null; 1554 this.currentAbortController = null; 1555 this.languageDropdown = null; 1556 this.translatedLabels = void 0; 1557 this.isTranslating = false; 1558 this.originalTitle = null; 1559 this.originalDescription = null; 1560 this.apiService = apiService; 1561 this.configManager = configManager; 1562 this.sender = new DebouncedSender( 1563 (payload) => this.translateBatch(payload), 1564 TIMINGS.DYNAMIC_TRANSLATION_DEBOUNCE 1565 ); 1566 this.navigationService = new NavigationService();
1567 // Removed URL change handler to prevent translations on navigation 1568 this.navigationService.setHandler(() => this.handleUrlChange()); 1569 } 1570 setTranslating(isTranslating) { 1571 this.isTranslating = isTranslating; 1572 this.updateLanguageDropdown(this.translatedLabels); 1573 } 1574 // eslint-disable-next-line @typescript-eslint/no-unused-vars 1575 updateLanguageDropdown(translatedDropdownLabels) { 1576 const existing = this.languageDropdown; 1577 if (existing) { 1578 try { 1579 existing.remove(); 1580 } catch {} 1581 this.languageDropdown = null; 1582 } 1583 return; 1584 } 1585 abortCurrentTranslation() { 1586 if (this.currentAbortController) { 1587 this.currentAbortController.abort(); 1588 this.currentAbortController = null; 1589 } 1590 } 1591 async restoreToSourceLanguage() { 1592 var _a; 1593 (_a = this.observer) == null ? void 0 : _a.stop(); 1594 document 1595 .querySelectorAll(`[${ATTRIBUTES.SOURCE_TEXT}]`) 1596 .forEach((element) => { 1597 const sourceText = element.getAttribute(ATTRIBUTES.SOURCE_TEXT); 1598 const childNodes = Array.from(element.childNodes); 1599 const textNode = childNodes.find( 1600 (node) => node.nodeType === Node.TEXT_NODE 1601 ); 1602 if (sourceText && textNode) { 1603 textNode.textContent = sourceText; 1604 clearTranslatedText(textNode); 1605 } 1606 element.removeAttribute(ATTRIBUTES.TRANSLATED_TO); 1607 element.removeAttribute(ATTRIBUTES.TRANSLATION_STATE); 1608 }); 1609 document 1610 .querySelectorAll(`[${ATTRIBUTES.TRANSLATED_TO}]`) 1611 .forEach((element) => { 1612 const attributesToRemove = []; 1613 for (let i = 0; i < element.attributes.length; i++) { 1614 const attr = element.attributes[i]; 1615 if (attr.name.startsWith(ATTRIBUTES.SOURCE_ATTRIBUTE_PREFIX)) { 1616 const originalAttrName = attr.name.substring( 1617 ATTRIBUTES.SOURCE_ATTRIBUTE_PREFIX.length 1618 ); 1619 element.setAttribute(originalAttrName, attr.value); 1620 attributesToRemove.push(attr.name); 1621 } 1622 } 1623 attributesToRemove.forEach((attrName) => 1624 element.removeAttribute(attrName) 1625 ); 1626 element.removeAttribute(ATTRIBUTES.TRANSLATED_TO); 1627 element.removeAttribute(ATTRIBUTES.TRANSLATION_STATE); 1628 clearTranslatedAttribute(element); 1629 }); 1630 document 1631 .querySelectorAll(`[${ATTRIBUTES.TRANSLATION_STATE}]`) 1632 .forEach((element) => { 1633 element.removeAttribute(ATTRIBUTES.TRANSLATION_STATE); 1634 }); 1635 if (this.originalTitle) { 1636 document.title = this.originalTitle; 1637 } 1638 const meta = document.querySelector('meta[name="description"]'); 1639 if (meta && this.originalDescription) { 1640 meta.content = this.originalDescription; 1641 } 1642 this.translatedLabels = void 0; 1643 } 1644 applyTranslatedTextNodes(translatedTexts, nodeMap, targetLang) { 1645 translatedTexts.forEach((translatedText, index) => { 1646 var _a, _b; 1647 const originalNode = nodeMap.get(index); 1648 if (!originalNode || !originalNode.parentElement) return; 1649 const parentElement = originalNode.parentElement; 1650 const originalText = 1651 parentElement.getAttribute(ATTRIBUTES.SOURCE_TEXT) || ""; 1652 if (!parentElement.hasAttribute(ATTRIBUTES.SOURCE_TEXT)) { 1653 parentElement.setAttribute( 1654 ATTRIBUTES.SOURCE_TEXT, 1655 originalNode.textContent || "" 1656 ); 1657 } 1658 parentElement.setAttribute(ATTRIBUTES.TRANSLATION_STATE, "translated"); 1659 parentElement.setAttribute(ATTRIBUTES.TRANSLATED_TO, targetLang); 1660 const currentSourceText = 1661 parentElement.getAttribute(ATTRIBUTES.SOURCE_TEXT) || ""; 1662 const trimmedTranslated = translatedText.trim(); 1663 try { 1664 // Log text translations: source -> translated (targetLang) 1665 // console.log( 1666 // "[WebsiteTranslate] Text:", 1667 // `"${currentSourceText}" -> "${trimmedTranslated}" [${targetLang}]` 1668 // ); 1669 } catch {} 1670 setTranslatedText(originalNode, trimmedTranslated); 1671 if (currentSourceText.trim() !== trimmedTranslated) { 1672 const leadingWs = 1673 ((_a = originalText.match(/^\s+/)) == null ? void 0 : _a[0]) || ""; 1674 const trailingWs = 1675 ((_b = originalText.match(/\s+$/)) == null ? void 0 : _b[0]) || ""; 1676 originalNode.textContent = `${leadingWs}${trimmedTranslated}${trailingWs}`; 1677 } 1678 }); 1679 const config = this.configManager.getTranslationConfig(); 1680 if (config) { 1681 DomUtils.updateCanonicalUrl(targetLang, config.defaultLang); 1682 } 1683 } 1684 async translatePage(targetLang) { 1685 // Add guard to prevent multiple simultaneous translations 1686 if (this.isTranslating) { 1687 console.log("â ï¸ Translation already in progress, skipping duplicate request..."); 1688 return; 1689 } 1690 1691 var translationApi = localStorage.getItem('translationApi'); 1692 1693 if(translationApi) { 1694 try { 1695 var translationApi = JSON.parse(translationApi); 1696 console.log('translationApi', translationApi); 1697 var translationLimit = translationApi.translationLimit; 1698 var translationUsed = translationApi.used; 1699 1700 if(translationUsed >= translationLimit) { 1701 ErrorHandler.showErrorMessage( 1702 "translation-limit", 1703 "You have reached the 24 hour translation limit. Please try again later." 1704 ); 1705 this.setTranslating(false); 1706 return; 1707 } 1708 } catch (error) { 1709 console.error('Error parsing translationApi:', error); 1710 return; 1711 } 1712 } 1713 1714 1715 this.abortCurrentTranslation(); 1716 this.setTranslating(true); 1717 const config = this.configManager.getTranslationConfig(); 1718 if (!config) { 1719 console.error("â No config available for translation"); 1720 this.setTranslating(false); 1721 return; 1722 } 1723 if (targetLang === config.defaultLang) { 1724 this.setTranslating(false); 1725 return this.restoreToSourceLanguage(); 1726 } 1727 const { textNodes, nodeMap, attributeTexts, attributeMap } = 1728 TextExtractor.extractFreshContentForLanguage( 1729 document, 1730 targetLang, 1731 (text) => 1732 TextExtractor.isJsonString(text) || 1733 TextExtractor.isUnwantedContent(text) 1734 ); 1735 let dropdownLabels = []; 1736 let dropdownLabelKeys = []; 1737 if (config) { 1738 const allLangs = /* @__PURE__ */ new Set([ 1739 ...config.targetLanguages, 1740 config.defaultLang, 1741 ]); 1742 dropdownLabelKeys = Array.from(allLangs).filter( 1743 (key) => config.languageLabels[key] 1744 ); 1745 dropdownLabels = dropdownLabelKeys.map( 1746 (key) => config.languageLabels[key] 1747 ); 1748 } 1749 if (!this.originalTitle) { 1750 this.originalTitle = document.title; 1751 } 1752 const metaDesc = document.querySelector('meta[name="description"]'); 1753 if (metaDesc && !this.originalDescription) { 1754 this.originalDescription = metaDesc.content; 1755 } 1756 const metadata = MetadataUtils.collectMetadata(); 1757 const metadataTexts = metadata.map(([, value]) => value); 1758 if ( 1759 textNodes.length === 0 && 1760 metadataTexts.length === 0 && 1761 attributeTexts.length === 0 1762 ) { 1763 this.setTranslating(false); 1764 return; 1765 } 1766 const skeletonMap = /* @__PURE__ */ new Map(); 1767 // eslint-disable-next-line @typescript-eslint/no-unused-vars
1768 nodeMap.forEach((node, _key) => { 1769 const parentElement = node.parentElement; 1770 if ( 1771 parentElement && 1772 !parentElement.hasAttribute(ATTRIBUTES.SOURCE_TEXT) 1773 ) { 1774 const originalText = node.textContent || ""; 1775 parentElement.setAttribute(ATTRIBUTES.SOURCE_TEXT, originalText); 1776 } 1777 }); 1778 nodeMap.forEach((node, key) => { 1779 const skeleton = SkeletonManager.apply(node); 1780 if (skeleton) { 1781 skeletonMap.set(key, skeleton); 1782 } 1783 }); 1784 try { 1785 const translationConfig = this.configManager.getTranslationConfig(); 1786 const apiConfig = this.configManager.getApiConfig(); 1787 if (!translationConfig || !apiConfig) { 1788 console.error("â No config available for translation"); 1789 this.setTranslating(false); 1790 return; 1791 } 1792 const translateConfig = { 1793 targetLanguage: targetLang, 1794 sourceLanguage: translationConfig.defaultLang, 1795 websiteId: translationConfig.websiteId, 1796 }; 1797 this.currentAbortController = new AbortController(); 1798 const pageContext = PageContextCollector.collectPageContext(); 1799 const fullTextNodes = [ 1800 ...textNodes, 1801 ...attributeTexts, 1802 ...metadataTexts, 1803 ]; 1804 const response = await this.apiService.translateTextBased({ 1805 textNodes: fullTextNodes, 1806 translateConfig, 1807 apiConfig, 1808 dropdownLabels, 1809 pageContext, 1810 abortSignal: this.currentAbortController.signal, 1811 }); 1812 skeletonMap.forEach((skeleton) => { 1813 SkeletonManager.remove(skeleton); 1814 }); 1815 const translatedFullNodes = response.translatedTextNodes || []; 1816 const translatedDropdownLabels = 1817 response.translatedDropdownLabels || []; 1818 const translatedTextNodes = translatedFullNodes.slice( 1819 0, 1820 textNodes.length 1821 ); 1822 const translatedAttributeTexts = translatedFullNodes.slice( 1823 textNodes.length, 1824 textNodes.length + attributeTexts.length 1825 ); 1826 const translatedMetadataTexts = translatedFullNodes.slice( 1827 textNodes.length + attributeTexts.length 1828 ); 1829 this.applyTranslatedTextNodes(translatedTextNodes, nodeMap, targetLang); 1830 this.applyTranslatedAttributes( 1831 translatedAttributeTexts, 1832 attributeMap, 1833 targetLang 1834 ); 1835 const translatedMetadata = metadata.map(([key], i) => [ 1836 key, 1837 translatedMetadataTexts[i] || "", 1838 ]); 1839 MetadataUtils.applyMetadata(translatedMetadata); 1840 document.documentElement.lang = targetLang; 1841 const translatedDropdownLabelsMap = {}; 1842 dropdownLabelKeys.forEach((key, i) => { 1843 if (translatedDropdownLabels[i]) { 1844 translatedDropdownLabelsMap[key] = translatedDropdownLabels[i]; 1845 } 1846 }); 1847 this.translatedLabels = translatedDropdownLabelsMap; 1848 this.updateLanguageDropdown(translatedDropdownLabelsMap); 1849 this.setTranslating(false); 1850 } catch (error) { 1851 skeletonMap.forEach((skeleton) => { 1852 SkeletonManager.remove(skeleton); 1853 }); 1854 if (ApiService.isAbortError(error)) { 1855 this.setTranslating(false); 1856 return; 1857 } 1858 console.error("â Text translation failed:", error); 1859 this.setTranslating(false); 1860 throw error; 1861 } finally { 1862 this.currentAbortController = null; 1863 } 1864 // Removed recursive call to prevent translation loops 1865 appUserRegistration.startTranslation(); 1866 } 1867 applyTranslatedAttributes(translatedAttributes, attributeMap, targetLang) { 1868 translatedAttributes.forEach((translatedText, index) => { 1869 const attributeInfo = attributeMap.get(index); 1870 if (!attributeInfo) return; 1871 const { element, attribute } = attributeInfo; 1872 const sourceAttributeName = `${ATTRIBUTES.SOURCE_ATTRIBUTE_PREFIX}${attribute}`; 1873 const originalText = element.getAttribute(attribute) || ""; 1874 if (!element.hasAttribute(sourceAttributeName)) { 1875 element.setAttribute(sourceAttributeName, originalText); 1876 } 1877 element.setAttribute(ATTRIBUTES.TRANSLATION_STATE, "translated"); 1878 element.setAttribute(ATTRIBUTES.TRANSLATED_TO, targetLang); 1879 const trimmedTranslated = translatedText.trim(); 1880 try { 1881 // Log attribute translations: source -> translated (targetLang) 1882 // console.log( 1883 // "[WebsiteTranslate] Attribute:", 1884 // attribute, 1885 // `"${originalText}" -> "${trimmedTranslated}" [${targetLang}]` 1886 // ); 1887 } catch {} 1888 setTranslatedAttribute(element, attribute, trimmedTranslated); 1889 if (originalText.trim() !== trimmedTranslated) { 1890 element.setAttribute(attribute, trimmedTranslated); 1891 } 1892 }); 1893 } 1894 async translateBatch(payload) { 1895 const { texts, textMap, attributeTexts, attributeMap } = payload; 1896 if (texts.length === 0 && attributeTexts.length === 0) return; 1897 1898 // Add guard to prevent multiple simultaneous batch translations 1899 if (this.isTranslating) { 1900 console.log("â ï¸ Translation already in progress, skipping batch request..."); 1901 return; 1902 }
1903 textMap.forEach((node, index) => { 1904 const parent = node.parentElement; 1905 if (parent && !parent.hasAttribute(ATTRIBUTES.SOURCE_TEXT)) { 1906 parent.setAttribute(ATTRIBUTES.SOURCE_TEXT, texts[index]); 1907 } 1908 }); 1909 attributeMap.forEach(({ element, attribute }, index) => { 1910 const sourceAttributeName = `${ATTRIBUTES.SOURCE_ATTRIBUTE_PREFIX}${attribute}`; 1911 if (!element.hasAttribute(sourceAttributeName)) { 1912 element.setAttribute(sourceAttributeName, attributeTexts[index]); 1913 } 1914 }); 1915 textMap.forEach((node) => { 1916 var _a; 1917 (_a = node.parentElement) == null 1918 ? void 0 1919 : _a.setAttribute(ATTRIBUTES.TRANSLATION_STATE, "translating"); 1920 }); 1921 attributeMap.forEach(({ element }) => { 1922 element.setAttribute(ATTRIBUTES.TRANSLATION_STATE, "translating"); 1923 }); 1924 const apiConfig = this.configManager.getApiConfig(); 1925 const translationConfig = this.configManager.getTranslationConfig(); 1926 if (!apiConfig || !translationConfig) return; 1927 if (this.configManager.isInDefaultLanguage()) { 1928 textMap.forEach((n) => { 1929 queued.delete(n); 1930 translated.add(n); 1931 }); 1932 attributeMap.forEach(({ element }) => { 1933 queued.delete(element); 1934 translated.add(element); 1935 }); 1936 return; 1937 } 1938 const skeletonMap = /* @__PURE__ */ new Map(); 1939 textMap.forEach((node, key) => { 1940 const skeleton = SkeletonManager.apply(node); 1941 if (skeleton) { 1942 skeletonMap.set(key, skeleton); 1943 } 1944 }); 1945 const translateCfg = { 1946 targetLanguage: this.configManager.getCurrentLanguage(), 1947 sourceLanguage: translationConfig.defaultLang, 1948 websiteId: translationConfig.websiteId, 1949 }; 1950 try { 1951 const allTexts = [...texts, ...attributeTexts]; 1952 const resp = await this.apiService.translateTextBased({ 1953 textNodes: allTexts, 1954 translateConfig: translateCfg, 1955 apiConfig, 1956 dropdownLabels: [], 1957 pageContext: PageContextCollector.collectPageContext(), 1958 }); 1959 const allTranslated = resp.translatedTextNodes || []; 1960 const translatedTexts = allTranslated.slice(0, texts.length); 1961 const translatedAttributes = allTranslated.slice(texts.length); 1962 skeletonMap.forEach((skeleton) => { 1963 SkeletonManager.remove(skeleton); 1964 }); 1965 this.applyTranslatedTextNodes( 1966 translatedTexts, 1967 textMap, 1968 translateCfg.targetLanguage 1969 ); 1970 this.applyTranslatedAttributes( 1971 translatedAttributes, 1972 attributeMap, 1973 translateCfg.targetLanguage 1974 ); 1975 textMap.forEach((n) => { 1976 queued.delete(n); 1977 translated.add(n); 1978 }); 1979 attributeMap.forEach(({ element }) => { 1980 queued.delete(element); 1981 translated.add(element); 1982 }); 1983 } catch (error) { 1984 console.error("â translateBatch failed:", error); 1985 textMap.forEach((node) => { 1986 var _a; 1987 (_a = node.parentElement) == null 1988 ? void 0 1989 : _a.removeAttribute(ATTRIBUTES.TRANSLATION_STATE); 1990 }); 1991 attributeMap.forEach(({ element }) => { 1992 element.removeAttribute(ATTRIBUTES.TRANSLATION_STATE); 1993 }); 1994 skeletonMap.forEach((skeleton) => SkeletonManager.remove(skeleton)); 1995 } 1996 } 1997 async switchLanguage(newLang) { 1998 var _a; 1999 if (newLang === this.configManager.getCurrentLanguage()) return; 2000 const config = this.configManager.getTranslationConfig(); 2001 if (!config) { 2002 console.error("â No configuration available"); 2003 return; 2004 } 2005 if (newLang === config.defaultLang) { 2006 this.configManager.setCurrentLanguage(newLang); 2007 window.location.reload(); 2008 return; 2009 } 2010 try { 2011 this.abortCurrentTranslation(); 2012 (_a = this.observer) == null ? void 0 : _a.stop(); 2013 await this.restoreToSourceLanguage(); 2014 await this.translatePage(newLang); 2015 this.configManager.setCurrentLanguage(newLang); 2016 applyDirection(newLang); 2017 this.updateLanguageDropdown(this.translatedLabels); 2018 this.startObserver(); 2019 } catch (e) { 2020 console.error("â Switch language failed:", e); 2021 if (e instanceof Error) {
2022 if (e.message === "NETWORK_ERROR") { 2023 ErrorHandler.showErrorMessage( 2024 "network", 2025 "Failed to switch language. Please check your connection." 2026 ); 2027 } else if (e.message === "RATE_LIMIT") { 2028 ErrorHandler.showErrorMessage( 2029 "rate-limit", 2030 "Too many requests. Please try again later." 2031 ); 2032 } else { 2033 ErrorHandler.showErrorMessage( 2034 "server", 2035 "Failed to switch language. Please try again." 2036 ); 2037 } 2038 } 2039 this.updateLanguageDropdown(); 2040 this.startObserver(); 2041 } 2042 } 2043 clearMetadataSourceAttributes() { 2044 this.originalTitle = null; 2045 this.originalDescription = null; 2046 } 2047 clearTranslationAttributes() { 2048 document 2049 .querySelectorAll(`[${ATTRIBUTES.TRANSLATED_TO}]`) 2050 .forEach((element) => { 2051 element.removeAttribute(ATTRIBUTES.TRANSLATED_TO); 2052 }); 2053 document 2054 .querySelectorAll(`[${ATTRIBUTES.TRANSLATION_STATE}]`) 2055 .forEach((element) => { 2056 element.removeAttribute(ATTRIBUTES.TRANSLATION_STATE); 2057 }); 2058 } 2059 async waitForInitialContent() { 2060 await waitForDomToSettle(document.body); 2061 } 2062 async initialize() { 2063 if (!SCRIPT_CONFIG.apiKey) { 2064 console.error("â No API key provided"); 2065 ErrorHandler.showErrorMessage( 2066 "auth", 2067 "Translation service not configured. Missing API key." 2068 ); 2069 return; 2070 } 2071 if (!SCRIPT_CONFIG.domain) { 2072 console.error("â No domain provided"); 2073 ErrorHandler.showErrorMessage( 2074 "config-invalid", 2075 "Translation service not configured. Missing domain." 2076 ); 2077 return; 2078 } 2079 DomUtils.detectCSPViolations(); 2080 await this.waitForDOMReady(); 2081 await this.waitForInitialContent(); 2082 const config = await this.configManager.initializeConfig(); 2083 if (!config) { 2084 console.error("Initialization failed: No config available"); 2085 return; 2086 } 2087 const style = document.createElement("style"); 2088 style.textContent = ` 2089 .tl-skeleton { 2090 color: transparent !important; 2091 background-color: var(--skeleton-bg-color, #e0e0e0) !important; 2092 background-image: linear-gradient( 2093 90deg, 2094 var(--skeleton-bg-color, #e0e0e0), 2095 var(--skeleton-highlight-color, #f0f0f0), 2096 var(--skeleton-bg-color, #e0e0e0) 2097 ) !important; 2098 background-size: 200% 100% !important; 2099 animation: skeleton-pulse 1.5s linear infinite !important; 2100 border-radius: 4px !important; 2101 user-select: none !important; 2102 margin-left: 1px !important; 2103 margin-right: 1px !important; 2104 -webkit-user-select: none !important; 2105 } 2106 2107 @keyframes skeleton-pulse { 2108 0% { 2109 background-position: 100% 0; 2110 } 2111 100% { 2112 background-position: -100% 0; 2113 } 2114 } 2115 `; 2116 document.head.appendChild(style); 2117 this.updateLanguageDropdown(); 2118 this.startObserver(); 2119 if (!this.configManager.isInDefaultLanguage()) { 2120 const currentLang = this.configManager.getCurrentLanguage(); 2121 const config2 = this.configManager.getTranslationConfig(); 2122 if (currentLang === (config2 == null ? void 0 : config2.defaultLang)) { 2123 await this.restoreToSourceLanguage(); 2124 } else { 2125 await this.translatePage(currentLang); 2126 } 2127 } 2128 applyDirection(this.configManager.getCurrentLanguage()); 2129 // Disabled navigation service to prevent URL change translations 2130 this.navigationService.start(); 2131 window.addEventListener("offline", () => { 2132 ErrorHandler.showErrorMessage( 2133 "offline", 2134 "You are now offline. Using cached translations where available." 2135 ); 2136 }); 2137 window.addEventListener("online", () => { 2138 ErrorHandler.clearError(); 2139 }); 2140 window.addEventListener("beforeunload", () => { 2141 this.abortCurrentTranslation(); 2142 // Navigation service disabled - no need to stop 2143 this.navigationService.stop(); 2144 }); 2145 } 2146 async waitForDOMReady() { 2147 if (document.readyState === "loading") { 2148 await new Promise((resolve) => { 2149 document.addEventListener("DOMContentLoaded", resolve); 2150 }); 2151 } 2152 } 2153 async handleUrlChange() { 2154 console.log("ð URL change detected"); 2155 2156 // Clear any existing debounce timer 2157 if (this.urlChangeDebounceTimer) { 2158 clearTimeout(this.urlChangeDebounceTimer); 2159 } 2160 2161 // Debounce URL changes to prevent rapid multiple calls 2162 this.urlChangeDebounceTimer = setTimeout(async () => { 2163 const now = Date.now(); 2164 2165 // Prevent multiple URL change translations within 1 second 2166 if (now - this.lastUrlChangeTime < 1000) { 2167 console.log("â ï¸ URL change too recent, skipping duplicate translation..."); 2168 return; 2169 } 2170
2171 this.lastUrlChangeTime = now; 2172 2173 // Check if translation is already in progress 2174 if (this.isTranslating) { 2175 console.log("â ï¸ Translation already in progress, skipping URL change translation..."); 2176 return; 2177 } 2178 2179 this.abortCurrentTranslation(); 2180 this.clearTranslationAttributes(); 2181 this.clearMetadataSourceAttributes(); 2182 await waitForDomToSettle(document.body); 2183 2184 if (!this.configManager.isInDefaultLanguage()) { 2185 try { 2186 console.log("ð Starting translation for URL change..."); 2187 await this.translatePage(this.configManager.getCurrentLanguage()); 2188 } catch (err) { 2189 console.error("â Translation failed during URL change:", err); 2190 } 2191 } else { 2192 console.log("â¹ï¸ Already in default language, no translation needed"); 2193 } 2194 }, 300); // 300ms debounce delay 2195 } 2196 enqueue(element) { 2197 this.sender.enqueue(element); 2198 } 2199 startObserver() { 2200 // Stop existing observer before creating new one to prevent multiple instances 2201 if (this.observer) { 2202 console.log("â ï¸ Stopping existing observer to prevent multiple instances"); 2203 this.observer.stop(); 2204 this.observer = null; 2205 } 2206 2207 const config = this.configManager.getTranslationConfig(); 2208 if (!config) return; 2209 2210 console.log("ð Starting new observer..."); 2211 this.observer = new TranslationObserver( 2212 () => {}, 2213 (element) => this.enqueue(element), 2214 this.configManager 2215 ); 2216 this.observer.start(); 2217 } 2218 } 2219 const SCRIPT_CONFIG = (() => { 2220 const tag = document.currentScript; 2221 const rawDomain = 2222 (tag == null ? void 0 : tag.dataset.domain) || window.location.host; 2223 const domain = DomUtils.normalizeDomain(rawDomain); 2224 2225 var apiKey = ''; 2226 var apiRoot = ''; 2227 2228 var translationApiConfig = localStorage.getItem('translationApi'); 2229 2230 if(!translationApiConfig) { 2231 ErrorHandler.showErrorMessage( 2232 "offline", 2233 "You need to Login first to use the translation service." 2234 ); 2235 } 2236 2237 if (translationApiConfig) { 2238 var userApiConfig = JSON.parse(translationApiConfig); 2239 console.log('SCRIPT_CONFIG userApiConfig', userApiConfig); 2240 apiKey = userApiConfig.apiKey; 2241 apiRoot = userApiConfig.host; 2242 } else { 2243 apiKey = tag?.dataset.apiKey; 2244 apiRoot = tag?.dataset.apiRoot; 2245 } 2246 2247 2248 const theme = (tag == null ? void 0 : tag.dataset.theme) || "dark"; 2249 return { apiRoot, apiKey, domain, theme }; 2250 })(); 2251 (async () => { 2252 if (window.__translateDone) { 2253 throw "already injected"; 2254 } 2255 window.__translateDone = true; 2256 const apiService = new ApiService(); 2257 const configManager = new ConfigManager(apiService, SCRIPT_CONFIG); 2258 const translationEngine = new TextTranslationEngine( 2259 apiService, 2260 configManager 2261 ); 2262 await translationEngine.initialize(); 2263 })(); 2264})(); 2265//# sourceMappingURL=translator.dev.js.map
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.