Skip to content

Commit

Permalink
Localization Fixes (Pycord-Development#1232)
Browse files Browse the repository at this point in the history
* Add missing l10n features

* Context menus support localizations

* Validate names and descriptions inside localization dictionaries

* Add support to fetch localization(s)

* Payload typhints to match data

* fix typo

* Remove print function
  • Loading branch information
Middledot authored Apr 7, 2022
1 parent e06af8c commit 9d44673
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 18 deletions.
100 changes: 87 additions & 13 deletions discord/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,13 +602,20 @@ def __init__(self, func: Callable, *args, **kwargs) -> None:

validate_chat_input_name(self.name)
self.name_localizations: Optional[Dict[str, str]] = kwargs.get("name_localizations", None)
if self.name_localizations:
for locale, string in self.name_localizations.items():
validate_chat_input_name(string, locale=locale)

description = kwargs.get("description") or (
inspect.cleandoc(func.__doc__).splitlines()[0] if func.__doc__ is not None else "No description provided"
)
validate_chat_input_description(description)
self.description: str = description
self.description_localizations: Optional[Dict[str, str]] = kwargs.get("description_localizations", None)
if self.description_localizations:
for locale, string in self.description_localizations.items():
validate_chat_input_description(string, locale=locale)

self.attached_to_group: bool = False

self.cog = None
Expand Down Expand Up @@ -1152,6 +1159,8 @@ class ContextMenuCommand(ApplicationCommand):
These are not created manually, instead they are created via the
decorator or functional interface.
.. versionadded:: 2.0
Attributes
-----------
name: :class:`str`
Expand Down Expand Up @@ -1180,8 +1189,9 @@ class ContextMenuCommand(ApplicationCommand):
cooldown: Optional[:class:`~discord.ext.commands.Cooldown`]
The cooldown applied when the command is invoked. ``None`` if the command
doesn't have a cooldown.
.. versionadded:: 2.0
name_localizations: Optional[Dict[:class:`str`, :class:`str`]]
The name localizations for this command. The values of this should be ``"locale": "name"``. See
`here <https://discord.com/developers/docs/reference#locales>`_ for a list of valid locales.
"""

def __new__(cls, *args, **kwargs) -> ContextMenuCommand:
Expand All @@ -1196,6 +1206,8 @@ def __init__(self, func: Callable, *args, **kwargs) -> None:
raise TypeError("Callback must be a coroutine.")
self.callback = func

self.name_localizations: Optional[Dict[str, str]] = kwargs.get("name_localizations", None)

# Discord API doesn't support setting descriptions for context menu commands
# so it must be empty
self.description = ""
Expand Down Expand Up @@ -1253,13 +1265,18 @@ def qualified_name(self):
return self.name

def to_dict(self) -> Dict[str, Union[str, int]]:
return {
as_dict = {
"name": self.name,
"description": self.description,
"type": self.type,
"default_permission": self.default_permission,
}

if self.name_localizations is not None:
as_dict["name_localizations"] = self.name_localizations

return as_dict


class UserCommand(ContextMenuCommand):
r"""A class that implements the protocol for user context menu commands.
Expand Down Expand Up @@ -1539,27 +1556,84 @@ def command(**kwargs):


docs = "https://discord.com/developers/docs"
valid_locales = [
"da",
"de",
"en-GB",
"en-US",
"es-ES",
"fr",
"hr",
"it",
"lt",
"hu",
"nl",
"no",
"pl",
"pt-BR",
"ro",
"fi",
"sv-SE",
"vi",
"tr",
"cs",
"el",
"bg",
"ru",
"uk",
"hi",
"th",
"zh-CN",
"ja",
"zh-TW",
"ko",
]


# Validation
def validate_chat_input_name(name: Any):
def validate_chat_input_name(name: Any, locale: str = None):
# Must meet the regex ^[\w-]{1,32}$
if locale not in valid_locales and locale is not None:
raise ValidationError(
f"Locale {locale} is not a valid locale, in command names, "
f"see {docs}/reference#locales for list of supported locales."
)
if not isinstance(name, str):
raise TypeError(f"Chat input command names and options must be of type str. Received {name}")
raise TypeError(
f"Chat input command names and options must be of type str."
f"Received {name}" + f" in locale {locale}" if locale else ""
)
if not re.match(r"^[\w-]{1,32}$", name):
raise ValidationError(
r'Chat input command names and options must follow the regex "^[\w-]{1,32}$". For more information, see '
f"{docs}/interactions/application-commands#application-command-object-application-command-naming. Received "
f"{name}"
'Chat input command names and options must follow the regex '
r'"^[\w-]{1,32}$". For more information, see '
f"{docs}/interactions/application-commands#application-command-object-application-command-naming. "
f"Received {name}" + f" in locale {locale}" if locale else ""
)
if not 1 <= len(name) <= 32:
raise ValidationError(f"Chat input command names and options must be 1-32 characters long. Received {name}")
raise ValidationError(
"Chat input command names and options must be 1-32 characters long. "
f"Received {name}" + f" in locale {locale}" if locale else ""
)
if not name.lower() == name: # Can't use islower() as it fails if none of the chars can be lower. See #512.
raise ValidationError(f"Chat input command names and options must be lowercase. Received {name}")
raise ValidationError(
"Chat input command names and options must be lowercase. "
f"Received {name}" + f" in locale {locale}" if locale else ""
)


def validate_chat_input_description(description: Any):
def validate_chat_input_description(description: Any, locale: str = None):
if locale not in valid_locales and locale is not None:
raise ValidationError(
f"Locale {locale} is not a valid locale, in command descriptions, "
f"see {docs}/reference#locales for list of supported locales."
)
if not isinstance(description, str):
raise TypeError(f"Command description must be of type str. Received {description}")
raise TypeError(
f"Command description must be of type str. Received {description} " + f" in locale {locale}" if locale else ""
)
if not 1 <= len(description) <= 100:
raise ValidationError(f"Command description must be 1-100 characters long. Received {description}")
raise ValidationError(
"Command description must be 1-100 characters long. "
f"Received {description}" + f" in locale {locale}" if locale else ""
)
19 changes: 15 additions & 4 deletions discord/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ async def request(
if reason:
headers["X-Audit-Log-Reason"] = _uriquote(reason, safe="/ ")

if locale := kwargs.pop('locale', None):
headers["X-Discord-Locale"] = locale

kwargs["headers"] = headers
# Proxy support
if self.proxy is not None:
Expand Down Expand Up @@ -2063,25 +2066,33 @@ def get_scheduled_event_users(

# Application commands (global)

def get_global_commands(self, application_id: Snowflake) -> Response[List[interactions.ApplicationCommand]]:
def get_global_commands(
self, application_id: Snowflake, *, with_localizations: bool = True, locale: str = None,
) -> Response[List[interactions.ApplicationCommand]]:
params = {
"with_localizations": int(with_localizations)
}

return self.request(
Route(
"GET",
"/applications/{application_id}/commands",
application_id=application_id,
)
),
params=params,
locale=locale,
)

def get_global_command(
self, application_id: Snowflake, command_id: Snowflake
self, application_id: Snowflake, command_id: Snowflake, locale: str = None,
) -> Response[interactions.ApplicationCommand]:
r = Route(
"GET",
"/applications/{application_id}/commands/{command_id}",
application_id=application_id,
command_id=command_id,
)
return self.request(r)
return self.request(r, locale=locale)

def upsert_global_command(self, application_id: Snowflake, payload) -> Response[interactions.ApplicationCommand]:
r = Route(
Expand Down
12 changes: 11 additions & 1 deletion discord/types/interactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@
class _ApplicationCommandOptional(TypedDict, total=False):
options: List[ApplicationCommandOption]
type: ApplicationCommandType
name_localized: str
name_localizations: Dict[str, str]
description_localized: str
description_localizations: Dict[str, str]


class ApplicationCommand(_ApplicationCommandOptional):
Expand All @@ -58,6 +62,8 @@ class ApplicationCommand(_ApplicationCommandOptional):
class _ApplicationCommandOptionOptional(TypedDict, total=False):
choices: List[ApplicationCommandOptionChoice]
options: List[ApplicationCommandOption]
name_localizations: Dict[str, str]
description_localizations: Dict[str, str]


ApplicationCommandOptionType = Literal[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
Expand All @@ -70,7 +76,11 @@ class ApplicationCommandOption(_ApplicationCommandOptionOptional):
required: bool


class ApplicationCommandOptionChoice(TypedDict):
class _ApplicationCommandOptionChoiceOptional(TypedDict, total=False):
name_localizations: Dict[str, str]


class ApplicationCommandOptionChoice(_ApplicationCommandOptionChoiceOptional):
name: str
value: Union[str, int]

Expand Down

0 comments on commit 9d44673

Please sign in to comment.