translation helper functions import functools import gettext as gettextmodule import os import re import sys import warnings from asgiref local import local from django apps import apps from django conf import settings from django conf locale import langinfo from django core exceptions import appregistrynotready from django core signals import settingchanged from django dispatch import receiver from django utils regexhelper import lazyrecompile from django utils safestring import safedata marksafe from import tolanguage tolocale 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 contextseparator x04 maximum number of characters that will be parsed from the acceptlanguage header to prevent possible denial of service or memory exhaustion attacks about 10x longer than the longest value shown on mdns acceptlanguage page acceptlanguageheadermaxlength 500 format of acceptlanguage header values from rfc 9110 sections 12 4 2 and 12 5 4 and rfc 5646 section 2 1 acceptlanguagere lazyrecompile r en enau xyz es419 azaz1 8 azaz091 8 optional q1 00 q0 8 s sq0 09 3 1 0 3 multiple accepts per header s s reset global state when languages setting has been changed as some languages should no longer be accepted simulate a dict for djangotranslation catalog so as multiple catalogs with different plural equations are kept separate merge if plural function is the same else prepend 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 create a gnutranslations using many locale directories gettextmodule gnutranslations initself if domain is not none self domain domain self language language self tolanguage tolanguagelanguage self locale tolocalelanguage 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 intn 1 if self domain django if localedirs is not none a modulelevel cache is used for caching django translations warnings warn localedirs is ignored when domain is django runtimewarning localedirs none self inittranslationcatalog if localedirs for localedir in localedirs translation self newgnutranslocaledir self mergetranslation else self addinstalledappstranslations self addlocaltranslations if self language settings languagecode and self domain django and self catalog is none default lang should have at least one translation file available raise oserror no translation files found for default language s settings languagecode self addfallbacklocaledirs if self catalog is none no catalogs found for this language set an empty catalog self catalog translationcatalog def reprself return djangotranslation lang s self language def newgnutransself localedir usenullfallbacktrue return gettextmodule translation domainself domain localedirlocaledir languagesself locale fallbackusenullfallback def inittranslationcatalogself merge translations from each installed app try appconfigs reversedapps getappconfigs except appregistrynotready raise appregistrynotready the translation infrastructure cannot be initialized before the apps registry is ready check that you don t make nonlazy gettext calls at import time for appconfig in appconfigs localedir os path joinappconfig path locale if os path existslocaledir translation self newgnutranslocaledir self mergetranslation def addlocaltranslationsself 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 languagecode or self language startswith en return if self domain django get from cache defaulttranslation translationsettings languagecode else defaulttranslation djangotranslation settings languagecode domainself domain localedirslocaledirs self addfallbackdefaulttranslation def mergeself other take plural and info from first catalog found generally django s return the translation language return self language def tolanguageself return a translation object in the default django domain fetch the translation object for a given language and install it as the current translation object for the current thread uninstall the active translation object so that further calls resolve to the default translation object 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 return the currently selected language t getattractive value none if t is not none try return t tolanguage except attributeerror pass if we don t have a real translation object assume it s the default language return settings languagecode def getlanguagebidi lang getlanguage if lang is none return false else baselang getlanguage split0 return baselang in settings languagesbidi def catalog global default t getattractive value none if t is not none return t if default is none default translationsettings languagecode return default def gettextmessage global default eolmessage message replacern n replacer n if eolmessage default default or translationsettings languagecode translationobject getattractive value default result translationobject gettexteolmessage 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 typemessage if isinstancemessage safedata return marksaferesult return result def pgettextcontext message msgwithctxt sss context contextseparator message result gettextmsgwithctxt if contextseparator in result translation not found result message elif isinstancemessage safedata result marksaferesult return result def gettextnoopmessage return message def dontranslatesingular plural number translationfunction global default t getattractive value none if t is not none return getattrt translationfunctionsingular plural number if default is none default translationsettings languagecode return getattrdefault translationfunctionsingular plural number def ngettextsingular plural number return dontranslatesingular plural number ngettext def npgettextcontext singular plural number msgswithctxt sss context contextseparator singular sss context contextseparator plural number result ngettextmsgswithctxt if contextseparator in result translation not found result ngettextsingular plural number return result def alllocalepaths globalpath os path join os path dirnamesys modulessettings module file locale apppaths for appconfig in apps getappconfigs localepath os path joinappconfig path locale if os path existslocalepath apppaths appendlocalepath return globalpath settings localepaths apppaths functools lrucachemaxsize1000 def checkforlanguagelangcode first a quick check to make sure langcode is wellformed 21458 if langcode is none or not languagecodere searchlangcode return false return any gettextmodule finddjango path tolocalelangcode is not none for path in alllocalepaths functools lrucache def getlanguages return key lower value for key value in dictsettings languages items functools lrucachemaxsize1000 def getsupportedlanguagevariantlangcode strictfalse if langcode if zhhanttw is not supported try special fallback or subsequent language codes i e zhhant and zh possiblelangcodes langcode try possiblelangcodes extendlanginfolangcodefallback except keyerror pass i none while i langcode rfind 0 i 1 possiblelangcodes appendlangcode i genericlangcode possiblelangcodes1 supportedlangcodes getlanguages for code in possiblelangcodes if code lower in supportedlangcodes and checkforlanguagecode return code if not strict if frfr is not supported try frca for supportedcode in supportedlangcodes if supportedcode startswithgenericlangcode return supportedcode raise lookuperrorlangcode def getlanguagefrompathpath strictfalse regexmatch languagecodeprefixre matchpath if not regexmatch return none langcode regexmatch1 try return getsupportedlanguagevariantlangcode strictstrict except lookuperror return none def getlanguagefromrequestrequest checkpathfalse if checkpath langcode getlanguagefrompathrequest pathinfo if langcode is not none return langcode langcode request cookies getsettings languagecookiename if langcode is not none and langcode in getlanguages and checkforlanguagelangcode return langcode try return getsupportedlanguagevariantlangcode except lookuperror pass accept request meta gethttpacceptlanguage for acceptlang unused in parseacceptlangheaderaccept if acceptlang break if not languagecodere searchacceptlang continue try return getsupportedlanguagevariantacceptlang except lookuperror continue try return getsupportedlanguagevariantsettings languagecode except lookuperror return settings languagecode functools lrucachemaxsize1000 def parseacceptlangheaderlangstring result pieces acceptlanguagere splitlangstring lower if pieces1 return for i in range0 lenpieces 1 3 first lang priority piecesi i 3 if first return if priority priority floatpriority else priority 1 0 result appendlang priority result sortkeylambda k k1 reversetrue return tupleresult def parseacceptlangheaderlangstring if the header value doesn t exceed the maximum allowed length parse it if lenlangstring acceptlanguageheadermaxlength return parseacceptlangheaderlangstring if there is at least one comma in the value parse up to the last comma before the max length skipping any truncated parts at the end of the header value if index langstring rfind 0 acceptlanguageheadermaxlength 0 return parseacceptlangheaderlangstring index don t attempt to parse if there is only one languagerange value which is longer than the maximum allowed length and so truncated return translations are cached in a dictionary for every language the active translations are stored by threadid to make them thread local the default translation is based on the settings file magic gettext number to separate context from message maximum number of characters that will be parsed from the accept language header to prevent possible denial of service or memory exhaustion attacks about 10x longer than the longest value shown on mdn s accept language page format of accept language header values from rfc 9110 sections 12 4 2 and 12 5 4 and rfc 5646 section 2 1 en en au x y z es 419 a za z 1 8 a za z0 9 1 8 optional q 1 00 q 0 8 s s q 0 0 9 3 1 0 3 multiple accepts per header s s reset global state when languages setting has been changed as some languages should no longer be accepted simulate a dict for djangotranslation _catalog so as multiple catalogs with different plural equations are kept separate merge if plural function is the same else prepend 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 create a gnutranslations using many locale directories if a language doesn t have a catalog use the germanic default for pluralization anything except one is pluralized a module level cache is used for caching django translations default lang should have at least one translation file available no catalogs found for this language set an empty catalog 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 create a base catalog using global django translations merge translations from each installed app merge translations defined in locale_paths 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 get from cache merge another translation into this catalog nulltranslations has no _catalog take plural and _info from first catalog found generally django s return the translation language return the translation language name return a translation object in the default django domain fetch the translation object for a given language and install it as the current translation object for the current thread uninstall the active translation object so that further _ calls resolve to the default translation object 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 return the currently selected language if we don t have a real translation object assume it s the default language return selected language s bidi layout false left to right layout true right to left layout 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 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 return an empty value of the corresponding type if an empty message is given instead of metadata which is the default gettext behavior translation not found 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 a string of the translation of either the singular or plural based on the number translation not found return a list of paths to user provides languages files 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 cache of settings languages in a dictionary for easy lookups by key convert keys to lowercase as they should be treated as case insensitive 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 zh hant tw is not supported try special fallback or subsequent language codes i e zh hant and zh if fr fr is not supported try fr ca 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 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 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 parse the value of the accept language header up to a maximum length the value of the header is truncated to a maximum length to avoid potential denial of service and memory exhaustion attacks excessive memory could be used if the raw value is very large as it would be cached due to the use of functools lru_cache to avoid repetitive parsing of common header values if the header value doesn t exceed the maximum allowed length parse it if there is at least one comma in the value parse up to the last comma before the max length skipping any truncated parts at the end of the header value don t attempt to parse if there is only one language range value which is longer than the maximum allowed length and so truncated
import functools
import gettext as gettext_module
import os
import re
import sys
import warnings

