# -*- coding: utf-8 -*- import discord import pkgutil 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 def reverse_map(m): return {v: k for k, v in m.items()} class Translator(CommandsMixin, TranslationLogicMixin, commands.Cog): """A cog for translating text into various languages.""" def __init__(self, bot): self.bot = bot 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={}) self.all_languages = {} self.translate_to_common_context_menu = app_commands.ContextMenu( name="Translate to Common", callback=self.translate_to_common_context, ) async def cog_load(self): """Load all languages from config and build the runtime objects.""" self.bot.tree.add_command(self.translate_to_common_context_menu) await self._initialize_languages() async def cog_unload(self): """Remove context menu on cog unload.""" self.bot.tree.remove_command(self.translate_to_common_context_menu.name, type=self.translate_to_common_context_menu.type) def _load_base_languages_from_files(self): """Dynamically loads all base languages from the languages/ subfolder.""" 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 def _build_runtime_languages(self, language_data): """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 prefix = (await self.bot.get_prefix(message))[0] if message.content.startswith(prefix): return if message.reference and message.content.lower() == 'translate': try: replied_message = await message.channel.fetch_message(message.reference.message_id) text_to_translate = replied_message.content possible_translations = await self._auto_translate_to_common(text_to_translate) if not possible_translations: response_msg = "Could not find a valid translation for the replied message." else: response_msg = "\n\n".join(possible_translations) view = DismissView(author=message.author) sent_message = await message.reply( response_msg, view=view, allowed_mentions=discord.AllowedMentions.none() ) view.message = sent_message if message.channel.permissions_for(message.guild.me).manage_messages: await message.delete() return except Exception: return user_settings = await self.config.user(message.author).all() if not user_settings['sonas']: return content = message.content matched_sona = None for sona_data in user_settings['sonas'].values(): 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 )