Codewars 沉浸式题目汉化工具

Codewars 沉浸式题目汉化工具,支持中英对照、用户 API Key(Gemini),并在 Kata 切换时自动刷新页面。

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Codewars 沉浸式题目汉化工具
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Codewars 沉浸式题目汉化工具,支持中英对照、用户 API Key(Gemini),并在 Kata 切换时自动刷新页面。
// @author       Cerry2025 & AI Assistant & User Request
// @license      MIT
// @homepageURL  https://github.com/Cerry2022/Codewars-Immersive-Translator
// @supportURL   https://github.com/Cerry2022/Codewars-Immersive-Translator/issues
// @match        https://*.codewars.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @connect      generativelanguage.googleapis.com
// ==/UserScript==

(function() {
    'use strict';

    // 配置
    const CONFIG = {
        TARGET_SELECTOR: '#description',
        LOADING_TEXT: 'Loading description...',
        TRANSLATE_DELAY: 0,
        STORAGE_KEY_MODE: 'codewars_translate_mode',
        STORAGE_KEY_APIKEY: 'codewars_gemini_apikey',
        ROUTE_CHECK_INTERVAL: 500,
        API_ENDPOINT: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:generateContent',
        TRANSLATION_STATE_ATTR: 'data-translation-state',
        NOTIFICATION_DURATION: 3000, // 通知显示时长
    };

    // API Key 管理

    /**
     * 获取 API Key。
     * 如果未设置,则提示用户输入并存储。
     * @returns {string|null} API Key 或 null(如果未设置)
     */
    function getApiKey() {
        let apiKey = GM_getValue(CONFIG.STORAGE_KEY_APIKEY, null);
        if (!apiKey || apiKey.trim() === '') {
            apiKey = prompt('请输入您的 Google AI Gemini API 密钥以启用翻译功能。\n(您可以在 Google AI Studio 免费获取)');
            if (apiKey && apiKey.trim() !== '') {
                apiKey = apiKey.trim();
                // 简单验证 API Key 格式 (可以根据 Gemini API 的 Key 格式进行更严格的验证)
                if (!apiKey.startsWith('AIza')) {
                    alert('API 密钥格式不正确,请检查!');
                    return null;
                }
                GM_setValue(CONFIG.STORAGE_KEY_APIKEY, apiKey);
                showNotification('API 密钥已保存。', 'success');
                return apiKey;
            } else {
                showNotification('未提供有效的 API 密钥。翻译功能将无法使用。\n您可以通过油猴菜单 "设置/更新 API 密钥" 来设置。', 'warning');
                return null;
            }
        }
        return apiKey;
    }

    // 注册油猴菜单命令,用于设置或更新 API Key
    GM_registerMenuCommand('设置/更新 API 密钥', () => {
        const currentKey = GM_getValue(CONFIG.STORAGE_KEY_APIKEY, '');
        const newKey = prompt('请输入或更新您的 Google AI Gemini API 密钥:', currentKey);
        if (newKey !== null) {
            const trimmedKey = newKey.trim();
            // 简单验证 API Key 格式
             if (trimmedKey && !trimmedKey.startsWith('AIza')) {
                alert('API 密钥格式不正确,请检查!');
                return;
             }
            GM_setValue(CONFIG.STORAGE_KEY_APIKEY, trimmedKey);
            showNotification(trimmedKey ? 'API 密钥已更新!请刷新页面或导航到新题目以生效。' : 'API 密钥已清除。', 'info');
        } else {
            showNotification('操作已取消。', 'info');
        }
    });

    // 样式与 UI

    /**
     * 注入 CSS 样式。
     */
    function addStyles() {
        const style = document.createElement('style');
        style.id = 'codewars-translator-styles';
        style.textContent = `
            /* 整个双语容器的样式 */
            .bilingual-container .original-text {
                opacity: 0.75; /* 降低原文的透明度,使其略微不显眼 */
                font-size: 0.95em; /* 稍微缩小原文的字体大小 */
                line-height: 1.3; /* 设置原文的行高 */
                margin-bottom: 0; /* 移除原文底部的外边距 */
            }

            /* 分割原文和译文的水平线的样式 */
            .bilingual-container hr.translation-separator {
                margin: 15px 0; /* 设置水平线的上下外边距 */
                border: none; /* 移除默认的边框 */
                border-top: 1px solid #eee; /* 设置水平线为浅灰色细线 */
            }

            /* 译文文本的样式 */
            .bilingual-container .translated-text {
                padding: 5px; /* 设置译文的内边距 */
                line-height: 1.3; /* 设置译文的行高 */
                margin-top: 0; /* 移除译文顶部的外边距 */
            }

            /* 原文和译文中 <strong> 标签的样式 (通常用于标题或关键词) */
            .bilingual-container .original-text strong,
            .bilingual-container .translated-text strong {
                display: block; /* 使 <strong> 标签占据整行 */
                margin-bottom: 0px; /* 移除 <strong> 标签底部的外边距 */
                font-size: 0.8em; /* 缩小 <strong> 标签的字体大小 */
                color: #777; /* 设置 <strong> 标签的颜色为深灰色 */
                text-transform: uppercase; /* 将 <strong> 标签的文本转换为大写 */
                font-weight: bold; /* 加粗 <strong> 标签的文本 */
            }

            /* 头部切换按钮的样式 */
            .header-toggle {
                margin-left: auto; /* 将按钮推到右侧 */
                display: flex; /* 使用 Flexbox 布局 */
                align-items: center; /* 垂直居中对齐按钮内容 */
                gap: 6px; /* 设置按钮内容之间的间距 */
                cursor: pointer; /* 将鼠标指针变为手型,表示可点击 */
                padding-right: 10px; /* 右侧内边距 */
            }

            /* 翻译状态提示信息的样式 */
            .translation-status-tip {
                padding: 5px 0; /* 设置提示信息的上下内边距 */
                margin-bottom: 10px; /* 设置提示信息底部的外边距 */
                display: block; /* 使提示信息占据整行 */
                font-size: 0.9em; /* 缩小提示信息的字体大小 */
            }

            /* 普通提示信息的样式 */
            .translation-status-tip.tip {
                color: #666; /* 设置提示信息的颜色为灰色 */
                font-style: italic; /* 将提示信息设置为斜体 */
                border-bottom: 1px dashed #eee; /* 添加浅灰色虚线下划线 */
            }

            /* 错误提示信息的样式 */
            .translation-status-tip.error {
                color: #f44336; /* 设置错误信息的颜色为红色 */
                font-weight: bold; /* 加粗错误信息 */
                border: 1px solid #f44336; /* 添加红色边框 */
                padding: 8px 10px; /* 设置错误信息的内边距 */
                background-color: #ffebee; /* 设置错误信息的背景色为浅红色 */
                border-radius: 4px; /* 添加圆角 */
                margin: 10px 0; /* 设置错误信息的上下外边距 */
            }

            /* 通知消息的样式 */
            .notification {
                position: fixed; /* 使用固定定位,使其始终显示在屏幕的固定位置 */
                top: 20px; /* 距离顶部 20px */
                right: 20px; /* 距离右侧 20px */
                background-color: #f0f0f0; /* 设置背景色为浅灰色 */
                padding: 10px 15px; /* 设置内边距 */
                border-radius: 5px; /* 添加圆角 */
                box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* 添加阴影 */
                z-index: 1000; /* 设置 z-index 属性,使其显示在其他元素的上方 */
                font-size: 14px; /* 设置字体大小 */
                color: #333; /* 设置颜色 */
            }

            /* 成功通知的样式 */
            .notification.success {
                background-color: #d4edda; /* 设置背景色为浅绿色 */
                color: #155724; /* 设置颜色为深绿色 */
                border: 1px solid #c3e6cb; /* 设置边框 */
            }

            /* 警告通知的样式 */
            .notification.warning {
                background-color: #fff3cd; /* 设置背景色为浅黄色 */
                color: #856404; /* 设置颜色为深黄色 */
                border: 1px solid #ffeeba; /* 设置边框 */
            }

             /* 信息通知的样式 */
            .notification.info {
                background-color: #bee5eb; /* 设置背景色为浅蓝色 */
                color: #0c5460; /* 设置颜色为深蓝色 */
                border: 1px solid #b7e1e8; /* 设置边框 */
            }

            /* 加载动画的样式 */
            .loading-animation {
              border: 5px solid #f3f3f3; /* 浅灰色边框 */
              border-top: 5px solid #3498db; /* 蓝色顶部边框 */
              border-radius: 50%; /* 圆形 */
              width: 20px; /* 宽度 */
              height: 20px; /* 高度 */
              animation: spin 2s linear infinite; /* 应用旋转动画 */
              display: inline-block; /* 行内块元素,可以设置宽高 */
              margin-right: 5px; /* 右侧外边距 */
            }

            /* 旋转动画的关键帧 */
            @keyframes spin {
              0% { transform: rotate(0deg); } /* 初始角度 */
              100% { transform: rotate(360deg); } /* 旋转 360 度 */
            }
        `;
        if (!document.getElementById(style.id)) {
            document.head.appendChild(style);
        }
    }

     /**
     * 显示通知消息。
     * @param {string} message 通知内容
     * @param {string} type 通知类型 (success, warning, info)
     */
    function showNotification(message, type = 'info') {
        const notification = document.createElement('div');
        notification.className = `notification ${type}`;
        notification.textContent = message;
        document.body.appendChild(notification);

        setTimeout(() => {
            notification.remove();
        }, CONFIG.NOTIFICATION_DURATION);
    }


    /**
     * 使用 MutationObserver 添加中英对照切换开关到页面头部。
     * 目标:添加到 "Instructions"/"Output" 选项卡右侧。
     */
    function addHeaderSwitch() {
        console.log("开始监控 DOM 以添加头部切换开关...");

        // 更精确的目标容器选择器,基于提供的 HTML
        const headerTargetSelector = '.mb-2.border-0.overflow-hidden.flex.items-center.justify-start';
        const toggleContainerId = 'codewars-translator-header-toggle-container'; // 唯一 ID

        const observer = new MutationObserver((mutationsList, obs) => {
            // 检查开关是否已存在
            if (document.getElementById(toggleContainerId)) {
                // console.log("头部开关已存在,停止监控。"); // 可以取消注释以进行调试
                obs.disconnect();
                return;
            }

            // 查找目标容器
            const targetArea = document.querySelector(headerTargetSelector);

            // 检查目标容器是否存在,并确保它至少包含一个选项卡链接作为稳定标志
            const stableElementCheck = targetArea?.querySelector('a.inline-block.px-4.py-2');

            if (targetArea && stableElementCheck) {
                console.log("找到头部目标区域,尝试添加开关...");

                // 再次确认开关不存在(双重检查,虽然有 ID 可能多余,但保险)
                 if (!targetArea.querySelector(`#${toggleContainerId}`)) {
                    const isBilingual = localStorage.getItem(CONFIG.STORAGE_KEY_MODE) === 'bilingual';

                    // 创建开关的容器 div
                    const toggleWrapperDiv = document.createElement('div');
                    toggleWrapperDiv.id = toggleContainerId; // 设置唯一 ID
                    // 使用 margin-left: auto 将其推到 flex 容器的右侧
                    toggleWrapperDiv.style.marginLeft = 'auto';
                    // 保持 flex 布局以垂直居中
                    toggleWrapperDiv.style.display = 'flex';
                    toggleWrapperDiv.style.alignItems = 'center';
                    toggleWrapperDiv.style.gap = '6px'; // 复刻之前的样式
                    toggleWrapperDiv.style.paddingRight = '10px'; // 增加一些右边距,避免太贴边

                    // 创建开关内部的 HTML
                    toggleWrapperDiv.innerHTML = `
                        <input type="checkbox" id="bilingualToggleHeader" style="cursor: pointer;" ${isBilingual ? 'checked' : ''}>
                        <label for="bilingualToggleHeader" style="cursor: pointer; user-select: none;">中英对照</label>
                    `;

                    // 将开关容器添加到目标区域(flex 容器)的末尾
                    // 由于设置了 margin-left: auto,它会被推到右边
                    targetArea.appendChild(toggleWrapperDiv);
                    console.log("头部开关已添加。");

                    // 获取新添加的 checkbox 并添加事件监听器
                    const toggle = toggleWrapperDiv.querySelector('#bilingualToggleHeader');
                    toggle.addEventListener('change', () => {
                        localStorage.setItem(CONFIG.STORAGE_KEY_MODE, toggle.checked ? 'bilingual' : 'replace');
                        showNotification('模式已切换。刷新页面或导航到新题目以查看效果。', 'info');
                        location.reload(); // 保留刷新逻辑
                    });

                    // 成功添加后,停止观察
                    obs.disconnect();
                    console.log("停止监控 DOM (开关已添加)。");

                } else {
                     // console.log("开关已存在于目标区域内(二次检查),停止监控。");
                     obs.disconnect();
                }
            }
            // else {
            //    console.log("未找到目标区域或稳定元素,继续监控..."); // 可选日志
            //}
        });

        // 配置观察选项: 监控整个文档的子节点添加/删除
        const config = { childList: true, subtree: true };

        // 开始观察 document.body
        observer.observe(document.body, config);

        // 添加一个超时,以防万一目标元素永远不出现
        setTimeout(() => {
            observer.disconnect();
        }, 5000); // 设置 5 秒超时
    }

    /**
     * 设置状态提示。
     * @param {HTMLElement} element 目标元素
     * @param {string} text 提示文本
     * @param {boolean} isError 是否为错误提示
     */
    function setStatusTip(element, text, isError = false, showLoading = false) {
        removeStatusTip(element);
        const tipElement = document.createElement('div');
        tipElement.className = `translation-status-tip ${isError ? 'error' : 'tip'}`;

        if(showLoading){
          const loadingAnimation = document.createElement('span');
          loadingAnimation.className = 'loading-animation';
          tipElement.appendChild(loadingAnimation);
        }

        tipElement.appendChild(document.createTextNode(text)); // 使用 createTextNode 防止 HTML 注入
        element.insertBefore(tipElement, element.firstChild);
    }

    /**
     * 移除状态提示。
     * @param {HTMLElement} element
     */
    function removeStatusTip(element) {
        const tipElement = element.querySelector(':scope > .translation-status-tip');
        if (tipElement) {
            tipElement.remove();
        }
    }

    // 路由变化检测(简化:页面刷新)
    const initialPath = location.pathname;

    function checkForRouteChange() {
        if (location.pathname !== initialPath && location.pathname.includes('/kata/')) {
            console.log(`Codewars Translator: Kata 切换,从 ${initialPath} 到 ${location.pathname}。 刷新页面。`);
            clearInterval(routeCheckInterval);
            location.reload();
        }
    }

    const routeCheckInterval = setInterval(checkForRouteChange, CONFIG.ROUTE_CHECK_INTERVAL);

    // 翻译核心逻辑

    /**
     * 检查元素是否包含加载文本。
     * @param {HTMLElement} element
     * @returns {boolean}
     */
    const isLoading = (element) => element.textContent.includes(CONFIG.LOADING_TEXT);

    /**
     * 等待内容加载完成。
     * @param {HTMLElement} element
     * @returns {Promise<void>}
     */
    function waitForContentReady(element) {
        return new Promise((resolve, reject) => {
            if (!isLoading(element)) return resolve();

            let resolved = false;
            const observer = new MutationObserver(() => {
                if (!isLoading(element)) {
                    if(resolved) return;
                    resolved = true;
                    observer.disconnect();
                    resolve();
                }
            });
            observer.observe(element, { childList: true, subtree: true, characterData: true });

             const timeoutId = setTimeout(() => {
                if (resolved) return;
                observer.disconnect();
                console.warn("waitForContentReady 超时。");
                resolve();
            }, 10000);

             const originalResolve = resolve;
             resolve = () => {
                 clearTimeout(timeoutId);
                 originalResolve();
             }
        });
    }


    /**
     * 处理单个元素:检查状态、加载、API Key,然后翻译。
     * @param {HTMLElement} element
     */
    /**
     * 处理单个元素:检查状态、加载、API Key,然后翻译。
     * @param {HTMLElement} element
     */
    async function processElement(element) {
        const currentState = element.getAttribute(CONFIG.TRANSLATION_STATE_ATTR);

        // 如果元素已处理、正在处理、出错或明确标记为空,则跳过
        if (['processing', 'translated', 'error', 'empty'].includes(currentState)) {
            // console.log(`Skipping element, state: ${currentState}`); // 可选调试日志
            return;
        }

        console.log("Processing element:", element.id || element.className);

        let originalHTML;

        // --- 步骤 1: 处理 Codewars 自身的加载状态 ---
        if (isLoading(element)) {
            console.log("Element is initially loading (Codewars)...");
            element.setAttribute(CONFIG.TRANSLATION_STATE_ATTR, 'loading_codewars'); // 标记为 Codewars 加载状态
            setStatusTip(element, '等待题目内容加载...'); // 显示 Codewars 加载提示
            await waitForContentReady(element);
            removeStatusTip(element); // 移除 Codewars 加载提示
            console.log("Codewars loading finished.");
            // 再次检查状态,因为 waitForContentReady 可能改变了元素状态或内容
            if (!element.textContent || element.textContent.trim() === '') {
                console.log("Element became empty after Codewars loading.");
                element.setAttribute(CONFIG.TRANSLATION_STATE_ATTR, 'empty');
                // 如果希望显示空状态,可以保留元素为空,或者恢复可能的原始空结构
                // element.innerHTML = ''; // 确保是空的
                return; // 内容为空,不进行翻译
            }
            // 内容加载完成后,清除 Codewars 加载状态标记,以便后续捕获 HTML
            element.removeAttribute(CONFIG.TRANSLATION_STATE_ATTR);
        }

        // --- 步骤 2: 捕获干净的原始 HTML ---
        // 确保在添加我们自己的翻译提示之前捕获
        if (!element.dataset.originalHtml) {
            // 再次检查内容是否为空(可能在非 loading 状态下初始为空)
             if (!element.textContent || element.textContent.trim() === '') {
                 console.log("Element is initially empty.");
                 element.setAttribute(CONFIG.TRANSLATION_STATE_ATTR, 'empty');
                 return;
             }
            originalHTML = element.innerHTML;
            element.dataset.originalHtml = originalHTML;
            console.log("Original HTML captured successfully.");
            // console.log("Captured HTML:", originalHTML.substring(0, 200) + "..."); // 调试日志,只显示部分
        } else {
            originalHTML = element.dataset.originalHtml;
            console.log("Reusing cached original HTML.");
        }

        // --- 步骤 3: 检查 API Key ---
        const apiKey = getApiKey();
        if (!apiKey) {
            // 只有在尚未显示错误时才添加提示
            if (element.getAttribute(CONFIG.TRANSLATION_STATE_ATTR) !== 'error') {
                 setStatusTip(element, '错误:未设置 API 密钥。请通过脚本菜单设置。', true);
                 element.setAttribute(CONFIG.TRANSLATION_STATE_ATTR, 'error');
            }
            return;
        }

        // --- 步骤 4: 设置处理状态并显示我们的翻译加载提示 ---
        console.log("Setting state to 'processing' and showing translation tip.");
        element.setAttribute(CONFIG.TRANSLATION_STATE_ATTR, 'processing');
        // *** 确保此时才添加我们自己的加载提示 ***
        setStatusTip(element, '正在翻译 (使用 Gemini)...', false, true);

        // --- 步骤 5: 执行翻译 ---
        try {
            // console.log("Extracting placeholders from original HTML...");
            const { cleanedHTML, placeholders } = extractPlaceholders(originalHTML); // 必须使用捕获的 originalHTML

            // 检查清理后的 HTML 是否只包含占位符或为空
            if (cleanedHTML.replace(/<!-- PLACEHOLDER_\d+ -->/g, '').trim() === '') {
                 console.log("Cleaned HTML is effectively empty, skipping API call.");
                 removeStatusTip(element); // 移除我们的加载提示
                 element.setAttribute(CONFIG.TRANSLATION_STATE_ATTR, 'translated'); // 标记为已翻译(即使是空的)
                 // 恢复原始 HTML(可能只包含占位符或确实是空的)
                 element.innerHTML = originalHTML;
                 return;
            }

            // console.log("Calling translation API...");
            const translatedHTMLRaw = await callTranslationAPI(cleanedHTML, apiKey);
            console.log("Translation API call successful.");

            // *** 在应用翻译之前移除我们的加载提示 ***
            console.log("Removing translation status tip before applying translation.");
            removeStatusTip(element); // 关键一步!

            // 应用翻译结果
            console.log("Applying translation...");
            applyTranslation(element, originalHTML, translatedHTMLRaw, placeholders); // 使用正确的 originalHTML

            element.setAttribute(CONFIG.TRANSLATION_STATE_ATTR, 'translated');
            console.log("Translation applied. Final state: translated.");

        } catch (error) {
            console.error('翻译流程出错:', error);
            // 移除可能存在的加载提示,并显示错误信息
            removeStatusTip(element); // 先尝试移除加载提示
            setStatusTip(element, `翻译失败:${error.message || error}`, true); // 再设置错误提示
            element.setAttribute(CONFIG.TRANSLATION_STATE_ATTR, 'error');
        }
    }

    /**
     * 提取 HTML 中的 img 和 pre/code 标签,替换为占位符。
     * @param {string} html
     * @returns {{cleanedHTML: string, placeholders: Array<string>}}
     */
    function extractPlaceholders(html) {
        const placeholders = [];
        let index = 0;
        const cleanedHTML = html.replace(/<(img|pre|code)\b[^>]*>.*?<\/\1>|<(img)\b[^>]*?\/?>(?!\s*<\/(img)>)/gis, (match) => {
            placeholders.push(match);
            return `<!-- PLACEHOLDER_${index++} -->`;
        });
        return { cleanedHTML, placeholders };
    }

    /**
     * 恢复占位符。
     * @param {string} translatedHTML
     * @param {Array<string>} placeholders
     * @returns {string}
     */
    function restorePlaceholders(translatedHTML, placeholders) {
        return translatedHTML.replace(/<!-- PLACEHOLDER_(\d+) -->/g, (_, indexStr) => {
            const index = parseInt(indexStr, 10);
            return placeholders[index] !== undefined ? placeholders[index] : `<!-- MISSING_PLACEHOLDER_${index} -->`;
        });
    }

    /**
     * 调用 Gemini API 翻译。
     * @param {string} htmlToTranslate - 清理后的 HTML
     * @param {string} apiKey
     * @returns {Promise<string>} 翻译后的 HTML
     */
    function callTranslationAPI(htmlToTranslate, apiKey) {
      const maxRetries = 2; // 最大重试次数
      let retryCount = 0;

      return new Promise((resolve, reject) => {
        function makeRequest() {
          GM_xmlhttpRequest({
            method: 'POST',
            url: `${CONFIG.API_ENDPOINT}?key=${apiKey}`,
            headers: { 'Content-Type': 'application/json' },
            data: JSON.stringify({
              contents: [{
                parts: [{
                  text: `将以下 HTML 片段中的可读文本内容翻译成 **简体中文**。
请严格保留所有原始 HTML 标签(例如 <img>、<pre>、<code>、<a>、<strong>、<em> 等),其属性、结构以及任何占位符(例如 <!-- PLACEHOLDER_0 -->)。
**不要翻译** <pre>...</pre> 或 <code>...</code> 标签内的内容。
仅翻译这些受保护元素之外的用户可见文本。确保输出是有效的 HTML。

输入 HTML:
\`\`\`html
${htmlToTranslate}
\`\`\`

翻译后的 HTML (简体中文):`
                }]
              }],
              generationConfig: {}
            }),
            responseType: 'json',
            timeout: 45000,
            onload: (res) => {
              if (res.status === 200 && res.response) {
                const candidate = res.response.candidates?.[0];
                let text = candidate?.content?.parts?.[0]?.text;
                const finishReason = candidate?.finishReason;
                const blockReason = res.response.promptFeedback?.blockReason;

                if (blockReason) {
                  return reject(new Error(`API 请求被阻止: ${blockReason}。检查内容安全设置或提示。`));
                }
                if (finishReason && finishReason !== "STOP" && finishReason !== "MAX_TOKENS") {
                  if (finishReason === "MAX_TOKENS") {
                    console.warn("翻译可能由于 MAX_TOKENS 限制而不完整。");
                  } else {
                    return reject(new Error(`API 完成原因问题: ${finishReason}。内容可能不安全或发生错误。`));
                  }
                }
                if (text) {
                  text = text.replace(/^```(?:html)?\s*|```$/gi, '').trim();
                  resolve(text);
                } else if (finishReason === "STOP" && !text) {
                  console.warn("API 返回 STOP 但没有文本。假设没有可翻译内容。");
                  resolve("");
                } else {
                  console.error("API 响应详情:", JSON.stringify(res.response, null, 2));
                  reject(new Error('API 响应格式错误:在 candidate 部分中未找到有效的文本。'));
                }
              } else {
                let errorMsg = `API 请求失败,状态码 ${res.status}`;
                let errorDetails = '(无更多详细信息)';
                try {
                  if (res.response && res.response.error) {
                    errorDetails = res.response.error.message || JSON.stringify(res.response.error);
                  } else if (res.responseText) {
                    try {
                      const errJson = JSON.parse(res.responseText);
                      errorDetails = errJson.error?.message || res.responseText;
                    } catch (e) {
                      errorDetails = res.responseText;
                    }
                  }
                  if (errorDetails) errorMsg += `: ${errorDetails}`;
                  if (res.status === 400) errorMsg += " (Bad request - 检查 API key/请求格式)";
                  if (res.status === 403) errorMsg += " (Forbidden - 检查 API key 权限)";
                  if (res.status === 429) errorMsg += " (Rate limit exceeded)";
                  if (res.status >= 500) errorMsg += " (服务器端 API 错误)";
                } catch (e) {
                  console.error("错误解析错误响应:", e);
                }

                // 重试机制
                if (res.status >= 500 && retryCount < maxRetries) { //  仅对服务器错误进行重试
                  retryCount++;
                  console.log(`API 请求失败,正在重试 (${retryCount}/${maxRetries})...`);
                  setTimeout(makeRequest, 1000 * retryCount); // 延迟重试
                } else {
                  reject(new Error(errorMsg));
                }
              }
            },
            onerror: (err) => reject(new Error(`翻译期间的网络错误: ${err.error || '未知网络问题'}`)),
            ontimeout: () => reject(new Error('翻译请求超时'))
          });
        }

        makeRequest(); // 启动首次请求
      });
    }


    /**
     * 应用翻译结果。
     * @param {HTMLElement} element 目标元素
     * @param {string} originalHTML 原始 HTML
     * @param {string} translatedRaw 翻译后的原始文本
     * @param {Array<string>} placeholders 占位符
     */
    function applyTranslation(element, originalHTML, translatedRaw, placeholders) {
        const isBilingual = localStorage.getItem(CONFIG.STORAGE_KEY_MODE) === 'bilingual';
        const cleanTranslation = translatedRaw
            .replace(/^```(?:html)?\s*|```$/gi, '')
            .trim();

        const translatedWithContent = restorePlaceholders(cleanTranslation, placeholders);

        element.innerHTML = '';
        element.classList.remove('bilingual-container');

        if (isBilingual) {
            element.classList.add('bilingual-container');
            element.innerHTML = `
                <div class="original-text">
                  <strong>原文:</strong>
                  <div>${originalHTML}</div>
                </div>
                <hr class="translation-separator">
                <div class="translated-text">
                   <strong>翻译:</strong>
                   <div>${translatedWithContent || '(翻译为空)'}</div>
                </div>
            `;
        } else {
            element.innerHTML = translatedWithContent || originalHTML;
        }

    }

    // 初始化

    /**
     * 脚本初始化。
     */
    function initialize() {
        console.log("Codewars Translator初始化...");
        addStyles();
        addHeaderSwitch();

        // 使用 MutationObserver 监听目标元素的变化
        const observer = new MutationObserver((mutations) => {
            const targetElement = document.querySelector(CONFIG.TARGET_SELECTOR);
            if (targetElement) {
                observer.disconnect(); // 找到目标元素后停止监听
                processElement(targetElement);
            }
        });

        // 开始监听 document body 的变化
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        console.log("Codewars Translator 初始化完成。 监控路由变化以进行页面刷新。");
    }

    // 启动脚本
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }

})();