This repository has been archived by the owner on Mar 15, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #187 from mozilla/feature/url-case
Add custom middleware and URL resolvers for ISO 3166 language prefixes.
- Loading branch information
Showing
6 changed files
with
218 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
from django.conf import settings | ||
from django.conf.urls.i18n import is_language_prefix_patterns_used | ||
from django.middleware.locale import LocaleMiddleware as BaseLocaleMiddleware | ||
from django.urls import get_script_prefix, is_valid_path | ||
from django.utils import translation | ||
from django.utils.cache import patch_vary_headers | ||
|
||
from .utils import get_language_from_request, language_code_to_iso_3166 | ||
|
||
|
||
class LocaleMiddleware(BaseLocaleMiddleware): | ||
""" | ||
A replacement for django.middleware.locale.LocaleMiddleware that | ||
redirects to an ISO 3166 format language prefix (en-US) instead of | ||
the form used by Django (en-us) | ||
""" | ||
|
||
def process_request(self, request): | ||
urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF) | ||
i18n_patterns_used, prefixed_default_language = is_language_prefix_patterns_used(urlconf) | ||
# We're using our version of get_language_from_request here. | ||
language = get_language_from_request(request, check_path=i18n_patterns_used) | ||
language_from_path = translation.get_language_from_path(request.path_info) | ||
if not language_from_path and i18n_patterns_used and not prefixed_default_language: | ||
language = settings.LANGUAGE_CODE | ||
translation.activate(language) | ||
request.LANGUAGE_CODE = translation.get_language() | ||
|
||
def process_response(self, request, response): | ||
language = translation.get_language() | ||
# Rewrite language to the format we want it in. | ||
language = language_code_to_iso_3166(language) | ||
language_from_path = translation.get_language_from_path(request.path_info) | ||
urlconf = getattr(request, 'urlconf', settings.ROOT_URLCONF) | ||
i18n_patterns_used, prefixed_default_language = is_language_prefix_patterns_used(urlconf) | ||
if (response.status_code == 404 and not language_from_path and | ||
i18n_patterns_used and prefixed_default_language): | ||
# Maybe the language code is missing in the URL? Try adding the | ||
# language prefix and redirecting to that URL. | ||
language_path = '/%s%s' % (language, request.path_info) | ||
path_valid = is_valid_path(language_path, urlconf) | ||
path_needs_slash = ( | ||
not path_valid and ( | ||
settings.APPEND_SLASH and not language_path.endswith('/') and | ||
is_valid_path('%s/' % language_path, urlconf) | ||
) | ||
) | ||
|
||
if path_valid or path_needs_slash: | ||
script_prefix = get_script_prefix() | ||
# Insert language after the script prefix and before the | ||
# rest of the URL | ||
language_url = request.get_full_path(force_append_slash=path_needs_slash).replace( | ||
script_prefix, | ||
'%s%s/' % (script_prefix, language), | ||
1 | ||
) | ||
return self.response_redirect_class(language_url) | ||
|
||
if not (i18n_patterns_used and language_from_path): | ||
patch_vary_headers(response, ('Accept-Language',)) | ||
response.setdefault('Content-Language', language) | ||
return response |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from django.http import HttpResponse | ||
from django.test import RequestFactory, TestCase | ||
from django.utils import translation | ||
|
||
from ..middleware import LocaleMiddleware | ||
|
||
|
||
class LocaleMiddlewareTestCase(TestCase): | ||
|
||
def test_process_request_sets_iso_3166_language_code(self): | ||
request = RequestFactory().get('/en-GB/') | ||
LocaleMiddleware().process_request(request) | ||
self.assertEqual(request.LANGUAGE_CODE, 'en-gb') | ||
|
||
def test_process_response_redirects_to_iso_3166_language_code(self): | ||
request = RequestFactory().get('/') | ||
response = HttpResponse(status=404) | ||
translation.activate('en-gb') | ||
new_response = LocaleMiddleware().process_response(request, response) | ||
self.assertEqual(new_response.status_code, 302) | ||
self.assertEqual(new_response['Location'], '/en-GB/') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
from django.test import RequestFactory, TestCase | ||
from django.utils import translation | ||
|
||
from ..utils import ISO3166LocalePrefixPattern, get_language_from_request, language_code_to_iso_3166 | ||
|
||
|
||
class UtilsTestCase(TestCase): | ||
|
||
def test_get_language_code_to_iso_3166(self): | ||
self.assertEqual(language_code_to_iso_3166('en-gb'), 'en-GB') | ||
self.assertEqual(language_code_to_iso_3166('en-us'), 'en-US') | ||
self.assertEqual(language_code_to_iso_3166('fr'), 'fr') | ||
|
||
def test_ISO3166LocalePrefixPattern(self): | ||
translation.deactivate() | ||
pattern = ISO3166LocalePrefixPattern(prefix_default_language=True) | ||
# Pattern should not match lowercase URLs | ||
self.assertIsNone(pattern.match('en-us/')) | ||
# It should match this URL | ||
self.assertTrue(pattern.match('en-US/')) | ||
|
||
def test_get_language_from_request_returns_iso_3166_language(self): | ||
request = RequestFactory().get('/') | ||
request.META['HTTP_ACCEPT_LANGUAGE'] = 'en-GB,en;q=0.5' | ||
language = get_language_from_request(request) | ||
self.assertEqual(language, 'en-GB') | ||
|
||
def test_get_language_from_request_fallback_language(self): | ||
request = RequestFactory().get('/') | ||
request.META['HTTP_ACCEPT_LANGUAGE'] = 'en-FO,en;q=0.5' | ||
language = get_language_from_request(request) | ||
self.assertEqual(language, 'en-US') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
from django.conf import settings | ||
from django.urls import LocalePrefixPattern, URLResolver | ||
from django.utils.translation.trans_real import ( | ||
check_for_language, get_languages, get_language_from_path, | ||
get_supported_language_variant, parse_accept_lang_header, language_code_re | ||
) | ||
|
||
|
||
class ISO3166LocalePrefixPattern(LocalePrefixPattern): | ||
"""LocalePrefixPattern subclass that enforces URL prefixes in the form en-US""" | ||
|
||
def match(self, path): | ||
language_prefix = language_code_to_iso_3166(self.language_prefix) | ||
if path.startswith(language_prefix): | ||
return path[len(language_prefix):], (), {} | ||
return None | ||
|
||
|
||
def i18n_patterns(*urls, prefix_default_language=True): | ||
""" | ||
Replacement for django.conf.urls.i18_patterns that uses ISO3166LocalePrefixPattern | ||
instead of django.urls.resolvers.LocalePrefixPattern. | ||
""" | ||
if not settings.USE_I18N: | ||
return list(urls) | ||
return [ | ||
URLResolver( | ||
ISO3166LocalePrefixPattern(prefix_default_language=prefix_default_language), | ||
list(urls), | ||
) | ||
] | ||
|
||
|
||
def language_code_to_iso_3166(language): | ||
"""Turn a language name (en-us) into an ISO 3166 format (en-US).""" | ||
language, _, country = language.lower().partition('-') | ||
if country: | ||
return language + '-' + country.upper() | ||
return language | ||
|
||
|
||
def get_language_from_request(request, check_path=False): | ||
""" | ||
Replacement for django.utils.translation.get_language_from_request. | ||
The portion of code that is modified is identified below with a comment. | ||
""" | ||
if check_path: | ||
lang_code = get_language_from_path(request.path_info) | ||
if lang_code is not None: | ||
return lang_code | ||
|
||
lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME) | ||
if lang_code is not None and lang_code in get_languages() and check_for_language(lang_code): | ||
return lang_code | ||
|
||
try: | ||
return get_supported_language_variant(lang_code) | ||
except LookupError: | ||
pass | ||
|
||
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', '') | ||
for accept_lang, unused in parse_accept_lang_header(accept): | ||
if accept_lang == '*': | ||
break | ||
|
||
# Convert lowercase region to uppercase before attempting to find a variant. | ||
# This is the only portion of code that is modified from the core function. | ||
accept_lang = language_code_to_iso_3166(accept_lang) | ||
|
||
if not language_code_re.search(accept_lang): | ||
continue | ||
|
||
try: | ||
return get_supported_language_variant(accept_lang) | ||
except LookupError: | ||
continue | ||
|
||
try: | ||
return get_supported_language_variant(settings.LANGUAGE_CODE) | ||
except LookupError: | ||
return settings.LANGUAGE_CODE |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters