summaryrefslogtreecommitdiff
path: root/repoze/bfg/i18n.py
diff options
context:
space:
mode:
Diffstat (limited to 'repoze/bfg/i18n.py')
-rw-r--r--repoze/bfg/i18n.py306
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)
+