PageSourceSearch

https://www.arabnews.com/translation/websiteTranslation.js

js arabnews.com collected 2026-10-01 07:29:41 UTC 77,042 bytes, 2,265 lines download raw bytes

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        "&": "&amp;",
677        "<": "&lt;",
678        ">": "&gt;",
679        '"': "&quot;",
680        "'": "&#x27;",
681        "/": "&#x2F;",
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.