ASGI (django/core/asgi.py, core/handlers/asgi.py)

ASGI support

Django 3.0 begins our journey to making Django fully async-capable by providing support for running as an ASGI application.

This is in addition to our existing WSGI support.

Django intends to support both for the foreseeable future.

Async features will only be available to applications that run under ASGI, however.

There is no need to switch your applications over unless you want to start experimenting with asynchronous code, but we have documentation on deploying with ASGI if you want to learn more.

Note that as a side-effect of this change, Django is now aware of asynchronous event loops and will block you calling code marked as “async unsafe” - such as ORM operations - from an asynchronous context.

If you were using Django from async code before, this may trigger if you were doing it incorrectly.

If you see a SynchronousOnlyOperation error, then closely examine your code and move any database operations to be in a synchronous child thread.

Articles

django/core/asgi.py

 1import django
 2from django.core.handlers.asgi import ASGIHandler
 3
 4
 5def get_asgi_application():
 6    """
 7    The public interface to Django's ASGI support. Return an ASGI 3 callable.
 8
 9    Avoids making django.core.handlers.ASGIHandler a public API, in case the
10    internal implementation changes or moves in the future.
11    """
12    django.setup(set_prefix=False)
13    return ASGIHandler()

django/core/handlers/asgi.py

  1import asyncio
  2import logging
  3import sys
  4import tempfile
  5import traceback
  6
  7from asgiref.sync import sync_to_async
  8
  9from django.conf import settings
 10from django.core import signals
 11from django.core.exceptions import RequestAborted, RequestDataTooBig
 12from django.core.handlers import base
 13from django.http import (
 14    FileResponse,
 15    HttpRequest,
 16    HttpResponse,
 17    HttpResponseBadRequest,
 18    HttpResponseServerError,
 19    QueryDict,
 20    parse_cookie,
 21)
 22from django.urls import set_script_prefix
 23from django.utils.functional import cached_property
 24
 25logger = logging.getLogger("django.request")
 26
 27
 28class ASGIRequest(HttpRequest):
 29    """
 30    Custom request subclass that decodes from an ASGI-standard request dict
 31    and wraps request body handling.
 32    """
 33
 34    # Number of seconds until a Request gives up on trying to read a request
 35    # body and aborts.
 36    body_receive_timeout = 60
 37
 38    def __init__(self, scope, body_file):
 39        self.scope = scope
 40        self._post_parse_error = False
 41        self._read_started = False
 42        self.resolver_match = None
 43        self.script_name = self.scope.get("root_path", "")
 44        if self.script_name and scope["path"].startswith(self.script_name):
 45            # TODO: Better is-prefix checking, slash handling?
 46            self.path_info = scope["path"][len(self.script_name) :]
 47        else:
 48            self.path_info = scope["path"]
 49        # The Django path is different from ASGI scope path args, it should
 50        # combine with script name.
 51        if self.script_name:
 52            self.path = "%s/%s" % (
 53                self.script_name.rstrip("/"),
 54                self.path_info.replace("/", "", 1),
 55            )
 56        else:
 57            self.path = scope["path"]
 58        # HTTP basics.
 59        self.method = self.scope["method"].upper()
 60        # Ensure query string is encoded correctly.
 61        query_string = self.scope.get("query_string", "")
 62        if isinstance(query_string, bytes):
 63            query_string = query_string.decode()
 64        self.META = {
 65            "REQUEST_METHOD": self.method,
 66            "QUERY_STRING": query_string,
 67            "SCRIPT_NAME": self.script_name,
 68            "PATH_INFO": self.path_info,
 69            # WSGI-expecting code will need these for a while
 70            "wsgi.multithread": True,
 71            "wsgi.multiprocess": True,
 72        }
 73        if self.scope.get("client"):
 74            self.META["REMOTE_ADDR"] = self.scope["client"][0]
 75            self.META["REMOTE_HOST"] = self.META["REMOTE_ADDR"]
 76            self.META["REMOTE_PORT"] = self.scope["client"][1]
 77        if self.scope.get("server"):
 78            self.META["SERVER_NAME"] = self.scope["server"][0]
 79            self.META["SERVER_PORT"] = str(self.scope["server"][1])
 80        else:
 81            self.META["SERVER_NAME"] = "unknown"
 82            self.META["SERVER_PORT"] = "0"
 83        # Headers go into META.
 84        for name, value in self.scope.get("headers", []):
 85            name = name.decode("latin1")
 86            if name == "content-length":
 87                corrected_name = "CONTENT_LENGTH"
 88            elif name == "content-type":
 89                corrected_name = "CONTENT_TYPE"
 90            else:
 91                corrected_name = "HTTP_%s" % name.upper().replace("-", "_")
 92            # HTTP/2 say only ASCII chars are allowed in headers, but decode
 93            # latin1 just in case.
 94            value = value.decode("latin1")
 95            if corrected_name in self.META:
 96                value = self.META[corrected_name] + "," + value
 97            self.META[corrected_name] = value
 98        # Pull out request encoding, if provided.
 99        self._set_content_type_params(self.META)
100        # Directly assign the body file to be our stream.
101        self._stream = body_file
102        # Other bits.
103        self.resolver_match = None
104
105    @cached_property
106    def GET(self):
107        return QueryDict(self.META["QUERY_STRING"])
108
109    def _get_scheme(self):
110        return self.scope.get("scheme") or super()._get_scheme()
111
112    def _get_post(self):
113        if not hasattr(self, "_post"):
114            self._load_post_and_files()
115        return self._post
116
117    def _set_post(self, post):
118        self._post = post
119
120    def _get_files(self):
121        if not hasattr(self, "_files"):
122            self._load_post_and_files()
123        return self._files
124
125    POST = property(_get_post, _set_post)
126    FILES = property(_get_files)
127
128    @cached_property
129    def COOKIES(self):
130        return parse_cookie(self.META.get("HTTP_COOKIE", ""))
131
132
133class ASGIHandler(base.BaseHandler):
134    """Handler for ASGI requests."""
135
136    request_class = ASGIRequest
137    # Size to chunk response bodies into for multiple response messages.
138    chunk_size = 2 ** 16
139
140    def __init__(self):
141        super().__init__()
142        self.load_middleware()
143
144    async def __call__(self, scope, receive, send):
145        """
146        Async entrypoint - parses the request and hands off to get_response.
147        """
148        # Serve only HTTP connections.
149        # FIXME: Allow to override this.
150        if scope["type"] != "http":
151            raise ValueError(
152                "Django can only handle ASGI/HTTP connections, not %s." % scope["type"]
153            )
154        # Receive the HTTP request body as a stream object.
155        try:
156            body_file = await self.read_body(receive)
157        except RequestAborted:
158            return
159        # Request is complete and can be served.
160        set_script_prefix(self.get_script_prefix(scope))
161        await sync_to_async(signals.request_started.send)(
162            sender=self.__class__, scope=scope
163        )
164        # Get the request and check for basic issues.
165        request, error_response = self.create_request(scope, body_file)
166        if request is None:
167            await self.send_response(error_response, send)
168            return
169        # Get the response, using a threadpool via sync_to_async, if needed.
170        if asyncio.iscoroutinefunction(self.get_response):
171            response = await self.get_response(request)
172        else:
173            # If get_response is synchronous, run it non-blocking.
174            response = await sync_to_async(self.get_response)(request)
175        response._handler_class = self.__class__
176        # Increase chunk size on file responses (ASGI servers handles low-level
177        # chunking).
178        if isinstance(response, FileResponse):
179            response.block_size = self.chunk_size
180        # Send the response.
181        await self.send_response(response, send)
182
183    async def read_body(self, receive):
184        """Reads a HTTP body from an ASGI connection."""
185        # Use the tempfile that auto rolls-over to a disk file as it fills up.
186        body_file = tempfile.SpooledTemporaryFile(
187            max_size=settings.FILE_UPLOAD_MAX_MEMORY_SIZE, mode="w+b"
188        )
189        while True:
190            message = await receive()
191            if message["type"] == "http.disconnect":
192                # Early client disconnect.
193                raise RequestAborted()
194            # Add a body chunk from the message, if provided.
195            if "body" in message:
196                body_file.write(message["body"])
197            # Quit out if that's the end.
198            if not message.get("more_body", False):
199                break
200        body_file.seek(0)
201        return body_file
202
203    def create_request(self, scope, body_file):
204        """
205        Create the Request object and returns either (request, None) or
206        (None, response) if there is an error response.
207        """
208        try:
209            return self.request_class(scope, body_file), None
210        except UnicodeDecodeError:
211            logger.warning(
212                "Bad Request (UnicodeDecodeError)",
213                exc_info=sys.exc_info(),
214                extra={"status_code": 400},
215            )
216            return None, HttpResponseBadRequest()
217        except RequestDataTooBig:
218            return None, HttpResponse("413 Payload too large", status=413)
219
220    def handle_uncaught_exception(self, request, resolver, exc_info):
221        """Last-chance handler for exceptions."""
222        # There's no WSGI server to catch the exception further up
223        # if this fails, so translate it into a plain text response.
224        try:
225            return super().handle_uncaught_exception(request, resolver, exc_info)
226        except Exception:
227            return HttpResponseServerError(
228                traceback.format_exc() if settings.DEBUG else "Internal Server Error",
229                content_type="text/plain",
230            )
231
232    async def send_response(self, response, send):
233        """Encode and send a response out over ASGI."""
234        # Collect cookies into headers. Have to preserve header case as there
235        # are some non-RFC compliant clients that require e.g. Content-Type.
236        response_headers = []
237        for header, value in response.items():
238            if isinstance(header, str):
239                header = header.encode("ascii")
240            if isinstance(value, str):
241                value = value.encode("latin1")
242            response_headers.append((bytes(header), bytes(value)))
243        for c in response.cookies.values():
244            response_headers.append(
245                (b"Set-Cookie", c.output(header="").encode("ascii").strip())
246            )
247        # Initial response message.
248        await send(
249            {
250                "type": "http.response.start",
251                "status": response.status_code,
252                "headers": response_headers,
253            }
254        )
255        # Streaming responses need to be pinned to their iterator.
256        if response.streaming:
257            # Access `__iter__` and not `streaming_content` directly in case
258            # it has been overridden in a subclass.
259            for part in response:
260                for chunk, _ in self.chunk_bytes(part):
261                    await send(
262                        {
263                            "type": "http.response.body",
264                            "body": chunk,
265                            # Ignore "more" as there may be more parts; instead,
266                            # use an empty final closing message with False.
267                            "more_body": True,
268                        }
269                    )
270            # Final closing message.
271            await send({"type": "http.response.body"})
272        # Other responses just need chunking.
273        else:
274            # Yield chunks of response.
275            for chunk, last in self.chunk_bytes(response.content):
276                await send(
277                    {
278                        "type": "http.response.body",
279                        "body": chunk,
280                        "more_body": not last,
281                    }
282                )
283        response.close()
284
285    @classmethod
286    def chunk_bytes(cls, data):
287        """
288        Chunks some data up so it can be sent in reasonable size messages.
289        Yields (chunk, last_chunk) tuples.
290        """
291        position = 0
292        if not data:
293            yield data, True
294            return
295        while position < len(data):
296            yield (
297                data[position : position + cls.chunk_size],
298                (position + cls.chunk_size) >= len(data),
299            )
300            position += cls.chunk_size
301
302    def get_script_prefix(self, scope):
303        """
304        Return the script prefix to use from either the scope or a setting.
305        """
306        if settings.FORCE_SCRIPT_NAME:
307            return settings.FORCE_SCRIPT_NAME
308        return scope.get("root_path", "") or ""

django/test/signals.py

  1import os
  2import time
  3import warnings
  4
  5from asgiref.local import Local
  6
  7from django.apps import apps
  8from django.core.exceptions import ImproperlyConfigured
  9from django.core.signals import setting_changed
 10from django.db import connections, router
 11from django.db.utils import ConnectionRouter
 12from django.dispatch import Signal, receiver
 13from django.utils import timezone
 14from django.utils.formats import FORMAT_SETTINGS, reset_format_cache
 15from django.utils.functional import empty
 16
 17template_rendered = Signal(providing_args=["template", "context"])
 18
 19# Most setting_changed receivers are supposed to be added below,
 20# except for cases where the receiver is related to a contrib app.
 21
 22# Settings that may not work well when using 'override_settings' (#19031)
 23COMPLEX_OVERRIDE_SETTINGS = {"DATABASES"}
 24
 25
 26@receiver(setting_changed)
 27def clear_cache_handlers(**kwargs):
 28    if kwargs["setting"] == "CACHES":
 29        from django.core.cache import caches
 30
 31        caches._caches = Local()
 32
 33
 34@receiver(setting_changed)
 35def update_installed_apps(**kwargs):
 36    if kwargs["setting"] == "INSTALLED_APPS":
 37        # Rebuild any AppDirectoriesFinder instance.
 38        from django.contrib.staticfiles.finders import get_finder
 39
 40        get_finder.cache_clear()
 41        # Rebuild management commands cache
 42        from django.core.management import get_commands
 43
 44        get_commands.cache_clear()
 45        # Rebuild get_app_template_dirs cache.
 46        from django.template.utils import get_app_template_dirs
 47
 48        get_app_template_dirs.cache_clear()
 49        # Rebuild translations cache.
 50        from django.utils.translation import trans_real
 51
 52        trans_real._translations = {}
 53
 54
 55@receiver(setting_changed)
 56def update_connections_time_zone(**kwargs):
 57    if kwargs["setting"] == "TIME_ZONE":
 58        # Reset process time zone
 59        if hasattr(time, "tzset"):
 60            if kwargs["value"]:
 61                os.environ["TZ"] = kwargs["value"]
 62            else:
 63                os.environ.pop("TZ", None)
 64            time.tzset()
 65
 66        # Reset local time zone cache
 67        timezone.get_default_timezone.cache_clear()
 68
 69    # Reset the database connections' time zone
 70    if kwargs["setting"] in {"TIME_ZONE", "USE_TZ"}:
 71        for conn in connections.all():
 72            try:
 73                del conn.timezone
 74            except AttributeError:
 75                pass
 76            try:
 77                del conn.timezone_name
 78            except AttributeError:
 79                pass
 80            conn.ensure_timezone()
 81
 82
 83@receiver(setting_changed)
 84def clear_routers_cache(**kwargs):
 85    if kwargs["setting"] == "DATABASE_ROUTERS":
 86        router.routers = ConnectionRouter().routers
 87
 88
 89@receiver(setting_changed)
 90def reset_template_engines(**kwargs):
 91    if kwargs["setting"] in {
 92        "TEMPLATES",
 93        "DEBUG",
 94        "INSTALLED_APPS",
 95    }:
 96        from django.template import engines
 97
 98        try:
 99            del engines.templates
100        except AttributeError:
101            pass
102        engines._templates = None
103        engines._engines = {}
104        from django.template.engine import Engine
105
106        Engine.get_default.cache_clear()
107        from django.forms.renderers import get_default_renderer
108
109        get_default_renderer.cache_clear()
110
111
112@receiver(setting_changed)
113def clear_serializers_cache(**kwargs):
114    if kwargs["setting"] == "SERIALIZATION_MODULES":
115        from django.core import serializers
116
117        serializers._serializers = {}
118
119
120@receiver(setting_changed)
121def language_changed(**kwargs):
122    if kwargs["setting"] in {"LANGUAGES", "LANGUAGE_CODE", "LOCALE_PATHS"}:
123        from django.utils.translation import trans_real
124
125        trans_real._default = None
126        trans_real._active = Local()
127    if kwargs["setting"] in {"LANGUAGES", "LOCALE_PATHS"}:
128        from django.utils.translation import trans_real
129
130        trans_real._translations = {}
131        trans_real.check_for_language.cache_clear()
132
133
134@receiver(setting_changed)
135def localize_settings_changed(**kwargs):
136    if (
137        kwargs["setting"] in FORMAT_SETTINGS
138        or kwargs["setting"] == "USE_THOUSAND_SEPARATOR"
139    ):
140        reset_format_cache()
141
142
143@receiver(setting_changed)
144def file_storage_changed(**kwargs):
145    if kwargs["setting"] == "DEFAULT_FILE_STORAGE":
146        from django.core.files.storage import default_storage
147
148        default_storage._wrapped = empty
149
150
151@receiver(setting_changed)
152def complex_setting_changed(**kwargs):
153    if kwargs["enter"] and kwargs["setting"] in COMPLEX_OVERRIDE_SETTINGS:
154        # Considering the current implementation of the signals framework,
155        # this stacklevel shows the line containing the override_settings call.
156        warnings.warn(
157            "Overriding setting %s can lead to unexpected behavior."
158            % kwargs["setting"],
159            stacklevel=6,
160        )
161
162
163@receiver(setting_changed)
164def root_urlconf_changed(**kwargs):
165    if kwargs["setting"] == "ROOT_URLCONF":
166        from django.urls import clear_url_caches, set_urlconf
167
168        clear_url_caches()
169        set_urlconf(None)
170
171
172@receiver(setting_changed)
173def static_storage_changed(**kwargs):
174    if kwargs["setting"] in {
175        "STATICFILES_STORAGE",
176        "STATIC_ROOT",
177        "STATIC_URL",
178    }:
179        from django.contrib.staticfiles.storage import staticfiles_storage
180
181        staticfiles_storage._wrapped = empty
182
183
184@receiver(setting_changed)
185def static_finders_changed(**kwargs):
186    if kwargs["setting"] in {
187        "STATICFILES_DIRS",
188        "STATIC_ROOT",
189    }:
190        from django.contrib.staticfiles.finders import get_finder
191
192        get_finder.cache_clear()
193
194
195@receiver(setting_changed)
196def auth_password_validators_changed(**kwargs):
197    if kwargs["setting"] == "AUTH_PASSWORD_VALIDATORS":
198        from django.contrib.auth.password_validation import (
199            get_default_password_validators,
200        )
201
202        get_default_password_validators.cache_clear()
203
204
205@receiver(setting_changed)
206def user_model_swapped(**kwargs):
207    if kwargs["setting"] == "AUTH_USER_MODEL":
208        apps.clear_cache()
209        try:
210            from django.contrib.auth import get_user_model
211
212            UserModel = get_user_model()
213        except ImproperlyConfigured:
214            # Some tests set an invalid AUTH_USER_MODEL.
215            pass
216        else:
217            from django.contrib.auth import backends
218
219            backends.UserModel = UserModel
220
221            from django.contrib.auth import forms
222
223            forms.UserModel = UserModel
224
225            from django.contrib.auth.handlers import modwsgi
226
227            modwsgi.UserModel = UserModel
228
229            from django.contrib.auth.management.commands import changepassword
230
231            changepassword.UserModel = UserModel
232
233            from django.contrib.auth import views
234
235            views.UserModel = UserModel

django/utils/translation/reloader.py

 1from pathlib import Path
 2
 3from asgiref.local import Local
 4
 5from django.apps import apps
 6
 7
 8def watch_for_translation_changes(sender, **kwargs):
 9    """Register file watchers for .mo files in potential locale paths."""
10    from django.conf import settings
11
12    if settings.USE_I18N:
13        directories = [Path("locale")]
14        directories.extend(
15            Path(config.path) / "locale" for config in apps.get_app_configs()
16        )
17        directories.extend(Path(p) for p in settings.LOCALE_PATHS)
18        for path in directories:
19            sender.watch_dir(path, "**/*.mo")
20
21
22def translation_file_changed(sender, file_path, **kwargs):
23    """Clear the internal translations cache if a .mo file is modified."""
24    if file_path.suffix == ".mo":
25        import gettext
26        from django.utils.translation import trans_real
27
28        gettext._translations = {}
29        trans_real._translations = {}
30        trans_real._default = None
31        trans_real._active = Local()
32        return True

django/utils/translation/trans_real.py

  1"""Translation helper functions."""
  2import functools
  3import gettext as gettext_module
  4import os
  5import re
  6import sys
  7import warnings
  8
  9from asgiref.local import Local
 10
 11from django.apps import apps
 12from django.conf import settings
 13from django.conf.locale import LANG_INFO
 14from django.core.exceptions import AppRegistryNotReady
 15from django.core.signals import setting_changed
 16from django.dispatch import receiver
 17from django.utils.regex_helper import _lazy_re_compile
 18from django.utils.safestring import SafeData, mark_safe
 19
 20from . import to_language, to_locale
 21
 22# Translations are cached in a dictionary for every language.
 23# The active translations are stored by threadid to make them thread local.
 24_translations = {}
 25_active = Local()
 26
 27# The default translation is based on the settings file.
 28_default = None
 29
 30# magic gettext number to separate context from message
 31CONTEXT_SEPARATOR = "\x04"
 32
 33# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9
 34# and RFC 3066, section 2.1
 35accept_language_re = _lazy_re_compile(
 36    r"""
 37        ([A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*|\*)      # "en", "en-au", "x-y-z", "es-419", "*"
 38        (?:\s*;\s*q=(0(?:\.\d{,3})?|1(?:\.0{,3})?))?  # Optional "q=1.00", "q=0.8"
 39        (?:\s*,\s*|$)                                 # Multiple accepts per header.
 40        """,
 41    re.VERBOSE,
 42)
 43
 44language_code_re = _lazy_re_compile(
 45    r"^[a-z]{1,8}(?:-[a-z0-9]{1,8})*(?:@[a-z0-9]{1,20})?$", re.IGNORECASE
 46)
 47
 48language_code_prefix_re = _lazy_re_compile(r"^/(\w+([@-]\w+)?)(/|$)")
 49
 50
 51@receiver(setting_changed)
 52def reset_cache(**kwargs):
 53    """
 54    Reset global state when LANGUAGES setting has been changed, as some
 55    languages should no longer be accepted.
 56    """
 57    if kwargs["setting"] in ("LANGUAGES", "LANGUAGE_CODE"):
 58        check_for_language.cache_clear()
 59        get_languages.cache_clear()
 60        get_supported_language_variant.cache_clear()
 61
 62
 63class DjangoTranslation(gettext_module.GNUTranslations):
 64    """
 65    Set up the GNUTranslations context with regard to output charset.
 66
 67    This translation object will be constructed out of multiple GNUTranslations
 68    objects by merging their catalogs. It will construct an object for the
 69    requested language and add a fallback to the default language, if it's
 70    different from the requested language.
 71    """
 72
 73    domain = "django"
 74
 75    def __init__(self, language, domain=None, localedirs=None):
 76        """Create a GNUTranslations() using many locale directories"""
 77        gettext_module.GNUTranslations.__init__(self)
 78        if domain is not None:
 79            self.domain = domain
 80
 81        self.__language = language
 82        self.__to_language = to_language(language)
 83        self.__locale = to_locale(language)
 84        self._catalog = None
 85        # If a language doesn't have a catalog, use the Germanic default for
 86        # pluralization: anything except one is pluralized.
 87        self.plural = lambda n: int(n != 1)
 88
 89        if self.domain == "django":
 90            if localedirs is not None:
 91                # A module-level cache is used for caching 'django' translations
 92                warnings.warn(
 93                    "localedirs is ignored when domain is 'django'.", RuntimeWarning
 94                )
 95                localedirs = None
 96            self._init_translation_catalog()
 97
 98        if localedirs:
 99            for localedir in localedirs:
