Enhance dashboard overview with activity summary and sidebar commands
- Integrated activity summary into the Guild Overview page, displaying message count, voice minutes, and active user count for the last 7 days. - Updated sidebar to include a new "Commands" link for easier navigation. - Added corresponding localization keys for activity metrics in both English and German JSON files.
This commit is contained in:
13
apps/webui/src/app/dashboard/[guildId]/commands/loading.tsx
Normal file
13
apps/webui/src/app/dashboard/[guildId]/commands/loading.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Skeleton } from '@/components/ui/skeleton';
|
||||||
|
|
||||||
|
export default function CommandsLoading() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="h-8 w-48" />
|
||||||
|
<Skeleton className="h-4 w-80" />
|
||||||
|
</div>
|
||||||
|
<Skeleton className="h-96 rounded-lg" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
22
apps/webui/src/app/dashboard/[guildId]/commands/page.tsx
Normal file
22
apps/webui/src/app/dashboard/[guildId]/commands/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { CommandsManager } from '@/components/modules/commands-manager';
|
||||||
|
import { getLocale, t } from '@/lib/i18n';
|
||||||
|
import { listCommandOverridesDashboard } from '@/lib/module-configs/commands';
|
||||||
|
|
||||||
|
interface CommandsPageProps {
|
||||||
|
params: Promise<{ guildId: string }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function CommandsPage({ params }: CommandsPageProps) {
|
||||||
|
const { guildId } = await params;
|
||||||
|
const [locale, commands] = await Promise.all([getLocale(), listCommandOverridesDashboard(guildId)]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-semibold">{t(locale, 'modulePages.commands.title')}</h1>
|
||||||
|
<p className="text-sm text-muted-foreground">{t(locale, 'modulePages.commands.description')}</p>
|
||||||
|
</div>
|
||||||
|
<CommandsManager guildId={guildId} initialCommands={commands} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { getActivitySummary } from '@/lib/activity';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { listRecentDashboardAudit } from '@/lib/audit';
|
import { listRecentDashboardAudit } from '@/lib/audit';
|
||||||
@@ -19,15 +20,41 @@ function formatDate(iso: string, locale: string): string {
|
|||||||
export default async function GuildOverviewPage({ params }: OverviewPageProps) {
|
export default async function GuildOverviewPage({ params }: OverviewPageProps) {
|
||||||
const { guildId } = await params;
|
const { guildId } = await params;
|
||||||
const locale = await getLocale();
|
const locale = await getLocale();
|
||||||
const [modules, auditEntries] = await Promise.all([
|
const [modules, auditEntries, activitySummary] = await Promise.all([
|
||||||
getModuleStatuses(guildId),
|
getModuleStatuses(guildId),
|
||||||
listRecentDashboardAudit(guildId, 10)
|
listRecentDashboardAudit(guildId, 10),
|
||||||
|
getActivitySummary(guildId)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<h1 className="text-2xl font-semibold">{t(locale, 'dashboard.overview.title')}</h1>
|
<h1 className="text-2xl font-semibold">{t(locale, 'dashboard.overview.title')}</h1>
|
||||||
|
|
||||||
|
<section className="space-y-4">
|
||||||
|
<h2 className="text-lg font-medium">{t(locale, 'dashboard.overview.activityTitle')}</h2>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-3">
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<p className="text-xs text-muted-foreground">{t(locale, 'dashboard.overview.activityMessages')}</p>
|
||||||
|
<p className="text-2xl font-semibold">{activitySummary.messageCount.toLocaleString(locale === 'de' ? 'de-DE' : 'en-US')}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<p className="text-xs text-muted-foreground">{t(locale, 'dashboard.overview.activityVoiceMinutes')}</p>
|
||||||
|
<p className="text-2xl font-semibold">{activitySummary.voiceMinutes.toLocaleString(locale === 'de' ? 'de-DE' : 'en-US')}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<p className="text-xs text-muted-foreground">{t(locale, 'dashboard.overview.activityActiveUsers')}</p>
|
||||||
|
<p className="text-2xl font-semibold">{activitySummary.activeUserCount.toLocaleString(locale === 'de' ? 'de-DE' : 'en-US')}</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">{t(locale, 'dashboard.overview.activityHint')}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-lg font-medium">{t(locale, 'dashboard.overview.moduleStatusTitle')}</h2>
|
<h2 className="text-lg font-medium">{t(locale, 'dashboard.overview.moduleStatusTitle')}</h2>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { DASHBOARD_MODULES, type DashboardModuleGroup } from '@nexumi/shared';
|
import { DASHBOARD_MODULES, type DashboardModuleGroup } from '@nexumi/shared';
|
||||||
import { LayoutGrid, Settings, ShieldCheck, SlidersHorizontal } from 'lucide-react';
|
import { LayoutGrid, Settings, ShieldCheck, SlidersHorizontal, Terminal } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
import { useTranslations } from '@/components/locale-provider';
|
import { useTranslations } from '@/components/locale-provider';
|
||||||
@@ -64,6 +64,10 @@ export function Sidebar({ guildId }: SidebarProps) {
|
|||||||
<ShieldCheck className="size-4" />
|
<ShieldCheck className="size-4" />
|
||||||
{t('nav.access')}
|
{t('nav.access')}
|
||||||
</NavLink>
|
</NavLink>
|
||||||
|
<NavLink href={`${base}/commands`} active={pathname === `${base}/commands`}>
|
||||||
|
<Terminal className="size-4" />
|
||||||
|
{t('nav.commands')}
|
||||||
|
</NavLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{groups.map(({ group, modules }) => (
|
{groups.map(({ group, modules }) => (
|
||||||
|
|||||||
198
apps/webui/src/components/modules/commands-manager.tsx
Normal file
198
apps/webui/src/components/modules/commands-manager.tsx
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import type { CommandOverrideDashboard } from '@nexumi/shared';
|
||||||
|
import { RotateCcw } from 'lucide-react';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { useTranslations } from '@/components/locale-provider';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Switch } from '@/components/ui/switch';
|
||||||
|
|
||||||
|
function toCsv(ids: string[]): string {
|
||||||
|
return ids.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromCsv(text: string): string[] {
|
||||||
|
return text
|
||||||
|
.split(',')
|
||||||
|
.map((entry) => entry.trim())
|
||||||
|
.filter((entry) => entry.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LocalOverride extends CommandOverrideDashboard {
|
||||||
|
saving?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CommandsManagerProps {
|
||||||
|
guildId: string;
|
||||||
|
initialCommands: CommandOverrideDashboard[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CommandsManager({ guildId, initialCommands }: CommandsManagerProps) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const [commands, setCommands] = useState<LocalOverride[]>(initialCommands);
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const query = search.trim().toLowerCase();
|
||||||
|
if (!query) {
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
return commands.filter((entry) => entry.commandName.toLowerCase().includes(query));
|
||||||
|
}, [commands, search]);
|
||||||
|
|
||||||
|
function patchLocal(commandName: string, patch: Partial<LocalOverride>) {
|
||||||
|
setCommands((prev) =>
|
||||||
|
prev.map((entry) => (entry.commandName === commandName ? { ...entry, ...patch } : entry))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave(entry: LocalOverride) {
|
||||||
|
patchLocal(entry.commandName, { saving: true });
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/guilds/${guildId}/commands/${entry.commandName}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
enabled: entry.enabled,
|
||||||
|
allowedRoleIds: entry.allowedRoleIds,
|
||||||
|
deniedRoleIds: entry.deniedRoleIds,
|
||||||
|
allowedChannelIds: entry.allowedChannelIds,
|
||||||
|
deniedChannelIds: entry.deniedChannelIds,
|
||||||
|
cooldownSeconds: entry.cooldownSeconds
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const body = (await response.json().catch(() => null)) as (CommandOverrideDashboard & { error?: string }) | null;
|
||||||
|
if (!response.ok || !body) {
|
||||||
|
toast.error(body?.error ?? t('common.saveError'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
patchLocal(entry.commandName, body);
|
||||||
|
toast.success(t('common.saveSuccess'));
|
||||||
|
} catch {
|
||||||
|
toast.error(t('common.saveError'));
|
||||||
|
} finally {
|
||||||
|
patchLocal(entry.commandName, { saving: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReset(entry: LocalOverride) {
|
||||||
|
patchLocal(entry.commandName, { saving: true });
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/guilds/${guildId}/commands/${entry.commandName}`, { method: 'DELETE' });
|
||||||
|
const body = (await response.json().catch(() => null)) as CommandOverrideDashboard | null;
|
||||||
|
if (!response.ok || !body) {
|
||||||
|
toast.error(t('common.saveError'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
patchLocal(entry.commandName, body);
|
||||||
|
toast.success(t('common.saveSuccess'));
|
||||||
|
} catch {
|
||||||
|
toast.error(t('common.saveError'));
|
||||||
|
} finally {
|
||||||
|
patchLocal(entry.commandName, { saving: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{t('modulePages.commands.title')}</CardTitle>
|
||||||
|
<CardDescription>{t('modulePages.commands.description')}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<Input
|
||||||
|
value={search}
|
||||||
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
|
placeholder={t('modulePages.commands.searchPlaceholder')}
|
||||||
|
className="max-w-sm"
|
||||||
|
/>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{filtered.length === 0 && (
|
||||||
|
<p className="text-sm text-muted-foreground">{t('modulePages.commands.empty')}</p>
|
||||||
|
)}
|
||||||
|
{filtered.map((entry) => (
|
||||||
|
<div key={entry.commandName} className="space-y-3 rounded-md border border-border p-4">
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||||
|
<p className="font-mono text-sm font-medium">/{entry.commandName}</p>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Switch
|
||||||
|
checked={entry.enabled}
|
||||||
|
disabled={entry.saving}
|
||||||
|
onCheckedChange={(enabled) => patchLocal(entry.commandName, { enabled })}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{entry.enabled ? t('common.enabled') : t('common.disabled')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs text-muted-foreground">{t('modulePages.commands.cooldownSeconds')}</p>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
value={entry.cooldownSeconds ?? ''}
|
||||||
|
onChange={(event) =>
|
||||||
|
patchLocal(entry.commandName, {
|
||||||
|
cooldownSeconds: event.target.value === '' ? null : Number(event.target.value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
placeholder={t('common.optional')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs text-muted-foreground">{t('modulePages.commands.allowedRoleIds')}</p>
|
||||||
|
<Input
|
||||||
|
value={toCsv(entry.allowedRoleIds)}
|
||||||
|
onChange={(event) => patchLocal(entry.commandName, { allowedRoleIds: fromCsv(event.target.value) })}
|
||||||
|
placeholder={t('modulePages.commands.idsPlaceholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs text-muted-foreground">{t('modulePages.commands.deniedRoleIds')}</p>
|
||||||
|
<Input
|
||||||
|
value={toCsv(entry.deniedRoleIds)}
|
||||||
|
onChange={(event) => patchLocal(entry.commandName, { deniedRoleIds: fromCsv(event.target.value) })}
|
||||||
|
placeholder={t('modulePages.commands.idsPlaceholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs text-muted-foreground">{t('modulePages.commands.allowedChannelIds')}</p>
|
||||||
|
<Input
|
||||||
|
value={toCsv(entry.allowedChannelIds)}
|
||||||
|
onChange={(event) =>
|
||||||
|
patchLocal(entry.commandName, { allowedChannelIds: fromCsv(event.target.value) })
|
||||||
|
}
|
||||||
|
placeholder={t('modulePages.commands.idsPlaceholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-xs text-muted-foreground">{t('modulePages.commands.deniedChannelIds')}</p>
|
||||||
|
<Input
|
||||||
|
value={toCsv(entry.deniedChannelIds)}
|
||||||
|
onChange={(event) =>
|
||||||
|
patchLocal(entry.commandName, { deniedChannelIds: fromCsv(event.target.value) })
|
||||||
|
}
|
||||||
|
placeholder={t('modulePages.commands.idsPlaceholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button type="button" variant="ghost" size="sm" disabled={entry.saving} onClick={() => handleReset(entry)}>
|
||||||
|
<RotateCcw className="size-4" />
|
||||||
|
{t('modulePages.commands.reset')}
|
||||||
|
</Button>
|
||||||
|
<Button type="button" variant="outline" size="sm" disabled={entry.saving} onClick={() => handleSave(entry)}>
|
||||||
|
{entry.saving ? t('common.saving') : t('common.save')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
apps/webui/src/lib/activity.ts
Normal file
31
apps/webui/src/lib/activity.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import type { ActivityStatSummary } from '@nexumi/shared';
|
||||||
|
import { prisma } from './prisma';
|
||||||
|
|
||||||
|
const SUMMARY_WINDOW_DAYS = 7;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aggregates `ActivityStat` rows (written by the bot's leveling/stats
|
||||||
|
* modules) for the last {@link SUMMARY_WINDOW_DAYS} days into totals shown
|
||||||
|
* on the guild overview page.
|
||||||
|
*/
|
||||||
|
export async function getActivitySummary(guildId: string): Promise<ActivityStatSummary> {
|
||||||
|
const since = new Date(Date.now() - SUMMARY_WINDOW_DAYS * 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const [aggregate, activeUsers] = await Promise.all([
|
||||||
|
prisma.activityStat.aggregate({
|
||||||
|
where: { guildId, day: { gte: since } },
|
||||||
|
_sum: { messageCount: true, voiceMinutes: true }
|
||||||
|
}),
|
||||||
|
prisma.activityStat.findMany({
|
||||||
|
where: { guildId, day: { gte: since } },
|
||||||
|
distinct: ['userId'],
|
||||||
|
select: { userId: true }
|
||||||
|
})
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
messageCount: aggregate._sum.messageCount ?? 0,
|
||||||
|
voiceMinutes: aggregate._sum.voiceMinutes ?? 0,
|
||||||
|
activeUserCount: activeUsers.length
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"unsavedChanges": "Du hast ungespeicherte Änderungen",
|
"unsavedChanges": "Du hast ungespeicherte Änderungen",
|
||||||
"saveSuccess": "Änderungen gespeichert",
|
"saveSuccess": "Änderungen gespeichert",
|
||||||
"saveError": "Änderungen konnten nicht gespeichert werden",
|
"saveError": "Änderungen konnten nicht gespeichert werden",
|
||||||
|
"optional": "Optional",
|
||||||
"back": "Zurück",
|
"back": "Zurück",
|
||||||
"comingSoon": "Demnächst verfügbar",
|
"comingSoon": "Demnächst verfügbar",
|
||||||
"close": "Schließen"
|
"close": "Schließen"
|
||||||
@@ -24,7 +25,8 @@
|
|||||||
"overview": "Übersicht",
|
"overview": "Übersicht",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"modules": "Module",
|
"modules": "Module",
|
||||||
"access": "Dashboard-Zugriff"
|
"access": "Dashboard-Zugriff",
|
||||||
|
"commands": "Commands"
|
||||||
},
|
},
|
||||||
"moduleGroups": {
|
"moduleGroups": {
|
||||||
"moderation": "Moderation",
|
"moderation": "Moderation",
|
||||||
@@ -83,7 +85,12 @@
|
|||||||
"recentAuditTitle": "Letzte Dashboard-Aktivität",
|
"recentAuditTitle": "Letzte Dashboard-Aktivität",
|
||||||
"auditEmpty": "Noch keine Dashboard-Änderungen.",
|
"auditEmpty": "Noch keine Dashboard-Änderungen.",
|
||||||
"auditPath": "Pfad",
|
"auditPath": "Pfad",
|
||||||
"viewAllModules": "Module verwalten"
|
"viewAllModules": "Module verwalten",
|
||||||
|
"activityTitle": "Aktivität (letzte 7 Tage)",
|
||||||
|
"activityMessages": "Nachrichten",
|
||||||
|
"activityVoiceMinutes": "Voice-Minuten",
|
||||||
|
"activityActiveUsers": "Aktive Mitglieder",
|
||||||
|
"activityHint": "Aggregiert aus der Aktivitätserfassung der Leveling- und Stats-Module."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -270,6 +277,240 @@
|
|||||||
"enabledHint": "Aktiviert Minispiele wie Trivia, Tic-Tac-Toe und 8-Ball.",
|
"enabledHint": "Aktiviert Minispiele wie Trivia, Tic-Tac-Toe und 8-Ball.",
|
||||||
"mediaEnabledLabel": "Medien-Commands aktiviert",
|
"mediaEnabledLabel": "Medien-Commands aktiviert",
|
||||||
"mediaEnabledHint": "Aktiviert API-basierte Commands wie /cat und /dog."
|
"mediaEnabledHint": "Aktiviert API-basierte Commands wie /cat und /dog."
|
||||||
|
},
|
||||||
|
"tickets": {
|
||||||
|
"title": "Tickets",
|
||||||
|
"description": "Lege fest, wie Support-Tickets erstellt und verwaltet werden.",
|
||||||
|
"enabledLabel": "Ticket-Modul aktiviert",
|
||||||
|
"mode": "Ticket-Modus",
|
||||||
|
"mode.CHANNEL": "Privater Kanal",
|
||||||
|
"mode.THREAD": "Privater Thread",
|
||||||
|
"categoryId": "Ticket-Kategorie-ID",
|
||||||
|
"logChannelId": "Log-Kanal-ID",
|
||||||
|
"inactivityHours": "Automatisch schließen nach Inaktivität (Stunden)",
|
||||||
|
"transcriptDm": "Transkript per DM an Ticket-Ersteller",
|
||||||
|
"transcriptDmHint": "Sendet dem Nutzer nach Schließung ein Transkript per DM.",
|
||||||
|
"ratingEnabled": "Nach Schließung um Bewertung bitten",
|
||||||
|
"categoriesTitle": "Ticket-Kategorien",
|
||||||
|
"categoriesDescription": "Kategorien, die Mitglieder beim Öffnen eines Tickets auswählen können.",
|
||||||
|
"categoriesEmpty": "Noch keine Ticket-Kategorien konfiguriert.",
|
||||||
|
"categoryName": "Name",
|
||||||
|
"categoryEmoji": "Emoji",
|
||||||
|
"categoryDescription": "Beschreibung",
|
||||||
|
"supportRoleIds": "Support-Rollen-IDs",
|
||||||
|
"idsPlaceholder": "Eine oder mehrere IDs, kommagetrennt",
|
||||||
|
"addCategory": "Kategorie hinzufügen",
|
||||||
|
"nameRequired": "Ein Name ist erforderlich."
|
||||||
|
},
|
||||||
|
"giveaways": {
|
||||||
|
"formIncomplete": "Bitte Kanal, Preis und Enddatum ausfüllen.",
|
||||||
|
"created": "Giveaway erstellt.",
|
||||||
|
"confirmDelete": "Dieses Giveaway wirklich löschen?",
|
||||||
|
"createTitle": "Giveaway starten",
|
||||||
|
"createDescription": "Postet eine Giveaway-Nachricht in Discord und ermittelt Gewinner automatisch bei Ablauf.",
|
||||||
|
"channelId": "Kanal-ID",
|
||||||
|
"prize": "Preis",
|
||||||
|
"winnerCount": "Anzahl Gewinner",
|
||||||
|
"endsAt": "Endet am",
|
||||||
|
"requiredRoleId": "Benötigte Rollen-ID",
|
||||||
|
"requiredLevel": "Benötigtes Level",
|
||||||
|
"requiredMemberDays": "Benötigte Mitgliedschaftsdauer (Tage)",
|
||||||
|
"create": "Giveaway starten",
|
||||||
|
"listTitle": "Giveaways",
|
||||||
|
"listDescription": "Aktive und beendete Giveaways für diesen Server.",
|
||||||
|
"empty": "Noch keine Giveaways.",
|
||||||
|
"entrants": "Teilnehmer",
|
||||||
|
"statusEnded": "Beendet",
|
||||||
|
"statusPaused": "Pausiert",
|
||||||
|
"statusActive": "Aktiv",
|
||||||
|
"endedAt": "Beendet am",
|
||||||
|
"winners": "Gewinner",
|
||||||
|
"pause": "Pausieren",
|
||||||
|
"unpause": "Fortsetzen",
|
||||||
|
"end": "Jetzt beenden"
|
||||||
|
},
|
||||||
|
"selfroles": {
|
||||||
|
"formIncomplete": "Bitte Kanal, Titel und mindestens eine Rolle ausfüllen.",
|
||||||
|
"confirmDelete": "Dieses Panel wirklich löschen?",
|
||||||
|
"createTitle": "Self-Role-Panel erstellen",
|
||||||
|
"createDescription": "Postet eine Nachricht in Discord, über die sich Mitglieder selbst Rollen zuweisen können.",
|
||||||
|
"channelId": "Kanal-ID",
|
||||||
|
"panelTitle": "Panel-Titel",
|
||||||
|
"mode": "Modus",
|
||||||
|
"modes": { "BUTTONS": "Buttons", "DROPDOWN": "Dropdown", "REACTIONS": "Reaktionen" },
|
||||||
|
"behavior": "Verhalten",
|
||||||
|
"behaviors": {
|
||||||
|
"NORMAL": "Normal (frei umschaltbar)",
|
||||||
|
"UNIQUE": "Exklusiv (nur eine Rolle aus diesem Panel)",
|
||||||
|
"VERIFY": "Verifizierungsrolle",
|
||||||
|
"TEMPORARY": "Temporär (läuft nach einer Zeit ab)"
|
||||||
|
},
|
||||||
|
"description": "Beschreibung",
|
||||||
|
"roles": "Rollen",
|
||||||
|
"rolesPlaceholder": "roleId | Label | Emoji | Beschreibung (eine pro Zeile)",
|
||||||
|
"rolesHint": "Eine Rolle pro Zeile: roleId | Label | Emoji (optional) | Beschreibung (optional).",
|
||||||
|
"create": "Panel erstellen",
|
||||||
|
"listTitle": "Self-Role-Panels",
|
||||||
|
"listDescription": "Vorhandene Panels für diesen Server.",
|
||||||
|
"empty": "Noch keine Self-Role-Panels."
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"nameRequired": "Ein Name ist erforderlich.",
|
||||||
|
"confirmDelete": "Diesen Tag wirklich löschen?",
|
||||||
|
"createTitle": "Tag erstellen",
|
||||||
|
"createDescription": "Custom Commands und Auto-Responder, ausgelöst per Name oder Stichwort.",
|
||||||
|
"name": "Name",
|
||||||
|
"responseType": "Antworttyp",
|
||||||
|
"responseTypes": { "TEXT": "Text", "EMBED": "Embed" },
|
||||||
|
"triggerWord": "Auslöse-Wort (optional)",
|
||||||
|
"content": "Inhalt",
|
||||||
|
"allowedRoleIds": "Erlaubte Rollen-IDs",
|
||||||
|
"allowedChannelIds": "Erlaubte Kanal-IDs",
|
||||||
|
"idsPlaceholder": "Eine oder mehrere IDs, kommagetrennt",
|
||||||
|
"create": "Tag erstellen",
|
||||||
|
"listTitle": "Tags",
|
||||||
|
"listDescription": "Vorhandene Tags für diesen Server.",
|
||||||
|
"empty": "Noch keine Tags."
|
||||||
|
},
|
||||||
|
"starboard": {
|
||||||
|
"title": "Starboard",
|
||||||
|
"description": "Beliebte Nachrichten in einem eigenen Kanal hervorheben.",
|
||||||
|
"enabledLabel": "Starboard aktiviert",
|
||||||
|
"channelId": "Starboard-Kanal-ID",
|
||||||
|
"emoji": "Auslöse-Emoji",
|
||||||
|
"threshold": "Stern-Schwellenwert",
|
||||||
|
"allowSelfStar": "Eigene Nachrichten dürfen selbst markiert werden",
|
||||||
|
"ignoredChannelIds": "Ignorierte Kanal-IDs",
|
||||||
|
"idsHint": "Kommagetrennte Liste von Kanal-IDs, die vom Starboard ausgeschlossen werden."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"configTitle": "Vorschläge-Einstellungen",
|
||||||
|
"configDescription": "Lege fest, wo Vorschläge gepostet und nach der Prüfung verschoben werden.",
|
||||||
|
"enabledLabel": "Vorschläge aktiviert",
|
||||||
|
"openChannelId": "Kanal-ID für offene Vorschläge",
|
||||||
|
"approvedChannelId": "Kanal-ID für angenommene Vorschläge",
|
||||||
|
"deniedChannelId": "Kanal-ID für abgelehnte Vorschläge",
|
||||||
|
"createThread": "Diskussions-Thread erstellen",
|
||||||
|
"createThreadHint": "Öffnet automatisch einen Thread unter jedem neuen Vorschlag.",
|
||||||
|
"listTitle": "Vorschläge",
|
||||||
|
"listDescription": "Von Mitgliedern eingereichte Vorschläge prüfen und moderieren.",
|
||||||
|
"statusAll": "Alle Status",
|
||||||
|
"status": {
|
||||||
|
"OPEN": "Offen",
|
||||||
|
"APPROVED": "Angenommen",
|
||||||
|
"DENIED": "Abgelehnt",
|
||||||
|
"CONSIDERED": "In Erwägung",
|
||||||
|
"IMPLEMENTED": "Umgesetzt"
|
||||||
|
},
|
||||||
|
"empty": "Keine Vorschläge gefunden.",
|
||||||
|
"staffReason": "Begründung des Teams",
|
||||||
|
"reasonPlaceholder": "Optionale Begründung, die dem Ersteller angezeigt wird",
|
||||||
|
"approve": "Annehmen",
|
||||||
|
"deny": "Ablehnen",
|
||||||
|
"consider": "In Erwägung ziehen",
|
||||||
|
"implement": "Umsetzen"
|
||||||
|
},
|
||||||
|
"birthdays": {
|
||||||
|
"title": "Geburtstage",
|
||||||
|
"description": "Geburtstage von Mitgliedern ankündigen und optional eine Geburtstagsrolle vergeben.",
|
||||||
|
"enabledLabel": "Geburtstage aktiviert",
|
||||||
|
"channelId": "Ankündigungs-Kanal-ID",
|
||||||
|
"roleId": "Geburtstagsrolle-ID",
|
||||||
|
"timezone": "Zeitzone",
|
||||||
|
"message": "Ankündigungstext",
|
||||||
|
"messagePlaceholder": "Optionale eigene Nachricht, z. B. Alles Gute zum Geburtstag {user}!"
|
||||||
|
},
|
||||||
|
"tempvoice": {
|
||||||
|
"title": "Temp-Voice",
|
||||||
|
"description": "Mitglieder, die dem Hub-Kanal beitreten, erhalten einen eigenen temporären Voice-Kanal.",
|
||||||
|
"enabledLabel": "Temp-Voice aktiviert",
|
||||||
|
"hubChannelId": "Hub-Kanal-ID",
|
||||||
|
"categoryId": "Kategorie-ID für erstellte Kanäle",
|
||||||
|
"defaultName": "Standard-Kanalname",
|
||||||
|
"defaultLimit": "Standard-Nutzerlimit",
|
||||||
|
"defaultLimitHint": "0 setzen für kein Limit."
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"title": "Server-Statistiken",
|
||||||
|
"description": "Automatisch aktualisierte Voice-Kanäle mit Live-Serverstatistiken.",
|
||||||
|
"enabledLabel": "Server-Statistiken aktiviert",
|
||||||
|
"membersChannelId": "Mitglieder-Kanal-ID",
|
||||||
|
"membersTemplate": "Mitglieder-Vorlage",
|
||||||
|
"onlineChannelId": "Online-Kanal-ID",
|
||||||
|
"onlineTemplate": "Online-Vorlage",
|
||||||
|
"boostsChannelId": "Boosts-Kanal-ID",
|
||||||
|
"boostsTemplate": "Boosts-Vorlage",
|
||||||
|
"templateHint": "Nutze {count} als Platzhalter für den Live-Wert."
|
||||||
|
},
|
||||||
|
"feeds": {
|
||||||
|
"title": "Social Feeds",
|
||||||
|
"description": "Benachrichtigungen posten, wenn neuer Inhalt auf Twitch, YouTube, RSS oder Reddit erscheint.",
|
||||||
|
"empty": "Noch keine Feeds konfiguriert.",
|
||||||
|
"addFeed": "Feed hinzufügen",
|
||||||
|
"type": "Feed-Typ",
|
||||||
|
"sourceId": "Quell-ID",
|
||||||
|
"sourceIdPlaceholder": "Kanalname, Subreddit oder Feed-URL",
|
||||||
|
"channelId": "Discord-Kanal-ID",
|
||||||
|
"rolePingId": "Zu erwähnende Rolle",
|
||||||
|
"template": "Nachrichtenvorlage",
|
||||||
|
"templatePlaceholder": "Optionale eigene Nachrichtenvorlage",
|
||||||
|
"formIncomplete": "Bitte Quelle und Kanal-ID ausfüllen.",
|
||||||
|
"confirmDelete": "Diesen Feed wirklich löschen?"
|
||||||
|
},
|
||||||
|
"scheduler": {
|
||||||
|
"createTitle": "Nachricht planen",
|
||||||
|
"createDescription": "Sende eine Nachricht einmalig zu einem bestimmten Zeitpunkt oder wiederholt per Cron-Zeitplan.",
|
||||||
|
"channelId": "Kanal-ID",
|
||||||
|
"rolePingId": "Zu erwähnende Rolle",
|
||||||
|
"cron": "Cron-Ausdruck",
|
||||||
|
"runAt": "Ausführen am",
|
||||||
|
"content": "Nachrichtentext",
|
||||||
|
"cronHint": "Cron-Ausdruck für wiederkehrende Nachrichten ODER ein einmaliges Datum/Uhrzeit angeben - nicht beides.",
|
||||||
|
"create": "Nachricht planen",
|
||||||
|
"created": "Nachricht geplant.",
|
||||||
|
"formIncomplete": "Bitte Kanal, Inhalt sowie Cron-Ausdruck oder Datum/Uhrzeit ausfüllen.",
|
||||||
|
"confirmDelete": "Diesen Zeitplan wirklich löschen?",
|
||||||
|
"listTitle": "Geplante Nachrichten",
|
||||||
|
"listDescription": "Anstehende und wiederkehrende geplante Nachrichten.",
|
||||||
|
"empty": "Noch keine geplanten Nachrichten.",
|
||||||
|
"noContent": "(nur Embed)"
|
||||||
|
},
|
||||||
|
"guildbackup": {
|
||||||
|
"createTitle": "Backup erstellen",
|
||||||
|
"createDescription": "Erstellt einen Snapshot von Rollen, Kanälen und Servereinstellungen für spätere Wiederherstellung.",
|
||||||
|
"name": "Backup-Name",
|
||||||
|
"namePlaceholder": "z. B. Vor Season-3-Update",
|
||||||
|
"create": "Backup erstellen",
|
||||||
|
"created": "Backup erstellt.",
|
||||||
|
"nameRequired": "Ein Name ist erforderlich.",
|
||||||
|
"listTitle": "Backups",
|
||||||
|
"listDescription": "Vorhandene Backups für diesen Server.",
|
||||||
|
"empty": "Noch keine Backups.",
|
||||||
|
"info": "Info",
|
||||||
|
"infoUnavailable": "Backup-Details nicht verfügbar.",
|
||||||
|
"infoGuildName": "Servername",
|
||||||
|
"infoRoles": "Rollen",
|
||||||
|
"infoChannels": "Kanäle",
|
||||||
|
"download": "JSON herunterladen",
|
||||||
|
"restore": "Wiederherstellen",
|
||||||
|
"confirmRestoreStep1": "Dieses Backup wiederherstellen? Fehlende Rollen/Kanäle werden ergänzt und Overwrites best-effort angewendet. Bestehende Kanäle werden nicht gelöscht.",
|
||||||
|
"confirmRestoreStep2": "Letzte Bestätigung: Dieses Backup jetzt wiederherstellen? Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||||
|
"restoreQueued": "Wiederherstellung eingereiht - läuft im Hintergrund.",
|
||||||
|
"confirmDelete": "Dieses Backup wirklich löschen?",
|
||||||
|
"ownerOnlyBadge": "Wiederherstellung nur durch Owner"
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"title": "Commands",
|
||||||
|
"description": "Einzelne Slash-Commands aktivieren/deaktivieren und per Rolle, Kanal oder Cooldown einschränken.",
|
||||||
|
"searchPlaceholder": "Commands durchsuchen…",
|
||||||
|
"empty": "Keine Commands entsprechen der Suche.",
|
||||||
|
"cooldownSeconds": "Cooldown (Sekunden)",
|
||||||
|
"allowedRoleIds": "Erlaubte Rollen-IDs",
|
||||||
|
"deniedRoleIds": "Verbotene Rollen-IDs",
|
||||||
|
"allowedChannelIds": "Erlaubte Kanal-IDs",
|
||||||
|
"deniedChannelIds": "Verbotene Kanal-IDs",
|
||||||
|
"idsPlaceholder": "Eine oder mehrere IDs, kommagetrennt",
|
||||||
|
"reset": "Auf Standard zurücksetzen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userMenu": {
|
"userMenu": {
|
||||||
|
|||||||
@@ -17,14 +17,16 @@
|
|||||||
"saveError": "Could not save changes",
|
"saveError": "Could not save changes",
|
||||||
"back": "Back",
|
"back": "Back",
|
||||||
"comingSoon": "Coming soon",
|
"comingSoon": "Coming soon",
|
||||||
"close": "Close"
|
"close": "Close",
|
||||||
|
"optional": "Optional"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"dashboard": "Dashboard",
|
"dashboard": "Dashboard",
|
||||||
"overview": "Overview",
|
"overview": "Overview",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"modules": "Modules",
|
"modules": "Modules",
|
||||||
"access": "Dashboard access"
|
"access": "Dashboard access",
|
||||||
|
"commands": "Commands"
|
||||||
},
|
},
|
||||||
"moduleGroups": {
|
"moduleGroups": {
|
||||||
"moderation": "Moderation",
|
"moderation": "Moderation",
|
||||||
@@ -83,7 +85,12 @@
|
|||||||
"recentAuditTitle": "Recent dashboard activity",
|
"recentAuditTitle": "Recent dashboard activity",
|
||||||
"auditEmpty": "No dashboard changes yet.",
|
"auditEmpty": "No dashboard changes yet.",
|
||||||
"auditPath": "Path",
|
"auditPath": "Path",
|
||||||
"viewAllModules": "Manage modules"
|
"viewAllModules": "Manage modules",
|
||||||
|
"activityTitle": "Activity (last 7 days)",
|
||||||
|
"activityMessages": "Messages",
|
||||||
|
"activityVoiceMinutes": "Voice minutes",
|
||||||
|
"activityActiveUsers": "Active members",
|
||||||
|
"activityHint": "Aggregated from the leveling and stats modules' activity tracking."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -270,6 +277,240 @@
|
|||||||
"enabledHint": "Enables mini games like trivia, tic-tac-toe and 8-ball.",
|
"enabledHint": "Enables mini games like trivia, tic-tac-toe and 8-ball.",
|
||||||
"mediaEnabledLabel": "Media commands enabled",
|
"mediaEnabledLabel": "Media commands enabled",
|
||||||
"mediaEnabledHint": "Enables API-based commands like /cat and /dog."
|
"mediaEnabledHint": "Enables API-based commands like /cat and /dog."
|
||||||
|
},
|
||||||
|
"tickets": {
|
||||||
|
"title": "Tickets",
|
||||||
|
"description": "Configure how support tickets are created and managed.",
|
||||||
|
"enabledLabel": "Tickets module enabled",
|
||||||
|
"mode": "Ticket mode",
|
||||||
|
"mode.CHANNEL": "Private channel",
|
||||||
|
"mode.THREAD": "Private thread",
|
||||||
|
"categoryId": "Ticket category ID",
|
||||||
|
"logChannelId": "Log channel ID",
|
||||||
|
"inactivityHours": "Auto-close after inactivity (hours)",
|
||||||
|
"transcriptDm": "DM transcript to ticket creator",
|
||||||
|
"transcriptDmHint": "Sends a transcript to the user via DM once their ticket is closed.",
|
||||||
|
"ratingEnabled": "Ask for a rating after closing",
|
||||||
|
"categoriesTitle": "Ticket categories",
|
||||||
|
"categoriesDescription": "Categories members can choose from when opening a ticket.",
|
||||||
|
"categoriesEmpty": "No ticket categories configured yet.",
|
||||||
|
"categoryName": "Name",
|
||||||
|
"categoryEmoji": "Emoji",
|
||||||
|
"categoryDescription": "Description",
|
||||||
|
"supportRoleIds": "Support role IDs",
|
||||||
|
"idsPlaceholder": "One or more IDs, comma-separated",
|
||||||
|
"addCategory": "Add category",
|
||||||
|
"nameRequired": "A name is required."
|
||||||
|
},
|
||||||
|
"giveaways": {
|
||||||
|
"formIncomplete": "Please fill in the channel, prize and end date.",
|
||||||
|
"created": "Giveaway created.",
|
||||||
|
"confirmDelete": "Really delete this giveaway?",
|
||||||
|
"createTitle": "Start a giveaway",
|
||||||
|
"createDescription": "Posts a giveaway message in Discord and picks winners automatically when it ends.",
|
||||||
|
"channelId": "Channel ID",
|
||||||
|
"prize": "Prize",
|
||||||
|
"winnerCount": "Winner count",
|
||||||
|
"endsAt": "Ends at",
|
||||||
|
"requiredRoleId": "Required role ID",
|
||||||
|
"requiredLevel": "Required level",
|
||||||
|
"requiredMemberDays": "Required membership (days)",
|
||||||
|
"create": "Start giveaway",
|
||||||
|
"listTitle": "Giveaways",
|
||||||
|
"listDescription": "Active and ended giveaways for this server.",
|
||||||
|
"empty": "No giveaways yet.",
|
||||||
|
"entrants": "Entrants",
|
||||||
|
"statusEnded": "Ended",
|
||||||
|
"statusPaused": "Paused",
|
||||||
|
"statusActive": "Active",
|
||||||
|
"endedAt": "Ended at",
|
||||||
|
"winners": "Winners",
|
||||||
|
"pause": "Pause",
|
||||||
|
"unpause": "Resume",
|
||||||
|
"end": "End now"
|
||||||
|
},
|
||||||
|
"selfroles": {
|
||||||
|
"formIncomplete": "Please fill in the channel, title and at least one role.",
|
||||||
|
"confirmDelete": "Really delete this panel?",
|
||||||
|
"createTitle": "Create a self-role panel",
|
||||||
|
"createDescription": "Posts a message in Discord that members can use to assign themselves roles.",
|
||||||
|
"channelId": "Channel ID",
|
||||||
|
"panelTitle": "Panel title",
|
||||||
|
"mode": "Mode",
|
||||||
|
"modes": { "BUTTONS": "Buttons", "DROPDOWN": "Dropdown", "REACTIONS": "Reactions" },
|
||||||
|
"behavior": "Behavior",
|
||||||
|
"behaviors": {
|
||||||
|
"NORMAL": "Normal (toggle freely)",
|
||||||
|
"UNIQUE": "Unique (only one role from this panel)",
|
||||||
|
"VERIFY": "Verification role",
|
||||||
|
"TEMPORARY": "Temporary (expires after a while)"
|
||||||
|
},
|
||||||
|
"description": "Description",
|
||||||
|
"roles": "Roles",
|
||||||
|
"rolesPlaceholder": "roleId | label | emoji | description (one per line)",
|
||||||
|
"rolesHint": "One role per line: roleId | label | emoji (optional) | description (optional).",
|
||||||
|
"create": "Create panel",
|
||||||
|
"listTitle": "Self-role panels",
|
||||||
|
"listDescription": "Existing panels for this server.",
|
||||||
|
"empty": "No self-role panels yet."
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"nameRequired": "A name is required.",
|
||||||
|
"confirmDelete": "Really delete this tag?",
|
||||||
|
"createTitle": "Create a tag",
|
||||||
|
"createDescription": "Custom commands and auto-responders triggered by name or keyword.",
|
||||||
|
"name": "Name",
|
||||||
|
"responseType": "Response type",
|
||||||
|
"responseTypes": { "TEXT": "Text", "EMBED": "Embed" },
|
||||||
|
"triggerWord": "Trigger word (optional)",
|
||||||
|
"content": "Content",
|
||||||
|
"allowedRoleIds": "Allowed role IDs",
|
||||||
|
"allowedChannelIds": "Allowed channel IDs",
|
||||||
|
"idsPlaceholder": "One or more IDs, comma-separated",
|
||||||
|
"create": "Create tag",
|
||||||
|
"listTitle": "Tags",
|
||||||
|
"listDescription": "Existing tags for this server.",
|
||||||
|
"empty": "No tags yet."
|
||||||
|
},
|
||||||
|
"starboard": {
|
||||||
|
"title": "Starboard",
|
||||||
|
"description": "Highlight popular messages in a dedicated channel.",
|
||||||
|
"enabledLabel": "Starboard enabled",
|
||||||
|
"channelId": "Starboard channel ID",
|
||||||
|
"emoji": "Trigger emoji",
|
||||||
|
"threshold": "Star threshold",
|
||||||
|
"allowSelfStar": "Allow starring your own messages",
|
||||||
|
"ignoredChannelIds": "Ignored channel IDs",
|
||||||
|
"idsHint": "Comma-separated list of channel IDs to exclude from the starboard."
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"configTitle": "Suggestions settings",
|
||||||
|
"configDescription": "Configure where suggestions are posted and moved once reviewed.",
|
||||||
|
"enabledLabel": "Suggestions enabled",
|
||||||
|
"openChannelId": "Open channel ID",
|
||||||
|
"approvedChannelId": "Approved channel ID",
|
||||||
|
"deniedChannelId": "Denied channel ID",
|
||||||
|
"createThread": "Create a discussion thread",
|
||||||
|
"createThreadHint": "Automatically opens a thread under every new suggestion.",
|
||||||
|
"listTitle": "Suggestions",
|
||||||
|
"listDescription": "Review and moderate suggestions submitted by members.",
|
||||||
|
"statusAll": "All statuses",
|
||||||
|
"status": {
|
||||||
|
"OPEN": "Open",
|
||||||
|
"APPROVED": "Approved",
|
||||||
|
"DENIED": "Denied",
|
||||||
|
"CONSIDERED": "Considered",
|
||||||
|
"IMPLEMENTED": "Implemented"
|
||||||
|
},
|
||||||
|
"empty": "No suggestions found.",
|
||||||
|
"staffReason": "Staff reason",
|
||||||
|
"reasonPlaceholder": "Optional reason shown to the author",
|
||||||
|
"approve": "Approve",
|
||||||
|
"deny": "Deny",
|
||||||
|
"consider": "Consider",
|
||||||
|
"implement": "Implement"
|
||||||
|
},
|
||||||
|
"birthdays": {
|
||||||
|
"title": "Birthdays",
|
||||||
|
"description": "Announce member birthdays and optionally assign a birthday role.",
|
||||||
|
"enabledLabel": "Birthdays enabled",
|
||||||
|
"channelId": "Announcement channel ID",
|
||||||
|
"roleId": "Birthday role ID",
|
||||||
|
"timezone": "Timezone",
|
||||||
|
"message": "Announcement message",
|
||||||
|
"messagePlaceholder": "Optional custom message, e.g. Happy birthday {user}!"
|
||||||
|
},
|
||||||
|
"tempvoice": {
|
||||||
|
"title": "Temp Voice",
|
||||||
|
"description": "Members joining the hub channel get their own temporary voice channel.",
|
||||||
|
"enabledLabel": "Temp voice enabled",
|
||||||
|
"hubChannelId": "Hub channel ID",
|
||||||
|
"categoryId": "Category ID for created channels",
|
||||||
|
"defaultName": "Default channel name",
|
||||||
|
"defaultLimit": "Default user limit",
|
||||||
|
"defaultLimitHint": "Set to 0 for no limit."
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"title": "Server Stats",
|
||||||
|
"description": "Auto-updating voice channels showing live server statistics.",
|
||||||
|
"enabledLabel": "Server stats enabled",
|
||||||
|
"membersChannelId": "Members channel ID",
|
||||||
|
"membersTemplate": "Members template",
|
||||||
|
"onlineChannelId": "Online channel ID",
|
||||||
|
"onlineTemplate": "Online template",
|
||||||
|
"boostsChannelId": "Boosts channel ID",
|
||||||
|
"boostsTemplate": "Boosts template",
|
||||||
|
"templateHint": "Use {count} as a placeholder for the live value."
|
||||||
|
},
|
||||||
|
"feeds": {
|
||||||
|
"title": "Social Feeds",
|
||||||
|
"description": "Post notifications when new content is published on Twitch, YouTube, RSS or Reddit.",
|
||||||
|
"empty": "No feeds configured yet.",
|
||||||
|
"addFeed": "Add feed",
|
||||||
|
"type": "Feed type",
|
||||||
|
"sourceId": "Source ID",
|
||||||
|
"sourceIdPlaceholder": "Channel name, subreddit or feed URL",
|
||||||
|
"channelId": "Discord channel ID",
|
||||||
|
"rolePingId": "Role to ping",
|
||||||
|
"template": "Message template",
|
||||||
|
"templatePlaceholder": "Optional custom message template",
|
||||||
|
"formIncomplete": "Please fill in the source and channel ID.",
|
||||||
|
"confirmDelete": "Really delete this feed?"
|
||||||
|
},
|
||||||
|
"scheduler": {
|
||||||
|
"createTitle": "Schedule a message",
|
||||||
|
"createDescription": "Send a message once at a specific time or repeatedly on a cron schedule.",
|
||||||
|
"channelId": "Channel ID",
|
||||||
|
"rolePingId": "Role to ping",
|
||||||
|
"cron": "Cron expression",
|
||||||
|
"runAt": "Run at",
|
||||||
|
"content": "Message content",
|
||||||
|
"cronHint": "Set a cron expression for recurring messages, or a one-time date/time - not both.",
|
||||||
|
"create": "Schedule message",
|
||||||
|
"created": "Message scheduled.",
|
||||||
|
"formIncomplete": "Please fill in the channel, content and either a cron expression or a date/time.",
|
||||||
|
"confirmDelete": "Really delete this schedule?",
|
||||||
|
"listTitle": "Scheduled messages",
|
||||||
|
"listDescription": "Upcoming and recurring scheduled messages.",
|
||||||
|
"empty": "No scheduled messages yet.",
|
||||||
|
"noContent": "(embed only)"
|
||||||
|
},
|
||||||
|
"guildbackup": {
|
||||||
|
"createTitle": "Create a backup",
|
||||||
|
"createDescription": "Snapshots roles, channels and server settings for later restore.",
|
||||||
|
"name": "Backup name",
|
||||||
|
"namePlaceholder": "e.g. Before season 3 update",
|
||||||
|
"create": "Create backup",
|
||||||
|
"created": "Backup created.",
|
||||||
|
"nameRequired": "A name is required.",
|
||||||
|
"listTitle": "Backups",
|
||||||
|
"listDescription": "Existing backups for this server.",
|
||||||
|
"empty": "No backups yet.",
|
||||||
|
"info": "Info",
|
||||||
|
"infoUnavailable": "Backup details are unavailable.",
|
||||||
|
"infoGuildName": "Server name",
|
||||||
|
"infoRoles": "Roles",
|
||||||
|
"infoChannels": "Channels",
|
||||||
|
"download": "Download JSON",
|
||||||
|
"restore": "Restore",
|
||||||
|
"confirmRestoreStep1": "Restore this backup? Missing roles/channels will be added and overwrites applied best-effort. Existing channels will not be deleted.",
|
||||||
|
"confirmRestoreStep2": "Final confirmation: restore this backup now? This cannot be undone.",
|
||||||
|
"restoreQueued": "Restore queued - it will run in the background.",
|
||||||
|
"confirmDelete": "Really delete this backup?",
|
||||||
|
"ownerOnlyBadge": "Owner-only restore"
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"title": "Commands",
|
||||||
|
"description": "Enable/disable individual slash commands and restrict them by role, channel or cooldown.",
|
||||||
|
"searchPlaceholder": "Search commands…",
|
||||||
|
"empty": "No commands match your search.",
|
||||||
|
"cooldownSeconds": "Cooldown (seconds)",
|
||||||
|
"allowedRoleIds": "Allowed role IDs",
|
||||||
|
"deniedRoleIds": "Denied role IDs",
|
||||||
|
"allowedChannelIds": "Allowed channel IDs",
|
||||||
|
"deniedChannelIds": "Denied channel IDs",
|
||||||
|
"idsPlaceholder": "One or more IDs, comma-separated",
|
||||||
|
"reset": "Reset to default"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"userMenu": {
|
"userMenu": {
|
||||||
|
|||||||
Reference in New Issue
Block a user