Add giveaway, ticket, self-role, starboard, and suggestion features to the bot

- Introduced new models in the Prisma schema for giveaways, tickets, self-role panels, starboard configurations, and suggestions, enhancing the bot's functionality.
- Implemented job handling for self-role expiration and ticket management, improving user experience.
- Updated command localization to support new features in both German and English.
- Enhanced the job processing system to include dedicated workers for managing tickets and self-role expirations.
- Documented the new features and their setup processes in PHASE-TRACKING.md for clarity on implementation steps.
This commit is contained in:
smueller
2026-07-22 13:08:37 +02:00
parent f2f856628d
commit a583db7a15
50 changed files with 7820 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ export * from './command-locales.js';
export * from './discord-localization.js';
export * from './phase2.js';
export * from './phase3.js';
export * from './phase4.js';
export const LocaleSchema = z.enum(['de', 'en']);
export type Locale = z.infer<typeof LocaleSchema>;
@@ -238,7 +239,204 @@ const de: Dictionary = {
'fun.media.disabled': 'Medien-Commands sind auf diesem Server deaktiviert.',
'fun.media.fetchFailed': 'Bild konnte nicht geladen werden. Versuche es später erneut.',
'fun.cat.title': 'Random Cat',
'fun.dog.title': 'Random Dog'
'fun.dog.title': 'Random Dog',
'starboard.notConfigured': 'Starboard ist noch nicht konfiguriert. Nutze `/starboard setup`.',
'starboard.invalidChannel': 'Der Starboard-Kanal ist ungültig.',
'starboard.setupDone': 'Starboard konfiguriert.',
'starboard.noContent': '*Kein Textinhalt*',
'starboard.field.stars': 'Sterne',
'starboard.field.source': 'Quelle',
'starboard.jumpLink': 'Zur Nachricht',
'suggestion.created': 'Vorschlag eingereicht (ID: `{id}`).',
'suggestion.setupDone': 'Vorschlagssystem konfiguriert.',
'suggestion.staffUpdated': 'Vorschlag aktualisiert ({status}).',
'suggestion.votedUp': 'Upvote gezählt.',
'suggestion.votedDown': 'Downvote gezählt.',
'suggestion.threadName': 'Diskussion #{id}',
'suggestion.error.not_configured': 'Vorschläge sind nicht konfiguriert. Nutze `/suggestion setup`.',
'suggestion.error.invalid_channel': 'Der konfigurierte Kanal ist ungültig.',
'suggestion.error.not_found': 'Vorschlag nicht gefunden.',
'suggestion.error.not_open': 'Dieser Vorschlag ist nicht mehr offen.',
'suggestion.error.already_voted': 'Du hast bereits so abgestimmt.',
'suggestion.embed.title': 'Vorschlag #{id}',
'suggestion.embed.status': 'Status',
'suggestion.embed.votes': 'Stimmen',
'suggestion.embed.author': 'Von {author}',
'suggestion.embed.staffReason': 'Team-Begründung',
'suggestion.status.open': 'Offen',
'suggestion.status.approved': 'Angenommen',
'suggestion.status.denied': 'Abgelehnt',
'suggestion.status.considered': 'In Prüfung',
'suggestion.status.implemented': 'Umgesetzt',
'suggestion.dm.statusUpdate': 'Dein Vorschlag wurde aktualisiert: **{status}**\nBegründung: {reason}',
'giveaway.embed.title': 'Giveaway',
'giveaway.embed.prize': 'Preis',
'giveaway.embed.winners': 'Gewinner',
'giveaway.embed.entrants': 'Teilnehmer',
'giveaway.embed.host': 'Host',
'giveaway.embed.requirements': 'Anforderungen',
'giveaway.embed.active': 'Klicke auf **Join**, um teilzunehmen!',
'giveaway.embed.paused': '⏸️ Dieses Giveaway ist pausiert.',
'giveaway.embed.ended': 'Beendet',
'giveaway.embed.endedWinners': 'Gewinner: {winners}',
'giveaway.embed.endedNoWinners': 'Keine gültigen Teilnehmer.',
'giveaway.embed.endsAt': 'Endet {time}',
'giveaway.button.join': 'Teilnehmen',
'giveaway.requirement.role': 'Rolle: {role}',
'giveaway.requirement.level': 'Mindest-Level: {level}',
'giveaway.requirement.memberDays': 'Server-Mitglied seit mindestens {days} Tagen',
'giveaway.started': 'Giveaway gestartet (ID: `{id}`).',
'giveaway.ended': 'Giveaway beendet.',
'giveaway.rerolled': 'Neue Gewinner: {winners}',
'giveaway.deleted': 'Giveaway gelöscht.',
'giveaway.paused': 'Giveaway pausiert.',
'giveaway.resumed': 'Giveaway fortgesetzt.',
'giveaway.joined': 'Du nimmst am Giveaway teil!',
'giveaway.list.header': '**Aktive Giveaways**',
'giveaway.list.empty': 'Keine aktiven Giveaways.',
'giveaway.list.entrants': 'Teilnehmer',
'giveaway.dm.won': 'Glückwunsch! Du hast **{prize}** auf **{guild}** gewonnen!',
'giveaway.error.not_found': 'Giveaway nicht gefunden oder bereits beendet.',
'giveaway.error.not_ended': 'Giveaway ist noch nicht beendet.',
'giveaway.error.paused': 'Dieses Giveaway ist pausiert.',
'giveaway.error.already_joined': 'Du nimmst bereits teil.',
'giveaway.error.member_not_found': 'Mitglied nicht gefunden.',
'giveaway.error.missing_role': 'Dir fehlt die erforderliche Rolle.',
'giveaway.error.insufficient_level': 'Dein Level ist zu niedrig.',
'giveaway.error.insufficient_member_days': 'Du bist noch nicht lange genug auf dem Server.',
'giveaway.error.invalid_duration': 'Ungültige Dauer. Nutze z. B. 1h, 2d.',
'giveaway.error.duration_too_short': 'Die Dauer muss mindestens 1 Minute betragen.',
'giveaway.error.invalid_channel': 'Ungültiger Kanal.',
'ticket.panel.defaultDescription': 'Wähle eine Kategorie, um ein Ticket zu öffnen.',
'ticket.panel.created': 'Ticket-Panel erstellt.',
'ticket.category.created': 'Kategorie **{name}** erstellt.',
'ticket.created': 'Dein Ticket wurde erstellt: {channel}',
'ticket.opened': '{user} hat ein Ticket geöffnet.',
'ticket.subjectLine': 'Betreff: {subject}',
'ticket.claimed': '{user} hat dieses Ticket übernommen.',
'ticket.userAdded': '{user} wurde zum Ticket hinzugefügt.',
'ticket.userRemoved': '{user} wurde aus dem Ticket entfernt.',
'ticket.renamed': 'Ticket umbenannt in **{name}**.',
'ticket.prioritySet': 'Priorität gesetzt: **{priority}**',
'ticket.closed': 'Ticket geschlossen von {user}.',
'ticket.closedWithReason': 'Ticket geschlossen von {user}.\nGrund: {reason}',
'ticket.close.success': 'Ticket geschlossen.',
'ticket.claim.success': 'Ticket übernommen.',
'ticket.add.success': '{user} hinzugefügt.',
'ticket.remove.success': '{user} entfernt.',
'ticket.rename.success': 'Ticket umbenannt in **{name}**.',
'ticket.priority.success': 'Priorität: **{priority}**',
'ticket.transcriptDm': 'Hier ist dein Ticket-Transkript.',
'ticket.ratePrompt': 'Wie zufrieden warst du mit dem Support? (15)',
'ticket.rated': 'Danke für deine Bewertung: {rating}/5',
'ticket.autoCloseReason': 'Automatisch wegen Inaktivität geschlossen',
'ticket.modal.subject': 'Betreff',
'ticket.log.closed': 'Ticket geschlossen',
'ticket.log.opener': 'Ersteller',
'ticket.log.closedBy': 'Geschlossen von',
'ticket.priority.LOW': 'Niedrig',
'ticket.priority.NORMAL': 'Normal',
'ticket.priority.HIGH': 'Hoch',
'ticket.priority.URGENT': 'Dringend',
'ticket.error.disabled': 'Das Ticket-System ist deaktiviert.',
'ticket.error.category_not_found': 'Kategorie nicht gefunden.',
'ticket.error.no_categories': 'Keine Ticket-Kategorien vorhanden.',
'ticket.error.not_in_ticket': 'Dieser Befehl kann nur in einem Ticket-Kanal genutzt werden.',
'ticket.error.not_staff': 'Du hast keine Berechtigung für diese Ticket-Aktion.',
'ticket.error.already_open': 'Du hast bereits ein offenes Ticket.',
'ticket.error.closed': 'Dieses Ticket ist bereits geschlossen.',
'ticket.error.already_claimed': 'Du hast dieses Ticket bereits übernommen.',
'ticket.error.cannot_remove_opener': 'Der Ticket-Ersteller kann nicht entfernt werden.',
'ticket.error.no_channel': 'Ticket-Kanal nicht gefunden.',
'ticket.error.invalid_name': 'Ungültiger Name.',
'ticket.error.invalid_channel': 'Ungültiger Kanal.',
'ticket.error.no_parent_channel': 'Kein Elternkanal für Thread-Tickets konfiguriert.',
'ticket.error.invalid_parent_channel': 'Der konfigurierte Elternkanal ist ungültig.',
'ticket.error.not_found': 'Ticket nicht gefunden.',
'ticket.error.already_rated': 'Du hast dieses Ticket bereits bewertet.',
'birthdays.error.invalidDate': 'Ungültiges Datum. Prüfe Tag, Monat und optionales Jahr.',
'birthdays.error.invalidTimezone': 'Ungültige Zeitzone. Nutze z. B. Europe/Berlin.',
'birthdays.setup.done': 'Geburtstags-Modul konfiguriert.',
'birthdays.set.success': 'Geburtstag gespeichert: {day}.{month}.{year}',
'birthdays.remove.none': 'Du hast keinen Geburtstag eingetragen.',
'birthdays.remove.success': 'Dein Geburtstag wurde entfernt.',
'birthdays.list.empty': 'Noch keine Geburtstage eingetragen.',
'birthdays.list.header': '**Geburtstage (sortiert nach nächstem Datum):**',
'birthdays.next.header': '**Nächste Geburtstage:**',
'birthdays.list.line': '{rank}. {user} — {date} (in {days} T.)',
'tempvoice.error.disabled': 'Temp-Voice ist auf diesem Server deaktiviert.',
'tempvoice.error.invalidCategory': 'Ungültige Kategorie.',
'tempvoice.error.notOwner': 'Du bist nicht der Besitzer dieses Temp-Voice-Kanals.',
'tempvoice.error.notInChannel': 'Du musst in einem Temp-Voice-Kanal sein.',
'tempvoice.error.notFound': 'Temp-Voice-Kanal nicht gefunden.',
'tempvoice.error.invalidName': 'Ungültiger Kanalname.',
'tempvoice.error.invalidLimit': 'Limit muss zwischen 0 und 99 liegen.',
'tempvoice.setup.done': 'Temp-Voice konfiguriert.',
'tempvoice.rename.success': 'Kanal umbenannt in **{name}**.',
'tempvoice.limit.success': 'Nutzerlimit auf **{limit}** gesetzt (0 = unbegrenzt).',
'tempvoice.lock.success': 'Kanal gesperrt (Connect für @everyone entfernt).',
'tempvoice.hide.success': 'Kanal versteckt (ViewChannel für @everyone entfernt).',
'tempvoice.kick.self': 'Du kannst dich nicht selbst kicken.',
'tempvoice.kick.notInChannel': 'Dieser Nutzer ist nicht in deinem Kanal.',
'tempvoice.kick.success': '{user} wurde aus dem Kanal entfernt.',
'tempvoice.transfer.self': 'Du kannst den Kanal nicht an dich selbst übertragen.',
'tempvoice.transfer.notInChannel': 'Der neue Besitzer muss in deinem Kanal sein.',
'tempvoice.transfer.success': 'Kanal an {user} übertragen.',
'tempvoice.claim.ownerPresent': 'Der Besitzer ist noch im Kanal.',
'tempvoice.claim.success': 'Du bist jetzt Besitzer dieses Kanals.',
'tempvoice.panel.title': 'Temp-Voice Steuerung',
'tempvoice.panel.description': 'Verwalte **{channel}** mit den Buttons oder `/voice`-Commands.',
'tempvoice.button.rename': 'Umbenennen',
'tempvoice.button.limit': 'Limit',
'tempvoice.button.lock': 'Sperren',
'tempvoice.button.hide': 'Verstecken',
'tempvoice.button.kick': 'Kicken',
'tempvoice.button.transfer': 'Übertragen',
'tempvoice.button.claim': 'Übernehmen',
'selfroles.panel.defaultDescription': 'Wähle unten deine Rollen aus.',
'selfroles.panel.rolesField': 'Rollen',
'selfroles.panel.temporaryFooter': 'Rollen sind temporär und laufen automatisch ab.',
'selfroles.panel.created': 'Self-Role-Panel erstellt (ID: `{id}`).',
'selfroles.panel.updated': 'Self-Role-Panel aktualisiert (ID: `{id}`).',
'selfroles.panel.deleted': 'Self-Role-Panel gelöscht.',
'selfroles.role.added': 'Rolle hinzugefügt.',
'selfroles.role.removed': 'Rolle entfernt.',
'selfroles.role.verified': 'Rolle vergeben.',
'selfroles.role.unchanged': 'Du hast diese Rolle bereits.',
'selfroles.error.not_found': 'Self-Role-Panel nicht gefunden.',
'selfroles.error.invalid_roles': 'Ungültiges Rollen-Format. Nutze roleId:label:emoji.',
'selfroles.error.too_many_roles': 'Maximal 25 Rollen pro Panel.',
'selfroles.error.invalid_channel': 'Ungültiger Kanal.',
'selfroles.error.invalid_duration': 'Ungültige Dauer. Nutze z. B. 1h, 2d.',
'selfroles.error.duration_too_short': 'Die Dauer muss mindestens 1 Minute betragen.',
'selfroles.error.duration_required': 'Für temporäre Panels ist eine Dauer erforderlich.',
'selfroles.error.reaction_emoji_required': 'Reaktions-Modus erfordert Emojis für alle Rollen.',
'selfroles.error.member_not_found': 'Mitglied nicht gefunden.',
'selfroles.error.role_not_in_panel': 'Diese Rolle gehört nicht zu diesem Panel.',
'selfroles.error.role_not_found': 'Rolle nicht gefunden.',
'selfroles.error.role_hierarchy': 'Die Rolle liegt über der höchsten Bot-Rolle.',
'selfroles.error.role_managed': 'Integrierte Rollen können nicht vergeben werden.',
'selfroles.error.bot_missing_permissions': 'Dem Bot fehlt die Berechtigung Rollen zu verwalten.',
'tag.created': 'Tag `{name}` erstellt.',
'tag.updated': 'Tag `{name}` aktualisiert.',
'tag.deleted': 'Tag gelöscht.',
'tag.list.header': '**Tags**',
'tag.list.empty': 'Keine Tags vorhanden.',
'tag.list.uses': 'Aufrufe',
'tag.info.name': 'Name: `{name}`',
'tag.info.type': 'Typ: {type}',
'tag.info.uses': 'Aufrufe: {count}',
'tag.info.trigger': 'Trigger: `{word}`',
'tag.info.noTrigger': 'Kein Auto-Responder.',
'tag.info.roles': 'Rollen: {roles}',
'tag.info.allRoles': 'Rollen: alle',
'tag.info.channels': 'Kanäle: {channels}',
'tag.info.allChannels': 'Kanäle: alle',
'tag.error.not_found': 'Tag nicht gefunden.',
'tag.error.not_allowed': 'Du darfst diesen Tag hier nicht nutzen.',
'tag.error.invalid_name': 'Ungültiger Tag-Name (132 Zeichen, a-z, 0-9, _, -).',
'tag.error.content_required': 'Text-Tags benötigen Inhalt.',
'tag.error.embed_required': 'Embed-Tags benötigen Titel oder Beschreibung.'
};
const en: Dictionary = {
'generic.noPermission': 'You do not have permission for this action.',
@@ -439,7 +637,204 @@ const en: Dictionary = {
'fun.media.disabled': 'Media commands are disabled on this server.',
'fun.media.fetchFailed': 'Could not load image. Try again later.',
'fun.cat.title': 'Random Cat',
'fun.dog.title': 'Random Dog'
'fun.dog.title': 'Random Dog',
'starboard.notConfigured': 'Starboard is not configured yet. Use `/starboard setup`.',
'starboard.invalidChannel': 'The starboard channel is invalid.',
'starboard.setupDone': 'Starboard configured.',
'starboard.noContent': '*No text content*',
'starboard.field.stars': 'Stars',
'starboard.field.source': 'Source',
'starboard.jumpLink': 'Jump to message',
'suggestion.created': 'Suggestion submitted (ID: `{id}`).',
'suggestion.setupDone': 'Suggestion system configured.',
'suggestion.staffUpdated': 'Suggestion updated ({status}).',
'suggestion.votedUp': 'Upvote recorded.',
'suggestion.votedDown': 'Downvote recorded.',
'suggestion.threadName': 'Discussion #{id}',
'suggestion.error.not_configured': 'Suggestions are not configured. Use `/suggestion setup`.',
'suggestion.error.invalid_channel': 'The configured channel is invalid.',
'suggestion.error.not_found': 'Suggestion not found.',
'suggestion.error.not_open': 'This suggestion is no longer open.',
'suggestion.error.already_voted': 'You already voted this way.',
'suggestion.embed.title': 'Suggestion #{id}',
'suggestion.embed.status': 'Status',
'suggestion.embed.votes': 'Votes',
'suggestion.embed.author': 'By {author}',
'suggestion.embed.staffReason': 'Staff reason',
'suggestion.status.open': 'Open',
'suggestion.status.approved': 'Approved',
'suggestion.status.denied': 'Denied',
'suggestion.status.considered': 'Under consideration',
'suggestion.status.implemented': 'Implemented',
'suggestion.dm.statusUpdate': 'Your suggestion was updated: **{status}**\nReason: {reason}',
'giveaway.embed.title': 'Giveaway',
'giveaway.embed.prize': 'Prize',
'giveaway.embed.winners': 'Winners',
'giveaway.embed.entrants': 'Entrants',
'giveaway.embed.host': 'Host',
'giveaway.embed.requirements': 'Requirements',
'giveaway.embed.active': 'Click **Join** to enter!',
'giveaway.embed.paused': '⏸️ This giveaway is paused.',
'giveaway.embed.ended': 'Ended',
'giveaway.embed.endedWinners': 'Winners: {winners}',
'giveaway.embed.endedNoWinners': 'No valid entrants.',
'giveaway.embed.endsAt': 'Ends {time}',
'giveaway.button.join': 'Join',
'giveaway.requirement.role': 'Role: {role}',
'giveaway.requirement.level': 'Minimum level: {level}',
'giveaway.requirement.memberDays': 'Member for at least {days} days',
'giveaway.started': 'Giveaway started (ID: `{id}`).',
'giveaway.ended': 'Giveaway ended.',
'giveaway.rerolled': 'New winners: {winners}',
'giveaway.deleted': 'Giveaway deleted.',
'giveaway.paused': 'Giveaway paused.',
'giveaway.resumed': 'Giveaway resumed.',
'giveaway.joined': 'You entered the giveaway!',
'giveaway.list.header': '**Active giveaways**',
'giveaway.list.empty': 'No active giveaways.',
'giveaway.list.entrants': 'entrants',
'giveaway.dm.won': 'Congratulations! You won **{prize}** on **{guild}**!',
'giveaway.error.not_found': 'Giveaway not found or already ended.',
'giveaway.error.not_ended': 'Giveaway has not ended yet.',
'giveaway.error.paused': 'This giveaway is paused.',
'giveaway.error.already_joined': 'You already entered.',
'giveaway.error.member_not_found': 'Member not found.',
'giveaway.error.missing_role': 'You are missing the required role.',
'giveaway.error.insufficient_level': 'Your level is too low.',
'giveaway.error.insufficient_member_days': 'You have not been on the server long enough.',
'giveaway.error.invalid_duration': 'Invalid duration. Use e.g. 1h, 2d.',
'giveaway.error.duration_too_short': 'Duration must be at least 1 minute.',
'giveaway.error.invalid_channel': 'Invalid channel.',
'ticket.panel.defaultDescription': 'Select a category to open a ticket.',
'ticket.panel.created': 'Ticket panel created.',
'ticket.category.created': 'Category **{name}** created.',
'ticket.created': 'Your ticket was created: {channel}',
'ticket.opened': '{user} opened a ticket.',
'ticket.subjectLine': 'Subject: {subject}',
'ticket.claimed': '{user} claimed this ticket.',
'ticket.userAdded': '{user} was added to the ticket.',
'ticket.userRemoved': '{user} was removed from the ticket.',
'ticket.renamed': 'Ticket renamed to **{name}**.',
'ticket.prioritySet': 'Priority set to **{priority}**',
'ticket.closed': 'Ticket closed by {user}.',
'ticket.closedWithReason': 'Ticket closed by {user}.\nReason: {reason}',
'ticket.close.success': 'Ticket closed.',
'ticket.claim.success': 'Ticket claimed.',
'ticket.add.success': 'Added {user}.',
'ticket.remove.success': 'Removed {user}.',
'ticket.rename.success': 'Ticket renamed to **{name}**.',
'ticket.priority.success': 'Priority: **{priority}**',
'ticket.transcriptDm': 'Here is your ticket transcript.',
'ticket.ratePrompt': 'How satisfied were you with support? (15)',
'ticket.rated': 'Thanks for your rating: {rating}/5',
'ticket.autoCloseReason': 'Auto-closed due to inactivity',
'ticket.modal.subject': 'Subject',
'ticket.log.closed': 'Ticket closed',
'ticket.log.opener': 'Opener',
'ticket.log.closedBy': 'Closed by',
'ticket.priority.LOW': 'Low',
'ticket.priority.NORMAL': 'Normal',
'ticket.priority.HIGH': 'High',
'ticket.priority.URGENT': 'Urgent',
'ticket.error.disabled': 'The ticket system is disabled.',
'ticket.error.category_not_found': 'Category not found.',
'ticket.error.no_categories': 'No ticket categories configured.',
'ticket.error.not_in_ticket': 'This command can only be used inside a ticket channel.',
'ticket.error.not_staff': 'You do not have permission for this ticket action.',
'ticket.error.already_open': 'You already have an open ticket.',
'ticket.error.closed': 'This ticket is already closed.',
'ticket.error.already_claimed': 'You already claimed this ticket.',
'ticket.error.cannot_remove_opener': 'The ticket opener cannot be removed.',
'ticket.error.no_channel': 'Ticket channel not found.',
'ticket.error.invalid_name': 'Invalid name.',
'ticket.error.invalid_channel': 'Invalid channel.',
'ticket.error.no_parent_channel': 'No parent channel configured for thread tickets.',
'ticket.error.invalid_parent_channel': 'The configured parent channel is invalid.',
'ticket.error.not_found': 'Ticket not found.',
'ticket.error.already_rated': 'You already rated this ticket.',
'birthdays.error.invalidDate': 'Invalid date. Check day, month, and optional year.',
'birthdays.error.invalidTimezone': 'Invalid timezone. Use e.g. Europe/Berlin.',
'birthdays.setup.done': 'Birthday module configured.',
'birthdays.set.success': 'Birthday saved: {day}/{month}/{year}',
'birthdays.remove.none': 'You have not set a birthday.',
'birthdays.remove.success': 'Your birthday was removed.',
'birthdays.list.empty': 'No birthdays registered yet.',
'birthdays.list.header': '**Birthdays (sorted by next date):**',
'birthdays.next.header': '**Upcoming birthdays:**',
'birthdays.list.line': '{rank}. {user} — {date} (in {days} d)',
'tempvoice.error.disabled': 'Temp voice is disabled on this server.',
'tempvoice.error.invalidCategory': 'Invalid category.',
'tempvoice.error.notOwner': 'You are not the owner of this temp voice channel.',
'tempvoice.error.notInChannel': 'You must be in a temp voice channel.',
'tempvoice.error.notFound': 'Temp voice channel not found.',
'tempvoice.error.invalidName': 'Invalid channel name.',
'tempvoice.error.invalidLimit': 'Limit must be between 0 and 99.',
'tempvoice.setup.done': 'Temp voice configured.',
'tempvoice.rename.success': 'Channel renamed to **{name}**.',
'tempvoice.limit.success': 'User limit set to **{limit}** (0 = unlimited).',
'tempvoice.lock.success': 'Channel locked (Connect denied for @everyone).',
'tempvoice.hide.success': 'Channel hidden (ViewChannel denied for @everyone).',
'tempvoice.kick.self': 'You cannot kick yourself.',
'tempvoice.kick.notInChannel': 'That user is not in your channel.',
'tempvoice.kick.success': 'Removed {user} from the channel.',
'tempvoice.transfer.self': 'You cannot transfer the channel to yourself.',
'tempvoice.transfer.notInChannel': 'The new owner must be in your channel.',
'tempvoice.transfer.success': 'Channel transferred to {user}.',
'tempvoice.claim.ownerPresent': 'The owner is still in the channel.',
'tempvoice.claim.success': 'You are now the owner of this channel.',
'tempvoice.panel.title': 'Temp Voice Control',
'tempvoice.panel.description': 'Manage **{channel}** with the buttons below or `/voice` commands.',
'tempvoice.button.rename': 'Rename',
'tempvoice.button.limit': 'Limit',
'tempvoice.button.lock': 'Lock',
'tempvoice.button.hide': 'Hide',
'tempvoice.button.kick': 'Kick',
'tempvoice.button.transfer': 'Transfer',
'tempvoice.button.claim': 'Claim',
'selfroles.panel.defaultDescription': 'Choose your roles below.',
'selfroles.panel.rolesField': 'Roles',
'selfroles.panel.temporaryFooter': 'Roles are temporary and expire automatically.',
'selfroles.panel.created': 'Self-role panel created (ID: `{id}`).',
'selfroles.panel.updated': 'Self-role panel updated (ID: `{id}`).',
'selfroles.panel.deleted': 'Self-role panel deleted.',
'selfroles.role.added': 'Role added.',
'selfroles.role.removed': 'Role removed.',
'selfroles.role.verified': 'Role granted.',
'selfroles.role.unchanged': 'You already have this role.',
'selfroles.error.not_found': 'Self-role panel not found.',
'selfroles.error.invalid_roles': 'Invalid roles format. Use roleId:label:emoji.',
'selfroles.error.too_many_roles': 'Maximum 25 roles per panel.',
'selfroles.error.invalid_channel': 'Invalid channel.',
'selfroles.error.invalid_duration': 'Invalid duration. Use e.g. 1h, 2d.',
'selfroles.error.duration_too_short': 'Duration must be at least 1 minute.',
'selfroles.error.duration_required': 'Temporary panels require a duration.',
'selfroles.error.reaction_emoji_required': 'Reaction mode requires emojis for all roles.',
'selfroles.error.member_not_found': 'Member not found.',
'selfroles.error.role_not_in_panel': 'This role is not part of this panel.',
'selfroles.error.role_not_found': 'Role not found.',
'selfroles.error.role_hierarchy': 'The role is above the bot\'s highest role.',
'selfroles.error.role_managed': 'Managed roles cannot be assigned.',
'selfroles.error.bot_missing_permissions': 'The bot lacks Manage Roles permission.',
'tag.created': 'Tag `{name}` created.',
'tag.updated': 'Tag `{name}` updated.',
'tag.deleted': 'Tag deleted.',
'tag.list.header': '**Tags**',
'tag.list.empty': 'No tags configured.',
'tag.list.uses': 'uses',
'tag.info.name': 'Name: `{name}`',
'tag.info.type': 'Type: {type}',
'tag.info.uses': 'Uses: {count}',
'tag.info.trigger': 'Trigger: `{word}`',
'tag.info.noTrigger': 'No auto-responder.',
'tag.info.roles': 'Roles: {roles}',
'tag.info.allRoles': 'Roles: all',
'tag.info.channels': 'Channels: {channels}',
'tag.info.allChannels': 'Channels: all',
'tag.error.not_found': 'Tag not found.',
'tag.error.not_allowed': 'You are not allowed to use this tag here.',
'tag.error.invalid_name': 'Invalid tag name (132 chars, a-z, 0-9, _, -).',
'tag.error.content_required': 'Text tags require content.',
'tag.error.embed_required': 'Embed tags require a title or description.'
};
const locales: Record<Locale, Dictionary> = { de, en };