summaryrefslogtreecommitdiff
path: root/repoze/bfg
diff options
context:
space:
mode:
Diffstat (limited to 'repoze/bfg')
-rw-r--r--repoze/bfg/chameleon_text.py8
-rw-r--r--repoze/bfg/chameleon_zpt.py8
-rw-r--r--repoze/bfg/configuration.py93
-rw-r--r--repoze/bfg/i18n.py306
-rw-r--r--repoze/bfg/includes/meta.zcml12
-rw-r--r--repoze/bfg/interfaces.py17
-rw-r--r--repoze/bfg/paster_templates/alchemy/+project+.ini_tmpl1
-rw-r--r--repoze/bfg/paster_templates/alchemy/setup.cfg_tmpl27
-rw-r--r--repoze/bfg/paster_templates/routesalchemy/+project+.ini_tmpl1
-rw-r--r--repoze/bfg/paster_templates/routesalchemy/setup.cfg_tmpl21
-rw-r--r--repoze/bfg/paster_templates/starter/+project+.ini_tmpl1
-rw-r--r--repoze/bfg/paster_templates/starter/setup.cfg_tmpl27
-rw-r--r--repoze/bfg/paster_templates/zodb/+project+.ini_tmpl1
-rw-r--r--repoze/bfg/paster_templates/zodb/setup.cfg_tmpl27
-rw-r--r--repoze/bfg/settings.py4
-rw-r--r--repoze/bfg/tests/fixtures/__init__.py1
-rw-r--r--repoze/bfg/tests/fixtures/locale/GARBAGE1
-rw-r--r--repoze/bfg/tests/fixtures/locale/be/LC_MESSAGES1
-rw-r--r--repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.mobin0 -> 543 bytes
-rw-r--r--repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.po31
-rw-r--r--repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.mobin0 -> 543 bytes
-rw-r--r--repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.po31
-rw-r--r--repoze/bfg/tests/test_chameleon_text.py20
-rw-r--r--repoze/bfg/tests/test_chameleon_zpt.py20
-rw-r--r--repoze/bfg/tests/test_configuration.py57
-rw-r--r--repoze/bfg/tests/test_i18n.py382
-rw-r--r--repoze/bfg/tests/test_zcml.py50
-rw-r--r--repoze/bfg/zcml.py35
28 files changed, 1172 insertions, 11 deletions
diff --git a/repoze/bfg/chameleon_text.py b/repoze/bfg/chameleon_text.py
index f1dc8e3aa..deb1cc43d 100644
--- a/repoze/bfg/chameleon_text.py
+++ b/repoze/bfg/chameleon_text.py
@@ -22,6 +22,7 @@ except ImportError: # pragma: no cover
from repoze.bfg.interfaces import IResponseFactory
from repoze.bfg.interfaces import ITemplateRenderer
+from repoze.bfg.interfaces import IChameleonTranslate
from repoze.bfg.decorator import reify
from repoze.bfg.renderers import template_renderer_factory
@@ -50,7 +51,12 @@ class TextTemplateRenderer(object):
def template(self):
settings = get_settings()
auto_reload = settings and settings['reload_templates']
- return TextTemplateFile(self.path, auto_reload=auto_reload)
+ reg = get_current_registry()
+ translate = None
+ if reg is not None:
+ translate = reg.queryUtility(IChameleonTranslate)
+ return TextTemplateFile(self.path, auto_reload=auto_reload,
+ translate=translate)
def implementation(self):
return self.template
diff --git a/repoze/bfg/chameleon_zpt.py b/repoze/bfg/chameleon_zpt.py
index f597ebd5f..687a11305 100644
--- a/repoze/bfg/chameleon_zpt.py
+++ b/repoze/bfg/chameleon_zpt.py
@@ -13,6 +13,7 @@ except ImportError: # pragma: no cover
def __init__(self, *arg, **kw):
raise ImportError, exc, tb
+from repoze.bfg.interfaces import IChameleonTranslate
from repoze.bfg.interfaces import IResponseFactory
from repoze.bfg.interfaces import ITemplateRenderer
@@ -33,7 +34,12 @@ class ZPTTemplateRenderer(object):
def template(self):
settings = get_settings()
auto_reload = settings and settings['reload_templates']
- return PageTemplateFile(self.path, auto_reload=auto_reload)
+ reg = get_current_registry()
+ translate = None
+ if reg is not None:
+ translate = reg.queryUtility(IChameleonTranslate)
+ return PageTemplateFile(self.path, auto_reload=auto_reload,
+ translate=translate)
def implementation(self):
return self.template
diff --git a/repoze/bfg/configuration.py b/repoze/bfg/configuration.py
index 4673479da..9ebb62a01 100644
--- a/repoze/bfg/configuration.py
+++ b/repoze/bfg/configuration.py
@@ -8,6 +8,8 @@ import inspect
from webob import Response
import venusian
+from translationstring import ChameleonTranslate
+
from zope.configuration import xmlconfig
from zope.interface import Interface
@@ -17,9 +19,11 @@ from zope.interface import implements
from repoze.bfg.interfaces import IAuthenticationPolicy
from repoze.bfg.interfaces import IAuthorizationPolicy
+from repoze.bfg.interfaces import IChameleonTranslate
from repoze.bfg.interfaces import IDebugLogger
from repoze.bfg.interfaces import IDefaultRootFactory
from repoze.bfg.interfaces import IExceptionViewClassifier
+from repoze.bfg.interfaces import ILocaleNegotiator
from repoze.bfg.interfaces import IMultiView
from repoze.bfg.interfaces import IPackageOverrides
from repoze.bfg.interfaces import IRendererFactory
@@ -31,6 +35,7 @@ from repoze.bfg.interfaces import IRoutesMapper
from repoze.bfg.interfaces import ISecuredView
from repoze.bfg.interfaces import ISettings
from repoze.bfg.interfaces import ITemplateRenderer
+from repoze.bfg.interfaces import ITranslationDirectories
from repoze.bfg.interfaces import ITraverser
from repoze.bfg.interfaces import IView
from repoze.bfg.interfaces import IViewClassifier
@@ -45,8 +50,10 @@ from repoze.bfg.events import WSGIApplicationCreatedEvent
from repoze.bfg.exceptions import Forbidden
from repoze.bfg.exceptions import NotFound
from repoze.bfg.exceptions import ConfigurationError
+from repoze.bfg.i18n import get_localizer
from repoze.bfg.log import make_stream_logger
from repoze.bfg.path import caller_package
+from repoze.bfg.path import package_path
from repoze.bfg.registry import Registry
from repoze.bfg.request import route_request_iface
from repoze.bfg.resource import PackageOverrides
@@ -54,6 +61,7 @@ from repoze.bfg.resource import resolve_resource_spec
from repoze.bfg.settings import Settings
from repoze.bfg.static import StaticRootFactory
from repoze.bfg.threadlocal import get_current_registry
+from repoze.bfg.threadlocal import get_current_request
from repoze.bfg.threadlocal import manager
from repoze.bfg.traversal import traversal_path
from repoze.bfg.traversal import DefaultRootFactory
@@ -135,14 +143,16 @@ class Configurator(object):
logs to stderr will be used. If it is passed, it should be an
instance of the :class:`logging.Logger` (PEP 282) standard library
class. The debug logger is used by :mod:`repoze.bfg` itself to
- log warnings and authorization debugging information. """
-
+ log warnings and authorization debugging information.
+
+ """
manager = manager # for testing injection
venusian = venusian # for testing injection
def __init__(self, registry=None, package=None, settings=None,
root_factory=None, authentication_policy=None,
authorization_policy=None, renderers=DEFAULT_RENDERERS,
- debug_logger=None):
+ debug_logger=None,
+ locale_negotiator=None):
self.package = package or caller_package()
self.registry = registry
if registry is None:
@@ -154,7 +164,8 @@ class Configurator(object):
authentication_policy=authentication_policy,
authorization_policy=authorization_policy,
renderers=renderers,
- debug_logger=debug_logger)
+ debug_logger=debug_logger,
+ locale_negotiator=locale_negotiator)
def _set_settings(self, mapping):
settings = Settings(mapping or {})
@@ -270,7 +281,8 @@ class Configurator(object):
def setup_registry(self, settings=None, root_factory=None,
authentication_policy=None, authorization_policy=None,
- renderers=DEFAULT_RENDERERS, debug_logger=None):
+ renderers=DEFAULT_RENDERERS, debug_logger=None,
+ locale_negotiator=None):
""" When you pass a non-``None`` ``registry`` argument to the
:term:`Configurator` constructor, no initial 'setup' is
performed against the registry. This is because the registry
@@ -304,6 +316,8 @@ class Configurator(object):
self.add_renderer(name, renderer)
self.set_notfound_view(default_notfound_view)
self.set_forbidden_view(default_forbidden_view)
+ if locale_negotiator:
+ registry.registerUtility(locale_negotiator, ILocaleNegotiator)
# getSiteManager is a unit testing dep injection
def hook_zca(self, getSiteManager=None):
@@ -1225,10 +1239,6 @@ class Configurator(object):
found via context-finding or ``None`` if no context could
be found. The exception causing the registered view to be
called is however still available as ``request.exception``.
- .. warning:: This method has been deprecated in
- :mod:`repoze.bfg` 1.3. See
- :ref:`changing_the_forbidden_view` to see how a not found
- view should be registered in :mod:`repoze.bfg` 1.3+.
The ``view`` argument should be a :term:`view callable`.
@@ -1299,6 +1309,71 @@ class Configurator(object):
return self.add_view(bwcompat_view, context=NotFound,
wrapper=wrapper, _info=_info)
+ def set_locale_negotiator(self, negotiator):
+ """
+ Set the :term:`locale negotiator` for this application. The
+ :term:`locale negotiator` is a callable which accepts a
+ :term:`request` object and which returns a :term:`locale
+ name`. Later calls to this method override earlier calls;
+ there can be only one locale negotiator active at a time
+ within an application. See :ref:`activating_translation` for
+ more information.
+
+ .. note: This API is new as of :mod:`repoze.bfg` version 1.3.
+ """
+ self.registry.registerUtility(negotiator, ILocaleNegotiator)
+
+ def add_translation_dirs(self, *specs):
+ """ Add one or more :term:`translation directory` paths to the
+ current configuration state. The ``specs`` argument is a
+ sequence that may contain absolute directory paths
+ (e.g. ``/usr/share/locale``) or :term:`resource specification`
+ names naming a directory path (e.g. ``some.package:locale``)
+ or a combination of the two.
+
+ Example:
+
+ .. code-block:: python
+
+ add_translations_dirs('/usr/share/locale', 'some.package:locale')
+
+ .. note: This API is new as of :mod:`repoze.bfg` version 1.3.
+ """
+ for spec in specs:
+
+ package_name, filename = self._split_spec(spec)
+ if package_name is None: # absolute filename
+ directory = filename
+ else:
+ __import__(package_name)
+ package = sys.modules[package_name]
+ directory = os.path.join(package_path(package), filename)
+
+ if not os.path.isdir(os.path.realpath(directory)):
+ raise ConfigurationError('"%s" is not a directory' % directory)
+
+ tdirs = self.registry.queryUtility(ITranslationDirectories)
+ if tdirs is None:
+ tdirs = []
+ self.registry.registerUtility(tdirs, ITranslationDirectories)
+
+ tdirs.insert(0, directory)
+
+ if specs:
+
+ # We actually only need an IChameleonTranslate function
+ # utility to be registered zero or one times. We register the
+ # same function once for each added translation directory,
+ # which does too much work, but has the same effect.
+
+ def translator(msg):
+ request = get_current_request()
+ localizer = get_localizer(request)
+ return localizer.translate(msg)
+
+ ctranslate = ChameleonTranslate(translator)
+ self.registry.registerUtility(ctranslate, IChameleonTranslate)
+
def add_static_view(self, name, path, cache_max_age=3600, _info=u''):
""" Add a view used to render static resources to the current
configuration state.
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)
+
diff --git a/repoze/bfg/includes/meta.zcml b/repoze/bfg/includes/meta.zcml
index 25894df71..5ecebd868 100644
--- a/repoze/bfg/includes/meta.zcml
+++ b/repoze/bfg/includes/meta.zcml
@@ -77,6 +77,18 @@
/>
<meta:directive
+ name="translationdir"
+ schema="repoze.bfg.zcml.ITranslationDirDirective"
+ handler="repoze.bfg.zcml.translationdir"
+ />
+
+ <meta:directive
+ name="localenegotiator"
+ schema="repoze.bfg.zcml.ILocaleNegotiatorDirective"
+ handler="repoze.bfg.zcml.localenegotiator"
+ />
+
+ <meta:directive
name="adapter"
schema="repoze.bfg.zcml.IAdapterDirective"
handler="repoze.bfg.zcml.adapter"
diff --git a/repoze/bfg/interfaces.py b/repoze/bfg/interfaces.py
index 40d29348c..09b639525 100644
--- a/repoze/bfg/interfaces.py
+++ b/repoze/bfg/interfaces.py
@@ -229,3 +229,20 @@ class IPackageOverrides(Interface):
# VH_ROOT_KEY is an interface; its imported from other packages (e.g.
# traversalwrapper)
VH_ROOT_KEY = 'HTTP_X_VHM_ROOT'
+
+class IChameleonTranslate(Interface):
+ """ Internal interface representing a chameleon translate function """
+ def __call__(msgid, domain=None, mapping=None, context=None,
+ target_language=None, default=None):
+ """ Translate a mess of arguments to a Unicode object """
+
+class ILocalizer(Interface):
+ """ Localizer for a specific language """
+
+class ILocaleNegotiator(Interface):
+ def __call__(request):
+ """ Return a locale name """
+
+class ITranslationDirectories(Interface):
+ """ A list object representing all known translation directories
+ for an application"""
diff --git a/repoze/bfg/paster_templates/alchemy/+project+.ini_tmpl b/repoze/bfg/paster_templates/alchemy/+project+.ini_tmpl
index c3d79e08b..29fc7e806 100644
--- a/repoze/bfg/paster_templates/alchemy/+project+.ini_tmpl
+++ b/repoze/bfg/paster_templates/alchemy/+project+.ini_tmpl
@@ -6,6 +6,7 @@ use = egg:{{project}}#app
reload_templates = true
debug_authorization = false
debug_notfound = false
+default_locale_name = en
db_string = sqlite:///%(here)s/{{package}}.db
db_echo = false
diff --git a/repoze/bfg/paster_templates/alchemy/setup.cfg_tmpl b/repoze/bfg/paster_templates/alchemy/setup.cfg_tmpl
new file mode 100644
index 000000000..5bec29823
--- /dev/null
+++ b/repoze/bfg/paster_templates/alchemy/setup.cfg_tmpl
@@ -0,0 +1,27 @@
+[nosetests]
+match=^test
+nocapture=1
+cover-package={{package}}
+with-coverage=1
+cover-erase=1
+
+[compile_catalog]
+directory = {{package}}/locale
+domain = {{project}}
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = {{package}}/locale/{{project}}.pot
+width = 80
+
+[init_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+
+[update_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+previous = true
diff --git a/repoze/bfg/paster_templates/routesalchemy/+project+.ini_tmpl b/repoze/bfg/paster_templates/routesalchemy/+project+.ini_tmpl
index 72c373fc2..c249172d3 100644
--- a/repoze/bfg/paster_templates/routesalchemy/+project+.ini_tmpl
+++ b/repoze/bfg/paster_templates/routesalchemy/+project+.ini_tmpl
@@ -6,6 +6,7 @@ use = egg:{{package}}#app
reload_templates = true
debug_authorization = false
debug_notfound = false
+default_locale_name = en
db_string = sqlite:///%(here)s/{{package}}.db
[pipeline:main]
diff --git a/repoze/bfg/paster_templates/routesalchemy/setup.cfg_tmpl b/repoze/bfg/paster_templates/routesalchemy/setup.cfg_tmpl
index 8bf5d22c1..5bec29823 100644
--- a/repoze/bfg/paster_templates/routesalchemy/setup.cfg_tmpl
+++ b/repoze/bfg/paster_templates/routesalchemy/setup.cfg_tmpl
@@ -4,3 +4,24 @@ nocapture=1
cover-package={{package}}
with-coverage=1
cover-erase=1
+
+[compile_catalog]
+directory = {{package}}/locale
+domain = {{project}}
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = {{package}}/locale/{{project}}.pot
+width = 80
+
+[init_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+
+[update_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+previous = true
diff --git a/repoze/bfg/paster_templates/starter/+project+.ini_tmpl b/repoze/bfg/paster_templates/starter/+project+.ini_tmpl
index cc1e2aa12..15be80cf4 100644
--- a/repoze/bfg/paster_templates/starter/+project+.ini_tmpl
+++ b/repoze/bfg/paster_templates/starter/+project+.ini_tmpl
@@ -6,6 +6,7 @@ use = egg:{{project}}#app
reload_templates = true
debug_authorization = false
debug_notfound = false
+default_locale_name = en
[server:main]
use = egg:Paste#http
diff --git a/repoze/bfg/paster_templates/starter/setup.cfg_tmpl b/repoze/bfg/paster_templates/starter/setup.cfg_tmpl
new file mode 100644
index 000000000..5bec29823
--- /dev/null
+++ b/repoze/bfg/paster_templates/starter/setup.cfg_tmpl
@@ -0,0 +1,27 @@
+[nosetests]
+match=^test
+nocapture=1
+cover-package={{package}}
+with-coverage=1
+cover-erase=1
+
+[compile_catalog]
+directory = {{package}}/locale
+domain = {{project}}
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = {{package}}/locale/{{project}}.pot
+width = 80
+
+[init_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+
+[update_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+previous = true
diff --git a/repoze/bfg/paster_templates/zodb/+project+.ini_tmpl b/repoze/bfg/paster_templates/zodb/+project+.ini_tmpl
index ca6fe99a2..51b4e1ab8 100644
--- a/repoze/bfg/paster_templates/zodb/+project+.ini_tmpl
+++ b/repoze/bfg/paster_templates/zodb/+project+.ini_tmpl
@@ -6,6 +6,7 @@ use = egg:{{project}}#app
reload_templates = true
debug_authorization = false
debug_notfound = false
+default_locale_name = en
zodb_uri = file://%(here)s/Data.fs?connection_cache_size=20000
[pipeline:main]
diff --git a/repoze/bfg/paster_templates/zodb/setup.cfg_tmpl b/repoze/bfg/paster_templates/zodb/setup.cfg_tmpl
new file mode 100644
index 000000000..5bec29823
--- /dev/null
+++ b/repoze/bfg/paster_templates/zodb/setup.cfg_tmpl
@@ -0,0 +1,27 @@
+[nosetests]
+match=^test
+nocapture=1
+cover-package={{package}}
+with-coverage=1
+cover-erase=1
+
+[compile_catalog]
+directory = {{package}}/locale
+domain = {{project}}
+statistics = true
+
+[extract_messages]
+add_comments = TRANSLATORS:
+output_file = {{package}}/locale/{{project}}.pot
+width = 80
+
+[init_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+
+[update_catalog]
+domain = {{project}}
+input_file = {{package}}/locale/{{project}}.pot
+output_dir = {{package}}/locale
+previous = true
diff --git a/repoze/bfg/settings.py b/repoze/bfg/settings.py
index 069d25e6c..9cb2502a1 100644
--- a/repoze/bfg/settings.py
+++ b/repoze/bfg/settings.py
@@ -36,12 +36,16 @@ class Settings(dict):
config_reload_resources))
configure_zcml = self.get('configure_zcml', '')
eff_configure_zcml = eget('BFG_CONFIGURE_ZCML', configure_zcml)
+ locale_name = self.get('locale_name', 'en')
+ eff_locale_name = eget('BFG_DEFAULT_LOCALE_NAME', locale_name)
+
update = {
'debug_authorization': eff_debug_all or eff_debug_auth,
'debug_notfound': eff_debug_all or eff_debug_notfound,
'reload_templates': eff_reload_all or eff_reload_templates,
'reload_resources':eff_reload_all or eff_reload_resources,
'configure_zcml':eff_configure_zcml,
+ 'default_locale_name':eff_locale_name,
}
self.update(update)
diff --git a/repoze/bfg/tests/fixtures/__init__.py b/repoze/bfg/tests/fixtures/__init__.py
new file mode 100644
index 000000000..1a35cdb4a
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/__init__.py
@@ -0,0 +1 @@
+# a file
diff --git a/repoze/bfg/tests/fixtures/locale/GARBAGE b/repoze/bfg/tests/fixtures/locale/GARBAGE
new file mode 100644
index 000000000..032c55584
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/locale/GARBAGE
@@ -0,0 +1 @@
+Garbage file.
diff --git a/repoze/bfg/tests/fixtures/locale/be/LC_MESSAGES b/repoze/bfg/tests/fixtures/locale/be/LC_MESSAGES
new file mode 100644
index 000000000..909cf6a3b
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/locale/be/LC_MESSAGES
@@ -0,0 +1 @@
+busted.
diff --git a/repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.mo b/repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.mo
new file mode 100644
index 000000000..2924a5eb5
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.mo
Binary files differ
diff --git a/repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.po b/repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.po
new file mode 100644
index 000000000..17f87bc19
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/locale/de/LC_MESSAGES/deformsite.po
@@ -0,0 +1,31 @@
+# German translations for deformsite.
+# Copyright (C) 2010 ORGANIZATION
+# This file is distributed under the same license as the deformsite project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: deformsite 0.0\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2010-04-22 14:17+0400\n"
+"PO-Revision-Date: 2010-04-22 14:17-0400\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: de <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.5\n"
+
+#: deformsite/__init__.py:458
+msgid "Approve"
+msgstr "Genehmigen"
+
+#: deformsite/__init__.py:459
+msgid "Show approval"
+msgstr "Zeigen Genehmigung"
+
+#: deformsite/__init__.py:466
+msgid "Submit"
+msgstr "Beugen"
+
diff --git a/repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.mo b/repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.mo
new file mode 100644
index 000000000..2924a5eb5
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.mo
Binary files differ
diff --git a/repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.po b/repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.po
new file mode 100644
index 000000000..17f87bc19
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/locale/en/LC_MESSAGES/deformsite.po
@@ -0,0 +1,31 @@
+# German translations for deformsite.
+# Copyright (C) 2010 ORGANIZATION
+# This file is distributed under the same license as the deformsite project.
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2010.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: deformsite 0.0\n"
+"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
+"POT-Creation-Date: 2010-04-22 14:17+0400\n"
+"PO-Revision-Date: 2010-04-22 14:17-0400\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: de <LL@li.org>\n"
+"Plural-Forms: nplurals=2; plural=(n != 1)\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=utf-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: Babel 0.9.5\n"
+
+#: deformsite/__init__.py:458
+msgid "Approve"
+msgstr "Genehmigen"
+
+#: deformsite/__init__.py:459
+msgid "Show approval"
+msgstr "Zeigen Genehmigung"
+
+#: deformsite/__init__.py:466
+msgid "Submit"
+msgstr "Beugen"
+
diff --git a/repoze/bfg/tests/test_chameleon_text.py b/repoze/bfg/tests/test_chameleon_text.py
index 007c102e3..204805adb 100644
--- a/repoze/bfg/tests/test_chameleon_text.py
+++ b/repoze/bfg/tests/test_chameleon_text.py
@@ -28,6 +28,16 @@ class Base:
class TextTemplateRendererTests(Base, unittest.TestCase):
+ def setUp(self):
+ from repoze.bfg.configuration import Configurator
+ from repoze.bfg.registry import Registry
+ registry = Registry()
+ self.config = Configurator(registry=registry)
+ self.config.begin()
+
+ def tearDown(self):
+ self.config.end()
+
def _getTargetClass(self):
from repoze.bfg.chameleon_text import TextTemplateRenderer
return TextTemplateRenderer
@@ -54,6 +64,16 @@ class TextTemplateRendererTests(Base, unittest.TestCase):
template = instance.template
self.assertEqual(template, instance.__dict__['template'])
+ def test_template_with_ichameleon_translate(self):
+ from repoze.bfg.interfaces import IChameleonTranslate
+ def ct(): pass
+ self.config.registry.registerUtility(ct, IChameleonTranslate)
+ minimal = self._getTemplatePath('minimal.txt')
+ instance = self._makeOne(minimal)
+ self.failIf('template' in instance.__dict__)
+ template = instance.template
+ self.assertEqual(template.translate, ct)
+
def test_call(self):
minimal = self._getTemplatePath('minimal.txt')
instance = self._makeOne(minimal)
diff --git a/repoze/bfg/tests/test_chameleon_zpt.py b/repoze/bfg/tests/test_chameleon_zpt.py
index e4bf8f766..cbf9dd10b 100644
--- a/repoze/bfg/tests/test_chameleon_zpt.py
+++ b/repoze/bfg/tests/test_chameleon_zpt.py
@@ -21,6 +21,16 @@ class Base(object):
return reg
class ZPTTemplateRendererTests(Base, unittest.TestCase):
+ def setUp(self):
+ from repoze.bfg.configuration import Configurator
+ from repoze.bfg.registry import Registry
+ registry = Registry()
+ self.config = Configurator(registry=registry)
+ self.config.begin()
+
+ def tearDown(self):
+ self.config.end()
+
def _getTargetClass(self):
from repoze.bfg.chameleon_zpt import ZPTTemplateRenderer
return ZPTTemplateRenderer
@@ -55,6 +65,16 @@ class ZPTTemplateRendererTests(Base, unittest.TestCase):
template = instance.template
self.assertEqual(template, instance.__dict__['template'])
+ def test_template_with_ichameleon_translate(self):
+ from repoze.bfg.interfaces import IChameleonTranslate
+ def ct(): pass
+ self.config.registry.registerUtility(ct, IChameleonTranslate)
+ minimal = self._getTemplatePath('minimal.pt')
+ instance = self._makeOne(minimal)
+ self.failIf('template' in instance.__dict__)
+ template = instance.template
+ self.assertEqual(template.translate, ct)
+
def test_call_with_nondict_value(self):
minimal = self._getTemplatePath('minimal.pt')
instance = self._makeOne(minimal)
diff --git a/repoze/bfg/tests/test_configuration.py b/repoze/bfg/tests/test_configuration.py
index 99d564b91..a1e753179 100644
--- a/repoze/bfg/tests/test_configuration.py
+++ b/repoze/bfg/tests/test_configuration.py
@@ -255,6 +255,15 @@ class ConfiguratorTests(unittest.TestCase):
config.setup_registry()
self.failUnless(reg.getUtility(IRootFactory))
+ def test_setup_registry_locale_negotiator(self):
+ from repoze.bfg.registry import Registry
+ from repoze.bfg.interfaces import ILocaleNegotiator
+ reg = Registry()
+ config = self._makeOne(reg)
+ config.setup_registry(locale_negotiator='abc')
+ utility = reg.getUtility(ILocaleNegotiator)
+ self.assertEqual(utility, 'abc')
+
def test_setup_registry_alternate_renderers(self):
from repoze.bfg.registry import Registry
from repoze.bfg.interfaces import IRendererFactory
@@ -1797,6 +1806,54 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(
config.registry.getUtility(IAuthorizationPolicy), policy)
+ def test_set_locale_negotiator(self):
+ from repoze.bfg.interfaces import ILocaleNegotiator
+ config = self._makeOne()
+ def negotiator(request): pass
+ config.set_locale_negotiator(negotiator)
+ self.assertEqual(config.registry.getUtility(ILocaleNegotiator),
+ negotiator)
+
+ def test_add_translation_dirs_missing_dir(self):
+ from repoze.bfg.exceptions import ConfigurationError
+ config = self._makeOne()
+ self.assertRaises(ConfigurationError,
+ config.add_translation_dirs,
+ '/wont/exist/on/my/system')
+
+ def test_add_translation_dirs_resource_spec(self):
+ import os
+ from repoze.bfg.interfaces import ITranslationDirectories
+ config = self._makeOne()
+ config.add_translation_dirs('repoze.bfg.tests.fixtures:locale')
+ here = os.path.dirname(__file__)
+ locale = os.path.join(here, 'fixtures', 'locale')
+ self.assertEqual(config.registry.getUtility(ITranslationDirectories),
+ [locale])
+
+ def test_add_translation_dirs_registers_chameleon_translate(self):
+ from repoze.bfg.interfaces import IChameleonTranslate
+ from repoze.bfg.threadlocal import manager
+ request = DummyRequest()
+ config = self._makeOne()
+ manager.push({'request':request, 'registry':config.registry})
+ try:
+ config.add_translation_dirs('repoze.bfg.tests.fixtures:locale')
+ translate = config.registry.getUtility(IChameleonTranslate)
+ self.assertEqual(translate('Approve'), u'Approve')
+ finally:
+ manager.pop()
+
+ def test_add_translation_dirs_abspath(self):
+ import os
+ from repoze.bfg.interfaces import ITranslationDirectories
+ config = self._makeOne()
+ here = os.path.dirname(__file__)
+ locale = os.path.join(here, 'fixtures', 'locale')
+ config.add_translation_dirs(locale)
+ self.assertEqual(config.registry.getUtility(ITranslationDirectories),
+ [locale])
+
def test__renderer_from_name_default_renderer(self):
from repoze.bfg.interfaces import IRendererFactory
config = self._makeOne()
diff --git a/repoze/bfg/tests/test_i18n.py b/repoze/bfg/tests/test_i18n.py
new file mode 100644
index 000000000..9c4b226e4
--- /dev/null
+++ b/repoze/bfg/tests/test_i18n.py
@@ -0,0 +1,382 @@
+# -*- coding: utf-8 -*-
+#
+
+import unittest
+from repoze.bfg.testing import cleanUp
+
+class TestTranslationString(unittest.TestCase):
+ def _makeOne(self, *arg, **kw):
+ from repoze.bfg.i18n import TranslationString
+ return TranslationString(*arg, **kw)
+
+ def test_it(self):
+ # this is part of the API, we don't actually need to test much more
+ # than that it's importable
+ ts = self._makeOne('a')
+ self.assertEqual(ts, 'a')
+
+class TestTranslationStringFactory(unittest.TestCase):
+ def _makeOne(self, *arg, **kw):
+ from repoze.bfg.i18n import TranslationStringFactory
+ return TranslationStringFactory(*arg, **kw)
+
+ def test_it(self):
+ # this is part of the API, we don't actually need to test much more
+ # than that it's importable
+ factory = self._makeOne('a')
+ self.assertEqual(factory('').domain, 'a')
+
+class TestLocalizer(unittest.TestCase):
+ def _makeOne(self, *arg, **kw):
+ from repoze.bfg.i18n import Localizer
+ return Localizer(*arg, **kw)
+
+ def test_ctor(self):
+ localizer = self._makeOne('en_US', None)
+ self.assertEqual(localizer.locale_name, 'en_US')
+ self.assertEqual(localizer.translations, None)
+
+ def test_translate(self):
+ translations = DummyTranslations()
+ localizer = self._makeOne(None, translations)
+ self.assertEqual(localizer.translate('123', domain='1',
+ mapping={}), '123')
+ self.failUnless(localizer.translator)
+
+ def test_pluralize(self):
+ translations = DummyTranslations()
+ localizer = self._makeOne(None, translations)
+ self.assertEqual(localizer.pluralize('singular', 'plural', 1,
+ domain='1', mapping={}),
+ 'singular')
+ self.failUnless(localizer.pluralizer)
+
+class Test_negotiate_locale_name(unittest.TestCase):
+ def setUp(self):
+ cleanUp()
+
+ def tearDown(self):
+ cleanUp()
+
+ def _callFUT(self, request):
+ from repoze.bfg.i18n import negotiate_locale_name
+ return negotiate_locale_name(request)
+
+ def _registerImpl(self, impl):
+ from repoze.bfg.threadlocal import get_current_registry
+ registry = get_current_registry()
+ from repoze.bfg.interfaces import ILocaleNegotiator
+ registry.registerUtility(impl, ILocaleNegotiator)
+
+ def test_no_registry_on_request(self):
+ self._registerImpl(dummy_negotiator)
+ request = DummyRequest()
+ result = self._callFUT(request)
+ self.assertEqual(result, 'bogus')
+
+ def test_with_registry_on_request(self):
+ from repoze.bfg.threadlocal import get_current_registry
+ registry = get_current_registry()
+ self._registerImpl(dummy_negotiator)
+ request = DummyRequest()
+ request.registry = registry
+ result = self._callFUT(request)
+ self.assertEqual(result, 'bogus')
+
+ def test_default_from_settings(self):
+ from repoze.bfg.threadlocal import get_current_registry
+ registry = get_current_registry()
+ settings = {'default_locale_name':'settings'}
+ from repoze.bfg.interfaces import ISettings
+ registry.registerUtility(settings, ISettings)
+ request = DummyRequest()
+ request.registry = registry
+ result = self._callFUT(request)
+ self.assertEqual(result, 'settings')
+
+ def test_default_default(self):
+ request = DummyRequest()
+ result = self._callFUT(request)
+ self.assertEqual(result, 'en')
+
+class Test_get_locale_name(unittest.TestCase):
+ def setUp(self):
+ cleanUp()
+
+ def tearDown(self):
+ cleanUp()
+
+ def _callFUT(self, request):
+ from repoze.bfg.i18n import get_locale_name
+ return get_locale_name(request)
+
+ def _registerImpl(self, impl):
+ from repoze.bfg.threadlocal import get_current_registry
+ registry = get_current_registry()
+ from repoze.bfg.interfaces import ILocaleNegotiator
+ registry.registerUtility(impl, ILocaleNegotiator)
+
+ def test_name_on_request(self):
+ request = DummyRequest()
+ request.bfg_locale_name = 'ie'
+ result = self._callFUT(request)
+ self.assertEqual(result, 'ie')
+
+ def test_name_not_on_request(self):
+ self._registerImpl(dummy_negotiator)
+ request = DummyRequest()
+ result = self._callFUT(request)
+ self.assertEqual(result, 'bogus')
+ self.assertEqual(request.bfg_locale_name, 'bogus')
+
+class Test_get_localizer(unittest.TestCase):
+ def setUp(self):
+ cleanUp()
+
+ def tearDown(self):
+ cleanUp()
+
+ def _callFUT(self, request):
+ from repoze.bfg.i18n import get_localizer
+ return get_localizer(request)
+
+ def test_no_registry_on_request(self):
+ request = DummyRequest()
+ request.bfg_localizer = '123'
+ result = self._callFUT(request)
+ self.assertEqual(result, '123')
+
+ def test_with_registry_on_request(self):
+ from repoze.bfg.threadlocal import get_current_registry
+ registry = get_current_registry()
+ request = DummyRequest()
+ request.bfg_localizer = '123'
+ request.registry = registry
+ result = self._callFUT(request)
+ self.assertEqual(result, '123')
+
+ def test_locale_on_request(self):
+ request = DummyRequest()
+ request.bfg_localizer = 'abc'
+ result = self._callFUT(request)
+ self.assertEqual(result, 'abc')
+
+ def test_locale_from_registry(self):
+ from repoze.bfg.threadlocal import get_current_registry
+ from repoze.bfg.interfaces import ILocalizer
+ registry = get_current_registry()
+ locale = 'abc'
+ registry.registerUtility(locale, ILocalizer, name='en')
+ request = DummyRequest()
+ request.bfg_locale_name = 'en'
+ result = self._callFUT(request)
+ self.assertEqual(result, 'abc')
+
+ def test_locale_from_mo(self):
+ import os
+ from repoze.bfg.threadlocal import get_current_registry
+ from repoze.bfg.interfaces import ITranslationDirectories
+ from repoze.bfg.i18n import Localizer
+ registry = get_current_registry()
+ here = os.path.dirname(__file__)
+ localedir = os.path.join(here, 'fixtures', 'locale')
+ localedirs = [localedir]
+ registry.registerUtility(localedirs, ITranslationDirectories)
+ request = DummyRequest()
+ request.bfg_locale_name = 'de'
+ result = self._callFUT(request)
+ self.assertEqual(result.__class__, Localizer)
+ self.assertEqual(result.translate('Approve', 'deformsite'),
+ 'Genehmigen')
+ self.assertEqual(result.translate('Approve'), 'Approve')
+ self.failUnless(hasattr(result, 'pluralize'))
+
+ def test_locale_from_mo_bad_mo(self):
+ import os
+ from repoze.bfg.threadlocal import get_current_registry
+ from repoze.bfg.interfaces import ITranslationDirectories
+ from repoze.bfg.i18n import Localizer
+ registry = get_current_registry()
+ here = os.path.dirname(__file__)
+ localedir = os.path.join(here, 'fixtures', 'locale')
+ localedirs = [localedir]
+ registry.registerUtility(localedirs, ITranslationDirectories)
+ request = DummyRequest()
+ request.bfg_locale_name = 'be'
+ result = self._callFUT(request)
+ self.assertEqual(result.__class__, Localizer)
+ self.assertEqual(result.translate('Approve', 'deformsite'),
+ 'Approve')
+
+class Test_default_locale_negotiator(unittest.TestCase):
+ def setUp(self):
+ cleanUp()
+
+ def tearDown(self):
+ cleanUp()
+
+ def _callFUT(self, request):
+ from repoze.bfg.i18n import default_locale_negotiator
+ return default_locale_negotiator(request)
+
+ def test_from_settings(self):
+ from repoze.bfg.interfaces import ISettings
+ from repoze.bfg.threadlocal import get_current_registry
+ settings = {'default_locale_name':'dude'}
+ registry = get_current_registry()
+ registry.registerUtility(settings, ISettings)
+ request = DummyRequest()
+ result = self._callFUT(request)
+ self.assertEqual(result, 'dude')
+
+ def test_settings_empty(self):
+ request = DummyRequest()
+ result = self._callFUT(request)
+ self.assertEqual(result, 'en')
+
+ def test_from_params(self):
+ request = DummyRequest()
+ request.params['locale'] = 'foo'
+ result = self._callFUT(request)
+ self.assertEqual(result, 'foo')
+
+class TestTranslations(unittest.TestCase):
+ def _getTargetClass(self):
+ from repoze.bfg.i18n import Translations
+ return Translations
+
+ def _makeOne(self):
+ messages1 = [
+ ('foo', 'Voh'),
+ (('foo1', 1), 'Voh1'),
+ ]
+ messages2 = [
+ ('foo', 'VohD'),
+ (('foo1', 1), 'VohD1'),
+ ]
+
+ klass = self._getTargetClass()
+
+ translations1 = klass(None, domain='messages')
+ translations1._catalog = dict(messages1)
+ translations1.plural = lambda *arg: 1
+ translations2 = klass(None, domain='messages1')
+ translations2._catalog = dict(messages2)
+ translations2.plural = lambda *arg: 1
+ translations = translations1.add(translations2, merge=False)
+ return translations
+
+ def test_load_domain_None(self):
+ import gettext
+ import os
+ here = os.path.dirname(__file__)
+ localedir = os.path.join(here, 'fixtures', 'locale')
+ locales = ['de', 'en']
+ klass = self._getTargetClass()
+ result = klass.load(localedir, locales, domain=None)
+ self.assertEqual(result.__class__, gettext.NullTranslations)
+
+ def test_load_found_locale_and_domain(self):
+ import os
+ here = os.path.dirname(__file__)
+ localedir = os.path.join(here, 'fixtures', 'locale')
+ locales = ['de', 'en']
+ klass = self._getTargetClass()
+ result = klass.load(localedir, locales, domain='deformsite')
+ self.assertEqual(result.__class__, klass)
+
+ def test_load_found_locale_and_domain_locale_is_string(self):
+ import os
+ here = os.path.dirname(__file__)
+ localedir = os.path.join(here, 'fixtures', 'locale')
+ locales = 'de'
+ klass = self._getTargetClass()
+ result = klass.load(localedir, locales, domain='deformsite')
+ self.assertEqual(result.__class__, klass)
+
+ def test___repr__(self):
+ inst = self._makeOne()
+ result = repr(inst)
+ self.assertEqual(result, '<Translations: "None">')
+
+ def test_merge_not_gnutranslations(self):
+ inst = self._makeOne()
+ self.assertEqual(inst.merge(None), inst)
+
+ def test_merge_gnutranslations(self):
+ inst = self._makeOne()
+ inst2 = self._makeOne()
+ inst2._catalog['a'] = 'b'
+ inst.merge(inst2)
+ self.assertEqual(inst._catalog['a'], 'b')
+
+ def test_add_different_domain_merge_true_notexisting(self):
+ inst = self._makeOne()
+ inst2 = self._makeOne()
+ inst2.domain = 'domain2'
+ inst.add(inst2)
+ self.assertEqual(inst._domains['domain2'], inst2)
+
+ def test_add_different_domain_merge_true_existing(self):
+ inst = self._makeOne()
+ inst2 = self._makeOne()
+ inst3 = self._makeOne()
+ inst2.domain = 'domain2'
+ inst2._catalog['a'] = 'b'
+ inst3.domain = 'domain2'
+ inst._domains['domain2'] = inst3
+ inst.add(inst2)
+ self.assertEqual(inst._domains['domain2'], inst3)
+ self.assertEqual(inst3._catalog['a'], 'b')
+
+ def test_add_same_domain_merge_true(self):
+ inst = self._makeOne()
+ inst2 = self._makeOne()
+ inst2._catalog['a'] = 'b'
+ inst.add(inst2)
+ self.assertEqual(inst._catalog['a'], 'b')
+
+ def test_dgettext(self):
+ t = self._makeOne()
+ self.assertEqual(t.dgettext('messages', 'foo'), 'Voh')
+ self.assertEqual(t.dgettext('messages1', 'foo'), 'VohD')
+
+ def test_ldgettext(self):
+ t = self._makeOne()
+ self.assertEqual(t.ldgettext('messages', 'foo'), 'Voh')
+ self.assertEqual(t.ldgettext('messages1', 'foo'), 'VohD')
+
+ def test_dugettext(self):
+ t = self._makeOne()
+ self.assertEqual(t.dugettext('messages', 'foo'), 'Voh')
+ self.assertEqual(t.dugettext('messages1', 'foo'), 'VohD')
+
+ def test_dngettext(self):
+ t = self._makeOne()
+ self.assertEqual(t.dngettext('messages', 'foo1', 'foos1', 1), 'Voh1')
+ self.assertEqual(t.dngettext('messages1', 'foo1', 'foos1', 1), 'VohD1')
+
+ def test_ldngettext(self):
+ t = self._makeOne()
+ self.assertEqual(t.ldngettext('messages', 'foo1', 'foos1', 1), 'Voh1')
+ self.assertEqual(t.ldngettext('messages1', 'foo1', 'foos1', 1), 'VohD1')
+
+ def test_dungettext(self):
+ t = self._makeOne()
+ self.assertEqual(t.dungettext('messages', 'foo1', 'foos1', 1), 'Voh1')
+ self.assertEqual(t.dungettext('messages1', 'foo1', 'foos1', 1), 'VohD1')
+
+
+class DummyRequest(object):
+ def __init__(self):
+ self.params = {}
+
+def dummy_negotiator(request):
+ return 'bogus'
+
+class DummyTranslations(object):
+ def ugettext(self, text):
+ return text
+
+ def ungettext(self, singular, plural, n):
+ return singular
diff --git a/repoze/bfg/tests/test_zcml.py b/repoze/bfg/tests/test_zcml.py
index 7f42e9f09..5fdf25f33 100644
--- a/repoze/bfg/tests/test_zcml.py
+++ b/repoze/bfg/tests/test_zcml.py
@@ -1041,6 +1041,56 @@ class TestUtilityDirective(unittest.TestCase):
self.assertEqual(utility['args'], (component, IFactory, '', None))
self.assertEqual(utility['kw'], {})
+class TestTranslationDirDirective(unittest.TestCase):
+ def setUp(self):
+ testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def _callFUT(self, *arg, **kw):
+ from repoze.bfg.zcml import translationdir
+ return translationdir(*arg, **kw)
+
+ def test_it(self):
+ from repoze.bfg.configuration import Configurator
+ context = DummyContext()
+ tdir = 'repoze.bfg.tests.fixtures:locale'
+ self._callFUT(context, tdir)
+ actions = context.actions
+ self.assertEqual(len(actions), 1)
+ action = context.actions[0]
+ self.assertEqual(action['discriminator'], ('tdir', tdir))
+ self.assertEqual(action['callable'].im_func,
+ Configurator.add_translation_dirs.im_func)
+ self.assertEqual(action['args'], (tdir,))
+ action['callable'](*action['args']) # doesn't blow up
+
+class TestLocaleNegotiatorDirective(unittest.TestCase):
+ def setUp(self):
+ testing.setUp()
+
+ def tearDown(self):
+ testing.tearDown()
+
+ def _callFUT(self, *arg, **kw):
+ from repoze.bfg.zcml import localenegotiator
+ return localenegotiator(*arg, **kw)
+
+ def test_it(self):
+ from repoze.bfg.configuration import Configurator
+ context = DummyContext()
+ dummy_negotiator = object()
+ self._callFUT(context, dummy_negotiator)
+ actions = context.actions
+ self.assertEqual(len(actions), 1)
+ action = context.actions[0]
+ self.assertEqual(action['discriminator'], 'lnegotiator')
+ self.assertEqual(action['callable'].im_func,
+ Configurator.set_locale_negotiator.im_func)
+ self.assertEqual(action['args'], (dummy_negotiator,))
+ action['callable'](*action['args']) # doesn't blow up
+
class TestLoadZCML(unittest.TestCase):
def setUp(self):
testing.setUp()
diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py
index 7ba82bfe7..3e8bb0c50 100644
--- a/repoze/bfg/zcml.py
+++ b/repoze/bfg/zcml.py
@@ -585,6 +585,41 @@ def scan(_context, package):
args=(package, None, _context.info)
)
+class ITranslationDirDirective(Interface):
+ dir = TextLine(
+ title=u"Add a translation directory",
+ description=(u"Add a translation directory"),
+ required=True,
+ )
+
+def translationdir(_context, dir):
+ path = path_spec(_context, dir)
+ reg = get_current_registry()
+ config = Configurator(reg, package=_context.package)
+
+ _context.action(
+ discriminator = ('tdir', path),
+ callable=config.add_translation_dirs,
+ args = (dir,),
+ )
+
+class ILocaleNegotiatorDirective(Interface):
+ negotiator = GlobalObject(
+ title=u"Configure a locale negotiator",
+ description=(u'Configure a locale negotiator'),
+ required=True,
+ )
+
+def localenegotiator(_context, negotiator):
+ reg = get_current_registry()
+ config = Configurator(reg, package=_context.package)
+
+ _context.action(
+ discriminator = 'lnegotiator',
+ callable=config.set_locale_negotiator,
+ args = (negotiator,)
+ )
+
class IAdapterDirective(Interface):
"""
Register an adapter