diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index c3a1253564..1ae323e902 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -5,6 +5,7 @@ from django.apps import apps as django_apps from django.conf import settings from django.core.exceptions import ImproperlyConfigured, PermissionDenied from django.utils.module_loading import import_string +from django.utils.translation import LANGUAGE_SESSION_KEY from django.middleware.csrf import rotate_token from .signals import user_logged_in, user_logged_out, user_login_failed @@ -108,12 +109,12 @@ def logout(request): # remember language choice saved to session # for backwards compatibility django_language is also checked (remove in 1.8) - language = request.session.get('_language', request.session.get('django_language')) + language = request.session.get(LANGUAGE_SESSION_KEY, request.session.get('django_language')) request.session.flush() if language is not None: - request.session['_language'] = language + request.session[LANGUAGE_SESSION_KEY] = language if hasattr(request, 'user'): from django.contrib.auth.models import AnonymousUser diff --git a/django/contrib/auth/tests/test_views.py b/django/contrib/auth/tests/test_views.py index d37afa3eee..605ed4462e 100644 --- a/django/contrib/auth/tests/test_views.py +++ b/django/contrib/auth/tests/test_views.py @@ -14,6 +14,7 @@ from django.http import QueryDict, HttpRequest from django.utils.encoding import force_text from django.utils.http import urlquote from django.utils.six.moves.urllib.parse import urlparse, ParseResult +from django.utils.translation import LANGUAGE_SESSION_KEY from django.utils._os import upath from django.test import TestCase, override_settings from django.test.utils import patch_logger @@ -718,12 +719,12 @@ class LogoutTest(AuthViewsTestCase): # Create a new session with language engine = import_module(settings.SESSION_ENGINE) session = engine.SessionStore() - session['_language'] = 'pl' + session[LANGUAGE_SESSION_KEY] = 'pl' session.save() self.client.cookies[settings.SESSION_COOKIE_NAME] = session.session_key self.client.get('/logout/') - self.assertEqual(self.client.session['_language'], 'pl') + self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], 'pl') @skipIfCustomUser diff --git a/django/utils/translation/__init__.py b/django/utils/translation/__init__.py index 9d46ce67e4..54e1244c23 100644 --- a/django/utils/translation/__init__.py +++ b/django/utils/translation/__init__.py @@ -21,6 +21,7 @@ __all__ = [ 'npgettext', 'npgettext_lazy', ] +LANGUAGE_SESSION_KEY = '_language' class TranslatorCommentWarning(SyntaxWarning): pass diff --git a/django/utils/translation/trans_real.py b/django/utils/translation/trans_real.py index 29c133006e..ea872e1ec9 100644 --- a/django/utils/translation/trans_real.py +++ b/django/utils/translation/trans_real.py @@ -18,7 +18,7 @@ from django.utils._os import upath from django.utils.safestring import mark_safe, SafeData from django.utils import six, lru_cache from django.utils.six import StringIO -from django.utils.translation import TranslatorCommentWarning, trim_whitespace +from django.utils.translation import TranslatorCommentWarning, trim_whitespace, LANGUAGE_SESSION_KEY # Translations are cached in a dictionary for every language+app tuple. @@ -478,7 +478,7 @@ def get_language_from_request(request, check_path=False): if hasattr(request, 'session'): # for backwards compatibility django_language is also checked (remove in 1.8) - lang_code = request.session.get('_language', request.session.get('django_language')) + lang_code = request.session.get(LANGUAGE_SESSION_KEY, request.session.get('django_language')) if lang_code in supported and lang_code is not None and check_for_language(lang_code): return lang_code diff --git a/django/views/i18n.py b/django/views/i18n.py index 7182f4f3cd..a62ca29e54 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -7,7 +7,7 @@ from django import http from django.apps import apps from django.conf import settings from django.template import Context, Template -from django.utils.translation import check_for_language, to_locale, get_language +from django.utils.translation import check_for_language, to_locale, get_language, LANGUAGE_SESSION_KEY from django.utils.encoding import smart_text from django.utils.formats import get_format_modules, get_format from django.utils._os import upath @@ -36,7 +36,7 @@ def set_language(request): lang_code = request.POST.get('language', None) if lang_code and check_for_language(lang_code): if hasattr(request, 'session'): - request.session['_language'] = lang_code + request.session[LANGUAGE_SESSION_KEY] = lang_code else: response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang_code) return response diff --git a/docs/ref/utils.txt b/docs/ref/utils.txt index 5313a1de19..15242f030f 100644 --- a/docs/ref/utils.txt +++ b/docs/ref/utils.txt @@ -937,6 +937,10 @@ For a complete discussion on the usage of the following see the so by translating the Django translation tags into standard gettext function invocations. +.. data:: LANGUAGE_SESSION_KEY + + Session key under which the active language for the current session is stored. + .. _time-zone-selection-functions: ``django.utils.timezone`` diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 400c5d0759..c7b545b501 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -537,11 +537,13 @@ Internationalization attribute allows you to customize the redirects issued by the middleware. * The :class:`~django.middleware.locale.LocaleMiddleware` now stores the user's - selected language with the session key ``_language``. Previously it was - stored with the key ``django_language``, but keys reserved for Django should - start with an underscore. For backwards compatibility ``django_language`` is - still read from in 1.7. Sessions will be migrated to the new ``_language`` - key as they are written. + selected language with the session key ``_language``. This should only be + accessed using the :data:`~django.utils.translation.LANGUAGE_SESSION_KEY` + constant. Previously it was stored with the key ``django_language`` and the + ``LANGUAGE_SESSION_KEY`` constant did not exist, but keys reserved for Django + should start with an underscore. For backwards compatibility ``django_language`` + is still read from in 1.7. Sessions will be migrated to the new key + as they are written. * The :ttag:`blocktrans` now supports a ``trimmed`` option. This option will remove newline characters from the beginning and the end of the diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 62729d55da..0a5fa925bc 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -1510,6 +1510,38 @@ Here's example HTML template code: In this example, Django looks up the URL of the page to which the user will be redirected in the ``redirect_to`` context variable. +Explicitly setting the active language +-------------------------------------- + +.. highlightlang:: python + +You may want to set the active language for the current session explicitly. Perhaps +a user's language preference is retrieved from another system, for example. +You've already been introduced to :func:`django.utils.translation.activate()`. That +applies to the current thread only. To persist the language for the entire +session, also modify :data:`~django.utils.translation.LANGUAGE_SESSION_KEY` +in the session:: + + from django.utils import translation + user_language = 'fr' + translation.activate(user_language) + request.session[translation.LANGUAGE_SESSION_KEY] = user_language + +You would typically want to use both: :func:`django.utils.translation.activate()` +will change the language for this thread, and modifying the session makes this +preference persist in future requests. + +If you are not using sessions, the language will persist in a cookie, whose name +is configured in :setting:`LANGUAGE_COOKIE_NAME`. For example:: + + from django.utils import translation + from django import http + from django.conf import settings + user_language = 'fr' + translation.activate(user_language) + response = http.HttpResponse(...) + response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language) + Using translations outside views and templates ---------------------------------------------- @@ -1621,13 +1653,13 @@ following this algorithm: root URLconf. See :ref:`url-internationalization` for more information about the language prefix and how to internationalize URL patterns. -* Failing that, it looks for a ``_language`` key in the current user's session. +* Failing that, it looks for the :data:`~django.utils.translation.LANGUAGE_SESSION_KEY` + key in the current user's session. .. versionchanged:: 1.7 - In previous versions, the key was named ``django_language`` but it was - renamed to start with an underscore to denote a Django reserved session - key. + In previous versions, the key was named ``django_language``, and the + ``LANGUAGE_SESSION_KEY`` constant did not exist. * Failing that, it looks for a cookie. diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index c7179d6117..56c3f63316 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -31,7 +31,7 @@ from django.utils.translation import (activate, deactivate, pgettext, npgettext, npgettext_lazy, check_for_language, - string_concat) + string_concat, LANGUAGE_SESSION_KEY) from .forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm from .models import Company, TestModel @@ -1267,7 +1267,7 @@ class LocaleMiddlewareTests(TestCase): session on every request.""" # Regression test for #21473 self.client.get('/fr/simple/') - self.assertNotIn('_language', self.client.session) + self.assertNotIn(LANGUAGE_SESSION_KEY, self.client.session) @override_settings( diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py index 72da88e92c..7d6466d44f 100644 --- a/tests/view_tests/tests/test_i18n.py +++ b/tests/view_tests/tests/test_i18n.py @@ -11,7 +11,7 @@ from django.test import ( LiveServerTestCase, TestCase, modify_settings, override_settings) from django.utils import six from django.utils._os import upath -from django.utils.translation import override +from django.utils.translation import override, LANGUAGE_SESSION_KEY try: from selenium.webdriver.firefox import webdriver as firefox @@ -35,7 +35,7 @@ class I18NTests(TestCase): post_data = dict(language=lang_code, next='/') response = self.client.post('/i18n/setlang/', data=post_data) self.assertRedirects(response, 'http://testserver/') - self.assertEqual(self.client.session['_language'], lang_code) + self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) def test_setlang_unsafe_next(self): """ @@ -46,7 +46,7 @@ class I18NTests(TestCase): post_data = dict(language=lang_code, next='//unsafe/redirection/') response = self.client.post('/i18n/setlang/', data=post_data) self.assertEqual(response.url, 'http://testserver/') - self.assertEqual(self.client.session['_language'], lang_code) + self.assertEqual(self.client.session[LANGUAGE_SESSION_KEY], lang_code) def test_setlang_reversal(self): self.assertEqual(reverse('set_language'), '/i18n/setlang/')