Source code for redbot.core.i18n

import contextlib
import functools
import io
import os
from pathlib import Path
from typing import Callable, Union, Dict, Optional

import babel.localedata
from babel.core import Locale

__all__ = [

_current_locale = "en-US"
_current_regional_format = None


MSGID = 'msgid "'
MSGSTR = 'msgstr "'

_translators = []

def get_locale() -> str:
    return _current_locale

def set_locale(locale: str) -> None:
    global _current_locale
    _current_locale = locale

def get_regional_format() -> str:
    if _current_regional_format is None:
        return _current_locale
    return _current_regional_format

def set_regional_format(regional_format: Optional[str]) -> None:
    global _current_regional_format
    _current_regional_format = regional_format

def reload_locales() -> None:
    for translator in _translators:

def _parse(translation_file: io.TextIOWrapper) -> Dict[str, str]:
    Custom gettext parsing of translation files.

    translation_file : io.TextIOWrapper
        An open text file containing translations.

    Dict[str, str]
        A dict mapping the original strings to their translations. Empty
        translated strings are omitted.

    step = None
    untranslated = ""
    translated = ""
    translations = {}
    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)
            step = IN_MSGID
            untranslated = line[len(MSGID) : -1]
        elif line.startswith('"') and line.endswith('"'):
            if step is IN_MSGID:
                # Line continuing on from msgid
                untranslated += line[1:-1]
            elif step is IN_MSGSTR:
                # Line continuing on from msgstr
                translated += line[1:-1]
        elif line.startswith(MSGSTR):
            # New msgstr
            step = IN_MSGSTR
            translated = line[len(MSGSTR) : -1]

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

def _unescape(string):
    string = string.replace(r"\\", "\\")
    string = string.replace(r"\t", "\t")
    string = string.replace(r"\r", "\r")
    string = string.replace(r"\n", "\n")
    string = string.replace(r"\"", '"')
    return string

def get_locale_path(cog_folder: Path, extension: str) -> Path:
    Gets the folder path containing localization files.

    :param Path cog_folder:
        The cog folder that we want localizations for.
    :param str extension:
        Extension of localization files.
        Path of possible localization file, it may not exist.
    return cog_folder / "locales" / "{}.{}".format(get_locale(), extension)

[docs]class Translator(Callable[[str], str]): """Function to get translated strings at runtime.""" def __init__(self, name: str, file_location: Union[str, Path, os.PathLike]): """ Initializes an internationalization object. Parameters ---------- name : str Your cog name. file_location : `str` or `pathlib.Path` This should always be ``__file__`` otherwise your localizations will not load. """ self.cog_folder = Path(file_location).resolve().parent self.cog_name = name self.translations = {} _translators.append(self) self.load_translations()
[docs] def __call__(self, untranslated: str) -> str: """Translate the given string. This will look for the string in the translator's :code:`.pot` file, with respect to the current locale. """ try: return self.translations[untranslated] except KeyError: return untranslated
[docs] def load_translations(self): """ Loads the current translations. """ self.translations = {} locale_path = get_locale_path(self.cog_folder, "po") with contextlib.suppress(IOError, FileNotFoundError): with"utf-8") as file: self._parse(file)
def _parse(self, translation_file): self.translations.update(_parse(translation_file)) def _add_translation(self, untranslated, translated): untranslated = _unescape(untranslated) translated = _unescape(translated) if translated: self.translations[untranslated] = translated
@functools.lru_cache() def _get_babel_locale(red_locale: str) -> babel.core.Locale: supported_locales = babel.localedata.locale_identifiers() try: # Handles cases where red_locale is already Babel supported babel_locale = Locale(*babel.parse_locale(red_locale)) except (ValueError, babel.core.UnknownLocaleError): try: babel_locale = Locale(*babel.parse_locale(red_locale, sep="-")) except (ValueError, babel.core.UnknownLocaleError): # ValueError is Raised by `parse_locale` when an invalid Locale is given to it # Lets handle it silently and default to "en_US" try: # Try to find a babel locale that's close to the one used by red babel_locale = Locale(Locale.negotiate([red_locale], supported_locales, sep="-")) except (ValueError, TypeError, babel.core.UnknownLocaleError): # If we fail to get a close match we will then default to "en_US" babel_locale = Locale("en", "US") return babel_locale
[docs]def get_babel_locale(locale: Optional[str] = None) -> babel.core.Locale: """Function to convert a locale to a `babel.core.Locale`. Parameters ---------- locale : Optional[str] The locale to convert, if not specified it defaults to the bot's locale. Returns ------- babel.core.Locale The babel locale object. """ if locale is None: locale = get_locale() return _get_babel_locale(locale)
[docs]def get_babel_regional_format(regional_format: Optional[str] = None) -> babel.core.Locale: """Function to convert a regional format to a `babel.core.Locale`. If ``regional_format`` parameter is passed, this behaves the same as `get_babel_locale`. Parameters ---------- regional_format : Optional[str] The regional format to convert, if not specified it defaults to the bot's regional format. Returns ------- babel.core.Locale The babel locale object. """ if regional_format is None: regional_format = get_regional_format() return _get_babel_locale(regional_format)
# This import to be down here to avoid circular import issues. # This will be cleaned up at a later date # noinspection PyPep8 from . import commands
[docs]def cog_i18n(translator: Translator): """Get a class decorator to link the translator to this cog.""" def decorator(cog_class: type): cog_class.__translator__ = translator for name, attr in cog_class.__dict__.items(): if isinstance(attr, (commands.Group, commands.Command)): attr.translator = translator setattr(cog_class, name, attr) return cog_class return decorator