Translator for Whatsapp

Translator for Whatsapp web

Від 17.03.2017. Дивіться остання версія.

  1. // ==UserScript==
  2. // @name Translator for Whatsapp
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.1
  5. // @description Translator for Whatsapp web
  6. // @author JedLiu
  7. // @match https://web.whatsapp.com/*
  8. // @run-at document-start
  9. // @grant GM_setValue
  10. // @grant GM_getValue
  11. // @grant GM_xmlhttpRequest
  12. // @connect translate.googleapis.com
  13. // @require https://code.jquery.com/jquery-latest.js
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18.  
  19. //All supported languages
  20. //Remove // if you want to add new one
  21. //所有支持的预言
  22. //将前面的//符号移除即可添加
  23. var all_languages = [
  24. //{id:'af', name:'Afrikaans'},
  25. //{id:'sq', name:'Albanian'},
  26. //{id:'ar', name:'Arabic'},
  27. //{id:'hy', name:'Armenian'},
  28. //{id:'az', name:'Azerbaijani'},
  29. //{id:'eu', name:'Basque'},
  30. //{id:'be', name:'Belarusian'},
  31. //{id:'bn', name:'Bengali'},
  32. //{id:'bs', name:'Bosnian'},
  33. //{id:'bg', name:'Bulgarian'},
  34. //{id:'ca', name:'Catalan'},
  35. //{id:'ceb', name:'Cebuano'},
  36. //{id:'ny', name:'Chichewa'},
  37. //{id:'zh-CN', name:'Chinese Simplified'},
  38. //{id:'zh-TW', name:'Chinese Traditional'},
  39. //{id:'co', name:'Corsican'},
  40. //{id:'hr', name:'Croatian'},
  41. //{id:'cs', name:'Czech'},
  42. //{id:'da', name:'Danish'},
  43. //{id:'nl', name:'Dutch'},
  44. //{id:'en', name:'English'},
  45. //{id:'eo', name:'Esperanto'},
  46. //{id:'et', name:'Estonian'},
  47. //{id:'tl', name:'Filipino'},
  48. //{id:'fi', name:'Finnish'},
  49. {id:'fr', name:'French'},
  50. //{id:'fy', name:'Frisian'},
  51. //{id:'gl', name:'Galician'},
  52. //{id:'ka', name:'Georgian'},
  53. {id:'de', name:'German'},
  54. //{id:'el', name:'Greek'},
  55. //{id:'gu', name:'Gujarati'},
  56. //{id:'ht', name:'Haitian Creole'},
  57. //{id:'ha', name:'Hausa'},
  58. //{id:'haw', name:'Hawaiian'},
  59. //{id:'iw', name:'Hebrew'},
  60. //{id:'hi', name:'Hindi'},
  61. //{id:'hmn', name:'Hmong'},
  62. //{id:'hu', name:'Hungarian'},
  63. //{id:'is', name:'Icelandic'},
  64. //{id:'ig', name:'Igbo'},
  65. //{id:'id', name:'Indonesian'},
  66. //{id:'ga', name:'Irish'},
  67. {id:'it', name:'Italian'},
  68. {id:'ja', name:'Japanese'},
  69. //{id:'jw', name:'Javanese'},
  70. //{id:'kn', name:'Kannada'},
  71. //{id:'kk', name:'Kazakh'},
  72. //{id:'km', name:'Khmer'},
  73. {id:'ko', name:'Korean'},
  74. //{id:'ku', name:'Kurdish (Kurmanji)'},
  75. //{id:'ky', name:'Kyrgyz'},
  76. //{id:'lo', name:'Lao'},
  77. //{id:'la', name:'Latin'},
  78. //{id:'lv', name:'Latvian'},
  79. //{id:'lt', name:'Lithuanian'},
  80. //{id:'lb', name:'Luxembourgish'},
  81. //{id:'mk', name:'Macedonian'},
  82. //{id:'mg', name:'Malagasy'},
  83. //{id:'ms', name:'Malay'},
  84. //{id:'ml', name:'Malayalam'},
  85. //{id:'mt', name:'Maltese'},
  86. //{id:'mi', name:'Maori'},
  87. //{id:'mr', name:'Marathi'},
  88. //{id:'mn', name:'Mongolian'},
  89. //{id:'my', name:'Myanmar (Burmese)'},
  90. //{id:'ne', name:'Nepali'},
  91. //{id:'no', name:'Norwegian'},
  92. //{id:'ps', name:'Pashto'},
  93. //{id:'fa', name:'Persian'},
  94. //{id:'pl', name:'Polish'},
  95. {id:'pt', name:'Portuguese'},
  96. //{id:'ma', name:'Punjabi'},
  97. //{id:'ro', name:'Romanian'},
  98. {id:'ru', name:'Russian'},
  99. //{id:'sm', name:'Samoan'},
  100. //{id:'gd', name:'Scots Gaelic'},
  101. //{id:'sr', name:'Serbian'},
  102. //{id:'st', name:'Sesotho'},
  103. //{id:'sn', name:'Shona'},
  104. //{id:'sd', name:'Sindhi'},
  105. //{id:'si', name:'Sinhala'},
  106. //{id:'sk', name:'Slovak'},
  107. //{id:'sl', name:'Slovenian'},
  108. //{id:'so', name:'Somali'},
  109. {id:'es', name:'Spanish'},
  110. //{id:'su', name:'Sudanese'},
  111. //{id:'sw', name:'Swahili'},
  112. //{id:'sv', name:'Swedish'},
  113. //{id:'tg', name:'Tajik'},
  114. //{id:'ta', name:'Tamil'},
  115. //{id:'te', name:'Telugu'},
  116. //{id:'th', name:'Thai'},
  117. //{id:'tr', name:'Turkish'},
  118. //{id:'uk', name:'Ukrainian'},
  119. //{id:'ur', name:'Urdu'},
  120. //{id:'uz', name:'Uzbek'},
  121. //{id:'vi', name:'Vietnamese'},
  122. //{id:'cy', name:'Welsh'},
  123. //{id:'xh', name:'Xhosa'},
  124. //{id:'yi', name:'Yiddish'},
  125. //{id:'yo', name:'Yoruba'},
  126. //{id:'zu', name:'Zulu'}
  127. ];
  128.  
  129. var $ = $ || window.$,
  130. addListenerInterval = null,
  131. translateInterval = null,
  132. translateTimeout = null,
  133. translate_enabled = true,
  134. translate_ready = false,
  135. translate_string = '',
  136. custom_style = '.language_selected{background-color: #00bfa5;} .language_menu a:hover{background-color: #f4f5f5;}',
  137. image_uri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAFZklEQVR42sWXe0xTVxzHv5drWXkIKBQfE61Y2DIyR3Sy+FrqRgRF1IFD54IawU03dUE2I3ukxTgqPuYjK6KOZRndnFPYJoEpcUunokNxYnFT4wOQp9A/BshD4N6zc1sKvaWV0C7xhEt/59Hf93N+5/c7uWXwlBtjMV6JeBnz5r0KhmGG/BIhxKYPtLW1Qaf7Aa30c9gA4eFTof+9KJ5l2WzaDRhakIhs+sdT6/T5C5cSFi9JaB82wHsb1iFz5/ZmJ8UFy2Jv9JdN1DoLQGwnrcXWvbsJsYsWmB574n1jan/ZpHSXAU4VFA0CKCj4FaGhCjwXGiJI9s8LY6Ehij4gog4IlLsOkPzORptQiDvEajwmJhqLYqL6AWRjJrsGIDjiOF6kah2Jzs4upKRuwxtLYxE1/3WLcD9A4Nhg5wFsE+7q1Wuoq2tAZKQSUqnUdOaGihvQao8gdcsmKBTBgwDGjJviHMBOTbo40NRhebkBWYe+wpLFMViwYD4d47F7zwF0d3cjbVtq/zprgLHjFcMH2LAhGZmagRywympos47i5s1b2PpRCm78/Q9OnSrCh6mbIZdPtBUHzxP1+AkhrgHY1nl7ewcyd+9Da2sbOjs6sGLFMsydM8ueuCkCzwaFOg9g75Lp6elBru4YSkuvwF3ijqTk1XAPDEdF3TOgmn3AZoDG1hH6kkov/QCY7RGZPBtbKs9/c/v4inbucWsfwPpkUQ5YFldVV+Pb3GMwNhuR8GYcymhSGh7wePTCDirJ2LsJRZ+Dx/vtouritJjGUu1gAGFRVfUDFBf/RqugHMHBcqxKfAsyWQDdJY/ss904V69wRVx4+No/NGz9+Z1iAMuiO3fuQvfdcURHRSIiYroo20+W+yLvuo8r4ia77pyGsQJIgiYjnVg7fdwL3Gtm8fyYHtE5mgF8HYrHvsRhpBQouQPcb4ZDEAGg4UKmGWC9APB5Oken3Czi2wtHoqJ+BD6JbsX0oMf9DgRx4bEnPmEUQXZiL4RXCv0tQFPIOIwC3f0AwPLl8cjO2l9IJxZaxP+qkZhCL2EJPp7fgmlBXaZMF8TzDb52w540h8PicIILdPezFMDKwwxaOuxXBBUfAJBIJMjSfuH12jzlmtzLHgG/XJcq3T39lJaqkDId6u3R9Qj07oXOMFlZUumttBVn3QhykzhanoDuIoNDqwgO64GTVwaLC02IQGPJLth9/5oYd1w1clyY2lsmN/Vr9RlMnT7DZM/57KGKlfqqbRNubghBWgyPT/MZXL4PHHybwENCo5IDel+IxS0ReCIA6+6t9vSfBAHCGmBhRqPqEe+nts32jDgOE0YDiUcYcPSoFk4lSIli8IGOh6FGLG4G2MU8vOgAYErCCRXv5qkWbAHiRb8qhmm6aJpbtHKLqvi2p9o6w8f6EOSs5XGvCbh011xJnu7AshkMzlQQaAo4kbjw0N1TgN32AdIOnJHPnDmbniAm2Zvvpf5+vsbQMjU7XTObR0IEUFHTV8Z96wJ9gNFeQPxBDo+6xFeyANB0yQGAj18AdmQVyuWKsCdCHCsFGlpo8q2jZfUvkPK9OOGm0W/uXcli32kOP5Vxomqg4acAe+DwR8BQEMIR5JURzJhMoF4KaM8S5F8Vh9mNes/bzMLYRrD2aI8Ijoafaf7zCQAWCNX+fPm4oODVljEvb19lpZFRCuI9nKOrVpxw9ubp7ocGsNdSc2pVl2t91a6IC30q7hzAjK0177NSvy9dEafNSBNQZizdO3wARdzXXv5hy36kzqKFY3ZGnPDc+qoTS/I6G8qGD8C4SeAfFg/WY5TpzZ2Y/1l+qgzYZKAcbd+2O+pK0dVkMPsbLsD/3f4DRTYAbJ65vloAAAAASUVORK5CYII=',
  138. custom_html = '<div class="block-compose tranlate-bottom"><div style="padding-bottom:6px;padding-right:10px;color: green;"><img alt="Translator" draggable="false" src="' + image_uri + '" style="width:30px;height:30px;"></div><div tabindex="-1" class="input-container"><div class="input" dir="auto"></div></div></div>',
  139. source_language = 'en',
  140. dest_language = 'es',
  141. html_language1 = '<div class="menu-item" style="display:table"><img alt="Translator" draggable="false" src="data:'+ image_uri +'" style="width:25px;height:25px;" /><span style="padding-left:10px; display:table-cell; vertical-align:middle;" id="destination_language_label"></span><span><div tabindex="-1" class="dropdown dropdown-right" style="transform-origin: right top 0px; transform: scale(1); opacity: 1; display:none" id="translate_menu"><ul> '+
  142. '<li tabindex="-1" class="menu-item menu-shortcut language_menu language_selected" style="opacity: 1; transform: translateY(0px);" data-language="off"><a class="ellipsify" title="Turn off">Off</a></li> ',
  143. username = '',
  144. is_debug = true;
  145.  
  146.  
  147. //For menu html
  148. for(var i=0;i<all_languages.length;i++){
  149. html_language1 = html_language1 +'<li tabindex="-1" class="menu-item menu-shortcut language_menu" style="opacity: 1; transform: translateY(0px);" data-language="' + all_languages[i].id + '"><a class="ellipsify" title="French">' + all_languages[i].name + '</a></li>';
  150. }
  151. html_language1= html_language1 + '</ul></div></span></div>';
  152.  
  153. //Add style
  154. var customStyleNode = document.createElement('style');
  155. customStyleNode.textContent = custom_style;
  156. document.querySelector('head').appendChild(customStyleNode);
  157.  
  158. //Replace all function
  159. function replaceAll(str, find, replace) {
  160. return str.replace(new RegExp(find, 'g'), replace);
  161. }
  162.  
  163. //Show debug
  164. var debugMessage = function(mes){
  165. if(is_debug)
  166. console.info(mes);
  167. };
  168.  
  169. //Send translated content
  170. var dispatchTranslateChange = function(){
  171. var isEmojiVisible = false;
  172. //If emoji is visible
  173. if($('.compose-box-items-overlay-container div:first').children().length === 0 ){
  174. $('.compose-box-items-overlay-container').hide();
  175. $('.btn-emoji').click();
  176. } else {
  177. isEmojiVisible = true;
  178. }
  179. //Add empty emoji
  180. $('.emoji-panel-body div div').append('<span data-unicode=" " tabindex="-1" data-emoji-index="0" class="emojik" id="emptyEmojik"></span>');
  181. //Click to send
  182. $("#emptyEmojik").trigger("click");
  183. if(!isEmojiVisible){
  184. $('.btn-emoji').click();
  185. setTimeout(function(){
  186. $('.compose-box-items-overlay-container').removeAttr("style");
  187. $('.send-container').trigger('click');
  188. },500);
  189. }
  190. };
  191.  
  192. //Do translation
  193. //sl - source language
  194. //dl - target language
  195. //txt - content to be translated
  196. //cb - callback after translation
  197. var translate = function(sl,dl,txt,cb){
  198. debugMessage('Source:'+ txt);
  199. GM_xmlhttpRequest({
  200. method: "GET",
  201. url: "https://translate.googleapis.com/translate_a/single?client=gtx&sl="+ sl + "&tl=" + dl +"&dt=t&q=" + encodeURI(txt),
  202. onload: function(response) {
  203. //replace the \n
  204. var _r_text = replaceAll(response.responseText, '\n"', '"');
  205. var _r = eval(_r_text);
  206. translate_string = '';
  207. for(var i=0; i<_r[0].length;i++){
  208. translate_string += _r[0][i][0];
  209. }
  210. debugMessage('Translation:'+translate_string);
  211. cb.apply({text: translate_string});
  212. //translate_ready = true;
  213. }
  214. });
  215. };
  216.  
  217. //Bind to get user input
  218. var onDomSubtreeModified = function(){
  219. var $_translate_input_1 = $('.tranlate-bottom').find('.input');
  220. $_translate_input_1.html('Typing...');
  221. translate_ready = false;
  222. var _this = $(this);
  223. delay(function(){
  224. var _input = $.trim(_this.text());
  225. if(_input){
  226. translate(source_language, dest_language, _input, function(){
  227. $_translate_input_1.html(this.text);
  228. translate_ready = true;
  229. });
  230. }else{
  231. $_translate_input_1.html('');
  232. }
  233. }, 1000);
  234. };
  235.  
  236. //Bind to send the translated content
  237. var onEnterKeyPressed = function( event ) {
  238. if (event.which == 13 && translate_enabled) {
  239. debugMessage('Waiting translation');
  240. event.preventDefault();
  241. var _this = $(this);
  242. translateInterval = setInterval(function(){
  243. if(translate_ready){
  244. _this.html(translate_string);
  245. dispatchTranslateChange();
  246. translate_string = '';
  247. translate_ready = false;
  248. debugMessage('Message sent');
  249. clearInterval(translateInterval);
  250. }
  251. }, 100);
  252. }
  253. };
  254.  
  255. //Add translation bindings
  256. var addTranslateFunc = function(l){
  257. if(!username){
  258. alert('Can not get the username');
  259. return;
  260. }
  261. var _language = GM_getValue(username);
  262. if(l){
  263. _language = l;
  264. }else if(!_language){
  265. _language = 'off';
  266. }
  267.  
  268. //Menu
  269. debugMessage('Set language to:' + _language);
  270. $('.language_menu.language_selected').removeClass('language_selected');
  271. $('.language_menu[data-language="'+ _language +'"]').addClass('language_selected');
  272. $('#destination_language_label').html($('.language_selected').text());
  273. dest_language = _language;
  274. GM_setValue(username, _language);
  275.  
  276. //Add translation input
  277. if(_language !== 'off' && $('.tranlate-bottom').length === 0){
  278. $('.pane-chat-footer').append($(custom_html));
  279.  
  280. //binding
  281. $('div.input[contenteditable]')
  282. .on('DOMSubtreeModified', onDomSubtreeModified)
  283. .on('keydown', onEnterKeyPressed);
  284.  
  285. //translate sent or received messages
  286. $('.message-list').on('click', '.selectable-text', function(){
  287. var $_t_this = $(this);
  288. translate('auto', source_language, $(this).text(), function(){
  289. $_t_this.html(this.text);
  290. });
  291. });
  292. }else if(_language === 'off' && $('.tranlate-bottom').length !== 0){
  293. //remove bindings
  294. $('.tranlate-bottom').remove();
  295. $('div.input[contenteditable]')
  296. .off('DOMSubtreeModified', onDomSubtreeModified)
  297. .off('keydown', onEnterKeyPressed);
  298. }
  299. };
  300.  
  301. //Add linster when user activates a new chat
  302. addListenerInterval = setInterval(function(){
  303. var $_div_chat = $('div.chat');
  304. if($_div_chat.length){
  305. $_div_chat.on('click', function(){
  306.  
  307. //Get the username
  308. username = escape($(this).find('.chat-title').text());
  309.  
  310. //Return if the translation input is added
  311. if($('.language_menu').length>0)
  312. return;
  313.  
  314. //Add translation input
  315. var _menu = $(html_language1).on('click', function(){
  316. $(this).find('div').slideToggle();
  317. });
  318. _menu.find('.language_menu').on('click', function(){
  319. addTranslateFunc($(this).data('language'));
  320. });
  321. $('.pane-chat-controls div:first').prepend(_menu);
  322.  
  323. //Apply the translate function
  324. addTranslateFunc();
  325. });
  326.  
  327. clearInterval(addListenerInterval);
  328. }
  329. }, 1000);
  330.  
  331. //Delay function
  332. var delay = (function(){
  333. var timer = 0;
  334. return function(callback, ms){
  335. clearTimeout (timer);
  336. timer = setTimeout(callback, ms);
  337. };
  338. })();
  339. })();