docker setup
This commit is contained in:
		| @ -0,0 +1,301 @@ | ||||
| """ | ||||
| Internationalization support. | ||||
| """ | ||||
| from contextlib import ContextDecorator | ||||
| from decimal import ROUND_UP, Decimal | ||||
|  | ||||
| from django.utils.autoreload import autoreload_started, file_changed | ||||
| from django.utils.functional import lazy | ||||
| from django.utils.regex_helper import _lazy_re_compile | ||||
|  | ||||
| __all__ = [ | ||||
|     "activate", | ||||
|     "deactivate", | ||||
|     "override", | ||||
|     "deactivate_all", | ||||
|     "get_language", | ||||
|     "get_language_from_request", | ||||
|     "get_language_info", | ||||
|     "get_language_bidi", | ||||
|     "check_for_language", | ||||
|     "to_language", | ||||
|     "to_locale", | ||||
|     "templatize", | ||||
|     "gettext", | ||||
|     "gettext_lazy", | ||||
|     "gettext_noop", | ||||
|     "ngettext", | ||||
|     "ngettext_lazy", | ||||
|     "pgettext", | ||||
|     "pgettext_lazy", | ||||
|     "npgettext", | ||||
|     "npgettext_lazy", | ||||
| ] | ||||
|  | ||||
|  | ||||
| class TranslatorCommentWarning(SyntaxWarning): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| # Here be dragons, so a short explanation of the logic won't hurt: | ||||
| # We are trying to solve two problems: (1) access settings, in particular | ||||
| # settings.USE_I18N, as late as possible, so that modules can be imported | ||||
| # without having to first configure Django, and (2) if some other code creates | ||||
| # a reference to one of these functions, don't break that reference when we | ||||
| # replace the functions with their real counterparts (once we do access the | ||||
| # settings). | ||||
|  | ||||
|  | ||||
| class Trans: | ||||
|     """ | ||||
|     The purpose of this class is to store the actual translation function upon | ||||
|     receiving the first call to that function. After this is done, changes to | ||||
|     USE_I18N will have no effect to which function is served upon request. If | ||||
|     your tests rely on changing USE_I18N, you can delete all the functions | ||||
|     from _trans.__dict__. | ||||
|  | ||||
|     Note that storing the function with setattr will have a noticeable | ||||
|     performance effect, as access to the function goes the normal path, | ||||
|     instead of using __getattr__. | ||||
|     """ | ||||
|  | ||||
|     def __getattr__(self, real_name): | ||||
|         from django.conf import settings | ||||
|  | ||||
|         if settings.USE_I18N: | ||||
|             from django.utils.translation import trans_real as trans | ||||
|             from django.utils.translation.reloader import ( | ||||
|                 translation_file_changed, | ||||
|                 watch_for_translation_changes, | ||||
|             ) | ||||
|  | ||||
|             autoreload_started.connect( | ||||
|                 watch_for_translation_changes, dispatch_uid="translation_file_changed" | ||||
|             ) | ||||
|             file_changed.connect( | ||||
|                 translation_file_changed, dispatch_uid="translation_file_changed" | ||||
|             ) | ||||
|         else: | ||||
|             from django.utils.translation import trans_null as trans | ||||
|         setattr(self, real_name, getattr(trans, real_name)) | ||||
|         return getattr(trans, real_name) | ||||
|  | ||||
|  | ||||
| _trans = Trans() | ||||
|  | ||||
| # The Trans class is no more needed, so remove it from the namespace. | ||||
| del Trans | ||||
|  | ||||
|  | ||||
| def gettext_noop(message): | ||||
|     return _trans.gettext_noop(message) | ||||
|  | ||||
|  | ||||
| def gettext(message): | ||||
|     return _trans.gettext(message) | ||||
|  | ||||
|  | ||||
| def ngettext(singular, plural, number): | ||||
|     return _trans.ngettext(singular, plural, number) | ||||
|  | ||||
|  | ||||
| def pgettext(context, message): | ||||
|     return _trans.pgettext(context, message) | ||||
|  | ||||
|  | ||||
| def npgettext(context, singular, plural, number): | ||||
|     return _trans.npgettext(context, singular, plural, number) | ||||
|  | ||||
|  | ||||
| gettext_lazy = lazy(gettext, str) | ||||
| pgettext_lazy = lazy(pgettext, str) | ||||
|  | ||||
|  | ||||
| def lazy_number(func, resultclass, number=None, **kwargs): | ||||
|     if isinstance(number, int): | ||||
|         kwargs["number"] = number | ||||
|         proxy = lazy(func, resultclass)(**kwargs) | ||||
|     else: | ||||
|         original_kwargs = kwargs.copy() | ||||
|  | ||||
|         class NumberAwareString(resultclass): | ||||
|             def __bool__(self): | ||||
|                 return bool(kwargs["singular"]) | ||||
|  | ||||
|             def _get_number_value(self, values): | ||||
|                 try: | ||||
|                     return values[number] | ||||
|                 except KeyError: | ||||
|                     raise KeyError( | ||||
|                         "Your dictionary lacks key '%s'. Please provide " | ||||
|                         "it, because it is required to determine whether " | ||||
|                         "string is singular or plural." % number | ||||
|                     ) | ||||
|  | ||||
|             def _translate(self, number_value): | ||||
|                 kwargs["number"] = number_value | ||||
|                 return func(**kwargs) | ||||
|  | ||||
|             def format(self, *args, **kwargs): | ||||
|                 number_value = ( | ||||
|                     self._get_number_value(kwargs) if kwargs and number else args[0] | ||||
|                 ) | ||||
|                 return self._translate(number_value).format(*args, **kwargs) | ||||
|  | ||||
|             def __mod__(self, rhs): | ||||
|                 if isinstance(rhs, dict) and number: | ||||
|                     number_value = self._get_number_value(rhs) | ||||
|                 else: | ||||
|                     number_value = rhs | ||||
|                 translated = self._translate(number_value) | ||||
|                 try: | ||||
|                     translated %= rhs | ||||
|                 except TypeError: | ||||
|                     # String doesn't contain a placeholder for the number. | ||||
|                     pass | ||||
|                 return translated | ||||
|  | ||||
|         proxy = lazy(lambda **kwargs: NumberAwareString(), NumberAwareString)(**kwargs) | ||||
|         proxy.__reduce__ = lambda: ( | ||||
|             _lazy_number_unpickle, | ||||
|             (func, resultclass, number, original_kwargs), | ||||
|         ) | ||||
|     return proxy | ||||
|  | ||||
|  | ||||
| def _lazy_number_unpickle(func, resultclass, number, kwargs): | ||||
|     return lazy_number(func, resultclass, number=number, **kwargs) | ||||
|  | ||||
|  | ||||
| def ngettext_lazy(singular, plural, number=None): | ||||
|     return lazy_number(ngettext, str, singular=singular, plural=plural, number=number) | ||||
|  | ||||
|  | ||||
| def npgettext_lazy(context, singular, plural, number=None): | ||||
|     return lazy_number( | ||||
|         npgettext, str, context=context, singular=singular, plural=plural, number=number | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def activate(language): | ||||
|     return _trans.activate(language) | ||||
|  | ||||
|  | ||||
| def deactivate(): | ||||
|     return _trans.deactivate() | ||||
|  | ||||
|  | ||||
| class override(ContextDecorator): | ||||
|     def __init__(self, language, deactivate=False): | ||||
|         self.language = language | ||||
|         self.deactivate = deactivate | ||||
|  | ||||
|     def __enter__(self): | ||||
|         self.old_language = get_language() | ||||
|         if self.language is not None: | ||||
|             activate(self.language) | ||||
|         else: | ||||
|             deactivate_all() | ||||
|  | ||||
|     def __exit__(self, exc_type, exc_value, traceback): | ||||
|         if self.old_language is None: | ||||
|             deactivate_all() | ||||
|         elif self.deactivate: | ||||
|             deactivate() | ||||
|         else: | ||||
|             activate(self.old_language) | ||||
|  | ||||
|  | ||||
| def get_language(): | ||||
|     return _trans.get_language() | ||||
|  | ||||
|  | ||||
| def get_language_bidi(): | ||||
|     return _trans.get_language_bidi() | ||||
|  | ||||
|  | ||||
| def check_for_language(lang_code): | ||||
|     return _trans.check_for_language(lang_code) | ||||
|  | ||||
|  | ||||
| def to_language(locale): | ||||
|     """Turn a locale name (en_US) into a language name (en-us).""" | ||||
|     p = locale.find("_") | ||||
|     if p >= 0: | ||||
|         return locale[:p].lower() + "-" + locale[p + 1 :].lower() | ||||
|     else: | ||||
|         return locale.lower() | ||||
|  | ||||
|  | ||||
| def to_locale(language): | ||||
|     """Turn a language name (en-us) into a locale name (en_US).""" | ||||
|     lang, _, country = language.lower().partition("-") | ||||
|     if not country: | ||||
|         return language[:3].lower() + language[3:] | ||||
|     # A language with > 2 characters after the dash only has its first | ||||
|     # character after the dash capitalized; e.g. sr-latn becomes sr_Latn. | ||||
|     # A language with 2 characters after the dash has both characters | ||||
|     # capitalized; e.g. en-us becomes en_US. | ||||
|     country, _, tail = country.partition("-") | ||||
|     country = country.title() if len(country) > 2 else country.upper() | ||||
|     if tail: | ||||
|         country += "-" + tail | ||||
|     return lang + "_" + country | ||||
|  | ||||
|  | ||||
| def get_language_from_request(request, check_path=False): | ||||
|     return _trans.get_language_from_request(request, check_path) | ||||
|  | ||||
|  | ||||
| def get_language_from_path(path): | ||||
|     return _trans.get_language_from_path(path) | ||||
|  | ||||
|  | ||||
| def get_supported_language_variant(lang_code, *, strict=False): | ||||
|     return _trans.get_supported_language_variant(lang_code, strict) | ||||
|  | ||||
|  | ||||
| def templatize(src, **kwargs): | ||||
|     from .template import templatize | ||||
|  | ||||
|     return templatize(src, **kwargs) | ||||
|  | ||||
|  | ||||
| def deactivate_all(): | ||||
|     return _trans.deactivate_all() | ||||
|  | ||||
|  | ||||
| def get_language_info(lang_code): | ||||
|     from django.conf.locale import LANG_INFO | ||||
|  | ||||
|     try: | ||||
|         lang_info = LANG_INFO[lang_code] | ||||
|         if "fallback" in lang_info and "name" not in lang_info: | ||||
|             info = get_language_info(lang_info["fallback"][0]) | ||||
|         else: | ||||
|             info = lang_info | ||||
|     except KeyError: | ||||
|         if "-" not in lang_code: | ||||
|             raise KeyError("Unknown language code %s." % lang_code) | ||||
|         generic_lang_code = lang_code.split("-")[0] | ||||
|         try: | ||||
|             info = LANG_INFO[generic_lang_code] | ||||
|         except KeyError: | ||||
|             raise KeyError( | ||||
|                 "Unknown language code %s and %s." % (lang_code, generic_lang_code) | ||||
|             ) | ||||
|  | ||||
|     if info: | ||||
|         info["name_translated"] = gettext_lazy(info["name"]) | ||||
|     return info | ||||
|  | ||||
|  | ||||
| trim_whitespace_re = _lazy_re_compile(r"\s*\n\s*") | ||||
|  | ||||
|  | ||||
| def trim_whitespace(s): | ||||
|     return trim_whitespace_re.sub(" ", s.strip()) | ||||
|  | ||||
|  | ||||
| def round_away_from_one(value): | ||||
|     return int(Decimal(value - 1).quantize(Decimal("0"), rounding=ROUND_UP)) + 1 | ||||
| @ -0,0 +1,36 @@ | ||||
| from pathlib import Path | ||||
|  | ||||
| from asgiref.local import Local | ||||
|  | ||||
| from django.apps import apps | ||||
| from django.utils.autoreload import is_django_module | ||||
|  | ||||
|  | ||||
| def watch_for_translation_changes(sender, **kwargs): | ||||
|     """Register file watchers for .mo files in potential locale paths.""" | ||||
|     from django.conf import settings | ||||
|  | ||||
|     if settings.USE_I18N: | ||||
|         directories = [Path("locale")] | ||||
|         directories.extend( | ||||
|             Path(config.path) / "locale" | ||||
|             for config in apps.get_app_configs() | ||||
|             if not is_django_module(config.module) | ||||
|         ) | ||||
|         directories.extend(Path(p) for p in settings.LOCALE_PATHS) | ||||
|         for path in directories: | ||||
|             sender.watch_dir(path, "**/*.mo") | ||||
|  | ||||
|  | ||||
| def translation_file_changed(sender, file_path, **kwargs): | ||||
|     """Clear the internal translations cache if a .mo file is modified.""" | ||||
|     if file_path.suffix == ".mo": | ||||
|         import gettext | ||||
|  | ||||
|         from django.utils.translation import trans_real | ||||
|  | ||||
|         gettext._translations = {} | ||||
|         trans_real._translations = {} | ||||
|         trans_real._default = None | ||||
|         trans_real._active = Local() | ||||
|         return True | ||||
| @ -0,0 +1,246 @@ | ||||
| import warnings | ||||
| from io import StringIO | ||||
|  | ||||
| from django.template.base import Lexer, TokenType | ||||
| from django.utils.regex_helper import _lazy_re_compile | ||||
|  | ||||
| from . import TranslatorCommentWarning, trim_whitespace | ||||
|  | ||||
| TRANSLATOR_COMMENT_MARK = "Translators" | ||||
|  | ||||
| dot_re = _lazy_re_compile(r"\S") | ||||
|  | ||||
|  | ||||
| def blankout(src, char): | ||||
|     """ | ||||
|     Change every non-whitespace character to the given char. | ||||
|     Used in the templatize function. | ||||
|     """ | ||||
|     return dot_re.sub(char, src) | ||||
|  | ||||
|  | ||||
| context_re = _lazy_re_compile(r"""^\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?'))\s*""") | ||||
| inline_re = _lazy_re_compile( | ||||
|     # Match the trans/translate 'some text' part. | ||||
|     r"""^\s*trans(?:late)?\s+((?:"[^"]*?")|(?:'[^']*?'))""" | ||||
|     # Match and ignore optional filters | ||||
|     r"""(?:\s*\|\s*[^\s:]+(?::(?:[^\s'":]+|(?:"[^"]*?")|(?:'[^']*?')))?)*""" | ||||
|     # Match the optional context part | ||||
|     r"""(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?')))?\s*""" | ||||
| ) | ||||
| block_re = _lazy_re_compile( | ||||
|     r"""^\s*blocktrans(?:late)?(\s+.*context\s+((?:"[^"]*?")|(?:'[^']*?')))?(?:\s+|$)""" | ||||
| ) | ||||
| endblock_re = _lazy_re_compile(r"""^\s*endblocktrans(?:late)?$""") | ||||
| plural_re = _lazy_re_compile(r"""^\s*plural$""") | ||||
| constant_re = _lazy_re_compile(r"""_\(((?:".*?")|(?:'.*?'))\)""") | ||||
|  | ||||
|  | ||||
| def templatize(src, origin=None): | ||||
|     """ | ||||
|     Turn a Django template into something that is understood by xgettext. It | ||||
|     does so by translating the Django translation tags into standard gettext | ||||
|     function invocations. | ||||
|     """ | ||||
|     out = StringIO("") | ||||
|     message_context = None | ||||
|     intrans = False | ||||
|     inplural = False | ||||
|     trimmed = False | ||||
|     singular = [] | ||||
|     plural = [] | ||||
|     incomment = False | ||||
|     comment = [] | ||||
|     lineno_comment_map = {} | ||||
|     comment_lineno_cache = None | ||||
|     # Adding the u prefix allows gettext to recognize the string (#26093). | ||||
|     raw_prefix = "u" | ||||
|  | ||||
|     def join_tokens(tokens, trim=False): | ||||
|         message = "".join(tokens) | ||||
|         if trim: | ||||
|             message = trim_whitespace(message) | ||||
|         return message | ||||
|  | ||||
|     for t in Lexer(src).tokenize(): | ||||
|         if incomment: | ||||
|             if t.token_type == TokenType.BLOCK and t.contents == "endcomment": | ||||
|                 content = "".join(comment) | ||||
|                 translators_comment_start = None | ||||
|                 for lineno, line in enumerate(content.splitlines(True)): | ||||
|                     if line.lstrip().startswith(TRANSLATOR_COMMENT_MARK): | ||||
|                         translators_comment_start = lineno | ||||
|                 for lineno, line in enumerate(content.splitlines(True)): | ||||
|                     if ( | ||||
|                         translators_comment_start is not None | ||||
|                         and lineno >= translators_comment_start | ||||
|                     ): | ||||
|                         out.write(" # %s" % line) | ||||
|                     else: | ||||
|                         out.write(" #\n") | ||||
|                 incomment = False | ||||
|                 comment = [] | ||||
|             else: | ||||
|                 comment.append(t.contents) | ||||
|         elif intrans: | ||||
|             if t.token_type == TokenType.BLOCK: | ||||
|                 endbmatch = endblock_re.match(t.contents) | ||||
|                 pluralmatch = plural_re.match(t.contents) | ||||
|                 if endbmatch: | ||||
|                     if inplural: | ||||
|                         if message_context: | ||||
|                             out.write( | ||||
|                                 " npgettext({p}{!r}, {p}{!r}, {p}{!r},count) ".format( | ||||
|                                     message_context, | ||||
|                                     join_tokens(singular, trimmed), | ||||
|                                     join_tokens(plural, trimmed), | ||||
|                                     p=raw_prefix, | ||||
|                                 ) | ||||
|                             ) | ||||
|                         else: | ||||
|                             out.write( | ||||
|                                 " ngettext({p}{!r}, {p}{!r}, count) ".format( | ||||
|                                     join_tokens(singular, trimmed), | ||||
|                                     join_tokens(plural, trimmed), | ||||
|                                     p=raw_prefix, | ||||
|                                 ) | ||||
|                             ) | ||||
|                         for part in singular: | ||||
|                             out.write(blankout(part, "S")) | ||||
|                         for part in plural: | ||||
|                             out.write(blankout(part, "P")) | ||||
|                     else: | ||||
|                         if message_context: | ||||
|                             out.write( | ||||
|                                 " pgettext({p}{!r}, {p}{!r}) ".format( | ||||
|                                     message_context, | ||||
|                                     join_tokens(singular, trimmed), | ||||
|                                     p=raw_prefix, | ||||
|                                 ) | ||||
|                             ) | ||||
|                         else: | ||||
|                             out.write( | ||||
|                                 " gettext({p}{!r}) ".format( | ||||
|                                     join_tokens(singular, trimmed), | ||||
|                                     p=raw_prefix, | ||||
|                                 ) | ||||
|                             ) | ||||
|                         for part in singular: | ||||
|                             out.write(blankout(part, "S")) | ||||
|                     message_context = None | ||||
|                     intrans = False | ||||
|                     inplural = False | ||||
|                     singular = [] | ||||
|                     plural = [] | ||||
|                 elif pluralmatch: | ||||
|                     inplural = True | ||||
|                 else: | ||||
|                     filemsg = "" | ||||
|                     if origin: | ||||
|                         filemsg = "file %s, " % origin | ||||
|                     raise SyntaxError( | ||||
|                         "Translation blocks must not include other block tags: " | ||||
|                         "%s (%sline %d)" % (t.contents, filemsg, t.lineno) | ||||
|                     ) | ||||
|             elif t.token_type == TokenType.VAR: | ||||
|                 if inplural: | ||||
|                     plural.append("%%(%s)s" % t.contents) | ||||
|                 else: | ||||
|                     singular.append("%%(%s)s" % t.contents) | ||||
|             elif t.token_type == TokenType.TEXT: | ||||
|                 contents = t.contents.replace("%", "%%") | ||||
|                 if inplural: | ||||
|                     plural.append(contents) | ||||
|                 else: | ||||
|                     singular.append(contents) | ||||
|         else: | ||||
|             # Handle comment tokens (`{# ... #}`) plus other constructs on | ||||
|             # the same line: | ||||
|             if comment_lineno_cache is not None: | ||||
|                 cur_lineno = t.lineno + t.contents.count("\n") | ||||
|                 if comment_lineno_cache == cur_lineno: | ||||
|                     if t.token_type != TokenType.COMMENT: | ||||
|                         for c in lineno_comment_map[comment_lineno_cache]: | ||||
|                             filemsg = "" | ||||
|                             if origin: | ||||
|                                 filemsg = "file %s, " % origin | ||||
|                             warn_msg = ( | ||||
|                                 "The translator-targeted comment '%s' " | ||||
|                                 "(%sline %d) was ignored, because it wasn't " | ||||
|                                 "the last item on the line." | ||||
|                             ) % (c, filemsg, comment_lineno_cache) | ||||
|                             warnings.warn(warn_msg, TranslatorCommentWarning) | ||||
|                         lineno_comment_map[comment_lineno_cache] = [] | ||||
|                 else: | ||||
|                     out.write( | ||||
|                         "# %s" % " | ".join(lineno_comment_map[comment_lineno_cache]) | ||||
|                     ) | ||||
|                 comment_lineno_cache = None | ||||
|  | ||||
|             if t.token_type == TokenType.BLOCK: | ||||
|                 imatch = inline_re.match(t.contents) | ||||
|                 bmatch = block_re.match(t.contents) | ||||
|                 cmatches = constant_re.findall(t.contents) | ||||
|                 if imatch: | ||||
|                     g = imatch[1] | ||||
|                     if g[0] == '"': | ||||
|                         g = g.strip('"') | ||||
|                     elif g[0] == "'": | ||||
|                         g = g.strip("'") | ||||
|                     g = g.replace("%", "%%") | ||||
|                     if imatch[2]: | ||||
|                         # A context is provided | ||||
|                         context_match = context_re.match(imatch[2]) | ||||
|                         message_context = context_match[1] | ||||
|                         if message_context[0] == '"': | ||||
|                             message_context = message_context.strip('"') | ||||
|                         elif message_context[0] == "'": | ||||
|                             message_context = message_context.strip("'") | ||||
|                         out.write( | ||||
|                             " pgettext({p}{!r}, {p}{!r}) ".format( | ||||
|                                 message_context, g, p=raw_prefix | ||||
|                             ) | ||||
|                         ) | ||||
|                         message_context = None | ||||
|                     else: | ||||
|                         out.write(" gettext({p}{!r}) ".format(g, p=raw_prefix)) | ||||
|                 elif bmatch: | ||||
|                     for fmatch in constant_re.findall(t.contents): | ||||
|                         out.write(" _(%s) " % fmatch) | ||||
|                     if bmatch[1]: | ||||
|                         # A context is provided | ||||
|                         context_match = context_re.match(bmatch[1]) | ||||
|                         message_context = context_match[1] | ||||
|                         if message_context[0] == '"': | ||||
|                             message_context = message_context.strip('"') | ||||
|                         elif message_context[0] == "'": | ||||
|                             message_context = message_context.strip("'") | ||||
|                     intrans = True | ||||
|                     inplural = False | ||||
|                     trimmed = "trimmed" in t.split_contents() | ||||
|                     singular = [] | ||||
|                     plural = [] | ||||
|                 elif cmatches: | ||||
|                     for cmatch in cmatches: | ||||
|                         out.write(" _(%s) " % cmatch) | ||||
|                 elif t.contents == "comment": | ||||
|                     incomment = True | ||||
|                 else: | ||||
|                     out.write(blankout(t.contents, "B")) | ||||
|             elif t.token_type == TokenType.VAR: | ||||
|                 parts = t.contents.split("|") | ||||
|                 cmatch = constant_re.match(parts[0]) | ||||
|                 if cmatch: | ||||
|                     out.write(" _(%s) " % cmatch[1]) | ||||
|                 for p in parts[1:]: | ||||
|                     if p.find(":_(") >= 0: | ||||
|                         out.write(" %s " % p.split(":", 1)[1]) | ||||
|                     else: | ||||
|                         out.write(blankout(p, "F")) | ||||
|             elif t.token_type == TokenType.COMMENT: | ||||
|                 if t.contents.lstrip().startswith(TRANSLATOR_COMMENT_MARK): | ||||
|                     lineno_comment_map.setdefault(t.lineno, []).append(t.contents) | ||||
|                     comment_lineno_cache = t.lineno | ||||
|             else: | ||||
|                 out.write(blankout(t.contents, "X")) | ||||
|     return out.getvalue() | ||||
| @ -0,0 +1,67 @@ | ||||
| # These are versions of the functions in django.utils.translation.trans_real | ||||
| # that don't actually do anything. This is purely for performance, so that | ||||
| # settings.USE_I18N = False can use this module rather than trans_real.py. | ||||
|  | ||||
| from django.conf import settings | ||||
|  | ||||
|  | ||||
| def gettext(message): | ||||
|     return message | ||||
|  | ||||
|  | ||||
| gettext_noop = gettext_lazy = _ = gettext | ||||
|  | ||||
|  | ||||
| def ngettext(singular, plural, number): | ||||
|     if number == 1: | ||||
|         return singular | ||||
|     return plural | ||||
|  | ||||
|  | ||||
| ngettext_lazy = ngettext | ||||
|  | ||||
|  | ||||
| def pgettext(context, message): | ||||
|     return gettext(message) | ||||
|  | ||||
|  | ||||
| def npgettext(context, singular, plural, number): | ||||
|     return ngettext(singular, plural, number) | ||||
|  | ||||
|  | ||||
| def activate(x): | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def deactivate(): | ||||
|     return None | ||||
|  | ||||
|  | ||||
| deactivate_all = deactivate | ||||
|  | ||||
|  | ||||
| def get_language(): | ||||
|     return settings.LANGUAGE_CODE | ||||
|  | ||||
|  | ||||
| def get_language_bidi(): | ||||
|     return settings.LANGUAGE_CODE in settings.LANGUAGES_BIDI | ||||
|  | ||||
|  | ||||
| def check_for_language(x): | ||||
|     return True | ||||
|  | ||||
|  | ||||
| def get_language_from_request(request, check_path=False): | ||||
|     return settings.LANGUAGE_CODE | ||||
|  | ||||
|  | ||||
| def get_language_from_path(request): | ||||
|     return None | ||||
|  | ||||
|  | ||||
| def get_supported_language_variant(lang_code, strict=False): | ||||
|     if lang_code and lang_code.lower() == settings.LANGUAGE_CODE.lower(): | ||||
|         return lang_code | ||||
|     else: | ||||
|         raise LookupError(lang_code) | ||||
| @ -0,0 +1,639 @@ | ||||
| """Translation helper functions.""" | ||||
| 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 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" | ||||
|  | ||||
| # 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. | ||||
| ACCEPT_LANGUAGE_HEADER_MAX_LENGTH = 500 | ||||
|  | ||||
| # Format of Accept-Language header values. From RFC 9110 Sections 12.4.2 and | ||||
| # 12.5.4, and RFC 5646 Section 2.1. | ||||
| accept_language_re = _lazy_re_compile( | ||||
|     r""" | ||||
|         # "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*|$) | ||||
|     """, | ||||
|     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): | ||||
|     """ | ||||
|     Reset global state when LANGUAGES setting has been changed, as some | ||||
|     languages should no longer be accepted. | ||||
|     """ | ||||
|     if setting in ("LANGUAGES", "LANGUAGE_CODE"): | ||||
|         check_for_language.cache_clear() | ||||
|         get_languages.cache_clear() | ||||
|         get_supported_language_variant.cache_clear() | ||||
|  | ||||
|  | ||||
| class TranslationCatalog: | ||||
|     """ | ||||
|     Simulate a dict for DjangoTranslation._catalog so as multiple catalogs | ||||
|     with different plural equations are kept separate. | ||||
|     """ | ||||
|  | ||||
|     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): | ||||
|         # Merge if plural function is the same, else prepend. | ||||
|         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): | ||||
|     """ | ||||
|     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 OSError( | ||||
|                 "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 = TranslationCatalog() | ||||
|  | ||||
|     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(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 = TranslationCatalog(other) | ||||
|         else: | ||||
|             self._catalog.update(other) | ||||
|         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 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): | ||||
|     """ | ||||
|     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 a dictionary for easy lookups by key. | ||||
|     Convert keys to lowercase as they should be treated as case-insensitive. | ||||
|     """ | ||||
|     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): | ||||
|     """ | ||||
|     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 'zh-hant-tw' is not supported, try special fallback or subsequent | ||||
|         # language codes i.e. 'zh-hant' and 'zh'. | ||||
|         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: | ||||
|             # 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[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 | ||||
|  | ||||
|     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): | ||||
|     """ | ||||
|     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) | ||||
|  | ||||
|  | ||||
| def parse_accept_lang_header(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 len(lang_string) <= ACCEPT_LANGUAGE_HEADER_MAX_LENGTH: | ||||
|         return _parse_accept_lang_header(lang_string) | ||||
|  | ||||
|     # 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 := lang_string.rfind(",", 0, ACCEPT_LANGUAGE_HEADER_MAX_LENGTH)) > 0: | ||||
|         return _parse_accept_lang_header(lang_string[:index]) | ||||
|  | ||||
|     # Don't attempt to parse if there is only one language-range value which is | ||||
|     # longer than the maximum allowed length and so truncated. | ||||
|     return () | ||||
		Reference in New Issue
	
	Block a user