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