from asgiref.local 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.regex_helper import _lazy_re_compile
from django.utils.safestring import SafeData, mark_safe

from . import to_language, to_locale



_translations = {}
_active = Local()


_default = None


CONTEXT_SEPARATOR = "\x04"




ACCEPT_LANGUAGE_HEADER_MAX_LENGTH = 500



accept_language_re = _lazy_re_compile(
    r,
    re.VERBOSE,
)

language_code_re = _lazy_re_compile(
    r"^[a-z]{1,8}(?:-[a-z0-9]{1,8})*(?:@[a-z0-9]{1,20})?$", re.IGNORECASE
)

language_code_prefix_re = _lazy_re_compile(r"^/(\w+([@-]\w+){0,2})(/|$)")


@receiver(setting_changed)
def reset_cache(*, setting, **kwargs):
    
    if setting in ("LANGUAGES", "LANGUAGE_CODE"):
        check_for_language.cache_clear()
        get_languages.cache_clear()
        get_supported_language_variant.cache_clear()


class TranslationCatalog:
    

    def __init__(self, trans=None):
        self._catalogs = [trans._catalog.copy()] if trans else [{}]
        self._plurals = [trans.plural] if trans else [lambda n: int(n != 1)]

    def __getitem__(self, key):
        for cat in self._catalogs:
            try:
                return cat[key]
            except KeyError:
                pass
        raise KeyError(key)

    def __setitem__(self, key, value):
        self._catalogs[0][key] = value

    def __contains__(self, key):
        return any(key in cat for cat in self._catalogs)

    def items(self):
        for cat in self._catalogs:
            yield from cat.items()

    def keys(self):
        for cat in self._catalogs:
            yield from cat.keys()

    def update(self, trans):
        
        for cat, plural in zip(self._catalogs, self._plurals):
            if trans.plural.__code__ == plural.__code__:
                cat.update(trans._catalog)
                break
        else:
            self._catalogs.insert(0, trans._catalog.copy())
            self._plurals.insert(0, trans.plural)

    def get(self, key, default=None):
        missing = object()
        for cat in self._catalogs:
            result = cat.get(key, missing)
            if result is not missing:
                return result
        return default

    def plural(self, msgid, num):
        for cat, plural in zip(self._catalogs, self._plurals):
            tmsg = cat.get((msgid, plural(num)))
            if tmsg is not None:
                return tmsg
        raise KeyError


