rewrite (babel): Complete configuration UI.

This commit is contained in:
2023-06-05 15:51:03 +03:00
parent 68fb0e9c62
commit eb0731e185
8 changed files with 397 additions and 170 deletions

View File

@@ -8,138 +8,23 @@ from typing import Optional
import discord import discord
from discord.ext import commands as cmds from discord.ext import commands as cmds
from discord import app_commands as appcmds from discord import app_commands as appcmds
from discord.ui.button import ButtonStyle
from meta import LionBot, LionCog, LionContext from meta import LionBot, LionCog, LionContext
from meta.errors import UserInputError from meta.errors import UserInputError
from utils.ui import AButton, AsComponents
from wards import low_management from wards import low_management
from settings import ModelData from .translator import ctx_locale, ctx_translator, SOURCE_LOCALE
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
from . import babel from . import babel
from .enums import locale_names
from .settings import LocaleSettings
from .settingui import LocaleSettingUI
_ = babel._ _ = babel._
_p = babel._p _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): class BabelCog(LionCog):
depends = {'CoreCog'} depends = {'CoreCog'}
@@ -158,6 +43,9 @@ class BabelCog(LionCog):
configcog = self.bot.get_cog('ConfigCog') configcog = self.bot.get_cog('ConfigCog')
self.crossload_group(self.configure_group, configcog.configure_group) self.crossload_group(self.configure_group, configcog.configure_group)
userconfigcog = self.bot.get_cog('UserConfigCog')
self.crossload_group(self.userconfig_group, userconfigcog.userconfig_group)
async def cog_unload(self): async def cog_unload(self):
pass pass
@@ -201,22 +89,6 @@ class BabelCog(LionCog):
ctx_translator.set(self.bot.translator) ctx_translator.set(self.bot.translator)
return True 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)
@LionCog.placeholder_group @LionCog.placeholder_group
@cmds.hybrid_group('configure', with_app_command=False) @cmds.hybrid_group('configure', with_app_command=False)
async def configure_group(self, ctx: LionContext): async def configure_group(self, ctx: LionContext):
@@ -244,9 +116,9 @@ class BabelCog(LionCog):
) )
@appcmds.guild_only() # Can be removed when attached as a subcommand @appcmds.guild_only() # Can be removed when attached as a subcommand
@cmds.check(low_management) @cmds.check(low_management)
async def cmd_configure_language( async def cmd_configure_language(self, ctx: LionContext,
self, ctx: LionContext, language: Optional[str] = None, force_language: Optional[appcmds.Choice[int]] = None language: Optional[str] = None,
): force_language: Optional[appcmds.Choice[int]] = None):
if not ctx.interaction: if not ctx.interaction:
# This command is not available as a text command # This command is not available as a text command
return return
@@ -284,10 +156,10 @@ class BabelCog(LionCog):
force_setting.data = force_data force_setting.data = force_data
await force_setting.write() await force_setting.write()
lines.append(force_setting.update_message) lines.append(force_setting.update_message)
if lines:
result = '\n'.join( result = '\n'.join(
f"{self.bot.config.emojis.tick} {line}" for line in lines f"{self.bot.config.emojis.tick} {line}" for line in lines
) )
# TODO: Setting group widget
await ctx.reply( await ctx.reply(
embed=discord.Embed( embed=discord.Embed(
colour=discord.Colour.green(), colour=discord.Colour.green(),
@@ -296,24 +168,100 @@ class BabelCog(LionCog):
) )
) )
@cmd_configure_language.autocomplete('language') if ctx.channel.id not in LocaleSettingUI._listening or not lines:
async def cmd_configure_language_acmpl_language(self, interaction: discord.Interaction, partial: str): ui = LocaleSettingUI(self.bot, ctx.guild.id, ctx.channel.id)
# TODO: More friendly language names await ui.run(ctx.interaction)
supported = self.bot.translator.supported_locales await ui.wait()
matching = [lang for lang in supported if partial.lower() in lang]
t = self.t @LionCog.placeholder_group
if not matching: @cmds.hybrid_group(name='my')
return [ async def userconfig_group(self, ctx: LionContext):
appcmds.Choice( pass
name=t(_p(
'cmd:configure_language|acmpl:language', @userconfig_group.command(
"No supported languages matching {partial}" name=_p('cmd:userconfig_language', "language"),
)).format(partial=partial), description=_p(
value='None' 'cmd:userconfig_language|desc',
"Set your preferred interaction language."
) )
)
@appcmds.rename(
language=_p('cmd:userconfig_language|param:language', "language")
)
@appcmds.describe(
language=_p(
'cmd:userconfig_language|param:language|desc',
"Which language do you want me to respond in?"
)
)
async def userconfig_language_cmd(self, ctx: LionContext, language: Optional[str] = None):
if not ctx.interaction:
return
t = self.bot.translator.t
setting = await self.settings.UserLocale.get(ctx.author.id)
if language:
new_data = await setting._parse_string(ctx.author.id, language)
await setting.interactive_set(new_data, ctx.interaction, ephemeral=True)
else:
if setting.value:
desc = t(_p(
'cmd:userconfig_language|response:set',
"Your preferred language is currently set to {language}"
)).format(language=setting.formatted)
@AButton(
label=t(_p('cmd:userconfig_language|button:reset|label', "Reset")),
style=ButtonStyle.red
)
async def reset_button(_press: discord.Interaction, pressed):
await _press.response.defer()
await setting.interactive_set(None, ctx.interaction, view=None)
view = AsComponents(reset_button)
else:
desc = t(_p(
'cmd:userconfig_language|response:unset',
"You have not set a preferred language!"
))
view = None
embed = discord.Embed(
colour=discord.Colour.orange(),
description=desc
)
await ctx.reply(embed=embed, ephemeral=True, view=view)
@userconfig_language_cmd.autocomplete('language')
@cmd_configure_language.autocomplete('language')
async def acmpl_language(self, interaction: discord.Interaction, partial: str):
"""
Shared autocomplete for language options.
"""
t = self.bot.translator.t
supported = self.bot.translator.supported_locales
formatted = []
for locale in supported:
name = locale_names.get(locale, None)
if name:
localestr = f"{locale} ({t(name)})"
else:
localestr = locale
formatted.append((locale, localestr))
matching = {item for item in formatted if partial in item[1]}
if matching:
choices = [
appcmds.Choice(name=localestr, value=locale)
for locale, localestr in matching
] ]
else: else:
return [ choices = [
appcmds.Choice(name=lang, value=lang) appcmds.Choice(
for lang in matching name=t(_p(
'acmpl:language|no_match',
"No supported languages matching {partial}"
)).format(partial=partial),
value=partial
)
] ]
return choices