100                translation = self._new_gnu_trans(localedir)
101                self.merge(translation)
102        else:
103            self._add_installed_apps_translations()
104
105        self._add_local_translations()
106        if (
107            self.__language == settings.LANGUAGE_CODE
108            and self.domain == "django"
109            and self._catalog is None
110        ):
111            # default lang should have at least one translation file available.
112            raise OSError(
113                "No translation files found for default language %s."
114                % settings.LANGUAGE_CODE
115            )
116        self._add_fallback(localedirs)
117        if self._catalog is None:
118            # No catalogs found for this language, set an empty catalog.
119            self._catalog = {}
120
121    def __repr__(self):
122        return "<DjangoTranslation lang:%s>" % self.__language
123
124    def _new_gnu_trans(self, localedir, use_null_fallback=True):
125        """
126        Return a mergeable gettext.GNUTranslations instance.
127
128        A convenience wrapper. By default gettext uses 'fallback=False'.
129        Using param `use_null_fallback` to avoid confusion with any other
130        references to 'fallback'.
131        """
132        return gettext_module.translation(
133            domain=self.domain,
134            localedir=localedir,
135            languages=[self.__locale],
136            fallback=use_null_fallback,
137        )
138
139    def _init_translation_catalog(self):
140        """Create a base catalog using global django translations."""
141        settingsfile = sys.modules[settings.__module__].__file__
142        localedir = os.path.join(os.path.dirname(settingsfile), "locale")
143        translation = self._new_gnu_trans(localedir)
144        self.merge(translation)
145
146    def _add_installed_apps_translations(self):
147        """Merge translations from each installed app."""
148        try:
149            app_configs = reversed(list(apps.get_app_configs()))
150        except AppRegistryNotReady:
151            raise AppRegistryNotReady(
152                "The translation infrastructure cannot be initialized before the "
153                "apps registry is ready. Check that you don't make non-lazy "
154                "gettext calls at import time."
155            )
156        for app_config in app_configs:
157            localedir = os.path.join(app_config.path, "locale")
158            if os.path.exists(localedir):
159                translation = self._new_gnu_trans(localedir)
160                self.merge(translation)
161
162    def _add_local_translations(self):
163        """Merge translations defined in LOCALE_PATHS."""
164        for localedir in reversed(settings.LOCALE_PATHS):
165            translation = self._new_gnu_trans(localedir)
166            self.merge(translation)
167
168    def _add_fallback(self, localedirs=None):
169        """Set the GNUTranslations() fallback with the default language."""
170        # Don't set a fallback for the default language or any English variant
171        # (as it's empty, so it'll ALWAYS fall back to the default language)
172        if self.__language == settings.LANGUAGE_CODE or self.__language.startswith(
173            "en"
174        ):
175            return
176        if self.domain == "django":
177            # Get from cache
178            default_translation = translation(settings.LANGUAGE_CODE)
179        else:
180            default_translation = DjangoTranslation(
181                settings.LANGUAGE_CODE, domain=self.domain, localedirs=localedirs
182            )
183        self.add_fallback(default_translation)
184
185    def merge(self, other):
186        """Merge another translation into this catalog."""
187        if not getattr(other, "_catalog", None):
188            return  # NullTranslations() has no _catalog
189        if self._catalog is None:
190            # Take plural and _info from first catalog found (generally Django's).
191            self.plural = other.plural
192            self._info = other._info.copy()
193            self._catalog = other._catalog.copy()
194        else:
195            self._catalog.update(other._catalog)
196        if other._fallback:
197            self.add_fallback(other._fallback)
198
199    def language(self):
200        """Return the translation language."""
201        return self.__language
202
203    def to_language(self):
204        """Return the translation language name."""
205        return self.__to_language
206
207
208def translation(language):
209    """
210    Return a translation object in the default 'django' domain.
211    """
212    global _translations
213    if language not in _translations:
214        _translations[language] = DjangoTranslation(language)
215    return _translations[language]
216
217
218def activate(language):
219    """
220    Fetch the translation object for a given language and install it as the
221    current translation object for the current thread.
222    """
223    if not language:
224        return
225    _active.value = translation(language)
226
227
228def deactivate():
229    """
230    Uninstall the active translation object so that further _() calls resolve
231    to the default translation object.
232    """
233    if hasattr(_active, "value"):
234        del _active.value
235
236
237def deactivate_all():
238    """
239    Make the active translation object a NullTranslations() instance. This is
240    useful when we want delayed translations to appear as the original string
241    for some reason.
242    """
243    _active.value = gettext_module.NullTranslations()
244    _active.value.to_language = lambda *args: None
245
246
247def get_language():
248    """Return the currently selected language."""
249    t = getattr(_active, "value", None)
250    if t is not None:
251        try:
252            return t.to_language()
253        except AttributeError:
254            pass
255    # If we don't have a real translation object, assume it's the default language.
256    return settings.LANGUAGE_CODE
257
258
259def get_language_bidi():
260    """
261    Return selected language's BiDi layout.
262
263    * False = left-to-right layout
264    * True = right-to-left layout
265    """
266    lang = get_language()
267    if lang is None:
268        return False
269    else:
270        base_lang = get_language().split("-")[0]
271        return base_lang in settings.LANGUAGES_BIDI
272
273
274def catalog():
275    """
276    Return the current active catalog for further processing.
277    This can be used if you need to modify the catalog or want to access the
278    whole message catalog instead of just translating one string.
279    """
280    global _default
281
282    t = getattr(_active, "value", None)
283    if t is not None:
284        return t
285    if _default is None:
286        _default = translation(settings.LANGUAGE_CODE)
287    return _default
288
289
290def gettext(message):
291    """
292    Translate the 'message' string. It uses the current thread to find the
293    translation object to use. If no current translation is activated, the
294    message will be run through the default translation object.
295    """
296    global _default
297
298    eol_message = message.replace("\r\n", "\n").replace("\r", "\n")
299
300    if eol_message:
301        _default = _default or translation(settings.LANGUAGE_CODE)
302        translation_object = getattr(_active, "value", _default)
303
304        result = translation_object.gettext(eol_message)
305    else:
306        # Return an empty value of the corresponding type if an empty message
307        # is given, instead of metadata, which is the default gettext behavior.
308        result = type(message)("")
309
310    if isinstance(message, SafeData):
311        return mark_safe(result)
312
313    return result
314
315
316def pgettext(context, message):
317    msg_with_ctxt = "%s%s%s" % (context, CONTEXT_SEPARATOR, message)
318    result = gettext(msg_with_ctxt)
319    if CONTEXT_SEPARATOR in result:
320        # Translation not found
321        result = message
322    elif isinstance(message, SafeData):
323        result = mark_safe(result)
324    return result
325
326
327def gettext_noop(message):
328    """
329    Mark strings for translation but don't translate them now. This can be
330    used to store strings in global variables that should stay in the base
331    language (because they might be used externally) and will be translated
332    later.
333    """
334    return message
335
336
337def do_ntranslate(singular, plural, number, translation_function):
338    global _default
339
340    t = getattr(_active, "value", None)
341    if t is not None:
342        return getattr(t, translation_function)(singular, plural, number)
343    if _default is None:
344        _default = translation(settings.LANGUAGE_CODE)
345    return getattr(_default, translation_function)(singular, plural, number)
346
347
348def ngettext(singular, plural, number):
349    """
350    Return a string of the translation of either the singular or plural,
351    based on the number.
352    """
353    return do_ntranslate(singular, plural, number, "ngettext")
354
355
356def npgettext(context, singular, plural, number):
357    msgs_with_ctxt = (
358        "%s%s%s" % (context, CONTEXT_SEPARATOR, singular),
359        "%s%s%s" % (context, CONTEXT_SEPARATOR, plural),
360        number,
361    )
362    result = ngettext(*msgs_with_ctxt)
363    if CONTEXT_SEPARATOR in result:
364        # Translation not found
365        result = ngettext(singular, plural, number)
366    return result
367
368
369def all_locale_paths():
370    """
371    Return a list of paths to user-provides languages files.
372    """
373    globalpath = os.path.join(
374        os.path.dirname(sys.modules[settings.__module__].__file__), "locale"
375    )
376    app_paths = []
377    for app_config in apps.get_app_configs():
378        locale_path = os.path.join(app_config.path, "locale")
379        if os.path.exists(locale_path):
380            app_paths.append(locale_path)
381    return [globalpath, *settings.LOCALE_PATHS, *app_paths]
382
383
384@functools.lru_cache(maxsize=1000)
385def check_for_language(lang_code):
386    """
387    Check whether there is a global language file for the given language
388    code. This is used to decide whether a user-provided language is
389    available.
390
391    lru_cache should have a maxsize to prevent from memory exhaustion attacks,
392    as the provided language codes are taken from the HTTP request. See also
393    <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
394    """
395    # First, a quick check to make sure lang_code is well-formed (#21458)
396    if lang_code is None or not language_code_re.search(lang_code):
397        return False
398    return any(
399        gettext_module.find("django", path, [to_locale(lang_code)]) is not None
400        for path in all_locale_paths()
401    )
402
403
404@functools.lru_cache()
405def get_languages():
406    """
407    Cache of settings.LANGUAGES in a dictionary for easy lookups by key.
408    """
409    return dict(settings.LANGUAGES)
410
411
412@functools.lru_cache(maxsize=1000)
413def get_supported_language_variant(lang_code, strict=False):
414    """
415    Return the language code that's listed in supported languages, possibly
416    selecting a more generic variant. Raise LookupError if nothing is found.
417
418    If `strict` is False (the default), look for a country-specific variant
419    when neither the language code nor its generic variant is found.
420
421    lru_cache should have a maxsize to prevent from memory exhaustion attacks,
422    as the provided language codes are taken from the HTTP request. See also
423    <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>.
424    """
425    if lang_code:
426        # If 'fr-ca' is not supported, try special fallback or language-only 'fr'.
427        possible_lang_codes = [lang_code]
428        try:
429            possible_lang_codes.extend(LANG_INFO[lang_code]["fallback"])
430        except KeyError:
431            pass
432        generic_lang_code = lang_code.split("-")[0]
433        possible_lang_codes.append(generic_lang_code)
434        supported_lang_codes = get_languages()
435
436        for code in possible_lang_codes:
437            if code in supported_lang_codes and check_for_language(code):
438                return code
439        if not strict:
440            # if fr-fr is not supported, try fr-ca.
441            for supported_code in supported_lang_codes:
442                if supported_code.startswith(generic_lang_code + "-"):
443                    return supported_code
444    raise LookupError(lang_code)
445
446
447def get_language_from_path(path, strict=False):
448    """
449    Return the language code if there's a valid language code found in `path`.
450
451    If `strict` is False (the default), look for a country-specific variant
452    when neither the language code nor its generic variant is found.
453    """
454    regex_match = language_code_prefix_re.match(path)
455    if not regex_match:
456        return None
457    lang_code = regex_match.group(1)
458    try:
459        return get_supported_language_variant(lang_code, strict=strict)
460    except LookupError:
461        return None
462
463
464def get_language_from_request(request, check_path=False):
465    """
466    Analyze the request to find what language the user wants the system to
467    show. Only languages listed in settings.LANGUAGES are taken into account.
468    If the user requests a sublanguage where we have a main language, we send
469    out the main language.
470
471    If check_path is True, the URL path prefix will be checked for a language
472    code, otherwise this is skipped for backwards compatibility.
473    """
474    if check_path:
475        lang_code = get_language_from_path(request.path_info)
476        if lang_code is not None:
477            return lang_code
478
479    lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME)
480    if (
481        lang_code is not None
482        and lang_code in get_languages()
483        and check_for_language(lang_code)
484    ):
485        return lang_code
486
487    try:
488        return get_supported_language_variant(lang_code)
489    except LookupError:
490        pass
491
492    accept = request.META.get("HTTP_ACCEPT_LANGUAGE", "")
493    for accept_lang, unused in parse_accept_lang_header(accept):
494        if accept_lang == "*":
495            break
496
497        if not language_code_re.search(accept_lang):
498            continue
499
500        try:
501            return get_supported_language_variant(accept_lang)
502        except LookupError:
503            continue
504
505    try:
506        return get_supported_language_variant(settings.LANGUAGE_CODE)
507    except LookupError:
508        return settings.LANGUAGE_CODE
509
510
511@functools.lru_cache(maxsize=1000)
512def parse_accept_lang_header(lang_string):
513    """
514    Parse the lang_string, which is the body of an HTTP Accept-Language
515    header, and return a tuple of (lang, q-value), ordered by 'q' values.
516
517    Return an empty tuple if there are any format errors in lang_string.
518    """
519    result = []
520    pieces = accept_language_re.split(lang_string.lower())
521    if pieces[-1]:
522        return ()
523    for i in range(0, len(pieces) - 1, 3):
524        first, lang, priority = pieces[i : i + 3]
525        if first:
526            return ()
527        if priority:
528            priority = float(priority)
529        else:
530            priority = 1.0
531        result.append((lang, priority))
532    result.sort(key=lambda k: k[1], reverse=True)
533    return tuple(result)

django/utils/timezone.py

  1"""
  2Timezone-related classes and functions.
  3"""
  4
  5import functools
  6from contextlib import ContextDecorator
  7from datetime import datetime, timedelta, timezone, tzinfo
  8
  9import pytz
 10from asgiref.local import Local
 11
 12from django.conf import settings
 13
 14__all__ = [
 15    "utc",
 16    "get_fixed_timezone",
 17    "get_default_timezone",
 18    "get_default_timezone_name",
 19    "get_current_timezone",
 20    "get_current_timezone_name",
 21    "activate",
 22    "deactivate",
 23    "override",
 24    "localtime",
 25    "now",
 26    "is_aware",
 27    "is_naive",
 28    "make_aware",
 29    "make_naive",
 30]
 31
 32
 33# UTC time zone as a tzinfo instance.
 34utc = pytz.utc
 35
 36
 37def get_fixed_timezone(offset):
 38    """Return a tzinfo instance with a fixed offset from UTC."""
 39    if isinstance(offset, timedelta):
 40        offset = offset.total_seconds() // 60
 41    sign = "-" if offset < 0 else "+"
 42    hhmm = "%02d%02d" % divmod(abs(offset), 60)
 43    name = sign + hhmm
 44    return timezone(timedelta(minutes=offset), name)
 45
 46
 47# In order to avoid accessing settings at compile time,
 48# wrap the logic in a function and cache the result.
 49@functools.lru_cache()
 50def get_default_timezone():
 51    """
 52    Return the default time zone as a tzinfo instance.
 53
 54    This is the time zone defined by settings.TIME_ZONE.
 55    """
 56    return pytz.timezone(settings.TIME_ZONE)
 57
 58
 59# This function exists for consistency with get_current_timezone_name
 60def get_default_timezone_name():
 61    """Return the name of the default time zone."""
 62    return _get_timezone_name(get_default_timezone())
 63
 64
 65_active = Local()
 66
 67
 68def get_current_timezone():
 69    """Return the currently active time zone as a tzinfo instance."""
 70    return getattr(_active, "value", get_default_timezone())
 71
 72
 73def get_current_timezone_name():
 74    """Return the name of the currently active time zone."""
 75    return _get_timezone_name(get_current_timezone())
 76
 77
 78def _get_timezone_name(timezone):
 79    """Return the name of ``timezone``."""
 80    return timezone.tzname(None)
 81
 82
 83# Timezone selection functions.
 84
 85# These functions don't change os.environ['TZ'] and call time.tzset()
 86# because it isn't thread safe.
 87
 88
 89def activate(timezone):
 90    """
 91    Set the time zone for the current thread.
 92
 93    The ``timezone`` argument must be an instance of a tzinfo subclass or a
 94    time zone name.
 95    """
 96    if isinstance(timezone, tzinfo):
 97        _active.value = timezone
 98    elif isinstance(timezone, str):
 99        _active.value = pytz.timezone(timezone)
100    else:
101        raise ValueError("Invalid timezone: %r" % timezone)
102
103
104def deactivate():
105    """
106    Unset the time zone for the current thread.
107
108    Django will then use the time zone defined by settings.TIME_ZONE.
109    """
110    if hasattr(_active, "value"):
111        del _active.value
112
113
114class override(ContextDecorator):
115    """
116    Temporarily set the time zone for the current thread.
117
118    This is a context manager that uses django.utils.timezone.activate()
119    to set the timezone on entry and restores the previously active timezone
120    on exit.
121
122    The ``timezone`` argument must be an instance of a ``tzinfo`` subclass, a
123    time zone name, or ``None``. If it is ``None``, Django enables the default
124    time zone.
125    """
126
127    def __init__(self, timezone):
128        self.timezone = timezone
129
130    def __enter__(self):
131        self.old_timezone = getattr(_active, "value", None)
132        if self.timezone is None:
133            deactivate()
134        else:
135            activate(self.timezone)
136
137    def __exit__(self, exc_type, exc_value, traceback):
138        if self.old_timezone is None:
139            deactivate()
140        else:
141            _active.value = self.old_timezone
142
143
144# Templates
145
146
147def template_localtime(value, use_tz=None):
148    """
149    Check if value is a datetime and converts it to local time if necessary.
150
151    If use_tz is provided and is not None, that will force the value to
152    be converted (or not), overriding the value of settings.USE_TZ.
153
154    This function is designed for use by the template engine.
155    """
156    should_convert = (
157        isinstance(value, datetime)
158        and (settings.USE_TZ if use_tz is None else use_tz)
159        and not is_naive(value)
160        and getattr(value, "convert_to_local_time", True)
161    )
162    return localtime(value) if should_convert else value
163
164
165# Utilities
166
167
168def localtime(value=None, timezone=None):
169    """
170    Convert an aware datetime.datetime to local time.
171
172    Only aware datetimes are allowed. When value is omitted, it defaults to
173    now().
174
175    Local time is defined by the current time zone, unless another time zone
176    is specified.
177    """
178    if value is None:
179        value = now()
180    if timezone is None:
181        timezone = get_current_timezone()
182    # Emulate the behavior of astimezone() on Python < 3.6.
183    if is_naive(value):
184        raise ValueError("localtime() cannot be applied to a naive datetime")
185    return value.astimezone(timezone)
186
187
188def localdate(value=None, timezone=None):
189    """
190    Convert an aware datetime to local time and return the value's date.
191
192    Only aware datetimes are allowed. When value is omitted, it defaults to
193    now().
194
195    Local time is defined by the current time zone, unless another time zone is
196    specified.
197    """
198    return localtime(value, timezone).date()
199
200
201def now():
202    """
203    Return an aware or naive datetime.datetime, depending on settings.USE_TZ.
204    """
205    if settings.USE_TZ:
206        # timeit shows that datetime.now(tz=utc) is 24% slower
207        return datetime.utcnow().replace(tzinfo=utc)
208    else:
209        return datetime.now()
210
211
212# By design, these four functions don't perform any checks on their arguments.
213# The caller should ensure that they don't receive an invalid value like None.
214
215
216def is_aware(value):
217    """
218    Determine if a given datetime.datetime is aware.
219
220    The concept is defined in Python's docs:
221    https://docs.python.org/library/datetime.html#datetime.tzinfo
222
223    Assuming value.tzinfo is either None or a proper datetime.tzinfo,
224    value.utcoffset() implements the appropriate logic.
225    """
226    return value.utcoffset() is not None
227
228
229def is_naive(value):
230    """
231    Determine if a given datetime.datetime is naive.
232
233    The concept is defined in Python's docs:
234    https://docs.python.org/library/datetime.html#datetime.tzinfo
235
236    Assuming value.tzinfo is either None or a proper datetime.tzinfo,
237    value.utcoffset() implements the appropriate logic.
238    """
239    return value.utcoffset() is None
240
241
242def make_aware(value, timezone=None, is_dst=None):
243    """Make a naive datetime.datetime in a given time zone aware."""
244    if timezone is None:
245        timezone = get_current_timezone()
246    if hasattr(timezone, "localize"):
247        # This method is available for pytz time zones.
248        return timezone.localize(value, is_dst=is_dst)
249    else:
250        # Check that we won't overwrite the timezone of an aware datetime.
251        if is_aware(value):
252            raise ValueError("make_aware expects a naive datetime, got %s" % value)
253        # This may be wrong around DST changes!
254        return value.replace(tzinfo=timezone)
255
256
257def make_naive(value, timezone=None):
258    """Make an aware datetime.datetime naive in a given time zone."""
259    if timezone is None:
260        timezone = get_current_timezone()
261    # Emulate the behavior of astimezone() on Python < 3.6.
262    if is_naive(value):
263        raise ValueError("make_naive() cannot be applied to a naive datetime")
264    return value.astimezone(timezone).replace(tzinfo=None)

django/contrib/staticfiles/handlers.py

 1from urllib.parse import urlparse
 2from urllib.request import url2pathname
 3
 4from django.conf import settings
 5from django.contrib.staticfiles import utils
 6from django.contrib.staticfiles.views import serve
 7from django.core.handlers.asgi import ASGIHandler
 8from django.core.handlers.exception import response_for_exception
 9from django.core.handlers.wsgi import WSGIHandler, get_path_info
10from django.http import Http404
11
12
13class StaticFilesHandlerMixin:
14    """
15    Common methods used by WSGI and ASGI handlers.
16    """
17
18    # May be used to differentiate between handler types (e.g. in a
19    # request_finished signal)
20    handles_files = True
21
22    def load_middleware(self):
23        # Middleware are already loaded for self.application; no need to reload
24        # them for self.
25        pass
26
27    def get_base_url(self):
28        utils.check_settings()
29        return settings.STATIC_URL
30
31    def _should_handle(self, path):
32        """
33        Check if the path should be handled. Ignore the path if:
34        * the host is provided as part of the base_url
35        * the request's path isn't under the media path (or equal)
36        """
37        return path.startswith(self.base_url[2]) and not self.base_url[1]
38
39    def file_path(self, url):
40        """
41        Return the relative path to the media file on disk for the given URL.
42        """
43        relative_url = url[len(self.base_url[2]) :]
44        return url2pathname(relative_url)
45
46    def serve(self, request):
47        """Serve the request path."""
48        return serve(request, self.file_path(request.path), insecure=True)
49
50    def get_response(self, request):
51        try:
52            return self.serve(request)
53        except Http404 as e:
54            return response_for_exception(request, e)
55
56
57class StaticFilesHandler(StaticFilesHandlerMixin, WSGIHandler):
58    """
59    WSGI middleware that intercepts calls to the static files directory, as
60    defined by the STATIC_URL setting, and serves those files.
61    """
62
63    def __init__(self, application):
64        self.application = application
65        self.base_url = urlparse(self.get_base_url())
66        super().__init__()
67
68    def __call__(self, environ, start_response):
69        if not self._should_handle(get_path_info(environ)):
70            return self.application(environ, start_response)
71        return super().__call__(environ, start_response)
72
73
74class ASGIStaticFilesHandler(StaticFilesHandlerMixin, ASGIHandler):
75    """
76    ASGI application which wraps another and intercepts requests for static
77    files, passing them off to Django's static file serving.
78    """
79
80    def __init__(self, application):
81        self.application = application
82        self.base_url = urlparse(self.get_base_url())
83
84    async def __call__(self, scope, receive, send):
85        # Only even look at HTTP requests
86        if scope["type"] == "http" and self._should_handle(scope["path"]):
87            # Serve static content
88            # (the one thing super() doesn't do is __call__, apparently)
89            return await super().__call__(scope, receive, send)
90        # Hand off to the main app
91        return await self.application(scope, receive, send)

tests/asgi/tests.py

  1import asyncio
  2import sys
  3from unittest import skipIf
  4
  5from asgiref.sync import async_to_sync
  6from asgiref.testing import ApplicationCommunicator
  7
  8from django.core.asgi import get_asgi_application
  9from django.core.signals import request_started
 10from django.db import close_old_connections
 11from django.test import SimpleTestCase, override_settings
 12
 13from .urls import test_filename
 14
 15
 16@skipIf(
 17    sys.platform == "win32" and (3, 8, 0) < sys.version_info < (3, 8, 1),
 18    "https://bugs.python.org/issue38563",
 19)
 20@override_settings(ROOT_URLCONF="asgi.urls")
 21class ASGITest(SimpleTestCase):
 22    def setUp(self):
 23        request_started.disconnect(close_old_connections)
 24
 25    def _get_scope(self, **kwargs):
 26        return {
 27            "type": "http",
 28            "asgi": {"version": "3.0", "spec_version": "2.1"},
 29            "http_version": "1.1",
 30            "method": "GET",
 31            "query_string": b"",
 32            "server": ("testserver", 80),
 33            **kwargs,
 34        }
 35
 36    def tearDown(self):
 37        request_started.connect(close_old_connections)
 38
 39    @async_to_sync
 40    async def test_get_asgi_application(self):
 41        """
 42        get_asgi_application() returns a functioning ASGI callable.
 43        """
 44        application = get_asgi_application()
 45        # Construct HTTP request.
 46        communicator = ApplicationCommunicator(application, self._get_scope(path="/"))
 47        await communicator.send_input({"type": "http.request"})
 48        # Read the response.
 49        response_start = await communicator.receive_output()
 50        self.assertEqual(response_start["type"], "http.response.start")
 51        self.assertEqual(response_start["status"], 200)
 52        self.assertEqual(
 53            set(response_start["headers"]),
 54            {
 55                (b"Content-Length", b"12"),
 56                (b"Content-Type", b"text/html; charset=utf-8"),
 57            },
 58        )
 59        response_body = await communicator.receive_output()
 60        self.assertEqual(response_body["type"], "http.response.body")
 61        self.assertEqual(response_body["body"], b"Hello World!")
 62
 63    @async_to_sync
 64    async def test_file_response(self):
 65        """
 66        Makes sure that FileResponse works over ASGI.
 67        """
 68        application = get_asgi_application()
 69        # Construct HTTP request.
 70        communicator = ApplicationCommunicator(
 71            application, self._get_scope(path="/file/")
 72        )
 73        await communicator.send_input({"type": "http.request"})
 74        # Get the file content.
 75        with open(test_filename, "rb") as test_file:
 76            test_file_contents = test_file.read()
 77        # Read the response.
 78        response_start = await communicator.receive_output()
 79        self.assertEqual(response_start["type"], "http.response.start")
 80        self.assertEqual(response_start["status"], 200)
 81        self.assertEqual(
 82            set(response_start["headers"]),
 83            {
 84                (b"Content-Length", str(len(test_file_contents)).encode("ascii")),
 85                (
 86                    b"Content-Type",
 87                    b"text/plain" if sys.platform == "win32" else b"text/x-python",
 88                ),
 89                (b"Content-Disposition", b'inline; filename="urls.py"'),
 90            },
 91        )
 92        response_body = await communicator.receive_output()
 93        self.assertEqual(response_body["type"], "http.response.body")
 94        self.assertEqual(response_body["body"], test_file_contents)
 95
 96    @async_to_sync
 97    async def test_headers(self):
 98        application = get_asgi_application()
 99        communicator = ApplicationCommunicator(
100            application,
101            self._get_scope(
102                path="/meta/",
103                headers=[
104                    [b"content-type", b"text/plain; charset=utf-8"],
105                    [b"content-length", b"77"],
106                    [b"referer", b"Scotland"],
107                    [b"referer", b"Wales"],
108                ],
109            ),
110        )
111        await communicator.send_input({"type": "http.request"})
112        response_start = await communicator.receive_output()
113        self.assertEqual(response_start["type"], "http.response.start")
114        self.assertEqual(response_start["status"], 200)
115        self.assertEqual(
116            set(response_start["headers"]),
117            {
118                (b"Content-Length", b"19"),
119                (b"Content-Type", b"text/plain; charset=utf-8"),
120            },
121        )
122        response_body = await communicator.receive_output()
123        self.assertEqual(response_body["type"], "http.response.body")
124        self.assertEqual(response_body["body"], b"From Scotland,Wales")
125
126    @async_to_sync
127    async def test_get_query_string(self):
128        application = get_asgi_application()
129        for query_string in (b"name=Andrew", "name=Andrew"):
130            with self.subTest(query_string=query_string):
131                communicator = ApplicationCommunicator(
132                    application, self._get_scope(path="/", query_string=query_string),
133                )
134                await communicator.send_input({"type": "http.request"})
135                response_start = await communicator.receive_output()
136                self.assertEqual(response_start["type"], "http.response.start")
137                self.assertEqual(response_start["status"], 200)
138                response_body = await communicator.receive_output()
139                self.assertEqual(response_body["type"], "http.response.body")
140                self.assertEqual(response_body["body"], b"Hello Andrew!")
141
142    @async_to_sync
143    async def test_disconnect(self):
144        application = get_asgi_application()
145        communicator = ApplicationCommunicator(application, self._get_scope(path="/"))
146        await communicator.send_input({"type": "http.disconnect"})
147        with self.assertRaises(asyncio.TimeoutError):
148            await communicator.receive_output()
149
150    @async_to_sync
151    async def test_wrong_connection_type(self):
152        application = get_asgi_application()
153        communicator = ApplicationCommunicator(
154            application, self._get_scope(path="/", type="other"),
155        )
156        await communicator.send_input({"type": "http.request"})
157        msg = "Django can only handle ASGI/HTTP connections, not other."
158        with self.assertRaisesMessage(ValueError, msg):
159            await communicator.receive_output()
160
161    @async_to_sync
162    async def test_non_unicode_query_string(self):
163        application = get_asgi_application()
164        communicator = ApplicationCommunicator(
165            application, self._get_scope(path="/", query_string=b"\xff"),
166        )
167        await communicator.send_input({"type": "http.request"})
168        response_start = await communicator.receive_output()
169        self.assertEqual(response_start["type"], "http.response.start")
170        self.assertEqual(response_start["status"], 400)
171        response_body = await communicator.receive_output()
172        self.assertEqual(response_body["type"], "http.response.body")
173        self.assertEqual(response_body["body"], b"")

tests/i18n/tests.py

   1import datetime
   2import decimal
   3import gettext as gettext_module
   4import os
   5import pickle
   6import re
   7import tempfile
   8from contextlib import contextmanager
   9from importlib import import_module
  10from pathlib import Path
  11from unittest import mock
  12
  13from asgiref.local import Local
  14
  15from django import forms
  16from django.apps import AppConfig
  17from django.conf import settings
  18from django.conf.locale import LANG_INFO
  19from django.conf.urls.i18n import i18n_patterns
  20from django.template import Context, Template
  21from django.test import (
  22    RequestFactory,
  23    SimpleTestCase,
  24    TestCase,
  25    override_settings,
  26)
  27from django.utils import translation
  28from django.utils.deprecation import RemovedInDjango40Warning
  29from django.utils.formats import (
  30    date_format,
  31    get_format,
  32    get_format_modules,
  33    iter_format_modules,
  34    localize,
  35    localize_input,
  36    reset_format_cache,
  37    sanitize_separators,
  38    time_format,
  39)
  40from django.utils.numberformat import format as nformat
  41from django.utils.safestring import SafeString, mark_safe
  42from django.utils.translation import (
  43    LANGUAGE_SESSION_KEY,
  44    activate,
  45    check_for_language,
  46    deactivate,
  47    get_language,
  48    get_language_bidi,
  49    get_language_from_request,
  50    get_language_info,
  51    gettext,
  52    gettext_lazy,
  53    ngettext,
  54    ngettext_lazy,
  55    npgettext,
  56    npgettext_lazy,
  57    pgettext,
  58    round_away_from_one,
  59    to_language,
  60    to_locale,
  61    trans_null,
  62    trans_real,
  63    ugettext,
  64    ugettext_lazy,
  65    ugettext_noop,
  66    ungettext,
  67    ungettext_lazy,
  68)
  69from django.utils.translation.reloader import (
  70    translation_file_changed,
  71    watch_for_translation_changes,
  72)
  73
  74from .forms import CompanyForm, I18nForm, SelectDateForm
  75from .models import Company, TestModel
  76
  77here = os.path.dirname(os.path.abspath(__file__))
  78extended_locale_paths = settings.LOCALE_PATHS + [
  79    os.path.join(here, "other", "locale"),
  80]
  81
  82
  83class AppModuleStub:
  84    def __init__(self, **kwargs):
  85        self.__dict__.update(kwargs)
  86
  87
  88@contextmanager
  89def patch_formats(lang, **settings):
  90    from django.utils.formats import _format_cache
  91
  92    # Populate _format_cache with temporary values
  93    for key, value in settings.items():
  94        _format_cache[(key, lang)] = value
  95    try:
  96        yield
  97    finally:
  98        reset_format_cache()
  99
 100
 101class TranslationTests(SimpleTestCase):
 102    @translation.override("de")
 103    def test_legacy_aliases(self):
 104        """
 105        Pre-Django 2.0 aliases with u prefix are still available.
 106        """
 107        msg = (
 108            "django.utils.translation.ugettext_noop() is deprecated in favor "
 109            "of django.utils.translation.gettext_noop()."
 110        )
 111        with self.assertWarnsMessage(RemovedInDjango40Warning, msg):
 112            self.assertEqual(ugettext_noop("Image"), "Image")
 113        msg = (
 114            "django.utils.translation.ugettext() is deprecated in favor of "
 115            "django.utils.translation.gettext()."
 116        )
 117        with self.assertWarnsMessage(RemovedInDjango40Warning, msg):
 118            self.assertEqual(ugettext("Image"), "Bild")
 119        msg = (
 120            "django.utils.translation.ugettext_lazy() is deprecated in favor "
 121            "of django.utils.translation.gettext_lazy()."
 122        )
 123        with self.assertWarnsMessage(RemovedInDjango40Warning, msg):
 124            self.assertEqual(ugettext_lazy("Image"), gettext_lazy("Image"))
 125        msg = (
 126            "django.utils.translation.ungettext() is deprecated in favor of "
 127            "django.utils.translation.ngettext()."
 128        )
 129        with self.assertWarnsMessage(RemovedInDjango40Warning, msg):
 130            self.assertEqual(ungettext("%d year", "%d years", 0) % 0, "0 Jahre")
 131        msg = (
 132            "django.utils.translation.ungettext_lazy() is deprecated in favor "
 133            "of django.utils.translation.ngettext_lazy()."
 134        )
 135        with self.assertWarnsMessage(RemovedInDjango40Warning, msg):
 136            self.assertEqual(
 137                ungettext_lazy("%d year", "%d years", 0) % 0,
 138                ngettext_lazy("%d year", "%d years", 0) % 0,
 139            )
 140
 141    @translation.override("fr")
 142    def test_plural(self):
 143        """
 144        Test plurals with ngettext. French differs from English in that 0 is singular.
 145        """
 146        self.assertEqual(ngettext("%d year", "%d years", 0) % 0, "0 année")
 147        self.assertEqual(ngettext("%d year", "%d years", 2) % 2, "2 années")
 148        self.assertEqual(
 149            ngettext("%(size)d byte", "%(size)d bytes", 0) % {"size": 0}, "0 octet"
 150        )
 151        self.assertEqual(
 152            ngettext("%(size)d byte", "%(size)d bytes", 2) % {"size": 2}, "2 octets"
 153        )
 154
 155    def test_plural_null(self):
 156        g = trans_null.ngettext
 157        self.assertEqual(g("%d year", "%d years", 0) % 0, "0 years")
 158        self.assertEqual(g("%d year", "%d years", 1) % 1, "1 year")
 159        self.assertEqual(g("%d year", "%d years", 2) % 2, "2 years")
 160
 161    def test_override(self):
 162        activate("de")
 163        try:
 164            with translation.override("pl"):
 165                self.assertEqual(get_language(), "pl")
 166            self.assertEqual(get_language(), "de")
 167            with translation.override(None):
 168                self.assertIsNone(get_language())
 169                with translation.override("pl"):
 170                    pass
 171                self.assertIsNone(get_language())
 172            self.assertEqual(get_language(), "de")
 173        finally:
 174            deactivate()
 175
 176    def test_override_decorator(self):
 177        @translation.override("pl")
 178        def func_pl():
 179            self.assertEqual(get_language(), "pl")
 180
 181        @translation.override(None)
 182        def func_none():
 183            self.assertIsNone(get_language())
 184
 185        try:
 186            activate("de")
 187            func_pl()
 188            self.assertEqual(get_language(), "de")
 189            func_none()
 190            self.assertEqual(get_language(), "de")
 191        finally:
 192            deactivate()
 193
 194    def test_override_exit(self):
 195        """
 196        The language restored is the one used when the function was
 197        called, not the one used when the decorator was initialized (#23381).
 198        """
 199        activate("fr")
 200
 201        @translation.override("pl")
 202        def func_pl():
 203            pass
 204
 205        deactivate()
 206
 207        try:
 208            activate("en")
 209            func_pl()
 210            self.assertEqual(get_language(), "en")
 211        finally:
 212            deactivate()
 213
 214    def test_lazy_objects(self):
 215        """
 216        Format string interpolation should work with *_lazy objects.
 217        """
 218        s = gettext_lazy("Add %(name)s")
 219        d = {"name": "Ringo"}
 220        self.assertEqual("Add Ringo", s % d)
 221        with translation.override("de", deactivate=True):
 222            self.assertEqual("Ringo hinzuf\xfcgen", s % d)
 223            with translation.override("pl"):
 224                self.assertEqual("Dodaj Ringo", s % d)
 225
 226        # It should be possible to compare *_lazy objects.
 227        s1 = gettext_lazy("Add %(name)s")
 228        self.assertEqual(s, s1)
 229        s2 = gettext_lazy("Add %(name)s")
 230        s3 = gettext_lazy("Add %(name)s")
 231        self.assertEqual(s2, s3)
 232        self.assertEqual(s, s2)
 233        s4 = gettext_lazy("Some other string")
 234        self.assertNotEqual(s, s4)
 235
 236    def test_lazy_pickle(self):
 237        s1 = gettext_lazy("test")
 238        self.assertEqual(str(s1), "test")
 239        s2 = pickle.loads(pickle.dumps(s1))
 240        self.assertEqual(str(s2), "test")
 241
 242    @override_settings(LOCALE_PATHS=extended_locale_paths)
 243    def test_ngettext_lazy(self):
 244        simple_with_format = ngettext_lazy("%d good result", "%d good results")
 245        simple_context_with_format = npgettext_lazy(
 246            "Exclamation", "%d good result", "%d good results"
 247        )
 248        simple_without_format = ngettext_lazy("good result", "good results")
 249        with translation.override("de"):
 250            self.assertEqual(simple_with_format % 1, "1 gutes Resultat")
 251            self.assertEqual(simple_with_format % 4, "4 guten Resultate")
 252            self.assertEqual(simple_context_with_format % 1, "1 gutes Resultat!")
 253            self.assertEqual(simple_context_with_format % 4, "4 guten Resultate!")
 254            self.assertEqual(simple_without_format % 1, "gutes Resultat")
 255            self.assertEqual(simple_without_format % 4, "guten Resultate")
 256
 257        complex_nonlazy = ngettext_lazy(
 258            "Hi %(name)s, %(num)d good result", "Hi %(name)s, %(num)d good results", 4
 259        )
 260        complex_deferred = ngettext_lazy(
 261            "Hi %(name)s, %(num)d good result",
 262            "Hi %(name)s, %(num)d good results",
 263            "num",
 264        )
 265        complex_context_nonlazy = npgettext_lazy(
 266            "Greeting",
 267            "Hi %(name)s, %(num)d good result",
 268            "Hi %(name)s, %(num)d good results",
 269            4,
 270        )
 271        complex_context_deferred = npgettext_lazy(
 272            "Greeting",
 273            "Hi %(name)s, %(num)d good result",
 274            "Hi %(name)s, %(num)d good results",
 275            "num",
 276        )
 277        with translation.override("de"):
 278            self.assertEqual(
 279                complex_nonlazy % {"num": 4, "name": "Jim"},
 280                "Hallo Jim, 4 guten Resultate",
 281            )
 282            self.assertEqual(
 283                complex_deferred % {"name": "Jim", "num": 1},
 284                "Hallo Jim, 1 gutes Resultat",
 285            )
 286            self.assertEqual(
 287                complex_deferred % {"name": "Jim", "num": 5},
 288                "Hallo Jim, 5 guten Resultate",
 289            )
 290            with self.assertRaisesMessage(KeyError, "Your dictionary lacks key"):
 291                complex_deferred % {"name": "Jim"}
 292            self.assertEqual(
 293                complex_context_nonlazy % {"num": 4, "name": "Jim"},
 294                "Willkommen Jim, 4 guten Resultate",
 295            )
 296            self.assertEqual(
 297                complex_context_deferred % {"name": "Jim", "num": 1},
 298                "Willkommen Jim, 1 gutes Resultat",
 299            )
 300            self.assertEqual(
 301                complex_context_deferred % {"name": "Jim", "num": 5},
 302                "Willkommen Jim, 5 guten Resultate",
 303            )
 304            with self.assertRaisesMessage(KeyError, "Your dictionary lacks key"):
 305                complex_context_deferred % {"name": "Jim"}
 306
 307    @override_settings(LOCALE_PATHS=extended_locale_paths)
 308    def test_ngettext_lazy_format_style(self):
 309        simple_with_format = ngettext_lazy("{} good result", "{} good results")
 310        simple_context_with_format = npgettext_lazy(
 311            "Exclamation", "{} good result", "{} good results"
 312        )
 313
 314        with translation.override("de"):
 315            self.assertEqual(simple_with_format.format(1), "1 gutes Resultat")
 316            self.assertEqual(simple_with_format.format(4), "4 guten Resultate")
 317            self.assertEqual(simple_context_with_format.format(1), "1 gutes Resultat!")
 318            self.assertEqual(simple_context_with_format.format(4), "4 guten Resultate!")
 319
 320        complex_nonlazy = ngettext_lazy(
 321            "Hi {name}, {num} good result", "Hi {name}, {num} good results", 4
 322        )
 323        complex_deferred = ngettext_lazy(
 324            "Hi {name}, {num} good result", "Hi {name}, {num} good results", "num"
 325        )
 326        complex_context_nonlazy = npgettext_lazy(
 327            "Greeting",
 328            "Hi {name}, {num} good result",
 329            "Hi {name}, {num} good results",
 330            4,
 331        )
 332        complex_context_deferred = npgettext_lazy(
 333            "Greeting",
 334            "Hi {name}, {num} good result",
 335            "Hi {name}, {num} good results",
 336            "num",
 337        )
 338        with translation.override("de"):
 339            self.assertEqual(
 340                complex_nonlazy.format(num=4, name="Jim"),
 341                "Hallo Jim, 4 guten Resultate",
 342            )
 343            self.assertEqual(
 344                complex_deferred.format(name="Jim", num=1),
 345                "Hallo Jim, 1 gutes Resultat",
 346            )
 347            self.assertEqual(
 348                complex_deferred.format(name="Jim", num=5),
 349                "Hallo Jim, 5 guten Resultate",
 350            )
 351            with self.assertRaisesMessage(KeyError, "Your dictionary lacks key"):
 352                complex_deferred.format(name="Jim")
 353            self.assertEqual(
 354                complex_context_nonlazy.format(num=4, name="Jim"),
 355                "Willkommen Jim, 4 guten Resultate",
 356            )
 357            self.assertEqual(
 358                complex_context_deferred.format(name="Jim", num=1),
 359                "Willkommen Jim, 1 gutes Resultat",
 360            )
 361            self.assertEqual(
 362                complex_context_deferred.format(name="Jim", num=5),
 363                "Willkommen Jim, 5 guten Resultate",
 364            )
 365            with self.assertRaisesMessage(KeyError, "Your dictionary lacks key"):
 366                complex_context_deferred.format(name="Jim")
 367
 368    def test_ngettext_lazy_bool(self):
 369        self.assertTrue(ngettext_lazy("%d good result", "%d good results"))
 370        self.assertFalse(ngettext_lazy("", ""))
 371
 372    def test_ngettext_lazy_pickle(self):
 373        s1 = ngettext_lazy("%d good result", "%d good results")
 374        self.assertEqual(s1 % 1, "1 good result")
 375        self.assertEqual(s1 % 8, "8 good results")
 376        s2 = pickle.loads(pickle.dumps(s1))
 377        self.assertEqual(s2 % 1, "1 good result")
 378        self.assertEqual(s2 % 8, "8 good results")
 379
 380    @override_settings(LOCALE_PATHS=extended_locale_paths)
 381    def test_pgettext(self):
 382        trans_real._active = Local()
 383        trans_real._translations = {}
 384        with translation.override("de"):
 385            self.assertEqual(pgettext("unexisting", "May"), "May")
 386            self.assertEqual(pgettext("month name", "May"), "Mai")
 387            self.assertEqual(pgettext("verb", "May"), "Kann")
 388            self.assertEqual(
 389                npgettext("search", "%d result", "%d results", 4) % 4, "4 Resultate"
 390            )
 391
 392    def test_empty_value(self):
 393        """Empty value must stay empty after being translated (#23196)."""
 394        with translation.override("de"):
 395            self.assertEqual("", gettext(""))
 396            s = mark_safe("")
 397            self.assertEqual(s, gettext(s))
 398
 399    @override_settings(LOCALE_PATHS=extended_locale_paths)
 400    def test_safe_status(self):
 401        """
 402        Translating a string requiring no auto-escaping with gettext or pgettext
 403        shouldn't change the "safe" status.
 404        """
 405        trans_real._active = Local()
 406        trans_real._translations = {}
 407        s1 = mark_safe("Password")
 408        s2 = mark_safe("May")
 409        with translation.override("de", deactivate=True):
 410            self.assertIs(type(gettext(s1)), SafeString)
 411            self.assertIs(type(pgettext("month name", s2)), SafeString)
 412        self.assertEqual("aPassword", SafeString("a") + s1)
 413        self.assertEqual("Passworda", s1 + SafeString("a"))
 414        self.assertEqual("Passworda", s1 + mark_safe("a"))
 415        self.assertEqual("aPassword", mark_safe("a") + s1)
 416        self.assertEqual("as", mark_safe("a") + mark_safe("s"))
 417
 418    def test_maclines(self):
 419        """
 420        Translations on files with Mac or DOS end of lines will be converted
 421        to unix EOF in .po catalogs.
 422        """
 423        ca_translation = trans_real.translation("ca")
 424        ca_translation._catalog["Mac\nEOF\n"] = "Catalan Mac\nEOF\n"
 425        ca_translation._catalog["Win\nEOF\n"] = "Catalan Win\nEOF\n"
 426        with translation.override("ca", deactivate=True):
 427            self.assertEqual("Catalan Mac\nEOF\n", gettext("Mac\rEOF\r"))
 428            self.assertEqual("Catalan Win\nEOF\n", gettext("Win\r\nEOF\r\n"))
 429
 430    def test_to_locale(self):
 431        tests = (
 432            ("en", "en"),
 433            ("EN", "en"),
 434            ("en-us", "en_US"),
 435            ("EN-US", "en_US"),
 436            # With > 2 characters after the dash.
 437            ("sr-latn", "sr_Latn"),
 438            ("sr-LATN", "sr_Latn"),
 439            # With private use subtag (x-informal).
 440            ("nl-nl-x-informal", "nl_NL-x-informal"),
 441            ("NL-NL-X-INFORMAL", "nl_NL-x-informal"),
 442            ("sr-latn-x-informal", "sr_Latn-x-informal"),
 443            ("SR-LATN-X-INFORMAL", "sr_Latn-x-informal"),
 444        )
 445        for lang, locale in tests:
 446            with self.subTest(lang=lang):
 447                self.assertEqual(to_locale(lang), locale)
 448
 449    def test_to_language(self):
 450        self.assertEqual(to_language("en_US"), "en-us")
 451        self.assertEqual(to_language("sr_Lat"), "sr-lat")
 452
 453    def test_language_bidi(self):
 454        self.assertIs(get_language_bidi(), False)
 455        with translation.override(None):
 456            self.assertIs(get_language_bidi(), False)
 457
 458    def test_language_bidi_null(self):
 459        self.assertIs(trans_null.get_language_bidi(), False)
 460        with override_settings(LANGUAGE_CODE="he"):
 461            self.assertIs(get_language_bidi(), True)
 462
 463
 464class TranslationThreadSafetyTests(SimpleTestCase):
 465    def setUp(self):
 466        self._old_language = get_language()
 467        self._translations = trans_real._translations
 468
 469        # here we rely on .split() being called inside the _fetch()
 470        # in trans_real.translation()
 471        class sideeffect_str(str):
 472            def split(self, *args, **kwargs):
 473                res = str.split(self, *args, **kwargs)
 474                trans_real._translations["en-YY"] = None
 475                return res
 476
 477        trans_real._translations = {sideeffect_str("en-XX"): None}
 478
 479    def tearDown(self):
 480        trans_real._translations = self._translations
 481        activate(self._old_language)
 482
 483    def test_bug14894_translation_activate_thread_safety(self):
 484        translation_count = len(trans_real._translations)
 485        # May raise RuntimeError if translation.activate() isn't thread-safe.
 486        translation.activate("pl")
 487        # make sure sideeffect_str actually added a new translation
 488        self.assertLess(translation_count, len(trans_real._translations))
 489
 490
 491@override_settings(USE_L10N=True)
 492class FormattingTests(SimpleTestCase):
 493    def setUp(self):
 494        super().setUp()
 495        self.n = decimal.Decimal("66666.666")
 496        self.f = 99999.999
 497        self.d = datetime.date(2009, 12, 31)
 498        self.dt = datetime.datetime(2009, 12, 31, 20, 50)
 499        self.t = datetime.time(10, 15, 48)
 500        self.long = 10000
 501        self.ctxt = Context(
 502            {
 503                "n": self.n,
 504                "t": self.t,
 505                "d": self.d,
 506                "dt": self.dt,
 507                "f": self.f,
 508                "l": self.long,
 509            }
 510        )
 511
 512    def test_all_format_strings(self):
 513        all_locales = LANG_INFO.keys()
 514        some_date = datetime.date(2017, 10, 14)
 515        some_datetime = datetime.datetime(2017, 10, 14, 10, 23)
 516        for locale in all_locales:
 517            with self.subTest(locale=locale), translation.override(locale):
 518                self.assertIn(
 519                    "2017", date_format(some_date)
 520                )  # Uses DATE_FORMAT by default
 521                self.assertIn(
 522                    "23", time_format(some_datetime)
 523                )  # Uses TIME_FORMAT by default
 524                self.assertIn(
 525                    "2017",
 526                    date_format(some_datetime, format=get_format("DATETIME_FORMAT")),
 527                )
 528                self.assertIn(
 529                    "2017",
 530                    date_format(some_date, format=get_format("YEAR_MONTH_FORMAT")),
 531                )
 532                self.assertIn(
 533                    "14", date_format(some_date, format=get_format("MONTH_DAY_FORMAT"))
 534                )
 535                self.assertIn(
 536                    "2017",
 537                    date_format(some_date, format=get_format("SHORT_DATE_FORMAT")),
 538                )
 539                self.assertIn(
 540                    "2017",
 541                    date_format(
 542                        some_datetime, format=get_format("SHORT_DATETIME_FORMAT")
 543                    ),
 544                )
 545
 546    def test_locale_independent(self):
 547        """
 548        Localization of numbers
 549        """
 550        with self.settings(USE_THOUSAND_SEPARATOR=False):
 551            self.assertEqual(
 552                "66666.66",
 553                nformat(
 554                    self.n, decimal_sep=".", decimal_pos=2, grouping=3, thousand_sep=","
 555                ),
 556            )
 557            self.assertEqual(
 558                "66666A6",
 559                nformat(
 560                    self.n, decimal_sep="A", decimal_pos=1, grouping=1, thousand_sep="B"
 561                ),
 562            )
 563            self.assertEqual(
 564                "66666",
 565                nformat(
 566                    self.n, decimal_sep="X", decimal_pos=0, grouping=1, thousand_sep="Y"
 567                ),
 568            )
 569
 570        with self.settings(USE_THOUSAND_SEPARATOR=True):
 571            self.assertEqual(
 572                "66,666.66",
 573                nformat(
 574                    self.n, decimal_sep=".", decimal_pos=2, grouping=3, thousand_sep=","
 575                ),
 576            )
 577            self.assertEqual(
 578                "6B6B6B6B6A6",
 579                nformat(
 580                    self.n, decimal_sep="A", decimal_pos=1, grouping=1, thousand_sep="B"
 581                ),
 582            )
 583            self.assertEqual(
 584                "-66666.6", nformat(-66666.666, decimal_sep=".", decimal_pos=1)
 585            )
 586            self.assertEqual(
 587                "-66666.0", nformat(int("-66666"), decimal_sep=".", decimal_pos=1)
 588            )
 589            self.assertEqual(
 590                "10000.0", nformat(self.long, decimal_sep=".", decimal_pos=1)
 591            )
 592            self.assertEqual(
 593                "10,00,00,000.00",
 594                nformat(
 595                    100000000.00,
 596                    decimal_sep=".",
 597                    decimal_pos=2,
 598                    grouping=(3, 2, 0),
 599                    thousand_sep=",",
 600                ),
 601            )
 602            self.assertEqual(
 603                "1,0,00,000,0000.00",
 604                nformat(
 605                    10000000000.00,
 606                    decimal_sep=".",
 607                    decimal_pos=2,
 608                    grouping=(4, 3, 2, 1, 0),
 609                    thousand_sep=",",
 610                ),
 611            )
 612            self.assertEqual(
 613                "10000,00,000.00",
 614                nformat(
 615                    1000000000.00,
 616                    decimal_sep=".",
 617                    decimal_pos=2,
 618                    grouping=(3, 2, -1),
 619                    thousand_sep=",",
 620                ),
 621            )
 622            # This unusual grouping/force_grouping combination may be triggered by the intcomma filter (#17414)
 623            self.assertEqual(
 624                "10000",
 625                nformat(
 626                    self.long,
 627                    decimal_sep=".",
 628                    decimal_pos=0,
 629                    grouping=0,
 630                    force_grouping=True,
 631                ),
 632            )
 633            # date filter
 634            self.assertEqual(
 635                "31.12.2009 в 20:50",
 636                Template('{{ dt|date:"d.m.Y в H:i" }}').render(self.ctxt),
 637            )
 638            self.assertEqual(
 639                "⌚ 10:15", Template('{{ t|time:"⌚ H:i" }}').render(self.ctxt)
 640            )
 641
 642    @override_settings(USE_L10N=False)
 643    def test_l10n_disabled(self):
 644        """
 645        Catalan locale with format i18n disabled translations will be used,
 646        but not formats
 647        """
 648        with translation.override("ca", deactivate=True):
 649            self.maxDiff = 3000
 650            self.assertEqual("N j, Y", get_format("DATE_FORMAT"))
 651            self.assertEqual(0, get_format("FIRST_DAY_OF_WEEK"))
 652            self.assertEqual(".", get_format("DECIMAL_SEPARATOR"))
 653            self.assertEqual("10:15 a.m.", time_format(self.t))
 654            self.assertEqual("des. 31, 2009", date_format(self.d))
 655            self.assertEqual("desembre 2009", date_format(self.d, "YEAR_MONTH_FORMAT"))
 656            self.assertEqual(
 657                "12/31/2009 8:50 p.m.", date_format(self.dt, "SHORT_DATETIME_FORMAT")
 658            )
 659            self.assertEqual("No localizable", localize("No localizable"))
 660            self.assertEqual("66666.666", localize(self.n))
 661            self.assertEqual("99999.999", localize(self.f))
 662            self.assertEqual("10000", localize(self.long))
 663            self.assertEqual("des. 31, 2009", localize(self.d))
 664            self.assertEqual("des. 31, 2009, 8:50 p.m.", localize(self.dt))
 665            self.assertEqual("66666.666", Template("{{ n }}").render(self.ctxt))
 666            self.assertEqual("99999.999", Template("{{ f }}").render(self.ctxt))
 667            self.assertEqual("des. 31, 2009", Template("{{ d }}").render(self.ctxt))
 668            self.assertEqual(
 669                "des. 31, 2009, 8:50 p.m.", Template("{{ dt }}").render(self.ctxt)
 670            )
 671            self.assertEqual(
 672                "66666.67", Template("{{ n|floatformat:2 }}").render(self.ctxt)
 673            )
 674            self.assertEqual(
 675                "100000.0", Template("{{ f|floatformat }}").render(self.ctxt)
 676            )
 677            self.assertEqual(
 678                "10:15 a.m.", Template('{{ t|time:"TIME_FORMAT" }}').render(self.ctxt)
 679            )
 680            self.assertEqual(
 681                "12/31/2009",
 682                Template('{{ d|date:"SHORT_DATE_FORMAT" }}').render(self.ctxt),
 683            )
 684            self.assertEqual(
 685                "12/31/2009 8:50 p.m.",
 686                Template('{{ dt|date:"SHORT_DATETIME_FORMAT" }}').render(self.ctxt),
 687            )
 688
 689            form = I18nForm(
 690                {
 691                    "decimal_field": "66666,666",
 692                    "float_field": "99999,999",
 693                    "date_field": "31/12/2009",
 694                    "datetime_field": "31/12/2009 20:50",
 695                    "time_field": "20:50",
 696                    "integer_field": "1.234",
 697                }
 698            )
 699            self.assertFalse(form.is_valid())
 700            self.assertEqual(["Introdu\xefu un n\xfamero."], form.errors["float_field"])
 701            self.assertEqual(
 702                ["Introdu\xefu un n\xfamero."], form.errors["decimal_field"]
 703            )
 704            self.assertEqual(
 705                ["Introdu\xefu una data v\xe0lida."], form.errors["date_field"]
 706            )
 707            self.assertEqual(
 708                ["Introdu\xefu una data/hora v\xe0lides."],
 709                form.errors["datetime_field"],
 710            )
 711            self.assertEqual(
 712                ["Introdu\xefu un n\xfamero sencer."], form.errors["integer_field"]
 713            )
 714
 715            form2 = SelectDateForm(
 716                {
 717                    "date_field_month": "12",
 718                    "date_field_day": "31",
 719                    "date_field_year": "2009",
 720                }
 721            )
 722            self.assertTrue(form2.is_valid())
 723            self.assertEqual(
 724                datetime.date(2009, 12, 31), form2.cleaned_data["date_field"]
 725            )
 726            self.assertHTMLEqual(
 727                '<select name="mydate_month" id="id_mydate_month">'
 728                '<option value="">---</option>'
 729                '<option value="1">gener</option>'
 730                '<option value="2">febrer</option>'
 731                '<option value="3">mar\xe7</option>'
 732                '<option value="4">abril</option>'
 733                '<option value="5">maig</option>'
 734                '<option value="6">juny</option>'
 735                '<option value="7">juliol</option>'
 736                '<option value="8">agost</option>'
 737                '<option value="9">setembre</option>'
 738                '<option value="10">octubre</option>'
 739                '<option value="11">novembre</option>'
 740                '<option value="12" selected>desembre</option>'
 741                "</select>"
 742                '<select name="mydate_day" id="id_mydate_day">'
 743                '<option value="">---</option>'
 744                '<option value="1">1</option>'
 745                '<option value="2">2</option>'
 746                '<option value="3">3</option>'
 747                '<option value="4">4</option>'
 748                '<option value="5">5</option>'
 749                '<option value="6">6</option>'
 750                '<option value="7">7</option>'
 751                '<option value="8">8</option>'
 752                '<option value="9">9</option>'
 753                '<option value="10">10</option>'
 754                '<option value="11">11</option>'
 755                '<option value="12">12</option>'
 756                '<option value="13">13</option>'
 757                '<option value="14">14</option>'
 758                '<option value="15">15</option>'
 759                '<option value="16">16</option>'
 760                '<option value="17">17</option>'
 761                '<option value="18">18</option>'
 762                '<option value="19">19</option>'
 763                '<option value="20">20</option>'
 764                '<option value="21">21</option>'
 765                '<option value="22">22</option>'
 766                '<option value="23">23</option>'
 767                '<option value="24">24</option>'
 768                '<option value="25">25</option>'
 769                '<option value="26">26</option>'
 770                '<option value="27">27</option>'
 771                '<option value="28">28</option>'
 772                '<option value="29">29</option>'
 773                '<option value="30">30</option>'
 774                '<option value="31" selected>31</option>'
 775                "</select>"
 776                '<select name="mydate_year" id="id_mydate_year">'
 777                '<option value="">---</option>'
 778                '<option value="2009" selected>2009</option>'
 779                '<option value="2010">2010</option>'
 780                '<option value="2011">2011</option>'
 781                '<option value="2012">2012</option>'
 782                '<option value="2013">2013</option>'
 783                '<option value="2014">2014</option>'
 784                '<option value="2015">2015</option>'
 785                '<option value="2016">2016</option>'
 786                '<option value="2017">2017</option>'
 787                '<option value="2018">2018</option>'
 788                "</select>",
 789                forms.SelectDateWidget(years=range(2009, 2019)).render(
 790                    "mydate", datetime.date(2009, 12, 31)
 791                ),
 792            )
 793
 794            # We shouldn't change the behavior of the floatformat filter re:
 795            # thousand separator and grouping when USE_L10N is False even
 796            # if the USE_THOUSAND_SEPARATOR, NUMBER_GROUPING and
 797            # THOUSAND_SEPARATOR settings are specified
 798            with self.settings(
 799                USE_THOUSAND_SEPARATOR=True, NUMBER_GROUPING=1, THOUSAND_SEPARATOR="!"
 800            ):
 801                self.assertEqual(
 802                    "66666.67", Template("{{ n|floatformat:2 }}").render(self.ctxt)
 803                )
 804                self.assertEqual(
 805                    "100000.0", Template("{{ f|floatformat }}").render(self.ctxt)
 806                )
 807
 808    def test_false_like_locale_formats(self):
 809        """
 810        The active locale's formats take precedence over the default settings
 811        even if they would be interpreted as False in a conditional test
 812        (e.g. 0 or empty string) (#16938).
 813        """
 814        with translation.override("fr"):
 815            with self.settings(USE_THOUSAND_SEPARATOR=True, THOUSAND_SEPARATOR="!"):
 816                self.assertEqual("\xa0", get_format("THOUSAND_SEPARATOR"))
 817                # Even a second time (after the format has been cached)...
 818                self.assertEqual("\xa0", get_format("THOUSAND_SEPARATOR"))
 819
 820            with self.settings(FIRST_DAY_OF_WEEK=0):
 821                self.assertEqual(1, get_format("FIRST_DAY_OF_WEEK"))
 822                # Even a second time (after the format has been cached)...
 823                self.assertEqual(1, get_format("FIRST_DAY_OF_WEEK"))
 824
 825    def test_l10n_enabled(self):
 826        self.maxDiff = 3000
 827        # Catalan locale
 828        with translation.override("ca", deactivate=True):
 829            self.assertEqual(r"j \d\e F \d\e Y", get_format("DATE_FORMAT"))
 830            self.assertEqual(1, get_format("FIRST_DAY_OF_WEEK"))
 831            self.assertEqual(",", get_format("DECIMAL_SEPARATOR"))
 832            self.assertEqual("10:15", time_format(self.t))
 833            self.assertEqual("31 de desembre de 2009", date_format(self.d))
 834            self.assertEqual(
 835                "desembre del 2009", date_format(self.d, "YEAR_MONTH_FORMAT")
 836            )
 837            self.assertEqual(
 838                "31/12/2009 20:50", date_format(self.dt, "SHORT_DATETIME_FORMAT")
 839            )
 840            self.assertEqual("No localizable", localize("No localizable"))
 841
 842            with self.settings(USE_THOUSAND_SEPARATOR=True):
 843                self.assertEqual("66.666,666", localize(self.n))
 844                self.assertEqual("99.999,999", localize(self.f))
 845                self.assertEqual("10.000", localize(self.long))
 846                self.assertEqual("True", localize(True))
 847
 848            with self.settings(USE_THOUSAND_SEPARATOR=False):
 849                self.assertEqual("66666,666", localize(self.n))
 850                self.assertEqual("99999,999", localize(self.f))
 851                self.assertEqual("10000", localize(self.long))
 852                self.assertEqual("31 de desembre de 2009", localize(self.d))
 853                self.assertEqual(
 854                    "31 de desembre de 2009 a les 20:50", localize(self.dt)
 855                )
 856
 857            with self.settings(USE_THOUSAND_SEPARATOR=True):
 858                self.assertEqual("66.666,666", Template("{{ n }}").render(self.ctxt))
 859                self.assertEqual("99.999,999", Template("{{ f }}").render(self.ctxt))
 860                self.assertEqual("10.000", Template("{{ l }}").render(self.ctxt))
 861
 862            with self.settings(USE_THOUSAND_SEPARATOR=True):
 863                form3 = I18nForm(
 864                    {
 865                        "decimal_field": "66.666,666",
 866                        "float_field": "99.999,999",
 867                        "date_field": "31/12/2009",
 868                        "datetime_field": "31/12/2009 20:50",
 869                        "time_field": "20:50",
 870                        "integer_field": "1.234",
 871                    }
 872                )
 873                self.assertTrue(form3.is_valid())
 874                self.assertEqual(
 875                    decimal.Decimal("66666.666"), form3.cleaned_data["decimal_field"]
 876                )
 877                self.assertEqual(99999.999, form3.cleaned_data["float_field"])
 878                self.assertEqual(
 879                    datetime.date(2009, 12, 31), form3.cleaned_data["date_field"]
 880                )
 881                self.assertEqual(
 882                    datetime.datetime(2009, 12, 31, 20, 50),
 883                    form3.cleaned_data["datetime_field"],
 884                )
 885                self.assertEqual(
 886                    datetime.time(20, 50), form3.cleaned_data["time_field"]
 887                )
 888                self.assertEqual(1234, form3.cleaned_data["integer_field"])
 889
 890            with self.settings(USE_THOUSAND_SEPARATOR=False):
 891                self.assertEqual("66666,666", Template("{{ n }}").render(self.ctxt))
 892                self.assertEqual("99999,999", Template("{{ f }}").render(self.ctxt))
 893                self.assertEqual(
 894                    "31 de desembre de 2009", Template("{{ d }}").render(self.ctxt)
 895                )
 896                self.assertEqual(
 897                    "31 de desembre de 2009 a les 20:50",
 898                    Template("{{ dt }}").render(self.ctxt),
 899                )
 900                self.assertEqual(
 901                    "66666,67", Template("{{ n|floatformat:2 }}").render(self.ctxt)
 902                )
 903                self.assertEqual(
 904                    "100000,0", Template("{{ f|floatformat }}").render(self.ctxt)
 905                )
 906                self.assertEqual(
 907                    "10:15", Template('{{ t|time:"TIME_FORMAT" }}').render(self.ctxt)
 908                )
 909                self.assertEqual(
 910                    "31/12/2009",
 911                    Template('{{ d|date:"SHORT_DATE_FORMAT" }}').render(self.ctxt),
 912                )
 913                self.assertEqual(
 914                    "31/12/2009 20:50",
 915                    Template('{{ dt|date:"SHORT_DATETIME_FORMAT" }}').render(self.ctxt),
 916                )
 917                self.assertEqual(
 918                    date_format(datetime.datetime.now(), "DATE_FORMAT"),
 919                    Template('{% now "DATE_FORMAT" %}').render(self.ctxt),
 920                )
 921
 922            with self.settings(USE_THOUSAND_SEPARATOR=False):
 923                form4 = I18nForm(
 924                    {
 925                        "decimal_field": "66666,666",
 926                        "float_field": "99999,999",
 927                        "date_field": "31/12/2009",
 928                        "datetime_field": "31/12/2009 20:50",
 929                        "time_field": "20:50",
 930                        "integer_field": "1234",
 931                    }
 932                )
 933                self.assertTrue(form4.is_valid())
 934                self.assertEqual(
 935                    decimal.Decimal("66666.666"), form4.cleaned_data["decimal_field"]
 936                )
 937                self.assertEqual(99999.999, form4.cleaned_data["float_field"])
 938                self.assertEqual(
 939                    datetime.date(2009, 12, 31), form4.cleaned_data["date_field"]
 940                )
 941                self.assertEqual(
 942                    datetime.datetime(2009, 12, 31, 20, 50),
 943                    form4.cleaned_data["datetime_field"],
 944                )
 945                self.assertEqual(
 946                    datetime.time(20, 50), form4.cleaned_data["time_field"]
 947                )
 948                self.assertEqual(1234, form4.cleaned_data["integer_field"])
 949
 950            form5 = SelectDateForm(
 951                {
 952                    "date_field_month": "12",
 953                    "date_field_day": "31",
 954                    "date_field_year": "2009",
 955                }
 956            )
 957            self.assertTrue(form5.is_valid())
 958            self.assertEqual(
 959                datetime.date(2009, 12, 31), form5.cleaned_data["date_field"]
 960            )
 961            self.assertHTMLEqual(
 962                '<select name="mydate_day" id="id_mydate_day">'
 963                '<option value="">---</option>'
 964                '<option value="1">1</option>'
 965                '<option value="2">2</option>'
 966                '<option value="3">3</option>'
 967                '<option value="4">4</option>'
 968                '<option value="5">5</option>'
 969                '<option value="6">6</option>'
 970                '<option value="7">7</option>'
 971                '<option value="8">8</option>'
 972                '<option value="9">9</option>'
 973                '<option value="10">10</option>'
 974                '<option value="11">11</option>'
 975                '<option value="12">12</option>'
 976                '<option value="13">13</option>'
 977                '<option value="14">14</option>'
 978                '<option value="15">15</option>'
 979                '<option value="16">16</option>'
 980                '<option value="17">17</option>'
 981                '<option value="18">18</option>'
 982                '<option value="19">19</option>'
 983                '<option value="20">20</option>'
 984                '<option value="21">21</option>'
 985                '<option value="22">22</option>'
 986                '<option value="23">23</option>'
 987                '<option value="24">24</option>'
 988                '<option value="25">25</option>'
 989                '<option value="26">26</option>'
 990                '<option value="27">27</option>'
 991                '<option value="28">28</option>'
 992                '<option value="29">29</option>'
 993                '<option value="30">30</option>'
 994                '<option value="31" selected>31</option>'
 995                "</select>"
 996                '<select name="mydate_month" id="id_mydate_month">'
 997                '<option value="">---</option>'
 998                '<option value="1">gener</option>'
 999                '<option value="2">febrer</option>'
1000                '<option value="3">mar\xe7</option>'
1001                '<option value="4">abril</option>'
1002                '<option value="5">maig</option>'
1003                '<option value="6">juny</option>'
1004                '<option value="7">juliol</option>'
1005                '<option value="8">agost</option>'
1006                '<option value="9">setembre</option>'
1007                '<option value="10">octubre</option>'
1008                '<option value="11">novembre</option>'
1009                '<option value="12" selected>desembre</option>'
1010                "</select>"
1011                '<select name="mydate_year" id="id_mydate_year">'
1012                '<option value="">---</option>'
1013                '<option value="2009" selected>2009</option>'
1014                '<option value="2010">2010</option>'
1015                '<option value="2011">2011</option>'
1016                '<option value="2012">2012</option>'
1017                '<option value="2013">2013</option>'
1018                '<option value="2014">2014</option>'
1019                '<option value="2015">2015</option>'
1020                '<option value="2016">2016</option>'
1021                '<option value="2017">2017</option>'
1022                '<option value="2018">2018</option>'
1023                "</select>",
1024                forms.SelectDateWidget(years=range(2009, 2019)).render(
1025                    "mydate", datetime.date(2009, 12, 31)
1026                ),
1027            )
1028
1029        # Russian locale (with E as month)
1030        with translation.override("ru", deactivate=True):
1031            self.assertHTMLEqual(
1032                '<select name="mydate_day" id="id_mydate_day">'
1033                '<option value="">---</option>'
1034                '<option value="1">1</option>'
1035                '<option value="2">2</option>'
1036                '<option value="3">3</option>'
1037                '<option value="4">4</option>'
1038                '<option value="5">5</option>'
1039                '<option value="6">6</option>'
1040                '<option value="7">7</option>'
1041                '<option value="8">8</option>'
1042                '<option value="9">9</option>'
1043                '<option value="10">10</option>'
1044                '<option value="11">11</option>'
1045                '<option value="12">12</option>'
1046                '<option value="13">13</option>'
1047                '<option value="14">14</option>'
1048                '<option value="15">15</option>'
1049                '<option value="16">16</option>'
1050                '<option value="17">17</option>'
1051                '<option value="18">18</option>'
1052                '<option value="19">19</option>'
1053                '<option value="20">20</option>'
1054                '<option value="21">21</option>'
1055                '<option value="22">22</option>'
1056                '<option value="23">23</option>'
1057                '<option value="24">24</option>'
1058                '<option value="25">25</option>'
1059                '<option value="26">26</option>'
1060                '<option value="27">27</option>'
1061                '<option value="28">28</option>'
1062                '<option value="29">29</option>'
1063                '<option value="30">30</option>'
1064                '<option value="31" selected>31</option>'
1065                "</select>"
1066                '<select name="mydate_month" id="id_mydate_month">'
1067                '<option value="">---</option>'
1068                '<option value="1">\u042f\u043d\u0432\u0430\u0440\u044c</option>'
1069                '<option value="2">\u0424\u0435\u0432\u0440\u0430\u043b\u044c</option>'
1070                '<option value="3">\u041c\u0430\u0440\u0442</option>'
1071                '<option value="4">\u0410\u043f\u0440\u0435\u043b\u044c</option>'
1072                '<option value="5">\u041c\u0430\u0439</option>'
1073                '<option value="6">\u0418\u044e\u043d\u044c</option>'
1074                '<option value="7">\u0418\u044e\u043b\u044c</option>'
1075                '<option value="8">\u0410\u0432\u0433\u0443\u0441\u0442</option>'
1076                '<option value="9">\u0421\u0435\u043d\u0442\u044f\u0431\u0440\u044c</option>'
1077                '<option value="10">\u041e\u043a\u0442\u044f\u0431\u0440\u044c</option>'
1078                '<option value="11">\u041d\u043e\u044f\u0431\u0440\u044c</option>'
1079                '<option value="12" selected>\u0414\u0435\u043a\u0430\u0431\u0440\u044c</option>'
1080                "</select>"
1081                '<select name="mydate_year" id="id_mydate_year">'
1082                '<option value="">---</option>'
1083                '<option value="2009" selected>2009</option>'
1084                '<option value="2010">2010</option>'
1085                '<option value="2011">2011</option>'
1086                '<option value="2012">2012</option>'
1087                '<option value="2013">2013</option>'
1088                '<option value="2014">2014</option>'
1089                '<option value="2015">2015</option>'
1090                '<option value="2016">2016</option>'
1091                '<option value="2017">2017</option>'
1092                '<option value="2018">2018</option>'
1093                "</select>",
1094                forms.SelectDateWidget(years=range(2009, 2019)).render(
1095                    "mydate", datetime.date(2009, 12, 31)
1096                ),
1097            )
1098
1099        # English locale
1100        with translation.override("en", deactivate=True):
1101            self.assertEqual("N j, Y", get_format("DATE_FORMAT"))
1102            self.assertEqual(0, get_format("FIRST_DAY_OF_WEEK"))
1103            self.assertEqual(".", get_format("DECIMAL_SEPARATOR"))
1104            self.assertEqual("Dec. 31, 2009", date_format(self.d))
1105            self.assertEqual("December 2009", date_format(self.d, "YEAR_MONTH_FORMAT"))
1106            self.assertEqual(
1107                "12/31/2009 8:50 p.m.", date_format(self.dt, "SHORT_DATETIME_FORMAT")
1108            )
1109            self.assertEqual("No localizable", localize("No localizable"))
1110
1111            with self.settings(USE_THOUSAND_SEPARATOR=True):
1112                self.assertEqual("66,666.666", localize(self.n))
1113                self.assertEqual("99,999.999", localize(self.f))
1114                self.assertEqual("10,000", localize(self.long))
1115
1116            with self.settings(USE_THOUSAND_SEPARATOR=False):
1117                self.assertEqual("66666.666", localize(self.n))
1118                self.assertEqual("99999.999", localize(self.f))
1119                self.assertEqual("10000", localize(self.long))
1120                self.assertEqual("Dec. 31, 2009", localize(self.d))
1121                self.assertEqual("Dec. 31, 2009, 8:50 p.m.", localize(self.dt))
1122
1123            with self.settings(USE_THOUSAND_SEPARATOR=True):
1124                self.assertEqual("66,666.666", Template("{{ n }}").render(self.ctxt))
1125                self.assertEqual("99,999.999", Template("{{ f }}").render(self.ctxt))
1126                self.assertEqual("10,000", Template("{{ l }}").render(self.ctxt))
1127
1128            with self.settings(USE_THOUSAND_SEPARATOR=False):
1129                self.assertEqual("66666.666", Template("{{ n }}").render(self.ctxt))
1130                self.assertEqual("99999.999", Template("{{ f }}").render(self.ctxt))
1131                self.assertEqual("Dec. 31, 2009", Template("{{ d }}").render(self.ctxt))
1132                self.assertEqual(
1133                    "Dec. 31, 2009, 8:50 p.m.", Template("{{ dt }}").render(self.ctxt)
1134                )
1135                self.assertEqual(
1136                    "66666.67", Template("{{ n|floatformat:2 }}").render(self.ctxt)
1137                )
1138                self.assertEqual(
1139                    "100000.0", Template("{{ f|floatformat }}").render(self.ctxt)
1140                )
1141                self.assertEqual(
1142                    "12/31/2009",
1143                    Template('{{ d|date:"SHORT_DATE_FORMAT" }}').render(self.ctxt),
1144                )
1145                self.assertEqual(
1146                    "12/31/2009 8:50 p.m.",
1147                    Template('{{ dt|date:"SHORT_DATETIME_FORMAT" }}').render(self.ctxt),
1148                )
1149
1150            form5 = I18nForm(
1151                {
1152                    "decimal_field": "66666.666",
1153                    "float_field": "99999.999",
1154                    "date_field": "12/31/2009",
1155                    "datetime_field": "12/31/2009 20:50",
1156                    "time_field": "20:50",
1157                    "integer_field": "1234",
1158                }
1159            )
1160            self.assertTrue(form5.is_valid())
1161            self.assertEqual(
1162                decimal.Decimal("66666.666"), form5.cleaned_data["decimal_field"]
1163            )
1164            self.assertEqual(99999.999, form5.cleaned_data["float_field"])
1165            self.assertEqual(
1166                datetime.date(2009, 12, 31), form5.cleaned_data["date_field"]
1167            )
1168            self.assertEqual(
1169                datetime.datetime(2009, 12, 31, 20, 50),
1170                form5.cleaned_data["datetime_field"],
1171            )
1172            self.assertEqual(datetime.time(20, 50), form5.cleaned_data["time_field"])
1173            self.assertEqual(1234, form5.cleaned_data["integer_field"])
1174
1175            form6 = SelectDateForm(
1176                {
1177                    "date_field_month": "12",
1178                    "date_field_day": "31",
1179                    "date_field_year": "2009",
1180                }
1181            )
1182            self.assertTrue(form6.is_valid())
1183            self.assertEqual(
1184                datetime.date(2009, 12, 31), form6.cleaned_data["date_field"]
1185            )
1186            self.assertHTMLEqual(
1187                '<select name="mydate_month" id="id_mydate_month">'
1188                '<option value="">---</option>'
1189                '<option value="1">January</option>'
1190                '<option value="2">February</option>'
1191                '<option value="3">March</option>'
1192                '<option value="4">April</option>'
1193                '<option value="5">May</option>'
1194                '<option value="6">June</option>'
1195                '<option value="7">July</option>'
1196                '<option value="8">August</option>'
1197                '<option value="9">September</option>'
1198                '<option value="10">October</option>'
1199                '<option value="11">November</option>'
1200                '<option value="12" selected>December</option>'
1201                "</select>"
1202                '<select name="mydate_day" id="id_mydate_day">'
1203                '<option value="">---</option>'
1204                '<option value="1">1</option>'
1205                '<option value="2">2</option>'
1206                '<option value="3">3</option>'
1207                '<option value="4">4</option>'
1208                '<option value="5">5</option>'
1209                '<option value="6">6</option>'
1210                '<option value="7">7</option>'
1211                '<option value="8">8</option>'
1212                '<option value="9">9</option>'
1213                '<option value="10">10</option>'
1214                '<option value="11">11</option>'
1215                '<option value="12">12</option>'
1216                '<option value="13">13</option>'
1217                '<option value="14">14</option>'
1218                '<option value="15">15</option>'
1219                '<option value="16">16</option>'
1220                '<option value="17">17</option>'
1221                '<option value="18">18</option>'
1222                '<option value="19">19</option>'
1223                '<option value="20">20</option>'
1224                '<option value="21">21</option>'
1225                '<option value="22">22</option>'
1226                '<option value="23">23</option>'
1227                '<option value="24">24</option>'
1228                '<option value="25">25</option>'
1229                '<option value="26">26</option>'
1230                '<option value="27">27</option>'
1231                '<option value="28">28</option>'
1232                '<option value="29">29</option>'
1233                '<option value="30">30</option>'
1234                '<option value="31" selected>31</option>'
1235                "</select>"
1236                '<select name="mydate_year" id="id_mydate_year">'
1237                '<option value="">---</option>'
1238                '<option value="2009" selected>2009</option>'
1239                '<option value="2010">2010</option>'
1240                '<option value="2011">2011</option>'
1241                '<option value="2012">2012</option>'
1242                '<option value="2013">2013</option>'
1243                '<option value="2014">2014</option>'
1244                '<option value="2015">2015</option>'
1245                '<option value="2016">2016</option>'
1246                '<option value="2017">2017</option>'
1247                '<option value="2018">2018</option>'
1248                "</select>",
1249                forms.SelectDateWidget(years=range(2009, 2019)).render(
1250                    "mydate", datetime.date(2009, 12, 31)
1251                ),
1252            )
1253
1254    def test_sub_locales(self):
1255        """
1256        Check if sublocales fall back to the main locale
1257        """
1258        with self.settings(USE_THOUSAND_SEPARATOR=True):
1259            with translation.override("de-at", deactivate=True):
1260                self.assertEqual("66.666,666", Template("{{ n }}").render(self.ctxt))
1261            with translation.override("es-us", deactivate=True):
1262                self.assertEqual("31 de Diciembre de 2009", date_format(self.d))
1263
1264    def test_localized_input(self):
1265        """
1266        Tests if form input is correctly localized
1267        """
1268        self.maxDiff = 1200
1269        with translation.override("de-at", deactivate=True):
1270            form6 = CompanyForm(
1271                {
1272                    "name": "acme",
1273                    "date_added": datetime.datetime(2009, 12, 31, 6, 0, 0),
1274                    "cents_paid": decimal.Decimal("59.47"),
1275                    "products_delivered": 12000,
1276                }
1277            )
1278            self.assertTrue(form6.is_valid())
1279            self.assertHTMLEqual(
1280                form6.as_ul(),
1281                '<li><label for="id_name">Name:</label>'
1282                '<input id="id_name" type="text" name="name" value="acme" maxlength="50" required></li>'
1283                '<li><label for="id_date_added">Date added:</label>'
1284                '<input type="text" name="date_added" value="31.12.2009 06:00:00" id="id_date_added" required></li>'
1285                '<li><label for="id_cents_paid">Cents paid:</label>'
1286                '<input type="text" name="cents_paid" value="59,47" id="id_cents_paid" required></li>'
1287                '<li><label for="id_products_delivered">Products delivered:</label>'
1288                '<input type="text" name="products_delivered" value="12000" id="id_products_delivered" required>'
1289                "</li>",
1290            )
1291            self.assertEqual(
1292                localize_input(datetime.datetime(2009, 12, 31, 6, 0, 0)),
1293                "31.12.2009 06:00:00",
1294            )
1295            self.assertEqual(
1296                datetime.datetime(2009, 12, 31, 6, 0, 0),
1297                form6.cleaned_data["date_added"],
1298            )
1299            with self.settings(USE_THOUSAND_SEPARATOR=True):
1300                # Checking for the localized "products_delivered" field
1301                self.assertInHTML(
1302                    '<input type="text" name="products_delivered" '
1303                    'value="12.000" id="id_products_delivered" required>',
1304                    form6.as_ul(),
1305                )
1306
1307    def test_localized_input_func(self):
1308        tests = (
1309            (True, "True"),
1310            (datetime.date(1, 1, 1), "0001-01-01"),
1311            (datetime.datetime(1, 1, 1), "0001-01-01 00:00:00"),
1312        )
1313        with self.settings(USE_THOUSAND_SEPARATOR=True):
1314            for value, expected in tests:
1315                with self.subTest(value=value):
1316                    self.assertEqual(localize_input(value), expected)
1317
1318    def test_sanitize_separators(self):
1319        """
1320        Tests django.utils.formats.sanitize_separators.
1321        """
1322        # Non-strings are untouched
1323        self.assertEqual(sanitize_separators(123), 123)
1324
1325        with translation.override("ru", deactivate=True):
1326            # Russian locale has non-breaking space (\xa0) as thousand separator
1327            # Usual space is accepted too when sanitizing inputs
1328            with self.settings(USE_THOUSAND_SEPARATOR=True):
1329                self.assertEqual(sanitize_separators("1\xa0234\xa0567"), "1234567")
1330                self.assertEqual(sanitize_separators("77\xa0777,777"), "77777.777")
1331                self.assertEqual(sanitize_separators("12 345"), "12345")
1332                self.assertEqual(sanitize_separators("77 777,777"), "77777.777")
1333            with self.settings(USE_THOUSAND_SEPARATOR=True, USE_L10N=False):
1334                self.assertEqual(sanitize_separators("12\xa0345"), "12\xa0345")
1335
1336        with self.settings(USE_THOUSAND_SEPARATOR=True):
1337            with patch_formats(
1338                get_language(), THOUSAND_SEPARATOR=".", DECIMAL_SEPARATOR=","
1339            ):
1340                self.assertEqual(sanitize_separators("10.234"), "10234")
1341                # Suspicion that user entered dot as decimal separator (#22171)
1342                self.assertEqual(sanitize_separators("10.10"), "10.10")
1343
1344        with self.settings(USE_L10N=False, DECIMAL_SEPARATOR=","):
1345            self.assertEqual(sanitize_separators("1001,10"), "1001.10")
1346            self.assertEqual(sanitize_separators("1001.10"), "1001.10")
1347
1348        with self.settings(
1349            USE_L10N=False,
1350            DECIMAL_SEPARATOR=",",
1351            USE_THOUSAND_SEPARATOR=True,
1352            THOUSAND_SEPARATOR=".",
1353        ):
1354            self.assertEqual(sanitize_separators("1.001,10"), "1001.10")
1355            self.assertEqual(sanitize_separators("1001,10"), "1001.10")
1356            self.assertEqual(sanitize_separators("1001.10"), "1001.10")
1357            self.assertEqual(
1358                sanitize_separators("1,001.10"), "1.001.10"
1359            )  # Invalid output
1360
1361    def test_iter_format_modules(self):
1362        """
1363        Tests the iter_format_modules function.
1364        """
1365        # Importing some format modules so that we can compare the returned
1366        # modules with these expected modules
1367        default_mod = import_module("django.conf.locale.de.formats")
1368        test_mod = import_module("i18n.other.locale.de.formats")
1369        test_mod2 = import_module("i18n.other2.locale.de.formats")
1370
1371        with translation.override("de-at", deactivate=True):
1372            # Should return the correct default module when no setting is set
1373            self.assertEqual(list(iter_format_modules("de")), [default_mod])
1374
1375            # When the setting is a string, should return the given module and
1376            # the default module
1377            self.assertEqual(
1378                list(iter_format_modules("de", "i18n.other.locale")),
1379                [test_mod, default_mod],
1380            )
1381
1382            # When setting is a list of strings, should return the given
1383            # modules and the default module
1384            self.assertEqual(
1385                list(
1386                    iter_format_modules(
1387                        "de", ["i18n.other.locale", "i18n.other2.locale"]
1388                    )
1389                ),
1390                [test_mod, test_mod2, default_mod],
1391            )
1392
1393    def test_iter_format_modules_stability(self):
1394        """
1395        Tests the iter_format_modules function always yields format modules in
1396        a stable and correct order in presence of both base ll and ll_CC formats.
1397        """
1398        en_format_mod = import_module("django.conf.locale.en.formats")
1399        en_gb_format_mod = import_module("django.conf.locale.en_GB.formats")
1400        self.assertEqual(
1401            list(iter_format_modules("en-gb")), [en_gb_format_mod, en_format_mod]
1402        )
1403
1404    def test_get_format_modules_lang(self):
1405        with translation.override("de", deactivate=True):
1406            self.assertEqual(".", get_format("DECIMAL_SEPARATOR", lang="en"))
1407
1408    def test_get_format_modules_stability(self):
1409        with self.settings(FORMAT_MODULE_PATH="i18n.other.locale"):
1410            with translation.override("de", deactivate=True):
1411                old = "%r" % get_format_modules(reverse=True)
1412                new = "%r" % get_format_modules(reverse=True)  # second try
1413                self.assertEqual(
1414                    new,
1415                    old,
1416                    "Value returned by get_formats_modules() must be preserved between calls.",
1417                )
1418
1419    def test_localize_templatetag_and_filter(self):
1420        """
1421        Test the {% localize %} templatetag and the localize/unlocalize filters.
1422        """
1423        context = Context(
1424            {"int": 1455, "float": 3.14, "date": datetime.date(2016, 12, 31)}
1425        )
1426        template1 = Template(
1427            "{% load l10n %}{% localize %}{{ int }}/{{ float }}/{{ date }}{% endlocalize %}; "
1428            "{% localize on %}{{ int }}/{{ float }}/{{ date }}{% endlocalize %}"
1429        )
1430        template2 = Template(
1431            "{% load l10n %}{{ int }}/{{ float }}/{{ date }}; "
1432            "{% localize off %}{{ int }}/{{ float }}/{{ date }};{% endlocalize %} "
1433            "{{ int }}/{{ float }}/{{ date }}"
1434        )
1435        template3 = Template(
1436            "{% load l10n %}{{ int }}/{{ float }}/{{ date }}; "
1437            "{{ int|unlocalize }}/{{ float|unlocalize }}/{{ date|unlocalize }}"
1438        )
1439        template4 = Template(
1440            "{% load l10n %}{{ int }}/{{ float }}/{{ date }}; "
1441            "{{ int|localize }}/{{ float|localize }}/{{ date|localize }}"
1442        )
1443        expected_localized = "1.455/3,14/31. Dezember 2016"
1444        expected_unlocalized = "1455/3.14/Dez. 31, 2016"
1445        output1 = "; ".join([expected_localized, expected_localized])
1446        output2 = "; ".join(
1447            [expected_localized, expected_unlocalized, expected_localized]
1448        )
1449        output3 = "; ".join([expected_localized, expected_unlocalized])
1450        output4 = "; ".join([expected_unlocalized, expected_localized])
1451        with translation.override("de", deactivate=True):
1452            with self.settings(USE_L10N=False, USE_THOUSAND_SEPARATOR=True):
1453                self.assertEqual(template1.render(context), output1)
1454                self.assertEqual(template4.render(context), output4)
1455            with self.settings(USE_L10N=True, USE_THOUSAND_SEPARATOR=True):
1456                self.assertEqual(template1.render(context), output1)
1457                self.assertEqual(template2.render(context), output2)
1458                self.assertEqual(template3.render(context), output3)
1459
1460    def test_localized_as_text_as_hidden_input(self):
1461        """
1462        Tests if form input with 'as_hidden' or 'as_text' is correctly localized. Ticket #18777
1463        """
1464        self.maxDiff = 1200
1465
1466        with translation.override("de-at", deactivate=True):
1467            template = Template(
1468                "{% load l10n %}{{ form.date_added }}; {{ form.cents_paid }}"
1469            )
1470            template_as_text = Template(
1471                "{% load l10n %}{{ form.date_added.as_text }}; {{ form.cents_paid.as_text }}"
1472            )
1473            template_as_hidden = Template(
1474                "{% load l10n %}{{ form.date_added.as_hidden }}; {{ form.cents_paid.as_hidden }}"
1475            )
1476            form = CompanyForm(
1477                {
1478                    "name": "acme",
1479                    "date_added": datetime.datetime(2009, 12, 31, 6, 0, 0),
1480                    "cents_paid": decimal.Decimal("59.47"),
1481                    "products_delivered": 12000,
1482                }
1483            )
1484            context = Context({"form": form})
1485            self.assertTrue(form.is_valid())
1486
1487            self.assertHTMLEqual(
1488                template.render(context),
1489                '<input id="id_date_added" name="date_added" type="text" value="31.12.2009 06:00:00" required>;'
1490                '<input id="id_cents_paid" name="cents_paid" type="text" value="59,47" required>',
1491            )
1492            self.assertHTMLEqual(
1493                template_as_text.render(context),
1494                '<input id="id_date_added" name="date_added" type="text" value="31.12.2009 06:00:00" required>;'
1495                ' <input id="id_cents_paid" name="cents_paid" type="text" value="59,47" required>',
1496            )
1497            self.assertHTMLEqual(
1498                template_as_hidden.render(context),
1499                '<input id="id_date_added" name="date_added" type="hidden" value="31.12.2009 06:00:00">;'
1500                '<input id="id_cents_paid" name="cents_paid" type="hidden" value="59,47">',
1501            )
1502
1503    def test_format_arbitrary_settings(self):
1504        self.assertEqual(get_format("DEBUG"), "DEBUG")
1505
1506    def test_get_custom_format(self):
1507        with self.settings(FORMAT_MODULE_PATH="i18n.other.locale"):
1508            with translation.override("fr", deactivate=True):
1509                self.assertEqual("d/m/Y CUSTOM", get_format("CUSTOM_DAY_FORMAT"))
1510
1511    def test_admin_javascript_supported_input_formats(self):
1512        """
1513        The first input format for DATE_INPUT_FORMATS, TIME_INPUT_FORMATS, and
1514        DATETIME_INPUT_FORMATS must not contain %f since that's unsupported by
1515        the admin's time picker widget.
1516        """
1517        regex = re.compile("%([^BcdHImMpSwxXyY%])")
1518        for language_code, language_name in settings.LANGUAGES:
1519            for format_name in (
1520                "DATE_INPUT_FORMATS",
1521                "TIME_INPUT_FORMATS",
1522                "DATETIME_INPUT_FORMATS",
1523            ):
1524                with self.subTest(language=language_code, format=format_name):
1525                    formatter = get_format(format_name, lang=language_code)[0]
1526                    self.assertEqual(
1527                        regex.findall(formatter),
1528                        [],
1529                        "%s locale's %s uses an unsupported format code."
1530                        % (language_code, format_name),
1531                    )
1532
1533
1534class MiscTests(SimpleTestCase):
1535    rf = RequestFactory()
1536
1537    @override_settings(LANGUAGE_CODE="de")
1538    def test_english_fallback(self):
1539        """
1540        With a non-English LANGUAGE_CODE and if the active language is English
1541        or one of its variants, the untranslated string should be returned
1542        (instead of falling back to LANGUAGE_CODE) (See #24413).
1543        """
1544        self.assertEqual(gettext("Image"), "Bild")
1545        with translation.override("en"):
1546            self.assertEqual(gettext("Image"), "Image")
1547        with translation.override("en-us"):
1548            self.assertEqual(gettext("Image"), "Image")
1549        with translation.override("en-ca"):
1550            self.assertEqual(gettext("Image"), "Image")
1551
1552    def test_parse_spec_http_header(self):
1553        """
1554        Testing HTTP header parsing. First, we test that we can parse the
1555        values according to the spec (and that we extract all the pieces in
1556        the right order).
1557        """
1558        tests = [
1559            # Good headers
1560            ("de", [("de", 1.0)]),
1561            ("en-AU", [("en-au", 1.0)]),
1562            ("es-419", [("es-419", 1.0)]),
1563            ("*;q=1.00", [("*", 1.0)]),
1564            ("en-AU;q=0.123", [("en-au", 0.123)]),
1565            ("en-au;q=0.5", [("en-au", 0.5)]),
1566            ("en-au;q=1.0", [("en-au", 1.0)]),
1567            ("da, en-gb;q=0.25, en;q=0.5", [("da", 1.0), ("en", 0.5), ("en-gb", 0.25)]),
1568            ("en-au-xx", [("en-au-xx", 1.0)]),
1569            (
1570                "de,en-au;q=0.75,en-us;q=0.5,en;q=0.25,es;q=0.125,fa;q=0.125",
1571                [
1572                    ("de", 1.0),
1573                    ("en-au", 0.75),
1574                    ("en-us", 0.5),
1575                    ("en", 0.25),
1576                    ("es", 0.125),
1577                    ("fa", 0.125),
1578                ],
1579            ),
1580            ("*", [("*", 1.0)]),
1581            ("de;q=0.", [("de", 0.0)]),
1582            ("en; q=1,", [("en", 1.0)]),
1583            ("en; q=1.0, * ; q=0.5", [("en", 1.0), ("*", 0.5)]),
1584            # Bad headers
1585            ("en-gb;q=1.0000", []),
1586            ("en;q=0.1234", []),
1587            ("en;q=.2", []),
1588            ("abcdefghi-au", []),
1589            ("**", []),
1590            ("en,,gb", []),
1591            ("en-au;q=0.1.0", []),
1592            (("X" * 97) + "Z,en", []),
1593            ("da, en-gb;q=0.8, en;q=0.7,#", []),
1594            ("de;q=2.0", []),
1595            ("de;q=0.a", []),
1596            ("12-345", []),
1597            ("", []),
1598            ("en;q=1e0", []),
1599        ]
1600        for value, expected in tests:
1601            with self.subTest(value=value):
1602                self.assertEqual(
1603                    trans_real.parse_accept_lang_header(value), tuple(expected)
1604                )
1605
1606    def test_parse_literal_http_header(self):
1607        """
1608        Now test that we parse a literal HTTP header correctly.
1609        """
1610        g = get_language_from_request
1611        r = self.rf.get("/")
1612        r.COOKIES = {}
1613        r.META = {"HTTP_ACCEPT_LANGUAGE": "pt-br"}
1614        self.assertEqual("pt-br", g(r))
1615
1616        r.META = {"HTTP_ACCEPT_LANGUAGE": "pt"}
1617        self.assertEqual("pt", g(r))
1618
1619        r.META = {"HTTP_ACCEPT_LANGUAGE": "es,de"}
1620        self.assertEqual("es", g(r))
1621
1622        r.META = {"HTTP_ACCEPT_LANGUAGE": "es-ar,de"}
1623        self.assertEqual("es-ar", g(r))
1624
1625        # This test assumes there won't be a Django translation to a US
1626        # variation of the Spanish language, a safe assumption. When the
1627        # user sets it as the preferred language, the main 'es'
1628        # translation should be selected instead.
1629        r.META = {"HTTP_ACCEPT_LANGUAGE": "es-us"}
1630        self.assertEqual(g(r), "es")
1631
1632        # This tests the following scenario: there isn't a main language (zh)
1633        # translation of Django but there is a translation to variation (zh-hans)
1634        # the user sets zh-hans as the preferred language, it should be selected
1635        # by Django without falling back nor ignoring it.
1636        r.META = {"HTTP_ACCEPT_LANGUAGE": "zh-hans,de"}
1637        self.assertEqual(g(r), "zh-hans")
1638
1639        r.META = {"HTTP_ACCEPT_LANGUAGE": "NL"}
1640        self.assertEqual("nl", g(r))
1641
1642        r.META = {"HTTP_ACCEPT_LANGUAGE": "fy"}
1643        self.assertEqual("fy", g(r))
1644
1645        r.META = {"HTTP_ACCEPT_LANGUAGE": "ia"}
1646        self.assertEqual("ia", g(r))
1647
1648        r.META = {"HTTP_ACCEPT_LANGUAGE": "sr-latn"}
1649        self.assertEqual("sr-latn", g(r))
1650
1651        r.META = {"HTTP_ACCEPT_LANGUAGE": "zh-hans"}
1652        self.assertEqual("zh-hans", g(r))
1653
1654        r.META = {"HTTP_ACCEPT_LANGUAGE": "zh-hant"}
1655        self.assertEqual("zh-hant", g(r))
1656
1657    @override_settings(
1658        LANGUAGES=[
1659            ("en", "English"),
1660            ("zh-hans", "Simplified Chinese"),
1661            ("zh-hant", "Traditional Chinese"),
1662        ]
1663    )
1664    def test_support_for_deprecated_chinese_language_codes(self):
1665        """
1666        Some browsers (Firefox, IE, etc.) use deprecated language codes. As these
1667        language codes will be removed in Django 1.9, these will be incorrectly
1668        matched. For example zh-tw (traditional) will be interpreted as zh-hans
1669        (simplified), which is wrong. So we should also accept these deprecated
1670        language codes.
1671
1672        refs #18419 -- this is explicitly for browser compatibility
1673        """
1674        g = get_language_from_request
1675        r = self.rf.get("/")
1676        r.COOKIES = {}
1677        r.META = {"HTTP_ACCEPT_LANGUAGE": "zh-cn,en"}
1678        self.assertEqual(g(r), "zh-hans")
1679
1680        r.META = {"HTTP_ACCEPT_LANGUAGE": "zh-tw,en"}
1681        self.assertEqual(g(r), "zh-hant")
1682
1683    def test_special_fallback_language(self):
1684        """
1685        Some languages may have special fallbacks that don't follow the simple
1686        'fr-ca' -> 'fr' logic (notably Chinese codes).
1687        """
1688        r = self.rf.get("/")
1689        r.COOKIES = {}
1690        r.META = {"HTTP_ACCEPT_LANGUAGE": "zh-my,en"}
1691        self.assertEqual(get_language_from_request(r), "zh-hans")
1692
1693    def test_parse_language_cookie(self):
1694        """
1695        Now test that we parse language preferences stored in a cookie correctly.
1696        """
1697        g = get_language_from_request
1698        r = self.rf.get("/")
1699        r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: "pt-br"}
1700        r.META = {}
1701        self.assertEqual("pt-br", g(r))
1702
1703        r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: "pt"}
1704        r.META = {}
1705        self.assertEqual("pt", g(r))
1706
1707        r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: "es"}
1708        r.META = {"HTTP_ACCEPT_LANGUAGE": "de"}
1709        self.assertEqual("es", g(r))
1710
1711        # This test assumes there won't be a Django translation to a US
1712        # variation of the Spanish language, a safe assumption. When the
1713        # user sets it as the preferred language, the main 'es'
1714        # translation should be selected instead.
1715        r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: "es-us"}
1716        r.META = {}
1717        self.assertEqual(g(r), "es")
1718
1719        # This tests the following scenario: there isn't a main language (zh)
1720        # translation of Django but there is a translation to variation (zh-hans)
1721        # the user sets zh-hans as the preferred language, it should be selected
1722        # by Django without falling back nor ignoring it.
1723        r.COOKIES = {settings.LANGUAGE_COOKIE_NAME: "zh-hans"}
1724        r.META = {"HTTP_ACCEPT_LANGUAGE": "de"}
1725        self.assertEqual(g(r), "zh-hans")
1726
1727    @override_settings(
1728        USE_I18N=True,
1729        LANGUAGES=[
1730            ("en", "English"),
1731            ("de", "German"),
1732            ("de-at", "Austrian German"),
1733            ("pt-br", "Portuguese (Brazil)"),
1734        ],
1735    )
1736    def test_get_supported_language_variant_real(self):
1737        g = trans_real.get_supported_language_variant
1738        self.assertEqual(g("en"), "en")
1739        self.assertEqual(g("en-gb"), "en")
1740        self.assertEqual(g("de"), "de")
1741        self.assertEqual(g("de-at"), "de-at")
1742        self.assertEqual(g("de-ch"), "de")
1743        self.assertEqual(g("pt-br"), "pt-br")
1744        self.assertEqual(g("pt"), "pt-br")
1745        self.assertEqual(g("pt-pt"), "pt-br")
1746        with self.assertRaises(LookupError):
1747            g("pt", strict=True)
1748        with self.assertRaises(LookupError):
1749            g("pt-pt", strict=True)
1750        with self.assertRaises(LookupError):
1751            g("xyz")
1752        with self.assertRaises(LookupError):
1753            g("xy-zz")
1754
1755    def test_get_supported_language_variant_null(self):
1756        g = trans_null.get_supported_language_variant
1757        self.assertEqual(g(settings.LANGUAGE_CODE), settings.LANGUAGE_CODE)
1758        with self.assertRaises(LookupError):
1759            g("pt")
1760        with self.assertRaises(LookupError):
1761            g("de")
1762        with self.assertRaises(LookupError):
1763            g("de-at")
1764        with self.assertRaises(LookupError):
1765            g("de", strict=True)
1766        with self.assertRaises(LookupError):
1767            g("de-at", strict=True)
1768        with self.assertRaises(LookupError):
1769            g("xyz")
1770
1771    @override_settings(
1772        LANGUAGES=[
1773            ("en", "English"),
1774            ("de", "German"),
1775            ("de-at", "Austrian German"),
1776            ("pl", "Polish"),
1777        ],
1778    )
1779    def test_get_language_from_path_real(self):
1780        g = trans_real.get_language_from_path
1781        self.assertEqual(g("/pl/"), "pl")
1782        self.assertEqual(g("/pl"), "pl")
1783        self.assertIsNone(g("/xyz/"))
1784        self.assertEqual(g("/en/"), "en")
1785        self.assertEqual(g("/en-gb/"), "en")
1786        self.assertEqual(g("/de/"), "de")
1787        self.assertEqual(g("/de-at/"), "de-at")
1788        self.assertEqual(g("/de-ch/"), "de")
1789        self.assertIsNone(g("/de-simple-page/"))
1790
1791    def test_get_language_from_path_null(self):
1792        g = trans_null.get_language_from_path
1793        self.assertIsNone(g("/pl/"))
1794        self.assertIsNone(g("/pl"))
1795        self.assertIsNone(g("/xyz/"))
1796
1797    def test_cache_resetting(self):
1798        """
1799        After setting LANGUAGE, the cache should be cleared and languages
1800        previously valid should not be used (#14170).
1801        """
1802        g = get_language_from_request
1803        r = self.rf.get("/")
1804        r.COOKIES = {}
1805        r.META = {"HTTP_ACCEPT_LANGUAGE": "pt-br"}
1806        self.assertEqual("pt-br", g(r))
1807        with self.settings(LANGUAGES=[("en", "English")]):
1808            self.assertNotEqual("pt-br", g(r))
1809
1810    def test_i18n_patterns_returns_list(self):
1811        with override_settings(USE_I18N=False):
1812            self.assertIsInstance(i18n_patterns([]), list)
1813        with override_settings(USE_I18N=True):
1814            self.assertIsInstance(i18n_patterns([]), list)
1815
1816
1817class ResolutionOrderI18NTests(SimpleTestCase):
1818    def setUp(self):
1819        super().setUp()
1820        activate("de")
1821
1822    def tearDown(self):
1823        deactivate()
1824        super().tearDown()
1825
1826    def assertGettext(self, msgid, msgstr):
1827        result = gettext(msgid)
1828        self.assertIn(
1829            msgstr,
1830            result,
1831            "The string '%s' isn't in the translation of '%s'; the actual result is '%s'."
1832            % (msgstr, msgid, result),
1833        )
1834
1835
1836class AppResolutionOrderI18NTests(ResolutionOrderI18NTests):
1837    @override_settings(LANGUAGE_CODE="de")
1838    def test_app_translation(self):
1839        # Original translation.
1840        self.assertGettext("Date/time", "Datum/Zeit")
1841
1842        # Different translation.
1843        with self.modify_settings(INSTALLED_APPS={"append": "i18n.resolution"}):
1844            # Force refreshing translations.
1845            activate("de")
1846
1847            # Doesn't work because it's added later in the list.
1848            self.assertGettext("Date/time", "Datum/Zeit")
1849
1850            with self.modify_settings(
1851                INSTALLED_APPS={"remove": "django.contrib.admin.apps.SimpleAdminConfig"}
1852            ):
1853                # Force refreshing translations.
1854                activate("de")
1855
1856                # Unless the original is removed from the list.
1857                self.assertGettext("Date/time", "Datum/Zeit (APP)")
1858
1859
1860@override_settings(LOCALE_PATHS=extended_locale_paths)
1861class LocalePathsResolutionOrderI18NTests(ResolutionOrderI18NTests):
1862    def test_locale_paths_translation(self):
1863        self.assertGettext("Time", "LOCALE_PATHS")
1864
1865    def test_locale_paths_override_app_translation(self):
1866        with self.settings(INSTALLED_APPS=["i18n.resolution"]):
1867            self.assertGettext("Time", "LOCALE_PATHS")
1868
1869
1870class DjangoFallbackResolutionOrderI18NTests(ResolutionOrderI18NTests):
1871    def test_django_fallback(self):
1872        self.assertEqual(gettext("Date/time"), "Datum/Zeit")
1873
1874
1875@override_settings(INSTALLED_APPS=["i18n.territorial_fallback"])
1876class TranslationFallbackI18NTests(ResolutionOrderI18NTests):
1877    def test_sparse_territory_catalog(self):
1878        """
1879        Untranslated strings for territorial language variants use the
1880        translations of the generic language. In this case, the de-de
1881        translation falls back to de.
1882        """
1883        with translation.override("de-de"):
1884            self.assertGettext("Test 1 (en)", "(de-de)")
1885            self.assertGettext("Test 2 (en)", "(de)")
1886
1887
1888class TestModels(TestCase):
1889    def test_lazy(self):
1890        tm = TestModel()
1891        tm.save()
1892
1893    def test_safestr(self):
1894        c = Company(cents_paid=12, products_delivered=1)
1895        c.name = SafeString("Iñtërnâtiônàlizætiøn1")
1896        c.save()
1897
1898
1899class TestLanguageInfo(SimpleTestCase):
1900    def test_localized_language_info(self):
1901        li = get_language_info("de")
1902        self.assertEqual(li["code"], "de")
1903        self.assertEqual(li["name_local"], "Deutsch")
1904        self.assertEqual(li["name"], "German")
1905        self.assertIs(li["bidi"], False)
1906
1907    def test_unknown_language_code(self):
1908        with self.assertRaisesMessage(KeyError, "Unknown language code xx"):
1909            get_language_info("xx")
1910        with translation.override("xx"):
1911            # A language with no translation catalogs should fallback to the
1912            # untranslated string.
1913            self.assertEqual(gettext("Title"), "Title")
1914
1915    def test_unknown_only_country_code(self):
1916        li = get_language_info("de-xx")
1917        self.assertEqual(li["code"], "de")
1918        self.assertEqual(li["name_local"], "Deutsch")
1919        self.assertEqual(li["name"], "German")
1920        self.assertIs(li["bidi"], False)
1921
1922    def test_unknown_language_code_and_country_code(self):
1923        with self.assertRaisesMessage(KeyError, "Unknown language code xx-xx and xx"):
1924            get_language_info("xx-xx")
1925
1926    def test_fallback_language_code(self):
1927        """
1928        get_language_info return the first fallback language info if the lang_info
1929        struct does not contain the 'name' key.
1930        """
1931        li = get_language_info("zh-my")
1932        self.assertEqual(li["code"], "zh-hans")
1933        li = get_language_info("zh-hans")
1934        self.assertEqual(li["code"], "zh-hans")
1935
1936
1937@override_settings(
1938    USE_I18N=True,
1939    LANGUAGES=[("en", "English"), ("fr", "French"),],
1940    MIDDLEWARE=[
1941        "django.middleware.locale.LocaleMiddleware",
1942        "django.middleware.common.CommonMiddleware",
1943    ],
1944    ROOT_URLCONF="i18n.urls",
1945)
1946class LocaleMiddlewareTests(TestCase):
1947    def test_streaming_response(self):
1948        # Regression test for #5241
1949        response = self.client.get("/fr/streaming/")
1950        self.assertContains(response, "Oui/Non")
1951        response = self.client.get("/en/streaming/")
1952        self.assertContains(response, "Yes/No")
1953
1954    @override_settings(
1955        MIDDLEWARE=[
1956            "django.contrib.sessions.middleware.SessionMiddleware",
1957            "django.middleware.locale.LocaleMiddleware",
1958            "django.middleware.common.CommonMiddleware",
1959        ],
1960    )
1961    def test_language_not_saved_to_session(self):
1962        """
1963        The Current language isno' automatically saved to the session on every
1964        request (#21473).
1965        """
1966        self.client.get("/fr/simple/")
1967        self.assertNotIn(LANGUAGE_SESSION_KEY, self.client.session)
1968
1969
1970@override_settings(
1971    USE_I18N=True,
1972    LANGUAGES=[("en", "English"), ("de", "German"), ("fr", "French"),],
1973    MIDDLEWARE=[
1974        "django.middleware.locale.LocaleMiddleware",
1975        "django.middleware.common.CommonMiddleware",
1976    ],
1977    ROOT_URLCONF="i18n.urls_default_unprefixed",
1978    LANGUAGE_CODE="en",
1979)
1980class UnprefixedDefaultLanguageTests(SimpleTestCase):
1981    def test_default_lang_without_prefix(self):
1982        """
1983        With i18n_patterns(..., prefix_default_language=False), the default
1984        language (settings.LANGUAGE_CODE) should be accessible without a prefix.
1985        """
1986        response = self.client.get("/simple/")
1987        self.assertEqual(response.content, b"Yes")
1988
1989    def test_other_lang_with_prefix(self):
1990        response = self.client.get("/fr/simple/")
1991        self.assertEqual(response.content, b"Oui")
1992
1993    def test_unprefixed_language_other_than_accept_language(self):
1994        response = self.client.get("/simple/", HTTP_ACCEPT_LANGUAGE="fr")
1995        self.assertEqual(response.content, b"Yes")
1996
1997    def test_page_with_dash(self):
1998        # A page starting with /de* shouldn't match the 'de' language code.
1999        response = self.client.get("/de-simple-page/")
2000        self.assertEqual(response.content, b"Yes")
2001
2002    def test_no_redirect_on_404(self):
2003        """
2004        A request for a nonexistent URL shouldn't cause a redirect to
2005        /<default_language>/<request_url> when prefix_default_language=False and
2006        /<default_language>/<request_url> has a URL match (#27402).
2007        """
2008        # A match for /group1/group2/ must exist for this to act as a
2009        # regression test.
2010        response = self.client.get("/group1/group2/")
2011        self.assertEqual(response.status_code, 200)
2012
2013        response = self.client.get("/nonexistent/")
2014        self.assertEqual(response.status_code, 404)
2015
2016
2017@override_settings(
2018    USE_I18N=True,
2019    LANGUAGES=[
2020        ("bg", "Bulgarian"),
2021        ("en-us", "English"),
2022        ("pt-br", "Portuguese (Brazil)"),
2023    ],
2024    MIDDLEWARE=[
2025        "django.middleware.locale.LocaleMiddleware",
2026        "django.middleware.common.CommonMiddleware",
2027    ],
2028    ROOT_URLCONF="i18n.urls",
2029)
2030class CountrySpecificLanguageTests(SimpleTestCase):
2031    rf = RequestFactory()
2032
2033    def test_check_for_language(self):
2034        self.assertTrue(check_for_language("en"))
2035        self.assertTrue(check_for_language("en-us"))
2036        self.assertTrue(check_for_language("en-US"))
2037        self.assertFalse(check_for_language("en_US"))
2038        self.assertTrue(check_for_language("be"))
2039        self.assertTrue(check_for_language("be@latin"))
2040        self.assertTrue(check_for_language("sr-RS@latin"))
2041        self.assertTrue(check_for_language("sr-RS@12345"))
2042        self.assertFalse(check_for_language("en-ü"))
2043        self.assertFalse(check_for_language("en\x00"))
2044        self.assertFalse(check_for_language(None))
2045        self.assertFalse(check_for_language("be@ "))
2046        # Specifying encoding is not supported (Django enforces UTF-8)
2047        self.assertFalse(check_for_language("tr-TR.UTF-8"))
2048        self.assertFalse(check_for_language("tr-TR.UTF8"))
2049        self.assertFalse(check_for_language("de-DE.utf-8"))
2050
2051    def test_check_for_language_null(self):
2052        self.assertIs(trans_null.check_for_language("en"), True)
2053
2054    def test_get_language_from_request(self):
2055        # issue 19919
2056        r = self.rf.get("/")
2057        r.COOKIES = {}
2058        r.META = {"HTTP_ACCEPT_LANGUAGE": "en-US,en;q=0.8,bg;q=0.6,ru;q=0.4"}
2059        lang = get_language_from_request(r)
2060        self.assertEqual("en-us", lang)
2061        r = self.rf.get("/")
2062        r.COOKIES = {}
2063        r.META = {"HTTP_ACCEPT_LANGUAGE": "bg-bg,en-US;q=0.8,en;q=0.6,ru;q=0.4"}
2064        lang = get_language_from_request(r)
2065        self.assertEqual("bg", lang)
2066
2067    def test_get_language_from_request_null(self):
2068        lang = trans_null.get_language_from_request(None)
2069        self.assertEqual(lang, "en")
2070        with override_settings(LANGUAGE_CODE="de"):
2071            lang = trans_null.get_language_from_request(None)
2072            self.assertEqual(lang, "de")
2073
2074    def test_specific_language_codes(self):
2075        # issue 11915
2076        r = self.rf.get("/")
2077        r.COOKIES = {}
2078        r.META = {"HTTP_ACCEPT_LANGUAGE": "pt,en-US;q=0.8,en;q=0.6,ru;q=0.4"}
2079        lang = get_language_from_request(r)
2080        self.assertEqual("pt-br", lang)
2081        r = self.rf.get("/")
2082        r.COOKIES = {}
2083        r.META = {"HTTP_ACCEPT_LANGUAGE": "pt-pt,en-US;q=0.8,en;q=0.6,ru;q=0.4"}
2084        lang = get_language_from_request(r)
2085        self.assertEqual("pt-br", lang)
2086
2087
2088class TranslationFilesMissing(SimpleTestCase):
2089    def setUp(self):
2090        super().setUp()
2091        self.gettext_find_builtin = gettext_module.find
2092
2093    def tearDown(self):
2094        gettext_module.find = self.gettext_find_builtin
2095        super().tearDown()
2096
2097    def patchGettextFind(self):
2098        gettext_module.find = lambda *args, **kw: None
2099
2100    def test_failure_finding_default_mo_files(self):
2101        """OSError is raised if the default language is unparseable."""
2102        self.patchGettextFind()
2103        trans_real._translations = {}
2104        with self.assertRaises(OSError):
2105            activate("en")
2106
2107
2108class NonDjangoLanguageTests(SimpleTestCase):
2109    """
2110    A language non present in default Django languages can still be
2111    installed/used by a Django project.
2112    """
2113
2114    @override_settings(
2115        USE_I18N=True,
2116        LANGUAGES=[("en-us", "English"), ("xxx", "Somelanguage"),],
2117        LANGUAGE_CODE="xxx",
2118        LOCALE_PATHS=[os.path.join(here, "commands", "locale")],
2119    )
2120    def test_non_django_language(self):
2121        self.assertEqual(get_language(), "xxx")
2122        self.assertEqual(gettext("year"), "reay")
2123
2124    @override_settings(USE_I18N=True)
2125    def test_check_for_language(self):
2126        with tempfile.TemporaryDirectory() as app_dir:
2127            os.makedirs(os.path.join(app_dir, "locale", "dummy_Lang", "LC_MESSAGES"))
2128            open(
2129                os.path.join(
2130                    app_dir, "locale", "dummy_Lang", "LC_MESSAGES", "django.mo"
2131                ),
2132                "w",
2133            ).close()
2134            app_config = AppConfig("dummy_app", AppModuleStub(__path__=[app_dir]))
2135            with mock.patch(
2136                "django.apps.apps.get_app_configs", return_value=[app_config]
2137            ):
2138                self.assertIs(check_for_language("dummy-lang"), True)
2139
2140    @override_settings(
2141        USE_I18N=True,
2142        LANGUAGES=[
2143            ("en-us", "English"),
2144            # xyz language has no locale files
2145            ("xyz", "XYZ"),
2146        ],
2147    )
2148    @translation.override("xyz")
2149    def test_plural_non_django_language(self):
2150        self.assertEqual(get_language(), "xyz")
2151        self.assertEqual(ngettext("year", "years", 2), "years")
2152
2153
2154@override_settings(USE_I18N=True)
2155class WatchForTranslationChangesTests(SimpleTestCase):
2156    @override_settings(USE_I18N=False)
2157    def test_i18n_disabled(self):
2158        mocked_sender = mock.MagicMock()
2159        watch_for_translation_changes(mocked_sender)
2160        mocked_sender.watch_dir.assert_not_called()
2161
2162    def test_i18n_enabled(self):
2163        mocked_sender = mock.MagicMock()
2164        watch_for_translation_changes(mocked_sender)
2165        self.assertGreater(mocked_sender.watch_dir.call_count, 1)
2166
2167    def test_i18n_locale_paths(self):
2168        mocked_sender = mock.MagicMock()
2169        with tempfile.TemporaryDirectory() as app_dir:
2170            with self.settings(LOCALE_PATHS=[app_dir]):
2171                watch_for_translation_changes(mocked_sender)
2172            mocked_sender.watch_dir.assert_any_call(Path(app_dir), "**/*.mo")
2173
2174    def test_i18n_app_dirs(self):
2175        mocked_sender = mock.MagicMock()
2176        with self.settings(INSTALLED_APPS=["tests.i18n.sampleproject"]):
2177            watch_for_translation_changes(mocked_sender)
2178        project_dir = Path(__file__).parent / "sampleproject" / "locale"
2179        mocked_sender.watch_dir.assert_any_call(project_dir, "**/*.mo")
2180
2181    def test_i18n_local_locale(self):
2182        mocked_sender = mock.MagicMock()
2183        watch_for_translation_changes(mocked_sender)
2184        locale_dir = Path(__file__).parent / "locale"
2185        mocked_sender.watch_dir.assert_any_call(locale_dir, "**/*.mo")
2186
2187
2188class TranslationFileChangedTests(SimpleTestCase):
2189    def setUp(self):
2190        self.gettext_translations = gettext_module._translations.copy()
2191        self.trans_real_translations = trans_real._translations.copy()
2192
2193    def tearDown(self):
2194        gettext._translations = self.gettext_translations
2195        trans_real._translations = self.trans_real_translations
2196
2197    def test_ignores_non_mo_files(self):
2198        gettext_module._translations = {"foo": "bar"}
2199        path = Path("test.py")
2200        self.assertIsNone(translation_file_changed(None, path))
2201        self.assertEqual(gettext_module._translations, {"foo": "bar"})
2202
2203    def test_resets_cache_with_mo_files(self):
2204        gettext_module._translations = {"foo": "bar"}
2205        trans_real._translations = {"foo": "bar"}
2206        trans_real._default = 1
2207        trans_real._active = False
2208        path = Path("test.mo")
2209        self.assertIs(translation_file_changed(None, path), True)
2210        self.assertEqual(gettext_module._translations, {})
2211        self.assertEqual(trans_real._translations, {})
2212        self.assertIsNone(trans_real._default)
2213        self.assertIsInstance(trans_real._active, Local)
2214
2215
2216class UtilsTests(SimpleTestCase):
2217    def test_round_away_from_one(self):
2218        tests = [
2219            (0, 0),
2220            (0.0, 0),
2221            (0.25, 0),
2222            (0.5, 0),
2223            (0.75, 0),
2224            (1, 1),
2225            (1.0, 1),
2226            (1.25, 2),
2227            (1.5, 2),
2228            (1.75, 2),
2229            (-0.0, 0),
2230            (-0.25, -1),
2231            (-0.5, -1),
2232            (-0.75, -1),
2233            (-1, -1),
2234            (-1.0, -1),
2235            (-1.25, -2),
2236            (-1.5, -2),
2237            (-1.75, -2),
2238        ]
2239        for value, expected in tests:
2240            with self.subTest(value=value):
2241                self.assertEqual(round_away_from_one(value), expected)

