Browse Source

Refactor TranslationContext to be more useful

The previous enum was good at accomplishing dynamic key generation for
a few cases, but it fell short in others:

1. It could not discern group names and command names
2. It could not give you more contextual data such as the full object
   currently being translated.

On top of that, the context being a required parameter for
Translator.translate meant that it wouldn't be possible to re-use the
translator for other use cases outside of the rigid ones defined in the
library.

To alleviate these concerns, new enum attributes were added along with
a richer type for obtaining even more context.
pull/8318/head
Rapptz 3 years ago
parent
commit
c32567ea81
  1. 30
      discord/app_commands/commands.py
  2. 5
      discord/app_commands/models.py
  3. 27
      discord/app_commands/transformers.py
  4. 86
      discord/app_commands/translator.py
  5. 22
      docs/interactions/api.rst

30
discord/app_commands/commands.py

@ -52,7 +52,7 @@ from ..enums import AppCommandOptionType, AppCommandType, ChannelType, Locale
from .models import Choice from .models import Choice
from .transformers import annotation_to_parameter, CommandParameter, NoneType from .transformers import annotation_to_parameter, CommandParameter, NoneType
from .errors import AppCommandError, CheckFailure, CommandInvokeError, CommandSignatureMismatch, CommandAlreadyRegistered 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 ..message import Message
from ..user import User from ..user import User
from ..member import Member from ..member import Member
@ -737,22 +737,27 @@ class Command(Generic[GroupT, P, T]):
base = self.to_dict() base = self.to_dict()
name_localizations: Dict[str, str] = {} name_localizations: Dict[str, str] = {}
description_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: for locale in Locale:
if self._locale_name: 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: if translation is not None:
name_localizations[locale.value] = translation name_localizations[locale.value] = translation
if self._locale_description: if self._locale_description:
translation = await translator._checked_translate( translation = await translator._checked_translate(self._locale_description, locale, description_context)
self._locale_description, locale, TranslationContext.command_description
)
if translation is not None: if translation is not None:
description_localizations[locale.value] = translation description_localizations[locale.value] = translation
base['name_localizations'] = name_localizations base['name_localizations'] = name_localizations
base['description_localizations'] = description_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 return base
def to_dict(self) -> Dict[str, Any]: def to_dict(self) -> Dict[str, Any]:
@ -1225,10 +1230,11 @@ class ContextMenu:
async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]: async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]:
base = self.to_dict() base = self.to_dict()
context = TranslationContext(location=TranslationContextLocation.command_name, data=self)
if self._locale_name: if self._locale_name:
name_localizations: Dict[str, str] = {} name_localizations: Dict[str, str] = {}
for locale in Locale: 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: if translation is not None:
name_localizations[locale.value] = translation name_localizations[locale.value] = translation
@ -1638,16 +1644,18 @@ class Group:
base = self.to_dict() base = self.to_dict()
name_localizations: Dict[str, str] = {} name_localizations: Dict[str, str] = {}
description_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: for locale in Locale:
if self._locale_name: 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: if translation is not None:
name_localizations[locale.value] = translation name_localizations[locale.value] = translation
if self._locale_description: if self._locale_description:
translation = await translator._checked_translate( translation = await translator._checked_translate(self._locale_description, locale, description_context)
self._locale_description, locale, TranslationContext.command_description
)
if translation is not None: if translation is not None:
description_localizations[locale.value] = translation description_localizations[locale.value] = translation

5
discord/app_commands/models.py

@ -26,7 +26,7 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
from .errors import MissingApplicationID from .errors import MissingApplicationID
from .translator import Translator, TranslationContext, locale_str from .translator import TranslationContextLocation, Translator, TranslationContext, locale_str
from ..permissions import Permissions from ..permissions import Permissions
from ..enums import AppCommandOptionType, AppCommandType, AppCommandPermissionType, ChannelType, Locale, try_enum from ..enums import AppCommandOptionType, AppCommandType, AppCommandPermissionType, ChannelType, Locale, try_enum
from ..mixins import Hashable from ..mixins import Hashable
@ -463,9 +463,10 @@ class Choice(Generic[ChoiceT]):
async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]: async def get_translated_payload(self, translator: Translator) -> Dict[str, Any]:
base = self.to_dict() base = self.to_dict()
name_localizations: Dict[str, str] = {} name_localizations: Dict[str, str] = {}
context = TranslationContext(location=TranslationContextLocation.choice_name, data=self)
if self._locale_name: if self._locale_name:
for locale in Locale: 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: if translation is not None:
name_localizations[locale.value] = translation name_localizations[locale.value] = translation

