Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/pyramid/i18n.py : 25%

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 gettext
2import os
4from translationstring import (
5 Translator,
6 Pluralizer,
7 TranslationString, # API
8 TranslationStringFactory, # API
9)
11from pyramid.compat import PY2
12from pyramid.decorator import reify
14from pyramid.interfaces import (
15 ILocalizer,
16 ITranslationDirectories,
17 ILocaleNegotiator,
18)
20from pyramid.threadlocal import get_current_registry
22TranslationString = TranslationString # PyFlakes
23TranslationStringFactory = TranslationStringFactory # PyFlakes
25DEFAULT_PLURAL = lambda n: int(n != 1)
28class Localizer(object):
29 """
30 An object providing translation and pluralizations related to
31 the current request's locale name. A
32 :class:`pyramid.i18n.Localizer` object is created using the
33 :func:`pyramid.i18n.get_localizer` function.
34 """
36 def __init__(self, locale_name, translations):
37 self.locale_name = locale_name
38 self.translations = translations
39 self.pluralizer = None
40 self.translator = None
42 def translate(self, tstring, domain=None, mapping=None):
43 """
44 Translate a :term:`translation string` to the current language
45 and interpolate any *replacement markers* in the result. The
46 ``translate`` method accepts three arguments: ``tstring``
47 (required), ``domain`` (optional) and ``mapping`` (optional).
48 When called, it will translate the ``tstring`` translation
49 string to a ``unicode`` object using the current locale. If
50 the current locale could not be determined, the result of
51 interpolation of the default value is returned. The optional
52 ``domain`` argument can be used to specify or override the
53 domain of the ``tstring`` (useful when ``tstring`` is a normal
54 string rather than a translation string). The optional
55 ``mapping`` argument can specify or override the ``tstring``
56 interpolation mapping, useful when the ``tstring`` argument is
57 a simple string instead of a translation string.
59 Example::
61 from pyramid.18n import TranslationString
62 ts = TranslationString('Add ${item}', domain='mypackage',
63 mapping={'item':'Item'})
64 translated = localizer.translate(ts)
66 Example::
68 translated = localizer.translate('Add ${item}', domain='mypackage',
69 mapping={'item':'Item'})
71 """
72 if self.translator is None:
73 self.translator = Translator(self.translations)
74 return self.translator(tstring, domain=domain, mapping=mapping)
76 def pluralize(self, singular, plural, n, domain=None, mapping=None):
77 """
78 Return a Unicode string translation by using two
79 :term:`message identifier` objects as a singular/plural pair
80 and an ``n`` value representing the number that appears in the
81 message using gettext plural forms support. The ``singular``
82 and ``plural`` objects should be unicode strings. There is no
83 reason to use translation string objects as arguments as all
84 metadata is ignored.
86 ``n`` represents the number of elements. ``domain`` is the
87 translation domain to use to do the pluralization, and ``mapping``
88 is the interpolation mapping that should be used on the result. If
89 the ``domain`` is not supplied, a default domain is used (usually
90 ``messages``).
92 Example::
94 num = 1
95 translated = localizer.pluralize('Add ${num} item',
96 'Add ${num} items',
97 num,
98 mapping={'num':num})
100 If using the gettext plural support, which is required for
101 languages that have pluralisation rules other than n != 1, the
102 ``singular`` argument must be the message_id defined in the
103 translation file. The plural argument is not used in this case.
105 Example::
107 num = 1
108 translated = localizer.pluralize('item_plural',
109 '',
110 num,
111 mapping={'num':num})
114 """
115 if self.pluralizer is None:
116 self.pluralizer = Pluralizer(self.translations)
117 return self.pluralizer(
118 singular, plural, n, domain=domain, mapping=mapping
119 )
122def default_locale_negotiator(request):
123 """ The default :term:`locale negotiator`. Returns a locale name
124 or ``None``.
126 - First, the negotiator looks for the ``_LOCALE_`` attribute of
127 the request object (possibly set by a view or a listener for an
128 :term:`event`). If the attribute exists and it is not ``None``,
129 its value will be used.
131 - Then it looks for the ``request.params['_LOCALE_']`` value.
133 - Then it looks for the ``request.cookies['_LOCALE_']`` value.
135 - Finally, the negotiator returns ``None`` if the locale could not
136 be determined via any of the previous checks (when a locale
137 negotiator returns ``None``, it signifies that the
138 :term:`default locale name` should be used.)
139 """
140 name = '_LOCALE_'
141 locale_name = getattr(request, name, None)
142 if locale_name is None:
143 locale_name = request.params.get(name)
144 if locale_name is None:
145 locale_name = request.cookies.get(name)
146 return locale_name
149def negotiate_locale_name(request):
150 """ Negotiate and return the :term:`locale name` associated with
151 the current request."""
152 try:
153 registry = request.registry
154 except AttributeError:
155 registry = get_current_registry()
156 negotiator = registry.queryUtility(
157 ILocaleNegotiator, default=default_locale_negotiator
158 )
159 locale_name = negotiator(request)
161 if locale_name is None:
162 settings = registry.settings or {}
163 locale_name = settings.get('default_locale_name', 'en')
165 return locale_name
168def get_locale_name(request):
169 """
170 .. deprecated:: 1.5
171 Use :attr:`pyramid.request.Request.locale_name` directly instead.
172 Return the :term:`locale name` associated with the current request.
173 """
174 return request.locale_name
177def make_localizer(current_locale_name, translation_directories):
178 """ Create a :class:`pyramid.i18n.Localizer` object
179 corresponding to the provided locale name from the
180 translations found in the list of translation directories."""
181 translations = Translations()
182 translations._catalog = {}
184 locales_to_try = []
185 if '_' in current_locale_name:
186 locales_to_try = [current_locale_name.split('_')[0]]
187 locales_to_try.append(current_locale_name)
189 # intent: order locales left to right in least specific to most specific,
190 # e.g. ['de', 'de_DE']. This services the intent of creating a
191 # translations object that returns a "more specific" translation for a
192 # region, but will fall back to a "less specific" translation for the
193 # locale if necessary. Ordering from least specific to most specific
194 # allows us to call translations.add in the below loop to get this
195 # behavior.
197 for tdir in translation_directories:
198 locale_dirs = []
199 for lname in locales_to_try:
200 ldir = os.path.realpath(os.path.join(tdir, lname))
201 if os.path.isdir(ldir):
202 locale_dirs.append(ldir)
204 for locale_dir in locale_dirs:
205 messages_dir = os.path.join(locale_dir, 'LC_MESSAGES')
206 if not os.path.isdir(os.path.realpath(messages_dir)):
207 continue
208 for mofile in os.listdir(messages_dir):
209 mopath = os.path.realpath(os.path.join(messages_dir, mofile))
210 if mofile.endswith('.mo') and os.path.isfile(mopath):
211 with open(mopath, 'rb') as mofp:
212 domain = mofile[:-3]
213 dtrans = Translations(mofp, domain)
214 translations.add(dtrans)
216 return Localizer(
217 locale_name=current_locale_name, translations=translations
218 )
221def get_localizer(request):
222 """
223 .. deprecated:: 1.5
224 Use the :attr:`pyramid.request.Request.localizer` attribute directly
225 instead. Retrieve a :class:`pyramid.i18n.Localizer` object
226 corresponding to the current request's locale name.
227 """
228 return request.localizer
231class Translations(gettext.GNUTranslations, object):
232 """An extended translation catalog class (ripped off from Babel) """
234 DEFAULT_DOMAIN = 'messages'
236 def __init__(self, fileobj=None, domain=DEFAULT_DOMAIN):
237 """Initialize the translations catalog.
239 :param fileobj: the file-like object the translation should be read
240 from
241 """
242 # germanic plural by default; self.plural will be overwritten by
243 # GNUTranslations._parse (called as a side effect if fileobj is
244 # passed to GNUTranslations.__init__) with a "real" self.plural for
245 # this domain; see https://github.com/Pylons/pyramid/issues/235
246 # It is only overridden the first time a new message file is found
247 # for a given domain, so all message files must have matching plural
248 # rules if they are in the same domain. We keep track of if we have
249 # overridden so we can special case the default domain, which is always
250 # instantiated before a message file is read.
251 # See also https://github.com/Pylons/pyramid/pull/2102
252 self.plural = DEFAULT_PLURAL
253 gettext.GNUTranslations.__init__(self, fp=fileobj)
254 self.files = list(filter(None, [getattr(fileobj, 'name', None)]))
255 self.domain = domain
256 self._domains = {}
258 @classmethod
259 def load(cls, dirname=None, locales=None, domain=DEFAULT_DOMAIN):
260 """Load translations from the given directory.
262 :param dirname: the directory containing the ``MO`` files
263 :param locales: the list of locales in order of preference (items in
264 this list can be either `Locale` objects or locale
265 strings)
266 :param domain: the message domain
267 :return: the loaded catalog, or a ``NullTranslations`` instance if no
268 matching translations were found
269 :rtype: `Translations`
270 """
271 if locales is not None:
272 if not isinstance(locales, (list, tuple)):
273 locales = [locales]
274 locales = [str(l) for l in locales]
275 if not domain:
276 domain = cls.DEFAULT_DOMAIN
277 filename = gettext.find(domain, dirname, locales)
278 if not filename:
279 return gettext.NullTranslations()
280 with open(filename, 'rb') as fp:
281 return cls(fileobj=fp, domain=domain)
283 def __repr__(self):
284 return '<%s: "%s">' % (
285 type(self).__name__,
286 self._info.get('project-id-version'),
287 )
289 def add(self, translations, merge=True):
290 """Add the given translations to the catalog.
292 If the domain of the translations is different than that of the
293 current catalog, they are added as a catalog that is only accessible
294 by the various ``d*gettext`` functions.
296 :param translations: the `Translations` instance with the messages to
297 add
298 :param merge: whether translations for message domains that have
299 already been added should be merged with the existing
300 translations
301 :return: the `Translations` instance (``self``) so that `merge` calls
302 can be easily chained
303 :rtype: `Translations`
304 """
305 domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN)
306 if domain == self.DEFAULT_DOMAIN and self.plural is DEFAULT_PLURAL:
307 self.plural = translations.plural
309 if merge and domain == self.domain:
310 return self.merge(translations)
312 existing = self._domains.get(domain)
313 if merge and existing is not None:
314 existing.merge(translations)
315 else:
316 translations.add_fallback(self)
317 self._domains[domain] = translations
319 return self
321 def merge(self, translations):
322 """Merge the given translations into the catalog.
324 Message translations in the specified catalog override any messages
325 with the same identifier in the existing catalog.
327 :param translations: the `Translations` instance with the messages to
328 merge
329 :return: the `Translations` instance (``self``) so that `merge` calls
330 can be easily chained
331 :rtype: `Translations`
332 """
333 if isinstance(translations, gettext.GNUTranslations):
334 self._catalog.update(translations._catalog)
335 if isinstance(translations, Translations):
336 self.files.extend(translations.files)
338 return self
340 def dgettext(self, domain, message):
341 """Like ``gettext()``, but look the message up in the specified
342 domain.
343 """
344 return self._domains.get(domain, self).gettext(message)
346 def ldgettext(self, domain, message):
347 """Like ``lgettext()``, but look the message up in the specified
348 domain.
349 """
350 return self._domains.get(domain, self).lgettext(message)
352 def dugettext(self, domain, message):
353 """Like ``ugettext()``, but look the message up in the specified
354 domain.
355 """
356 if PY2:
357 return self._domains.get(domain, self).ugettext(message)
358 else:
359 return self._domains.get(domain, self).gettext(message)
361 def dngettext(self, domain, singular, plural, num):
362 """Like ``ngettext()``, but look the message up in the specified
363 domain.
364 """
365 return self._domains.get(domain, self).ngettext(singular, plural, num)
367 def ldngettext(self, domain, singular, plural, num):
368 """Like ``lngettext()``, but look the message up in the specified
369 domain.
370 """
371 return self._domains.get(domain, self).lngettext(singular, plural, num)
373 def dungettext(self, domain, singular, plural, num):
374 """Like ``ungettext()`` but look the message up in the specified
375 domain.
376 """
377 if PY2:
378 return self._domains.get(domain, self).ungettext(
379 singular, plural, num
380 )
381 else:
382 return self._domains.get(domain, self).ngettext(
383 singular, plural, num
384 )
387class LocalizerRequestMixin(object):
388 @reify
389 def localizer(self):
390 """ Convenience property to return a localizer """
391 registry = self.registry
393 current_locale_name = self.locale_name
394 localizer = registry.queryUtility(ILocalizer, name=current_locale_name)
396 if localizer is None:
397 # no localizer utility registered yet
398 tdirs = registry.queryUtility(ITranslationDirectories, default=[])
399 localizer = make_localizer(current_locale_name, tdirs)
401 registry.registerUtility(
402 localizer, ILocalizer, name=current_locale_name
403 )
405 return localizer
407 @reify
408 def locale_name(self):
409 locale_name = negotiate_locale_name(self)
410 return locale_name