tests/template_tests/syntax_tests/i18n/test_blocktrans.py

  1import os
  2
  3from asgiref.local import Local
  4
  5from django.template import Context, Template, TemplateSyntaxError
  6from django.test import SimpleTestCase, override_settings
  7from django.utils import translation
  8from django.utils.safestring import mark_safe
  9from django.utils.translation import trans_real
 10
 11from ...utils import setup
 12from .base import MultipleLocaleActivationTestCase, extended_locale_paths, here
 13
 14
 15class I18nBlockTransTagTests(SimpleTestCase):
 16    libraries = {"i18n": "django.templatetags.i18n"}
 17
 18    @setup({"i18n03": "{% load i18n %}{% blocktrans %}{{ anton }}{% endblocktrans %}"})
 19    def test_i18n03(self):
 20        """simple translation of a variable"""
 21        output = self.engine.render_to_string("i18n03", {"anton": "Å"})
 22        self.assertEqual(output, "Å")
 23
 24    @setup(
 25        {
 26            "i18n04": "{% load i18n %}{% blocktrans with berta=anton|lower %}{{ berta }}{% endblocktrans %}"
 27        }
 28    )
 29    def test_i18n04(self):
 30        """simple translation of a variable and filter"""
 31        output = self.engine.render_to_string("i18n04", {"anton": "Å"})
 32        self.assertEqual(output, "å")
 33
 34    @setup(
 35        {
 36            "legacyi18n04": "{% load i18n %}"
 37            "{% blocktrans with anton|lower as berta %}{{ berta }}{% endblocktrans %}"
 38        }
 39    )
 40    def test_legacyi18n04(self):
 41        """simple translation of a variable and filter"""
 42        output = self.engine.render_to_string("legacyi18n04", {"anton": "Å"})
 43        self.assertEqual(output, "å")
 44
 45    @setup(
 46        {
 47            "i18n05": "{% load i18n %}{% blocktrans %}xxx{{ anton }}xxx{% endblocktrans %}"
 48        }
 49    )
 50    def test_i18n05(self):
 51        """simple translation of a string with interpolation"""
 52        output = self.engine.render_to_string("i18n05", {"anton": "yyy"})
 53        self.assertEqual(output, "xxxyyyxxx")
 54
 55    @setup(
 56        {
 57            "i18n07": "{% load i18n %}"
 58            "{% blocktrans count counter=number %}singular{% plural %}"
 59            "{{ counter }} plural{% endblocktrans %}"
 60        }
 61    )
 62    def test_i18n07(self):
 63        """translation of singular form"""
 64        output = self.engine.render_to_string("i18n07", {"number": 1})
 65        self.assertEqual(output, "singular")
 66
 67    @setup(
 68        {
 69            "legacyi18n07": "{% load i18n %}"
 70            "{% blocktrans count number as counter %}singular{% plural %}"
 71            "{{ counter }} plural{% endblocktrans %}"
 72        }
 73    )
 74    def test_legacyi18n07(self):
 75        """translation of singular form"""
 76        output = self.engine.render_to_string("legacyi18n07", {"number": 1})
 77        self.assertEqual(output, "singular")
 78
 79    @setup(
 80        {
 81            "i18n08": "{% load i18n %}"
 82            "{% blocktrans count number as counter %}singular{% plural %}"
 83            "{{ counter }} plural{% endblocktrans %}"
 84        }
 85    )
 86    def test_i18n08(self):
 87        """translation of plural form"""
 88        output = self.engine.render_to_string("i18n08", {"number": 2})
 89        self.assertEqual(output, "2 plural")
 90
 91    @setup(
 92        {
 93            "legacyi18n08": "{% load i18n %}"
 94            "{% blocktrans count counter=number %}singular{% plural %}"
 95            "{{ counter }} plural{% endblocktrans %}"
 96        }
 97    )
 98    def test_legacyi18n08(self):
 99        """translation of plural form"""
