diff --git a/django/views/i18n.py b/django/views/i18n.py index 30ecbbeaf3..3df4f47c88 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -94,87 +94,87 @@ js_catalog_template = r""" django.pluralidx = function (count) { return (count == 1) ? 0 : 1; }; {% endif %} - {% if catalog_str %} /* gettext library */ - django.catalog = {{ catalog_str }}; - - django.gettext = function (msgid) { - var value = django.catalog[msgid]; - if (typeof(value) == 'undefined') { - return msgid; - } else { - return (typeof(value) == 'string') ? value : value[0]; - } - }; - - django.ngettext = function (singular, plural, count) { - var value = django.catalog[singular]; - if (typeof(value) == 'undefined') { - return (count == 1) ? singular : plural; - } else { - return value[django.pluralidx(count)]; - } - }; - - django.gettext_noop = function (msgid) { return msgid; }; - - django.pgettext = function (context, msgid) { - var value = django.gettext(context + '\x04' + msgid); - if (value.indexOf('\x04') != -1) { - value = msgid; - } - return value; - }; - - django.npgettext = function (context, singular, plural, count) { - var value = django.ngettext(context + '\x04' + singular, context + '\x04' + plural, count); - if (value.indexOf('\x04') != -1) { - value = django.ngettext(singular, plural, count); - } - return value; - }; - {% else %} - /* gettext identity library */ - - django.gettext = function (msgid) { return msgid; }; - django.ngettext = function (singular, plural, count) { return (count == 1) ? singular : plural; }; - django.gettext_noop = function (msgid) { return msgid; }; - django.pgettext = function (context, msgid) { return msgid; }; - django.npgettext = function (context, singular, plural, count) { return (count == 1) ? singular : plural; }; + django.catalog = django.catalog || {}; + {% if catalog_str %} + var newcatalog = {{ catalog_str }}; + for (var key in newcatalog) { + django.catalog[key] = newcatalog[key]; + } {% endif %} - django.interpolate = function (fmt, obj, named) { - if (named) { - return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])}); - } else { - return fmt.replace(/%s/g, function(match){return String(obj.shift())}); - } - }; + if (!django.jsi18n_initialized) { + django.gettext = function (msgid) { + var value = django.catalog[msgid]; + if (typeof(value) == 'undefined') { + return msgid; + } else { + return (typeof(value) == 'string') ? value : value[0]; + } + }; + django.ngettext = function (singular, plural, count) { + var value = django.catalog[singular]; + if (typeof(value) == 'undefined') { + return (count == 1) ? singular : plural; + } else { + return value[django.pluralidx(count)]; + } + }; - /* formatting library */ + django.gettext_noop = function (msgid) { return msgid; }; - django.formats = {{ formats_str }}; - - django.get_format = function (format_type) { - var value = django.formats[format_type]; - if (typeof(value) == 'undefined') { - return format_type; - } else { + django.pgettext = function (context, msgid) { + var value = django.gettext(context + '\x04' + msgid); + if (value.indexOf('\x04') != -1) { + value = msgid; + } return value; - } - }; + }; - /* add to global namespace */ - globals.pluralidx = django.pluralidx; - globals.gettext = django.gettext; - globals.ngettext = django.ngettext; - globals.gettext_noop = django.gettext_noop; - globals.pgettext = django.pgettext; - globals.npgettext = django.npgettext; - globals.interpolate = django.interpolate; - globals.get_format = django.get_format; + django.npgettext = function (context, singular, plural, count) { + var value = django.ngettext(context + '\x04' + singular, context + '\x04' + plural, count); + if (value.indexOf('\x04') != -1) { + value = django.ngettext(singular, plural, count); + } + return value; + }; + + django.interpolate = function (fmt, obj, named) { + if (named) { + return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])}); + } else { + return fmt.replace(/%s/g, function(match){return String(obj.shift())}); + } + }; + + + /* formatting library */ + + django.formats = {{ formats_str }}; + + django.get_format = function (format_type) { + var value = django.formats[format_type]; + if (typeof(value) == 'undefined') { + return format_type; + } else { + return value; + } + }; + + /* add to global namespace */ + globals.pluralidx = django.pluralidx; + globals.gettext = django.gettext; + globals.ngettext = django.ngettext; + globals.gettext_noop = django.gettext_noop; + globals.pgettext = django.pgettext; + globals.npgettext = django.npgettext; + globals.interpolate = django.interpolate; + globals.get_format = django.get_format; + + django.jsi18n_initialized = true; + } }(this)); {% endautoescape %} diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index bce55085ef..6ec8a3f455 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -143,6 +143,9 @@ 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 + if used multiple times with different configurations on the same page. + Management Commands ^^^^^^^^^^^^^^^^^^^ diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index 0f7e76756b..ea295ae8ca 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -952,6 +952,31 @@ 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. + +.. versionchanged:: 1.9 + + Before Django 1.9, the catalogs completely overwrote each other and you + could only use one at a time. + 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 diff --git a/tests/view_tests/templates/jsi18n-multi-catalogs.html b/tests/view_tests/templates/jsi18n-multi-catalogs.html new file mode 100644 index 0000000000..8ec902e1c7 --- /dev/null +++ b/tests/view_tests/templates/jsi18n-multi-catalogs.html @@ -0,0 +1,17 @@ + + + + + +

+ +

+

+ +

+ + diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py index ae3025b5f1..04643d0508 100644 --- a/tests/view_tests/tests/test_i18n.py +++ b/tests/view_tests/tests/test_i18n.py @@ -1,4 +1,6 @@ # -*- coding:utf-8 -*- +from __future__ import unicode_literals + import gettext import json import os @@ -100,7 +102,7 @@ class I18NTests(TestCase): self.assertContains(response, json.dumps(trans_txt), 1) if lang_code == 'fr': # Message with context (msgctxt) - self.assertContains(response, r'"month name\u0004May": "mai"', 1) + self.assertContains(response, '"month name\\u0004May": "mai"', 1) @override_settings(ROOT_URLCONF='view_tests.urls') @@ -270,6 +272,16 @@ class JavascriptI18nTests(LiveServerTestCase): 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, '/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') + class JavascriptI18nChromeTests(JavascriptI18nTests): webdriver_class = 'selenium.webdriver.chrome.webdriver.WebDriver' diff --git a/tests/view_tests/urls.py b/tests/view_tests/urls.py index 061dd19751..1d34b1f934 100644 --- a/tests/view_tests/urls.py +++ b/tests/view_tests/urls.py @@ -38,6 +38,16 @@ js_info_dict_admin = { 'packages': ('django.contrib.admin', 'view_tests'), } +js_info_dict_app1 = { + 'domain': 'djangojs', + 'packages': ('view_tests.app1',), +} + +js_info_dict_app2 = { + 'domain': 'djangojs', + 'packages': ('view_tests.app2',), +} + js_info_dict_app5 = { 'domain': 'djangojs', 'packages': ('view_tests.app5',), @@ -64,12 +74,15 @@ urlpatterns = [ # 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_template/$', views.jsi18n), + url(r'^jsi18n_multi_catalogs/$', views.jsi18n_multi_catalogs), # 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 ad688573df..debc7bd7df 100644 --- a/tests/view_tests/views.py +++ b/tests/view_tests/views.py @@ -82,6 +82,10 @@ def jsi18n(request): return render_to_response('jsi18n.html') +def jsi18n_multi_catalogs(request): + return render_to_response('jsi18n-multi-catalogs.html') + + def raises_template_does_not_exist(request, path='i_dont_exist.html'): # We need to inspect the HTML generated by the fancy 500 debug view but # the test client ignores it, so we send it explicitly.