mirror of
https://github.com/Rapptz/discord.py.git
synced 2025-10-22 16:32:59 +00:00
Add initial support for app command localisation
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user