diff --git a/.github/labeler.yml b/.github/labeler.yml index 1ad48337740..3fcdadadb6b 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -216,6 +216,7 @@ - redbot/core/commands/help.py "Category: Core - i18n": # Source + - redbot/core/_i18n.py - redbot/core/i18n.py # Locale files - redbot/**/locales/* diff --git a/redbot/core/_i18n.py b/redbot/core/_i18n.py new file mode 100644 index 00000000000..f2b5e7ec9de --- /dev/null +++ b/redbot/core/_i18n.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from contextvars import ContextVar +from typing import TYPE_CHECKING, List, Optional + +from babel.core import Locale, UnknownLocaleError + +if TYPE_CHECKING: + from redbot.core.i18n import Translator + + +__all__ = ( + "current_locale", + "current_locale_default", + "current_regional_format", + "current_regional_format_default", + "translators", + "set_global_locale", + "set_global_regional_format", + "set_contextual_locale", + "set_contextual_regional_format", +) + + +current_locale = ContextVar("current_locale") +current_locale_default = "en-US" +current_regional_format = ContextVar("current_regional_format") +current_regional_format_default = None + +translators: List[Translator] = [] + + +def _reload_locales() -> None: + for translator in translators: + translator.load_translations() + + +def _get_standardized_locale_name(language_code: str) -> str: + try: + locale = Locale.parse(language_code, sep="-") + except (ValueError, UnknownLocaleError): + raise ValueError("Invalid language code. Use format: `en-US`") + if locale.territory is None: + raise ValueError( + "Invalid format - language code has to include country code, e.g. `en-US`" + ) + return f"{locale.language}-{locale.territory}" + + +def set_global_locale(language_code: str, /) -> str: + global current_locale_default + current_locale_default = _get_standardized_locale_name(language_code) + _reload_locales() + return current_locale_default + + +def set_global_regional_format(language_code: Optional[str], /) -> Optional[str]: + global current_regional_format_default + if language_code is not None: + language_code = _get_standardized_locale_name(language_code) + current_regional_format_default = language_code + return language_code + + +def set_contextual_locale(language_code: str, /, verify_language_code: bool = False) -> str: + if verify_language_code: + language_code = _get_standardized_locale_name(language_code) + current_locale.set(language_code) + _reload_locales() + return language_code + + +def set_contextual_regional_format( + language_code: str, /, verify_language_code: bool = False +) -> str: + if verify_language_code and language_code is not None: + language_code = _get_standardized_locale_name(language_code) + current_regional_format.set(language_code) + return language_code diff --git a/redbot/core/bot.py b/redbot/core/bot.py index c63b2bb9584..a1d1048a2e8 100644 --- a/redbot/core/bot.py +++ b/redbot/core/bot.py @@ -37,7 +37,7 @@ from discord.ext import commands as dpy_commands from discord.ext.commands import when_mentioned_or -from . import Config, i18n, app_commands, commands, errors, _drivers, modlog, bank +from . import Config, _i18n, i18n, app_commands, commands, errors, _drivers, modlog, bank from ._cli import ExitCodes from ._cog_manager import CogManager, CogManagerUI from .core_commands import Core @@ -1145,9 +1145,9 @@ async def _pre_login(self) -> None: self.owner_ids.add(self._owner_id_overwrite) i18n_locale = await self._config.locale() - i18n.set_locale(i18n_locale) + _i18n.set_global_locale(i18n_locale) i18n_regional_format = await self._config.regional_format() - i18n.set_regional_format(i18n_regional_format) + _i18n.set_global_regional_format(i18n_regional_format) async def _pre_connect(self) -> None: """ diff --git a/redbot/core/core_commands.py b/redbot/core/core_commands.py index d3acfa3c8d9..93cacdeb840 100644 --- a/redbot/core/core_commands.py +++ b/redbot/core/core_commands.py @@ -38,7 +38,6 @@ import aiohttp import discord -from babel import Locale as BabelLocale, UnknownLocaleError from redbot.core.data_manager import storage_type from . import ( @@ -46,6 +45,7 @@ version_info as red_version_info, commands, errors, + _i18n, i18n, bank, modlog, @@ -3522,17 +3522,10 @@ async def _set_locale_global(self, ctx: commands.Context, language_code: str): - `` - The default locale to use for the bot. This can be any language code with country code included. """ try: - locale = BabelLocale.parse(language_code, sep="-") - except (ValueError, UnknownLocaleError): + standardized_locale_name = _i18n.set_global_locale(language_code) + except ValueError: await ctx.send(_("Invalid language code. Use format: `en-US`")) return - if locale.territory is None: - await ctx.send( - _("Invalid format - language code has to include country code, e.g. `en-US`") - ) - return - standardized_locale_name = f"{locale.language}-{locale.territory}" - i18n.set_locale(standardized_locale_name) await self.bot._i18n_cache.set_locale(None, standardized_locale_name) await i18n.set_contextual_locales_from_guild(self.bot, ctx.guild) await ctx.send(_("Global locale has been set.")) @@ -3565,17 +3558,10 @@ async def _set_locale_local(self, ctx: commands.Context, language_code: str): await ctx.send(_("Locale has been set to the default.")) return try: - locale = BabelLocale.parse(language_code, sep="-") - except (ValueError, UnknownLocaleError): + standardized_locale_name = i18n.set_contextual_locale(language_code) + except ValueError: await ctx.send(_("Invalid language code. Use format: `en-US`")) return - if locale.territory is None: - await ctx.send( - _("Invalid format - language code has to include country code, e.g. `en-US`") - ) - return - standardized_locale_name = f"{locale.language}-{locale.territory}" - i18n.set_contextual_locale(standardized_locale_name) await self.bot._i18n_cache.set_locale(ctx.guild, standardized_locale_name) await ctx.send(_("Locale has been set.")) @@ -3621,23 +3607,16 @@ async def _set_regional_format_global(self, ctx: commands.Context, language_code - `[language_code]` - The default region format to use for the bot. """ if language_code.lower() == "reset": - i18n.set_regional_format(None) + _i18n.set_global_regional_format(None) await self.bot._i18n_cache.set_regional_format(None, None) await ctx.send(_("Global regional formatting will now be based on bot's locale.")) return try: - locale = BabelLocale.parse(language_code, sep="-") - except (ValueError, UnknownLocaleError): + standardized_locale_name = _i18n.set_global_regional_format(language_code) + except ValueError: await ctx.send(_("Invalid language code. Use format: `en-US`")) return - if locale.territory is None: - await ctx.send( - _("Invalid format - language code has to include country code, e.g. `en-US`") - ) - return - standardized_locale_name = f"{locale.language}-{locale.territory}" - i18n.set_regional_format(standardized_locale_name) await self.bot._i18n_cache.set_regional_format(None, standardized_locale_name) await ctx.send( _("Global regional formatting will now be based on `{language_code}` locale.").format( @@ -3672,17 +3651,10 @@ async def _set_regional_format_local(self, ctx: commands.Context, language_code: return try: - locale = BabelLocale.parse(language_code, sep="-") - except (ValueError, UnknownLocaleError): + standardized_locale_name = i18n.set_contextual_regional_format(language_code) + except ValueError: await ctx.send(_("Invalid language code. Use format: `en-US`")) return - if locale.territory is None: - await ctx.send( - _("Invalid format - language code has to include country code, e.g. `en-US`") - ) - return - standardized_locale_name = f"{locale.language}-{locale.territory}" - i18n.set_contextual_regional_format(standardized_locale_name) await self.bot._i18n_cache.set_regional_format(ctx.guild, standardized_locale_name) await ctx.send( _("Regional formatting will now be based on `{language_code}` locale.").format( diff --git a/redbot/core/i18n.py b/redbot/core/i18n.py index b3eebb689c4..7b793a95692 100644 --- a/redbot/core/i18n.py +++ b/redbot/core/i18n.py @@ -9,18 +9,27 @@ from pathlib import Path from typing import Callable, TYPE_CHECKING, Union, Dict, Optional, TypeVar -from contextvars import ContextVar import babel.localedata from babel.core import Locale +from redbot.core import _i18n +from redbot.core._i18n import ( + current_locale as _current_locale, + current_regional_format as _current_regional_format, + set_contextual_locale as _set_contextual_locale, + set_contextual_regional_format as _set_contextual_regional_format, +) + if TYPE_CHECKING: from redbot.core.bot import Red -__all__ = [ +__all__ = ( "get_locale", "get_regional_format", + "set_contextual_locale", + "set_contextual_regional_format", "get_locale_from_guild", "get_regional_format_from_guild", "set_contextual_locales_from_guild", @@ -28,13 +37,10 @@ "get_babel_locale", "get_babel_regional_format", "cog_i18n", -] +) log = logging.getLogger("red.i18n") -_current_locale = ContextVar("_current_locale", default="en-US") -_current_regional_format = ContextVar("_current_regional_format", default=None) - WAITING_FOR_MSGID = 1 IN_MSGID = 2 WAITING_FOR_MSGSTR = 3 @@ -43,8 +49,6 @@ MSGID = 'msgid "' MSGSTR = 'msgstr "' -_translators = [] - def get_locale() -> str: """ @@ -55,18 +59,7 @@ def get_locale() -> str: str Current locale's language code with country code included, e.g. "en-US". """ - return str(_current_locale.get()) - - -def set_locale(locale: str) -> None: - global _current_locale - _current_locale = ContextVar("_current_locale", default=locale) - reload_locales() - - -def set_contextual_locale(locale: str) -> None: - _current_locale.set(locale) - reload_locales() + return _current_locale.get(_i18n.current_locale_default) def get_regional_format() -> str: @@ -78,23 +71,55 @@ def get_regional_format() -> str: str Current regional format's language code with country code included, e.g. "en-US". """ - if _current_regional_format.get() is None: - return str(_current_locale.get()) - return str(_current_regional_format.get()) + regional_format = _current_regional_format.get(_i18n.current_regional_format_default) + if regional_format is None: + return _current_locale.get(_i18n.current_locale_default) + return regional_format + + +def set_contextual_locale(language_code: str, /) -> str: + """ + Set contextual locale (without regional format) to the given value. + + Parameters + ---------- + language_code: str + Locale's language code with country code included, e.g. "en-US". + Returns + ------- + str + Standardized locale name. -def set_regional_format(regional_format: Optional[str]) -> None: - global _current_regional_format - _current_regional_format = ContextVar("_current_regional_format", default=regional_format) + Raises + ------ + ValueError + Language code is invalid. + """ + return _set_contextual_locale(language_code, verify_language_code=True) -def set_contextual_regional_format(regional_format: Optional[str]) -> None: - _current_regional_format.set(regional_format) +def set_contextual_regional_format(language_code: Optional[str], /) -> Optional[str]: + """ + Set contextual regional format to the given value. + Parameters + ---------- + language_code: str, optional + Contextual regional's language code with country code included, e.g. "en-US" + or ``None`` if regional format should inherit the contextual locale's value. + + Returns + ------- + str + Standardized locale name or ``None`` if ``None`` was passed. -def reload_locales() -> None: - for translator in _translators: - translator.load_translations() + Raises + ------ + ValueError + Language code is invalid. + """ + return _set_contextual_regional_format(language_code, verify_language_code=True) async def get_locale_from_guild(bot: Red, guild: Optional[discord.Guild]) -> str: @@ -151,8 +176,8 @@ async def set_contextual_locales_from_guild(bot: Red, guild: Optional[discord.Gu """ locale = await get_locale_from_guild(bot, guild) regional_format = await get_regional_format_from_guild(bot, guild) - set_contextual_locale(locale) - set_contextual_regional_format(regional_format) + _set_contextual_locale(locale) + _set_contextual_regional_format(regional_format) def _parse(translation_file: io.TextIOWrapper) -> Dict[str, str]: @@ -216,7 +241,7 @@ def _unescape(string): return string -def get_locale_path(cog_folder: Path, extension: str) -> Path: +def _get_locale_path(cog_folder: Path, extension: str) -> Path: """ Gets the folder path containing localization files. @@ -250,7 +275,7 @@ def __init__(self, name: str, file_location: Union[str, Path, os.PathLike]): self.cog_name = name self.translations = {} - _translators.append(self) + _i18n.translators.append(self) self.load_translations() @@ -280,7 +305,7 @@ def load_translations(self): # self.translations return - locale_path = get_locale_path(self.cog_folder, "po") + locale_path = _get_locale_path(self.cog_folder, "po") with contextlib.suppress(IOError, FileNotFoundError): with locale_path.open(encoding="utf-8") as file: self._parse(file)