Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/utils/translation/trans_real.py: 49%
309 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1"""Translation helper functions."""
2import functools
3import gettext as gettext_module
4import os
5import re
6import sys
7import warnings
9from asgiref.local import Local
11from django.apps import apps
12from django.conf import settings
13from django.conf.locale import LANG_INFO
14from django.core.exceptions import AppRegistryNotReady
15from django.core.signals import setting_changed
16from django.dispatch import receiver
17from django.utils.regex_helper import _lazy_re_compile
18from django.utils.safestring import SafeData, mark_safe
20from . import to_language, to_locale
22# Translations are cached in a dictionary for every language.
23# The active translations are stored by threadid to make them thread local.
24_translations = {}
25_active = Local()
27# The default translation is based on the settings file.
28_default = None
30# magic gettext number to separate context from message
31CONTEXT_SEPARATOR = "\x04"
33# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9
34# and RFC 3066, section 2.1
35accept_language_re = _lazy_re_compile(
36 r"""
37 # "en", "en-au", "x-y-z", "es-419", "*"
38 ([A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*|\*)
39 # Optional "q=1.00", "q=0.8"
40 (?:\s*;\s*q=(0(?:\.\d{,3})?|1(?:\.0{,3})?))?
41 # Multiple accepts per header.
42 (?:\s*,\s*|$)
43 """,
44 re.VERBOSE,
45)
47language_code_re = _lazy_re_compile(
48 r"^[a-z]{1,8}(?:-[a-z0-9]{1,8})*(?:@[a-z0-9]{1,20})?$", re.IGNORECASE
49)
51language_code_prefix_re = _lazy_re_compile(r"^/(\w+([@-]\w+)?)(/|$)")
54@receiver(setting_changed)
55def reset_cache(**kwargs):
56 """
57 Reset global state when LANGUAGES setting has been changed, as some
58 languages should no longer be accepted.
59 """
60 if kwargs["setting"] in ("LANGUAGES", "LANGUAGE_CODE"):
61 check_for_language.cache_clear()
62 get_languages.cache_clear()
63 get_supported_language_variant.cache_clear()
66class TranslationCatalog:
67 """
68 Simulate a dict for DjangoTranslation._catalog so as multiple catalogs
69 with different plural equations are kept separate.
70 """
72 def __init__(self, trans=None):
73 self._catalogs = [trans._catalog.copy()] if trans else [{}]
74 self._plurals = [trans.plural] if trans else [lambda n: int(n != 1)] 74 ↛ exitline 74 didn't run the lambda on line 74
76 def __getitem__(self, key):
77 for cat in self._catalogs:
78 try:
79 return cat[key]
80 except KeyError:
81 pass
82 raise KeyError(key)
84 def __setitem__(self, key, value):
85 self._catalogs[0][key] = value
87 def __contains__(self, key):
88 return any(key in cat for cat in self._catalogs)
90 def items(self):
91 for cat in self._catalogs:
92 yield from cat.items()
94 def keys(self):
95 for cat in self._catalogs:
96 yield from cat.keys()
98 def update(self, trans):
99 # Merge if plural function is the same, else prepend.
100 for cat, plural in zip(self._catalogs, self._plurals):
101 if trans.plural.__code__ == plural.__code__:
102 cat.update(trans._catalog)
103 break
104 else:
105 self._catalogs.insert(0, trans._catalog.copy())
106 self._plurals.insert(0, trans.plural)
108 def get(self, key, default=None):
109 missing = object()
110 for cat in self._catalogs:
111 result = cat.get(key, missing)
112 if result is not missing:
113 return result
114 return default
116 def plural(self, msgid, num):
117 for cat, plural in zip(self._catalogs, self._plurals):
118 tmsg = cat.get((msgid, plural(num)))
119 if tmsg is not None:
120 return tmsg
121 raise KeyError
124class DjangoTranslation(gettext_module.GNUTranslations):
125 """
126 Set up the GNUTranslations context with regard to output charset.
128 This translation object will be constructed out of multiple GNUTranslations
129 objects by merging their catalogs. It will construct an object for the
130 requested language and add a fallback to the default language, if it's
131 different from the requested language.
132 """
134 domain = "django"
136 def __init__(self, language, domain=None, localedirs=None):
137 """Create a GNUTranslations() using many locale directories"""
138 gettext_module.GNUTranslations.__init__(self)
139 if domain is not None: 139 ↛ 140line 139 didn't jump to line 140, because the condition on line 139 was never true
140 self.domain = domain
142 self.__language = language
143 self.__to_language = to_language(language)
144 self.__locale = to_locale(language)
145 self._catalog = None
146 # If a language doesn't have a catalog, use the Germanic default for
147 # pluralization: anything except one is pluralized.
148 self.plural = lambda n: int(n != 1) 148 ↛ exitline 148 didn't run the lambda on line 148
150 if self.domain == "django": 150 ↛ 159line 150 didn't jump to line 159, because the condition on line 150 was never false
151 if localedirs is not None: 151 ↛ 153line 151 didn't jump to line 153, because the condition on line 151 was never true
152 # A module-level cache is used for caching 'django' translations
153 warnings.warn(
154 "localedirs is ignored when domain is 'django'.", RuntimeWarning
155 )
156 localedirs = None
157 self._init_translation_catalog()
159 if localedirs: 159 ↛ 160line 159 didn't jump to line 160, because the condition on line 159 was never true
160 for localedir in localedirs:
161 translation = self._new_gnu_trans(localedir)
162 self.merge(translation)
163 else:
164 self._add_installed_apps_translations()
166 self._add_local_translations()
167 if ( 167 ↛ 173line 167 didn't jump to line 173
168 self.__language == settings.LANGUAGE_CODE
169 and self.domain == "django"
170 and self._catalog is None
171 ):
172 # default lang should have at least one translation file available.
173 raise OSError(
174 "No translation files found for default language %s."
175 % settings.LANGUAGE_CODE
176 )
177 self._add_fallback(localedirs)
178 if self._catalog is None: 178 ↛ 180line 178 didn't jump to line 180, because the condition on line 178 was never true
179 # No catalogs found for this language, set an empty catalog.
180 self._catalog = TranslationCatalog()
182 def __repr__(self):
183 return "<DjangoTranslation lang:%s>" % self.__language
185 def _new_gnu_trans(self, localedir, use_null_fallback=True):
186 """
187 Return a mergeable gettext.GNUTranslations instance.
189 A convenience wrapper. By default gettext uses 'fallback=False'.
190 Using param `use_null_fallback` to avoid confusion with any other
191 references to 'fallback'.
192 """
193 return gettext_module.translation(
194 domain=self.domain,
195 localedir=localedir,
196 languages=[self.__locale],
197 fallback=use_null_fallback,
198 )
200 def _init_translation_catalog(self):
201 """Create a base catalog using global django translations."""
202 settingsfile = sys.modules[settings.__module__].__file__
203 localedir = os.path.join(os.path.dirname(settingsfile), "locale")
204 translation = self._new_gnu_trans(localedir)
205 self.merge(translation)
207 def _add_installed_apps_translations(self):
208 """Merge translations from each installed app."""
209 try:
210 app_configs = reversed(list(apps.get_app_configs()))
211 except AppRegistryNotReady:
212 raise AppRegistryNotReady(
213 "The translation infrastructure cannot be initialized before the "
214 "apps registry is ready. Check that you don't make non-lazy "
215 "gettext calls at import time."
216 )
217 for app_config in app_configs:
218 localedir = os.path.join(app_config.path, "locale")
219 if os.path.exists(localedir):
220 translation = self._new_gnu_trans(localedir)
221 self.merge(translation)
223 def _add_local_translations(self):
224 """Merge translations defined in LOCALE_PATHS."""
225 for localedir in reversed(settings.LOCALE_PATHS): 225 ↛ 226line 225 didn't jump to line 226, because the loop on line 225 never started
226 translation = self._new_gnu_trans(localedir)
227 self.merge(translation)
229 def _add_fallback(self, localedirs=None):
230 """Set the GNUTranslations() fallback with the default language."""
231 # Don't set a fallback for the default language or any English variant
232 # (as it's empty, so it'll ALWAYS fall back to the default language)
233 if self.__language == settings.LANGUAGE_CODE or self.__language.startswith( 233 ↛ 237line 233 didn't jump to line 237, because the condition on line 233 was never false
234 "en"
235 ):
236 return
237 if self.domain == "django":
238 # Get from cache
239 default_translation = translation(settings.LANGUAGE_CODE)
240 else:
241 default_translation = DjangoTranslation(
242 settings.LANGUAGE_CODE, domain=self.domain, localedirs=localedirs
243 )
244 self.add_fallback(default_translation)
246 def merge(self, other):
247 """Merge another translation into this catalog."""
248 if not getattr(other, "_catalog", None): 248 ↛ 249line 248 didn't jump to line 249, because the condition on line 248 was never true
249 return # NullTranslations() has no _catalog
250 if self._catalog is None:
251 # Take plural and _info from first catalog found (generally Django's).
252 self.plural = other.plural
253 self._info = other._info.copy()
254 self._catalog = TranslationCatalog(other)
255 else:
256 self._catalog.update(other)
257 if other._fallback:
258 self.add_fallback(other._fallback)
260 def language(self):
261 """Return the translation language."""
262 return self.__language
264 def to_language(self):
265 """Return the translation language name."""
266 return self.__to_language
268 def ngettext(self, msgid1, msgid2, n):
269 try:
270 tmsg = self._catalog.plural(msgid1, n)
271 except KeyError:
272 if self._fallback:
273 return self._fallback.ngettext(msgid1, msgid2, n)
274 if n == 1:
275 tmsg = msgid1
276 else:
277 tmsg = msgid2
278 return tmsg
281def translation(language):
282 """
283 Return a translation object in the default 'django' domain.
284 """
285 global _translations
286 if language not in _translations:
287 _translations[language] = DjangoTranslation(language)
288 return _translations[language]
291def activate(language):
292 """
293 Fetch the translation object for a given language and install it as the
294 current translation object for the current thread.
295 """
296 if not language: 296 ↛ 297line 296 didn't jump to line 297, because the condition on line 296 was never true
297 return
298 _active.value = translation(language)
301def deactivate():
302 """
303 Uninstall the active translation object so that further _() calls resolve
304 to the default translation object.
305 """
306 if hasattr(_active, "value"): 306 ↛ 307line 306 didn't jump to line 307, because the condition on line 306 was never true
307 del _active.value
310def deactivate_all():
311 """
312 Make the active translation object a NullTranslations() instance. This is
313 useful when we want delayed translations to appear as the original string
314 for some reason.
315 """
316 _active.value = gettext_module.NullTranslations()
317 _active.value.to_language = lambda *args: None
320def get_language():
321 """Return the currently selected language."""
322 t = getattr(_active, "value", None)
323 if t is not None:
324 try:
325 return t.to_language()
326 except AttributeError:
327 pass
328 # If we don't have a real translation object, assume it's the default language.
329 return settings.LANGUAGE_CODE
332def get_language_bidi():
333 """
334 Return selected language's BiDi layout.
336 * False = left-to-right layout
337 * True = right-to-left layout
338 """
339 lang = get_language()
340 if lang is None:
341 return False
342 else:
343 base_lang = get_language().split("-")[0]
344 return base_lang in settings.LANGUAGES_BIDI
347def catalog():
348 """
349 Return the current active catalog for further processing.
350 This can be used if you need to modify the catalog or want to access the
351 whole message catalog instead of just translating one string.
352 """
353 global _default
355 t = getattr(_active, "value", None)
356 if t is not None:
357 return t
358 if _default is None:
359 _default = translation(settings.LANGUAGE_CODE)
360 return _default
363def gettext(message):
364 """
365 Translate the 'message' string. It uses the current thread to find the
366 translation object to use. If no current translation is activated, the
367 message will be run through the default translation object.
368 """
369 global _default
371 eol_message = message.replace("\r\n", "\n").replace("\r", "\n")
373 if eol_message: 373 ↛ 381line 373 didn't jump to line 381, because the condition on line 373 was never false
374 _default = _default or translation(settings.LANGUAGE_CODE)
375 translation_object = getattr(_active, "value", _default)
377 result = translation_object.gettext(eol_message)
378 else:
379 # Return an empty value of the corresponding type if an empty message
380 # is given, instead of metadata, which is the default gettext behavior.
381 result = type(message)("")
383 if isinstance(message, SafeData): 383 ↛ 384line 383 didn't jump to line 384, because the condition on line 383 was never true
384 return mark_safe(result)
386 return result
389def pgettext(context, message):
390 msg_with_ctxt = "%s%s%s" % (context, CONTEXT_SEPARATOR, message)
391 result = gettext(msg_with_ctxt)
392 if CONTEXT_SEPARATOR in result:
393 # Translation not found
394 result = message
395 elif isinstance(message, SafeData):
396 result = mark_safe(result)
397 return result
400def gettext_noop(message):
401 """
402 Mark strings for translation but don't translate them now. This can be
403 used to store strings in global variables that should stay in the base
404 language (because they might be used externally) and will be translated
405 later.
406 """
407 return message
410def do_ntranslate(singular, plural, number, translation_function):
411 global _default
413 t = getattr(_active, "value", None)
414 if t is not None:
415 return getattr(t, translation_function)(singular, plural, number)
416 if _default is None:
417 _default = translation(settings.LANGUAGE_CODE)
418 return getattr(_default, translation_function)(singular, plural, number)
421def ngettext(singular, plural, number):
422 """
423 Return a string of the translation of either the singular or plural,
424 based on the number.
425 """
426 return do_ntranslate(singular, plural, number, "ngettext")
429def npgettext(context, singular, plural, number):
430 msgs_with_ctxt = (
431 "%s%s%s" % (context, CONTEXT_SEPARATOR, singular),
432 "%s%s%s" % (context, CONTEXT_SEPARATOR, plural),
433 number,
434 )
435 result = ngettext(*msgs_with_ctxt)
436 if CONTEXT_SEPARATOR in result:
437 # Translation not found
438 result = ngettext(singular, plural, number)
439 return result
442def all_locale_paths():
443 """
444 Return a list of paths to user-provides languages files.
445 """
446 globalpath = os.path.join(
447 os.path.dirname(sys.modules[settings.__module__].__file__), "locale"
448 )
449 app_paths = []
450 for app_config in apps.get_app_configs():
451 locale_path = os.path.join(app_config.path, "locale")
452 if os.path.exists(locale_path):
453 app_paths.append(locale_path)
454 return [globalpath, *settings.LOCALE_PATHS, *app_paths]
457@functools.lru_cache(maxsize=1000)
458def check_for_language(lang_code):
459 """
460 Check whether there is a global language file for the given language
461 code. This is used to decide whether a user-provided language is
462 available.
464 lru_cache should have a maxsize to prevent from memory exhaustion attacks,
465 as the provided language codes are taken from the HTTP request. See also
466 <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
467 """
468 # First, a quick check to make sure lang_code is well-formed (#21458)
469 if lang_code is None or not language_code_re.search(lang_code): 469 ↛ 470line 469 didn't jump to line 470, because the condition on line 469 was never true
470 return False
471 return any( 471 ↛ exitline 471 didn't finish the generator expression on line 471
472 gettext_module.find("django", path, [to_locale(lang_code)]) is not None
473 for path in all_locale_paths()
474 )
477@functools.lru_cache()
478def get_languages():
479 """
480 Cache of settings.LANGUAGES in a dictionary for easy lookups by key.
481 """
482 return dict(settings.LANGUAGES)
485@functools.lru_cache(maxsize=1000)
486def get_supported_language_variant(lang_code, strict=False):
487 """
488 Return the language code that's listed in supported languages, possibly
489 selecting a more generic variant. Raise LookupError if nothing is found.
491 If `strict` is False (the default), look for a country-specific variant
492 when neither the language code nor its generic variant is found.
494 lru_cache should have a maxsize to prevent from memory exhaustion attacks,
495 as the provided language codes are taken from the HTTP request. See also
496 <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
497 """
498 if lang_code: 498 ↛ 520line 498 didn't jump to line 520, because the condition on line 498 was never false
499 # If 'zh-hant-tw' is not supported, try special fallback or subsequent
500 # language codes i.e. 'zh-hant' and 'zh'.
501 possible_lang_codes = [lang_code]
502 try:
503 possible_lang_codes.extend(LANG_INFO[lang_code]["fallback"])
504 except KeyError:
505 pass
506 i = None
507 while (i := lang_code.rfind("-", 0, i)) > -1:
508 possible_lang_codes.append(lang_code[:i])
509 generic_lang_code = possible_lang_codes[-1]
510 supported_lang_codes = get_languages()
512 for code in possible_lang_codes: 512 ↛ 515line 512 didn't jump to line 515, because the loop on line 512 didn't complete
513 if code in supported_lang_codes and check_for_language(code): 513 ↛ 512line 513 didn't jump to line 512, because the condition on line 513 was never false
514 return code
515 if not strict:
516 # if fr-fr is not supported, try fr-ca.
517 for supported_code in supported_lang_codes:
518 if supported_code.startswith(generic_lang_code + "-"):
519 return supported_code
520 raise LookupError(lang_code)
523def get_language_from_path(path, strict=False):
524 """
525 Return the language code if there's a valid language code found in `path`.
527 If `strict` is False (the default), look for a country-specific variant
528 when neither the language code nor its generic variant is found.
529 """
530 regex_match = language_code_prefix_re.match(path)
531 if not regex_match:
532 return None
533 lang_code = regex_match[1]
534 try:
535 return get_supported_language_variant(lang_code, strict=strict)
536 except LookupError:
537 return None
540def get_language_from_request(request, check_path=False):
541 """
542 Analyze the request to find what language the user wants the system to
543 show. Only languages listed in settings.LANGUAGES are taken into account.
544 If the user requests a sublanguage where we have a main language, we send
545 out the main language.
547 If check_path is True, the URL path prefix will be checked for a language
548 code, otherwise this is skipped for backwards compatibility.
549 """
550 if check_path:
551 lang_code = get_language_from_path(request.path_info)
552 if lang_code is not None:
553 return lang_code
555 lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
556 if (
557 lang_code is not None
558 and lang_code in get_languages()
559 and check_for_language(lang_code)
560 ):
561 return lang_code
563 try:
564 return get_supported_language_variant(lang_code)
565 except LookupError:
566 pass
568 accept = request.META.get("HTTP_ACCEPT_LANGUAGE", "")
569 for accept_lang, unused in parse_accept_lang_header(accept):
570 if accept_lang == "*":
571 break
573 if not language_code_re.search(accept_lang):
574 continue
576 try:
577 return get_supported_language_variant(accept_lang)
578 except LookupError:
579 continue
581 try:
582 return get_supported_language_variant(settings.LANGUAGE_CODE)
583 except LookupError:
584 return settings.LANGUAGE_CODE
587@functools.lru_cache(maxsize=1000)
588def parse_accept_lang_header(lang_string):
589 """
590 Parse the lang_string, which is the body of an HTTP Accept-Language
591 header, and return a tuple of (lang, q-value), ordered by 'q' values.
593 Return an empty tuple if there are any format errors in lang_string.
594 """
595 result = []
596 pieces = accept_language_re.split(lang_string.lower())
597 if pieces[-1]:
598 return ()
599 for i in range(0, len(pieces) - 1, 3):
600 first, lang, priority = pieces[i : i + 3]
601 if first:
602 return ()
603 if priority:
604 priority = float(priority)
605 else:
606 priority = 1.0
607 result.append((lang, priority))
608 result.sort(key=lambda k: k[1], reverse=True)
609 return tuple(result)