summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2010-04-19 08:58:54 +0000
committerChris McDonough <chrism@agendaless.com>2010-04-19 08:58:54 +0000
commitcdae91d72b7bfa5edb26e12e721891a3ce2f2aa7 (patch)
tree5345a838f8f1aa868f8dc1d819ba1d034f9375c3
parentf5c6c574ada26ec0b2766f5ca20bb2b5b7393ec5 (diff)
downloadpyramid-cdae91d72b7bfa5edb26e12e721891a3ce2f2aa7.tar.gz
pyramid-cdae91d72b7bfa5edb26e12e721891a3ce2f2aa7.tar.bz2
pyramid-cdae91d72b7bfa5edb26e12e721891a3ce2f2aa7.zip
Pass along translate function to templates.
-rw-r--r--CHANGES.txt2
-rw-r--r--docs/api/i18n.rst2
-rw-r--r--repoze/bfg/chameleon_text.py8
-rw-r--r--repoze/bfg/chameleon_zpt.py8
-rw-r--r--repoze/bfg/configuration.py4
-rw-r--r--repoze/bfg/i18n.py96
-rw-r--r--repoze/bfg/interfaces.py12
-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.py4
-rw-r--r--repoze/bfg/tests/test_i18n.py63
-rw-r--r--setup.py2
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)
diff --git a/setup.py b/setup.py
index 3dbed2c2a..e5cf8d80b 100644
--- a/setup.py
+++ b/setup.py
@@ -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',