27
discord/app_commands/transformers.py

@ -46,7 +46,7 @@ from typing import (
from .errors import AppCommandError, TransformerError from .errors import AppCommandError, TransformerError
from .models import AppCommandChannel, AppCommandThread, Choice 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 ..channel import StageChannel, VoiceChannel, TextChannel, CategoryChannel
from ..abc import GuildChannel from ..abc import GuildChannel
from ..threads import Thread from ..threads import Thread
@ -70,6 +70,7 @@ NoneType = type(None)
if TYPE_CHECKING: if TYPE_CHECKING:
from ..interactions import Interaction from ..interactions import Interaction
from .commands import Parameter
@dataclass @dataclass
@ -89,29 +90,27 @@ class CommandParameter:
_rename: Union[str, locale_str] = MISSING _rename: Union[str, locale_str] = MISSING
_annotation: Any = 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() base = self.to_dict()
needs_name_translations = isinstance(self._rename, locale_str) rename = self._rename
needs_description_translations = isinstance(self.description, locale_str) description = self.description
needs_name_translations = isinstance(rename, locale_str)
needs_description_translations = isinstance(description, locale_str)
name_localizations: Dict[str, str] = {} name_localizations: Dict[str, str] = {}
description_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: for locale in Locale:
if needs_name_translations: if needs_name_translations:
translation = await translator._checked_translate( translation = await translator._checked_translate(rename, locale, name_context)
self._rename, # type: ignore # This will always be locale_str
locale,
TranslationContext.parameter_name,
)
if translation is not None: if translation is not None:
name_localizations[locale.value] = translation name_localizations[locale.value] = translation
if needs_description_translations: if needs_description_translations:
translation = await translator._checked_translate( translation = await translator._checked_translate(description, locale, description_context)
self.description, # type: ignore # This will always be locale_str
locale,
TranslationContext.parameter_description,
)
if translation is not None: if translation is not None:
description_localizations[locale.value] = translation description_localizations[locale.value] = translation

86
discord/app_commands/translator.py

@ -23,24 +23,100 @@ DEALINGS IN THE SOFTWARE.
""" """
from __future__ import annotations from __future__ import annotations
from typing import Any, Optional from typing import TYPE_CHECKING, Any, Literal, Optional, Union
from .errors import TranslationError from .errors import TranslationError
from ..enums import Enum, Locale from ..enums import Enum, Locale
__all__ = ( __all__ = (
'TranslationContextLocation',
'TranslationContext', 'TranslationContext',
'Translator', 'Translator',
'locale_str', 'locale_str',
) )
class TranslationContext(Enum): class TranslationContextLocation(Enum):
command_name = 0 command_name = 0
command_description = 1 command_description = 1
parameter_name = 2 group_name = 2
parameter_description = 3 group_description = 3
choice_name = 4 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: class Translator:

22
docs/interactions/api.rst

@ -626,10 +626,18 @@ locale_str
TranslationContext TranslationContext
+++++++++++++++++++ +++++++++++++++++++
.. class:: TranslationContext .. attributetable:: discord.app_commands.TranslationContext
.. autoclass:: discord.app_commands.TranslationContext
:members:
TranslationContextLocation
+++++++++++++++++++++++++++
.. class:: TranslationContextLocation
:module: discord.app_commands :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 .. versionadded:: 2.0
@ -639,6 +647,13 @@ TranslationContext
.. attribute:: command_description .. attribute:: command_description
The translation involved a 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 .. attribute:: parameter_name
The translation involved a parameter name. The translation involved a parameter name.
@ -648,7 +663,10 @@ TranslationContext
.. attribute:: choice_name .. attribute:: choice_name
The translation involved a 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 Exceptions
~~~~~~~~~~~ ~~~~~~~~~~~

Loading…
Cancel
Save