Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Core] Guild scoped I18n #3896

Merged
merged 36 commits into from Oct 26, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1e38984
Guild I18n
Kowlin May 29, 2020
2210270
Finish off guild scoped i18n
Kowlin Jun 2, 2020
77a5aee
Black formatting
Kowlin Jun 2, 2020
68afba7
Added guild only flags.
Kowlin Aug 30, 2020
761b0de
Merge remote-tracking branch 'origin/V3/develop' into pr/guild-i18n
Kowlin Aug 30, 2020
611fea7
Fix missing import.
Kowlin Aug 30, 2020
ed1af2f
Added listing of guild i18n settings
Kowlin Aug 30, 2020
73b9820
Added API support
Kowlin Aug 30, 2020
f570b01
Added API support... properly!
Kowlin Aug 30, 2020
c2f0b3e
Added API support... for realsies!
Kowlin Aug 31, 2020
33e0ec6
Auto-translate create_cases instances
Kowlin Aug 31, 2020
f6c424b
Fix get_regional_format to actually return properly
Kowlin Aug 31, 2020
9f07ae0
Cleanup `set showsettings`
Kowlin Aug 31, 2020
1984887
Style pass
Kowlin Aug 31, 2020
612ca79
Update redbot/core/core_commands.py
Kowlin Sep 20, 2020
5bc2aa2
Update redbot/core/events.py
Kowlin Sep 20, 2020
e1666e0
Update redbot/core/core_commands.py
Kowlin Sep 20, 2020
d05566a
Fix missing import
Kowlin Sep 20, 2020
7a56711
Improve caching
Kowlin Sep 20, 2020
807dd4e
Removal of unneeded function
Kowlin Sep 20, 2020
a4fe1d0
Fix some naming
Kowlin Sep 20, 2020
36df48a
IDFK anymore...
Kowlin Sep 20, 2020
14c9d86
Reformat
Kowlin Sep 20, 2020
c7507e9
Update redbot/core/settings_caches.py
Kowlin Sep 23, 2020
5955b90
Update redbot/core/settings_caches.py
Kowlin Sep 23, 2020
7971759
Update redbot/core/settings_caches.py
Kowlin Sep 23, 2020
d4c8d8a
Remove line number
Kowlin Sep 23, 2020
fd915bc
Fix global sets
Jackenmen Oct 13, 2020
bbb42d6
Set contextual locale manually where needed
Jackenmen Oct 13, 2020
35f0378
Reports cog is wonderful...
Jackenmen Oct 15, 2020
836121d
Merge branch 'V3/develop' into pr/guild-i18n
Jackenmen Oct 19, 2020
d106a5f
Update redbot/core/core_commands.py
Jackenmen Oct 19, 2020
e304619
Merge remote-tracking branch 'upstream/V3/develop' into pr/guild-i18n
Drapersniper Oct 23, 2020
d976dac
Merge branch 'V3/develop' into pr/guild-i18n
Jackenmen Oct 26, 2020
a1af6b3
Set contextual locale manually where needed in Mutes cog
Jackenmen Oct 26, 2020
d835929
s
Jackenmen Oct 26, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion redbot/cogs/alias/alias.py
Expand Up @@ -83,7 +83,7 @@ async def get_prefix(self, message: discord.Message) -> str:
for p in prefixes:
if content.startswith(p):
return p
raise ValueError(_("No prefix found."))
raise ValueError("No prefix found.")

async def call_alias(self, message: discord.Message, prefix: str, alias: AliasEntry):
new_message = copy(message)
Expand Down
5 changes: 4 additions & 1 deletion redbot/core/bot.py
Expand Up @@ -40,7 +40,7 @@
from .events import init_events
from .global_checks import init_global_checks

from .settings_caches import PrefixManager, IgnoreManager, WhitelistBlacklistManager
from .settings_caches import PrefixManager, IgnoreManager, WhitelistBlacklistManager, I18nManager

from .rpc import RPCMixin
from .utils import common_filters
Expand Down Expand Up @@ -131,6 +131,8 @@ def __init__(self, *args, cli_flags=None, bot_dir: Path = Path.cwd(), **kwargs):
disabled_commands=[],
autoimmune_ids=[],
delete_delay=-1,
locale=None,
regional_format=None
)

