Translate webpage

Mimicks chrome's built-in page translator. Can be used as a userscript (from the context menu or always on) or as a bookmarklet (clicking the bookmark translates the page). For Firefox and chrome

当前为 2023-07-15 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Translate webpage
  3. // @namespace https://github.com/Procyon-b
  4. // @version 0.5
  5. // @description Mimicks chrome's built-in page translator. Can be used as a userscript (from the context menu or always on) or as a bookmarklet (clicking the bookmark translates the page). For Firefox and chrome
  6. // @author Achernar
  7. // @match *://*/*
  8. // @run-at context-menu
  9. // @run-at document-idle
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant GM_deleteValue
  13. // ==/UserScript==
  14.  
  15. javascript:
  16. (function(){
  17. "use strict";
  18.  
  19. var defLang="",toolbarLang="";
  20.  
  21. if (document.querySelector('body > .skiptranslate > iframe')) return;
  22. try {
  23. if (window.frameElement.classList.contains('skiptranslate')) return;
  24. }catch(e){}
  25.  
  26. var nLang=navigator.language.split('-')[0];
  27.  
  28. if (defLang == '?') defLang=nLang;
  29. if (toolbarLang == '?') toolbarLang=nLang;
  30.  
  31. function getCookies() {
  32. var r={}, a=document.cookie;
  33. a.split(';').forEach(function(e){
  34. var p=e.split('=');
  35. if (p[0]) r[p.shift().trim()]=p.join('=');
  36. });
  37. return r;
  38. };
  39.  
  40. function clearCookie(n) {
  41. var h=location.host, s=h.split('.'), a=[' ', h, '.'+h], d, old=(new Date(0)).toUTCString();
  42. while (s.length > 2) {
  43. if (s[0]) s[0]='';
  44. else s.shift();
  45. a.push(s.join('.'));
  46. }
  47. while (d=a.shift()) {
  48. document.cookie= n+'=;path=/;expires='+old+';'+(d==' ' ? '':'domain='+d+';');
  49. if (!getCookies()[n]) break;
  50. }
  51. }
  52.  
  53. var translate_to=defLang || '',
  54. translate_toS='',
  55. done=false,
  56. force=true,
  57. btnAdded=true,
  58. curVBtn,
  59. setDefBtn,
  60. closeBtn,
  61. GM=false;
  62.  
  63. try{
  64. [translate_to, toolbarLang]=getDef();
  65. btnAdded=false;
  66. GM=true;
  67. }catch(e){}
  68.  
  69. var curLang='', curLangS='';
  70. function getDef() {
  71. return [GM_getValue('translate_to',''), GM_getValue('toolbarLang','')];
  72. }
  73. function setDef() {
  74. GM_setValue('translate_to', curLang);
  75. translate_to=curLang;
  76. updCurBtn();
  77. }
  78.  
  79. function updCurBtn() {
  80. if (curVBtn) curVBtn.innerText=(translate_toS && translate_toS+' ('+translate_to+')') || translate_to;
  81. toggleBtn();
  82. }
  83.  
  84. function toggleSite(force) {
  85. var h='no:'+location.host;
  86. try{
  87. aBlocked=GM_getValue('no-all', false);
  88. blocked= force !== undefined ? !force : GM_getValue(h, false);
  89. if (force === null) return;
  90. if (blocked) {
  91. if (aBlocked) GM_setValue(h, false);
  92. else GM_deleteValue(h);
  93. }
  94. else GM_setValue(h, true);
  95. }catch(e){}
  96. }
  97.  
  98. var trans=true, blocked=false, aBlocked=false, cookies=getCookies();
  99. if (cookies.googtransopt) trans=false;
  100.  
  101. function isBlocked() {
  102. let blocked=GM_getValue('no:'+location.host, null);
  103. aBlocked=GM_getValue('no-all', false);
  104.  
  105. if (aBlocked && (blocked === false) ) return false;
  106. return aBlocked || Boolean(blocked);
  107. }
  108.  
  109. try{
  110. blocked=isBlocked();
  111. trans=!blocked;
  112. if (trans && cookies.googtransopt) clearCookie('googtransopt');
  113. }catch(e){}
  114.  
  115. if (!GM && trans && translate_to && !getCookies().googtrans) document.cookie='googtrans=/auto/'+translate_to+'/; path=/';
  116.  
  117. var eael=Element.prototype.addEventListener;
  118. Element.prototype.addEventListener=function(ev){
  119. var e=this;
  120. if (ev == 'mouseover') {
  121. if (String(arguments[1]).startsWith('function(c){return a.call(')) return;
  122. }
  123. return eael.apply(e,arguments);
  124. };
  125.  
  126. var sa=HTMLElement.prototype.setAttribute;
  127. HTMLElement.prototype.setAttribute=function(a){
  128. var e=this;
  129. if (a == 'aria-label') {
  130. let err=(new Error()).stack;
  131. if (err.includes('https://translate.googleapis.com/_/translate_http/_/js/')) return;
  132. }
  133. return sa.apply(e,arguments);
  134. };
  135.  
  136. var pm=Performance.prototype.measure;
  137. Performance.prototype.measure=function() {
  138. var e=this;
  139. try{
  140. return pm.apply(e, arguments);
  141. }catch(e){}
  142. return false;
  143. };
  144.  
  145. new MutationObserver(function(muts) {
  146. for (let mut of muts) {
  147. if (mut.addedNodes.length) {
  148. for (let i=0,n; n=mut.addedNodes[i]; i++) {
  149. if (n.nodeName == 'IFRAME' && n.classList.contains('skiptranslate') ) {
  150. addSt(frameSt, n.contentWindow);
  151. n.onload=function(){
  152. let e=this;
  153. frameMut(e);
  154. }
  155. }
  156. else if (n.nodeName == 'DIV') {
  157. if (n.classList.contains('skiptranslate') ) {
  158. let tb=document.body.querySelector('body > .skiptranslate > iframe.skiptranslate');
  159. if (tb) {
  160. addSt(frameSt, tb.contentWindow);
  161. frameMut(tb);
  162. }
  163. }
  164. else if (n.parentNode && (n.attributes.length ==1) && n.className && /^[^-_]{6}-[^-_]{6}-[^-_]{6}-[^-_]{6}/i.test(n.className) ) {
  165. let s=getComputedStyle(n);
  166. if ( (s.zIndex == '1000') && (s.transitionDelay == '0.6s') ) addSt('.'+n.className.split(' ')[0]+' {display: none;}');
  167. }
  168. }
  169. }
  170. }
  171. }
  172. }).observe(document.body, { attributes: false, subtree: false, childList: true });
  173.  
  174. function frameMut(f) {
  175. try{
  176. f.dataset.done=true;
  177. new MutationObserver(function(muts) {
  178. handleFrame(f);
  179. }).observe(f.contentWindow.document.body, { attributes: false, subtree: true, childList: true });
  180. handleFrame(f);
  181. }catch(e){}
  182. }
  183.  
  184. function clickIF(ev) {
  185. var v=this.value;
  186. if (v === undefined) return;
  187. if (v == 'turn_off_site') {
  188. toggleSite(true);
  189. toggleMenu(false);
  190. }
  191. if (v == 'turn_on_site') {
  192. clearCookie('googtransopt');
  193. menuFrame.style.display='none';
  194. toggleSite(false);
  195. toggleMenu(true);
  196. }
  197. else if (v == 'turn_off_all') {
  198. GM_setValue('no-all', true);
  199. menuFrame.style.display='none';
  200. toggleSite(null);
  201. toggleMenu(!isBlocked());
  202. closeBtn && closeBtn.click();
  203. }
  204. else if (v == 'turn_off_all_m') {
  205. GM_setValue('no-all', true);
  206. menuFrame.style.display='none';
  207. toggleSite(false);
  208. toggleMenu(!isBlocked());
  209. }
  210. else if (v == 'turn_on_all') {
  211. GM_deleteValue('no-all');
  212. menuFrame.style.display='none';
  213. toggleSite(null);
  214. toggleMenu(!isBlocked());
  215. }
  216. else if (v == 'forceMnuLang') {
  217. let L=null;
  218.  
  219. if ( (ev.target.nodeName == 'SELECT') || (ev.target.nodeName == 'OPTION') ) L=ev.target.value;
  220. else if (!LSelect) L=toolbarLang || nLang;
  221.  
  222. if (!LSelect) {
  223. let r=this.querySelector('div:not(.select)');
  224. if (r) {
  225. r.classList.add('select');
  226. let s='';
  227. LSelect=document.createElement('select');
  228. for (let k of Object.keys(CCode).sort() ) {
  229. s+='<option value="'+k+'">'+k+'</option>';
  230. }
  231. LSelect.innerHTML=s;
  232. r.appendChild(LSelect);
  233. }
  234. }
  235.  
  236. if (L !== null) {
  237. toolbarLang=L;
  238. if (L) GM_setValue('toolbarLang', L);
  239. else GM_deleteValue('toolbarLang');
  240. if (LSelect) LSelect.value=toolbarLang;
  241. }
  242. }
  243. else {
  244. curLang=v;
  245. curLangS=this.innerText;
  246. updCurBtn();
  247. }
  248. }
  249.  
  250. function toggleMenu(t) {
  251. if (!menuFrame) return;
  252. if (t !== undefined) trans=t;
  253. menuFrame.contentWindow.document.body.classList.toggle('t_off', !trans);
  254. menuFrame.contentWindow.document.body.classList.toggle('t_all_off', aBlocked);
  255. }
  256.  
  257.  
  258. function toggleBtn() {
  259. if (curVBtn) {
  260. curVBtn.parentNode.classList.toggle('hidden', !translate_to);
  261. curVBtn.parentNode.classList.toggle('disabled', curLang == translate_to );
  262. }
  263. if (setDefBtn) {
  264. setDefBtn.parentNode.classList.toggle('disabled', curLang == translate_to );
  265. }
  266. }
  267.  
  268. function fixLinks(a, s) {
  269. if (s) void(0);
  270. var r;
  271. for (let i=0, e; e=a[i]; i++) {
  272. if (e.attributes.href.value == "#") {
  273. e.href='javascript:void(0)';
  274. if (!e._cIF_) e.addEventListener('click', clickIF);
  275. e._cIF_=true;
  276. if (s && !r && (e.value == s)) r=e;
  277. }
  278. }
  279. return r;
  280. }
  281.  
  282. var langFrame, menuFrame, CCode={}, LSelect;
  283.  
  284. function forceLang(L, A, set=true) {
  285. var a= A || (langFrame && langFrame.contentWindow.document.body.querySelectorAll('a')) || [];
  286. if (!L) {
  287. let i, e, c, cl={};
  288. for (i=0; e=a[i]; i++) {
  289. c=e.classList.value;
  290. if (!cl[c]) cl[c]={c: 0, e: null};
  291. cl[c].c++;
  292. cl[c].e=e;
  293. if (set) CCode[e.value]=e.innerText;
  294. if (translate_to && !translate_toS && (translate_to == e.value) ) {
  295. translate_toS=e.innerText;
  296. updCurBtn();
  297. }
  298. }
  299. for (c in cl) {
  300. if ( (cl[c].c == 1) && cl[c].e.value) {
  301. curLang=cl[c].e.value;
  302. curLangS=cl[c].e.innerText;
  303. updCurBtn();
  304. break;
  305. }
  306. }
  307. return;
  308. }
  309. for (let i=0, e; e=a[i]; i++) {
  310. if (e.value == L) {
  311. if (set) e.click();
  312. return true;
  313. }
  314. }
  315. }
  316.  
  317. function handleFrame(f) {
  318. if (!langFrame) langFrame=f;
  319. var a;
  320. try{
  321. a=f.contentWindow.document.body.querySelectorAll('a');
  322. }catch(e){
  323. return;}
  324. var mnu=fixLinks(a, menuFrame?'':'turn_off_site');
  325. if (!menuFrame && mnu) {
  326. function createMnu(mnu, cfg) {
  327. let e=mnu.cloneNode(true);
  328. e.addEventListener('click', clickIF);
  329. e._cIF_=true;
  330. e.classList.add(cfg.c);
  331. e.value=cfg.v;
  332. let t=e.querySelector('.text');
  333. if (t) {
  334. t.innerHTML=cfg.s;
  335. }
  336. if (cfg.p == 'last') mnu.parentNode.appendChild(e);
  337. else mnu.parentNode.insertBefore(e, cfg.ns);
  338. return e;
  339. }
  340. menuFrame=f;
  341. if (GM) {
  342. let ns=mnu.nextSibling;
  343. let e=createMnu(mnu,{v: 'turn_on_site', c: 't_on', s: locS('tr_on'), ns});
  344. createMnu(mnu, {ns, v: 'turn_off_all', c: 't_off_all', s: locS('tr_all_off') });
  345. createMnu(mnu, {ns, v: 'turn_off_all_m', c: 't_off_allm', s: locS('tr_all_off_s') });
  346. createMnu(mnu, {ns, v: 'turn_on_all', c: 't_on_all', s: locS('tr_all_on') });
  347. createMnu(mnu, {ns, v: 'forceMnuLang', c: 'forceMnu', s: '&iquest;?', p: 'last' });
  348. mnu.classList.add('t_off');
  349. toggleMenu(trans);
  350. }
  351. }
  352. if (!curLang && (langFrame == f)) forceLang(null, a);
  353.  
  354. if (!done) {
  355. fixLinks(document.body.querySelectorAll('body > #google_translate_element a'));
  356. }
  357.  
  358. var cookies=getCookies();
  359.  
  360. if (translate_to && cookies.googtrans && !done) force=false;
  361. if (translate_to && (!cookies.googtrans || !force) && !done) {
  362. if (trans && forceLang(translate_to, a, force)) {
  363. done=true;
  364. return;
  365. }
  366. }
  367.  
  368. function createBt(r, cfg, d=document) {
  369. var m=r.cloneNode(true),
  370. b=m.querySelector('button[id$="restore"]'),
  371. e=d.createElement('button');
  372. e.innerText=cfg.text || '';
  373. e.id=cfg.id;
  374. b.parentNode.classList.add(cfg.id);
  375. b.parentNode.classList.add('addedBtn');
  376. b.parentNode.replaceChild(e, b);
  377. r.parentNode.appendChild(m);
  378. return e;
  379. }
  380.  
  381. if (!btnAdded) {
  382. let btn=f.contentWindow.document.body.querySelector('button[id$="restore"]');
  383. if (btn) {
  384. let r=btn.closest('td');
  385. setDefBtn=createBt(r, {text: locS('set_def'), id: 'setDefLn'} );
  386. setDefBtn.onclick=setDef;
  387. curVBtn=createBt(r, {text: (translate_toS && translate_toS+' ('+translate_to+')') || translate_to, id: 'curDefLn'} );
  388. curVBtn.onclick=function(){forceLang(translate_to);};
  389. toggleBtn();
  390. btnAdded=true;
  391. closeBtn=f.contentWindow.document.body.querySelector('a[id$="close"]');
  392. }
  393. }
  394. }
  395.  
  396.  
  397. function addSt(s, r) {
  398. try {
  399. var st=document.createElement('style');
  400. var R= r || window;
  401. R.document.documentElement.appendChild(st);
  402. st.textContent=s;
  403. }catch(e){
  404. setTimeout(function(){addSt(s, r)},0); }
  405. };
  406.  
  407. addSt(`
  408. html[style*="height: 100%;"] {
  409. height: auto !important;
  410. }
  411. body[style*="position: relative; min-height: 100%; top: 40px;"],
  412. body[style*="position: relative; min-height: 100%; top: 0px;"] {
  413. min-height: initial !important;
  414. top: unset !important;
  415. position: initial !important;
  416. }
  417. body > .skiptranslate > iframe:not(:hover) {
  418. height: 4px;
  419. opacity: 0;
  420. }
  421. iframe.skiptranslate,
  422. body > #google_translate_element {
  423. zoom: ${1/window.getComputedStyle(document.body).zoom};
  424. }
  425. iframe.skiptranslate[title] {
  426. max-width: 95% !important;
  427. margin-right: 1em;
  428. }
  429. iframe.skiptranslate[title][style*="; left: 0px;"] {
  430. left: 1em !important;
  431. }
  432. body > .skiptranslate:not([id]):not([style*="display:"]) ~ #google_translate_element,
  433. body > #google_translate_element:empty {
  434. display: none;
  435. }
  436. body > #google_translate_element {
  437. position: fixed;
  438. top: 0px;
  439. right: 0px;
  440. z-index: 9999999;
  441. }
  442. body > #google_translate_element.T:hover {
  443. background: white;
  444. color: black;
  445. padding: 0.5em;
  446. border: 2px solid red;
  447. font: bold 14px arial;
  448. }
  449. body > #google_translate_element.T:not(:hover) {
  450. opacity: 0;
  451. max-width: 3px;
  452. max-height: 3px;
  453. }
  454.  
  455. no #google_translate_element img {
  456. display: none !important;
  457. }
  458. `);
  459.  
  460. var frameSt=`
  461. body[scroll="no"] {
  462. overflow-x: auto !important;
  463. }
  464. body[scroll="no"] > div {
  465. padding-bottom: 20px !important;
  466. }
  467. body[scroll="no"] .setDefLn {
  468. margin-left: 2em !important;
  469. }
  470. body[scroll="no"] .addedBtn {
  471. margin-left: 0.5em;
  472. }
  473. body[scroll="no"] .addedBtn.hidden,
  474. body[scroll="no"]:not(.t_off) .t_on,
  475. body[scroll="no"].t_off .t_off,
  476. body[scroll="no"]:not(.t_all_off) .t_on_all,
  477. body[scroll="no"].t_all_off .t_off_all,
  478. body[scroll="no"].t_all_off .t_off_allm {
  479. display: none !important;
  480. }
  481. body[scroll="no"] .addedBtn.disabled {
  482. opacity: 0.6;
  483. pointer-events: none;
  484. }
  485. body[scroll="no"] .forceMnu select {
  486. margin: 0 1em;
  487. }
  488. `;
  489.  
  490. function startGT() {
  491. var r=document.head || document.documentElement;
  492. var el=document.createElement('script');
  493. el.textContent=`
  494. function googleTranslateElementInit() {
  495. new google.translate.TranslateElement({
  496. pageLanguage: '',
  497. autoDisplay: false,
  498. layout: google.translate.TranslateElement.InlineLayout.SIMPLE
  499. }, 'google_translate_element');
  500. }`;
  501. r.insertBefore(el,r.firstChild);
  502.  
  503. el=document.createElement('script');
  504. el.src='https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit&ok='+(toolbarLang?'&hl='+toolbarLang:'');
  505. r.insertBefore(el,r.firstChild);
  506. }
  507.  
  508. var gte=document.createElement('div');
  509. gte.id='google_translate_element';
  510. document.body.appendChild(gte);
  511.  
  512. var gmi='';
  513. try{
  514. gmi=GM_info.script['run-at'];
  515. }catch(e){}
  516.  
  517. function locS(id) {
  518. if (!gtLang) {
  519. try{if (gtLang=google && google.translate && google.translate._const && google.translate._const._cl) loc='';
  520. }catch(e){}
  521. }
  522. if (!loc) {
  523. loc= local[gtLang] || local[gtLang.split('-')[0]] || local[toolbarLang] || local[toolbarLang.split('-')[0]] || local[navigator.language] || local[navigator.language.split('-')[0]] || local.en;
  524. if (loc._ && (Object.keys(loc).length == 1) ) {
  525. let L=loc._.split(/\n/);
  526. [loc.tr_on, loc.tr_all_off, loc.tr_all_off_s, loc.tr_all_on, loc.set_def, loc.float_tit]=L;
  527. }
  528. for (let k in local.en) {
  529. if (!loc[k]) loc[k]=local.en[k];
  530. }
  531. }
  532. return loc[id];
  533. }
  534.  
  535. var loc, gtLang='';
  536. const local = {
  537. en: {
  538. tr_on: 'Turn on translation for this site',
  539. tr_all_off: 'Turn off for all sites',
  540. tr_all_off_s: 'Turn off for all sites except this one',
  541. tr_all_on: 'Allow translation back on all sites',
  542. set_def: 'Set as default language',
  543. float_tit: 'Click to translate'
  544. },
  545. fr: {
  546. tr_on: 'Activer la traduction pour le site',
  547. tr_all_off: 'Désactiver pour tous les sites',
  548. tr_all_off_s: 'Désactiver pour tous les sites sauf celui-ci',
  549. tr_all_on: 'Réactiver la traduction sur tous les sites',
  550. set_def: 'Choisir comme langue par défaut',
  551. float_tit: 'Cliquer pour traduire'
  552. },
  553. nl: { _: `Schakel in vertaling voor deze site
  554. Schakel uit voor alle sites
  555. Schakel uit voor alle sites behalve deze
  556. Sta vertaling weer toe op alle sites
  557. Instellen als standaardtaal
  558. Klik om te vertalen`},
  559. es: { _: `Activar la traducción para este sitio
  560. Desactivar para todos los sitios
  561. Desactivar para todos los sitios excepto este
  562. Permitir la traducción en todos los sitios
  563. Establecer como idioma predeterminado
  564. Haga clic para traducir`},
  565. it: { _:`Attiva la traduzione per questo sito
  566. Disabilita per tutti i siti
  567. Disabilita per tutti i siti tranne questo
  568. Reattivare la traduzione su tutti i siti
  569. Scegli come lingua predefinita
  570. Fai clic per tradurre`},
  571. de: { _: `Aktivieren Sie die Übersetzung für diese Site
  572. Deaktivieren Sie für alle Websites
  573. Deaktivieren Sie für alle Websites außer diesem
  574. Reaktivieren Sie die Übersetzung an allen Standorten
  575. Wählen Sie als Standardsprache
  576. Klicken Sie hier, um zu übersetzen`},
  577. pt: { _: `Ative a tradução para este site
  578. Desativar para todos os sites
  579. Desativar para todos os sites, exceto isso
  580. Reativar a tradução em todos os sites
  581. Escolha como linguagem padrão
  582. Clique para traduzir`},
  583. ja: { _: `このサイトの翻訳をオンにする
  584. すべてのサイトの電源を切る
  585. それ以外のすべてのサイトの電源を切る
  586. すべてのサイトで翻訳を返す
  587. デフォルト言語として設定する
  588. クリックして翻訳する`},
  589. ar: { _: `تنشيط الترجمة لهذا الموقع
  590. تعطيل لجميع المواقع
  591. تعطيل لجميع المواقع باستثناء هذا
  592. إعادة تنشيط الترجمة على جميع المواقع
  593. اختر كلغة افتراضية
  594. انقر للترجمةة`},
  595. iw: { _: `הפעל תרגום לאתר זה
  596. כבה לכל האתרים
  597. כבה לכל האתרים למעט זה
  598. אפשר תרגום בחזרה לכל האתרים
  599. הגדר כשפת ברירת מחדל
  600. לחץ לתרגום`},
  601. 'zh-CN': { _: `打开此网站的翻译
  602. 关闭所有站点
  603. 除此之外,所有网站都关闭
  604. 允许在所有站点上翻译
  605. 设置为默认语言
  606. 单击以翻译`},
  607. 'zh-TW': { _: `打開此網站的翻譯
  608. 關閉所有站點
  609. 除此之外,所有網站都關閉
  610. 允許在所有站點上翻譯
  611. 設置為默認語言
  612. 單擊以翻譯`}
  613. };
  614.  
  615.  
  616. if (trans || getCookies().googtrans || !GM || ( gmi && (gmi == 'context-menu') ) && (gmi && !gmi.startsWith('document-') ) ) startGT();
  617. else {
  618. gte.innerHTML='&#167;';
  619. gte.title=locS('float_tit');
  620. gte.classList.add('T');
  621. gte.addEventListener('click', function(){
  622. this.innerHTML='';
  623. this.title='';
  624. this.classList.remove('T');
  625. startGT();
  626. }, {once: true});
  627. }
  628.  
  629. })()