// ==UserScript== // @name 超级翻译助手 // @namespace https://github.com/syhyz1990/translate // @version 1.0.3 // @author YouXiaoHou // @description 用鼠标选中文字,按下快捷键(默认为F9),可自动翻译文字。已支持超过 14 种语言。 // @license MIT // @homepage https://www.youxiaohou.com/tool/install-translate.html // @supportURL https://github.com/syhyz1990/translate // @match *://*/* // @require https://unpkg.com/sweetalert2@10.16.6/dist/sweetalert2.min.js // @require https://unpkg.com/hotkeys-js/dist/hotkeys.min.js // @resource swalStyle https://unpkg.com/sweetalert2@10.16.6/dist/sweetalert2.min.css // @connect translate.youxiaohou.com // @run-at document-idle // @noframes // @grant GM_setValue // @grant GM_getValue // @grant GM_setClipboard // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_getResourceText // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDI0IDEwMjQiPjxwYXRoIGQ9Ik02NTguMyA0MDIuM2gyOTIuNmM0MC40IDAgNzMuMSAzMi41IDczLjEgNzMuMXY0NzUuNGMwIDQwLjQtMzIuNSA3My4xLTczLjEgNzMuMUg0NzUuNGMtNDAuNCAwLTczLjEtMzIuNS03My4xLTczLjFWNjU4LjNoMTQ2LjNjNjAuOSAwIDEwOS43LTQ5LjEgMTA5LjctMTA5LjdWNDAyLjN6TTAgNzMuMUMwIDMyLjcgMzIuNSAwIDczLjEgMGg0NzUuNGM0MC40IDAgNzMuMSAzMi41IDczLjEgNzMuMXY0NzUuNGMwIDQwLjQtMzIuNSA3My4xLTczLjEgNzMuMUg3My4xYy00MC40LjEtNzMuMS0zMi40LTczLjEtNzNWNzMuMXptMTQ2LjMgMzE1LjhoNTMuNHYtMjguM2g3N3YxMzUuMmg1Ni42VjM2MC42aDc4LjZ2MjMuNkg0NzBWMjA1SDMzMy4zdi0zOS4zYzAtMTEuNSAxLjYtMjEuNSA0LjgtMjkuOC44LTEuNSAxLjQtMy4xIDEuNS00LjggMC0xLTMuNy0yLTExLTMuMWgtNTMuNXY3N0gxNDYuM3YxODMuOXpNMTk5LjcgMjQ5aDc3djY5LjFoLTc3VjI0OXptMjEyLjIgNjkuMWgtNzguNlYyNDloNzguNnY2OS4xem0yMzIuOSA1NTcuN2wyMi02MS4zaDExNC43bDIyIDYxLjNoNjIuOGwtMTAyLTI5MC43aC03My45TDU4NS4xIDg3NS45bDU5LjctLjF6bTM3LjgtMTEwTDcyNSA2NDEuN2gxLjZsMzkuMyAxMjQuMWgtODMuM3ptMjY4LjMtNDczLjJoLTczLjFjMC04MC44LTY1LjUtMTQ2LjMtMTQ2LjMtMTQ2LjNWNzMuMWMxMjEuMSAwIDIxOS40IDk4LjMgMjE5LjQgMjE5LjV6TTczLjEgNzMxLjRoNzMuMWMwIDgwLjggNjUuNSAxNDYuMyAxNDYuMyAxNDYuM3Y3My4xYy0xMjEuMS4xLTIxOS40LTk4LjItMjE5LjQtMjE5LjR6IiBmaWxsPSIjZDgxZDQ1Ii8+PC9zdmc+ // @downloadURL none // ==/UserScript== (function () { 'use strict'; const customClass = { container: 'translate-container', popup: 'translate-popup', content: 'translate-content', }; const toastClass = { container: 'translate-d-container', popup: 'translate-d-popup', }; let toast = Swal.mixin({ toast: true, position: 'top', showConfirmButton: false, timerProgressBar: false, customClass: toastClass, didOpen: (toast) => { toast.addEventListener('mouseenter', Swal.stopTimer); toast.addEventListener('mouseleave', Swal.resumeTimer); } }); let languageMap = { 'auto': '自动检测', 'ar': '阿拉伯语', 'de': '德语', 'ru': '俄语', 'fr': '法语', 'ko': '韩语', 'la': '拉丁语', 'pt': '葡萄牙语', 'ja': '日语', 'th': '泰语', 'es': '西班牙语', 'it': '意大利语', 'en': '英语', 'zh-CN': '简体中文', 'zh-TW': '繁体中文', }; let util = { getValue(name) { return GM_getValue(name); }, setValue(name, value) { GM_setValue(name, value); }, sleep(time) { return new Promise((resolve) => setTimeout(resolve, time)); }, addStyle(id, tag, css) { tag = tag || 'style'; let doc = document, styleDom = doc.getElementById(id); if (styleDom) return; let style = doc.createElement(tag); style.rel = 'stylesheet'; style.id = id; tag === 'style' ? style.innerHTML = css : style.href = css; document.head.appendChild(style); }, isType(obj) { return Object.prototype.toString.call(obj).replace(/^\[object (.+)\]$/, '$1').toLowerCase(); }, post(url, data, headers, type) { if (this.isType(data) === 'object') { data = JSON.stringify(data); } return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "POST", url, headers, data, responseType: type || 'json', onload: (res) => { type === 'blob' ? resolve(res) : resolve(res.response || res.responseText); }, onerror: (err) => { reject(err); }, }); }); }, parseLanguage(language) { return languageMap[language] || language; }, }; let main = { untranslatedText: '', translatedText: '', translating: false, speechSynthesis: null, speakPlaySvg: '', //初始化配置数据 initValue() { let value = [{ name: 'setting_success_times', value: 0 }, { name: 'hotkey', value: 'f9' }, { name: 'from', value: 'auto' }, { name: 'to', value: 'zh-CN' }]; value.forEach((v) => { if (util.getValue(v.name) === undefined) { util.setValue(v.name, v.value); } }); }, async startTranslate() { this.translatedText = ''; let select = window.getSelection().toString(); if (select) this.untranslatedText = select; if (this.untranslatedText) this.translatedText = await this.translate(this.untranslatedText); await this.showPopup(this.untranslatedText, this.translatedText); }, async translate(text, showToast = true) { let btn = document.querySelector('.translate-btn'); try { if (this.translating) return; this.translating = true; showToast && toast.fire({title: '正在翻译...', icon: 'info'}); btn && (btn.innerHTML = '翻译中'); let res = await util.post('https://translate.youxiaohou.com', { str: text, from: util.getValue('from'), to: util.getValue('to') }, { 'Content-Type': 'application/json' }); this.translating = false; btn && (btn.innerHTML = '翻译'); if (res.code === 200) { util.setValue('setting_success_times', util.getValue('setting_success_times') + 1); return res.data; } return res?.msg || ''; } catch (e) { this.translating = false; btn && (btn.innerHTML = '翻译失败'); return ''; } }, async showPopup(untranslatedText, translatedText) { let commonLangFrom = ['auto', 'zh-CN', 'en']; let commonLangTo = ['zh-CN', 'en', 'zh-TW']; let langFrom = util.getValue('from'); let langTo = util.getValue('to'); if (!commonLangFrom.includes(langFrom)) commonLangFrom.push(langFrom); if (!commonLangTo.includes(langTo)) commonLangTo.push(langTo); let langDomFrom = commonLangFrom.map(val => { if (val === langFrom) { return `
${languageMap[val]}
`; } else { return `
${languageMap[val]}
`; } }).join(''); let langDomTo = commonLangTo.map(val => { if (val === langTo) { return `
${languageMap[val]}
`; } else { return `
${languageMap[val]}
`; } }).join(''); let html = `
${langDomFrom}
${main.speakPlaySvg}
字数:${untranslatedText.length}
${langDomTo}
${translatedText}
${main.speakPlaySvg}
`; Swal.fire({ width: '1200px', position: 'center', allowOutsideClick: false, showCloseButton: true, showConfirmButton: false, html: html, customClass }); document.querySelector('.translate-copy').addEventListener("click", (e) => { GM_setClipboard(document.querySelector('.translated-text').innerText); let copyTip = document.querySelector('.translate-copy-tip'); copyTip.innerText = '复制成功'; setTimeout(() => copyTip.innerText = '', 3000); }); document.querySelector('.translate-btn').addEventListener("click", async (e) => { if (this.translating) return; let text = document.querySelector('.untranslated-text').value; document.querySelector('.translated-text').innerText = await this.translate(text, false); }); document.querySelector('.untranslated-text').addEventListener("input", async (e) => { this.untranslatedText = e.target.value; document.querySelector('#word-count').innerText = '字数:' + e.target.value.length; }); document.querySelector('.untranslated-text').addEventListener("change", async (e) => { document.querySelector('.translate-btn').click(); }); document.querySelector('.translate-clear').addEventListener("click", async (e) => { this.untranslatedText = ''; document.querySelector('.untranslated-text').value = ''; document.querySelector('.translated-text').innerText = ''; document.querySelector('#word-count').innerText = '字数:0'; }); document.querySelector('#dropdown-from').addEventListener("click", async (e) => { this.setFromLanguage(true); }); document.querySelector('#dropdown-to').addEventListener("click", async (e) => { this.setToLanguage(true); }); document.querySelector('#speak-from').addEventListener("click", async (e) => { this.speakWord(document.querySelector('.untranslated-text').value); }); document.querySelector('#speak-to').addEventListener("click", async (e) => { this.speakWord(document.querySelector('.translated-text').innerText); }); document.querySelector('.from-side .translate-lang').addEventListener("click", async (e) => { let current = e.target, parent = e.currentTarget; let lang = current?.dataset?.lang; if (!lang) return; let items = parent.querySelectorAll('.item'); for (let i = 0; i < items.length; i++) { items[i].classList.remove("on"); } current.classList.add('on'); util.setValue('from', lang); document.querySelector('.translate-btn').click(); }); document.querySelector('.to-side .translate-lang').addEventListener("click", async (e) => { let current = e.target, parent = e.currentTarget; let lang = current?.dataset?.lang; if (!lang) return; let items = parent.querySelectorAll('.item'); for (let i = 0; i < items.length; i++) { items[i].classList.remove("on"); } current.classList.add('on'); util.setValue('to', lang); document.querySelector('.translate-btn').click(); }); //自动聚焦 let textarea = document.querySelector('.untranslated-text'); textarea.focus(); textarea.setSelectionRange(textarea.value.length, textarea.value.length); }, addHotKey() { hotkeys(util.getValue('hotkey'), async (event, handler) => { event.preventDefault(); await this.startTranslate(); }); }, speakWord(text) { if ('speechSynthesis' in window && text) { if (speechSynthesis.speaking) { speechSynthesis.cancel(); } else { let msg = new SpeechSynthesisUtterance(); msg.text = text; speechSynthesis.speak(msg); } } }, //重置翻译次数 clearTranslateTimes() { Swal.fire({ showCancelButton: true, title: '确定要重置翻译次数吗?', icon: 'warning', confirmButtonText: '确定', cancelButtonText: '取消', customClass: toastClass, }).then(res => { if (res.isConfirmed) { util.setValue('setting_success_times', 0); history.go(0); } }); }, setHotkey() { Swal.fire({ title: '请选择快捷键', text: '注意:避免与其他程序产生冲突', input: 'select', inputValue: util.getValue('hotkey').toUpperCase(), inputOptions: { 'F1': 'F1', 'F2': 'F2', 'F3': 'F3', 'F4': 'F4', 'F5': 'F5', 'F6': 'F6', 'F7': 'F7', 'F8': 'F8', 'F9': 'F9(默认)', 'F10': 'F10', 'F11': 'F11', 'F12': 'F12', }, confirmButtonText: '确定', customClass: toastClass, }).then(async (result) => { if (result.isConfirmed) { util.setValue('hotkey', result.value.toLowerCase()); history.go(0); } }); }, setFromLanguage(popAgain = false) { Swal.fire({ title: '请选择原始语言', text: '选择您要翻译的原始语言', input: 'select', inputValue: util.getValue('from'), inputOptions: languageMap, confirmButtonText: '确定', customClass: toastClass, }).then(async (result) => { if (result.isConfirmed) { util.setValue('from', result.value); popAgain && this.startTranslate(); } }); }, setToLanguage(popAgain = false) { let map = JSON.parse(JSON.stringify(languageMap)); delete map.auto; Swal.fire({ title: '请选择目标语言', text: '选择您要翻译的目标语言', input: 'select', inputValue: util.getValue('to'), inputOptions: map, confirmButtonText: '确定', customClass: toastClass, }).then(async (result) => { if (result.isConfirmed) { util.setValue('to', result.value); popAgain && this.startTranslate(); } }); }, registerMenuCommand() { GM_registerMenuCommand(`⚙️ 设置快捷键:${util.getValue('hotkey').toUpperCase()}`, () => { this.setHotkey(); }); GM_registerMenuCommand(`⚙️ 设置原始语言:${util.parseLanguage(util.getValue('from'))}`, () => { this.setFromLanguage(); }); GM_registerMenuCommand(`⚙️ 设置目标语言:${util.parseLanguage(util.getValue('to'))}`, () => { this.setToLanguage(); }); GM_registerMenuCommand('👀 已翻译:' + util.getValue('setting_success_times') + '次', () => { this.clearTranslateTimes(); }); }, addPluginStyle() { let style = ` .translate-container { z-index: 99999!important; } .translate-popup { font-size: 14px !important;padding:0 !important; border-radius: 10px;} .translate-d-container { z-index: 999999!important;} .translate-d-popup { font-size: 14px !important;} .translate-content { padding:0 !important; } .translate-setting-label { display: flex;align-items: center;justify-content: space-between;padding-top: 20px; } .translate-setting-checkbox { width: 16px;height: 16px; } .translate-wrapper { margin: 28px 24px 10px; font-family: Roboto,RobotoDraft,Helvetica,Arial,sans-serif;} .translate-box { display: flex; min-height: calc(100vh - 130px);box-shadow: 1px 1px 9px #eaeaea; border-radius: 15px;} .translate-box .from-side { flex:1; width:50%; border: 1px solid #ddd; border-radius: 15px 0 0 15px; border-right:none;color: #666;position: relative;padding: 0 0 36px;} .translate-box .to-side { flex:1; width:50%; border:1px solid #ddd; border-radius: 0 15px 15px 0; border-left: none;background: #f5f5f5;color: #000;position: relative;padding: 0 0 36px;} .untranslated-text { width: 100%; height: calc(100% - 50px); border-radius: 15px 0 0 15px; resize: none; line-height: 28px; font-size: 16px; color: #666; border: none; text-align: left; padding: 20px; box-sizing: border-box; outline:none; overflow-wrap: break-word; word-break: break-word; word-wrap: break-word;} .untranslated-text::-webkit-scrollbar { width: 6px; height: 6px;} .untranslated-text::-webkit-scrollbar-thumb { background-color: rgba(85,85,85,.4)} .untranslated-text::-webkit-scrollbar-thumb, .translate-box .untranslated-text::-webkit-scrollbar-thumb:hover { border-radius: 5px; box-shadow: inset 0 0 6px rgb(0 0 0 / 20%);} .translate-toolbar { position: absolute; bottom: 0; font-size: 13px; color: #999; height: 36px; text-align: right; left: 20px; right: 20px; display: flex; align-items: center; justify-content: space-between;} .translated-text { width: 100%; line-height: 28px; font-size: 16px; border: none; text-align: left; white-space: pre-wrap; padding: 20px; box-sizing: border-box; overflow-wrap: break-word; word-break: break-word; word-wrap: break-word; position: relative;} .translate-title { border-bottom: 1px solid #ddd; padding: 0 20px 0 10px; position: sticky; top: -10px;display: flex;align-items:center;justify-content: space-between; z-index: 99999; background: #fff;user-select:none} .translate-lang { display: flex; align-items:center;} .translate-lang .item { position:relative; color:#666;padding: 0 15px; cursor:pointer;font-size:15px;font-weight: 500;height: 48px;display: flex;align-items:center;} .translate-lang .item.on { color:#1a73e8;font-weight: 700} .translate-lang .item.on:after { content: '';display:block;position: absolute; left: 0; right: 0; bottom: 0; width: 100%; height: 2px; background: #1a73e8;} .translate-lang .item:hover { background: #f6fafe; color:#333} .translate-dropdown { cursor:pointer; fill: #999;margin: 0 10px 0 20px; padding:5px;border-radius:50%} .translate-dropdown:hover { fill: #666; background:#f5f5f5 } .translate-box .from-side .translate-title { border-radius: 15px 0 0 0;} .translate-box .to-side .translate-title { border-radius: 0 15px 0 0;} .translate-btn { border: 0; border-radius: 5px; color: #fff; font-size: 14px; padding: 8px 13px; background: #4396fc; cursor: pointer; line-height: 1; display: flex; align-items: center;} .translate-btn:hover { background: #187efa;} .translate-loading { width: 12px; height: 12px; border: 2px solid #FFF; margin-right:6px; border-bottom-color: transparent; border-radius: 50%; display: inline-block; box-sizing: border-box; animation: rotation 1s linear infinite; } @keyframes rotation { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); }} .translate-copy-box {display: flex;align-items:center;gap:5px;} .translate-copy,.translate-speak svg {cursor: pointer;opacity: 0.2;transition: opacity .3s;display: flex;align-items:center;} .translate-copy:hover,.translate-speak svg:hover {opacity: 0.5;transition: opacity .3s} .translate-clear {cursor: pointer; fill: #666; padding:6px; border-radius:50%;position: absolute; top: 66px; right: 13px;background: rgb(255 255 255 / 50%)} .translate-clear:hover {fill: #333; background:#f5f5f5; } .translate-footer {margin-top: 8px; font-size: 14px; color: #999} .translate-footer a {color: rgb(26 115 232 / 70%)} .translate-footer a:hover {color: rgb(26 115 232 / 90%)} .swal2-close { font-size: 30px!important} .swal2-close:focus {box-shadow:none!important} `; if (document.head) { util.addStyle('swal-pub-style', 'style', GM_getResourceText('swalStyle')); util.addStyle('translate-style', 'style', style); } const headObserver = new MutationObserver(() => { util.addStyle('swal-pub-style', 'style', GM_getResourceText('swalStyle')); util.addStyle('translate-style', 'style', style); }); headObserver.observe(document.head, {childList: true, subtree: true}); }, isTopWindow() { return window.self === window.top; }, init() { this.initValue(); this.addPluginStyle(); this.addHotKey(); this.isTopWindow() && this.registerMenuCommand(); }, }; main.init(); })();