Codedex题目翻译器

// ==UserScript== // @name Codedex 自动翻译(Chrome Translator API / Google 降级) // @namespace https://github.com/yourname/codedex-translator // @version 5....
Codedex题目翻译器
Codedex题目翻译器
// ==UserScript==
// @name         Codedex 自动翻译(Chrome Translator API / Google 降级)
// @namespace    https://github.com/yourname/codedex-translator
// @version      5.0.0
// @description  检测到 .challenge-container 加载完毕后自动翻译,无需按钮
// @author       You
// @match        https://www.codedex.io/*
// @grant        GM_xmlhttpRequest
// @connect      translate.googleapis.com
// @run-at       document-idle
// ==/UserScript==

(async function () {
  'use strict';

  const CONFIG = {
    sourceLang: 'en',
    targetLang: 'zh',
    targetLangGoogle: 'zh-CN',
  };

  const SELECTORS = [
    '.challenge-container h2',
    '.challenge-container h3',
    '.challenge-container p',
    '.challenge-container li',
    '.challenge-container .clone p',
  ];

  // ─── Chrome Translator API ───────────────────────────────────────────────────
  let chromeTranslator = null;

  async function initChromeTranslator() {
    if ('Translator' in self) {
      try {
        const avail = await Translator.availability({
          sourceLanguage: CONFIG.sourceLang,
          targetLanguage: CONFIG.targetLang,
        });
        if (avail === 'no') return false;
        chromeTranslator = await Translator.create({
          sourceLanguage: CONFIG.sourceLang,
          targetLanguage: CONFIG.targetLang,
        });
        console.log('[Codedex Translator] Chrome Translator 就绪:', avail);
        return true;
      } catch (e) {
        console.warn('[Codedex Translator] Chrome Translator 失败:', e);
        return false;
      }
    }
    // 旧版兼容 Chrome 138-140
    if (window.ai?.translator) {
      try {
        const canDo = await window.ai.translator.canTranslate({
          sourceLanguage: CONFIG.sourceLang,
          targetLanguage: CONFIG.targetLang,
        });
        if (canDo === 'no') return false;
        chromeTranslator = await window.ai.translator.createTranslator({
          sourceLanguage: CONFIG.sourceLang,
          targetLanguage: CONFIG.targetLang,
        });
        if (chromeTranslator?.ready) await chromeTranslator.ready;
        return true;
      } catch (e) {
        console.warn('[Codedex Translator] window.ai.translator 失败:', e);
        return false;
      }
    }
    return false;
  }

  // ─── Chrome LanguageDetector ─────────────────────────────────────────────────
  let langDetector = null;

  async function initLangDetector() {
    if ('LanguageDetector' in self) {
      try {
        const avail = await LanguageDetector.availability();
        if (avail === 'no') return;
        langDetector = await LanguageDetector.create();
        console.log('[Codedex Translator] LanguageDetector 就绪:', avail);
      } catch (e) {
        console.warn('[Codedex Translator] LanguageDetector 初始化失败:', e);
      }
    }
  }

  // 取容器内一段代表性文本做语言检测,返回 true 表示需要翻译
  async function shouldTranslate(container) {
    if (!langDetector) return true; // 没有检测器就默认翻译
    const sample = container.innerText.trim().slice(0, 200);
    if (!sample) return false;
    try {
      const results = await langDetector.detect(sample);
      const top = results?.[0]?.detectedLanguage;
      console.log('[Codedex Translator] 检测语言:', top);
      return top === 'en'; // 只翻译英文内容
    } catch {
      return true;
    }
  }

  // ─── Google Translate 降级 ───────────────────────────────────────────────────
  function translateWithGoogle(text) {
    return new Promise((resolve) => {
      if (!text.trim()) return resolve(text);
      const url =
        `https://translate.googleapis.com/translate_a/single` +
        `?client=gtx&sl=${CONFIG.sourceLang}&tl=${CONFIG.targetLangGoogle}` +
        `&dt=t&q=${encodeURIComponent(text)}`;
      GM_xmlhttpRequest({
        method: 'GET',
        url,
        onload(res) {
          try {
            const data = JSON.parse(res.responseText);
            resolve(data[0].filter(Boolean).map((s) => s[0]).join('') || text);
          } catch { resolve(text); }
        },
        onerror()   { resolve(text); },
        ontimeout() { resolve(text); },
        timeout: 8000,
      });
    });
  }

  // ─── 统一翻译 ─────────────────────────────────────────────────────────────────
  let mode = 'none';

  async function translateText(text) {
    if (!text.trim()) return text;
    if (mode === 'chrome') {
      try {
        const res = await chromeTranslator.translate(text);
        if (res) return res;
      } catch (e) {
        console.warn('[Codedex Translator] Chrome 翻译单条失败,降级:', e);
      }
    }
    return translateWithGoogle(text);
  }

  // ─── DOM 翻译 ─────────────────────────────────────────────────────────────────
  const DONE = 'data-cdx-done';
  const ORIG = 'data-cdx-orig';

  function collectNodes() {
    const nodes = [];
    SELECTORS.forEach((sel) =>
      document.querySelectorAll(sel).forEach((el) => {
        if (!el.hasAttribute(DONE) && el.innerText.trim()) nodes.push(el);
      })
    );
    return nodes;
  }

  // 收集叶子文本节点,跳过代码块
  const SKIP_TAGS = new Set(['CODE', 'PRE', 'KBD', 'VAR', 'SAMP']);

  function collectLeafTextNodes(el) {
    const result = [];
    function walk(node) {
      if (node.nodeType === Node.ELEMENT_NODE && SKIP_TAGS.has(node.tagName)) return;
      if (node.nodeType === Node.TEXT_NODE) {
        if (node.textContent.trim()) result.push(node);
        return;
      }
      node.childNodes.forEach(walk);
    }
    walk(el);
    return result;
  }

  async function translateContainer(container) {
    const els = [];
    SELECTORS.forEach((sel) =>
      container.querySelectorAll(sel).forEach((el) => {
        if (!el.hasAttribute(DONE) && el.innerText.trim()) els.push(el);
      })
    );
    SELECTORS.forEach((sel) => {
      if (container.matches?.(sel) && !container.hasAttribute(DONE) && container.innerText.trim())
        els.push(container);
    });
    if (!els.length) return;

    for (const el of els) {
      el.setAttribute(DONE, '1');
      const leafNodes = collectLeafTextNodes(el);
      for (const textNode of leafNodes) {
        const orig = textNode.textContent.trim();
        if (!orig) continue;
        const result = await translateText(orig);
        if (result && result !== orig) {
          textNode.textContent = textNode.textContent.replace(orig, result);
        }
      }
    }
  }

  // ─── 等待 .challenge-container 出现/内容变化后翻译 ──────────────────────────
  let translateTimer = null;

  async function maybeTranslate(container) {
    if (await shouldTranslate(container)) {
      await translateContainer(container);
    }
  }

  function scheduleTranslate(container) {
    clearTimeout(translateTimer);
    translateTimer = setTimeout(() => maybeTranslate(container), 300);
  }

  function waitAndTranslate() {
    const existing = document.querySelector('.challenge-container');
    if (existing) maybeTranslate(existing);

    // 监听 .challenge-container 的新增 和 内部文本内容变化
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        // 新节点插入:检查是否是或包含 challenge-container
        for (const node of mutation.addedNodes) {
          if (node.nodeType !== Node.ELEMENT_NODE) continue;
          if (node.classList?.contains('challenge-container')) {
            scheduleTranslate(node); return;
          }
          const inner = node.querySelector?.('.challenge-container');
          if (inner) { scheduleTranslate(inner); return; }
        }
        // 文本内容变化:如果变化发生在 challenge-container 内部
        if (mutation.type === 'characterData' || mutation.type === 'childList') {
          const container = mutation.target.closest?.('.challenge-container')
            ?? (mutation.target.nodeType === Node.TEXT_NODE
              ? mutation.target.parentElement?.closest('.challenge-container')
              : null);
          if (container) { scheduleTranslate(container); return; }
        }
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
      characterData: true,
    });
  }

  // ─── 启动 ─────────────────────────────────────────────────────────────────────
  const ok = await initChromeTranslator();
  mode = ok ? 'chrome' : 'google';
  await initLangDetector();
  console.log('[Codedex Translator] 模式:', mode);

  waitAndTranslate();
})();

效果:

image

优先使用Chrome Translate API(Chrome 本地翻译)服务
其次才是Google API,所以支持Translator API的情况下速度更快

Enjoy it!

1 个帖子 - 1 位参与者

阅读完整话题

来源: linux.do查看原文