Implement en/fr localization across admin and user UIs
Introduce client-side localization helper and en/fr locale bundles. Route admin control, picks, movies, stats, winners and settings UIs through i18n keys instead of hardcoded text. Remove French text from the English UI and standardize shared messages (saving, network errors, confirmations). Update .gitignore to ignore editor metadata and localization helper artifacts.
This commit is contained in:
+53
-3
@@ -16,6 +16,7 @@ const APP_VERSION_KEY = "app_version_control";
|
||||
const PLAYER_ADMIN_STATUS_UI_KEY = "player_admin_status_ui";
|
||||
const POINTS_CONFIG_KEY = "points_config";
|
||||
const VISIBILITY_CONFIG_KEY = "visibility_config";
|
||||
const LANGUAGE_KEY = "site_language";
|
||||
|
||||
const crypto = require("crypto");
|
||||
|
||||
@@ -38,10 +39,14 @@ const DEFAULT_POINTS_CONFIG = Object.freeze({
|
||||
});
|
||||
|
||||
const DEFAULT_VISIBILITY_CONFIG = Object.freeze({
|
||||
showPicksButton: true,
|
||||
showBonPicksColumn: true,
|
||||
showPicksButton: false,
|
||||
showBonPicksColumn: false,
|
||||
showVersionTab: true,
|
||||
});
|
||||
|
||||
const DEFAULT_LANGUAGE = "en";
|
||||
const SUPPORTED_LANGUAGES = Object.freeze(["en", "fr"]);
|
||||
|
||||
function parseOscarYear(raw) {
|
||||
if (raw === undefined || raw === null || raw === "") return null;
|
||||
const n = Number(raw);
|
||||
@@ -772,7 +777,8 @@ function normalizeVisibilityConfig(rawValue) {
|
||||
const v = rawValue && typeof rawValue === "object" && !Array.isArray(rawValue) ? rawValue : {};
|
||||
const showPicksButton = typeof v.showPicksButton === "boolean" ? v.showPicksButton : DEFAULT_VISIBILITY_CONFIG.showPicksButton;
|
||||
const showBonPicksColumn = typeof v.showBonPicksColumn === "boolean" ? v.showBonPicksColumn : DEFAULT_VISIBILITY_CONFIG.showBonPicksColumn;
|
||||
return { showPicksButton, showBonPicksColumn };
|
||||
const showVersionTab = typeof v.showVersionTab === "boolean" ? v.showVersionTab : DEFAULT_VISIBILITY_CONFIG.showVersionTab;
|
||||
return { showPicksButton, showBonPicksColumn, showVersionTab };
|
||||
}
|
||||
|
||||
// Public: get points configuration
|
||||
@@ -851,5 +857,49 @@ router.put("/visibility-config", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Language normalization
|
||||
function normalizeLanguage(raw) {
|
||||
const lang = String(raw || "").trim().toLowerCase();
|
||||
return SUPPORTED_LANGUAGES.includes(lang) ? lang : DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
// Public: get current language setting
|
||||
router.get("/language", async (req, res) => {
|
||||
try {
|
||||
const existing = await Setting.findOne({ key: LANGUAGE_KEY }).select("value");
|
||||
const language = normalizeLanguage(existing?.value);
|
||||
return res.status(200).json({ language, supported: SUPPORTED_LANGUAGES });
|
||||
} catch (err) {
|
||||
return res.status(200).json({ language: DEFAULT_LANGUAGE, supported: SUPPORTED_LANGUAGES });
|
||||
}
|
||||
});
|
||||
|
||||
// Admin-only: set language
|
||||
router.put("/language", async (req, res) => {
|
||||
const token = req.headers.authorization?.split(" ")[1];
|
||||
if (!token) return res.status(401).json({ message: "Authentication token is required" });
|
||||
|
||||
try {
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET);
|
||||
if (!decoded.admin) {
|
||||
return res.status(403).json({ message: "You do not have admin privileges" });
|
||||
}
|
||||
|
||||
const language = normalizeLanguage(req.body?.language);
|
||||
await Setting.findOneAndUpdate(
|
||||
{ key: LANGUAGE_KEY },
|
||||
{ $set: { value: language } },
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
|
||||
return res.status(200).json({ language, supported: SUPPORTED_LANGUAGES });
|
||||
} catch (err) {
|
||||
if (err instanceof jwt.JsonWebTokenError) {
|
||||
return res.status(401).json({ message: "Invalid or expired token" });
|
||||
}
|
||||
return res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
|
||||
+24
-7
@@ -501,7 +501,10 @@ router.post("/register", async (req, res) => {
|
||||
throw new Error("Le nom du chien doit être défini");
|
||||
}
|
||||
if (!registerVerification()) {
|
||||
return res.status(400).json({ message: "Mauvais nom de chien" });
|
||||
return res.status(400).json({
|
||||
messageKey: 'auth.dogAnswerIncorrect',
|
||||
message: 'Incorrect dog name.',
|
||||
});
|
||||
}
|
||||
|
||||
const existingUser = await User.findOne({ email: emailNorm });
|
||||
@@ -515,7 +518,10 @@ router.post("/register", async (req, res) => {
|
||||
const user = new User({ name: nameNorm, email: emailNorm, password: hashedPassword, role: 0 });
|
||||
await user.save();
|
||||
|
||||
res.status(201).json({ message: "L'utilisateur s'est enregistré avec succès !" });
|
||||
res.status(201).json({
|
||||
messageKey: 'auth.registrationSuccess',
|
||||
message: 'User registered successfully!',
|
||||
});
|
||||
} catch (err) {
|
||||
return res.status(400).json({ message: err?.message || "Erreur interne" });
|
||||
}
|
||||
@@ -527,19 +533,30 @@ router.post("/login", async (req, res) => {
|
||||
|
||||
try {
|
||||
const user = await User.findOne({ email });
|
||||
if (!user) return res.status(404).json({ message: "L'utilisateur n'a pas été trouvé" });
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
messageKey: 'auth.userNotFound',
|
||||
message: 'User not found',
|
||||
});
|
||||
}
|
||||
|
||||
// If this account is in "temp password" mode and the temp password has expired, block login.
|
||||
if (user.mustChangePassword && user.tempPasswordExpiresAt && user.tempPasswordExpiresAt instanceof Date) {
|
||||
if (Date.now() > user.tempPasswordExpiresAt.getTime()) {
|
||||
return res.status(401).json({
|
||||
message: "Le mot de passe temporaire a expiré. Demandez à un admin de réinitialiser votre mot de passe.",
|
||||
messageKey: 'auth.tempPasswordExpired',
|
||||
message: 'Temporary password expired. Ask an admin to reset your password.',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const isMatch = await bcrypt.compare(password, user.password);
|
||||
if (!isMatch) return res.status(401).json({ message: "Informations d'identification non valides" });
|
||||
if (!isMatch) {
|
||||
return res.status(401).json({
|
||||
messageKey: 'auth.invalidCredentials',
|
||||
message: 'Invalid credentials',
|
||||
});
|
||||
}
|
||||
|
||||
console.log("[debug] " + user.name + " logged in")
|
||||
|
||||
@@ -613,7 +630,7 @@ router.post("/admin/reset-temp-password", verifyToken, async (req, res) => {
|
||||
await user.save();
|
||||
|
||||
return res.status(200).json({
|
||||
message: "Mot de passe temporaire généré. L'utilisateur devra le changer à sa prochaine connexion.",
|
||||
message: "Temporary password generated. The user must change it on next login.",
|
||||
tempPassword,
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
user: { id: String(user._id), name: user.name, email: user.email },
|
||||
@@ -697,7 +714,7 @@ router.post("/admin/create", verifyToken, async (req, res) => {
|
||||
await user.save();
|
||||
|
||||
return res.status(201).json({
|
||||
message: "Utilisateur créé. Mot de passe temporaire généré.",
|
||||
message: "User created. Temporary password generated.",
|
||||
tempPassword,
|
||||
expiresAt: expiresAt.toISOString(),
|
||||
user: { id: String(user._id), name: user.name, email: user.email, admin: user.role === 69, mustChangePassword: true },
|
||||
|
||||
+4
-2
@@ -79,7 +79,8 @@ router.get("/", async (req, res) => {
|
||||
if (!active) {
|
||||
return res.status(200).json({
|
||||
version: getPackageVersion(),
|
||||
message: "Version non configurée (définis-la dans Admin → Version).",
|
||||
messageKey: 'footer.versionNotConfigured',
|
||||
message: 'Version not configured (set it in Admin → Version).',
|
||||
author: "",
|
||||
date: nowFormatted(),
|
||||
configured: false,
|
||||
@@ -103,7 +104,8 @@ router.get("/", async (req, res) => {
|
||||
// If DB is down, still return something useful.
|
||||
return res.status(200).json({
|
||||
version: getPackageVersion(),
|
||||
message: "Version indisponible (DB).",
|
||||
messageKey: 'footer.versionUnavailable',
|
||||
message: 'Version unavailable (DB).',
|
||||
author: "",
|
||||
date: nowFormatted(),
|
||||
configured: false,
|
||||
|
||||
Reference in New Issue
Block a user