Initial commit: Sudoku game with accounts, history, leaderboard, themes and i18n
Node/Express + SQLite backend, vanilla JS frontend, deployed via Docker Compose.
This commit is contained in:
258
app/i18n.js
Normal file
258
app/i18n.js
Normal file
@@ -0,0 +1,258 @@
|
||||
(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();
|
||||
})();
|
||||
Reference in New Issue
Block a user