Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/django/utils/translation/trans_real.py: 19%
306 statements
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
« prev ^ index » next coverage.py v7.0.5, created at 2023-01-17 06:13 +0000
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 9110 Sections 12.4.2 and
34# 12.5.4, and RFC 5646 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(?:\.[0-9]{,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+){0,2})(/|$)")
54@receiver(setting_changed)
55def reset_cache(*, setting, **kwargs):
56 """
57 Reset global state when LANGUAGES setting has been changed, as some
58 languages should no longer be accepted.
59 """
60 if 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)]
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:
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)
150 if self.domain == "django":
151 if localedirs is not None:
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:
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 (
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:
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(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):
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(
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):
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:
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"):
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:
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):
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):
470 return False
471 return any(
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 Convert keys to lowercase as they should be treated as case-insensitive.
482 """
483 return {key.lower(): value for key, value in dict(settings.LANGUAGES).items()}
486@functools.lru_cache(maxsize=1000)
487def get_supported_language_variant(lang_code, strict=False):
488 """
489 Return the language code that's listed in supported languages, possibly
490 selecting a more generic variant. Raise LookupError if nothing is found.
492 If `strict` is False (the default), look for a country-specific variant
493 when neither the language code nor its generic variant is found.
495 lru_cache should have a maxsize to prevent from memory exhaustion attacks,
496 as the provided language codes are taken from the HTTP request. See also
497 <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
498 """
499 if lang_code:
500 # If 'zh-hant-tw' is not supported, try special fallback or subsequent
501 # language codes i.e. 'zh-hant' and 'zh'.
502 possible_lang_codes = [lang_code]
503 try:
504 possible_lang_codes.extend(LANG_INFO[lang_code]["fallback"])
505 except KeyError:
506 pass
507 i = None
508 while (i := lang_code.rfind("-", 0, i)) > -1:
509 possible_lang_codes.append(lang_code[:i])
510 generic_lang_code = possible_lang_codes[-1]
511 supported_lang_codes = get_languages()
513 for code in possible_lang_codes:
514 if code.lower() in supported_lang_codes and check_for_language(code):
515 return code
516 if not strict:
517 # if fr-fr is not supported, try fr-ca.
518 for supported_code in supported_lang_codes:
519 if supported_code.startswith(generic_lang_code + "-"):
520 return supported_code
521 raise LookupError(lang_code)
524def get_language_from_path(path, strict=False):
525 """
526 Return the language code if there's a valid language code found in `path`.
528 If `strict` is False (the default), look for a country-specific variant
529 when neither the language code nor its generic variant is found.
530 """
531 regex_match = language_code_prefix_re.match(path)
532 if not regex_match:
533 return None
534 lang_code = regex_match[1]
535 try:
536 return get_supported_language_variant(lang_code, strict=strict)
537 except LookupError:
538 return None
541def get_language_from_request(request, check_path=False):
542 """
543 Analyze the request to find what language the user wants the system to
544 show. Only languages listed in settings.LANGUAGES are taken into account.
545 If the user requests a sublanguage where we have a main language, we send
546 out the main language.
548 If check_path is True, the URL path prefix will be checked for a language
549 code, otherwise this is skipped for backwards compatibility.
550 """
551 if check_path:
552 lang_code = get_language_from_path(request.path_info)
553 if lang_code is not None:
554 return lang_code
556 lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
557 if (
558 lang_code is not None
559 and lang_code in get_languages()
560 and check_for_language(lang_code)
561 ):
562 return lang_code
564 try:
565 return get_supported_language_variant(lang_code)
566 except LookupError:
567 pass
569 accept = request.META.get("HTTP_ACCEPT_LANGUAGE", "")
570 for accept_lang, unused in parse_accept_lang_header(accept):
571 if accept_lang == "*":
572 break
574 if not language_code_re.search(accept_lang):
575 continue
577 try:
578 return get_supported_language_variant(accept_lang)
579 except LookupError:
580 continue
581 return None
584@functools.lru_cache(maxsize=1000)
585def parse_accept_lang_header(lang_string):
586 """
587 Parse the lang_string, which is the body of an HTTP Accept-Language
588 header, and return a tuple of (lang, q-value), ordered by 'q' values.
590 Return an empty tuple if there are any format errors in lang_string.
591 """
592 result = []
593 pieces = accept_language_re.split(lang_string.lower())
594 if pieces[-1]:
595 return ()
596 for i in range(0, len(pieces) - 1, 3):
597 first, lang, priority = pieces[i : i + 3]
598 if first:
599 return ()
600 if priority:
601 priority = float(priority)
602 else:
603 priority = 1.0
604 result.append((lang, priority))
605 result.sort(key=lambda k: k[1], reverse=True)
606 return tuple(result)