Fixed #25933 -- Allowed an unprefixed default language in i18n_patterns().

This commit is contained in:
Krzysztof Urbaniak 2016-02-29 09:24:19 +01:00 committed by Tim Graham
parent 4b129ac81f
commit 839a955d08
7 changed files with 112 additions and 30 deletions

View File

@ -1,10 +1,11 @@
from django.conf import settings from django.conf import settings
from django.conf.urls import url from django.conf.urls import url
from django.urls import LocaleRegexURLResolver from django.urls import LocaleRegexURLResolver, get_resolver
from django.utils import lru_cache
from django.views.i18n import set_language from django.views.i18n import set_language
def i18n_patterns(*urls): def i18n_patterns(*urls, **kwargs):
""" """
Adds the language code prefix to every URL pattern within this Adds the language code prefix to every URL pattern within this
function. This may only be used in the root URLconf, not in an included function. This may only be used in the root URLconf, not in an included
@ -12,7 +13,23 @@ def i18n_patterns(*urls):
""" """
if not settings.USE_I18N: if not settings.USE_I18N:
return urls return urls
return [LocaleRegexURLResolver(list(urls))] prefix_default_language = kwargs.pop('prefix_default_language', True)
assert not kwargs, 'Unexpected kwargs for i18n_patterns(): %s' % kwargs
return [LocaleRegexURLResolver(list(urls), prefix_default_language=prefix_default_language)]
@lru_cache.lru_cache(maxsize=None)
def is_language_prefix_patterns_used(urlconf):
"""
Return a tuple of two booleans: (
`True` if LocaleRegexURLResolver` is used in the `urlconf`,
`True` if the default language should be prefixed
)
"""
for url_pattern in get_resolver(urlconf).url_patterns:
if isinstance(url_pattern, LocaleRegexURLResolver):
return True, url_pattern.prefix_default_language
return False, False
urlpatterns = [ urlpatterns = [

View File

@ -1,11 +1,10 @@
"This is the locale selecting middleware that will look at accept headers" "This is the locale selecting middleware that will look at accept headers"
from django.conf import settings from django.conf import settings
from django.conf.urls.i18n import is_language_prefix_patterns_used
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.urls import ( from django.urls import get_script_prefix, is_valid_path
LocaleRegexURLResolver, get_resolver, get_script_prefix, is_valid_path, from django.utils import translation
)
from django.utils import lru_cache, translation
from django.utils.cache import patch_vary_headers from django.utils.cache import patch_vary_headers
@ -21,9 +20,10 @@ class LocaleMiddleware(object):
def process_request(self, request): def process_request(self, request):
urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF) urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF)
language = translation.get_language_from_request( i18n_patterns_used, prefixed_default_language = is_language_prefix_patterns_used(urlconf)
request, check_path=self.is_language_prefix_patterns_used(urlconf) language = translation.get_language_from_request(request, check_path=i18n_patterns_used)
) if not language and i18n_patterns_used and not prefixed_default_language:
language = settings.LANGUAGE_CODE
translation.activate(language) translation.activate(language)
request.LANGUAGE_CODE = translation.get_language() request.LANGUAGE_CODE = translation.get_language()
@ -31,8 +31,12 @@ class LocaleMiddleware(object):
language = translation.get_language() language = translation.get_language()
language_from_path = translation.get_language_from_path(request.path_info) language_from_path = translation.get_language_from_path(request.path_info)
urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF) urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF)
if (response.status_code == 404 and not language_from_path i18n_patterns_used, prefixed_default_language = is_language_prefix_patterns_used(urlconf)
and self.is_language_prefix_patterns_used(urlconf)):
if not language_from_path and i18n_patterns_used and not prefixed_default_language:
language_from_path = settings.LANGUAGE_CODE
if response.status_code == 404 and not language_from_path and i18n_patterns_used:
language_path = '/%s%s' % (language, request.path_info) language_path = '/%s%s' % (language, request.path_info)
path_valid = is_valid_path(language_path, urlconf) path_valid = is_valid_path(language_path, urlconf)
path_needs_slash = ( path_needs_slash = (
@ -53,20 +57,8 @@ class LocaleMiddleware(object):
) )
return self.response_redirect_class(language_url) return self.response_redirect_class(language_url)
if not (self.is_language_prefix_patterns_used(urlconf) if not (i18n_patterns_used and language_from_path):
and language_from_path):
patch_vary_headers(response, ('Accept-Language',)) patch_vary_headers(response, ('Accept-Language',))
if 'Content-Language' not in response: if 'Content-Language' not in response:
response['Content-Language'] = language response['Content-Language'] = language
return response return response
@lru_cache.lru_cache(maxsize=None)
def is_language_prefix_patterns_used(self, urlconf):
"""
Returns `True` if the `LocaleRegexURLResolver` is used
at root level of the urlpatterns, else it returns `False`.
"""
for url_pattern in get_resolver(urlconf).url_patterns:
if isinstance(url_pattern, LocaleRegexURLResolver):
return True
return False

View File

