diff options
| -rw-r--r-- | CHANGES.txt | 2 | ||||
| -rw-r--r-- | TODO.txt | 2 | ||||
| -rw-r--r-- | docs/api.rst | 1 | ||||
| -rw-r--r-- | docs/api/i18n.rst | 13 | ||||
| -rw-r--r-- | docs/glossary.rst | 24 | ||||
| -rw-r--r-- | docs/index.rst | 1 | ||||
| -rw-r--r-- | docs/latexindex.rst | 1 | ||||
| -rw-r--r-- | docs/narr/i18n.rst | 36 | ||||
| -rw-r--r-- | docs/narr/views.rst | 5 | ||||
| -rw-r--r-- | repoze/bfg/configuration.py | 31 | ||||
| -rw-r--r-- | repoze/bfg/i18n.py | 87 | ||||
| -rw-r--r-- | repoze/bfg/interfaces.py | 10 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_configuration.py | 17 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_i18n.py | 175 |
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 @@ -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 + + |
