feat(hk): expand search and add operational user overview

This commit is contained in:
Simo committed 2026-09-09 21:14:10 +02:00
1 parent 27447ce029
commit 89526af344
44 files changed
+3740 -416

No files matched your search

+20
View File
@@ -81,3 +81,23 @@ Validation: 1,506 tests passed (six skipped), type checking, lint, translation
contracts and a production build with fixture configuration. A browser fixture
verified the real dialog at 1280 and 390 pixels; server actions were simulated.
The new database migration and package publication have not run in production.
## Housekeeping search and user overview
The existing global search now includes furniture, normal/Club catalog categories
and both ticket sources. Results respect module permissions, accept single-digit
IDs, and remain usable when one source fails. Keyboard navigation and cancellation
prevent stale search responses from replacing newer results.
User details open on an operational overview with up to five records from each
authorized source: bans, active mute, support tickets, help tickets, reports,
payments, catalog purchases and audit activity. Failed sources are distinguished
from empty results. User detail and edit pages also apply log permissions before
loading activity and exposing counters.
Italian and Dutch navigation, user management, news and support labels were
reviewed. The new search and overview copy has English, Italian and Dutch text;
other locales receive English fallback strings. This is a focused editorial pass,
not a full translation of every CMS page. No database migration or dependency
change is required. Browser checks use real components with simulated data at
1280 and 390 pixels; production database behavior still needs deployment validation.
@@ -1,6 +1,7 @@
"use client";
import { Loader2, UserPlus } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react";
import { toast } from "sonner";
import { createUser } from "@/actions/users";
@@ -28,17 +29,10 @@ interface CreateUserDialogProps {
sessionRank: number;
}
const RANK_OPTIONS: { value: number; label: string }[] = [
{ value: 1, label: "User (1)" },
{ value: 2, label: "Bronze (2)" },
{ value: 3, label: "Silver (3)" },
{ value: 4, label: "Gold (4)" },
{ value: 5, label: "Moderator (5)" },
{ value: 6, label: "Admin (6)" },
{ value: 7, label: "Super Admin (7)" },
];
const RANK_OPTIONS = [1, 2, 3, 4, 5, 6, 7];
export function CreateUserDialog({ sessionRank }: CreateUserDialogProps) {
const t = useTranslations("pages.admin.users.create");
const { run, isPending } = useServerAction();
const [open, setOpen] = useState(false);
const [username, setUsername] = useState("");
@@ -48,7 +42,7 @@ export function CreateUserDialog({ sessionRank }: CreateUserDialogProps) {
const [motto, setMotto] = useState("");
const allowedRanks = RANK_OPTIONS.filter((r) =>
sessionRank >= 7 ? true : r.value < sessionRank,
sessionRank >= 7 ? true : r < sessionRank,
);
function reset() {
@@ -67,22 +61,22 @@ export function CreateUserDialog({ sessionRank }: CreateUserDialogProps) {
for (let i = 0; i < buf.length; i++)
out += charset[buf[i] % charset.length];
setPassword(out);
toast.message("Password generated", { description: out });
toast.message(t("passwordGenerated"));
}
function handleCreate() {
const u = username.trim();
const m = mail.trim();
if (!u || !m || !password) {
toast.error("Username, email and password are required");
toast.error(t("required"));
return;
}
if (u.length < 3 || u.length > 20 || !/^[a-zA-Z0-9_-]+$/.test(u)) {
toast.error("Invalid username (3-20 chars, letters/digits/_/-)");
toast.error(t("invalidUsername"));
return;
}
if (password.length < 6) {
toast.error("Password must be at least 6 characters");
toast.error(t("passwordLength"));
return;
}
@@ -96,7 +90,7 @@ export function CreateUserDialog({ sessionRank }: CreateUserDialogProps) {
motto: motto.trim(),
}),
{
successMessage: `User "${u}" created.`,
successMessage: t("created", { username: u }),
onSuccess: () => {
setOpen(false);
reset();
@@ -116,31 +110,28 @@ export function CreateUserDialog({ sessionRank }: CreateUserDialogProps) {
<DialogTrigger asChild>
<Button>
<UserPlus className="mr-2 h-4 w-4" />
New User
{t("newUser")}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create User</DialogTitle>
<DialogDescription>
Account is created immediately with default look and 5,000
credits/duckets/diamonds.
</DialogDescription>
<DialogTitle>{t("title")}</DialogTitle>
<DialogDescription>{t("description")}</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="cu-username">Username</Label>
<Label htmlFor="cu-username">{t("username")}</Label>
<Input
id="cu-username"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="HabboName"
placeholder={t("usernamePlaceholder")}
autoComplete="off"
maxLength={20}
/>
</div>
<div className="space-y-2">
<Label htmlFor="cu-mail">Email</Label>
<Label htmlFor="cu-mail">{t("email")}</Label>
<Input
id="cu-mail"
type="email"
@@ -151,14 +142,14 @@ export function CreateUserDialog({ sessionRank }: CreateUserDialogProps) {
/>
</div>
<div className="space-y-2">
<Label htmlFor="cu-password">Password</Label>
<Label htmlFor="cu-password">{t("password")}</Label>
<div className="flex gap-2">
<Input
id="cu-password"
type="text"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Minimum 6 characters"
placeholder={t("passwordPlaceholder")}
autoComplete="new-password"
className="font-mono"
/>
@@ -167,33 +158,33 @@ export function CreateUserDialog({ sessionRank }: CreateUserDialogProps) {
variant="outline"
onClick={generatePassword}
>
Generate
{t("generate")}
</Button>
</div>
</div>
<div className="space-y-2">
<Label>Rank</Label>
<Label>{t("rank")}</Label>
<Select value={rank} onValueChange={setRank}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{allowedRanks.map((r) => (
<SelectItem key={r.value} value={String(r.value)}>
{r.label}
<SelectItem key={r} value={String(r)}>
{t("rankOption", { rank: r })}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="cu-motto">Motto (optional)</Label>
<Label htmlFor="cu-motto">{t("motto")}</Label>
<Input
id="cu-motto"
value={motto}
onChange={(e) => setMotto(e.target.value)}
maxLength={127}
placeholder="I'm new here!"
placeholder={t("mottoPlaceholder")}
/>
</div>
@@ -207,7 +198,7 @@ export function CreateUserDialog({ sessionRank }: CreateUserDialogProps) {
) : (
<UserPlus className="mr-2 h-4 w-4" />
)}
Create User
{t("title")}
</Button>
</div>
</DialogContent>
@@ -6,6 +6,7 @@ import {
MonitorSmartphone,
Shield,
} from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "@/components/link";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -25,40 +26,41 @@ interface UserAccountInfoProps {
}
export function UserAccountInfo({ user }: UserAccountInfoProps) {
const t = useTranslations("pages.admin.users.presentation");
return (
<Card>
<CardHeader>
<CardTitle>Account Details</CardTitle>
<CardTitle>{t("accountDetails")}</CardTitle>
</CardHeader>
<CardContent>
<div className="grid gap-4 sm:grid-cols-2">
<div className="space-y-3">
<div className="flex items-center gap-3 text-sm">
<Calendar className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="text-muted-foreground">Registered</span>
<span className="text-muted-foreground">{t("registered")}</span>
<span className="ml-auto font-medium">
{formatDate(new Date(user.accountCreated * 1000), "date")}
</span>
</div>
<div className="flex items-center gap-3 text-sm">
<Clock className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="text-muted-foreground">Last Login</span>
<span className="text-muted-foreground">{t("lastLogin")}</span>
<span className="ml-auto font-medium">
{user.lastLogin
? formatDate(new Date(user.lastLogin * 1000))
: "Never"}
: t("never")}
</span>
</div>
<div className="flex items-center gap-3 text-sm">
<Globe className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="text-muted-foreground">IP Register</span>
<span className="text-muted-foreground">{t("ipRegister")}</span>
<span className="ml-auto flex items-center gap-1.5">
<code className="text-xs font-mono bg-muted px-2 py-0.5 rounded">
{user.ipRegister}
</code>
<Link
href={`/admin/users?search=${user.ipRegister}`}
title="Find other users with this IP"
title={t("findIp")}
>
<ExternalLink className="h-3 w-3 text-primary" />
</Link>
@@ -66,14 +68,14 @@ export function UserAccountInfo({ user }: UserAccountInfoProps) {
</div>
<div className="flex items-center gap-3 text-sm">
<Globe className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="text-muted-foreground">IP Current</span>
<span className="text-muted-foreground">{t("ipCurrent")}</span>
<span className="ml-auto flex items-center gap-1.5">
<code className="text-xs font-mono bg-muted px-2 py-0.5 rounded">
{user.ipCurrent}
</code>
<Link
href={`/admin/users?search=${user.ipCurrent}`}
title="Find other users with this IP"
title={t("findIp")}
>
<ExternalLink className="h-3 w-3 text-primary" />
</Link>
@@ -88,22 +90,22 @@ export function UserAccountInfo({ user }: UserAccountInfoProps) {
variant={user.twoFactorConfirmed ? "default" : "outline"}
className="ml-auto"
>
{user.twoFactorConfirmed ? "Enabled" : "Disabled"}
{user.twoFactorConfirmed ? t("enabled") : t("disabled")}
</Badge>
</div>
<div className="flex items-center gap-3 text-sm">
<MonitorSmartphone className="h-4 w-4 text-muted-foreground shrink-0" />
<span className="text-muted-foreground">Machine ID</span>
<span className="text-muted-foreground">{t("machineId")}</span>
<code className="ml-auto text-xs font-mono bg-muted px-2 py-0.5 rounded truncate max-w-[180px]">
{user.machine_id || "N/A"}
{user.machine_id || t("unavailable")}
</code>
</div>
<div className="flex items-center gap-3 text-sm">
<span className="text-muted-foreground">Email</span>
<span className="text-muted-foreground">{t("email")}</span>
<span className="ml-auto font-medium">{user.mail}</span>
</div>
<div className="flex items-center gap-3 text-sm">
<span className="text-muted-foreground">Gender</span>
<span className="text-muted-foreground">{t("gender")}</span>
<Badge variant="outline" className="ml-auto">
{user.gender}
</Badge>
@@ -1,4 +1,5 @@
"use client";
import { useTranslations } from "next-intl";
interface Badge {
id: number;
@@ -6,8 +7,9 @@ interface Badge {
}
export function UserBadgesReadonly({ badges }: { badges: Badge[] }) {
const t = useTranslations("pages.admin.users.presentation");
if (badges.length === 0) {
return <p className="text-sm text-muted-foreground">No badges</p>;
return <p className="text-sm text-muted-foreground">{t("noBadges")}</p>;
}
return (
+45 -23
View File
@@ -10,6 +10,7 @@ import {
WifiOff,
} from "lucide-react";
import { useSearchParams } from "next/navigation";
import { useTranslations } from "next-intl";
import { AdminBreadcrumb } from "@/components/admin/admin-breadcrumb";
import { UserWatchToggle } from "@/components/admin/user-watch-toggle";
import Link from "@/components/link";
@@ -42,6 +43,7 @@ interface UserHeroProps {
activeBan: boolean;
mode: "show" | "edit";
canEdit: boolean;
canViewLogs?: boolean;
/** When non-null, render the watch toggle with this initial state. */
watching?: boolean | null;
}
@@ -58,7 +60,9 @@ export function UserHero({
mode,
canEdit,
watching,
canViewLogs = true,
}: UserHeroProps) {
const t = useTranslations("pages.admin.users.presentation");
const params = useSearchParams();
const list = params.get("list") ?? "";
const listHref = userListHref(list);
@@ -66,9 +70,9 @@ export function UserHero({
<>
<AdminBreadcrumb
items={[
{ label: "Users", href: listHref },
{ label: t("users"), href: listHref },
{ label: user.username },
{ label: mode === "edit" ? "Edit" : "View" },
{ label: mode === "edit" ? t("edit") : t("view") },
]}
/>
@@ -107,14 +111,14 @@ export function UserHero({
{mode === "show" && canEdit && (
<Button variant="outline" size="sm" asChild className="ml-2">
<Link href={userDetailHref(user.id, "edit", list)}>
<Pencil className="mr-1.5 h-3.5 w-3.5" /> Edit
<Pencil className="mr-1.5 h-3.5 w-3.5" /> {t("edit")}
</Link>
</Button>
)}
{mode === "edit" && (
<Button variant="outline" size="sm" asChild className="ml-2">
<Link href={userDetailHref(user.id, "show", list)}>
<Eye className="mr-1.5 h-3.5 w-3.5" /> View
<Eye className="mr-1.5 h-3.5 w-3.5" /> {t("view")}
</Link>
</Button>
)}
@@ -134,22 +138,24 @@ export function UserHero({
)}
<div className="flex flex-wrap items-center gap-2">
<Badge>{user.permission.rankName || `Rank ${user.rank}`}</Badge>
<Badge>
{user.permission.rankName || t("rank", { rank: user.rank })}
</Badge>
<Badge
variant={isOnline ? "default" : "outline"}
className={isOnline ? "bg-[var(--admin-success-subtle)]" : ""}
>
{isOnline ? (
<>
<Wifi className="mr-1 h-3 w-3" /> Online
<Wifi className="mr-1 h-3 w-3" /> {t("online")}
</>
) : (
<>
<WifiOff className="mr-1 h-3 w-3" /> Offline
<WifiOff className="mr-1 h-3 w-3" /> {t("offline")}
</>
)}
</Badge>
{activeBan && <Badge variant="destructive">Banned</Badge>}
{activeBan && <Badge variant="destructive">{t("banned")}</Badge>}
{user.twoFactorConfirmed ? (
<Badge
variant="outline"
@@ -167,24 +173,32 @@ export function UserHero({
<span className="font-semibold">
{formatNumber(user.credits)}
</span>
<span className="text-muted-foreground text-xs">credits</span>
<span className="text-muted-foreground text-xs">
{t("credits")}
</span>
</div>
<div className="flex items-center gap-1.5 text-sm">
<CurrencyIcon kind="diamonds" size={18} />
<span className="font-semibold">{formatNumber(diamonds)}</span>
<span className="text-muted-foreground text-xs">diamonds</span>
<span className="text-muted-foreground text-xs">
{t("diamonds")}
</span>
</div>
<div className="flex items-center gap-1.5 text-sm">
<CurrencyIcon kind="duckets" size={18} />
<span className="font-semibold">{formatNumber(duckets)}</span>
<span className="text-muted-foreground text-xs">duckets</span>
<span className="text-muted-foreground text-xs">
{t("duckets")}
</span>
</div>
<div className="flex items-center gap-1.5 text-sm">
<CurrencyIcon kind="pixels" size={18} />
<span className="font-semibold">
{formatNumber(user.pixels)}
</span>
<span className="text-muted-foreground text-xs">pixels</span>
<span className="text-muted-foreground text-xs">
{t("pixels")}
</span>
</div>
</div>
</div>
@@ -197,18 +211,26 @@ export function UserHero({
>
<DoorOpen className="h-4 w-4 mx-auto text-muted-foreground" />
<p className="text-lg font-bold">{roomCount}</p>
<p className="text-[10px] text-muted-foreground">Rooms</p>
<p className="text-[10px] text-muted-foreground">{t("rooms")}</p>
</Link>
<div className="rounded-lg border bg-card px-3 py-2 text-center">
<MessageSquare className="h-4 w-4 mx-auto text-muted-foreground" />
<p className="text-lg font-bold">{formatNumber(chatCount)}</p>
<p className="text-[10px] text-muted-foreground">Messages</p>
</div>
<div className="rounded-lg border bg-card px-3 py-2 text-center">
<ArrowLeftRight className="h-4 w-4 mx-auto text-muted-foreground" />
<p className="text-lg font-bold">{tradeCount}</p>
<p className="text-[10px] text-muted-foreground">Trades</p>
</div>
{canViewLogs && (
<div className="rounded-lg border bg-card px-3 py-2 text-center">
<MessageSquare className="h-4 w-4 mx-auto text-muted-foreground" />
<p className="text-lg font-bold">{formatNumber(chatCount)}</p>
<p className="text-[10px] text-muted-foreground">
{t("messages")}
</p>
</div>
)}
{canViewLogs && (
<div className="rounded-lg border bg-card px-3 py-2 text-center">
<ArrowLeftRight className="h-4 w-4 mx-auto text-muted-foreground" />
<p className="text-lg font-bold">{tradeCount}</p>
<p className="text-[10px] text-muted-foreground">
{t("trades")}
</p>
</div>
)}
</div>
</div>
</div>
@@ -0,0 +1,123 @@
import { getTranslations } from "next-intl/server";
import Link from "@/components/link";
import { formatDate } from "@/lib/format-date";
import type { OverviewSection } from "../_lib/load-user-overview";
function eventDate(value: string | number | Date | null | undefined) {
if (!value) return "—";
const numeric =
typeof value === "number" ||
(typeof value === "string" && /^\d+$/.test(value));
const date = numeric ? new Date(Number(value) * 1000) : new Date(value);
return Number.isNaN(date.getTime()) ? "—" : formatDate(date);
}
export async function UserOperationalOverview({
sections,
}: {
sections: OverviewSection[];
}) {
const t = await getTranslations("pages.admin.users.overview");
return (
<section className="space-y-4" aria-label={t("title")}>
<p className="text-sm text-muted-foreground">{t("hint")}</p>
{sections.length === 0 && (
<p className="rounded-lg border bg-card p-4 text-sm text-muted-foreground">
{t("restricted")}
</p>
)}
<div className="grid gap-4 xl:grid-cols-2">
{sections.map((section) => (
<section
key={section.key}
className="min-w-0 rounded-lg border bg-card p-4"
>
<div className="mb-3 flex items-center justify-between gap-3">
<h3 className="font-semibold text-sm">{t(section.key)}</h3>
{section.href && (
<Link
href={section.href}
className="text-xs text-primary hover:underline"
>
{t("openModule")}
</Link>
)}
</div>
{section.key === "purchases" && (
<p className="mb-2 text-xs text-muted-foreground">
{t("purchaseHint")}
</p>
)}
{section.key === "activity" && (
<p className="mb-2 text-xs text-muted-foreground">
{t("activityHint")}
</p>
)}
{section.state === "error" ? (
<p role="status" className="text-sm text-destructive">
{t("error")}
</p>
) : section.rows.length === 0 ? (
<p className="text-sm text-muted-foreground">{t("empty")}</p>
) : (
<ul className="divide-y">
{section.rows.map((row) => (
<li key={String(row.id)} className="space-y-1 py-2 text-sm">
<div className="flex items-start justify-between gap-2">
{row.href ? (
<Link
href={row.href}
className="min-w-0 break-words font-medium text-primary hover:underline"
>
#{String(row.id)} · {row.title || "—"}
</Link>
) : (
<span className="min-w-0 break-words font-medium">
#{String(row.id)} · {row.title || "—"}
</span>
)}
{row.status && (
<span className="max-w-28 shrink-0 break-words rounded bg-muted px-1.5 py-0.5 text-xs">
{[
"active",
"expired",
"open",
"closed",
"reported",
"reporter",
].includes(row.status)
? t(`status.${row.status}`)
: row.status}
</span>
)}
</div>
{section.key === "purchases" && (
<p className="text-xs text-muted-foreground">
{t("purchaseDetail", {
amount: row.amount ?? "—",
credits: row.credits ?? "—",
points: row.points ?? "—",
type: row.pointsType ?? "—",
})}
</p>
)}
{row.detail && (
<p className="break-words text-muted-foreground">
{row.detail}
</p>
)}
<p className="text-xs text-muted-foreground">
{section.key === "mute"
? t("muteUntil", { date: eventDate(row.at) })
: eventDate(row.at)}
</p>
</li>
))}
</ul>
)}
</section>
))}
</div>
</section>
);
}
@@ -0,0 +1,95 @@
import { MySqlDialect } from "drizzle-orm/mysql-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import type { PermissionSet } from "@/lib/permissions";
const state = vi.hoisted(() => ({
allowed: new Set<string>(),
execute: vi.fn(),
}));
vi.mock("@/lib/db", () => ({ db: { execute: state.execute } }));
vi.mock("@/lib/permissions", async () => ({
...(await import("@/lib/permission-slugs")),
canAccess: (_: unknown, permission: string) => state.allowed.has(permission),
}));
import { loadUserOverview } from "./load-user-overview";
const permissions = {} as PermissionSet;
const dialect = new MySqlDialect();
beforeEach(() => {
state.allowed.clear();
state.execute
.mockReset()
.mockResolvedValue([[{ id: 12, title: "Existing record" }], []]);
});
describe("user operational overview", () => {
it("does not query or expose any restricted source", async () => {
expect(await loadUserOverview(42, permissions, 4)).toEqual([]);
expect(state.execute).not.toHaveBeenCalled();
});
it.each([
[PERMS.BANS_VIEW, ["sanctions"]],
[PERMS.TICKETS_VIEW, ["tickets", "help"]],
[PERMS.MODERATION_VIEW, ["mute", "reports"]],
[PERMS.SHOP_VIEW, ["transactions"]],
[PERMS.LOGS_VIEW, ["purchases", "activity"]],
])("queries only sources allowed by %s", async (permission, keys) => {
state.allowed.add(permission);
const result = await loadUserOverview(42, permissions, 4);
expect(result.map((section) => section.key)).toEqual(keys);
expect(state.execute).toHaveBeenCalledTimes(keys.length);
for (const [query] of state.execute.mock.calls) {
const compiled = dialect.sqlToQuery(query);
expect(compiled.sql).toMatch(/LIMIT 5$/);
expect(compiled.params).toContain(42);
}
});
it("keeps accessible sources available when another query fails", async () => {
state.allowed.add(PERMS.TICKETS_VIEW);
state.execute.mockRejectedValueOnce(new Error("database unavailable"));
const result = await loadUserOverview(42, permissions, 4);
expect(result[0]).toMatchObject({
key: "tickets",
state: "error",
rows: [],
});
expect(result[1]).toMatchObject({
key: "help",
state: "ready",
rows: [{ href: "/admin/help-tickets/12" }],
});
});
it("distinguishes an empty source from a failed source", async () => {
state.allowed.add(PERMS.SHOP_VIEW);
state.execute.mockResolvedValueOnce([[], []]);
expect(await loadUserOverview(42, permissions, 4)).toEqual([
{
key: "transactions",
state: "ready",
rows: [],
href: "/admin/transactions",
},
]);
});
});
it("selects and preserves recorded purchase quantities and currency costs", async () => {
state.allowed.add(PERMS.LOGS_VIEW);
const purchase = {
id: 5,
title: "Chair",
amount: 2,
credits: 10,
points: 3,
pointsType: 5,
};
state.execute.mockResolvedValueOnce([[purchase], []]);
const result = await loadUserOverview(42, permissions, 4);
const query = dialect.sqlToQuery(state.execute.mock.calls[0][0]).sql;
expect(query).toContain(
"amount, cost_credits AS credits, cost_points AS points, points_type AS pointsType",
);
expect(
result.find((section) => section.key === "purchases")?.rows[0],
).toMatchObject(purchase);
});
@@ -0,0 +1,137 @@
import { type SQL, sql } from "drizzle-orm";
import { db } from "@/lib/db";
import { canAccess, PERMS, type PermissionSet } from "@/lib/permissions";
export type OverviewRow = {
id: string | number;
title: string;
detail?: string | null;
status?: string | null;
at?: string | number | Date | null;
href?: string;
amount?: number | null;
credits?: number | null;
points?: number | null;
pointsType?: number | null;
};
export type OverviewSection = {
key: string;
href?: string;
state: "ready" | "error";
rows: OverviewRow[];
};
export async function loadUserOverview(
userId: number,
permissions: PermissionSet,
rank: number,
): Promise<OverviewSection[]> {
const read = async (query: SQL): Promise<OverviewRow[]> => {
const [rows] = await db.execute(query);
return rows as unknown as OverviewRow[];
};
const sources: {
key: string;
permission: string;
href?: string;
query: () => Promise<OverviewRow[]>;
rowHref?: (id: string | number) => string;
}[] = [
{
key: "sanctions",
permission: PERMS.BANS_VIEW,
href: "/admin/bans",
query: () =>
read(
sql`SELECT id, type AS title, LEFT(ban_reason, 180) AS detail, timestamp AS at, CASE WHEN ban_expire = 0 OR ban_expire > UNIX_TIMESTAMP() THEN 'active' ELSE 'expired' END AS status FROM bans WHERE user_id = ${userId} ORDER BY timestamp DESC, id DESC LIMIT 5`,
),
},
{
key: "mute",
permission: PERMS.MODERATION_VIEW,
query: () =>
read(
sql`SELECT user_id AS id, '' AS title, mute_end_timestamp AS at FROM users_settings WHERE user_id = ${userId} AND mute_end_timestamp > UNIX_TIMESTAMP() LIMIT 5`,
),
},
{
key: "tickets",
permission: PERMS.TICKETS_VIEW,
query: () =>
read(
sql`SELECT id, subject AS title, status, updated_at AS at FROM website_tickets WHERE creator_id = ${userId} ORDER BY updated_at DESC, id DESC LIMIT 5`,
),
rowHref: (id) => `/admin/tickets/${id}`,
},
{
key: "help",
permission: PERMS.TICKETS_VIEW,
query: () =>
read(
sql`SELECT id, title, CASE WHEN open = 1 THEN 'open' ELSE 'closed' END AS status, created_at AS at FROM website_help_center_tickets WHERE user_id = ${userId} ORDER BY created_at DESC, id DESC LIMIT 5`,
),
rowHref: (id) => `/admin/help-tickets/${id}`,
},
{
key: "reports",
permission: PERMS.MODERATION_VIEW,
query: () =>
read(
sql`SELECT id, LEFT(issue, 180) AS title, CASE WHEN reported_id = ${userId} THEN 'reported' ELSE 'reporter' END AS status, timestamp AS at FROM support_tickets WHERE reported_id = ${userId} OR sender_id = ${userId} ORDER BY timestamp DESC, id DESC LIMIT 5`,
),
rowHref: (id) => `/admin/moderation/cfh/${id}`,
},
{
key: "transactions",
permission: PERMS.SHOP_VIEW,
href: "/admin/transactions",
query: () =>
read(
sql`SELECT id, COALESCE(description, transaction_id) AS title, CONCAT(amount, ' ', currency) AS detail, status, created_at AS at FROM website_paypal_transactions WHERE user_id = ${userId} ORDER BY created_at DESC, id DESC LIMIT 5`,
),
},
{
key: "purchases",
permission: PERMS.LOGS_VIEW,
query: () =>
read(
sql`SELECT id, catalog_name AS title, amount, cost_credits AS credits, cost_points AS points, points_type AS pointsType, timestamp AS at FROM logs_shop_purchases WHERE user_id = ${userId} ORDER BY timestamp DESC, id DESC LIMIT 5`,
),
},
{
key: "activity",
permission: PERMS.LOGS_VIEW,
href: "/admin/logs/audit",
query: () =>
read(
sql`SELECT id, action AS title, created_at AS at FROM admin_audit_log WHERE target = 'User' AND target_id = ${userId} ORDER BY id DESC LIMIT 5`,
),
},
];
// Filter before invoking the query; an inaccessible source must never touch its table.
return Promise.all(
sources
.filter((source) => canAccess(permissions, source.permission, rank))
.map(async (source) => {
try {
const rows = await source.query();
return {
key: source.key,
href: source.href,
state: "ready" as const,
rows: rows.map((row) => ({
...row,
href: source.rowHref?.(row.id),
})),
};
} catch {
return {
key: source.key,
href: source.href,
state: "error" as const,
rows: [],
};
}
}),
);
}
@@ -0,0 +1,87 @@
import { MySqlDialect } from "drizzle-orm/mysql-core";
import { beforeEach, expect, it, vi } from "vitest";
const state = vi.hoisted(() => ({
sql: [] as string[],
tables: [] as string[],
failLogs: false,
}));
vi.mock("next/navigation", () => ({
notFound: () => {
throw new Error("not found");
},
}));
vi.mock("@/lib/db", () => {
const tables = Object.fromEntries(
["User", "Rooms", "RoomTradeLog", "UsersBadges", "UsersCurrency"].map(
(name) => [name, { name }],
),
);
return {
...tables,
db: {
select() {
let name = "";
const rows = () =>
name === "User"
? [{ id: 42, username: "User", rank: 2, online: "0" }]
: [];
const query = {
from(table: { name: string }) {
name = table.name;
state.tables.push(name);
return query;
},
where() {
return query;
},
orderBy() {
return query;
},
limit() {
return query;
},
// biome-ignore lint/suspicious/noThenProperty: Drizzle queries are intentionally awaitable.
then(resolve: (value: unknown[]) => unknown) {
return Promise.resolve(rows()).then(resolve);
},
catch() {
return Promise.resolve(rows());
},
};
return query;
},
async execute(query: Parameters<MySqlDialect["sqlToQuery"]>[0]) {
const compiled = new MySqlDialect().sqlToQuery(query).sql;
state.sql.push(compiled);
if (state.failLogs && compiled.includes("chatlogs_room"))
throw new Error("unavailable");
return [[], []];
},
},
};
});
import { loadUserById } from "./load-user";
beforeEach(() => {
state.sql = [];
state.tables = [];
state.failLogs = false;
});
it("skips chat, commands, trade and ban queries when excluded by profile ACL", async () => {
const user = await loadUserById("42", { logs: false, bans: false });
expect(state.sql.join(" ")).not.toMatch(
/chatlogs_room|commandlogs|FROM bans/,
);
expect(state.tables).not.toContain("RoomTradeLog");
expect(user.chatLogs).toEqual([]);
expect(user.commandLogs).toEqual([]);
expect(user.user.bansReceived).toEqual([]);
});
it("keeps the user profile available and identifies a failed activity source", async () => {
state.failLogs = true;
const user = await loadUserById("42", { logs: true, bans: false });
expect(user.user.id).toBe(42);
expect(user.activityUnavailable).toBe(true);
});
+63 -34
View File
@@ -14,7 +14,10 @@ async function rawRows<T>(query: ReturnType<typeof sql>): Promise<T[]> {
return rows ?? [];
}
export async function loadUserById(id: string) {
export async function loadUserById(
id: string,
access: { logs: boolean; bans: boolean },
) {
const numericId = Number(id);
const isNumeric =
Number.isSafeInteger(numericId) &&
@@ -44,6 +47,7 @@ export async function loadUserById(id: string) {
.limit(1);
if (!user) notFound();
let activityUnavailable = false;
const [
currencies,
badges,
@@ -64,30 +68,40 @@ export async function loadUserById(id: string) {
.from(UsersBadges)
.where(eq(UsersBadges.userId, user.id))
.orderBy(asc(UsersBadges.slotId)),
rawRows<{
id: number;
banReason: string;
type: string;
timestamp: number;
banExpire: number;
staffName: string;
}>(sql`
access.bans
? rawRows<{
id: number;
banReason: string;
type: string;
timestamp: number;
banExpire: number;
staffName: string;
}>(sql`
SELECT b.id, b.ban_reason AS banReason, b.type, b.timestamp, b.ban_expire AS banExpire,
COALESCE(s.username, 'Unknown') AS staffName
FROM bans b LEFT JOIN users s ON s.id = b.user_staff_id
WHERE b.user_id = ${user.id} ORDER BY b.id DESC LIMIT 5`),
WHERE b.user_id = ${user.id} ORDER BY b.id DESC LIMIT 5`).catch(
() => [],
)
: [],
rawRows<{ rankName: string }>(sql`
SELECT COALESCE(rank_name, CONCAT('Rank ', id)) AS rankName FROM permissions WHERE id = ${user.rank} LIMIT 1`),
db.select({ total: count() }).from(Rooms).where(eq(Rooms.ownerId, user.id)),
db
.select({ total: count() })
.from(RoomTradeLog)
.where(
or(
eq(RoomTradeLog.userOneId, user.id),
eq(RoomTradeLog.userTwoId, user.id),
),
),
access.logs
? db
.select({ total: count() })
.from(RoomTradeLog)
.where(
or(
eq(RoomTradeLog.userOneId, user.id),
eq(RoomTradeLog.userTwoId, user.id),
),
)
.catch(() => {
activityUnavailable = true;
return [];
})
: [],
db
.select({
id: Rooms.id,
@@ -100,23 +114,37 @@ export async function loadUserById(id: string) {
.where(eq(Rooms.ownerId, user.id))
.orderBy(desc(Rooms.users))
.limit(20),
rawRows<{
id: number;
roomId: number;
timestamp: number;
message: string;
}>(sql`
access.logs
? rawRows<{
id: number;
roomId: number;
timestamp: number;
message: string;
}>(sql`
SELECT ROW_NUMBER() OVER (ORDER BY timestamp DESC) AS id, room_id AS roomId, timestamp, message
FROM chatlogs_room WHERE user_from_id = ${user.id} ORDER BY timestamp DESC LIMIT 20`),
rawRows<{
id: number;
timestamp: number;
command: string;
params: string;
succes: string;
}>(sql`
FROM chatlogs_room WHERE user_from_id = ${user.id} ORDER BY timestamp DESC LIMIT 20`).catch(
() => {
activityUnavailable = true;
return [];
},
)
: [],
access.logs
? rawRows<{
id: number;
timestamp: number;
command: string;
params: string;
succes: string;
}>(sql`
SELECT ROW_NUMBER() OVER (ORDER BY timestamp DESC) AS id, timestamp, command, params, succes
FROM commandlogs WHERE user_id = ${user.id} ORDER BY timestamp DESC LIMIT 10`),
FROM commandlogs WHERE user_id = ${user.id} ORDER BY timestamp DESC LIMIT 10`).catch(
() => {
activityUnavailable = true;
return [];
},
)
: [],
]);
const roomCount = Number(roomCountRows[0]?.total ?? 0);
@@ -157,6 +185,7 @@ export async function loadUserById(id: string) {
userRooms,
chatLogs,
commandLogs,
activityUnavailable,
isOnline: user.online === "1",
activeBan,
};
+58 -28
View File
@@ -1,5 +1,6 @@
import { sql } from "drizzle-orm";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { db } from "@/lib/db";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
@@ -25,7 +26,18 @@ export default async function UserEditPage({
redirect("/admin");
}
const data = await loadUserById(id);
const overviewT = await getTranslations("pages.admin.users.overview");
const canViewLogs = canAccess(
permissions,
PERMS.LOGS_VIEW,
session.user.rank,
);
const canViewBans = canAccess(
permissions,
PERMS.BANS_VIEW,
session.user.rank,
);
const data = await loadUserById(id, { logs: canViewLogs, bans: canViewBans });
const watched = await getWatchedUserIds(session.user.id);
// Fetch rank list from permissions table for the rank dropdown
@@ -45,6 +57,7 @@ export default async function UserEditPage({
duckets={data.duckets}
roomCount={data.roomCount}
chatCount={data.chatCount}
canViewLogs={canViewLogs && !data.activityUnavailable}
tradeCount={data.tradeCount}
isOnline={data.isOnline}
activeBan={!!data.activeBan}
@@ -57,17 +70,21 @@ export default async function UserEditPage({
<Tabs defaultValue="edit" className="space-y-4 min-w-0">
<TabsList className="flex-wrap">
<TabsTrigger value="edit">Edit Profile</TabsTrigger>
<TabsTrigger value="bans">
Bans
{data.activeBan && (
<span className="ml-1.5 flex h-2 w-2 rounded-full bg-destructive" />
)}
</TabsTrigger>
{canViewBans && (
<TabsTrigger value="bans">
Bans
{data.activeBan && (
<span className="ml-1.5 flex h-2 w-2 rounded-full bg-destructive" />
)}
</TabsTrigger>
)}
<TabsTrigger value="badges">
Badges ({data.user.badges.length})
</TabsTrigger>
<TabsTrigger value="rooms">Rooms ({data.roomCount})</TabsTrigger>
<TabsTrigger value="activity">Activity</TabsTrigger>
{canViewLogs && (
<TabsTrigger value="activity">Activity</TabsTrigger>
)}
<TabsTrigger value="info">Account Info</TabsTrigger>
</TabsList>
@@ -88,20 +105,22 @@ export default async function UserEditPage({
/>
</TabsContent>
<TabsContent value="bans">
<UserBanSection
userId={data.user.id}
username={data.user.username}
bans={data.user.bansReceived.map((b) => ({
id: b.id,
reason: b.banReason,
type: b.type,
staffName: b.staff.username,
timestamp: b.timestamp,
banExpire: b.banExpire,
}))}
/>
</TabsContent>
{canViewBans && (
<TabsContent value="bans">
<UserBanSection
userId={data.user.id}
username={data.user.username}
bans={data.user.bansReceived.map((b) => ({
id: b.id,
reason: b.banReason,
type: b.type,
staffName: b.staff.username,
timestamp: b.timestamp,
banExpire: b.banExpire,
}))}
/>
</TabsContent>
)}
<TabsContent value="badges">
<UserBadgesSection
@@ -117,12 +136,23 @@ export default async function UserEditPage({
<UserRooms rooms={data.userRooms} />
</TabsContent>
<TabsContent value="activity">
<UserActivityLog
chatLogs={data.chatLogs}
commandLogs={data.commandLogs}
/>
</TabsContent>
{canViewLogs && (
<TabsContent value="activity">
{data.activityUnavailable ? (
<p
role="status"
className="rounded-lg border bg-card p-4 text-sm text-destructive"
>
{overviewT("error")}
</p>
) : (
<UserActivityLog
chatLogs={data.chatLogs}
commandLogs={data.commandLogs}
/>
)}
</TabsContent>
)}
<TabsContent value="info">
<UserAccountInfo user={data.user} />
+41 -23
View File
@@ -6,9 +6,9 @@ import { getWatchedUserIds } from "@/lib/services/watch";
import { UserAccountInfo } from "../../_components/user-account-info";
import { UserBadgesReadonly } from "../../_components/user-badges-readonly";
import { UserHero } from "../../_components/user-hero";
import { UserSanctionTimeline } from "../../_components/user-sanction-timeline";
import { UserOperationalOverview } from "../../_components/user-operational-overview";
import { loadUserById } from "../../_lib/load-user";
import { loadUserSanctions } from "../../_lib/load-user-sanctions";
import { loadUserOverview } from "../../_lib/load-user-overview";
import { UserActivityLog } from "../../[id]/user-activity-log";
import { UserQuickActions } from "../../[id]/user-quick-actions";
import { UserRooms } from "../../[id]/user-rooms";
@@ -25,10 +25,19 @@ export default async function UserShowPage({
}
const t = await getTranslations("pages.admin.users.detail");
const overviewT = await getTranslations("pages.admin.users.overview");
const canEdit = canAccess(permissions, PERMS.USERS_EDIT, session.user.rank);
const data = await loadUserById(id);
const [sanctions, watched] = await Promise.all([
loadUserSanctions(data.user.id),
const canViewLogs = canAccess(
permissions,
PERMS.LOGS_VIEW,
session.user.rank,
);
const data = await loadUserById(id, {
logs: canViewLogs,
bans: canAccess(permissions, PERMS.BANS_VIEW, session.user.rank),
});
const [overview, watched] = await Promise.all([
loadUserOverview(data.user.id, permissions, session.user.rank),
getWatchedUserIds(session.user.id),
]);
@@ -40,6 +49,7 @@ export default async function UserShowPage({
duckets={data.duckets}
roomCount={data.roomCount}
chatCount={data.chatCount}
canViewLogs={canViewLogs && !data.activityUnavailable}
tradeCount={data.tradeCount}
isOnline={data.isOnline}
activeBan={!!data.activeBan}
@@ -55,8 +65,9 @@ export default async function UserShowPage({
: undefined
}
>
<Tabs defaultValue="info" className="space-y-4 min-w-0">
<Tabs defaultValue="overview" className="space-y-4 min-w-0">
<TabsList className="h-auto flex-wrap justify-start">
<TabsTrigger value="overview">{overviewT("title")}</TabsTrigger>
<TabsTrigger value="info">{t("tabInfo")}</TabsTrigger>
<TabsTrigger value="badges">
{t("tabBadges", { count: data.user.badges.length })}
@@ -64,12 +75,15 @@ export default async function UserShowPage({
<TabsTrigger value="rooms">
{t("tabRooms", { count: data.roomCount })}
</TabsTrigger>
<TabsTrigger value="sanctions">
{t("tabSanctions", { count: sanctions.events.length })}
</TabsTrigger>
<TabsTrigger value="activity">{t("tabActivity")}</TabsTrigger>
{canViewLogs && (
<TabsTrigger value="activity">{t("tabActivity")}</TabsTrigger>
)}
</TabsList>
<TabsContent value="overview">
<UserOperationalOverview sections={overview} />
</TabsContent>
<TabsContent value="info">
<UserAccountInfo user={data.user} />
</TabsContent>
@@ -87,19 +101,23 @@ export default async function UserShowPage({
<UserRooms rooms={data.userRooms} />
</TabsContent>
<TabsContent value="sanctions">
<UserSanctionTimeline
events={sanctions.events}
muteUntil={sanctions.muteUntil}
/>
</TabsContent>
<TabsContent value="activity">
<UserActivityLog
chatLogs={data.chatLogs}
commandLogs={data.commandLogs}
/>
</TabsContent>
{canViewLogs && (
<TabsContent value="activity">
{data.activityUnavailable ? (
<p
role="status"
className="rounded-lg border bg-card p-4 text-sm text-destructive"
>
{overviewT("error")}
</p>
) : (
<UserActivityLog
chatLogs={data.chatLogs}
commandLogs={data.commandLogs}
/>
)}
</TabsContent>
)}
</Tabs>
{canEdit && (
+247
View File
@@ -0,0 +1,247 @@
import { MySqlDialect } from "drizzle-orm/mysql-core";
import { NextRequest } from "next/server";
import { beforeEach, describe, expect, it, vi } from "vitest";
const mocks = vi.hoisted(() => ({
select: vi.fn(),
guard: vi.fn(),
permissions: new Set<string>(),
rateLimit: vi.fn(),
}));
vi.mock("@/lib/db", async () => ({
...(await import("@/db/schema")),
db: { select: mocks.select },
}));
vi.mock("@/lib/permissions", async () => ({
...(await import("@/lib/permission-slugs")),
canAccess: (permissions: { has: (slug: string) => boolean }, slug: string) =>
permissions.has(slug),
}));
vi.mock("@/lib/rate-limit", () => ({ rateLimit: mocks.rateLimit }));
vi.mock("@/lib/api-handler", () => ({
withAdmin: (
options: unknown,
handler: (request: NextRequest, context: unknown) => Promise<Response>,
) => {
mocks.guard(options);
return (request: NextRequest) =>
handler(request, {
session: { user: { id: 1, rank: 5 } },
permissions: {
isSuperAdmin: false,
has: (slug: string) => mocks.permissions.has(slug),
},
});
},
}));
import {
CatalogPages,
CatalogPagesBc,
Guilds,
ItemsBase,
User,
WebsiteArticles,
WebsiteHelpCenterTickets,
WebsiteTicket,
} from "@/lib/db";
import { PERMS } from "@/lib/permission-slugs";
import { GET } from "./route";
const calls: {
table: unknown;
fields: Record<string, unknown>;
condition: unknown;
limit: number;
}[] = [];
const dialect = new MySqlDialect();
const rows = new Map<unknown, unknown[]>();
beforeEach(() => {
mocks.permissions.clear();
mocks.rateLimit.mockResolvedValue({ ok: true });
mocks.select.mockReset();
rows.clear();
calls.length = 0;
mocks.select.mockImplementation((fields) => {
const call = {
table: undefined as unknown,
fields,
condition: undefined as unknown,
limit: 0,
};
const builder = {
from(table: unknown) {
call.table = table;
return builder;
},
where(condition: unknown) {
call.condition = condition;
return builder;
},
orderBy() {
return builder;
},
limit(limit: number) {
call.limit = limit;
calls.push(call);
return Promise.resolve(rows.get(call.table) ?? []);
},
};
return builder;
});
});
const search = (q: string) =>
GET(
new NextRequest(
`https://cms.test/api/admin/search?q=${encodeURIComponent(q)}`,
),
);
describe("unified admin search", () => {
it("requires the staff dashboard gate and performs no entity reads without category permission", async () => {
expect(mocks.guard).toHaveBeenCalledWith({
permission: PERMS.ADMIN_DASHBOARD,
});
expect(await (await search("chair")).json()).toEqual({
ok: true,
results: [],
partial: false,
});
expect(mocks.select).not.toHaveBeenCalled();
});
it.each([
[PERMS.USERS_VIEW, [User, Guilds]],
[PERMS.NEWS_VIEW, [WebsiteArticles]],
[PERMS.CATALOG_VIEW, [ItemsBase, CatalogPages, CatalogPagesBc]],
[PERMS.TICKETS_VIEW, [WebsiteTicket, WebsiteHelpCenterTickets]],
])("reads only categories granted by %s", async (permission, tables) => {
mocks.permissions.add(permission);
await search("chair");
expect(calls.map((call) => call.table)).toEqual(tables);
expect(calls.every((call) => call.limit === 5)).toBe(true);
});
it("returns exact detail destinations without private user or ticket fields", async () => {
[PERMS.USERS_VIEW, PERMS.CATALOG_VIEW, PERMS.TICKETS_VIEW].forEach((p) => {
mocks.permissions.add(p);
});
rows.set(User, [{ id: 12, title: "Alice" }]);
rows.set(ItemsBase, [{ id: 12, title: "Chair", subtitle: "chair" }]);
rows.set(CatalogPagesBc, [{ id: 12, title: "Chairs" }]);
rows.set(WebsiteHelpCenterTickets, [{ id: 12n, title: "Help" }]);
const payload = await (await search("12")).json();
expect(payload.results.map((r: { url: string }) => r.url)).toEqual([
"/admin/users/show/12",
"/admin/items/12",
"/admin/catalog/builder-club/12",
"/admin/help-tickets/12",
]);
expect(calls.find((call) => call.table === User)?.fields).toEqual({
id: User.id,
title: User.username,
});
expect(
calls.find((call) => call.table === WebsiteHelpCenterTickets)?.fields,
).toEqual({
id: WebsiteHelpCenterTickets.id,
title: WebsiteHelpCenterTickets.title,
});
});
it("ignores short queries and treats wildcard input literally", async () => {
mocks.permissions.add(PERMS.USERS_VIEW);
await search(" a ");
expect(calls).toHaveLength(0);
await search("_%");
const query = dialect.sqlToQuery(
calls[0].condition as Parameters<MySqlDialect["sqlToQuery"]>[0],
);
expect(query.params).toEqual(["%\\_\\%%"]);
expect(query.sql).not.toContain("mail");
});
it("bounds oversized queries and rejects partial numeric identifiers", async () => {
mocks.permissions.add(PERMS.USERS_VIEW);
await search("a".repeat(200));
expect(
dialect.sqlToQuery(
calls[0].condition as Parameters<MySqlDialect["sqlToQuery"]>[0],
).params,
).toEqual([`%${"a".repeat(64)}%`]);
await search("12abc");
expect(
dialect.sqlToQuery(
calls[2].condition as Parameters<MySqlDialect["sqlToQuery"]>[0],
).params,
).toEqual(["%12abc%"]);
});
it("supports one-digit identifiers", async () => {
mocks.permissions.add(PERMS.USERS_VIEW);
await search("1");
expect(
dialect.sqlToQuery(
calls[0].condition as Parameters<MySqlDialect["sqlToQuery"]>[0],
).params,
).toEqual(["%1%", 1]);
});
it("reports a partial failure while retaining authorized results", async () => {
mocks.permissions.add(PERMS.CATALOG_VIEW);
rows.set(ItemsBase, [{ id: 12, title: "Chair", subtitle: "chair" }]);
const implementation = mocks.select.getMockImplementation();
mocks.select.mockImplementation((fields) => {
const builder = implementation?.(fields);
const originalFrom = builder.from;
builder.from = (table: unknown) => {
originalFrom(table);
if (table === CatalogPagesBc)
builder.limit = () => Promise.reject(new Error("missing table"));
return builder;
};
return builder;
});
const payload = await (await search("chair")).json();
expect(payload.partial).toBe(true);
expect(payload.results).toHaveLength(1);
expect(payload.results[0].url).toBe("/admin/items/12");
});
it("caps the combined results while retaining each requested category", async () => {
[
PERMS.USERS_VIEW,
PERMS.NEWS_VIEW,
PERMS.CATALOG_VIEW,
PERMS.TICKETS_VIEW,
].forEach((permission) => {
mocks.permissions.add(permission);
});
[
User,
WebsiteArticles,
ItemsBase,
CatalogPages,
CatalogPagesBc,
WebsiteTicket,
WebsiteHelpCenterTickets,
].forEach((table) => {
rows.set(
table,
Array.from({ length: 5 }, (_, index) => ({
id: index + 1,
title: "Match",
subtitle: "Name",
})),
);
});
const payload = await (await search("Match")).json();
expect(payload.results).toHaveLength(20);
expect(
new Set(payload.results.map((row: { type: string }) => row.type)),
).toEqual(
new Set(["users", "articles", "furniture", "catalog", "tickets"]),
);
});
it("rate limits without reading records", async () => {
mocks.permissions.add(PERMS.USERS_VIEW);
mocks.rateLimit.mockResolvedValue({ ok: false, retryAfter: 20 });
expect((await search("Alice")).status).toBe(429);
expect(mocks.select).not.toHaveBeenCalled();
});
});
+314 -125
View File
@@ -1,20 +1,24 @@
import { eq, like, or } from "drizzle-orm";
import { asc, eq, like, or } from "drizzle-orm";
import { NextResponse } from "next/server";
import { ADMIN_NAV_GROUPS, navItemIsAllowed } from "@/lib/admin-nav";
import { apiOk } from "@/lib/api";
import { withAdmin } from "@/lib/api-handler";
import {
CatalogPages,
CatalogPagesBc,
db,
Guilds,
ItemsBase,
Rooms,
User,
WebsiteArticles,
WebsiteHelpCenterTickets,
WebsiteRareValues,
WebsiteShopArticles,
WebsiteTicket,
} from "@/lib/db";
import { canAccess, PERMS } from "@/lib/permissions";
import { rateLimit } from "@/lib/rate-limit";
export type AdminSearchResult = {
type: string;
id: number | string;
@@ -22,16 +26,13 @@ export type AdminSearchResult = {
subtitle: string;
url: string;
};
const PER_TYPE = 5;
const MAX_TOTAL = 20;
const MAX_QUERY_LENGTH = 64;
/** Escape LIKE wildcards so user input can't widen the match. */
export function escapeLike(input: string): string {
function escapeLike(input: string): string {
return input.replace(/[\\%_]/g, (m) => `\\${m}`);
}
export const GET = withAdmin(
{ permission: PERMS.ADMIN_DASHBOARD },
async (request, context) => {
@@ -46,14 +47,12 @@ export const GET = withAdmin(
{ status: 429, headers: { "retry-after": String(limited.retryAfter) } },
);
}
const q = (request.nextUrl.searchParams.get("q") || "")
.trim()
.slice(0, MAX_QUERY_LENGTH);
if (q.length < 2) {
if (q.length < 2 && !/^[1-9]$/.test(q)) {
return apiOk({ results: [] });
}
const canSearch = (href: string) => {
const item = ADMIN_NAV_GROUPS.flatMap((group) => group.items).find(
(item) => item.href === href,
@@ -67,123 +66,272 @@ export const GET = withAdmin(
})
);
};
const pattern = `%${escapeLike(q)}%`;
const idExact = Number.parseInt(q, 10);
const hasId = Number.isFinite(idExact) && String(idExact) === q;
const [users, articles, rooms, guilds, shopArticles, rareValues] =
await Promise.all([
canSearch("/admin/users")
? db
.select({
id: User.id,
title: User.username,
subtitle: User.mail,
})
.from(User)
.where(
or(
like(User.username, pattern),
like(User.mail, pattern),
...(hasId ? [eq(User.id, idExact)] : []),
),
)
.limit(PER_TYPE)
: Promise.resolve([]),
canSearch("/admin/articles")
? db
.select({
id: WebsiteArticles.id,
title: WebsiteArticles.title,
subtitle: WebsiteArticles.slug,
})
.from(WebsiteArticles)
.where(
or(
like(WebsiteArticles.title, pattern),
like(WebsiteArticles.slug, pattern),
),
)
.limit(PER_TYPE)
: Promise.resolve([]),
canSearch("/admin/rooms")
? db
.select({
id: Rooms.id,
title: Rooms.name,
subtitle: Rooms.ownerName,
})
.from(Rooms)
.where(
or(
like(Rooms.name, pattern),
...(hasId ? [eq(Rooms.id, idExact)] : []),
),
)
.limit(PER_TYPE)
: Promise.resolve([]),
canSearch("/admin/guilds")
? db
.select({
id: Guilds.id,
title: Guilds.name,
subtitle: Guilds.description,
})
.from(Guilds)
.where(
or(
like(Guilds.name, pattern),
...(hasId ? [eq(Guilds.id, idExact)] : []),
),
)
.limit(PER_TYPE)
: Promise.resolve([]),
canSearch("/admin/shop")
? db
.select({
id: WebsiteShopArticles.id,
title: WebsiteShopArticles.name,
subtitle: WebsiteShopArticles.info,
})
.from(WebsiteShopArticles)
.where(
or(
like(WebsiteShopArticles.name, pattern),
...(hasId
? [eq(WebsiteShopArticles.id, BigInt(idExact))]
: []),
),
)
.limit(PER_TYPE)
: Promise.resolve([]),
canSearch("/admin/rare-values")
? db
.select({
id: WebsiteRareValues.id,
title: WebsiteRareValues.name,
subtitle: WebsiteRareValues.itemId,
})
.from(WebsiteRareValues)
.where(
or(
like(WebsiteRareValues.name, pattern),
...(hasId ? [eq(WebsiteRareValues.itemId, idExact)] : []),
),
)
.limit(PER_TYPE)
: Promise.resolve([]),
]);
const hasId =
Number.isSafeInteger(idExact) && idExact > 0 && String(idExact) === q;
let partial = false;
const [
users,
articles,
rooms,
guilds,
shopArticles,
rareValues,
furniture,
catalog,
catalogBc,
tickets,
helpTickets,
] = await Promise.all([
canSearch("/admin/users")
? db
.select({
id: User.id,
title: User.username,
})
.from(User)
.where(
or(
like(User.username, pattern),
...(hasId ? [eq(User.id, idExact)] : []),
),
)
.orderBy(asc(User.id))
.limit(PER_TYPE)
.catch(() => {
partial = true;
return [];
})
: Promise.resolve([]),
canSearch("/admin/articles")
? db
.select({
id: WebsiteArticles.id,
title: WebsiteArticles.title,
subtitle: WebsiteArticles.slug,
})
.from(WebsiteArticles)
.where(
or(
like(WebsiteArticles.title, pattern),
like(WebsiteArticles.slug, pattern),
),
)
.orderBy(asc(WebsiteArticles.id))
.limit(PER_TYPE)
.catch(() => {
partial = true;
return [];
})
: Promise.resolve([]),
canSearch("/admin/rooms")
? db
.select({
id: Rooms.id,
title: Rooms.name,
subtitle: Rooms.ownerName,
})
.from(Rooms)
.where(
or(
like(Rooms.name, pattern),
...(hasId ? [eq(Rooms.id, idExact)] : []),
),
)
.orderBy(asc(Rooms.id))
.limit(PER_TYPE)
.catch(() => {
partial = true;
return [];
})
: Promise.resolve([]),
canSearch("/admin/guilds")
? db
.select({
id: Guilds.id,
title: Guilds.name,
subtitle: Guilds.description,
})
.from(Guilds)
.where(
or(
like(Guilds.name, pattern),
...(hasId ? [eq(Guilds.id, idExact)] : []),
),
)
.orderBy(asc(Guilds.id))
.limit(PER_TYPE)
.catch(() => {
partial = true;
return [];
})
: Promise.resolve([]),
canSearch("/admin/shop")
? db
.select({
id: WebsiteShopArticles.id,
title: WebsiteShopArticles.name,
subtitle: WebsiteShopArticles.info,
})
.from(WebsiteShopArticles)
.where(
or(
like(WebsiteShopArticles.name, pattern),
...(hasId ? [eq(WebsiteShopArticles.id, BigInt(idExact))] : []),
),
)
.orderBy(asc(WebsiteShopArticles.id))
.limit(PER_TYPE)
.catch(() => {
partial = true;
return [];
})
: Promise.resolve([]),
canSearch("/admin/rare-values")
? db
.select({
id: WebsiteRareValues.id,
title: WebsiteRareValues.name,
subtitle: WebsiteRareValues.itemId,
})
.from(WebsiteRareValues)
.where(
or(
like(WebsiteRareValues.name, pattern),
...(hasId ? [eq(WebsiteRareValues.itemId, idExact)] : []),
),
)
.orderBy(asc(WebsiteRareValues.id))
.limit(PER_TYPE)
.catch(() => {
partial = true;
return [];
})
: Promise.resolve([]),
canAccess(
context.permissions,
PERMS.CATALOG_VIEW,
context.session.user.rank,
)
? db
.select({
id: ItemsBase.id,
title: ItemsBase.publicName,
subtitle: ItemsBase.itemName,
})
.from(ItemsBase)
.where(
or(
like(ItemsBase.publicName, pattern),
like(ItemsBase.itemName, pattern),
...(hasId ? [eq(ItemsBase.id, idExact)] : []),
),
)
.orderBy(asc(ItemsBase.id))
.limit(PER_TYPE)
.catch(() => {
partial = true;
return [];
})
: Promise.resolve([]),
canAccess(
context.permissions,
PERMS.CATALOG_VIEW,
context.session.user.rank,
)
? db
.select({ id: CatalogPages.id, title: CatalogPages.caption })
.from(CatalogPages)
.where(
or(
like(CatalogPages.caption, pattern),
...(hasId ? [eq(CatalogPages.id, idExact)] : []),
),
)
.orderBy(asc(CatalogPages.id))
.limit(PER_TYPE)
.catch(() => {
partial = true;
return [];
})
: Promise.resolve([]),
canAccess(
context.permissions,
PERMS.CATALOG_VIEW,
context.session.user.rank,
)
? db
.select({ id: CatalogPagesBc.id, title: CatalogPagesBc.caption })
.from(CatalogPagesBc)
.where(
or(
like(CatalogPagesBc.caption, pattern),
...(hasId ? [eq(CatalogPagesBc.id, idExact)] : []),
),
)
.orderBy(asc(CatalogPagesBc.id))
.limit(PER_TYPE)
.catch(() => {
partial = true;
return [];
})
: Promise.resolve([]),
canAccess(
context.permissions,
PERMS.TICKETS_VIEW,
context.session.user.rank,
)
? db
.select({ id: WebsiteTicket.id, title: WebsiteTicket.subject })
.from(WebsiteTicket)
.where(
or(
like(WebsiteTicket.subject, pattern),
...(hasId ? [eq(WebsiteTicket.id, idExact)] : []),
),
)
.orderBy(asc(WebsiteTicket.id))
.limit(PER_TYPE)
.catch(() => {
partial = true;
return [];
})
: Promise.resolve([]),
canAccess(
context.permissions,
PERMS.TICKETS_VIEW,
context.session.user.rank,
)
? db
.select({
id: WebsiteHelpCenterTickets.id,
title: WebsiteHelpCenterTickets.title,
})
.from(WebsiteHelpCenterTickets)
.where(
or(
like(WebsiteHelpCenterTickets.title, pattern),
...(hasId
? [eq(WebsiteHelpCenterTickets.id, BigInt(idExact))]
: []),
),
)
.orderBy(asc(WebsiteHelpCenterTickets.id))
.limit(PER_TYPE)
.catch(() => {
partial = true;
return [];
})
: Promise.resolve([]),
]);
const groupMap: Record<
string,
{ type: string; items: Array<AdminSearchResult> }
> = {
furniture: { type: "furniture", items: [] },
catalog: { type: "catalog", items: [] },
tickets: { type: "tickets", items: [] },
users: { type: "users", items: [] },
articles: { type: "articles", items: [] },
rooms: { type: "rooms", items: [] },
@@ -191,13 +339,12 @@ export const GET = withAdmin(
shop: { type: "shop", items: [] },
rareValues: { type: "rareValues", items: [] },
};
for (const r of users) {
groupMap.users.items.push({
type: "users",
id: r.id,
title: r.title,
subtitle: r.subtitle || "",
subtitle: `#${r.id}`,
url: `/admin/users/show/${r.id}`,
});
}
@@ -246,10 +393,53 @@ export const GET = withAdmin(
url: `/admin/rare-values/${r.id}`,
});
}
for (const r of furniture) {
groupMap.furniture.items.push({
type: "furniture",
id: r.id,
title: r.title || r.subtitle || `#${r.id}`,
subtitle: `${r.subtitle || ""} · #${r.id}`,
url: `/admin/items/${r.id}`,
});
}
for (const [rows, kind] of [
[catalog, "normal"],
[catalogBc, "bc"],
] as const) {
for (const r of rows)
groupMap.catalog.items.push({
type: "catalog",
id: `${kind}-${r.id}`,
title: r.title || `#${r.id}`,
subtitle: kind === "bc" ? `BC · #${r.id}` : `#${r.id}`,
url:
kind === "bc"
? `/admin/catalog/builder-club/${r.id}`
: `/admin/catalog/${r.id}`,
});
}
for (const r of tickets)
groupMap.tickets.items.push({
type: "tickets",
id: `cms-${r.id}`,
title: r.title,
subtitle: `#${r.id}`,
url: `/admin/tickets/${r.id}`,
});
for (const r of helpTickets)
groupMap.tickets.items.push({
type: "tickets",
id: `help-${r.id}`,
title: r.title,
subtitle: `#${r.id}`,
url: `/admin/help-tickets/${r.id}`,
});
const results: AdminSearchResult[] = [];
const order = [
"users",
"furniture",
"catalog",
"tickets",
"articles",
"rooms",
"guilds",
@@ -268,7 +458,6 @@ export const GET = withAdmin(
}
if (!added) break;
}
return apiOk({ results });
return apiOk({ results, partial });
},
);
+74 -26
View File
@@ -18,18 +18,21 @@ import {
DialogContent,
DialogDescription,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { useDebounce } from "@/hooks/use-debounce";
import {
type AdminNavigationOptions,
visibleAdminNavigation,
} from "@/lib/admin-navigation";
import { AdminSearchRequest } from "@/lib/admin-search-request";
type SearchResult = AdminSearchResult;
type SearchResponse = {
ok: boolean;
results: SearchResult[];
partial?: boolean;
};
export function SearchDialog({
@@ -56,8 +59,9 @@ export function SearchDialog({
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
const [failed, setFailed] = useState(false);
const [partial, setPartial] = useState(false);
const debouncedQuery = useDebounce(query, 250);
const abortRef = useRef<AbortController | null>(null);
const requestRef = useRef(new AdminSearchRequest());
const groupLabels: Record<string, string> = {
users: t("users"),
@@ -66,19 +70,22 @@ export function SearchDialog({
guilds: t("guilds"),
shop: t("shop"),
rareValues: t("rareValues"),
furniture: t("furniture"),
catalog: t("catalog"),
tickets: t("tickets"),
};
const fetchResults = useCallback(async (q: string) => {
abortRef.current?.abort();
requestRef.current.cancel();
setFailed(false);
setPartial(false);
setResults([]);
if (q.length < 2) {
if (!canSearchRecords(q)) {
setResults([]);
setLoading(false);
return;
}
const controller = new AbortController();
abortRef.current = controller;
const controller = requestRef.current.start();
setLoading(true);
try {
const res = await fetch(`/api/admin/search?q=${encodeURIComponent(q)}`, {
@@ -86,28 +93,30 @@ export function SearchDialog({
});
if (!res.ok) throw new Error("Search request failed");
const data: SearchResponse = await res.json();
if (abortRef.current !== controller || controller.signal.aborted) return;
if (data.ok) setResults(data.results);
else setFailed(true);
if (!requestRef.current.accepts(controller)) return;
if (data.ok) {
setResults(data.results);
setPartial(Boolean(data.partial));
} else setFailed(true);
} catch (error) {
if (error instanceof DOMException && error.name === "AbortError") return;
if (abortRef.current === controller) {
if (requestRef.current.accepts(controller)) {
setResults([]);
setFailed(true);
}
} finally {
if (abortRef.current === controller) setLoading(false);
if (requestRef.current.accepts(controller)) setLoading(false);
}
}, []);
useEffect(() => {
if (!open) return;
if (!open || query !== debouncedQuery) return;
fetchResults(debouncedQuery);
return () => abortRef.current?.abort();
}, [debouncedQuery, fetchResults, open]);
return () => requestRef.current.cancel();
}, [debouncedQuery, fetchResults, open, query]);
useEffect(() => {
return () => abortRef.current?.abort();
return () => requestRef.current.cancel();
}, []);
useEffect(() => {
@@ -121,8 +130,29 @@ export function SearchDialog({
return () => document.removeEventListener("keydown", handleKeyDown);
}, []);
function handleQueryChange(value: string) {
requestRef.current.cancel();
setQuery(value.slice(0, 64));
setResults([]);
setFailed(false);
setPartial(false);
setLoading(canSearchRecords(value));
}
function handleOpenChange(next: boolean) {
if (!next) {
requestRef.current.cancel();
setQuery("");
setResults([]);
setFailed(false);
setPartial(false);
setLoading(false);
}
setOpen(next);
}
function handleSelect(url: string) {
setOpen(false);
handleOpenChange(false);
setQuery("");
setResults([]);
router.push(url);
@@ -132,19 +162,18 @@ export function SearchDialog({
const pages = getPages(query.trim().toLocaleLowerCase()).slice(0, 8);
return (
<Dialog open={open} onOpenChange={setOpen}>
<button
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogTrigger
type="button"
onClick={() => setOpen(true)}
aria-label={t("placeholder")}
className="flex items-center gap-2 rounded-lg border border-[var(--admin-border)] bg-[var(--admin-surface)] px-3 py-1.5 text-xs text-[var(--admin-text-muted)] hover:border-[var(--admin-accent)]/40 hover:text-[var(--admin-text)] transition-colors cursor-pointer"
>
<SearchIcon size={14} />
<span className="hidden sm:inline">{t("placeholder")}</span>
<kbd className="pointer-events-none hidden sm:inline-flex h-5 select-none items-center gap-1 rounded border bg-muted px-1.5 font-mono text-[10px] font-medium opacity-60">
⌘K
Ctrl/⌘ K
</kbd>
</button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg p-0 gap-0 overflow-hidden">
<DialogTitle className="sr-only">{t("placeholder")}</DialogTitle>
<DialogDescription className="sr-only">
@@ -157,21 +186,33 @@ export function SearchDialog({
<CommandInput
placeholder={t("placeholder")}
value={query}
onValueChange={setQuery}
onValueChange={handleQueryChange}
aria-label={t("placeholder")}
maxLength={64}
className="h-12"
/>
<CommandList className="max-h-[400px]">
{loading && query.length >= 2 ? (
{loading && canSearchRecords(query) ? (
<div className="py-6 text-center text-sm text-muted-foreground">
…
</div>
) : null}
{!loading && !failed && (
{!loading && !failed && !partial && canSearchRecords(query) && (
<CommandEmpty>{t("noResults")}</CommandEmpty>
)}
{failed && (
{!canSearchRecords(query) && (
<p className="p-4 text-sm text-muted-foreground">{t("hint")}</p>
)}
{(failed || partial) && (
<p role="alert" className="p-4 text-sm">
{t("loadError")}
{t(partial ? "partialError" : "loadError")}
<button
type="button"
onClick={() => fetchResults(query)}
className="ml-2 underline"
>
{t("retry")}
</button>
</p>
)}
{pages.length > 0 && (
@@ -200,7 +241,9 @@ export function SearchDialog({
onSelect={() => handleSelect(item.url)}
className="flex flex-col items-start gap-0.5 py-2"
>
<span className="text-sm font-medium">{item.title}</span>
<span className="w-full min-w-0 truncate text-sm font-medium">
{item.title}
</span>
{item.subtitle ? (
<span className="text-xs text-muted-foreground truncate w-full">
{item.subtitle}
@@ -217,6 +260,11 @@ export function SearchDialog({
);
}
function canSearchRecords(value: string) {
const q = value.trim();
return q.length >= 2 || /^[1-9]$/.test(q);
}
function groupResults(results: SearchResult[]) {
const grouped: Record<string, SearchResult[]> = {};
for (const r of results) {
+3
View File
@@ -54,6 +54,9 @@ vi.mock("@/lib/db", () => {
where() {
return query;
},
orderBy() {
return query;
},
async limit() {
state.queried.push(table);
return [{ id: 1, title: "Match", subtitle: "Data" }];
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { AdminSearchRequest } from "./admin-search-request";
describe("admin search request ownership", () => {
it("rejects a late response after the input changes, even before the next debounce fires", async () => {
const requests = new AdminSearchRequest();
const first = requests.start();
const late = Promise.resolve().then(() => requests.accepts(first));
requests.cancel();
expect(await late).toBe(false);
expect(first.signal.aborted).toBe(true);
});
it("accepts only the newest request and rejects completion after closing", () => {
const requests = new AdminSearchRequest();
const first = requests.start();
const second = requests.start();
expect(requests.accepts(first)).toBe(false);
expect(requests.accepts(second)).toBe(true);
requests.cancel();
expect(requests.accepts(second)).toBe(false);
});
});
+20
View File
@@ -0,0 +1,20 @@
/** Owns the single request allowed to update a search dialog. */
export class AdminSearchRequest {
private current: AbortController | null = null;
cancel() {
this.current?.abort();
this.current = null;
}
start() {
this.cancel();
const controller = new AbortController();
this.current = controller;
return controller;
}
accepts(controller: AbortController) {
return this.current === controller && !controller.signal.aborted;
}
}
+6 -4
View File
@@ -36,16 +36,18 @@ describe("staff smoke contract", () => {
expect(detail).toContain("canLiftBan");
});
it("exposes sanction timeline on user show", () => {
it("exposes operational overview on user show", () => {
expect(
existsSync("src/app/admin/users/_components/user-sanction-timeline.tsx"),
existsSync(
"src/app/admin/users/_components/user-operational-overview.tsx",
),
).toBe(true);
expect(existsSync("src/app/admin/users/_lib/load-user-sanctions.ts")).toBe(
expect(existsSync("src/app/admin/users/_lib/load-user-overview.ts")).toBe(
true,
);
expect(
readFileSync("src/app/admin/users/show/[id]/page.tsx", "utf8"),
).toContain("UserSanctionTimeline");
).toContain("UserOperationalOverview");
});
it("applies CMS SQL from drizzle/migrations (not Prisma)", () => {
+90
View File
@@ -281,6 +281,96 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"users": {
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"myDashboard": {
+88
View File
@@ -1232,6 +1232,84 @@
"save": "Запазете промените",
"saving": "Запазва се...",
"cancel": "Отказ"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3237,6 +3315,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+88
View File
@@ -1232,6 +1232,84 @@
"save": "Uložte změny",
"saving": "Ukládání…",
"cancel": "Zrušit"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3237,6 +3315,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+88
View File
@@ -1232,6 +1232,84 @@
"save": "Save changes",
"saving": "Gemmer...",
"cancel": "Annuller"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3237,6 +3315,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+88
View File
@@ -1203,6 +1203,84 @@
"save": "Änderungen speichern",
"saving": "Wird gespeichert…",
"cancel": "Abbrechen"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3208,6 +3286,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"radioRequests": {
+88
View File
@@ -1232,6 +1232,84 @@
"save": "Αποθήκευση αλλαγών",
"saving": "Αποθήκευση…",
"cancel": "Ακύρωση"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3237,6 +3315,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+87 -2
View File
@@ -1566,6 +1566,84 @@
"save": "Save changes",
"saving": "Saving…",
"cancel": "Cancel"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3815,7 +3893,7 @@
},
"search": {
"pages": "Pages and tools",
"description": "Find an admin page or search hotel records.",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"loadError": "Records could not be loaded. You can still open the pages above.",
"placeholder": "Search pages, users, rooms…",
"users": "Users",
@@ -3824,7 +3902,14 @@
"guilds": "Groups",
"shop": "Shop",
"rareValues": "Rare values",
"noResults": "No results found."
"noResults": "No results found.",
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"partialError": "Some results could not be loaded. Try again."
},
"table": {
"search": "Search…",
+88
View File
@@ -1203,6 +1203,84 @@
"save": "Guardar cambios",
"saving": "Guardando…",
"cancel": "Cancelar"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3208,6 +3286,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"radioRequests": {
+90
View File
@@ -281,6 +281,96 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"users": {
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"myDashboard": {
+88
View File
@@ -1203,6 +1203,84 @@
"save": "Enregistrer les modifications",
"saving": "Enregistrement…",
"cancel": "Annuler"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3208,6 +3286,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"radioRequests": {
+88
View File
@@ -1232,6 +1232,84 @@
"save": "Spremite promjene",
"saving": "Spremanje...",
"cancel": "Odustani"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3237,6 +3315,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+88
View File
@@ -1232,6 +1232,84 @@
"save": "Módosítások mentése",
"saving": "Mentés…",
"cancel": "Mégse"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3237,6 +3315,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+129 -44
View File
@@ -21,8 +21,8 @@
"friends": "Amici",
"messages": "Messaggi",
"admin": "Admin",
"openMenu": "Open menu",
"closeMenu": "Close menu",
"openMenu": "Apri menu",
"closeMenu": "Chiudi menu",
"drawBadge": "Crea distintivo",
"mod": "Mod"
},
@@ -991,7 +991,7 @@
"economy": "Economia",
"catalog": "Catalogo",
"shop": "Negozio",
"marketplace": "Marketplace",
"marketplace": "Mercatino",
"transactions": "Transazioni",
"vouchers": "Buoni",
"subscriptions": "Abbonamenti",
@@ -1042,12 +1042,12 @@
"hubNewsMedia": "Notizie e media",
"hubSiteContent": "Contenuti sito",
"hubEngagement": "Community",
"hubUsers": "Users",
"hubAccess": "Access & security",
"hubCatalog": "Catalog & rares",
"hubShop": "Shop & payments",
"hubTools": "Tools",
"hubObservability": "Observability",
"hubUsers": "Utenti",
"hubAccess": "Accesso e sicurezza",
"hubCatalog": "Catalogo e rari",
"hubShop": "Negozio e pagamenti",
"hubTools": "Strumenti",
"hubObservability": "Monitoraggio e diagnostica",
"community": "Community",
"monitoring": "Monitoraggio",
"staffAccess": "Gradi e permessi",
@@ -1057,7 +1057,7 @@
"tradeLog": "Log trade",
"tools": "Strumenti",
"guilds": "Gruppi",
"itemsBase": "Items base",
"itemsBase": "Definizioni furni",
"studio": "Studio",
"peopleSupport": "Utenti e assistenza",
"catalogFurniture": "Catalogo e furni",
@@ -1519,11 +1519,11 @@
"resetPassword": "Reimposta password",
"deleteUser": "Elimina utente",
"confirmDelete": "Sei sicuro di voler eliminare {username}? Questa azione è irreversibile.",
"tabInfo": "Account Info",
"tabBadges": "Badges ({count})",
"tabRooms": "Rooms ({count})",
"tabSanctions": "Sanctions ({count})",
"tabActivity": "Activity"
"tabInfo": "Informazioni account",
"tabBadges": "Distintivi ({count})",
"tabRooms": "Stanze ({count})",
"tabSanctions": "Sanzioni ({count})",
"tabActivity": "Attività"
},
"editForm": {
"title": "Modifica utente: {username}",
@@ -1541,6 +1541,84 @@
"save": "Salva modifiche",
"saving": "Salvataggio…",
"cancel": "Annulla"
},
"presentation": {
"users": "Utenti",
"edit": "Modifica",
"view": "Visualizza",
"rank": "Grado {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Bannato",
"credits": "crediti",
"diamonds": "diamanti",
"duckets": "duckets",
"pixels": "pixel",
"rooms": "Stanze",
"messages": "Messaggi",
"trades": "Scambi",
"accountDetails": "Dettagli account",
"registered": "Registrazione",
"lastLogin": "Ultimo accesso",
"ipRegister": "IP di registrazione",
"ipCurrent": "IP attuale",
"machineId": "ID dispositivo",
"email": "Email",
"gender": "Genere",
"never": "Mai",
"findIp": "Cerca utenti con questo IP",
"enabled": "Attiva",
"disabled": "Disattiva",
"unavailable": "Non disponibile",
"noBadges": "Nessun distintivo assegnato"
},
"create": {
"newUser": "Nuovo utente",
"title": "Crea utente",
"description": "Crea subito l’account con un avatar predefinito e 5.000 crediti, pixel, duckets e diamanti.",
"username": "Nome utente",
"email": "Email",
"password": "Password",
"rank": "Grado",
"rankOption": "Grado {rank}",
"motto": "Motto (facoltativo)",
"usernamePlaceholder": "NuovoHabbo",
"passwordPlaceholder": "Almeno 6 caratteri",
"mottoPlaceholder": "Sono nuovo qui!",
"generate": "Genera",
"passwordGenerated": "Password generata nel campo password.",
"required": "Nome utente, email e password sono obbligatori.",
"invalidUsername": "Usa da 3 a 20 caratteri: lettere, numeri, _ o -.",
"passwordLength": "La password deve contenere almeno 6 caratteri.",
"created": "Utente {username} creato."
},
"overview": {
"title": "Riepilogo",
"hint": "Fino a 5 record recenti per fonte. Sono mostrate solo le sezioni a cui hai accesso.",
"restricted": "Non hai i permessi per visualizzare i dati operativi.",
"sanctions": "Ban recenti",
"mute": "Mute attivo",
"tickets": "Ticket di assistenza",
"help": "Richieste al centro assistenza",
"reports": "Segnalazioni in gioco",
"transactions": "Pagamenti",
"purchases": "Acquisti dal catalogo",
"activity": "Azioni dello staff registrate",
"openModule": "Apri gestione",
"purchaseHint": "Quantità e costi nelle valute registrate.",
"purchaseDetail": "Quantità: {amount} · Crediti: {credits} · Punti: {points} (tipo {type})",
"activityHint": "Voci del registro staff relative a questo account. Non è la cronologia completa delle attività del giocatore.",
"muteUntil": "Mute fino al {date}",
"error": "Impossibile caricare questa fonte. Ricarica la pagina per riprovare.",
"empty": "Nessun record trovato.",
"status": {
"active": "Attivo",
"expired": "Scaduto",
"open": "Aperto",
"closed": "Chiuso",
"reported": "Utente segnalato",
"reporter": "Segnalante"
}
}
},
"bans": {
@@ -1686,19 +1764,19 @@
"actionsTitle": "Azioni",
"requesterTitle": "Richiedente",
"detailsTitle": "Dettagli",
"rankLabel": "Rank {rank}",
"rankLabel": "Grado {rank}",
"messageCountLabel": "Messaggi",
"success": {
"replied": "Risposta inviata.",
"closed": "Ticket chiuso.",
"reopened": "Ticket riaperto.",
"banLifted": "Ban lifted and ticket closed."
"banLifted": "Ban revocato e ticket chiuso."
},
"queueHint": "Questa coda e il help center pubblico. I ticket del supporto CMS stanno nella tab Supporto CMS.",
"queueOtherCta": "Apri supporto CMS",
"queueCounts": "Aperti qui: {here} · Supporto CMS: {other}",
"liftBanSubmit": "Lift requester ban & close",
"requesterBanned": "Requester banned"
"liftBanSubmit": "Revoca il ban del richiedente e chiudi",
"requesterBanned": "Richiedente bannato"
},
"cfh": {
"title": "Call for Help",
@@ -3383,11 +3461,11 @@
"urlCopied": "URL copiato negli appunti"
},
"hubs": {
"newsMedia": "News & media",
"newsMedia": "Notizie e contenuti multimediali",
"newsMediaSubtitle": "Articoli, foto, banner, ads e media library",
"siteContent": "Contenuti sito",
"siteContentSubtitle": "Navigazione, help, box, tag e prefissi",
"engagement": "Engagement",
"engagement": "Attività della community",
"engagementSubtitle": "Eventi, sondaggi e ticket di supporto",
"users": "Utenti",
"usersSubtitle": "Directory, online, candidature e team",
@@ -3411,10 +3489,10 @@
"articles": "Articoli",
"photos": "Foto",
"banners": "Banner",
"ads": "Ads",
"ads": "Pubblicità",
"media": "Media",
"navigator": "Navigator",
"help": "Help",
"navigator": "Navigatore",
"help": "Assistenza",
"boxes": "Box",
"tags": "Tag",
"prefixes": "Prefissi",
@@ -3422,9 +3500,9 @@
"eventTypes": "Tipi evento",
"polls": "Sondaggi",
"tickets": "Supporto CMS",
"helpTickets": "Help center",
"helpTickets": "Centro assistenza",
"templates": "Template",
"directory": "Directory",
"directory": "Elenco utenti",
"multiAccounts": "Multi-account",
"online": "Online",
"applications": "Candidature",
@@ -3433,26 +3511,26 @@
"ip": "IP",
"vpn": "VPN",
"permissions": "Permessi",
"wordFilter": "Word filter",
"wordFilter": "Filtro parole",
"overview": "Panoramica",
"actions": "Azioni",
"cfh": "CFH",
"cfh": "Richieste di moderazione",
"team": "Team",
"catalogTab": "Catalogo",
"rares": "Rare",
"badges": "Badge",
"achievements": "Achievement",
"sounds": "Suoni",
"shopTab": "Shop",
"shopTab": "Negozio",
"transactions": "Transazioni",
"vouchers": "Voucher",
"subscriptions": "Abbonamenti",
"calendar": "Calendario",
"history": "Cronologia",
"moderationTab": "Moderazione",
"monitoring": "Monitoring",
"monitoring": "Monitoraggio",
"settingsTab": "Impostazioni",
"embed": "Embed",
"embed": "Incorporamento",
"apiKeys": "API key",
"points": "Punti",
"ranks": "Rank",
@@ -3461,37 +3539,37 @@
"theme": "Tema",
"maintenance": "Manutenzione",
"favicon": "Favicon",
"emulator": "Emulator",
"emulator": "Emulatore",
"email": "Email",
"housekeeping": "Archivio permessi",
"commando": "Commando",
"commando": "Centro operativo",
"rooms": "Stanze",
"import": "Import",
"import": "Importazione",
"importOverview": "Panoramica",
"importBadges": "Badge",
"importFurni": "Furni",
"importUpload": "Upload",
"importUpload": "Caricamento",
"importClothing": "Abbigliamento",
"importEffects": "Effetti",
"importPets": "Pet",
"importClone": "Clone",
"importSync": "Sync",
"importClone": "Clonazione",
"importSync": "Sincronizzazione",
"importRepairIcons": "Ripara icone",
"importAudit": "Audit",
"importAudit": "Verifica dati",
"translations": "Traduzioni",
"logs": "Log",
"audit": "Audit",
"audit": "Registro modifiche",
"chat": "Chat",
"commands": "Comandi",
"trades": "Trade",
"analytics": "Analytics",
"analytics": "Statistiche",
"activity": "Attività",
"economy": "Economia",
"devops": "DevOps",
"errors": "Errori",
"alerts": "Alert",
"toolsGroup": "Strumenti",
"ticketInbox": "Inbox",
"ticketInbox": "Ticket ricevuti",
"cmsErrors": "Errori CMS",
"themeBuilder": "Editor del tema",
"studio": "Studio"
@@ -3506,7 +3584,7 @@
"analyticsSubtitle": "Attività hotel, retention ed economia",
"devops": "DevOps",
"devopsSubtitle": "Salute servizi ed errori emulator",
"import": "Import",
"import": "Importazione",
"importSubtitle": "Furni, badge, abbigliamento e strumenti asset"
},
"settings": {
@@ -3790,7 +3868,7 @@
},
"search": {
"pages": "Pagine e strumenti",
"description": "Trova una pagina di gestione o cerca nei dati dell’hotel.",
"description": "Cerca pagine, utenti, furni, categorie del catalogo, articoli e ticket di assistenza.",
"loadError": "Impossibile caricare i risultati. Puoi comunque aprire le pagine elencate.",
"placeholder": "Cerca pagine, utenti, stanze…",
"users": "Utenti",
@@ -3799,7 +3877,14 @@
"guilds": "Gruppi",
"shop": "Negozio",
"rareValues": "Valori dei rari",
"noResults": "Nessun risultato trovato."
"noResults": "Nessun risultato trovato.",
"furniture": "Furni",
"catalog": "Categorie del catalogo",
"tickets": "Ticket di assistenza",
"loading": "Ricerca in corso…",
"hint": "Inserisci un ID numerico o almeno 2 caratteri oppure scegli una pagina.",
"retry": "Riprova",
"partialError": "Alcuni risultati non sono disponibili. Riprova."
},
"table": {
"search": "Cerca…",
+90
View File
@@ -281,6 +281,96 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"users": {
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"myDashboard": {
+185 -60
View File
@@ -5,7 +5,7 @@
"articles": "Artikelen",
"photos": "Foto's",
"staff": "Team",
"rankings": "Rankings",
"rankings": "Ranglijsten",
"guilds": "Gildes",
"events": "Evenementen",
"polls": "Peilingen",
@@ -20,7 +20,7 @@
"radio": "Radio",
"friends": "Vrienden",
"messages": "Berichten",
"admin": "Admin",
"admin": "Beheer",
"openMenu": "Menu openen",
"closeMenu": "Menu sluiten",
"drawBadge": "Badge tekenen",
@@ -987,26 +987,26 @@
"photos": "Foto's",
"tickets": "Supportbalie",
"helpTickets": "Helpcentrum-tickets",
"events": "Events",
"events": "Evenementen",
"polls": "Polls",
"guilds": "Gildes",
"banners": "Banners",
"prefixes": "Prefixes",
"prefixes": "Voorvoegsels",
"usersAndAccess": "Gebruikers & toegang",
"users": "Gebruikers",
"bans": "Bans",
"bans": "Verbanningen",
"ipManagement": "IP-beheer",
"vpn": "VPN",
"applications": "Sollicitaties",
"teams": "Teams",
"permissions": "Permissies",
"onlineUsers": "Online users",
"moderation": "Moderation",
"onlineUsers": "Online gebruikers",
"moderation": "Moderatie",
"economy": "Economie",
"catalog": "Catalogus",
"catalogMaintenance": "Catalogus-onderhoud",
"shop": "Winkel",
"marketplace": "Marketplace",
"marketplace": "Marktplaats",
"transactions": "Transacties",
"vouchers": "Vouchers",
"subscriptions": "Abonnementen",
@@ -1017,11 +1017,11 @@
"radio": "Radio",
"radioHistory": "Radio geschiedenis",
"radioBanners": "Banners",
"radioEmbed": "Embed",
"radioEmbed": "Insluiten",
"radioApiKeys": "API-sleutels",
"radioModeration": "Moderatie",
"radioSettings": "Instellingen",
"radioMonitoring": "Monitoring",
"radioMonitoring": "Bewaking",
"radioPoints": "Punten",
"radioRanks": "Rangen",
"radioAutodj": "Auto DJ",
@@ -1030,8 +1030,8 @@
"emulator": "Emulator",
"alerts": "Meldingen",
"logs": "Logboeken",
"auditLog": "Audit log",
"analytics": "Analytics",
"auditLog": "Wijzigingslogboek",
"analytics": "Statistieken",
"devops": "DevOps",
"wordFilter": "Woordfilter",
"maintenance": "Onderhoud",
@@ -1053,26 +1053,44 @@
"housekeeping": "Permissie-archief",
"hotelManagementPanel": "Hotelbeheerpaneel",
"rankLabel": "Rang {rank}",
"hubNewsMedia": "News & media",
"hubSiteContent": "Site content",
"hubEngagement": "Engagement",
"hubUsers": "Users",
"hubAccess": "Access & security",
"hubCatalog": "Catalog & rares",
"hubShop": "Shop & payments",
"hubTools": "Tools",
"hubObservability": "Observability",
"hubNewsMedia": "Nieuws en media",
"hubSiteContent": "Website-inhoud",
"hubEngagement": "Communityactiviteiten",
"hubUsers": "Gebruikers",
"hubAccess": "Toegang en beveiliging",
"hubCatalog": "Catalogus en zeldzame meubels",
"hubShop": "Winkel en betalingen",
"hubTools": "Hulpmiddelen",
"hubObservability": "Bewaking en diagnostiek",
"community": "Community",
"monitoring": "Monitoring",
"monitoring": "Bewaking",
"staffAccess": "Rangen & rechten",
"multiAccounts": "Multi-accounts",
"multiAccounts": "Gekoppelde accounts",
"chatLog": "Chatlog",
"commandLog": "Commandolog",
"tradeLog": "Tradelog",
"tools": "Tools",
"itemsBase": "Items base",
"tools": "Hulpmiddelen",
"itemsBase": "Meubeldefinities",
"adminMenu": "Adminmenu",
"studio": "Studio"
"studio": "Studio",
"peopleSupport": "Gebruikers en ondersteuning",
"catalogFurniture": "Catalogus en meubels",
"communityContent": "Community en inhoud",
"staffPermissions": "Medewerkers en toegang",
"cmsErrors": "CMS-fouten",
"themeBuilder": "Thema-editor",
"openSidebar": "Menu openen",
"closeSidebar": "Menu sluiten",
"search": {
"placeholder": "Zoeken…",
"noResults": "Geen resultaten gevonden",
"users": "Gebruikers",
"articles": "Artikelen",
"rooms": "Kamers",
"guilds": "Groepen",
"shop": "Winkel",
"hint": "Typ om te zoeken naar gebruikers, artikelen, kamers, groepen en winkelartikelen."
}
},
"dashboard": {
"title": "Dashboard",
@@ -1153,7 +1171,12 @@
"restoreRevision": "Herstellen in editor",
"confirmRevision": "De huidige inhoud vervangen door deze vorige versie als concept? Controleer de inhoud en kies een publicatiestatus voordat je opslaat.",
"editConflict": "Dit artikel is gewijzigd sinds je het hebt geopend. Je wijzigingen zijn niet toegepast. Kopieer je werk, herlaad het artikel en voeg de wijzigingen samen voordat je opnieuw opslaat.",
"retryRecovery": "Herstelgegevens opnieuw laden zonder dit formulier te wijzigen"
"retryRecovery": "Herstelgegevens opnieuw laden zonder dit formulier te wijzigen",
"statusDraft": "Concept",
"statusScheduled": "Ingepland",
"statusPublished": "Gepubliceerd",
"publishAt": "Publiceren op",
"publishedAt": "Gepubliceerd op"
},
"listActionError": "De actie voor dit artikel kon niet worden voltooid.",
"listStatus": "Publicatiestatus",
@@ -1172,7 +1195,11 @@
"listPagination": "Artikelpagina’s",
"listPrevious": "Vorige",
"listNext": "Volgende",
"listPage": "Pagina {page} van {total}"
"listPage": "Pagina {page} van {total}",
"articlesShown": "Getoonde artikelen",
"latest": "Laatste artikel",
"mostRecentPost": "Meest recente publicatie",
"recentArticles": "Recente artikelen"
},
"events": {
"newEvent": "+ Nieuw evenement",
@@ -1472,7 +1499,7 @@
"registered": "Geregistreerd",
"lastOnline": "Laatst online",
"ipAddress": "IP-adres",
"machineId": "Machine ID",
"machineId": "Apparaat-ID",
"sendAlert": "Melding sturen",
"sendMessage": "Bericht sturen",
"disconnectUser": "Verbinding verbreken",
@@ -1480,11 +1507,11 @@
"resetPassword": "Wachtwoord resetten",
"deleteUser": "Gebruiker verwijderen",
"confirmDelete": "Weet je zeker dat je {username} wilt verwijderen? Dit kan niet ongedaan worden gemaakt.",
"tabInfo": "Account Info",
"tabInfo": "Accountgegevens",
"tabBadges": "Badges ({count})",
"tabRooms": "Rooms ({count})",
"tabSanctions": "Sanctions ({count})",
"tabActivity": "Activity"
"tabRooms": "Kamers ({count})",
"tabSanctions": "Sancties ({count})",
"tabActivity": "Activiteit"
},
"editForm": {
"title": "Gebruiker bewerken: {username}",
@@ -1502,6 +1529,84 @@
"save": "Wijzigingen opslaan",
"saving": "Opslaan…",
"cancel": "Annuleren"
},
"presentation": {
"users": "Gebruikers",
"edit": "Bewerken",
"view": "Bekijken",
"rank": "Rang {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Verbannen",
"credits": "credits",
"diamonds": "diamanten",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Kamers",
"messages": "Berichten",
"trades": "Ruiltransacties",
"accountDetails": "Accountgegevens",
"registered": "Geregistreerd",
"lastLogin": "Laatste login",
"ipRegister": "Registratie-IP",
"ipCurrent": "Huidig IP",
"machineId": "Apparaat-ID",
"email": "E-mail",
"gender": "Geslacht",
"never": "Nooit",
"findIp": "Gebruikers met dit IP zoeken",
"enabled": "Ingeschakeld",
"disabled": "Uitgeschakeld",
"unavailable": "Niet beschikbaar",
"noBadges": "Geen badges toegewezen"
},
"create": {
"newUser": "Nieuwe gebruiker",
"title": "Gebruiker aanmaken",
"description": "Maakt het account direct aan met een standaardavatar en 5.000 credits, pixels, duckets en diamanten.",
"username": "Gebruikersnaam",
"email": "E-mail",
"password": "Wachtwoord",
"rank": "Rang",
"rankOption": "Rang {rank}",
"motto": "Motto (optioneel)",
"usernamePlaceholder": "NieuweHabbo",
"passwordPlaceholder": "Minimaal 6 tekens",
"mottoPlaceholder": "Ik ben hier nieuw!",
"generate": "Genereren",
"passwordGenerated": "Wachtwoord gegenereerd in het wachtwoordveld.",
"required": "Gebruikersnaam, e-mail en wachtwoord zijn verplicht.",
"invalidUsername": "Gebruik 3–20 tekens: letters, cijfers, _ of -.",
"passwordLength": "Het wachtwoord moet minimaal 6 tekens bevatten.",
"created": "Gebruiker {username} aangemaakt."
},
"overview": {
"title": "Overzicht",
"hint": "Maximaal 5 recente registraties per bron. Alleen onderdelen waarvoor je toegang hebt worden getoond.",
"restricted": "Je hebt geen toestemming om operationele gegevens te bekijken.",
"sanctions": "Recente bans",
"mute": "Actieve mute",
"tickets": "Supporttickets",
"help": "Helpcentrumverzoeken",
"reports": "Meldingen in het spel",
"transactions": "Betalingen",
"purchases": "Catalogusaankopen",
"activity": "Geregistreerde stafacties",
"openModule": "Beheer openen",
"purchaseHint": "Geregistreerde aantallen en valutakosten.",
"purchaseDetail": "Aantal: {amount} · Credits: {credits} · Punten: {points} (type {type})",
"activityHint": "Staflogboekregels over dit account. Dit is geen volledige geschiedenis van de activiteit van de speler.",
"muteUntil": "Gedempt tot {date}",
"error": "Deze bron kon niet worden geladen. Vernieuw de pagina om het opnieuw te proberen.",
"empty": "Geen registraties gevonden.",
"status": {
"active": "Actief",
"expired": "Verlopen",
"open": "Open",
"closed": "Gesloten",
"reported": "Gemelde gebruiker",
"reporter": "Melder"
}
}
},
"bans": {
@@ -3123,7 +3228,7 @@
"radioSubtitle": "Stream, DJ-tools, embed en monitoring",
"settings": "Instellingen",
"settingsSubtitle": "CMS-configuratie, thema, onderhoud en gerelateerde tools",
"tools": "Tools",
"tools": "Hulpmiddelen",
"toolsSubtitle": "RCON, kamers, import en vertalingen",
"observability": "Observeerbaarheid",
"observabilitySubtitle": "Logs, analytics, DevOps en hotelmeldingen",
@@ -3134,10 +3239,10 @@
"ads": "Advertenties",
"media": "Media",
"navigator": "Navigator",
"help": "Help",
"help": "Ondersteuning",
"boxes": "Boxen",
"tags": "Tags",
"prefixes": "Prefixes",
"prefixes": "Voorvoegsels",
"events": "Evenementen",
"eventTypes": "Evenementtypen",
"polls": "Polls",
@@ -3145,21 +3250,21 @@
"helpTickets": "Helpcentrum",
"templates": "Sjablonen",
"directory": "Adresboek",
"multiAccounts": "Multi-accounts",
"multiAccounts": "Gekoppelde accounts",
"online": "Online",
"applications": "Sollicitaties",
"teams": "Teams",
"bans": "Bans",
"bans": "Verbanningen",
"ip": "IP",
"vpn": "VPN",
"permissions": "Permissies",
"wordFilter": "Woordfilter",
"overview": "Overzicht",
"actions": "Acties",
"cfh": "CFH",
"cfh": "Moderatieverzoeken",
"team": "Team",
"catalogTab": "Catalogus",
"rares": "Rares",
"rares": "Zeldzame meubels",
"badges": "Badges",
"achievements": "Prestaties",
"sounds": "Geluiden",
@@ -3170,9 +3275,9 @@
"calendar": "Kalender",
"history": "Geschiedenis",
"moderationTab": "Moderatie",
"monitoring": "Monitoring",
"monitoring": "Bewaking",
"settingsTab": "Instellingen",
"embed": "Embed",
"embed": "Insluiten",
"apiKeys": "API-sleutels",
"points": "Punten",
"ranks": "Rangen",
@@ -3185,35 +3290,36 @@
"emulator": "Emulator",
"email": "E-mail",
"housekeeping": "Permissie-archief",
"commando": "Commando",
"commando": "Operationeel centrum",
"rooms": "Kamers",
"import": "Import",
"import": "Importeren",
"studio": "Studio",
"importOverview": "Overzicht",
"importBadges": "Badges",
"importFurni": "Meubels",
"importUpload": "Upload",
"importUpload": "Uploaden",
"importClothing": "Kleding",
"importEffects": "Effecten",
"importPets": "Huisdieren",
"importClone": "Kloon",
"importSync": "Sync",
"importSync": "Synchronisatie",
"importRepairIcons": "Pictogrammen repareren",
"importAudit": "Audit",
"importAudit": "Gegevenscontrole",
"translations": "Vertalingen",
"logs": "Logs",
"audit": "Audit",
"logs": "Logboeken",
"audit": "Wijzigingslogboek",
"chat": "Chat",
"commands": "Commands",
"trades": "Trades",
"analytics": "Analytics",
"commands": "Commando's",
"trades": "Ruiltransacties",
"analytics": "Statistieken",
"activity": "Activiteit",
"economy": "Economie",
"devops": "DevOps",
"errors": "Fouten",
"alerts": "Meldingen",
"toolsGroup": "Tools",
"ticketInbox": "Inbox"
"toolsGroup": "Hulpmiddelen",
"ticketInbox": "Ontvangen tickets",
"cmsErrors": "CMS-fouten"
},
"events": "Evenementen",
"eventsSubtitle": "Hotel-evenementen en evenementtypes",
@@ -3221,11 +3327,11 @@
"ticketsSubtitle": "Twee aparte wachtrijen: CMS support desk en publieke helpcenter-tickets",
"staffAccess": "Rangen & rechten",
"staffAccessSubtitle": "Staffrangen en live toegang. Archief is alleen vergelijken/exporteren.",
"analytics": "Analytics",
"analytics": "Statistieken",
"analyticsSubtitle": "Hotelactiviteit, retentie en economie",
"devops": "DevOps",
"devopsSubtitle": "Servicegezondheid en emulatorfouten",
"import": "Import",
"import": "Importeren",
"importSubtitle": "Meubels, badges, kleding en asset-tools"
},
"tickets": {
@@ -3275,19 +3381,19 @@
"actionsTitle": "Acties",
"requesterTitle": "Aanvrager",
"detailsTitle": "Details",
"rankLabel": "Rank {rank}",
"rankLabel": "Rang {rank}",
"messageCountLabel": "Berichten",
"success": {
"replied": "Antwoord verzonden.",
"closed": "Ticket gesloten.",
"reopened": "Ticket heropend.",
"banLifted": "Ban lifted and ticket closed."
"banLifted": "Verbanning opgeheven en ticket gesloten."
},
"queueHint": "Deze wachtrij is het publieke helpcenter. CMS support desk tickets staan onder Supportbalie.",
"queueOtherCta": "Open CMS support desk",
"queueCounts": "Open hier: {here} · Supportbalie: {other}",
"liftBanSubmit": "Lift requester ban & close",
"requesterBanned": "Requester banned"
"liftBanSubmit": "Verbanning van aanvrager opheffen en sluiten",
"requesterBanned": "Aanvrager is verbannen"
},
"cfh": {
"title": "Call for Help",
@@ -3583,6 +3689,25 @@
"specialPricing": "Speciale prijs: {credits} credits, {points} punten (type {type})",
"restrictedOffers": "{count} aanbiedingen in deze categorie vereisen een Club-lidmaatschap.",
"clubOffer": "Club-aanbieding"
},
"actions": {
"saved": "Wijzigingen opgeslagen.",
"error": "De bewerking kon niet worden voltooid.",
"saving": "Bezig met opslaan…",
"saveError": "Opslaan mislukt. Je wijzigingen zijn behouden; probeer het opnieuw.",
"unsaved": "Niet-opgeslagen wijzigingen",
"leaveUnsaved": "Je hebt niet-opgeslagen wijzigingen. Wil je deze pagina verlaten?",
"preview": "Voorbeeld"
},
"search": {
"furniture": "Meubels",
"catalog": "Cataloguscategorieën",
"tickets": "Supporttickets",
"loading": "Zoeken…",
"hint": "Voer een numeriek ID of minstens 2 tekens in, of kies een pagina.",
"retry": "Opnieuw proberen",
"description": "Zoek pagina’s, gebruikers, meubels, cataloguscategorieën, artikelen en supporttickets.",
"partialError": "Sommige resultaten konden niet worden geladen. Probeer opnieuw."
}
},
"mod": {
+88
View File
@@ -1232,6 +1232,84 @@
"save": "Lagre endringer",
"saving": "Lagrer …",
"cancel": "Avbryt"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3237,6 +3315,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+88
View File
@@ -1233,6 +1233,84 @@
"save": "Zapisz zmiany",
"saving": "Zapisuję…",
"cancel": "Anuluj"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3238,6 +3316,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+88
View File
@@ -1233,6 +1233,84 @@
"save": "Salvar alterações",
"saving": "Salvando…",
"cancel": "Cancelar"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3238,6 +3316,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+88
View File
@@ -1232,6 +1232,84 @@
"save": "Salvați modificările",
"saving": "Se salvează…",
"cancel": "Anulează"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3237,6 +3315,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+88
View File
@@ -1231,6 +1231,84 @@
"save": "Save changes",
"saving": "Saving…",
"cancel": "Cancel"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3236,6 +3314,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+88
View File
@@ -1232,6 +1232,84 @@
"save": "Uložiť zmeny",
"saving": "Ukladá sa…",
"cancel": "Zrušiť"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3237,6 +3315,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+88
View File
@@ -1232,6 +1232,84 @@
"save": "Сачувај промене",
"saving": "Чување…",
"cancel": "Откажи"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3237,6 +3315,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+88
View File
@@ -1232,6 +1232,84 @@
"save": "Spara ändringar",
"saving": "Sparar...",
"cancel": "Avbryt"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3237,6 +3315,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+88
View File
@@ -1233,6 +1233,84 @@
"save": "Değişiklikleri kaydet",
"saving": "Kaydediliyor…",
"cancel": "İptal"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3238,6 +3316,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {
+88
View File
@@ -1232,6 +1232,84 @@
"save": "Зберегти зміни",
"saving": "Збереження…",
"cancel": "Скасувати"
},
"presentation": {
"users": "Users",
"edit": "Edit",
"view": "View",
"rank": "Rank {rank}",
"online": "Online",
"offline": "Offline",
"banned": "Banned",
"credits": "credits",
"diamonds": "diamonds",
"duckets": "duckets",
"pixels": "pixels",
"rooms": "Rooms",
"messages": "Messages",
"trades": "Trades",
"accountDetails": "Account details",
"registered": "Registered",
"lastLogin": "Last login",
"ipRegister": "Registration IP",
"ipCurrent": "Current IP",
"machineId": "Device ID",
"email": "Email",
"gender": "Gender",
"never": "Never",
"findIp": "Find users with this IP",
"enabled": "Enabled",
"disabled": "Disabled",
"unavailable": "Not available",
"noBadges": "No badges assigned"
},
"create": {
"newUser": "New user",
"title": "Create user",
"description": "Creates the account immediately with a default avatar and 5,000 credits, pixels, duckets and diamonds.",
"username": "Username",
"email": "Email",
"password": "Password",
"rank": "Rank",
"rankOption": "Rank {rank}",
"motto": "Motto (optional)",
"usernamePlaceholder": "NewHabbo",
"passwordPlaceholder": "At least 6 characters",
"mottoPlaceholder": "I'm new here!",
"generate": "Generate",
"passwordGenerated": "Password generated in the password field.",
"required": "Username, email and password are required.",
"invalidUsername": "Use 3–20 characters: letters, numbers, _ or -.",
"passwordLength": "The password must contain at least 6 characters.",
"created": "User {username} created."
},
"overview": {
"title": "Overview",
"hint": "Up to 5 recent records per source. Only sections you can access are shown.",
"restricted": "You do not have permission to view operational records.",
"sanctions": "Recent bans",
"mute": "Active mute",
"tickets": "Support tickets",
"help": "Help center requests",
"reports": "In-game reports",
"transactions": "Payments",
"purchases": "Catalog purchases",
"activity": "Recorded staff actions",
"openModule": "Open management",
"purchaseHint": "Recorded quantities and currency costs.",
"purchaseDetail": "Quantity: {amount} · Credits: {credits} · Points: {points} (type {type})",
"activityHint": "Staff audit entries targeting this account. This is not a complete history of player activity.",
"muteUntil": "Muted until {date}",
"error": "This source could not be loaded. Refresh the page to try again.",
"empty": "No records found.",
"status": {
"active": "Active",
"expired": "Expired",
"open": "Open",
"closed": "Closed",
"reported": "Reported user",
"reporter": "Reporter"
}
}
},
"bans": {
@@ -3237,6 +3315,16 @@
"specialPricing": "Special pricing: {credits} credits, {points} points (type {type})",
"restrictedOffers": "{count} offers in this category require Club membership.",
"clubOffer": "Club offer"
},
"search": {
"furniture": "Furniture",
"catalog": "Catalog categories",
"tickets": "Support tickets",
"loading": "Searching…",
"hint": "Enter a numeric ID or at least 2 characters, or choose a page.",
"retry": "Retry",
"description": "Search pages, users, furniture, catalog categories, articles and support tickets.",
"partialError": "Some results could not be loaded. Try again."
}
},
"error": {