feat(i18n): make admin and catalog components translatable
CI / check (push) Failing after 30s
CI / preflight (push) Skipped
CI / deploy (push) Skipped

This commit is contained in:
openhands committed 2026-09-18 14:56:36 +02:00
1 parent d6111387e4
commit 03774bb411
9 files changed
+142 -87

No files matched your search

@@ -10,6 +10,7 @@ import {
Trash2, Trash2,
X, X,
} from "lucide-react"; } from "lucide-react";
// import { useTranslations } from "next-intl";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@@ -147,6 +147,7 @@ export function NitroEditorDialog({
onOpenChange, onOpenChange,
}: NitroEditorDialogProps) { }: NitroEditorDialogProps) {
const loadingT = useTranslations("pages.admin.housekeeping"); const loadingT = useTranslations("pages.admin.housekeeping");
const t = useTranslations("pages.admin.import.furni.nitroEditorDialog");
const [metadata, setMetadata] = useState<NitroMetadata | null>(null); const [metadata, setMetadata] = useState<NitroMetadata | null>(null);
const [originalMetadata, setOriginalMetadata] = const [originalMetadata, setOriginalMetadata] =
useState<NitroMetadata | null>(null); useState<NitroMetadata | null>(null);
@@ -191,7 +192,7 @@ export function NitroEditorDialog({
cansiton: result.canSit, cansiton: result.canSit,
canlayon: result.canLay, canlayon: result.canLay,
}); });
toast.success(`Auto-detected: ${result.reason}`); toast.success(t("autoDetected", { reason: result.reason }));
}; };
const patchMeta = (path: string, value: unknown) => { const patchMeta = (path: string, value: unknown) => {
@@ -217,7 +218,7 @@ export function NitroEditorDialog({
const data = await res.json(); const data = await res.json();
if (controller.signal.aborted) return; if (controller.signal.aborted) return;
if (!res.ok) { if (!res.ok) {
toast.error(data.error || "Failed to load .nitro metadata"); toast.error(data.error || t("loadFailed"));
closeAfterLoadError(); closeAfterLoadError();
return; return;
} }
@@ -228,7 +229,7 @@ export function NitroEditorDialog({
setOriginalFlags({ ...data.flags }); setOriginalFlags({ ...data.flags });
} catch { } catch {
if (!controller.signal.aborted) { if (!controller.signal.aborted) {
toast.error("Network error loading .nitro metadata"); toast.error(t("networkErrorLoad"));
closeAfterLoadError(); closeAfterLoadError();
} }
} finally { } finally {
@@ -245,7 +246,7 @@ export function NitroEditorDialog({
setMetadata(parsed); setMetadata(parsed);
setJsonError(null); setJsonError(null);
} catch { } catch {
toast.error("Invalid JSON — fix it before switching tabs"); toast.error(t("invalidJsonSwitchTabs"));
return; return;
} }
} else if (activeTab !== "json" && newTab === "json") { } else if (activeTab !== "json" && newTab === "json") {
@@ -260,7 +261,7 @@ export function NitroEditorDialog({
setFlags({ ...originalFlags }); setFlags({ ...originalFlags });
setJsonText(JSON.stringify(originalMetadata, null, 2)); setJsonText(JSON.stringify(originalMetadata, null, 2));
setJsonError(null); setJsonError(null);
toast.info("Reset to original values"); toast.info(t("resetToOriginal"));
}; };
const handleSave = async () => { const handleSave = async () => {
@@ -270,7 +271,7 @@ export function NitroEditorDialog({
try { try {
metadataToSave = JSON.parse(jsonText); metadataToSave = JSON.parse(jsonText);
} catch { } catch {
toast.error("Invalid JSON"); toast.error(t("invalidJson"));
return; return;
} }
} }
@@ -289,7 +290,7 @@ export function NitroEditorDialog({
if (!res.ok) { if (!res.ok) {
const err = await res.json().catch(() => ({})); const err = await res.json().catch(() => ({}));
toast.error(err.error || "Save failed"); toast.error(err.error || t("saveFailed"));
return; return;
} }
@@ -1,5 +1,6 @@
"use client"; "use client";
import { useTranslations } from "next-intl";
import { AlertCircle, Check, Loader2, Upload } from "lucide-react"; import { AlertCircle, Check, Loader2, Upload } from "lucide-react";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -22,6 +23,9 @@ interface UploadResult {
} }
export function UploadFurniClient() { export function UploadFurniClient() {
const t = useTranslations(
"pages.admin.import.furni.upload.uploadFurniClient",
);
const nitroRef = useRef<HTMLInputElement>(null); const nitroRef = useRef<HTMLInputElement>(null);
const iconRef = useRef<HTMLInputElement>(null); const iconRef = useRef<HTMLInputElement>(null);
@@ -73,20 +77,19 @@ export function UploadFurniClient() {
const trimmedClass = classname.trim(); const trimmedClass = classname.trim();
if (!CLASSNAME_RE.test(trimmedClass)) { if (!CLASSNAME_RE.test(trimmedClass)) {
const msg = const msg = t("errorClassname");
"Classname must contain only letters, numbers, and underscores";
setError(msg); setError(msg);
toast.error(msg); toast.error(msg);
return; return;
} }
if (!nitroFile) { if (!nitroFile) {
const msg = "Nitro file is required"; const msg = t("errorNitroRequired");
setError(msg); setError(msg);
toast.error(msg); toast.error(msg);
return; return;
} }
if (nitroFile.size > NITRO_MAX_BYTES) { if (nitroFile.size > NITRO_MAX_BYTES) {
const msg = "Nitro file must be 50MB or smaller"; const msg = t("errorNitroSize");
setError(msg); setError(msg);
toast.error(msg); toast.error(msg);
return; return;
@@ -11,6 +11,7 @@ import {
VolumeX, VolumeX,
Zap, Zap,
} from "lucide-react"; } from "lucide-react";
import { useTranslations } from "next-intl";
import { useState } from "react"; import { useState } from "react";
import { import {
assignCfhTicket, assignCfhTicket,
@@ -36,16 +37,14 @@ import { Textarea } from "@/components/ui/textarea";
import { useServerAction } from "@/hooks/use-server-action"; import { useServerAction } from "@/hooks/use-server-action";
import { formatDate } from "@/lib/format-date"; import { formatDate } from "@/lib/format-date";
const CFH_STATES: Record<number, { label: string; class: string }> = { const CFH_STATES: Record<number, { class: string }> = {
0: { 0: {
label: "Open",
class: "bg-[var(--admin-error-subtle)] text-[var(--admin-error)]", class: "bg-[var(--admin-error-subtle)] text-[var(--admin-error)]",
}, },
1: { 1: {
label: "Picked",
class: "bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)]", class: "bg-[var(--admin-warning-subtle)] text-[var(--admin-warning)]",
}, },
2: { label: "Closed", class: "bg-muted text-muted-foreground" }, 2: { class: "bg-muted text-muted-foreground" },
}; };
interface UserInfo { interface UserInfo {
@@ -97,11 +96,18 @@ export function CfhDetail({
userLinkMode?: "admin" | "public"; userLinkMode?: "admin" | "public";
}) { }) {
void currentUserId; void currentUserId;
const t = useTranslations("pages.admin.moderation.cfh.id.cfhDetail");
const { run, isPending } = useServerAction(); const { run, isPending } = useServerAction();
const [alertMsg, setAlertMsg] = useState(""); const [alertMsg, setAlertMsg] = useState("");
const [muteDuration, setMuteDuration] = useState(10); const [muteDuration, setMuteDuration] = useState(10);
const state = CFH_STATES[ticket.state] ?? CFH_STATES[0]; const state = CFH_STATES[ticket.state] ?? CFH_STATES[0];
const stateKey =
ticket.state === 1
? "statePicked"
: ticket.state === 2
? "stateClosed"
: "stateOpen";
function userHref(user: UserInfo) { function userHref(user: UserInfo) {
if (userLinkMode === "public") { if (userLinkMode === "public") {
@@ -122,17 +128,22 @@ export function CfhDetail({
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<span className="text-sm text-muted-foreground font-mono"> <span className="text-sm text-muted-foreground font-mono">
CFH #{ticket.id} {t("cfhId", { id: ticket.id })}
</span> </span>
<Badge className={state.class}>{state.label}</Badge> <Badge className={state.class}>{t(stateKey)}</Badge>
<span className="text-xs text-muted-foreground"> <span className="text-xs text-muted-foreground">
Type {ticket.type} · Category {ticket.category} {t("typeCategory", {
type: ticket.type,
category: ticket.category,
})}
</span> </span>
</div> </div>
<h1 className="text-xl font-bold">Call For Help Report</h1> <h1 className="text-xl font-bold">{t("title")}</h1>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{formatDate(new Date(ticket.timestamp * 1000))} · Room # {t("roomMeta", {
{ticket.roomId} when: formatDate(new Date(ticket.timestamp * 1000)),
room: ticket.roomId,
})}
</p> </p>
</div> </div>
</div> </div>
@@ -145,12 +156,12 @@ export function CfhDetail({
<CardHeader className="pb-2"> <CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2"> <CardTitle className="text-sm flex items-center gap-2">
<MessageSquare className="h-4 w-4" /> <MessageSquare className="h-4 w-4" />
Report Message {t("reportMessage")}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-sm leading-relaxed whitespace-pre-wrap"> <p className="text-sm leading-relaxed whitespace-pre-wrap">
{ticket.issue || "No description provided."} {ticket.issue || t("noDescription")}
</p> </p>
</CardContent> </CardContent>
</Card> </Card>
@@ -12,6 +12,7 @@ import {
Terminal, Terminal,
UserCheck, UserCheck,
} from "lucide-react"; } from "lucide-react";
import { useTranslations } from "next-intl";
import { useMemo, useState } from "react"; import { useMemo, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { import {
@@ -45,6 +46,7 @@ export function RankEditClient({
allCmsPermissions, allCmsPermissions,
}: RankEditorProps) { }: RankEditorProps) {
const { run, isPending } = useServerAction(); const { run, isPending } = useServerAction();
const t = useTranslations("pages.admin.permissions.id.rankEditClient");
const [general, setGeneral] = useState({ const [general, setGeneral] = useState({
rank_name: initialRank.rank_name, rank_name: initialRank.rank_name,
@@ -89,7 +91,7 @@ export function RankEditClient({
]), ]),
); );
run(() => saveRankAction({ id: initialRank.id, fields }), { run(() => saveRankAction({ id: initialRank.id, fields }), {
successMessage: "Rank saved", successMessage: t("rankSaved"),
}); });
} }
@@ -107,11 +109,11 @@ export function RankEditClient({
}); });
if (!r.ok) { if (!r.ok) {
setCmsPermSlugs(prev); setCmsPermSlugs(prev);
toast.error("Failed"); toast.error(t("failed"));
} }
} catch { } catch {
setCmsPermSlugs(prev); setCmsPermSlugs(prev);
toast.error("Failed"); toast.error(t("failed"));
} }
} }
@@ -195,7 +197,7 @@ export function RankEditClient({
setSectionAll(filtered, "1"); setSectionAll(filtered, "1");
}} }}
> >
All On {t("allOn")}
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
@@ -206,7 +208,7 @@ export function RankEditClient({
setSectionAll(filtered, "0"); setSectionAll(filtered, "0");
}} }}
> >
All Off {t("allOff")}
</Button> </Button>
</div> </div>
)} )}
@@ -262,11 +264,14 @@ export function RankEditClient({
)} )}
<div> <div>
<h1 className="text-2xl font-bold"> <h1 className="text-2xl font-bold">
{general.rank_name || `Rank ${initialRank.id}`} {general.rank_name || t("rankFallback", { id: initialRank.id })}
</h1> </h1>
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
Rank #{initialRank.id} &middot; Level {initialRank.level} &middot;{" "} {t("rankMeta", {
{totalUsers} users id: initialRank.id,
level: initialRank.level,
count: totalUsers,
})}
</p> </p>
</div> </div>
</div> </div>
@@ -275,28 +280,31 @@ export function RankEditClient({
<Tabs defaultValue="general"> <Tabs defaultValue="general">
<TabsList> <TabsList>
<TabsTrigger value="general"> <TabsTrigger value="general">
<Settings className="w-3.5 h-3.5 mr-1" /> General <Settings className="w-3.5 h-3.5 mr-1" /> {t("tabGeneral")}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="emulator"> <TabsTrigger value="emulator">
<Terminal className="w-3.5 h-3.5 mr-1" /> Emulator ({enabledCount}) <Terminal className="w-3.5 h-3.5 mr-1" />{" "}
{t("tabEmulator", { count: enabledCount })}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="cms"> <TabsTrigger value="cms">
<UserCheck className="w-3.5 h-3.5 mr-1" /> CMS ( <UserCheck className="w-3.5 h-3.5 mr-1" />{" "}
{cmsPermSlugs.length}) {t("tabCms", { count: cmsPermSlugs.length })}
</TabsTrigger>
<TabsTrigger value="users">
{t("tabUsers", { count: totalUsers })}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="users">Users ({totalUsers})</TabsTrigger>
</TabsList> </TabsList>
{/* ═══ General ═══ */} {/* ═══ General ═══ */}
<TabsContent value="general" className="space-y-4"> <TabsContent value="general" className="space-y-4">
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base">Rank Settings</CardTitle> <CardTitle className="text-base">{t("rankSettings")}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs">Rank Name</Label> <Label className="text-xs">{t("rankName")}</Label>
<Input <Input
value={general.rank_name} value={general.rank_name}
onChange={(e) => onChange={(e) =>
@@ -305,7 +313,7 @@ export function RankEditClient({
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs">Badge</Label> <Label className="text-xs">{t("badge")}</Label>
<Input <Input
value={general.badge} value={general.badge}
onChange={(e) => onChange={(e) =>
@@ -314,7 +322,7 @@ export function RankEditClient({
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs">Prefix</Label> <Label className="text-xs">{t("prefix")}</Label>
<Input <Input
value={general.prefix} value={general.prefix}
onChange={(e) => onChange={(e) =>
@@ -323,7 +331,7 @@ export function RankEditClient({
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs">Prefix Color</Label> <Label className="text-xs">{t("prefixColor")}</Label>
<div className="flex gap-2"> <div className="flex gap-2">
<Input <Input
value={general.prefix_color} value={general.prefix_color}
@@ -348,10 +356,10 @@ export function RankEditClient({
<div className="flex items-center justify-between rounded-lg border p-3"> <div className="flex items-center justify-between rounded-lg border p-3">
<div> <div>
<Label htmlFor="rank-hidden_rank" className="text-sm"> <Label htmlFor="rank-hidden_rank" className="text-sm">
Hidden Rank {t("hiddenRank")}
</Label> </Label>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Hide from public {t("hideFromPublic")}
</p> </p>
</div> </div>
<Switch <Switch
@@ -365,10 +373,10 @@ export function RankEditClient({
<div className="flex items-center justify-between rounded-lg border p-3"> <div className="flex items-center justify-between rounded-lg border p-3">
<div> <div>
<Label htmlFor="rank-log_commands" className="text-sm"> <Label htmlFor="rank-log_commands" className="text-sm">
Log Commands {t("logCommands")}
</Label> </Label>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
Record to audit log {t("recordToAuditLog")}
</p> </p>
</div> </div>
<Switch <Switch
@@ -380,7 +388,7 @@ export function RankEditClient({
/> />
</div> </div>
<div className="space-y-1.5 rounded-lg border p-3"> <div className="space-y-1.5 rounded-lg border p-3">
<Label className="text-xs">Room Effect ID</Label> <Label className="text-xs">{t("roomEffectId")}</Label>
<Input <Input
type="number" type="number"
value={general.room_effect} value={general.room_effect}
@@ -390,7 +398,7 @@ export function RankEditClient({
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-4"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-4 mt-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs">Auto Credits</Label> <Label className="text-xs">{t("autoCredits")}</Label>
<Input <Input
type="number" type="number"
value={general.auto_credits_amount} value={general.auto_credits_amount}
@@ -400,7 +408,7 @@ export function RankEditClient({
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs">Auto Pixels</Label> <Label className="text-xs">{t("autoPixels")}</Label>
<Input <Input
type="number" type="number"
value={general.auto_pixels_amount} value={general.auto_pixels_amount}
@@ -410,7 +418,7 @@ export function RankEditClient({
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs">Auto GOTW</Label> <Label className="text-xs">{t("autoGotw")}</Label>
<Input <Input
type="number" type="number"
value={general.auto_gotw_amount} value={general.auto_gotw_amount}
@@ -418,7 +426,7 @@ export function RankEditClient({
/> />
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label className="text-xs">Auto Points</Label> <Label className="text-xs">{t("autoPoints")}</Label>
<Input <Input
type="number" type="number"
value={general.auto_points_amount} value={general.auto_points_amount}
@@ -435,7 +443,7 @@ export function RankEditClient({
) : ( ) : (
<Save className="w-4 h-4 mr-1" /> <Save className="w-4 h-4 mr-1" />
)} )}
Save {t("save")}
</Button> </Button>
</div> </div>
</CardContent> </CardContent>
@@ -445,10 +453,13 @@ export function RankEditClient({
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base"> <CardTitle className="text-base">
Users with this rank {t("usersWithThisRank")}
{totalUsers > users.length && ( {totalUsers > users.length && (
<span className="text-muted-foreground font-normal ml-1 text-sm"> <span className="text-muted-foreground font-normal ml-1 text-sm">
(showing {users.length} of {totalUsers}) {t("showingUsers", {
shown: users.length,
total: totalUsers,
})}
</span> </span>
)} )}
</CardTitle> </CardTitle>
@@ -456,7 +467,7 @@ export function RankEditClient({
<CardContent> <CardContent>
{users.length === 0 ? ( {users.length === 0 ? (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
No users with this rank. {t("noUsersWithThisRank")}
</p> </p>
) : ( ) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-2"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-2">
@@ -490,7 +501,10 @@ export function RankEditClient({
<TabsContent value="emulator" className="space-y-4"> <TabsContent value="emulator" className="space-y-4">
<div className="flex items-center justify-between flex-wrap gap-2"> <div className="flex items-center justify-between flex-wrap gap-2">
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
{enabledCount} of {totalCount} permissions enabled {t("permissionsEnabled", {
enabled: enabledCount,
total: totalCount,
})}
</p> </p>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex gap-1"> <div className="flex gap-1">
@@ -500,7 +514,7 @@ export function RankEditClient({
className="h-7 text-xs" className="h-7 text-xs"
onClick={() => setAllPerms("1")} onClick={() => setAllPerms("1")}
> >
Enable All {t("enableAll")}
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
@@ -508,36 +522,48 @@ export function RankEditClient({
className="h-7 text-xs" className="h-7 text-xs"
onClick={() => setAllPerms("0")} onClick={() => setAllPerms("0")}
> >
Disable All {t("disableAll")}
</Button> </Button>
</div> </div>
<div className="flex gap-2 text-[11px] text-muted-foreground"> <div className="flex gap-2 text-[11px] text-muted-foreground">
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<span className="w-3 h-3 rounded bg-[var(--admin-text-muted)] inline-block" />{" "} <span className="w-3 h-3 rounded bg-[var(--admin-text-muted)] inline-block" />{" "}
Off {t("off")}
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<span className="w-3 h-3 rounded bg-[var(--admin-success-subtle)] inline-block" />{" "} <span className="w-3 h-3 rounded bg-[var(--admin-success-subtle)] inline-block" />{" "}
On {t("on")}
</span> </span>
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
<span className="w-3 h-3 rounded bg-[var(--admin-warning-subtle)] inline-block" />{" "} <span className="w-3 h-3 rounded bg-[var(--admin-warning-subtle)] inline-block" />{" "}
Owner {t("owner")}
</span> </span>
</div> </div>
</div> </div>
</div> </div>
<SearchInput <SearchInput
placeholder="Search permissions..." placeholder={t("searchPermissionsPlaceholder")}
value={permSearch} value={permSearch}
onChange={setPermSearch} onChange={setPermSearch}
/> />
<PermSection title="Commands" keys={cats.commands} sectionKey="cmd" /> <PermSection
<PermSection title="Account" keys={cats.account} sectionKey="acc" /> title={t("commands")}
keys={cats.commands}
sectionKey="cmd"
/>
<PermSection
title={t("account")}
keys={cats.account}
sectionKey="acc"
/>
{cats.other.length > 0 && ( {cats.other.length > 0 && (
<PermSection title="Other" keys={cats.other} sectionKey="other" /> <PermSection
title={t("other")}
keys={cats.other}
sectionKey="other"
/>
)} )}
<div className="flex justify-end"> <div className="flex justify-end">
@@ -547,7 +573,7 @@ export function RankEditClient({
) : ( ) : (
<Save className="w-4 h-4 mr-1" /> <Save className="w-4 h-4 mr-1" />
)} )}
Save Rank {t("saveRank")}
</Button> </Button>
</div> </div>
</TabsContent> </TabsContent>
@@ -663,10 +689,13 @@ export function RankEditClient({
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="text-base"> <CardTitle className="text-base">
Users with this rank {t("usersWithThisRank")}
{totalUsers > users.length && ( {totalUsers > users.length && (
<span className="text-muted-foreground font-normal ml-1 text-sm"> <span className="text-muted-foreground font-normal ml-1 text-sm">
(showing {users.length} of {totalUsers}) {t("showingUsers", {
shown: users.length,
total: totalUsers,
})}
</span> </span>
)} )}
</CardTitle> </CardTitle>
@@ -674,7 +703,7 @@ export function RankEditClient({
<CardContent> <CardContent>
{users.length === 0 ? ( {users.length === 0 ? (
<p className="text-sm text-muted-foreground"> <p className="text-sm text-muted-foreground">
No users with this rank. {t("noUsersWithThisRank")}
</p> </p>
) : ( ) : (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-2"> <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-2">
+16 -12
View File
@@ -1,6 +1,7 @@
"use client"; "use client";
import { Pencil, Plus, Save, Search, Trash2 } from "lucide-react"; import { Pencil, Plus, Save, Search, Trash2 } from "lucide-react";
import { useTranslations } from "next-intl";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { import {
addBlacklistWord as addBlacklistWordAction, addBlacklistWord as addBlacklistWordAction,
@@ -35,6 +36,7 @@ import {
type UserPrefix, type UserPrefix,
} from "./prefix-model"; } from "./prefix-model";
export function PrefixesClient({ canEdit }: { canEdit: boolean }) { export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
const t = useTranslations("pages.admin.prefixes.prefixesClient");
const { run, isPending } = useServerAction(); const { run, isPending } = useServerAction();
const { confirm, dialog: confirmDialog } = useConfirmDialog(); const { confirm, dialog: confirmDialog } = useConfirmDialog();
const [prefixes, setPrefixes] = useState<UserPrefix[]>([]); const [prefixes, setPrefixes] = useState<UserPrefix[]>([]);
@@ -93,14 +95,14 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
const handleDeletePrefix = async (id: number) => { const handleDeletePrefix = async (id: number) => {
const ok = await confirm({ const ok = await confirm({
title: "Delete prefix", title: t("confirmDeleteTitle"),
description: "Are you sure you want to delete this prefix?", description: t("confirmDeleteDesc"),
confirmLabel: "Delete", confirmLabel: t("confirmDelete"),
cancelLabel: "Cancel", cancelLabel: t("cancel"),
}); });
if (!ok) return; if (!ok) return;
run(() => deletePrefixAction({ id }), { run(() => deletePrefixAction({ id }), {
successMessage: "Prefix deleted!", successMessage: t("successPrefixDeleted"),
onSuccess: () => { onSuccess: () => {
fetchPrefixes(); fetchPrefixes();
}, },
@@ -126,7 +128,9 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
? updatePrefix({ id: editId, ...fields }) ? updatePrefix({ id: editId, ...fields })
: createPrefix({ username: data.username, ...fields }), : createPrefix({ username: data.username, ...fields }),
{ {
successMessage: editId ? "Prefix updated!" : "Prefix created!", successMessage: editId
? t("successPrefixUpdated")
: t("successPrefixCreated"),
onSuccess: () => { onSuccess: () => {
void fetchPrefixes(); void fetchPrefixes();
resolve(true); resolve(true);
@@ -150,7 +154,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
const handleAddBlacklistWord = async () => { const handleAddBlacklistWord = async () => {
if (!newWord.trim()) return; if (!newWord.trim()) return;
run(() => addBlacklistWordAction({ word: newWord.trim() }), { run(() => addBlacklistWordAction({ word: newWord.trim() }), {
successMessage: "Word added to blacklist!", successMessage: t("successWordAdded"),
onSuccess: () => { onSuccess: () => {
setNewWord(""); setNewWord("");
fetchBlacklist(); fetchBlacklist();
@@ -160,7 +164,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
const handleRemoveBlacklistWord = async (id: number) => { const handleRemoveBlacklistWord = async (id: number) => {
run(() => removeBlacklistWordAction({ id }), { run(() => removeBlacklistWordAction({ id }), {
successMessage: "Word removed from blacklist!", successMessage: t("successWordRemoved"),
onSuccess: () => { onSuccess: () => {
fetchBlacklist(); fetchBlacklist();
}, },
@@ -174,7 +178,7 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
settings: settings as unknown as Record<string, string>, settings: settings as unknown as Record<string, string>,
}), }),
{ {
successMessage: "Settings saved!", successMessage: t("successSettingsSaved"),
}, },
); );
}; };
@@ -191,9 +195,9 @@ export function PrefixesClient({ canEdit }: { canEdit: boolean }) {
<Tabs defaultValue="prefixes"> <Tabs defaultValue="prefixes">
<TabsList> <TabsList>
<TabsTrigger value="prefixes">User Prefixes</TabsTrigger> <TabsTrigger value="prefixes">{t("tabUserPrefixes")}</TabsTrigger>
<TabsTrigger value="blacklist">Blacklist</TabsTrigger> <TabsTrigger value="blacklist">{t("tabBlacklist")}</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger> <TabsTrigger value="settings">{t("tabSettings")}</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="prefixes" className="space-y-4"> <TabsContent value="prefixes" className="space-y-4">
+10 -6
View File
@@ -10,6 +10,7 @@ import {
Trash2, Trash2,
Upload, Upload,
} from "lucide-react"; } from "lucide-react";
import { useTranslations } from "next-intl";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useMemo, useRef, useState } from "react"; import { useMemo, useRef, useState } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
@@ -82,6 +83,7 @@ export function SoundsClient({
canEdit: boolean; canEdit: boolean;
baseUrl: string; baseUrl: string;
}) { }) {
const t = useTranslations("pages.admin.sounds.soundsClient");
const router = useRouter(); const router = useRouter();
const { run, isPending } = useServerAction(); const { run, isPending } = useServerAction();
@@ -122,8 +124,8 @@ export function SoundsClient({
if (!deleting) return; if (!deleting) return;
const id = deleting.id; const id = deleting.id;
run(() => deleteSoundtrack({ id }), { run(() => deleteSoundtrack({ id }), {
successMessage: `Song disk #${id} deleted.`, successMessage: t("deleteSuccess", { id }),
errorMessage: `Unable to delete song disk #${id}.`, errorMessage: t("deleteError", { id }),
onSuccess: () => { onSuccess: () => {
invalidateSongPickerCache(); invalidateSongPickerCache();
setDeleting(null); setDeleting(null);
@@ -140,7 +142,7 @@ export function SoundsClient({
<Input <Input
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="Search by name, author, code or id…" placeholder={t("searchPlaceholder")}
className="pl-8" className="pl-8"
/> />
</div> </div>
@@ -152,12 +154,14 @@ export function SoundsClient({
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="all">All ({soundtracks.length})</SelectItem> <SelectItem value="all">
{t("filterAll", { count: soundtracks.length })}
</SelectItem>
<SelectItem value="single"> <SelectItem value="single">
Single MP3s ({counts.single}) {t("filterSingle", { count: counts.single })}
</SelectItem> </SelectItem>
<SelectItem value="composition"> <SelectItem value="composition">
Composition ({counts.composition}) {t("filterComposition", { count: counts.composition })}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@@ -1,5 +1,6 @@
"use client"; "use client";
// import { useTranslations } from "next-intl";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { import {
@@ -179,6 +179,7 @@ export function CatalogItemsTable({
const router = useRouter(); const router = useRouter();
const bulk = useTranslations("pages.admin.catalog.bulk"); const bulk = useTranslations("pages.admin.catalog.bulk");
const discovery = useTranslations("pages.admin.catalog.discovery"); const discovery = useTranslations("pages.admin.catalog.discovery");
const t = useTranslations("pages.admin.catalog.catalogItemsTable.catalogItemsTable");
const [bulkRefreshFrom, setBulkRefreshFrom] = useState< const [bulkRefreshFrom, setBulkRefreshFrom] = useState<
CatalogItemData[] | null CatalogItemData[] | null
>(null); >(null);
@@ -818,7 +819,7 @@ export function CatalogItemsTable({
<div className="flex flex-row items-center justify-between gap-4"> <div className="flex flex-row items-center justify-between gap-4">
<div className="min-w-0"> <div className="min-w-0">
<CardTitle> <CardTitle>
Items ({items.length}) {t("itemsCount", { count: items.length })}
{hasDiscoveryFilter && {hasDiscoveryFilter &&
filteredItems.length !== items.length && ( filteredItems.length !== items.length && (
<span className="text-muted-foreground font-normal text-sm ml-2"> <span className="text-muted-foreground font-normal text-sm ml-2">