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 .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

5
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

27
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

86
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:

22
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
~~~~~~~~~~~

Loading…
Cancel
Save