Completa i18n IT/EN/FR/DE/ES su sito pubblico, area autenticata e admin.

Tutte le view marketing, legali, viewer, dashboard e admin usano t(); mailer utente-facing e flash localizzati; admin con LocaleResolver e selettore lingua.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-24 00:38:50 +02:00
co-authored by Cursor
parent fb7068f86c
commit b88f44ab1c
158 changed files with 9444 additions and 1536 deletions
+88 -31
View File
@@ -3,6 +3,63 @@
var pendingScope = null;
var dialog = null;
var DEFAULT_I18N = {
eyebrow: "Conferma richiesta",
title: "Sei sicuro?",
deleteTitle: "Eliminare definitivamente?",
confirmOk: "Conferma",
deleteOk: "Elimina",
cancel: "Annulla",
multiDeleteTitle: "Cosa vuoi eliminare?",
multiDeleteMessage: "Scegli dove rimuovere il replay. L'operazione è irreversibile.",
deleteYoutubeTitle: "Eliminare da YouTube?",
deleteYoutubeMessage: "Il video verrà rimosso da YouTube. Il replay resterà disponibile sul sito.",
deleteYoutubeOk: "Elimina da YouTube",
deleteSiteTitle: "Eliminare dal sito?",
deleteSiteMessageWithYoutube: "Il replay verrà rimosso dal sito. Il video su YouTube resterà online.",
deleteSiteMessageOnly: "Stai per eliminare definitivamente questo replay dal sito. L'operazione è irreversibile.",
deleteSiteOk: "Elimina dal sito",
choiceSiteLabel: "Solo dal sito",
choiceSiteHintWithYoutube: "Il video resta su YouTube",
choiceSiteHintOnly: "Rimuove i file dal sito",
choiceYoutubeLabel: "Solo da YouTube",
choiceYoutubeHintWithSite: "Resta disponibile sul sito",
choiceYoutubeHintOnly: "Rimuove solo il video YouTube",
choiceBothLabel: "Da sito e YouTube",
choiceBothHint: "Eliminazione completa"
};
function loadI18n() {
var raw = document.body && document.body.getAttribute("data-confirm-i18n");
if (!raw) return DEFAULT_I18N;
try {
var parsed = JSON.parse(raw);
var merged = {};
for (var key in DEFAULT_I18N) {
merged[key] = parsed[key] || DEFAULT_I18N[key];
}
return merged;
} catch (err) {
return DEFAULT_I18N;
}
}
var I18N = loadI18n();
function escapeHtml(str) {
return String(str == null ? "" : str).replace(/[&<>"']/g, function (ch) {
return (
{
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
'"': "&quot;",
"'": "&#39;"
}[ch] || ch
);
});
}
function ensureDialog() {
if (dialog) return dialog;
@@ -15,26 +72,26 @@
dialog.innerHTML =
'<div class="site-confirm__backdrop" data-confirm-cancel></div>' +
'<div class="site-confirm__panel site-confirm__panel--choices">' +
' <p class="site-confirm__eyebrow">Conferma richiesta</p>' +
' <h2 class="site-confirm__title" id="site-confirm-title">Sei sicuro?</h2>' +
' <p class="site-confirm__eyebrow">' + escapeHtml(I18N.eyebrow) + '</p>' +
' <h2 class="site-confirm__title" id="site-confirm-title">' + escapeHtml(I18N.title) + '</h2>' +
' <p class="site-confirm__message" id="site-confirm-message"></p>' +
' <div class="site-confirm__choices" id="site-confirm-choices" hidden>' +
' <button type="button" class="site-confirm__choice" data-confirm-scope="site">' +
" <span class=\"site-confirm__choice-label\">Solo dal sito</span>" +
" <span class=\"site-confirm__choice-hint\" data-hint-site>Il video resta su YouTube</span>" +
' <span class="site-confirm__choice-label">' + escapeHtml(I18N.choiceSiteLabel) + '</span>' +
' <span class="site-confirm__choice-hint" data-hint-site>' + escapeHtml(I18N.choiceSiteHintWithYoutube) + '</span>' +
" </button>" +
' <button type="button" class="site-confirm__choice" data-confirm-scope="youtube">' +
" <span class=\"site-confirm__choice-label\">Solo da YouTube</span>" +
" <span class=\"site-confirm__choice-hint\" data-hint-youtube>Resta disponibile sul sito</span>" +
' <span class="site-confirm__choice-label">' + escapeHtml(I18N.choiceYoutubeLabel) + '</span>' +
' <span class="site-confirm__choice-hint" data-hint-youtube>' + escapeHtml(I18N.choiceYoutubeHintWithSite) + '</span>' +
" </button>" +
' <button type="button" class="site-confirm__choice site-confirm__choice--danger" data-confirm-scope="both">' +
" <span class=\"site-confirm__choice-label\">Da sito e YouTube</span>" +
" <span class=\"site-confirm__choice-hint\" data-hint-both>Eliminazione completa</span>" +
' <span class="site-confirm__choice-label">' + escapeHtml(I18N.choiceBothLabel) + '</span>' +
' <span class="site-confirm__choice-hint" data-hint-both>' + escapeHtml(I18N.choiceBothHint) + '</span>' +
" </button>" +
" </div>" +
' <div class="site-confirm__actions">' +
' <button type="button" class="btn btn-secondary site-confirm__btn" data-confirm-cancel>Annulla</button>' +
' <button type="button" class="btn btn-primary site-confirm__btn site-confirm__btn--danger" data-confirm-ok>Elimina</button>' +
' <button type="button" class="btn btn-secondary site-confirm__btn" data-confirm-cancel>' + escapeHtml(I18N.cancel) + '</button>' +
' <button type="button" class="btn btn-primary site-confirm__btn site-confirm__btn--danger" data-confirm-ok>' + escapeHtml(I18N.deleteOk) + '</button>' +
" </div>" +
"</div>";
@@ -89,6 +146,7 @@
options = options || {};
var hasSite = options.hasSite !== false;
var hasYoutube = !!options.hasYoutube;
var isDelete = !!options.isDelete;
var canSite = hasSite;
var canYoutube = hasYoutube;
var canBoth = hasSite && hasYoutube;
@@ -116,29 +174,25 @@
if (titleEl) {
if (multi) {
titleEl.textContent = "Cosa vuoi eliminare?";
titleEl.textContent = I18N.multiDeleteTitle;
} else if (pendingScope === "youtube") {
titleEl.textContent = "Eliminare da YouTube?";
titleEl.textContent = I18N.deleteYoutubeTitle;
} else if (pendingScope === "site") {
titleEl.textContent = "Eliminare dal sito?";
titleEl.textContent = I18N.deleteSiteTitle;
} else {
titleEl.textContent = /elimin/i.test(message)
? "Eliminare definitivamente?"
: "Confermi questa operazione?";
titleEl.textContent = isDelete ? I18N.deleteTitle : I18N.title;
}
}
if (messageEl) {
if (multi) {
messageEl.textContent =
"Scegli dove rimuovere il replay. L'operazione è irreversibile.";
messageEl.textContent = I18N.multiDeleteMessage;
} else if (pendingScope === "youtube") {
messageEl.textContent =
"Il video verrà rimosso da YouTube. Il replay resterà disponibile sul sito.";
messageEl.textContent = I18N.deleteYoutubeMessage;
} else if (pendingScope === "site") {
messageEl.textContent = hasYoutube
? "Il replay verrà rimosso dal sito. Il video su YouTube resterà online."
: "Stai per eliminare definitivamente questo replay dal sito. L'operazione è irreversibile.";
? I18N.deleteSiteMessageWithYoutube
: I18N.deleteSiteMessageOnly;
} else {
messageEl.textContent = message;
}
@@ -149,9 +203,9 @@
if (okBtn instanceof HTMLElement) {
okBtn.hidden = multi;
if (pendingScope === "youtube") okBtn.textContent = "Elimina da YouTube";
else if (pendingScope === "site") okBtn.textContent = "Elimina dal sito";
else okBtn.textContent = /elimin/i.test(message) ? "Elimina" : "Conferma";
if (pendingScope === "youtube") okBtn.textContent = I18N.deleteYoutubeOk;
else if (pendingScope === "site") okBtn.textContent = I18N.deleteSiteOk;
else okBtn.textContent = isDelete ? I18N.deleteOk : I18N.confirmOk;
}
setChoiceVisible(siteBtn, canSite);
@@ -163,15 +217,15 @@
setChoiceEnabled(bothBtn, canBoth);
if (hintSite) {
hintSite.textContent = canYoutube
? "Il video resta su YouTube"
: "Rimuove i file dal sito";
? I18N.choiceSiteHintWithYoutube
: I18N.choiceSiteHintOnly;
}
if (hintYoutube) {
hintYoutube.textContent = canSite
? "Resta disponibile sul sito"
: "Rimuove solo il video YouTube";
? I18N.choiceYoutubeHintWithSite
: I18N.choiceYoutubeHintOnly;
}
if (hintBoth) hintBoth.textContent = "Eliminazione completa";
if (hintBoth) hintBoth.textContent = I18N.choiceBothHint;
}
el.hidden = false;
@@ -249,11 +303,14 @@
mode === "delete-replay-youtube";
var hasSiteAttr = form.getAttribute("data-has-site");
var hasSite = hasSiteAttr == null ? true : hasSiteAttr === "1";
var isDelete =
form.getAttribute("data-confirm-kind") === "delete" || isReplayDelete;
openDialog(message, {
multi: isReplayDelete,
hasYoutube: hasYoutube,
hasSite: hasSite
hasSite: hasSite,
isDelete: isDelete
});
},
true
+108 -43
View File
@@ -2,6 +2,73 @@
const root = document.getElementById("regia-app");
if (!root) return;
const DEFAULT_I18N = {
periodUpdated: "Periodo aggiornato",
nowInPeriodTemplate: "Ora in %{period}",
setWonTitle: "Set vinto",
setWonBodyTemplate: "%{winner} ha vinto il set. Chiudere il set?",
matchWonTitle: "Partita vinta",
matchWonBodyTemplate: "%{winner} ha vinto la partita. Chiudere la diretta?",
closeSetTitle: "Chiudi set",
closeSetConfirm: "Confermi la chiusura del set corrente?",
scoreUpdateError: "Errore aggiornamento punteggio",
genericError: "Errore",
setsLineTemplate: "Set %{current} · Set vinti %{home}-%{away}",
setPartialLabelTemplate: "Set %{number}",
freeScore: "Punteggio libero",
stopwatch: "Cronometro",
quarterLabelTemplate: "Q%{n}",
halfLabelTemplate: "%{n}° tempo",
overtimeBasketTemplate: "OT%{n}",
overtimeOther: "Suppl.",
unknownPeriod: "—",
partialsPrefix: "Parziali:",
resumed: "Diretta ripresa",
pausedCover: "Diretta in pausa — copertina in onda",
closed: "Diretta chiusa",
resumeError: "Errore ripresa diretta",
pauseError: "Errore pausa diretta",
closeError: "Errore chiusura",
closeConfirm: "Chiudere definitivamente la diretta?",
linkUnavailable: "Link non disponibile",
linkCopied: "Link copiato",
copyPrompt: "Copia il link:",
shareRegiaText: "Apri questo link per gestire il punteggio della diretta:",
shareLiveText: "Guarda la diretta su Match Live TV:",
previewWaiting: "Anteprima in attesa del segnale…",
streamEnded: "Diretta terminata",
resumeLabel: "Riprendi diretta",
pauseLabel: "Metti in pausa",
endedBadge: "Terminata",
pausedBadge: "In pausa",
liveBadge: "In onda",
waitingBadge: "In attesa",
subtitlePrefix: "Regia ·"
};
function loadI18n() {
const raw = root.dataset.i18n;
if (!raw) return DEFAULT_I18N;
try {
const parsed = JSON.parse(raw);
const merged = {};
for (const key in DEFAULT_I18N) {
merged[key] = parsed[key] || DEFAULT_I18N[key];
}
return merged;
} catch (err) {
return DEFAULT_I18N;
}
}
const I18N = loadI18n();
function fillTemplate(template, values) {
return template.replace(/%\{(\w+)\}/g, (match, key) =>
Object.prototype.hasOwnProperty.call(values, key) ? values[key] : match
);
}
const cfg = {
token: root.dataset.token,
board: root.dataset.board || "volley",
@@ -88,7 +155,7 @@
function formatPartials(partials) {
if (!partials || !partials.length) return "";
return partials
.map((p) => `Set ${p.set || p["set"]} ${p.home || p["home"]}-${p.away || p["away"]}`)
.map((p) => `${fillTemplate(I18N.setPartialLabelTemplate, { number: p.set || p["set"] })} ${p.home || p["home"]}-${p.away || p["away"]}`)
.join(" · ");
}
@@ -104,24 +171,30 @@
const raw = score.period;
if (typeof raw === "string" && raw.trim()) return raw.trim();
if (typeof raw === "number" && raw > 0) {
return board === "basket" ? `Q${raw}` : `${raw}° tempo`;
return board === "basket"
? fillTemplate(I18N.quarterLabelTemplate, { n: raw })
: fillTemplate(I18N.halfLabelTemplate, { n: raw });
}
const nested = score.data || {};
const dataPeriod = nested.period ?? nested["period"];
const periodNum = parseInt(dataPeriod, 10);
if (periodNum > 0) {
if (nested.overtime || nested["overtime"]) {
return board === "basket" ? `OT${Math.max(periodNum - 4, 1)}` : "Suppl.";
return board === "basket"
? fillTemplate(I18N.overtimeBasketTemplate, { n: Math.max(periodNum - 4, 1) })
: I18N.overtimeOther;
}
return board === "basket" ? `Q${periodNum}` : `${periodNum}° tempo`;
return board === "basket"
? fillTemplate(I18N.quarterLabelTemplate, { n: periodNum })
: fillTemplate(I18N.halfLabelTemplate, { n: periodNum });
}
return "—";
return I18N.unknownPeriod;
}
function updateRegiaSubtitle(periodLabel, board) {
if (!els.subtitle) return;
if (board === "basket" || board === "timed") {
els.subtitle.textContent = `Regia · ${periodLabel}`;
els.subtitle.textContent = `${I18N.subtitlePrefix} ${periodLabel}`;
}
}
@@ -150,17 +223,17 @@
updateRegiaSubtitle(periodLabel, board);
} else if (board === "timer") {
const clockSecs = s.clock_secs ?? s.clockSecs ?? s.data?.clock_secs;
if (els.setsLine) els.setsLine.textContent = "Cronometro";
if (els.setsLine) els.setsLine.textContent = I18N.stopwatch;
if (clockLine) clockLine.textContent = formatClock(clockSecs, true);
} else if (board === "generic") {
if (els.setsLine) els.setsLine.textContent = "Punteggio libero";
if (els.setsLine) els.setsLine.textContent = I18N.freeScore;
if (els.scoreLine) els.scoreLine.textContent = `${cfg.homeName} ${homePts} - ${awayPts} ${cfg.awayName}`;
} else {
if (els.setsLine) els.setsLine.textContent = `Set ${currentSet} · Set vinti ${homeSets}-${awaySets}`;
if (els.setsLine) els.setsLine.textContent = fillTemplate(I18N.setsLineTemplate, { current: currentSet, home: homeSets, away: awaySets });
if (els.scoreLine) els.scoreLine.textContent = `${cfg.homeName} ${homePts} - ${awayPts} ${cfg.awayName}`;
const partialsText = formatPartials(s.set_partials || s.setPartials);
if (els.partialsLine && partialsText) {
els.partialsLine.textContent = `Parziali: ${partialsText}`;
els.partialsLine.textContent = `${I18N.partialsPrefix} ${partialsText}`;
els.partialsLine.hidden = false;
}
}
@@ -170,26 +243,26 @@
if (!els.btnPause) return;
const paused = !!(data.paused || data.status === "paused");
streamPaused = paused;
els.btnPause.textContent = paused ? "Riprendi diretta" : "Metti in pausa";
els.btnPause.textContent = paused ? I18N.resumeLabel : I18N.pauseLabel;
}
function setBadge(data) {
syncPauseButton(data);
if (data.stream_closed) {
els.badge.textContent = "Terminata";
els.badge.textContent = I18N.endedBadge;
els.badge.className = "regia-badge regia-badge--ended";
return;
}
if (data.status === "paused" || data.paused) {
els.badge.textContent = "In pausa";
els.badge.textContent = I18N.pausedBadge;
els.badge.className = "regia-badge regia-badge--wait";
return;
}
if (data.on_air) {
els.badge.textContent = "In onda";
els.badge.textContent = I18N.liveBadge;
els.badge.className = "regia-badge regia-badge--live";
} else {
els.badge.textContent = "In attesa";
els.badge.textContent = I18N.waitingBadge;
els.badge.className = "regia-badge regia-badge--wait";
}
}
@@ -202,7 +275,7 @@
});
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || "Errore aggiornamento punteggio");
throw new Error(body.error || I18N.scoreUpdateError);
}
return res.json();
}
@@ -219,24 +292,24 @@
if (action === "advance_period") {
const periodLabel = resolvePeriodLabel(data.score || {}, cfg.board);
toast(periodLabel === "—" ? "Periodo aggiornato" : `Ora in ${periodLabel}`);
toast(periodLabel === I18N.unknownPeriod ? I18N.periodUpdated : fillTemplate(I18N.nowInPeriodTemplate, { period: periodLabel }));
}
if (cfg.board === "volley" || cfg.board === "racket") {
if (data.set_won && action !== "close_set") {
const winner = data.winner === "home" ? cfg.homeName : cfg.awayName;
openModal("Set vinto", `${winner} ha vinto il set. Chiudere il set?`, () => postScore("close_set"));
openModal(I18N.setWonTitle, fillTemplate(I18N.setWonBodyTemplate, { winner }), () => postScore("close_set"));
}
}
if (data.match_won) {
const winner = data.winner === "home" ? cfg.homeName : cfg.awayName;
openModal("Partita vinta", `${winner} ha vinto la partita. Chiudere la diretta?`, () => stopStream());
openModal(I18N.matchWonTitle, fillTemplate(I18N.matchWonBodyTemplate, { winner }), () => stopStream());
}
} catch (e) {
if (prevHome != null && els.homePoints) els.homePoints.textContent = prevHome;
const awayEl = document.getElementById("away-points");
if (prevAway != null && awayEl) awayEl.textContent = prevAway;
toast(e.message || "Errore");
toast(e.message || I18N.genericError);
} finally {
if (trigger) trigger.disabled = false;
}
@@ -265,7 +338,7 @@
setBadge(data);
}
} catch (e) {
toast(e.message || "Errore");
toast(e.message || I18N.genericError);
}
}
});
@@ -276,7 +349,7 @@
btn.addEventListener("click", () => {
const action = btn.dataset.action;
if (action === "close_set") {
openModal("Chiudi set", "Confermi la chiusura del set corrente?", () => postScore("close_set"));
openModal(I18N.closeSetTitle, I18N.closeSetConfirm, () => postScore("close_set"));
} else {
handleAction(action);
}
@@ -304,7 +377,7 @@
}
} else {
destroyPreview();
showPreviewPlaceholder("Anteprima in attesa del segnale…");
showPreviewPlaceholder(I18N.previewWaiting);
}
wasPausedPreview = paused;
}
@@ -367,7 +440,7 @@
function stopPreview() {
destroyPreview();
if (els.preview) els.preview.style.display = "none";
showPreviewPlaceholder("Diretta terminata");
showPreviewPlaceholder(I18N.streamEnded);
}
async function togglePauseStream() {
@@ -375,25 +448,25 @@
const res = await fetch(url, { method: "POST", headers: { Accept: "application/json" } });
if (!res.ok) {
const body = await res.json().catch(() => ({}));
throw new Error(body.error || (streamPaused ? "Errore ripresa diretta" : "Errore pausa diretta"));
throw new Error(body.error || (streamPaused ? I18N.resumeError : I18N.pauseError));
}
const data = await res.json();
const wasPaused = streamPaused;
applyScorePayload(data);
setBadge(data);
syncPreviewFromStatus(data);
toast(wasPaused ? "Diretta ripresa" : "Diretta in pausa — copertina in onda");
toast(wasPaused ? I18N.resumed : I18N.pausedCover);
}
async function stopStream() {
const ok = confirm("Chiudere definitivamente la diretta?");
const ok = confirm(I18N.closeConfirm);
if (!ok) return null;
const res = await fetch(cfg.stopUrl, { method: "POST", headers: { Accept: "application/json" } });
if (!res.ok) throw new Error("Errore chiusura");
if (!res.ok) throw new Error(I18N.closeError);
const data = await res.json();
cfg.streamClosed = true;
stopPreview();
toast("Diretta chiusa");
toast(I18N.closed);
return data;
}
@@ -402,7 +475,7 @@
async function shareLink(url, title, text) {
if (!url) {
toast("Link non disponibile");
toast(I18N.linkUnavailable);
return;
}
const payload = { title, text, url };
@@ -419,31 +492,23 @@
async function copyLink(url) {
if (!url) {
toast("Link non disponibile");
toast(I18N.linkUnavailable);
return;
}
try {
await navigator.clipboard.writeText(url);
toast("Link copiato");
toast(I18N.linkCopied);
} catch (_) {
prompt("Copia il link:", url);
prompt(I18N.copyPrompt, url);
}
}
document.getElementById("btn-share-regia")?.addEventListener("click", () =>
shareLink(
cfg.regiaShareUrl,
cfg.regiaShareTitle,
"Apri questo link per gestire il punteggio della diretta:"
)
shareLink(cfg.regiaShareUrl, cfg.regiaShareTitle, I18N.shareRegiaText)
);
document.getElementById("btn-copy-regia")?.addEventListener("click", () => copyLink(cfg.regiaShareUrl));
document.getElementById("btn-share-live")?.addEventListener("click", () =>
shareLink(
cfg.liveShareUrl,
cfg.liveShareTitle,
"Guarda la diretta su Match Live TV:"
)
shareLink(cfg.liveShareUrl, cfg.liveShareTitle, I18N.shareLiveText)
);
document.getElementById("btn-copy-live")?.addEventListener("click", () => copyLink(cfg.liveShareUrl));