259 lines
12 KiB
JavaScript
259 lines
12 KiB
JavaScript
(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 <b>anonyme</b>. 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 <b>guest</b>. 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();
|
||
})();
|