Files
sudoku/app/i18n.js
snoopy 00e8dcb7d2 Initial commit: Sudoku game with accounts, history, leaderboard, themes and i18n
Node/Express + SQLite backend, vanilla JS frontend, deployed via Docker Compose.
2026-08-27 21:24:38 +02:00

259 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(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();
})();