NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Google Translation for LNMTL // @namespace gtlnmtl // @version 0.7.1 // @description Neither machine learning-based nor phrase-based translation is optimal for light novels. Enjoy the best of both worlds! // @author mmtf // @match https://lnmtl.com/chapter/* // @grant GM_setValue // @grant GM_getValue // @require https://userscripts-mirror.org/scripts/source/107941.user.js#sha384=Q8t880BurrlGKTdpvYv2+da12PYnvljdiU8aJvakk1uE3QMbzb190ueXNpAUY98p // @license MIT // ==/UserScript== // Just to reduce the warnings in the editor var $ = jQuery; // Constants const errorModal = `<div class="modal fade" tabindex="-1" id="error-modal" role="dialog" style="display: block; padding-right: 16px;"> <div class="modal-dialog modal-lg" role="document"> <div class="modal-content"> <div class="modal-body"> <div> <div class="well well-lg v-cloak--hidden"> <p class="lead">An error has occurred with the Google Translate for LNMTL Userscript. Your error reporting status: </p> <p>Loading</p> </div> </div> <button class="btn btn-primary" type="button" data-dismiss="modal">Close</button></div> </div> </div> </div>` const themes = { Default: ".gt { color:white; font-size: 2.3rem; margin-bottom:42px; font-family: Roboto }", LNMTL_EN: "", LNMTL_ZN: ".gt {font-size: 150%;}", Custom: "customStyleSheet" }; // Variables for tracking application status var state = new Object(); { let rawsReplaced = false; Object.defineProperty(state, 'rawsReplaced', { enumerable:true, configurable:true, set: function(newVal){rawsReplaced = newVal; if (this.resolve && newVal) this.resolve();}, get: function () {return rawsReplaced}}); } state.testing = false; var disclaimerShown = false; // User options let customStyleSheet = GM_SuperValue.get('customStyleSheet', ""); let autoSwitchOn = GM_SuperValue.get('autoSwitchOn', false); let selectedTheme = GM_SuperValue.get('selectedTheme', 'Default'); let autoSwitchLNMTL = GM_SuperValue.get('autoSwitchLNMTL', false); /* TODO in 0.7 let advancedSettings = GM_SuperValue.get('advancedSettings', false); let waitForUGMTL = GM_SuperValue.get('waitForUGMTL', 50); let waitInBetweenReq = GM_SuperValue.get('waitInBetweenReq', 350); */ // Main functionality function main() { addGTSettings(); state.waited = true; rawsReplacedPromise().then(() => { $('.js-toggle-gt').text('Loading...'); UGMTLUpdatedWarning(); let chunks = seperateInto5KChunks(getRawParagraphs()); let translatedChunks = []; let translatePromises = []; chunks.forEach((chunk, id) => { setTimeout(() => translatePromises.push(translateText(chunk).then((tchunk) => { translatedChunks[id] = tchunk })), 350 * id); }); setTimeout(() => Promise.all(translatePromises).then(() => createUI(getMTLParagraphs(), seperateChunksIntoPars(translatedChunks))), 350 * chunks.length); }); } function addGTSettings() { let title = $('<h3> Google Translate Settings </h3>'); let checked = autoSwitchOn ? 'checked' : ''; let optionAutoswitch = $('<sub><input id="autoSwitchOn" type="checkbox" ' + checked + '></sub> <label for="autoSwitchOn">Automatically show the Google Translation after loading</label>') .on('change', function () { autoSwitchOn = $('#autoSwitchOn')[0].checked; GM_SuperValue.set('autoSwitchOn', autoSwitchOn); disclaimerChangesApplyAfterReload(); }); let checked2 = autoSwitchLNMTL ? 'checked' : ''; let optionAutoswitchLNMTL = $('<sub><input id="autoSwitchLNMTL" type="checkbox" ' + checked + '></sub> <label for="autoSwitchLNMTL">Automatically hide English LNMTL Translation after loading</label>') .on('change', function () { autoSwitchOn = $('#autoSwitchLNMTL')[0].checked; GM_SuperValue.set('autoSwitchLNMTL', autoSwitchOn); disclaimerChangesApplyAfterReload(); }); let row = $('<div class="row"/>'); let label1 = $('<label class="control-label">Theme:</label>'); let br = $('<br>'); let label2 = $('<label class="control-label">Custom Stylesheet(choose the theme Custom to use):</label>'); let col = $('<div class="col-xs-12"></div>'); let textarea = $('<textarea placeholder=".gt { color:white; font-size: 2.3rem; margin-bottom:42px; font-family: Roboto }" class="form-control" id="custom-stylesheet" rows="2" input type="text" name="custom-stylesheet" wrap="soft">').val(customStyleSheet).attr('disabled', selectedTheme != 'Custom') .on('change paste keyup', function () { customStyleSheet = $(this).val(); GM_SuperValue.set('customStyleSheet', customStyleSheet); disclaimerChangesApplyAfterReload(); }); let selectTheme = $(`<select class="form-control" id="selectTheme"> <option>Default</option> <option>LNMTL_EN</option> <option>LNMTL_ZN</option> <option>Custom</option> </select>`); selectTheme.find('option:contains(' + selectedTheme + ')').attr('selected', true); selectTheme.on('change', function () { selectedTheme = $(this).find('option:selected').text(); GM_SuperValue.set('selectedTheme', selectedTheme); disclaimerChangesApplyAfterReload(); textarea.attr('disabled', selectedTheme != 'Custom'); }); col.append(label1).append(selectTheme).append(br).append(label2).append(textarea); row.append(col); $("#chapter-display-options-modal .modal-body").append(title).append(optionAutoswitch).append('<br>').append(optionAutoswitchLNMTL).append(row); } function createUI(mtlpars, gtpars) { gtpars.forEach((par, index) => { $('.translated').eq(index).after("<div class='gt' tab-index=0><sentence data-index=" + index + ">" + par + "</sentence></div>") }); let div = $('.gt'); let styleSheetToUse = selectedTheme == 'Custom' ? customStyleSheet : themes[selectedTheme]; $('<style type="text/css"/>').text(styleSheetToUse).appendTo('head'); div.attr('data-title', function () { return mtlpars[$(this).children().first().data('index')] }).popover({ animation: true, container: '.chapter-body', placement: 'top', trigger: 'click' }); div.hide(); $('.js-toggle-gt').click(function () { div.animate({ 'height': 'toggle', 'opacity': 'toggle' }); $(this).toggleClass('btn-primary').toggleClass('btn-default'); }).text('GT').addClass('btn-default').removeClass('btn-disabled'); if (autoSwitchOn) { div.animate({ 'height': 'toggle', 'opacity': 'toggle' }); $('.js-toggle-gt').toggleClass('btn-primary').toggleClass('btn-default'); } if (autoSwitchLNMTL) { $('.translated').animate({ 'height': 'toggle', 'opacity': 'toggle' }); $('.js-toggle-translated').toggleClass('btn-primary').toggleClass('btn-default'); } } // Utilities function disclaimerChangesApplyAfterReload() { if (disclaimerShown) return; let disclaimer = $(`<div class="alert alert-warning" role="alert"> <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> <span class="sr-only">Note:</span> Changes apply after reloading the page. </div>`); $('#custom-stylesheet').after(disclaimer); disclaimerShown = true; } function getRawParagraphs() { return $('.original').text().trim().split('\n') } function getMTLParagraphs() { return $('.translated').text().trim().split('\n'); } function isUGMTLReplacingRaws() { if ($('#replaceInOriginal')[0] && $('#replaceInOriginal')[0].checked) { return true; } return false; } function observerCallback(mutationList, observer) { mutationList.forEach((mutation) => { switch (mutation.type) { case 'attributes': if (mutation.attributeName == "data-title") { setTimeout(function(){state.rawsReplaced = true}, 50); } break; } }); } function rawsObserver() { var observerOptions = { childList: false, attributes: true } var observer = new MutationObserver(observerCallback); if($('.original t').get(-1)!=undefined) { observer.observe($('.original t').get(-1), observerOptions); } observeDocument(); if(window.sessionStorage.getItem('userjs_UGMTLComplete') && window.sessionStorage.getItem('userjs_UGMTLComplete') > window.performance.timing.fetchStart) { state.rawsReplaced = true; state.UGMTLUpdated = true; } } function observeDocument() { document.addEventListener('userjs_UGMTLComplete', function(){ devLog('userjs_UGMTLComplete'); state.rawsReplaced = true; state.UGMTLUpdated = true; }, false); } function rawsReplacedPromise() { if (isUGMTLReplacingRaws()) { if (state.rawsReplaced) { return Promise.resolve(true); } else { return new Promise(function(res) {state.resolve = res}); } } else { return new Promise((res) => { rawsReplace(); res(); }); } } function translateText(text) { return new Promise((res, rej) => $.ajax('https://translate.googleapis.com/translate_a/single?client=gtx&sl=zh-CN&tl=en&dt=t', { method: 'POST', data: { q: text }, dataType: "json" }).done((t) => { let paragraph = ""; for (let i = 0; i < t[0].length; i++) { paragraph += t[0][i][0]; } return res(paragraph); }).fail((err) => { state.errorCode = 1; console.log( "An error occurred while fetching the translations:" + err ); console.error(err) reportError(err); })); } function seperateInto5KChunks(paragraphs) { let chunks = []; let currentchunk = ""; for (let i = 0; i < paragraphs.length; i++) { if ((currentchunk + paragraphs[i]).length >= 5000) { chunks.push(currentchunk); currentchunk = paragraphs[i]; } else { currentchunk = currentchunk + "\n\n" + paragraphs[i]; } } if (paragraphs.length != 0) { chunks.push(currentchunk); } return chunks; } function seperateChunksIntoPars(chunks) { let pars = []; chunks.forEach((chunk) => chunk.split('\n\n').forEach((par) => pars.push(par))); return pars; } function rawsReplace() { $('.original t').each(function () { let mytext = $(this).text(); $(this).text($(this).attr('data-title').replace(/\(\w+\)/g, '')); $(this).attr('data-title', mytext); }); // add whitespace between two english words $('.original').find('t').filter(function (index) { var prev = $(this).get(0).previousSibling; return prev ? $(this).get(0).previousSibling.nodeName == 'T' : false; }).each(function () { $(this).text(' ' + $(this).text()); }); } function UGMTLUpdatedWarning(){ devLog(state.UGMTLUpdated, isUGMTLReplacingRaws()); if (!state.UGMTLUpdated && isUGMTLReplacingRaws()) { let disclaimer = $(`<div class="alert alert-danger" role="alert"> <span class="glyphicon glyphicon-exclamation-sign"></span> <span class="sr-only">Warning:</span> Please update UGMTL to 1.6.8+ for better and stabler integration between the two extensions. </div>`); $('#chapter-display-options-modal > div > div > div.modal-body > h3:nth-child(10)').after(disclaimer); } } function devLog(message) { return state.testing ? console.log(message) : false; } function reportError(error) { if($('#error-modal').get(0)) { return postForm(error); } $('.js-toggle-gt').text('ERROR').addClass('btn-danger'); let errmodal = $(errorModal); $('body').append(errmodal); errmodal.modal().modal('hide'); $('.js-toggle-gt').on('click', errmodal.modal.bind(errmodal, 'toggle')); postForm(error).always(() => { errmodal.find('p').eq(1).text('Your error has been reported'); }); } function postForm(error) { return $.ajax({ url: "https://docs.google.com/forms/u/0/d/e/1FAIpQLSf9_pdqwA36TaHjxxCKeT8iv-eLhXIx1DO2bxD7V7tKG3UXXw/formResponse", data: {"entry.1958338513": isUGMTLReplacingRaws()?"true" : (state.waited?"false":"not waited"), "entry.743789703": state.UGMTLUpdated?"true":"false", "entry.879260605":state.rawsReplaced?"true":"false", "entry.1237602720":state.errorCode, "entry.765414833": error.name +"\n"+ error.message}, type: "POST", dataType: "xml" }) } (function () { try { // Add UI indicator for loading $('.js-toggle-original').after('<button class="btn btn-disabled js-toggle-gt">Waiting for UGMTL...</button>'); // Observe the phrases to see if they change rawsObserver(); // Wait for UGMTL and then start the program setTimeout(main, 50); } catch (error) { reportError(error); } })(); /* Reconsider using the inherent google translate api instead of googleapis.com API. (Requires CORS-bypass/GM_xmlHttpRequest) let onload = function (res) {console.log(res)}; let request = {method:"GET", url:"https://translate.google.com/translate_a/single?client=gtx&sl=en&tl=zh-CN&dt=t&q=hello%20my%20name%20is%20whatever", onload:onload}; GM_xmlhttpRequest(request); */