ASGI (django/core/asgi.py, core/handlers/asgi.py) ¶
See also
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, "α & β")
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, "α & β")
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 & 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, "α & β")
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, "α & β")
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 ¶
See also
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 & 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, "α & β")
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, "α & β")
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 & 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, "α & β")
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, "α & β")
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 )