import gettext import os from contextvars import ContextVar from typing import Any, Dict, Tuple, Optional from babel import Locale from babel.support import LazyProxy from ... import types from ...dispatcher.middlewares import BaseMiddleware class I18nMiddleware(BaseMiddleware): """ I18n middleware based on gettext util >>> dp = Dispatcher(bot) >>> i18n = I18nMiddleware(DOMAIN, LOCALES_DIR) >>> dp.middleware.setup(i18n) and then >>> _ = i18n.gettext or >>> _ = i18n = I18nMiddleware(DOMAIN_NAME, LOCALES_DIR) """ ctx_locale = ContextVar('ctx_user_locale', default=None) def __init__(self, domain, path=None, default='en'): """ :param domain: domain :param path: path where located all *.mo files :param default: default locale name """ super(I18nMiddleware, self).__init__() if path is None: path = os.path.join(os.getcwd(), 'locales') self.domain = domain self.path = path self.default = default self.locales = self.find_locales() def find_locales(self) -> Dict[str, gettext.GNUTranslations]: """ Load all compiled locales from path :return: dict with locales """ translations = {} for name in os.listdir(self.path): if not os.path.isdir(os.path.join(self.path, name)): continue mo_path = os.path.join(self.path, name, 'LC_MESSAGES', self.domain + '.mo') if os.path.exists(mo_path): with open(mo_path, 'rb') as fp: translations[name] = gettext.GNUTranslations(fp) elif os.path.exists(mo_path[:-2] + 'po'): raise RuntimeError(f"Found locale '{name}' but this language is not compiled!") return translations def reload(self): """ Hot reload locales """ self.locales = self.find_locales() @property def available_locales(self) -> Tuple[str]: """ list of loaded locales :return: """ return tuple(self.locales.keys()) def __call__(self, singular, plural=None, n=1, locale=None) -> str: return self.gettext(singular, plural, n, locale) def gettext(self, singular, plural=None, n=1, locale=None) -> str: """ Get text :param singular: :param plural: :param n: :param locale: :return: """ if locale is None: locale = self.ctx_locale.get() if locale not in self.locales: if n == 1: return singular return plural translator = self.locales[locale] if plural is None: return translator.gettext(singular) return translator.ngettext(singular, plural, n) def lazy_gettext(self, singular, plural=None, n=1, locale=None, enable_cache=False) -> LazyProxy: """ Lazy get text :param singular: :param plural: :param n: :param locale: :param enable_cache: :return: """ return LazyProxy(self.gettext, singular, plural, n, locale, enable_cache=enable_cache) # noinspection PyMethodMayBeStatic,PyUnusedLocal async def get_user_locale(self, action: str, args: Tuple[Any]) -> Optional[str]: """ User locale getter You can override the method if you want to use different way of getting user language. :param action: event name :param args: event arguments :return: locale name or None """ user: Optional[types.User] = types.User.get_current() locale: Optional[Locale] = user.locale if user else None if locale and locale.language in self.locales: *_, data = args language = data['locale'] = locale.language return language return self.default async def trigger(self, action, args): """ Event trigger :param action: event name :param args: event arguments :return: """ if 'update' not in action \ and 'error' not in action \ and action.startswith('pre_process'): locale = await self.get_user_locale(action, args) self.ctx_locale.set(locale) return True