View File

@@ -1,4 +1,7 @@
from enum import Enum from enum import Enum
from . import babel
_p = babel._p
class LocaleMap(Enum): class LocaleMap(Enum):
@@ -32,3 +35,37 @@ class LocaleMap(Enum):
turkish = 'tr' turkish = 'tr'
ukrainian = 'uk' ukrainian = 'uk'
vietnamese = 'vi' vietnamese = 'vi'
locale_names = {
'en-US': _p('localenames|locale:en-US', "American English"),
'en-GB': _p('localenames|locale:en-GB', "British English"),
'bg': _p('localenames|locale:bg', "Bulgarian"),
'zh-CN': _p('localenames|locale:zh-CN', "Chinese"),
'zh-TW': _p('localenames|locale:zh-TW', "Taiwan Chinese"),
'hr': _p('localenames|locale:hr', "Croatian"),
'cs': _p('localenames|locale:cs', "Czech"),
'da': _p('localenames|locale:da', "Danish"),
'nl': _p('localenames|locale:nl', "Dutch"),
'fi': _p('localenames|locale:fi', "Finnish"),
'fr': _p('localenames|locale:fr', "French"),
'de': _p('localenames|locale:de', "German"),
'el': _p('localenames|locale:el', "Greek"),
'hi': _p('localenames|locale:hi', "Hindi"),
'hu': _p('localenames|locale:hu', "Hungarian"),
'it': _p('localenames|locale:it', "Italian"),
'ja': _p('localenames|locale:ja', "Japanese"),
'ko': _p('localenames|locale:ko', "Korean"),
'lt': _p('localenames|locale:lt', "Lithuanian"),
'no': _p('localenames|locale:no', "Norwegian"),
'pl': _p('localenames|locale:pl', "Polish"),
'pt-BR': _p('localenames|locale:pt-BR', "Brazil Portuguese"),
'ro': _p('localenames|locale:ro', "Romanian"),
'ru': _p('localenames|locale:ru', "Russian"),
'es-ES': _p('localenames|locale:es-ES', "Spain Spanish"),
'sv-SE': _p('localenames|locale:sv-SE', "Swedish"),
'th': _p('localenames|locale:th', "Thai"),
'tr': _p('localenames|locale:tr', "Turkish"),
'uk': _p('localenames|locale:uk', "Ukrainian"),
'vi': _p('localenames|locale:vi', "Vietnamese"),
}

