diff options
Diffstat (limited to 'repoze/bfg/i18n.py')
| -rw-r--r-- | repoze/bfg/i18n.py | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/repoze/bfg/i18n.py b/repoze/bfg/i18n.py new file mode 100644 index 000000000..e3b276331 --- /dev/null +++ b/repoze/bfg/i18n.py @@ -0,0 +1,306 @@ +import gettext +import os + +from translationstring import Translator +from translationstring import Pluralizer +from translationstring import TranslationString # API +from translationstring import TranslationStringFactory # API + +TranslationString = TranslationString # PyFlakes +TranslationStringFactory = TranslationStringFactory # PyFlakes + +from repoze.bfg.interfaces import ILocalizer +from repoze.bfg.interfaces import ITranslationDirectories +from repoze.bfg.interfaces import ILocaleNegotiator + +from repoze.bfg.settings import get_settings +from repoze.bfg.threadlocal import get_current_registry + +class Localizer(object): + """ + An object providing translation and pluralizations related to + the current request's locale name. A + :class:`repoze.bfg.i18n.Localizer` object is created using the + :func:`repoze.bfg.i18n.get_localizer` function. + """ + def __init__(self, locale_name, translations): + self.locale_name = locale_name + self.translations = translations + self.pluralizer = None + self.translator = None + + def translate(self, tstring, domain=None, mapping=None): + """ + Translate a :term:`translation string` to the current language + and interpolate any *replacement markers* in the result. The + ``translate`` method accepts three arguments: ``tstring`` + (required), ``domain`` (optional) and ``mapping`` (optional). + When called, it will translate the ``tstring`` translation + string to a ``unicode`` object using the current locale. If + the current locale could not be determined, the result of + interpolation of the default value is returned. The optional + ``domain`` argument can be used to specify or override the + domain of the ``tstring`` (useful when ``tstring`` is a normal + string rather than a translation string). The optional + ``mapping`` argument can specify or override the ``tstring`` + interpolation mapping, useful when the ``tstring`` argument is + a simple string instead of a translation string. + + Example:: + + from repoze.bfg.18n import TranslationString + ts = TranslationString('Add ${item}', domain='mypackage', + mapping={'item':'Item'}) + translated = localizer.translate(ts) + + Example:: + + translated = localizer.translate('Add ${item}', domain='mypackage', + mapping={'item':'Item'}) + + """ + if self.translator is None: + self.translator = Translator(self.translations) + return self.translator(tstring, domain=domain, mapping=mapping) + + def pluralize(self, singular, plural, n, domain=None, mapping=None): + """ + Return a Unicode string translation by using two + :term:`message identifier` objects as a singular/plural pair + and an ``n`` value representing the number that appears in the + message using gettext plural forms support. The ``singular`` + and ``plural`` objects passed may be translation strings or + unicode strings. ``n`` represents the number of elements. + ``domain`` is the translation domain to use to do the + pluralization, and ``mapping`` is the interpolation mapping + that should be used on the result. Note that if the objects + passed are translation strings, their domains and mappings are + ignored. The domain and mapping arguments must be used + instead. If the ``domain`` is not supplied, a default domain + is used (usually ``messages``). + + Example:: + + num = 1 + translated = localizer.pluralize('Add ${num} item', + 'Add ${num} items', + num, + mapping={'num':num}) + + + """ + if self.pluralizer is None: + self.pluralizer = Pluralizer(self.translations) + return self.pluralizer(singular, plural, n, domain=domain, + mapping=mapping) + + +def default_locale_negotiator(request): + """ The default :term:`locale negotiator`. Returns a locale + name based on ``request.params.get('locale')`` or the + ``default_locale_name`` settings option; or ``en`` if all else + fails.""" + locale_name = request.params.get('locale') + if locale_name is None: + settings = get_settings() or {} + locale_name = settings.get('default_locale_name', 'en') + return locale_name + +def negotiate_locale_name(request): + """ Negotiate and return the :term:`locale name` associated with + the current request (never cached).""" + try: + registry = request.registry + except AttributeError: + registry = get_current_registry() + negotiator = registry.queryUtility(ILocaleNegotiator) + if negotiator is None: + settings = get_settings() or {} + locale_name = settings.get('default_locale_name', 'en') + else: + locale_name = negotiator(request) + return locale_name + +def get_locale_name(request): + """ Return the :term:`locale name` associated with the current + request (possibly cached).""" + locale_name = getattr(request, 'bfg_locale_name', None) + if locale_name is None: + locale_name = negotiate_locale_name(request) + request.bfg_locale_name = locale_name + return locale_name + +def get_localizer(request): + """ Retrieve a :class:`repoze.bfg.i18n.Localizer` object + corresponding to the current request's locale name. """ + localizer = getattr(request, 'bfg_localizer', None) + + if localizer is None: + # no locale object cached on request + try: + registry = request.registry + except AttributeError: + registry = get_current_registry() + + current_locale_name = get_locale_name(request) + localizer = registry.queryUtility(ILocalizer, name=current_locale_name) + + if localizer is None: + # no localizer utility registered yet + translations = Translations() + translations._catalog = {} + tdirs = registry.queryUtility(ITranslationDirectories, default=[]) + for tdir in tdirs: + locale_dirs = [ (lname, os.path.join(tdir, lname)) for lname in + os.listdir(tdir) ] + for locale_name, locale_dir in locale_dirs: + if locale_name != current_locale_name: + continue + messages_dir = os.path.join(locale_dir, 'LC_MESSAGES') + if not os.path.isdir(os.path.realpath(messages_dir)): + continue + for mofile in os.listdir(messages_dir): + mopath = os.path.realpath(os.path.join(messages_dir, + mofile)) + if mofile.endswith('.mo') and os.path.isfile(mopath): + mofp = open(mopath, 'rb') + domain = mofile[:-3] + dtrans = Translations(mofp, domain) + translations.add(dtrans) + + localizer = Localizer(locale_name=current_locale_name, + translations=translations) + registry.registerUtility(localizer, ILocalizer, + name=current_locale_name) + request.bfg_localizer = localizer + + return localizer + +class Translations(gettext.GNUTranslations, object): + """An extended translation catalog class (ripped off from Babel) """ + + DEFAULT_DOMAIN = 'messages' + + def __init__(self, fileobj=None, domain=DEFAULT_DOMAIN): + """Initialize the translations catalog. + + :param fileobj: the file-like object the translation should be read + from + """ + gettext.GNUTranslations.__init__(self, fp=fileobj) + self.files = filter(None, [getattr(fileobj, 'name', None)]) + self.domain = domain + self._domains = {} + + @classmethod + def load(cls, dirname=None, locales=None, domain=DEFAULT_DOMAIN): + """Load translations from the given directory. + + :param dirname: the directory containing the ``MO`` files + :param locales: the list of locales in order of preference (items in + this list can be either `Locale` objects or locale + strings) + :param domain: the message domain + :return: the loaded catalog, or a ``NullTranslations`` instance if no + matching translations were found + :rtype: `Translations` + """ + if locales is not None: + if not isinstance(locales, (list, tuple)): + locales = [locales] + locales = [str(l) for l in locales] + if not domain: + domain = cls.DEFAULT_DOMAIN + filename = gettext.find(domain, dirname, locales) + if not filename: + return gettext.NullTranslations() + return cls(fileobj=open(filename, 'rb'), domain=domain) + + def __repr__(self): + return '<%s: "%s">' % (type(self).__name__, + self._info.get('project-id-version')) + + def add(self, translations, merge=True): + """Add the given translations to the catalog. + + If the domain of the translations is different than that of the + current catalog, they are added as a catalog that is only accessible + by the various ``d*gettext`` functions. + + :param translations: the `Translations` instance with the messages to + add + :param merge: whether translations for message domains that have + already been added should be merged with the existing + translations + :return: the `Translations` instance (``self``) so that `merge` calls + can be easily chained + :rtype: `Translations` + """ + domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN) + if merge and domain == self.domain: + return self.merge(translations) + + existing = self._domains.get(domain) + if merge and existing is not None: + existing.merge(translations) + else: + translations.add_fallback(self) + self._domains[domain] = translations + + return self + + def merge(self, translations): + """Merge the given translations into the catalog. + + Message translations in the specified catalog override any messages + with the same identifier in the existing catalog. + + :param translations: the `Translations` instance with the messages to + merge + :return: the `Translations` instance (``self``) so that `merge` calls + can be easily chained + :rtype: `Translations` + """ + if isinstance(translations, gettext.GNUTranslations): + self._catalog.update(translations._catalog) + if isinstance(translations, Translations): + self.files.extend(translations.files) + + return self + + def dgettext(self, domain, message): + """Like ``gettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).gettext(message) + + def ldgettext(self, domain, message): + """Like ``lgettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).lgettext(message) + + def dugettext(self, domain, message): + """Like ``ugettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).ugettext(message) + + def dngettext(self, domain, singular, plural, num): + """Like ``ngettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).ngettext(singular, plural, num) + + def ldngettext(self, domain, singular, plural, num): + """Like ``lngettext()``, but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).lngettext(singular, plural, num) + + def dungettext(self, domain, singular, plural, num): + """Like ``ungettext()`` but look the message up in the specified + domain. + """ + return self._domains.get(domain, self).ungettext(singular, plural, num) + |
