Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/translationstring/__init__.py : 28%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import re
2from gettext import NullTranslations
3from translationstring.compat import text_type
4from translationstring.compat import string_types
5from translationstring.compat import PY3
7NAME_RE = r"[a-zA-Z][-a-zA-Z0-9_]*"
9_interp_regex = re.compile(r'(?<!\$)(\$(?:(%(n)s)|{(%(n)s)}))'
10 % ({'n': NAME_RE}))
12CONTEXT_MASK = text_type('%s\x04%s')
14class TranslationString(text_type):
15 """
16 The constructor for a :term:`translation string`. A translation
17 string is a Unicode-like object that has some extra metadata.
19 This constructor accepts one required argument named ``msgid``.
20 ``msgid`` must be the :term:`message identifier` for the
21 translation string. It must be a ``unicode`` object or a ``str``
22 object encoded in the default system encoding.
24 Optional keyword arguments to this object's constructor include
25 ``domain``, ``default``, and ``mapping``.
27 ``domain`` represents the :term:`translation domain`. By default,
28 the translation domain is ``None``, indicating that this
29 translation string is associated with the default translation
30 domain (usually ``messages``).
32 ``default`` represents an explicit *default text* for this
33 translation string. Default text appears when the translation
34 string cannot be translated. Usually, the ``msgid`` of a
35 translation string serves double duty as its default text.
36 However, using this option you can provide a different default
37 text for this translation string. This feature is useful when the
38 default of a translation string is too complicated or too long to
39 be used as a message identifier. If ``default`` is provided, it
40 must be a ``unicode`` object or a ``str`` object encoded in the
41 default system encoding (usually means ASCII). If ``default`` is
42 ``None`` (its default value), the ``msgid`` value used by this
43 translation string will be assumed to be the value of ``default``.
45 ``mapping``, if supplied, must be a dictionary-like object which
46 represents the replacement values for any :term:`translation
47 string` *replacement marker* instances found within the ``msgid``
48 (or ``default``) value of this translation string.
50 ``context`` represents the :term:`translation context`. By default,
51 the translation context is ``None``.
53 After a translation string is constructed, it behaves like most
54 other ``unicode`` objects; its ``msgid`` value will be displayed
55 when it is treated like a ``unicode`` object. Only when its
56 ``ugettext`` method is called will it be translated.
58 Its default value is available as the ``default`` attribute of the
59 object, its :term:`translation domain` is available as the
60 ``domain`` attribute, and the ``mapping`` is available as the
61 ``mapping`` attribute. The object otherwise behaves much like a
62 Unicode string.
63 """
64 __slots__ = ('domain', 'context', 'default', 'mapping')
66 def __new__(self, msgid, domain=None, default=None, mapping=None, context=None):
68 # NB: this function should never never lose the *original
69 # identity* of a non-``None`` but empty ``default`` value
70 # provided to it. See the comment in ChameleonTranslate.
72 self = text_type.__new__(self, msgid)
73 if isinstance(msgid, self.__class__):
74 domain = domain or msgid.domain and msgid.domain[:]
75 context = context or msgid.context and msgid.context[:]
76 default = default or msgid.default and msgid.default[:]
77 if msgid.mapping:
78 if mapping:
79 for k, v in msgid.mapping.items():
80 mapping.setdefault(k, v)
81 else:
82 mapping = msgid.mapping.copy()
83 msgid = text_type(msgid)
84 self.domain = domain
85 self.context = context
86 if default is None:
87 default = text_type(msgid)
88 self.default = default
89 self.mapping = mapping
90 return self
92 def __mod__(self, options):
93 """Create a new TranslationString instance with an updated mapping.
94 This makes it possible to use the standard python %-style string
95 formatting with translatable strings. Only dictionary
96 arguments are supported.
97 """
98 if not isinstance(options, dict):
99 raise ValueError(
100 'Can only interpolate translationstring '
101 'with dictionaries.')
102 if self.mapping:
103 mapping = self.mapping.copy()
104 mapping.update(options)
105 else:
106 mapping = options.copy()
107 return TranslationString(self, mapping=mapping)
109 def interpolate(self, translated=None):
110 """ Interpolate the value ``translated`` which is assumed to
111 be a Unicode object containing zero or more *replacement
112 markers* (``$foo`` or ``${bar}``) using the ``mapping``
113 dictionary attached to this instance. If the ``mapping``
114 dictionary is empty or ``None``, no interpolation is
115 performed.
117 If ``translated`` is ``None``, interpolation will be performed
118 against the ``default`` value.
119 """
120 if translated is None:
121 translated = self.default
123 # NB: this function should never never lose the *original
124 # identity* of a non-``None`` but empty ``default`` value it
125 # is provided. If (translated == default) , it should return the
126 # *original* default, not a derivation. See the comment below in
127 # ChameleonTranslate.
129 if self.mapping and translated:
130 def replace(match):
131 whole, param1, param2 = match.groups()
132 return text_type(self.mapping.get(param1 or param2, whole))
133 translated = _interp_regex.sub(replace, translated)
135 return translated
137 def __reduce__(self):
138 return self.__class__, self.__getstate__()
140 def __getstate__(self):
141 return text_type(self), self.domain, self.default, self.mapping, self.context
143def TranslationStringFactory(factory_domain):
144 """ Create a factory which will generate translation strings
145 without requiring that each call to the factory be passed a
146 ``domain`` value. A single argument is passed to this class'
147 constructor: ``domain``. This value will be used as the
148 ``domain`` values of :class:`translationstring.TranslationString`
149 objects generated by the ``__call__`` of this class. The
150 ``msgid``, ``mapping``, and ``default`` values provided to the
151 ``__call__`` method of an instance of this class have the meaning
152 as described by the constructor of the
153 :class:`translationstring.TranslationString`"""
154 def create(msgid, mapping=None, default=None, context=None):
155 """ Provided a msgid (Unicode object or :term:`translation
156 string`) and optionally a mapping object, and a *default
157 value*, return a :term:`translation string` object."""
159 # if we are passing in a TranslationString as the msgid, then
160 # use its domain
161 if isinstance(msgid, TranslationString):
162 domain = msgid.domain or factory_domain
163 else:
164 domain = factory_domain
166 return TranslationString(msgid, domain=domain, default=default,
167 mapping=mapping, context=context)
168 return create
170def ChameleonTranslate(translator):
171 """
172 When necessary, use the result of calling this function as a
173 Chameleon template 'translate' function (e.g. the ``translate``
174 argument to the ``chameleon.zpt.template.PageTemplate``
175 constructor) to allow our translation machinery to drive template
176 translation. A single required argument ``translator`` is
177 passsed. The ``translator`` provided should be a callable which
178 accepts a single argument ``translation_string`` ( a
179 :class:`translationstring.TranslationString` instance) which
180 returns a ``unicode`` object as a translation. ``translator`` may
181 also optionally be ``None``, in which case no translation is
182 performed (the ``msgid`` or ``default`` value is returned
183 untranslated).
184 """
185 def translate(msgid, domain=None, mapping=None, context=None,
186 target_language=None, default=None):
188 # NB: note that both TranslationString._init__ and
189 # TranslationString.interpolate are careful to never lose the
190 # *identity* of an empty but non-``None`` ``default`` value we
191 # provide to them. For example, neither of those functions
192 # are permitted to run an empty but non-``None`` ``default``
193 # through ``unicode`` and throw the original default value
194 # away afterwards.
196 # This has a dubious cause: for Chameleon API reasons we must
197 # ensure that, after translation, if ( (translated == msgid)
198 # and (not default) and (default is not None) ) that we return
199 # the ``default`` value provided to us *unmodified*, because
200 # Chameleon uses it as a sentinel (it compares the return
201 # value of this function by identity to what it passed in as
202 # ``default``; this marker is a
203 # chameleon.core.i18n.StringMarker instance, a subclass of str
204 # that == ''). This is, of course, totally absurd, because
205 # Chameleon *also* wants us to use ``default`` as the input to
206 # a translation string in some cases, and maintaining the
207 # identity of this object through translation operations isn't
208 # a contract it spells out in its docs.
210 # Chameleon's use of ``default`` to represent both a sentinel
211 # and input to a translation string is a Chameleon i18n
212 # extensibility design bug. Until we overhaul its hook point
213 # for translation extensibility, we need to appease it by
214 # preserving ``default`` in the aforementioned case. So we
215 # spray these indignant comments all over this module. ;-)
217 if not isinstance(msgid, string_types):
218 if msgid is not None:
219 msgid = text_type(msgid)
220 return msgid
222 tstring = msgid
224 if not hasattr(tstring, 'interpolate'):
225 tstring = TranslationString(msgid, domain, default, mapping, context)
226 if translator is None:
227 result = tstring.interpolate()
228 else:
229 result = translator(tstring)
231 return result
233 return translate
235def ugettext_policy(translations, tstring, domain, context):
236 """ A translator policy function which unconditionally uses the
237 ``ugettext`` API on the translations object."""
239 if PY3: # pragma: no cover
240 _gettext = translations.gettext
241 else: # pragma: no cover
242 _gettext = translations.ugettext
244 if context:
245 # Workaround for http://bugs.python.org/issue2504?
246 msgid = CONTEXT_MASK % (context, tstring)
247 else:
248 msgid = tstring
250 translated = _gettext(msgid)
251 return tstring if translated == msgid else translated
253def dugettext_policy(translations, tstring, domain, context):
254 """ A translator policy function which assumes the use of a
255 :class:`babel.support.Translations` translations object, which
256 supports the dugettext API; fall back to ugettext."""
257 if domain is None:
258 default_domain = getattr(translations, 'domain', None) or 'messages'
259 domain = getattr(tstring, 'domain', None) or default_domain
260 context = context or getattr(tstring, 'context', None)
261 if context:
262 # Workaround for http://bugs.python.org/issue2504?
263 msgid = CONTEXT_MASK % (context, tstring)
264 else:
265 msgid = tstring
267 if getattr(translations, 'dugettext', None) is not None:
268 translated = translations.dugettext(domain, msgid)
269 else:
270 if PY3: # pragma: no cover
271 _gettext = translations.gettext
272 else: # pragma: no cover
273 _gettext = translations.ugettext
275 translated = _gettext(msgid)
276 return tstring if translated == msgid else translated
278def Translator(translations=None, policy=None):
279 """
280 Return a translator object based on the ``translations`` and
281 ``policy`` provided. ``translations`` should be an object
282 supporting *at least* the Python :class:`gettext.NullTranslations`
283 API but ideally the :class:`babel.support.Translations` API, which
284 has support for domain lookups like dugettext.
286 ``policy`` should be a callable which accepts three arguments:
287 ``translations``, ``tstring`` and ``domain``. It must perform the
288 actual translation lookup. If ``policy`` is ``None``, the
289 :func:`translationstring.dugettext_policy` policy will be used.
291 The callable returned accepts three arguments: ``tstring``
292 (required), ``domain`` (optional) and ``mapping`` (optional).
293 When called, it will translate the ``tstring`` translation string
294 to a ``unicode`` object using the ``translations`` provided. If
295 ``translations`` is ``None``, the result of interpolation of the
296 default value is returned. The optional ``domain`` argument can
297 be used to specify or override the domain of the ``tstring``
298 (useful when ``tstring`` is a normal string rather than a
299 translation string). The optional ``mapping`` argument can
300 specify or override the ``tstring`` interpolation mapping, useful
301 when the ``tstring`` argument is a simple string instead of a
302 translation string.
303 """
304 if policy is None:
305 policy = dugettext_policy
306 def translator(tstring, domain=None, mapping=None, context=None):
307 if not hasattr(tstring, 'interpolate'):
308 tstring = TranslationString(tstring, domain=domain, mapping=mapping, context=context)
309 elif mapping:
310 if tstring.mapping:
311 new_mapping = tstring.mapping.copy()
312 new_mapping.update(mapping)
313 else:
314 new_mapping = mapping
315 tstring = TranslationString(tstring, domain=domain, mapping=new_mapping, context=context)
316 translated = tstring
317 domain = domain or tstring.domain
318 context = context or tstring.context
319 if translations is not None:
320 translated = policy(translations, tstring, domain, context)
321 if translated == tstring:
322 translated = tstring.default
323 if translated and '$' in translated and tstring.mapping:
324 translated = tstring.interpolate(translated)
325 return translated
326 return translator
328def ungettext_policy(translations, singular, plural, n, domain, context):
329 """ A pluralizer policy function which unconditionally uses the
330 ``ungettext`` API on the translations object."""
332 if PY3: # pragma: no cover
333 _gettext = translations.ngettext
334 else: # pragma: no cover
335 _gettext = translations.ungettext
337 if context:
338 # Workaround for http://bugs.python.org/issue2504?
339 msgid = CONTEXT_MASK % (context, singular)
340 else:
341 msgid = singular
343 translated = _gettext(msgid, plural, n)
344 return singular if translated == msgid else translated
346def dungettext_policy(translations, singular, plural, n, domain, context):
347 """ A pluralizer policy function which assumes the use of the
348 :class:`babel.support.Translations` class, which supports the
349 dungettext API; falls back to ungettext."""
351 default_domain = getattr(translations, 'domain', None) or 'messages'
352 domain = domain or default_domain
353 if context:
354 # Workaround for http://bugs.python.org/issue2504?
355 msgid = CONTEXT_MASK % (context, singular)
356 else:
357 msgid = singular
358 if getattr(translations, 'dungettext', None) is not None:
359 translated = translations.dungettext(domain, msgid, plural, n)
360 else:
361 if PY3: # pragma: no cover
362 _gettext = translations.ngettext
363 else: # pragma: no cover
364 _gettext = translations.ungettext
366 translated = _gettext(msgid, plural, n)
367 return singular if translated == msgid else translated
369def Pluralizer(translations=None, policy=None):
370 """
371 Return a pluralizer object based on the ``translations`` and
372 ``policy`` provided. ``translations`` should be an object
373 supporting *at least* the Python :class:`gettext.NullTranslations`
374 API but ideally the :class:`babel.support.Translations` API, which
375 has support for domain lookups like dugettext.
377 ``policy`` should be a callable which accepts five arguments:
378 ``translations``, ``singular`` and ``plural``, ``n`` and
379 ``domain``. It must perform the actual pluralization lookup. If
380 ``policy`` is ``None``, the
381 :func:`translationstring.dungettext_policy` policy will be used.
383 The object returned will be a callable which has the following
384 signature::
386 def pluralizer(singular, plural, n, domain=None, mapping=None):
387 ...
389 The ``singular`` and ``plural`` objects passed may be translation
390 strings or unicode strings. ``n`` represents the number of
391 elements. ``domain`` is the translation domain to use to do the
392 pluralization, and ``mapping`` is the interpolation mapping that
393 should be used on the result. Note that if the objects passed are
394 translation strings, their domains and mappings are ignored. The
395 domain and mapping arguments must be used instead. If the ``domain`` is
396 not supplied, a default domain is used (usually ``messages``).
397 """
399 if policy is None:
400 policy = dungettext_policy
401 if translations is None:
402 translations = NullTranslations()
403 def pluralizer(singular, plural, n, domain=None, mapping=None, context=None):
404 """ Pluralize this object """
405 translated = text_type(
406 policy(translations, singular, plural, n, domain, context))
407 if translated and '$' in translated and mapping:
408 return TranslationString(translated, mapping=mapping).interpolate()
409 return translated
410 return pluralizer