From 86b743457bbaed5b8d689b3abcd868e6040aa365 Mon Sep 17 00:00:00 2001 From: SansGuidon Date: Fri, 25 Apr 2025 09:14:46 +0000 Subject: [PATCH] feat(miniflux): transalte entries --- miniflux_scripts/translate_entries.js | 134 ++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 miniflux_scripts/translate_entries.js diff --git a/miniflux_scripts/translate_entries.js b/miniflux_scripts/translate_entries.js new file mode 100644 index 0000000..a7b4629 --- /dev/null +++ b/miniflux_scripts/translate_entries.js @@ -0,0 +1,134 @@ +// ==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(); +})();