snippets/miniflux_scripts/translate_entries.js

135 lines
4.0 KiB
JavaScript

// ==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(/&lt;/g, `<`)
.replace(/&gt;/g, `>`)
.replace(/&#x27;/g, `'`)
.replace(/&nbsp;/g, ' ')
.replace(/&#160;/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(/<textarea[^>]*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();
})();