144
src/babel/settings.py Normal file
View File

@@ -0,0 +1,144 @@
from settings import ModelData
from settings.setting_types import StringSetting, BoolSetting
from settings.groups import SettingGroup
from meta.errors import UserInputError
from core.data import CoreData
from .translator import ctx_translator
from . import babel
from .enums import locale_names
_p = babel._p
class LocaleSetting(StringSetting):
"""
Base class describing a LocaleSetting.
"""
@classmethod
def _format_data(cls, parent_id, data, **kwargs):
t = ctx_translator.get().t
if data is None:
formatted = t(_p('set_type:locale|formatted:unset', "Unset"))
else:
name = locale_names.get(data, None)
if name:
formatted = f"`{data} ({t(name)})`"
else:
formatted = f"`{data}`"
return formatted
@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('set_type:locale|error', "Sorry, we do not support the language `{lang}` at this time!")
).format(lang=lang)
)
return string
class LocaleSettings(SettingGroup):
class UserLocale(ModelData, LocaleSetting):
"""
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.formatted
)
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."
))
@classmethod
def _format_data(cls, parent_id, data, **kwargs):
t = ctx_translator.get().t
return t(cls._outputs[data])
class GuildLocale(ModelData, LocaleSetting):
"""
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.")
_long_desc = _p(
'guildset:locale|long_desc',
"The default language to use for responses and interactions in this server. "
"Member's own configured language will override this for their commands "
"unless `force_language` is enabled."
)
_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 unset the guild language."))
else:
return t(_p('guildset:locale|response', "You have set the guild language to {lang}.")).format(
lang=self.formatted
)

86
src/babel/settingui.py Normal file
View File

@@ -0,0 +1,86 @@
import asyncio
import discord
from discord.ui.button import button, Button, ButtonStyle
from meta import LionBot
from utils.ui import ConfigUI, MessageUI, DashboardSection
from utils.lib import MessageArgs
from .settings import LocaleSettings
from . import babel
_p = babel._p
class LocaleSettingUI(ConfigUI):
setting_classes = [
LocaleSettings.GuildLocale,
LocaleSettings.ForceLocale,
]
def __init__(self, bot: LionBot, guildid: int, channelid: int, **kwargs):
self.settings = bot.get_cog('BabelCog').settings
super().__init__(bot, guildid, channelid, **kwargs)
# ----- UI Components -----
@button(label="FORCE_BUTTON_PLACEHOLDER", style=ButtonStyle.grey)
async def force_button(self, press: discord.Interaction, pressed: Button):
await press.response.defer()
setting = next(inst for inst in self.instances if inst.setting_id == LocaleSettings.ForceLocale.setting_id)
setting.value = not setting.value
await setting.write()
async def force_button_refresh(self):
button = self.force_button
setting = next(inst for inst in self.instances if inst.setting_id == LocaleSettings.ForceLocale.setting_id)
button.label = self.bot.translator.t(_p(
'ui:locale_config|button:force|label',
"Toggle Force"
))
button.style = ButtonStyle.green if setting.value else ButtonStyle.grey
# ----- UI Flow -----
async def make_message(self) -> MessageArgs:
t = self.bot.translator.t
title = t(_p(
'ui:locale_config|embed|title',
"Language Configuration Panel"
))
embed = discord.Embed(
colour=discord.Colour.orange(),
title=title
)
for setting in self.instances:
embed.add_field(**setting.embed_field, inline=False)
args = MessageArgs(embed=embed)
return args
async def reload(self):
lguild = await self.bot.core.lions.fetch_guild(self.guildid)
self.instances = tuple(
lguild.config.get(setting.setting_id)
for setting in self.setting_classes
)
async def refresh_components(self):
await asyncio.gather(
self.force_button_refresh(),
self.edit_button_refresh(),
self.close_button_refresh(),
self.reset_button_refresh(),
)
self.set_layout(
(self.force_button, self.edit_button, self.reset_button, self.close_button)
)
class LocaleDashboard(DashboardSection):
section_name = _p(
'dash:locale|title',
"Server Language Configuration"
)
configui = LocaleSettingUI
setting_classes = LocaleSettingUI.setting_classes

