片假名终结者

在网页中的日语外来语上方标注英文原词

安装此脚本?
作者推荐脚本

您可能也喜欢CNKI PDF Download

安装此脚本
  1. // ==UserScript==
  2. // @name Katakana Terminator
  3. // @description Convert gairaigo (Japanese loan words) back to English
  4. // @author Arnie97
  5. // @license MIT
  6. // @copyright 2017-2021, Katakana Terminator Contributors (https://github.com/Arnie97/katakana-terminator/graphs/contributors)
  7. // @namespace https://github.com/Arnie97
  8. // @homepageURL https://github.com/Arnie97/katakana-terminator
  9. // @supportURL https://gf.qytechs.cn/scripts/33268/feedback
  10. // @icon https://upload.wikimedia.org/wikipedia/commons/2/28/Ja-Ruby.png
  11. // @match *://*/*
  12. // @exclude *://*.bilibili.com/video/*
  13. // @grant GM.xmlHttpRequest
  14. // @grant GM_xmlhttpRequest
  15. // @grant GM_addStyle
  16. // @connect translate.googleapis.com
  17. // @version 2022.02.18
  18. // @name:ja-JP カタカナターミネーター
  19. // @name:zh-CN 片假名终结者
  20. // @description:zh-CN 在网页中的日语外来语上方标注英文原词
  21. // ==/UserScript==
  22.  
  23. // define some shorthands
  24. var _ = document;
  25.  
  26. var queue = {}; // {"カタカナ": [rtNodeA, rtNodeB]}
  27. var cachedTranslations = {}; // {"ターミネーター": "Terminator"}
  28. var newNodes = [_.body];
  29.  
  30. // Recursively traverse the given node and its descendants (Depth-first search)
  31. function scanTextNodes(node) {
  32. // The node could have been detached from the DOM tree
  33. if (!node.parentNode || !_.body.contains(node)) {
  34. return;
  35. }
  36.  
  37. // Ignore text boxes and echoes
  38. var excludeTags = {ruby: true, script: true, select: true, textarea: true};
  39.  
  40. switch (node.nodeType) {
  41. case Node.ELEMENT_NODE:
  42. if (node.tagName.toLowerCase() in excludeTags || node.isContentEditable) {
  43. return;
  44. }
  45. return node.childNodes.forEach(scanTextNodes);
  46.  
  47. case Node.TEXT_NODE:
  48. while ((node = addRuby(node)));
  49. }
  50. }
  51.  
  52. // Recursively add ruby tags to text nodes
  53. // Inspired by http://www.the-art-of-web.com/javascript/search-highlight/
  54. function addRuby(node) {
  55. var katakana = /[\u30A1-\u30FA\u30FD-\u30FF][\u3099\u309A\u30A1-\u30FF]*[\u3099\u309A\u30A1-\u30FA\u30FC-\u30FF]|[\uFF66-\uFF6F\uFF71-\uFF9D][\uFF65-\uFF9F]*[\uFF66-\uFF9F]/, match;
  56. if (!node.nodeValue || !(match = katakana.exec(node.nodeValue))) {
  57. return false;
  58. }
  59. var ruby = _.createElement('ruby');
  60. ruby.appendChild(_.createTextNode(match[0]));
  61. var rt = _.createElement('rt');
  62. rt.classList.add('katakana-terminator-rt');
  63. ruby.appendChild(rt);
  64.  
  65. // Append the ruby title node to the pending-translation queue
  66. queue[match[0]] = queue[match[0]] || [];
  67. queue[match[0]].push(rt);
  68.  
  69. // <span>[startカナmiddleテストend]</span> =>
  70. // <span>start<ruby>カナ<rt data-rt="Kana"></rt></ruby>[middleテストend]</span>
  71. var after = node.splitText(match.index);
  72. node.parentNode.insertBefore(ruby, after);
  73. after.nodeValue = after.nodeValue.substring(match[0].length);
  74. return after;
  75. }
  76.  
  77. // Split word list into chunks to limit the length of API requests
  78. function translateTextNodes() {
  79. var apiRequestCount = 0;
  80. var phraseCount = 0;
  81. var chunkSize = 200;
  82. var chunk = [];
  83.  
  84. for (var phrase in queue) {
  85. phraseCount++;
  86. if (phrase in cachedTranslations) {
  87. updateRubyByCachedTranslations(phrase);
  88. continue;
  89. }
  90.  
  91. chunk.push(phrase);
  92. if (chunk.length >= chunkSize) {
  93. apiRequestCount++;
  94. googleTranslate('ja', 'en', chunk);
  95. chunk = [];
  96. }
  97. }
  98.  
  99. if (chunk.length) {
  100. apiRequestCount++;
  101. googleTranslate('ja', 'en', chunk);
  102. }
  103.  
  104. if (phraseCount) {
  105. console.debug('Katakana Terminator:', phraseCount, 'phrases translated in', apiRequestCount, 'requests, frame', window.location.href);
  106. }
  107. }
  108.  
  109. // {"keyA": 1, "keyB": 2} => "?keyA=1&keyB=2"
  110. function buildQueryString(params) {
  111. return '?' + Object.keys(params).map(function(k) {
  112. return encodeURIComponent(k) + '=' + encodeURIComponent(params[k]);
  113. }).join('&');
  114. }
  115.  
  116. // Google Dictionary API, https://github.com/ssut/py-googletrans/issues/268
  117. function googleTranslate(srcLang, destLang, phrases) {
  118. // Prevent duplicate HTTP requests before the request completes
  119. phrases.forEach(function(phrase) {
  120. cachedTranslations[phrase] = null;
  121. });
  122.  
  123. var joinedText = phrases.join('\n').replace(/\s+$/, ''),
  124. api = 'https://translate.googleapis.com/translate_a/single',
  125. params = {
  126. client: 'gtx',
  127. dt: 't',
  128. sl: srcLang,
  129. tl: destLang,
  130. q: joinedText,
  131. };
  132.  
  133. GM_xmlhttpRequest({
  134. method: "GET",
  135. url: api + buildQueryString(params),
  136. onload: function(dom) {
  137. try {
  138. var resp = JSON.parse(dom.responseText.replace("'", '\u2019'));
  139. } catch (err) {
  140. console.error('Katakana Terminator: invalid response', dom.responseText);
  141. return;
  142. }
  143. resp[0].forEach(function(item) {
  144. var translated = item[0].replace(/\s+$/, ''),
  145. original = item[1].replace(/\s+$/, '');
  146. cachedTranslations[original] = translated;
  147. updateRubyByCachedTranslations(original);
  148. });
  149. },
  150. onerror: function(dom) {
  151. console.error('Katakana Terminator: request error', dom.statusText);
  152. },
  153. });
  154. }
  155.  
  156. // Clear the pending-translation queue
  157. function updateRubyByCachedTranslations(phrase) {
  158. if (!cachedTranslations[phrase]) {
  159. return;
  160. }
  161. (queue[phrase] || []).forEach(function(node) {
  162. node.dataset.rt = cachedTranslations[phrase];
  163. });
  164. delete queue[phrase];
  165. }
  166.  
  167. // Watch newly added DOM nodes, and save them for later use
  168. function mutationHandler(mutationList) {
  169. mutationList.forEach(function(mutationRecord) {
  170. mutationRecord.addedNodes.forEach(function(node) {
  171. newNodes.push(node);
  172. });
  173. });
  174. }
  175.  
  176. function main() {
  177. GM_addStyle("rt.katakana-terminator-rt::before { content: attr(data-rt); }");
  178.  
  179. var observer = new MutationObserver(mutationHandler);
  180. observer.observe(_.body, {childList: true, subtree: true});
  181.  
  182. function rescanTextNodes() {
  183. // Deplete buffered mutations
  184. mutationHandler(observer.takeRecords());
  185. if (!newNodes.length) {
  186. return;
  187. }
  188.  
  189. console.debug('Katakana Terminator:', newNodes.length, 'new nodes were added, frame', window.location.href);
  190. newNodes.forEach(scanTextNodes);
  191. newNodes.length = 0;
  192. translateTextNodes();
  193. }
  194.  
  195. // Limit the frequency of API requests
  196. rescanTextNodes();
  197. setInterval(rescanTextNodes, 500);
  198. }
  199.  
  200. // Polyfill for Greasemonkey 4
  201. if (typeof GM_xmlhttpRequest === 'undefined' &&
  202. typeof GM === 'object' && typeof GM.xmlHttpRequest === 'function') {
  203. GM_xmlhttpRequest = GM.xmlHttpRequest;
  204. }
  205.  
  206. if (typeof GM_addStyle === 'undefined') {
  207. GM_addStyle = function(css) {
  208. var head = _.getElementsByTagName('head')[0];
  209. if (!head) {
  210. return null;
  211. }
  212.  
  213. var style = _.createElement('style');
  214. style.setAttribute('type', 'text/css');
  215. style.textContent = css;
  216. head.appendChild(style);
  217. return style;
  218. };
  219. }
  220.  
  221. // Polyfill for ES5
  222. if (typeof NodeList.prototype.forEach === 'undefined') {
  223. NodeList.prototype.forEach = function(callback, thisArg) {
  224. thisArg = thisArg || window;
  225. for (var i = 0; i < this.length; i++) {
  226. callback.call(thisArg, this[i], i, this);
  227. }
  228. };
  229. }
  230.  
  231. main();

QingJ © 2025

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