100        output = self.engine.render_to_string("legacyi18n08", {"number": 2})
101        self.assertEqual(output, "2 plural")
102
103    @setup(
104        {
105            "i18n17": "{% load i18n %}"
106            "{% blocktrans with berta=anton|escape %}{{ berta }}{% endblocktrans %}"
107        }
108    )
109    def test_i18n17(self):
110        """
111        Escaping inside blocktrans and trans works as if it was directly in the
112        template.
113        """
114        output = self.engine.render_to_string("i18n17", {"anton": "α & β"})
115        self.assertEqual(output, "α &amp; β")
116
117    @setup(
118        {
119            "i18n18": "{% load i18n %}"
120            "{% blocktrans with berta=anton|force_escape %}{{ berta }}{% endblocktrans %}"
121        }
122    )
123    def test_i18n18(self):
124        output = self.engine.render_to_string("i18n18", {"anton": "α & β"})
125        self.assertEqual(output, "α &amp; β")
126
127    @setup({"i18n19": "{% load i18n %}{% blocktrans %}{{ andrew }}{% endblocktrans %}"})
128    def test_i18n19(self):
129        output = self.engine.render_to_string("i18n19", {"andrew": "a & b"})
130        self.assertEqual(output, "a &amp; b")
131
132    @setup({"i18n21": "{% load i18n %}{% blocktrans %}{{ andrew }}{% endblocktrans %}"})
133    def test_i18n21(self):
134        output = self.engine.render_to_string("i18n21", {"andrew": mark_safe("a & b")})
135        self.assertEqual(output, "a & b")
136
137    @setup(
138        {
139            "legacyi18n17": "{% load i18n %}"
140            "{% blocktrans with anton|escape as berta %}{{ berta }}{% endblocktrans %}"
141        }
142    )
143    def test_legacyi18n17(self):
144        output = self.engine.render_to_string("legacyi18n17", {"anton": "α & β"})
145        self.assertEqual(output, "α &amp; β")
146
147    @setup(
148        {
149            "legacyi18n18": "{% load i18n %}"
150            "{% blocktrans with anton|force_escape as berta %}"
151            "{{ berta }}{% endblocktrans %}"
152        }
153    )
154    def test_legacyi18n18(self):
155        output = self.engine.render_to_string("legacyi18n18", {"anton": "α & β"})
156        self.assertEqual(output, "α &amp; β")
157
158    @setup(
159        {
160            "i18n26": "{% load i18n %}"
161            "{% blocktrans with extra_field=myextra_field count counter=number %}"
162            "singular {{ extra_field }}{% plural %}plural{% endblocktrans %}"
163        }
164    )
165    def test_i18n26(self):
166        """
167        translation of plural form with extra field in singular form (#13568)
168        """
169        output = self.engine.render_to_string(
170            "i18n26", {"myextra_field": "test", "number": 1}
171        )
172        self.assertEqual(output, "singular test")
173
174    @setup(
175        {
176            "legacyi18n26": "{% load i18n %}"
177            "{% blocktrans with myextra_field as extra_field count number as counter %}"
178            "singular {{ extra_field }}{% plural %}plural{% endblocktrans %}"
179        }
180    )
181    def test_legacyi18n26(self):
182        output = self.engine.render_to_string(
183            "legacyi18n26", {"myextra_field": "test", "number": 1}
184        )
185        self.assertEqual(output, "singular test")
186
187    @setup(
188        {
189            "i18n27": "{% load i18n %}{% blocktrans count counter=number %}"
190            "{{ counter }} result{% plural %}{{ counter }} results"
191            "{% endblocktrans %}"
192        }
193    )
194    def test_i18n27(self):
195        """translation of singular form in Russian (#14126)"""
196        with translation.override("ru"):
197            output = self.engine.render_to_string("i18n27", {"number": 1})
198        self.assertEqual(
199            output, "1 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442"
200        )
201
202    @setup(
203        {
204            "legacyi18n27": "{% load i18n %}"
205            "{% blocktrans count number as counter %}{{ counter }} result"
206            "{% plural %}{{ counter }} results{% endblocktrans %}"
207        }
208    )
209    def test_legacyi18n27(self):
210        with translation.override("ru"):
211            output = self.engine.render_to_string("legacyi18n27", {"number": 1})
212        self.assertEqual(
213            output, "1 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442"
214        )
215
216    @setup(
217        {
218            "i18n28": "{% load i18n %}"
219            "{% blocktrans with a=anton b=berta %}{{ a }} + {{ b }}{% endblocktrans %}"
220        }
221    )
222    def test_i18n28(self):
223        """simple translation of multiple variables"""
224        output = self.engine.render_to_string("i18n28", {"anton": "α", "berta": "β"})
225        self.assertEqual(output, "α + β")
226
227    @setup(
228        {
229            "legacyi18n28": "{% load i18n %}"
230            "{% blocktrans with anton as a and berta as b %}"
231            "{{ a }} + {{ b }}{% endblocktrans %}"
232        }
233    )
234    def test_legacyi18n28(self):
235        output = self.engine.render_to_string(
236            "legacyi18n28", {"anton": "α", "berta": "β"}
237        )
238        self.assertEqual(output, "α + β")
239
240    # blocktrans handling of variables which are not in the context.
241    # this should work as if blocktrans was not there (#19915)
242    @setup(
243        {"i18n34": "{% load i18n %}{% blocktrans %}{{ missing }}{% endblocktrans %}"}
244    )
245    def test_i18n34(self):
246        output = self.engine.render_to_string("i18n34")
247        if self.engine.string_if_invalid:
248            self.assertEqual(output, "INVALID")
249        else:
250            self.assertEqual(output, "")
251
252    @setup(
253        {
254            "i18n34_2": "{% load i18n %}{% blocktrans with a='α' %}{{ missing }}{% endblocktrans %}"
255        }
256    )
257    def test_i18n34_2(self):
258        output = self.engine.render_to_string("i18n34_2")
259        if self.engine.string_if_invalid:
260            self.assertEqual(output, "INVALID")
261        else:
262            self.assertEqual(output, "")
263
264    @setup(
265        {
266            "i18n34_3": "{% load i18n %}{% blocktrans with a=anton %}{{ missing }}{% endblocktrans %}"
267        }
268    )
269    def test_i18n34_3(self):
270        output = self.engine.render_to_string("i18n34_3", {"anton": "\xce\xb1"})
271        if self.engine.string_if_invalid:
272            self.assertEqual(output, "INVALID")
273        else:
274            self.assertEqual(output, "")
275
276    @setup(
277        {
278            "i18n37": "{% load i18n %}"
279            '{% trans "Page not found" as page_not_found %}'
280            "{% blocktrans %}Error: {{ page_not_found }}{% endblocktrans %}"
281        }
282    )
283    def test_i18n37(self):
284        with translation.override("de"):
285            output = self.engine.render_to_string("i18n37")
286        self.assertEqual(output, "Error: Seite nicht gefunden")
287
288    # blocktrans tag with asvar
289    @setup(
290        {
291            "i18n39": "{% load i18n %}"
292            "{% blocktrans asvar page_not_found %}Page not found{% endblocktrans %}"
293            ">{{ page_not_found }}<"
294        }
295    )
296    def test_i18n39(self):
297        with translation.override("de"):
298            output = self.engine.render_to_string("i18n39")
299        self.assertEqual(output, ">Seite nicht gefunden<")
300
301    @setup(
302        {
303            "i18n40": "{% load i18n %}"
304            '{% trans "Page not found" as pg_404 %}'
305            "{% blocktrans with page_not_found=pg_404 asvar output %}"
306            "Error: {{ page_not_found }}"
307            "{% endblocktrans %}"
308        }
309    )
310    def test_i18n40(self):
311        output = self.engine.render_to_string("i18n40")
312        self.assertEqual(output, "")
313
314    @setup(
315        {
316            "i18n41": "{% load i18n %}"
317            '{% trans "Page not found" as pg_404 %}'
318            "{% blocktrans with page_not_found=pg_404 asvar output %}"
319            "Error: {{ page_not_found }}"
320            "{% endblocktrans %}"
321            ">{{ output }}<"
322        }
323    )
324    def test_i18n41(self):
325        with translation.override("de"):
326            output = self.engine.render_to_string("i18n41")
327        self.assertEqual(output, ">Error: Seite nicht gefunden<")
328
329    @setup({"template": "{% load i18n %}{% blocktrans asvar %}Yes{% endblocktrans %}"})
330    def test_blocktrans_syntax_error_missing_assignment(self):
331        msg = "No argument provided to the 'blocktrans' tag for the asvar option."
332        with self.assertRaisesMessage(TemplateSyntaxError, msg):
333            self.engine.render_to_string("template")
334
335    @setup({"template": "{% load i18n %}{% blocktrans %}%s{% endblocktrans %}"})
336    def test_blocktrans_tag_using_a_string_that_looks_like_str_fmt(self):
337        output = self.engine.render_to_string("template")
338        self.assertEqual(output, "%s")
339
340    @setup(
341        {
342            "template": "{% load i18n %}{% blocktrans %}{% block b %} {% endblock %}{% endblocktrans %}"
343        }
344    )
345    def test_with_block(self):
346        msg = "'blocktrans' doesn't allow other block tags (seen 'block b') inside it"
347        with self.assertRaisesMessage(TemplateSyntaxError, msg):
348            self.engine.render_to_string("template")
349
350    @setup(
351        {
352            "template": "{% load i18n %}{% blocktrans %}{% for b in [1, 2, 3] %} {% endfor %}{% endblocktrans %}"
353        }
354    )
355    def test_with_for(self):
356        msg = "'blocktrans' doesn't allow other block tags (seen 'for b in [1, 2, 3]') inside it"
357        with self.assertRaisesMessage(TemplateSyntaxError, msg):
358            self.engine.render_to_string("template")
359
360    @setup(
361        {
362            "template": "{% load i18n %}{% blocktrans with foo=bar with %}{{ foo }}{% endblocktrans %}"
363        }
364    )
365    def test_variable_twice(self):
366        with self.assertRaisesMessage(
367            TemplateSyntaxError, "The 'with' option was specified more than once"
368        ):
369            self.engine.render_to_string("template", {"foo": "bar"})
370
371    @setup({"template": "{% load i18n %}{% blocktrans with %}{% endblocktrans %}"})
372    def test_no_args_with(self):
373        msg = "\"with\" in 'blocktrans' tag needs at least one keyword argument."
374        with self.assertRaisesMessage(TemplateSyntaxError, msg):
375            self.engine.render_to_string("template")
376
377    @setup({"template": "{% load i18n %}{% blocktrans count a %}{% endblocktrans %}"})
378    def test_count(self):
379        msg = "\"count\" in 'blocktrans' tag expected exactly one keyword argument."
380        with self.assertRaisesMessage(TemplateSyntaxError, msg):
381            self.engine.render_to_string("template", {"a": [1, 2, 3]})
382
383    @setup(
384        {
385            "template": (
386                "{% load i18n %}{% blocktrans count count=var|length %}"
387                "There is {{ count }} object. {% block a %} {% endblock %}"
388                "{% endblocktrans %}"
389            )
390        }
391    )
392    def test_plural_bad_syntax(self):
393        msg = "'blocktrans' doesn't allow other block tags inside it"
394        with self.assertRaisesMessage(TemplateSyntaxError, msg):
395            self.engine.render_to_string("template", {"var": [1, 2, 3]})
396
397
398class TranslationBlockTransTagTests(SimpleTestCase):
399    @override_settings(LOCALE_PATHS=extended_locale_paths)
400    def test_template_tags_pgettext(self):
401        """{% blocktrans %} takes message contexts into account (#14806)."""
402        trans_real._active = Local()
403        trans_real._translations = {}
404        with translation.override("de"):
405            # Nonexistent context
406            t = Template(
407                '{% load i18n %}{% blocktrans context "nonexistent" %}May{% endblocktrans %}'
408            )
409            rendered = t.render(Context())
410            self.assertEqual(rendered, "May")
411
412            # Existing context...  using a literal
413            t = Template(
414                '{% load i18n %}{% blocktrans context "month name" %}May{% endblocktrans %}'
415            )
416            rendered = t.render(Context())
417            self.assertEqual(rendered, "Mai")
418            t = Template(
419                '{% load i18n %}{% blocktrans context "verb" %}May{% endblocktrans %}'
420            )
421            rendered = t.render(Context())
422            self.assertEqual(rendered, "Kann")
423
424            # Using a variable
425            t = Template(
426                "{% load i18n %}{% blocktrans context message_context %}May{% endblocktrans %}"
427            )
428            rendered = t.render(Context({"message_context": "month name"}))
429            self.assertEqual(rendered, "Mai")
430            t = Template(
431                "{% load i18n %}{% blocktrans context message_context %}May{% endblocktrans %}"
432            )
433            rendered = t.render(Context({"message_context": "verb"}))
434            self.assertEqual(rendered, "Kann")
435
436            # Using a filter
437            t = Template(
438                "{% load i18n %}{% blocktrans context message_context|lower %}May{% endblocktrans %}"
439            )
440            rendered = t.render(Context({"message_context": "MONTH NAME"}))
441            self.assertEqual(rendered, "Mai")
442            t = Template(
443                "{% load i18n %}{% blocktrans context message_context|lower %}May{% endblocktrans %}"
444            )
445            rendered = t.render(Context({"message_context": "VERB"}))
446            self.assertEqual(rendered, "Kann")
447
448            # Using 'count'
449            t = Template(
450                '{% load i18n %}{% blocktrans count number=1 context "super search" %}'
451                "{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}"
452            )
453            rendered = t.render(Context())
454            self.assertEqual(rendered, "1 Super-Ergebnis")
455            t = Template(
456                '{% load i18n %}{% blocktrans count number=2 context "super search" %}{{ number }}'
457                " super result{% plural %}{{ number }} super results{% endblocktrans %}"
458            )
459            rendered = t.render(Context())
460            self.assertEqual(rendered, "2 Super-Ergebnisse")
461            t = Template(
462                '{% load i18n %}{% blocktrans context "other super search" count number=1 %}'
463                "{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}"
464            )
465            rendered = t.render(Context())
466            self.assertEqual(rendered, "1 anderen Super-Ergebnis")
467            t = Template(
468                '{% load i18n %}{% blocktrans context "other super search" count number=2 %}'
469                "{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}"
470            )
471            rendered = t.render(Context())
472            self.assertEqual(rendered, "2 andere Super-Ergebnisse")
473
474            # Using 'with'
475            t = Template(
476                '{% load i18n %}{% blocktrans with num_comments=5 context "comment count" %}'
477                "There are {{ num_comments }} comments{% endblocktrans %}"
478            )
479            rendered = t.render(Context())
480            self.assertEqual(rendered, "Es gibt 5 Kommentare")
481            t = Template(
482                '{% load i18n %}{% blocktrans with num_comments=5 context "other comment count" %}'
483                "There are {{ num_comments }} comments{% endblocktrans %}"
484            )
485            rendered = t.render(Context())
486            self.assertEqual(rendered, "Andere: Es gibt 5 Kommentare")
487
488            # Using trimmed
489            t = Template(
490                "{% load i18n %}{% blocktrans trimmed %}\n\nThere\n\t are 5  "
491                "\n\n   comments\n{% endblocktrans %}"
492            )
493            rendered = t.render(Context())
494            self.assertEqual(rendered, "There are 5 comments")
495            t = Template(
496                '{% load i18n %}{% blocktrans with num_comments=5 context "comment count" trimmed %}\n\n'
497                "There are  \t\n  \t {{ num_comments }} comments\n\n{% endblocktrans %}"
498            )
499            rendered = t.render(Context())
500            self.assertEqual(rendered, "Es gibt 5 Kommentare")
501            t = Template(
502                '{% load i18n %}{% blocktrans context "other super search" count number=2 trimmed %}\n'
503                "{{ number }} super \n result{% plural %}{{ number }} super results{% endblocktrans %}"
504            )
505            rendered = t.render(Context())
506            self.assertEqual(rendered, "2 andere Super-Ergebnisse")
507
508            # Misuses
509            msg = "Unknown argument for 'blocktrans' tag: %r."
510            with self.assertRaisesMessage(TemplateSyntaxError, msg % 'month="May"'):
511                Template(
512                    '{% load i18n %}{% blocktrans context with month="May" %}{{ month }}{% endblocktrans %}'
513                )
514            msg = '"context" in %r tag expected exactly one argument.' % "blocktrans"
515            with self.assertRaisesMessage(TemplateSyntaxError, msg):
516                Template("{% load i18n %}{% blocktrans context %}{% endblocktrans %}")
517            with self.assertRaisesMessage(TemplateSyntaxError, msg):
518                Template(
519                    "{% load i18n %}{% blocktrans count number=2 context %}"
520                    "{{ number }} super result{% plural %}{{ number }}"
521                    " super results{% endblocktrans %}"
522                )
523
524    @override_settings(LOCALE_PATHS=[os.path.join(here, "other", "locale")])
525    def test_bad_placeholder_1(self):
526        """
527        Error in translation file should not crash template rendering (#16516).
528        (%(person)s is translated as %(personne)s in fr.po).
529        """
530        with translation.override("fr"):
531            t = Template(
532                "{% load i18n %}{% blocktrans %}My name is {{ person }}.{% endblocktrans %}"
533            )
534            rendered = t.render(Context({"person": "James"}))
535            self.assertEqual(rendered, "My name is James.")
536
537    @override_settings(LOCALE_PATHS=[os.path.join(here, "other", "locale")])
538    def test_bad_placeholder_2(self):
539        """
540        Error in translation file should not crash template rendering (#18393).
541        (%(person) misses a 's' in fr.po, causing the string formatting to fail)
542        .
543        """
544        with translation.override("fr"):
545            t = Template(
546                "{% load i18n %}{% blocktrans %}My other name is {{ person }}.{% endblocktrans %}"
547            )
548            rendered = t.render(Context({"person": "James"}))
549            self.assertEqual(rendered, "My other name is James.")
550
551
552class MultipleLocaleActivationBlockTransTests(MultipleLocaleActivationTestCase):
553    def test_single_locale_activation(self):
554        """
555        Simple baseline behavior with one locale for all the supported i18n
556        constructs.
557        """
558        with translation.override("fr"):
559            self.assertEqual(
560                Template(
561                    "{% load i18n %}{% blocktrans %}Yes{% endblocktrans %}"
562                ).render(Context({})),
563                "Oui",
564            )
565
566    def test_multiple_locale_btrans(self):
567        with translation.override("de"):
568            t = Template("{% load i18n %}{% blocktrans %}No{% endblocktrans %}")
569        with translation.override(self._old_language), translation.override("nl"):
570            self.assertEqual(t.render(Context({})), "Nee")
571
572    def test_multiple_locale_deactivate_btrans(self):
573        with translation.override("de", deactivate=True):
574            t = Template("{% load i18n %}{% blocktrans %}No{% endblocktrans %}")
575        with translation.override("nl"):
576            self.assertEqual(t.render(Context({})), "Nee")
577
578    def test_multiple_locale_direct_switch_btrans(self):
579        with translation.override("de"):
580            t = Template("{% load i18n %}{% blocktrans %}No{% endblocktrans %}")
581        with translation.override("nl"):
582            self.assertEqual(t.render(Context({})), "Nee")
583
584
585class MiscTests(SimpleTestCase):
586    @override_settings(LOCALE_PATHS=extended_locale_paths)
587    def test_percent_in_translatable_block(self):
588        t_sing = Template(
589            "{% load i18n %}{% blocktrans %}The result was {{ percent }}%{% endblocktrans %}"
590        )
591        t_plur = Template(
592            "{% load i18n %}{% blocktrans count num as number %}"
593            "{{ percent }}% represents {{ num }} object{% plural %}"
594            "{{ percent }}% represents {{ num }} objects{% endblocktrans %}"
595        )
596        with translation.override("de"):
597            self.assertEqual(
598                t_sing.render(Context({"percent": 42})), "Das Ergebnis war 42%"
599            )
600            self.assertEqual(
601                t_plur.render(Context({"percent": 42, "num": 1})),
602                "42% stellt 1 Objekt dar",
603            )
604            self.assertEqual(
605                t_plur.render(Context({"percent": 42, "num": 4})),
606                "42% stellt 4 Objekte dar",
607            )
608
609    @override_settings(LOCALE_PATHS=extended_locale_paths)
610    def test_percent_formatting_in_blocktrans(self):
611        """
612        Python's %-formatting is properly escaped in blocktrans, singular, or
613        plural.
614        """
615        t_sing = Template(
616            "{% load i18n %}{% blocktrans %}There are %(num_comments)s comments{% endblocktrans %}"
617        )
618        t_plur = Template(
619            "{% load i18n %}{% blocktrans count num as number %}"
620            "%(percent)s% represents {{ num }} object{% plural %}"
621            "%(percent)s% represents {{ num }} objects{% endblocktrans %}"
622        )
623        with translation.override("de"):
624            # Strings won't get translated as they don't match after escaping %
625            self.assertEqual(
626                t_sing.render(Context({"num_comments": 42})),
627                "There are %(num_comments)s comments",
628            )
629            self.assertEqual(
630                t_plur.render(Context({"percent": 42, "num": 1})),
631                "%(percent)s% represents 1 object",
632            )
633            self.assertEqual(
634                t_plur.render(Context({"percent": 42, "num": 4})),
635                "%(percent)s% represents 4 objects",
636            )

