Update README and moderation commands for localization and migration support

- Revised README to include database migration steps and a new section for database management.
- Enhanced moderation command responses to utilize localized messages for unknown commands.
- Refactored moderation commands to use centralized command data definitions, improving maintainability and consistency.
- Added localization support for command descriptions and options in both German and English.
- Versioned Prisma migrations for better tracking of database changes.
This commit is contained in:
smueller
2026-07-22 12:06:16 +02:00
parent 405b70bdfc
commit a44f4d6641
12 changed files with 784 additions and 164 deletions

View File

@@ -0,0 +1,220 @@
export const commandLocales = {
'moderation.ban.description': {
de: 'Bannt einen Nutzer',
en: 'Ban a user'
},
'moderation.ban.options.user': {
de: 'Zielnutzer',
en: 'Target user'
},
'moderation.ban.options.reason': {
de: 'Grund',
en: 'Reason'
},
'moderation.ban.options.duration': {
de: 'Optionale Temp-Ban-Dauer (z. B. 7d)',
en: 'Optional temp-ban duration (e.g. 7d)'
},
'moderation.unban.description': {
de: 'Entbannt einen Nutzer',
en: 'Unban a user'
},
'moderation.unban.options.user_id': {
de: 'Discord-Nutzer-ID',
en: 'Discord user id'
},
'moderation.kick.description': {
de: 'Kickt einen Nutzer',
en: 'Kick a user'
},
'moderation.timeout.description': {
de: 'Timeout für einen Nutzer',
en: 'Timeout a user'
},
'moderation.timeout.options.duration': {
de: 'Dauer (z. B. 15m)',
en: 'Duration (e.g. 15m)'
},
'moderation.untimeout.description': {
de: 'Entfernt den Timeout eines Nutzers',
en: 'Remove timeout from a user'
},
'moderation.warn.description': {
de: 'Verwarnungen verwalten',
en: 'Manage user warnings'
},
'moderation.warn.add.description': {
de: 'Verwarnung hinzufügen',
en: 'Add warning'
},
'moderation.warn.list.description': {
de: 'Verwarnungen anzeigen',
en: 'List warnings'
},
'moderation.warn.remove.description': {
de: 'Verwarnung entfernen',
en: 'Remove warning'
},
'moderation.warn.clear.description': {
de: 'Verwarnungen eines Nutzers löschen',
en: 'Clear warnings for user'
},
'moderation.warn.escalation_set.description': {
de: 'Eskalationsregel erstellen oder aktualisieren',
en: 'Create or update escalation rule'
},
'moderation.warn.escalation_list.description': {
de: 'Eskalationsregeln anzeigen',
en: 'List escalation rules'
},
'moderation.warn.escalation_remove.description': {
de: 'Eskalationsregel entfernen',
en: 'Remove escalation rule'
},
'moderation.warn.options.count': {
de: 'Schwellenwert für Verwarnungen',
en: 'Warning count threshold'
},
'moderation.warn.options.action': {
de: 'Eskalationsaktion',
en: 'Escalation action'
},
'moderation.warn.options.escalation_duration': {
de: 'Timeout-Dauer (für TIMEOUT erforderlich)',
en: 'Timeout duration (required for TIMEOUT)'
},
'moderation.warn.options.escalation_reason': {
de: 'Optionaler Eskalationsgrund',
en: 'Optional escalation reason'
},
'moderation.warn.options.warning_id': {
de: 'Verwarnungs-ID',
en: 'Warning ID'
},
'moderation.choice.timeout': {
de: 'Timeout',
en: 'timeout'
},
'moderation.choice.kick': {
de: 'Kick',
en: 'kick'
},
'moderation.choice.ban': {
de: 'Ban',
en: 'ban'
},
'moderation.purge.description': {
de: 'Nachrichten massenhaft löschen',
en: 'Bulk delete messages'
},
'moderation.purge.options.amount': {
de: '2-100',
en: '2-100'
},
'moderation.purge.options.user': {
de: 'Nur Nachrichten dieses Nutzers löschen',
en: 'Only purge this user messages'
},
'moderation.purge.options.bots': {
de: 'Nur Bot-Nachrichten löschen',
en: 'Only purge bot messages'
},
'moderation.purge.options.links': {
de: 'Nur Nachrichten mit Links löschen',
en: 'Only purge messages containing links'
},
'moderation.purge.options.attachments': {
de: 'Nur Nachrichten mit Anhängen löschen',
en: 'Only purge messages with attachments'
},
'moderation.purge.options.regex': {
de: 'Nur Nachrichten per Regex löschen',
en: 'Only purge messages matching regex'
},
'moderation.slowmode.description': {
de: 'Slowmode für den Kanal setzen',
en: 'Set channel slowmode'
},
'moderation.slowmode.options.seconds': {
de: '0-21600',
en: '0-21600'
},
'moderation.lock.description': {
de: 'Aktuellen Textkanal sperren',
en: 'Lock current text channel'
},
'moderation.unlock.description': {
de: 'Aktuellen Textkanal entsperren',
en: 'Unlock current text channel'
},
'moderation.nick.description': {
de: 'Nicknames verwalten',
en: 'Manage nicknames'
},
'moderation.nick.set.description': {
de: 'Nickname setzen',
en: 'Set nickname'
},
'moderation.nick.reset.description': {
de: 'Nickname zurücksetzen',
en: 'Reset nickname'
},
'moderation.nick.options.nickname': {
de: 'Neuer Nickname',
en: 'New nickname'
},
'moderation.case.description': {
de: 'Moderationsfälle verwalten',
en: 'Manage moderation cases'
},
'moderation.case.view.description': {
de: 'Fall anzeigen',
en: 'View case'
},
'moderation.case.edit.description': {
de: 'Fallgrund bearbeiten',
en: 'Edit case reason'
},
'moderation.case.delete.description': {
de: 'Fall soft-löschen',
en: 'Soft-delete case'
},
'moderation.case.options.id': {
de: 'Fallnummer',
en: 'Case number'
},
'moderation.case.options.reason': {
de: 'Neuer Grund',
en: 'New reason'
},
'moderation.modnote.description': {
de: 'Mod-Notizen verwalten',
en: 'Manage mod notes'
},
'moderation.modnote.add.description': {
de: 'Notiz hinzufügen',
en: 'Add note'
},
'moderation.modnote.list.description': {
de: 'Notizen anzeigen',
en: 'List notes'
},
'moderation.modnote.options.note': {
de: 'Notiz',
en: 'Note'
},
'moderation.common.options.user': {
de: 'Zielnutzer',
en: 'Target user'
},
'moderation.common.options.reason': {
de: 'Grund',
en: 'Reason'
},
'moderation.common.options.target': {
de: 'Ziel',
en: 'Target'
}
} as const;
export type CommandLocaleKey = keyof typeof commandLocales;

