diff --git a/django/views/i18n.py b/django/views/i18n.py index 49bcfead4d..68039bcf2a 100644 --- a/django/views/i18n.py +++ b/django/views/i18n.py @@ -17,6 +17,9 @@ from django.utils.translation import ( LANGUAGE_SESSION_KEY, check_for_language, get_language, to_locale, ) +DEFAULT_PACKAGES = ['django.conf'] +LANGUAGE_QUERY_PARAMETER = 'language' + def set_language(request): """ @@ -36,7 +39,7 @@ def set_language(request): next = '/' response = http.HttpResponseRedirect(next) if request.method == 'POST': - lang_code = request.POST.get('language') + lang_code = request.POST.get(LANGUAGE_QUERY_PARAMETER) if lang_code and check_for_language(lang_code): next_trans = translate_url(next, lang_code) if next_trans != next: @@ -199,7 +202,7 @@ def get_javascript_catalog(locale, domain, packages): default_locale = to_locale(settings.LANGUAGE_CODE) app_configs = apps.get_app_configs() allowable_packages = set(app_config.name for app_config in app_configs) - allowable_packages.add('django.conf') + allowable_packages.update(DEFAULT_PACKAGES) packages = [p for p in packages if p in allowable_packages] t = {} paths = [] @@ -284,6 +287,21 @@ def get_javascript_catalog(locale, domain, packages): 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., @@ -305,16 +323,35 @@ 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. """ - locale = to_locale(get_language()) - - if request.GET and 'language' in request.GET: - if check_for_language(request.GET['language']): - locale = to_locale(request.GET['language']) - - if packages is None: - packages = ['django.conf'] - if isinstance(packages, six.string_types): - packages = packages.split('+') - + 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. + } + """ + 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) diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index 6f2611f267..1f1183d9a7 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -357,6 +357,10 @@ 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 + client-side i18n library upon Django translations. It returns a JSON object + containing a translations catalog, formatting settings, and a plural rule. + * Added the ``name_translated`` attribute to the object returned by the :ttag:`get_language_info` template tag. Also added a corresponding template filter: :tfilter:`language_name_translated`. diff --git a/docs/topics/i18n/translation.txt b/docs/topics/i18n/translation.txt index d976ddf2ef..974f6d31ce 100644 --- a/docs/topics/i18n/translation.txt +++ b/docs/topics/i18n/translation.txt @@ -1213,6 +1213,52 @@ Additionally, if there are complex rules around pluralization, the catalog view will render a conditional expression. This will evaluate to either a ``true`` (should pluralize) or ``false`` (should **not** pluralize) value. +The ``json_catalog`` view +------------------------- + +.. versionadded:: 1.9 + +.. function:: json_catalog(request, domain='djangojs', packages=None) + +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:: json + + { + "catalog": { + # Translations catalog + }, + "formats": { + # Language formats for date, time, etc. + }, + "plural": "..." # Expression for plural forms, or null. + } + Note on performance ------------------- diff --git a/tests/view_tests/tests/test_i18n.py b/tests/view_tests/tests/test_i18n.py index 886c10f5fc..a10a3bc7e1 100644 --- a/tests/view_tests/tests/test_i18n.py +++ b/tests/view_tests/tests/test_i18n.py @@ -105,6 +105,20 @@ class I18NTests(TestCase): # 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('/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)') + @override_settings(ROOT_URLCONF='view_tests.urls') class JsI18NTests(SimpleTestCase): @@ -127,6 +141,21 @@ class JsI18NTests(SimpleTestCase): response = self.client.get('/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('/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 diff --git a/tests/view_tests/urls.py b/tests/view_tests/urls.py index 4215a4b26a..e8f2855c98 100644 --- a/tests/view_tests/urls.py +++ b/tests/view_tests/urls.py @@ -84,6 +84,7 @@ urlpatterns = [ 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), + url(r'^jsoni18n/$', i18n.json_catalog, js_info_dict), # Static views url(r'^site_media/(?P.*)$', static.serve, {'document_root': media_dir}),