""" Babel Cog. Calculates and sets current locale before command runs (via check_once). Also defines the relevant guild and user settings for localisation. """ from typing import Optional import discord from discord.ext import commands as cmds from discord import app_commands as appcmds from meta import LionBot, LionCog, LionContext from meta.errors import UserInputError from settings import ModelData from settings.setting_types import StringSetting, BoolSetting from settings.groups import SettingGroup from core.data import CoreData from .translator import ctx_locale, ctx_translator, LocalBabel, SOURCE_LOCALE babel = LocalBabel('babel') _ = babel._ _p = babel._p class LocaleSettings(SettingGroup): class UserLocale(ModelData, StringSetting): """ User-configured locale. Exposed via dedicated setting command. """ setting_id = 'user_locale' display_name = _p('userset:locale', 'language') desc = _p('userset:locale|desc', "Your preferred language for interacting with me.") _model = CoreData.User _column = CoreData.User.locale.name @property def update_message(self): t = ctx_translator.get().t if self.data is None: return t(_p('userset:locale|response', "You have unset your language.")) else: return t(_p('userset:locale|response', "You have set your language to `{lang}`.")).format( lang=self.data ) @classmethod async def _parse_string(cls, parent_id, string, **kwargs): translator = ctx_translator.get() if string not in translator.supported_locales: lang = string[:20] raise UserInputError( translator.t( _p('userset:locale|error', "Sorry, we do not support the `{lang}` language at this time!") ).format(lang=lang) ) return string class ForceLocale(ModelData, BoolSetting): """ Guild configuration for whether to force usage of the guild locale. Exposed via `/configure language` command and standard configuration interface. """ setting_id = 'force_locale' display_name = _p('guildset:force_locale', 'force_language') desc = _p('guildset:force_locale|desc', "Whether to force all members to use the configured guild language when interacting with me.") long_desc = _p( 'guildset:force_locale|long_desc', "When enabled, commands in this guild will always use the configured guild language, " "regardless of the member's personally configured language." ) _outputs = { True: _p('guildset:force_locale|output', 'Enabled (members will be forced to use the server language)'), False: _p('guildset:force_locale|output', 'Disabled (members may set their own language)'), None: 'Not Set' # This should be impossible, since we have a default } _default = False _model = CoreData.Guild _column = CoreData.Guild.force_locale.name @property def update_message(self): t = ctx_translator.get().t if self.data: return t(_p( 'guildset:force_locale|response', "I will always use the set language in this server." )) else: return t(_p( 'guildset:force_locale|response', "I will now allow the members to set their own language here." )) class GuildLocale(ModelData, StringSetting): """ Guild-configured locale. Exposed via `/configure language` command, and standard configuration interface. """ setting_id = 'guild_locale' display_name = _p('guildset:locale', 'language') desc = _p('guildset:locale|desc', "Your preferred language for interacting with me.") _model = CoreData.Guild _column = CoreData.Guild.locale.name @property def update_message(self): t = ctx_translator.get().t if self.data is None: return t(_p('guildset:locale|response', "You have reset the guild language.")) else: return t(_p('guildset:locale|response', "You have set the guild language to `{lang}`.")).format( lang=self.data ) @classmethod async def _parse_string(cls, parent_id, string, **kwargs): translator = ctx_translator.get() if string not in translator.supported_locales: lang = string[:20] raise UserInputError( translator.t( _p('guildset:locale|error', "Sorry, we do not support the `{lang}` language at this time!") ).format(lang=lang) ) return string class BabelCog(LionCog): depends = {'CoreCog'} def __init__(self, bot: LionBot): self.bot = bot self.settings = LocaleSettings() self.t = self.bot.translator.t async def cog_load(self): if not self.bot.core: raise ValueError("CoreCog must be loaded first!") self.bot.core.guild_settings.attach(LocaleSettings.ForceLocale) self.bot.core.guild_settings.attach(LocaleSettings.GuildLocale) self.bot.core.user_settings.attach(LocaleSettings.UserLocale) async def cog_unload(self): pass async def bot_check_once(self, ctx: LionContext): # type: ignore # Type checker doesn't understand coro checks """ Calculate and inject the current locale before the command begins. Locale resolution is calculated as follows: If the guild has force_locale enabled, and a locale set, then the guild's locale will be used. Otherwise, the priority is user_locale -> command_locale -> user_locale_hint -> guild_locale -> default_locale """ locale = None if ctx.guild: forced = ctx.alion.guild_settings['force_locale'].value guild_locale = ctx.alion.guild_settings['guild_locale'].value if forced: locale = guild_locale locale = locale or ctx.alion.user_settings['user_locale'].value if ctx.interaction: locale = locale or ctx.interaction.locale.value if ctx.guild: locale = locale or guild_locale locale = locale or SOURCE_LOCALE ctx_locale.set(locale) ctx_translator.set(self.bot.translator) return True @cmds.hybrid_command( name=LocaleSettings.UserLocale.display_name, description=LocaleSettings.UserLocale.desc ) async def cmd_language(self, ctx: LionContext, language: str): """ Dedicated user setting command for the `locale` setting. """ if not ctx.interaction: # This command is not available as a text command return setting = await self.settings.UserLocale.get(ctx.author.id) new_data = await setting._parse_string(ctx.author.id, language) await setting.interactive_set(new_data, ctx.interaction) @cmds.hybrid_command( name=_p('cmd:configure_language', "configure_language"), description=_p('cmd:configure_language|desc', "Configure the default language I will use in this server.") ) @appcmds.choices( force_language=[ appcmds.Choice(name=LocaleSettings.ForceLocale._outputs[True], value=1), appcmds.Choice(name=LocaleSettings.ForceLocale._outputs[False], value=0), ] ) @appcmds.guild_only() # Can be removed when attached as a subcommand async def cmd_configure_language( self, ctx: LionContext, language: Optional[str] = None, force_language: Optional[appcmds.Choice[int]] = None ): if not ctx.interaction: # This command is not available as a text command return if not ctx.guild: # This is impossible by decorators, but adding this guard for the type checker return t = self.t # TODO: Setting group, and group setting widget # We can attach the command to the setting group as an application command # Then load it into the configure command group dynamically lang_setting = await self.settings.GuildLocale.get(ctx.guild.id) force_setting = await self.settings.ForceLocale.get(ctx.guild.id) if language: lang_data = await lang_setting._parse_string(ctx.guild.id, language) if force_language is not None: force_data = bool(force_language) if force_language is not None and not (lang_data if language is not None else lang_setting.value): # Setting force without having a language! raise UserInputError( t(_p( 'cmd:configure_language|error', "You cannot enable `{force_setting}` without having a configured language!" )).format(force_setting=t(LocaleSettings.ForceLocale.display_name)) ) # TODO: Really need simultaneous model writes, or batched writes lines = [] if language: lang_setting.data = lang_data await lang_setting.write() lines.append(lang_setting.update_message) if force_language is not None: force_setting.data = force_data await force_setting.write() lines.append(force_setting.update_message) result = '\n'.join( f"{self.bot.config.emojis.tick} {line}" for line in lines ) # TODO: Setting group widget await ctx.reply( embed=discord.Embed( colour=discord.Colour.green(), title=t(_p('cmd:configure_language|success', "Language settings updated!")), description=result ) ) @cmd_configure_language.autocomplete('language') async def cmd_configure_language_acmpl_language(self, interaction: discord.Interaction, partial: str): # TODO: More friendly language names supported = self.bot.translator.supported_locales matching = [lang for lang in supported if partial.lower() in lang] t = self.t if not matching: return [ appcmds.Choice( name=t(_p( 'cmd:configure_language|acmpl:language', "No supported languages matching {partial}" )).format(partial=partial), value='None' ) ] else: return [ appcmds.Choice(name=lang, value=lang) for lang in matching ]