diff --git a/src/core/i18n.py b/src/core/i18n.py index 3422123..f6ba47b 100644 --- a/src/core/i18n.py +++ b/src/core/i18n.py @@ -13,8 +13,6 @@ from core.constants import TranslationContextLimits if TYPE_CHECKING: - from types import FrameType - from discord.app_commands import TranslationContextTypes, locale_str BASE_DIR = "." @@ -63,29 +61,98 @@ def i18n( _l: int = -1, # size limit **kwargs: Any, ) -> str: + """Function to translate a string. + + All strings should be passed through this function to be translated. + Most of the time, this function will be called from a function that has an interaction as an argument. + + ```py + @app_command.command() + async def test(self, inter: Interaction): + await inter.response.send_message(_("Hello, World!")) + ``` + + When doing so, the function will automatically detect the user's locale and translate the string accordingly. + This function also supports string formatting. + + ```py + def second_function(): + return _("Note that this will also catch the Interaction object if it is called from a command callback.") + + @app_command.command() + async def test(self, inter: Interaction): + await inter.response.send_message(_("Hello, {0}!", "World")) + await inter.followup.send(content=_("Hello, {world}!", world="World")) + await inter.followup.send(content=second_function()) + ``` + + If you are not calling this function from an interaction callback, you can manually set the locale with the + `_locale` argument. + + ```py + def test(): + return _("Hello, World!", _locale=Locale.french) + ``` + + Or, if when you define the string, you don't know the locale, you can set `_locale` to `None`. + + ```py + my_strings = { + "blue": _("The sea is blue!", _locale=None), + "red": _("Red is the color of blood!", _locale=None), + } + + @app_command.command() + async def color(self, inter: Interaction, color: Literal["blue", "red"]): + await inter.response.send_message(_(my_strings[color])) # note the use of the `_` function here + ``` + + The last special case is you your context can have an interaction in the stack, **OR** not. Then you can use the + `_silent` argument to silent the warning message if the interaction was not found. + This is useful for persistant view, that will be loaded at startup and when sending new views. + + ```py + class MyView(View): + def __init__(self): + super().__init__() + self.click_me.label = _("Click me!", _silent=True) + + @ui.button(custom_id="click_me") + async def click_me(self, inter: Interaction, button: Button): + await inter.response.send_message(_("You clicked me!")) + ``` + + Args: + string: the string to translate. + _locale: allow to manually set the target language. + _silent: silent the warning message if the interaction was not found. + _l: allow to set a max size for translated strings. + + Returns: + The translated string according to the user's locale. + """ if string == "": return string - if _silent: - logger.warning("Deprecated usage of _silent parameter in i18n function.") - _locale = None if _locale is MISSING: - frame: FrameType | None = inspect.currentframe() + stack = inspect.stack() - while frame is not None: - inter: Interaction | None = find((lambda _item: isinstance(_item, Interaction)), frame.f_locals.values()) + inter: Interaction | None = None + for frame_info in stack: + inter = find((lambda _item: isinstance(_item, Interaction)), frame_info.frame.f_locals.values()) if inter is not None: - del frame break - frame = frame.f_back - else: - inter = None if inter is None: if not _silent: - logger.warning("i18n function cannot retrieve an interaction for this string.\nstring=%s", string) + caller = inspect.getframeinfo(inspect.stack()[1][0]) + logger.warning( + 'i18n function cannot retrieve an interaction for the string "%s" at line %s in file %s', + string, + caller.lineno, + caller.filename, + ) return string - _locale = inter.locale if _locale is None: result = string.format(*args, **kwargs)