Pixiv Novel Translator (Pixiv 小说翻译器)

Pixiv翻译器,支持PC端列表页、小说页面和Fanbox.cc投稿页面的翻译,使用彩云小译API。

// ==UserScript==
// @name         Pixiv Novel Translator (Pixiv 小说翻译器)
// @namespace    http://tampermonkey.net/
// @version      0.4.1
// @description  Pixiv翻译器,支持PC端列表页、小说页面和Fanbox.cc投稿页面的翻译,使用彩云小译API。
// @author       Archeb
// @match        https://www.pixiv.net/*
// @match        https://*.fanbox.cc/posts/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=pixiv.net
// @grant        none
// @license      GPL
// ==/UserScript==

var _wr = function (type) {
	var orig = history[type];
	return function () {
		var rv = orig.apply(this, arguments);
		var e = new Event(type);
		e.arguments = arguments;
		window.dispatchEvent(e);
		return rv;
	};
};
(history.pushState = _wr("pushState")), (history.replaceState = _wr("replaceState"));

function getElementByXpath(path) {
	return document.evaluate(path, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
}

async function getTranslation(translateList) {
	let url = "https://api.interpreter.caiyunai.com/v1/translator";
	let token = localStorage.getItem("pixivtranslate-archeb-caiyunkey");

	// 检测语言,因为一次翻译的文本量是比较长的所以可以用这个办法
	let trans_type;
	let translateText = translateList.join("");
	if (translateText.match(/[\u3040-\u30ff]/)) {
		// 有假名,是日语
		trans_type = "ja2zh";
	} else if (translateText.match(/[\u4E90-\u9FFF]/)) {
		// 没有假名但是有汉字,是中文
		// 直接返回原文
		return { target: translateList };
	} else {
		// 就当他是英文
		trans_type = "en2zh";
	}

	let payload = {
		source: translateList,
		trans_type: trans_type,
		request_id: "403bfbf13b56220e46f9ff66725ead46",
	};

	let headers = {
		"content-type": "application/json",
		"x-authorization": "token " + token,
	};
	const response = await fetch(url, {
		method: "POST",
		headers: headers,
		mode: "cors",
		cache: "no-cache",
		body: JSON.stringify(payload),
	});
	return response.json();
}

function doContentTranslate() {
	let contentMainElement = getElementByXpath('//*[@id="root"]/div[2]/div/div[3]/div/div/div/main/section/div[2]/div[3]/div/div[1]/main/div');
	for (let paragraph of contentMainElement.querySelectorAll("p")) {

		doParagraphTranslate(paragraph);
	}
}

function doFanboxContentTranslate() {
	let contentMainElement = document.querySelector('.public-DraftEditor-content');
    contentMainElement=contentMainElement?contentMainElement:document.querySelector('.tHqFl>article');
	for (let paragraph of contentMainElement.querySelectorAll("div")) {
		doParagraphTranslate(paragraph);
	}
}

async function doNovelInfoTranslate() {
	let titleElement = getElementByXpath('//*[@id="root"]/div[2]/div/div[2]/div/div/main/section/div[1]/div/div[2]/h1');
	let seriesTitleElement = getElementByXpath('//*[@id="root"]/div[2]/div/div[2]/div/div/main/section/div[1]/div/div[2]/div[2]/a') || { innerText: "" };
	let descriptionElement = document.querySelector("main>section p[id^=expandable-paragraph");
	let result = await getTranslation([titleElement.innerText, seriesTitleElement.innerText]);
	titleElement.innerText = result.target[0];
	seriesTitleElement.innerText = result.target[1];
	doParagraphTranslate(descriptionElement);
}

async function doParagraphTranslate(paragraph) {
	let translationMap = {};
	let translationList = [];
	for (let node of paragraph.childNodes) {
		if ((node.nodeName == "#text" || node.nodeName == "SPAN") && node.textContent.length > 0 && !node.translated) {
			translationMap[translationList.length] = node;
			translationList.push(node.textContent);
			node.translated = true;
		}
	}
	if (translationList.length > 0) {
		let result = await getTranslation(translationList);
		for (let translationIndex in result.target) {
			translationMap[translationIndex].textContent = result.target[translationIndex];
		}
	}
}

async function doListTranslate(list) {
	let translationMap = {};
	let translationList = [];
	for (let item of list.querySelectorAll("ul>li")) {
        if(!item.translated){
		let titleElement = item.querySelector("div[title]>a[href]");
		let seriesTitleElement = item.querySelector("div>div:nth-child(2)>div>div:nth-child(1)>a") || { innerText: "" };
		let descriptionElement = item.querySelector("div>div:nth-child(2)>div div.sc-1c4k3wn-20 div.sc-1utla24-0") || { innerText: "" };
		translationMap[translationList.length] = titleElement;
		translationList.push(titleElement.innerText);
		translationMap[translationList.length] = seriesTitleElement;
		translationList.push(seriesTitleElement.innerText);
		translationMap[translationList.length] = descriptionElement;
		translationList.push(descriptionElement.innerText);
        item.translated=true;
        }
	}
	if (translationList.length > 0) {
		let result = await getTranslation(translationList);
		for (let translationIndex in result.target) {
			translationMap[translationIndex].innerText = result.target[translationIndex];
		}
	}
}

function doPageTranslate() {
    let listItemElements = document.querySelectorAll('div>div>ul>li[size="1"]')
	if (listItemElements.length>0) {
        for(let listItemEl of listItemElements){
            doListTranslate(listItemEl.parentElement);
        }
	}

	if (window.location.href.match(/pixiv\.net\/novel\/show.php/)) {
		console.log("Novel Page Detected");
		doContentTranslate();
		doNovelInfoTranslate();
	}
    if (window.location.href.match(/fanbox\.cc\/posts\//)) {
		console.log("Fanbox Page Detected");
		doFanboxContentTranslate();
	}
}

function promptSetKey() {
	let caiyunkey = prompt("请输入彩云小译Key,如果没有请到彩云开发者平台申请","");
	if(caiyunkey){
        localStorage.setItem("pixivtranslate-archeb-caiyunkey", caiyunkey);
        alert("保存完毕,如需修改请打开Pixiv边栏拉到最下面找【设置翻译key】");
    }
}

// 检查有没有存key
if (!localStorage.getItem("pixivtranslate-archeb-caiyunkey")) {
	promptSetKey();
}

function addSettingBtn() {
	var settingBtn = document.createElement("a");
	settingBtn.onclick = promptSetKey;
	settingBtn.innerHTML = "设置翻译key";
	settingBtn.href = "#";
	let findingElement = document.querySelector('a[href="https://policies.pixiv.net/#privacy"]')
    if(findingElement){
        findingElement.parentElement.append(settingBtn);
        clearInterval(addSettingBtnIntervalId);
    }
}

const addSettingBtnIntervalId = setInterval(addSettingBtn,1000);

window.addEventListener("pushState", () => {
	console.log("Location Changed");
	setTimeout(doPageTranslate, 500);
});
setInterval(doPageTranslate, 1000);