diff --git a/src/actions/prefixes.ts b/src/actions/prefixes.ts
index 93ff79a6..b0b6ddfd 100644
--- a/src/actions/prefixes.ts
+++ b/src/actions/prefixes.ts
@@ -116,10 +116,14 @@ const SETTINGS_WHITELIST = new Set([
"enabled",
"max_length",
"min_rank",
+ "min_rank_to_buy",
"allow_colors",
"allow_bold",
"allow_italic",
"default_color",
+ "price_credits",
+ "price_points",
+ "points_type",
]);
const updatePrefixSettingsSchema = z.object({
diff --git a/src/app/admin/import/furni/nitro-editor-dialog.tsx b/src/app/admin/import/furni/nitro-editor-dialog.tsx
index e571ff0a..a54f6c14 100644
--- a/src/app/admin/import/furni/nitro-editor-dialog.tsx
+++ b/src/app/admin/import/furni/nitro-editor-dialog.tsx
@@ -408,7 +408,7 @@ export function NitroEditorDialog({
htmlFor="dim-y"
className="text-xs text-muted-foreground"
>
- Y (profondità)
+ Y (depth)
onOpenChange(false)}
disabled={saving}
>
- Annulla
+ Cancel
)}
- Salva
+ Save
diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx
index 0c806e3d..05727df2 100644
--- a/src/app/admin/layout.tsx
+++ b/src/app/admin/layout.tsx
@@ -109,24 +109,24 @@ function getNavGroups(t: (key: string) => string) {
},
{
href: "/admin/events",
- label: "Events",
+ label: t("events"),
icon: ,
},
{
href: "/admin/polls",
- label: "Polls",
+ label: t("polls"),
icon: ,
},
- { href: "/admin/banners", label: "Banners", icon: },
+ { href: "/admin/banners", label: t("banners"), icon: },
{
href: "/admin/prefixes",
- label: "Prefixes",
+ label: t("prefixes"),
icon: ,
},
],
},
{
- label: t("usersAccess"),
+ label: t("usersAndAccess"),
icon: ,
items: [
{ href: "/admin/users", label: t("users"), icon: },
@@ -150,12 +150,12 @@ function getNavGroups(t: (key: string) => string) {
},
{
href: "/admin/online",
- label: "Online Users",
+ label: t("onlineUsers"),
icon: ,
},
{
href: "/admin/moderation",
- label: "Moderation",
+ label: t("moderation"),
icon: ,
},
],
@@ -286,15 +286,15 @@ function getNavGroups(t: (key: string) => string) {
{ href: "/admin/logs", label: t("logs"), icon: },
{
href: "/admin/logs/audit",
- label: "Audit Log",
+ label: t("auditLog"),
icon: ,
},
{
href: "/admin/analytics",
- label: "Analytics",
+ label: t("analytics"),
icon: ,
},
- { href: "/admin/devops", label: "DevOps", icon: },
+ { href: "/admin/devops", label: t("devops"), icon: },
{
href: "/admin/wordfilter",
label: t("wordFilter"),
diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx
index c2689b58..4123eb8f 100644
--- a/src/app/admin/page.tsx
+++ b/src/app/admin/page.tsx
@@ -168,7 +168,7 @@ export default async function AdminDashboard() {
{t("recentStaffActivity")}
{t("viewAll")}
diff --git a/src/app/admin/permissions/[id]/page.tsx b/src/app/admin/permissions/[id]/page.tsx
index ea3be42d..954b9551 100644
--- a/src/app/admin/permissions/[id]/page.tsx
+++ b/src/app/admin/permissions/[id]/page.tsx
@@ -1,6 +1,6 @@
import { notFound, redirect } from "next/navigation";
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
-import { getAdminContext } from "@/lib/permissions";
+import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { fetchEmulatorRankForEdit } from "@/lib/services/permission-ranks";
import { RankEditClient } from "./rank-edit-client";
@@ -11,8 +11,12 @@ export default async function RankEditPage({
params: Promise<{ locale: string; id: string }>;
}) {
const { id: rawId } = await params;
- const { permissions } = await getAdminContext();
- if (!permissions.isSuperAdmin) redirect("/admin");
+ const { session, permissions } = await getAdminContext();
+ if (
+ !canAccess(permissions, PERMS.PERMISSIONS_MANAGE, session.user.rank)
+ ) {
+ redirect("/admin");
+ }
const rankId = parseInt(rawId, 10);
if (Number.isNaN(rankId)) notFound();
diff --git a/src/app/admin/permissions/page.tsx b/src/app/admin/permissions/page.tsx
index f2efddca..3ab1da5f 100644
--- a/src/app/admin/permissions/page.tsx
+++ b/src/app/admin/permissions/page.tsx
@@ -1,6 +1,6 @@
import { redirect } from "next/navigation";
import { Prisma } from "@/generated/prisma/client";
-import { getAdminContext } from "@/lib/permissions";
+import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { fetchEmulatorRankSummaries } from "@/lib/services/permission-ranks";
import { PermissionsList } from "./permissions-list";
@@ -8,8 +8,12 @@ import { PermissionsList } from "./permissions-list";
export default async function PermissionsPage(_props: {
params: Promise<{ locale: string }>;
}) {
- const { permissions } = await getAdminContext();
- if (!permissions.isSuperAdmin) redirect("/admin");
+ const { session, permissions } = await getAdminContext();
+ if (
+ !canAccess(permissions, PERMS.PERMISSIONS_MANAGE, session.user.rank)
+ ) {
+ redirect("/admin");
+ }
const ranks = await fetchEmulatorRankSummaries(prisma);
diff --git a/src/app/admin/prefixes/page.tsx b/src/app/admin/prefixes/page.tsx
index 104d0697..e4d2a3ef 100644
--- a/src/app/admin/prefixes/page.tsx
+++ b/src/app/admin/prefixes/page.tsx
@@ -20,7 +20,7 @@ export default async function PrefixesPage() {
Custom Prefixes
- Gestisci i prefissi degli utenti, la blacklist e le impostazioni
+ Manage user prefixes, blacklist and settings
diff --git a/src/app/admin/prefixes/prefixes-client.tsx b/src/app/admin/prefixes/prefixes-client.tsx
index 886a614e..f8143e75 100644
--- a/src/app/admin/prefixes/prefixes-client.tsx
+++ b/src/app/admin/prefixes/prefixes-client.tsx
@@ -60,46 +60,46 @@ interface PrefixFormData {
}
const PRESET_EFFECTS = [
- { id: "", label: "Nessuno" },
- { id: "glow", label: "✨ Bagliore" },
- { id: "shadow", label: "🌑 Ombra" },
- { id: "italic", label: "𝑰 Corsivo" },
- { id: "outline", label: "🔲 Contorno" },
- { id: "pulse", label: "💫 Pulsante" },
+ { id: "", label: "None" },
+ { id: "glow", label: "✨ Glow" },
+ { id: "shadow", label: "🌑 Shadow" },
+ { id: "italic", label: "𝑰 Italic" },
+ { id: "outline", label: "🔲 Outline" },
+ { id: "pulse", label: "💫 Pulse" },
{ id: "bold-glow", label: "💡 Neon" },
];
const PRESET_ICONS = [
- { emoji: "", label: "Nessuna" },
- { emoji: "👑", label: "Corona" },
- { emoji: "⭐", label: "Stella" },
- { emoji: "❤️", label: "Cuore" },
- { emoji: "💎", label: "Diamante" },
- { emoji: "🔥", label: "Fuoco" },
- { emoji: "⚡", label: "Fulmine" },
- { emoji: "🛡️", label: "Scudo" },
- { emoji: "💀", label: "Teschio" },
- { emoji: "🎵", label: "Musica" },
- { emoji: "💠", label: "Gemma" },
- { emoji: "⚔️", label: "Spade" },
- { emoji: "🏆", label: "Trofeo" },
- { emoji: "🚀", label: "Razzo" },
- { emoji: "👻", label: "Fantasma" },
- { emoji: "🐾", label: "Zampa" },
- { emoji: "🍃", label: "Foglia" },
+ { emoji: "", label: "None" },
+ { emoji: "👑", label: "Crown" },
+ { emoji: "⭐", label: "Star" },
+ { emoji: "❤️", label: "Heart" },
+ { emoji: "💎", label: "Diamond" },
+ { emoji: "🔥", label: "Fire" },
+ { emoji: "⚡", label: "Lightning" },
+ { emoji: "🛡️", label: "Shield" },
+ { emoji: "💀", label: "Skull" },
+ { emoji: "🎵", label: "Music" },
+ { emoji: "💠", label: "Gem" },
+ { emoji: "⚔️", label: "Swords" },
+ { emoji: "🏆", label: "Trophy" },
+ { emoji: "🚀", label: "Rocket" },
+ { emoji: "👻", label: "Ghost" },
+ { emoji: "🐾", label: "Paw" },
+ { emoji: "🍃", label: "Leaf" },
{ emoji: "😎", label: "Cool" },
- { emoji: "🐺", label: "Lupo" },
- { emoji: "🦁", label: "Leone" },
- { emoji: "🐉", label: "Drago" },
- { emoji: "🦄", label: "Unicorno" },
- { emoji: "🌙", label: "Luna" },
- { emoji: "☀️", label: "Sole" },
- { emoji: "❄️", label: "Neve" },
- { emoji: "🌹", label: "Rosa" },
+ { emoji: "🐺", label: "Wolf" },
+ { emoji: "🦁", label: "Lion" },
+ { emoji: "🐉", label: "Dragon" },
+ { emoji: "🦄", label: "Unicorn" },
+ { emoji: "🌙", label: "Moon" },
+ { emoji: "☀️", label: "Sun" },
+ { emoji: "❄️", label: "Snow" },
+ { emoji: "🌹", label: "Rose" },
{ emoji: "🎮", label: "Controller" },
- { emoji: "💰", label: "Soldi" },
- { emoji: "🔮", label: "Sfera" },
- { emoji: "🏴☠️", label: "Pirata" },
+ { emoji: "💰", label: "Money" },
+ { emoji: "🔮", label: "Orb" },
+ { emoji: "🏴☠️", label: "Pirate" },
];
function PrefixDialog({
@@ -167,7 +167,7 @@ function PrefixDialog({
- {editPrefix ? "Modifica Prefisso" : "Aggiungi Prefisso"}
+ {editPrefix ? "Edit Prefix" : "Add Prefix"}
Username
setForm({ ...form, username: e.target.value })}
/>
@@ -221,10 +221,10 @@ function PrefixDialog({
)}
-
Testo
+
Text
setForm({ ...form, text: e.target.value })}
/>
@@ -235,7 +235,7 @@ function PrefixDialog({
- Colore (HEX o multi: #FF0000,#00FF00)
+ Color (HEX or multi: #FF0000,#00FF00)
-
Icona
+
Icon
- Effetto
+ Effect
setForm({ ...form, active: e.target.checked })}
/>
- Attivo
+ Active
- Annulla
+ Cancel
- {saving ? "Salvando..." : "Salva"}
+ {saving ? "Saving..." : "Save"}
@@ -338,6 +338,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
const params = new URLSearchParams({ page: String(page) });
if (searchQuery) params.set("q", searchQuery);
const res = await fetch(`/api/admin/prefixes?${params}`);
+ if (!res.ok) return;
const data = await res.json();
if (data.ok) {
setPrefixes(data.prefixes);
@@ -347,12 +348,14 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
const fetchBlacklist = useCallback(async () => {
const res = await fetch("/api/admin/prefixes/blacklist");
+ if (!res.ok) return;
const data = await res.json();
if (data.ok) setBlacklist(data.words);
}, []);
const fetchSettings = useCallback(async () => {
const res = await fetch("/api/admin/prefixes/settings");
+ if (!res.ok) return;
const data = await res.json();
if (data.ok) setSettings(data.settings);
}, []);
@@ -367,9 +370,9 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
}, [fetchBlacklist, fetchSettings]);
const handleDeletePrefix = async (id: number) => {
- if (!confirm("Sei sicuro di voler eliminare questo prefisso?")) return;
+ if (!confirm("Are you sure you want to delete this prefix?")) return;
run(() => deletePrefixAction({ id }), {
- successMessage: "Prefisso eliminato!",
+ successMessage: "Prefix deleted!",
onSuccess: () => {
fetchPrefixes();
},
@@ -389,7 +392,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
active: data.active ? 1 : 0,
}),
{
- successMessage: "Prefisso aggiornato!",
+ successMessage: "Prefix updated!",
onSuccess: () => {
fetchPrefixes();
},
@@ -407,7 +410,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
active: data.active ? 1 : 0,
}),
{
- successMessage: "Prefisso creato!",
+ successMessage: "Prefix created!",
onSuccess: () => {
fetchPrefixes();
},
@@ -429,7 +432,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
const handleAddBlacklistWord = async () => {
if (!newWord.trim()) return;
run(() => addBlacklistWordAction({ word: newWord.trim() }), {
- successMessage: "Parola aggiunta alla blacklist!",
+ successMessage: "Word added to blacklist!",
onSuccess: () => {
setNewWord("");
fetchBlacklist();
@@ -439,7 +442,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
const handleRemoveBlacklistWord = async (id: number) => {
run(() => removeBlacklistWordAction({ id }), {
- successMessage: "Parola rimossa dalla blacklist!",
+ successMessage: "Word removed from blacklist!",
onSuccess: () => {
fetchBlacklist();
},
@@ -453,7 +456,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
settings: settings as unknown as Record
,
}),
{
- successMessage: "Impostazioni salvate!",
+ successMessage: "Settings saved!",
},
);
};
@@ -469,9 +472,9 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
- Prefissi Utenti
+ User Prefixes
Blacklist
- Impostazioni
+ Settings
@@ -480,7 +483,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
{
setSearchQuery(e.target.value);
@@ -489,12 +492,12 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
/>
- Cerca
+ Search
{canEdit && (
- Aggiungi
+ Add
)}
@@ -503,14 +506,14 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
ID
- Utente
+ User
Prefix
- Colore
- Icona
- Effetto
- Attivo
- Data
- {canEdit && Azioni }
+ Color
+ Icon
+ Effect
+ Active
+ Date
+ {canEdit && Actions }
@@ -580,9 +583,11 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
?.label || prefix.effect
: "-"}
- {prefix.active ? "Si" : "No"}
+ {prefix.active ? "Yes" : "No"}
- {new Date(prefix.created_at).toLocaleDateString()}
+ {prefix.created_at
+ ? new Date(prefix.created_at).toLocaleDateString()
+ : "—"}
{canEdit && (
@@ -614,7 +619,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
className="text-center text-muted-foreground"
colSpan={9}
>
- Nessun prefisso trovato
+ No prefixes found
)}
@@ -629,10 +634,10 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
variant="outline"
onClick={() => setPage(page - 1)}
>
- Precedente
+ Previous
- Pagina {page} di {totalPages}
+ Page {page} of {totalPages}
= totalPages}
@@ -640,7 +645,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
variant="outline"
onClick={() => setPage(page + 1)}
>
- Successiva
+ Next
)}
@@ -649,13 +654,13 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
- Parole Vietate
+ Forbidden Words
{canEdit && (
)}
@@ -689,7 +694,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
))}
{blacklist.length === 0 && (
- Nessuna parola in blacklist
+ No blacklist words
)}
@@ -700,12 +705,12 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
- Impostazioni Prefissi
+ Prefix Settings
{canEdit && (
- Salva Impostazioni
+ Save Settings
)}
diff --git a/src/app/admin/sounds/page.tsx b/src/app/admin/sounds/page.tsx
index 6fb14ff2..643853fe 100644
--- a/src/app/admin/sounds/page.tsx
+++ b/src/app/admin/sounds/page.tsx
@@ -70,11 +70,11 @@ export default async function SoundsPage() {
Song Disks
- Carica e gestisci i file MP3 dei song disk riprodotti dai jukebox
- in-game. I file sono serviti da {baseUrl} (configurabile
- in{" "}
-
- Impostazioni > CMS
+ Upload and manage MP3 files for song disks played by in-game
+ jukeboxes. Files are served from {baseUrl}{" "}
+ (configurable in{" "}
+
+ Settings
).
diff --git a/src/app/admin/sounds/sounds-client.tsx b/src/app/admin/sounds/sounds-client.tsx
index c1ff421b..c259d408 100644
--- a/src/app/admin/sounds/sounds-client.tsx
+++ b/src/app/admin/sounds/sounds-client.tsx
@@ -121,8 +121,8 @@ export function SoundsClient({
if (!deleting) return;
const id = deleting.id;
run(() => deleteSoundtrack({ id }), {
- successMessage: `Song disk #${id} eliminato.`,
- errorMessage: `Impossibile eliminare il song disk #${id}.`,
+ successMessage: `Song disk #${id} deleted.`,
+ errorMessage: `Unable to delete song disk #${id}.`,
onSuccess: () => {
invalidateSongPickerCache();
setDeleting(null);
@@ -139,7 +139,7 @@ export function SoundsClient({
setQuery(e.target.value)}
- placeholder="Cerca per nome, autore, codice o id…"
+ placeholder="Search by name, author, code or id…"
className="pl-8"
/>
@@ -151,9 +151,9 @@ export function SoundsClient({
- Tutti ({soundtracks.length})
+ All ({soundtracks.length})
- Singoli MP3 ({counts.single})
+ Single MP3s ({counts.single})
Composition ({counts.composition})
@@ -199,15 +199,15 @@ export function SoundsClient({
{soundtracks.length === 0 ? (
-
Nessun song disk caricato.
+
No song disks uploaded yet.
{canEdit && (
- Clicca “Upload MP3” per iniziare.
+ Click “Upload MP3” to get started.
)}
) : (
- <>Nessun risultato per i filtri attivi.>
+ <>No results for the active filters.>
)}
@@ -293,7 +293,7 @@ export function SoundsClient({
className="h-8 w-8"
onClick={() => setEditing(s)}
disabled={isPending}
- title="Modifica"
+ title="Edit"
>
@@ -303,7 +303,7 @@ export function SoundsClient({
className="h-8 w-8 text-destructive hover:text-destructive"
onClick={() => setDeleting(s)}
disabled={isPending}
- title="Elimina"
+ title="Delete"
>
@@ -345,27 +345,26 @@ export function SoundsClient({
- Eliminare “{deleting.name}”?
+ Delete “{deleting.name}”?
{deleting.usedBy > 0 ? (
<>
- Questo song disk è attualmente usato da{" "}
+ This song disk is currently used by{" "}
{deleting.usedBy} catalog item
- {deleting.usedBy === 1 ? "" : "s"}. Eliminandolo, quei
- catalog item verranno riportati a “nessun song
- disk”.
+ {deleting.usedBy === 1 ? "" : "s"}. Deleting it will reset
+ those catalog items to “no song disk”.
{deleting.kind === "single" && (
- <> Il file MP3 sarà rimosso dal disco.>
+ <> The MP3 file will be removed from disk.>
)}{" "}
- L'azione è irreversibile.
+ This action cannot be undone.
>
) : (
<>
{deleting.kind === "single"
- ? "Il file MP3 sarà rimosso dal disco e la riga dal database."
- : "La riga sarà rimossa dal database. Nessun file viene eliminato (una composition non ha MP3 dedicato)."}{" "}
- L'azione è irreversibile.
+ ? "The MP3 file will be removed from disk and the row from the database."
+ : "The row will be removed from the database. No file is deleted (a composition has no dedicated MP3)."}{" "}
+ This action cannot be undone.
>
)}
@@ -376,7 +375,7 @@ export function SoundsClient({
onClick={() => setDeleting(null)}
disabled={isPending}
>
- Annulla
+ Cancel
{isPending && }
- Elimina
+ Delete
@@ -420,11 +419,11 @@ function UploadDialog({
e.preventDefault();
const file = fileRef.current?.files?.[0];
if (!file) {
- toast.error("Seleziona un file MP3.");
+ toast.error("Select an MP3 file.");
return;
}
if (!name.trim()) {
- toast.error("Il nome è obbligatorio.");
+ toast.error("Name is required.");
return;
}
@@ -445,16 +444,16 @@ function UploadDialog({
id?: number;
};
if (!res.ok || !json.ok) {
- toast.error(json.error || "Upload fallito.");
+ toast.error(json.error || "Upload failed.");
return;
}
- toast.success(`Song disk #${json.id} caricato.`);
+ toast.success(`Song disk #${json.id} uploaded.`);
reset();
onOpenChange(false);
onUploaded();
} catch (err) {
console.error(err);
- toast.error("Errore di rete durante l'upload.");
+ toast.error("Network error during upload.");
} finally {
setUploading(false);
}
@@ -471,14 +470,14 @@ function UploadDialog({
Upload Song Disk
- Carica un file MP3. Dimensione massima 10 MB. Il codice sarà
- generato automaticamente come song_{id}.
+ Upload an MP3 file. Max size 10 MB. The code will be generated
+ automatically as song_{id}.
@@ -555,12 +554,12 @@ function EditDialog({
function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!name.trim()) {
- toast.error("Il nome è obbligatorio.");
+ toast.error("Name is required.");
return;
}
const lenNum = Number.parseInt(length, 10);
if (!Number.isFinite(lenNum) || lenNum < 0) {
- toast.error("La durata deve essere un numero ≥ 0.");
+ toast.error("Length must be a number ≥ 0.");
return;
}
run(
@@ -573,8 +572,8 @@ function EditDialog({
length: lenNum,
}),
{
- successMessage: "Song disk aggiornato.",
- errorMessage: "Impossibile aggiornare il song disk.",
+ successMessage: "Song disk updated.",
+ errorMessage: "Unable to update the song disk.",
onSuccess: onSaved,
},
);
@@ -585,21 +584,21 @@ function EditDialog({
- Modifica {isComposition ? "Trax Composition" : "Song Disk"}
+ Edit {isComposition ? "Trax Composition" : "Song Disk"}
{soundtrack.code}
{isComposition ? (
<>
{" "}
- — composition di {soundtrack.samples.length} sample. Modifica il
- campo track solo se sai cosa stai facendo.
+ — composition of {soundtrack.samples.length} samples. Edit the{" "}
+ track field only if you know what you are doing.
>
) : (
<>
{" "}
- — il file MP3 non può essere sostituito: elimina e ricarica se
- devi cambiare audio.
+ — the MP3 file cannot be replaced: delete and re-upload if you
+ need to change the audio.
>
)}
@@ -607,7 +606,7 @@ function EditDialog({
diff --git a/src/app/admin/translations/client/client-translations.tsx b/src/app/admin/translations/client/client-translations.tsx
index ac30bd62..9c1fe597 100644
--- a/src/app/admin/translations/client/client-translations.tsx
+++ b/src/app/admin/translations/client/client-translations.tsx
@@ -74,11 +74,11 @@ export function ClientTranslations({
function handleSave() {
if (activeFile.readOnly) return;
run(() => saveClientTranslations({ fileId: activeFile.id, data }), {
- successMessage: "File salvato.",
+ successMessage: "File saved.",
onSuccess: (result) => {
if (result?.commentsLost) {
toast.warning(
- "Alcune chiavi non corrispondevano al formato standard: file riserializzato e commenti rimossi.",
+ "Some keys did not match the standard format: file was reserialized and comments were removed.",
);
}
router.refresh();
@@ -115,7 +115,7 @@ export function ClientTranslations({
disabled={isPending || activeFile.readOnly || !!parseError}
>
{" "}
- {isPending ? "Salvataggio..." : "Salva"}
+ {isPending ? "Saving..." : "Save"}
@@ -144,7 +144,7 @@ export function ClientTranslations({
{
setSearch(e.target.value);
@@ -154,7 +154,7 @@ export function ClientTranslations({
/>
- {filteredKeys.length} chiavi
+ {filteredKeys.length} keys
@@ -174,7 +174,7 @@ export function ClientTranslations({
)}
{activeFile.hasComments && !activeFile.readOnly && (
- commenti preservati
+ comments preserved
)}
@@ -189,9 +189,9 @@ export function ClientTranslations({
- Questo file è di sola lettura.{" "}
+ This file is read-only.{" "}
{activeFile.note ??
- "Modificarlo manualmente non è supportato dal CMS."}
+ "Manual editing is not supported by the CMS."}
)}
@@ -200,24 +200,23 @@ export function ClientTranslations({
- I commenti vengono preservati: il salvataggio modifica solo le righe
- dei valori cambiati. Se una chiave usa un formato non standard ed è
- impossibile patchare in-place, il file viene riserializzato
- (commenti persi) e ti avviseremo.
+ Comments are preserved: saving only patches changed value lines. If
+ a key uses a non-standard format and cannot be patched in-place, the
+ file is reserialized (comments lost) and you will be warned.
)}
{parseError && (
- Errore di parsing: {parseError}
+ Parse error: {parseError}
)}
{pageKeys.length === 0 && !parseError && (
- Nessuna voce.
+ No entries.
)}
{pageKeys.map((key) => (
@@ -241,7 +240,7 @@ export function ClientTranslations({
{totalPages > 1 && (
- Pagina {page} di {totalPages}
+ Page {page} of {totalPages}
{t("subtitle")}
-
+
{t("fullSettings")}
diff --git a/src/app/api/admin/prefixes/blacklist/route.ts b/src/app/api/admin/prefixes/blacklist/route.ts
new file mode 100644
index 00000000..c755d212
--- /dev/null
+++ b/src/app/api/admin/prefixes/blacklist/route.ts
@@ -0,0 +1,48 @@
+import { withAdmin } from "@/lib/api-handler";
+import { apiError, apiOk } from "@/lib/api-response";
+import { PERMS } from "@/lib/permissions";
+import { prisma } from "@/lib/prisma";
+
+interface BlacklistWord {
+ id: number;
+ word: string;
+}
+
+export const GET = withAdmin({ permission: PERMS.PREFIXES_VIEW }, async () => {
+ const words = await prisma.$queryRaw`
+ SELECT id, word FROM custom_prefix_blacklist ORDER BY word ASC
+ `;
+
+ return apiOk({
+ words: words.map((w) => ({ ...w, created_at: "" })),
+ });
+});
+
+export const POST = withAdmin(
+ { permission: PERMS.PREFIXES_EDIT },
+ async (request) => {
+ const body = await request.json();
+ const word = typeof body.word === "string" ? body.word.trim() : "";
+
+ if (!word) return apiError("Word is required");
+
+ await prisma.$executeRaw`INSERT INTO custom_prefix_blacklist (word) VALUES (${word})`;
+
+ return apiOk();
+ },
+);
+
+export const DELETE = withAdmin(
+ { permission: PERMS.PREFIXES_EDIT },
+ async (request) => {
+ const body = await request.json();
+ const id = Number(body.id);
+
+ if (!Number.isInteger(id) || id <= 0)
+ return apiError("Missing or invalid word id");
+
+ await prisma.$executeRaw`DELETE FROM custom_prefix_blacklist WHERE id = ${id}`;
+
+ return apiOk({ deleted: id });
+ },
+);
diff --git a/src/app/api/admin/prefixes/route.ts b/src/app/api/admin/prefixes/route.ts
new file mode 100644
index 00000000..346c1127
--- /dev/null
+++ b/src/app/api/admin/prefixes/route.ts
@@ -0,0 +1,124 @@
+import { Prisma } from "@/generated/prisma/client";
+import { withAdmin } from "@/lib/api-handler";
+import { apiError, apiOk } from "@/lib/api-response";
+import { PERMS } from "@/lib/permissions";
+import { prisma } from "@/lib/prisma";
+
+interface UserPrefix {
+ id: number;
+ user_id: number;
+ text: string;
+ color: string;
+ icon: string;
+ effect: string;
+ active: number;
+ username?: string | null;
+}
+
+export const GET = withAdmin(
+ { permission: PERMS.PREFIXES_VIEW },
+ async (request) => {
+ const url = request.nextUrl;
+ const q = url.searchParams.get("q") || "";
+ const page = Math.max(1, Number(url.searchParams.get("page") || 1));
+ const limit = 50;
+ const offset = (page - 1) * limit;
+
+ const whereFragment = q
+ ? Prisma.sql`WHERE up.text LIKE ${`%${q}%`} OR u.username LIKE ${`%${q}%`}`
+ : Prisma.empty;
+
+ const prefixes = await prisma.$queryRaw(
+ Prisma.sql`SELECT up.id, up.user_id, up.text, up.color, up.icon, up.effect, up.active, u.username
+ FROM custom_prefixes up
+ LEFT JOIN users u ON u.id = up.user_id
+ ${whereFragment}
+ ORDER BY up.id DESC
+ LIMIT ${limit} OFFSET ${offset}`,
+ );
+
+ const countResult = await prisma.$queryRaw<[{ total: bigint }]>(
+ Prisma.sql`SELECT COUNT(*) as total FROM custom_prefixes up
+ LEFT JOIN users u ON u.id = up.user_id
+ ${whereFragment}`,
+ );
+
+ const total = Number(countResult[0]?.total || 0);
+
+ return apiOk({
+ prefixes: prefixes.map((p) => ({
+ ...p,
+ icon: p.icon || "",
+ effect: p.effect || "",
+ active: Boolean(p.active),
+ created_at: "",
+ })),
+ total,
+ page,
+ pages: Math.max(1, Math.ceil(total / limit)),
+ });
+ },
+);
+
+export const POST = withAdmin(
+ { permission: PERMS.PREFIXES_EDIT },
+ async (request) => {
+ const body = await request.json();
+ const { username, text, color, icon, effect, active } = body;
+
+ if (!username || !text || !color) {
+ return apiError("Missing required fields: username, text, color");
+ }
+
+ const users = await prisma.$queryRaw<{ id: number }[]>(
+ Prisma.sql`SELECT id FROM users WHERE username = ${username} LIMIT 1`,
+ );
+
+ if (!users || users.length === 0) {
+ return apiError("User not found");
+ }
+
+ const userId = users[0].id;
+
+ await prisma.$executeRaw(
+ Prisma.sql`INSERT INTO custom_prefixes (user_id, text, color, icon, effect, active)
+ VALUES (${userId}, ${text}, ${color}, ${icon || ""}, ${effect || ""}, ${active ? 1 : 0})`,
+ );
+
+ return apiOk({ created: true });
+ },
+);
+
+export const PUT = withAdmin(
+ { permission: PERMS.PREFIXES_EDIT },
+ async (request) => {
+ const body = await request.json();
+ const { id, text, color, icon, effect, active } = body;
+
+ if (!id) return apiError("Missing prefix id");
+
+ await prisma.$executeRaw(
+ Prisma.sql`UPDATE custom_prefixes
+ SET text = ${text}, color = ${color}, icon = ${icon || ""}, effect = ${effect || ""}, active = ${active ? 1 : 0}
+ WHERE id = ${id}`,
+ );
+
+ return apiOk({ updated: id });
+ },
+);
+
+export const DELETE = withAdmin(
+ { permission: PERMS.PREFIXES_EDIT },
+ async (request) => {
+ const body = await request.json();
+ const id = body.id;
+
+ if (!id) return apiError("Missing prefix id");
+
+ await prisma.$executeRaw(
+ Prisma.sql`DELETE FROM custom_prefixes WHERE id = ${id}`,
+ );
+
+ return apiOk({ deleted: id });
+ },
+);
diff --git a/src/app/api/admin/prefixes/settings/route.ts b/src/app/api/admin/prefixes/settings/route.ts
new file mode 100644
index 00000000..101f37ba
--- /dev/null
+++ b/src/app/api/admin/prefixes/settings/route.ts
@@ -0,0 +1,70 @@
+import { withAdmin } from "@/lib/api-handler";
+import { apiError, apiOk } from "@/lib/api-response";
+import { PERMS } from "@/lib/permissions";
+import { prisma } from "@/lib/prisma";
+
+interface PrefixSetting {
+ key: string;
+ value: string;
+}
+
+const VALID_KEYS = new Set([
+ "enabled",
+ "max_length",
+ "min_rank",
+ "min_rank_to_buy",
+ "allow_colors",
+ "allow_bold",
+ "allow_italic",
+ "default_color",
+ "price_credits",
+ "price_points",
+ "points_type",
+]);
+
+const DEFAULT_SETTINGS: Record = {
+ max_length: "15",
+ min_rank_to_buy: "1",
+ price_credits: "5",
+ price_points: "0",
+ points_type: "0",
+};
+
+export const GET = withAdmin({ permission: PERMS.PREFIXES_VIEW }, async () => {
+ const settings = await prisma.$queryRaw`
+ SELECT \`key\`, \`value\` FROM custom_prefix_settings
+ `;
+
+ const result: Record = { ...DEFAULT_SETTINGS };
+ for (const s of settings) {
+ result[s.key] = s.value;
+ }
+
+ return apiOk({ settings: result });
+});
+
+export const PUT = withAdmin(
+ { permission: PERMS.PREFIXES_EDIT },
+ async (request) => {
+ const body = await request.json();
+ const settings = body.settings;
+
+ if (!settings || typeof settings !== "object") {
+ return apiError("Settings object is required");
+ }
+
+ for (const [key, value] of Object.entries(settings)) {
+ if (!VALID_KEYS.has(key)) {
+ return apiError(`Invalid setting key: ${key}`);
+ }
+ const strValue = String(value);
+ await prisma.$executeRaw`
+ INSERT INTO custom_prefix_settings (\`key\`, \`value\`)
+ VALUES (${key}, ${strValue})
+ ON DUPLICATE KEY UPDATE \`value\` = ${strValue}
+ `;
+ }
+
+ return apiOk();
+ },
+);
diff --git a/src/lib/permissions.ts b/src/lib/permissions.ts
index b69891e4..d224241a 100644
--- a/src/lib/permissions.ts
+++ b/src/lib/permissions.ts
@@ -104,10 +104,20 @@ const getCurrentAuthorizationState = cache(async (userId: number) =>
resolveAuthorizationState(userId, {
user: prisma.user,
highestRank: async () => {
+ // Prefer the highest rank actually held by a user. Unused high IDs in
+ // permission_ranks (common on Habbo DBs) would otherwise lock the real
+ // owner out of super-admin / permissions management.
const rows = await prisma.$queryRaw<
{ highest_rank: number | bigint | null }[]
>`
- SELECT MAX(id) AS highest_rank FROM permission_ranks
+ SELECT COALESCE(
+ (
+ SELECT MAX(u.\`rank\`)
+ FROM users u
+ INNER JOIN permission_ranks pr ON pr.id = u.\`rank\`
+ ),
+ (SELECT MAX(id) FROM permission_ranks)
+ ) AS highest_rank
`;
return rows[0]?.highest_rank == null
? null
diff --git a/src/messages/bg.json b/src/messages/bg.json
index 70462489..a7640112 100644
--- a/src/messages/bg.json
+++ b/src/messages/bg.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Кутии за писане",
"photos": "Снимки",
"tickets": "Билети",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Потребители и достъп",
"users": "Потребители",
"bans": "забрани",
@@ -706,6 +710,8 @@
"applications": "Приложения",
"teams": "Екипи",
"permissions": "Разрешения",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "икономичност",
"catalog": "Каталог",
"shop": "Магазин",
@@ -732,6 +738,9 @@
"emulator": "Емулатор",
"alerts": "Сигнали",
"logs": "трупи",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Филтър за думи",
"maintenance": "Поддръжка",
"rooms": "Стаи",
diff --git a/src/messages/cs.json b/src/messages/cs.json
index bafab552..adba05ab 100644
--- a/src/messages/cs.json
+++ b/src/messages/cs.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Popsatelné krabice",
"photos": "Fotografie",
"tickets": "Vstupenky",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Uživatelé a přístup",
"users": "Uživatelé",
"bans": "Zákazy",
@@ -706,6 +710,8 @@
"applications": "Aplikace",
"teams": "Týmy",
"permissions": "Oprávnění",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Ekonomika",
"catalog": "Katalog",
"shop": "Obchod",
@@ -732,6 +738,9 @@
"emulator": "Emulátor",
"alerts": "Upozornění",
"logs": "Protokoly",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Filtr slov",
"maintenance": "Údržba",
"rooms": "Pokoje",
diff --git a/src/messages/da.json b/src/messages/da.json
index 42d1ab09..a12c74b0 100644
--- a/src/messages/da.json
+++ b/src/messages/da.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Skrivbare kasser",
"photos": "Fotos",
"tickets": "Billetter",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Brugere og adgang",
"users": "Brugere",
"bans": "Forbud",
@@ -706,6 +710,8 @@
"applications": "Ansøgninger",
"teams": "Hold",
"permissions": "Tilladelser",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Økonomi",
"catalog": "Katalog",
"shop": "Butik",
@@ -732,6 +738,9 @@
"emulator": "Emulator",
"alerts": "Advarsler",
"logs": "Logs",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Ord filter",
"maintenance": "Vedligeholdelse",
"rooms": "Værelser",
diff --git a/src/messages/de.json b/src/messages/de.json
index 7e8a66c6..524a96bd 100644
--- a/src/messages/de.json
+++ b/src/messages/de.json
@@ -662,6 +662,10 @@
"writeableBoxes": "Editierbare Boxen",
"photos": "Fotos",
"tickets": "Tickets",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Benutzer & Zugang",
"users": "Benutzer",
"bans": "Banns",
@@ -670,6 +674,8 @@
"applications": "Bewerbungen",
"teams": "Teams",
"permissions": "Berechtigungen",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Wirtschaft",
"catalog": "Katalog",
"shop": "Shop",
@@ -696,21 +702,24 @@
"emulator": "Emulator",
"alerts": "Alarme",
"logs": "Logs",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Wortfilter",
"maintenance": "Wartung",
- "housekeeping": "Hausverwaltung",
"rooms": "Räume",
"emailTemplates": "E-Mail-Vorlagen",
"theme": "Design",
"media": "Medien",
- "settings": "Einstellungen",
- "backToSite": "Zurück zur Website",
- "hotelManagementPanel": "Hotel-Verwaltungsbereich",
- "rankLabel": "Rang {rank}",
"favicon": "Favicon",
"sounds": "Sounds",
"translations": "Übersetzungen",
- "import": "Importieren"
+ "import": "Importieren",
+ "settings": "Einstellungen",
+ "backToSite": "Zurück zur Website",
+ "housekeeping": "Hausverwaltung",
+ "hotelManagementPanel": "Hotel-Verwaltungsbereich",
+ "rankLabel": "Rang {rank}"
},
"dashboard": {
"title": "Dashboard",
diff --git a/src/messages/el.json b/src/messages/el.json
index 6ce3d331..5217cc6d 100644
--- a/src/messages/el.json
+++ b/src/messages/el.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Εγγράψιμα κουτιά",
"photos": "Φωτογραφίες",
"tickets": "Εισιτήρια",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Χρήστες και πρόσβαση",
"users": "Χρήστες",
"bans": "Απαγορεύσεις",
@@ -706,6 +710,8 @@
"applications": "Εφαρμογές",
"teams": "Ομάδες",
"permissions": "Άδειες",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Οικονομία",
"catalog": "Κατάλογος",
"shop": "Κατάστημα",
@@ -732,6 +738,9 @@
"emulator": "Εξομοιωτής",
"alerts": "Ειδοποιήσεις",
"logs": "κούτσουρα",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Φίλτρο λέξεων",
"maintenance": "Συντήρηση",
"rooms": "Δωμάτια",
diff --git a/src/messages/en.json b/src/messages/en.json
index 2e75c6d4..2c847e17 100644
--- a/src/messages/en.json
+++ b/src/messages/en.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Writeable boxes",
"photos": "Photos",
"tickets": "Tickets",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Users & access",
"users": "Users",
"bans": "Bans",
@@ -706,6 +710,8 @@
"applications": "Applications",
"teams": "Teams",
"permissions": "Permissions",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Economy",
"catalog": "Catalog",
"shop": "Shop",
@@ -732,6 +738,9 @@
"emulator": "Emulator",
"alerts": "Alerts",
"logs": "Logs",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Word filter",
"maintenance": "Maintenance",
"rooms": "Rooms",
diff --git a/src/messages/es.json b/src/messages/es.json
index 4c3575a8..8d8e287c 100644
--- a/src/messages/es.json
+++ b/src/messages/es.json
@@ -662,6 +662,10 @@
"writeableBoxes": "Cajas editables",
"photos": "Fotos",
"tickets": "Tickets",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Usuarios y acceso",
"users": "Usuarios",
"bans": "Sanciones",
@@ -670,6 +674,8 @@
"applications": "Solicitudes",
"teams": "Equipos",
"permissions": "Permisos",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Economía",
"catalog": "Catálogo",
"shop": "Tienda",
@@ -696,21 +702,24 @@
"emulator": "Emulador",
"alerts": "Alertas",
"logs": "Registros",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Filtro de palabras",
"maintenance": "Mantenimiento",
- "housekeeping": "Housekeeping",
"rooms": "Salas",
"emailTemplates": "Plantillas de correo",
"theme": "Tema",
"media": "Multimedia",
- "settings": "Ajustes",
- "backToSite": "Volver al sitio",
- "hotelManagementPanel": "Panel de gestión del hotel",
- "rankLabel": "Rango {rank}",
"favicon": "Favicon",
"sounds": "Sonidos",
"translations": "Traducciones",
- "import": "Importar"
+ "import": "Importar",
+ "settings": "Ajustes",
+ "backToSite": "Volver al sitio",
+ "housekeeping": "Housekeeping",
+ "hotelManagementPanel": "Panel de gestión del hotel",
+ "rankLabel": "Rango {rank}"
},
"dashboard": {
"title": "Panel",
diff --git a/src/messages/fr.json b/src/messages/fr.json
index 36220f1c..aeaf4c66 100644
--- a/src/messages/fr.json
+++ b/src/messages/fr.json
@@ -662,6 +662,10 @@
"writeableBoxes": "Boîtes modifiables",
"photos": "Photos",
"tickets": "Tickets",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Utilisateurs & accès",
"users": "Utilisateurs",
"bans": "Bannissements",
@@ -670,6 +674,8 @@
"applications": "Candidatures",
"teams": "Équipes",
"permissions": "Permissions",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Économie",
"catalog": "Catalogue",
"shop": "Boutique",
@@ -696,21 +702,24 @@
"emulator": "Émulateur",
"alerts": "Alertes",
"logs": "Journaux",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Filtre de mots",
"maintenance": "Maintenance",
- "housekeeping": "Gouvernance",
"rooms": "Salons",
"emailTemplates": "Modèles d'e-mail",
"theme": "Thème",
"media": "Médias",
- "settings": "Paramètres",
- "backToSite": "Retour au site",
- "hotelManagementPanel": "Panneau de gestion de l'hôtel",
- "rankLabel": "Rang {rank}",
"favicon": "Favicon",
"sounds": "Sons",
"translations": "Traductions",
- "import": "Importer"
+ "import": "Importer",
+ "settings": "Paramètres",
+ "backToSite": "Retour au site",
+ "housekeeping": "Gouvernance",
+ "hotelManagementPanel": "Panneau de gestion de l'hôtel",
+ "rankLabel": "Rang {rank}"
},
"dashboard": {
"title": "Tableau de bord",
diff --git a/src/messages/hr.json b/src/messages/hr.json
index 70d9af13..76e929c9 100644
--- a/src/messages/hr.json
+++ b/src/messages/hr.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Kutije za pisanje",
"photos": "Fotografije",
"tickets": "Ulaznice",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Korisnici i pristup",
"users": "Korisnici",
"bans": "Zabrane",
@@ -706,6 +710,8 @@
"applications": "Prijave",
"teams": "Timovi",
"permissions": "Dozvole",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Gospodarstvo",
"catalog": "Katalog",
"shop": "Trgovina",
@@ -732,6 +738,9 @@
"emulator": "Emulator",
"alerts": "upozorenja",
"logs": "Dnevnici",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Filter riječi",
"maintenance": "Održavanje",
"rooms": "Sobe",
diff --git a/src/messages/hu.json b/src/messages/hu.json
index 16b1212a..66e03219 100644
--- a/src/messages/hu.json
+++ b/src/messages/hu.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Írható dobozok",
"photos": "Fényképek",
"tickets": "Jegyek",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Felhasználók és hozzáférés",
"users": "Felhasználók",
"bans": "Tiltások",
@@ -706,6 +710,8 @@
"applications": "Alkalmazások",
"teams": "Csapatok",
"permissions": "Engedélyek",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Gazdaság",
"catalog": "Katalógus",
"shop": "Bolt",
@@ -732,6 +738,9 @@
"emulator": "Emulátor",
"alerts": "Figyelmeztetések",
"logs": "Naplók",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Szószűrő",
"maintenance": "Karbantartás",
"rooms": "Szobák",
diff --git a/src/messages/it.json b/src/messages/it.json
index 8fcc91e5..f84008b0 100644
--- a/src/messages/it.json
+++ b/src/messages/it.json
@@ -662,6 +662,10 @@
"writeableBoxes": "Box modificabili",
"photos": "Foto",
"tickets": "Ticket",
+ "events": "Eventi",
+ "polls": "Sondaggi",
+ "banners": "Banner",
+ "prefixes": "Prefissi",
"usersAndAccess": "Utenti e accesso",
"users": "Utenti",
"bans": "Ban",
@@ -670,6 +674,8 @@
"applications": "Candidature",
"teams": "Team",
"permissions": "Permessi",
+ "onlineUsers": "Utenti online",
+ "moderation": "Moderazione",
"economy": "Economia",
"catalog": "Catalogo",
"shop": "Negozio",
@@ -696,6 +702,9 @@
"emulator": "Emulatore",
"alerts": "Avvisi",
"logs": "Registri",
+ "auditLog": "Log di audit",
+ "analytics": "Analitiche",
+ "devops": "DevOps",
"wordFilter": "Filtro parole",
"maintenance": "Manutenzione",
"housekeeping": "Housekeeping",
diff --git a/src/messages/nl.json b/src/messages/nl.json
index fb0f8a5e..400717cd 100644
--- a/src/messages/nl.json
+++ b/src/messages/nl.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Beschrijfbare boxes",
"photos": "Foto's",
"tickets": "Tickets",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Gebruikers & toegang",
"users": "Gebruikers",
"bans": "Bans",
@@ -706,6 +710,8 @@
"applications": "Sollicitaties",
"teams": "Teams",
"permissions": "Permissies",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Economie",
"catalog": "Catalogus",
"shop": "Winkel",
@@ -732,9 +738,11 @@
"emulator": "Emulator",
"alerts": "Meldingen",
"logs": "Logboeken",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Woordfilter",
"maintenance": "Onderhoud",
- "housekeeping": "Housekeeping",
"rooms": "Kamers",
"emailTemplates": "E-mailsjablonen",
"theme": "Thema",
@@ -745,6 +753,7 @@
"import": "Importeren",
"settings": "Instellingen",
"backToSite": "Terug naar site",
+ "housekeeping": "Housekeeping",
"hotelManagementPanel": "Hotelbeheerpaneel",
"rankLabel": "Rang {rank}"
},
@@ -1504,7 +1513,7 @@
},
"embedPage": {
"title": "Radio Embed",
- "subtitle": "Copy-ready embed code for the live stream. Read-only \u2014 nothing here writes to the database. Configure the stream URL in Radio Settings (radio_stream_url).",
+ "subtitle": "Copy-ready embed code for the live stream. Read-only — nothing here writes to the database. Configure the stream URL in Radio Settings (radio_stream_url).",
"streamUrl": "Stream URL",
"configured": "Configured",
"missing": "Missing",
@@ -1521,7 +1530,7 @@
"audioSnippet": "Audio player snippet",
"audioSnippetDesc": "Drop-in HTML5 audio player pointed straight at the stream. Works anywhere plain HTML is allowed.",
"iframeSnippet": "Iframe wrapper snippet",
- "iframeSnippetDesc": "Embeds the site\u2019s own radio player page in an iframe \u2014 keeps your styling and now-playing info. Adjust width/height to taste.",
+ "iframeSnippetDesc": "Embeds the site’s own radio player page in an iframe — keeps your styling and now-playing info. Adjust width/height to taste.",
"audioUnsupported": "Your browser does not support the audio element."
},
"banners": {
@@ -1531,7 +1540,7 @@
"addBanner": "Banner toevoegen",
"createBanner": "Create banner",
"deleteBanner": "Banner verwijderen",
- "noBanners": "No banners yet \u2014 add one above.",
+ "noBanners": "No banners yet — add one above.",
"existing": "Existing banners ({count})",
"save": "Save",
"delete": "Delete",
@@ -1562,7 +1571,7 @@
"description": "Description",
"createRank": "Create rank",
"existing": "Existing ranks ({count})",
- "noRanks": "No ranks yet \u2014 add one above.",
+ "noRanks": "No ranks yet — add one above.",
"activeLabel": "active",
"inactiveLabel": "inactive",
"save": "Save",
@@ -1589,13 +1598,13 @@
},
"apiKeysForm": {
"title": "API-sleutels",
- "subtitle": "API keys for external radio integrations (radio_api_keys). Keys are generated server-side and shown masked \u2014 copy the full value from the database when first issued.",
+ "subtitle": "API keys for external radio integrations (radio_api_keys). Keys are generated server-side and shown masked — copy the full value from the database when first issued.",
"dbError": "Could not load API keys (database unavailable).",
"totalKeys": "Total keys",
"active": "Active",
"inactive": "Inactive",
"generateKey": "Nieuwe sleutel genereren",
- "generateHint": "The secret key is generated automatically with cryptographic randomness \u2014 you do not enter it.",
+ "generateHint": "The secret key is generated automatically with cryptographic randomness — you do not enter it.",
"name": "Name",
"namePlaceholder": "AzureCast bridge",
"rateLimit": "Rate limit (requests/min)",
@@ -1603,7 +1612,7 @@
"allowedIpsPlaceholder": "comma-separated, blank = any",
"generate": "Genereren",
"existingKeys": "Existing keys ({count})",
- "noKeys": "No API keys yet \u2014 generate one above.",
+ "noKeys": "No API keys yet — generate one above.",
"colName": "Name",
"colKey": "Sleutel",
"colAllowedIps": "Allowed IPs",
@@ -1656,12 +1665,12 @@
"artist": "Artist",
"album": "Album",
"artworkUrl": "Artwork URL",
- "artworkUrlPlaceholder": "https://\u2026/cover.png",
+ "artworkUrlPlaceholder": "https://…/cover.png",
"duration": "Duration (seconds)",
"durationPlaceholder": "e.g. 215",
"sortOrder": "Sort order",
"playlist": "Playlist ({count})",
- "noTracks": "No AutoDJ tracks yet \u2014 add one above.",
+ "noTracks": "No AutoDJ tracks yet — add one above.",
"colOrder": "Order",
"colTitle": "Title",
"colArtist": "Artist",
@@ -1679,7 +1688,7 @@
},
"monitoring": {
"title": "Radiomonitoring",
- "subtitle": "Read-only live probe of the configured radio endpoints (radio_now_playing_api_url, radio_listeners_api_url). Each request fails soft with a {timeout}s timeout \u2014 no data is written. Configure URLs under Settings.",
+ "subtitle": "Read-only live probe of the configured radio endpoints (radio_now_playing_api_url, radio_listeners_api_url). Each request fails soft with a {timeout}s timeout — no data is written. Configure URLs under Settings.",
"stream": "Stream",
"online": "Online",
"offline": "Offline",
@@ -1694,7 +1703,7 @@
"noNowPlayingHint": "no now-playing API set",
"noListenersHint": "no listeners API set",
"endpointDiagnostics": "Endpoint diagnostics",
- "diagnosticsLegend": "OK reachable \u00b7 WARN not configured \u00b7 ERROR fetch failed / timed out.",
+ "diagnosticsLegend": "OK reachable · WARN not configured · ERROR fetch failed / timed out.",
"streamUrl": "Stream URL",
"unreachable": "unreachable",
"refresh": "Vernieuwen"
diff --git a/src/messages/no.json b/src/messages/no.json
index f1d77e87..e292c408 100644
--- a/src/messages/no.json
+++ b/src/messages/no.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Skrivbare bokser",
"photos": "Bilder",
"tickets": "Billetter",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Brukere og tilgang",
"users": "Brukere",
"bans": "Forbud",
@@ -706,6 +710,8 @@
"applications": "Søknader",
"teams": "Lag",
"permissions": "Tillatelser",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Økonomi",
"catalog": "Katalog",
"shop": "Handle",
@@ -732,6 +738,9 @@
"emulator": "Emulator",
"alerts": "Varsler",
"logs": "Logger",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Ordfilter",
"maintenance": "Vedlikehold",
"rooms": "Rom",
diff --git a/src/messages/pl.json b/src/messages/pl.json
index 4c8188e5..f42224e6 100644
--- a/src/messages/pl.json
+++ b/src/messages/pl.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Pudełka z możliwością zapisu",
"photos": "Zdjęcia",
"tickets": "Bilety",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Użytkownicy i dostęp",
"users": "Użytkownicy",
"bans": "Zakazy",
@@ -706,6 +710,8 @@
"applications": "Aplikacje",
"teams": "Zespoły",
"permissions": "Uprawnienia",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Ekonomia",
"catalog": "Katalog",
"shop": "Sklep",
@@ -732,6 +738,9 @@
"emulator": "Emulator",
"alerts": "Alerty",
"logs": "Dzienniki",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Filtr słów",
"maintenance": "Konserwacja",
"rooms": "Pokoje",
diff --git a/src/messages/pt.json b/src/messages/pt.json
index 813ceeda..b0d108c3 100644
--- a/src/messages/pt.json
+++ b/src/messages/pt.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Caixas graváveis",
"photos": "Fotos",
"tickets": "Ingressos",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Usuários e acesso",
"users": "Usuários",
"bans": "Proibições",
@@ -706,6 +710,8 @@
"applications": "Aplicativos",
"teams": "Equipes",
"permissions": "Permissões",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Economia",
"catalog": "Catálogo",
"shop": "Loja",
@@ -732,6 +738,9 @@
"emulator": "Emulador",
"alerts": "Alertas",
"logs": "Registros",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Filtro de palavras",
"maintenance": "Manutenção",
"rooms": "Quartos",
diff --git a/src/messages/ro.json b/src/messages/ro.json
index 4b76457c..821d86f4 100644
--- a/src/messages/ro.json
+++ b/src/messages/ro.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Cutii care pot fi scrise",
"photos": "Fotografii",
"tickets": "bilete",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Utilizatori și acces",
"users": "Utilizatori",
"bans": "Interdicții",
@@ -706,6 +710,8 @@
"applications": "Aplicații",
"teams": "Echipe",
"permissions": "Permisiuni",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Economie",
"catalog": "Catalog",
"shop": "Magazin",
@@ -732,6 +738,9 @@
"emulator": "Emulator",
"alerts": "Alerte",
"logs": "Bușteni",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Filtru de cuvinte",
"maintenance": "Întreținere",
"rooms": "Camerele",
diff --git a/src/messages/ru.json b/src/messages/ru.json
index cd219085..bd38e723 100644
--- a/src/messages/ru.json
+++ b/src/messages/ru.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Ящики для записи",
"photos": "Фото",
"tickets": "Билеты",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Пользователи и доступ",
"users": "Пользователи",
"bans": "Запреты",
@@ -706,6 +710,8 @@
"applications": "Области применения",
"teams": "Команды",
"permissions": "Разрешения",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Экономика",
"catalog": "Каталог",
"shop": "Магазин",
@@ -732,6 +738,9 @@
"emulator": "Эмулятор",
"alerts": "Оповещения",
"logs": "Журналы",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Фильтр слов",
"maintenance": "Техническое обслуживание",
"rooms": "Номера",
diff --git a/src/messages/sk.json b/src/messages/sk.json
index b72a24b5..97ed464f 100644
--- a/src/messages/sk.json
+++ b/src/messages/sk.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Popisovateľné boxy",
"photos": "Fotografie",
"tickets": "Vstupenky",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Používatelia a prístup",
"users": "Používatelia",
"bans": "Zákazy",
@@ -706,6 +710,8 @@
"applications": "Aplikácie",
"teams": "Tímy",
"permissions": "Povolenia",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "ekonomika",
"catalog": "Katalóg",
"shop": "Obchod",
@@ -732,6 +738,9 @@
"emulator": "Emulátor",
"alerts": "Upozornenia",
"logs": "Denníky",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Filter slov",
"maintenance": "Údržba",
"rooms": "Izby",
diff --git a/src/messages/sr.json b/src/messages/sr.json
index 2351112f..22e95ac6 100644
--- a/src/messages/sr.json
+++ b/src/messages/sr.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Кутије за писање",
"photos": "Фотографије",
"tickets": "Улазнице",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Корисници и приступ",
"users": "Корисници",
"bans": "Банс",
@@ -706,6 +710,8 @@
"applications": "Апликације",
"teams": "Тимови",
"permissions": "Дозволе",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Економија",
"catalog": "Каталог",
"shop": "Схоп",
@@ -732,6 +738,9 @@
"emulator": "Емулатор",
"alerts": "Алертс",
"logs": "Дневници",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Филтер речи",
"maintenance": "Одржавање",
"rooms": "Собе",
diff --git a/src/messages/sv.json b/src/messages/sv.json
index 61efe082..fc9f4927 100644
--- a/src/messages/sv.json
+++ b/src/messages/sv.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Skrivbara lådor",
"photos": "Foton",
"tickets": "Biljetter",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Användare och åtkomst",
"users": "Användare",
"bans": "Förbud",
@@ -706,6 +710,8 @@
"applications": "Ansökningar",
"teams": "Lag",
"permissions": "Behörigheter",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Ekonomi",
"catalog": "Katalog",
"shop": "Handla",
@@ -732,6 +738,9 @@
"emulator": "Emulator",
"alerts": "Varningar",
"logs": "Loggar",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Ordfilter",
"maintenance": "Underhåll",
"rooms": "Rum",
diff --git a/src/messages/tr.json b/src/messages/tr.json
index bfbfec27..c215089d 100644
--- a/src/messages/tr.json
+++ b/src/messages/tr.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Yazılabilir kutular",
"photos": "Fotoğraflar",
"tickets": "Biletler",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Kullanıcılar ve erişim",
"users": "Kullanıcılar",
"bans": "Yasaklar",
@@ -706,6 +710,8 @@
"applications": "Uygulamalar",
"teams": "Takımlar",
"permissions": "İzinler",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Ekonomi",
"catalog": "Katalog",
"shop": "Mağaza",
@@ -732,6 +738,9 @@
"emulator": "Emülatör",
"alerts": "Uyarılar",
"logs": "Günlükler",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Kelime filtresi",
"maintenance": "Bakım",
"rooms": "Odalar",
diff --git a/src/messages/uk.json b/src/messages/uk.json
index 243b685c..1089e6f9 100644
--- a/src/messages/uk.json
+++ b/src/messages/uk.json
@@ -698,6 +698,10 @@
"writeableBoxes": "Ящики для запису",
"photos": "Фотографії",
"tickets": "квитки",
+ "events": "Events",
+ "polls": "Polls",
+ "banners": "Banners",
+ "prefixes": "Prefixes",
"usersAndAccess": "Користувачі та доступ",
"users": "Користувачі",
"bans": "Заборони",
@@ -706,6 +710,8 @@
"applications": "Додатки",
"teams": "Команди",
"permissions": "Дозволи",
+ "onlineUsers": "Online users",
+ "moderation": "Moderation",
"economy": "Економіка",
"catalog": "Каталог",
"shop": "Магазин",
@@ -732,6 +738,9 @@
"emulator": "Емулятор",
"alerts": "Оповіщення",
"logs": "Журнали",
+ "auditLog": "Audit log",
+ "analytics": "Analytics",
+ "devops": "DevOps",
"wordFilter": "Фільтр Word",
"maintenance": "Технічне обслуговування",
"rooms": "Кімнати",