// ==UserScript== // @name Pixiv Novel Translator (Pixiv 小说翻译器) // @namespace http://tampermonkey.net/ // @version 0.3.1 // @description Pixiv翻译器,支持PC端列表页和小说页面的翻译,使用彩云小译API。 // @author Archeb // @match https://www.pixiv.net/* // @icon https://www.google.com/s2/favicons?sz=64&domain=pixiv.net // @grant none // @license GPL // @downloadURL none // ==/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[2]/div/div/main/section/div[2]/div[3]/div/div[1]/main'); for (let paragraph of contentMainElement.querySelectorAll("p")) { 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(); } } function promptSetKey() { let caiyunkey = prompt("请输入彩云小译Key\n已经内置「沙拉查词」的彩云小译key,可以直接回车使用;或者您也可以用自己的Key","wno8rhqiranvo8ducvbw"); 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);