decembre

GM - Greasyfork – Auto-Translator (v16) - TEST 3 for CITRUS - Draggable+Keepe Wrape line

Nov 8th, 2025 (edited)
540
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
CSS 10.47 KB | None | 0 0
  1. // ==UserScript==
  2. // @name         Greasyfork – Auto-Translator (v16.2) - TEST for CITRUS - Draggable, Wrape line and Decription at once - IN USE -Stay pb retranslate when show original
  3. // @namespace    http://tampermonkey.net/
  4. // @version      16.2
  5. // @description  Auto-translates non-Latin-language text on Greasyfork listing, user profile, and script info pages while preserving HTML structure in #additional-info.
  6. // @match        https://greasyfork.org/*/users/*
  7. // @match        https://greasyfork.org/*/scripts*
  8. // @match        https://greasyfork.org/*/users/*?sort=*
  9. // @match        https://greasyfork.org/*/users/*-*?sort=*
  10. // @grant        GM_xmlhttpRequest
  11. // @grant        GM_addStyle
  12. // @grant        GM_setValue
  13. // @grant        GM_getValue
  14. // @connect      translate.googleapis.com
  15. // @downloadURL  https://update.greasyfork.org/scripts/555224/Greasyfork%20%E2%80%93%20Auto-Translator%20%28v16%29.user.js
  16. // @updateURL    https://update.greasyfork.org/scripts/555224/Greasyfork%20%E2%80%93%20Auto-Translator%20%28v16%29.meta.js
  17. // ==/UserScript==
  18.  
  19. (() => {
  20.   'use strict';
  21.  
  22.   const SELECTORS = {
  23.     listingTitle: '#script-table tbody td:nth-child(2) .thetitle > a > b',
  24.     listingDesc: 'body[pagetype="ListingPage"] #script-table td .thetitle + div',
  25.     profileDesc: 'body[pagetype="UserProfile"] #script-table td .thetitle + div',
  26.     infoTitle: '.width-constraint #script-info > #script-content .gf-formatted-text span.gf-item',
  27.     infoDesc: '.width-constraint #script-info > #script-content #additional-info',
  28.     scriptDescription: '.width-constraint:has(#additional-info) #script-info header p#script-description.script-description',
  29.     additionalInfoP: '#additional-info p',
  30.     headerTitle: 'header h2',
  31.     scriptInfoTitle: '#script-info h2'
  32.   };
  33.  
  34.   const TRANSLATED_CLASS = 'gf-translated-text';
  35.  
  36.   const state = {
  37.     queue: [],
  38.     processing: false,
  39.     processed: new WeakSet(),
  40.     count: 0,
  41.     autoTranslate: true
  42.   };
  43.  
  44.   const EN_SKIP = /^[a-zA-Z\s\d\.,;:!?()\-"']+$/;
  45.   const LATIN_SKIP = /^[a-zA-ZÀ-ÿ\s\d\.,;:!?¿¡()\-"']+$/;
  46.   const NON_LATIN = /[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f\u0400-\u04ff\u0600-\u06ff\u0e00-\u0e7f]/;
  47.  
  48.   GM_addStyle(`
  49.     #gf-panel {
  50.       position: fixed;
  51.       top: 20px;
  52.       right: 20px;
  53.       z-index: 999999;
  54.       background: #111;
  55.       color: #fff;
  56.       padding: 10px;
  57.       border-radius: 8px;
  58.       font-size: 13px;
  59.       max-width: 280px;
  60.       box-shadow: 0 4px 14px rgba(0,0,0,.35);
  61.       cursor: move;
  62.       user-select: none;
  63.     }
  64.     #gf-panel button {
  65.       margin-top: 6px;
  66.       margin-right: 6px;
  67.       cursor: pointer;
  68.     }
  69.     .${TRANSLATED_CLASS} {
  70.       direction: ltr !important;
  71.       unicode-bidi: plaintext !important;
  72.     }
  73.     #additional-info, #additional-info p, #additional-info br {
  74.       white-space: pre-wrap !important;
  75.       word-break: break-word !important;
  76.       overflow-wrap: anywhere !important;
  77.     }
  78.   `);
  79.  
  80.   function shouldTranslate(text) {
  81.     if (!text) return false;
  82.     const t = text.trim();
  83.     if (!t) return false;
  84.     if (EN_SKIP.test(t) || LATIN_SKIP.test(t)) return false;
  85.     return NON_LATIN.test(t);
  86.   }
  87.  
  88.   function markTranslated(el) {
  89.     if (el && el.classList) el.classList.add(TRANSLATED_CLASS);
  90.   }
  91.  
  92.   function unmarkTranslated(el) {
  93.     if (el && el.classList) el.classList.remove(TRANSLATED_CLASS);
  94.   }
  95.  
  96.   function translateText(text) {
  97.     return new Promise((resolve) => {
  98.       GM_xmlhttpRequest({
  99.         method: 'GET',
  100.         url: 'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=en&dt=t&q=' + encodeURIComponent(text),
  101.         onload: (res) => {
  102.           try {
  103.             const data = JSON.parse(res.responseText);
  104.             resolve(data?.[0]?.map(part => part?.[0]).join('') || text);
  105.           } catch {
  106.             resolve(text);
  107.           }
  108.         },
  109.         onerror: () => resolve(text)
  110.       });
  111.     });
  112.   }
  113.  
  114.   function setTranslatedText(el, text) {
  115.     el.textContent = text;
  116.     markTranslated(el);
  117.   }
  118.  
  119.   function setTranslatedHtml(el, text) {
  120.     el.innerHTML = text.replace(/\n/g, '<br>');
  121.     markTranslated(el);
  122.   }
  123.  
  124.   async function translateAdditionalInfo(el) {
  125.     const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT, null);
  126.     const nodes = [];
  127.     while (walker.nextNode()) {
  128.       const node = walker.currentNode;
  129.       const text = (node.nodeValue || '').trim();
  130.       if (text && shouldTranslate(text)) nodes.push(node);
  131.     }
  132.     for (const node of nodes) {
  133.       const original = node.nodeValue;
  134.       const translated = await translateText(original);
  135.       if (translated && translated !== original) {
  136.         node.nodeValue = translated;
  137.         if (node.parentElement) markTranslated(node.parentElement);
  138.       }
  139.       await new Promise(r => setTimeout(r, 200));
  140.     }
  141.   }
  142.  
  143.   async function translateScriptInfoPage() {
  144.     const info = document.querySelector('#script-info');
  145.     if (!info) return;
  146.  
  147.     const scriptDesc = info.querySelector('header p#script-description.script-description');
  148.     if (scriptDesc && !state.processed.has(scriptDesc)) {
  149.       state.processed.add(scriptDesc);
  150.       const text = (scriptDesc.textContent || '').trim();
  151.       if (text && shouldTranslate(text)) {
  152.         const translated = await translateText(text);
  153.         if (translated && translated !== text) setTranslatedText(scriptDesc, translated);
  154.       }
  155.     }
  156.  
  157.     const titleNodes = info.querySelectorAll('.gf-formatted-text .gf-item, header h2, #script-info h2');
  158.     for (const el of titleNodes) {
  159.       if (state.processed.has(el)) continue;
  160.       const text = (el.textContent || '').trim();
  161.       if (!text || !shouldTranslate(text)) continue;
  162.       state.processed.add(el);
  163.       const translated = await translateText(text);
  164.       if (translated && translated !== text) setTranslatedText(el, translated);
  165.     }
  166.  
  167.     const additionalInfo = info.querySelector('#additional-info');
  168.     if (additionalInfo && !state.processed.has(additionalInfo)) {
  169.       state.processed.add(additionalInfo);
  170.       await translateAdditionalInfo(additionalInfo);
  171.       markTranslated(additionalInfo);
  172.     }
  173.   }
  174.  
  175.   async function processElement(el) {
  176.     if (state.processed.has(el)) return;
  177.     if (!state.autoTranslate) return;
  178.  
  179.     if (el.matches('#script-info') || el.closest('#script-info')) {
  180.       await translateScriptInfoPage();
  181.       return;
  182.     }
  183.  
  184.     const text = (el.textContent || '').trim();
  185.     if (!text) return;
  186.     if (!shouldTranslate(text) && text.length > 20) return;
  187.  
  188.     state.processed.add(el);
  189.  
  190.     if (el.matches(SELECTORS.infoDesc)) {
  191.       await translateAdditionalInfo(el);
  192.       markTranslated(el);
  193.       return;
  194.     }
  195.  
  196.     const translated = await translateText(text);
  197.     if (!translated || translated === text) return;
  198.  
  199.     const htmlTargets = [
  200.       SELECTORS.listingDesc,
  201.       SELECTORS.profileDesc,
  202.       SELECTORS.scriptDescription,
  203.       SELECTORS.additionalInfoP,
  204.       SELECTORS.infoTitle,
  205.       SELECTORS.infoDesc
  206.     ].some(sel => el.matches(sel));
  207.  
  208.     if (htmlTargets) setTranslatedHtml(el, translated);
  209.     else setTranslatedText(el, translated);
  210.   }
  211.  
  212.   async function runQueue() {
  213.     if (state.processing || !state.autoTranslate) return;
  214.     state.processing = true;
  215.     while (state.queue.length && state.autoTranslate) {
  216.       const el = state.queue.shift();
  217.       try {
  218.         await processElement(el);
  219.         state.count++;
  220.         updatePanel();
  221.       } catch {}
  222.       await new Promise(r => setTimeout(r, 200));
  223.     }
  224.     state.processing = false;
  225.   }
  226.  
  227.   function enqueueMatches() {
  228.     if (!state.autoTranslate) return;
  229.  
  230.     const found = new Set();
  231.     for (const sel of Object.values(SELECTORS)) {
  232.       document.querySelectorAll(sel).forEach(el => found.add(el));
  233.     }
  234.     found.forEach(el => {
  235.       if (!state.processed.has(el)) state.queue.push(el);
  236.     });
  237.  
  238.     const infoPage = document.querySelector('#script-info');
  239.     if (infoPage && !state.queue.includes(infoPage) && state.autoTranslate) {
  240.       state.queue.unshift(infoPage);
  241.     }
  242.  
  243.     runQueue();
  244.   }
  245.  
  246.   function updatePanel() {
  247.     const el = document.getElementById('gf-panel');
  248.     if (!el) return;
  249.     const countEl = el.querySelector('.count');
  250.     if (countEl) countEl.textContent = `Translated: ${state.count}`;
  251.     const btn = el.querySelector('#gf-toggle');
  252.     if (btn) btn.textContent = state.autoTranslate ? 'Pause Translate' : 'Translate';
  253.   }
  254.  
  255.   function createPanel() {
  256.     const panel = document.createElement('div');
  257.     panel.id = 'gf-panel';
  258.     panel.innerHTML = `
  259.       <div>Greasyfork Auto-Translator v.16</div>
  260.       <div class="count">Translated: 0</div>
  261.       <button id="gf-toggle">Pause Translate</button>
  262.       <button id="gf-hide">Hide</button>
  263.       <button id="gf-reload">Restore Page</button>
  264.     `;
  265.     document.body.appendChild(panel);
  266.  
  267.     panel.querySelector('#gf-toggle').addEventListener('click', () => {
  268.       state.autoTranslate = !state.autoTranslate;
  269.       updatePanel();
  270.       if (state.autoTranslate) enqueueMatches();
  271.     });
  272.  
  273.     panel.querySelector('#gf-hide').addEventListener('click', () => {
  274.       panel.remove();
  275.     });
  276.  
  277.     panel.querySelector('#gf-reload').addEventListener('click', () => {
  278.       state.autoTranslate = false;
  279.       updatePanel();
  280.       state.queue = [];
  281.       state.processed = new WeakSet();
  282.       location.reload();
  283.     });
  284.  
  285.     let dragging = false;
  286.     let offsetX = 0;
  287.     let offsetY = 0;
  288.  
  289.     panel.addEventListener('mousedown', (e) => {
  290.       if (e.target.tagName === 'BUTTON') return;
  291.       dragging = true;
  292.       offsetX = e.clientX - panel.getBoundingClientRect().left;
  293.       offsetY = e.clientY - panel.getBoundingClientRect().top;
  294.     });
  295.  
  296.     document.addEventListener('mousemove', (e) => {
  297.       if (!dragging) return;
  298.       panel.style.left = `${e.clientX - offsetX}px`;
  299.       panel.style.top = `${e.clientY - offsetY}px`;
  300.       panel.style.right = 'auto';
  301.       panel.style.bottom = 'auto';
  302.     });
  303.  
  304.     document.addEventListener('mouseup', () => {
  305.       dragging = false;
  306.     });
  307.   }
  308.  
  309.   const observer = new MutationObserver(() => enqueueMatches());
  310.  
  311.   function init() {
  312.     if (!document.body) return;
  313.     createPanel();
  314.     enqueueMatches();
  315.     observer.observe(document.body, { childList: true, subtree: true });
  316.   }
  317.  
  318.   if (document.readyState === 'loading') {
  319.     document.addEventListener('DOMContentLoaded', init);
  320.   } else {
  321.     init();
  322.   }
  323. })();
  324.  
Advertisement
Add Comment
Please, Sign In to add comment