Split public and private i18n APIs (#6022)

This commit is contained in:
Jakub Kuczys 2025-01-27 01:33:06 +01:00 committed by GitHub
parent 8b1daf1ad0
commit a0c1713e78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 154 additions and 77 deletions

1
.github/labeler.yml vendored
View File

@ -216,6 +216,7 @@
- redbot/core/commands/help.py - redbot/core/commands/help.py
"Category: Core - i18n": "Category: Core - i18n":
# Source # Source
- redbot/core/_i18n.py
- redbot/core/i18n.py - redbot/core/i18n.py
# Locale files # Locale files
- redbot/**/locales/* - redbot/**/locales/*

79
redbot/core/_i18n.py Normal file
View File

@ -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

View File

@ -37,7 +37,7 @@ import discord
from discord.ext import commands as dpy_commands from discord.ext import commands as dpy_commands
from discord.ext.commands import when_mentioned_or 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 ._cli import ExitCodes
from ._cog_manager import CogManager, CogManagerUI from ._cog_manager import CogManager, CogManagerUI
from .core_commands import Core from .core_commands import Core
@ -1145,9 +1145,9 @@ class Red(
self.owner_ids.add(self._owner_id_overwrite) self.owner_ids.add(self._owner_id_overwrite)
i18n_locale = await self._config.locale() 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_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: async def _pre_connect(self) -> None:
""" """

View File

@ -38,7 +38,6 @@ from typing import (
import aiohttp import aiohttp
import discord import discord
from babel import Locale as BabelLocale, UnknownLocaleError
from redbot.core.data_manager import storage_type from redbot.core.data_manager import storage_type
from . import ( from . import (
@ -46,6 +45,7 @@ from . import (
version_info as red_version_info, version_info as red_version_info,
commands, commands,
errors, errors,
_i18n,
i18n, i18n,
bank, bank,
modlog, modlog,
@ -3522,17 +3522,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
- `<language_code>` - The default locale to use for the bot. This can be any language code with country code included. - `<language_code>` - The default locale to use for the bot. This can be any language code with country code included.
""" """
try: try:
locale = BabelLocale.parse(language_code, sep="-") standardized_locale_name = _i18n.set_global_locale(language_code)
except (ValueError, UnknownLocaleError): except ValueError:
await ctx.send(_("Invalid language code. Use format: `en-US`")) await ctx.send(_("Invalid language code. Use format: `en-US`"))
return 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 self.bot._i18n_cache.set_locale(None, standardized_locale_name)
await i18n.set_contextual_locales_from_guild(self.bot, ctx.guild) await i18n.set_contextual_locales_from_guild(self.bot, ctx.guild)
await ctx.send(_("Global locale has been set.")) await ctx.send(_("Global locale has been set."))
@ -3565,17 +3558,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
await ctx.send(_("Locale has been set to the default.")) await ctx.send(_("Locale has been set to the default."))
return return
try: try:
locale = BabelLocale.parse(language_code, sep="-") standardized_locale_name = i18n.set_contextual_locale(language_code)
except (ValueError, UnknownLocaleError): except ValueError:
await ctx.send(_("Invalid language code. Use format: `en-US`")) await ctx.send(_("Invalid language code. Use format: `en-US`"))
return 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 self.bot._i18n_cache.set_locale(ctx.guild, standardized_locale_name)
await ctx.send(_("Locale has been set.")) await ctx.send(_("Locale has been set."))
@ -3621,23 +3607,16 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
- `[language_code]` - The default region format to use for the bot. - `[language_code]` - The default region format to use for the bot.
""" """
if language_code.lower() == "reset": 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 self.bot._i18n_cache.set_regional_format(None, None)
await ctx.send(_("Global regional formatting will now be based on bot's locale.")) await ctx.send(_("Global regional formatting will now be based on bot's locale."))
return return
try: try:
locale = BabelLocale.parse(language_code, sep="-") standardized_locale_name = _i18n.set_global_regional_format(language_code)
except (ValueError, UnknownLocaleError): except ValueError:
await ctx.send(_("Invalid language code. Use format: `en-US`")) await ctx.send(_("Invalid language code. Use format: `en-US`"))
return 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 self.bot._i18n_cache.set_regional_format(None, standardized_locale_name)
await ctx.send( await ctx.send(
_("Global regional formatting will now be based on `{language_code}` locale.").format( _("Global regional formatting will now be based on `{language_code}` locale.").format(
@ -3672,17 +3651,10 @@ class Core(commands.commands._RuleDropper, commands.Cog, CoreLogic):
return return
try: try:
locale = BabelLocale.parse(language_code, sep="-") standardized_locale_name = i18n.set_contextual_regional_format(language_code)
except (ValueError, UnknownLocaleError): except ValueError:
await ctx.send(_("Invalid language code. Use format: `en-US`")) await ctx.send(_("Invalid language code. Use format: `en-US`"))
return 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 self.bot._i18n_cache.set_regional_format(ctx.guild, standardized_locale_name)
await ctx.send( await ctx.send(
_("Regional formatting will now be based on `{language_code}` locale.").format( _("Regional formatting will now be based on `{language_code}` locale.").format(

View File

@ -9,18 +9,27 @@ import discord
from pathlib import Path from pathlib import Path
from typing import Callable, TYPE_CHECKING, Union, Dict, Optional, TypeVar from typing import Callable, TYPE_CHECKING, Union, Dict, Optional, TypeVar
from contextvars import ContextVar
import babel.localedata import babel.localedata
from babel.core import Locale 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: if TYPE_CHECKING:
from redbot.core.bot import Red from redbot.core.bot import Red
__all__ = [ __all__ = (
"get_locale", "get_locale",
"get_regional_format", "get_regional_format",
"set_contextual_locale",
"set_contextual_regional_format",
"get_locale_from_guild", "get_locale_from_guild",
"get_regional_format_from_guild", "get_regional_format_from_guild",
"set_contextual_locales_from_guild", "set_contextual_locales_from_guild",
@ -28,13 +37,10 @@ __all__ = [
"get_babel_locale", "get_babel_locale",
"get_babel_regional_format", "get_babel_regional_format",
"cog_i18n", "cog_i18n",
] )
log = logging.getLogger("red.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 WAITING_FOR_MSGID = 1
IN_MSGID = 2 IN_MSGID = 2
WAITING_FOR_MSGSTR = 3 WAITING_FOR_MSGSTR = 3
@ -43,8 +49,6 @@ IN_MSGSTR = 4
MSGID = 'msgid "' MSGID = 'msgid "'
MSGSTR = 'msgstr "' MSGSTR = 'msgstr "'
_translators = []
def get_locale() -> str: def get_locale() -> str:
""" """
@ -55,18 +59,7 @@ def get_locale() -> str:
str str
Current locale's language code with country code included, e.g. "en-US". Current locale's language code with country code included, e.g. "en-US".
""" """
return str(_current_locale.get()) return _current_locale.get(_i18n.current_locale_default)
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()
def get_regional_format() -> str: def get_regional_format() -> str:
@ -78,23 +71,55 @@ def get_regional_format() -> str:
str str
Current regional format's language code with country code included, e.g. "en-US". Current regional format's language code with country code included, e.g. "en-US".
""" """
if _current_regional_format.get() is None: regional_format = _current_regional_format.get(_i18n.current_regional_format_default)
return str(_current_locale.get()) if regional_format is None:
return str(_current_regional_format.get()) return _current_locale.get(_i18n.current_locale_default)
return regional_format
def set_regional_format(regional_format: Optional[str]) -> None: def set_contextual_locale(language_code: str, /) -> str:
global _current_regional_format """
_current_regional_format = ContextVar("_current_regional_format", default=regional_format) 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.
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: def set_contextual_regional_format(language_code: Optional[str], /) -> Optional[str]:
_current_regional_format.set(regional_format) """
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.
def reload_locales() -> None: Returns
for translator in _translators: -------
translator.load_translations() str
Standardized locale name or ``None`` if ``None`` was passed.
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: 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) locale = await get_locale_from_guild(bot, guild)
regional_format = await get_regional_format_from_guild(bot, guild) regional_format = await get_regional_format_from_guild(bot, guild)
set_contextual_locale(locale) _set_contextual_locale(locale)
set_contextual_regional_format(regional_format) _set_contextual_regional_format(regional_format)
def _parse(translation_file: io.TextIOWrapper) -> Dict[str, str]: def _parse(translation_file: io.TextIOWrapper) -> Dict[str, str]:
@ -216,7 +241,7 @@ def _unescape(string):
return 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. Gets the folder path containing localization files.
@ -250,7 +275,7 @@ class Translator(Callable[[str], str]):
self.cog_name = name self.cog_name = name
self.translations = {} self.translations = {}
_translators.append(self) _i18n.translators.append(self)
self.load_translations() self.load_translations()
@ -280,7 +305,7 @@ class Translator(Callable[[str], str]):
# self.translations # self.translations
return 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 contextlib.suppress(IOError, FileNotFoundError):
with locale_path.open(encoding="utf-8") as file: with locale_path.open(encoding="utf-8") as file:
self._parse(file) self._parse(file)