Skip to content

Commit 588a4be

Browse files
committed
🌐 Add i18n
1 parent 39382fa commit 588a4be

File tree

8 files changed

+526
-13
lines changed

8 files changed

+526
-13
lines changed

src/custom/__init__.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from typing_extensions import override
2+
import discord
3+
from src.i18n import apply_locale
4+
from src.i18n.classes import ExtensionTranslation, TranslationWrapper
5+
from typing import Any
6+
7+
8+
class ApplicationContext(discord.ApplicationContext):
9+
def __init__(self, bot: discord.Bot, interaction: discord.Interaction):
10+
self.translations: TranslationWrapper = TranslationWrapper(
11+
{}, "en-US"
12+
) # empty placeholder
13+
super().__init__(bot=bot, interaction=interaction)
14+
15+
@override
16+
def __setattr__(self, key: Any, value: Any):
17+
if key == "command":
18+
if hasattr(value, "translations"):
19+
self.translations = apply_locale(
20+
value.translations,
21+
self.locale,
22+
)
23+
super().__setattr__(key, value)
24+
25+
26+
class Bot(discord.Bot):
27+
def __init__(self, *args: Any, **options: Any):
28+
self.translations: list[ExtensionTranslation] = options.pop("translations", [])
29+
super().__init__(*args, **options) # pyright: ignore[reportUnknownMemberType]
30+
31+
@override
32+
async def get_application_context(
33+
self,
34+
interaction: discord.Interaction,
35+
cls: type[discord.ApplicationContext] = ApplicationContext,
36+
):
37+
return await super().get_application_context(interaction, cls=cls)

src/extensions/ping/ping.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from discord.ext import commands
66
from schema import Schema
77
from src.log import logger
8-
8+
from src import custom
99