View File

@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import { commandLocale, localizedChoice } from './discord-localization.js';
describe('discord localization helpers', () => {
it('maps command locales to Discord localization keys', () => {
const locale = commandLocale('moderation.ban.description');
expect(locale.description).toBe('Ban a user');
expect(locale.descriptionLocalizations.de).toBe('Bannt einen Nutzer');
expect(locale.descriptionLocalizations['en-US']).toBe('Ban a user');
});
it('maps choice labels with localizations', () => {
const choice = localizedChoice('moderation.choice.ban', 'BAN');
expect(choice.value).toBe('BAN');
expect(choice.name_localizations?.de).toBe('Ban');
});
});

View File

@@ -0,0 +1,48 @@
import { commandLocales, type CommandLocaleKey } from './command-locales.js';
export function commandLocale(key: CommandLocaleKey): {
description: string;
descriptionLocalizations: Record<string, string>;
} {
const entry = commandLocales[key];
return {
description: entry.en,
descriptionLocalizations: {
de: entry.de,
'en-US': entry.en
}
};
}
export function localizedChoice(key: CommandLocaleKey, value: string) {
const entry = commandLocales[key];
return {
name: entry.en,
name_localizations: {
de: entry.de,
'en-US': entry.en
},
value
};
}
export function applyCommandDescription<
T extends {
setDescription: (description: string) => T;
setDescriptionLocalizations: (localizations: Record<string, string>) => T;
}
>(builder: T, key: CommandLocaleKey): T {
const locale = commandLocale(key);
return builder
.setDescription(locale.description)
.setDescriptionLocalizations(locale.descriptionLocalizations);
}
export function applyOptionDescription<
T extends {
setDescription: (description: string) => T;
setDescriptionLocalizations: (localizations: Record<string, string>) => T;
}
>(option: T, key: CommandLocaleKey): T {
return applyCommandDescription(option, key);
}

View File

@@ -1,6 +1,8 @@
import { z } from 'zod';
export * from './audit.js';
export * from './command-locales.js';
export * from './discord-localization.js';
export const LocaleSchema = z.enum(['de', 'en']);
export type Locale = z.infer<typeof LocaleSchema>;
@@ -40,6 +42,7 @@ const de: Dictionary = {
'generic.botMissingPermission': 'Dem Bot fehlt die erforderliche Berechtigung ({permission}).',
'generic.invalidRegex': 'Ungültiges Regex-Muster.',
'generic.error': 'Es ist ein Fehler aufgetreten.',
'generic.unknownCommand': 'Unbekannter Befehl.',
'moderation.success': 'Aktion erfolgreich ausgeführt.',
'moderation.reason.none': 'Kein Grund angegeben',
'moderation.warning.created': 'Verwarnung erstellt: {warningId}',
@@ -71,6 +74,7 @@ const en: Dictionary = {
'generic.botMissingPermission': 'Bot lacks required permission ({permission}).',
'generic.invalidRegex': 'Invalid regex pattern.',
'generic.error': 'An error occurred.',
'generic.unknownCommand': 'Unknown command.',
'moderation.success': 'Action executed successfully.',
'moderation.reason.none': 'No reason provided',
'moderation.warning.created': 'Warning created: {warningId}',