diff options
| author | Chris McDonough <chrism@agendaless.com> | 2010-04-19 08:58:54 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2010-04-19 08:58:54 +0000 |
| commit | cdae91d72b7bfa5edb26e12e721891a3ce2f2aa7 (patch) | |
| tree | 5345a838f8f1aa868f8dc1d819ba1d034f9375c3 | |
| parent | f5c6c574ada26ec0b2766f5ca20bb2b5b7393ec5 (diff) | |
| download | pyramid-cdae91d72b7bfa5edb26e12e721891a3ce2f2aa7.tar.gz pyramid-cdae91d72b7bfa5edb26e12e721891a3ce2f2aa7.tar.bz2 pyramid-cdae91d72b7bfa5edb26e12e721891a3ce2f2aa7.zip | |
Pass along translate function to templates.
| -rw-r--r-- | CHANGES.txt | 2 | ||||
| -rw-r--r-- | docs/api/i18n.rst | 2 | ||||
| -rw-r--r-- | repoze/bfg/chameleon_text.py | 8 | ||||
| -rw-r--r-- | repoze/bfg/chameleon_zpt.py | 8 | ||||
| -rw-r--r-- | repoze/bfg/configuration.py | 4 | ||||
| -rw-r--r-- | repoze/bfg/i18n.py | 96 | ||||
| -rw-r--r-- | repoze/bfg/interfaces.py | 12 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_chameleon_text.py | 20 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_chameleon_zpt.py | 20 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_configuration.py | 4 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_i18n.py | 63 | ||||
| -rw-r--r-- | setup.py | 2 |
12 files changed, 190 insertions, 51 deletions
diff --git a/CHANGES.txt b/CHANGES.txt index a0d36c4a9..5021093bf 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -74,6 +74,8 @@ Dependencies - A new install-time dependency on the ``venusian`` distribution was added. +- Chameleon 1.2.2 or better is now required (internationalization). + Internal -------- diff --git a/docs/api/i18n.rst b/docs/api/i18n.rst index 936f8aced..48c6d09a4 100644 --- a/docs/api/i18n.rst +++ b/docs/api/i18n.rst @@ -5,7 +5,7 @@ .. automodule:: repoze.bfg.i18n - .. autofunction:: get_translator + .. autofunction:: get_translator(request) See :ref:`i18n_chapter` for more information about using :mod:`repoze.bfg` internationalization services in an application. 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 862f8b3a6..a98b3e7d6 100644 --- a/repoze/bfg/configuration.py +++ b/repoze/bfg/configuration.py @@ -17,6 +17,7 @@ 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 @@ -46,6 +47,7 @@ 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 ChameleonTranslate from repoze.bfg.log import make_stream_logger from repoze.bfg.path import caller_package from repoze.bfg.registry import Registry @@ -1313,6 +1315,8 @@ class Configurator(object): """ Set ``factory`` up as the current application :term:`translator factory` (for internationalization)""" self.registry.registerUtility(factory, ITranslatorFactory) + ctranslate = ChameleonTranslate(factory) + 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 diff --git a/repoze/bfg/i18n.py b/repoze/bfg/i18n.py index bc0ae4423..6fa809bf6 100644 --- a/repoze/bfg/i18n.py +++ b/repoze/bfg/i18n.py @@ -1,32 +1,48 @@ +import re + 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.interfaces import IChameleonTranslate + 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 +def get_translator(request, translator_factory=None): + """ Return a :term:`translator` for the given request based on the + :term:`translator factory` registered for the current application + and the :term:`request` passed in as the request object. If no + translator factory was sent to the + :class:`repoze.bfg.configuration.Configurator` constructor at + application startup, this function will return ``None``. + Note that the translation factory will only be called once per + request instance. + """ + 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: + try: + reg = request.registry + except AttributeError: + reg = get_current_registry() + if reg is None: # pragma: no cover + return None # only in insane circumstances + 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 @@ -41,8 +57,8 @@ class InterpolationOnlyTranslator(object): self.request = request def __call__(self, message): - mapping = getattr(message, 'mapping', {}) #should be a TranslationString - return message % mapping + mapping = getattr(message, 'mapping', None) + return interpolate(message, mapping) class TranslationString(unicode): __slots__ = ('msgid', 'domain', 'mapping') @@ -61,27 +77,43 @@ class TranslationString(unicode): 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) +class ChameleonTranslate(object): + implements(IChameleonTranslate) + def __init__(self, translator_factory): + self.translator_factory = translator_factory + + def __call__(self, 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, self.translator_factory) + 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) +NAME_RE = r"[a-zA-Z][-a-zA-Z0-9_]*" + +_interp_regex = re.compile(r'(?<!\$)(\$(?:(%(n)s)|{(%(n)s)}))' + % ({'n': NAME_RE})) - - +def interpolate(text, mapping=None): + def replace(match): + whole, param1, param2 = match.groups() + return unicode(mapping.get(param1 or param2, whole)) + + if not text or not mapping: + return text + + return _interp_regex.sub(replace, text) diff --git a/repoze/bfg/interfaces.py b/repoze/bfg/interfaces.py index 92003a9f9..f09ad7e36 100644 --- a/repoze/bfg/interfaces.py +++ b/repoze/bfg/interfaces.py @@ -232,10 +232,16 @@ VH_ROOT_KEY = 'HTTP_X_VHM_ROOT' class ITranslatorFactory(Interface): """ Internal interface representing an i18n translator factory """ - def __call__(self, request): + def __call__(request): """ Return a translator """ class ITranslator(Interface): - def __call__(self, tstr): + def __call__(tstr): """ Return a translation based on the translation string tstr """ - + +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 """ + 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 b4f7600f5..efd58139f 100644 --- a/repoze/bfg/tests/test_configuration.py +++ b/repoze/bfg/tests/test_configuration.py @@ -1738,11 +1738,15 @@ class ConfiguratorTests(unittest.TestCase): def test_set_translator_factory(self): from repoze.bfg.interfaces import ITranslatorFactory + from repoze.bfg.interfaces import IChameleonTranslate def factory(): pass config = self._makeOne() config.set_translator_factory(factory) self.assertEqual(config.registry.getUtility(ITranslatorFactory), factory) + self.assertEqual( + config.registry.getUtility(IChameleonTranslate).translator_factory, + factory) def test_set_notfound_view(self): from zope.interface import implementedBy diff --git a/repoze/bfg/tests/test_i18n.py b/repoze/bfg/tests/test_i18n.py index 603f26a55..ebbbfd8d8 100644 --- a/repoze/bfg/tests/test_i18n.py +++ b/repoze/bfg/tests/test_i18n.py @@ -43,7 +43,7 @@ class TestInterpolationOnlyTranslator(unittest.TestCase): return InterpolationOnlyTranslator(request) def test_it(self): - message = DummyMessage('text %(a)s', mapping={'a':'1'}) + message = DummyMessage('text ${a}', mapping={'a':'1'}) translator = self._makeOne(None) result = translator(message) self.assertEqual(result, u'text 1') @@ -83,7 +83,7 @@ class TestTranslationString(unittest.TestCase): result = ts.__getstate__() self.assertEqual(result, (u'text', 'msgid', 'domain', 'mapping')) -class Test_chameleon_translate(unittest.TestCase): +class TestChameleonTranslate(unittest.TestCase): def setUp(self): request = DummyRequest() from repoze.bfg.configuration import Configurator @@ -94,21 +94,24 @@ class Test_chameleon_translate(unittest.TestCase): def tearDown(self): self.config.end() - def _callFUT(self, text, **kw): - from repoze.bfg.i18n import chameleon_translate - return chameleon_translate(text, **kw) + def _makeOne(self, factory): + from repoze.bfg.i18n import ChameleonTranslate + return ChameleonTranslate(factory) def test_text_None(self): - result = self._callFUT(None) + trans = self._makeOne(None) + result = trans(None) self.assertEqual(result, None) def test_no_current_request(self): self.config.manager.pop() - result = self._callFUT('text') + trans = self._makeOne(None) + result = trans('text') self.assertEqual(result, 'text') def test_with_current_request_no_translator(self): - result = self._callFUT('text') + trans = self._makeOne(None) + result = trans('text') self.assertEqual(result, 'text') self.assertEqual(self.request.chameleon_target_language, None) @@ -117,7 +120,8 @@ class Test_chameleon_translate(unittest.TestCase): translator = DummyTranslator() factory = DummyTranslatorFactory(translator) self.config.registry.registerUtility(factory, ITranslatorFactory) - result = self._callFUT('text') + trans = self._makeOne(None) + result = trans('text') self.assertEqual(result, 'text') self.assertEqual(self.request.chameleon_target_language, None) self.assertEqual(result.msgid, 'text') @@ -129,15 +133,50 @@ class Test_chameleon_translate(unittest.TestCase): 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') + trans = self._makeOne(None) + result = trans('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 Test_interpolate(unittest.TestCase): + def _callFUT(self, text, mapping=None): + from repoze.bfg.i18n import interpolate + return interpolate(text, mapping) + + def test_substitution(self): + mapping = {"name": "Zope", "version": 3} + result = self._callFUT(u"This is $name version ${version}.", mapping) + self.assertEqual(result, u'This is Zope version 3.') + + def test_subsitution_more_than_once(self): + mapping = {"name": "Zope", "version": 3} + result = self._callFUT( + u"This is $name version ${version}. ${name} $version!", + mapping) + self.assertEqual(result, u'This is Zope version 3. Zope 3!') + + def test_double_dollar_escape(self): + mapping = {"name": "Zope", "version": 3} + result = self._callFUT('$$name', mapping) + self.assertEqual(result, u'$$name') + + def test_missing_not_interpolated(self): + mapping = {"name": "Zope", "version": 3} + result = self._callFUT( + u"This is $name $version. $unknown $$name $${version}.", + mapping) + self.assertEqual(result, + u'This is Zope 3. $unknown $$name $${version}.') + + def test_missing_mapping(self): + result = self._callFUT(u"This is ${name}") + self.assertEqual(result, u'This is ${name}') + class DummyMessage(unicode): def __new__(cls, text, msgid=None, domain=None, mapping=None): self = unicode.__new__(cls, text) @@ -27,7 +27,7 @@ except IOError: README = CHANGES = '' install_requires=[ - 'Chameleon', + 'Chameleon >= 1.2.2', 'Paste > 1.7', # temp version pin to prevent PyPi install failure :-( 'PasteDeploy', 'PasteScript', |
