// --- 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 = '
';
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 = '';
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"));
}