translator

划词翻译

  1. // ==UserScript==
  2. // @name translator
  3. // @namespace https://lufei.so
  4. // @supportURL https://github.com/intellilab/translator.user.js
  5. // @description 划词翻译
  6. // @version 1.6.8
  7. // @run-at document-start
  8. // @grant GM_addStyle
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_xmlhttpRequest
  12. // @require https://cdn.jsdelivr.net/combine/npm/@violentmonkey/dom@1,npm/@violentmonkey/ui@0.4
  13. // @include *
  14. // ==/UserScript==
  15.  
  16. (function () {
  17. 'use strict';
  18.  
  19. function dumpQuery(query) {
  20. return Object.entries(query).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join('&');
  21. }
  22. function request({
  23. method = 'GET',
  24. url,
  25. params,
  26. responseType,
  27. data,
  28. headers
  29. }) {
  30. return new Promise((resolve, reject) => {
  31. if (params) {
  32. const sep = url.includes('?') ? '&' : '?';
  33. url += sep + dumpQuery(params);
  34. }
  35.  
  36. GM_xmlhttpRequest({
  37. method,
  38. url,
  39. responseType,
  40. data,
  41. headers,
  42.  
  43. onload(res) {
  44. if (res.status >= 300) return reject();
  45. resolve(res.response);
  46. },
  47.  
  48. onerror: reject
  49. });
  50. });
  51. }
  52.  
  53. const provider = {
  54. name: 'youdao',
  55. handle: async text => {
  56. const payload = {
  57. type: 'data',
  58. doctype: 'json',
  59. version: '1.1',
  60. relatedUrl: 'http://fanyi.youdao.com/',
  61. keyfrom: 'fanyiweb',
  62. key: null,
  63. translate: 'on',
  64. q: text,
  65. ts: Date.now()
  66. };
  67. const result = await request({
  68. url: 'https://fanyi.youdao.com/openapi.do',
  69. params: payload,
  70. responseType: 'json'
  71. });
  72. if (result.errorCode) throw result;
  73. const {
  74. basic,
  75. query,
  76. translation
  77. } = result;
  78.  
  79. if (basic) {
  80. const noPhonetic = '♥';
  81. const {
  82. explains,
  83. 'us-phonetic': us,
  84. 'uk-phonetic': uk
  85. } = basic;
  86. return {
  87. query,
  88. phonetic: [{
  89. html: `UK: [${uk || noPhonetic}]`,
  90. url: `https://dict.youdao.com/dictvoice?audio=${encodeURIComponent(query)}&type=1`
  91. }, {
  92. html: `US: [${us || noPhonetic}]`,
  93. url: `https://dict.youdao.com/dictvoice?audio=${encodeURIComponent(query)}&type=2`
  94. }],
  95. explains,
  96. detailUrl: `http://dict.youdao.com/search?q=${encodeURIComponent(query)}`
  97. };
  98. }
  99.  
  100. if (translation != null && translation[0]) {
  101. return {
  102. translations: translation
  103. };
  104. }
  105. }
  106. };
  107.  
  108. const LANG_EN = 'en';
  109. const LANG_ZH_HANS = 'zh-Hans';
  110.  
  111. async function translate(text, to) {
  112. const [data] = await request({
  113. method: 'POST',
  114. url: 'https://cn.bing.com/ttranslatev3',
  115. responseType: 'json',
  116. data: dumpQuery({
  117. fromLang: 'auto-detect',
  118. to,
  119. text
  120. }),
  121. headers: {
  122. 'Content-Type': 'application/x-www-form-urlencoded'
  123. }
  124. });
  125. const {
  126. detectedLanguage,
  127. translations
  128. } = data;
  129. return {
  130. language: {
  131. from: detectedLanguage.language,
  132. to
  133. },
  134. translations: translations.map(item => item.text)
  135. };
  136. }
  137.  
  138. const provider$1 = {
  139. name: 'bing',
  140. handle: async source => {
  141. let data = await translate(source, LANG_ZH_HANS);
  142. if (data.language.from === LANG_ZH_HANS) data = await translate(source, LANG_EN);
  143. return data;
  144. }
  145. };
  146.  
  147. const LANG_EN$1 = 'en';
  148. const LANG_ZH_CN = 'zh-CN';
  149.  
  150. async function translate$1(text, to) {
  151. var _data$;
  152.  
  153. const data = await request({
  154. url: 'https://translate.google.cn/translate_a/single',
  155. params: {
  156. q: text,
  157. client: 'gtx',
  158. sl: 'auto',
  159. tl: to,
  160. dt: 'at'
  161. },
  162. responseType: 'json'
  163. });
  164. const language = {
  165. from: data[8][0][0],
  166. to
  167. };
  168. const translations = (_data$ = data[5]) == null ? void 0 : _data$.map(item => {
  169. var _item$, _item$$;
  170.  
  171. return (_item$ = item[2]) == null ? void 0 : (_item$$ = _item$[0]) == null ? void 0 : _item$$[0];
  172. }).filter(Boolean);
  173. return {
  174. language,
  175. translations
  176. };
  177. }
  178.  
  179. const provider$2 = {
  180. name: 'google',
  181. handle: async source => {
  182. let data = await translate$1(source, LANG_ZH_CN);
  183. if (data.language.from === LANG_ZH_CN) data = await translate$1(source, LANG_EN$1);
  184. return data;
  185. }
  186. };
  187.  
  188. var styles = {"link":"style-module_link__YVV7m","section":"style-module_section__1Eiq1","block-top":"style-module_block__1NZsy","block-bottom":"style-module_block__1NZsy","label":"style-module_label__JD9KX","content":"style-module_content__1MvKK","phonetic":"style-module_phonetic__2SIsx","item":"style-module_item__2QPXK"};
  189. var stylesheet=":host .style-module_link__YVV7m{position:relative;color:#7cbef0;cursor:pointer}:host .style-module_link__YVV7m:hover{text-decoration:underline}:host .style-module_section__1Eiq1{display:flex;align-items:flex-start;font-size:12px;line-height:1.2}:host .style-module_section__1Eiq1:not(:first-child){border-top:1px solid #eee}:host .style-module_block__1NZsy{display:block}:host .style-module_label__JD9KX{display:block;margin:8px 8px 8px 0;padding:2px 0;color:#fff;background:#bbb;border-radius:4px;font-size:12px;line-height:1.4;text-transform:uppercase;writing-mode:vertical-rl}:host .style-module_content__1MvKK{flex:1;min-width:0;padding:8px 0}:host .style-module_content__1MvKK>*{display:block}:host .style-module_content__1MvKK>:not(:first-child){margin-top:8px}:host .style-module_phonetic__2SIsx{display:inline-block;margin-left:8px}:host .style-module_item__2QPXK{display:block}:host .style-module_item__2QPXK~.style-module_item__2QPXK{margin-top:8px}";
  190.  
  191. const React = VM;
  192. let audio;
  193.  
  194. function play(url) {
  195. if (!audio) audio = /*#__PURE__*/React.createElement("audio", {
  196. autoPlay: true
  197. });
  198. audio.src = url;
  199. }
  200.  
  201. function getPlayer(url) {
  202. return () => {
  203. play(url);
  204. };
  205. }
  206.  
  207. function handleOpenUrl(e) {
  208. const {
  209. href
  210. } = e.target.dataset;
  211. const a = /*#__PURE__*/React.createElement("a", {
  212. href: href,
  213. target: "_blank",
  214. rel: "noopener noreferrer"
  215. });
  216. a.click();
  217. }
  218.  
  219. function render(results, {
  220. event,
  221. panel
  222. }) {
  223. panel.clear();
  224.  
  225. for (const [name, result] of Object.entries(results)) {
  226. const {
  227. query,
  228. phonetic,
  229. detailUrl,
  230. explains,
  231. translations
  232. } = result;
  233. panel.append( /*#__PURE__*/React.createElement(panel.id, {
  234. className: styles.section
  235. }, /*#__PURE__*/React.createElement(panel.id, {
  236. className: styles.label
  237. }, name), /*#__PURE__*/React.createElement(panel.id, {
  238. className: styles.content
  239. }, !!(query || phonetic != null && phonetic.length) && /*#__PURE__*/React.createElement(panel.id, {
  240. className: styles.block
  241. }, query && /*#__PURE__*/React.createElement(panel.id, null, query), phonetic == null ? void 0 : phonetic.map(({
  242. html,
  243. url
  244. }) => /*#__PURE__*/React.createElement(panel.id, {
  245. className: `${styles.phonetic} ${styles.link}`,
  246. dangerouslySetInnerHTML: {
  247. __html: html
  248. },
  249. onClick: getPlayer(url)
  250. }))), explains && /*#__PURE__*/React.createElement(panel.id, {
  251. className: styles.block
  252. }, explains.map(item => /*#__PURE__*/React.createElement(panel.id, {
  253. className: styles.item,
  254. dangerouslySetInnerHTML: {
  255. __html: item
  256. }
  257. }))), detailUrl && /*#__PURE__*/React.createElement(panel.id, {
  258. className: styles.block
  259. }, /*#__PURE__*/React.createElement(panel.id, {
  260. className: styles.link,
  261. "data-href": detailUrl,
  262. onClick: handleOpenUrl
  263. }, "\u66F4\u591A...")), translations && /*#__PURE__*/React.createElement(panel.id, {
  264. className: styles.block
  265. }, translations.map(item => /*#__PURE__*/React.createElement(panel.id, {
  266. className: styles.item,
  267. dangerouslySetInnerHTML: {
  268. __html: item
  269. }
  270. }))))));
  271. }
  272.  
  273. const {
  274. wrapper
  275. } = panel;
  276. const {
  277. innerWidth,
  278. innerHeight
  279. } = window;
  280. const {
  281. clientX,
  282. clientY
  283. } = event;
  284.  
  285. if (clientY > innerHeight * 0.5) {
  286. wrapper.style.top = 'auto';
  287. wrapper.style.bottom = `${innerHeight - clientY + 10}px`;
  288. } else {
  289. wrapper.style.top = `${clientY + 10}px`;
  290. wrapper.style.bottom = 'auto';
  291. }
  292.  
  293. if (clientX > innerWidth * 0.5) {
  294. wrapper.style.left = 'auto';
  295. wrapper.style.right = `${innerWidth - clientX}px`;
  296. } else {
  297. wrapper.style.left = `${clientX}px`;
  298. wrapper.style.right = 'auto';
  299. }
  300.  
  301. panel.show();
  302. }
  303.  
  304. const providers = [provider, provider$1, provider$2];
  305. let session;
  306.  
  307. function translate$2(context) {
  308. const sel = window.getSelection();
  309. const text = sel.toString().trim();
  310. if (/^\s*$/.test(text)) return;
  311. const {
  312. activeElement
  313. } = document;
  314. if (['input', 'textarea'].indexOf(activeElement.tagName.toLowerCase()) < 0 && !activeElement.contains(sel.getRangeAt(0).startContainer)) return;
  315. context.source = text;
  316. const results = {};
  317. session = results;
  318. providers.forEach(async provider => {
  319. const result = await provider.handle(text);
  320. if (!result || session !== results) return;
  321. results[provider.name] = result;
  322. render(results, context);
  323. });
  324. }
  325.  
  326. function debounce(func, delay) {
  327. let timer;
  328.  
  329. function exec(...args) {
  330. timer = null;
  331. func(...args);
  332. }
  333.  
  334. return (...args) => {
  335. if (timer) clearTimeout(timer);
  336. timer = setTimeout(exec, delay, ...args);
  337. };
  338. }
  339.  
  340. function initialize() {
  341. const panel = VM.getPanel({
  342. css: stylesheet,
  343. shadow: false
  344. });
  345. const panelStyle = panel.body.style;
  346. panelStyle.maxHeight = '50vh';
  347. panelStyle.padding = '0 8px';
  348. panelStyle.overflow = 'auto';
  349. panelStyle.overscrollBehavior = 'contain';
  350. const debouncedTranslate = debounce(event => translate$2({
  351. event,
  352. panel
  353. }));
  354. let isSelecting;
  355. document.addEventListener('mousedown', e => {
  356. isSelecting = false;
  357. if (panel.body.contains(e.target)) return;
  358. panel.hide();
  359. session = null;
  360. }, true);
  361. document.addEventListener('mousemove', () => {
  362. isSelecting = true;
  363. }, true);
  364. document.addEventListener('mouseup', e => {
  365. if (panel.body.contains(e.target) || !isSelecting) return;
  366. debouncedTranslate(e);
  367. }, true);
  368. document.addEventListener('dblclick', e => {
  369. if (panel.body.contains(e.target)) return;
  370. debouncedTranslate(e);
  371. }, true);
  372. }
  373.  
  374. initialize();
  375.  
  376. }());

QingJ © 2025

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