2 Commits

Author SHA1 Message Date
01a484ddb6 Merge pull request 'feat: add advanced translator cog with proxy and languages' (#13) from translator into main
Reviewed-on: #13
2025-09-23 01:17:12 -04:00
0e5afcb999 feat: add advanced translator cog with proxy and languages
Introduces multilingual translation cog supporting fantasy languages
Adds proxying features for role-playing with sonas and auto-translation
Includes admin commands for managing custom languages and context menus

Replaces basic translation with enhanced functionality for Discord bot
2025-09-22 23:23:57 -04:00
44 changed files with 1065 additions and 145 deletions

286
translator/commands.py Normal file
View File

@@ -0,0 +1,286 @@
# -*- coding: utf-8 -*-
import discord
import json
from typing import Optional
from redbot.core import commands, app_commands
from redbot.core.utils.chat_formatting import box, pagify
class CommandsMixin:
"""This class holds all the commands for the cog."""
@commands.hybrid_command(aliases=["ts"])
@app_commands.describe(
to_language="The language to translate to.",
text="The text to translate. For prefix commands, wrap multi-word text in quotes.",
from_language="[Optional] The language to translate from. Defaults to Common."
)
async def translate(self, ctx: commands.Context, to_language: str, text: str, from_language: Optional[str] = None):
"""Translates text from one language to another."""
from_lang_name = from_language if from_language else "common"
from_matches = await self._find_language(from_lang_name)
if not from_matches:
return await ctx.send(f"Could not find the 'from' language: `{from_lang_name}`")
if len(from_matches) > 1:
possible = [f"`{self.all_languages[k]['name']}` (`{k}`)" for k in from_matches]
return await ctx.send(f"Multiple 'from' languages found for `{from_lang_name}`. Please be more specific:\n" + ", ".join(possible))
from_lang_key = from_matches[0]
to_matches = await self._find_language(to_language)
if not to_matches:
return await ctx.send(f"Could not find the 'to' language: `{to_language}`")
if len(to_matches) > 1:
possible = [f"`{self.all_languages[k]['name']}` (`{k}`)" for k in to_matches]
return await ctx.send(f"Multiple 'to' languages found for `{to_language}`. Please be more specific:\n" + ", ".join(possible))
to_lang_key = to_matches[0]
from_lang_obj = self.all_languages[from_lang_key]
to_lang_obj = self.all_languages[to_lang_key]
try:
common_text = from_lang_obj['from_func'](text)
translated_text = to_lang_obj['to_func'](common_text)
except Exception as e:
await ctx.send(f"An error occurred during translation: `{e}`")
return
webhook = None
if ctx.guild and ctx.guild.me.guild_permissions.manage_webhooks:
for wh in await ctx.channel.webhooks():
if wh.name == "Translator Cog Webhook":
webhook = wh
break
if webhook is None:
webhook = await ctx.channel.create_webhook(name="Translator Cog Webhook")
if webhook:
if ctx.interaction:
await ctx.interaction.response.defer(ephemeral=True)
await webhook.send(content=translated_text, username=ctx.author.display_name, avatar_url=ctx.author.display_avatar.url)
await ctx.interaction.followup.send("Translation sent.", ephemeral=True)
else:
if ctx.channel.permissions_for(ctx.guild.me).manage_messages:
try:
await ctx.message.delete()
except discord.Forbidden:
pass
await webhook.send(content=translated_text, username=ctx.author.display_name, avatar_url=ctx.author.display_avatar.url)
else:
embed = discord.Embed(title=f"Translation to {to_lang_obj['name']}", color=await ctx.embed_color())
embed.add_field(name="Original Text", value=box(text), inline=False)
embed.add_field(name="Translated Text", value=box(translated_text), inline=False)
await ctx.send(embed=embed)
@commands.hybrid_command()
async def languages(self, ctx: commands.Context):
"""Lists all available languages."""
sorted_langs = sorted(self.all_languages.values(), key=lambda x: x['name'])
lang_list = [f"* `{lang['name']}`" for lang in sorted_langs]
output = "Available Languages:\n" + "\n".join(lang_list)
pages = [box(page) for page in pagify(output, page_length=1000)]
await ctx.send_interactive(pages, box_lang="md")
@commands.group(aliases=["px"], invoke_without_command=True)
async def proxy(self, ctx: commands.Context):
"""Toggles your translation proxy on or off."""
current_setting = await self.config.user(ctx.author).proxy_enabled()
new_setting = not current_setting
await self.config.user(ctx.author).proxy_enabled.set(new_setting)
status = "enabled" if new_setting else "disabled"
await ctx.send(f"Proxying is now `{status}`.")
@proxy.command(name="list")
async def proxy_list(self, ctx: commands.Context):
"""Shows your currently registered sonas."""
sonas = await self.config.user(ctx.author).sonas()
if not sonas:
return await ctx.send("You have no sonas registered.")
msg = "Your registered sonas:\n"
for name, data in sonas.items():
display_name = data.get("display_name", name)
lang_name = self.all_languages.get(data['language'], {}).get('name', 'Unknown Language')
if 'claws' in data:
start_claw, end_claw = data['claws']
elif 'brackets' in data: # Fallback for old data
start_claw, end_claw = data['brackets']
else:
continue
claw_info = f"Starts with `{start_claw}`" if not end_claw else f"`{start_claw}` and `{end_claw}`"
msg += f" - **{display_name}** (Internal Name: `{name}`): Translates to `{lang_name}`. Claws: {claw_info}\n"
for page in pagify(msg):
await ctx.send(page)
@proxy.command(name="add", aliases=["+"])
async def proxy_add(self, ctx: commands.Context, name: str, language: str, display_name: str, start_claw: str, end_claw: str = "", avatar: Optional[str] = None):
"""Registers a new sona with a name and avatar."""
matches = await self._find_language(language)
if not matches:
return await ctx.send(f"Language `{language}` not found.")
if len(matches) > 1:
possible = [f"`{self.all_languages[k]['name']}` (`{k}`)" for k in matches]
return await ctx.send(f"Multiple languages found for `{language}`. Please be more specific:\n" + ", ".join(possible))
lang_key = matches[0]
sona_key = name.lower()
avatar_url = avatar
if not avatar_url and ctx.message.attachments:
avatar_url = ctx.message.attachments[0].url
async with self.config.user(ctx.author).sonas() as sonas:
if sona_key in sonas:
return await ctx.send(f"A sona named `{name}` already exists.")
sonas[sona_key] = {
"display_name": display_name,
"avatar_url": avatar_url,
"language": lang_key,
"claws": [start_claw, end_claw]
}
await ctx.send(f"Sona `{name}` registered as `{display_name}` to translate to `{self.all_languages[lang_key]['name']}`.")
@proxy.command(name="remove", aliases=["-"])
async def proxy_remove(self, ctx: commands.Context, *, name: str):
"""Removes a sona."""
sona_key = name.lower()
async with self.config.user(ctx.author).sonas() as sonas:
if sona_key not in sonas:
return await ctx.send(f"No sona named `{name}` found.")
del sonas[sona_key]
await ctx.send(f"Sona `{name}` has been removed.")
@proxy.group()
async def sona(self, ctx: commands.Context):
"""Commands for managing a sona's appearance."""
pass
@proxy.group(name="auto")
async def proxy_auto(self, ctx: commands.Context):
"""Manage automatic translation in a channel."""
pass
@proxy_auto.command(name="set")
async def proxy_auto_set(self, ctx: commands.Context, sona_name: str):
"""Sets a sona to automatically translate your messages in this channel."""
sona_key = sona_name.lower()
sonas = await self.config.user(ctx.author).sonas()
if sona_key not in sonas:
return await ctx.send(f"No sona named `{sona_name}` found. Please register it first.")
async with self.config.channel(ctx.channel).autotranslate_users() as autotranslate_users:
autotranslate_users[str(ctx.author.id)] = sona_key
await ctx.send(f"Autotranslation enabled for you in this channel as **{sonas[sona_key]['display_name']}**.")
@proxy_auto.command(name="off")
async def proxy_auto_off(self, ctx: commands.Context):
"""Disables autotranslation for you in this channel."""
async with self.config.channel(ctx.channel).autotranslate_users() as autotranslate_users:
if str(ctx.author.id) in autotranslate_users:
del autotranslate_users[str(ctx.author.id)]
await ctx.send("Autotranslation has been disabled for you in this channel.")
else:
await ctx.send("You do not have autotranslation enabled in this channel.")
@sona.command(name="name")
async def sona_name(self, ctx: commands.Context, name: str, *, display_name: str):
"""Changes the display name of a sona."""
sona_key = name.lower()
async with self.config.user(ctx.author).sonas() as sonas:
if sona_key not in sonas:
return await ctx.send(f"No sona named `{name}` found.")
sonas[sona_key]["display_name"] = display_name
await ctx.send(f"Sona `{name}`'s display name has been changed to `{display_name}`.")
@sona.command(name="avatar")
async def sona_avatar(self, ctx: commands.Context, name: str, url: Optional[str] = None):
"""Changes the avatar of a sona.
You can either provide a direct image URL or upload an image with the command.
"""
sona_key = name.lower()
if not url and not ctx.message.attachments:
return await ctx.send("You must provide an image URL or upload an image.")
avatar_url = url
if ctx.message.attachments:
avatar_url = ctx.message.attachments[0].url
async with self.config.user(ctx.author).sonas() as sonas:
if sona_key not in sonas:
return await ctx.send(f"No sona named `{name}` found.")
sonas[sona_key]["avatar_url"] = avatar_url
await ctx.send(f"Sona `{name}`'s avatar has been updated.")
@commands.group(aliases=["tset"])
@commands.has_permissions(manage_guild=True)
async def translatorset(self, ctx: commands.Context):
"""Admin commands for the Translator cog."""
pass
@translatorset.group(name="language", aliases=["lang"])
async def translatorset_language(self, ctx: commands.Context):
"""Manage custom languages."""
pass
@translatorset_language.command(name="add", aliases=["+"])
async def translatorset_language_add(self, ctx: commands.Context, name: str, *, json_map: str):
"""Adds a new custom language."""
lang_key = name.lower()
if lang_key in self.all_languages:
return await ctx.send(f"A language with the key `{lang_key}` already exists.")
try:
lang_map = json.loads(json_map)
if not isinstance(lang_map, dict):
raise ValueError("JSON map must be an object/dictionary.")
except (json.JSONDecodeError, ValueError) as e:
return await ctx.send(f"Invalid JSON map provided: `{e}`")
new_lang_data = {'name': name.capitalize(), 'type': 'greedy', 'map': lang_map, 'is_custom': True}
async with self.config.languages() as languages:
languages[lang_key] = new_lang_data
await self._initialize_languages()
await ctx.send(f"Custom language `{name}` added successfully.")
@translatorset_language.command(name="remove", aliases=["-"])
async def translatorset_language_remove(self, ctx: commands.Context, name: str):
"""Removes a custom language."""
lang_key = name.lower()
current_data = await self.config.languages()
lang_obj = current_data.get(lang_key)
if not lang_obj or not lang_obj.get('is_custom', False):
return await ctx.send(f"No custom language named `{name}` found.")
async with self.config.languages() as languages:
if lang_key in languages:
del languages[lang_key]
await self._initialize_languages()
await ctx.send(f"Custom language `{name}` removed.")
@translatorset_language.command(name="listcustom")
async def translatorset_language_listcustom(self, ctx: commands.Context):
"""Lists all custom-added languages."""
custom_langs = [f"`{lang['name']}`" for lang in self.all_languages.values() if lang.get('is_custom')]
if not custom_langs:
return await ctx.send("There are no custom languages.")
await ctx.send("Custom Languages:\n" + ", ".join(custom_langs))
@translatorset_language.command(name="listbase")
async def translatorset_language_listbase(self, ctx: commands.Context):
"""Lists all base (built-in) languages."""
base_langs = [f"`{lang['name']}`" for lang in self.all_languages.values() if not lang.get('is_custom')]
await ctx.send("Base Languages:\n" + ", ".join(base_langs))

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,8 @@
MAP = {'a':'azg','b':'braz','c':'kraz','d':'dorg','e':'ezg','f':'fraz','g':'gor','h':'hath','i':'ix','j':'jraz','k':'kral','l':'laz','m':'maz','n':'naz','o':'oz','p':'praz','q':'qor','r':'raz','s':'saz','t':'taz','u':'uzg','v':'vraz','w':'waz','x':'xul','y':'yaz','z':'zaz'}
def get_language():
return {
'name': 'Abyssal',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'adriel','b':'baraqiel','c':'camael','d':'divinus','e':'elohim','f':'fanuel','g':'gloria','h':'hesed','i':'israfel','j':'jophiel','k':'kyrie','l':'lux','m':'michael','n':'netzach','o':'ophaniel','p':'peniel','q':'qadish','r':'raphael','s':'seraph','t':'tiferet','u':'uriel','v':'virtues','w':'wele\'el','x':'xathanael','y':'yesod','z':'zadkiel'}
def get_language():
return {
'name': 'Angelic',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,9 @@
MAP = {'a':'aqua','b':'blub','c':'\'cress','d':'drop','e':'eelee','f':'flow','g':'glur','h':'hydro','i':'ishi','j':'\'jyr','k':'\'kyr','l':'luu','m':'\'myr','n':'\'nyr','o':'oro','p':'ploop','q':'\'qyr','r':'\'ryp','s':'sh\'l','t':'tide','u':'\'urn','v':'\'vyr','w':'wash','x':'\'xyr','y':'\'yyr','z':'\'zyr'}
def get_language():
return {
'name': 'Aquan',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'abyss','b':'brine','c':'coral','d':'depth','e':'eel','f':'fin','g':'gurgle','h':'hydro','i':'ink','j':'jelly','k':'krill','l':'lagoon','m':'murk','n':'naut','o':'ocean','p':'pearl','q':'quatic','r':'reef','s':'salt','t':'tide','u':'urchin','v':'void','w':'wave','x':'xiph','y':'yacht','z':'zone'}
def get_language():
return {
'name': 'Aquatic',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'\'arr','b':'\'brach','c':'ch\'t','d':'\'drach','e':'\'err','f':'\'fune','g':'\'goss','h':'\'harr','i':'\'itt','j':'\'jarr','k':'klik\'','l':'\'lar','m':'\'marr','n':'\'narr','o':'\'orr','p':'\'parr','q':'\'qarr','r':'\'rarr','s':'skitter\'','t':'th\'k','u':'\'urr','v':'\'varr','w':'web\'','x':'\'xarr','y':'\'yarr','z':'\'zarr'}
def get_language():
return {
'name': 'Arachnid',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'\'aara','b':'\'bree','c':'chir\'','d':'\'dree','e':'eek','f':'\'flutter','g':'gree\'','h':'hrooa','i':'ii\'','j':'\'jakk','k':'kree\'','l':'\'liri','m':'\'meeka','n':'\'neer','o':'\'oroo','p':'pip\'','q':'\'qree','r':'\'reea','s':'\'skraw','t':'tweet\'','u':'\'urr','v':'\'vree','w':'\'warble','x':'\'xee','y':'\'yari','z':'\'zeer'}
def get_language():
return {
'name': 'Avian',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'\'ana','b':'\'bara','c':'\'cera','d':'\'dona','e':'\'elara','f':'\'fana','g':'\'gala','h':'\'hylia','i':'\'iana','j':'\'jana','k':'\'kana','l':'\'lora','m':'\'mara','n':'\'nara','o':'\'ora','p':'\'pera','q':'\'qana','r':'\'ria','s':'\'sera','t':'\'tara','u':'\'ura','v':'\'vara','w':'\'wana','x':'\'xara','y':'\'yana','z':'\'zara'}
def get_language():
return {
'name': 'Celestial',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
# This language is handled by its 'rule' type in the main cog.
# It doesn't need a map.
def get_language():
return {
'name': 'Common',
'type': 'rule'
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'auto','b':'bolt','c':'clank','d':'diode','e':'engine','f':'forge','g':'gear','h':'hydro','i':'iron','j':'joint','k':'kinetic','l':'link','m':'motor','n':'node','o':'optic','p':'piston','q':'quantum','r':'rivet','s':'servo','t':'titan','u':'unit','v':'volt','w':'whirr','x':'xenon','y':'yoke','z':'zinc'}
def get_language():
return {
'name': 'Construct',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'\'ayl','b':'\'baal','c':'\'cress','d':'\'drev','e':'\'eyl','f':'\'fane','g':'\'gyl','h':'hysh\'','i':'\'iyl','j':'\'jex','k':'\'krys','l':'\'lyl','m':'\'mal','n':'\'nyl','o':'\'oyl','p':'\'prax','q':'\'qyl','r':'\'ryl','s':'\'shayd','t':'\'trys','u':'\'uyl','v':'\'vyl','w':'\'wryl','x':'\'xyl','y':'\'yyl','z':'\'zyll'}
def get_language():
return {
'name': 'Devilish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'ax','b':'baxis','c':'caex','d':'drak','e':'ess','f':'faex','g':'gix','h':'heth','i':'ir','j':'jyss','k':'kex','l':'lix','m':'maex','n':'nex','o':'oth','p':'pex','q':'qexis','r':'rax','s':'syth','t':'thrax','u':'ur','v':'vyx','w':'wess','x':'xis','y':'yth','z':'zix'}
def get_language():
return {
'name': 'Draconic',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'az','b':'bar','c':'krag','d':'dur','e':'ek','f':'fol','g':'grum','h':'hur','i':'in','j':'jor','k':'kaz','l':'lur','m':'mor','n':'nur','o':'ok','p':'por','q':'qur','r':'ruk','s':'son','t':'thor','u':'um','v':'val','w':'wor','x':'xor','z':'zul'}
def get_language():
return {
'name': 'Dwarvish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'aer','b':'breeze','c':'cinder','d':'dust','e':'ember','f':'flow','g':'gust','h':'hail','i':'ignis','j':'jet','k':'kinetic','l':'lava','m':'mist','n':'nova','o':'ozone','p':'pyre','q':'quake','r':'rain','s':'stone','t':'terra','u':'umbra','v':'vapor','w':'wave','x':'xenon','y':'yon','z':'zephyr'}
def get_language():
return {
'name': 'Elemental',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,11 @@
# -*- coding: utf-8 -*-
MAP = {'a':'ael','b':'\'eth','c':'cal','d':'dor','e':'elen','f':'fae','g':'\'gan','h':'h<EFBFBD>r','i':'ia','j':'yel','k':'\'ken','l':'lael','m':'mel','n':'n<EFBFBD>n','o':'oia','p':'\'pes','q':'qen','r':'rae','s':'sil','t':'t<EFBFBD>','u':'ui','v':'vae','w':'win','x':'\'xal','y':'yl','z':'z<EFBFBD>r'}
def get_language():
return {
'name': 'Elvish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'meow','b':'brrt','c':'chrr','d':'drrt','e':'eek','f':'frrt','g':'grrowl','h':'hiss','i':'mii','j':'jrr','k':'krr','l':'lrr','m':'mrow','n':'nyah','o':'oww','p':'purr','q':'qrr','r':'rrr','s':'sss','t':'trill','u':'urr','v':'vrr','w':'wrr','x':'xrr','y':'yowl','z':'zzz'}
def get_language():
return {
'name': 'Feline',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'az\'ael','b':'ba\'al','c':'cre\'z','d':'dre\'th','e':'esh\'','f':'fiir\'','g':'gre\'th','h':'ha\'el','i':'i\'z','j':'je\'th','k':'krez\'','l':'le\'th','m':'morn\'','n':'ne\'th','o':'o\'z','p':'pre\'th','q':'qe\'th','r':'re\'th','s':'se\'th','t':'te\'th','u':'u\'z','v':'ve\'th','w':'we\'th','x':'xith\'','y':'ye\'th','z':'zael\''}
def get_language():
return {
'name': 'Fiendish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'akk','b':'bink','c':'clank','d':'dink','e':'enk','f':'fizz','g':'giz','h':'hink','i':'ink','j':'jink','k':'kink','l':'link','m':'mink','n':'nink','o':'onk','p':'sprok','q':'qink','r':'rink','s':'sprock','t':'tink','u':'unk','v':'vink','w':'whirr','x':'xink','y':'yink','z':'zink'}
def get_language():
return {
'name': 'Gnomish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'az','b':'bik','c':'clik','d':'dik','e':'ek','f':'fiz','g':'gib','h':'hik','i':'ik','j':'jik','k':'krik','l':'lik','m':'mik','n':'nik','o':'ok','p':'pik','q':'qik','r':'rik','s':'snik','t':'tik','u':'uk','v':'vik','w':'wik','x':'xik','y':'yik','z':'zik'}
def get_language():
return {
'name': 'Goblin',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'apple','b':'bramble','c':'crumpet','d':'dale','e':'elderberry','f':'fiddle','g':'garden','h':'hearth','i':'iris','j':'jam','k':'kettle','l':'lazy','m':'meadow','n':'nimble','o':'oats','p':'pudding','q':'quaint','r':'river','s':'sunny','t':'tater','u':'underhill','v':'vine','w':'willow','x':'extra','y':'yarn','z':'zesty'}
def get_language():
return {
'name': 'Half-Tongue',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'aloe','b':'bark','c':'clover','d':'dand','e':'elder','f':'fern','g':'groot','h':'herb','i':'ivy','j':'juni','k':'kelp','l':'leaf','m':'moss','n':'nettle','o':'oak','p':'petal','q':'quin','r':'root','s':'sprout','t':'thyme','u':'ursi','v':'vine','w':'willow','x':'xylem','y':'yarrow','z':'zinni'}
def get_language():
return {
'name': 'Herbilore',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'az\'','b':'\'baal','c':'\'krez','d':'\'drak','e':'ez\'','f':'\'fel','g':'\'gor','h':'\'hath','i':'iz\'','j':'\'jaz','k':'\'kraz','l':'\'laz','m':'\'mor','n':'\'naz','o':'oz\'','p':'\'paz','q':'\'qaz','r':'\'raz','s':'\'saz','t':'\'taz','u':'uz\'','v':'\'vaz','w':'\'waz','x':'\'xaz','y':'\'yaz','z':'\'zaz'}
def get_language():
return {
'name': 'Infernal',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,9 @@
MAP = {'a':'ka','b':'be','c':'chi','d':'de','e':'e','f':'fu','g':'ga','h':'hi','i':'i','j':'ji','k':'ki','l':'ru','m':'ma','n':'na','o':'o','p':'pe','q':'kyu','r':'re','s':'sa','t':'to','u':'u','v':'ve','w':'wa','x':'za','y':'ya','z':'ze'}
def get_language():
return {
'name': 'Kitsune',
'type': 'greedy',
'map': MAP,
'separator': ''
}

View File

@@ -0,0 +1,9 @@
MAP = {'a':'4','b':'8','c':'(','d':')','e':'3','f':'|=','g':'6','h':'#','i':'1','j':']','k':'|<','l':'1','m':'/\\/\\','n':'/\\/','o':'0','p':'|D','q':'(,)','r':'|2','s':'5','t':'7','u':'|_|','v':'\\/','w':'\\/\\/','x':'><','y':'`/','z':'2'}
def get_language():
return {
'name': 'Leet Speak',
'type': 'special',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'\'ax','b':'\'bax','c':'\'caz','d':'\'daz','e':'\'ex','f':'\'fax','g':'\'gaz','h':'h\'ss','i':'\'ix','j':'\'jax','k':'sk\'ex','l':'\'lax','m':'\'max','n':'\'nax','o':'\'ox','p':'\'pax','q':'\'qax','r':'\'rax','s':'s\'lith','t':'\'char','u':'\'ux','v':'\'vax','w':'\'wax','x':'\'xax','y':'\'yax','z':'\'zax'}
def get_language():
return {
'name': 'Lizardfolk',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'.-','b':'-...','c':'-.-.','d':'-..','e':'.','f':'..-.','g':'--.','h':'....','i':'..','j':'.---','k':'-.-','l':'.-..','m':'--','n':'-.','o':'---','p':'.--.','q':'--.-','r':'.-.','s':'...','t':'-','u':'..-','v':'...-','w':'.--','x':'-..-','y':'-.--','z':'--..','1':'.----','2':'..---','3':'...--','4':'....-','5':'.....','6':'-....','7':'--...','8':'---..','9':'----.','0':'-----',' ':'/'}
def get_language():
return {
'name': 'Morse Code',
'type': 'special',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'agaric','b':'bolete','c':'cap','d':'decay','e':'enoki','f':'fungi','g':'gill','h':'hyphae','i':'indigo','j':'jelly','k':'kombu','l':'lichen','m':'myco\'','n':'nidur','o':'oyster','p':'puff','q':'quorn','r':'rhizo','s':'spore\'','t':'thallus','u':'umbra','v':'velvet','w':'wart','x':'xero','y':'yeast','z':'zoospore'}
def get_language():
return {
'name': 'Myconid',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'ug','b':'blud','c':'crag','d':'dug','e':'eeg','f':'fug','g':'gron','h':'hug','i':'ig','j':'jug','k':'krug','l':'lug','m':'mush','n':'nug','o':'og','p':'pug','q':'qug','r':'rug','s':'slog','t':'thok','u':'urk','v':'vog','w':'wug','x':'xug','y':'yug','z':'zug'}
def get_language():
return {
'name': 'Ogrish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'agh','b':'bug','c':'karg','d':'dur','e':'egh','f':'fug','g':'grol','h':'hosh','i':'izg','j':'jug','k':'krunk','l':'lug','m':'mog','n':'nog','o':'ogg','p':'pug','q':'qug','r':'ruk','s':'snaga','t':'tusk','u':'uruk','v':'vug','w':'warg','x':'xug','y':'yag','z':'zug'}
def get_language():
return {
'name': 'Orcish',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
# This language is handled by its 'rule' type in the main cog.
# It doesn't need a map.
def get_language():
return {
'name': 'Pig Latin',
'type': 'rule'
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'skree','b':'bite','c':'claw','d':'dark-thing','e':'eek','f':'filth','g':'gnaw','h':'hiss','i':'itch','j':'junk','k':'kill','l':'long-tail','m':'muck','n':'nest-thing','o':'rot-stink','p':'plague','q':'quick-quick','r':'rust','s':'skitter','t':'twitch','u':'under-thing','v':'vermin','w':'waste','x':'pox','y':'yes-yes','z':'zap-tail'}
def get_language():
return {
'name': 'Ratfolk',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'sk\'','b':'t\'k','c':'k\'ss','d':'d\'th','e':'e\'sk','f':'f\'t','g':'g\'th','h':'h\'k','i':'i\'s','j':'j\'t','k':'k\'t','l':'l\'k','m':'m\'k','n':'n\'t','o':'o\'s','p':'p\'k','q':'q\'t','r':'r\'k','s':'s\'k','t':'t\'s','u':'u\'s','v':'v\'t','w':'w\'k','x':'x\'s','y':'y\'k','z':'z\'t'}
def get_language():
return {
'name': 'Scorpion',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,10 @@
MAP = {'a':'a!','b':'7b','c':'c#','d':'d4','e':'3e','f':'f^','g':'g&','h':'h8','i':'(i','j':'j0','k':'_k','l':'l2','m':'=m','n':'n+','o':'5o','p':'-p','q':'q{','r':'}r','s':'[s','t':'t]','u':'|u','v':':v','w':'"w','x':'x<','y':'>y','z':'?z'}
def get_language():
return {
'name': 'Sinary',
'type': 'generic',
'map': MAP,
'chunk_size': 2,
'separator': ''
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'\'aura','b':'\'breth','c':'\'ciel','d':'\'dion','e':'\'ethys','f':'\'fey','g':'\'glyn','h':'\'hymn','i':'\'ia','j':'\'jora','k':'\'kye','l':'\'lume','m':'\'mana','n':'\'nima','o':'\'omni','p':'\'pria','q':'\'qia','r':'\'reth','s':'\'seren','t':'\'thyme','u':'\'umbra','v':'\'vym','w':'\'wisp','x':'\'xia','y':'\'yara','z':'\'zion'}
def get_language():
return {
'name': 'Spiritual',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'ah\'','b':'\'bel','c':'\'chae','d':'\'des','e':'\'esh','f':'\'fey','g':'\'gis','h':'hah\'','i':'\'ish','j':'\'jo','k':'\'ka','l':'\'lis','m':'\'mah','n':'\'nah','o':'oh\'','p':'\'pah','q':'\'qia','r':'\'rah','s':'\'sha','t':'\'thae','u':'uh\'','v':'\'vi','w':'\'wah','x':'\'xi','y':'\'yah','z':'\'zah'}
def get_language():
return {
'name': 'Succubus',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'ani','b':'bri','c':'cae','d':'dae','e':'eni','f':'fae','g':'gra','h':'hae','i':'ia','j':'jae','k':'kae','l':'lor','m':'mae','n':'nem','o':'olo','p':'pae','q':'qae','r':'rae','s':'sae','t':'tae','u':'uni','v':'vae','w':'wae','x':'xae','y':'yae','z':'zae'}
def get_language():
return {
'name': 'Sylvan',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'velk','b':'xund','c':'k\'yorl','d':'d\'ruth','e':'e\'trorn','f':'faer','g':'gol','h':'h\'chak','i':'i\'lith','j':'j\'lar','k':'k\'lar','l':'lil','m':'m\'lar','n':'nind','o':'olath','p':'p\'lar','q':'qu\'ellar','r':'ril','s':'sorn','t':'\'lar','u':'uss','v':'v\'lar','w':'wyl','x':'x\'lar','y':'y\'lar','z':'z\'ress'}
def get_language():
return {
'name': 'Undercommon',
'type': 'greedy',
'map': MAP
}

View File

@@ -0,0 +1,8 @@
# This language is handled by its 'rule' type in the main cog.
# It doesn't need a map.
def get_language():
return {
'name': 'UwU',
'type': 'rule'
}

View File

@@ -0,0 +1,10 @@
MAP = {'a':'ak','b':'ba','c':'ce','d':'di','e':'ek','f':'fo','g':'gu','h':'ha','i':'ik','j':'je','k':'ki','l':'lo','m':'mu','n':'na','o':'ok','p':'pe','q':'qi','r':'ro','s':'su','t':'ta','u':'uk','v':'ve','w':'wi','x':'xo','y':'yk','z':'zu'}
def get_language():
return {
'name': 'Valspiren',
'type': 'generic',
'map': MAP,
'chunk_size': 2,
'separator': ''
}

View File

@@ -0,0 +1,8 @@
MAP = {'a':'a\'th','b':'b\'zoth','c':'c\'thun','d':'d\'gol','e':'e\'th','f':'f\'thagn','g':'g\'noth','h':'h\'zoth','i':'i\'th','j':'j\'th','k':'k\'th','l':'l\'th','m':'m\'th','n':'n\'th','o':'o\'th','p':'p\'th','q':'qor\'','r':'r\'lyeh','s':'s\'th','t':'t\'th','u':'u\'th','v':'v\'lath','w':'w\'th','x':'x\'thul','y':'y\'th','z':'zy\'th'}
def get_language():
return {
'name': 'Voidtouched',
'type': 'greedy',
'map': MAP
}

196
translator/logic.py Normal file
View File

@@ -0,0 +1,196 @@
# -*- coding: utf-8 -*-
import re
import random
from redbot.core.utils.chat_formatting import box
def reverse_map(m): return {v: k for k, v in m.items()}
class TranslationLogicMixin:
"""This class holds all the translation logic for the cog."""
def _escape_regex(self, s):
return re.escape(s)
def _generic_translator(self, text, lang_map, char_separator):
# Regex to find custom emojis
emoji_regex = re.compile(r"<a?:\w+:\d+>")
# Find all emojis and store them
emojis = emoji_regex.findall(text)
# Replace emojis with a unique placeholder
placeholder = "||EMOJI||"
text_with_placeholders = emoji_regex.sub(placeholder, text)
# Translate the text with placeholders
word_separator = ' ' if char_separator == '' else ' '
words = text_with_placeholders.split(' ')
translated_words = []
emoji_counter = 0
for word in words:
if placeholder in word:
# If a word contains a placeholder, it might be part of the emoji code that got split.
# We simply re-insert the emoji from our list.
translated_words.append(emojis[emoji_counter])
emoji_counter += 1
else:
translated_word = char_separator.join([lang_map.get(char.lower(), char) for char in word])
translated_words.append(translated_word)
return word_separator.join(translated_words)
def _generic_decoder(self, text, reverse_map, chunk_size):
result = ""
text_no_space = text.replace(' ','')
for i in range(0, len(text_no_space), chunk_size):
chunk = text_no_space[i:i+chunk_size]
result += reverse_map.get(chunk, '?')
return result
def _greedy_decoder(self, text, reverse_map):
syllables = sorted(reverse_map.keys(), key=len, reverse=True)
regex = re.compile('|'.join(map(self._escape_regex, syllables)))
matches = regex.findall(text.replace(' ', ''))
return "".join([reverse_map.get(m, '?') for m in matches])
def _leet_decoder(self, text, reverse_map):
decoded_text = text
sorted_keys = sorted(reverse_map.keys(), key=len, reverse=True)
for key in sorted_keys:
decoded_text = decoded_text.replace(key, reverse_map[key])
return decoded_text
def _morse_decoder(self, text, reverse_map):
words = text.split(' ')
decoded_words = []
for word in words:
chars = word.split(' ')
decoded_words.append("".join([reverse_map.get(c, '?') for c in chars]))
return " ".join(decoded_words)
def _pig_latin_translator(self, text):
vowels = "aeiou"
translated_words = []
for word in text.split(' '):
if not word: continue
match = re.match(r"^([^a-zA-Z0-9]*)(.*?)([^a-zA-Z0-9]*)$", word)
leading_punct, clean_word, trailing_punct = match.groups()
if not clean_word:
translated_words.append(word)
continue
if clean_word and clean_word[0].lower() in vowels:
translated_word = clean_word + "way"
else:
first_vowel_index = -1
for i, char in enumerate(clean_word):
if char.lower() in vowels:
first_vowel_index = i
break
if first_vowel_index == -1:
translated_word = clean_word + "ay"
else:
translated_word = clean_word[first_vowel_index:] + clean_word[:first_vowel_index] + "ay"
translated_words.append(leading_punct + translated_word + trailing_punct)
return " ".join(translated_words)
def _pig_latin_decoder(self, text):
decoded_words = []
for word in text.split(' '):
if not word: continue
match = re.match(r"^([^a-zA-Z0-9]*)(.*?)([^a-zA-Z0-9]*)$", word)
leading_punct, clean_word, trailing_punct = match.groups()
if not clean_word:
decoded_words.append(word)
continue
original_word = ""
if clean_word.lower().endswith("way"):
original_word = clean_word[:-3]
elif clean_word.lower().endswith("ay"):
base_word = clean_word[:-2]
last_consonant_block_index = -1
for i in range(len(base_word) - 1, -1, -1):
if base_word[i].lower() not in "aeiou":
last_consonant_block_index = i
else:
break
if last_consonant_block_index != -1:
while last_consonant_block_index > 0 and base_word[last_consonant_block_index-1].lower() not in "aeiou":
last_consonant_block_index -= 1
consonants = base_word[last_consonant_block_index:]
stem = base_word[:last_consonant_block_index]
original_word = consonants + stem
else:
original_word = base_word
else:
original_word = clean_word
decoded_words.append(leading_punct + original_word + trailing_punct)
return " ".join(decoded_words)
def _uwu_translator(self, text):
text = text.lower()
text = text.replace('l', 'ww')
text = text.replace('r', 'w')
text = text.replace('na', 'nya').replace('ne', 'nye').replace('ni', 'nyi').replace('no', 'nyo').replace('nu', 'nyu')
text = text.replace('ove', 'uv')
words = text.split(' ')
uwu_words = []
for word in words:
if len(word) > 3 and random.random() < 0.3:
uwu_words.append(f"{word[0]}-{word}")
else:
uwu_words.append(word)
text = " ".join(uwu_words)
if random.random() < 0.5:
emoticons = [' uwu', ' owo', ' >w<', ' ^-^', ' ;;']
text += random.choice(emoticons)
return text
def _uwu_decoder(self, text):
emoticons = [' uwu', ' owo', ' >w<', ' ^-^', ' ;;']
for emo in emoticons:
if text.endswith(emo):
text = text[:-len(emo)]
text = re.sub(r'(\w)-(\w+)', r'\2', text)
text = text.replace('uv', 'ove')
text = text.replace('nyu', 'nu').replace('nyo', 'no').replace('nyi', 'ni').replace('nye', 'ne').replace('nya', 'na')
text = text.replace('ww', 'l')
text = text.replace('w', 'r')
return text
async def _find_language(self, query: str):
"""Finds languages by key or name, case-insensitively."""
query = query.lower()
exact_key_match = [key for key in self.all_languages if key.lower() == query]
if exact_key_match:
return exact_key_match
exact_name_match = [key for key, lang in self.all_languages.items() if lang['name'].lower() == query]
if exact_name_match:
return exact_name_match
partial_matches = [
key for key, lang in self.all_languages.items()
if query in key.lower() or query in lang['name'].lower()
]
return partial_matches
async def _auto_translate_to_common(self, text: str) -> list:
"""Helper to find all possible translations for a given text."""
possible_translations = []
for lang_key, lang_obj in self.all_languages.items():
if lang_key == 'common':
continue
try:
decoded_text = lang_obj['from_func'](text)
re_encoded_text = lang_obj['to_func'](decoded_text)
if re_encoded_text == text:
possible_translations.append(f"**As {lang_obj['name']}:**\n{box(decoded_text)}")
except Exception:
continue
return possible_translations

View File

@@ -1,162 +1,249 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import discord import discord
import re import pkgutil
from redbot.core import commands, app_commands from redbot.core import commands, app_commands, Config
from redbot.core.utils.chat_formatting import box
from . import languages as lang_pkg
from .views import DismissView
from .logic import TranslationLogicMixin
from .commands import CommandsMixin
# --- Translation Maps (Ported from JavaScript) --- def reverse_map(m): return {v: k for k, v in m.items()}
VALSPIREN_MAP = {'a':'ak','b':'ba','c':'ce','d':'di','e':'ek','f':'fo','g':'gu','h':'ha','i':'ik','j':'je','k':'ki','l':'lo','m':'mu','n':'na','o':'ok','p':'pe','q':'qi','r':'ro','s':'su','t':'ta','u':'uk','v':'ve','w':'wi','x':'xo','y':'yk','z':'zu'}
KITSUNE_MAP = {'a':'ka','b':'be','c':'chi','d':'de','e':'e','f':'fu','g':'ga','h':'hi','i':'i','j':'ji','k':'ki','l':'ru','m':'ma','n':'na','o':'o','p':'pe','q':'kyu','r':'re','s':'sa','t':'to','u':'u','v':'ve','w':'wa','x':'za','y':'ya','z':'ze'}
ELVISH_MAP = {'a':'ael','b':'\'eth','c':'cal','d':'dor','e':'elen','f':'fae','g':'\'gan','h':'hîr','i':'ia','j':'yel','k':'\'ken','l':'lael','m':'mel','n':'nîn','o':'oia','p':'\'pes','q':'qen','r':'rae','s':'sil','t':'','u':'ui','v':'vae','w':'win','x':'\'xal','y':'yl','z':'zîr'}
DWARVISH_MAP = {'a':'az','b':'bar','c':'krag','d':'dur','e':'ek','f':'fol','g':'grum','h':'hur','i':'in','j':'jor','k':'kaz','l':'lur','m':'mor','n':'nur','o':'ok','p':'por','q':'qur','r':'ruk','s':'son','t':'thor','u':'um','v':'val','w':'wor','x':'xor','z':'zul'}
ORCISH_MAP = {'a':'agh','b':'bug','c':'karg','d':'dur','e':'egh','f':'fug','g':'grol','h':'hosh','i':'izg','j':'jug','k':'krunk','l':'lug','m':'mog','n':'nog','o':'ogg','p':'pug','q':'qug','r':'ruk','s':'snaga','t':'tusk','u':'uruk','v':'vug','w':'warg','x':'xug','y':'yag','z':'zug'}
GOBLIN_MAP = {'a':'az','b':'bik','c':'clik','d':'dik','e':'ek','f':'fiz','g':'gib','h':'hik','i':'ik','j':'jik','k':'krik','l':'lik','m':'mik','n':'nik','o':'ok','p':'pik','q':'qik','r':'rik','s':'snik','t':'tik','u':'uk','v':'vik','w':'wik','x':'xik','y':'yik','z':'zik'}
DRACONIC_MAP = {'a':'ax','b':'baxis','c':'caex','d':'drak','e':'ess','f':'faex','g':'gix','h':'heth','i':'ir','j':'jyss','k':'kex','l':'lix','m':'maex','n':'nex','o':'oth','p':'pex','q':'qexis','r':'rax','s':'syth','t':'thrax','u':'ur','v':'vyx','w':'wess','x':'xis','y':'yth','z':'zix'}
FELINE_MAP = {'a':'meow','b':'brrt','c':'chrr','d':'drrt','e':'eek','f':'frrt','g':'grrowl','h':'hiss','i':'mii','j':'jrr','k':'krr','l':'lrr','m':'mrow','n':'nyah','o':'oww','p':'purr','q':'qrr','r':'rrr','s':'sss','t':'trill','u':'urr','v':'vrr','w':'wrr','x':'xrr','y':'yowl','z':'zzz'}
AQUAN_MAP = {'a':'aqua','b':'blub','c':'\'cress','d':'drop','e':'eelee','f':'flow','g':'glur','h':'hydro','i':'ishi','j':'\'jyr','k':'\'kyr','l':'luu','m':'\'myr','n':'\'nyr','o':'oro','p':'ploop','q':'\'qyr','r':'\'ryp','s':'sh\'l','t':'tide','u':'\'urn','v':'\'vyr','w':'wash','x':'\'xyr','y':'\'yyr','z':'\'zyr'}
CELESTIAL_MAP = {'a':'\'ana','b':'\'bara','c':'\'cera','d':'\'dona','e':'\'elara','f':'\'fana','g':'\'gala','h':'\'hylia','i':'\'iana','j':'\'jana','k':'\'kana','l':'\'lora','m':'\'mara','n':'\'nara','o':'\'ora','p':'\'pera','q':'\'qana','r':'\'ria','s':'\'sera','t':'\'tara','u':'\'ura','v':'\'vara','w':'\'wana','x':'\'xara','y':'\'yana','z':'\'zara'}
INFERNAL_MAP = {'a':'az\'','b':'\'baal','c':'\'krez','d':'\'drak','e':'ez\'','f':'\'fel','g':'\'gor','h':'\'hath','i':'iz\'','j':'\'jaz','k':'\'kraz','l':'\'laz','m':'\'mor','n':'\'naz','o':'oz\'','p':'\'paz','q':'\'qaz','r':'\'raz','s':'\'saz','t':'\'taz','u':'uz\'','v':'\'vaz','w':'\'waz','x':'\'xaz','y':'\'yaz','z':'\'zaz'}
SYLVAN_MAP = {'a':'ani','b':'bri','c':'cae','d':'dae','e':'eni','f':'fae','g':'gra','h':'hae','i':'ia','j':'jae','k':'kae','l':'lor','m':'mae','n':'nem','o':'olo','p':'pae','q':'qae','r':'rae','s':'sae','t':'tae','u':'uni','v':'vae','w':'wae','x':'xae','y':'yae','z':'zae'}
GNOMISH_MAP = {'a':'akk','b':'bink','c':'clank','d':'dink','e':'enk','f':'fizz','g':'giz','h':'hink','i':'ink','j':'jink','k':'kink','l':'link','m':'mink','n':'nink','o':'onk','p':'sprok','q':'qink','r':'rink','s':'sprock','t':'tink','u':'unk','v':'vink','w':'whirr','x':'xink','y':'yink','z':'zink'}
SINARY_MAP = {'a':'a!','b':'7b','c':'c#','d':'d4','e':'3e','f':'f^','g':'g&','h':'h8','i':'(i','j':'j0','k':'_k','l':'l2','m':'=m','n':'n+','o':'5o','p':'-p','q':'q{','r':'}r','s':'[s','t':'t]','u':'|u','v':':v','w':'"w','x':'x<','y':'>y','z':'?z'}
SUCCUBUS_MAP = {'a':'ah\'','b':'\'bel','c':'\'chae','d':'\'des','e':'\'esh','f':'\'fey','g':'\'gis','h':'hah\'','i':'\'ish','j':'\'jo','k':'\'ka','l':'\'lis','m':'\'mah','n':'\'nah','o':'oh\'','p':'\'pah','q':'\'qia','r':'\'rah','s':'\'sha','t':'\'thae','u':'uh\'','v':'\'vi','w':'\'wah','x':'\'xi','y':'\'yah','z':'\'zah'}
SCORPION_MAP = {'a':'sk\'','b':'t\'k','c':'k\'ss','d':'d\'th','e':'e\'sk','f':'f\'t','g':'g\'th','h':'h\'k','i':'i\'s','j':'j\'t','k':'k\'t','l':'l\'k','m':'m\'k','n':'n\'t','o':'o\'s','p':'p\'k','q':'q\'t','r':'r\'k','s':'s\'k','t':'t\'s','u':'u\'s','v':'v\'t','w':'w\'k','x':'x\'s','y':'y\'k','z':'z\'t'}
ABYSSAL_MAP = {'a':'azg','b':'braz','c':'kraz','d':'dorg','e':'ezg','f':'fraz','g':'gor','h':'hath','i':'ix','j':'jraz','k':'kral','l':'laz','m':'maz','n':'naz','o':'oz','p':'praz','q':'qor','r':'raz','s':'saz','t':'taz','u':'uzg','v':'vraz','w':'waz','x':'xul','y':'yaz','z':'zaz'}
SPIRITUAL_MAP = {'a':'\'aura','b':'\'breth','c':'\'ciel','d':'\'dion','e':'\'ethys','f':'\'fey','g':'\'glyn','h':'\'hymn','i':'\'ia','j':'\'jora','k':'\'kye','l':'\'lume','m':'\'mana','n':'\'nima','o':'\'omni','p':'\'pria','q':'\'qia','r':'\'reth','s':'\'seren','t':'\'thyme','u':'\'umbra','v':'\'vym','w':'\'wisp','x':'\'xia','y':'\'yara','z':'\'zion'}
OGRISH_MAP = {'a':'ug','b':'blud','c':'crag','d':'dug','e':'eeg','f':'fug','g':'gron','h':'hug','i':'ig','j':'jug','k':'krug','l':'lug','m':'mush','n':'nug','o':'og','p':'pug','q':'qug','r':'rug','s':'slog','t':'thok','u':'urk','v':'vog','w':'wug','x':'xug','y':'yug','z':'zug'}
AVIAN_MAP = {'a':'\'aara','b':'\'bree','c':'chir\'','d':'\'dree','e':'eek','f':'\'flutter','g':'gree\'','h':'hrooa','i':'ii\'','j':'\'jakk','k':'kree\'','l':'\'liri','m':'\'meeka','n':'\'neer','o':'\'oroo','p':'pip\'','q':'\'qree','r':'\'reea','s':'\'skraw','t':'tweet\'','u':'\'urr','v':'\'vree','w':'\'warble','x':'\'xee','y':'\'yari','z':'\'zeer'}
DEVILISH_MAP = {'a':'\'ayl','b':'\'baal','c':'\'cress','d':'\'drev','e':'\'eyl','f':'\'fane','g':'\'gyl','h':'hysh\'','i':'\'iyl','j':'\'jex','k':'\'krys','l':'\'lyl','m':'\'mal','n':'\'nyl','o':'\'oyl','p':'\'prax','q':'\'qyl','r':'\'ryl','s':'\'shayd','t':'\'trys','u':'\'uyl','v':'\'vyl','w':'\'wryl','x':'\'xyl','y':'\'yyl','z':'\'zyll'}
HERBILORE_MAP = {'a':'aloe','b':'bark','c':'clover','d':'dand','e':'elder','f':'fern','g':'groot','h':'herb','i':'ivy','j':'juni','k':'kelp','l':'leaf','m':'moss','n':'nettle','o':'oak','p':'petal','q':'quin','r':'root','s':'sprout','t':'thyme','u':'ursi','v':'vine','w':'willow','x':'xylem','y':'yarrow','z':'zinni'}
FIENDISH_MAP = {'a':'az\'ael','b':'ba\'al','c':'cre\'z','d':'dre\'th','e':'esh\'','f':'fiir\'','g':'gre\'th','h':'ha\'el','i':'i\'z','j':'je\'th','k':'krez\'','l':'le\'th','m':'morn\'','n':'ne\'th','o':'o\'z','p':'pre\'th','q':'qe\'th','r':'re\'th','s':'se\'th','t':'te\'th','u':'u\'z','v':'ve\'th','w':'we\'th','x':'xith\'','y':'ye\'th','z':'zael\''}
ELEMENTAL_MAP = {'a':'aer','b':'breeze','c':'cinder','d':'dust','e':'ember','f':'flow','g':'gust','h':'hail','i':'ignis','j':'jet','k':'kinetic','l':'lava','m':'mist','n':'nova','o':'ozone','p':'pyre','q':'quake','r':'rain','s':'stone','t':'terra','u':'umbra','v':'vapor','w':'wave','x':'xenon','y':'yon','z':'zephyr'}
AQUATIC_MAP = {'a':'abyss','b':'brine','c':'coral','d':'depth','e':'eel','f':'fin','g':'gurgle','h':'hydro','i':'ink','j':'jelly','k':'krill','l':'lagoon','m':'murk','n':'naut','o':'ocean','p':'pearl','q':'quatic','r':'reef','s':'salt','t':'tide','u':'urchin','v':'void','w':'wave','x':'xiph','y':'yacht','z':'zone'}
CONSTRUCT_MAP = {'a':'auto','b':'bolt','c':'clank','d':'diode','e':'engine','f':'forge','g':'gear','h':'hydro','i':'iron','j':'joint','k':'kinetic','l':'link','m':'motor','n':'node','o':'optic','p':'piston','q':'quantum','r':'rivet','s':'servo','t':'titan','u':'unit','v':'volt','w':'whirr','x':'xenon','y':'yoke','z':'zinc'}
MYCONID_MAP = {'a':'agaric','b':'bolete','c':'cap','d':'decay','e':'enoki','f':'fungi','g':'gill','h':'hyphae','i':'indigo','j':'jelly','k':'kombu','l':'lichen','m':'myco\'','n':'nidur','o':'oyster','p':'puff','q':'quorn','r':'rhizo','s':'spore\'','t':'thallus','u':'umbra','v':'velvet','w':'wart','x':'xero','y':'yeast','z':'zoospore'}
VOIDTOUCHED_MAP = {'a':'a\'th','b':'b\'zoth','c':'c\'thun','d':'d\'gol','e':'e\'th','f':'f\'thagn','g':'g\'noth','h':'h\'zoth','i':'i\'th','j':'j\'th','k':'k\'th','l':'l\'th','m':'m\'th','n':'n\'th','o':'o\'th','p':'p\'th','q':'qor\'','r':'r\'lyeh','s':'s\'th','t':'t\'th','u':'u\'th','v':'v\'lath','w':'w\'th','x':'x\'thul','y':'y\'th','z':'zy\'th'}
LIZARDFOLK_MAP = {'a':'\'ax','b':'\'bax','c':'\'caz','d':'\'daz','e':'\'ex','f':'\'fax','g':'\'gaz','h':'h\'ss','i':'\'ix','j':'\'jax','k':'sk\'ex','l':'\'lax','m':'\'max','n':'\'nax','o':'\'ox','p':'\'pax','q':'\'qax','r':'\'rax','s':'s\'lith','t':'t\'char','u':'\'ux','v':'\'vax','w':'\'wax','x':'\'xax','y':'\'yax','z':'\'zax'}
ARACHNID_MAP = {'a':'\'arr','b':'\'brach','c':'ch\'t','d':'\'drach','e':'\'err','f':'\'fune','g':'\'goss','h':'\'harr','i':'\'itt','j':'\'jarr','k':'klik\'','l':'\'lar','m':'\'marr','n':'\'narr','o':'\'orr','p':'\'parr','q':'\'qarr','r':'\'rarr','s':'skitter\'','t':'th\'k','u':'\'urr','v':'\'varr','w':'web\'','x':'\'xarr','y':'\'yarr','z':'\'zarr'}
ANGELIC_MAP = {'a':'adriel','b':'baraqiel','c':'camael','d':'divinus','e':'elohim','f':'fanuel','g':'gloria','h':'hesed','i':'israfel','j':'jophiel','k':'kyrie','l':'lux','m':'michael','n':'netzach','o':'ophaniel','p':'peniel','q':'qadish','r':'raphael','s':'seraph','t':'tiferet','u':'uriel','v':'virtues','w':'wele\'el','x':'xathanael','y':'yesod','z':'zadkiel'}
RATFOLK_MAP = {'a':'skree','b':'bite','c':'claw','d':'dark-thing','e':'eek','f':'filth','g':'gnaw','h':'hiss','i':'itch','j':'junk','k':'kill','l':'long-tail','m':'muck','n':'nest-thing','o':'rot-stink','p':'plague','q':'quick-quick','r':'rust','s':'skitter','t':'twitch','u':'under-thing','v':'vermin','w':'waste','x':'pox','y':'yes-yes','z':'zap-tail'}
HALFTONGUE_MAP = {'a':'apple','b':'bramble','c':'crumpet','d':'dale','e':'elderberry','f':'fiddle','g':'garden','h':'hearth','i':'iris','j':'jam','k':'kettle','l':'lazy','m':'meadow','n':'nimble','o':'oats','p':'pudding','q':'quaint','r':'river','s':'sunny','t':'tater','u':'underhill','v':'vine','w':'willow','x':'extra','y':'yarn','z':'zesty'}
UNDERCOMMON_MAP = {'a':'velk','b':'xund','c':'k\'yorl','d':'d\'ruth','e':'e\'trorn','f':'faer','g':'gol','h':'h\'chak','i':'i\'lith','j':'j\'lar','k':'k\'lar','l':'lil','m':'m\'lar','n':'nind','o':'olath','p':'p\'lar','q':'qu\'ellar','r':'ril','s':'sorn','t':'t\'lar','u':'uss','v':'v\'lar','w':'wyl','x':'x\'lar','y':'y\'lar','z':'z\'ress'}
MORSE_MAP = {'a':'.-','b':'-...','c':'-.-.','d':'-..','e':'.','f':'..-.','g':'--.','h':'....','i':'..','j':'.---','k':'-.-','l':'.-..','m':'--','n':'-.','o':'---','p':'.--.','q':'--.-','r':'.-.','s':'...','t':'-','u':'..-','v':'...-','w':'.--','x':'-..-','y':'-.--','z':'--..','1':'.----','2':'..---','3':'...--','4':'....-','5':'.....','6':'-....','7':'--...','8':'---..','9':'----.','0':'-----',' ':'/'}
LEET_MAP = {'a':'4','b':'8','c':'(','d':')','e':'3','f':'|=','g':'6','h':'#','i':'1','j':']','k':'|<','l':'1','m':'/\\/\\','n':'/\\/','o':'0','p':'|D','q':'(,)','r':'|2','s':'5','t':'7','u':'|_|','v':'\\/','w':'\\/\\/','x':'><','y':'`/','z':'2'}
# --- Translation Logic Functions --- class Translator(CommandsMixin, TranslationLogicMixin, commands.Cog):
def generic_translator(text, translation_map, separator):
"""Translate text by mapping characters and joining them."""
translated_words = []
for word in text.split(' '):
translated_chars = [translation_map.get(char.lower(), char) for char in word]
translated_words.append(separator.join(translated_chars))
return (' ' if separator else ' ').join(translated_words)
# --- Main Cog Class ---
class Translator(commands.Cog):
"""A cog for translating text into various languages.""" """A cog for translating text into various languages."""
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
self.languages = self._setup_languages() self.config = Config.get_conf(self, identifier=8675309, force_registration=True)
self.config.register_global(languages={})
self.config.register_user(sonas={}, proxy_enabled=False)
self.config.register_channel(autotranslate_users={})
def _setup_languages(self): self.all_languages = {}
"""Initializes the language definitions.""" self.translate_to_common_context_menu = app_commands.ContextMenu(
# Using lambdas to keep it concise, matching the JS structure name="Translate to Common",
return { callback=self.translate_to_common_context,
'common': {'name': 'Common', 'translate': lambda text: text}, )
'abyssal': {'name': 'Abyssal', 'translate': lambda text: generic_translator(text, ABYSSAL_MAP, ' ')},
'angelic': {'name': 'Angelic', 'translate': lambda text: generic_translator(text, ANGELIC_MAP, ' ')},
'aquan': {'name': 'Aquan', 'translate': lambda text: generic_translator(text, AQUAN_MAP, ' ')},
'aquatic': {'name': 'Aquatic', 'translate': lambda text: generic_translator(text, AQUATIC_MAP, ' ')},
'arachnid': {'name': 'Arachnid', 'translate': lambda text: generic_translator(text, ARACHNID_MAP, ' ')},
'avian': {'name': 'Avian', 'translate': lambda text: generic_translator(text, AVIAN_MAP, ' ')},
'celestial': {'name': 'Celestial', 'translate': lambda text: generic_translator(text, CELESTIAL_MAP, ' ')},
'construct': {'name': 'Construct', 'translate': lambda text: generic_translator(text, CONSTRUCT_MAP, ' ')},
'devilish': {'name': 'Devilish', 'translate': lambda text: generic_translator(text, DEVILISH_MAP, ' ')},
'draconic': {'name': 'Draconic', 'translate': lambda text: generic_translator(text, DRACONIC_MAP, ' ')},
'dwarvish': {'name': 'Dwarvish', 'translate': lambda text: generic_translator(text, DWARVISH_MAP, ' ')},
'elemental': {'name': 'Elemental', 'translate': lambda text: generic_translator(text, ELEMENTAL_MAP, ' ')},
'elvish': {'name': 'Elvish', 'translate': lambda text: generic_translator(text, ELVISH_MAP, ' ')},
'feline': {'name': 'Feline', 'translate': lambda text: generic_translator(text, FELINE_MAP, ' ')},
'fiendish': {'name': 'Fiendish', 'translate': lambda text: generic_translator(text, FIENDISH_MAP, ' ')},
'gnomish': {'name': 'Gnomish', 'translate': lambda text: generic_translator(text, GNOMISH_MAP, ' ')},
'goblin': {'name': 'Goblin', 'translate': lambda text: generic_translator(text, GOBLIN_MAP, ' ')},
'halftongue': {'name': 'Half-Tongue', 'translate': lambda text: generic_translator(text, HALFTONGUE_MAP, ' ')},
'herbilore': {'name': 'Herbilore', 'translate': lambda text: generic_translator(text, HERBILORE_MAP, ' ')},
'infernal': {'name': 'Infernal', 'translate': lambda text: generic_translator(text, INFERNAL_MAP, ' ')},
'kitsune': {'name': 'Kitsune', 'translate': lambda text: generic_translator(text, KITSUNE_MAP, '')},
'leet': {'name': 'Leet Speak', 'translate': lambda text: generic_translator(text, LEET_MAP, '')},
'lizardfolk': {'name': 'Lizardfolk', 'translate': lambda text: generic_translator(text, LIZARDFOLK_MAP, ' ')},
'morse': {'name': 'Morse Code', 'translate': lambda text: generic_translator(text, MORSE_MAP, ' ')},
'myconid': {'name': 'Myconid', 'translate': lambda text: generic_translator(text, MYCONID_MAP, ' ')},
'ogrish': {'name': 'Ogrish', 'translate': lambda text: generic_translator(text, OGRISH_MAP, ' ')},
'orcish': {'name': 'Orcish', 'translate': lambda text: generic_translator(text, ORCISH_MAP, ' ')},
'ratfolk': {'name': 'Ratfolk', 'translate': lambda text: generic_translator(text, RATFOLK_MAP, ' ')},
'scorpion': {'name': 'Scorpion', 'translate': lambda text: generic_translator(text, SCORPION_MAP, ' ')},
'sinary': {'name': 'Sinary', 'translate': lambda text: generic_translator(text, SINARY_MAP, '')},
'spiritual': {'name': 'Spiritual', 'translate': lambda text: generic_translator(text, SPIRITUAL_MAP, ' ')},
'succubus': {'name': 'Succubus', 'translate': lambda text: generic_translator(text, SUCCUBUS_MAP, ' ')},
'sylvan': {'name': 'Sylvan', 'translate': lambda text: generic_translator(text, SYLVAN_MAP, ' ')},
'undercommon': {'name': 'Undercommon', 'translate': lambda text: generic_translator(text, UNDERCOMMON_MAP, ' ')},
'valspiren': {'name': 'Valspiren', 'translate': lambda text: generic_translator(text, VALSPIREN_MAP, '')},
'voidtouched': {'name': 'Voidtouched', 'translate': lambda text: generic_translator(text, VOIDTOUCHED_MAP, ' ')},
}
async def language_autocomplete(self, interaction: discord.Interaction, current: str) -> list[app_commands.Choice[str]]: async def cog_load(self):
"""Autocomplete for language choices.""" """Load all languages from config and build the runtime objects."""
choices = [ self.bot.tree.add_command(self.translate_to_common_context_menu)
app_commands.Choice(name=lang_data['name'], value=lang_key) await self._initialize_languages()
for lang_key, lang_data in self.languages.items()
if current.lower() in lang_data['name'].lower()
]
return choices[:25]
@commands.hybrid_command(name="translate", description="Translate text to a selected language.") async def cog_unload(self):
@app_commands.describe( """Remove context menu on cog unload."""
language="The language to translate the text into.", self.bot.tree.remove_command(self.translate_to_common_context_menu.name, type=self.translate_to_common_context_menu.type)
text="The text to translate."
)
@app_commands.autocomplete(language=language_autocomplete)
async def translate(self, ctx: commands.Context, language: str, *, text: str):
"""
Translates the given text into the specified language.
To see a list of available languages, use the command without specifying a language. def _load_base_languages_from_files(self):
""" """Dynamically loads all base languages from the languages/ subfolder."""
language_key = language.lower() base_languages_data = {}
for importer, modname, ispkg in pkgutil.iter_modules(lang_pkg.__path__, f"{lang_pkg.__name__}."):
if not ispkg:
try:
module = __import__(modname, fromlist=["get_language"])
if hasattr(module, "get_language"):
lang_data = module.get_language()
lang_key = modname.split('.')[-1]
base_languages_data[lang_key] = lang_data
except Exception as e:
print(f"Error loading language module {modname}: {e}")
return base_languages_data
if language_key not in self.languages: def _build_runtime_languages(self, language_data):
await ctx.send(f"Sorry, '{language}' is not a valid language. Please choose from the list.", ephemeral=True) """Builds the final language dict with functions from stored data."""
runtime_langs = {}
for key, data in language_data.items():
lang_type = data.get("type", "greedy") # Default to greedy for custom
runtime_langs[key] = {"name": data["name"], "is_custom": data.get("is_custom", False), "type": lang_type}
if lang_type == "rule":
if key == "common":
runtime_langs[key]['to_func'] = lambda text: text
runtime_langs[key]['from_func'] = lambda text: text
elif key == "piglatin":
runtime_langs[key]['to_func'] = self._pig_latin_translator
runtime_langs[key]['from_func'] = self._pig_latin_decoder
elif key == "uwu":
runtime_langs[key]['to_func'] = self._uwu_translator
runtime_langs[key]['from_func'] = self._uwu_decoder
elif lang_type == "generic":
lang_map = data.get("map", {})
chunk_size = data.get("chunk_size", 2)
char_separator = data.get("separator", "")
word_separator = ' ' if char_separator == '' else ' '
runtime_langs[key]['to_func'] = lambda text, m=lang_map, s=char_separator: self._generic_translator(text, m, s)
runtime_langs[key]['from_func'] = lambda text, rm=reverse_map(lang_map), cs=chunk_size, ws=word_separator: " ".join([self._generic_decoder(word, rm, cs) for word in text.split(ws)])
elif lang_type == "special":
if key == "leet":
lang_map = data.get("map", {})
runtime_langs[key]['to_func'] = lambda text, m=lang_map: self._generic_translator(text, m, "")
runtime_langs[key]['from_func'] = lambda text, rm=reverse_map(lang_map): self._leet_decoder(text, rm)
elif key == "morse":
lang_map = data.get("map", {})
runtime_langs[key]['to_func'] = lambda text, m=lang_map: self._generic_translator(text, m, " ")
runtime_langs[key]['from_func'] = lambda text, rm=reverse_map(lang_map): self._morse_decoder(text, rm)
else: # Greedy is the default
lang_map = data.get("map", {})
char_separator = data.get("separator", " ")
word_separator = ' ' if char_separator == '' else ' '
runtime_langs[key]['to_func'] = lambda text, m=lang_map, s=char_separator: self._generic_translator(text, m, s)
runtime_langs[key]['from_func'] = lambda text, rm=reverse_map(lang_map), ws=word_separator: " ".join([self._greedy_decoder(word, rm) for word in text.split(ws)])
return runtime_langs
async def _initialize_languages(self):
"""Merge default and custom languages from config."""
base_languages_data = self._load_base_languages_from_files()
stored_languages_data = await self.config.languages()
if not stored_languages_data:
await self.config.languages.set(base_languages_data)
final_data = base_languages_data
else:
final_data = base_languages_data.copy()
for key, data in stored_languages_data.items():
if data.get("is_custom"):
final_data[key] = data
await self.config.languages.set(final_data)
self.all_languages = self._build_runtime_languages(final_data)
async def translate_to_common_context(self, interaction: discord.Interaction, message: discord.Message):
"""Translate a message to Common."""
await interaction.response.defer(ephemeral=True)
possible_translations = await self._auto_translate_to_common(message.content)
if not possible_translations:
await interaction.followup.send("Could not find a valid translation for this message.", ephemeral=True)
else:
await interaction.followup.send("\n\n".join(possible_translations), ephemeral=True)
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if message.author.bot or not message.guild:
return return
translator_func = self.languages[language_key]['translate'] prefix = (await self.bot.get_prefix(message))[0]
translated_text = translator_func(text) if message.content.startswith(prefix):
return
embed = discord.Embed( if message.reference and message.content.lower() == 'translate':
title=f"Translation to {self.languages[language_key]['name']}", try:
color=discord.Color.blue() replied_message = await message.channel.fetch_message(message.reference.message_id)
) text_to_translate = replied_message.content
embed.add_field(name="Original Text", value=f"```\n{text}\n```", inline=False) possible_translations = await self._auto_translate_to_common(text_to_translate)
embed.add_field(name="Translated Text", value=f"```\n{translated_text}\n```", inline=False)
await ctx.send(embed=embed) if not possible_translations:
response_msg = "Could not find a valid translation for the replied message."
else:
response_msg = "\n\n".join(possible_translations)
@commands.hybrid_command(name="languages", description="List all available languages for translation.") view = DismissView(author=message.author)
async def list_languages(self, ctx: commands.Context): sent_message = await message.reply(
"""Provides a list of all available languages.""" response_msg,
view=view,
allowed_mentions=discord.AllowedMentions.none()
)
view.message = sent_message
sorted_languages = sorted(self.languages.values(), key=lambda x: x['name']) if message.channel.permissions_for(message.guild.me).manage_messages:
await message.delete()
return
except Exception:
return
description = "\n".join([lang['name'] for lang in sorted_languages]) user_settings = await self.config.user(message.author).all()
if not user_settings['sonas']:
return
embed = discord.Embed( content = message.content
title="Available Languages", matched_sona = None
description=description,
color=discord.Color.green()
)
await ctx.send(embed=embed)
async def setup(bot): for sona_data in user_settings['sonas'].values():
await bot.add_cog(Translator(bot)) if 'claws' in sona_data:
start_claw, end_claw = sona_data['claws']
elif 'brackets' in sona_data:
start_claw, end_claw = sona_data['brackets']
else:
continue
if not content.startswith(start_claw): continue
if end_claw and end_claw != "":
if not content.endswith(end_claw) or len(content) < len(start_claw) + len(end_claw):
continue
matched_sona = sona_data
break
if matched_sona:
if not user_settings['proxy_enabled']:
msg = (
f"{message.author.mention}, it looks like you tried to proxy as **{matched_sona['display_name']}**, "
f"but your proxy is turned off. You can re-enable it with the `{prefix}proxy` command."
)
try:
view = DismissView(author=message.author)
sent_message = await message.channel.send(msg, view=view, allowed_mentions=discord.AllowedMentions(users=True))
view.message = sent_message
except discord.Forbidden:
pass
return
if 'claws' in matched_sona:
start_claw, end_claw = matched_sona['claws']
else:
start_claw, end_claw = matched_sona['brackets']
text_to_translate = content[len(start_claw):-len(end_claw)] if end_claw else content[len(start_claw):]
lang_key = matched_sona['language']
else:
autotranslate_users = await self.config.channel(message.channel).autotranslate_users()
user_id_str = str(message.author.id)
if user_id_str not in autotranslate_users:
return
sona_key = autotranslate_users[user_id_str]
sonas = user_settings.get('sonas', {})
if sona_key not in sonas:
return
matched_sona = sonas[sona_key]
text_to_translate = content
lang_key = matched_sona['language']
to_lang_obj = self.all_languages.get(lang_key)
if not to_lang_obj: return
try:
translated_text = to_lang_obj['to_func'](text_to_translate)
except Exception:
return
webhook = None
if message.channel.permissions_for(message.guild.me).manage_webhooks:
webhooks = await message.channel.webhooks()
webhook = next((wh for wh in webhooks if wh.name == "Translator Cog Webhook"), None)
if webhook is None:
webhook = await message.channel.create_webhook(name="Translator Cog Webhook")
if webhook:
if message.channel.permissions_for(message.guild.me).manage_messages:
try:
await message.delete()
except discord.Forbidden:
pass
display_name = matched_sona.get("display_name", message.author.display_name)
avatar_url = matched_sona.get("avatar_url") or message.author.display_avatar.url
await webhook.send(
content=translated_text,
username=display_name,
avatar_url=avatar_url
)

28
translator/views.py Normal file
View File

@@ -0,0 +1,28 @@
# -*- coding: utf-8 -*-
import discord
class DismissView(discord.ui.View):
def __init__(self, author: discord.Member):
super().__init__(timeout=1800) # 30 minute timeout
self.author = author
self.message: discord.Message = None
async def interaction_check(self, interaction: discord.Interaction) -> bool:
if interaction.user.id != self.author.id:
await interaction.response.send_message("You are not authorized to dismiss this message.", ephemeral=True)
return False
return True
@discord.ui.button(label="Dismiss", style=discord.ButtonStyle.grey, emoji="??")
async def dismiss_button(self, interaction: discord.Interaction, button: discord.ui.Button):
await self.message.delete()
self.stop()
async def on_timeout(self):
if self.message:
try:
await self.message.delete()
except (discord.NotFound, discord.Forbidden):
pass # Message was already deleted or permissions are missing
self.stop()