翻译机

该脚本用于翻译各类常用社交网站为中文,不会经过中间服务器。

  1. // ==UserScript==
  2. // @name 翻译机
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.99
  5. // @description 该脚本用于翻译各类常用社交网站为中文,不会经过中间服务器。
  6. // @author HolynnChen
  7. // @match *://*.twitter.com/*
  8. // @match *://*.x.com/*
  9. // @match *://*.youtube.com/*
  10. // @match *://*.facebook.com/*
  11. // @match *://*.reddit.com/*
  12. // @match *://*.5ch.net/*
  13. // @match *://*.discord.com/*
  14. // @match *://*.telegram.org/*
  15. // @match *://*.quora.com/*
  16. // @match *://*.tiktok.com/*
  17. // @match *://*.instagram.com/*
  18. // @match *://*.threads.net/*
  19. // @match *://*.github.com/*
  20. // @match *://*.bsky.app/*
  21. // @connect fanyi.baidu.com
  22. // @connect translate.google.com
  23. // @connect ifanyi.iciba.com
  24. // @connect www.bing.com
  25. // @connect fanyi.youdao.com
  26. // @connect dict.youdao.com
  27. // @connect m.youdao.com
  28. // @connect api.interpreter.caiyunai.com
  29. // @connect papago.naver.com
  30. // @connect fanyi.qq.com
  31. // @connect translate.alibaba.com
  32. // @connect www2.deepl.com
  33. // @connect transmart.qq.com
  34. // @grant GM_xmlhttpRequest
  35. // @grant GM_setValue
  36. // @grant GM_getValue
  37. // @grant GM_deleteValue
  38. // @grant GM_registerMenuCommand
  39. // @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
  40. // @require https://cdn.jsdelivr.net/npm/js-base64@3.7.4/base64.min.js
  41. // @require https://cdn.jsdelivr.net/npm/lz-string@1.5.0/libs/lz-string.min.js
  42. // @require https://cdn.jsdelivr.net/gh/Tampermonkey/utils@3b32b826e84ccc99a0a3e3d8d6e5ce0fa9834f23/requires/gh_2215_make_GM_xhr_more_parallel_again.js
  43. // @run-at document-body
  44. // ==/UserScript==
  45.  
  46.  
  47. GM_registerMenuCommand('重置控制面板位置(刷新应用)', () => {
  48. GM_setValue('position_top', '9px');
  49. GM_setValue('position_right', '9px');
  50. })
  51.  
  52. GM_registerMenuCommand('全局隐藏/展示悬浮球(刷新应用)', () => {
  53. GM_setValue('show_translate_ball', !GM_getValue('show_translate_ball',true));
  54. })
  55.  
  56.  
  57. const transdict = {
  58. '谷歌翻译': translate_gg,
  59. '谷歌翻译mobile': translate_ggm,
  60. '腾讯翻译': translate_tencent,
  61. '腾讯AI翻译': translate_tencentai,
  62. //'有道翻译':translate_youdao,
  63. '有道翻译mobile': translate_youdao_mobile,
  64. '百度翻译': translate_baidu,
  65. '彩云小译': translate_caiyun,
  66. '必应翻译': translate_biying,
  67. 'Papago翻译': translate_papago,
  68. '阿里翻译': translate_alibaba,
  69. '爱词霸翻译': translate_icib,
  70. 'Deepl翻译': translate_deepl,
  71. '关闭翻译': () => { }
  72. };
  73. const startup = {
  74. //'有道翻译':translate_youdao_startup,
  75. '腾讯翻译': translate_tencent_startup,
  76. '彩云小译': translate_caiyun_startup,
  77. 'Papago翻译': translate_papago_startup
  78. };
  79. const baseoptions = {
  80. 'enable_pass_lang': {
  81. declare: '不翻译中文(简体)',
  82. default_value: true,
  83. change_func: self => {
  84. if (self.checked) sessionStorage.clear()
  85. }
  86. },
  87. 'enable_pass_lang_cht': {
  88. declare: '不翻译中文(繁体)',
  89. default_value: true,
  90. change_func: self => {
  91. if (self.checked) sessionStorage.clear()
  92. }
  93. },
  94. 'remove_url': {
  95. declare: '自动过滤url',
  96. default_value: true,
  97. },
  98. 'show_info': {
  99. declare: '显示翻译源',
  100. default_value: true,
  101. option_enable: true
  102. },
  103. 'fullscrenn_hidden': {
  104. declare: '全屏时不显示',
  105. default_value: true,
  106. },
  107. 'replace_translate': {
  108. declare: '替换式翻译',
  109. default_value: false,
  110. option_enable: true
  111. },
  112. 'compress_storage':{
  113. declare: '压缩缓存',
  114. default_value: false,
  115. }
  116. };
  117.  
  118. const [enable_pass_lang, enable_pass_lang_cht, remove_url, show_info, fullscrenn_hidden, replace_translate, compress_storage] = Object.keys(baseoptions).map(key => GM_getValue(key, baseoptions[key].default_value));
  119.  
  120. const globalProcessingSave = [];
  121.  
  122. const sessionStorage = compress_storage?CompressMergeSession(window.sessionStorage):window.sessionStorage;
  123.  
  124. const p = window.trustedTypes !== undefined ? window.trustedTypes.createPolicy('translator', { createHTML: (string, sink) => string }) : { createHTML: (string, sink) => string };
  125.  
  126. function initPanel() {
  127. let choice = GM_getValue('translate_choice', '谷歌翻译');
  128. let select = document.createElement("select");
  129. select.className = 'js_translate';
  130. select.style = 'height:35px;width:100px;background-color:#fff;border-radius:17.5px;text-align-last:center;color:#000000;margin:5px 0';
  131. select.onchange = () => {
  132. GM_setValue('translate_choice', select.value);
  133. title.innerText = "控制面板(请刷新以应用)"
  134. };
  135. for (let i in transdict) select.innerHTML = p.createHTML(select.innerHTML+'<option value="' + i + '">' + i + '</option>');
  136. //
  137. let enable_details = document.createElement('details');
  138. enable_details.innerHTML = p.createHTML(enable_details.innerHTML+"<summary>启用规则</summary>");
  139. for (let i of rules) {
  140. let temp = document.createElement('input');
  141. temp.type = 'checkbox';
  142. temp.name = i.name;
  143. if (GM_getValue("enable_rule:" + temp.name, true)) temp.setAttribute('checked', true)
  144. enable_details.appendChild(temp);
  145. enable_details.innerHTML = p.createHTML(enable_details.innerHTML+"<span>" + i.name + "</span><br>");
  146. }
  147. let current_details = document.createElement('details');
  148. let mask = document.createElement('div'), dialog = document.createElement("div"), js_dialog = document.createElement("div"), title = document.createElement('p');
  149. //
  150. let shadowRoot = document.createElement('div');
  151. shadowRoot.style = "position: absolute;visibility: hidden;";
  152. window.top.document.body.appendChild(shadowRoot);
  153. let shadow = shadowRoot.attachShadow({ mode: "closed" })
  154. shadow.appendChild(mask);
  155. //window.top.document.body.appendChild(shadow);
  156. dialog.appendChild(js_dialog);
  157. mask.appendChild(dialog);
  158. js_dialog.appendChild(title)
  159. js_dialog.appendChild(document.createElement('p').appendChild(select));
  160. js_dialog.appendChild(document.createElement('p').appendChild(enable_details));
  161. js_dialog.appendChild(document.createElement('p').appendChild(current_details));
  162. //
  163. mask.style = "display: none;position: fixed;height: 100vh;width: 100vw;z-index: 99999;top: 0;left: 0;overflow: hidden;background-color: rgba(0,0,0,0.4);justify-content: center;align-items: center;visibility: visible;"
  164. mask.addEventListener('click', event => { if (event.target === mask) mask.style.display = 'none' });
  165. dialog.style = 'padding:0;border-radius:10px;background-color: #fff;box-shadow: 0 0 5px 4px rgba(0,0,0,0.3);';
  166. js_dialog.style = "min-height:10vh;min-width:10vw;display:flex;flex-direction:column;align-items:center;padding:10px;border-radius:4px;color:#000";
  167. title.style = 'margin:5px 0;font-size:20px;';
  168. title.innerText = "控制面板";
  169. for (let i in baseoptions) {
  170. let temp = document.createElement('input'), temp_p = document.createElement('p');
  171. js_dialog.appendChild(temp_p);
  172. temp_p.appendChild(temp);
  173. temp.type = 'checkbox';
  174. temp.name = i;
  175. temp_p.style = "display:flex;align-items: center;margin:5px 0"
  176. temp_p.innerHTML = p.createHTML(temp_p.innerHTML+baseoptions[i].declare);
  177. }
  178. for (let i of js_dialog.querySelectorAll('input')) {
  179. if (i.name && baseoptions[i.name]) {
  180. i.onclick = _ => { title.innerText = "控制面板(请刷新以应用)"; GM_setValue(i.name, i.checked); if (baseoptions[i.name].change_func) baseoptions[i.name].change_func(i) }
  181. i.checked = GM_getValue(i.name, baseoptions[i.name].default_value)
  182. }
  183. };
  184. for (let i of enable_details.querySelectorAll('input')) i.onclick = _ => { title.innerText = "控制面板(请刷新以应用)"; GM_setValue('enable_rule:' + i.name, i.checked) }
  185. let open = document.createElement('div');
  186. open.style = `z-index:9999;height:35px;width:35px;background-color:#fff;position:fixed;border:1px solid rgba(0,0,0,0.2);border-radius:17.5px;right:${GM_getValue('position_right', '9px')};top:${GM_getValue('position_top', '9px')};text-align-last:center;color:#000000;display:flex;align-items:center;justify-content:center;cursor: pointer;font-size:15px;user-select:none;visibility: visible;`;
  187. open.innerHTML = p.createHTML("译");
  188. const renderCurrentRule = () => {
  189. // 触发启用规则重建
  190. current_details.style.display = "none";
  191. current_details.innerHTML = p.createHTML('');
  192. const currentRule = GetActiveRule();
  193. if (currentRule) {
  194. current_details.style.display = "flex";
  195. current_details.innerHTML = p.createHTML(`<summary>当前启用-${currentRule.name}</summary>`)
  196. for (const option of currentRule.options) {
  197. const fieldset = document.createElement("fieldset")
  198. fieldset.innerHTML = p.createHTML(fieldset.innerHTML+`<legend>${option.name}</legend>`)
  199. current_details.appendChild(fieldset);
  200. fieldset.innerHTML = p.createHTML(fieldset.innerHTML+`<div style="display:flex;align-items:center"><span>启用翻译</span><input type="checkbox"></input></div>`)
  201. for (const key in baseoptions) {
  202. if (!baseoptions[key].option_enable) {
  203. continue;
  204. }
  205. fieldset.innerHTML = p.createHTML(fieldset.innerHTML+`<span>${baseoptions[key].declare}</span><br>`)
  206. const baseValueList = [["", "默认"], ["true", "启用"], ["false", "禁用"]]
  207. fieldset.innerHTML = p.createHTML(fieldset.innerHTML+"<div>" + baseValueList.map(value => `<input type="radio" value="${value[0]}" name="${key}:${currentRule.name}-${option.name}">${value[1]}</input>`).join('') + "</div>")
  208. }
  209. // 补充值与事件
  210. const enableInput = fieldset.querySelector('input[type=checkbox]')
  211. const enableKey = `enable_option:${currentRule.name}-${option.name}`
  212. enableInput.checked = GM_getValue(enableKey, true);
  213. enableInput.onchange = () => {
  214. title.innerText = "控制面板(请刷新以应用)";
  215. GM_setValue(enableKey, enableInput.checked);
  216. }
  217. const optionInputs = fieldset.querySelectorAll("input[type=radio]")
  218. for (const input of optionInputs) {
  219. const key = `option_setting:${input.name}`
  220. if (GM_getValue(key, '').toString() === input.value) {
  221. input.checked = true;
  222. }
  223. input.onchange = () => {
  224. title.innerText = "控制面板(请刷新以应用)";
  225. switch (input.value) {
  226. case 'true':
  227. GM_setValue(key, true);
  228. break;
  229. case 'false':
  230. GM_setValue(key, false);
  231. break;
  232. case '':
  233. GM_deleteValue(key);
  234. break
  235. }
  236. }
  237. }
  238. }
  239. }
  240. }
  241. open.onclick = () => {
  242. renderCurrentRule();
  243. mask.style.display = 'flex';
  244. };
  245. open.draggable = true;
  246. open.addEventListener("dragstart", function (ev) { ev.stopImmediatePropagation(); this.tempNode = document.createElement('div'); this.tempNode.style = "width:1px;height:1px;opacity:0"; document.body.appendChild(this.tempNode); ev.dataTransfer.setDragImage(this.tempNode, 0, 0); this.oldX = ev.offsetX - Number(this.style.width.replace('px', '')); this.oldY = ev.offsetY });
  247. open.addEventListener("drag", function (ev) { ev.stopImmediatePropagation(); if (!ev.x && !ev.y) return; this.style.right = Math.max(window.innerWidth - ev.x + this.oldX, 0) + "px"; this.style.top = Math.max(ev.y - this.oldY, 0) + "px" });
  248. open.addEventListener("dragend", function (ev) { ev.stopImmediatePropagation(); GM_setValue("position_right", this.style.right); GM_setValue("position_top", this.style.top); document.body.removeChild(this.tempNode) });
  249. open.addEventListener("touchstart", ev => { ev.stopImmediatePropagation(); ev.preventDefault(); ev = ev.touches[0]; open._tempTouch = {}; const base = open.getClientRects()[0]; open._tempTouch.oldX = base.x + base.width - ev.clientX; open._tempTouch.oldY = base.y - ev.clientY });
  250. open.addEventListener("touchmove", ev => { ev.stopImmediatePropagation(); ev = ev.touches[0]; open.style.right = Math.max(window.innerWidth - open._tempTouch.oldX - ev.clientX, 0) + 'px'; open.style.top = Math.max(ev.clientY + open._tempTouch.oldY, 0) + 'px'; open._tempIsMove = true });
  251. open.addEventListener("touchend", ev => { ev.stopImmediatePropagation(); GM_setValue("position_right", open.style.right); GM_setValue("position_top", open.style.top); if (!open._tempIsMove) { renderCurrentRule(); mask.style.display = 'flex' }; open._tempIsMove = false })
  252. shadow.appendChild(open);
  253. shadow.querySelector('.js_translate option[value=' + choice + ']').selected = true;
  254. if (fullscrenn_hidden) window.top.document.addEventListener('fullscreenchange', () => { open.style.display = window.top.document.fullscreenElement ? "none" : "flex" });
  255. }
  256.  
  257. const rules = [
  258. {
  259. name: '推特通用',
  260. matcher: /https:\/\/([a-zA-Z.]*?\.|)twitter\.com/,
  261. options: [
  262. {
  263. name: "推文",
  264. selector: baseSelector('div[dir="auto"][lang]'),
  265. textGetter: baseTextGetter,
  266. textSetter: options => {
  267. options.element.style = options.element.style.cssText.replace(/-webkit-line-clamp.*?;/, '')
  268. baseTextSetter(options).style.display = 'flex';
  269. }
  270. },
  271. {
  272. name: "背景信息",
  273. selector: baseSelector('div[data-testid=birdwatch-pivot]>div[dir=ltr]'),
  274. textGetter: baseTextGetter,
  275. textSetter: options => {
  276. options.element.style = options.element.style.cssText.replace(/-webkit-line-clamp.*?;/, '')
  277. baseTextSetter(options).style.display = 'flex';
  278. }
  279. }
  280. ]
  281. },
  282. {
  283. name: 'x通用',
  284. matcher: /https:\/\/([a-zA-Z.]*?.|)x\.com/,
  285. options: [
  286. {
  287. name: "推文",
  288. selector: baseSelector('div[dir="auto"][lang]'),
  289. textGetter: baseTextGetter,
  290. textSetter: options => {
  291. options.element.style = options.element.style.cssText.replace(/-webkit-line-clamp.*?;/, '')
  292. baseTextSetter(options).style.display = 'flex';
  293. }
  294. },
  295. {
  296. name: "背景信息",
  297. selector: baseSelector('div[data-testid=birdwatch-pivot]>div[dir=ltr]'),
  298. textGetter: baseTextGetter,
  299. textSetter: options => {
  300. options.element.style = options.element.style.cssText.replace(/-webkit-line-clamp.*?;/, '')
  301. baseTextSetter(options).style.display = 'flex';
  302. }
  303. }
  304. ]
  305. },
  306. {
  307. name: 'youtube pc通用',
  308. matcher: /https:\/\/www.youtube.com\/(watch|shorts|results\?)/,
  309. options: [
  310. {
  311. name: "评论区",
  312. selector: baseSelector("#content>#content-text"),
  313. textGetter: baseTextGetter,
  314. textSetter: options => {
  315. baseTextSetter(options);
  316. options.element.parentNode.parentNode.removeAttribute('collapsed');
  317. }
  318. },
  319. {
  320. name: "视频简介",
  321. selector: baseSelector("#content>#description>.content,.ytd-text-inline-expander>.yt-core-attributed-string"),
  322. textGetter: baseTextGetter,
  323. textSetter: options => {
  324. baseTextSetter(options);
  325. options.element.parentNode.parentNode.removeAttribute('collapsed');
  326. }
  327. },
  328. {
  329. name: "CC字幕",
  330. selector: baseSelector(".ytp-caption-segment"),
  331. textGetter: baseTextGetter,
  332. textSetter: baseTextSetter
  333. }
  334. ]
  335. },
  336. {
  337. name: 'youtube mobile通用',
  338. matcher: /https:\/\/m.youtube.com\/watch/,
  339. options: [
  340. {
  341. name: "评论区",
  342. selector: baseSelector(".comment-text.user-text"),
  343. textGetter: baseTextGetter,
  344. textSetter: baseTextSetter,
  345. },
  346. {
  347. name: "视频简介",
  348. selector: baseSelector(".slim-video-metadata-description"),
  349. textGetter: baseTextGetter,
  350. textSetter: baseTextSetter,
  351. }
  352. ]
  353. },
  354. {
  355. name: 'youtube 短视频',
  356. matcher: /https:\/\/(www|m).youtube.com\/shorts/,
  357. options: [
  358. {
  359. name: "评论区",
  360. selector: baseSelector("#comment-content #content-text,.comment-content .comment-text"),
  361. textGetter: baseTextGetter,
  362. textSetter: baseTextSetter,
  363. }
  364. ]
  365. },
  366. {
  367. name: 'youtube 社区',
  368. matcher: /https:\/\/(www|m).youtube.com\/(.*?\/community|post)/,
  369. options: [
  370. {
  371. name: "评论区",
  372. selector: baseSelector("#post #content #content-text,#comment #content #content-text,#replies #content #content-text"),
  373. textGetter: baseTextGetter,
  374. textSetter: options => {
  375. baseTextSetter(options);
  376. options.element.parentNode.parentNode.removeAttribute('collapsed');
  377. }
  378. }
  379. ]
  380. },
  381. {
  382. name: 'facebook通用',
  383. matcher: /https:\/\/www.facebook.com\/.+/,
  384. options: [
  385. {
  386. name: "帖子内容",
  387. selector: baseSelector("div[data-ad-comet-preview=message],div[role=article] div[id]"),
  388. textGetter: baseTextGetter,
  389. textSetter: options => setTimeout(baseTextSetter, 0, options),
  390. },
  391. {
  392. name: "评论区",
  393. selector: baseSelector("div[role=article] div>span[dir=auto][lang]"),
  394. textGetter: baseTextGetter,
  395. textSetter: options => setTimeout(baseTextSetter, 0, options),
  396. }
  397. ]
  398. },
  399. {
  400. name: 'reddit通用',
  401. matcher: /https:\/\/www.reddit.com\/.*/,
  402. options: [
  403. {
  404. name: '帖子标题',
  405. selector: baseSelector("*[slot=title][id|=post-title]"),
  406. textGetter: baseTextGetter,
  407. textSetter: baseTextSetter,
  408. },
  409. {
  410. name: '帖子内容',
  411. selector: baseSelector("div[slot=text-body]>div>div[id*=-post-rtjson-content]"),
  412. textGetter: baseTextGetter,
  413. textSetter: baseTextSetter,
  414. },
  415. {
  416. name: '评论区',
  417. selector: baseSelector("div[slot=comment]>div[id$=-post-rtjson-content]"),
  418. textGetter: baseTextGetter,
  419. textSetter: baseTextSetter,
  420. }
  421. // a[data-click-id=body]:not([class=undefined]),.RichTextJSON-root
  422. ]
  423. },
  424. {
  425. name: '5ch评论',
  426. matcher: /http(|s):\/\/(.*?\.|)5ch.net\/.*/,
  427. options: [
  428. {
  429. name: "标题",
  430. selector: baseSelector('.post>.post-content,#threadtitle,.thread_title'),
  431. textGetter: baseTextGetter,
  432. textSetter: baseTextSetter,
  433. },
  434. {
  435. name: "内容",
  436. selector: baseSelector('.threadview_response_body'),
  437. textGetter: baseTextGetter,
  438. textSetter: baseTextSetter,
  439. }
  440. ]
  441. },
  442. {
  443. name: 'discord聊天',
  444. matcher: /https:\/\/discord.com\/.+/,
  445. options: [
  446. {
  447. name: "聊天内容",
  448. selector: baseSelector('div[class*=messageContent]'),
  449. textGetter: baseTextGetter,
  450. textSetter: baseTextSetter,
  451. }
  452. ]
  453. },
  454. {
  455. name: 'telegram聊天新',
  456. matcher: /https:\/\/.*?.telegram.org\/(a|z)\//,
  457. options: [
  458. {
  459. name: "聊天内容",
  460. selector: baseSelector('p.text-content[dir=auto],div.text-content'),
  461. textGetter: e => Array.from(e.childNodes).filter(item => !item.className).map(item => item.nodeName === "BR" ? "\n" : item.textContent).join(''),
  462. textSetter: baseTextSetter,
  463. }
  464. ]
  465. },
  466. {
  467. name: 'telegram聊天',
  468. matcher: /https:\/\/.*?.telegram.org\/.+/,
  469. options: [
  470. {
  471. name: "聊天内容",
  472. selector: baseSelector('div.message[dir=auto],div.im_message_text'),
  473. textGetter: e => Array.from(e.childNodes).filter(item => !item.className || item.className === 'translatable-message').map(item => item.nodeValue || item.innerText).join(" "),
  474. textSetter: baseTextSetter,
  475. }
  476. ]
  477. },
  478. {
  479. name: 'quora通用',
  480. matcher: /https:\/\/www.quora.com/,
  481. options: [
  482. {
  483. name: "标题",
  484. selector: baseSelector(".puppeteer_test_question_title>span>span"),
  485. textGetter: baseTextGetter,
  486. textSetter: options => {
  487. options.element.parentNode.parentNode.style = options.element.parentNode.parentNode.style.cssText.replace(/-webkit-line-clamp.*?;/, '')
  488. baseTextSetter(options).style.display = 'flex';
  489. },
  490. },
  491. {
  492. name: "帖子内容",
  493. selector: baseSelector('div.q-text>span>span.q-box:has(p.q-text),div.q-box>div.q-box>div.q-text>span.q-box:has(p.q-text)'),
  494. textGetter: baseTextGetter,
  495. textSetter: baseTextSetter,
  496. }
  497. ]
  498. },
  499. {
  500. name: 'tiktok评论',
  501. matcher: /https:\/\/www.tiktok.com/,
  502. options: [
  503. {
  504. name: "评论区",
  505. selector: baseSelector('p[data-e2e|=comment-level]'),
  506. textGetter: baseTextGetter,
  507. textSetter: baseTextSetter,
  508. }
  509. ]
  510. },
  511. {
  512. name: 'instagram评论',
  513. matcher: /https:\/\/www.instagram.com/,
  514. options: [
  515. {
  516. name: "评论区",
  517. selector: baseSelector('li>div>div>div>div>span[dir=auto]'),
  518. textGetter: baseTextGetter,
  519. textSetter: baseTextSetter,
  520. }
  521. ]
  522. },
  523. {
  524. name: 'threads',
  525. matcher: /https:\/\/www.threads.net/,
  526. options: [
  527. {
  528. name: "帖子",
  529. selector: baseSelector('div[data-pressable-container=true][data-interactive-id]>div>div:last-child>div>div:has(span[dir=auto]):not(:has(div[role=button]))'),
  530. textGetter: baseTextGetter,
  531. textSetter: baseTextSetter,
  532. }
  533. ]
  534. },
  535. {
  536. name: 'github',
  537. matcher: /https:\/\/github.com\/.+\/.+\/\w+\/\d+/,
  538. options: [
  539. {
  540. name: "issues",
  541. selector: baseSelector(".edit-comment-hide > task-lists > table > tbody > tr > td > p",items=>items.filter(i=>{
  542. const nodeNameList = [...new Set([...i.childNodes].map(i=>i.nodeName))];
  543. return nodeNameList.length>1 || (nodeNameList.length == 1 && nodeNameList[0] == "#text")
  544. })),
  545. textGetter: baseTextGetter,
  546. textSetter: baseTextSetter,
  547. },
  548. {
  549. name: "discussions",
  550. selector: baseSelector(".edit-comment-hide > task-lists > table > tbody > tr > td > p",items=>items.filter(i=>{
  551. const nodeNameList=[...new Set([...i.childNodes].map(i=>i.nodeName))];
  552. return nodeNameList.length>1 || (nodeNameList.length == 1 && nodeNameList[0] == "#text")
  553. })),
  554. textGetter: baseTextGetter,
  555. textSetter: baseTextSetter,
  556. },
  557. ]
  558. },
  559. {
  560. name: 'bsky',
  561. matcher: /https:\/\/bsky.app/,
  562. options: [
  563. {
  564. name: "主页帖子",
  565. selector: baseSelector('div[dir=auto][data-testid=postText]'),
  566. textGetter: baseTextGetter,
  567. textSetter: baseTextSetter,
  568. },
  569. {
  570. name: "内容帖子与回复",
  571. selector: baseSelector('div[data-testid^="postThreadItem-by"] div[dir=auto][data-word-wrap]'),
  572. textGetter: baseTextGetter,
  573. textSetter: baseTextSetter,
  574. }
  575. ]
  576. },
  577. ];
  578.  
  579. const GetActiveRule = () => rules.find(item => item.matcher.test(document.location.href) && GM_getValue('enable_rule:' + item.name, true));
  580.  
  581.  
  582. (function () {
  583. 'use strict';
  584. let url = document.location.href;
  585. let rule = GetActiveRule();
  586. setInterval(() => {
  587. if (document.location.href != url) {
  588. url = document.location.href;
  589. const ruleNew = GetActiveRule();
  590. if (ruleNew != rule) {
  591. if (ruleNew != null) {
  592. console.log(`【翻译机】检测到URl变更,改为使用【${ruleNew.name}】规则`)
  593. } else {
  594. console.log("【翻译机】检测到URl变更,当前无匹配规则")
  595. }
  596. rule = ruleNew;
  597. }
  598. }
  599. }, 200)
  600. console.log(rule ? `【翻译机】使用【${rule.name}】规则` : "【翻译机】当前无匹配规则");
  601. console.log(document.location.href)
  602. let main = _ => {
  603. if (!rule) return;
  604. const choice = GM_getValue('translate_choice', '谷歌翻译');
  605. for (const option of rule.options) {
  606. if (!GM_getValue("enable_option:" + rule.name + "-" + option.name, true)) {
  607. continue
  608. }
  609. const temp = [...new Set(option.selector())];
  610. for (let i = 0; i < temp.length; i++) {
  611. const now = temp[i];
  612. if (globalProcessingSave.includes(now)) continue;
  613. globalProcessingSave.push(now);
  614. const rawText = option.textGetter(now);
  615. const text = remove_url ? url_filter(rawText) : rawText;
  616. if (text.length == 0) continue;
  617. const setterParams = {
  618. element: now,
  619. translatorName: choice,
  620. rawText: rawText,
  621. rule: rule,
  622. option: option
  623. }
  624. if (sessionStorage.getItem(choice + '-' + text)) {
  625. setterParams.text = sessionStorage.getItem(choice + '-' + text)
  626. option.textSetter(setterParams);
  627. removeItem(globalProcessingSave, now)
  628. } else {
  629. pass_lang(text).then(lang => transdict[choice](text, lang)).then(s => {
  630. setterParams.text = s
  631. option.textSetter(setterParams);
  632. removeItem(globalProcessingSave, now);
  633. })
  634. }
  635. }
  636. }
  637. };
  638. PromiseRetryWrap(startup[GM_getValue('translate_choice', '谷歌翻译')]).then(() => { document.js_translater = setInterval(main, 20) });
  639. if(!GM_getValue('show_translate_ball',true))return;
  640. initPanel();
  641. })();
  642.  
  643. //--综合工具区--start
  644.  
  645. function removeItem(arr, item) {
  646. const index = arr.indexOf(item);
  647. if (index > -1) arr.splice(index, 1);
  648. }
  649.  
  650. function baseSelector(selector,customFilter) {
  651. return () => {
  652. const items = document.querySelectorAll(selector);
  653. let filterResult = Array.from(items).filter(item => {
  654. const nodes = item.querySelectorAll('[data-translate]');
  655. return !item.dataset.translate && !(nodes && Array.from(nodes).some(node => node.parentNode === item));
  656. })
  657. if(customFilter){
  658. filterResult = customFilter(filterResult);
  659. }
  660. filterResult.map(item => item.dataset.translate = "processed");
  661. return filterResult;
  662. }
  663. }
  664.  
  665. function baseTextGetter(e) {
  666. return e.innerText;
  667. }
  668.  
  669. function baseTextSetter({ element, translatorName, text, rawText, rule, option }) {//change element text
  670. if ((text || "").length == 0) text = '翻译异常';
  671. const currentReplaceTranslate = GM_getValue("option_setting:replace_translate:" + rule.name + "-" + option.name, replace_translate)
  672. const currentShowInfo = GM_getValue("option_setting:show_info:" + rule.name + "-" + option.name, show_info)
  673. if (currentReplaceTranslate) {
  674. const spanNode = document.createElement('span');
  675. spanNode.style.whiteSpace = "pre-wrap";
  676. spanNode.innerText = `${currentShowInfo ? "-----------" + translatorName + "-----------\n\n" : ""}` + text;
  677. spanNode.dataset.translate = "processed";
  678. spanNode.title = rawText;
  679. spanNode.class = "translate-processed-node"
  680. element.innerHTML = p.createHTML('');
  681. element.appendChild(spanNode);
  682. element.style.cssText += "-webkit-line-clamp: unset;max-height: unset";
  683. return spanNode;
  684. } else {
  685. const spanNode = document.createElement('span');
  686. spanNode.style.whiteSpace = "pre-wrap";
  687. spanNode.innerText = `\n\n${currentShowInfo ? "-----------" + translatorName + "-----------\n\n" : ""}` + text;
  688. spanNode.dataset.translate = "processed";
  689. spanNode.class = "translate-processed-node"
  690. element.appendChild(spanNode);
  691. element.style.cssText += "-webkit-line-clamp: unset;max-height: unset";
  692. return spanNode;
  693. }
  694.  
  695. }
  696.  
  697. function url_filter(text) {
  698. return text.replace(/(https?|ftp|file):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/g, '');
  699. }
  700.  
  701. async function pass_lang(raw) {//确认是否为中文,是则中断promise
  702. if (!enable_pass_lang && !enable_pass_lang_cht) return;
  703. try {
  704. const result = await check_lang(raw)
  705. if (enable_pass_lang && result == 'zh') return new Promise(() => { });
  706. if (enable_pass_lang_cht && (result == 'cht'|| result == 'zh-tw')) return new Promise(() => { });
  707. return result
  708. } catch (err) {
  709. console.log(err);
  710. return
  711. }
  712. return
  713. }
  714.  
  715. async function check_lang(raw) {
  716. let t = Date.now();
  717. const options = {
  718. method: "POST",
  719. url: 'https://fanyi.baidu.com/langdetect',
  720. data: 'query=' + encodeURIComponent(raw.replace(/[\uD800-\uDBFF]$/, "").slice(0, 50)),
  721. headers: {
  722. "Content-Type": "application/x-www-form-urlencoded",
  723. }
  724. }
  725. const res = await Request(options);
  726. //console.log(`语言加载完毕,耗时${Date.now()-t}ms`)
  727. try {
  728. return JSON.parse(res.responseText).lan
  729. } catch (err) {
  730. console.log(err);
  731. return
  732. }
  733. // let t = Date.now();
  734. //const options = {
  735. // method: "GET",
  736. // url: 'https://translate.alibaba.com/trans/GetDetectLanguage.do?srcData='+encodeURIComponent(raw.replace(/[\uD800-\uDBFF]$/, "").slice(0, 50)),
  737. //}
  738. //const res = await Request(options);
  739. //console.log(options,res.responseText);
  740. //// console.log(`语言加载完毕,耗时${Date.now()-t}ms`)
  741. //try {
  742. // return JSON.parse(res.responseText).renognize
  743. //} catch (err) {
  744. // console.log(err);
  745. // return
  746. //}
  747. }
  748.  
  749.  
  750. function guid() {
  751. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
  752. let r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
  753. return v.toString(16);
  754. });
  755. }
  756.  
  757. //--综合工具区--end
  758.  
  759. //--谷歌翻译--start
  760. async function translate_gg(raw) {
  761. const options = {
  762. method: "GET",
  763. url: `https://translate.google.com/translate_a/t?client=gtx&sl=auto&tl=zh-CN&q=` + encodeURIComponent(raw),
  764. anonymous: true,
  765. nocache: true,
  766. }
  767. return await BaseTranslate('谷歌翻译', raw, options, res => JSON.parse(res)[0][0])
  768. }
  769.  
  770. //--谷歌翻译--end
  771.  
  772. //--谷歌翻译mobile--start
  773. async function translate_ggm(raw) {
  774. const options = {
  775. method: "GET",
  776. url: "https://translate.google.com/m?tl=zh-CN&q=" + encodeURIComponent(raw),
  777. headers: {
  778. "Host": "translate.google.com",
  779. },
  780. anonymous: true,
  781. nocache: true,
  782. }
  783. return await BaseTranslate('谷歌翻译mobile', raw, options, res => /class="result-container">((?:.|\n)*?)<\/div/.exec(res)[1])
  784. }
  785. //--谷歌翻译mobile--end
  786.  
  787. //--百度翻译--start
  788. async function translate_baidu(raw, lang) {
  789. if (!lang) {
  790. lang = await check_lang(raw)
  791. }
  792. const options = {
  793. method: "POST",
  794. url: 'https://fanyi.baidu.com/ait/text/translate',
  795. data: JSON.stringify({ query: raw, from: lang, to: "zh" }),
  796. headers: {
  797. "referer": 'https://fanyi.baidu.com',
  798. 'Content-Type': 'application/json',
  799. 'Origin': 'https://fanyi.baidu.com',
  800. 'accept': 'text/event-stream',
  801. },
  802. }
  803. return await BaseTranslate('百度翻译', raw, options, res => res.split('\n').filter(item => item.startsWith('data: ')).map(item => JSON.parse(item.slice(6))).find(item => item.data.list).data.list.map(item => item.dst).join('\n'))
  804. }
  805.  
  806. //--百度翻译--end
  807.  
  808. //--爱词霸翻译--start
  809.  
  810. async function translate_icib(raw) {
  811. const sign = CryptoJS.MD5("6key_web_fanyi" + "ifanyiweb8hc9s98e" + raw.replace(/(^\s*)|(\s*$)/g, "")).toString().substring(0, 16)
  812. const options = {
  813. method: "POST",
  814. url: `https://ifanyi.iciba.com/index.php?c=trans&m=fy&client=6&auth_user=key_web_fanyi&sign=${sign}`,
  815. data: 'from=auto&t=zh&q=' + encodeURIComponent(raw),
  816. headers: {
  817. "Content-Type": "application/x-www-form-urlencoded",
  818. },
  819. }
  820. return await BaseTranslate('爱词霸翻译', raw, options, res => JSON.parse(res).content.out)
  821. }
  822.  
  823. //--爱词霸翻译--end
  824.  
  825.  
  826. //--必应翻译--start
  827.  
  828. async function translate_biying(raw) {
  829. const options = {
  830. method: "POST",
  831. url: 'https://www.bing.com/ttranslatev3',
  832. data: 'fromLang=auto-detect&to=zh-Hans&text=' + encodeURIComponent(raw),
  833. headers: {
  834. "Content-Type": "application/x-www-form-urlencoded",
  835. },
  836. }
  837. return await BaseTranslate('必应翻译', raw, options, res => JSON.parse(res)[0].translations[0].text)
  838. }
  839.  
  840. //--必应翻译--end
  841.  
  842. //--有道翻译--start
  843. async function translate_youdao_startup() {
  844. if (sessionStorage.getItem('youdao_key')) return;
  845. const ts = "" + (new Date).getTime();
  846. const params = {
  847. keyid: "webfanyi-key-getter",
  848. client: "fanyideskweb",
  849. product: "webfanyi",
  850. appVersion: "1.0.0",
  851. vendor: "web",
  852. pointParam: "client,mysticTime,product",
  853. mysticTime: ts,
  854. keyfrom: "fanyi.web",
  855. sign: CryptoJS.MD5(`client=fanyideskweb&mysticTime=${ts}&product=webfanyi&key=asdjnjfenknafdfsdfsd`)
  856. }
  857. const options = {
  858. method: "GET",
  859. url: `https://dict.youdao.com/webtranslate/key?${Object.entries(params).map(item => item.join('=')).join('&')}`,
  860. headers: {
  861. "Origin": "https://fanyi.youdao.com"
  862. }
  863. }
  864. const res = await Request(options);
  865. sessionStorage.setItem('youdao_key', JSON.parse(res.responseText).data.secretKey)
  866. }
  867.  
  868. async function translate_youdao(raw) {
  869. const ts = "" + (new Date).getTime();
  870. const params = {
  871. i: encodeURIComponent(raw),
  872. from: 'auto',
  873. to: '',
  874. dictResult: 'true',
  875. keyid: "webfanyi",
  876. client: "fanyideskweb",
  877. product: "webfanyi",
  878. appVersion: "1.0.0",
  879. vendor: "web",
  880. pointParam: "client,mysticTime,product",
  881. mysticTime: ts,
  882. keyfrom: "fanyi.web",
  883. sign: CryptoJS.MD5(`client=fanyideskweb&mysticTime=${ts}&product=webfanyi&key=${sessionStorage.getItem('youdao_key')}`) + ''
  884. }
  885. const options = {
  886. method: "POST",
  887. url: 'https://dict.youdao.com/webtranslate',
  888. data: Object.entries(params).map(item => item.join('=')).join('&'),
  889. headers: {
  890. "Content-Type": "application/x-www-form-urlencoded",
  891. "Referer": "https://fanyi.youdao.com/",
  892. "Origin": "https://fanyi.youdao.com",
  893. "Host": "dict.youdao.com",
  894. "Cookie": "OUTFOX_SEARCH_USER_ID=0@0.0.0.0"
  895. },
  896. anonymous: true,
  897. }
  898. const res = await Request(options);
  899. const decrypted = A(res);
  900. return await BaseTranslate('有道翻译', raw, options, res => JSON.parse(A(res)).translateResult.map(e => e.map(t => t.tgt).join('')).join('\n'))
  901. }
  902.  
  903. function m(e) {
  904. return CryptoJS.MD5(e).toString(CryptoJS.enc.Hex);
  905. }
  906.  
  907. function A(t, o, n) {
  908. o = "ydsecret://query/key/BRGygVywfNBwpmBaZgWT7SIOUP2T0C9WHMZN39j^DAdaZhAnxvGcCY6VYFwnHl"
  909. n = "ydsecret://query/iv/C@lZe2YzHtZ2CYgaXKSVfsb7Y4QWHjITPPZ0nQp87fBeJ!Iv6v^6fvi2WN@bYpJ4"
  910. if (!t)
  911. return null;
  912. const a = CryptoJS.enc.Hex.parse(m(o)),
  913. r = CryptoJS.enc.Hex.parse(m(n)),
  914. i = CryptoJS.AES.decrypt(t, a, {
  915. iv: r
  916. });
  917. return i.toString(CryptoJS.enc.Utf8);
  918. }
  919.  
  920. //--有道翻译--end
  921.  
  922. //--有道翻译m--start
  923. async function translate_youdao_mobile(raw) {
  924. const options = {
  925. method: "POST",
  926. url: 'http://m.youdao.com/translate',
  927. data: "inputtext=" + encodeURIComponent(raw) + "&type=AUTO",
  928. anonymous: true,
  929. headers: {
  930. "Content-Type": "application/x-www-form-urlencoded"
  931. }
  932. }
  933. return await BaseTranslate('有道翻译mobile', raw, options, res => /id="translateResult">\s*?<li>([\s\S]*?)<\/li>\s*?<\/ul/.exec(res)[1])
  934. }
  935. //--有道翻译m--end
  936.  
  937. //--腾讯翻译--start
  938.  
  939. async function translate_tencent_startup() {
  940. setTimeout(translate_tencent_startup, 10000)//token刷新
  941. const base_options = {
  942. method: 'GET',
  943. url: 'http://fanyi.qq.com',
  944. anonymous: true,
  945. headers: {
  946. "User-Agent": "test",
  947. }
  948. }
  949. const base_res = await Request(base_options)
  950. const uri = /reauthuri = "(.*?)"/.exec(base_res.responseText)[1]
  951. const options = {
  952. method: 'POST',
  953. url: 'https://fanyi.qq.com/api/' + uri
  954. }
  955. const res = await Request(options);
  956. const data = JSON.parse(res.responseText);
  957. sessionStorage.setItem('tencent_qtv', data.qtv)
  958. sessionStorage.setItem('tencent_qtk', data.qtk)
  959. }
  960.  
  961.  
  962. async function translate_tencent(raw) {
  963. const qtk = sessionStorage.getItem('tencent_qtk'), qtv = sessionStorage.getItem('tencent_qtv');
  964. const options = {
  965. method: 'POST',
  966. url: 'https://fanyi.qq.com/api/translate',
  967. data: `source=auto&target=zh&sourceText=${encodeURIComponent(raw)}&qtv=${encodeURIComponent(qtv)}&qtk=${encodeURIComponent(qtk)}&sessionUuid=translate_uuid${Date.now()}`,
  968. headers: {
  969. "Host": "fanyi.qq.com",
  970. "Origin": "https://fanyi.qq.com",
  971. "Content-Type": "application/x-www-form-urlencoded",
  972. "Referer": "https://fanyi.qq.com/",
  973. "X-Requested-With": "XMLHttpRequest",
  974. }
  975. }
  976. return await BaseTranslate('腾讯翻译', raw, options, res => JSON.parse(res).translate.records.map(e => e.targetText).join(''))
  977. }
  978.  
  979. //--腾讯翻译--end
  980.  
  981. //--彩云翻译--start
  982.  
  983. async function translate_caiyun_startup() {
  984. if (sessionStorage.getItem('caiyun_id') && sessionStorage.getItem('caiyun_jwt')) return;
  985. const browser_id = CryptoJS.MD5(Math.random().toString()).toString();
  986. sessionStorage.setItem('caiyun_id', browser_id);
  987. const options = {
  988. method: "POST",
  989. url: 'https://api.interpreter.caiyunai.com/v1/user/jwt/generate',
  990. headers: {
  991. "Content-Type": "application/json",
  992. "X-Authorization": "token:qgemv4jr1y38jyq6vhvi",
  993. "Origin": "https://fanyi.caiyunapp.com",
  994. },
  995. data: JSON.stringify({ browser_id }),
  996. }
  997. const res = await Request(options);
  998. sessionStorage.setItem('caiyun_jwt', JSON.parse(res.responseText).jwt);
  999. }
  1000.  
  1001. async function translate_caiyun(raw) {
  1002. const source = "NOPQRSTUVWXYZABCDEFGHIJKLMnopqrstuvwxyzabcdefghijklm";
  1003. const dic = [..."ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"].reduce((dic, current, index) => { dic[current] = source[index]; return dic }, {});
  1004. const decoder = line => Base64.decode([...line].map(i => dic[i] || i).join(""))
  1005. const options = {
  1006. method: "POST",
  1007. url: 'https://api.interpreter.caiyunai.com/v1/translator',
  1008. data: JSON.stringify({
  1009. "source": raw.split('\n'),
  1010. "trans_type": "auto2zh",
  1011. "detect": true,
  1012. "browser_id": sessionStorage.getItem('caiyun_id')
  1013. }),
  1014. headers: {
  1015. "Content-Type": "application/json",
  1016. "X-Authorization": "token:qgemv4jr1y38jyq6vhvi",
  1017. "T-Authorization": sessionStorage.getItem('caiyun_jwt')
  1018. }
  1019. }
  1020. return await BaseTranslate('彩云小译', raw, options, res => JSON.parse(res).target.map(decoder).join('\n'))
  1021. }
  1022.  
  1023. //--彩云翻译--end
  1024.  
  1025. //--papago翻译--start
  1026.  
  1027. async function translate_papago_startup() {
  1028. if (sessionStorage.getItem('papago_key')) return;
  1029. const base_options = {
  1030. method: 'GET',
  1031. url: 'https://papago.naver.com/',
  1032. anonymous: true,
  1033. }
  1034. const base_res = await Request(base_options)
  1035. const uri = /"\/(vendors~home\..*?.chunk.js)"/.exec(base_res.responseText)[1]
  1036. const options = {
  1037. method: 'GET',
  1038. url: 'https://papago.naver.com/' + uri
  1039. }
  1040. const res = await Request(options);
  1041. const key = /AUTH_KEY:"(.*?)"/.exec(res.responseText)[1];
  1042. sessionStorage.setItem('papago_key', key);
  1043. }
  1044.  
  1045. async function translate_papago(raw) {
  1046. const time = Date.now();
  1047. const options = {
  1048. method: 'POST',
  1049. url: 'https://papago.naver.com/apis/n2mt/translate',
  1050. data: `deviceId=${time}&source=auto&target=zh-CN&text=${encodeURIComponent(raw)}`,
  1051. headers: {
  1052. "authorization": 'PPG ' + time + ':' + CryptoJS.HmacMD5(time + '\nhttps://papago.naver.com/apis/n2mt/translate\n' + time, sessionStorage.getItem('papago_key')).toString(CryptoJS.enc.Base64),
  1053. "x-apigw-partnerid": "papago",
  1054. "device-type": 'pc',
  1055. "timestamp": time,
  1056. "Content-Type": "application/x-www-form-urlencoded",
  1057. }
  1058. }
  1059. return await BaseTranslate('Papago', raw, options, res => JSON.parse(res).translatedText)
  1060. }
  1061.  
  1062. //--papago翻译--end
  1063.  
  1064. //--阿里翻译--start
  1065. async function translate_alibaba(raw) {
  1066. const options = {
  1067. method: 'POST',
  1068. url: 'https://translate.alibaba.com/translationopenseviceapp/trans/TranslateTextAddAlignment.do',
  1069. data: `srcLanguage=auto&tgtLanguage=zh&bizType=message&srcText=${encodeURIComponent(raw)}`,
  1070. headers: {
  1071. "Content-Type": "application/x-www-form-urlencoded",
  1072. "origin": "https://translate.alibaba.com",
  1073. "referer": "https://translate.alibaba.com/",
  1074. "sec-fetch-site": "same-origin",
  1075. }
  1076. }
  1077. return await BaseTranslate('阿里翻译', raw, options, res => JSON.parse(res).listTargetText[0])
  1078. }
  1079. //--阿里翻译--end
  1080.  
  1081. //--Deepl翻译--start
  1082.  
  1083. function getTimeStamp(iCount) {
  1084. const ts = Date.now();
  1085. if (iCount !== 0) {
  1086. iCount = iCount + 1;
  1087. return ts - (ts % iCount) + iCount;
  1088. } else {
  1089. return ts;
  1090. }
  1091. }
  1092.  
  1093. async function translate_deepl(raw) {
  1094. const id = (Math.floor(Math.random() * 99999) + 100000) * 1000;
  1095. const data = {
  1096. jsonrpc: '2.0',
  1097. method: 'LMT_handle_texts',
  1098. id,
  1099. params: {
  1100. splitting: 'newlines',
  1101. lang: {
  1102. source_lang_user_selected: 'auto',
  1103. target_lang: 'ZH',
  1104. },
  1105. texts: [{
  1106. text: raw,
  1107. requestAlternatives: 3
  1108. }],
  1109. timestamp: getTimeStamp(raw.split('i').length - 1)
  1110. }
  1111. }
  1112. let postData = JSON.stringify(data);
  1113. if ((id + 5) % 29 === 0 || (id + 3) % 13 === 0) {
  1114. postData = postData.replace('"method":"', '"method" : "');
  1115. } else {
  1116. postData = postData.replace('"method":"', '"method": "');
  1117. }
  1118. const options = {
  1119. method: 'POST',
  1120. url: 'https://www2.deepl.com/jsonrpc',
  1121. data: postData,
  1122. headers: {
  1123. 'Content-Type': 'application/json',
  1124. 'Host': 'www.deepl.com',
  1125. 'Origin': 'https://www.deepl.com',
  1126. 'Referer': 'https://www.deepl.com/'
  1127. },
  1128. anonymous: true,
  1129. nocache: true,
  1130. }
  1131. return await BaseTranslate('Deepl翻译', raw, options, res => JSON.parse(res).result.texts[0].text)
  1132. }
  1133.  
  1134. //--Deepl翻译--end
  1135.  
  1136. //--腾讯AI翻译--start
  1137. async function translate_tencentai(raw) {
  1138. const data = {
  1139. "header": {
  1140. "fn": "auto_translation",
  1141. "client_key": `browser-chrome-121.0.0-Windows_10-${guid()}-${Date.now()}`,
  1142. "session": "",
  1143. "user": ""
  1144. },
  1145. "type": "plain",
  1146. "model_category": "normal",
  1147. "text_domain": "",
  1148. "source": {
  1149. "lang": "auto",
  1150. "text_list": [raw]
  1151. },
  1152. "target": {
  1153. "lang": "zh"
  1154. }
  1155. }
  1156. const options = {
  1157. method: 'POST',
  1158. url: 'https://transmart.qq.com/api/imt',
  1159. data: JSON.stringify(data),
  1160. headers: {
  1161. 'Content-Type': 'application/json',
  1162. 'Host': 'transmart.qq.com',
  1163. 'Origin': 'https://transmart.qq.com',
  1164. 'Referer': 'https://transmart.qq.com/'
  1165. },
  1166. anonymous: true,
  1167. nocache: true,
  1168. }
  1169. return await BaseTranslate('腾讯AI翻译', raw, options, res => JSON.parse(res).auto_translation[0])
  1170. }
  1171. //--腾讯Ai翻译--end
  1172.  
  1173. //--异步请求包装工具--start
  1174.  
  1175. async function PromiseRetryWrap(task, options, ...values) {
  1176. const { RetryTimes, ErrProcesser } = options || {};
  1177. let retryTimes = RetryTimes || 5;
  1178. const usedErrProcesser = ErrProcesser || (err => { throw err });
  1179. if (!task) return;
  1180. while (true) {
  1181. try {
  1182. return await task(...values);
  1183. } catch (err) {
  1184. if (!--retryTimes) {
  1185. console.log(err);
  1186. return usedErrProcesser(err);
  1187. }
  1188. }
  1189. }
  1190. }
  1191.  
  1192. async function BaseTranslate(name, raw, options, processer) {
  1193. const toDo = async () => {
  1194. var tmp;
  1195. try {
  1196. const data = await Request(options);
  1197. tmp = data.responseText;
  1198. const result = await processer(tmp);
  1199. if (result){
  1200. setTimeout(()=>sessionStorage.setItem(name + '-' + raw, result),0);
  1201. }
  1202. return result
  1203. } catch (err) {
  1204. throw {
  1205. responseText: tmp,
  1206. err: err
  1207. }
  1208. }
  1209. }
  1210. return await PromiseRetryWrap(toDo, { RetryTimes: 3, ErrProcesser: () => "翻译出错" })
  1211. }
  1212.  
  1213. function Request(options) {
  1214. return new Promise((reslove, reject) => GM_xmlhttpRequest({ ...options, onload: reslove, onerror: reject }))
  1215. }
  1216.  
  1217. //--异步请求包装工具--end
  1218.  
  1219. //--压缩存储--start
  1220.  
  1221. function CompressMergeSession(rawSession,compress) {
  1222. const rawCache = rawSession.getItem('translate-merge-cache')
  1223. const cache = JSON.parse(rawCache?(LZString.decompress(rawCache)||'{}'):'{}');
  1224. window.addEventListener('storage',e=>{
  1225. if(e && e.key && e.key.startsWith('translate-merge-cache-sync')){
  1226. const key = e.newValue.slice(0,32);
  1227. const value = e.newValue.slice(32);
  1228. cache[key]=value;
  1229. }
  1230. })
  1231. return {
  1232. getItem: (key) => {
  1233. return cache[CryptoJS.MD5(key).toString()] || "";
  1234. },
  1235. setItem: (key, value) => {
  1236. const hashKey = CryptoJS.MD5(key).toString();
  1237. cache[hashKey]=value;
  1238. localStorage.setItem('translate-merge-cache-sync',hashKey+value);
  1239. const rawSaveValue = JSON.stringify(cache);
  1240. const saveValue = LZString.compress(rawSaveValue);
  1241. // console.log(`压缩前:${rawSaveValue.length},压缩后:${saveValue.length},压缩率:${saveValue.length/rawSaveValue.length}`)
  1242. rawSession.setItem('translate-merge-cache',saveValue)
  1243. }
  1244. }
  1245. }
  1246.  
  1247. //--压缩存储--end

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址