// --- Configuration --- var USE_INTERNET_TRANSLATION = true; // https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes var SOURCE_LANG = 'en'; var TARGET_LANG = 'zh'; // Define the remote translation provider order using initials, in descending preference: // D (DeepSeek - paid but recommended) L (Lingva - free), M (MyMemory - free), G (Google - free but firewalled in China) // 1) Pick one or more providers in any order (d, dm, dg, gm, dlmg etc) // 2) Use Deepseek only after having a) obtained a DEEPSEEK_API_KEY b) paid into this key. Otherwise, the key will not work var TRANSLATION_ORDER = 'dlmg'; // DeepSeek API Key(请替换为您的实际密钥,付费了才能用,一分钱能用很多天) https://platform.deepseek.com var DEEPSEEK_API_KEY = 'sk-'; // Tells Translator which elements to translate var TRANSLATION_CSS_SELECTOR = '.definition, .quotation-text, .etymology-note, .small-type-note, p.txt-example, p.txt-variant-label-short, .txt-example'; /*******************************************************************************************************************/ /*******************************************************************************************************************/ /****************************************** TABS AND NAVS *******************************************************/ /*******************************************************************************************************************/ /*******************************************************************************************************************/ // Tab Click Handler for Nested Trees function zngq_clk(t, targetId) { var bar = t.parentNode; var grp = bar.parentNode; var tabs = bar.children; // Are we clicking the already-active tab? (Toggle Show All) var isShowAll = (t.classList.contains('active') && grp.getAttribute('data-showall') !== 'true'); // Reset all tabs in this specific bar for(var i = 0; i < tabs.length; i++) { tabs[i].classList.remove('active'); } // Find content panels that belong ONLY to this specific group level var cnts =[]; for(var i = 0; i < grp.children.length; i++) { if(grp.children[i].classList.contains('zngq-cnt')) { cnts.push(grp.children[i]); } } if (isShowAll) { grp.setAttribute('data-showall', 'true'); for(var i = 0; i < cnts.length; i++) { cnts[i].classList.add('active'); cnts[i].style.display = 'block'; } } else { grp.setAttribute('data-showall', 'false'); t.classList.add('active'); for(var i = 0; i < cnts.length; i++) { if(cnts[i].id === targetId) { cnts[i].classList.add('active'); cnts[i].style.display = 'block'; } else { cnts[i].classList.remove('active'); cnts[i].style.display = 'none'; } } } } // Previous Button function zngq_p(btn) { var grp = btn.parentNode.parentNode; var bars = grp.getElementsByClassName('zngq-bar'); if(bars.length === 0) return; var bar = bars[0]; // get the immediate bar for this group var tabs = bar.children; var activeIdx = -1; for(var i = 0; i < tabs.length; i++) { if(tabs[i].classList.contains('active')) { activeIdx = i; break; } } if (activeIdx === -1) activeIdx = 1; // Default to next from 0 if none active, or handle showall var nextIdx = (activeIdx - 1 + tabs.length) % tabs.length; // Automatically hide siblings if nested nav causes an isolation event var cnt = grp.parentNode; if (cnt && cnt.classList.contains('zngq-cnt')) { var parentGrp = cnt.parentNode; if (parentGrp && parentGrp.getAttribute('data-showall') === 'true') { parentGrp.setAttribute('data-showall', 'false'); var parentBars = parentGrp.getElementsByClassName('zngq-bar'); if (parentBars.length > 0) { var pTabs = parentBars[0].children; for (var t = 0; t < pTabs.length; t++) { if (pTabs[t].getAttribute('onclick') && pTabs[t].getAttribute('onclick').indexOf("'" + cnt.id + "'") !== -1) { pTabs[t].click(); break; } } } } } grp.setAttribute('data-showall', 'false'); tabs[nextIdx].click(); } // Next Button function zngq_n(btn) { var grp = btn.parentNode.parentNode; var bars = grp.getElementsByClassName('zngq-bar'); if(bars.length === 0) return; var bar = bars[0]; // get the immediate bar for this group var tabs = bar.children; var activeIdx = -1; for(var i = 0; i < tabs.length; i++) { if(tabs[i].classList.contains('active')) { activeIdx = i; break; } } var nextIdx = (activeIdx + 1) % tabs.length; // Automatically hide siblings if nested nav causes an isolation event var cnt = grp.parentNode; if (cnt && cnt.classList.contains('zngq-cnt')) { var parentGrp = cnt.parentNode; if (parentGrp && parentGrp.getAttribute('data-showall') === 'true') { parentGrp.setAttribute('data-showall', 'false'); var parentBars = parentGrp.getElementsByClassName('zngq-bar'); if (parentBars.length > 0) { var pTabs = parentBars[0].children; for (var t = 0; t < pTabs.length; t++) { if (pTabs[t].getAttribute('onclick') && pTabs[t].getAttribute('onclick').indexOf("'" + cnt.id + "'") !== -1) { pTabs[t].click(); break; } } } } } grp.setAttribute('data-showall', 'false'); tabs[nextIdx].click(); } // Default to Show All Panels on Load window.onload = function() { var groups = document.getElementsByClassName('zngq-grp'); for(var g = 0; g < groups.length; g++) { groups[g].setAttribute('data-showall', 'true'); var children = groups[g].children; for(var i = 0; i < children.length; i++) { // Remove highlight from default active tabs if(children[i].classList.contains('zngq-bar')) { var tabs = children[i].children; for(var t = 0; t < tabs.length; t++) tabs[t].classList.remove('active'); } // Show all contents if(children[i].classList.contains('zngq-cnt')) { children[i].classList.add('active'); children[i].style.display = 'block'; } } } }; (function() { // --------------------------------------------------------- // Global Tooltip Engine (Bulletproof viewport tracking) // --------------------------------------------------------- var tooltip; function initTooltip() { tooltip = document.getElementById('oed-chart-tooltip-el'); if (!tooltip) { tooltip = document.createElement('div'); tooltip.id = 'oed-chart-tooltip-el'; tooltip.className = 'oed-chart-tooltip'; tooltip.style.position = 'fixed'; tooltip.style.display = 'none'; tooltip.style.zIndex = '10000'; tooltip.style.pointerEvents = 'none'; document.body.appendChild(tooltip); } function handleMouseMove(e) { e = e || window.event; var target = e.target || e.srcElement; var isHitArea = false; if (target && target.getAttribute) { var cls = target.getAttribute('class') || target.className; if (typeof cls === 'object' && cls.baseVal !== undefined) cls = cls.baseVal; if (typeof cls === 'string' && cls.indexOf('oed-hit-area') !== -1) { isHitArea = true; } } if (isHitArea) { var xVal = target.getAttribute('data-x'); var yVal = target.getAttribute('data-y'); // Formatted: 0.001 PMW 1970s tooltip.innerHTML = '
' + '' + yVal + ' ' + ' / M ' + '' + xVal + 's' + '
'; tooltip.style.display = 'block'; tooltip.style.left = (e.clientX + 15) + 'px'; tooltip.style.top = (e.clientY + 15) + 'px'; } else { tooltip.style.display = 'none'; } } if (document.addEventListener) { document.addEventListener('mousemove', handleMouseMove, false); } else if (document.attachEvent) { document.attachEvent('onmousemove', handleMouseMove); } } // --------------------------------------------------------- // Chart Utility: Generates exact OED-style numerical ticks // --------------------------------------------------------- function getNeatMax(max) { if (max === 0) return 1; var magnitude = Math.pow(10, Math.floor(Math.log(max) / Math.LN10)); var normalized = max / magnitude; var targetInterval = normalized / 4; var interval; if (targetInterval <= 0.25) interval = 0.25; else if (targetInterval <= 0.5) interval = 0.5; else if (targetInterval <= 1) interval = 1; else if (targetInterval <= 2) interval = 2; else interval = 2.5; return interval * 4 * magnitude; } // --------------------------------------------------------- // Chart Parsing & Initialization // --------------------------------------------------------- function initCharts() { var tables = document.getElementsByTagName('table'); var targetTables =[]; for (var i = 0; i < tables.length; i++) { if (tables[i].className && tables[i].className.indexOf('frequency-table') !== -1) { targetTables.push(tables[i]); } } var hasSVG = !!(document.createElementNS && !!document.createElementNS('http://www.w3.org/2000/svg', 'svg').createSVGRect); for (var i = 0; i < targetTables.length; i++) { var table = targetTables[i]; if (!table.id) continue; var containerId = 'chart-' + table.id; var container = document.getElementById(containerId); if (!container) continue; var dataX =[]; var dataY =[]; var dataYText =[]; var tbody = table.getElementsByTagName('tbody')[0]; var rows = tbody ? tbody.getElementsByTagName('tr') : table.getElementsByTagName('tr'); for (var j = 0; j < rows.length; j++) { var cols = rows[j].getElementsByTagName('td'); if (cols.length >= 2) { var xText = cols[0].innerText || cols[0].textContent || ""; var yText = cols[1].innerText || cols[1].textContent || ""; var cleanXText = xText.replace(/^\s+|\s+$/g, ''); var cleanYText = yText.replace(/^\s+|\s+$/g, ''); var freqNum = parseFloat(cleanYText); if (!isNaN(freqNum)) { dataX.push(cleanXText); dataY.push(freqNum); dataYText.push(cleanYText); } } } if (dataX.length === 0) continue; if (hasSVG) { renderSVGChart(container, dataX, dataY, dataYText); } else { renderDOMChart(container, dataX, dataY, dataYText); } } } // --------------------------------------------------------- // Modern SVG Engine // --------------------------------------------------------- function renderSVGChart(container, dataX, dataY, dataYText) { var maxFreq = 0; for (var i = 0; i < dataY.length; i++) { if (dataY[i] > maxFreq) maxFreq = dataY[i]; } maxFreq = getNeatMax(maxFreq); var svgW = 800; var svgH = 300; // Increased bottom padding from 40 to 60 to comfortably fit the "Decade" legend var padding = { top: 20, right: 20, bottom: 60, left: 80 }; var w = svgW - padding.left - padding.right; var h = svgH - padding.top - padding.bottom; var stepX = w / Math.max(1, dataX.length - 1); var points =[]; for (var i = 0; i < dataY.length; i++) { var px = (i * stepX).toFixed(1); var py = (h - (dataY[i] / maxFreq) * h).toFixed(1); points.push(px + ',' + py); } var polyPoints = points.join(' '); var polygonPoints = '0,' + h + ' ' + polyPoints + ' ' + w + ',' + h; var svg = ''; svg += '' + '' + '' + ''; svg += ''; // Y-Axis Legend svg += 'Frequency per million words'; // X-Axis Legend (Decade) - Placed below the tick labels svg += 'Decade'; // Horizontal Y-Axis Grids for(var k = 0; k <= 4; k++) { var yPos = h - (h * (k / 4)); var yValRaw = maxFreq * (k / 4); var yVal = parseFloat(yValRaw.toFixed(5)); if (yValRaw === 0) yVal = "0"; svg += ''; svg += '' + yVal + ''; } // Vertical X-Axis Grids for (var i = 0; i < dataX.length; i++) { var px = (i * stepX).toFixed(1); svg += ''; } // Draw Area & Line svg += ''; svg += ''; // Draw Interactive Elements for (var i = 0; i < dataY.length; i++) { var px = (i * stepX).toFixed(1); var py = (h - (dataY[i] / maxFreq) * h).toFixed(1); // X-Axis tick label svg += '' + dataX[i] + ''; // Visible Marker svg += ''; // Invisible oversized hit-area svg += ''; } svg += ''; container.innerHTML = svg; } // --------------------------------------------------------- // Legacy Engine: IE8 fallback // --------------------------------------------------------- function renderDOMChart(container, dataX, dataY, dataYText) { var maxFreq = 0; for (var i = 0; i < dataY.length; i++) { if (dataY[i] > maxFreq) maxFreq = dataY[i]; } maxFreq = getNeatMax(maxFreq); // Increased bottom padding here as well var padding = { top: 20, right: 20, bottom: 60, left: 80 }; var html = '
'; var n = dataX.length; var widthPct = 100 / Math.max(1, n - 1); // Y-Axis Legend html += '
Frequency per million words
'; // X-Axis Legend (Decade) html += '
Decade
'; for(var k = 0; k <= 4; k++) { var bottomPct = (k / 4) * 100; var yValRaw = maxFreq * (k / 4); var yVal = parseFloat(yValRaw.toFixed(5)); if (yValRaw === 0) yVal = "0"; html += '
'; html += '
' + yVal + '
'; } for (var i = 0; i < n; i++) { var leftPct = i * widthPct; html += '
'; } for (var i = 0; i < n; i++) { var leftPct = i * widthPct; var heightPct = (dataY[i] / maxFreq) * 100; var blockWidth = (i === n - 1) ? 0 : widthPct; html += '
'; html += '
'; // X-Axis tick label html += '
' + dataX[i] + '
'; } html += '
'; container.innerHTML = html; } var oldLoad = window.onload; window.onload = function() { if (typeof oldLoad === 'function') oldLoad(); initTooltip(); initCharts(); }; })(); var AudioSequencer = (function() { var queue = []; var currentIndex = 0; var player = new Audio(); // Persistent player to avoid clipping player.onended = function() { currentIndex++; playNext(); }; function playNext() { if (currentIndex < queue.length) { var url = queue[currentIndex]; player.src = url; player.play(); } else { console.log("Sequence complete."); currentIndex = 0; // Reset for next time queue = []; } } return { // Pass a selector for the anchor buttons playButtons: function(selector) { var anchors = document.querySelectorAll(selector); queue = []; currentIndex = 0; for (var i = 0; i < anchors.length; i++) { // Extract the URL from the onclick string // e.g., "new Audio('sound.mp3').play()" -> "sound.mp3" var onclickStr = anchors[i].getAttribute('onclick') || ""; var match = onclickStr.match(/'([^']+)'/); if (match && match[1]) { queue.push(match[1]); } } if (queue.length > 0) playNext(); } }; })(); /*******************************************************************************************************************/ /*******************************************************************************************************************/ /******************************************ONLINE TRANSLATION*******************************************************/ /*******************************************************************************************************************/ /*******************************************************************************************************************/ /* stuff to make ancient browsers work */ function addClass(el, className) { if (el.className.indexOf(className) === -1) { el.className += (el.className ? ' ' : '') + className; } } function removeClass(el, className) { var reg = new RegExp('(\\s|^)' + className + '(\\s|$)'); el.className = el.className.replace(reg, ' ').replace(/^\s+|\s+$/g, ''); } function hasClass(el, className) { return el.className.indexOf(className) !== -1; } // Custom IE9 Cross-Domain AJAX Engine (now returning specific HTTP codes and JSON body data) function ajaxGet(url, timeoutMs, onSuccess, onError) { var xhr = new XMLHttpRequest(); if ("withCredentials" in xhr) { xhr.open("GET", url, true); } else if (typeof XDomainRequest !== "undefined") { xhr = new XDomainRequest(); xhr.open("GET", url); } else { return onError(new Error("CORS not supported on this browser version")); } var isDone = false; var timer = setTimeout(function() { if (isDone) return; isDone = true; xhr.abort(); onError(new Error("Timeout after " + timeoutMs + "ms")); }, timeoutMs); xhr.onload = function() { if (isDone) return; isDone = true; clearTimeout(timer); // IE8/9 XDomainRequest doesn't supply a status, so default to 200 on load. var status = ('status' in xhr) ? xhr.status : 200; if (status >= 200 && status < 300) { try { var data = JSON.parse(xhr.responseText); onSuccess(data); } catch (e) { var snippet = xhr.responseText ? xhr.responseText.substring(0, 150) : "(empty)"; onError(new Error("JSON Parse Error: " + e.message + " | Response: " + snippet)); } } else { var snippet = xhr.responseText ? xhr.responseText.substring(0, 200) : "(empty response)"; onError(new Error("HTTP Error " + status + " | Body: " + snippet)); } }; xhr.onerror = function() { if (isDone) return; isDone = true; clearTimeout(timer); onError(new Error("Network Error, DNS failure, or CORS blocked for GET: " + url.split('?')[0])); }; if (xhr.constructor === window.XDomainRequest) { xhr.onprogress = function() {}; xhr.ontimeout = function() {}; } setTimeout(function() { xhr.send(); }, 0); } // --- CSS Injector --- function injectTranslationStyles() { // Prevent duplicate style tags on repeated DOM scans if (document.getElementById('zngq-ai-styles')) return; var css = /* The Invisible Bridge Wrapper */ ".ai-btn-wrapper { display: none; padding-left: 8px; padding-right: 4px; }" + /* Show wrapper when Javascript adds the ai-show-btn class OR when active */ ".ai-hover-target.ai-show-btn .ai-btn-wrapper, .ai-hover-target.ai-btn-active .ai-btn-wrapper { display: inline-block; }" + /* Makes the button look like a real, graphical button */ ".ai-translate-btn { cursor: pointer; background: #ffffff; border: 1px solid #b0b0b0; border-radius: 4px; font-size: 11px; padding: 3px 8px; color: #333; font-family: Arial, sans-serif; box-shadow: 0 1px 2px rgba(0,0,0,0.1); user-select: none; -ms-user-select: none; vertical-align: middle; line-height: 1; text-decoration: none; }" + ".ai-translate-btn:hover { background: #f5f5f5; border-color: #888; }" + ".ai-translate-btn:active { background: #ebebeb; box-shadow: inset 0 1px 2px rgba(0,0,0,0.1); }" + /* Updated with line-height to support multi-line segmented translations gracefully */ ".translation-result { display: inline; color: #2e7d32; font-weight: bold; margin-left: 6px; padding-left: 6px; border-left: 2px solid #ccc; font-size: 0.95em; line-height: 1.5; }" + ".translation-result.translation-error { display: block; margin-top: 8px; margin-bottom: 4px; color: #d32f2f; border-left-color: #f44336; white-space: pre-wrap; font-family: monospace; font-size: 0.85em; font-weight: normal; line-height: 1.4; }" + ".api-marker { font-size: 0.85em; filter: alpha(opacity=60); opacity: 0.6; margin-right: 4px; cursor: help; }" + /* CSS Spinner Animation */ "@keyframes ai-spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }" + ".ai-spinner { display: inline-block; width: 8px; height: 8px; border: 2px solid rgba(0,0,0,0.2); border-top-color: #007bff; border-radius: 50%; vertical-align: middle; margin-right: 5px; animation: ai-spin 0.8s linear infinite; }"; var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.id = 'zngq-ai-styles'; // ID assigned here style.type = 'text/css'; if (style.styleSheet) { // IE8/9 specific logic style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } head.appendChild(style); } // --- Text Extraction Logic (Slices distinct elements with newlines) --- function extractDelimitedText(element) { // Exact word-boundary matching prevents 't' from triggering on 'mw_t_wi' var blockClasses = /(^|\s)(ex-sent|d-block|dtText|t|sub-content-thread|little_gems|syns_discussion|faqs|etymology-content-section)(\s|$)/i; function traverse(node) { if (node.nodeType === 3) { // Text node // FIX: Strip out invisible HTML source-code line breaks and indentation. // Convert them to simple spaces, exactly as a browser visually renders them. return node.nodeValue.replace(/[\r\n]+/g, ' '); } if (node.nodeType === 1) { // Element node var tag = node.tagName.toUpperCase(); if (tag === 'BR') return '\n'; // Skip over our own injected UI elements if scanning repeats if (node.className && typeof node.className === 'string' && (node.className.indexOf('ai-btn-wrapper') > -1 || node.className.indexOf('translation-result') > -1)) { return ''; } var isBlock = false; // Native block elements or custom dictionary blocks if (tag === 'DIV' || tag === 'P' || tag === 'LI' || tag === 'TR' || tag === 'UL' || tag === 'H1' || tag === 'H2' || tag === 'H3') { isBlock = true; } else if (node.className && typeof node.className === 'string' && blockClasses.test(node.className)) { isBlock = true; } var text = ''; for (var i = 0; i < node.childNodes.length; i++) { text += traverse(node.childNodes[i]); } // Bookend semantic blocks with newlines to segregate sentences if (isBlock) { return '\n' + text + '\n'; } return text; } return ''; } var raw = traverse(element); // Normalize step 1: Collapse multiple spaces into one raw = raw.replace(/[ \t]+/g, ' '); // Normalize step 2: Clean up spaces immediately clinging to our newlines raw = raw.replace(/[ \t]*\n[ \t]*/g, '\n'); // Normalize step 3: Collapse consecutive newlines into a single strict separator raw = raw.replace(/\n+/g, '\n'); // Trim edges return raw.replace(/^\s+|\s+$/g, ''); } // --- DOM Event Logic --- function initializeTranslator() { injectTranslationStyles(); var rawElements = document.querySelectorAll(TRANSLATION_CSS_SELECTOR); var elements =[]; // Array to hold the filtered result // Phase 1: Filter out duplicates, already-processed elements, and nested children for (var i = 0; i < rawElements.length; i++) { var el = rawElements[i]; // 1. Skip if already processed during a previous scan if (hasClass(el, 'ai-hover-target')) { continue; } var isNested = false; var parent = el.parentNode; // 2. Traverse up to see if any ancestor takes precedence while (parent && parent !== document) { // Check A: Is the parent already processed in a previous scan? if (parent.className && typeof parent.className === 'string' && parent.className.indexOf('ai-hover-target') !== -1) { isNested = true; break; } // Check B: Is the parent part of the current raw query selection? for (var j = 0; j < rawElements.length; j++) { if (parent === rawElements[j]) { isNested = true; break; } } if (isNested) break; parent = parent.parentNode; } if (!isNested) { // 3. Ensure absolute uniqueness (prevent duplicates inside array) var isDuplicate = false; for (var k = 0; k < elements.length; k++) { if (elements[k] === el) { isDuplicate = true; break; } } if (!isDuplicate) elements.push(el); } } // Phase 2: Attach Logic for (var i = 0; i < elements.length; i++) { (function(el) { // Apply the custom structural text extraction var rawText = extractDelimitedText(el); el.setAttribute('data-original-text', rawText); // Allow JS to identify the element addClass(el, 'ai-hover-target'); // 1. Create the invisible bridge wrapper var btnWrapper = document.createElement('span'); btnWrapper.className = 'ai-btn-wrapper'; // 2. Create the UI button itself var btn = document.createElement('span'); btn.className = 'ai-translate-btn'; btn.innerHTML = 'AI'; btn.setAttribute('title', 'Translate to Chinese'); // Assemble btnWrapper.appendChild(btn); el.appendChild(btnWrapper); // --- JAVASCRIPT HOVER DELAY TIMER --- var hoverTimer = null; el.onmouseenter = function() { // When mouse enters text or button, clear countdown and show immediately if (hoverTimer) clearTimeout(hoverTimer); addClass(el, 'ai-show-btn'); }; el.onmouseleave = function() { // When mouse leaves, wait 2 seconds (2000ms) before hiding button hoverTimer = setTimeout(function() { removeClass(el, 'ai-show-btn'); }, 2000); }; // ------------------------------------ // Click listener attached ONLY to the button btn.onclick = function(event) { var e = event || window.event; if (e.stopPropagation) { e.stopPropagation(); } else { e.cancelBubble = true; } if (hasClass(el, 'ai-translating')) return; var currentState = el.getAttribute('data-translation-state'); // State 1: VISIBLE -> Hide it if (currentState === 'visible') { var spanVis = el.querySelector('.translation-result'); if (spanVis) spanVis.style.display = 'none'; el.setAttribute('data-translation-state', 'hidden'); removeClass(el, 'ai-btn-active'); btn.innerHTML = 'Show'; return; } // State 2: HIDDEN -> Show it instantly if (currentState === 'hidden') { var spanHid = el.querySelector('.translation-result'); // Setting to empty string reverts back to the CSS class definition (inline vs block) if (spanHid) spanHid.style.display = ''; el.setAttribute('data-translation-state', 'visible'); addClass(el, 'ai-btn-active'); btn.innerHTML = 'Hide'; return; } // State 3: Fetch translation var textToTranslate = el.getAttribute('data-original-text'); if (!textToTranslate || textToTranslate === '') return; // Apply Loading State addClass(el, 'ai-translating'); addClass(el, 'ai-btn-active'); // Show CSS Spinner inside button btn.innerHTML = ' ...'; btn.style.cursor = 'wait'; // Trigger native OS "Spinning Beach Ball" document.body.style.cursor = 'wait'; translateWaterfall(textToTranslate, SOURCE_LANG, TARGET_LANG, function(result) { // Success Callback var translationSpan = document.createElement('span'); translationSpan.className = 'translation-result'; var markerNode = document.createElement('span'); markerNode.className = 'api-marker'; markerNode.title = 'Translated by ' + result.apiName; markerNode.appendChild(document.createTextNode('[' + result.marker + '] ')); translationSpan.appendChild(markerNode); // --- NEW: Handle rendering separated strings dynamically --- var lines = result.text.split('\n'); var isFirstLine = true; for (var j = 0; j < lines.length; j++) { var cleanLine = lines[j].replace(/^\s+|\s+$/g, ''); if (cleanLine !== '') { if (!isFirstLine) { // Use a hard line break for separation translationSpan.appendChild(document.createElement('br')); // Inject a spacer block to align trailing lines past the marker width cleanly var indent = document.createElement('span'); indent.style.display = 'inline-block'; indent.style.width = '24px'; translationSpan.appendChild(indent); } translationSpan.appendChild(document.createTextNode(cleanLine)); isFirstLine = false; } } el.appendChild(translationSpan); // Reset States el.setAttribute('data-translation-state', 'visible'); removeClass(el, 'ai-translating'); btn.innerHTML = 'Hide'; // Remove OS Beach Ball btn.style.cursor = 'pointer'; document.body.style.cursor = ''; }, function(errorList) { // Failure Callback (Shows specific dynamic errors directly inline) var errStr = (errorList && errorList.length > 0) ? errorList.join('\n') : "Unknown structural failure."; console.error("Translation Waterfall Failed:\n" + errStr); var errorSpan = document.createElement('span'); // Tag it with both classes so it can still toggle but takes on block/monospace styles errorSpan.className = 'translation-result translation-error'; var markerNode = document.createElement('span'); markerNode.className = 'api-marker'; markerNode.title = 'All Translation Networks Failed'; markerNode.appendChild(document.createTextNode('[Error]')); var textNode = document.createTextNode(" Translation Failed:\n" + errStr); errorSpan.appendChild(markerNode); errorSpan.appendChild(textNode); el.appendChild(errorSpan); // Reset States so the user has the option to click "Hide" el.setAttribute('data-translation-state', 'visible'); removeClass(el, 'ai-translating'); btn.innerHTML = 'Hide'; // Remove OS Beach Ball btn.style.cursor = 'pointer'; document.body.style.cursor = ''; } ); }; })(elements[i]); } } if (USE_INTERNET_TRANSLATION === true) { // Ensure script works even if loaded in if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeTranslator); } else { initializeTranslator(); } } // --- THE WATERFALL ENGINE (Dynamic Async Loop) --- function translateWaterfall(text, sourceLang, targetLang, onComplete, onUltimateFail) { var order = TRANSLATION_ORDER.toUpperCase(); var currentIndex = 0; var accumulatedErrors =[]; // This internal function acts as our async loop, catching actual raw error messages function tryNext(err) { if (err) { var prevProvider = order.charAt(currentIndex - 1); var errMessage = err.message || err.toString(); accumulatedErrors.push("[" + prevProvider + " Provider] " + errMessage); } // If we've reached the end of the string, all fallbacks failed if (currentIndex >= order.length) { return onUltimateFail(accumulatedErrors); } // Get the current letter and advance the index for the next potential failure var currentProvider = order.charAt(currentIndex); currentIndex++; // Route to the correct API based on the letter if (currentProvider === 'D') { fetchDeepSeek(text, sourceLang, targetLang, function(res) { onComplete({ text: res, marker: 'D', apiName: 'DeepSeek' }); }, tryNext); } else if (currentProvider === 'L') { fetchLingva(text, sourceLang, targetLang, function(res) { onComplete({ text: res, marker: 'L', apiName: 'Lingva' }); }, tryNext); // Pass tryNext as the error callback } else if (currentProvider === 'Y') { fetchYoudao(text, function(res) { onComplete({ text: res, marker: 'Y', apiName: 'Youdao' }); }, tryNext); } else if (currentProvider === 'M') { fetchMyMemory(text, sourceLang, targetLang, function(res) { onComplete({ text: res, marker: 'M', apiName: 'MyMemory' }); }, tryNext); } else if (currentProvider === 'G') { fetchGoogle(text, sourceLang, targetLang, function(res) { onComplete({ text: res, marker: 'G', apiName: 'Google Translate' }); }, tryNext); } else { // Unrecognized letter code tryNext(new Error("Unrecognized provider code '" + currentProvider + "' in TRANSLATION_ORDER string.")); } } // Kick off the loop tryNext(); } // --- THE API ENDPOINTS --- // --- DeepSeek API(POST 请求,非流式)--- function fetchDeepSeek(text, sourceLang, targetLang, onSuccess, onError) { var url = 'https://api.deepseek.com/chat/completions'; var data = { model: 'deepseek-chat', messages:[ { role: 'system', content: 'You are an internationally renowned maven for the ultimate 信达雅 in the art of translation. ' + 'Translate the given text from ' + sourceLang + ' to ' + targetLang + '. Preserve the exact line breaks of the original text. ' + 'Output only the translation, no explanation.' }, { role: 'user', content: text } ], temperature: 0.1, stream: false }; var xhr = new XMLHttpRequest(); var isDone = false; var timer = setTimeout(function() { if (isDone) return; isDone = true; xhr.abort(); onError(new Error("Timeout after 5000ms")); }, 5000); xhr.open('POST', url, true); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.setRequestHeader('Authorization', 'Bearer ' + DEEPSEEK_API_KEY); xhr.onload = function() { if (isDone) return; isDone = true; clearTimeout(timer); if (xhr.status >= 200 && xhr.status < 300) { try { var response = JSON.parse(xhr.responseText); if (response && response.choices && response.choices[0] && response.choices[0].message) { var translation = response.choices[0].message.content; onSuccess(translation); } else { onError(new Error("Invalid JSON structure: " + xhr.responseText.substring(0, 150))); } } catch(e) { onError(new Error("JSON Parsing failure: " + e.message)); } } else { // e.g. This will natively capture "402 Insufficient Balance", "401 Unauthorized" var bodySnippet = xhr.responseText ? xhr.responseText.substring(0, 200) : "(empty response body)"; onError(new Error("HTTP Error " + xhr.status + " | " + bodySnippet)); } }; xhr.onerror = function() { if (isDone) return; isDone = true; clearTimeout(timer); onError(new Error("Network Error, DNS failure, or CORS blocked for POST: " + url)); }; xhr.send(JSON.stringify(data)); } function fetchLingva(text, sourceLang, targetLang, onSuccess, onError) { var servers =[ 'https://translate.plausibility.cloud', 'https://lingva.lunar.icu', 'https://lingva.garudalinux.org' ]; var index = 0; var lingvaErrors =[]; function tryNextServer(err) { if (err) { lingvaErrors.push(servers[index - 1] + " -> " + (err.message || err.toString())); } if (index >= servers.length) { return onError(new Error("All mirrors failed: \n - " + lingvaErrors.join('\n - '))); } var url = servers[index] + '/api/v1/' + sourceLang + '/' + targetLang + '/' + encodeURIComponent(text); index++; ajaxGet(url, 3000, function(data) { if (data && data.translation) { onSuccess(data.translation); } else { tryNextServer(new Error("Missing 'translation' node in JSON: " + JSON.stringify(data).substring(0, 100))); } }, tryNextServer); } tryNextServer(); } function fetchMyMemory(text, sourceLang, targetLang, onSuccess, onError) { var email = 'fallback@example.com'; var url = 'https://api.mymemory.translated.net/get?q=' + encodeURIComponent(text) + '&langpair=' + sourceLang + '|' + targetLang + '&de=' + email; ajaxGet(url, 3500, function(data) { if (data && data.responseData && data.responseData.translatedText) { onSuccess(data.responseData.translatedText); } else { onError(new Error("Invalid API JSON layout received: " + JSON.stringify(data).substring(0, 150))); } }, onError); } function fetchGoogle(text, sourceLang, targetLang, onSuccess, onError) { var url = 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=' + sourceLang + '&tl=' + targetLang + '&dt=t&q=' + encodeURIComponent(text); ajaxGet(url, 4000, function(data) { if (data && data[0]) { var combined = ""; for(var i=0; i < data[0].length; i++) { combined += data[0][i][0]; } onSuccess(combined); } else { onError(new Error("Unexpected Google array structure: " + JSON.stringify(data).substring(0, 150))); } }, onError); } // 占位函数:若原页面未定义 fetchYoudao,此空实现保证不会崩溃 function fetchYoudao(text, onSuccess, onError) { onError(new Error("Youdao API endpoint not yet implemented in script")); }