class DjangoTranslation(gettext_module.GNUTranslations):
    

    domain = "django"

    def __init__(self, language, domain=None, localedirs=None):
        
        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
        
        
        self.plural = lambda n: int(n != 1)

        if self.domain == "django":
            if localedirs is not None:
                
                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
        ):
            
            raise OSError(
                "No translation files found for default language %s."
                % settings.LANGUAGE_CODE
            )
        self._add_fallback(localedirs)
        if self._catalog is None:
            
            self._catalog = TranslationCatalog()

    def __repr__(self):
        return "<DjangoTranslation lang:%s>" % self.__language

    def _new_gnu_trans(self, localedir, use_null_fallback=True):
        
        return gettext_module.translation(
            domain=self.domain,
            localedir=localedir,
            languages=[self.__locale],
            fallback=use_null_fallback,
        )

    def _init_translation_catalog(self):
        
        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):
        
        try:
            app_configs = reversed(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):
        
        for localedir in reversed(settings.LOCALE_PATHS):
            translation = self._new_gnu_trans(localedir)
            self.merge(translation)

    def _add_fallback(self, localedirs=None):
        
        
        
        if self.__language == settings.LANGUAGE_CODE or self.__language.startswith(
            "en"
        ):
            return
        if self.domain == "django":
            
            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):
        
        if not getattr(other, "_catalog", None):
            return  
        if self._catalog is None:
            
            self.plural = other.plural
            self._info = other._info.copy()
            self._catalog = TranslationCatalog(other)
        else:
            self._catalog.update(other)
        if other._fallback:
            self.add_fallback(other._fallback)

    def language(self):
        
        return self.__language

    def to_language(self):
        
        return self.__to_language

    def ngettext(self, msgid1, msgid2, n):
        try:
            tmsg = self._catalog.plural(msgid1, n)
        except KeyError:
            if self._fallback:
                return self._fallback.ngettext(msgid1, msgid2, n)
            if n == 1:
                tmsg = msgid1
            else:
                tmsg = msgid2
        return tmsg