View File

@@ -33,6 +33,7 @@ class LeoBabel(Translator):
locales = conf.babel.get('locales', '') locales = conf.babel.get('locales', '')
stripped = (loc.strip(', ') for loc in locales.split(',')) stripped = (loc.strip(', ') for loc in locales.split(','))
self.supported_locales = {loc for loc in stripped if loc} self.supported_locales = {loc for loc in stripped if loc}
self.supported_locales.add(SOURCE_LOCALE)
domains = conf.babel.get('domains', '') domains = conf.babel.get('domains', '')
stripped = (dom.strip(', ') for dom in domains.split(',')) stripped = (dom.strip(', ') for dom in domains.split(','))

View File

@@ -53,8 +53,9 @@ async def main():
db=db, db=db,
config=conf, config=conf,
initial_extensions=[ initial_extensions=[
'utils', 'core', 'analytics', 'babel', 'utils', 'core', 'analytics',
'modules', 'modules',
'babel',
'tracking.voice', 'tracking.text', 'tracking.voice', 'tracking.text',
], ],
web_client=session, web_client=session,
@@ -62,6 +63,7 @@ async def main():
testing_guilds=conf.bot.getintlist('admin_guilds'), testing_guilds=conf.bot.getintlist('admin_guilds'),
shard_id=sharding.shard_number, shard_id=sharding.shard_number,
shard_count=sharding.shard_count, shard_count=sharding.shard_count,
help_command=None,
translator=translator translator=translator
) as lionbot: ) as lionbot:
ctx_bot.set(lionbot) ctx_bot.set(lionbot)

View File

@@ -15,6 +15,7 @@ from tracking.text.ui import TextTrackerDashboard
from modules.ranks.ui.config import RankDashboard from modules.ranks.ui.config import RankDashboard
from modules.pomodoro.settingui import TimerDashboard from modules.pomodoro.settingui import TimerDashboard
from modules.rooms.settingui import RoomDashboard from modules.rooms.settingui import RoomDashboard
from babel.settingui import LocaleDashboard
# from modules.statistics.settings import StatisticsConfigUI # from modules.statistics.settings import StatisticsConfigUI
from . import babel, logger from . import babel, logger
@@ -28,7 +29,7 @@ class GuildDashboard(BasePager):
Paged UI providing an overview of the guild configuration. Paged UI providing an overview of the guild configuration.
""" """
pages = [ pages = [
(EconomyDashboard, TasklistDashboard), (LocaleDashboard, EconomyDashboard, TasklistDashboard),
(VoiceTrackerDashboard, TextTrackerDashboard, ), (VoiceTrackerDashboard, TextTrackerDashboard, ),
(RankDashboard, TimerDashboard, RoomDashboard, ) (RankDashboard, TimerDashboard, RoomDashboard, )
] ]

View File

@@ -176,3 +176,11 @@ class UserConfigCog(LionCog):
) )
choices.append(choice) choices.append(choice)
return choices return choices
@cmds.hybrid_group(
name=_p('cmd:userconfig', "my"),
description=_p('cmd:userconfig|desc', "User configuration commands.")
)
async def userconfig_group(self, ctx: LionContext):
# Group base command, no function.
pass