summaryrefslogtreecommitdiff
path: root/repoze/bfg/i18n.py
blob: adbdb1db6624839b501cb65dc678ccda74da9962 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
##############################################################################
#
# Copyright (c) 2004 Zope Corporation and Contributors.
# All Rights Reserved.
#
# This software is subject to the provisions of the Zope Public License,
# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE.
#
##############################################################################

import re

from zope.interface import implements
from zope.interface import classProvides

from zope.i18nmessageid import Message

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

class TranslationString(Message):
    """ The constructor for a :term:`translation string`.  This
    constructor accepts one required argument named ``text``.
    ``text`` must be the default text of the translation string,
    optionally including replacement markers such as ``${foo}``.

    Optional keyword arguments to the TranslationString constructor
    include ``msgid``, ``mapping`` and ``domain``.

    ``mapping``, if supplied, must be a dictionarylike object which
    represents the replacement values for any replacement markers
    found within the ``text`` value of this

    ``msgid`` represents an explicit :term:`message identifier` for
    this translation string.  Usually, the ``text`` of a translation
    string serves as its message identifier.  However, using this
    option you can pass an explicit message identifier, usually a
    simple string.  This is useful when the ``text`` of a translation
    string is too complicated or too long to be used as a translation
    key. If ``msgid`` is ``None`` (the default), the ``msgid`` value
    used by this translation string will be assumed to be the value of
    ``text``.

    ``domain`` represents the :term:`translation domain`.  By default,
    the translation domain is ``None``, indicating that this
    translation string is associated with no translation domain.

    After a translation string is constructed, its ``text`` value is
    available as the ``default`` attribute of the object, the
    ``msgid`` is available as the ``msgid`` attribute of the object,
    the ``domain`` is available as the ``domain`` attribute, and the
    ``mapping`` is available as the ``mapping`` attribute.
    """
    def __new__(cls, text, mapping=None, msgid=None, domain=None):
        if msgid is None:
            msgid = text
        return Message.__new__(cls, msgid, domain=domain, default=text,
                               mapping=mapping)

class TranslationStringFactory(object):
    """ Create a factory which will generate translation strings
    without requiring that each call to the factory be passed a
    ``domain`` value.  The ``domain`` value passed to this class'
    constructor will be used as the ``domain`` values of
    :class:`repoze.bfg.i18n.TranslationString` objects generated by
    the ``__call__`` of this class.  The ``text``, ``mapping``, and
    ``msgid`` values provided to ``__call__`` have the meaning as
    described by the constructor of the
    :class:`repoze.bfg.i18n.TranslationString`"""
    def __init__(self, domain):
        self.domain = domain

    def __call__(self, text, mapping=None, msgid=None):
        return TranslationString(text, mapping=mapping, msgid=msgid,
                                 domain=self.domain)
                               
bfg_tstr = TranslationStringFactory('bfg')
bfg_tstr.__doc__ = """\
    A :class:`repoze.bfg.i18n.TranslationStringFactory` instance with
    a default ``domain`` value of ``bfg``.  This object may be called
    with the values ``text``, ``mapping``, and ``msgid`` as per the
    documentation of the
    :class:`repoze.bfg.i18n.TranslationStringFactory` class."""

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 a very simple
    default 'interpolation only' translator.

    Note that the translation factory will only be constructed once
    per request instance.
    """
    
    translator = getattr(request, '_bfg_translator', None)

    if translator is None:

        if translator_factory is None:
            try:
                reg = request.registry
            except AttributeError:
                reg = get_current_registry()
            translator_factory = reg.queryUtility(
                ITranslatorFactory,
                default=InterpolationOnlyTranslator)

        translator = translator_factory(request)

        try:
            request._bfg_translator = translator
        except AttributeError: # pragma: no cover
            pass # it's only a cache

    return translator

class InterpolationOnlyTranslator(object):

    """ A class implementing the :term:`translator factory` interface
    as its constructor and the :term:`translator` interface as its
    ``__call__`` method.  Useful as a minimal translator factory, this
    class only does basic interpolation of mapping values; it does not
    actually do any language translations and ignores all
    :term:`translation domain` information. To use it explicitly::

        from repoze.bfg.configuration import Configurator
        from repoze.bfg.i18n import InterpolationOnlyTranslator
        config = Configurator(translator_factory=InterpolationOnlyTranslator)

    An instance of this class is returned by
    :func:`repoze.bfg.i18n.get_translator` if no explicit translator
    factory is registered.
    """
    classProvides(ITranslatorFactory)
    implements(ITranslator)
    def __init__(self, request):
        self.request = request

    def __call__(self, message):
        mapping = getattr(message, 'mapping', None)
        return interpolate(message, mapping)

class ChameleonTranslate(object):
    """ Registered as a Chameleon translate function 'under the hood'
    to allow our ITranslator and ITranslatorFactory to drive template
    translation."""
    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
        if default is None:
            default = text
        if mapping is None:
            mapping = {}
        if not hasattr(text, 'mapping'):
            text = TranslationString(default, mapping=mapping, msgid=text, 
                                     domain=domain)
        translator = self.make_translator(target_language)
        return translator(text)

    def make_translator(self, target_language):
        translator = None
        request = get_current_request()
        if request is not None:
            translator = get_translator(request, self.translator_factory)
        if translator is None:
            translator = InterpolationOnlyTranslator(request)
        return translator
        
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):
    """ Interpolate a string with one or more *replacement markers*
    (``${foo}`` or ``${bar}``).  Note that if a :term:`translation
    string` is passed to this function, it will be implicitly
    converted back to the Unicode object."""
    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)