135 lines
4.0 KiB
JavaScript
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(/</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(/<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();
|
|
})();
|