123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515 |
- """Translation helper functions."""
- import functools
- import gettext as gettext_module
- import os
- import re
- import sys
- import warnings
- from collections import OrderedDict
- from threading import local
-
- from django.apps import apps
- from django.conf import settings
- from django.conf.locale import LANG_INFO
- from django.core.exceptions import AppRegistryNotReady
- from django.core.signals import setting_changed
- from django.dispatch import receiver
- from django.utils.safestring import SafeData, mark_safe
-
- from . import LANGUAGE_SESSION_KEY, to_language, to_locale
-
- # Translations are cached in a dictionary for every language.
- # The active translations are stored by threadid to make them thread local.
- _translations = {}
- _active = local()
-
- # The default translation is based on the settings file.
- _default = None
-
- # magic gettext number to separate context from message
- CONTEXT_SEPARATOR = "\x04"
-
- # Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9
- # and RFC 3066, section 2.1
- accept_language_re = re.compile(r'''
- ([A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*|\*) # "en", "en-au", "x-y-z", "es-419", "*"
- (?:\s*;\s*q=(0(?:\.\d{,3})?|1(?:\.0{,3})?))? # Optional "q=1.00", "q=0.8"
- (?:\s*,\s*|$) # Multiple accepts per header.
- ''', re.VERBOSE)
-
- language_code_re = re.compile(
- r'^[a-z]{1,8}(?:-[a-z0-9]{1,8})*(?:@[a-z0-9]{1,20})?$',
- re.IGNORECASE
- )
-
- language_code_prefix_re = re.compile(r'^/(\w+([@-]\w+)?)(/|$)')
-
-
- @receiver(setting_changed)
- def reset_cache(**kwargs):
- """
- Reset global state when LANGUAGES setting has been changed, as some
- languages should no longer be accepted.
- """
- if kwargs['setting'] in ('LANGUAGES', 'LANGUAGE_CODE'):
- check_for_language.cache_clear()
- get_languages.cache_clear()
- get_supported_language_variant.cache_clear()
-
-
- class DjangoTranslation(gettext_module.GNUTranslations):
- """
- Set up the GNUTranslations context with regard to output charset.
-
- This translation object will be constructed out of multiple GNUTranslations
- objects by merging their catalogs. It will construct an object for the
- requested language and add a fallback to the default language, if it's
- different from the requested language.
- """
- domain = 'django'
-
- def __init__(self, language, domain=None, localedirs=None):
- """Create a GNUTranslations() using many locale directories"""
- gettext_module.GNUTranslations.__init__(self)
- if domain is not None:
- self.domain = domain
-
- self.__language = language
- self.__to_language = to_language(language)
- self.__locale = to_locale(language)
- self._catalog = None
- # If a language doesn't have a catalog, use the Germanic default for
- # pluralization: anything except one is pluralized.
- self.plural = lambda n: int(n != 1)
-
- if self.domain == 'django':
- if localedirs is not None:
- # A module-level cache is used for caching 'django' translations
- warnings.warn("localedirs is ignored when domain is 'django'.", RuntimeWarning)
- localedirs = None
- self._init_translation_catalog()
-
- if localedirs:
- for localedir in localedirs:
- translation = self._new_gnu_trans(localedir)
- self.merge(translation)
- else:
- self._add_installed_apps_translations()
-
- self._add_local_translations()
- if self.__language == settings.LANGUAGE_CODE and self.domain == 'django' and self._catalog is None:
- # default lang should have at least one translation file available.
- raise IOError("No translation files found for default language %s." % settings.LANGUAGE_CODE)
- self._add_fallback(localedirs)
- if self._catalog is None:
- # No catalogs found for this language, set an empty catalog.
- self._catalog = {}
-
- def __repr__(self):
- return "<DjangoTranslation lang:%s>" % self.__language
-
- def _new_gnu_trans(self, localedir, use_null_fallback=True):
- """
- Return a mergeable gettext.GNUTranslations instance.
-
- A convenience wrapper. By default gettext uses 'fallback=False'.
- Using param `use_null_fallback` to avoid confusion with any other
- references to 'fallback'.
- """
- return gettext_module.translation(
- domain=self.domain,
- localedir=localedir,
- languages=[self.__locale],
- fallback=use_null_fallback,
- )
-
- def _init_translation_catalog(self):
- """Create a base catalog using global django translations."""
- settingsfile = sys.modules[settings.__module__].__file__
- localedir = os.path.join(os.path.dirname(settingsfile), 'locale')
- translation = self._new_gnu_trans(localedir)
- self.merge(translation)
-
- def _add_installed_apps_translations(self):
- """Merge translations from each installed app."""
- try:
- app_configs = reversed(list(apps.get_app_configs()))
- except AppRegistryNotReady:
- raise AppRegistryNotReady(
- "The translation infrastructure cannot be initialized before the "
- "apps registry is ready. Check that you don't make non-lazy "
- "gettext calls at import time.")
- for app_config in app_configs:
- localedir = os.path.join(app_config.path, 'locale')
- if os.path.exists(localedir):
- translation = self._new_gnu_trans(localedir)
- self.merge(translation)
-
- def _add_local_translations(self):
- """Merge translations defined in LOCALE_PATHS."""
- for localedir in reversed(settings.LOCALE_PATHS):
- translation = self._new_gnu_trans(localedir)
- self.merge(translation)
-
- def _add_fallback(self, localedirs=None):
- """Set the GNUTranslations() fallback with the default language."""
- # Don't set a fallback for the default language or any English variant
- # (as it's empty, so it'll ALWAYS fall back to the default language)
- if self.__language == settings.LANGUAGE_CODE or self.__language.startswith('en'):
- return
- if self.domain == 'django':
- # Get from cache
- default_translation = translation(settings.LANGUAGE_CODE)
- else:
- default_translation = DjangoTranslation(
- settings.LANGUAGE_CODE, domain=self.domain, localedirs=localedirs
- )
- self.add_fallback(default_translation)
-
- def merge(self, other):
- """Merge another translation into this catalog."""
- if not getattr(other, '_catalog', None):
- return # NullTranslations() has no _catalog
- if self._catalog is None:
- # Take plural and _info from first catalog found (generally Django's).
- self.plural = other.plural
- self._info = other._info.copy()
- self._catalog = other._catalog.copy()
- else:
- self._catalog.update(other._catalog)
- if other._fallback:
- self.add_fallback(other._fallback)
-
- def language(self):
- """Return the translation language."""
- return self.__language
-
- def to_language(self):
- """Return the translation language name."""
- return self.__to_language
-
-
- def translation(language):
- """
- Return a translation object in the default 'django' domain.
- """
- global _translations
- if language not in _translations:
- _translations[language] = DjangoTranslation(language)
- return _translations[language]
-
-
- def activate(language):
- """
- Fetch the translation object for a given language and install it as the
- current translation object for the current thread.
- """
- if not language:
- return
- _active.value = translation(language)
-
-
- def deactivate():
- """
- Uninstall the active translation object so that further _() calls resolve
- to the default translation object.
- """
- if hasattr(_active, "value"):
- del _active.value
-
-
- def deactivate_all():
- """
- Make the active translation object a NullTranslations() instance. This is
- useful when we want delayed translations to appear as the original string
- for some reason.
- """
- _active.value = gettext_module.NullTranslations()
- _active.value.to_language = lambda *args: None
-
-
- def get_language():
- """Return the currently selected language."""
- t = getattr(_active, "value", None)
- if t is not None:
- try:
- return t.to_language()
- except AttributeError:
- pass
- # If we don't have a real translation object, assume it's the default language.
- return settings.LANGUAGE_CODE
-
-
- def get_language_bidi():
- """
- Return selected language's BiDi layout.
-
- * False = left-to-right layout
- * True = right-to-left layout
- """
- lang = get_language()
- if lang is None:
- return False
- else:
- base_lang = get_language().split('-')[0]
- return base_lang in settings.LANGUAGES_BIDI
-
-
- def catalog():
- """
- Return the current active catalog for further processing.
- This can be used if you need to modify the catalog or want to access the
- whole message catalog instead of just translating one string.
- """
- global _default
-
- t = getattr(_active, "value", None)
- if t is not None:
- return t
- if _default is None:
- _default = translation(settings.LANGUAGE_CODE)
- return _default
-
-
- def gettext(message):
- """
- Translate the 'message' string. It uses the current thread to find the
- translation object to use. If no current translation is activated, the
- message will be run through the default translation object.
- """
- global _default
-
- eol_message = message.replace('\r\n', '\n').replace('\r', '\n')
-
- if eol_message:
- _default = _default or translation(settings.LANGUAGE_CODE)
- translation_object = getattr(_active, "value", _default)
-
- result = translation_object.gettext(eol_message)
- else:
- # Return an empty value of the corresponding type if an empty message
- # is given, instead of metadata, which is the default gettext behavior.
- result = type(message)('')
-
- if isinstance(message, SafeData):
- return mark_safe(result)
-
- return result
-
-
- def pgettext(context, message):
- msg_with_ctxt = "%s%s%s" % (context, CONTEXT_SEPARATOR, message)
- result = gettext(msg_with_ctxt)
- if CONTEXT_SEPARATOR in result:
- # Translation not found
- result = message
- elif isinstance(message, SafeData):
- result = mark_safe(result)
- return result
-
-
- def gettext_noop(message):
- """
- Mark strings for translation but don't translate them now. This can be
- used to store strings in global variables that should stay in the base
- language (because they might be used externally) and will be translated
- later.
- """
- return message
-
-
- def do_ntranslate(singular, plural, number, translation_function):
- global _default
-
- t = getattr(_active, "value", None)
- if t is not None:
- return getattr(t, translation_function)(singular, plural, number)
- if _default is None:
- _default = translation(settings.LANGUAGE_CODE)
- return getattr(_default, translation_function)(singular, plural, number)
-
-
- def ngettext(singular, plural, number):
- """
- Return a string of the translation of either the singular or plural,
- based on the number.
- """
- return do_ntranslate(singular, plural, number, 'ngettext')
-
-
- def npgettext(context, singular, plural, number):
- msgs_with_ctxt = ("%s%s%s" % (context, CONTEXT_SEPARATOR, singular),
- "%s%s%s" % (context, CONTEXT_SEPARATOR, plural),
- number)
- result = ngettext(*msgs_with_ctxt)
- if CONTEXT_SEPARATOR in result:
- # Translation not found
- result = ngettext(singular, plural, number)
- return result
-
-
- def all_locale_paths():
- """
- Return a list of paths to user-provides languages files.
- """
- globalpath = os.path.join(
- os.path.dirname(sys.modules[settings.__module__].__file__), 'locale')
- app_paths = []
- for app_config in apps.get_app_configs():
- locale_path = os.path.join(app_config.path, 'locale')
- if os.path.exists(locale_path):
- app_paths.append(locale_path)
- return [globalpath, *settings.LOCALE_PATHS, *app_paths]
-
-
- @functools.lru_cache(maxsize=1000)
- def check_for_language(lang_code):
- """
- Check whether there is a global language file for the given language
- code. This is used to decide whether a user-provided language is
- available.
-
- lru_cache should have a maxsize to prevent from memory exhaustion attacks,
- as the provided language codes are taken from the HTTP request. See also
- <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
- """
- # First, a quick check to make sure lang_code is well-formed (#21458)
- if lang_code is None or not language_code_re.search(lang_code):
- return False
- return any(
- gettext_module.find('django', path, [to_locale(lang_code)]) is not None
- for path in all_locale_paths()
- )
-
-
- @functools.lru_cache()
- def get_languages():
- """
- Cache of settings.LANGUAGES in an OrderedDict for easy lookups by key.
- """
- return OrderedDict(settings.LANGUAGES)
-
-
- @functools.lru_cache(maxsize=1000)
- def get_supported_language_variant(lang_code, strict=False):
- """
- Return the language code that's listed in supported languages, possibly
- selecting a more generic variant. Raise LookupError if nothing is found.
-
- If `strict` is False (the default), look for a country-specific variant
- when neither the language code nor its generic variant is found.
-
- lru_cache should have a maxsize to prevent from memory exhaustion attacks,
- as the provided language codes are taken from the HTTP request. See also
- <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
- """
- if lang_code:
- # If 'fr-ca' is not supported, try special fallback or language-only 'fr'.
- possible_lang_codes = [lang_code]
- try:
- possible_lang_codes.extend(LANG_INFO[lang_code]['fallback'])
- except KeyError:
- pass
- generic_lang_code = lang_code.split('-')[0]
- possible_lang_codes.append(generic_lang_code)
- supported_lang_codes = get_languages()
-
- for code in possible_lang_codes:
- if code in supported_lang_codes and check_for_language(code):
- return code
- if not strict:
- # if fr-fr is not supported, try fr-ca.
- for supported_code in supported_lang_codes:
- if supported_code.startswith(generic_lang_code + '-'):
- return supported_code
- raise LookupError(lang_code)
-
-
- def get_language_from_path(path, strict=False):
- """
- Return the language code if there's a valid language code found in `path`.
-
- If `strict` is False (the default), look for a country-specific variant
- when neither the language code nor its generic variant is found.
- """
- regex_match = language_code_prefix_re.match(path)
- if not regex_match:
- return None
- lang_code = regex_match.group(1)
- try:
- return get_supported_language_variant(lang_code, strict=strict)
- except LookupError:
- return None
-
-
- def get_language_from_request(request, check_path=False):
- """
- Analyze the request to find what language the user wants the system to
- show. Only languages listed in settings.LANGUAGES are taken into account.
- If the user requests a sublanguage where we have a main language, we send
- out the main language.
-
- If check_path is True, the URL path prefix will be checked for a language
- code, otherwise this is skipped for backwards compatibility.
- """
- if check_path:
- lang_code = get_language_from_path(request.path_info)
- if lang_code is not None:
- return lang_code
-
- supported_lang_codes = get_languages()
-
- if hasattr(request, 'session'):
- lang_code = request.session.get(LANGUAGE_SESSION_KEY)
- if lang_code in supported_lang_codes and lang_code is not None and check_for_language(lang_code):
- return lang_code
-
- lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
-
- try:
- return get_supported_language_variant(lang_code)
- except LookupError:
- pass
-
- accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '')
- for accept_lang, unused in parse_accept_lang_header(accept):
- if accept_lang == '*':
- break
-
- if not language_code_re.search(accept_lang):
- continue
-
- try:
- return get_supported_language_variant(accept_lang)
- except LookupError:
- continue
-
- try:
- return get_supported_language_variant(settings.LANGUAGE_CODE)
- except LookupError:
- return settings.LANGUAGE_CODE
-
-
- @functools.lru_cache(maxsize=1000)
- def parse_accept_lang_header(lang_string):
- """
- Parse the lang_string, which is the body of an HTTP Accept-Language
- header, and return a tuple of (lang, q-value), ordered by 'q' values.
-
- Return an empty tuple if there are any format errors in lang_string.
- """
- result = []
- pieces = accept_language_re.split(lang_string.lower())
- if pieces[-1]:
- return ()
- for i in range(0, len(pieces) - 1, 3):
- first, lang, priority = pieces[i:i + 3]
- if first:
- return ()
- if priority:
- priority = float(priority)
- else:
- priority = 1.0
- result.append((lang, priority))
- result.sort(key=lambda k: k[1], reverse=True)
- return tuple(result)
|