summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.txt2
-rw-r--r--TODO.txt2
-rw-r--r--docs/api.rst1
-rw-r--r--docs/api/i18n.rst13
-rw-r--r--docs/glossary.rst24
-rw-r--r--docs/index.rst1
-rw-r--r--docs/latexindex.rst1
-rw-r--r--docs/narr/i18n.rst36
-rw-r--r--docs/narr/views.rst5
-rw-r--r--repoze/bfg/configuration.py31
-rw-r--r--repoze/bfg/i18n.py87
-rw-r--r--repoze/bfg/interfaces.py10
-rw-r--r--repoze/bfg/tests/test_configuration.py17
-rw-r--r--repoze/bfg/tests/test_i18n.py175
14 files changed, 396 insertions, 9 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index fdc67b191..a0d36c4a9 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -22,7 +22,7 @@ Features
returned.
Other normal view predicates can also be used in combination with an
- exception view registration:
+ exception view registration::
from repoze.bfg.view import bfg_view
from repoze.bfg.exceptions import NotFound
diff --git a/TODO.txt b/TODO.txt
index b16d806b2..0d19e8273 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -17,3 +17,5 @@
- Provide a webob.Response class facade for forward compat.
+- Replace default_notfound_view and default_forbidden_view with better
+ exception view candidates.
diff --git a/docs/api.rst b/docs/api.rst
index 2bd5fca01..a97c79fa9 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -15,6 +15,7 @@ documentation is organized alphabetically by module name.
api/configuration
api/events
api/exceptions
+ api/i18n
api/interfaces
api/location
api/paster
diff --git a/docs/api/i18n.rst b/docs/api/i18n.rst
new file mode 100644
index 000000000..936f8aced
--- /dev/null
+++ b/docs/api/i18n.rst
@@ -0,0 +1,13 @@
+.. _i18n_module:
+
+:mod:`repoze.bfg.i18n`
+----------------------
+
+.. automodule:: repoze.bfg.i18n
+
+ .. autofunction:: get_translator
+
+See :ref:`i18n_chapter` for more information about using
+:mod:`repoze.bfg` internationalization services in an application.
+
+
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 14bd4fc44..537af06b9 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -667,3 +667,27 @@ Glossary
at import time, the action usually taken by the decorator is
deferred until a separate "scan" phase. :mod:`repoze.bfg` relies
on Venusian to provide a basis for its :term:`scan` feature.
+
+ Translation String
+ An instance of the :class:`repoze.bfg.i18n.TranslationString`,
+ which is a class that behaves like a Unicode string, but has
+ several extra attributes such as ``domain``, ``msgid``, and
+ ``mapping`` for use during translation. Translation strings are
+ usually created by hand within software, but are sometimes
+ created on the behalf of the system for automatic template
+ translation. For more information, see :ref:`i18n_chapter`.
+
+ Translator
+ A callable which receives a :term:`translation string` and
+ returns a translated Unicode object for the purposes of
+ internationalization. A translator may be suppled to a
+ :mod:`repoze.bfg` application at startup time indirectly via the
+ ``translator_factory`` function, which is a :term:`translator
+ factory`.
+
+ Translator Factory
+ A callable which receives a :term:`request` and returns a
+ :term:`translator` for the purposes of internationalization. A
+ translator factory may be suppled to a :mod:`repoze.bfg`
+ application at startup time via the ``translator_factory``
+ function.
diff --git a/docs/index.rst b/docs/index.rst
index ee32cf691..c1b185352 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -53,6 +53,7 @@ Narrative documentation in chapter form explaining how to use
narr/templates
narr/models
narr/security
+ narr/i18n
narr/vhosting
narr/events
narr/environment
diff --git a/docs/latexindex.rst b/docs/latexindex.rst
index 45438e939..5850ee06c 100644
--- a/docs/latexindex.rst
+++ b/docs/latexindex.rst
@@ -42,6 +42,7 @@ Narrative Documentation
narr/templates
narr/models
narr/security
+ narr/i18n
narr/vhosting
narr/events
narr/environment
diff --git a/docs/narr/i18n.rst b/docs/narr/i18n.rst
new file mode 100644
index 000000000..e8ad0b554
--- /dev/null
+++ b/docs/narr/i18n.rst
@@ -0,0 +1,36 @@
+.. index::
+ single: i18n
+ single: internationalization
+
+.. _i18n_chapter:
+
+Using Internationalization
+==========================
+
+Setting Up Translation
+----------------------
+
+Pass a :term:`translator factory` object to your application's
+:mod:`repoze.bfg.configuration.Configurator` by supplying it with a
+``translator_factory`` argument. A translator factory is an object
+which accepts a :term:`request` and which returns a callable. The
+callable returned by a translator factory is a :term:`translator`; it
+must accept a single positional argument which represents a
+:term:`translation string` and should return a fully expanded
+translation of the translation string.
+
+The exact operation of a translator is left to the implementor of a
+particular translation factory.
+
+Obtaining the Translator via :func:`repoze.bfg.i18n.get_translator`
+--------------------------------------------------------------------
+
+If you need to perform translation "by hand" in an application, use
+the :func:`repoze.bfg.i18n.get_translator` function to obtain a
+translator. A translator is a callable which accepts either a
+:term:`translation string` or a normal Unicode object and which
+returns a Unicode object representing the translation.
+
+XXX
+
+
diff --git a/docs/narr/views.rst b/docs/narr/views.rst
index a24e4b7b5..eebaa63de 100644
--- a/docs/narr/views.rst
+++ b/docs/narr/views.rst
@@ -840,6 +840,11 @@ In all cases, the message provided to the exception constructor is
made available to the view which :mod:`repoze.bfg` invokes as
``request.exception.args[0]``.
+.. index::
+ single: exception views
+
+.. _exception_views:
+
Exception Views
~~~~~~~~~~~~~~~~
diff --git a/repoze/bfg/configuration.py b/repoze/bfg/configuration.py
index 4673479da..862f8b3a6 100644
--- a/repoze/bfg/configuration.py
+++ b/repoze/bfg/configuration.py
@@ -31,6 +31,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 ITranslatorFactory
from repoze.bfg.interfaces import ITraverser
from repoze.bfg.interfaces import IView
from repoze.bfg.interfaces import IViewClassifier
@@ -135,14 +136,23 @@ 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.
+
+ If ``translator_factory`` is passed, it should be a
+ :term:`translator factory` object. A translator factory is an
+ object which accepts a request and which returns a translation
+ function. The translation function accepts a :term:`translation
+ string` and returns a Unicode object representing the translated
+ string. The default for ``translator_factory`` is ``None``,
+ meaning that no translation is performed during template
+ rendering. """
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, translator_factory=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,
+ translator_factory=translator_factory)
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,
+ translator_factory=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 translator_factory is not None:
+ self.set_translator_factory(translator_factory)
# 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,11 @@ class Configurator(object):
return self.add_view(bwcompat_view, context=NotFound,
wrapper=wrapper, _info=_info)
+ def set_translator_factory(self, factory):
+ """ Set ``factory`` up as the current application
+ :term:`translator factory` (for internationalization)"""
+ self.registry.registerUtility(factory, ITranslatorFactory)
+
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..bc0ae4423
--- /dev/null
+++ b/repoze/bfg/i18n.py
@@ -0,0 +1,87 @@
+from zope.interface import implements
+from zope.interface import classProvides
+
+from repoze.bfg.interfaces import ITranslator
+from repoze.bfg.interfaces import ITranslatorFactory
+from repoze.bfg.threadlocal import get_current_registry
+from repoze.bfg.threadlocal import get_current_request
+
+def get_translator(request):
+ try:
+ reg = request.registry
+ except AttributeError:
+ reg = get_current_registry()
+
+ if reg is None: # pragma: no cover
+ return None # only in insane circumstances
+
+ translator = getattr(request, '_bfg_translator', None)
+
+ if translator is False:
+ return None
+
+ if translator is None:
+ translator_factory = reg.queryUtility(ITranslatorFactory)
+ if translator_factory is None:
+ request_value = False
+ else:
+ translator = translator_factory(request)
+ request_value = translator
+ try:
+ request._bfg_translator = request_value
+ except AttributeError: # pragma: no cover
+ pass # it's only a cache
+
+ return translator
+
+class InterpolationOnlyTranslator(object):
+ classProvides(ITranslatorFactory)
+ implements(ITranslator)
+ def __init__(self, request):
+ self.request = request
+
+ def __call__(self, message):
+ mapping = getattr(message, 'mapping', {}) #should be a TranslationString
+ return message % mapping
+
+class TranslationString(unicode):
+ __slots__ = ('msgid', 'domain', 'mapping')
+ def __new__(cls, text, msgid=None, domain=None, mapping=None):
+ self = unicode.__new__(cls, text)
+ if msgid is None:
+ msgid = unicode(text)
+ self.msgid = msgid
+ self.domain = domain
+ self.mapping = mapping or {}
+ return self
+
+ def __reduce__(self):
+ return self.__class__, self.__getstate__()
+
+ def __getstate__(self):
+ return unicode(self), self.msgid, self.domain, self.mapping
+
+def chameleon_translate(text, domain=None, mapping=None, context=None,
+ target_language=None, default=None):
+ if text is None:
+ return None
+ translator = None
+ request = get_current_request()
+ if request is not None:
+ request.chameleon_target_language = target_language
+ translator = get_translator(request)
+ if default is None:
+ default = text
+ if mapping is None:
+ mapping = {}
+ if translator is None:
+ return unicode(default) % mapping
+ if not isinstance(text, TranslationString):
+ text = TranslationString(default, msgid=text, domain=domain,
+ mapping=mapping)
+ return translator(text)
+
+
+
+
+
diff --git a/repoze/bfg/interfaces.py b/repoze/bfg/interfaces.py
index 40d29348c..92003a9f9 100644
--- a/repoze/bfg/interfaces.py
+++ b/repoze/bfg/interfaces.py
@@ -229,3 +229,13 @@ 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 ITranslatorFactory(Interface):
+ """ Internal interface representing an i18n translator factory """
+ def __call__(self, request):
+ """ Return a translator """
+
+class ITranslator(Interface):
+ def __call__(self, tstr):
+ """ Return a translation based on the translation string tstr """
+
diff --git a/repoze/bfg/tests/test_configuration.py b/repoze/bfg/tests/test_configuration.py
index 99d564b91..b4f7600f5 100644
--- a/repoze/bfg/tests/test_configuration.py
+++ b/repoze/bfg/tests/test_configuration.py
@@ -265,6 +265,15 @@ class ConfiguratorTests(unittest.TestCase):
self.assertEqual(reg.getUtility(IRendererFactory, 'yeah'),
renderer)
+ def test_setup_registry_translator_factory(self):
+ from repoze.bfg.registry import Registry
+ from repoze.bfg.interfaces import ITranslatorFactory
+ factory = object()
+ reg = Registry()
+ config = self._makeOne(reg)
+ config.setup_registry(translator_factory=factory)
+ self.assertEqual(reg.getUtility(ITranslatorFactory), factory)
+
def test_add_settings_settings_already_registered(self):
from repoze.bfg.registry import Registry
from repoze.bfg.interfaces import ISettings
@@ -1727,6 +1736,14 @@ class ConfiguratorTests(unittest.TestCase):
request = self._makeRequest(config)
self.assertEqual(wrapped(None, request).__class__, StaticURLParser)
+ def test_set_translator_factory(self):
+ from repoze.bfg.interfaces import ITranslatorFactory
+ def factory(): pass
+ config = self._makeOne()
+ config.set_translator_factory(factory)
+ self.assertEqual(config.registry.getUtility(ITranslatorFactory),
+ factory)
+
def test_set_notfound_view(self):
from zope.interface import implementedBy
from repoze.bfg.interfaces import IRequest
diff --git a/repoze/bfg/tests/test_i18n.py b/repoze/bfg/tests/test_i18n.py
new file mode 100644
index 000000000..603f26a55
--- /dev/null
+++ b/repoze/bfg/tests/test_i18n.py
@@ -0,0 +1,175 @@
+import unittest
+
+class Test_get_translator(unittest.TestCase):
+ def _callFUT(self, request):
+ from repoze.bfg.i18n import get_translator
+ return get_translator(request)
+
+ def test_no_ITranslatorFactory(self):
+ request = DummyRequest()
+ request.registry = DummyRegistry()
+ translator = self._callFUT(request)
+ self.assertEqual(translator, None)
+
+ def test_no_registry_on_request(self):
+ request = DummyRequest()
+ translator = self._callFUT(request)
+ self.assertEqual(translator, None)
+
+ def test_with_ITranslatorFactory_from_registry(self):
+ request = DummyRequest()
+ tfactory = DummyTranslatorFactory()
+ request.registry = DummyRegistry(tfactory)
+ translator = self._callFUT(request)
+ self.assertEqual(translator.request, request)
+
+ def test_with_ITranslatorFactory_from_request_cache(self):
+ request = DummyRequest()
+ request.registry = DummyRegistry()
+ request._bfg_translator = 'abc'
+ translator = self._callFUT(request)
+ self.assertEqual(translator, 'abc')
+
+ def test_with_ITranslatorFactory_from_request_neg_cache(self):
+ request = DummyRequest()
+ request.registry = DummyRegistry()
+ request._bfg_translator = False
+ translator = self._callFUT(request)
+ self.assertEqual(translator, None)
+
+class TestInterpolationOnlyTranslator(unittest.TestCase):
+ def _makeOne(self, request):
+ from repoze.bfg.i18n import InterpolationOnlyTranslator
+ return InterpolationOnlyTranslator(request)
+
+ def test_it(self):
+ message = DummyMessage('text %(a)s', mapping={'a':'1'})
+ translator = self._makeOne(None)
+ result = translator(message)
+ self.assertEqual(result, u'text 1')
+
+class TestTranslationString(unittest.TestCase):
+ def _getTargetClass(self):
+ from repoze.bfg.i18n import TranslationString
+ return TranslationString
+
+ def _makeOne(self, text, **kw):
+ return self._getTargetClass()(text, **kw)
+
+ def test_ctor_defaults(self):
+ ts = self._makeOne('text')
+ self.assertEqual(ts, u'text')
+ self.assertEqual(ts.msgid, u'text')
+ self.assertEqual(ts.domain, None)
+ self.assertEqual(ts.mapping, {})
+
+ def test_ctor_nondefaults(self):
+ ts = self._makeOne(
+ 'text', msgid='msgid', domain='domain', mapping='mapping')
+ self.assertEqual(ts, u'text')
+ self.assertEqual(ts.msgid, 'msgid')
+ self.assertEqual(ts.domain, 'domain')
+ self.assertEqual(ts.mapping, 'mapping')
+
+ def test___reduce__(self):
+ klass = self._getTargetClass()
+ ts = self._makeOne('text')
+ result = ts.__reduce__()
+ self.assertEqual(result, (klass, (u'text', u'text', None, {})))
+
+ def test___getstate__(self):
+ ts = self._makeOne(
+ 'text', msgid='msgid', domain='domain', mapping='mapping')
+ result = ts.__getstate__()
+ self.assertEqual(result, (u'text', 'msgid', 'domain', 'mapping'))
+
+class Test_chameleon_translate(unittest.TestCase):
+ def setUp(self):
+ request = DummyRequest()
+ from repoze.bfg.configuration import Configurator
+ self.config = Configurator()
+ self.config.begin(request=request)
+ self.request = request
+
+ def tearDown(self):
+ self.config.end()
+
+ def _callFUT(self, text, **kw):
+ from repoze.bfg.i18n import chameleon_translate
+ return chameleon_translate(text, **kw)
+
+ def test_text_None(self):
+ result = self._callFUT(None)
+ self.assertEqual(result, None)
+
+ def test_no_current_request(self):
+ self.config.manager.pop()
+ result = self._callFUT('text')
+ self.assertEqual(result, 'text')
+
+ def test_with_current_request_no_translator(self):
+ result = self._callFUT('text')
+ self.assertEqual(result, 'text')
+ self.assertEqual(self.request.chameleon_target_language, None)
+
+ def test_with_current_request_and_translator(self):
+ from repoze.bfg.interfaces import ITranslatorFactory
+ translator = DummyTranslator()
+ factory = DummyTranslatorFactory(translator)
+ self.config.registry.registerUtility(factory, ITranslatorFactory)
+ result = self._callFUT('text')
+ self.assertEqual(result, 'text')
+ self.assertEqual(self.request.chameleon_target_language, None)
+ self.assertEqual(result.msgid, 'text')
+ self.assertEqual(result.domain, None)
+ self.assertEqual(result.mapping, {})
+
+ def test_with_allargs(self):
+ from repoze.bfg.interfaces import ITranslatorFactory
+ translator = DummyTranslator()
+ factory = DummyTranslatorFactory(translator)
+ self.config.registry.registerUtility(factory, ITranslatorFactory)
+ result = self._callFUT('text', domain='domain', mapping={'a':'1'},
+ context=None, target_language='lang',
+ default='default')
+ self.assertEqual(self.request.chameleon_target_language, 'lang')
+ self.assertEqual(result, 'default')
+ self.assertEqual(result.msgid, 'text')
+ self.assertEqual(result.domain, 'domain')
+ self.assertEqual(result.mapping, {'a':'1'})
+
+class DummyMessage(unicode):
+ def __new__(cls, text, msgid=None, domain=None, mapping=None):
+ self = unicode.__new__(cls, text)
+ if msgid is None:
+ msgid = unicode(text)
+ self.msgid = msgid
+ self.domain = domain
+ self.mapping = mapping or {}
+ return self
+
+class DummyRequest(object):
+ pass
+
+class DummyRegistry(object):
+ def __init__(self, tfactory=None):
+ self.tfactory = tfactory
+
+ def queryUtility(self, iface):
+ return self.tfactory
+
+class DummyTranslator(object):
+ def __call__(self, message):
+ return message
+
+class DummyTranslatorFactory(object):
+ def __init__(self, translator=None):
+ self.translator = translator
+
+ def __call__(self, request):
+ self.request = request
+ if self.translator is None:
+ return self
+ return self.translator
+
+