diff --git a/arc/__init__.py b/arc/__init__.py index adf24c3..5485cf8 100644 --- a/arc/__init__.py +++ b/arc/__init__.py @@ -41,6 +41,14 @@ from .events import ArcEvent, CommandErrorEvent from .extension import loader, unloader from .internal.about import __author__, __author_email__, __license__, __maintainer__, __url__, __version__ +from .locale import ( + CommandLocaleRequest, + CustomLocaleRequest, + LocaleRequest, + LocaleRequestType, + LocaleResponse, + OptionLocaleRequest, +) from .plugin import GatewayPluginBase, PluginBase, RESTPluginBase from .utils import bot_has_permissions, dm_only, guild_only, has_permissions, owner_only @@ -96,6 +104,12 @@ "RESTPlugin", "GatewayPlugin", "HookResult", + "LocaleRequest", + "LocaleRequestType", + "LocaleResponse", + "CustomLocaleRequest", + "CommandLocaleRequest", + "OptionLocaleRequest", "abc", "command", "with_hook", diff --git a/arc/abc/client.py b/arc/abc/client.py index 42a33fe..834aa5d 100644 --- a/arc/abc/client.py +++ b/arc/abc/client.py @@ -22,7 +22,19 @@ from arc.context import AutodeferMode, Context from arc.errors import ExtensionLoadError, ExtensionUnloadError from arc.internal.sync import _sync_commands -from arc.internal.types import AppT, BuilderT, ErrorHandlerCallbackT, HookT, LifeCycleHookT, PostHookT, ResponseBuilderT +from arc.internal.types import ( + AppT, + BuilderT, + CommandLocaleRequestT, + CustomLocaleRequestT, + ErrorHandlerCallbackT, + HookT, + LifeCycleHookT, + OptionLocaleRequestT, + PostHookT, + ResponseBuilderT, +) +from arc.locale import CommandLocaleRequest, LocaleResponse, OptionLocaleRequest if t.TYPE_CHECKING: import typing_extensions as te @@ -70,13 +82,25 @@ class Client(t.Generic[AppT], abc.ABC): "_error_handler", "_startup_hook", "_shutdown_hook", + "_provided_locales", + "_command_locale_provider", + "_option_locale_provider", + "_custom_locale_provider", ) def __init__( - self, app: AppT, *, default_enabled_guilds: t.Sequence[hikari.Snowflake] | None = None, autosync: bool = True + self, + app: AppT, + *, + default_enabled_guilds: t.Sequence[hikari.Snowflake] | None = None, + autosync: bool = True, + provided_locales: t.Sequence[hikari.Locale] | None = None, ) -> None: self._app = app self._default_enabled_guilds = default_enabled_guilds + self._autosync = autosync + self._provided_locales: t.Sequence[hikari.Locale] | None = provided_locales + self._application: hikari.Application | None = None self._slash_commands: dict[str, SlashCommandLike[te.Self]] = {} self._message_commands: dict[str, MessageCommand[te.Self]] = {} @@ -84,13 +108,15 @@ def __init__( self._injector: alluka.Client = alluka.Client() self._plugins: dict[str, PluginBase[te.Self]] = {} self._loaded_extensions: list[str] = [] - self._autosync = autosync self._hooks: list[HookT[te.Self]] = [] self._post_hooks: list[PostHookT[te.Self]] = [] self._owner_ids: list[hikari.Snowflake] = [] self._error_handler: ErrorHandlerCallbackT[te.Self] | None = None self._startup_hook: LifeCycleHookT[te.Self] | None = None self._shutdown_hook: LifeCycleHookT[te.Self] | None = None + self._command_locale_provider: CommandLocaleRequestT | None = None + self._option_locale_provider: OptionLocaleRequestT | None = None + self._custom_locale_provider: CustomLocaleRequestT | None = None @property @abc.abstractmethod @@ -303,6 +329,20 @@ async def on_command_interaction(self, interaction: hikari.CommandInteraction) - f" Did you forget to respond?" ) + def _provide_command_locale(self, request: CommandLocaleRequest) -> LocaleResponse: + """Provide a locale for a command.""" + if self._command_locale_provider is None: + return LocaleResponse(name=request.command.name, description=getattr(request.command, "description", None)) + + return self._command_locale_provider(request) + + def _provide_option_locale(self, request: OptionLocaleRequest) -> LocaleResponse: + """Provide a locale for an option.""" + if self._option_locale_provider is None: + return LocaleResponse(name=request.option.name, description=request.option.description) + + return self._option_locale_provider(request) + async def on_autocomplete_interaction( self, interaction: hikari.AutocompleteInteraction ) -> hikari.api.InteractionAutocompleteBuilder | None: @@ -605,6 +645,84 @@ async def shutdown_hook(client: arc.GatewayClient) -> None: """ self._shutdown_hook = handler + def set_command_locale_provider(self, provider: CommandLocaleRequestT) -> None: + """Decorator to set the command locale provider for this client. + + This will be called for each command for each locale. + + Parameters + ---------- + provider : CommandLocaleRequestT + The command locale provider to set. + + Usage + ----- + ```py + @client.set_command_locale_provider + def command_locale_provider(request: arc.CommandLocaleRequest) -> arc.LocaleResponse: + ... + ``` + + Or, as a function: + + ```py + client.set_command_locale_provider(command_locale_provider) + ``` + """ + self._command_locale_provider = provider + + def set_option_locale_provider(self, provider: OptionLocaleRequestT) -> None: + """Decorator to set the option locale provider for this client. + + This will be called for each option of each command for each locale. + + Parameters + ---------- + provider : OptionLocaleRequestT + The option locale provider to set. + + Usage + ----- + ```py + @client.set_option_locale_provider + def option_locale_provider(request: arc.OptionLocaleRequest) -> arc.LocaleResponse: + ... + ``` + + Or, as a function: + + ```py + client.set_option_locale_provider(option_locale_provider) + ``` + """ + self._option_locale_provider = provider + + def set_custom_locale_provider(self, provider: CustomLocaleRequestT) -> None: + """Decorator to set the custom locale provider for this client. + + This will be called for each custom locale request performed via [`Context.loc()`][arc.context.base.Context.loc]. + + Parameters + ---------- + provider : CustomLocaleRequestT + The custom locale provider to set. + + Usage + ----- + ```py + @client.set_custom_locale_provider + def custom_locale_provider(request: arc.CustomLocaleRequest) -> str: + ... + ``` + + Or, as a function: + + ```py + client.set_custom_locale_provider(custom_locale_provider) + ``` + """ + self._custom_locale_provider = provider + def load_extension(self, path: str) -> te.Self: """Load a python module with path `path` as an extension. This will import the module, and call it's loader function. diff --git a/arc/abc/command.py b/arc/abc/command.py index 9c04a39..061a3ff 100644 --- a/arc/abc/command.py +++ b/arc/abc/command.py @@ -21,6 +21,7 @@ PostHookT, ResponseBuilderT, ) +from arc.locale import CommandLocaleRequest if t.TYPE_CHECKING: from arc.abc.plugin import PluginBase @@ -323,6 +324,25 @@ def _plugin_include_hook(self, plugin: PluginBase[ClientT]) -> None: self._plugin = plugin self._plugin._add_command(self) + def _request_command_locale(self) -> None: + """Request the locale for this command.""" + if self.name_localizations or self._client is None: + return + + if not self._client._provided_locales or not self._client._command_locale_provider: + return + + name_locales: dict[hikari.Locale, str] = {} + + for locale in self._client._provided_locales: + request = CommandLocaleRequest(self, locale, self.name) + resp = self._client._command_locale_provider(request) + + if resp.name is not None: + name_locales[locale] = resp.name + + self._name_localizations = name_locales + async def _handle_pre_hooks(self, command: CallableCommandProto[ClientT], ctx: Context[ClientT]) -> bool: """Handle all pre-execution hooks for a command. diff --git a/arc/abc/option.py b/arc/abc/option.py index a04af13..1e69e89 100644 --- a/arc/abc/option.py +++ b/arc/abc/option.py @@ -8,10 +8,14 @@ import hikari from arc.internal.types import AutocompleteCallbackT, ChoiceT, ClientT, ParamsT +from arc.locale import OptionLocaleRequest if t.TYPE_CHECKING: import typing_extensions as te + from arc.abc.client import Client + from arc.abc.command import CommandProto + __all__ = ("Option", "OptionParams", "OptionWithChoices", "OptionWithChoicesParams", "OptionBase", "CommandOptionBase") T = t.TypeVar("T") @@ -167,6 +171,28 @@ def to_command_option(self) -> hikari.CommandOption: """Convert this option to a hikari.CommandOption.""" return hikari.CommandOption(**self._to_dict()) + def _request_option_locale(self, client: Client[t.Any], command: CommandProto) -> None: + """Request the option's name and description in different locales.""" + if self.name_localizations or self.description_localizations: + return + + if not client._provided_locales or not client._option_locale_provider: + return + + name_locales: dict[hikari.Locale, str] = {} + desc_locales: dict[hikari.Locale, str] = {} + + for locale in client._provided_locales: + request = OptionLocaleRequest(command, locale, self.name, self.description, self) + resp = client._option_locale_provider(request) + + if resp.name is not None and resp.description is not None: + name_locales[locale] = resp.name + desc_locales[locale] = resp.description + + self.name_localizations = name_locales + self.description_localizations = desc_locales + @attr.define(slots=True, kw_only=True) class CommandOptionBase(OptionBase[T], t.Generic[T, ClientT, ParamsT]): diff --git a/arc/client.py b/arc/client.py index f76e411..6195e80 100644 --- a/arc/client.py +++ b/arc/client.py @@ -37,6 +37,8 @@ class GatewayClient(Client[hikari.GatewayBotAware]): The guilds that slash commands will be registered in by default, by default None autosync : bool, optional Whether to automatically sync commands on startup, by default True + provided_locales : t.Sequence[hikari.Locale] | None, optional + The locales that will be provided to the client by locale provider callbacks, by default None Usage ----- @@ -59,8 +61,11 @@ def __init__( *, default_enabled_guilds: t.Sequence[hikari.Snowflake] | None = None, autosync: bool = True, + provided_locales: t.Sequence[hikari.Locale] | None = None, ) -> None: - super().__init__(app, default_enabled_guilds=default_enabled_guilds, autosync=autosync) + super().__init__( + app, default_enabled_guilds=default_enabled_guilds, autosync=autosync, provided_locales=provided_locales + ) self.app.event_manager.subscribe(hikari.StartedEvent, self._on_gatewaybot_startup) self.app.event_manager.subscribe(hikari.StoppingEvent, self._on_gatewaybot_shutdown) self.app.event_manager.subscribe(hikari.InteractionCreateEvent, self._on_gatewaybot_interaction_create) @@ -129,6 +134,8 @@ class RESTClient(Client[hikari.RESTBotAware]): The guilds that slash commands will be registered in by default, by default None autosync : bool, optional Whether to automatically sync commands on startup, by default True + provided_locales : t.Sequence[hikari.Locale] | None, optional + The locales that will be provided to the client by locale provider callbacks, by default None Usage @@ -155,8 +162,11 @@ def __init__( *, default_enabled_guilds: t.Sequence[hikari.Snowflake] | None = None, autosync: bool = True, + provided_locales: t.Sequence[hikari.Locale] | None = None, ) -> None: - super().__init__(app, default_enabled_guilds=default_enabled_guilds, autosync=autosync) + super().__init__( + app, default_enabled_guilds=default_enabled_guilds, autosync=autosync, provided_locales=provided_locales + ) self.app.add_startup_callback(self._on_restbot_startup) self.app.add_shutdown_callback(self._on_restbot_shutdown) self.app.interaction_server.set_listener( diff --git a/arc/command/slash.py b/arc/command/slash.py index 8cc16f3..b881b92 100644 --- a/arc/command/slash.py +++ b/arc/command/slash.py @@ -12,11 +12,13 @@ from arc.errors import AutocompleteError, CommandInvokeError from arc.internal.sigparse import parse_function_signature from arc.internal.types import ClientT, CommandCallbackT, HookT, PostHookT, ResponseBuilderT, SlashCommandLike +from arc.locale import CommandLocaleRequest, LocaleResponse if t.TYPE_CHECKING: from asyncio.futures import Future - from arc.abc.command import CallableCommandProto + from arc.abc.client import Client + from arc.abc.command import CallableCommandProto, CommandProto from arc.abc.option import CommandOptionBase from arc.abc.plugin import PluginBase @@ -110,7 +112,7 @@ class SlashCommand(CallableCommandBase[ClientT, hikari.api.SlashCommandBuilder]) description: str = "No description provided." """The description of this slash command.""" - description_localizations: dict[hikari.Locale, str] = attr.field(factory=dict) + description_localizations: t.Mapping[hikari.Locale, str] = attr.field(factory=dict) """The localizations for this command's description.""" options: dict[str, CommandOptionBase[t.Any, ClientT, t.Any]] = attr.field(factory=dict) @@ -196,6 +198,33 @@ async def _on_autocomplete( await interaction.create_response(choices) + def _request_command_locale(self) -> None: + """Request the locale for this command.""" + if self.name_localizations or self.description_localizations or self._client is None: + return + + if not self._client._provided_locales or not self._client._command_locale_provider: + return + + name_locales: dict[hikari.Locale, str] = {} + desc_locales: dict[hikari.Locale, str] = {} + + for locale in self._client._provided_locales: + request = CommandLocaleRequest(self, locale, self.name) + resp = self._client._command_locale_provider(request) + + assert isinstance(resp, LocaleResponse) + + if resp.name is not None and resp.description is not None: + name_locales[locale] = resp.name + desc_locales[locale] = resp.description + + self.name_localizations: t.Mapping[hikari.Locale, str] = name_locales + self.description_localizations: t.Mapping[hikari.Locale, str] = desc_locales + + for option in self.options.values(): + option._request_option_locale(self._client, self) + @attr.define(slots=True, kw_only=True) class SlashGroup(CommandBase[ClientT, hikari.api.SlashCommandBuilder]): @@ -207,7 +236,7 @@ class SlashGroup(CommandBase[ClientT, hikari.api.SlashCommandBuilder]): description: str = "No description provided." """The description of this slash group.""" - description_localizations: dict[hikari.Locale, str] = attr.field(factory=dict) + description_localizations: t.Mapping[hikari.Locale, str] = attr.field(factory=dict) """The localizations for this group's description.""" _invoke_task: asyncio.Task[t.Any] | None = attr.field(init=False, default=None) @@ -359,6 +388,31 @@ async def _on_autocomplete( await interaction.create_response(choices) + def _request_command_locale(self) -> None: + """Request the locale for this command.""" + if self.name_localizations or self.description_localizations or self._client is None: + return + + if not self._client._provided_locales or not self._client._command_locale_provider: + return + + name_locales: dict[hikari.Locale, str] = {} + desc_locales: dict[hikari.Locale, str] = {} + + for locale in self._client._provided_locales: + request = CommandLocaleRequest(self, locale, self.name) + resp = self._client._command_locale_provider(request) + + if resp.name is not None and resp.description is not None: + name_locales[locale] = resp.name + desc_locales[locale] = resp.description + + self.name_localizations: t.Mapping[hikari.Locale, str] = name_locales + self.description_localizations: t.Mapping[hikari.Locale, str] = desc_locales + + for sub in self.children.values(): + sub._request_option_locale(self._client, self) + def include(self, command: SlashSubCommand[ClientT]) -> SlashSubCommand[ClientT]: """First-order decorator to add a subcommand to this group.""" command.parent = self @@ -468,6 +522,12 @@ async def _handle_exception(self, ctx: Context[ClientT], exc: Exception) -> None assert self.parent is not None await self.parent._handle_exception(ctx, e) + def _request_option_locale(self, client: Client[t.Any], command: CommandProto) -> None: + super()._request_option_locale(client, command) + + for subcommand in self.children.values(): + subcommand._request_option_locale(client, command) + def include(self, command: SlashSubCommand[ClientT]) -> SlashSubCommand[ClientT]: """First-order decorator to add a subcommand to this group.""" command.parent = self @@ -555,6 +615,12 @@ def plugin(self) -> PluginBase[ClientT] | None: """The plugin that includes this subcommand.""" return self.root.plugin + def _request_option_locale(self, client: Client[t.Any], command: CommandProto) -> None: + super()._request_option_locale(client, command) + + for option in self.options.values(): + option._request_option_locale(client, command) + def _resolve_autodefer(self) -> AutodeferMode: """Resolve autodefer for this subcommand.""" if self.autodefer is not hikari.UNDEFINED: diff --git a/arc/context/base.py b/arc/context/base.py index 3a5363f..690b610 100644 --- a/arc/context/base.py +++ b/arc/context/base.py @@ -12,6 +12,7 @@ from arc.errors import NoResponseIssuedError, ResponseAlreadyIssuedError from arc.internal.types import ClientT, ResponseBuilderT +from arc.locale import CustomLocaleRequest if t.TYPE_CHECKING: from arc.abc.command import CallableCommandProto @@ -395,6 +396,48 @@ def get_channel(self) -> hikari.TextableGuildChannel | None: """Gets the channel this context represents, None if in a DM. Requires application cache.""" return self._interaction.get_channel() + def loc(self, key: str, use_guild: bool = True, force_locale: hikari.Locale | None = None, **kwargs: t.Any) -> str: + """Get a localized string using the interaction's locale. + + Parameters + ---------- + key : str + The key of the string to localize. + use_guild : bool, optional + Whether to use the guild or not, if in a guild. + force_locale : hikari.Locale | None, optional + Force a locale to use, instead of the context's locale. + kwargs : t.Any + The keyword arguments to pass to the string formatter. + + Returns + ------- + str + The localized string. + """ + if not self._client._provided_locales: + raise RuntimeError("The client does not have any provided locales set.") + + if force_locale is None: + if not isinstance(self.locale, hikari.Locale): + raise RuntimeError("This context does not have a valid locale object.") + + if self.guild_locale and not isinstance(self.guild_locale, hikari.Locale): + raise RuntimeError("This context does not have a valid guild locale object.") + + locale = (self.guild_locale or self.locale) if use_guild else self.locale + else: + locale = force_locale + + if not self._client._custom_locale_provider: + raise RuntimeError("The client does not have a custom locale provider.") + + if locale not in self._client._provided_locales: + locale = hikari.Locale.EN_US + + request = CustomLocaleRequest(self.command, locale, self, key) + return self._client._custom_locale_provider(request).format(**kwargs) + async def get_last_response(self) -> InteractionResponse: """Get the last response issued to the interaction this context is proxying. diff --git a/arc/internal/sync.py b/arc/internal/sync.py index ea21b5d..5f7b122 100644 --- a/arc/internal/sync.py +++ b/arc/internal/sync.py @@ -2,6 +2,7 @@ import itertools import logging +import pprint import typing as t from collections import defaultdict from contextlib import suppress @@ -170,6 +171,23 @@ def _get_all_commands( return mapping +def _process_localizations( + client: Client[AppT], + commands: dict[hikari.Snowflake | None, dict[hikari.CommandType, dict[str, CommandBase[t.Any, t.Any]]]], +) -> None: + """Call localization providers for all commands.""" + if not client._provided_locales: + return + + logger.info("Processing localizations...") + + # trol + for a in commands.values(): + for b in a.values(): + for command in b.values(): + command._request_command_locale() + + async def _sync_global_commands(client: Client[AppT], commands: CommandMapping) -> None: """Add, edit, and delete global commands to match the client's slash commands. @@ -306,6 +324,9 @@ async def _sync_commands(client: Client[AppT]) -> None: raise RuntimeError("Application is not set, cannot sync commands") commands = _get_all_commands(client) + _process_localizations(client, commands) + pprint.PrettyPrinter(indent=4).pprint(commands) + global_commands = commands.pop(None, None) if commands: diff --git a/arc/internal/types.py b/arc/internal/types.py index f4805fa..7930e67 100644 --- a/arc/internal/types.py +++ b/arc/internal/types.py @@ -9,6 +9,7 @@ from arc.client import GatewayClient, RESTClient from arc.command import SlashCommand, SlashGroup from arc.context import AutocompleteData, Context + from arc.locale import CommandLocaleRequest, CustomLocaleRequest, LocaleResponse, OptionLocaleRequest # Generics @@ -21,6 +22,7 @@ BuilderT = t.TypeVar("BuilderT", bound="hikari.api.SlashCommandBuilder | hikari.api.ContextMenuCommandBuilder") ParamsT = t.TypeVar("ParamsT", bound="OptionParams[t.Any]") HookableT = t.TypeVar("HookableT", bound="Hookable[t.Any]") +P = t.ParamSpec("P") # Type aliases EventCallbackT: t.TypeAlias = "t.Callable[[EventT], t.Coroutine[t.Any, t.Any, None]]" @@ -38,3 +40,6 @@ HookT: t.TypeAlias = "t.Callable[[Context[ClientT]], t.Awaitable[HookResult]] | t.Callable[[Context[ClientT]], HookResult] | t.Callable[[Context[ClientT]], None] | t.Callable[[Context[ClientT]], t.Awaitable[None]]" PostHookT: t.TypeAlias = "t.Callable[[Context[ClientT]], None] | t.Callable[[Context[ClientT]], t.Awaitable[None]]" LifeCycleHookT: t.TypeAlias = "t.Callable[[ClientT], t.Awaitable[None]]" +CommandLocaleRequestT: t.TypeAlias = "t.Callable[t.Concatenate[CommandLocaleRequest, ...], LocaleResponse]" +OptionLocaleRequestT: t.TypeAlias = "t.Callable[t.Concatenate[OptionLocaleRequest, ...], LocaleResponse]" +CustomLocaleRequestT: t.TypeAlias = "t.Callable[t.Concatenate[CustomLocaleRequest, ...], str]" diff --git a/arc/locale.py b/arc/locale.py new file mode 100644 index 0000000..5a010a2 --- /dev/null +++ b/arc/locale.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +import abc +import enum +import typing as t + +import attr + +if t.TYPE_CHECKING: + import hikari + + from arc.abc.command import CommandProto + from arc.abc.option import OptionBase + from arc.context.base import Context + +__all__ = ( + "LocaleRequestType", + "LocaleRequest", + "LocaleResponse", + "CommandLocaleRequest", + "OptionLocaleRequest", + "CustomLocaleRequest", +) + + +class LocaleRequestType(enum.IntEnum): + """The type of locale request.""" + + COMMAND = 0 + """A command locale request.""" + + OPTION = 1 + """An option locale request.""" + + CUSTOM = 2 + """A custom locale request.""" + + +@attr.define(slots=True) +class LocaleRequest(abc.ABC): + """The base class for all locale requests.""" + + _command: CommandProto = attr.field() + _locale: hikari.Locale = attr.field() + + @property + @abc.abstractmethod + def type(self) -> LocaleRequestType: + """The type of locale request.""" + + @property + def locale(self) -> hikari.Locale: + """The locale that is being requested.""" + return self._locale + + @property + def command(self) -> CommandProto: + """The command that is requesting localization.""" + return self._command + + @property + def qualified_name(self) -> t.Sequence[str]: + """The qualified name of the command that is requesting localization.""" + return self._command.qualified_name + + +@attr.define(slots=True) +class LocaleResponse: + """The response to a command or option locale request. + + Parameters + ---------- + name : str + The localized name of the command or option. + description : str | None + The localized description of the command or option. + """ + + name: str | None = attr.field(default=None) + """The localized name of the command or option.""" + + description: str | None = attr.field(default=None) + """The localized description of the command or option.""" + + +@attr.define(slots=True) +class CommandLocaleRequest(LocaleRequest): + """A request to localize a command.""" + + _name: str = attr.field() + _description: str | None = attr.field(default=None) + + @property + def type(self) -> LocaleRequestType: + """The type of locale request.""" + return LocaleRequestType.COMMAND + + @property + def name(self) -> str: + """The name of the command to be localized.""" + return self._name + + @property + def description(self) -> str | None: + """The description of the command to be localized, if any.""" + return self._description + + +@attr.define(slots=True) +class OptionLocaleRequest(LocaleRequest): + """A request to localize a command option.""" + + _name: str = attr.field() + _description: str = attr.field() + _option: OptionBase[t.Any] = attr.field() + + @property + def type(self) -> LocaleRequestType: + """The type of locale request.""" + return LocaleRequestType.OPTION + + @property + def option(self) -> OptionBase[t.Any]: + """The option that is requesting localization.""" + return self._option + + @property + def name(self) -> str: + """The name of the option to be localized.""" + return self._name + + @property + def description(self) -> str: + """The description of the option to be localized.""" + return self._description + + +@attr.define(slots=True) +class CustomLocaleRequest(LocaleRequest): + """A custom locale request made by the user.""" + + _context: Context[t.Any] = attr.field() + _key: str = attr.field() + + @property + def type(self) -> LocaleRequestType: + """The type of locale request.""" + return LocaleRequestType.CUSTOM + + @property + def context(self) -> Context[t.Any]: + """The context that is requesting localization.""" + return self._context + + @property + def key(self) -> str: + """The key that is to be localized.""" + return self._key diff --git a/docs/api_reference/localization.md b/docs/api_reference/localization.md new file mode 100644 index 0000000..0a7e339 --- /dev/null +++ b/docs/api_reference/localization.md @@ -0,0 +1,8 @@ +--- +title: Localization +description: Localization API reference +--- + +# Localization + +::: arc.locale diff --git a/docs/changelog.md b/docs/changelog.md index eae042f..7308a0f 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -9,6 +9,10 @@ hide: Here you can find all the changelogs for `hikari-arc`. +## Unreleased + +- Add localization support through locale providers. + ## v0.3.0 - Add [hooks](./guides/hooks.md). diff --git a/examples/gateway/localization.py b/examples/gateway/localization.py new file mode 100644 index 0000000..8657988 --- /dev/null +++ b/examples/gateway/localization.py @@ -0,0 +1,78 @@ +import hikari +import arc + + +bot = hikari.GatewayBot("...") +# Set the locales that the client will request in the provider callbacks. +client = arc.GatewayClient(bot, provided_locales=[hikari.Locale.EN_US, hikari.Locale.ES_ES]) + +# These are just examples, you can provide localizations from anywhere you want. +COMMAND_LOCALES = { + "hi": { + hikari.Locale.EN_US: { + "name": "hi", + "description": "Say hi to someone!", + }, + hikari.Locale.ES_ES: { + "name": "hola", + "description": "¡Saluda a alguien!", + } + } + +} + +OPTION_LOCALES = { + "hi": { + "user": { + hikari.Locale.EN_US: { + "name": "user", + "description": "The user to say hi to.", + }, + hikari.Locale.ES_ES: { + "name": "usuario", + "description": "El usuario al que saludar.", + } + } + } +} + +CUSTOM_LOCALES = { + "say_hi": { + hikari.Locale.EN_US: "Hey {user}!", + hikari.Locale.ES_ES: "¡Hola {user}!", + } +} + +# This callback will be called when a command needs to be localized. +# The request includes the command and the locale to be used. +@client.set_command_locale_provider +def command_locale_provider(request: arc.CommandLocaleRequest) -> arc.LocaleResponse: + return arc.LocaleResponse(**COMMAND_LOCALES[request.name][request.locale]) + +# This callback will be called when an option needs to be localized. +# The request includes the option and the locale to be used. +@client.set_option_locale_provider +def option_locale_provider(request: arc.OptionLocaleRequest) -> arc.LocaleResponse: + return arc.LocaleResponse(**OPTION_LOCALES[request.command.name][request.name][request.locale]) + +# This callback will be called for every 'ctx.loc()' call. +# The '.key' attribute is the string that is passed to the 'ctx.loc()' method. +@client.set_custom_locale_provider +def custom_locale_provider(request: arc.CustomLocaleRequest) -> str: + return CUSTOM_LOCALES[request.key][request.locale] + +@client.include +@arc.slash_command(name="hi", description="Say hi to someone!") +async def hi_slash( + ctx: arc.GatewayContext, + user: arc.Option[hikari.User, arc.UserParams(description="The user to say hi to.")] +) -> None: + # ctx.loc can be used to request custom localizations that are not related to commands or options. + # Additional keyword arguments passed to the method will be passed to .format() + await ctx.respond(ctx.loc("say_hi", user=user.mention)) + +# Note: 'ctx.loc()' will by default request the guild's locale, outside of guilds it will request the user's locale. +# You can pass use_guild=False to always request the user's locale instead. + + +bot.run() diff --git a/examples/rest/localization.py b/examples/rest/localization.py new file mode 100644 index 0000000..78f5f9d --- /dev/null +++ b/examples/rest/localization.py @@ -0,0 +1,78 @@ +import hikari +import arc + + +bot = hikari.RESTBot("...") +# Set the locales that the client will request in the provider callbacks. +client = arc.RESTClient(bot, provided_locales=[hikari.Locale.EN_US, hikari.Locale.ES_ES]) + +# These are just examples, you can provide localizations from anywhere you want. +COMMAND_LOCALES = { + "hi": { + hikari.Locale.EN_US: { + "name": "hi", + "description": "Say hi to someone!", + }, + hikari.Locale.ES_ES: { + "name": "hola", + "description": "¡Saluda a alguien!", + } + } + +} + +OPTION_LOCALES = { + "hi": { + "user": { + hikari.Locale.EN_US: { + "name": "user", + "description": "The user to say hi to.", + }, + hikari.Locale.ES_ES: { + "name": "usuario", + "description": "El usuario al que saludar.", + } + } + } +} + +CUSTOM_LOCALES = { + "say_hi": { + hikari.Locale.EN_US: "Hey {user}!", + hikari.Locale.ES_ES: "¡Hola {user}!", + } +} + +# This callback will be called when a command needs to be localized. +# The request includes the command and the locale to be used. +@client.set_command_locale_provider +def command_locale_provider(request: arc.CommandLocaleRequest) -> arc.LocaleResponse: + return arc.LocaleResponse(**COMMAND_LOCALES[request.name][request.locale]) + +# This callback will be called when an option needs to be localized. +# The request includes the option and the locale to be used. +@client.set_option_locale_provider +def option_locale_provider(request: arc.OptionLocaleRequest) -> arc.LocaleResponse: + return arc.LocaleResponse(**OPTION_LOCALES[request.command.name][request.name][request.locale]) + +# This callback will be called for every 'ctx.loc()' call. +# The '.key' attribute is the string that is passed to the 'ctx.loc()' method. +@client.set_custom_locale_provider +def custom_locale_provider(request: arc.CustomLocaleRequest) -> str: + return CUSTOM_LOCALES[request.key][request.locale] + +@client.include +@arc.slash_command(name="hi", description="Say hi to someone!") +async def hi_slash( + ctx: arc.RESTContext, + user: arc.Option[hikari.User, arc.UserParams(description="The user to say hi to.")] +) -> None: + # ctx.loc can be used to request custom localizations that are not related to commands or options. + # Additional keyword arguments passed to the method will be passed to .format() + await ctx.respond(ctx.loc("say_hi", user=user.mention)) + +# Note: 'ctx.loc()' will by default request the guild's locale, outside of guilds it will request the user's locale. +# You can pass use_guild=False to always request the user's locale instead. + + +bot.run()