diff --git a/django/core/urlresolvers.py b/django/core/urlresolvers.py index 59114795bcb..cf42768add2 100644 --- a/django/core/urlresolvers.py +++ b/django/core/urlresolvers.py @@ -24,7 +24,8 @@ from django.utils.functional import cached_property, lazy from django.utils.http import RFC3986_SUBDELIMS, urlquote from django.utils.module_loading import module_has_submodule from django.utils.regex_helper import normalize -from django.utils.translation import get_language +from django.utils.six.moves.urllib.parse import urlsplit, urlunsplit +from django.utils.translation import get_language, override # SCRIPT_NAME prefixes for each thread are stored here. If there's no entry for # the current thread (which is the only one we ever access), it is assumed to @@ -652,3 +653,26 @@ def is_valid_path(path, urlconf=None): return True except Resolver404: return False + + +def translate_url(url, lang_code): + """ + Given a URL (absolute or relative), try to get its translated version in + the `lang_code` language (either by i18n_patterns or by translated regex). + Return the original URL if no translated version is found. + """ + parsed = urlsplit(url) + try: + match = resolve(parsed.path) + except Resolver404: + pass + else: + to_be_reversed = "%s:%s" % (match.namespace, match.url_name) if match.namespace else match.url_name + with override(lang_code): + try: + url = reverse(to_be_reversed, args=match.args, kwargs=match.kwargs) + except NoReverseMatch: + pass + else: + url = urlunsplit((parsed.scheme, parsed.netloc, url, parsed.query, parsed.fragment)) + return url diff --git a/django/views/i18n.py b/django/views/i18n.py index 7e21035319a..30ecbbeaf33 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -6,6 +6,7 @@ import os from django import http from django.apps import apps from django.conf import settings +from django.core.urlresolvers import translate_url from django.template import Context, Engine from django.utils import six from django.utils._os import upath @@ -37,6 +38,9 @@ def set_language(request): if request.method == 'POST': lang_code = request.POST.get('language', None) if lang_code and check_for_language(lang_code): + next_trans = translate_url(next, lang_code) + if next_trans != next: + response = http.HttpResponseRedirect(next_trans) if hasattr(request, 'session'): request.session[LANGUAGE_SESSION_KEY] = lang_code else: diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index da4984a9beb..e8dd955e29f 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -127,7 +127,8 @@ Generic Views Internationalization ^^^^^^^^^^^^^^^^^^^^ -* ... +* The :func:`django.views.i18n.set_language` view now properly redirects to + :ref:`translated URLs `, when available. Management Commands ^^^^^^^^^^^^^^^^^^^ diff --git a/tests/i18n/patterns/tests.py b/tests/i18n/patterns/tests.py index 1ac0e8a4a97..62ca49b67df 100644 --- a/tests/i18n/patterns/tests.py +++ b/tests/i18n/patterns/tests.py @@ -4,7 +4,7 @@ import os from django.core.exceptions import ImproperlyConfigured from django.core.urlresolvers import ( - clear_url_caches, reverse, set_script_prefix, + clear_url_caches, reverse, set_script_prefix, translate_url, ) from django.http import HttpResponsePermanentRedirect from django.middleware.locale import LocaleMiddleware @@ -135,6 +135,18 @@ class URLTranslationTests(URLTestCaseBase): with translation.override('pt-br'): self.assertEqual(reverse('users'), '/pt-br/usuarios/') + def test_translate_url_utility(self): + with translation.override('en'): + self.assertEqual(translate_url('/en/non-existent/', 'nl'), '/en/non-existent/') + self.assertEqual(translate_url('/en/users/', 'nl'), '/nl/gebruikers/') + # Namespaced URL + self.assertEqual(translate_url('/en/account/register/', 'nl'), '/nl/profiel/registeren/') + self.assertEqual(translation.get_language(), 'en') + + with translation.override('nl'): + self.assertEqual(translate_url('/nl/gebruikers/', 'en'), '/en/users/') + self.assertEqual(translation.get_language(), 'nl') + class URLNamespaceTests(URLTestCaseBase): """ diff --git a/tests/view_tests/locale/nl/LC_MESSAGES/django.mo b/tests/view_tests/locale/nl/LC_MESSAGES/django.mo index 90066d70ecf..ef12f2ade80 100644 Binary files a/tests/view_tests/locale/nl/LC_MESSAGES/django.mo and b/tests/view_tests/locale/nl/LC_MESSAGES/django.mo differ diff --git a/tests/view_tests/locale/nl/LC_MESSAGES/django.po b/tests/view_tests/locale/nl/LC_MESSAGES/django.po index e85f08ee801..4e5f7e2fb59 100644 --- a/tests/view_tests/locale/nl/LC_MESSAGES/django.po +++ b/tests/view_tests/locale/nl/LC_MESSAGES/django.po @@ -16,6 +16,10 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +#: urls.py:78 +msgid "^translated/$" +msgstr "^vertaald/$" + #: views/csrf.py:98 msgid "Forbidden" msgstr "Verboden" diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py index 2f47007b046..ef11d7d82aa 100644 --- a/tests/view_tests/tests/test_i18n.py +++ b/tests/view_tests/tests/test_i18n.py @@ -67,6 +67,23 @@ class I18NTests(TestCase): self.assertEqual(language_cookie['path'], '/test/') self.assertEqual(language_cookie['max-age'], 3600 * 7 * 2) + @modify_settings(MIDDLEWARE_CLASSES={ + 'append': 'django.middleware.locale.LocaleMiddleware', + }) + def test_lang_from_translated_i18n_pattern(self): + response = self.client.post( + '/i18n/setlang/', data={'language': 'nl'}, + follow=True, HTTP_REFERER='/en/translated/' + ) + self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], 'nl') + self.assertRedirects(response, 'http://testserver/nl/vertaald/') + # And reverse + response = self.client.post( + '/i18n/setlang/', data={'language': 'en'}, + follow=True, HTTP_REFERER='/nl/vertaald/' + ) + self.assertRedirects(response, 'http://testserver/en/translated/') + def test_jsi18n(self): """The javascript_catalog can be deployed with language settings""" for lang_code in ['es', 'fr', 'ru']: diff --git a/tests/view_tests/urls.py b/tests/view_tests/urls.py index 0bfcd587c24..061dd197515 100644 --- a/tests/view_tests/urls.py +++ b/tests/view_tests/urls.py @@ -2,7 +2,9 @@ from os import path from django.conf.urls import include, url +from django.conf.urls.i18n import i18n_patterns from django.utils._os import upath +from django.utils.translation import ugettext_lazy as _ from django.views import defaults, i18n, static from . import views @@ -73,6 +75,10 @@ urlpatterns = [ url(r'^site_media/(?P.*)$', static.serve, {'document_root': media_dir}), ] +urlpatterns += i18n_patterns( + url(_(r'^translated/$'), views.index_page, name='i18n_prefixed'), +) + urlpatterns += [ url(r'view_exception/(?P[0-9]+)/$', views.view_exception, name='view_exception'), url(r'template_exception/(?P[0-9]+)/$', views.template_exception, name='template_exception'),