fix(i18n): complete command center texts and merge client fallbacks
CI / check (push) Successful in 1m41s
CI / deploy (push) Successful in 1m37s
CI / e2e (push) Successful in 22s

This commit is contained in:
Simo committed 2026-09-05 20:24:41 +02:00
1 parent 8b59bd1d2d
commit b12e405b34
5 files changed
+272 -5

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
import { readFileSync } from "node:fs";
import { createTranslator } from "next-intl";
import { describe, expect, it } from "vitest";
import en from "@/messages/en.json";
import itMessages from "@/messages/it.json";
import nl from "@/messages/nl.json";
import { mergeMessages } from "./merge-messages";
const namespace = "pages.admin.commandocentrum";
const keys = new Set(
["page.tsx", "commandocentrum-controls.tsx"].flatMap((file) =>
Array.from(
readFileSync(`src/app/admin/commandocentrum/${file}`, "utf8").matchAll(
/\bt\("([^"]+)"/g,
),
(match) => match[1],
),
),
);
for (const group of ["hotel", "players", "rewards", "advanced"])
keys.add(`groups.${group}`);
describe("command center translations", () => {
it.each([
["en", en],
["it", itMessages],
] as const)("provides every rendered message in %s", (locale, messages) => {
const errors: unknown[] = [];
const t = createTranslator({
locale,
messages: messages as typeof en,
namespace,
onError: (error) => errors.push(error),
});
for (const key of keys) {
expect(t.has(key as Parameters<typeof t>[0]), key).toBe(true);
expect(
t(key as Parameters<typeof t>[0], { count: 42, time: "12:34" }),
key,
).not.toBe("");
}
expect(errors).toEqual([]);
});
it("supplies nested fallback messages to a client translator and formats values", () => {
const messages = mergeMessages(en, nl) as typeof en;
const t = createTranslator({ locale: "nl", messages, namespace });
expect(t("title")).toBe(nl.pages.admin.commandocentrum.title);
expect(t("totalAccounts", { count: 42 })).toBe("42 total accounts");
expect(t("groups.players")).toBe("Players");
expect(t("checkedAt", { time: "12:34" })).toBe("Checked at 12:34");
expect(nl.pages.admin.commandocentrum).not.toHaveProperty("totalAccounts");
});
});
+17
View File
@@ -0,0 +1,17 @@
export type MessageTree = { [key: string]: string | MessageTree };
/** Fill missing translations before sending messages to server and client consumers. */
export function mergeMessages(
fallback: MessageTree,
localized: MessageTree,
): MessageTree {
const result: MessageTree = { ...fallback };
for (const [key, value] of Object.entries(localized)) {
const base = fallback[key];
result[key] =
typeof value === "object" && typeof base === "object"
? mergeMessages(base, value)
: value;
}
return result;
}
+12 -3
View File
@@ -1,5 +1,6 @@
import { cookies, headers } from "next/headers";
import { getRequestConfig } from "next-intl/server";
import { mergeMessages } from "./merge-messages";
// i18n WITHOUT routing: the locale is chosen by a `NEXT_LOCALE` cookie, then
// the Accept-Language header, and finally English as fallback.
@@ -83,11 +84,19 @@ export default getRequestConfig(async () => {
return {
locale,
messages,
messages: mergeMessages(fallback, messages),
// Never throw on a missing message — use the English value, or a humanised
// key as a last resort.
getMessageFallback({ key }: { key: string }) {
const fromEnglish = key
getMessageFallback({
namespace,
key,
}: {
namespace?: string;
key: string;
}) {
const fromEnglish = [namespace, key]
.filter(Boolean)
.join(".")
.split(".")
.reduce<unknown>(
(o, k) =>
+95 -1
View File
@@ -2449,7 +2449,101 @@
"readFailed": "Data could not be loaded",
"emulatorConfig": "Emulator connection",
"emailConfig": "Email delivery",
"configured": "Configured"
"configured": "Configured",
"playersOnline": "Players online",
"totalAccounts": "{count} total accounts",
"dbUnavailable": "Database unavailable",
"emulator": "Emulator",
"online": "Online",
"offline": "Offline",
"rconReachable": "Emulator connection available",
"rconNotResponding": "Emulator is not responding",
"database": "Database",
"connected": "Connected",
"down": "Unavailable",
"queriesResponding": "Database queries are responding",
"cannotReachDb": "Cannot connect to the database",
"emulatorErrors": "Emulator errors",
"last50Logged": "Up to 50 latest records",
"serverInfo": "Server information",
"node": "Node.js",
"platform": "Platform",
"appUptime": "Application uptime",
"heapUsed": "Heap memory used",
"rss": "Process memory (RSS)",
"hostUptime": "Host uptime",
"load1m": "Load average (1 min)",
"cpus": "Logical CPUs",
"systemDiagnostics": "System diagnostics",
"connectionHealthy": "Connection is healthy",
"unreachable": "Unreachable",
"emulatorRcon": "Emulator connection",
"respondingToPing": "Responding to health checks",
"noResponse": "No response",
"notConfigured": "Not configured",
"twoFaEncryptionKey": "2FA encryption key",
"appKeyConfigured": "Encryption key configured",
"appKeyMissing": "Encryption key missing",
"maintenanceMode": "Maintenance mode",
"siteIsInMaintenance": "The site is in maintenance mode",
"siteIsLive": "The site is open",
"recentStaffActivity": "Recent staff activity",
"noStaffActivity": "No recent staff activity",
"colWhen": "Date and time",
"colStaff": "Staff member",
"colAction": "Action",
"colDescription": "Description",
"recentEmulatorErrors": "Recent emulator errors",
"noEmulatorErrors": "No emulator errors recorded",
"colType": "Type",
"colVersion": "Version",
"colStacktrace": "Error details",
"truncated": "Further lines omitted",
"empty": "No details available",
"defaultGiftMessage": "A gift for you!",
"emulatorControls": "Emulator commands",
"updateCatalog": "Reload catalog",
"updateCatalogDesc": "Reload the catalog from the database.",
"updateWordFilter": "Reload word filter",
"updateWordFilterDesc": "Apply the latest word filter settings.",
"updateNavigator": "Reload navigator",
"updateNavigatorDesc": "Reload the room navigator data.",
"hotelAlert": "Hotel announcement",
"hotelAlertDesc": "Send a message to all online players.",
"messageToBroadcast": "Message for all players",
"sendAlert": "Send announcement",
"disconnectDesc": "Disconnect the selected player from the hotel.",
"userId": "User ID",
"usernameRequired": "Username (required)",
"alertDesc": "Send a private alert to a player.",
"alertMessage": "Alert message",
"forwardToRoom": "Move player to room",
"forwardDesc": "Send the selected player to a specific room.",
"roomId": "Room ID",
"forwardUser": "Move player",
"giveCredits": "Give credits",
"giveCreditsDesc": "Add credits to the selected player’s balance.",
"amount": "Amount",
"giveDuckets": "Give duckets",
"giveDucketsDesc": "Add duckets to the selected player’s balance.",
"giveDiamonds": "Give diamonds",
"giveDiamondsDesc": "Add diamonds to the selected player’s balance.",
"giveBadge": "Give badge",
"giveBadgeDesc": "Give a badge to the selected player.",
"badgeCode": "Badge code",
"setMotto": "Change motto",
"setMottoDesc": "Update the selected player’s motto.",
"newMotto": "New motto",
"setRank": "Change rank",
"setRankDesc": "Update the selected player’s rank.",
"rankPlaceholder": "Rank (1–10)",
"executeCommand": "Execute emulator command",
"executeCommandDesc": "Execute a command for the selected player.",
"commandPlaceholder": "Command",
"sendGift": "Send gift",
"sendGiftDesc": "Send a furniture item as a gift to the selected player.",
"furniId": "Furniture ID",
"giftMessage": "Gift message"
},
"emulator": {
"title": "Emulator Config",
+95 -1
View File
@@ -2331,7 +2331,101 @@
"readFailed": "Impossibile caricare i dati",
"emulatorConfig": "Connessione emulatore",
"emailConfig": "Invio email",
"configured": "Configurato"
"configured": "Configurato",
"playersOnline": "Giocatori online",
"totalAccounts": "{count} account totali",
"dbUnavailable": "Database non disponibile",
"emulator": "Emulatore",
"online": "Online",
"offline": "Offline",
"rconReachable": "Connessione all’emulatore disponibile",
"rconNotResponding": "L’emulatore non risponde",
"database": "Database",
"connected": "Connesso",
"down": "Non disponibile",
"queriesResponding": "Il database risponde alle richieste",
"cannotReachDb": "Impossibile connettersi al database",
"emulatorErrors": "Errori emulatore",
"last50Logged": "Fino agli ultimi 50 eventi registrati",
"serverInfo": "Informazioni server",
"node": "Node.js",
"platform": "Piattaforma",
"appUptime": "Tempo di attività applicazione",
"heapUsed": "Memoria heap utilizzata",
"rss": "Memoria processo (RSS)",
"hostUptime": "Tempo di attività server",
"load1m": "Carico medio (1 min)",
"cpus": "CPU logiche",
"systemDiagnostics": "Diagnostica sistema",
"connectionHealthy": "Connessione funzionante",
"unreachable": "Non raggiungibile",
"emulatorRcon": "Connessione emulatore",
"respondingToPing": "Risponde ai controlli di stato",
"noResponse": "Nessuna risposta",
"notConfigured": "Non configurato",
"twoFaEncryptionKey": "Chiave di cifratura 2FA",
"appKeyConfigured": "Chiave di cifratura configurata",
"appKeyMissing": "Chiave di cifratura mancante",
"maintenanceMode": "Modalità manutenzione",
"siteIsInMaintenance": "Il sito è in manutenzione",
"siteIsLive": "Il sito è aperto",
"recentStaffActivity": "Attività recenti dello staff",
"noStaffActivity": "Nessuna attività recente dello staff",
"colWhen": "Data e ora",
"colStaff": "Membro dello staff",
"colAction": "Azione",
"colDescription": "Descrizione",
"recentEmulatorErrors": "Errori recenti dell’emulatore",
"noEmulatorErrors": "Nessun errore dell’emulatore registrato",
"colType": "Tipo",
"colVersion": "Versione",
"colStacktrace": "Dettagli errore",
"truncated": "Ulteriori righe omesse",
"empty": "Nessun dettaglio disponibile",
"defaultGiftMessage": "Un regalo per te!",
"emulatorControls": "Comandi emulatore",
"updateCatalog": "Ricarica catalogo",
"updateCatalogDesc": "Ricarica il catalogo dal database.",
"updateWordFilter": "Ricarica filtro parole",
"updateWordFilterDesc": "Applica le ultime impostazioni del filtro parole.",
"updateNavigator": "Ricarica navigatore",
"updateNavigatorDesc": "Ricarica i dati del navigatore stanze.",
"hotelAlert": "Avviso hotel",
"hotelAlertDesc": "Invia un messaggio a tutti i giocatori online.",
"messageToBroadcast": "Messaggio per tutti i giocatori",
"sendAlert": "Invia avviso",
"disconnectDesc": "Disconnetti il giocatore selezionato dall’hotel.",
"userId": "ID utente",
"usernameRequired": "Nome utente (obbligatorio)",
"alertDesc": "Invia un avviso privato a un giocatore.",
"alertMessage": "Messaggio di avviso",
"forwardToRoom": "Sposta giocatore in stanza",
"forwardDesc": "Invia il giocatore selezionato in una stanza specifica.",
"roomId": "ID stanza",
"forwardUser": "Sposta giocatore",
"giveCredits": "Assegna crediti",
"giveCreditsDesc": "Aggiungi crediti al saldo del giocatore selezionato.",
"amount": "Quantità",
"giveDuckets": "Assegna duckets",
"giveDucketsDesc": "Aggiungi duckets al saldo del giocatore selezionato.",
"giveDiamonds": "Assegna diamanti",
"giveDiamondsDesc": "Aggiungi diamanti al saldo del giocatore selezionato.",
"giveBadge": "Assegna distintivo",
"giveBadgeDesc": "Assegna un distintivo al giocatore selezionato.",
"badgeCode": "Codice distintivo",
"setMotto": "Modifica motto",
"setMottoDesc": "Aggiorna il motto del giocatore selezionato.",
"newMotto": "Nuovo motto",
"setRank": "Modifica rank",
"setRankDesc": "Aggiorna il rank del giocatore selezionato.",
"rankPlaceholder": "Rank (1–10)",
"executeCommand": "Esegui comando emulatore",
"executeCommandDesc": "Esegui un comando per il giocatore selezionato.",
"commandPlaceholder": "Comando",
"sendGift": "Invia regalo",
"sendGiftDesc": "Invia un furno in regalo al giocatore selezionato.",
"furniId": "ID furno",
"giftMessage": "Messaggio del regalo"
},
"emulator": {
"title": "Configurazione emulatore",