(function () { "use strict"; const DICTS = { fr: { auth: { login: "Se connecter", register: "S'inscrire", logout: "Déconnexion", loginTab: "Connexion", registerTab: "Inscription", identifierLabel: "Pseudo ou email", passwordLabel: "Mot de passe", submitLogin: "Se connecter", pseudoLabel: "Pseudo", emailLabel: "Email", submitRegister: "Créer mon compte", benefit1: "📜 Historique de toutes vos parties", benefit2: "⏱️ Suivi de votre vitesse de résolution", benefit3: "🏆 Apparition au classement des joueurs", }, banner: { html: "Vous jouez en anonyme. Créez un compte gratuit pour sauvegarder votre historique, suivre votre vitesse de résolution et grimper au classement !", }, topbar: { difficulty: "Difficulté", time: "Temps", mistakes: "Erreurs" }, difficulty: { easy: "Facile", medium: "Moyen", hard: "Difficile", expert: "Expert" }, board: { ready: "Prêt à jouer ?", start: "▶ Démarrer", paused: "⏸ En pause", resume: "▶ Reprendre" }, actions: { pause: "⏸ Pause", stop: "⏹ Stop", undo: "↩️ Annuler", erase: "🧹 Effacer", notes: "✏️ Notes", hint: "💡 Indice", history: "📜 Historique", leaderboard: "🏆 Classement", rules: "📖 Règles", }, confirm: { stop: "Abandonner la partie en cours ?" }, win: { title: "🎉 Bravo !", subtitle: "Grille {difficulty} résolue.", time: "TEMPS", mistakesLabel: "ERREURS", pointsSaved: "+{points} points enregistrés", saveError: "Erreur d'enregistrement : {msg}", ctaText: "Connectez-vous pour sauvegarder ce score et apparaître au classement.", ctaButton: "Créer un compte gratuit", replay: "Rejouer", }, msg: { welcome: "Bienvenue, {pseudo} !", accountCreated: "Compte créé, bienvenue {pseudo} !", loggedOut: "Vous êtes déconnecté.", loginRequiredHistory: "Connectez-vous pour consulter votre historique.", scoreSaved: "Score enregistré : +{points} points !", scoreSaveError: "Impossible d'enregistrer le score : {msg}", }, history: { title: "📜 Historique", loading: "Chargement…", empty: "Aucune partie enregistrée pour le moment.", error: "Erreur : {msg}", colDate: "Date", colDifficulty: "Difficulté", colTime: "Temps", colMistakes: "Erreurs", colResult: "Résultat", colPoints: "Points", }, leaderboard: { title: "🏆 Classement", tabTimes: "Meilleurs temps", tabPoints: "Points", emptyTimes: "Aucun score pour cette difficulté.", emptyPoints: "Aucun classement disponible.", colRank: "#", colPlayer: "Joueur", colTime: "Temps", colMistakes: "Erreurs", colPoints: "Points", colWins: "Victoires", error: "Erreur : {msg}", }, rules: { title: "📖 Règles & guide du débutant", objectiveTitle: "Objectif", objectiveText: "Remplissez la grille de 9×9 cases avec des chiffres de 1 à 9.", goldenRulesTitle: "Les 3 règles d'or", rule1: "Chaque ligne doit contenir les chiffres de 1 à 9, sans répétition.", rule2: "Chaque colonne doit contenir les chiffres de 1 à 9, sans répétition.", rule3: "Chaque bloc de 3×3 doit contenir les chiffres de 1 à 9, sans répétition.", howToTitle: "Comment jouer sur ce site", how1: "Cliquez sur une case vide puis sur un chiffre (ou utilisez les touches 1-9 du clavier).", how2: "✏️ Notes : activez ce mode pour noter les chiffres possibles dans une case, utile pour explorer les hypothèses.", how3: "💡 Indice : révèle la bonne valeur dans la case sélectionnée (ou une case au hasard).", how4: "↩️ Annuler : revient sur votre dernière action.", how5: "⏸ Pause / ⏹ Stop : mettez la partie en pause ou abandonnez-la à tout moment.", tipsTitle: "Astuces pour débutants", tip1: "Repérez d'abord les chiffres qui apparaissent le plus souvent dans la grille de départ.", tip2: "Pour une case donnée, éliminez les chiffres déjà présents dans sa ligne, sa colonne et son bloc.", tip3: "Si un chiffre ne peut aller qu'à un seul endroit dans un bloc, une ligne ou une colonne, placez-le.", tip4: "Utilisez les notes pour les cases à plusieurs possibilités, puis réduisez au fur et à mesure.", tip5: "Ne devinez pas au hasard : chaque grille a une solution logique unique.", }, footer: { text: "Fait maison • servi depuis un conteneur Docker" }, settings: { language: "Langue", theme: "Thème" }, theme: { blue: "Bleu", green: "Émeraude", amber: "Ambre", mono: "Noir & blanc" }, errors: { INVALID_PSEUDO: "Pseudo invalide (3 à 20 caractères, lettres/chiffres/_).", INVALID_EMAIL: "Adresse email invalide.", INVALID_PASSWORD: "Le mot de passe doit contenir au moins 6 caractères.", PSEUDO_TAKEN: "Ce pseudo est déjà utilisé.", EMAIL_TAKEN: "Cette adresse email est déjà utilisée.", REGISTER_FAILED: "Erreur lors de l'inscription.", MISSING_CREDENTIALS: "Identifiant et mot de passe requis.", INVALID_CREDENTIALS: "Identifiants incorrects.", AUTH_REQUIRED: "Authentification requise.", RATE_LIMITED: "Trop de tentatives, réessayez plus tard.", INVALID_DIFFICULTY: "Difficulté invalide.", INVALID_TIME: "Temps invalide.", INVALID_MISTAKES: "Nombre d'erreurs invalide.", TIME_TOO_SHORT: "Temps trop court pour cette difficulté.", UNKNOWN: "Une erreur est survenue.", }, }, en: { auth: { login: "Log in", register: "Sign up", logout: "Log out", loginTab: "Login", registerTab: "Sign up", identifierLabel: "Username or email", passwordLabel: "Password", submitLogin: "Log in", pseudoLabel: "Username", emailLabel: "Email", submitRegister: "Create my account", benefit1: "📜 History of all your games", benefit2: "⏱️ Track your solving speed", benefit3: "🏆 Appear on the player leaderboard", }, banner: { html: "You're playing as a guest. Create a free account to save your history, track your solving speed and climb the leaderboard!", }, topbar: { difficulty: "Difficulty", time: "Time", mistakes: "Mistakes" }, difficulty: { easy: "Easy", medium: "Medium", hard: "Hard", expert: "Expert" }, board: { ready: "Ready to play?", start: "▶ Start", paused: "⏸ Paused", resume: "▶ Resume" }, actions: { pause: "⏸ Pause", stop: "⏹ Stop", undo: "↩️ Undo", erase: "🧹 Erase", notes: "✏️ Notes", hint: "💡 Hint", history: "📜 History", leaderboard: "🏆 Leaderboard", rules: "📖 Rules", }, confirm: { stop: "Abandon the current game?" }, win: { title: "🎉 Well done!", subtitle: "{difficulty} grid solved.", time: "TIME", mistakesLabel: "MISTAKES", pointsSaved: "+{points} points saved", saveError: "Save error: {msg}", ctaText: "Log in to save this score and appear on the leaderboard.", ctaButton: "Create a free account", replay: "Play again", }, msg: { welcome: "Welcome, {pseudo}!", accountCreated: "Account created, welcome {pseudo}!", loggedOut: "You are logged out.", loginRequiredHistory: "Log in to view your history.", scoreSaved: "Score saved: +{points} points!", scoreSaveError: "Could not save the score: {msg}", }, history: { title: "📜 History", loading: "Loading…", empty: "No games recorded yet.", error: "Error: {msg}", colDate: "Date", colDifficulty: "Difficulty", colTime: "Time", colMistakes: "Mistakes", colResult: "Result", colPoints: "Points", }, leaderboard: { title: "🏆 Leaderboard", tabTimes: "Best times", tabPoints: "Points", emptyTimes: "No scores for this difficulty.", emptyPoints: "No leaderboard available.", colRank: "#", colPlayer: "Player", colTime: "Time", colMistakes: "Mistakes", colPoints: "Points", colWins: "Wins", error: "Error: {msg}", }, rules: { title: "📖 Rules & beginner's guide", objectiveTitle: "Objective", objectiveText: "Fill the 9×9 grid with numbers from 1 to 9.", goldenRulesTitle: "The 3 golden rules", rule1: "Each row must contain the numbers 1 to 9, without repetition.", rule2: "Each column must contain the numbers 1 to 9, without repetition.", rule3: "Each 3×3 block must contain the numbers 1 to 9, without repetition.", howToTitle: "How to play on this site", how1: "Click an empty cell then a number (or use the 1-9 keyboard keys).", how2: "✏️ Notes: turn this on to jot down candidate numbers in a cell — handy for exploring possibilities.", how3: "💡 Hint: reveals the correct value in the selected cell (or a random one).", how4: "↩️ Undo: reverts your last action.", how5: "⏸ Pause / ⏹ Stop: pause the game or abandon it at any time.", tipsTitle: "Tips for beginners", tip1: "Start by spotting the numbers that appear most often in the starting grid.", tip2: "For a given cell, eliminate numbers already present in its row, column and block.", tip3: "If a number can only go in one spot within a block, row or column, place it there.", tip4: "Use notes for cells with several possibilities, then narrow them down as you go.", tip5: "Don't guess randomly: every grid has a unique logical solution.", }, footer: { text: "Homemade • served from a Docker container" }, settings: { language: "Language", theme: "Theme" }, theme: { blue: "Blue", green: "Emerald", amber: "Amber", mono: "Black & white" }, errors: { INVALID_PSEUDO: "Invalid username (3 to 20 characters, letters/digits/_).", INVALID_EMAIL: "Invalid email address.", INVALID_PASSWORD: "Password must be at least 6 characters.", PSEUDO_TAKEN: "This username is already taken.", EMAIL_TAKEN: "This email address is already in use.", REGISTER_FAILED: "An error occurred during registration.", MISSING_CREDENTIALS: "Username/email and password required.", INVALID_CREDENTIALS: "Incorrect credentials.", AUTH_REQUIRED: "Authentication required.", RATE_LIMITED: "Too many attempts, please try again later.", INVALID_DIFFICULTY: "Invalid difficulty.", INVALID_TIME: "Invalid time.", INVALID_MISTAKES: "Invalid number of mistakes.", TIME_TOO_SHORT: "Time too short for this difficulty.", UNKNOWN: "An error occurred.", }, }, }; function detectInitialLang() { try { const saved = localStorage.getItem('sudoku-lang'); if (saved === 'fr' || saved === 'en') return saved; } catch (e) {} return 'fr'; } let lang = detectInitialLang(); function get(dict, path) { return path.split('.').reduce((o, k) => (o && o[k] !== undefined ? o[k] : undefined), dict); } function t(key, vars) { let str = get(DICTS[lang], key); if (str === undefined) str = get(DICTS.fr, key); if (str === undefined) return key; if (vars) { Object.keys(vars).forEach(k => { str = str.replace(new RegExp('\\{' + k + '\\}', 'g'), vars[k]); }); } return str; } function applyTranslations() { document.documentElement.lang = lang; document.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.getAttribute('data-i18n')); }); document.querySelectorAll('[data-i18n-html]').forEach(el => { el.innerHTML = t(el.getAttribute('data-i18n-html')); }); } function setLang(l) { if (l !== 'fr' && l !== 'en') return; lang = l; try { localStorage.setItem('sudoku-lang', l); } catch (e) {} applyTranslations(); if (typeof window.onLangChange === 'function') window.onLangChange(l); } window.I18N = { t, setLang, getLang: () => lang, applyTranslations }; applyTranslations(); })();