@ -382,15 +382,22 @@ class LocaleRegexURLResolver(RegexURLResolver):
Rather than taking a regex argument, we just override the ``regex`` Rather than taking a regex argument, we just override the ``regex``
function to always return the active language-code as regex. function to always return the active language-code as regex.
""" """
def __init__(self, urlconf_name, default_kwargs=None, app_name=None, namespace=None): def __init__(
self, urlconf_name, default_kwargs=None, app_name=None, namespace=None,
prefix_default_language=True,
):
super(LocaleRegexURLResolver, self).__init__( super(LocaleRegexURLResolver, self).__init__(
None, urlconf_name, default_kwargs, app_name, namespace, None, urlconf_name, default_kwargs, app_name, namespace,
) )
self.prefix_default_language = prefix_default_language
@property @property
def regex(self): def regex(self):
language_code = get_language() or settings.LANGUAGE_CODE language_code = get_language() or settings.LANGUAGE_CODE
if language_code not in self._regex_dict: if language_code not in self._regex_dict:
regex_compiled = re.compile('^%s/' % language_code, re.UNICODE) if language_code == settings.LANGUAGE_CODE and not self.prefix_default_language:
self._regex_dict[language_code] = regex_compiled regex_string = ''
else:
regex_string = '^%s/' % language_code
self._regex_dict[language_code] = re.compile(regex_string, re.UNICODE)
return self._regex_dict[language_code] return self._regex_dict[language_code]

View File

@ -248,6 +248,10 @@ Internationalization
used in a root URLConf specified using :attr:`request.urlconf used in a root URLConf specified using :attr:`request.urlconf
<django.http.HttpRequest.urlconf>`. <django.http.HttpRequest.urlconf>`.
* By setting the new ``prefix_default_language`` parameter for
:func:`~django.conf.urls.i18n.i18n_patterns` to ``False``, you can allow
accessing the default language without a URL prefix.
Management Commands Management Commands
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~

View File

@ -1326,11 +1326,17 @@ Django provides two mechanisms to internationalize URL patterns:
Language prefix in URL patterns Language prefix in URL patterns
------------------------------- -------------------------------
.. function:: i18n_patterns(*pattern_list) .. function:: i18n_patterns(*urls, prefix_default_language=True)
This function can be used in a root URLconf and Django will automatically This function can be used in a root URLconf and Django will automatically
prepend the current active language code to all url patterns defined within prepend the current active language code to all url patterns defined within
:func:`~django.conf.urls.i18n.i18n_patterns`. Example URL patterns:: :func:`~django.conf.urls.i18n.i18n_patterns`.
Setting ``prefix_default_language`` to ``False`` removes the prefix from the
default language (:setting:`LANGUAGE_CODE`). This can be useful when adding
translations to existing site so that the current URLs won't change.
Example URL patterns::
from django.conf.urls import include, url from django.conf.urls import include, url
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
@ -1371,6 +1377,21 @@ function. Example::
>>> reverse('news:detail', kwargs={'slug': 'news-slug'}) >>> reverse('news:detail', kwargs={'slug': 'news-slug'})
'/nl/news/news-slug/' '/nl/news/news-slug/'
With ``prefix_default_language=False`` and ``LANGUAGE_CODE='en'``, the URLs
will be::
>>> activate('en')
>>> reverse('news:index')
'/news/'
>>> activate('nl')
>>> reverse('news:index')
'/nl/news/'
.. versionadded:: 1.10
The ``prefix_default_language`` parameter was added.
.. warning:: .. warning::
:func:`~django.conf.urls.i18n.i18n_patterns` is only allowed in a root :func:`~django.conf.urls.i18n.i18n_patterns` is only allowed in a root

View File

@ -13,6 +13,7 @@ from unittest import skipUnless
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.conf.urls.i18n import i18n_patterns
from django.template import Context, Template, TemplateSyntaxError from django.template import Context, Template, TemplateSyntaxError
from django.test import ( from django.test import (
RequestFactory, SimpleTestCase, TestCase, override_settings, RequestFactory, SimpleTestCase, TestCase, override_settings,
@ -1781,6 +1782,37 @@ class LocaleMiddlewareTests(TestCase):
self.assertNotIn(LANGUAGE_SESSION_KEY, self.client.session) self.assertNotIn(LANGUAGE_SESSION_KEY, self.client.session)
@override_settings(
USE_I18N=True,
LANGUAGES=[
('en', 'English'),
('fr', 'French'),
],
MIDDLEWARE_CLASSES=[
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
],
ROOT_URLCONF='i18n.urls_default_unprefixed',
LANGUAGE_CODE='en',
)
class UnprefixedDefaultLanguageTests(SimpleTestCase):
def test_default_lang_without_prefix(self):
"""
With i18n_patterns(..., prefix_default_language=False), the default
language (settings.LANGUAGE_CODE) should be accessible without a prefix.
"""
response = self.client.get('/simple/')
self.assertEqual(response.content, b'Yes')
def test_other_lang_with_prefix(self):
response = self.client.get('/fr/simple/')
self.assertEqual(response.content, b'Oui')
def test_unexpected_kwarg_to_i18n_patterns(self):
with self.assertRaisesMessage(AssertionError, "Unexpected kwargs for i18n_patterns(): {'foo':"):
i18n_patterns(object(), foo='bar')
@override_settings( @override_settings(
USE_I18N=True, USE_I18N=True,
LANGUAGES=[ LANGUAGES=[

View File

@ -0,0 +1,9 @@
from django.conf.urls import url
from django.conf.urls.i18n import i18n_patterns
from django.http import HttpResponse
from django.utils.translation import ugettext_lazy as _
urlpatterns = i18n_patterns(
url(r'^simple/$', lambda r: HttpResponse(_("Yes"))),
prefix_default_language=False,
)