tests/template_tests/syntax_tests/i18n/test_trans.py

  1from asgiref.local import Local
  2
  3from django.template import Context, Template, TemplateSyntaxError
  4from django.templatetags.l10n import LocalizeNode
  5from django.test import SimpleTestCase, override_settings
  6from django.utils import translation
  7from django.utils.safestring import mark_safe
  8from django.utils.translation import trans_real
  9
 10from ...utils import setup
 11from .base import MultipleLocaleActivationTestCase, extended_locale_paths
 12
 13
 14class I18nTransTagTests(SimpleTestCase):
 15    libraries = {"i18n": "django.templatetags.i18n"}
 16
 17    @setup({"i18n01": "{% load i18n %}{% trans 'xxxyyyxxx' %}"})
 18    def test_i18n01(self):
 19        """simple translation of a string delimited by '."""
 20        output = self.engine.render_to_string("i18n01")
 21        self.assertEqual(output, "xxxyyyxxx")
 22
 23    @setup({"i18n02": '{% load i18n %}{% trans "xxxyyyxxx" %}'})
 24    def test_i18n02(self):
 25        """simple translation of a string delimited by "."""
 26        output = self.engine.render_to_string("i18n02")
 27        self.assertEqual(output, "xxxyyyxxx")
 28
 29    @setup({"i18n06": '{% load i18n %}{% trans "Page not found" %}'})
 30    def test_i18n06(self):
 31        """simple translation of a string to German"""
 32        with translation.override("de"):
 33            output = self.engine.render_to_string("i18n06")
 34        self.assertEqual(output, "Seite nicht gefunden")
 35
 36    @setup({"i18n09": '{% load i18n %}{% trans "Page not found" noop %}'})
 37    def test_i18n09(self):
 38        """simple non-translation (only marking) of a string to German"""
 39        with translation.override("de"):
 40            output = self.engine.render_to_string("i18n09")
 41        self.assertEqual(output, "Page not found")
 42
 43    @setup({"i18n20": "{% load i18n %}{% trans andrew %}"})
 44    def test_i18n20(self):
 45        output = self.engine.render_to_string("i18n20", {"andrew": "a & b"})
 46        self.assertEqual(output, "a &amp; b")
 47
 48    @setup({"i18n22": "{% load i18n %}{% trans andrew %}"})
 49    def test_i18n22(self):
 50        output = self.engine.render_to_string("i18n22", {"andrew": mark_safe("a & b")})
 51        self.assertEqual(output, "a & b")
 52
 53    @setup(
 54        {"i18n23": '{% load i18n %}{% trans "Page not found"|capfirst|slice:"6:" %}'}
 55    )
 56    def test_i18n23(self):
 57        """Using filters with the {% trans %} tag (#5972)."""
 58        with translation.override("de"):
 59            output = self.engine.render_to_string("i18n23")
 60        self.assertEqual(output, "nicht gefunden")
 61
 62    @setup({"i18n24": "{% load i18n %}{% trans 'Page not found'|upper %}"})
 63    def test_i18n24(self):
 64        with translation.override("de"):
 65            output = self.engine.render_to_string("i18n24")
 66        self.assertEqual(output, "SEITE NICHT GEFUNDEN")
 67
 68    @setup({"i18n25": "{% load i18n %}{% trans somevar|upper %}"})
 69    def test_i18n25(self):
 70        with translation.override("de"):
 71            output = self.engine.render_to_string(
 72                "i18n25", {"somevar": "Page not found"}
 73            )
 74        self.assertEqual(output, "SEITE NICHT GEFUNDEN")
 75
 76    # trans tag with as var
 77    @setup(
 78        {
 79            "i18n35": '{% load i18n %}{% trans "Page not found" as page_not_found %}{{ page_not_found }}'
 80        }
 81    )
 82    def test_i18n35(self):
 83        with translation.override("de"):
 84            output = self.engine.render_to_string("i18n35")
 85        self.assertEqual(output, "Seite nicht gefunden")
 86
 87    @setup(
 88        {
 89            "i18n36": "{% load i18n %}"
 90            '{% trans "Page not found" noop as page_not_found %}{{ page_not_found }}'
 91        }
 92    )
 93    def test_i18n36(self):
 94        with translation.override("de"):
 95            output = self.engine.render_to_string("i18n36")
 96        self.assertEqual(output, "Page not found")
 97
 98    @setup({"template": "{% load i18n %}{% trans %}A}"})
 99    def test_syntax_error_no_arguments(self):
