Server : Apache System : Linux iad1-shared-b8-43 6.6.49-grsec-jammy+ #10 SMP Thu Sep 12 23:23:08 UTC 2024 x86_64 User : dh_edsupp ( 6597262) PHP Version : 8.2.26 Disable Function : NONE Directory : /lib/python3/dist-packages/trac/util/ |
Upload File : |
# -*- coding: utf-8 -*- # # Copyright (C) 2007-2021 Edgewall Software # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at https://trac.edgewall.org/wiki/TracLicense. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at https://trac.edgewall.org/log/. """Utilities for text translation with gettext.""" import pkg_resources import re from trac.util.concurrency import ThreadLocal, threading from trac.util.html import tag from trac.util.text import cleandoc __all__ = ['gettext', 'ngettext', 'gettext_noop', 'ngettext_noop', 'tgettext', 'tgettext_noop', 'tngettext', 'tngettext_noop'] def safefmt(string, kwargs): if kwargs: try: return string % kwargs except KeyError: pass return string def gettext_noop(string, **kwargs): return safefmt(string, kwargs) def dgettext_noop(domain, string, **kwargs): return gettext_noop(string, **kwargs) N_ = _noop = lambda string: string cleandoc_ = cleandoc def ngettext_noop(singular, plural, num, **kwargs): string = singular if num == 1 else plural kwargs.setdefault('num', num) return safefmt(string, kwargs) def dngettext_noop(domain, singular, plural, num, **kwargs): return ngettext_noop(singular, plural, num, **kwargs) _param_re = re.compile(r"%\((\w+)\)(?:s|[\d]*d|\d*.?\d*[fg])") def _tag_kwargs(trans, kwargs): trans_elts = _param_re.split(trans) for i in range(1, len(trans_elts), 2): trans_elts[i] = kwargs.get(trans_elts[i], '???') return tag(*trans_elts) def tgettext_noop(string, **kwargs): return _tag_kwargs(string, kwargs) if kwargs else string def dtgettext_noop(domain, string, **kwargs): return tgettext_noop(string, **kwargs) def tngettext_noop(singular, plural, num, **kwargs): string = singular if num == 1 else plural kwargs.setdefault('num', num) return _tag_kwargs(string, kwargs) def dtngettext_noop(domain, singular, plural, num, **kwargs): return tngettext_noop(singular, plural, num, **kwargs) def add_domain(domain, env_path, locale_dir): pass def domain_functions(domain, *symbols): if symbols and not isinstance(symbols[0], str): symbols = symbols[0] _functions = { 'gettext': s_gettext, '_': gettext_noop, 'N_': _noop, 'ngettext': ngettext_noop, 'tgettext': tgettext_noop, 'tag_': tgettext_noop, 'tngettext': tngettext_noop, 'tagn_': tngettext_noop, 'add_domain': lambda env_path, locale_dir: None, } return [_functions[s] for s in symbols] from gettext import NullTranslations class NullTranslationsBabel(NullTranslations): """NullTranslations doesn't have the domain related methods.""" def dugettext(self, domain, string): return self.gettext(string) def dungettext(self, domain, singular, plural, num): return self.ngettext(singular, plural, num) has_babel = False try: from babel import Locale, UnknownLocaleError from babel.support import LazyProxy, Translations class TranslationsProxy(object): """Delegate Translations calls to the currently active Translations. If there's none, wrap those calls in LazyProxy objects. Activation is controlled by `activate` and `deactivate` methods. However, if retrieving the locale information is costly, it's also possible to enable activation on demand only, by providing a callable to `make_activable`. """ def __init__(self): self._current = ThreadLocal(args=None, translations=None) self._null_translations = NullTranslationsBabel() self._plugin_domains = {} self._plugin_domains_lock = threading.RLock() self._activate_failed = False # Public API def add_domain(self, domain, env_path, locales_dir): with self._plugin_domains_lock: domains = self._plugin_domains.setdefault(env_path, {}) domains[domain] = locales_dir def make_activable(self, get_locale, env_path=None): self._current.args = (get_locale, env_path) def activate(self, locale, env_path=None): try: locale_dir = pkg_resources.resource_filename('trac', 'locale') except Exception: self._activate_failed = True return t = Translations.load(locale_dir, locale or 'en_US') if not isinstance(t, Translations): t = self._null_translations else: self._add(t, Translations.load(locale_dir, locale or 'en_US', 'tracini')) if env_path: with self._plugin_domains_lock: domains = self._plugin_domains.get(env_path, {}) domains = list(domains.items()) for domain, dirname in domains: self._add(t, Translations.load(dirname, locale, domain)) self._current.translations = t self._activate_failed = False def deactivate(self): self._current.args = None t, self._current.translations = self._current.translations, None return t def reactivate(self, t): if t: self._current.translations = t @property def active(self): return self._current.translations or self._null_translations @property def isactive(self): if self._current.args is not None: get_locale, env_path = self._current.args self._current.args = None self.activate(get_locale(), env_path) # FIXME: The following always returns True: either a translation is # active, or activation has failed. return self._current.translations is not None \ or self._activate_failed # Internal methods def _add(self, t, translations): if isinstance(translations, Translations): t.add(translations) # Delegated methods def __getattr__(self, name): return getattr(self.active, name) def gettext(self, string, **kwargs): def _gettext(): return safefmt(self.active.gettext(string), kwargs) if not self.isactive: return LazyProxy(_gettext) return _gettext() def dgettext(self, domain, string, **kwargs): def _dgettext(): return safefmt(self.active.dugettext(domain, string), kwargs) if not self.isactive: return LazyProxy(_dgettext) return _dgettext() def ngettext(self, singular, plural, num, **kwargs): kwargs = kwargs.copy() kwargs.setdefault('num', num) def _ngettext(): trans = self.active.ngettext(singular, plural, num) return safefmt(trans, kwargs) if not self.isactive: return LazyProxy(_ngettext) return _ngettext() def dngettext(self, domain, singular, plural, num, **kwargs): kwargs = kwargs.copy() kwargs.setdefault('num', num) def _dngettext(): trans = self.active.dungettext(domain, singular, plural, num) return safefmt(trans, kwargs) if not self.isactive: return LazyProxy(_dngettext) return _dngettext() def tgettext(self, string, **kwargs): def _tgettext(): trans = self.active.gettext(string) return _tag_kwargs(trans, kwargs) if kwargs else trans if not self.isactive: return LazyProxy(_tgettext) return _tgettext() def dtgettext(self, domain, string, **kwargs): def _dtgettext(): trans = self.active.dugettext(domain, string) return _tag_kwargs(trans, kwargs) if kwargs else trans if not self.isactive: return LazyProxy(_dtgettext) return _dtgettext() def tngettext(self, singular, plural, num, **kwargs): kwargs = kwargs.copy() kwargs.setdefault('num', num) def _tngettext(): trans = self.active.ngettext(singular, plural, num) return _tag_kwargs(trans, kwargs) if not self.isactive: return LazyProxy(_tngettext) return _tngettext() def dtngettext(self, domain, singular, plural, num, **kwargs): kwargs = kwargs.copy() def _dtngettext(): trans = self.active.dungettext(domain, singular, plural, num) if '%(num)' in trans: kwargs.update(num=num) return _tag_kwargs(trans, kwargs) if kwargs else trans if not self.isactive: return LazyProxy(_dtngettext) return _dtngettext() translations = TranslationsProxy() def domain_functions(domain, *symbols): """Prepare partial instantiations of domain translation functions. :param domain: domain used for partial instantiation :param symbols: remaining parameters are the name of commonly used translation function which will be bound to the domain Note: the symbols can also be given as an iterable in the 2nd argument. """ if symbols and not isinstance(symbols[0], str): symbols = symbols[0] _functions = { 'gettext': s_dgettext, '_': translations.dgettext, 'ngettext': translations.dngettext, 'tgettext': translations.dtgettext, 'tag_': translations.dtgettext, 'tngettext': translations.dtngettext, 'tagn_': translations.dtngettext, 'add_domain': translations.add_domain, } def wrapdomain(symbol): if symbol == 'N_': return _noop return lambda *args, **kw: _functions[symbol](domain, *args, **kw) return [wrapdomain(s) for s in symbols] gettext = translations.gettext _ = gettext dgettext = translations.dgettext ngettext = translations.ngettext dngettext = translations.dngettext tgettext = translations.tgettext tag_ = tgettext dtgettext = translations.dtgettext tngettext = translations.tngettext tagn_ = tngettext dtngettext = translations.dtngettext def add_domain(domain, env_path, locale_dir): translations.add_domain(domain, env_path, locale_dir) def activate(locale, env_path=None): translations.activate(locale, env_path) def deactivate(): """Deactivate translations. :return: the current Translations, if any """ return translations.deactivate() def reactivate(t): """Reactivate previously deactivated translations. :param t: the Translations, as returned by `deactivate` """ return translations.reactivate(t) def make_activable(get_locale, env_path=None): """Defer activation of translations. :param get_locale: a callable returning a Babel Locale object :param env_path: the environment to use for looking up catalogs """ translations.make_activable(get_locale, env_path) def get_translations(): return translations def get_available_locales(): """Return a list of locale identifiers of the locales for which translations are available. """ try: locales = [dirname for dirname in pkg_resources.resource_listdir('trac', 'locale') if '.' not in dirname and pkg_resources.resource_exists( 'trac', 'locale/%s/LC_MESSAGES/messages.mo' % dirname)] return locales except Exception: return [] def get_negotiated_locale(preferred_locales): def normalize(locale_ids): return [id.replace('-', '_') for id in locale_ids if id] available_locales = get_available_locales() if 'en_US' not in available_locales: available_locales.append('en_US') locale = Locale.negotiate(normalize(preferred_locales), normalize(available_locales)) if locale and str(locale) not in available_locales: # The list of get_available_locales() must include locale # identifier from str(locale), but zh_* won't be included # after Babel 1.0. Avoid expanding zh_* to zh_Hans_CN and # zh_Hant_TW to clear "script" property of Locale # instance. See #11258. locale._data # load localedata before clear script property locale.script = None assert str(locale) in available_locales return locale def get_locale_name(locale_id): """"Return locale name from locale identifier, or `None` if identifier is not valid. """ try: locale = Locale.parse(locale_id) except (UnknownLocaleError, ValueError): return None else: return locale.display_name has_babel = True except ImportError: # fall back on 0.11 behavior, i18n functions are no-ops Locale = None gettext = _ = gettext_noop dgettext = dgettext_noop ngettext = ngettext_noop dngettext = dngettext_noop tgettext = tag_ = tgettext_noop dtgettext = dtgettext_noop tngettext = tagn_ = tngettext_noop dtngettext = dtngettext_noop translations = NullTranslationsBabel() def activate(locale, env_path=None): pass def deactivate(): pass def reactivate(t): pass def make_activable(get_locale, env_path=None): pass def get_translations(): return translations def get_available_locales(): return [] def get_negotiated_locale(preferred_locales): return None def get_locale_name(locale_id): return None # White-space simplification of msgids def s_dgettext(domain, msgid, **kwargs): """Retrieves translations for "squeezed" messages, in a domain. See `s_gettext` for additional details. """ msgid = ' '.join(msgid.split()) # avoid to extract ' ' return dgettext(domain, msgid, **kwargs) def s_gettext(msgid, **kwargs): """Retrieves translations for "squeezed" messages (in default domain). Squeezed messages are text blocks in which white-space has been simplified during extraction (see `trac.dist.extract_html`). The catalog contain msgid with minimal whitespace. As a consequence, the msgid have to be normalized as well at retrieval time (i.e. here). This typically happens for trans blocks and gettext functions in Jinja2 templates, as well as all the text extracted from legacy Genshi templates. """ msgid = ' '.join(msgid.split()) # avoid to extract ' ' return gettext(msgid, **kwargs) # TODO (1.3.2) do the same for pluralize (ngettext/dngettext_noop) functions = { '_': gettext, 'dgettext': s_dgettext, 'dngettext': dngettext, 'gettext': s_gettext, 'ngettext': ngettext, 'tag_': tag_, }