diff --git a/public/css/app.css b/public/css/app.css index 702e5a5..3733ce7 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -400,7 +400,7 @@ footer { } /* Smooth navbar transition while i18n loads - - Keep navbar visible but subtly blurred/faded to avoid abrupt disappearance. + - Keep navbar visible with a soft fade/slide to avoid abrupt disappearance. - Make elements non-interactive until translations are applied. */ body:not(.i18n-ready) nav.navbar, body:not(.i18n-ready) .navbar, @@ -409,12 +409,25 @@ body:not(.i18n-ready) .navbar .nav-link, body:not(.i18n-ready) .navbar .navbar-text, body:not(.i18n-ready) .navbar .btn { transition: filter 220ms ease, opacity 220ms ease, transform 220ms ease; - filter: blur(6px) saturate(0.9) grayscale(0.06); - opacity: 0.85; + /* Light mode & default: fully fade out navbar until i18n is ready + so fallback labels never flash. */ + opacity: 0; transform: translateY(-2px); pointer-events: none; } +/* Dark mode: keep the original soft blur effect while loading + so the navbar stays visible but its text is obscured. */ +html[data-theme="dark"] body:not(.i18n-ready) nav.navbar, +html[data-theme="dark"] body:not(.i18n-ready) .navbar, +html[data-theme="dark"] body:not(.i18n-ready) .navbar .navbar-brand, +html[data-theme="dark"] body:not(.i18n-ready) .navbar .nav-link, +html[data-theme="dark"] body:not(.i18n-ready) .navbar .navbar-text, +html[data-theme="dark"] body:not(.i18n-ready) .navbar .btn { + filter: blur(6px) saturate(0.9) grayscale(0.06); + opacity: 0.85; +} + /* When translations are ready, restore full clarity and interactivity */ body.i18n-ready nav.navbar, body.i18n-ready .navbar, diff --git a/public/js/app_checklist.js b/public/js/app_checklist.js index 6ac7911..8c3b9c8 100644 --- a/public/js/app_checklist.js +++ b/public/js/app_checklist.js @@ -155,7 +155,8 @@ window.addEventListener('DOMContentLoaded', async function () { } const DEFAULT_COMPLETION_MODAL = Object.freeze({ - title: (window.i18n && typeof window.i18n.t === 'function') ? window.i18n.t('checklist.congratulations') : 'Congratulations! very nice 🎉🎉🎉', + // Default title: use the localized 100% modal title if available. + title: t('admin.modal100.defaultTitle', 'Congratulations! very nice 🎉🎉🎉'), bodyText: '', videoSrc: 'video/reward.mp4', bodyHtml: '' @@ -194,7 +195,12 @@ window.addEventListener('DOMContentLoaded', async function () { const videoEl = document.getElementById('rewardVideo'); const videoSourceEl = document.getElementById('rewardVideoSource'); - const title = String(content?.title || '').trim() || DEFAULT_COMPLETION_MODAL.title; + const rawTitle = String(content?.title || '').trim(); + // If the stored value is the i18n key itself, translate it; otherwise + // use the stored title, falling back to the localized default. + const title = (!rawTitle || rawTitle === 'admin.modal100.defaultTitle') + ? t('admin.modal100.defaultTitle', 'Congratulations! very nice 🎉🎉🎉') + : rawTitle; const bodyText = String(content?.bodyText || ''); const bodyHtml = sanitizeHtml(String(content?.bodyHtml || '')); const videoSrc = String(content?.videoSrc || '').trim(); diff --git a/public/js/app_control.js b/public/js/app_control.js index 095d1d6..f85a39b 100644 --- a/public/js/app_control.js +++ b/public/js/app_control.js @@ -1637,7 +1637,8 @@ window.onload = async function () { if (!dbBackupListBody) return; const list = Array.isArray(backups) ? backups : []; if (!list.length) { - dbBackupListBody.innerHTML = 'Aucune sauvegarde.'; + const emptyText = t('admin.backup.noBackups', 'No backups found.'); + dbBackupListBody.innerHTML = `${emptyText}`; return; } diff --git a/public/locales/en.json b/public/locales/en.json index 51f4a0b..d83ab96 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -377,6 +377,8 @@ "admin.modal100.videoLabel": "Video", "admin.modal100.videoInfoShort": "accepts relative path (e.g.: video/reward.mp4) or full URL.", + "admin.modal100.defaultTitle": "Congratulations! very nice 🎉🎉🎉", + "admin.users.usersCount": "user(s)", "admin.users.addUser": "Add user", "admin.users.name": "Name", @@ -488,6 +490,7 @@ "admin.backup.downloadDesc": "retrieves the file locally (useful before a risky operation).", "admin.backup.restoreHowDesc": "imports the file. With drop, restored collections are deleted first.", "admin.backup.quickCheck": "Quick check", + "admin.backup.noBackups": "No backups found.", "admin.backup.quickCheckDesc": "After restore, reload the page and check \"Films\", \"Checklist\" and \"Stats\".", "admin.backup.confirmDelete": "Delete backup \"{name}\"?", "admin.backup.creating": "Creating…", diff --git a/public/locales/fr.json b/public/locales/fr.json index 7830eef..1f27797 100644 --- a/public/locales/fr.json +++ b/public/locales/fr.json @@ -488,6 +488,7 @@ "admin.backup.restoreWarning": "La restauration peut écraser les données. Fais un backup juste avant si besoin.", "admin.backup.backupFile": "Fichier de backup", "admin.backup.dropBeforeRestore": "Drop les collections avant restore (recommandé)", + "admin.backup.noBackups": "Aucune sauvegarde.", "admin.backup.tip": "Conseil", "admin.backup.tipDesc": "Utilise drop sauf si tu sais exactement ce que tu fais (sinon risque de doublons/erreurs).", "admin.backup.howItWorks": "Fonctionnement",