Skip to content

Commit

Permalink
Add translator
Browse files Browse the repository at this point in the history
  • Loading branch information
4Kaylum committed Jul 16, 2023
1 parent cc3e43a commit 275857f
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 32 deletions.
14 changes: 11 additions & 3 deletions novus/api/_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import aiohttp

from ..models import File
from ..utils import bytes_to_base64_data
from ..utils import TranslatedString, bytes_to_base64_data
from ._cache import APICache
from ._errors import Forbidden, HTTPException, NotFound, Unauthorized
from .application_role_connection_metadata import ApplicationRoleHTTPConnection
Expand Down Expand Up @@ -68,6 +68,14 @@ class FixableKwargs(TypedDict, total=False):
flags: Iterable[tuple[str, str] | str]


class NovusJSONEncoder(json.JSONEncoder):

def default(self, o: Any) -> Any:
if isinstance(o, TranslatedString):
return str(o)
return super().default(o)


class HTTPConnection:
"""
A wrapper around the API for HTTP handling.
Expand Down Expand Up @@ -214,7 +222,7 @@ def request_params(
data["attachments"] = attachments
form.append({
"name": "payload_json",
"value": json.dumps(data),
"value": json.dumps(data, cls=NovusJSONEncoder),
})
data = None

Expand All @@ -232,7 +240,7 @@ def request_params(
data_str: bytes | None = None
if data:
headers["Content-Type"] = "application/json"
data_str = json.dumps(data).encode()
data_str = json.dumps(data, cls=NovusJSONEncoder).encode()
return {
"data": data_str or writer,
"headers": headers,
Expand Down
5 changes: 3 additions & 2 deletions novus/models/api_mixins/channel.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
if TYPE_CHECKING:
from ... import enums, flags
from ...api import HTTPConnection
from ...utils import TranslatedString
from .. import (
ActionRow,
AllowedMentions,
Expand Down Expand Up @@ -349,7 +350,7 @@ class TextChannelAPIMixin(ChannelAPIMixin):

async def send(
self: abc.StateSnowflake,
content: str = MISSING,
content: str | TranslatedString = MISSING,
*,
tts: bool = MISSING,
embeds: list[Embed] = MISSING,
Expand Down Expand Up @@ -575,7 +576,7 @@ class ForumChannelAPIMixin(ChannelAPIMixin):

async def create_thread(
self: abc.StateSnowflake,
name: str,
name: str | TranslatedString,
*,
reason: str | None = None,
auto_archive_duration: Literal[60, 1_440, 4_320, 10_080] = MISSING,
Expand Down
3 changes: 2 additions & 1 deletion novus/models/api_mixins/interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
WebhookMessage,
flags,
)
from ...utils import TranslatedString

__all__ = (
'InteractionAPIMixin',
Expand Down Expand Up @@ -69,7 +70,7 @@ async def pong(self: Interaction) -> None:

async def send(
self: Interaction,
content: str = MISSING,
content: str | TranslatedString = MISSING,
*,
tts: bool = MISSING,
embeds: list[Embed] = MISSING,
Expand Down
3 changes: 2 additions & 1 deletion novus/models/api_mixins/webhook.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

if TYPE_CHECKING:
from ...api import HTTPConnection
from ...utils import TranslatedString
from .. import ActionRow, AllowedMentions, Embed, File, Message, Sticker, Webhook
from ..abc import Snowflake, StateSnowflake

Expand Down Expand Up @@ -151,7 +152,7 @@ async def edit_with_token(
@overload
async def send(
self: StateSnowflake,
content: str,
content: str | TranslatedString,
*,
wait=False,
thread: int | Snowflake | None,
Expand Down
42 changes: 21 additions & 21 deletions novus/models/embed.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@
from ..utils import parse_timestamp

if TYPE_CHECKING:

from ..payloads.embed import Embed as EmbedPayload
from ..payloads.embed import _EmbedAuthor as AuthorPayload
from ..payloads.embed import _EmbedField as FieldPayload
from ..payloads.embed import _EmbedFooter as FooterPayload
from ..payloads.embed import _EmbedMedia as MediaPayload
from ..utils import TranslatedString

__all__ = (
'Embed',
Expand All @@ -40,13 +40,13 @@

@dataclass
class EmbedFooter:
text: str
text: str | TranslatedString
icon_url: str | None = None
proxy_icon_url: str | None = None

def _to_data(self) -> FooterPayload:
v: FooterPayload = {
"text": self.text
"text": str(self.text)
}
if self.icon_url:
v["icon_url"] = self.icon_url
Expand Down Expand Up @@ -82,14 +82,14 @@ class EmbedProvider:

@dataclass
class EmbedAuthor:
name: str
name: str | TranslatedString
url: str | None = None
icon_url: str | None = None
proxy_icon_url: str | None = None

def _to_data(self) -> AuthorPayload:
v: AuthorPayload = {
"name": self.name
"name": str(self.name)
}
if self.url:
v["url"] = self.url
Expand All @@ -100,14 +100,14 @@ def _to_data(self) -> AuthorPayload:

@dataclass
class EmbedField:
name: str
value: str
name: str | TranslatedString
value: str | TranslatedString
inline: bool = True

def _to_data(self) -> FieldPayload:
return {
"name": self.name,
"value": self.value,
"name": str(self.name),
"value": str(self.value),
"inline": self.inline,
}

Expand Down Expand Up @@ -200,15 +200,15 @@ class Embed:
def __init__(
self,
*,
title: str | None = None,
title: str | TranslatedString | None = None,
type: str = "rich",
description: str | None = None,
description: str | TranslatedString | None = None,
url: str | None = None,
timestamp: dt | None = None,
color: int | None = None) -> None:
self.title: str | None = title
self.title: str | TranslatedString | None = title
self.type: str = type
self.description: str | None = description
self.description: str | TranslatedString | None = description
self.url: str | None = url
self.timestamp: dt | None = timestamp
self.color: int | None = color
Expand All @@ -224,9 +224,9 @@ def __init__(
def _to_data(self) -> EmbedPayload:
v: EmbedPayload = {}
if self.title is not None:
v["title"] = self.title
v["title"] = str(self.title)
if self.description is not None:
v["description"] = self.description
v["description"] = str(self.description)
if self.url is not None:
v["url"] = self.url
if self.timestamp is not None:
Expand Down Expand Up @@ -283,7 +283,7 @@ def footer(self) -> EmbedFooter | None:

def set_footer(
self,
text: str,
text: str | TranslatedString,
*,
icon_url: str | None = None) -> Self:
"""
Expand Down Expand Up @@ -377,7 +377,7 @@ def author(self) -> EmbedAuthor | None:

