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
287 lines
13 KiB
Python
287 lines
13 KiB
Python
# -*- 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))
|
|
|