Greasy Fork

来自缓存

Greasy Fork is available in English.

YouTube VK Toggle Translation for French, German, Russian, Ukrainian

Toggle translation for YouTube and VK videos with a fixed translation box

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YouTube VK Toggle Translation for French, German, Russian, Ukrainian
// @namespace    http://tampermonkey.net/
// @version      2.0
// @license      Unlicense
// @description  Toggle translation for YouTube and VK videos with a fixed translation box
// @author       Jim Chen
// @homepage     https://jimchen.me
// @match        https://www.youtube.com/*
// @match        https://m.youtube.com/*
// @match        https://m.youtube.com/*
// @match        https://www.youtube-nocookie.com/*
// @match        https://vkvideo.ru/*
// @run-at       document-idle
// ==/UserScript==
(function () {
  "use strict";
  let lastUrl = location.href;
  const observer = new MutationObserver(() => {
    if (location.href !== lastUrl) {
      lastUrl = location.href;
      handleVideoNavigation();
    }
  });
  observer.observe(document.body, { childList: true, subtree: true });
  handleVideoNavigation();

  // Function to remove existing subtitle and translation elements
  function cleanupSubtitles() {
    const captionWindows = document.querySelectorAll(".caption-window");
    captionWindows.forEach((window) => window.remove());
  }

  async function handleVideoNavigation() {
    // Clean up existing subtitles before adding new ones
    cleanupSubtitles();

    let subtitleURL;
    if (window.location.href.includes("youtube.com")) {
      subtitleURL = await extractSubtitleUrlYouTube();
    } else {
      subtitleURL = await extractSubtitleUrlVK();
    }
    if (!subtitleURL) return;
    if (window.location.href.includes("youtube.com")) {
      await addOneSubtitleYouTube(subtitleURL);
    } else {
      await addOneSubtitleVK(subtitleURL, 5, 1000);
    }
  }

  async function extractSubtitleUrlYouTube() {
    function extractYouTubeVideoID() {
      const url = window.location.href;

      const patterns = {
        standard: /(?:https?:\/\/)?(?:www\.)?youtube\.com\/watch\?v=([^&]+)/,
        embed: /(?:https?:\/\/)?(?:www\.)?youtube\.com\/embed\/([^?]+)/,
        mobile: /(?:https?:\/\/)?m\.youtube\.com\/watch\?v=([^&]+)/,
        nocookie: /(?:https?:\/\/)?(?:www\.)?youtube-nocookie\.com\/embed\/([^?]+)/,
      };

      let videoID = null;

      if (patterns.standard.test(url)) {
        videoID = url.match(patterns.standard)[1];
      } else if (patterns.embed.test(url)) {
        videoID = url.match(patterns.embed)[1];
      } else if (patterns.mobile.test(url)) {
        videoID = url.match(patterns.mobile)[1];
      } else if (patterns.nocookie.test(url)) {
        videoID = url.match(patterns.nocookie)[1];
      }

      return videoID;
    }
    let videoID = extractYouTubeVideoID();
    if (videoID == null) return;

    const playerData = await new Promise((resolve) => {
      const checkForPlayer = () => {
        let ytAppData = document.querySelector("#movie_player");
        let captionData = ytAppData?.getPlayerResponse()?.captions?.playerCaptionsTracklistRenderer?.captionTracks;
        if (captionData) {
          const fetchedBaseUrl = captionData[0].baseUrl;
          const fetchedVideoID = fetchedBaseUrl.match(/[?&]v=([^&]+)/)?.[1];
          if (fetchedVideoID !== videoID) setTimeout(checkForPlayer, 1000);
          else resolve(captionData);
        } else setTimeout(checkForPlayer, 1000);
      };
      checkForPlayer();
    });

    if (!playerData) return;
    const hasForeignTrack = playerData.some(({ vssId }) => /(ru|uk|de|fr)/.test(vssId));
    if (hasForeignTrack) {
      const autoGeneratedTrack = playerData.find((track) => ["a.ru", "a.uk", "a.de", "a.fr"].includes(track.vssId));
      const manualTrack = playerData.find((track) => ["ru", "uk", "de", "fr"].some((code) => track.vssId.includes(code)));
      const otherTrack = autoGeneratedTrack || manualTrack;
      if (!otherTrack) return;
      return `${otherTrack.baseUrl}&fmt=vtt`;
    }
  }

  async function extractSubtitleUrlVK() {
    const url = window.location.href;
    const vkPattern = /(?:https?:\/\/)?(?:www\.)?vkvideo\.ru\/video-?\d+_(\d+)/;
    const vkMatch = url.match(vkPattern);
    if (!vkMatch) return null;

    const subtitleElement = document.querySelector('[id^="vk_external_ru_"]');
    if (subtitleElement) {
      const subtitleUrl = subtitleElement.getAttribute("src");
      if (subtitleUrl) return subtitleUrl;
    }

    return await new Promise((resolve) => {
      const checkForSubtitle = () => {
        const subtitleElement = document.querySelector('[id^="vk_external_ru_"]');
        if (subtitleElement) {
          const subtitleUrl = subtitleElement.getAttribute("src");
          if (subtitleUrl) resolve(subtitleUrl);
          else resolve(null);
        } else {
          setTimeout(checkForSubtitle, 1000);
        }
      };
      checkForSubtitle();
    });
  }

  async function addOneSubtitleYouTube(url, maxRetries = 5, delay = 1000) {
    let currentVideo = document.querySelector("video");
    if (!currentVideo) return;

    try {
      console.log(`[Dual Subs] Starting Step 1, Subtitle URL ${url}`);
      const response = await fetch(url);
      const subtitleData = await response.text();

      function parseVTTTime(timeStr) {
        const parts = timeStr.split(/[:.]/);
        return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]) + parseInt(parts[3]) / 1000;
      }

      function parseVTT(subtitleData) {
        const subtitleQueue = [];
        const lines = subtitleData.trim().split("\n");
        let i = 0;
        while (i < lines.length && !lines[i].includes("-->")) i++;
        while (i < lines.length) {
          const line = lines[i].trim();
          if (!line) {
            i++;
            continue;
          }
          const timeMatch = line.match(/(\d+:\d+:\d+\.\d+)\s+-->\s+(\d+:\d+:\d+\.\d+)/);
          if (timeMatch) {
            const start = parseVTTTime(timeMatch[1]);
            const end = parseVTTTime(timeMatch[2]);
            const textLines = [];
            i++;
            while (i < lines.length && lines[i].trim()) {
              textLines.push(lines[i].trim());
              i++;
            }
            if (textLines.length > 0) {
              subtitleQueue.push({ start, end, textLines });
            }
          } else {
            i++;
          }
        }
        return subtitleQueue;
      }

      const subtitleQueue = parseVTT(subtitleData);

      console.log(`[Dual Subs] Starting Step 2, Trying to Insert Subtitle Element`);
      function createCaptionWindow() {
        const videoPlayer = document.querySelector(".html5-video-player");
        if (!videoPlayer) {
          console.error("HTML5 video player not found");
          return null;
        }

        const captionWindow = document.createElement("div");
        captionWindow.className = "caption-window ytp-caption-window-bottom";
        captionWindow.style.cssText = `
                    touch-action: none;
                    text-align: center;
                    position: absolute;
                    left: 50%;
                    transform: translateX(-50%);
                    bottom: 10%;
                    width: 90%;
                    max-width: 800px;
                `;

        const captionsText = document.createElement("span");
        captionsText.className = "captions-text";
        captionsText.style.cssText = "overflow-wrap: normal; display: block;";

        const captionVisualLine = document.createElement("span");
        captionVisualLine.className = "caption-visual-line";
        captionVisualLine.style.cssText = "display: block;";

        const ytpCaptionSegment = document.createElement("span");
        ytpCaptionSegment.className = "ytp-caption-segment";
        ytpCaptionSegment.style.cssText = `
                    display: inline-block;
                    white-space: pre-wrap;
                    background: rgba(8, 8, 8, 0.75);
                    font-size: 2.5vw;
                    color: rgb(255, 255, 255);
                    fill: rgb(255, 255, 255);
                `;

        const translationBox = document.createElement("div");
        translationBox.className = "translation-box";
        translationBox.style.cssText = `
                    position: absolute;
                    background: rgba(0, 0, 0, 0.9);
                    color: white;
                    padding: 8px;
                    border-radius: 4px;
                    font-size: 24px;
                    top: -40px;
                    left: 50%;
                    transform: translateX(-50%);
                    z-index: 9999;
                    display: none;
                    transition: opacity 0.1s ease;
                    opacity: 1;
                `;

        captionWindow.appendChild(translationBox);
        captionVisualLine.appendChild(ytpCaptionSegment);
        captionsText.appendChild(captionVisualLine);
        captionWindow.appendChild(captionsText);
        videoPlayer.appendChild(captionWindow);

        return { ytpCaptionSegment, translationBox };
      }

      const { ytpCaptionSegment, translationBox } = createCaptionWindow() || {};
      if (!ytpCaptionSegment || !translationBox) return;

      currentVideo.addEventListener("timeupdate", () => {
        const currentTime = currentVideo.currentTime;
        const currentSubtitle = subtitleQueue.find((sub) => currentTime >= sub.start && currentTime <= sub.end);
        updateSubtitle(currentSubtitle);
      });

      const translationCache = new Map();

      async function translateText(text, targetLang = "en") {
        if (translationCache.has(text)) return translationCache.get(text);
        try {
          const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
          const response = await fetch(url);
          const data = await response.json();
          const translatedText = data[0][0][0];
          translationCache.set(text, translatedText);
          return translatedText;
        } catch (error) {
          console.error("[Dual Subs] Translation error:", error);
          return text;
        }
      }

      function debounce(func, wait) {
        let timeout;
        return function (...args) {
          clearTimeout(timeout);
          timeout = setTimeout(() => func.apply(this, args), wait);
        };
      }

      let currentTranslation = {
        word: null,
        translation: null,
        visible: false,
      };

      function updateSubtitle(currentSubtitle) {
        while (ytpCaptionSegment.firstChild) ytpCaptionSegment.removeChild(ytpCaptionSegment.firstChild);

        if (!currentTranslation.visible) {
          translationBox.style.display = "none";
        } else {
          translationBox.textContent = currentTranslation.translation;
          translationBox.style.display = "block";
        }

        if (!currentSubtitle) {
          ytpCaptionSegment.style.display = "none";
          return;
        }

        ytpCaptionSegment.style.display = "inline-block";

        currentSubtitle.textLines.forEach((line) => {
          const lineSpan = document.createElement("span");
          lineSpan.style.display = "block";

          if (line.includes("<c>")) {
            const currentTime = currentVideo.currentTime;
            const timeTagRegex = /<(\d{2}:\d{2}:\d{2}\.\d{3})><c>(.*?)<\/c>/g;
            let lastIndex = 0;
            let wordArray = [];
            let timeArray = [];

            let match;
            while ((match = timeTagRegex.exec(line)) !== null) {
              const timeStr = match[1];
              const text = match[2];
              const time = parseVTTTime(timeStr);

              if (match.index > lastIndex) {
                const untaggedText = line.slice(lastIndex, match.index).trim();
                if (untaggedText) {
                  wordArray.push(untaggedText);
                  timeArray.push(currentSubtitle.start);
                }
              }

              wordArray.push(text);
              timeArray.push(time);
              lastIndex = timeTagRegex.lastIndex;
            }

            if (lastIndex < line.length) {
              const untaggedText = line.slice(lastIndex).trim();
              if (untaggedText) {
                wordArray.push(untaggedText);
                timeArray.push(currentSubtitle.start);
              }
            }

            let currentWordIndex = 0;
            for (let i = 0; i < wordArray.length; i++) {
              if (currentTime >= timeArray[i]) currentWordIndex = i;
              else break;
            }

            wordArray.forEach((word, index) => {
              const wordSpan = document.createElement("span");
              wordSpan.textContent = word + " ";
              wordSpan.className = "subtitle-word";
              wordSpan.style.cursor = "pointer";

              if (index < currentWordIndex) {
                wordSpan.style.color = "#ffffff";
              } else if (index === currentWordIndex && currentTime >= timeArray[index]) {
                const startTime = timeArray[index];
                const endTime = index + 1 < timeArray.length ? timeArray[index + 1] : currentSubtitle.end;
                const progress = (currentTime - startTime) / (endTime - startTime);
                wordSpan.style.cssText = `
                                    background: linear-gradient(to right, #ffffff 50%, #888888 50%);
                                    background-size: 200% 100%;
                                    background-position: ${100 - progress * 100}%;
                                    color: transparent;
                                    background-clip: text;
                                    -webkit-background-clip: text;
                                    transition: background-position 0.1s linear;
                                `;
              } else {
                wordSpan.style.color = "#888888";
              }

              const showTranslation = debounce(async () => {
                const translation = await translateText(word);
                translationBox.textContent = translation;
                translationBox.style.display = "block";
              }, 200);

              wordSpan.addEventListener("mouseenter", async () => {
                currentTranslation.word = word;
                currentTranslation.visible = true;

                if (!translationCache.has(word)) {
                  const translation = await translateText(word);
                  translationBox.textContent = translation;
                  currentTranslation.translation = translation;
                } else {
                  translationBox.textContent = translationCache.get(word);
                  currentTranslation.translation = translationCache.get(word);
                }

                translationBox.style.display = "block";
              });

              wordSpan.addEventListener("mouseleave", () => {
                currentTranslation.visible = false;
                translationBox.style.display = "none";
              });

              lineSpan.appendChild(wordSpan);
            });
          } else {
            const words = line.split(" ");
            words.forEach((word) => {
              const wordSpan = document.createElement("span");
              wordSpan.textContent = word + " ";
              wordSpan.className = "subtitle-word";
              wordSpan.style.color = "#ffffff";
              wordSpan.style.cursor = "pointer";

              wordSpan.addEventListener("mouseenter", async () => {
                currentTranslation.word = word;
                currentTranslation.visible = true;

                if (!translationCache.has(word)) {
                  const translation = await translateText(word);
                  translationBox.textContent = translation;
                  currentTranslation.translation = translation;
                } else {
                  translationBox.textContent = translationCache.get(word);
                  currentTranslation.translation = translationCache.get(word);
                }

                translationBox.style.display = "block";
              });

              wordSpan.addEventListener("mouseleave", () => {
                currentTranslation.visible = false;
                translationBox.style.display = "none";
              });

              lineSpan.appendChild(wordSpan);
            });
          }

          ytpCaptionSegment.appendChild(lineSpan);
        });
      }

      const styleSheet = document.createElement("style");
      styleSheet.textContent = `
                @keyframes slideColor {
                    0% { background-position: 100%; }
                    100% { background-position: 0%; }
                }
                .subtitle-word:hover {
                    text-decoration: underline;
                }
                @media (max-width: 768px) {
                    .ytp-caption-segment {
                        font-size: 20px;
                    }
                    .translation-box {
                        font-size: 24px;
                        padding: 6px;
                    }
                }
            `;
      document.head.appendChild(styleSheet);

      console.log(`[Dual Subs] Subtitle and Translation Setup Complete`);
    } catch (error) {
      console.error("[Dual Subs] Error:", error);
      if (maxRetries > 0) {
        await new Promise((resolve) => setTimeout(resolve, delay));
        return addOneSubtitleYouTube(url, maxRetries - 1, delay);
      }
    }
  }

  async function addOneSubtitleVK(url, maxRetries = 5, delay = 1000) {
    let currentVideo = document.querySelector("video");
    if (!currentVideo) return;

    try {
      console.log(`[Dual Subs VK] Starting Step 1, Subtitle URL ${url}`);
      const response = await fetch(url);
      const subtitleData = await response.text();

      function parseVTTTime(timeStr) {
        const parts = timeStr.split(/[:.]/);
        return parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(parts[2]) + parseInt(parts[3]) / 1000;
      }

      function parseVTT(subtitleData) {
        const subtitleQueue = [];
        const lines = subtitleData.trim().split("\n");
        let i = 0;
        while (i < lines.length && !lines[i].includes("-->")) i++;
        while (i < lines.length) {
          const line = lines[i].trim();
          if (!line) {
            i++;
            continue;
          }
          const timeMatch = line.match(/(\d+:\d+:\d+\.\d+)\s+-->\s+(\d+:\d+:\d+\.\d+)/);
          if (timeMatch) {
            const start = parseVTTTime(timeMatch[1]);
            const end = parseVTTTime(timeMatch[2]);
            const textLines = [];
            i++;
            while (i < lines.length && lines[i].trim()) {
              textLines.push(lines[i].trim());
              i++;
            }
            if (textLines.length > 0) {
              subtitleQueue.push({ start, end, textLines });
            }
          } else {
            i++;
          }
        }
        return subtitleQueue;
      }

      const subtitleQueue = parseVTT(subtitleData);

      console.log(`[Dual Subs VK] Starting Step 2, Trying to Insert Subtitle Element for VK`);

      function createCaptionWindow() {
        const videoPlayer = document.querySelector(".videoplayer_media");
        if (!videoPlayer) {
          console.error("VK video player container not found");
          return null;
        }

        const captionWindow = document.createElement("div");
        captionWindow.className = "caption-window vk-caption-window-bottom";
        captionWindow.style.cssText = `
                touch-action: none;
                text-align: center;
                position: absolute;
                left: 50%;
                transform: translateX(-50%);
                bottom: 10%;
                width: 90%;
                max-width: 800px;
            `;

        const captionsText = document.createElement("span");
        captionsText.className = "captions-text";
        captionsText.style.cssText = "overflow-wrap: normal; display: block;";

        const captionVisualLine = document.createElement("span");
        captionVisualLine.className = "caption-visual-line";
        captionVisualLine.style.cssText = "display: block;";

        const vkCaptionSegment = document.createElement("span");
        vkCaptionSegment.className = "vk-caption-segment";
        vkCaptionSegment.style.cssText = `
                display: inline-block;
                white-space: pre-wrap;
                background: rgba(8, 8, 8, 0.75);
                font-size: 2.5vw;
                color: rgb(255, 255, 255);
                fill: rgb(255, 255, 255);
            `;

        const translationBox = document.createElement("div");
        translationBox.className = "translation-box";
        translationBox.style.cssText = `
                position: absolute;
                background: rgba(0, 0, 0, 0.9);
                color: white;
                padding: 8px;
                border-radius: 4px;
                font-size: 24px;
                top: -40px;
                left: 50%;
                transform: translateX(-50%);
                z-index: 9999;
                display: none;
                transition: opacity 0.1s ease;
                opacity: 1;
            `;

        captionWindow.appendChild(translationBox);
        captionVisualLine.appendChild(vkCaptionSegment);
        captionsText.appendChild(captionVisualLine);
        captionWindow.appendChild(captionsText);
        videoPlayer.appendChild(captionWindow);

        return { vkCaptionSegment, translationBox };
      }

      const { vkCaptionSegment, translationBox } = createCaptionWindow() || {};
      if (!vkCaptionSegment || !translationBox) return;

      currentVideo.addEventListener("timeupdate", () => {
        const currentTime = currentVideo.currentTime;
        const currentSubtitle = subtitleQueue.find((sub) => currentTime >= sub.start && currentTime <= sub.end);
        updateSubtitle(currentSubtitle);
      });

      const translationCache = new Map();

      async function translateText(text, targetLang = "en") {
        if (translationCache.has(text)) return translationCache.get(text);
        try {
          const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=${targetLang}&dt=t&q=${encodeURIComponent(text)}`;
          const response = await fetch(url);
          const data = await response.json();
          const translatedText = data[0][0][0];
          translationCache.set(text, translatedText);
          return translatedText;
        } catch (error) {
          console.error("[Dual Subs VK] Translation error:", error);
          return text;
        }
      }

      function debounce(func, wait) {
        let timeout;
        return function (...args) {
          clearTimeout(timeout);
          timeout = setTimeout(() => func.apply(this, args), wait);
        };
      }

      let currentTranslation = {
        word: null,
        translation: null,
        visible: false,
      };

      function updateSubtitle(currentSubtitle) {
        while (vkCaptionSegment.firstChild) vkCaptionSegment.removeChild(vkCaptionSegment.firstChild);

        if (!currentTranslation.visible) {
          translationBox.style.display = "none";
        } else {
          translationBox.textContent = currentTranslation.translation;
          translationBox.style.display = "block";
        }

        if (!currentSubtitle) {
          vkCaptionSegment.style.display = "none";
          return;
        }

        vkCaptionSegment.style.display = "inline-block";

        currentSubtitle.textLines.forEach((line) => {
          const lineSpan = document.createElement("span");
          lineSpan.style.display = "block";

          if (line.includes("<c>")) {
            const currentTime = currentVideo.currentTime;
            const timeTagRegex = /<(\d{2}:\d{2}:\d{2}\.\d{3})><c>(.*?)<\/c>/g;
            let lastIndex = 0;
            let wordArray = [];
            let timeArray = [];

            let match;
            while ((match = timeTagRegex.exec(line)) !== null) {
              const timeStr = match[1];
              const text = match[2];
              const time = parseVTTTime(timeStr);

              if (match.index > lastIndex) {
                const untaggedText = line.slice(lastIndex, match.index).trim();
                if (untaggedText) {
                  wordArray.push(untaggedText);
                  timeArray.push(currentSubtitle.start);
                }
              }

              wordArray.push(text);
              timeArray.push(time);
              lastIndex = timeTagRegex.lastIndex;
            }

            if (lastIndex < line.length) {
              const untaggedText = line.slice(lastIndex).trim();
              if (untaggedText) {
                wordArray.push(untaggedText);
                timeArray.push(currentSubtitle.start);
              }
            }

            let currentWordIndex = 0;
            for (let i = 0; i < wordArray.length; i++) {
              if (currentTime >= timeArray[i]) currentWordIndex = i;
              else break;
            }

            wordArray.forEach((word, index) => {
              const wordSpan = document.createElement("span");
              wordSpan.textContent = word + " ";
              wordSpan.className = "subtitle-word";
              wordSpan.style.cursor = "pointer";

              if (index < currentWordIndex) {
                wordSpan.style.color = "#ffffff";
              } else if (index === currentWordIndex && currentTime >= timeArray[index]) {
                const startTime = timeArray[index];
                const endTime = index + 1 < timeArray.length ? timeArray[index + 1] : currentSubtitle.end;
                const progress = (currentTime - startTime) / (endTime - startTime);
                wordSpan.style.cssText = `
                                background: linear-gradient(to right, #ffffff 50%, #888888 50%);
                                background-size: 200% 100%;
                                background-position: ${100 - progress * 100}%;
                                color: transparent;
                                background-clip: text;
                                -webkit-background-clip: text;
                                transition: background-position 0.1s linear;
                            `;
              } else {
                wordSpan.style.color = "#888888";
              }
              wordSpan.addEventListener("mouseenter", async () => {
                currentTranslation.word = word;
                currentTranslation.visible = true;

                if (!translationCache.has(word)) {
                  const translation = await translateText(word);
                  translationBox.textContent = translation;
                  currentTranslation.translation = translation;
                } else {
                  translationBox.textContent = translationCache.get(word);
                  currentTranslation.translation = translationCache.get(word);
                }

                translationBox.style.display = "block";
              });

              wordSpan.addEventListener("mouseleave", () => {
                currentTranslation.visible = false;
                translationBox.style.display = "none";
              });

              lineSpan.appendChild(wordSpan);
            });
          } else {
            const words = line.split(" ");
            words.forEach((word) => {
              const wordSpan = document.createElement("span");
              wordSpan.textContent = word + " ";
              wordSpan.className = "subtitle-word";
              wordSpan.style.color = "#ffffff";
              wordSpan.style.cursor = "pointer";
              wordSpan.addEventListener("mouseenter", async () => {
                currentTranslation.word = word;
                currentTranslation.visible = true;

                if (!translationCache.has(word)) {
                  const translation = await translateText(word);
                  translationBox.textContent = translation;
                  currentTranslation.translation = translation;
                } else {
                  translationBox.textContent = translationCache.get(word);
                  currentTranslation.translation = translationCache.get(word);
                }

                translationBox.style.display = "block";
              });

              wordSpan.addEventListener("mouseleave", () => {
                currentTranslation.visible = false;
                translationBox.style.display = "none";
              });

              lineSpan.appendChild(wordSpan);
            });
          }

          vkCaptionSegment.appendChild(lineSpan);
        });
      }

      const styleSheet = document.createElement("style");
      styleSheet.textContent = `
            .vk-caption-window-bottom {
                z-index: 999999 !important;
                pointer-events: auto !important;
            }
            .vk-caption-segment {
                z-index: 999999 !important;
                pointer-events: auto !important;
            }
            .subtitle-word {
                z-index: 999999 !important;
                pointer-events: auto !important;
            }
            @keyframes slideColor {
                0% { background-position: 100%; }
                100% { background-position: 0%; }
            }
            .subtitle-word:hover {
                text-decoration: underline;
            }
            @media (max-width: 768px) {
                .vk-caption-segment {
                    font-size: 20px;
                }
                .translation-box {
                    font-size: 24px;
                    padding: 6px;
                }
            }
        `;
      document.head.appendChild(styleSheet);

      console.log(`[Dual Subs VK] Subtitle and Translation Setup Complete`);
    } catch (error) {
      console.error("[Dual Subs VK] Error:", error);
      if (maxRetries > 0) {
        await new Promise((resolve) => setTimeout(resolve, delay));
        return addOneSubtitleVK(url, maxRetries - 1, delay);
      }
    }
  }
})();