def set_author(
self,
name: str,
name: str | TranslatedString,
*,
url: str | None = None,
icon_url: str | None = None) -> Self:
Expand Down Expand Up @@ -415,8 +415,8 @@ def fields(self) -> list[EmbedField]:

def add_field(
self,
name: str,
value: str,
name: str | TranslatedString,
value: str | TranslatedString,
*,
inline: bool = True) -> Self:
"""
Expand Down Expand Up @@ -457,8 +457,8 @@ def remove_field(self, index: int) -> Self:
def insert_field_at(
self,
index: int,
name: str,
value: str,
name: str | TranslatedString,
value: str | TranslatedString,
*,
inline: bool = True) -> Self:
"""
Expand Down
18 changes: 16 additions & 2 deletions novus/models/interaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,17 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Generic, TypeVar
from typing import TYPE_CHECKING, Generic, Literal, TypeVar

from ..enums import ApplicationCommandType, ApplicationOptionType, InteractionType, Locale
from ..flags import Permissions
from ..utils import cached_slot_property, generate_repr, try_snowflake, walk_components
from ..utils import (
TranslatedString,
cached_slot_property,
generate_repr,
try_snowflake,
walk_components,
)
from .api_mixins.interaction import InteractionAPIMixin
from .application_command import ApplicationCommandOption
from .channel import Channel, channel_builder
Expand Down Expand Up @@ -563,3 +569,11 @@ def custom_id(self) -> str | None:
if self.data is None:
return None
return getattr(self.data, "custom_id", None)

def _(
self,
string: str,
guild: int | Literal[False] = 1,
user: int | Literal[False] = 0) -> TranslatedString:
return TranslatedString(string, context=self, guild=guild, user=user)

1 change: 1 addition & 0 deletions novus/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'Localization',
'ME',
'MISSING',
'TranslatedString',
'add_not_missing',
'bytes_to_base64_data',
'cached_slot_property',
Expand Down
82 changes: 80 additions & 2 deletions novus/utils/localization.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,19 @@

from __future__ import annotations

from typing import TYPE_CHECKING
import gettext
from typing import TYPE_CHECKING, Literal, Any

from ..enums import Locale
from .missing import MISSING

if TYPE_CHECKING:
from .. import payloads
from .. import Interaction, payloads

__all__ = (
'Localization',
'flatten_localization',
'TranslatedString',
)


Expand Down Expand Up @@ -81,4 +83,80 @@ def _to_data(self) -> dict[payloads.Locale, str]:
}


# any valid localisation type
LocType = dict[str, str] | dict[Locale, str] | Localization | None



class TranslatedString:
"""
An object to help with translation of strings.
Takes an input, takes a relevant context, gettexts the hell out of it.
"""

def __init__(
self,
original: str,
*,
context: Interaction[Any] | None = None,
guild: int | Literal[False] = 1,
user: int | Literal[False] = 0):
self.original: str = original
self.context: Interaction | None = context
self.languages: list[str] | None
self.languages = self._get_languages(guild=guild, user=user)

def _get_languages(
self,
*,
guild: int | Literal[False],
user: int | Literal[False]) -> list[str]:
"""
Get the languages for to use for the translation.
`guild` and `user` are in priority order (defaulting to guild
being higher priority), or ``False`` to disable.
"""

# We can only reutrn things if we have a context to give
if not (ctx := self.context):
return []

# Work out what languages we even have available
user_languages: list[str] = [
ctx.locale.value,
ctx.locale.value.split("-")[0],
]
guild_languages: list[str] = []
if guild and ctx.guild and ctx.guild_locale:
guild_languages = [
ctx.guild_locale.value,
ctx.guild_locale.value.split("-")[0],
]

# Work out what we can return
if guild is False and user is False:
return []
elif user is False:
return guild_languages
elif guild is False:
return user_languages
else:

# Return languages in order priority
if user == guild or guild > user:
return [*guild_languages, *user_languages]
else:
return [*user_languages, *guild_languages]

def __str__(self) -> str:
return gettext.translation(
domain="main",
localedir="./locales",
languages=self.languages,
fallback=True,
).gettext(self.original)

def __getattr__(self, v):
return getattr(str(self), v)

0 comments on commit 275857f

Please sign in to comment.