diff --git a/discord/app_commands/commands.py b/discord/app_commands/commands.py index f027438fa..42db8e3ea 100644 --- a/discord/app_commands/commands.py +++ b/discord/app_commands/commands.py @@ -52,7 +52,7 @@ from ..enums import AppCommandOptionType, AppCommandType, ChannelType, 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 .translator import TranslationContext, TranslationContextLocation, Translator, locale_str from ..message import Message from ..user import User from ..member import Member @@ -737,22 +737,27 @@ class Command(Generic[GroupT, P, T]): base = self.to_dict() name_localizations: Dict[str, str] = {} description_localizations: Dict[str, str] = {} + + # Prevent creating these objects in a heavy loop + name_context = TranslationContext(location=TranslationContextLocation.command_name, data=self) + description_context = TranslationContext(location=TranslationContextLocation.command_description, data=self) + for locale in Locale: if self._locale_name: - translation = await translator._checked_translate(self._locale_name, locale, TranslationContext.command_name) + translation = await translator._checked_translate(self._locale_name, locale, name_context) 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 - ) + translation = await translator._checked_translate(self._locale_description, locale, description_context) 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()] + base['options'] = [ + await param.get_translated_payload(translator, Parameter(param)) for param in self._params.values() + ] return base def to_dict(self) -> Dict[str, Any]: @@ -1225,10 +1230,11 @@ class ContextMenu: async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]: base = self.to_dict() + context = TranslationContext(location=TranslationContextLocation.command_name, data=self) 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) + translation = await translator._checked_translate(self._locale_name, locale, context) if translation is not None: name_localizations[locale.value] = translation @@ -1638,16 +1644,18 @@ class Group: base = self.to_dict() name_localizations: Dict[str, str] = {} description_localizations: Dict[str, str] = {} + + # Prevent creating these objects in a heavy loop + name_context = TranslationContext(location=TranslationContextLocation.group_name, data=self) + description_context = TranslationContext(location=TranslationContextLocation.group_description, data=self) for locale in Locale: if self._locale_name: - translation = await translator._checked_translate(self._locale_name, locale, TranslationContext.command_name) + translation = await translator._checked_translate(self._locale_name, locale, name_context) 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 - ) + translation = await translator._checked_translate(self._locale_description, locale, description_context) if translation is not None: description_localizations[locale.value] = translation diff --git a/discord/app_commands/models.py b/discord/app_commands/models.py index 4f4f42812..c28c1c4d0 100644 --- a/discord/app_commands/models.py +++ b/discord/app_commands/models.py @@ -26,7 +26,7 @@ from __future__ import annotations from datetime import datetime from .errors import MissingApplicationID -from .translator import Translator, TranslationContext, locale_str +from .translator import TranslationContextLocation, Translator, TranslationContext, locale_str from ..permissions import Permissions from ..enums import AppCommandOptionType, AppCommandType, AppCommandPermissionType, ChannelType, Locale, try_enum from ..mixins import Hashable @@ -463,9 +463,10 @@ class Choice(Generic[ChoiceT]): async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]: base = self.to_dict() name_localizations: Dict[str, str] = {} + context = TranslationContext(location=TranslationContextLocation.choice_name, data=self) if self._locale_name: for locale in Locale: - translation = await translator._checked_translate(self._locale_name, locale, TranslationContext.choice_name) + translation = await translator._checked_translate(self._locale_name, locale, context) if translation is not None: name_localizations[locale.value] = translation diff --git a/discord/app_commands/transformers.py b/discord/app_commands/transformers.py index fca0c6ec6..21291b55c 100644 --- a/discord/app_commands/transformers.py +++ b/discord/app_commands/transformers.py @@ -46,7 +46,7 @@ from typing import ( from .errors import AppCommandError, TransformerError from .models import AppCommandChannel, AppCommandThread, Choice -from .translator import locale_str, Translator, TranslationContext +from .translator import TranslationContextLocation, locale_str, Translator, TranslationContext from ..channel import StageChannel, VoiceChannel, TextChannel, CategoryChannel from ..abc import GuildChannel from ..threads import Thread @@ -70,6 +70,7 @@ NoneType = type(None) if TYPE_CHECKING: from ..interactions import Interaction + from .commands import Parameter @dataclass @@ -89,29 +90,27 @@ class CommandParameter: _rename: Union[str, locale_str] = MISSING _annotation: Any = MISSING - async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]: + async def get_translated_payload(self, translator: Translator, data: Parameter) -> Dict[str, Any]: base = self.to_dict() - needs_name_translations = isinstance(self._rename, locale_str) - needs_description_translations = isinstance(self.description, locale_str) + rename = self._rename + description = self.description + needs_name_translations = isinstance(rename, locale_str) + needs_description_translations = isinstance(description, locale_str) name_localizations: Dict[str, str] = {} description_localizations: Dict[str, str] = {} + + # Prevent creating these objects in a heavy loop + name_context = TranslationContext(location=TranslationContextLocation.parameter_name, data=data) + description_context = TranslationContext(location=TranslationContextLocation.parameter_description, data=data) 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, - ) + translation = await translator._checked_translate(rename, locale, name_context) 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, - ) + translation = await translator._checked_translate(description, locale, description_context) if translation is not None: description_localizations[locale.value] = translation diff --git a/discord/app_commands/translator.py b/discord/app_commands/translator.py index a4ecefed6..a0d366735 100644 --- a/discord/app_commands/translator.py +++ b/discord/app_commands/translator.py @@ -23,24 +23,100 @@ DEALINGS IN THE SOFTWARE. """ from __future__ import annotations -from typing import Any, Optional +from typing import TYPE_CHECKING, Any, Literal, Optional, Union from .errors import TranslationError from ..enums import Enum, Locale __all__ = ( + 'TranslationContextLocation', 'TranslationContext', 'Translator', 'locale_str', ) -class TranslationContext(Enum): +class TranslationContextLocation(Enum): command_name = 0 command_description = 1 - parameter_name = 2 - parameter_description = 3 - choice_name = 4 + group_name = 2 + group_description = 3 + parameter_name = 4 + parameter_description = 5 + choice_name = 6 + other = 7 + + +class TranslationContext: # type: ignore # See below + """A class that provides context for the :class:`locale_str` being translated. + + This is useful to determine where exactly the string is located and aid in looking + up the actual translation. + + Attributes + ----------- + location: :class:`TranslationContextLocation` + The location where this string is located. + data: Any + The extraneous data that is being translated. + """ + + __slots__ = ('location', 'data') + + def __init__(self, location: TranslationContextLocation, data: Any) -> None: + self.location: TranslationContextLocation = location + self.data: Any = data + + +if TYPE_CHECKING: + # For type checking purposes, it makes sense to allow the user to leverage type narrowing + # So code like this works as expected: + # if context.type is TranslationContextLocation.command_name: + # reveal_type(context.data) # Revealed type is Command | ContextMenu + # + # Unfortunately doing a trick like this requires lying to the type checker so + # this is what the code below enables. + # + # Should this trick stop working then it might be fair to remove this code. + # It's purely here for convenience. + + from .commands import Command, ContextMenu, Group, Parameter + from .models import Choice + + class _CommandNameTranslationContext: + location: Literal[TranslationContextLocation.command_name] + data: Union[Command[Any, ..., Any], ContextMenu] + + class _CommandDescriptionTranslationContext: + location: Literal[TranslationContextLocation.command_description] + data: Command[Any, ..., Any] + + class _GroupTranslationContext: + location: Literal[TranslationContextLocation.group_name, TranslationContextLocation.group_description] + data: Group + + class _ParameterTranslationContext: + location: Literal[TranslationContextLocation.parameter_description, TranslationContextLocation.parameter_name] + data: Parameter + + class _ChoiceTranslationContext: + location: Literal[TranslationContextLocation.choice_name] + data: Choice[Union[int, str, float]] + + class _OtherTranslationContext: + location: Literal[TranslationContextLocation.other] + data: Any + + class TranslationContext( + _CommandNameTranslationContext, + _CommandDescriptionTranslationContext, + _GroupTranslationContext, + _ParameterTranslationContext, + _ChoiceTranslationContext, + _OtherTranslationContext, + ): + def __init__(self, location: TranslationContextLocation, data: Any) -> None: + ... class Translator: diff --git a/docs/interactions/api.rst b/docs/interactions/api.rst index 621f7bb40..532bfc4e9 100644 --- a/docs/interactions/api.rst +++ b/docs/interactions/api.rst @@ -626,10 +626,18 @@ locale_str TranslationContext +++++++++++++++++++ -.. class:: TranslationContext +.. attributetable:: discord.app_commands.TranslationContext + +.. autoclass:: discord.app_commands.TranslationContext + :members: + +TranslationContextLocation ++++++++++++++++++++++++++++ + +.. class:: TranslationContextLocation :module: discord.app_commands - An enum representing the context that the translation occurs in when requested for translation. + An enum representing the location context that the translation occurs in when requested for translation. .. versionadded:: 2.0 @@ -639,6 +647,13 @@ TranslationContext .. attribute:: command_description The translation involved a command description. + + .. attribute:: group_name + + The translation involved a group name. + .. attribute:: group_description + + The translation involved a group description. .. attribute:: parameter_name The translation involved a parameter name. @@ -648,7 +663,10 @@ TranslationContext .. attribute:: choice_name The translation involved a choice name. + .. attribute:: other + The translation involved something else entirely. This is useful for running + :meth:`Translator.translate` for custom usage. Exceptions ~~~~~~~~~~~