// ==UserScript== // @name Miniflux: Translate Entry to French // @namespace https://zoemp.be // @version 0.7 // @description Translate Miniflux entries to French using SimplyTranslate (Google engine) // @include /^https?:\/\/[^\/]+\/.*\/entry\/.*/ // @grant GM_xmlhttpRequest // @run-at document-end // ==/UserScript== (function () { 'use strict'; console.log('[Miniflux Translate] Loaded on URL:', location.href); const isProbablyEnglish = (text) => { const ratio = (text.match(/[a-z]/gi) || []).length / text.length; const isLong = text.length > 100; const hasWords = /[a-z]{5,}/.test(text); const result = ratio > 0.4 && isLong && hasWords; console.log(`[Miniflux Translate] Heuristic result: ${result} (ratio: ${ratio.toFixed(2)}, length: ${text.length})`); return result; }; const decodeEntities = (str) => { return str .replace(/'/g, `'`) .replace(/"/g, `"`) .replace(/"/g, `"`) .replace(/&/g, `&`) .replace(/</g, `<`) .replace(/>/g, `>`) .replace(/'/g, `'`) .replace(/ /g, ' ') .replace(/ /g, ' ') .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(n)); }; const translate = (text, cb) => { const form = `from=en&to=fr&text=${encodeURIComponent(text)}`; console.log('[Miniflux Translate] Sending translation request...'); GM_xmlhttpRequest({ method: 'POST', url: 'https://simplytranslate.org/?engine=google', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'text/html', 'Origin': 'null' }, data: form, onload: (res) => { const match = res.responseText.match(/]*id="output"[^>]*>([^<]*)<\/textarea>/); if (match && match[1]) { const raw = match[1]; const cleaned = decodeEntities(raw); console.log('[Miniflux Translate] Translation successful'); cb(cleaned); } else { console.warn('[Miniflux Translate] Failed to extract translation'); cb('[Error: Translation not found]'); } }, onerror: () => { console.error('[Miniflux Translate] Network error during translation request'); cb('[Error: Network issue]'); } }); }; const injectButton = () => { const content = document.querySelector('.entry-content'); if (!content) { console.warn('[Miniflux Translate] .entry-content not found'); return; } const text = content.innerText.trim(); if (!isProbablyEnglish(text)) { console.log('[Miniflux Translate] Entry appears to be in French, skipping'); return; } if (content.querySelector('.translate-btn')) { console.log('[Miniflux Translate] Button already exists'); return; } const btn = document.createElement('button'); btn.textContent = '🈯 Traduire en français'; btn.className = 'translate-btn'; btn.style.cssText = ` margin-top: 12px; padding: 6px 12px; font-size: 0.9em; cursor: pointer; background-color: #222; color: #fff; border: 1px solid #444; border-radius: 4px; `; btn.onclick = () => { btn.disabled = true; btn.textContent = '⏳ Traduction en cours...'; translate(text, (translated) => { const div = document.createElement('div'); div.textContent = translated; div.style.cssText = ` margin-top: 12px; padding: 10px; background: rgb(51 85 67); border-left: 3px solid rgb(71 180 103); white-space: pre-wrap; font-family: "Geist Mono"; `; content.appendChild(div); btn.remove(); }); }; content.appendChild(btn); console.log('[Miniflux Translate] Button injected'); }; const waitUntilReady = () => { if (document.readyState !== 'complete') { console.log('[Miniflux Translate] Waiting for document...'); return setTimeout(waitUntilReady, 100); } injectButton(); }; waitUntilReady(); })();