diff --git a/django/contrib/admin/sites.py b/django/contrib/admin/sites.py index d867aacbc1..dbc0472ab7 100644 --- a/django/contrib/admin/sites.py +++ b/django/contrib/admin/sites.py @@ -14,6 +14,7 @@ from django.utils.text import capfirst from django.utils.translation import ugettext as _, ugettext_lazy from django.views.decorators.cache import never_cache from django.views.decorators.csrf import csrf_protect +from django.views.i18n import JavaScriptCatalog system_check_errors = [] @@ -316,15 +317,8 @@ class AdminSite(object): def i18n_javascript(self, request): """ Displays the i18n JavaScript that the Django admin requires. - - This takes into account the USE_I18N setting. If it's set to False, the - generated JavaScript will be leaner and faster. """ - if settings.USE_I18N: - from django.views.i18n import javascript_catalog - else: - from django.views.i18n import null_javascript_catalog as javascript_catalog - return javascript_catalog(request, packages=['django.conf', 'django.contrib.admin']) + return JavaScriptCatalog.as_view(packages=['django.contrib.admin'])(request) @never_cache def logout(self, request, extra_context=None): diff --git a/django/views/i18n.py b/django/views/i18n.py index 12fa1f70f6..03c771bf2f 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -2,6 +2,7 @@ import importlib import itertools import json import os +import warnings from django import http from django.apps import apps @@ -10,6 +11,7 @@ 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 smart_text from django.utils.formats import get_format, get_format_modules from django.utils.http import is_safe_url, urlunquote @@ -17,6 +19,7 @@ from django.utils.translation import ( LANGUAGE_SESSION_KEY, check_for_language, get_language, to_locale, ) from django.utils.translation.trans_real import DjangoTranslation +from django.views.generic import View DEFAULT_PACKAGES = ['django.conf'] LANGUAGE_QUERY_PARAMETER = 'language' @@ -291,6 +294,10 @@ def javascript_catalog(request, domain='djangojs', packages=None): 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) @@ -314,6 +321,10 @@ def json_catalog(request, domain='djangojs', packages=None): "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) @@ -323,3 +334,111 @@ def json_catalog(request, domain='djangojs', packages=None): 'plural': plural, } return http.JsonResponse(data) + + +class JavaScriptCatalog(View): + """ + Return the selected language catalog as a JavaScript library. + + Receives the list of packages to check for translations in the `packages` + kwarg either from the extra dictionary passed to the url() function or as a + plus-sign delimited string from the request. Default is 'django.conf'. + + 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. This + might be needed if you deliver your JavaScript source from Django templates. + """ + domain = 'djangojs' + packages = None + + def get(self, request, *args, **kwargs): + locale = get_language() + domain = kwargs.get('domain', self.domain) + # If packages are not provided, default to all installed packages, as + # DjangoTranslation without localedirs harvests them all. + packages = kwargs.get('packages', '').split('+') or self.packages + paths = self.get_paths(packages) if packages else None + self.translation = DjangoTranslation(locale, domain=domain, localedirs=paths) + context = self.get_context_data(**kwargs) + return self.render_to_response(context) + + def get_paths(self, packages): + allowable_packages = dict((app_config.name, app_config) for app_config in apps.get_app_configs()) + app_configs = [allowable_packages[p] for p in packages if p in allowable_packages] + # paths of requested packages + return [os.path.join(app.path, 'locale') for app in app_configs] + + def get_plural(self): + plural = None + if '' in self.translation._catalog: + for line in self.translation._catalog[''].split('\n'): + if line.startswith('Plural-Forms:'): + plural = line.split(':', 1)[1].strip() + if plural is not None: + # This should 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] + return plural + + def get_catalog(self): + pdict = {} + maxcnts = {} + catalog = {} + trans_cat = self.translation._catalog + trans_fallback_cat = self.translation._fallback._catalog if self.translation._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[msgid] + 1)] + return catalog + + def get_context_data(self, **kwargs): + return { + 'catalog': self.get_catalog(), + 'formats': get_formats(), + 'plural': self.get_plural(), + } + + def render_to_response(self, context, **response_kwargs): + def indent(s): + return s.replace('\n', '\n ') + + template = Engine().from_string(js_catalog_template) + context['catalog_str'] = indent( + json.dumps(context['catalog'], sort_keys=True, indent=2) + ) if context['catalog'] else None + context['formats_str'] = indent(json.dumps(context['formats'], sort_keys=True, indent=2)) + + return http.HttpResponse(template.render(Context(context)), 'text/javascript') + + +class JSONCatalog(JavaScriptCatalog): + """ + Return the selected language catalog as a JSON object. + + Receives the same parameters as JavaScriptCatalog and 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. + } + """ + def render_to_response(self, context, **response_kwargs): + return http.JsonResponse(context) diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index fee9c8cef7..a369767713 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -156,6 +156,8 @@ details on these changes. ``Field.contribute_to_class()`` and ``virtual`` in ``Model._meta.add_field()`` will be removed. +* The ``javascript_catalog()`` and ``json_catalog()`` views will be removed. + .. _deprecation-removed-in-1.10: 1.10 diff --git a/docs/releases/1.10.txt b/docs/releases/1.10.txt index c06602dc95..549bc765d0 100644 --- a/docs/releases/1.10.txt +++ b/docs/releases/1.10.txt @@ -277,6 +277,14 @@ Internationalization Content) for AJAX requests when there is no ``next`` parameter in ``POST`` or ``GET``. +* The :class:`~django.views.i18n.JavaScriptCatalog` and + :class:`~django.views.i18n.JSONCatalog` class-based views supersede the + deprecated ``javascript_catalog()`` and ``json_catalog()`` function-based + views. The new views are almost equivalent to the old ones except that by + default the new views collect all JavaScript strings in the ``djangojs`` + translation domain from all installed apps rather than only the JavaScript + strings from :setting:`LOCALE_PATHS`. + Management Commands ~~~~~~~~~~~~~~~~~~~ @@ -929,6 +937,10 @@ Miscellaneous ``Model._meta.add_field()`` are deprecated in favor of ``private_only`` and ``private``, respectively. +* The ``javascript_catalog()`` and ``json_catalog()`` views are deprecated in + favor of class-based views :class:`~django.views.i18n.JavaScriptCatalog` + and :class:`~django.views.i18n.JSONCatalog`. + .. _removed-features-1.10: Features removed in 1.10 diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 54e3483f97..ecdbd6f504 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -959,15 +959,76 @@ Django provides an integrated solution for these problems: It passes the translations into JavaScript, so you can call ``gettext``, etc., from within JavaScript. +The main solution to these problems is the following ``JavaScriptCatalog`` view, +which generates a JavaScript code library with functions that mimic the +``gettext`` interface, plus an array of translation strings. + .. _javascript_catalog-view: +The ``JavaScriptCatalog`` view +------------------------------ + +.. module:: django.views.i18n + +.. versionadded:: 1.10 + +.. class:: JavaScriptCatalog + + A view that produces a JavaScript code library with functions that mimic + the ``gettext`` interface, plus an array of translation strings. + + **Attributes** + + .. attribute:: domain + + Translation domain containing strings to add in the view output. + Defaults to ``'djangojs'``. + + .. attribute:: packages + + A list of :attr:`application names ` among + installed applications. Those apps should contain a ``locale`` + directory. All those catalogs plus all catalogs found in + :setting:`LOCALE_PATHS` (which are always included) are merged into one + catalog. Defaults to ``None``, which means that all available + translations from all :setting:`INSTALLED_APPS` are provided in the + JavaScript output. + + **Example with default values**:: + + from django.views.i18n import JavaScriptCatalog + + urlpatterns = [ + url(r'^jsi18n/$', JavaScriptCatalog.as_view(), name='javascript-catalog'), + ] + + **Example with custom packages**:: + + urlpatterns = [ + url(r'^jsi18n/myapp/$', + JavaScriptCatalog.as_view(packages=['your.app.label']), + name='javascript-catalog'), + ] + +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. + +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 ------------------------------- -.. module:: django.views.i18n - .. 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 @@ -1209,6 +1270,37 @@ will render a conditional expression. This will evaluate to either a ``true`` .. highlight:: python +The ``JSONCatalog`` view +------------------------ + +.. versionadded:: 1.10 + +.. class:: JSONCatalog + + In order to use another client-side library to handle translations, you may + want to take advantage of the ``JSONCatalog`` view. It's similar to + :class:`~django.views.i18n.JavaScriptCatalog` but returns a JSON response. + + See the documentation for :class:`~django.views.i18n.JavaScriptCatalog` + to learn about possible values and use of the ``domain`` and ``packages`` + attributes. + + 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. + The ``json_catalog`` view ------------------------- @@ -1216,6 +1308,11 @@ 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. @@ -1260,9 +1357,9 @@ The response format is as follows: Note on performance ------------------- -The :func:`~django.views.i18n.javascript_catalog` view generates the catalog -from ``.mo`` files on every request. Since its output is constant — at least -for a given version of a site — it's a good candidate for caching. +The various JavaScript/JSON i18n views generate the catalog from ``.mo`` files +on every request. Since its output is constant, at least for a given version +of a site, it's a good candidate for caching. Server-side caching will reduce CPU load. It's easily implemented with the :func:`~django.views.decorators.cache.cache_page` decorator. To trigger cache @@ -1271,12 +1368,14 @@ prefix, as shown in the example below, or map the view at a version-dependent URL:: from django.views.decorators.cache import cache_page - from django.views.i18n import javascript_catalog + from django.views.i18n import JavaScriptCatalog # The value returned by get_version() must change when translations change. - @cache_page(86400, key_prefix='js18n-%s' % get_version()) - def cached_javascript_catalog(request, domain='djangojs', packages=None): - return javascript_catalog(request, domain, packages) + urlpatterns = [ + url(r'^jsi18n/$', + cache_page(86400, key_prefix='js18n-%s' % get_version())(JavaScriptCatalog.as_view()), + name='javascript-catalog'), + ] Client-side caching will save bandwidth and make your site load faster. If you're using ETags (:setting:`USE_ETAGS = True `), you're already @@ -1286,13 +1385,15 @@ whenever you restart your application server:: from django.utils import timezone from django.views.decorators.http import last_modified - from django.views.i18n import javascript_catalog + from django.views.i18n import JavaScriptCatalog last_modified_date = timezone.now() - @last_modified(lambda req, **kw: last_modified_date) - def cached_javascript_catalog(request, domain='djangojs', packages=None): - return javascript_catalog(request, domain, packages) + urlpatterns = [ + url(r'^jsi18n/$', + last_modified(lambda req, **kw: last_modified_date)(JavaScriptCatalog.as_view()), + name='javascript-catalog'), + ] You can even pre-generate the JavaScript catalog as part of your deployment procedure and serve it as a static file. This radical technique is implemented diff --git a/tests/view_tests/templates/old_jsi18n-multi-catalogs.html b/tests/view_tests/templates/old_jsi18n-multi-catalogs.html new file mode 100644 index 0000000000..eccd2d6043 --- /dev/null +++ b/tests/view_tests/templates/old_jsi18n-multi-catalogs.html @@ -0,0 +1,17 @@ + + + + + +

+ +

+

+ +

+ + diff --git a/tests/view_tests/templates/old_jsi18n.html b/tests/view_tests/templates/old_jsi18n.html new file mode 100644 index 0000000000..03e738d867 --- /dev/null +++ b/tests/view_tests/templates/old_jsi18n.html @@ -0,0 +1,44 @@ + + + + + + +

+ +

+ +

+ +

+ +

+ +

+ +

+ +

+ +

+ +

+ +

+ +

+ + + diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py index 5e6e9d2f2d..84a381909d 100644 --- a/tests/view_tests/tests/test_i18n.py +++ b/tests/view_tests/tests/test_i18n.py @@ -167,6 +167,13 @@ class I18NTests(TestCase): ) self.assertRedirects(response, '/en/translated/') + +@override_settings(ROOT_URLCONF='view_tests.urls') +class JsI18NTests(SimpleTestCase): + """ + Tests views in django/views/i18n.py that need to change + settings.LANGUAGE_CODE. + """ def test_jsi18n(self): """The javascript_catalog can be deployed with language settings""" for lang_code in ['es', 'fr', 'ru']: @@ -185,6 +192,13 @@ class I18NTests(TestCase): # Message with context (msgctxt) self.assertContains(response, '"month name\\u0004May": "mai"', 1) + @override_settings(USE_I18N=False) + def test_jsi18n_USE_I18N_False(self): + response = self.client.get('/jsi18n/') + # default plural function + self.assertContains(response, 'django.pluralidx = function(count) { return (count == 1) ? 0 : 1; };') + self.assertNotContains(response, 'var newcatalog =') + def test_jsoni18n(self): """ The json_catalog returns the language catalog and settings as JSON. @@ -199,14 +213,6 @@ class I18NTests(TestCase): self.assertIn('DATETIME_FORMAT', data['formats']) self.assertEqual(data['plural'], '(n != 1)') - -@override_settings(ROOT_URLCONF='view_tests.urls') -class JsI18NTests(SimpleTestCase): - """ - Tests django views in django/views/i18n.py that need to change - settings.LANGUAGE_CODE. - """ - def test_jsi18n_with_missing_en_files(self): """ The javascript_catalog shouldn't load the fallback language in the @@ -304,7 +310,7 @@ class JsI18NTests(SimpleTestCase): @override_settings(ROOT_URLCONF='view_tests.urls') class JsI18NTestsMultiPackage(SimpleTestCase): """ - Tests for django views in django/views/i18n.py that need to change + Tests 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']}) diff --git a/tests/view_tests/tests/test_i18n_deprecated.py b/tests/view_tests/tests/test_i18n_deprecated.py new file mode 100644 index 0000000000..88ce57f47a --- /dev/null +++ b/tests/view_tests/tests/test_i18n_deprecated.py @@ -0,0 +1,232 @@ +# -*- 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_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 a8cb756f77..50a02e4604 100644 --- a/tests/view_tests/urls.py +++ b/tests/view_tests/urls.py @@ -72,19 +72,35 @@ 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.javascript_catalog, js_info_dict), - url(r'^jsi18n/app1/$', i18n.javascript_catalog, js_info_dict_app1), - url(r'^jsi18n/app2/$', i18n.javascript_catalog, js_info_dict_app2), - url(r'^jsi18n/app5/$', i18n.javascript_catalog, js_info_dict_app5), - url(r'^jsi18n_english_translation/$', i18n.javascript_catalog, js_info_dict_english_translation), - url(r'^jsi18n_multi_packages1/$', i18n.javascript_catalog, js_info_dict_multi_packages1), - url(r'^jsi18n_multi_packages2/$', i18n.javascript_catalog, js_info_dict_multi_packages2), - url(r'^jsi18n_admin/$', i18n.javascript_catalog, js_info_dict_admin), + url(r'^jsi18n/$', i18n.JavaScriptCatalog.as_view(packages=['view_tests'])), + url(r'^jsi18n/app1/$', i18n.JavaScriptCatalog.as_view(packages=['view_tests.app1'])), + url(r'^jsi18n/app2/$', i18n.JavaScriptCatalog.as_view(packages=['view_tests.app2'])), + url(r'^jsi18n/app5/$', i18n.JavaScriptCatalog.as_view(packages=['view_tests.app5'])), + url(r'^jsi18n_english_translation/$', i18n.JavaScriptCatalog.as_view(packages=['view_tests.app0'])), + url(r'^jsi18n_multi_packages1/$', + i18n.JavaScriptCatalog.as_view(packages=['view_tests.app1', 'view_tests.app2'])), + url(r'^jsi18n_multi_packages2/$', + i18n.JavaScriptCatalog.as_view(packages=['view_tests.app3', 'view_tests.app4'])), + url(r'^jsi18n_admin/$', + i18n.JavaScriptCatalog.as_view(packages=['django.contrib.admin', 'view_tests'])), url(r'^jsi18n_template/$', views.jsi18n), url(r'^jsi18n_multi_catalogs/$', views.jsi18n_multi_catalogs), - url(r'^jsoni18n/$', i18n.json_catalog, js_info_dict), + url(r'^jsoni18n/$', i18n.JSONCatalog.as_view(packages=['view_tests'])), # Static views url(r'^site_media/(?P.*)$', static.serve, {'document_root': media_dir}), diff --git a/tests/view_tests/views.py b/tests/view_tests/views.py index 01c0e0e9ea..97b984b758 100644 --- a/tests/view_tests/views.py +++ b/tests/view_tests/views.py @@ -85,8 +85,16 @@ def jsi18n(request): return render(request, 'jsi18n.html') +def old_jsi18n(request): + return render(request, 'old_jsi18n.html') + + def jsi18n_multi_catalogs(request): - return render(render, 'jsi18n-multi-catalogs.html') + return render(request, 'jsi18n-multi-catalogs.html') + + +def old_jsi18n_multi_catalogs(request): + return render(request, 'old_jsi18n-multi-catalogs.html') def raises_template_does_not_exist(request, path='i_dont_exist.html'):