MediaWiki:CommonTranslateUpdate.js: differenze tra le versioni
Nessun oggetto della modifica |
Nessun oggetto della modifica |
||
| Riga 1: | Riga 1: | ||
/* ======================= CommonTranslateUpdate.js ( | /* ======================= CommonTranslateUpdate.js (proxy + sezioni protette) ======================= */ | ||
console.log("🔄 Caricamento CommonTranslateUpdate.js..."); | console.log("🔄 Caricamento CommonTranslateUpdate.js..."); | ||
if (window.CommonTranslateUpdateLoaded) { | if (window.CommonTranslateUpdateLoaded) { | ||
console.warn("⚠️ CommonTranslateUpdate.js è già stato caricato."); | |||
} else { | } else { | ||
window.CommonTranslateUpdateLoaded = true; | |||
window.linguaOrigine = localStorage.getItem("linguaOrigine") || document.documentElement.lang || "en"; | |||
window.OPENAI_PROXY_URL = "/dashboard/api/openai_project_gpt.php"; | |||
console.log("✅ CommonTranslateUpdate.js caricato correttamente!"); | |||
} | } | ||
/* ======================= Utility ======================= */ | /* ======================= Utility ======================= */ | ||
function stripMarkers(t) { | function stripMarkers(t) { | ||
return (t || "") | return (t || "") | ||
| Riga 25: | Riga 23: | ||
} | } | ||
const HEADER_RE = /^={2,5}\s*([^=]+?)\s*={2,5}\s*$/; | const HEADER_RE = /^={2,5}\s*([^=]+?)\s*={2,5}\s*$/m; | ||
const | const isBiblioHeaderText = (s) => /^\s*(bibliography|references|riferimenti)\b/i.test((s||"").trim()); | ||
const isListItem = (s) => /^\s*(?:\d+\.\s+|[#*;:]+\s*)/.test(s||""); | |||
const isListItem = (s) => | |||
/* ======================= Protezioni / Segnaposti ======================= */ | |||
/* Mettiamo in cassaforte: bibliografia (intera sezione), tabelle, ref, math, gallery, templates, code/pre, references */ | |||
function buildPlaceholder(keyPrefix, idx) { return `%%${keyPrefix}_${idx}%%`; } | |||
function protectBlocks(originalText) { | |||
function | let text = originalText; | ||
const | const map = {}; | ||
/{{ | let i = 0; | ||
/ | |||
/< | // 1) Bibliografia/References/Riferimenti: sezione intatta (header + corpo fino al prossimo header) | ||
/<math>[\s\S]*?<\/math>/ | // - Sostituiamo OGNI sezione biblio con un placeholder unico | ||
/< | text = text.replace( | ||
/{{(?:[^{}]|\{\{[^{}]*\}\})*}}/g | /(^={2,5}\s*(?:Bibliography|References|Riferimenti)\s*={2,5}\s*$[\s\S]*?)(?=^={2,5}|\Z)/gmi, | ||
(m) => { | |||
const key = buildPlaceholder("BIBLIO", i++); | |||
map[key] = m; | |||
return key; | |||
} | |||
); | |||
// 2) Tabelle wiki e HTML | |||
const protectRegexes = [ | |||
/\{\|[\s\S]*?\|\}/g, // wiki table | |||
/<table[\s\S]*?<\/table>/gi, // html table | |||
/<syntaxhighlight[\s\S]*?<\/syntaxhighlight>/gi, | |||
/<pre[\s\S]*?<\/pre>/gi, | |||
/<code[\s\S]*?<\/code>/gi, | |||
/<gallery[\s\S]*?<\/gallery>/gi, | |||
/<math>[\s\S]*?<\/math>/gi, | |||
/<ref[^>]*>[\s\S]*?<\/ref>/gi, | |||
/<references\s*\/>/gi, | |||
/{{(?:[^{}]|\{\{[^{}]*\}\})*}}/g // templates (greedy-safe) | |||
]; | ]; | ||
protectRegexes.forEach((rx) => { | |||
text = text.replace(rx, (m) => { | |||
const key = | const key = buildPlaceholder("SAFE", i++); | ||
map[key] = m; | |||
return key; | |||
}); | }); | ||
}); | }); | ||
return { text, map }; | |||
} | |||
function restoreBlocks(text, map) { | |||
if (!map) return text; | |||
// Ripeti fino a che non ci sono più placeholder da sostituire | |||
} | let changed = true; | ||
while (changed) { | |||
changed = false; | |||
text = text.replace(/%%(BIBLIO|SAFE)_(\d+)%%/g, (m) => { | |||
if (map[m] !== undefined) { changed = true; return map[m]; } | |||
return m; | |||
}); | |||
} | |||
return text; | |||
} | |||
return | /* ======================= Segmentazione (per sezioni) ======================= */ | ||
function segmentaInSezioni(wikitestoProtetto) { | |||
// split per header di livello 2..5. Manteniamo anche eventuale intro prima del primo header. | |||
const parts = wikitestoProtetto.split(/\n(?=^={2,5}[^=\n]+={2,5}\s*$)/m); | |||
return parts.map(s => s.trim()).filter(Boolean); | |||
} | } | ||
/* ======================= | /* ======================= UI: avvio, popup, ecc. ======================= */ | ||
async function traduciTestoUpdate() { | async function traduciTestoUpdate() { | ||
const conferma = confirm("⚡ Vuoi tradurre il capitolo?"); | |||
if (!conferma) return; | |||
const linguaScelta = prompt( | |||
"🌍 Scegli la lingua:\n\nit = Italiano\nen = Inglese\nfr = Francese\nes = Spagnolo\nde = Tedesco\ntutte = Tutte le lingue", | |||
"en" | |||
); | |||
if (!linguaScelta) return; | |||
const lingue = ["it", "en", "fr", "es", "de"]; | |||
const lingueDaTradurre = linguaScelta === "tutte" ? lingue : [linguaScelta]; | |||
localStorage.setItem("lingueDaTradurre", JSON.stringify(lingueDaTradurre)); | |||
verificaURLCapitoli(lingueDaTradurre); | |||
} | } | ||
function verificaURLCapitoli(lingueDaTradurre) { | function verificaURLCapitoli(lingueDaTradurre) { | ||
let formHtml = `<form id="urlForm">`; | |||
for (let lang of lingueDaTradurre) { | |||
formHtml += ` | |||
<label for="url_${lang}">${lang.toUpperCase()}:</label> | |||
<input type="text" id="url_${lang}" value="${localStorage.getItem(`urlCapitolo_${lang}`) || ""}" style="width:100%;margin-bottom:5px;"><br>`; | |||
} | |||
formHtml += `</form>`; | |||
let popup = document.createElement("div"); | |||
popup.innerHTML = ` | |||
<div id="popup-url-traduzione" style="position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); | <div id="popup-url-traduzione" style="position:fixed; top:50%; left:50%; transform:translate(-50%, -50%); | ||
background:white; padding:20px; border-radius:10px; box-shadow:0px 4px 6px rgba(0,0,0,0.2); z-index:2000; font-family:Arial, sans-serif;"> | |||
<h3>🔍 Imposta gli URL per la traduzione</h3> | |||
<p> | |||
🧠 <b>Parole:</b> <span id="conteggio-parole">0</span><br> | |||
🔢 <b>Token stimati:</b> <span id="conteggio-token">0</span><br> | |||
💰 <b>Costo stimato:</b> <span id="costo-stimato">$0.0000</span> | |||
</p> | |||
${formHtml} | |||
<div style="margin-top:10px; text-align:right;"> | |||
<button id="salvaUrlBtn" type="button">✅ Salva URL</button> | |||
<button id="chiudiPopup" type="button">❌ Annulla</button> | |||
</div> | |||
</div>`; | |||
document.body.appendChild(popup); | |||
const testo = ottieniTestoDaEditor(); | |||
const parole = testo.trim().split(/\s+/).filter(Boolean).length; | |||
const tokenStimati = Math.ceil(parole / 0.75); | |||
const costo = (tokenStimati * 0.00002).toFixed(4); | |||
try { | |||
document.getElementById("conteggio-parole").innerText = parole; | |||
document.getElementById("conteggio-token").innerText = tokenStimati; | |||
document.getElementById("costo-stimato").innerText = `$${costo}`; | |||
} catch {} | |||
setTimeout(() => { | |||
const salvaBtn = document.getElementById("salvaUrlBtn"); | |||
const chiudiBtn = document.getElementById("chiudiPopup"); | |||
if (salvaBtn) { | |||
salvaBtn.onclick = function () { | |||
lingueDaTradurre.forEach(lang => { | |||
const newUrl = document.getElementById(`url_${lang}`).value.trim(); | |||
localStorage.setItem(`urlCapitolo_${lang}`, newUrl); | |||
}); | |||
document.body.removeChild(popup); | |||
avviaTraduzioneConSegmentazione(lingueDaTradurre); | |||
}; | |||
const | |||
const | |||
} | } | ||
if (chiudiBtn) chiudiBtn.onclick = () => document.body.removeChild(popup); | |||
}, 100); | |||
} | |||
function avviaTraduzioneConSegmentazione(lingueDaTradurre) { | |||
const testo = ottieniTestoDaEditor(); | |||
// Proteggi prima, poi segmenta | |||
const { text: protetto, map: PHmap } = protectBlocks(testo); | |||
window._CTU_PHMAP = PHmap; // metto in globale per ripristino finale | |||
const sezioni = segmentaInSezioni(protetto); | |||
const errori = analizzaErroriSegmentazione(sezioni); | |||
sostituisciTestoSegmentato(sezioni, errori); | |||
mostraPopupConferma(sezioni, errori, lingueDaTradurre); | |||
} | } | ||
/* ======================= Supporto UI ======================= */ | |||
function ottieniTestoDaEditor() { | function ottieniTestoDaEditor() { | ||
const area = document.getElementById("wpTextbox1"); | |||
return area ? area.value.trim() : ""; | |||
} | } | ||
function analizzaErroriSegmentazione(blocchi) { | function analizzaErroriSegmentazione(blocchi) { | ||
let errori = []; | |||
blocchi.forEach((b, i) => { | |||
let err = []; | |||
[["[", "]"], ["{", "}"], ["<", ">"]].forEach(([o, c]) => { | |||
const countO = (b.match(new RegExp(`\\${o}`, "g")) || []).length; | |||
const countC = (b.match(new RegExp(`\\${c}`, "g")) || []).length; | |||
if (countO !== countC) err.push(`${o}${c} non bilanciate`); | |||
}); | }); | ||
return errori; | if (err.length) errori.push({ index: i + 1, dettagli: err }); | ||
}); | |||
return errori; | |||
} | } | ||
function sostituisciTestoSegmentato(blocchi, errori) { | function sostituisciTestoSegmentato(blocchi, errori) { | ||
const area = document.getElementById("wpTextbox1"); | |||
const finale = blocchi.map((b, i) => { | |||
const e = errori.find(x => x.index === i + 1); | |||
const evidenza = e ? `⚠️ ERRORI:\n- ${e.dettagli.join("\n- ")}\n\n` : ""; | |||
return `${evidenza}🔹 BLOCCO ${i + 1}\n${b}`; | |||
}); | |||
area.value = finale.join("\n\n==============================\n\n"); | |||
} | } | ||
function mostraPopupConferma(blocchi, errori, lingueDaTradurre) { | function mostraPopupConferma(blocchi, errori, lingueDaTradurre) { | ||
const popup = document.createElement("div"); | |||
popup.style = ` | |||
position: fixed; bottom: 20px; right: 20px; background: white; | |||
padding: 15px; border: 2px solid #444; box-shadow: 4px 4px 10px rgba(0,0,0,0.3); | |||
z-index: 9999; resize: both; overflow: auto; cursor: move;`; | |||
popup.innerHTML = ` | |||
<b>✅ Segmentazione completata</b><br> | |||
🧠 Blocchi: ${blocchi.length}<br> | |||
${errori.length > 0 | |||
? `❗ <span style='color:red;'>${errori.length} errori rilevati. Procedere?</span><br>` | |||
: `<span style='color:green;'>✅ Nessun errore rilevato</span><br>`} | |||
<button id="btn-invia-traduzioni">✅ Invia Traduzioni</button> | |||
<button id="btn-salva-segmentato">💾 Salva localmente</button> | |||
<button onclick="this.parentNode.remove()">❌ Annulla</button>`; | |||
document.body.appendChild(popup); | |||
dragElement(popup); | |||
document. | document.getElementById("btn-invia-traduzioni").onclick = () => { | ||
popup.remove(); | |||
lingueDaTradurre.forEach(lang => inviaTraduzione(lang, blocchi)); | |||
}; | |||
document.getElementById("btn-salva-segmentato").onclick = () => { | |||
const testoSegmentato = document.getElementById("wpTextbox1").value.trim(); | |||
localStorage.setItem("segmentatoDaInviare", testoSegmentato); | |||
alert("✅ Testo segmentato salvato localmente!"); | |||
}; | |||
} | |||
function ricostruisciTesto(blocchi) { | |||
return blocchi.map(b => (HEADER_RE.test(b.trim()) ? `\n\n${b.trim()}\n\n` : b.trim())) | |||
.join("\n\n"); | |||
} | } | ||
/* ======================= Invio a MediaWiki (con ripristino segnaposti) ======================= */ | |||
async function inviaTraduzione(lingua, blocchi) { | |||
async function inviaTraduzione(lingua, blocchi){ | |||
const urlAPI = "https://staging.masticationpedia.org/api.php"; | const urlAPI = "https://staging.masticationpedia.org/api.php"; | ||
const urlDest = localStorage.getItem(`urlCapitolo_${lingua}`); | const urlDest = localStorage.getItem(`urlCapitolo_${lingua}`); | ||
if (!urlDest) return; | if (!urlDest) return; | ||
// CSRF token | |||
const tokenRes = await fetch(`${urlAPI}?action=query&meta=tokens&type=csrf&format=json`, { credentials: "include" }); | const tokenRes = await fetch(`${urlAPI}?action=query&meta=tokens&type=csrf&format=json`, { credentials: "include" }); | ||
const tokenData = await tokenRes.json(); | const tokenData = await tokenRes.json(); | ||
const csrfToken = tokenData?.query?.tokens?.csrftoken; | const csrfToken = tokenData?.query?.tokens?.csrftoken; | ||
// Traduci SOLO ciò che NON è biblio o segnaposto | |||
const tradotti = []; | const tradotti = []; | ||
for (const b of blocchi) { | for (const b of blocchi) { | ||
const | const raw = (b || "").trim(); | ||
if ( | // Se il blocco è un placeholder di biblio/safe → NON TRADURRE | ||
if (/^%%(BIBLIO|SAFE)_(\d+)%%$/.test(raw)) { | |||
tradotti.push( | tradotti.push(raw); | ||
continue; | continue; | ||
} | } | ||
if ( | // Se è un header di biblio → potrebbe essere stato già protetto come BIBLIO; ma se non lo fosse, lo lasciamo com’è. | ||
tradotti.push( | const headerMatch = raw.match(HEADER_RE); | ||
if (headerMatch && isBiblioHeaderText(headerMatch[1])) { | |||
tradotti.push(raw); // non tradurre | |||
continue; | continue; | ||
} | } | ||
tradotti.push(await traduciBloccoProxy( | // Altrimenti traduci | ||
tradotti.push(await traduciBloccoProxy(raw, lingua)); | |||
} | } | ||
let testoTradotto = | // Ricompone, poi ripristina segnaposti in blocco | ||
testoTradotto = | let testoTradotto = ricostruisciTesto(tradotti); | ||
testoTradotto = restoreBlocks(testoTradotto, window._CTU_PHMAP); | |||
testoTradotto = stripMarkers(testoTradotto); | testoTradotto = stripMarkers(testoTradotto); | ||
// Invio edit | |||
const form = new URLSearchParams(); | const form = new URLSearchParams(); | ||
form.append("action", "edit"); | form.append("action", "edit"); | ||
| Riga 294: | Riga 306: | ||
} | } | ||
/* ======================= | /* ======================= Traduzione singolo blocco via proxy (no markers) ======================= */ | ||
async function traduciBloccoProxy(blocco, lingua) { | async function traduciBloccoProxy(blocco, lingua) { | ||
try { | |||
const res = await fetch(window.OPENAI_PROXY_URL, { | |||
method: "POST", | |||
headers: { "Content-Type": "application/json" }, | |||
body: JSON.stringify({ | |||
model: "gpt-4o", | |||
prompt: [ | |||
"You are a professional MediaWiki translator.", | |||
"Translate the text into " + lingua + ".", | |||
"Preserve ALL MediaWiki syntax exactly: templates {{...}}, links [[...]], headings == ==, lists (*, #, ;, :), tables {| |}, <math>, <ref>, <gallery>, etc.", | |||
"Do NOT add or remove items; do NOT summarize; do NOT explain.", | |||
"Return ONLY the translated text, no comments, no wrappers." | |||
].join(" ") + "\n\n" + blocco | |||
}) | |||
}); | |||
const data = await res.json(); | |||
if (data.status !== "ok") throw new Error(data.error || ("HTTP " + res.status)); | |||
return stripMarkers(data.result); | |||
} catch (e) { | |||
console.error("❌ Traduzione fallita:", e); | |||
return "[Errore Traduzione]"; | |||
} | |||
} | } | ||
/* ======================= Drag helper ======================= */ | |||
function dragElement(elmnt) { | function dragElement(elmnt) { | ||
let pos1=0,pos2=0,pos3=0,pos4=0; | |||
elmnt.onmousedown = dragMouseDown; | |||
function dragMouseDown(e){ e = e || window.event; e.preventDefault(); pos3=e.clientX; pos4=e.clientY; document.onmouseup=closeDragElement; document.onmousemove=elementDrag; } | |||
function elementDrag(e){ e = e || window.event; e.preventDefault(); pos1=pos3-e.clientX; pos2=pos4-e.clientY; pos3=e.clientX; pos4=e.clientY; elmnt.style.top=(elmnt.offsetTop-pos2)+"px"; elmnt.style.left=(elmnt.offsetLeft-pos1)+"px"; } | |||
function closeDragElement(){ document.onmouseup=null; document.onmousemove=null; } | |||
} | } | ||
Versione attuale delle 15:18, 6 set 2025
/* ======================= CommonTranslateUpdate.js (proxy + sezioni protette) ======================= */
console.log("🔄 Caricamento CommonTranslateUpdate.js...");
if (window.CommonTranslateUpdateLoaded) {
console.warn("⚠️ CommonTranslateUpdate.js è già stato caricato.");
} else {
window.CommonTranslateUpdateLoaded = true;
window.linguaOrigine = localStorage.getItem("linguaOrigine") || document.documentElement.lang || "en";
window.OPENAI_PROXY_URL = "/dashboard/api/openai_project_gpt.php";
console.log("✅ CommonTranslateUpdate.js caricato correttamente!");
}
/* ======================= Utility ======================= */
function stripMarkers(t) {
return (t || "")
.replace(/^\s*[-–—]*\s*(TESTO\s*INIZIO|TEXT\s*START)\s*[-–—]*\s*$/gmi, "")
.replace(/^\s*[-–—]*\s*(TESTO\s*FINE|TEXT\s*END)\s*[-–—]*\s*$/gmi, "")
.replace(/---\s*(TESTO\s*INIZIO|TEXT\s*START)\s*---/gi, "")
.replace(/---\s*(TESTO\s*FINE|TEXT\s*END)\s*---/gi, "")
.trim();
}
const HEADER_RE = /^={2,5}\s*([^=]+?)\s*={2,5}\s*$/m;
const isBiblioHeaderText = (s) => /^\s*(bibliography|references|riferimenti)\b/i.test((s||"").trim());
const isListItem = (s) => /^\s*(?:\d+\.\s+|[#*;:]+\s*)/.test(s||"");
/* ======================= Protezioni / Segnaposti ======================= */
/* Mettiamo in cassaforte: bibliografia (intera sezione), tabelle, ref, math, gallery, templates, code/pre, references */
function buildPlaceholder(keyPrefix, idx) { return `%%${keyPrefix}_${idx}%%`; }
function protectBlocks(originalText) {
let text = originalText;
const map = {};
let i = 0;
// 1) Bibliografia/References/Riferimenti: sezione intatta (header + corpo fino al prossimo header)
// - Sostituiamo OGNI sezione biblio con un placeholder unico
text = text.replace(
/(^={2,5}\s*(?:Bibliography|References|Riferimenti)\s*={2,5}\s*$[\s\S]*?)(?=^={2,5}|\Z)/gmi,
(m) => {
const key = buildPlaceholder("BIBLIO", i++);
map[key] = m;
return key;
}
);
// 2) Tabelle wiki e HTML
const protectRegexes = [
/\{\|[\s\S]*?\|\}/g, // wiki table
/<table[\s\S]*?<\/table>/gi, // html table
/<syntaxhighlight[\s\S]*?<\/syntaxhighlight>/gi,
/<pre[\s\S]*?<\/pre>/gi,
/<code[\s\S]*?<\/code>/gi,
/<gallery[\s\S]*?<\/gallery>/gi,
/<math>[\s\S]*?<\/math>/gi,
/<ref[^>]*>[\s\S]*?<\/ref>/gi,
/<references\s*\/>/gi,
/{{(?:[^{}]|\{\{[^{}]*\}\})*}}/g // templates (greedy-safe)
];
protectRegexes.forEach((rx) => {
text = text.replace(rx, (m) => {
const key = buildPlaceholder("SAFE", i++);
map[key] = m;
return key;
});
});
return { text, map };
}
function restoreBlocks(text, map) {
if (!map) return text;
// Ripeti fino a che non ci sono più placeholder da sostituire
let changed = true;
while (changed) {
changed = false;
text = text.replace(/%%(BIBLIO|SAFE)_(\d+)%%/g, (m) => {
if (map[m] !== undefined) { changed = true; return map[m]; }
return m;
});
}
return text;
}
/* ======================= Segmentazione (per sezioni) ======================= */
function segmentaInSezioni(wikitestoProtetto) {
// split per header di livello 2..5. Manteniamo anche eventuale intro prima del primo header.
const parts = wikitestoProtetto.split(/\n(?=^={2,5}[^=\n]+={2,5}\s*$)/m);
return parts.map(s => s.trim()).filter(Boolean);
}
/* ======================= UI: avvio, popup, ecc. ======================= */
async function traduciTestoUpdate() {
const conferma = confirm("⚡ Vuoi tradurre il capitolo?");
if (!conferma) return;
const linguaScelta = prompt(
"🌍 Scegli la lingua:\n\nit = Italiano\nen = Inglese\nfr = Francese\nes = Spagnolo\nde = Tedesco\ntutte = Tutte le lingue",
"en"
);
if (!linguaScelta) return;
const lingue = ["it", "en", "fr", "es", "de"];
const lingueDaTradurre = linguaScelta === "tutte" ? lingue : [linguaScelta];
localStorage.setItem("lingueDaTradurre", JSON.stringify(lingueDaTradurre));
verificaURLCapitoli(lingueDaTradurre);
}
function verificaURLCapitoli(lingueDaTradurre) {
let formHtml = `<form id="urlForm">`;
for (let lang of lingueDaTradurre) {
formHtml += `
<label for="url_${lang}">${lang.toUpperCase()}:</label>
<input type="text" id="url_${lang}" value="${localStorage.getItem(`urlCapitolo_${lang}`) || ""}" style="width:100%;margin-bottom:5px;"><br>`;
}
formHtml += `</form>`;
let popup = document.createElement("div");
popup.innerHTML = `
<div id="popup-url-traduzione" style="position:fixed; top:50%; left:50%; transform:translate(-50%, -50%);
background:white; padding:20px; border-radius:10px; box-shadow:0px 4px 6px rgba(0,0,0,0.2); z-index:2000; font-family:Arial, sans-serif;">
<h3>🔍 Imposta gli URL per la traduzione</h3>
<p>
🧠 <b>Parole:</b> <span id="conteggio-parole">0</span><br>
🔢 <b>Token stimati:</b> <span id="conteggio-token">0</span><br>
💰 <b>Costo stimato:</b> <span id="costo-stimato">$0.0000</span>
</p>
${formHtml}
<div style="margin-top:10px; text-align:right;">
<button id="salvaUrlBtn" type="button">✅ Salva URL</button>
<button id="chiudiPopup" type="button">❌ Annulla</button>
</div>
</div>`;
document.body.appendChild(popup);
const testo = ottieniTestoDaEditor();
const parole = testo.trim().split(/\s+/).filter(Boolean).length;
const tokenStimati = Math.ceil(parole / 0.75);
const costo = (tokenStimati * 0.00002).toFixed(4);
try {
document.getElementById("conteggio-parole").innerText = parole;
document.getElementById("conteggio-token").innerText = tokenStimati;
document.getElementById("costo-stimato").innerText = `$${costo}`;
} catch {}
setTimeout(() => {
const salvaBtn = document.getElementById("salvaUrlBtn");
const chiudiBtn = document.getElementById("chiudiPopup");
if (salvaBtn) {
salvaBtn.onclick = function () {
lingueDaTradurre.forEach(lang => {
const newUrl = document.getElementById(`url_${lang}`).value.trim();
localStorage.setItem(`urlCapitolo_${lang}`, newUrl);
});
document.body.removeChild(popup);
avviaTraduzioneConSegmentazione(lingueDaTradurre);
};
}
if (chiudiBtn) chiudiBtn.onclick = () => document.body.removeChild(popup);
}, 100);
}
function avviaTraduzioneConSegmentazione(lingueDaTradurre) {
const testo = ottieniTestoDaEditor();
// Proteggi prima, poi segmenta
const { text: protetto, map: PHmap } = protectBlocks(testo);
window._CTU_PHMAP = PHmap; // metto in globale per ripristino finale
const sezioni = segmentaInSezioni(protetto);
const errori = analizzaErroriSegmentazione(sezioni);
sostituisciTestoSegmentato(sezioni, errori);
mostraPopupConferma(sezioni, errori, lingueDaTradurre);
}
/* ======================= Supporto UI ======================= */
function ottieniTestoDaEditor() {
const area = document.getElementById("wpTextbox1");
return area ? area.value.trim() : "";
}
function analizzaErroriSegmentazione(blocchi) {
let errori = [];
blocchi.forEach((b, i) => {
let err = [];
[["[", "]"], ["{", "}"], ["<", ">"]].forEach(([o, c]) => {
const countO = (b.match(new RegExp(`\\${o}`, "g")) || []).length;
const countC = (b.match(new RegExp(`\\${c}`, "g")) || []).length;
if (countO !== countC) err.push(`${o}${c} non bilanciate`);
});
if (err.length) errori.push({ index: i + 1, dettagli: err });
});
return errori;
}
function sostituisciTestoSegmentato(blocchi, errori) {
const area = document.getElementById("wpTextbox1");
const finale = blocchi.map((b, i) => {
const e = errori.find(x => x.index === i + 1);
const evidenza = e ? `⚠️ ERRORI:\n- ${e.dettagli.join("\n- ")}\n\n` : "";
return `${evidenza}🔹 BLOCCO ${i + 1}\n${b}`;
});
area.value = finale.join("\n\n==============================\n\n");
}
function mostraPopupConferma(blocchi, errori, lingueDaTradurre) {
const popup = document.createElement("div");
popup.style = `
position: fixed; bottom: 20px; right: 20px; background: white;
padding: 15px; border: 2px solid #444; box-shadow: 4px 4px 10px rgba(0,0,0,0.3);
z-index: 9999; resize: both; overflow: auto; cursor: move;`;
popup.innerHTML = `
<b>✅ Segmentazione completata</b><br>
🧠 Blocchi: ${blocchi.length}<br>
${errori.length > 0
? `❗ <span style='color:red;'>${errori.length} errori rilevati. Procedere?</span><br>`
: `<span style='color:green;'>✅ Nessun errore rilevato</span><br>`}
<button id="btn-invia-traduzioni">✅ Invia Traduzioni</button>
<button id="btn-salva-segmentato">💾 Salva localmente</button>
<button onclick="this.parentNode.remove()">❌ Annulla</button>`;
document.body.appendChild(popup);
dragElement(popup);
document.getElementById("btn-invia-traduzioni").onclick = () => {
popup.remove();
lingueDaTradurre.forEach(lang => inviaTraduzione(lang, blocchi));
};
document.getElementById("btn-salva-segmentato").onclick = () => {
const testoSegmentato = document.getElementById("wpTextbox1").value.trim();
localStorage.setItem("segmentatoDaInviare", testoSegmentato);
alert("✅ Testo segmentato salvato localmente!");
};
}
function ricostruisciTesto(blocchi) {
return blocchi.map(b => (HEADER_RE.test(b.trim()) ? `\n\n${b.trim()}\n\n` : b.trim()))
.join("\n\n");
}
/* ======================= Invio a MediaWiki (con ripristino segnaposti) ======================= */
async function inviaTraduzione(lingua, blocchi) {
const urlAPI = "https://staging.masticationpedia.org/api.php";
const urlDest = localStorage.getItem(`urlCapitolo_${lingua}`);
if (!urlDest) return;
// CSRF token
const tokenRes = await fetch(`${urlAPI}?action=query&meta=tokens&type=csrf&format=json`, { credentials: "include" });
const tokenData = await tokenRes.json();
const csrfToken = tokenData?.query?.tokens?.csrftoken;
// Traduci SOLO ciò che NON è biblio o segnaposto
const tradotti = [];
for (const b of blocchi) {
const raw = (b || "").trim();
// Se il blocco è un placeholder di biblio/safe → NON TRADURRE
if (/^%%(BIBLIO|SAFE)_(\d+)%%$/.test(raw)) {
tradotti.push(raw);
continue;
}
// Se è un header di biblio → potrebbe essere stato già protetto come BIBLIO; ma se non lo fosse, lo lasciamo com’è.
const headerMatch = raw.match(HEADER_RE);
if (headerMatch && isBiblioHeaderText(headerMatch[1])) {
tradotti.push(raw); // non tradurre
continue;
}
// Altrimenti traduci
tradotti.push(await traduciBloccoProxy(raw, lingua));
}
// Ricompone, poi ripristina segnaposti in blocco
let testoTradotto = ricostruisciTesto(tradotti);
testoTradotto = restoreBlocks(testoTradotto, window._CTU_PHMAP);
testoTradotto = stripMarkers(testoTradotto);
// Invio edit
const form = new URLSearchParams();
form.append("action", "edit");
form.append("title", urlDest.split("/").pop());
form.append("text", testoTradotto);
form.append("format", "json");
form.append("token", csrfToken);
const resp = await fetch(urlAPI, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: form,
credentials: "include"
});
const data = await resp.json();
if (data?.edit?.result === "Success") {
alert(`✅ Traduzione inviata con successo a: ${urlDest}`);
} else {
console.error("❌ Errore invio:", data);
}
}
/* ======================= Traduzione singolo blocco via proxy (no markers) ======================= */
async function traduciBloccoProxy(blocco, lingua) {
try {
const res = await fetch(window.OPENAI_PROXY_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "gpt-4o",
prompt: [
"You are a professional MediaWiki translator.",
"Translate the text into " + lingua + ".",
"Preserve ALL MediaWiki syntax exactly: templates {{...}}, links [[...]], headings == ==, lists (*, #, ;, :), tables {| |}, <math>, <ref>, <gallery>, etc.",
"Do NOT add or remove items; do NOT summarize; do NOT explain.",
"Return ONLY the translated text, no comments, no wrappers."
].join(" ") + "\n\n" + blocco
})
});
const data = await res.json();
if (data.status !== "ok") throw new Error(data.error || ("HTTP " + res.status));
return stripMarkers(data.result);
} catch (e) {
console.error("❌ Traduzione fallita:", e);
return "[Errore Traduzione]";
}
}
/* ======================= Drag helper ======================= */
function dragElement(elmnt) {
let pos1=0,pos2=0,pos3=0,pos4=0;
elmnt.onmousedown = dragMouseDown;
function dragMouseDown(e){ e = e || window.event; e.preventDefault(); pos3=e.clientX; pos4=e.clientY; document.onmouseup=closeDragElement; document.onmousemove=elementDrag; }
function elementDrag(e){ e = e || window.event; e.preventDefault(); pos1=pos3-e.clientX; pos2=pos4-e.clientY; pos3=e.clientX; pos4=e.clientY; elmnt.style.top=(elmnt.offsetTop-pos2)+"px"; elmnt.style.left=(elmnt.offsetLeft-pos1)+"px"; }
function closeDragElement(){ document.onmouseup=null; document.onmousemove=null; }
}