Inline-Translator

在每个段落之后加上中文翻译

  1. // ==UserScript==
  2. // @id Inline-Translator
  3. // @name Inline-Translator
  4. // @version 0.6
  5. // @namespace 12425
  6. // @author 12425
  7. // @description 在每个段落之后加上中文翻译
  8. // @license MIT
  9. // @include http://*
  10. // @include https://*
  11. // @run-at document-idle
  12. // @grant GM_registerMenuCommand
  13. // @grant GM_xmlhttpRequest
  14. // ==/UserScript==
  15.  
  16.  
  17. (function() {
  18.  
  19.  
  20. 'use strict';
  21.  
  22.  
  23. GM_registerMenuCommand("翻译成双语网页", () => {
  24. const root = document.body;
  25. if (root.classList.contains('_myscript_translated')) {
  26. return;
  27. }
  28. root.classList.add('_myscript_translated');
  29. translate(root);
  30. });
  31.  
  32.  
  33. const TranslationTags = new Set([
  34. 'blockquote',
  35. 'h1',
  36. 'h2',
  37. 'h3',
  38. 'h4',
  39. 'h5',
  40. 'h6',
  41. 'li',
  42. 'ol',
  43. 'p',
  44. ]);
  45.  
  46.  
  47. const TranslationClass = new Set([
  48. 'comment',
  49. ]);
  50.  
  51.  
  52. const DeleteTags = new Array([
  53. 'audio',
  54. 'iframe',
  55. 'img',
  56. 'meta',
  57. 'picture',
  58. 'script',
  59. 'svg',
  60. 'video',
  61. ]);
  62.  
  63.  
  64. const SkipTags = new Array([
  65. 'code',
  66. 'pre',
  67. ]);
  68.  
  69.  
  70. const EmptyTags = new Set([
  71. 'br',
  72. 'hr',
  73. ]);
  74.  
  75.  
  76. function fetch(text, callback, arg1, arg2, arg3) {
  77. GM_xmlhttpRequest({
  78. method: 'GET',
  79. url: `https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=zh&dt=t&q=${encodeURIComponent(text)}`,
  80. onload: function(resp) {
  81. let json = null;
  82. try {
  83. json = JSON.parse(resp.responseText)[0];
  84. } catch {
  85. return;
  86. }
  87.  
  88. if (!json) {
  89. return;
  90. }
  91.  
  92. const data = json.map(item => {
  93. return item[0].trim();
  94. }).join('');
  95. if (text !== data) {
  96. callback(data, arg1, arg2, arg3);
  97. }
  98. },
  99. });
  100. }
  101.  
  102.  
  103. function simplify(node, attrs, values) {
  104. // return false if node is removed
  105. node.querySelectorAll(DeleteTags.join(',')).forEach(el => {
  106. el.remove();
  107. });
  108. if (!removeEmptyNode(node)) {
  109. return false;
  110. }
  111. attrs.push(extractAttrs(node));
  112. node.querySelectorAll('*').forEach(el => {
  113. attrs.push(extractAttrs(el));
  114. });
  115. node.querySelectorAll(SkipTags.join(',')).forEach(el => {
  116. values.push(el.innerHTML);
  117. el.innerHTML = '';
  118. });
  119.  
  120. node.innerHTML = node.innerHTML.replace(/\s*(\n\s*)+\s*/g, ' ');
  121. return true;
  122. }
  123.  
  124.  
  125. function removeEmptyNode(node) {
  126. // return false if node is removed
  127. const children = node.querySelectorAll('*');
  128. if (children.length) {
  129. let toDelete = true;
  130. for (const child of children) {
  131. if (removeEmptyNode(child)) {
  132. toDelete = false;
  133. }
  134. }
  135. if (!toDelete) {
  136. return true;
  137. }
  138. }
  139.  
  140. if (EmptyTags.has(node.tagName)) {
  141. return true;
  142. }
  143.  
  144. if (!node.innerHTML.trim().length) {
  145. node.remove();
  146. return false;
  147. }
  148.  
  149. return true;
  150. }
  151.  
  152.  
  153. function extractAttrs(node) {
  154. let attr = {};
  155. Array.from(node.attributes).forEach(attr => {
  156. attr[attr.name] = attr.value;
  157. node.removeAttribute(attr.name);
  158. });
  159. return attr;
  160. }
  161.  
  162.  
  163. function resumeAttrs(node, attrs) {
  164. Object.keys(attrs).forEach(key => {
  165. node.setAttribute(key, attrs[key]);
  166. });
  167. }
  168.  
  169.  
  170. function updateNode(data, node, attrs, values) {
  171. let newNode = document.createElement(node.tagName);
  172. newNode.innerHTML = data;
  173. node.insertAdjacentElement('afterend', newNode);
  174. resumeAttrs(newNode, attrs[0]);
  175. node.querySelectorAll('*').forEach((el, i) => {
  176. resumeAttrs(el, attrs[i]);
  177. });
  178. node.querySelectorAll(SkipTags.join(',')).forEach((el, i) => {
  179. el.innerHTML = values[i];
  180. });
  181. if (node.parentNode.tagName !== 'SMALL') {
  182. const smallNode = document.createElement('small');
  183. node.insertAdjacentElement('afterend', smallNode);
  184. smallNode.appendChild(node);
  185. }
  186. }
  187.  
  188.  
  189. function translate(node) {
  190. const tag = node.tagName.toLowerCase();
  191. if (!tag) {
  192. return;
  193. }
  194. if (!TranslationTags.has(tag) && !Array.from(node.classList).some(c => TranslationClass.has(c))) {
  195. Array.from(node.children).forEach(translate);
  196. return;
  197. }
  198. let newNode = node.cloneNode(true);
  199. let attrs = [];
  200. let values = [];
  201. if (simplify(newNode, attrs, values)) {
  202. fetch(newNode.innerHTML, updateNode, node, attrs, values);
  203. }
  204. }
  205.  
  206.  
  207. })();