mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-05-14 09:50:03 +00:00
Add initial support for app command localisation
This commit is contained in:
parent
eb3bc7102b
commit
2d586ae805
@ -15,5 +15,6 @@ from .models import *
|
||||
from .tree import *
|
||||
from .namespace import *
|
||||
from .transformers import *
|
||||
from .translator import *
|
||||
from . import checks as checks
|
||||
from .checks import Cooldown as Cooldown
|
||||
|
@ -48,10 +48,11 @@ from textwrap import TextWrapper
|
||||
|
||||
import re
|
||||
|
||||
from ..enums import AppCommandOptionType, AppCommandType
|
||||
from ..enums import AppCommandOptionType, AppCommandType, Locale
|
||||
from .models import Choice
|
||||
from .transformers import annotation_to_parameter, CommandParameter, NoneType
|
||||
from .errors import AppCommandError, CheckFailure, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered
|
||||
from .translator import TranslationContext, Translator, locale_str
|
||||
from ..message import Message
|
||||
from ..user import User
|
||||
from ..member import Member
|
||||
@ -281,18 +282,21 @@ def _populate_descriptions(params: Dict[str, CommandParameter], descriptions: Di
|
||||
param.description = '…'
|
||||
continue
|
||||
|
||||
if not isinstance(description, str):
|
||||
if not isinstance(description, (str, locale_str)):
|
||||
raise TypeError('description must be a string')
|
||||
|
||||
param.description = _shorten(description)
|
||||
if isinstance(description, str):
|
||||
param.description = _shorten(description)
|
||||
else:
|
||||
param.description = description
|
||||
|
||||
if descriptions:
|
||||
first = next(iter(descriptions))
|
||||
raise TypeError(f'unknown parameter given: {first}')
|
||||
|
||||
|
||||
def _populate_renames(params: Dict[str, CommandParameter], renames: Dict[str, str]) -> None:
|
||||
rename_map: Dict[str, str] = {}
|
||||
def _populate_renames(params: Dict[str, CommandParameter], renames: Dict[str, Union[str, locale_str]]) -> None:
|
||||
rename_map: Dict[str, Union[str, locale_str]] = {}
|
||||
|
||||
# original name to renamed name
|
||||
|
||||
@ -306,7 +310,11 @@ def _populate_renames(params: Dict[str, CommandParameter], renames: Dict[str, st
|
||||
if name in rename_map:
|
||||
raise ValueError(f'{new_name} is already used')
|
||||
|
||||
new_name = validate_name(new_name)
|
||||
if isinstance(new_name, str):
|
||||
new_name = validate_name(new_name)
|
||||
else:
|
||||
validate_name(new_name.message)
|
||||
|
||||
rename_map[name] = new_name
|
||||
params[name]._rename = new_name
|
||||
|
||||
@ -449,6 +457,12 @@ def _get_context_menu_parameter(func: ContextMenuCallback) -> Tuple[str, Any, Ap
|
||||
return (parameter.name, resolved, type)
|
||||
|
||||
|
||||
async def _get_translation_payload(
|
||||
command: Union[Command[Any, ..., Any], Group, ContextMenu], translator: Translator
|
||||
) -> Dict[str, Any]:
|
||||
...
|
||||
|
||||
|
||||
class Command(Generic[GroupT, P, T]):
|
||||
"""A class that implements an application command.
|
||||
|
||||
@ -464,10 +478,12 @@ class Command(Generic[GroupT, P, T]):
|
||||
Attributes
|
||||
------------
|
||||
name: :class:`str`
|
||||
The name of the application command.
|
||||
The name of the application command. When passed as an argument
|
||||
to ``__init__`` this can also be :class:`locale_str`.
|
||||
description: :class:`str`
|
||||
The description of the application command. This shows up in the UI to describe
|
||||
the application command.
|
||||
the application command. When passed as an argument to ``__init__`` this
|
||||
can also be :class:`locale_str`.
|
||||
checks
|
||||
A list of predicates that take a :class:`~discord.Interaction` parameter
|
||||
to indicate whether the command callback should be executed. If an exception
|
||||
@ -501,16 +517,22 @@ class Command(Generic[GroupT, P, T]):
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
description: str,
|
||||
name: Union[str, locale_str],
|
||||
description: Union[str, locale_str],
|
||||
callback: CommandCallback[GroupT, P, T],
|
||||
nsfw: bool = False,
|
||||
parent: Optional[Group] = None,
|
||||
guild_ids: Optional[List[int]] = None,
|
||||
extras: Dict[Any, Any] = MISSING,
|
||||
):
|
||||
name, locale = (name.message, name) if isinstance(name, locale_str) else (name, None)
|
||||
self.name: str = validate_name(name)
|
||||
self._locale_name: Optional[locale_str] = locale
|
||||
description, locale = (
|
||||
(description.message, description) if isinstance(description, locale_str) else (description, None)
|
||||
)
|
||||
self.description: str = description
|
||||
self._locale_description: Optional[locale_str] = locale
|
||||
self._attr: Optional[str] = None
|
||||
self._callback: CommandCallback[GroupT, P, T] = callback
|
||||
self.parent: Optional[Group] = parent
|
||||
@ -561,9 +583,11 @@ class Command(Generic[GroupT, P, T]):
|
||||
cls = self.__class__
|
||||
copy = cls.__new__(cls)
|
||||
copy.name = self.name
|
||||
copy._locale_name = self._locale_name
|
||||
copy._guild_ids = self._guild_ids
|
||||
copy.checks = self.checks
|
||||
copy.description = self.description
|
||||
copy._locale_description = self._locale_description
|
||||
copy.default_permissions = self.default_permissions
|
||||
copy.guild_only = self.guild_only
|
||||
copy.nsfw = self.nsfw
|
||||
@ -581,6 +605,28 @@ class Command(Generic[GroupT, P, T]):
|
||||
|
||||
return copy
|
||||
|
||||
async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]:
|
||||
base = self.to_dict()
|
||||
name_localizations: Dict[str, str] = {}
|
||||
description_localizations: Dict[str, str] = {}
|
||||
for locale in Locale:
|
||||
if self._locale_name:
|
||||
translation = await translator._checked_translate(self._locale_name, locale, TranslationContext.command_name)
|
||||
if translation is not None:
|
||||
name_localizations[locale.value] = translation
|
||||
|
||||
if self._locale_description:
|
||||
translation = await translator._checked_translate(
|
||||
self._locale_description, locale, TranslationContext.command_description
|
||||
)
|
||||
if translation is not None:
|
||||
description_localizations[locale.value] = translation
|
||||
|
||||
base['name_localizations'] = name_localizations
|
||||
base['description_localizations'] = description_localizations
|
||||
base['options'] = [await param.get_translated_payload(translator) for param in self._params.values()]
|
||||
return base
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
# If we have a parent then our type is a subcommand
|
||||
# Otherwise, the type falls back to the specific command type (e.g. slash command or context menu)
|
||||
@ -929,7 +975,8 @@ class ContextMenu:
|
||||
Attributes
|
||||
------------
|
||||
name: :class:`str`
|
||||
The name of the context menu.
|
||||
The name of the context menu. When passed as an argument to ``__init__``
|
||||
this can be :class:`locale_str`.
|
||||
type: :class:`.AppCommandType`
|
||||
The type of context menu application command. By default, this is inferred
|
||||
by the parameter of the callback.
|
||||
@ -958,14 +1005,16 @@ class ContextMenu:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
name: Union[str, locale_str],
|
||||
callback: ContextMenuCallback,
|
||||
type: AppCommandType = MISSING,
|
||||
nsfw: bool = False,
|
||||
guild_ids: Optional[List[int]] = None,
|
||||
extras: Dict[Any, Any] = MISSING,
|
||||
):
|
||||
name, locale = (name.message, name) if isinstance(name, locale_str) else (name, None)
|
||||
self.name: str = validate_context_menu_name(name)
|
||||
self._locale_name: Optional[locale_str] = locale
|
||||
self._callback: ContextMenuCallback = callback
|
||||
(param, annotation, actual_type) = _get_context_menu_parameter(callback)
|
||||
if type is MISSING:
|
||||
@ -998,6 +1047,18 @@ class ContextMenu:
|
||||
""":class:`str`: Returns the fully qualified command name."""
|
||||
return self.name
|
||||
|
||||
async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]:
|
||||
base = self.to_dict()
|
||||
if self._locale_name:
|
||||
name_localizations: Dict[str, str] = {}
|
||||
for locale in Locale:
|
||||
translation = await translator._checked_translate(self._locale_name, locale, TranslationContext.command_name)
|
||||
if translation is not None:
|
||||
name_localizations[locale.value] = translation
|
||||
|
||||
base['name_localizations'] = name_localizations
|
||||
return base
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
'name': self.name,
|
||||
@ -1107,11 +1168,13 @@ class Group:
|
||||
------------
|
||||
name: :class:`str`
|
||||
The name of the group. If not given, it defaults to a lower-case
|
||||
kebab-case version of the class name.
|
||||
kebab-case version of the class name. When passed as an argument to
|
||||
``__init__`` or the class this can be :class:`locale_str`.
|
||||
description: :class:`str`
|
||||
The description of the group. This shows up in the UI to describe
|
||||
the group. If not given, it defaults to the docstring of the
|
||||
class shortened to 100 characters.
|
||||
class shortened to 100 characters. When passed as an argument to
|
||||
``__init__`` or the class this can be :class:`locale_str`.
|
||||
default_permissions: Optional[:class:`~discord.Permissions`]
|
||||
The default permissions that can execute this group on Discord. Note
|
||||
that server administrators can override this value in the client.
|
||||
@ -1140,6 +1203,8 @@ class Group:
|
||||
__discord_app_commands_skip_init_binding__: bool = False
|
||||
__discord_app_commands_group_name__: str = MISSING
|
||||
__discord_app_commands_group_description__: str = MISSING
|
||||
__discord_app_commands_group_locale_name__: Optional[locale_str] = None
|
||||
__discord_app_commands_group_locale_description__: Optional[locale_str] = None
|
||||
__discord_app_commands_group_nsfw__: bool = False
|
||||
__discord_app_commands_guild_only__: bool = MISSING
|
||||
__discord_app_commands_default_permissions__: Optional[Permissions] = MISSING
|
||||
@ -1151,8 +1216,8 @@ class Group:
|
||||
def __init_subclass__(
|
||||
cls,
|
||||
*,
|
||||
name: str = MISSING,
|
||||
description: str = MISSING,
|
||||
name: Union[str, locale_str] = MISSING,
|
||||
description: Union[str, locale_str] = MISSING,
|
||||
guild_only: bool = MISSING,
|
||||
nsfw: bool = False,
|
||||
default_permissions: Optional[Permissions] = MISSING,
|
||||
@ -1175,16 +1240,22 @@ class Group:
|
||||
|
||||
if name is MISSING:
|
||||
cls.__discord_app_commands_group_name__ = validate_name(_to_kebab_case(cls.__name__))
|
||||
else:
|
||||
elif isinstance(name, str):
|
||||
cls.__discord_app_commands_group_name__ = validate_name(name)
|
||||
else:
|
||||
cls.__discord_app_commands_group_name__ = validate_name(name.message)
|
||||
cls.__discord_app_commands_group_locale_name__ = name
|
||||
|
||||
if description is MISSING:
|
||||
if cls.__doc__ is None:
|
||||
cls.__discord_app_commands_group_description__ = '…'
|
||||
else:
|
||||
cls.__discord_app_commands_group_description__ = _shorten(cls.__doc__)
|
||||
else:
|
||||
elif isinstance(description, str):
|
||||
cls.__discord_app_commands_group_description__ = description
|
||||
else:
|
||||
cls.__discord_app_commands_group_description__ = description.message
|
||||
cls.__discord_app_commands_group_locale_description__ = description
|
||||
|
||||
if guild_only is not MISSING:
|
||||
cls.__discord_app_commands_guild_only__ = guild_only
|
||||
@ -1199,8 +1270,8 @@ class Group:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str = MISSING,
|
||||
description: str = MISSING,
|
||||
name: Union[str, locale_str] = MISSING,
|
||||
description: Union[str, locale_str] = MISSING,
|
||||
parent: Optional[Group] = None,
|
||||
guild_ids: Optional[List[int]] = None,
|
||||
guild_only: bool = MISSING,
|
||||
@ -1209,8 +1280,28 @@ class Group:
|
||||
extras: Dict[Any, Any] = MISSING,
|
||||
):
|
||||
cls = self.__class__
|
||||
self.name: str = validate_name(name) if name is not MISSING else cls.__discord_app_commands_group_name__
|
||||
self.description: str = description or cls.__discord_app_commands_group_description__
|
||||
|
||||
if name is MISSING:
|
||||
name, locale = cls.__discord_app_commands_group_name__, cls.__discord_app_commands_group_locale_name__
|
||||
elif isinstance(name, str):
|
||||
name, locale = validate_name(name), None
|
||||
else:
|
||||
name, locale = validate_name(name.message), name
|
||||
self.name: str = name
|
||||
self._locale_name: Optional[locale_str] = locale
|
||||
|
||||
if description is MISSING:
|
||||
description, locale = (
|
||||
cls.__discord_app_commands_group_description__,
|
||||
cls.__discord_app_commands_group_locale_description__,
|
||||
)
|
||||
elif isinstance(description, str):
|
||||
description, locale = description, None
|
||||
else:
|
||||
description, locale = description.message, description
|
||||
self.description: str = description
|
||||
self._locale_description: Optional[locale_str] = locale
|
||||
|
||||
self._attr: Optional[str] = None
|
||||
self._owner_cls: Optional[Type[Any]] = None
|
||||
self._guild_ids: Optional[List[int]] = guild_ids or getattr(cls, '__discord_app_commands_default_guilds__', None)
|
||||
@ -1291,8 +1382,10 @@ class Group:
|
||||
cls = self.__class__
|
||||
copy = cls.__new__(cls)
|
||||
copy.name = self.name
|
||||
copy._locale_name = self._locale_name
|
||||
copy._guild_ids = self._guild_ids
|
||||
copy.description = self.description
|
||||
copy._locale_description = self._locale_description
|
||||
copy.parent = parent
|
||||
copy.module = self.module
|
||||
copy.default_permissions = self.default_permissions
|
||||
@ -1321,6 +1414,28 @@ class Group:
|
||||
|
||||
return copy
|
||||
|
||||
async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]:
|
||||
base = self.to_dict()
|
||||
name_localizations: Dict[str, str] = {}
|
||||
description_localizations: Dict[str, str] = {}
|
||||
for locale in Locale:
|
||||
if self._locale_name:
|
||||
translation = await translator._checked_translate(self._locale_name, locale, TranslationContext.command_name)
|
||||
if translation is not None:
|
||||
name_localizations[locale.value] = translation
|
||||
|
||||
if self._locale_description:
|
||||
translation = await translator._checked_translate(
|
||||
self._locale_description, locale, TranslationContext.command_description
|
||||
)
|
||||
if translation is not None:
|
||||
description_localizations[locale.value] = translation
|
||||
|
||||
base['name_localizations'] = name_localizations
|
||||
base['description_localizations'] = description_localizations
|
||||
base['options'] = [await child.get_translated_payload(translator) for child in self._children.values()]
|
||||
return base
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
# If this has a parent command then it's part of a subcommand group
|
||||
# Otherwise, it's just a regular command
|
||||
@ -1535,8 +1650,8 @@ class Group:
|
||||
def command(
|
||||
self,
|
||||
*,
|
||||
name: str = MISSING,
|
||||
description: str = MISSING,
|
||||
name: Union[str, locale_str] = MISSING,
|
||||
description: Union[str, locale_str] = MISSING,
|
||||
nsfw: bool = False,
|
||||
extras: Dict[Any, Any] = MISSING,
|
||||
) -> Callable[[CommandCallback[GroupT, P, T]], Command[GroupT, P, T]]:
|
||||
@ -1544,10 +1659,10 @@ class Group:
|
||||
|
||||
Parameters
|
||||
------------
|
||||
name: :class:`str`
|
||||
name: Union[:class:`str`, :class:`locale_str`]
|
||||
The name of the application command. If not given, it defaults to a lower-case
|
||||
version of the callback name.
|
||||
description: :class:`str`
|
||||
description: Union[:class:`str`, :class:`locale_str`]
|
||||
The description of the application command. This shows up in the UI to describe
|
||||
the application command. If not given, it defaults to the first line of the docstring
|
||||
of the callback shortened to 100 characters.
|
||||
@ -1586,8 +1701,8 @@ class Group:
|
||||
|
||||
def command(
|
||||
*,
|
||||
name: str = MISSING,
|
||||
description: str = MISSING,
|
||||
name: Union[str, locale_str] = MISSING,
|
||||
description: Union[str, locale_str] = MISSING,
|
||||
nsfw: bool = False,
|
||||
extras: Dict[Any, Any] = MISSING,
|
||||
) -> Callable[[CommandCallback[GroupT, P, T]], Command[GroupT, P, T]]:
|
||||
@ -1637,7 +1752,7 @@ def command(
|
||||
|
||||
def context_menu(
|
||||
*,
|
||||
name: str = MISSING,
|
||||
name: Union[str, locale_str] = MISSING,
|
||||
nsfw: bool = False,
|
||||
extras: Dict[Any, Any] = MISSING,
|
||||
) -> Callable[[ContextMenuCallback], ContextMenu]:
|
||||
@ -1662,7 +1777,7 @@ def context_menu(
|
||||
|
||||
Parameters
|
||||
------------
|
||||
name: :class:`str`
|
||||
name: Union[:class:`str`, :class:`locale_str`]
|
||||
The name of the context menu command. If not given, it defaults to a title-case
|
||||
version of the callback name. Note that unlike regular slash commands this can
|
||||
have spaces and upper case characters in the name.
|
||||
@ -1685,7 +1800,7 @@ def context_menu(
|
||||
return decorator
|
||||
|
||||
|
||||
def describe(**parameters: str) -> Callable[[T], T]:
|
||||
def describe(**parameters: Union[str, locale_str]) -> Callable[[T], T]:
|
||||
r"""Describes the given parameters by their name using the key of the keyword argument
|
||||
as the name.
|
||||
|
||||
@ -1700,7 +1815,7 @@ def describe(**parameters: str) -> Callable[[T], T]:
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*\*parameters
|
||||
\*\*parameters: Union[:class:`str`, :class:`locale_str`]
|
||||
The description of the parameters.
|
||||
|
||||
Raises
|
||||
@ -1723,7 +1838,7 @@ def describe(**parameters: str) -> Callable[[T], T]:
|
||||
return decorator
|
||||
|
||||
|
||||
def rename(**parameters: str) -> Callable[[T], T]:
|
||||
def rename(**parameters: Union[str, locale_str]) -> Callable[[T], T]:
|
||||
r"""Renames the given parameters by their name using the key of the keyword argument
|
||||
as the name.
|
||||
|
||||
@ -1741,7 +1856,7 @@ def rename(**parameters: str) -> Callable[[T], T]:
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
\*\*parameters
|
||||
\*\*parameters: Union[:class:`str`, :class:`locale_str`]
|
||||
The name of the parameters.
|
||||
|
||||
Raises
|
||||
|
@ -27,13 +27,14 @@ from __future__ import annotations
|
||||
from typing import Any, TYPE_CHECKING, List, Optional, Union
|
||||
|
||||
|
||||
from ..enums import AppCommandOptionType, AppCommandType
|
||||
from ..enums import AppCommandOptionType, AppCommandType, Locale
|
||||
from ..errors import DiscordException
|
||||
|
||||
__all__ = (
|
||||
'AppCommandError',
|
||||
'CommandInvokeError',
|
||||
'TransformerError',
|
||||
'TranslationError',
|
||||
'CheckFailure',
|
||||
'CommandAlreadyRegistered',
|
||||
'CommandSignatureMismatch',
|
||||
@ -51,6 +52,7 @@ __all__ = (
|
||||
if TYPE_CHECKING:
|
||||
from .commands import Command, Group, ContextMenu
|
||||
from .transformers import Transformer
|
||||
from .translator import TranslationContext, locale_str
|
||||
from ..types.snowflake import Snowflake, SnowflakeList
|
||||
from .checks import Cooldown
|
||||
|
||||
@ -133,6 +135,50 @@ class TransformerError(AppCommandError):
|
||||
super().__init__(f'Failed to convert {value} to {transformer._error_display_name!s}')
|
||||
|
||||
|
||||
class TranslationError(AppCommandError):
|
||||
"""An exception raised when the library fails to translate a string.
|
||||
|
||||
This inherits from :exc:`~discord.app_commands.AppCommandError`.
|
||||
|
||||
If an exception occurs while calling :meth:`Translator.translate` that does
|
||||
not subclass this then the exception is wrapped into this exception.
|
||||
The original exception can be retrieved using the ``__cause__`` attribute.
|
||||
Otherwise it will be propagated as-is.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
-----------
|
||||
string: Optional[Union[:class:`str`, :class:`locale_str`]]
|
||||
The string that caused the error, if any.
|
||||
locale: Optional[:class:`~discord.Locale`]
|
||||
The locale that caused the error, if any.
|
||||
context: :class:`~discord.app_commands.TranslationContext`
|
||||
The context of the translation that triggered the error.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*msg: str,
|
||||
string: Optional[Union[str, locale_str]] = None,
|
||||
locale: Optional[Locale] = None,
|
||||
context: TranslationContext,
|
||||
) -> None:
|
||||
self.string: Optional[Union[str, locale_str]] = string
|
||||
self.locale: Optional[Locale] = locale
|
||||
self.context: TranslationContext = context
|
||||
|
||||
if msg:
|
||||
super().__init__(*msg)
|
||||
else:
|
||||
ctx = context.name.replace('_', ' ')
|
||||
fmt = f'Failed to translate {self.string!r} in a {ctx}'
|
||||
if self.locale is not None:
|
||||
fmt = f'{fmt} in the {self.locale.value} locale'
|
||||
|
||||
super().__init__(fmt)
|
||||
|
||||
|
||||
class CheckFailure(AppCommandError):
|
||||
"""An exception raised when check predicates in a command have failed.
|
||||
|
||||
|
@ -26,8 +26,9 @@ from __future__ import annotations
|
||||
from datetime import datetime
|
||||
|
||||
from .errors import MissingApplicationID
|
||||
from .translator import Translator, TranslationContext, locale_str
|
||||
from ..permissions import Permissions
|
||||
from ..enums import AppCommandOptionType, AppCommandType, AppCommandPermissionType, ChannelType, try_enum
|
||||
from ..enums import AppCommandOptionType, AppCommandType, AppCommandPermissionType, ChannelType, Locale, try_enum
|
||||
from ..mixins import Hashable
|
||||
from ..utils import _get_as_snowflake, parse_time, snowflake_time, MISSING
|
||||
from ..object import Object
|
||||
@ -56,7 +57,6 @@ def is_app_command_argument_type(value: int) -> bool:
|
||||
if TYPE_CHECKING:
|
||||
from ..types.command import (
|
||||
ApplicationCommand as ApplicationCommandPayload,
|
||||
ApplicationCommandOptionChoice,
|
||||
ApplicationCommandOption,
|
||||
ApplicationCommandPermissions,
|
||||
GuildApplicationCommandPermissions,
|
||||
@ -407,17 +407,22 @@ class Choice(Generic[ChoiceT]):
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
name: Union[:class:`str`, :class:`locale_str`]
|
||||
The name of the choice. Used for display purposes.
|
||||
name_localizations: Dict[:class:`~discord.Locale`, :class:`str`]
|
||||
The localised names of the choice. Used for display purposes.
|
||||
value: Union[:class:`int`, :class:`str`, :class:`float`]
|
||||
The value of the choice.
|
||||
"""
|
||||
|
||||
__slots__ = ('name', 'value')
|
||||
__slots__ = ('name', 'value', '_locale_name', 'name_localizations')
|
||||
|
||||
def __init__(self, *, name: str, value: ChoiceT):
|
||||
def __init__(self, *, name: Union[str, locale_str], value: ChoiceT, name_localizations: Dict[Locale, str] = MISSING):
|
||||
name, locale = (name.message, name) if isinstance(name, locale_str) else (name, None)
|
||||
self.name: str = name
|
||||
self._locale_name: Optional[locale_str] = locale
|
||||
self.value: ChoiceT = value
|
||||
self.name_localizations: Dict[Locale, str] = MISSING
|
||||
|
||||
def __eq__(self, o: object) -> bool:
|
||||
return isinstance(o, Choice) and self.name == o.name and self.value == o.value
|
||||
@ -441,11 +446,28 @@ class Choice(Generic[ChoiceT]):
|
||||
f'invalid Choice value type given, expected int, str, or float but received {self.value.__class__!r}'
|
||||
)
|
||||
|
||||
def to_dict(self) -> ApplicationCommandOptionChoice:
|
||||
return { # type: ignore
|
||||
async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]:
|
||||
base = self.to_dict()
|
||||
name_localizations: Dict[str, str] = {}
|
||||
if self._locale_name:
|
||||
for locale in Locale:
|
||||
translation = await translator._checked_translate(self._locale_name, locale, TranslationContext.choice_name)
|
||||
if translation is not None:
|
||||
name_localizations[locale.value] = translation
|
||||
|
||||
if name_localizations:
|
||||
base['name_localizations'] = name_localizations
|
||||
|
||||
return base
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
base = {
|
||||
'name': self.name,
|
||||
'value': self.value,
|
||||
}
|
||||
if self.name_localizations is not MISSING:
|
||||
base['name_localizations'] = {str(k): v for k, v in self.name_localizations.items()}
|
||||
return base
|
||||
|
||||
|
||||
class AppCommandChannel(Hashable):
|
||||
|
@ -46,10 +46,11 @@ from typing import (
|
||||
|
||||
from .errors import AppCommandError, TransformerError
|
||||
from .models import AppCommandChannel, AppCommandThread, Choice
|
||||
from .translator import locale_str, Translator, TranslationContext
|
||||
from ..channel import StageChannel, VoiceChannel, TextChannel, CategoryChannel
|
||||
from ..abc import GuildChannel
|
||||
from ..threads import Thread
|
||||
from ..enums import Enum as InternalEnum, AppCommandOptionType, ChannelType
|
||||
from ..enums import Enum as InternalEnum, AppCommandOptionType, ChannelType, Locale
|
||||
from ..utils import MISSING, maybe_coroutine
|
||||
from ..user import User
|
||||
from ..role import Role
|
||||
@ -95,8 +96,10 @@ class CommandParameter:
|
||||
The maximum supported value for this parameter.
|
||||
"""
|
||||
|
||||
# The name of the parameter is *always* the parameter name in the code
|
||||
# Therefore, it can't be Union[str, locale_str]
|
||||
name: str = MISSING
|
||||
description: str = MISSING
|
||||
description: Union[str, locale_str] = MISSING
|
||||
required: bool = MISSING
|
||||
default: Any = MISSING
|
||||
choices: List[Choice[Union[str, int, float]]] = MISSING
|
||||
@ -105,9 +108,49 @@ class CommandParameter:
|
||||
min_value: Optional[Union[int, float]] = None
|
||||
max_value: Optional[Union[int, float]] = None
|
||||
autocomplete: Optional[Callable[..., Coroutine[Any, Any, Any]]] = None
|
||||
_rename: str = MISSING
|
||||
_rename: Union[str, locale_str] = MISSING
|
||||
_annotation: Any = MISSING
|
||||
|
||||
async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]:
|
||||
base = self.to_dict()
|
||||
|
||||
needs_name_translations = isinstance(self._rename, locale_str)
|
||||
needs_description_translations = isinstance(self.description, locale_str)
|
||||
name_localizations: Dict[str, str] = {}
|
||||
description_localizations: Dict[str, str] = {}
|
||||
for locale in Locale:
|
||||
if needs_name_translations:
|
||||
translation = await translator._checked_translate(
|
||||
self._rename, # type: ignore # This will always be locale_str
|
||||
locale,
|
||||
TranslationContext.parameter_name,
|
||||
)
|
||||
if translation is not None:
|
||||
name_localizations[locale.value] = translation
|
||||
|
||||
if needs_description_translations:
|
||||
translation = await translator._checked_translate(
|
||||
self.description, # type: ignore # This will always be locale_str
|
||||
locale,
|
||||
TranslationContext.parameter_description,
|
||||
)
|
||||
if translation is not None:
|
||||
description_localizations[locale.value] = translation
|
||||
|
||||
if isinstance(self.description, locale_str):
|
||||
base['description'] = self.description.message
|
||||
|
||||
if self.choices:
|
||||
base['choices'] = [await choice.get_translated_payload(translator) for choice in self.choices]
|
||||
|
||||
if name_localizations:
|
||||
base['name_localizations'] = name_localizations
|
||||
|
||||
if description_localizations:
|
||||
base['description_localizations'] = description_localizations
|
||||
|
||||
return base
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
base = {
|
||||
'type': self.type.value,
|
||||
@ -158,7 +201,7 @@ class CommandParameter:
|
||||
@property
|
||||
def display_name(self) -> str:
|
||||
""":class:`str`: The name of the parameter as it should be displayed to the user."""
|
||||
return self.name if self._rename is MISSING else self._rename
|
||||
return self.name if self._rename is MISSING else str(self._rename)
|
||||
|
||||
|
||||
class Transformer:
|
||||
|
195
discord/app_commands/translator.py
Normal file
195
discord/app_commands/translator.py
Normal file
@ -0,0 +1,195 @@
|
||||
"""
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-present Rapptz
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import Any, Optional
|
||||
from .errors import TranslationError
|
||||
from ..enums import Enum, Locale
|
||||
|
||||
|
||||
__all__ = (
|
||||
'TranslationContext',
|
||||
'Translator',
|
||||
'locale_str',
|
||||
)
|
||||
|
||||
|
||||
class TranslationContext(Enum):
|
||||
command_name = 0
|
||||
command_description = 1
|
||||
parameter_name = 2
|
||||
parameter_description = 3
|
||||
choice_name = 4
|
||||
|
||||
|
||||
class Translator:
|
||||
"""A class that handles translations for commands, parameters, and choices.
|
||||
|
||||
Translations are done lazily in order to allow for async enabled translations as well
|
||||
as supporting a wide array of translation systems such as :mod:`gettext` and
|
||||
`Project Fluent <https://projectfluent.org>`_.
|
||||
|
||||
In order for a translator to be used, it must be set using the :meth:`CommandTree.set_translator`
|
||||
method. The translation flow for a string is as follows:
|
||||
|
||||
1. Use :class:`locale_str` instead of :class:`str` in areas of a command you want to be translated.
|
||||
- Currently, these are command names, command descriptions, parameter names, parameter descriptions, and choice names.
|
||||
- This can also be used inside the :func:`~discord.app_commands.describe` decorator.
|
||||
2. Call :meth:`CommandTree.set_translator` to the translator instance that will handle the translations.
|
||||
3. Call :meth:`CommandTree.sync`
|
||||
4. The library will call :meth:`Translator.translate` on all the relevant strings being translated.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
|
||||
async def load(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
An asynchronous setup function for loading the translation system.
|
||||
|
||||
The default implementation does nothing.
|
||||
|
||||
This is invoked when :meth:`CommandTree.set_translator` is called.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def unload(self) -> None:
|
||||
"""|coro|
|
||||
|
||||
An asynchronous teardown function for unloading the translation system.
|
||||
|
||||
The default implementation does nothing.
|
||||
|
||||
This is invoked when :meth:`CommandTree.set_translator` is called
|
||||
if a tree already has a translator or when :meth:`discord.Client.close` is called.
|
||||
"""
|
||||
pass
|
||||
|
||||
async def _checked_translate(self, string: locale_str, locale: Locale, context: TranslationContext) -> Optional[str]:
|
||||
try:
|
||||
return await self.translate(string, locale, context)
|
||||
except TranslationError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise TranslationError(string=string, locale=locale, context=context) from e
|
||||
|
||||
async def translate(self, string: locale_str, locale: Locale, context: TranslationContext) -> Optional[str]:
|
||||
"""|coro|
|
||||
|
||||
Translates the given string to the specified locale.
|
||||
|
||||
If the string cannot be translated, ``None`` should be returned.
|
||||
|
||||
The default implementation returns ``None``.
|
||||
|
||||
If an exception is raised in this method, it should inherit from :exc:`TranslationError`.
|
||||
If it doesn't, then when this is called the exception will be chained with it instead.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
string: :class:`locale_str`
|
||||
The string being translated.
|
||||
locale: :class:`~discord.Locale`
|
||||
The locale being requested for translation.
|
||||
context: :class:`TranslationContext`
|
||||
The translation context where the string originated from.
|
||||
"""
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class locale_str:
|
||||
"""Marks a string as ready for translation.
|
||||
|
||||
This is done lazily and is not actually translated until :meth:`CommandTree.sync` is called.
|
||||
|
||||
The sync method then ultimately defers the responsibility of translating to the :class:`Translator`
|
||||
instance used by the :class:`CommandTree`. For more information on the translation flow, see the
|
||||
:class:`Translator` documentation.
|
||||
|
||||
.. container:: operations
|
||||
|
||||
.. describe:: str(x)
|
||||
|
||||
Returns the message passed to the string.
|
||||
|
||||
.. describe:: x == y
|
||||
|
||||
Checks if the string is equal to another string.
|
||||
|
||||
.. describe:: x != y
|
||||
|
||||
Checks if the string is not equal to another string.
|
||||
|
||||
.. describe:: hash(x)
|
||||
|
||||
Returns the hash of the string.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Attributes
|
||||
------------
|
||||
message: :class:`str`
|
||||
The message being translated. Once set, this cannot be changed.
|
||||
|
||||
.. warning::
|
||||
|
||||
This must be the default "message" that you send to Discord.
|
||||
Discord sends this message back to the library and the library
|
||||
uses it to access the data in order to dispatch commands.
|
||||
|
||||
For example, in a command name context, if the command
|
||||
name is ``foo`` then the message *must* also be ``foo``.
|
||||
For other translation systems that require a message ID such
|
||||
as Fluent, consider using a keyword argument to pass it in.
|
||||
extras: :class:`dict`
|
||||
A dict of user provided extras to attach to the translated string.
|
||||
This can be used to add more context, information, or any metadata necessary
|
||||
to aid in actually translating the string.
|
||||
|
||||
Since these are passed via keyword arguments, the keys are strings.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, /, **kwargs: Any) -> None:
|
||||
self.__message: str = message
|
||||
self.extras: dict[str, Any] = kwargs
|
||||
|
||||
@property
|
||||
def message(self) -> str:
|
||||
return self.__message
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__message
|
||||
|
||||
def __repr__(self) -> str:
|
||||
kwargs = ', '.join(f'{k}={v!r}' for k, v in self.extras.items())
|
||||
if kwargs:
|
||||
return f'{self.__class__.__name__}({self.__message!r}, {kwargs})'
|
||||
return f'{self.__class__.__name__}({self.__message!r})'
|
||||
|
||||
def __eq__(self, obj: object) -> bool:
|
||||
return isinstance(obj, locale_str) and self.message == obj.message
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(self.__message)
|
@ -58,6 +58,7 @@ from .errors import (
|
||||
CommandLimitReached,
|
||||
MissingApplicationID,
|
||||
)
|
||||
from .translator import Translator, locale_str
|
||||
from ..errors import ClientException
|
||||
from ..enums import AppCommandType, InteractionType
|
||||
from ..utils import MISSING, _get_as_snowflake, _is_submodule
|
||||
@ -832,8 +833,8 @@ class CommandTree(Generic[ClientT]):
|
||||
def command(
|
||||
self,
|
||||
*,
|
||||
name: str = MISSING,
|
||||
description: str = MISSING,
|
||||
name: Union[str, locale_str] = MISSING,
|
||||
description: Union[str, locale_str] = MISSING,
|
||||
nsfw: bool = False,
|
||||
guild: Optional[Snowflake] = MISSING,
|
||||
guilds: Sequence[Snowflake] = MISSING,
|
||||
@ -843,10 +844,10 @@ class CommandTree(Generic[ClientT]):
|
||||
|
||||
Parameters
|
||||
------------
|
||||
name: :class:`str`
|
||||
name: Union[:class:`str`, :class:`locale_str`]
|
||||
The name of the application command. If not given, it defaults to a lower-case
|
||||
version of the callback name.
|
||||
description: :class:`str`
|
||||
description: Union[:class:`str`, :class:`locale_str`]
|
||||
The description of the application command. This shows up in the UI to describe
|
||||
the application command. If not given, it defaults to the first line of the docstring
|
||||
of the callback shortened to 100 characters.
|
||||
@ -894,7 +895,7 @@ class CommandTree(Generic[ClientT]):
|
||||
def context_menu(
|
||||
self,
|
||||
*,
|
||||
name: str = MISSING,
|
||||
name: Union[str, locale_str] = MISSING,
|
||||
nsfw: bool = False,
|
||||
guild: Optional[Snowflake] = MISSING,
|
||||
guilds: Sequence[Snowflake] = MISSING,
|
||||
@ -921,7 +922,7 @@ class CommandTree(Generic[ClientT]):
|
||||
|
||||
Parameters
|
||||
------------
|
||||
name: :class:`str`
|
||||
name: Union[:class:`str`, :class:`locale_str`]
|
||||
The name of the context menu command. If not given, it defaults to a title-case
|
||||
version of the callback name. Note that unlike regular slash commands this can
|
||||
have spaces and upper case characters in the name.
|
||||
@ -952,11 +953,54 @@ class CommandTree(Generic[ClientT]):
|
||||
|
||||
return decorator
|
||||
|
||||
@property
|
||||
def translator(self) -> Optional[Translator]:
|
||||
"""Optional[:class:`Translator`]: The translator, if any, responsible for handling translation of commands.
|
||||
|
||||
To change the translator, use :meth:`set_translator`.
|
||||
"""
|
||||
return self._state._translator
|
||||
|
||||
async def set_translator(self, translator: Optional[Translator]) -> None:
|
||||
"""Sets the translator to use for translating commands.
|
||||
|
||||
If a translator was previously set, it will be unloaded using its
|
||||
:meth:`Translator.unload` method.
|
||||
|
||||
When a translator is set, it will be loaded using its :meth:`Translator.load` method.
|
||||
|
||||
Parameters
|
||||
------------
|
||||
translator: Optional[:class:`Translator`]
|
||||
The translator to use. If ``None`` then the translator is just removed and unloaded.
|
||||
|
||||
Raises
|
||||
-------
|
||||
TypeError
|
||||
The translator was not ``None`` or a :class:`Translator` instance.
|
||||
"""
|
||||
|
||||
if translator is not None and not isinstance(translator, Translator):
|
||||
raise TypeError(f'expected None or Translator instance, received {translator.__class__!r} instead')
|
||||
|
||||
old_translator = self._state._translator
|
||||
if old_translator is not None:
|
||||
await old_translator.unload()
|
||||
|
||||
if translator is None:
|
||||
self._state._translator = None
|
||||
else:
|
||||
await translator.load()
|
||||
self._state._translator = translator
|
||||
|
||||
async def sync(self, *, guild: Optional[Snowflake] = None) -> List[AppCommand]:
|
||||
"""|coro|
|
||||
|
||||
Syncs the application commands to Discord.
|
||||
|
||||
This also runs the translator to get the translated strings necessary for
|
||||
feeding back into Discord.
|
||||
|
||||
This must be called for the application commands to show up.
|
||||
|
||||
Parameters
|
||||
@ -973,6 +1017,8 @@ class CommandTree(Generic[ClientT]):
|
||||
The client does not have the ``applications.commands`` scope in the guild.
|
||||
MissingApplicationID
|
||||
The client does not have an application ID.
|
||||
TranslationError
|
||||
An error occurred while translating the commands.
|
||||
|
||||
Returns
|
||||
--------
|
||||
@ -984,7 +1030,13 @@ class CommandTree(Generic[ClientT]):
|
||||
raise MissingApplicationID
|
||||
|
||||
commands = self._get_all_commands(guild=guild)
|
||||
payload = [command.to_dict() for command in commands]
|
||||
|
||||
translator = self.translator
|
||||
if translator:
|
||||
payload = [await command.get_translated_payload(translator) for command in commands]
|
||||
else:
|
||||
payload = [command.to_dict() for command in commands]
|
||||
|
||||
if guild is None:
|
||||
data = await self._http.bulk_upsert_global_commands(self.client.application_id, payload=payload)
|
||||
else:
|
||||
|
@ -737,12 +737,7 @@ class Client:
|
||||
|
||||
self._closed = True
|
||||
|
||||
for voice in self.voice_clients:
|
||||
try:
|
||||
await voice.disconnect(force=True)
|
||||
except Exception:
|
||||
# if an error happens during disconnects, disregard it.
|
||||
pass
|
||||
await self._connection.close()
|
||||
|
||||
if self.ws is not None and self.ws.open:
|
||||
await self.ws.close(code=1000)
|
||||
|
@ -253,7 +253,7 @@ class BotBase(GroupMixin[None]):
|
||||
|
||||
def hybrid_command(
|
||||
self,
|
||||
name: str = MISSING,
|
||||
name: Union[str, app_commands.locale_str] = MISSING,
|
||||
with_app_command: bool = True,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
@ -277,7 +277,7 @@ class BotBase(GroupMixin[None]):
|
||||
|
||||
def hybrid_group(
|
||||
self,
|
||||
name: str = MISSING,
|
||||
name: Union[str, app_commands.locale_str] = MISSING,
|
||||
with_app_command: bool = True,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
|
@ -124,12 +124,12 @@ class CogMeta(type):
|
||||
async def bar(self, ctx):
|
||||
pass # hidden -> False
|
||||
|
||||
group_name: :class:`str`
|
||||
group_name: Union[:class:`str`, :class:`~discord.app_commands.locale_str`]
|
||||
The group name of a cog. This is only applicable for :class:`GroupCog` instances.
|
||||
By default, it's the same value as :attr:`name`.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
group_description: :class:`str`
|
||||
group_description: Union[:class:`str`, :class:`~discord.app_commands.locale_str`]
|
||||
The group description of a cog. This is only applicable for :class:`GroupCog` instances.
|
||||
By default, it's the same value as :attr:`description`.
|
||||
|
||||
@ -143,8 +143,8 @@ class CogMeta(type):
|
||||
|
||||
__cog_name__: str
|
||||
__cog_description__: str
|
||||
__cog_group_name__: str
|
||||
__cog_group_description__: str
|
||||
__cog_group_name__: Union[str, app_commands.locale_str]
|
||||
__cog_group_description__: Union[str, app_commands.locale_str]
|
||||
__cog_group_nsfw__: bool
|
||||
__cog_settings__: Dict[str, Any]
|
||||
__cog_commands__: List[Command[Any, ..., Any]]
|
||||
@ -260,8 +260,8 @@ class Cog(metaclass=CogMeta):
|
||||
|
||||
__cog_name__: str
|
||||
__cog_description__: str
|
||||
__cog_group_name__: str
|
||||
__cog_group_description__: str
|
||||
__cog_group_name__: Union[str, app_commands.locale_str]
|
||||
__cog_group_description__: Union[str, app_commands.locale_str]
|
||||
__cog_settings__: Dict[str, Any]
|
||||
__cog_commands__: List[Command[Self, ..., Any]]
|
||||
__cog_app_commands__: List[Union[app_commands.Group, app_commands.Command[Self, ..., Any]]]
|
||||
|
@ -297,22 +297,22 @@ def replace_parameters(
|
||||
|
||||
|
||||
class HybridAppCommand(discord.app_commands.Command[CogT, P, T]):
|
||||
def __init__(self, wrapped: Command[CogT, Any, T]) -> None:
|
||||
def __init__(self, wrapped: Union[HybridCommand[CogT, Any, T], HybridGroup[CogT, Any, T]]) -> None:
|
||||
signature = inspect.signature(wrapped.callback)
|
||||
params = replace_parameters(wrapped.params, wrapped.callback, signature)
|
||||
wrapped.callback.__signature__ = signature.replace(parameters=params)
|
||||
nsfw = getattr(wrapped.callback, '__discord_app_commands_is_nsfw__', False)
|
||||
try:
|
||||
super().__init__(
|
||||
name=wrapped.name,
|
||||
name=wrapped._locale_name or wrapped.name,
|
||||
callback=wrapped.callback, # type: ignore # Signature doesn't match but we're overriding the invoke
|
||||
description=wrapped.description or wrapped.short_doc or '…',
|
||||
description=wrapped._locale_description or wrapped.description or wrapped.short_doc or '…',
|
||||
nsfw=nsfw,
|
||||
)
|
||||
finally:
|
||||
del wrapped.callback.__signature__
|
||||
|
||||
self.wrapped: Command[CogT, Any, T] = wrapped
|
||||
self.wrapped: Union[HybridCommand[CogT, Any, T], HybridGroup[CogT, Any, T]] = wrapped
|
||||
self.binding: Optional[CogT] = wrapped.cog
|
||||
# This technically means only one flag converter is supported
|
||||
self.flag_converter: Optional[Tuple[str, Type[FlagConverter]]] = getattr(
|
||||
@ -484,11 +484,25 @@ class HybridCommand(Command[CogT, P, T]):
|
||||
self,
|
||||
func: CommandCallback[CogT, Context[Any], P, T],
|
||||
/,
|
||||
*,
|
||||
name: Union[str, app_commands.locale_str] = MISSING,
|
||||
description: Union[str, app_commands.locale_str] = MISSING,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
name, name_locale = (name.message, name) if isinstance(name, app_commands.locale_str) else (name, None)
|
||||
if name is not MISSING:
|
||||
kwargs['name'] = name
|
||||
description, description_locale = (
|
||||
(description.message, description) if isinstance(description, app_commands.locale_str) else (description, None)
|
||||
)
|
||||
if description is not MISSING:
|
||||
kwargs['description'] = description
|
||||
|
||||
super().__init__(func, **kwargs)
|
||||
self.with_app_command: bool = kwargs.pop('with_app_command', True)
|
||||
self.with_command: bool = kwargs.pop('with_command', True)
|
||||
self._locale_name: Optional[app_commands.locale_str] = name_locale
|
||||
self._locale_description: Optional[app_commands.locale_str] = description_locale
|
||||
|
||||
if not self.with_command and not self.with_app_command:
|
||||
raise TypeError('cannot set both with_command and with_app_command to False')
|
||||
@ -586,10 +600,27 @@ class HybridGroup(Group[CogT, P, T]):
|
||||
|
||||
__commands_is_hybrid__: ClassVar[bool] = True
|
||||
|
||||
def __init__(self, *args: Any, fallback: Optional[str] = None, **attrs: Any) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
*args: Any,
|
||||
name: Union[str, app_commands.locale_str] = MISSING,
|
||||
description: Union[str, app_commands.locale_str] = MISSING,
|
||||
fallback: Optional[str] = None,
|
||||
**attrs: Any,
|
||||
) -> None:
|
||||
name, name_locale = (name.message, name) if isinstance(name, app_commands.locale_str) else (name, None)
|
||||
if name is not MISSING:
|
||||
attrs['name'] = name
|
||||
description, description_locale = (
|
||||
(description.message, description) if isinstance(description, app_commands.locale_str) else (description, None)
|
||||
)
|
||||
if description is not MISSING:
|
||||
attrs['description'] = description
|
||||
super().__init__(*args, **attrs)
|
||||
self.invoke_without_command = True
|
||||
self.with_app_command: bool = attrs.pop('with_app_command', True)
|
||||
self._locale_name: Optional[app_commands.locale_str] = name_locale
|
||||
self._locale_description: Optional[app_commands.locale_str] = description_locale
|
||||
|
||||
parent = None
|
||||
if self.parent is not None:
|
||||
@ -612,8 +643,8 @@ class HybridGroup(Group[CogT, P, T]):
|
||||
default_permissions = getattr(self.callback, '__discord_app_commands_default_permissions__', None)
|
||||
nsfw = getattr(self.callback, '__discord_app_commands_is_nsfw__', False)
|
||||
self.app_command = app_commands.Group(
|
||||
name=self.name,
|
||||
description=self.description or self.short_doc or '…',
|
||||
name=self._locale_name or self.name,
|
||||
description=self._locale_description or self.description or self.short_doc or '…',
|
||||
guild_ids=guild_ids,
|
||||
guild_only=guild_only,
|
||||
default_permissions=default_permissions,
|
||||
@ -762,7 +793,7 @@ class HybridGroup(Group[CogT, P, T]):
|
||||
|
||||
def command(
|
||||
self,
|
||||
name: str = MISSING,
|
||||
name: Union[str, app_commands.locale_str] = MISSING,
|
||||
*args: Any,
|
||||
with_app_command: bool = True,
|
||||
**kwargs: Any,
|
||||
@ -786,7 +817,7 @@ class HybridGroup(Group[CogT, P, T]):
|
||||
|
||||
def group(
|
||||
self,
|
||||
name: str = MISSING,
|
||||
name: Union[str, app_commands.locale_str] = MISSING,
|
||||
*args: Any,
|
||||
with_app_command: bool = True,
|
||||
**kwargs: Any,
|
||||
@ -810,7 +841,7 @@ class HybridGroup(Group[CogT, P, T]):
|
||||
|
||||
|
||||
def hybrid_command(
|
||||
name: str = MISSING,
|
||||
name: Union[str, app_commands.locale_str] = MISSING,
|
||||
*,
|
||||
with_app_command: bool = True,
|
||||
**attrs: Any,
|
||||
@ -837,7 +868,7 @@ def hybrid_command(
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
name: :class:`str`
|
||||
name: Union[:class:`str`, :class:`~discord.app_commands.locale_str`]
|
||||
The name to create the command with. By default this uses the
|
||||
function name unchanged.
|
||||
with_app_command: :class:`bool`
|
||||
@ -861,7 +892,7 @@ def hybrid_command(
|
||||
|
||||
|
||||
def hybrid_group(
|
||||
name: str = MISSING,
|
||||
name: Union[str, app_commands.locale_str] = MISSING,
|
||||
*,
|
||||
with_app_command: bool = True,
|
||||
**attrs: Any,
|
||||
|
@ -878,9 +878,15 @@ class InteractionResponse:
|
||||
if self._response_type:
|
||||
raise InteractionResponded(self._parent)
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
'choices': [option.to_dict() for option in choices],
|
||||
}
|
||||
translator = self._parent._state._translator
|
||||
if translator is not None:
|
||||
payload: Dict[str, Any] = {
|
||||
'choices': [await option.get_translated_payload(translator) for option in choices],
|
||||
}
|
||||
else:
|
||||
payload: Dict[str, Any] = {
|
||||
'choices': [option.to_dict() for option in choices],
|
||||
}
|
||||
|
||||
parent = self._parent
|
||||
if parent.type is not InteractionType.autocomplete:
|
||||
|
@ -471,12 +471,7 @@ class AutoShardedClient(Client):
|
||||
return
|
||||
|
||||
self._closed = True
|
||||
|
||||
for vc in self.voice_clients:
|
||||
try:
|
||||
await vc.disconnect(force=True)
|
||||
except Exception:
|
||||
pass
|
||||
await self._connection.close()
|
||||
|
||||
to_close = [asyncio.ensure_future(shard.close(), loop=self.loop) for shard in self.__shards.values()]
|
||||
if to_close:
|
||||
|
@ -83,7 +83,7 @@ if TYPE_CHECKING:
|
||||
from .voice_client import VoiceProtocol
|
||||
from .client import Client
|
||||
from .gateway import DiscordWebSocket
|
||||
from .app_commands import CommandTree
|
||||
from .app_commands import CommandTree, Translator
|
||||
|
||||
from .types.automod import AutoModerationRule, AutoModerationActionExecution
|
||||
from .types.snowflake import Snowflake
|
||||
@ -245,6 +245,7 @@ class ConnectionState:
|
||||
self._status: Optional[str] = status
|
||||
self._intents: Intents = intents
|
||||
self._command_tree: Optional[CommandTree] = None
|
||||
self._translator: Optional[Translator] = None
|
||||
|
||||
if not intents.members or cache_flags._empty:
|
||||
self.store_user = self.store_user_no_intents
|
||||
@ -257,6 +258,19 @@ class ConnectionState:
|
||||
|
||||
self.clear()
|
||||
|
||||
async def close(self) -> None:
|
||||
for voice in self.voice_clients:
|
||||
try:
|
||||
await voice.disconnect(force=True)
|
||||
except Exception:
|
||||
# if an error happens during disconnects, disregard it.
|
||||
pass
|
||||
|
||||
if self._translator:
|
||||
await self._translator.unload()
|
||||
|
||||
# Purposefully don't call `clear` because users rely on cache being available post-close
|
||||
|
||||
def clear(self, *, views: bool = True) -> None:
|
||||
self.user: Optional[ClientUser] = None
|
||||
self._users: weakref.WeakValueDictionary[int, User] = weakref.WeakValueDictionary()
|
||||
|
@ -24,7 +24,7 @@ DEALINGS IN THE SOFTWARE.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Literal, Optional, TypedDict, Union
|
||||
from typing import Dict, List, Literal, Optional, TypedDict, Union
|
||||
from typing_extensions import NotRequired, Required
|
||||
|
||||
from .channel import ChannelType
|
||||
@ -37,6 +37,8 @@ ApplicationCommandOptionType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
|
||||
class _BaseApplicationCommandOption(TypedDict):
|
||||
name: str
|
||||
description: str
|
||||
name_localizations: NotRequired[Optional[Dict[str, str]]]
|
||||
description_localizations: NotRequired[Optional[Dict[str, str]]]
|
||||
|
||||
|
||||
class _SubCommandCommandOption(_BaseApplicationCommandOption):
|
||||
@ -55,6 +57,7 @@ class _BaseValueApplicationCommandOption(_BaseApplicationCommandOption, total=Fa
|
||||
|
||||
class _StringApplicationCommandOptionChoice(TypedDict):
|
||||
name: str
|
||||
name_localizations: NotRequired[Optional[Dict[str, str]]]
|
||||
value: str
|
||||
|
||||
|
||||
@ -68,6 +71,7 @@ class _StringApplicationCommandOption(_BaseApplicationCommandOption):
|
||||
|
||||
class _IntegerApplicationCommandOptionChoice(TypedDict):
|
||||
name: str
|
||||
name_localizations: NotRequired[Optional[Dict[str, str]]]
|
||||
value: int
|
||||
|
||||
|
||||
@ -100,6 +104,7 @@ _SnowflakeApplicationCommandOptionChoice = Union[
|
||||
|
||||
class _NumberApplicationCommandOptionChoice(TypedDict):
|
||||
name: str
|
||||
name_localizations: NotRequired[Optional[Dict[str, str]]]
|
||||
value: float
|
||||
|
||||
|
||||
@ -140,6 +145,8 @@ class _BaseApplicationCommand(TypedDict):
|
||||
default_member_permissions: NotRequired[Optional[str]]
|
||||
nsfw: NotRequired[bool]
|
||||
version: Snowflake
|
||||
name_localizations: NotRequired[Optional[Dict[str, str]]]
|
||||
description_localizations: NotRequired[Optional[Dict[str, str]]]
|
||||
|
||||
|
||||
class _ChatInputApplicationCommand(_BaseApplicationCommand, total=False):
|
||||
|
@ -377,7 +377,7 @@ Enumerations
|
||||
A message context menu command.
|
||||
|
||||
.. class:: AppCommandPermissionType
|
||||
|
||||
|
||||
The application command's permission type.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
@ -596,6 +596,52 @@ Range
|
||||
.. autoclass:: discord.app_commands.Range
|
||||
:members:
|
||||
|
||||
Translations
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Translator
|
||||
+++++++++++
|
||||
|
||||
.. attributetable:: discord.app_commands.Translator
|
||||
|
||||
.. autoclass:: discord.app_commands.Translator
|
||||
:members:
|
||||
|
||||
locale_str
|
||||
+++++++++++
|
||||
|
||||
.. attributetable:: discord.app_commands.locale_str
|
||||
|
||||
.. autoclass:: discord.app_commands.locale_str
|
||||
:members:
|
||||
|
||||
TranslationContext
|
||||
+++++++++++++++++++
|
||||
|
||||
.. class:: TranslationContext
|
||||
:module: discord.app_commands
|
||||
|
||||
An enum representing the context that the translation occurs in when requested for translation.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
.. attribute:: command_name
|
||||
|
||||
The translation involved a command name.
|
||||
.. attribute:: command_description
|
||||
|
||||
The translation involved a command description.
|
||||
.. attribute:: parameter_name
|
||||
|
||||
The translation involved a parameter name.
|
||||
.. attribute:: parameter_description
|
||||
|
||||
The translation involved a parameter description.
|
||||
.. attribute:: choice_name
|
||||
|
||||
The translation involved a choice name.
|
||||
|
||||
|
||||
Exceptions
|
||||
~~~~~~~~~~~
|
||||
|
||||
@ -608,6 +654,9 @@ Exceptions
|
||||
.. autoexception:: discord.app_commands.TransformerError
|
||||
:members:
|
||||
|
||||
.. autoexception:: discord.app_commands.TranslationError
|
||||
:members:
|
||||
|
||||
.. autoexception:: discord.app_commands.CheckFailure
|
||||
:members:
|
||||
|
||||
@ -653,6 +702,7 @@ Exception Hierarchy
|
||||
- :exc:`~discord.app_commands.AppCommandError`
|
||||
- :exc:`~discord.app_commands.CommandInvokeError`
|
||||
- :exc:`~discord.app_commands.TransformerError`
|
||||
- :exc:`~discord.app_commands.TranslationError`
|
||||
- :exc:`~discord.app_commands.CheckFailure`
|
||||
- :exc:`~discord.app_commands.NoPrivateMessage`
|
||||
- :exc:`~discord.app_commands.MissingRole`
|
||||
|
Loading…
x
Reference in New Issue
Block a user