rewrite: Localisation support.

This commit is contained in:
2022-11-23 13:11:41 +02:00
parent 2eea40f679
commit 0d5e801945
18 changed files with 666 additions and 27 deletions

View File

@@ -1,3 +1,7 @@
from babel.translator import LocalBabel
babel = LocalBabel('settings_base')
from .data import ModelData
from .base import BaseSetting
from .ui import SettingWidget, InteractiveSetting
from .groups import SettingDotDict, SettingGroup, ModelSettings, ModelSetting

View File

@@ -26,6 +26,8 @@ class BaseSetting(Generic[ParentID, SettingData, SettingValue]):
Additionally, the setting has attributes attached describing
the setting in a user-friendly manner for display purposes.
"""
setting_id: str # Unique source identifier for the setting
_default: Optional[SettingData] = None # Default data value for the setting
def __init__(self, parent_id: ParentID, data: Optional[SettingData], **kwargs):

View File

@@ -23,6 +23,15 @@ class ModelData:
# High level data cache to use, leave as None to disable cache.
_cache = None # Map[id -> value]
@classmethod
def _read_from_row(cls, parent_id, row, **kwargs):
data = row[cls._column]
if cls._cache is not None:
cls._cache[parent_id] = data
return data
@classmethod
async def _reader(cls, parent_id, use_cache=True, **kwargs):
"""

View File

@@ -1,5 +1,10 @@
from typing import Generic, Type, TypeVar, Optional
from typing import Generic, Type, TypeVar, Optional, overload
from data import RowModel
from .data import ModelData
from .ui import InteractiveSetting
from .base import BaseSetting
from utils.lib import tabulate
@@ -47,7 +52,7 @@ class SettingGroup:
self.settings: SettingDotDict[InteractiveSetting] = self.__init_settings__()
def attach(self, cls: Type[T], name: Optional[str] = None):
name = name or cls.__name__
name = name or cls.setting_id
self.settings[name] = cls
return cls
@@ -77,3 +82,58 @@ class SettingGroup:
row_format="[`{invis}{key:<{pad}}{colon}`](https://lionbot.org \"{field[2]}\")\t{value}"
)
return '\n'.join(table_rows)
class ModelSetting(ModelData, BaseSetting):
...
class ModelSettings:
"""
A ModelSettings instance aggregates multiple `ModelSetting` instances
bound to the same parent id on a single Model.
This enables a single point of access
for settings of a given Model,
with support for caching or deriving as needed.
This is an abstract base class,
and should be subclassed to define the contained settings.
"""
_settings: SettingDotDict = SettingDotDict()
model: Type[RowModel]
def __init__(self, parent_id, row, **kwargs):
self.parent_id = parent_id
self.row = row
self.kwargs = kwargs
@classmethod
async def fetch(cls, *parent_id, **kwargs):
"""
Load an instance of this ModelSetting with the given parent_id
and setting keyword arguments.
"""
row = await cls.model.fetch_or_create(*parent_id)
return cls(parent_id, row, **kwargs)
@classmethod
def attach(self, setting_cls):
"""
Decorator to attach the given setting class to this modelsetting.
"""
# This violates the interface principle, use structured typing instead?
if not (issubclass(setting_cls, BaseSetting) and issubclass(setting_cls, ModelData)):
raise ValueError(
f"The provided setting class must be `ModelSetting`, not {setting_cls.__class__.__name__}."
)
self._settings[setting_cls.setting_id] = setting_cls
return setting_cls
def get(self, setting_id):
setting_cls = self._settings.get(setting_id)
data = setting_cls._read_from_row(self.parent_id, self.row, **self.kwargs)
return setting_cls(self.parent_id, data, **self.kwargs)
def __getitem__(self, setting_id):
return self.get(setting_id)

View File

@@ -8,17 +8,24 @@ from discord import ui
from discord.ui.button import button, Button, ButtonStyle
from meta.context import context
from utils.lib import strfdur, parse_dur
from meta.errors import UserInputError
from utils.lib import strfdur, parse_dur
from babel import ctx_translator
from .base import ParentID
from .ui import InteractiveSetting, SettingWidget
from . import babel
_, _p = babel._, babel._p
if TYPE_CHECKING:
from discord.guild import GuildChannel
# TODO: Localise this file
class StringSetting(InteractiveSetting[ParentID, str, str]):
"""
Setting type mixin describing an arbitrary string type.
@@ -34,7 +41,7 @@ class StringSetting(InteractiveSetting[ParentID, str, str]):
Default: True
"""
accepts = "Any text"
accepts = _p('settype:bool|accepts', "Any text")
_maxlen: int = 4000
_quote: bool = True
@@ -70,8 +77,14 @@ class StringSetting(InteractiveSetting[ParentID, str, str]):
Provides some minor input validation.
Treats an empty string as a `None` value.
"""
t = ctx_translator.get().t
if len(string) > cls._maxlen:
raise UserInputError("Provided string is too long! Maximum length: {} characters.".format(cls._maxlen))
raise UserInputError(
t(_p(
'settype:bool|error',
"Provided string is too long! Maximum length: {maxlen} characters."
)).format(maxlen=cls._maxlen)
)
elif len(string) == 0:
return None
else:

View File

@@ -223,14 +223,14 @@ class InteractiveSetting(BaseSetting[ParentID, SettingData, SettingValue]):
f"\nAccepts: {self.accepts}"
))
async def update_response(self, interaction: discord.Interaction, **kwargs):
async def update_response(self, interaction: discord.Interaction, message: Optional[str] = None, **kwargs):
"""
Respond to an interaction which triggered a setting update.
Usually just wraps `update_message` in an embed and sends it back.
Passes any extra `kwargs` to the message creation method.
"""
embed = discord.Embed(
description=f"{str(conf.emojis.tick)} {self.update_message}",
description=f"{str(conf.emojis.tick)} {message or self.update_message}",
colour=discord.Color.green()
)
if interaction.response.is_done():