100        msg = "'trans' takes at least one argument"
101        with self.assertRaisesMessage(TemplateSyntaxError, msg):
102            self.engine.render_to_string("template")
103
104    @setup({"template": '{% load i18n %}{% trans "Yes" badoption %}'})
105    def test_syntax_error_bad_option(self):
106        msg = "Unknown argument for 'trans' tag: 'badoption'"
107        with self.assertRaisesMessage(TemplateSyntaxError, msg):
108            self.engine.render_to_string("template")
109
110    @setup({"template": '{% load i18n %}{% trans "Yes" as %}'})
111    def test_syntax_error_missing_assignment(self):
112        msg = "No argument provided to the 'trans' tag for the as option."
113        with self.assertRaisesMessage(TemplateSyntaxError, msg):
114            self.engine.render_to_string("template")
115
116    @setup({"template": '{% load i18n %}{% trans "Yes" as var context %}'})
117    def test_syntax_error_missing_context(self):
118        msg = "No argument provided to the 'trans' tag for the context option."
119        with self.assertRaisesMessage(TemplateSyntaxError, msg):
120            self.engine.render_to_string("template")
121
122    @setup({"template": '{% load i18n %}{% trans "Yes" context as var %}'})
123    def test_syntax_error_context_as(self):
124        msg = "Invalid argument 'as' provided to the 'trans' tag for the context option"
125        with self.assertRaisesMessage(TemplateSyntaxError, msg):
126            self.engine.render_to_string("template")
127
128    @setup({"template": '{% load i18n %}{% trans "Yes" context noop %}'})
129    def test_syntax_error_context_noop(self):
130        msg = (
131            "Invalid argument 'noop' provided to the 'trans' tag for the context option"
132        )
133        with self.assertRaisesMessage(TemplateSyntaxError, msg):
134            self.engine.render_to_string("template")
135
136    @setup({"template": '{% load i18n %}{% trans "Yes" noop noop %}'})
137    def test_syntax_error_duplicate_option(self):
138        msg = "The 'noop' option was specified more than once."
139        with self.assertRaisesMessage(TemplateSyntaxError, msg):
140            self.engine.render_to_string("template")
141
142    @setup({"template": '{% load i18n %}{% trans "%s" %}'})
143    def test_trans_tag_using_a_string_that_looks_like_str_fmt(self):
144        output = self.engine.render_to_string("template")
145        self.assertEqual(output, "%s")
146
147
148class TranslationTransTagTests(SimpleTestCase):
149    @override_settings(LOCALE_PATHS=extended_locale_paths)
150    def test_template_tags_pgettext(self):
151        """{% trans %} takes message contexts into account (#14806)."""
152        trans_real._active = Local()
153        trans_real._translations = {}
154        with translation.override("de"):
155            # Nonexistent context...
156            t = Template('{% load i18n %}{% trans "May" context "nonexistent" %}')
157            rendered = t.render(Context())
158            self.assertEqual(rendered, "May")
159
160            # Existing context... using a literal
161            t = Template('{% load i18n %}{% trans "May" context "month name" %}')
162            rendered = t.render(Context())
163            self.assertEqual(rendered, "Mai")
164            t = Template('{% load i18n %}{% trans "May" context "verb" %}')
165            rendered = t.render(Context())
166            self.assertEqual(rendered, "Kann")
167
168            # Using a variable
169            t = Template('{% load i18n %}{% trans "May" context message_context %}')
170            rendered = t.render(Context({"message_context": "month name"}))
171            self.assertEqual(rendered, "Mai")
172            t = Template('{% load i18n %}{% trans "May" context message_context %}')
173            rendered = t.render(Context({"message_context": "verb"}))
174            self.assertEqual(rendered, "Kann")
175
176            # Using a filter
177            t = Template(
178                '{% load i18n %}{% trans "May" context message_context|lower %}'
179            )
180            rendered = t.render(Context({"message_context": "MONTH NAME"}))
181            self.assertEqual(rendered, "Mai")
182            t = Template(
183                '{% load i18n %}{% trans "May" context message_context|lower %}'
184            )
185            rendered = t.render(Context({"message_context": "VERB"}))
186            self.assertEqual(rendered, "Kann")
187
188            # Using 'as'
189            t = Template(
190                '{% load i18n %}{% trans "May" context "month name" as var %}Value: {{ var }}'
191            )
192            rendered = t.render(Context())
193            self.assertEqual(rendered, "Value: Mai")
194            t = Template(
195                '{% load i18n %}{% trans "May" as var context "verb" %}Value: {{ var }}'
196            )
197            rendered = t.render(Context())
198            self.assertEqual(rendered, "Value: Kann")
199
200
201class MultipleLocaleActivationTransTagTests(MultipleLocaleActivationTestCase):
202    def test_single_locale_activation(self):
203        """
204        Simple baseline behavior with one locale for all the supported i18n
205        constructs.
206        """
207        with translation.override("fr"):
208            self.assertEqual(
209                Template("{% load i18n %}{% trans 'Yes' %}").render(Context({})), "Oui"
210            )
211
212    def test_multiple_locale_trans(self):
213        with translation.override("de"):
214            t = Template("{% load i18n %}{% trans 'No' %}")
215        with translation.override(self._old_language), translation.override("nl"):
216            self.assertEqual(t.render(Context({})), "Nee")
217
218    def test_multiple_locale_deactivate_trans(self):
219        with translation.override("de", deactivate=True):
220            t = Template("{% load i18n %}{% trans 'No' %}")
221        with translation.override("nl"):
222            self.assertEqual(t.render(Context({})), "Nee")
223
224    def test_multiple_locale_direct_switch_trans(self):
225        with translation.override("de"):
226            t = Template("{% load i18n %}{% trans 'No' %}")
227        with translation.override("nl"):
228            self.assertEqual(t.render(Context({})), "Nee")
229
230
231class LocalizeNodeTests(SimpleTestCase):
232    def test_repr(self):
233        node = LocalizeNode(nodelist=[], use_l10n=True)
234        self.assertEqual(repr(node), "<LocalizeNode>")