self._config.register_channel(embeds=None, ignored=False)
Expand All @@ -144,6 +146,7 @@ def __init__(self, *args, cli_flags=None, bot_dir: Path = Path.cwd(), **kwargs):
self._prefix_cache = PrefixManager(self._config, cli_flags)
self._ignored_cache = IgnoreManager(self._config)
self._whiteblacklist_cache = WhitelistBlacklistManager(self._config)
self._i18n_cache = I18nManager(self._config)

async def prefix_manager(bot, message) -> List[str]:
prefixes = await self._prefix_cache.get_prefixes(message.guild)
Expand Down
40 changes: 37 additions & 3 deletions redbot/core/core_commands.py
Expand Up @@ -1302,7 +1302,7 @@ async def serverprefix(self, ctx: commands.Context, *prefixes: str):

@_set.command()
@checks.is_owner()
async def locale(self, ctx: commands.Context, language_code: str):
async def globallocale(self, ctx: commands.Context, language_code: str):
"""
Changes bot's locale.
Kowlin marked this conversation as resolved.
Show resolved Hide resolved

Expand All @@ -1327,11 +1327,44 @@ async def locale(self, ctx: commands.Context, language_code: str):
standardized_locale_name = f"{locale.language}-{locale.territory}"
i18n.set_locale(standardized_locale_name)
await ctx.bot._config.locale.set(standardized_locale_name)
await ctx.send(_("Global locale has been set."))

@_set.command()
@checks.guildowner_or_permissions(manage_guild=True)
async def locale(self, ctx: commands.Context, language_code: str):
Kowlin marked this conversation as resolved.
Show resolved Hide resolved
"""
Changes the bot's locale in this server.

`<language_code>` can be any language code with country code included,
e.g. `en-US`, `de-DE`, `fr-FR`, `pl-PL`, etc.

Go to Red's Crowdin page to see locales that are available with translations:
https://translate.discord.red

Use "default" to return to the bot's default set language.
To reset to English, use "en-US".
"""
if language_code.lower() == "default":
await self.bot._i18n_cache.set_guild_locale(ctx.guild, None)
await ctx.send(_("Locale has been set to the default."))
return
try:
locale = BabelLocale.parse(language_code, sep="-")
except (ValueError, UnknownLocaleError):
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}"
await self.bot._i18n_cache.set_guild_locale(ctx.guild, standardized_locale_name)
await ctx.send(_("Locale has been set."))

@_set.command(aliases=["region"])
@_set.command(aliases=["globalregion"])
@checks.is_owner()
async def regionalformat(self, ctx: commands.Context, language_code: str = None):
async def globalregionalformat(self, ctx: commands.Context, language_code: str = None):
"""
Changes bot's regional format. This is used for formatting date, time and numbers.

Expand Down Expand Up @@ -1364,6 +1397,7 @@ async def regionalformat(self, ctx: commands.Context, language_code: str = None)
language_code=standardized_locale_name
)
)
# TODO Regional formating i18n bullcrap what was its name again. IDFK

@_set.command()
@checks.is_owner()
Expand Down
5 changes: 4 additions & 1 deletion redbot/core/events.py
Expand Up @@ -15,7 +15,7 @@
from pkg_resources import DistributionNotFound

from redbot.core.commands import RedHelpFormatter, HelpSettings
from redbot.core.i18n import Translator
from redbot.core.i18n import Translator, set_locale, set_context_locale
from .utils import AsyncIter
from .. import __version__ as red_version, version_info as red_version_info, VersionInfo
from . import commands
Expand Down Expand Up @@ -294,6 +294,9 @@ async def on_command_error(ctx, error, unhandled_by_cog=False):

@bot.event
async def on_message(message):
locale = await bot._i18n_cache.get_locale(message.guild)
set_context_locale(locale)

await bot.process_commands(message)
discord_now = message.created_at
if (
Expand Down
47 changes: 40 additions & 7 deletions redbot/core/i18n.py
Expand Up @@ -2,8 +2,11 @@
import functools
import io
import os
import logging

from pathlib import Path
from typing import Callable, Union, Dict, Optional
from contextvars import ContextVar

import babel.localedata
from babel.core import Locale
Expand All @@ -17,7 +20,9 @@
"get_babel_locale",
]

