// ==UserScript==
// @name 虚幻引擎蓝图文档机翻v1.0.1
// @namespace http://tampermonkey.net/
// @version 1.0.1
// @description 使用百度翻译API翻译虚幻引擎蓝图文档,包含API条目与API内容描述翻译
// @author 苦旅Zz
// @match https://dev.epicgames.com/documentation/*/unreal-engine/BlueprintAPI*
// @match https://dev.epicgames.com/documentation/*/unreal-engine/BlueprintAPI/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=epicgames.com
// @grant GM_xmlhttpRequest
// @grant GM_registerMenuCommand
// @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
// ==/UserScript==
(function() {
'use strict';
console.log("==================== 欢迎使用 虚幻引擎蓝图文档机翻脚本v1.0.1 ====================");
const fanyiAPI = "https://fanyi-api.baidu.com/api/trans/vip/translate";
const timestamp = Math.floor(Date.now() / 1000); // 当前时间戳,单位:秒
let appid = "";
let key = "";
let pageKey = "";
let lastUrl = "";
let getRootBlockRenderDivInterval = null;
let transferDatas = [];
let renderIndex = 0;
let renderRun = false;
let rootBlockRenderDiv = null;
let overlay = null;
let modal = null;
function modelStrust(){
// 添加样式
const style = document.createElement('style');
style.innerHTML = `
#custom-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10000;
width: 400px;
background: #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
border-radius: 8px;
overflow: hidden;
font-family: Arial, sans-serif;
}
#custom-modal-header {
background: #007bff;
color: white;
padding: 10px;
font-size: 16px;
font-weight: bold;
text-align: center;
}
#custom-modal-body {
padding: 20px;
}
#custom-modal-footer {
padding: 10px;
text-align: right;
background: #f1f1f1;
}
#custom-modal-footer button {
padding: 8px 12px;
margin-left: 10px;
border: none;
border-radius: 4px;
cursor: pointer;
}
#btn-submit {
background: #007bff;
color: white;
}
#btn-cancel {
background: #d9534f;
color: white;
}
#custom-modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 9999;
}
`;
document.head.appendChild(style);
// 创建弹窗 HTML
const modalHTML = `
<div id="custom-modal-overlay" style="display: none;"></div>
<div id="custom-modal" style="display: none;">
<div id="custom-modal-header">输入百度翻译开发者秘钥</div>
<div id="custom-modal-body">
<label style="color:#000;">注册(不可用)链接:<a style="color:blue;" href="https://fanyi-api.baidu.com/manage/developer">百度翻译开放平台</a></label><br>
<form id="custom-form">
<label style="color:#000;" for="name">APP ID:</label><br>
<input type="text" id="name" value="${appid}" name="appid" style="width: 100%; padding: 5px; margin-bottom: 10px; color: #000;"><br>
<label style="color:#000; for="email">秘钥:</label><br>
<input type="email" id="email" value="${key}" name="key" style="width: 100%; padding: 5px; margin-bottom: 10px; color: #000;"><br>
</form>
</div>
<div id="custom-modal-footer">
<button id="btn-submit">提交</button>
<button id="btn-cancel">取消</button>
</div>
</div>
`;
// 将弹窗插入到页面
const div = document.createElement('div');
div.innerHTML = modalHTML;
document.body.appendChild(div);
// 获取元素
overlay = document.getElementById('custom-modal-overlay');
modal = document.getElementById('custom-modal');
const btnSubmit = document.getElementById('btn-submit');
const btnCancel = document.getElementById('btn-cancel');
const form = document.getElementById('custom-form');
// 提交按钮点击事件
btnSubmit.addEventListener('click',async (e) => {
e.preventDefault();
const formData = new FormData(form);
const data = Object.fromEntries(formData.entries());
console.log('表单数据:', data);
const db = await openDatabase();
await addData(db, {id: "baiduTransferApi", data: data});
closeModal();
location.reload();
});
// 取消按钮点击事件
btnCancel.addEventListener('click', closeModal);
overlay.addEventListener('click', closeModal);
}
// 关闭弹窗
function closeModal() {
modal.style.display = 'none';
overlay.style.display = 'none';
}
// 打开弹窗
function openModal() {
modal.style.display = 'block';
overlay.style.display = 'block';
}
GM_registerMenuCommand("设置百度翻译key", function(){
console.log("123")
openModal()
})
setInterval(function(){
let current = window.location.href;
if(lastUrl !== current){
if(getRootBlockRenderDivInterval){
clearInterval(getRootBlockRenderDivInterval);
}
lastUrl = current;
init();
}
},1000)
async function init(){
transferDatas = [];
renderIndex = 0;
renderRun = false;
const db = await openDatabase();
let cacheData = await getData(db, "baiduTransferApi");
if(!cacheData || cacheData.length === 0){
console.error("百度翻译key未设置!!!!")
modelStrust();
return
}else{
appid = cacheData.data.appid
key = cacheData.data.key
modelStrust();
}
getRootBlockRenderDivInterval = setInterval(function(){
console.log("尝试获取文档根元素。。。。")
rootBlockRenderDiv = document.getElementById("content-blocks-renderer");
if(rootBlockRenderDiv){
console.log("已成功获取元素");
let h1Doms = document.querySelectorAll("h1");
if(h1Doms.length > 0){
console.log("找到h1标题");
if("Unreal Engine Blueprint API Reference" === h1Doms[0].innerText){
pageKey = "home";
}else{
pageKey = h1Doms[0].innerText;
}
}
console.log("pageKey ->", pageKey);
rootBlockRenderSuccess();
}
},2000)
}
// 打开数据库
async function openDatabase() {
return new Promise((resolve, reject) => {
const request = indexedDB.open('TransferDatabase', 1);
request.onerror = () => reject('Database failed to open');
request.onsuccess = () => resolve(request.result);
request.onupgradeneeded = (e) => {
const db = e.target.result;
if (!db.objectStoreNames.contains('MyStore')) {
db.createObjectStore('MyStore', { keyPath: 'id' });
}
};
});
}
// 添加数据
async function addData(db, data) {
const transaction = db.transaction(['MyStore'], 'readwrite');
const store = transaction.objectStore('MyStore');
return store.add(data);
}
async function getData(db, id) {
return new Promise((resolve, reject) => {
const transaction = db.transaction(['MyStore'], 'readonly'); // 创建只读事务
const store = transaction.objectStore('MyStore');
const request = store.get(id);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject('Failed to fetch data');
});
}
function rootBlockRenderSuccess(){
console.log("页面加载完成");
clearInterval(getRootBlockRenderDivInterval);
let inputDom = document.getElementById("inputs");
let outputsDom = document.getElementById("outputs");
if(inputDom && outputsDom){
console.log("API页面")
getApiDir();
}else{
getBlockDir();
}
}
async function getApiDir(){
let colIndex = 3;
let renderDoms = []
let markdownDom = document.getElementById("navigation").parentElement;
let ps = markdownDom.querySelectorAll("p");
for(let i = 1; i < ps.length; i++){
renderDoms.push(ps[i])
}
let tabelDoms = markdownDom.querySelectorAll("table");
for(let i = 0; i < tabelDoms.length; i++){
let tableDom = tabelDoms[i];
let tds = tableDom.querySelectorAll("tr td");
for(let j = 1; j <= tds.length; j++){
if(j % 3 === 0){
let td = tds[j-1];
if(td.innerText.trim() !== ''){
renderDoms.push(td);
}
}
}
}
console.log("renderDoms ->", renderDoms);
const db = await openDatabase();
let cacheData = await getData(db, pageKey);
if(!cacheData || cacheData.length === 0){
let batchTransferWords = [];
let words = [];
for(let i = 0; i < renderDoms.length; i++){
words.push(renderDoms[i].innerText)
}
batchTransferWords.push(words);
arrayToString(batchTransferWords);
}else{
console.log("找到 cache key = ", pageKey)
transferDatas = cacheData.data
}
renderRun = true;
renderHtml(renderDoms);
}
async function getBlockDir(){
let blockItems = rootBlockRenderDiv.querySelectorAll("block-dir-item");
let pdoms = [];
let batchTransferWords = [];
let words = [];
for(let i = 0; i < blockItems.length; i++){
let item = blockItems[i];
let titleDom = item.querySelector(".title");
pdoms.push(titleDom);
words.push(titleDom.innerText);
if(words.length === 50){
batchTransferWords.push(words);
words = [];
}
}
batchTransferWords.push(words);
const db = await openDatabase();
let cacheData = await getData(db, pageKey);
console.log("cacheData ->",cacheData)
if(!cacheData || cacheData.length === 0){
arrayToString(batchTransferWords);
}else{
console.log("找到 cache key = ", pageKey)
transferDatas = cacheData.data
}
renderRun = true;
renderHtml(pdoms);
}
// 翻译后中文 渲染页面
async function renderHtml(blockItems){
console.log("开始渲染 ->", transferDatas, renderIndex)
for(;renderIndex < transferDatas.length; renderIndex++){
let item = blockItems[renderIndex];
//console.log(item)
let newSpan = document.createElement("span");
newSpan.textContent = " ("+ transferDatas[renderIndex].dst + ")";
newSpan.style.color = "#fff";
newSpan.style.fontWeight = "bold";
item.appendChild(newSpan);
}
if(renderRun){
if(renderIndex < blockItems.length){
setTimeout(function(){
renderHtml(blockItems);
},3000);
}else{
console.log("渲染完成!")
//插入 indexedDB
const db = await openDatabase();
await addData(db, { id: pageKey, data: transferDatas, length: transferDatas.length});
}
}else{
console.log("渲染停止...")
}
}
//翻译前 words参数构造
function arrayToString(batchTransferWords){
console.log("batchTransferWords ->",batchTransferWords)
batchTransferWords.forEach(item =>{
let words = item.join("\n");
//console.log(words)
setTimeout(function(){
transferCN(words);
},5000);
})
}
//百度翻译 英译中
function transferCN(words){
let params = fanyiAPI + "?q="+encodeURI(words)+"&from=en&to=zh&appid="+appid+"&salt="+timestamp+"&sign="+getBaiduFanyiSign(words)+"&needIntervene="+1;
GM_xmlhttpRequest({
method: 'GET', // 请求方法
url: params, // 第三方接口地址
headers: {
'Content-Type': 'application/json' // 可选:设置请求类型
},
onload: function (response) {
let transferData = JSON.parse(response.responseText);
transferDatas.splice(transferDatas.length, 0, ...transferData.trans_result);
},
onerror: function (error) {
console.error('Error:', error); // 处理错误
}
});
}
//获取百度翻译sign
function getBaiduFanyiSign(words){
console.log(words);
let signStr = appid + words + timestamp + key;
let md5sign = CryptoJS.MD5(signStr).toString();
console.log("sign =", md5sign);
return md5sign;
}
})();