def translation(language):
    
    global _translations
    if language not in _translations:
        _translations[language] = DjangoTranslation(language)
    return _translations[language]


def activate(language):
    
    if not language:
        return
    _active.value = translation(language)


def deactivate():
    
    if hasattr(_active, "value"):
        del _active.value


def deactivate_all():
    
    _active.value = gettext_module.NullTranslations()
    _active.value.to_language = lambda *args: None


def get_language():
    
    t = getattr(_active, "value", None)
    if t is not None:
        try:
            return t.to_language()
        except AttributeError:
            pass
    
    return settings.LANGUAGE_CODE


def get_language_bidi():
    
    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():
    
    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):
    
    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:
        
        
        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:
        
        result = message
    elif isinstance(message, SafeData):
        result = mark_safe(result)
    return result


def gettext_noop(message):
    
    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 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:
        
        result = ngettext(singular, plural, number)
    return result


def all_locale_paths():
    
    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):
    
    
    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():
    
    return {key.lower(): value for key, value in dict(settings.LANGUAGES).items()}


@functools.lru_cache(maxsize=1000)
def get_supported_language_variant(lang_code, strict=False):
    
    if lang_code:
        
        
        possible_lang_codes = [lang_code]
        try:
            possible_lang_codes.extend(LANG_INFO[lang_code]["fallback"])
        except KeyError:
            pass
        i = None
        while (i := lang_code.rfind("-", 0, i)) > -1:
            possible_lang_codes.append(lang_code[:i])
        generic_lang_code = possible_lang_codes[-1]
        supported_lang_codes = get_languages()

        for code in possible_lang_codes:
            if code.lower() in supported_lang_codes and check_for_language(code):
                return code
        if not strict:
            
            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):
    
    regex_match = language_code_prefix_re.match(path)
    if not regex_match:
        return None
    lang_code = regex_match[1]
    try:
        return get_supported_language_variant(lang_code, strict=strict)
    except LookupError:
        return None


def get_language_from_request(request, check_path=False):
    
    if check_path:
        lang_code = get_language_from_path(request.path_info)
        if lang_code is not None:
            return lang_code

    lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
    if (
        lang_code is not None
        and lang_code in get_languages()
        and check_for_language(lang_code)
    ):
        return lang_code

    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):
    
    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)


def parse_accept_lang_header(lang_string):
    
    
    if len(lang_string) <= ACCEPT_LANGUAGE_HEADER_MAX_LENGTH:
        return _parse_accept_lang_header(lang_string)

    
    
    
    if (index := lang_string.rfind(",", 0, ACCEPT_LANGUAGE_HEADER_MAX_LENGTH)) > 0:
        return _parse_accept_lang_header(lang_string[:index])

    
    
    return ()