_current_locale = "en-US"
log = logging.getLogger("red.i18n")

_current_locale = ContextVar("_current_locale", default="en-US")
_current_regional_format = None

WAITING_FOR_MSGID = 1
Expand All @@ -32,12 +37,17 @@


def get_locale() -> str:
return _current_locale
return str(_current_locale.get())


def set_locale(locale: str) -> None:
global _current_locale
_current_locale = locale
_current_locale = ContextVar("_current_locale", default=locale)
reload_locales()


def set_context_locale(locale: str) -> None:
_current_locale.set(locale)
reload_locales()


Expand Down Expand Up @@ -77,14 +87,18 @@ def _parse(translation_file: io.TextIOWrapper) -> Dict[str, str]:
untranslated = ""
translated = ""
translations = {}
locale = get_locale()

translations[locale] = {}

for line in translation_file:
line = line.strip()

if line.startswith(MSGID):
# New msgid
if step is IN_MSGSTR and translated:
# Store the last translation
translations[_unescape(untranslated)] = _unescape(translated)
translations[locale][_unescape(untranslated)] = _unescape(translated)
step = IN_MSGID
untranslated = line[len(MSGID) : -1]
elif line.startswith('"') and line.endswith('"'):
Expand All @@ -101,7 +115,7 @@ def _parse(translation_file: io.TextIOWrapper) -> Dict[str, str]:

if step is IN_MSGSTR and translated:
# Store the final translation
translations[_unescape(untranslated)] = _unescape(translated)
translations[locale][_unescape(untranslated)] = _unescape(translated)
return translations


Expand Down Expand Up @@ -147,6 +161,7 @@ def __init__(self, name: str, file_location: Union[str, Path, os.PathLike]):
self.cog_folder = Path(file_location).resolve().parent
self.cog_name = name
self.translations = {}
self.assume_loaded_locale = []

_translators.append(self)

Expand All @@ -158,20 +173,38 @@ def __call__(self, untranslated: str) -> str:
This will look for the string in the translator's :code:`.pot` file,
with respect to the current locale.
"""
locale = get_locale()
try:
return self.translations[untranslated]
return self.translations[locale][untranslated]
except KeyError:
log.debug(f"missed hit for {locale}/{untranslated[:10]}")
return untranslated

def check_cache_for_locale(self, locale: str) -> bool:
"""Check is an locale exists with in the cache"""
if locale in self.translations:
return True
return False

Kowlin marked this conversation as resolved.
Show resolved Hide resolved
def load_translations(self):
"""
Loads the current translations.
"""
self.translations = {}
locale = get_locale()

if locale.lower() == "en-us":
# Red is written in en-US, no point in loading it
return
if locale in self.translations:
return
if locale in self.assume_loaded_locale:
return

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)
self.assume_loaded_locale.append(locale)

def _parse(self, translation_file):
self.translations.update(_parse(translation_file))
Expand Down
26 changes: 26 additions & 0 deletions redbot/core/settings_caches.py
Expand Up @@ -53,6 +53,32 @@ async def set_prefixes(
await self._config.guild_from_id(gid).prefix.set(prefixes)


class I18nManager:
def __init__(self, config: Config):
self._config: Config = config
self._guild_locale: Dict[int, str] = {}

async def get_locale(self, guild: Union[discord.Guild, None]) -> Optional[str]:
"""Get the guild locale from the cache"""
if guild is None:
# Return the bot's globally set locale if its None on a guild scope.
out = await self._config.locale()
elif guild.id in self._guild_locale:
out = self._guild_locale[guild.id]
else:
out = await self._config.guild(guild).locale()
if out is None:
out = await self._config.locale()
self._guild_locale[guild.id] = out

return out

async def set_guild_locale(self, guild: discord.Guild, locale: Union[str, None]):
"""Set the guild locale in the cache"""
self._guild_locale[guild.id] = locale
await self._config.guild(guild).locale.set(locale)


class IgnoreManager:
def __init__(self, config: Config):
self._config: Config = config
Expand Down