tests/template_tests/syntax_tests/i18n/test_blocktrans.py

  1import os
  2
  3from asgiref.local import Local
  4
  5from django.template import Context, Template, TemplateSyntaxError
  6from django.test import SimpleTestCase, override_settings
  7from django.utils import translation
  8from django.utils.safestring import mark_safe
  9from django.utils.translation import trans_real
 10
 11from ...utils import setup
 12from .base import MultipleLocaleActivationTestCase, extended_locale_paths, here
 13
 14
 15class I18nBlockTransTagTests(SimpleTestCase):
 16    libraries = {"i18n": "django.templatetags.i18n"}
 17
 18    @setup({"i18n03": "{% load i18n %}{% blocktrans %}{{ anton }}{% endblocktrans %}"})
 19    def test_i18n03(self):
 20        """simple translation of a variable"""
 21        output = self.engine.render_to_string("i18n03", {"anton": "Å"})
 22        self.assertEqual(output, "Å")
 23
 24    @setup(
 25        {
 26            "i18n04": "{% load i18n %}{% blocktrans with berta=anton|lower %}{{ berta }}{% endblocktrans %}"
 27        }
 28    )
 29    def test_i18n04(self):
 30        """simple translation of a variable and filter"""
 31        output = self.engine.render_to_string("i18n04", {"anton": "Å"})
 32        self.assertEqual(output, "å")
 33
 34    @setup(
 35        {
 36            "legacyi18n04": "{% load i18n %}"
 37            "{% blocktrans with anton|lower as berta %}{{ berta }}{% endblocktrans %}"
 38        }
 39    )
 40    def test_legacyi18n04(self):
 41        """simple translation of a variable and filter"""
 42        output = self.engine.render_to_string("legacyi18n04", {"anton": "Å"})
 43        self.assertEqual(output, "å")
 44
 45    @setup(
 46        {
 47            "i18n05": "{% load i18n %}{% blocktrans %}xxx{{ anton }}xxx{% endblocktrans %}"
 48        }
 49    )
 50    def test_i18n05(self):
 51        """simple translation of a string with interpolation"""
 52        output = self.engine.render_to_string("i18n05", {"anton": "yyy"})
 53        self.assertEqual(output, "xxxyyyxxx")
 54
 55    @setup(
 56        {
 57            "i18n07": "{% load i18n %}"
 58            "{% blocktrans count counter=number %}singular{% plural %}"
 59            "{{ counter }} plural{% endblocktrans %}"
 60        }
 61    )
 62    def test_i18n07(self):
 63        """translation of singular form"""
 64        output = self.engine.render_to_string("i18n07", {"number": 1})
 65        self.assertEqual(output, "singular")
 66
 67    @setup(
 68        {
 69            "legacyi18n07": "{% load i18n %}"
 70            "{% blocktrans count number as counter %}singular{% plural %}"
 71            "{{ counter }} plural{% endblocktrans %}"
 72        }
 73    )
 74    def test_legacyi18n07(self):
 75        """translation of singular form"""
 76        output = self.engine.render_to_string("legacyi18n07", {"number": 1})
 77        self.assertEqual(output, "singular")
 78
 79    @setup(
 80        {
 81            "i18n08": "{% load i18n %}"
 82            "{% blocktrans count number as counter %}singular{% plural %}"
 83            "{{ counter }} plural{% endblocktrans %}"
 84        }
 85    )
 86    def test_i18n08(self):
 87        """translation of plural form"""
 88        output = self.engine.render_to_string("i18n08", {"number": 2})
 89        self.assertEqual(output, "2 plural")
 90
 91    @setup(
 92        {
 93            "legacyi18n08": "{% load i18n %}"
 94            "{% blocktrans count counter=number %}singular{% plural %}"
 95            "{{ counter }} plural{% endblocktrans %}"
 96        }
 97    )
 98    def test_legacyi18n08(self):
 99        """translation of plural form"""
100        output = self.engine.render_to_string("legacyi18n08", {"number": 2})
101        self.assertEqual(output, "2 plural")
102
103    @setup(
104        {
105            "i18n17": "{% load i18n %}"
106            "{% blocktrans with berta=anton|escape %}{{ berta }}{% endblocktrans %}"
107        }
108    )
109    def test_i18n17(self):
110        """
111        Escaping inside blocktrans and trans works as if it was directly in the
112        template.
113        """
114        output = self.engine.render_to_string("i18n17", {"anton": "α & β"})
115        self.assertEqual(output, "α &amp; β")
116
117    @setup(
118        {
119            "i18n18": "{% load i18n %}"
120            "{% blocktrans with berta=anton|force_escape %}{{ berta }}{% endblocktrans %}"
121        }
122    )
123    def test_i18n18(self):
124        output = self.engine.render_to_string("i18n18", {"anton": "α & β"})
125        self.assertEqual(output, "α &amp; β")
126
127    @setup({"i18n19": "{% load i18n %}{% blocktrans %}{{ andrew }}{% endblocktrans %}"})
128    def test_i18n19(self):
129        output = self.engine.render_to_string("i18n19", {"andrew": "a & b"})
130        self.assertEqual(output, "a &amp; b")
131
132    @setup({"i18n21": "{% load i18n %}{% blocktrans %}{{ andrew }}{% endblocktrans %}"})
133    def test_i18n21(self):
134        output = self.engine.render_to_string("i18n21", {"andrew": mark_safe("a & b")})
135        self.assertEqual(output, "a & b")
136
137    @setup(
138        {
139            "legacyi18n17": "{% load i18n %}"
140            "{% blocktrans with anton|escape as berta %}{{ berta }}{% endblocktrans %}"
141        }
142    )
143    def test_legacyi18n17(self):
144        output = self.engine.render_to_string("legacyi18n17", {"anton": "α & β"})
145        self.assertEqual(output, "α &amp; β")
146
147    @setup(
148        {
149            "legacyi18n18": "{% load i18n %}"
150            "{% blocktrans with anton|force_escape as berta %}"
151            "{{ berta }}{% endblocktrans %}"
152        }
153    )
154    def test_legacyi18n18(self):
155        output = self.engine.render_to_string("legacyi18n18", {"anton": "α & β"})
156        self.assertEqual(output, "α &amp; β")
157
158    @setup(
159        {
160            "i18n26": "{% load i18n %}"
161            "{% blocktrans with extra_field=myextra_field count counter=number %}"
162            "singular {{ extra_field }}{% plural %}plural{% endblocktrans %}"
163        }
164    )
165    def test_i18n26(self):
166        """
167        translation of plural form with extra field in singular form (#13568)
168        """
169        output = self.engine.render_to_string(
170            "i18n26", {"myextra_field": "test", "number": 1}
171        )
172        self.assertEqual(output, "singular test")
173
174    @setup(
175        {
176            "legacyi18n26": "{% load i18n %}"
177            "{% blocktrans with myextra_field as extra_field count number as counter %}"
178            "singular {{ extra_field }}{% plural %}plural{% endblocktrans %}"
179        }
180    )
181    def test_legacyi18n26(self):
182        output = self.engine.render_to_string(
183            "legacyi18n26", {"myextra_field": "test", "number": 1}
184        )
185        self.assertEqual(output, "singular test")
186
187    @setup(
188        {
189            "i18n27": "{% load i18n %}{% blocktrans count counter=number %}"
190            "{{ counter }} result{% plural %}{{ counter }} results"
191            "{% endblocktrans %}"
192        }
193    )
194    def test_i18n27(self):
195        """translation of singular form in Russian (#14126)"""
196        with translation.override("ru"):
197            output = self.engine.render_to_string("i18n27", {"number": 1})
198        self.assertEqual(
199            output, "1 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442"
200        )
201
202    @setup(
203        {
204            "legacyi18n27": "{% load i18n %}"
205            "{% blocktrans count number as counter %}{{ counter }} result"
206            "{% plural %}{{ counter }} results{% endblocktrans %}"
207        }
208    )
209    def test_legacyi18n27(self):
210        with translation.override("ru"):
211            output = self.engine.render_to_string("legacyi18n27", {"number": 1})
212        self.assertEqual(
213            output, "1 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442"
214        )
215
216    @setup(
217        {
218            "i18n28": "{% load i18n %}"
219            "{% blocktrans with a=anton b=berta %}{{ a }} + {{ b }}{% endblocktrans %}"
220        }
221    )
222    def test_i18n28(self):
223        """simple translation of multiple variables"""
224        output = self.engine.render_to_string("i18n28", {"anton": "α", "berta": "β"})
225        self.assertEqual(output, "α + β")
226
227    @setup(
228        {
229            "legacyi18n28": "{% load i18n %}"
230            "{% blocktrans with anton as a and berta as b %}"
231            "{{ a }} + {{ b }}{% endblocktrans %}"
232        }
233    )
234    def test_legacyi18n28(self):
235        output = self.engine.render_to_string(
236            "legacyi18n28", {"anton": "α", "berta": "β"}
237        )
238        self.assertEqual(output, "α + β")
239
240    # blocktrans handling of variables which are not in the context.
241    # this should work as if blocktrans was not there (#19915)
242    @setup(
243        {"i18n34": "{% load i18n %}{% blocktrans %}{{ missing }}{% endblocktrans %}"}
244    )
245    def test_i18n34(self):
246        output = self.engine.render_to_string("i18n34")
247        if self.engine.string_if_invalid:
248            self.assertEqual(output, "INVALID")
249        else:
250            self.assertEqual(output, "")
251
252    @setup(
253        {
254            "i18n34_2": "{% load i18n %}{% blocktrans with a='α' %}{{ missing }}{% endblocktrans %}"
255        }
256    )
257    def test_i18n34_2(self):
258        output = self.engine.render_to_string("i18n34_2")
259        if self.engine.string_if_invalid:
260            self.assertEqual(output, "INVALID")
261        else:
262            self.assertEqual(output, "")
263
264    @setup(
265        {
266            "i18n34_3": "{% load i18n %}{% blocktrans with a=anton %}{{ missing }}{% endblocktrans %}"
267        }
268    )
269    def test_i18n34_3(self):
270        output = self.engine.render_to_string("i18n34_3", {"anton": "\xce\xb1"})
271        if self.engine.string_if_invalid:
272            self.assertEqual(output, "INVALID")
273        else:
274            self.assertEqual(output, "")
275
276    @setup(
277        {
278            "i18n37": "{% load i18n %}"
279            '{% trans "Page not found" as page_not_found %}'
280            "{% blocktrans %}Error: {{ page_not_found }}{% endblocktrans %}"
281        }
282    )
283    def test_i18n37(self):
284        with translation.override("de"):
285            output = self.engine.render_to_string("i18n37")
286        self.assertEqual(output, "Error: Seite nicht gefunden")
287
288    # blocktrans tag with asvar
289    @setup(
290        {
291            "i18n39": "{% load i18n %}"
292            "{% blocktrans asvar page_not_found %}Page not found{% endblocktrans %}"
293            ">{{ page_not_found }}<"
294        }
295    )
296    def test_i18n39(self):
297        with translation.override("de"):
298            output = self.engine.render_to_string("i18n39")
299        self.assertEqual(output, ">Seite nicht gefunden<")
300
301    @setup(
302        {
303            "i18n40": "{% load i18n %}"
304            '{% trans "Page not found" as pg_404 %}'
305            "{% blocktrans with page_not_found=pg_404 asvar output %}"
306            "Error: {{ page_not_found }}"
307            "{% endblocktrans %}"
308        }
309    )
310    def test_i18n40(self):
311        output = self.engine.render_to_string("i18n40")
312        self.assertEqual(output, "")
313
314    @setup(
315        {
316            "i18n41": "{% load i18n %}"
317            '{% trans "Page not found" as pg_404 %}'
318            "{% blocktrans with page_not_found=pg_404 asvar output %}"
319            "Error: {{ page_not_found }}"
320            "{% endblocktrans %}"
321            ">{{ output }}<"
322        }
323    )
324    def test_i18n41(self):
325        with translation.override("de"):
326            output = self.engine.render_to_string("i18n41")
327        self.assertEqual(output, ">Error: Seite nicht gefunden<")
328
329    @setup({"template": "{% load i18n %}{% blocktrans asvar %}Yes{% endblocktrans %}"})
330    def test_blocktrans_syntax_error_missing_assignment(self):
331        msg = "No argument provided to the 'blocktrans' tag for the asvar option."
332        with self.assertRaisesMessage(TemplateSyntaxError, msg):
333            self.engine.render_to_string("template")
334
335    @setup({"template": "{% load i18n %}{% blocktrans %}%s{% endblocktrans %}"})
336    def test_blocktrans_tag_using_a_string_that_looks_like_str_fmt(self):
337        output = self.engine.render_to_string("template")
338        self.assertEqual(output, "%s")
339
340    @setup(
341        {
342            "template": "{% load i18n %}{% blocktrans %}{% block b %} {% endblock %}{% endblocktrans %}"
343        }
344    )
345    def test_with_block(self):
346        msg = "'blocktrans' doesn't allow other block tags (seen 'block b') inside it"
347        with self.assertRaisesMessage(TemplateSyntaxError, msg):
348            self.engine.render_to_string("template")
349
350    @setup(
351        {
352            "template": "{% load i18n %}{% blocktrans %}{% for b in [1, 2, 3] %} {% endfor %}{% endblocktrans %}"
353        }
354    )
355    def test_with_for(self):
356        msg = "'blocktrans' doesn't allow other block tags (seen 'for b in [1, 2, 3]') inside it"
357        with self.assertRaisesMessage(TemplateSyntaxError, msg):
358            self.engine.render_to_string("template")
359
360    @setup(
361        {
362            "template": "{% load i18n %}{% blocktrans with foo=bar with %}{{ foo }}{% endblocktrans %}"
363        }
364    )
365    def test_variable_twice(self):
366        with self.assertRaisesMessage(
367            TemplateSyntaxError, "The 'with' option was specified more than once"
368        ):
369            self.engine.render_to_string("template", {"foo": "bar"})
370
371    @setup({"template": "{% load i18n %}{% blocktrans with %}{% endblocktrans %}"})
372    def test_no_args_with(self):
373        msg = "\"with\" in 'blocktrans' tag needs at least one keyword argument."
374        with self.assertRaisesMessage(TemplateSyntaxError, msg):
375            self.engine.render_to_string("template")
376
377    @setup({"template": "{% load i18n %}{% blocktrans count a %}{% endblocktrans %}"})
378    def test_count(self):
379        msg = "\"count\" in 'blocktrans' tag expected exactly one keyword argument."
380        with self.assertRaisesMessage(TemplateSyntaxError, msg):
381            self.engine.render_to_string("template", {"a": [1, 2, 3]})
382
383    @setup(
384        {
385            "template": (
386                "{% load i18n %}{% blocktrans count count=var|length %}"
387                "There is {{ count }} object. {% block a %} {% endblock %}"
388                "{% endblocktrans %}"
389            )
390        }
391    )
392    def test_plural_bad_syntax(self):
393        msg = "'blocktrans' doesn't allow other block tags inside it"
394        with self.assertRaisesMessage(TemplateSyntaxError, msg):
395            self.engine.render_to_string("template", {"var": [1, 2, 3]})
396
397
398class TranslationBlockTransTagTests(SimpleTestCase):
399    @override_settings(LOCALE_PATHS=extended_locale_paths)
400    def test_template_tags_pgettext(self):
401        """{% blocktrans %} takes message contexts into account (#14806)."""
402        trans_real._active = Local()
403        trans_real._translations = {}
404        with translation.override("de"):
405            # Nonexistent context
406            t = Template(
407                '{% load i18n %}{% blocktrans context "nonexistent" %}May{% endblocktrans %}'
408            )
409            rendered = t.render(Context())
410            self.assertEqual(rendered, "May")
411
412            # Existing context...  using a literal
413            t = Template(
414                '{% load i18n %}{% blocktrans context "month name" %}May{% endblocktrans %}'
415            )
416            rendered = t.render(Context())
417            self.assertEqual(rendered, "Mai")
418            t = Template(
419                '{% load i18n %}{% blocktrans context "verb" %}May{% endblocktrans %}'
420            )
421            rendered = t.render(Context())
422            self.assertEqual(rendered, "Kann")
423
424            # Using a variable
425            t = Template(
426                "{% load i18n %}{% blocktrans context message_context %}May{% endblocktrans %}"
427            )
428            rendered = t.render(Context({"message_context": "month name"}))
429            self.assertEqual(rendered, "Mai")
430            t = Template(
431                "{% load i18n %}{% blocktrans context message_context %}May{% endblocktrans %}"
432            )
433            rendered = t.render(Context({"message_context": "verb"}))
434            self.assertEqual(rendered, "Kann")
435
436            # Using a filter
437            t = Template(
438                "{% load i18n %}{% blocktrans context message_context|lower %}May{% endblocktrans %}"
439            )
440            rendered = t.render(Context({"message_context": "MONTH NAME"}))
441            self.assertEqual(rendered, "Mai")
442            t = Template(
443                "{% load i18n %}{% blocktrans context message_context|lower %}May{% endblocktrans %}"
444            )
445            rendered = t.render(Context({"message_context": "VERB"}))
446            self.assertEqual(rendered, "Kann")
447
448            # Using 'count'
449            t = Template(
450                '{% load i18n %}{% blocktrans count number=1 context "super search" %}'
451                "{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}"
452            )
453            rendered = t.render(Context())
454            self.assertEqual(rendered, "1 Super-Ergebnis")
455            t = Template(
456                '{% load i18n %}{% blocktrans count number=2 context "super search" %}{{ number }}'
457                " super result{% plural %}{{ number }} super results{% endblocktrans %}"
458            )
459            rendered = t.render(Context())
460            self.assertEqual(rendered, "2 Super-Ergebnisse")
461            t = Template(
462                '{% load i18n %}{% blocktrans context "other super search" count number=1 %}'
463                "{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}"
464            )
465            rendered = t.render(Context())
466            self.assertEqual(rendered, "1 anderen Super-Ergebnis")
467            t = Template(
468                '{% load i18n %}{% blocktrans context "other super search" count number=2 %}'
469                "{{ number }} super result{% plural %}{{ number }} super results{% endblocktrans %}"
470            )
471            rendered = t.render(Context())
472            self.assertEqual(rendered, "2 andere Super-Ergebnisse")
473
474            # Using 'with'
475            t = Template(
476                '{% load i18n %}{% blocktrans with num_comments=5 context "comment count" %}'
477                "There are {{ num_comments }} comments{% endblocktrans %}"
478            )
479            rendered = t.render(Context())
480            self.assertEqual(rendered, "Es gibt 5 Kommentare")
481            t = Template(
482                '{% load i18n %}{% blocktrans with num_comments=5 context "other comment count" %}'
483                "There are {{ num_comments }} comments{% endblocktrans %}"
484            )
485            rendered = t.render(Context())
486            self.assertEqual(rendered, "Andere: Es gibt 5 Kommentare")
487
488            # Using trimmed
489            t = Template(
490                "{% load i18n %}{% blocktrans trimmed %}\n\nThere\n\t are 5  "
491                "\n\n   comments\n{% endblocktrans %}"
492            )
493            rendered = t.render(Context())
494            self.assertEqual(rendered, "There are 5 comments")
495            t = Template(
496                '{% load i18n %}{% blocktrans with num_comments=5 context "comment count" trimmed %}\n\n'
497                "There are  \t\n  \t {{ num_comments }} comments\n\n{% endblocktrans %}"
498            )
499            rendered = t.render(Context())
500            self.assertEqual(rendered, "Es gibt 5 Kommentare")
501            t = Template(
502                '{% load i18n %}{% blocktrans context "other super search" count number=2 trimmed %}\n'
503                "{{ number }} super \n result{% plural %}{{ number }} super results{% endblocktrans %}"
504            )
505            rendered = t.render(Context())
506            self.assertEqual(rendered, "2 andere Super-Ergebnisse")
507
508            # Misuses
509            msg = "Unknown argument for 'blocktrans' tag: %r."
510            with self.assertRaisesMessage(TemplateSyntaxError, msg % 'month="May"'):
511                Template(
512                    '{% load i18n %}{% blocktrans context with month="May" %}{{ month }}{% endblocktrans %}'
513                )
514            msg = '"context" in %r tag expected exactly one argument.' % "blocktrans"
515            with self.assertRaisesMessage(TemplateSyntaxError, msg):
516                Template("{% load i18n %}{% blocktrans context %}{% endblocktrans %}")
517            with self.assertRaisesMessage(TemplateSyntaxError, msg):
518                Template(
519                    "{% load i18n %}{% blocktrans count number=2 context %}"
520                    "{{ number }} super result{% plural %}{{ number }}"
521                    " super results{% endblocktrans %}"
522                )
523
524    @override_settings(LOCALE_PATHS=[os.path.join(here, "other", "locale")])
525    def test_bad_placeholder_1(self):
526        """
527        Error in translation file should not crash template rendering (#16516).
528        (%(person)s is translated as %(personne)s in fr.po).
529        """
530        with translation.override("fr"):
531            t = Template(
532                "{% load i18n %}{% blocktrans %}My name is {{ person }}.{% endblocktrans %}"
533            )
534            rendered = t.render(Context({"person": "James"}))
535            self.assertEqual(rendered, "My name is James.")
536
537    @override_settings(LOCALE_PATHS=[os.path.join(here, "other", "locale")])
538    def test_bad_placeholder_2(self):
539        """
540        Error in translation file should not crash template rendering (#18393).
541        (%(person) misses a 's' in fr.po, causing the string formatting to fail)
542        .
543        """
544        with translation.override("fr"):
545            t = Template(
546                "{% load i18n %}{% blocktrans %}My other name is {{ person }}.{% endblocktrans %}"
547            )
548            rendered = t.render(Context({"person": "James"}))
549            self.assertEqual(rendered, "My other name is James.")
550
551
552class MultipleLocaleActivationBlockTransTests(MultipleLocaleActivationTestCase):
553    def test_single_locale_activation(self):
554        """
555        Simple baseline behavior with one locale for all the supported i18n
556        constructs.
557        """
558        with translation.override("fr"):
559            self.assertEqual(
560                Template(
561                    "{% load i18n %}{% blocktrans %}Yes{% endblocktrans %}"
562                ).render(Context({})),
563                "Oui",
564            )
565
566    def test_multiple_locale_btrans(self):
567        with translation.override("de"):
568            t = Template("{% load i18n %}{% blocktrans %}No{% endblocktrans %}")
569        with translation.override(self._old_language), translation.override("nl"):
570            self.assertEqual(t.render(Context({})), "Nee")
571
572    def test_multiple_locale_deactivate_btrans(self):
573        with translation.override("de", deactivate=True):
574            t = Template("{% load i18n %}{% blocktrans %}No{% endblocktrans %}")
575        with translation.override("nl"):
576            self.assertEqual(t.render(Context({})), "Nee")
577
578    def test_multiple_locale_direct_switch_btrans(self):
579        with translation.override("de"):
580            t = Template("{% load i18n %}{% blocktrans %}No{% endblocktrans %}")
581        with translation.override("nl"):
582            self.assertEqual(t.render(Context({})), "Nee")
583
584
585class MiscTests(SimpleTestCase):
586    @override_settings(LOCALE_PATHS=extended_locale_paths)
587    def test_percent_in_translatable_block(self):
588        t_sing = Template(
589            "{% load i18n %}{% blocktrans %}The result was {{ percent }}%{% endblocktrans %}"
590        )
591        t_plur = Template(
592            "{% load i18n %}{% blocktrans count num as number %}"
593            "{{ percent }}% represents {{ num }} object{% plural %}"
594            "{{ percent }}% represents {{ num }} objects{% endblocktrans %}"
595        )
596        with translation.override("de"):
597            self.assertEqual(
598                t_sing.render(Context({"percent": 42})), "Das Ergebnis war 42%"
599            )
600            self.assertEqual(
601                t_plur.render(Context({"percent": 42, "num": 1})),
602                "42% stellt 1 Objekt dar",
603            )
604            self.assertEqual(
605                t_plur.render(Context({"percent": 42, "num": 4})),
606                "42% stellt 4 Objekte dar",
607            )
608
609    @override_settings(LOCALE_PATHS=extended_locale_paths)
610    def test_percent_formatting_in_blocktrans(self):
611        """
612        Python's %-formatting is properly escaped in blocktrans, singular, or
613        plural.
614        """
615        t_sing = Template(
616            "{% load i18n %}{% blocktrans %}There are %(num_comments)s comments{% endblocktrans %}"
617        )
618        t_plur = Template(
619            "{% load i18n %}{% blocktrans count num as number %}"
620            "%(percent)s% represents {{ num }} object{% plural %}"
621            "%(percent)s% represents {{ num }} objects{% endblocktrans %}"
622        )
623        with translation.override("de"):
624            # Strings won't get translated as they don't match after escaping %
625            self.assertEqual(
626                t_sing.render(Context({"num_comments": 42})),
627                "There are %(num_comments)s comments",
628            )
629            self.assertEqual(
630                t_plur.render(Context({"percent": 42, "num": 1})),
631                "%(percent)s% represents 1 object",
632            )
633            self.assertEqual(
634                t_plur.render(Context({"percent": 42, "num": 4})),
635                "%(percent)s% represents 4 objects",
636            )