1010
default = {
1111
"enabled": True,
@@ -25,15 +25,17 @@ def __init__(self, bot: discord.Bot):
2525
@discord.slash_command(name="ping")
2626
async def ping(
2727
self,
28-
ctx: discord.ApplicationContext,
28+
ctx: custom.ApplicationContext,
2929
ephemeral: bool = False,
30-
embed: bool = False,
30+
use_embed: bool = False,
3131
):
3232
await ctx.defer(ephemeral=ephemeral)
33-
if embed:
33+
if use_embed:
3434
embed = discord.Embed(
3535
title="Pong!",
36-
description=f"{round(self.bot.latency * 1000)}ms",
36+
description=ctx.translations.response.format(
37+
latency=round(self.bot.latency * 1000)
38+
),
3739
color=discord.Colour.blurple(),
3840
)
3941
return await ctx.respond(embed=embed, ephemeral=ephemeral)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
commands:
2+
ping:
3+
name:
4+
en-US: ping
5+
fr: ping
6+
de: ping
7+
es-ES: ping
8+
ru: пинг
9+
nl: ping
10+
description:
11+
en-US: Get the bot's latency
12+
fr: Obtenir la latence du bot
13+
de: Erhalte die Latenz des Bots
14+
es-ES: Obtener la latencia del bot
15+
ru: Получить задержку бота
16+
nl: Krijg de latentie van de bot
17+
options:
18+
ephemeral:
19+
name:
20+
en-US: ephemeral
21+
fr: éphémère
22+
de: flüchtig
23+
es-ES: efímero
24+
ru: мгновенное
25+
nl: tijdelijk
26+
description:
27+
en-US: Whether the response should be ephemeral
28+
fr: Si la réponse doit être éphémère
29+
de: Ob die Antwort flüchtig sein soll
30+
es-ES: Si la respuesta debe ser efímera
31+
ru: Должен ли ответ быть мгновенным
32+
nl: Of het antwoord tijdelijk moet zijn
33+
use_embed:
34+
name:
35+
en-US: embed
36+
fr: embed
37+
de: einbetten
38+
es-ES: incrustar
39+
ru: встроить
40+
nl: inbedden
41+
description:
42+
en-US: Whether the response should be an embed
43+
fr: Si la réponse doit être un embed
44+
de: Ob die Antwort eingebettet sein soll
45+
es-ES: Si la respuesta debe ser incrustada
46+
ru: Должен ли ответ быть встроенным
47+
nl: Of het antwoord moet worden ingebed
48+
strings:
49+
response:
50+
en-US: Pong! The bot's latency is {latency}ms.
51+
fr: Pong ! La latence du bot est de {latency}ms.
52+
de: Pong! Die Latenz des Bots beträgt {latency}ms.
53+
es-ES: ¡Pong! La latencia del bot es de {latency}ms.
54+
ru: Понг! Задержка бота составляет {latency}мс.
55+
nl: Pong! De latentie van de bot is {latency}ms.

src/i18n/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .utils import apply, load_translation
2+
from .classes import apply_locale
3+
4+
__all__ = ["apply", "load_translation", "apply_locale"]

src/i18n/classes.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
from typing import Any
2+
from typing_extensions import override
3+
from pydantic import BaseModel, Field
4+
5+
LOCALES = (
6+
"en-US",
7+
"en-GB",
8+
"bg",
9+
"zh-CN",
10+
"zh-TW",
11+
"hr",
12+
"cs",
13+
"da",
14+
"nl",
15+
"fi",
16+
"fr",
17+
"de",
18+
"el",
19+
"hi",
20+
"hu",
21+
"it",
22+
"ja",
23+
"ko",
24+
"lt",
25+
"no",
26+
"pl",
27+
"pt-BR",
28+
"ro",
29+
"ru",
30+
"es-ES",
31+
"es-419",
32+
"sv-SE",
33+
"th",
34+
"tr",
35+
"uk",
36+
"vi",
37+
)
38+
DEFAULT = "en-US"
39+
40+
41+
class RawTranslation(BaseModel):
42+
en_US: str | None = Field(None, alias="en-US")
43+
en_GB: str | None = Field(None, alias="en-GB")
44+
bg: str | None = None
45+
zh_CN: str | None = Field(None, alias="zh-CN")
46+
zh_TW: str | None = Field(None, alias="zh-TW")
47+
hr: str | None = None
48+
cs: str | None = None
49+
da: str | None = None
50+
nl: str | None = None
51+
fi: str | None = None
52+
fr: str | None = None
53+
de: str | None = None
54+
el: str | None = None
55+
hi: str | None = None
56+
hu: str | None = None
57+
it: str | None = None
58+
ja: str | None = None
59+
ko: str | None = None
60+
lt: str | None = None
61+
no: str | None = None
62+
pl: str | None = None
63+
pt_BR: str | None = Field(None, alias="pt-BR")
64+
ro: str | None = None
65+
ru: str | None = None
66+
es_ES: str | None = Field(None, alias="es-ES")
67+
es_419: str | None = Field(None, alias="es-419")
68+
sv_SE: str | None = Field(None, alias="sv-SE")
69+
th: str | None = None
70+
tr: str | None = None
71+
uk: str | None = None
72+
vi: str | None = None
73+
74+
class Config:
75+
populate_by_name = True
76+
77+
78+
class Translation(BaseModel):
79+
def get_for_locale(self, locale: str) -> "TranslationWrapper":
80+
return apply_locale(self, locale)
81+
82+
83+
class TranslationWrapper:
84+
def __init__(self, model: "Translatable", locale: str, default: str = DEFAULT):
85+
self._model = model
86+
self._default: str
87+
self.default = default
88+
self._locale: str
89+
self.locale = locale.replace("-", "_")
90+
91+
def __getattr__(self, key: str) -> Any:
92+
if isinstance(self._model, dict):
93+
applicable = self._model.get(key)
94+
if not applicable:
95+
raise AttributeError
96+
else:
97+
applicable = getattr(self._model, key)
98+
if isinstance(applicable, RawTranslation):
99+
try:
100+
print(id(self._model))
101+
return getattr(applicable, self._locale)
102+
except AttributeError:
103+
return getattr(applicable, self._default)
104+
return apply_locale(applicable, self._locale)
105+
106+
@property
107+
def locale(self) -> str:
108+
return self._locale
109+
110+
@locale.setter
111+
def locale(self, value: str | None) -> None: # pyright: ignore[reportPropertyTypeMismatch]
112+
if value is None:
113+
value = self.default
114+
if value not in LOCALES:
115+
raise ValueError(f"Invalid locale {value}")
116+
self._locale = value
117+
118+
@property
119+
def default(self) -> str:
120+
return self._default
121+
122+
@default.setter
123+
def default(self, value: str) -> None:
124+
if value not in LOCALES:
125+
raise ValueError(f"Invalid locale {value}")
126+
self._default = value
127+
128+
@override
129+
def __repr__(self) -> str:
130+
return repr(self._model)
131+
132+
@override
133+
def __str__(self) -> str:
134+
return str(self._model)
135+
136+
137+
Translatable = Translation | dict[str, Translation]
138+
139+
140+
class NameDescriptionTranslation(Translation):
141+
name: RawTranslation | None = None
142+
description: RawTranslation | None = None
143+
144+
145+
class CommandTranslation(NameDescriptionTranslation):
146+
strings: dict[str, RawTranslation] | None = None
147+
options: dict[str, NameDescriptionTranslation] | None = None
148+
149+
150+
class Deg3CommandTranslation(CommandTranslation): ...
151+
152+
153+
class Deg2CommandTranslation(CommandTranslation):
154+
commands: dict[str, Deg3CommandTranslation] | None = None
155+
156+
157+
class Deg1CommandTranslation(CommandTranslation):
158+
commands: dict[str, Deg2CommandTranslation] | None = None
159+
160+
161+
AnyCommandTranslation = (
162+
Deg1CommandTranslation | Deg2CommandTranslation | Deg3CommandTranslation
163+
)
164+
165+
166+
class ExtensionTranslation(Translation):
167+
commands: dict[str, Deg1CommandTranslation] | None = None
168+
strings: dict[str, RawTranslation] | None = None
169+
170+
171+
def apply_locale(
172+
model: "Translatable | TranslationWrapper", locale: str, default: str = DEFAULT
173+
) -> TranslationWrapper:
174+
if isinstance(model, TranslationWrapper):
175+
model.locale = locale
176+
model.default = default
177+
return model
178+
return TranslationWrapper(model, locale, default)

0 commit comments

Comments
 (0)