diff --git a/django/views/i18n.py b/django/views/i18n.py index c3a813b0fe..ba8fce67e7 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -1,8 +1,6 @@ -import importlib import itertools import json import os -import warnings from django import http from django.apps import apps @@ -10,18 +8,15 @@ from django.conf import settings from django.template import Context, Engine from django.urls import translate_url from django.utils import six -from django.utils._os import upath -from django.utils.deprecation import RemovedInDjango20Warning from django.utils.encoding import force_text from django.utils.formats import get_format from django.utils.http import is_safe_url, urlunquote from django.utils.translation import ( - LANGUAGE_SESSION_KEY, check_for_language, get_language, to_locale, + LANGUAGE_SESSION_KEY, check_for_language, get_language, ) from django.utils.translation.trans_real import DjangoTranslation from django.views.generic import View -DEFAULT_PACKAGES = ['django.conf'] LANGUAGE_QUERY_PARAMETER = 'language' @@ -210,69 +205,6 @@ def render_javascript_catalog(catalog=None, plural=None): return http.HttpResponse(template.render(context), 'text/javascript') -def get_javascript_catalog(locale, domain, packages): - app_configs = apps.get_app_configs() - allowable_packages = set(app_config.name for app_config in app_configs) - allowable_packages.update(DEFAULT_PACKAGES) - packages = [p for p in packages if p in allowable_packages] - paths = [] - # paths of requested packages - for package in packages: - p = importlib.import_module(package) - path = os.path.join(os.path.dirname(upath(p.__file__)), 'locale') - paths.append(path) - - trans = DjangoTranslation(locale, domain=domain, localedirs=paths) - trans_cat = trans._catalog - - plural = None - if '' in trans_cat: - for line in trans_cat[''].split('\n'): - if line.startswith('Plural-Forms:'): - plural = line.split(':', 1)[1].strip() - if plural is not None: - # this should actually be a compiled function of a typical plural-form: - # Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : - # n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2; - plural = [el.strip() for el in plural.split(';') if el.strip().startswith('plural=')][0].split('=', 1)[1] - - pdict = {} - maxcnts = {} - catalog = {} - trans_fallback_cat = trans._fallback._catalog if trans._fallback else {} - for key, value in itertools.chain(six.iteritems(trans_cat), six.iteritems(trans_fallback_cat)): - if key == '' or key in catalog: - continue - if isinstance(key, six.string_types): - catalog[key] = value - elif isinstance(key, tuple): - msgid = key[0] - cnt = key[1] - maxcnts[msgid] = max(cnt, maxcnts.get(msgid, 0)) - pdict.setdefault(msgid, {})[cnt] = value - else: - raise TypeError(key) - for k, v in pdict.items(): - catalog[k] = [v.get(i, '') for i in range(maxcnts[k] + 1)] - - return catalog, plural - - -def _get_locale(request): - language = request.GET.get(LANGUAGE_QUERY_PARAMETER) - if not (language and check_for_language(language)): - language = get_language() - return to_locale(language) - - -def _parse_packages(packages): - if packages is None: - packages = list(DEFAULT_PACKAGES) - elif isinstance(packages, six.string_types): - packages = packages.split('+') - return packages - - def null_javascript_catalog(request, domain=None, packages=None): """ Returns "identity" versions of the JavaScript i18n functions -- i.e., @@ -281,61 +213,6 @@ def null_javascript_catalog(request, domain=None, packages=None): return render_javascript_catalog() -def javascript_catalog(request, domain='djangojs', packages=None): - """ - Returns the selected language catalog as a javascript library. - - Receives the list of packages to check for translations in the - packages parameter either from an infodict or as a +-delimited - string from the request. Default is 'django.conf'. - - Additionally you can override the gettext domain for this view, - but usually you don't want to do that, as JavaScript messages - go to the djangojs domain. But this might be needed if you - deliver your JavaScript source from Django templates. - """ - warnings.warn( - "The javascript_catalog() view is deprecated in favor of the " - "JavaScriptCatalog view.", RemovedInDjango20Warning, stacklevel=2 - ) - locale = _get_locale(request) - packages = _parse_packages(packages) - catalog, plural = get_javascript_catalog(locale, domain, packages) - return render_javascript_catalog(catalog, plural) - - -def json_catalog(request, domain='djangojs', packages=None): - """ - Return the selected language catalog as a JSON object. - - Receives the same parameters as javascript_catalog(), but returns - a response with a JSON object of the following format: - - { - "catalog": { - # Translations catalog - }, - "formats": { - # Language formats for date, time, etc. - }, - "plural": '...' # Expression for plural forms, or null. - } - """ - warnings.warn( - "The json_catalog() view is deprecated in favor of the " - "JSONCatalog view.", RemovedInDjango20Warning, stacklevel=2 - ) - locale = _get_locale(request) - packages = _parse_packages(packages) - catalog, plural = get_javascript_catalog(locale, domain, packages) - data = { - 'catalog': catalog, - 'formats': get_formats(), - 'plural': plural, - } - return http.JsonResponse(data) - - class JavaScriptCatalog(View): """ Return the selected language catalog as a JavaScript library. diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index b7b489499d..9d0d9efd93 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -384,7 +384,7 @@ Internationalization * The :func:`django.views.i18n.set_language` view now properly redirects to :ref:`translated URLs `, when available. -* The :func:`django.views.i18n.javascript_catalog` view now works correctly +* The ``django.views.i18n.javascript_catalog()`` view now works correctly if used multiple times with different configurations on the same page. * The :func:`django.utils.timezone.make_aware` function gained an ``is_dst`` @@ -394,7 +394,7 @@ Internationalization for languages which can be written in different scripts, for example Latin and Cyrillic (e.g. ``be@latin``). -* Added the :func:`django.views.i18n.json_catalog` view to help build a custom +* Added the ``django.views.i18n.json_catalog()`` view to help build a custom client-side i18n library upon Django translations. It returns a JSON object containing a translations catalog, formatting settings, and a plural rule. diff --git a/docs/releases/2.0.txt b/docs/releases/2.0.txt index 385b25b28d..210fc84dd0 100644 --- a/docs/releases/2.0.txt +++ b/docs/releases/2.0.txt @@ -366,3 +366,5 @@ these features. * The keyword arguments ``virtual_only`` in ``Field.contribute_to_class()`` and ``virtual`` in ``Model._meta.add_field()`` are removed. + +* The ``javascript_catalog()`` and ``json_catalog()`` views are removed. diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index aa85aff907..4605f998e5 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -1049,88 +1049,6 @@ If you use more than one ``JavaScriptCatalog`` view on a site and some of them define the same strings, the strings in the catalog that was loaded last take precedence. -The ``javascript_catalog`` view -------------------------------- - -.. function:: javascript_catalog(request, domain='djangojs', packages=None) - -.. deprecated:: 1.10 - - ``javascript_catalog()`` is deprecated in favor of - :class:`JavaScriptCatalog` and will be removed in Django 2.0. - -The main solution to these problems is the -:meth:`django.views.i18n.javascript_catalog` view, which sends out a JavaScript -code library with functions that mimic the ``gettext`` interface, plus an array -of translation strings. Those translation strings are taken from applications or -Django core, according to what you specify in either the ``info_dict`` or the -URL. Paths listed in :setting:`LOCALE_PATHS` are also included. - -You hook it up like this:: - - from django.views.i18n import javascript_catalog - - js_info_dict = { - 'packages': ('your.app.package',), - } - - urlpatterns = [ - url(r'^jsi18n/$', javascript_catalog, js_info_dict, name='javascript-catalog'), - ] - -Each string in ``packages`` should be in Python dotted-package syntax (the -same format as the strings in :setting:`INSTALLED_APPS`) and should refer to a -package that contains a ``locale`` directory. If you specify multiple packages, -all those catalogs are merged into one catalog. This is useful if you have -JavaScript that uses strings from different applications. - -The precedence of translations is such that the packages appearing later in the -``packages`` argument have higher precedence than the ones appearing at the -beginning, this is important in the case of clashing translations for the same -literal. - -By default, the view uses the ``djangojs`` gettext domain. This can be -changed by altering the ``domain`` argument. - -You can make the view dynamic by putting the packages into the URL pattern:: - - urlpatterns = [ - url(r'^jsi18n/(?P\S+?)/$', javascript_catalog, name='javascript-catalog'), - ] - -With this, you specify the packages as a list of package names delimited by '+' -signs in the URL. This is especially useful if your pages use code from -different apps and this changes often and you don't want to pull in one big -catalog file. As a security measure, these values can only be either -``django.conf`` or any package from the :setting:`INSTALLED_APPS` setting. - -You can also split the catalogs in multiple URLs and load them as you need in -your sites:: - - js_info_dict_app = { - 'packages': ('your.app.package',), - } - - js_info_dict_other_app = { - 'packages': ('your.other.app.package',), - } - - urlpatterns = [ - url(r'^jsi18n/app/$', javascript_catalog, js_info_dict_app), - url(r'^jsi18n/other_app/$', javascript_catalog, js_info_dict_other_app), - ] - -If you use more than one ``javascript_catalog`` on a site and some of them -define the same strings, the strings in the catalog that was loaded last take -precedence. - -The JavaScript translations found in the paths listed in the -:setting:`LOCALE_PATHS` setting are also always included. To keep consistency -with the translations lookup order algorithm used for Python and templates, the -directories listed in :setting:`LOCALE_PATHS` have the highest precedence with -the ones appearing first having higher precedence than the ones appearing -later. - Using the JavaScript translation catalog ---------------------------------------- @@ -1326,57 +1244,6 @@ The ``JSONCatalog`` view .. JSON doesn't allow comments so highlighting as JSON won't work here. -The ``json_catalog`` view -------------------------- - -.. function:: json_catalog(request, domain='djangojs', packages=None) - -.. deprecated:: 1.10 - - ``json_catalog()`` is deprecated in favor of :class:`JSONCatalog` and will - be removed in Django 2.0. - -In order to use another client-side library to handle translations, you may -want to take advantage of the ``json_catalog()`` view. It's similar to -:meth:`~django.views.i18n.javascript_catalog` but returns a JSON response. - -The JSON object contains i18n formatting settings (those available for -`get_format`_), a plural rule (as a ``plural`` part of a GNU gettext -``Plural-Forms`` expression), and translation strings. The translation strings -are taken from applications or Django's own translations, according to what is -specified either via ``urlpatterns`` arguments or as request parameters. Paths -listed in :setting:`LOCALE_PATHS` are also included. - -The view is hooked up to your application and configured in the same fashion as -:meth:`~django.views.i18n.javascript_catalog` (namely, the ``domain`` and -``packages`` arguments behave identically):: - - from django.views.i18n import json_catalog - - js_info_dict = { - 'packages': ('your.app.package',), - } - - urlpatterns = [ - url(r'^jsoni18n/$', json_catalog, js_info_dict), - ] - -The response format is as follows: - -.. code-block:: text - - { - "catalog": { - # Translations catalog - }, - "formats": { - # Language formats for date, time, etc. - }, - "plural": "..." # Expression for plural forms, or null. - } - -.. JSON doesn't allow comments so highlighting as JSON won't work here. - Note on performance ------------------- diff --git a/tests/view_tests/tests/test_i18n_deprecated.py b/tests/view_tests/tests/test_i18n_deprecated.py deleted file mode 100644 index e60a04ad01..0000000000 --- a/tests/view_tests/tests/test_i18n_deprecated.py +++ /dev/null @@ -1,244 +0,0 @@ -# -*- coding:utf-8 -*- -from __future__ import unicode_literals - -import gettext -import json -from os import path - -from django.conf import settings -from django.test import ( - SimpleTestCase, ignore_warnings, modify_settings, override_settings, -) -from django.test.selenium import SeleniumTestCase -from django.utils import six -from django.utils._os import upath -from django.utils.deprecation import RemovedInDjango20Warning -from django.utils.translation import override - -from ..urls import locale_dir - - -@override_settings(ROOT_URLCONF='view_tests.urls') -@ignore_warnings(category=RemovedInDjango20Warning) -class JsI18NTests(SimpleTestCase): - """ - Tests deprecated django views in django/views/i18n.py - """ - def test_jsi18n(self): - """The javascript_catalog can be deployed with language settings""" - for lang_code in ['es', 'fr', 'ru']: - with override(lang_code): - catalog = gettext.translation('djangojs', locale_dir, [lang_code]) - if six.PY3: - trans_txt = catalog.gettext('this is to be translated') - else: - trans_txt = catalog.ugettext('this is to be translated') - response = self.client.get('/old_jsi18n/') - # response content must include a line like: - # "this is to be translated": - # json.dumps() is used to be able to check unicode strings - self.assertContains(response, json.dumps(trans_txt), 1) - if lang_code == 'fr': - # Message with context (msgctxt) - self.assertContains(response, '"month name\\u0004May": "mai"', 1) - - def test_jsoni18n(self): - """ - The json_catalog returns the language catalog and settings as JSON. - """ - with override('de'): - response = self.client.get('/old_jsoni18n/') - data = json.loads(response.content.decode('utf-8')) - self.assertIn('catalog', data) - self.assertIn('formats', data) - self.assertIn('plural', data) - self.assertEqual(data['catalog']['month name\x04May'], 'Mai') - self.assertIn('DATETIME_FORMAT', data['formats']) - self.assertEqual(data['plural'], '(n != 1)') - - def test_jsi18n_with_missing_en_files(self): - """ - The javascript_catalog shouldn't load the fallback language in the - case that the current selected language is actually the one translated - from, and hence missing translation files completely. - - This happens easily when you're translating from English to other - languages and you've set settings.LANGUAGE_CODE to some other language - than English. - """ - with self.settings(LANGUAGE_CODE='es'), override('en-us'): - response = self.client.get('/old_jsi18n/') - self.assertNotContains(response, 'esto tiene que ser traducido') - - def test_jsoni18n_with_missing_en_files(self): - """ - Same as above for the json_catalog view. Here we also check for the - expected JSON format. - """ - with self.settings(LANGUAGE_CODE='es'), override('en-us'): - response = self.client.get('/old_jsoni18n/') - data = json.loads(response.content.decode('utf-8')) - self.assertIn('catalog', data) - self.assertIn('formats', data) - self.assertIn('plural', data) - self.assertEqual(data['catalog'], {}) - self.assertIn('DATETIME_FORMAT', data['formats']) - self.assertIsNone(data['plural']) - - def test_jsi18n_fallback_language(self): - """ - Let's make sure that the fallback language is still working properly - in cases where the selected language cannot be found. - """ - with self.settings(LANGUAGE_CODE='fr'), override('fi'): - response = self.client.get('/old_jsi18n/') - self.assertContains(response, 'il faut le traduire') - self.assertNotContains(response, "Untranslated string") - - def test_i18n_fallback_language_plural(self): - """ - The fallback to a language with less plural forms maintains the real - language's number of plural forms. - """ - with self.settings(LANGUAGE_CODE='pt'), override('ru'): - response = self.client.get('/jsi18n/') - self.assertEqual( - response.context['catalog']['{count} plural3'], - ['{count} plural3', '{count} plural3s', '{count} plural3 p3t'] - ) - - def test_i18n_english_variant(self): - with override('en-gb'): - response = self.client.get('/old_jsi18n/') - self.assertIn( - '"this color is to be translated": "this colour is to be translated"', - response.context['catalog_str'] - ) - - def test_i18n_language_non_english_default(self): - """ - Check if the Javascript i18n view returns an empty language catalog - if the default language is non-English, the selected language - is English and there is not 'en' translation available. See #13388, - #3594 and #13726 for more details. - """ - with self.settings(LANGUAGE_CODE='fr'), override('en-us'): - response = self.client.get('/old_jsi18n/') - self.assertNotContains(response, 'Choisir une heure') - - @modify_settings(INSTALLED_APPS={'append': 'view_tests.app0'}) - def test_non_english_default_english_userpref(self): - """ - Same as above with the difference that there IS an 'en' translation - available. The Javascript i18n view must return a NON empty language catalog - with the proper English translations. See #13726 for more details. - """ - with self.settings(LANGUAGE_CODE='fr'), override('en-us'): - response = self.client.get('/old_jsi18n_english_translation/') - self.assertContains(response, 'this app0 string is to be translated') - - def test_i18n_language_non_english_fallback(self): - """ - Makes sure that the fallback language is still working properly - in cases where the selected language cannot be found. - """ - with self.settings(LANGUAGE_CODE='fr'), override('none'): - response = self.client.get('/old_jsi18n/') - self.assertContains(response, 'Choisir une heure') - - def test_escaping(self): - # Force a language via GET otherwise the gettext functions are a noop! - response = self.client.get('/old_jsi18n_admin/?language=de') - self.assertContains(response, '\\x04') - - @modify_settings(INSTALLED_APPS={'append': ['view_tests.app5']}) - def test_non_BMP_char(self): - """ - Non-BMP characters should not break the javascript_catalog (#21725). - """ - with self.settings(LANGUAGE_CODE='en-us'), override('fr'): - response = self.client.get('/old_jsi18n/app5/') - self.assertContains(response, 'emoji') - self.assertContains(response, '\\ud83d\\udca9') - - -@override_settings(ROOT_URLCONF='view_tests.urls') -@ignore_warnings(category=RemovedInDjango20Warning) -class JsI18NTestsMultiPackage(SimpleTestCase): - """ - Tests for django views in django/views/i18n.py that need to change - settings.LANGUAGE_CODE and merge JS translation from several packages. - """ - @modify_settings(INSTALLED_APPS={'append': ['view_tests.app1', 'view_tests.app2']}) - def test_i18n_language_english_default(self): - """ - Check if the JavaScript i18n view returns a complete language catalog - if the default language is en-us, the selected language has a - translation available and a catalog composed by djangojs domain - translations of multiple Python packages is requested. See #13388, - #3594 and #13514 for more details. - """ - with self.settings(LANGUAGE_CODE='en-us'), override('fr'): - response = self.client.get('/old_jsi18n_multi_packages1/') - self.assertContains(response, 'il faut traduire cette cha\\u00eene de caract\\u00e8res de app1') - - @modify_settings(INSTALLED_APPS={'append': ['view_tests.app3', 'view_tests.app4']}) - def test_i18n_different_non_english_languages(self): - """ - Similar to above but with neither default or requested language being - English. - """ - with self.settings(LANGUAGE_CODE='fr'), override('es-ar'): - response = self.client.get('/old_jsi18n_multi_packages2/') - self.assertContains(response, 'este texto de app3 debe ser traducido') - - def test_i18n_with_locale_paths(self): - extended_locale_paths = settings.LOCALE_PATHS + [ - path.join( - path.dirname(path.dirname(path.abspath(upath(__file__)))), - 'app3', - 'locale', - ), - ] - with self.settings(LANGUAGE_CODE='es-ar', LOCALE_PATHS=extended_locale_paths): - with override('es-ar'): - response = self.client.get('/old_jsi18n/') - self.assertContains(response, 'este texto de app3 debe ser traducido') - - -@override_settings(ROOT_URLCONF='view_tests.urls') -@ignore_warnings(category=RemovedInDjango20Warning) -class JavascriptI18nTests(SeleniumTestCase): - - # The test cases use fixtures & translations from these apps. - available_apps = [ - 'django.contrib.admin', 'django.contrib.auth', - 'django.contrib.contenttypes', 'view_tests', - ] - - @override_settings(LANGUAGE_CODE='de') - def test_javascript_gettext(self): - self.selenium.get('%s%s' % (self.live_server_url, '/old_jsi18n_template/')) - - elem = self.selenium.find_element_by_id("gettext") - self.assertEqual(elem.text, "Entfernen") - elem = self.selenium.find_element_by_id("ngettext_sing") - self.assertEqual(elem.text, "1 Element") - elem = self.selenium.find_element_by_id("ngettext_plur") - self.assertEqual(elem.text, "455 Elemente") - elem = self.selenium.find_element_by_id("pgettext") - self.assertEqual(elem.text, "Kann") - elem = self.selenium.find_element_by_id("npgettext_sing") - self.assertEqual(elem.text, "1 Resultat") - elem = self.selenium.find_element_by_id("npgettext_plur") - self.assertEqual(elem.text, "455 Resultate") - - @modify_settings(INSTALLED_APPS={'append': ['view_tests.app1', 'view_tests.app2']}) - @override_settings(LANGUAGE_CODE='fr') - def test_multiple_catalogs(self): - self.selenium.get('%s%s' % (self.live_server_url, '/old_jsi18n_multi_catalogs/')) - - elem = self.selenium.find_element_by_id('app1string') - self.assertEqual(elem.text, 'il faut traduire cette chaîne de caractères de app1') - elem = self.selenium.find_element_by_id('app2string') - self.assertEqual(elem.text, 'il faut traduire cette chaîne de caractères de app2') diff --git a/tests/view_tests/urls.py b/tests/view_tests/urls.py index 8d2e923a93..128be09eb6 100644 --- a/tests/view_tests/urls.py +++ b/tests/view_tests/urls.py @@ -72,19 +72,6 @@ urlpatterns = [ url(r'technical404/$', views.technical404, name="my404"), url(r'classbased404/$', views.Http404View.as_view()), - # deprecated i18n views - url(r'^old_jsi18n/$', i18n.javascript_catalog, js_info_dict), - url(r'^old_jsi18n/app1/$', i18n.javascript_catalog, js_info_dict_app1), - url(r'^old_jsi18n/app2/$', i18n.javascript_catalog, js_info_dict_app2), - url(r'^old_jsi18n/app5/$', i18n.javascript_catalog, js_info_dict_app5), - url(r'^old_jsi18n_english_translation/$', i18n.javascript_catalog, js_info_dict_english_translation), - url(r'^old_jsi18n_multi_packages1/$', i18n.javascript_catalog, js_info_dict_multi_packages1), - url(r'^old_jsi18n_multi_packages2/$', i18n.javascript_catalog, js_info_dict_multi_packages2), - url(r'^old_jsi18n_admin/$', i18n.javascript_catalog, js_info_dict_admin), - url(r'^old_jsi18n_template/$', views.old_jsi18n), - url(r'^old_jsi18n_multi_catalogs/$', views.old_jsi18n_multi_catalogs), - url(r'^old_jsoni18n/$', i18n.json_catalog, js_info_dict), - # i18n views url(r'^i18n/', include('django.conf.urls.i18n')), url(r'^jsi18n/$', i18n.JavaScriptCatalog.as_view(packages=['view_tests'])),