Fixed #19567 -- Added JavaScriptCatalog and JSONCatalog class-based views
Thanks Cristiano Coelho and Tim Graham for the reviews.
This commit is contained in:
parent
79a091820f
commit
de40cfbe74
|
@ -14,6 +14,7 @@ from django.utils.text import capfirst
|
||||||
from django.utils.translation import ugettext as _, ugettext_lazy
|
from django.utils.translation import ugettext as _, ugettext_lazy
|
||||||
from django.views.decorators.cache import never_cache
|
from django.views.decorators.cache import never_cache
|
||||||
from django.views.decorators.csrf import csrf_protect
|
from django.views.decorators.csrf import csrf_protect
|
||||||
|
from django.views.i18n import JavaScriptCatalog
|
||||||
|
|
||||||
system_check_errors = []
|
system_check_errors = []
|
||||||
|
|
||||||
|
@ -316,15 +317,8 @@ class AdminSite(object):
|
||||||
def i18n_javascript(self, request):
|
def i18n_javascript(self, request):
|
||||||
"""
|
"""
|
||||||
Displays the i18n JavaScript that the Django admin requires.
|
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:
|
return JavaScriptCatalog.as_view(packages=['django.contrib.admin'])(request)
|
||||||
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'])
|
|
||||||
|
|
||||||
@never_cache
|
@never_cache
|
||||||
def logout(self, request, extra_context=None):
|
def logout(self, request, extra_context=None):
|
||||||
|
|
|
@ -2,6 +2,7 @@ import importlib
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import warnings
|
||||||
|
|
||||||
from django import http
|
from django import http
|
||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
|
@ -10,6 +11,7 @@ from django.template import Context, Engine
|
||||||
from django.urls import translate_url
|
from django.urls import translate_url
|
||||||
from django.utils import six
|
from django.utils import six
|
||||||
from django.utils._os import upath
|
from django.utils._os import upath
|
||||||
|
from django.utils.deprecation import RemovedInDjango20Warning
|
||||||
from django.utils.encoding import smart_text
|
from django.utils.encoding import smart_text
|
||||||
from django.utils.formats import get_format, get_format_modules
|
from django.utils.formats import get_format, get_format_modules
|
||||||
from django.utils.http import is_safe_url, urlunquote
|
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,
|
LANGUAGE_SESSION_KEY, check_for_language, get_language, to_locale,
|
||||||
)
|
)
|
||||||
from django.utils.translation.trans_real import DjangoTranslation
|
from django.utils.translation.trans_real import DjangoTranslation
|
||||||
|
from django.views.generic import View
|
||||||
|
|
||||||
DEFAULT_PACKAGES = ['django.conf']
|
DEFAULT_PACKAGES = ['django.conf']
|
||||||
LANGUAGE_QUERY_PARAMETER = 'language'
|
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
|
go to the djangojs domain. But this might be needed if you
|
||||||
deliver your JavaScript source from Django templates.
|
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)
|
locale = _get_locale(request)
|
||||||
packages = _parse_packages(packages)
|
packages = _parse_packages(packages)
|
||||||
catalog, plural = get_javascript_catalog(locale, domain, 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.
|
"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)
|
locale = _get_locale(request)
|
||||||
packages = _parse_packages(packages)
|
packages = _parse_packages(packages)
|
||||||
catalog, plural = get_javascript_catalog(locale, domain, packages)
|
catalog, plural = get_javascript_catalog(locale, domain, packages)
|
||||||
|
@ -323,3 +334,111 @@ def json_catalog(request, domain='djangojs', packages=None):
|
||||||
'plural': plural,
|
'plural': plural,
|
||||||
}
|
}
|
||||||
return http.JsonResponse(data)
|
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)
|
||||||
|
|
|
@ -156,6 +156,8 @@ details on these changes.
|
||||||
``Field.contribute_to_class()`` and ``virtual`` in
|
``Field.contribute_to_class()`` and ``virtual`` in
|
||||||
``Model._meta.add_field()`` will be removed.
|
``Model._meta.add_field()`` will be removed.
|
||||||
|
|
||||||
|
* The ``javascript_catalog()`` and ``json_catalog()`` views will be removed.
|
||||||
|
|
||||||
.. _deprecation-removed-in-1.10:
|
.. _deprecation-removed-in-1.10:
|
||||||
|
|
||||||
1.10
|
1.10
|
||||||
|
|
|
@ -277,6 +277,14 @@ Internationalization
|
||||||
Content) for AJAX requests when there is no ``next`` parameter in ``POST`` or
|
Content) for AJAX requests when there is no ``next`` parameter in ``POST`` or
|
||||||
``GET``.
|
``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
|
Management Commands
|
||||||
~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -929,6 +937,10 @@ Miscellaneous
|
||||||
``Model._meta.add_field()`` are deprecated in favor of ``private_only``
|
``Model._meta.add_field()`` are deprecated in favor of ``private_only``
|
||||||
and ``private``, respectively.
|
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:
|
.. _removed-features-1.10:
|
||||||
|
|
||||||
Features removed in 1.10
|
Features removed in 1.10
|
||||||
|
|
|
@ -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
|
translations into JavaScript, so you can call ``gettext``, etc., from within
|
||||||
JavaScript.
|
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:
|
.. _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 <django.apps.AppConfig.name>` 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
|
The ``javascript_catalog`` view
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
||||||
.. module:: django.views.i18n
|
|
||||||
|
|
||||||
.. function:: javascript_catalog(request, domain='djangojs', packages=None)
|
.. 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
|
The main solution to these problems is the
|
||||||
:meth:`django.views.i18n.javascript_catalog` view, which sends out a JavaScript
|
:meth:`django.views.i18n.javascript_catalog` view, which sends out a JavaScript
|
||||||
code library with functions that mimic the ``gettext`` interface, plus an array
|
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
|
.. 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
|
The ``json_catalog`` view
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
|
@ -1216,6 +1308,11 @@ The ``json_catalog`` view
|
||||||
|
|
||||||
.. function:: json_catalog(request, domain='djangojs', packages=None)
|
.. 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
|
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
|
want to take advantage of the ``json_catalog()`` view. It's similar to
|
||||||
:meth:`~django.views.i18n.javascript_catalog` but returns a JSON response.
|
:meth:`~django.views.i18n.javascript_catalog` but returns a JSON response.
|
||||||
|
@ -1260,9 +1357,9 @@ The response format is as follows:
|
||||||
Note on performance
|
Note on performance
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
The :func:`~django.views.i18n.javascript_catalog` view generates the catalog
|
The various JavaScript/JSON i18n views generate the catalog from ``.mo`` files
|
||||||
from ``.mo`` files on every request. Since its output is constant — at least
|
on every request. Since its output is constant, at least for a given version
|
||||||
for a given version of a site — it's a good candidate for caching.
|
of a site, it's a good candidate for caching.
|
||||||
|
|
||||||
Server-side caching will reduce CPU load. It's easily implemented with the
|
Server-side caching will reduce CPU load. It's easily implemented with the
|
||||||
:func:`~django.views.decorators.cache.cache_page` decorator. To trigger cache
|
: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::
|
URL::
|
||||||
|
|
||||||
from django.views.decorators.cache import cache_page
|
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.
|
# The value returned by get_version() must change when translations change.
|
||||||
@cache_page(86400, key_prefix='js18n-%s' % get_version())
|
urlpatterns = [
|
||||||
def cached_javascript_catalog(request, domain='djangojs', packages=None):
|
url(r'^jsi18n/$',
|
||||||
return javascript_catalog(request, domain, packages)
|
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
|
Client-side caching will save bandwidth and make your site load faster. If
|
||||||
you're using ETags (:setting:`USE_ETAGS = True <USE_ETAGS>`), you're already
|
you're using ETags (:setting:`USE_ETAGS = True <USE_ETAGS>`), you're already
|
||||||
|
@ -1286,13 +1385,15 @@ whenever you restart your application server::
|
||||||
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.views.decorators.http import last_modified
|
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_date = timezone.now()
|
||||||
|
|
||||||
@last_modified(lambda req, **kw: last_modified_date)
|
urlpatterns = [
|
||||||
def cached_javascript_catalog(request, domain='djangojs', packages=None):
|
url(r'^jsi18n/$',
|
||||||
return javascript_catalog(request, domain, packages)
|
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
|
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
|
procedure and serve it as a static file. This radical technique is implemented
|
||||||
|
|
|
@ -0,0 +1,17 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript" src="/old_jsi18n/app1/"></script>
|
||||||
|
<script type="text/javascript" src="/old_jsi18n/app2/"></script>
|
||||||
|
<body>
|
||||||
|
<p id="app1string">
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.write(gettext('this app1 string is to be translated'))
|
||||||
|
</script>
|
||||||
|
</p>
|
||||||
|
<p id="app2string">
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.write(gettext('this app2 string is to be translated'))
|
||||||
|
</script>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,44 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script type="text/javascript" src="/old_jsi18n_admin/"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<p id="gettext">
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.write(gettext("Remove"));
|
||||||
|
</script>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p id="ngettext_sing">
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.write(interpolate(ngettext("%s item", "%s items", 1), [1]));
|
||||||
|
</script>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p id="ngettext_plur">
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.write(interpolate(ngettext("%s item", "%s items", 455), [455]));
|
||||||
|
</script>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p id="pgettext">
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.write(pgettext("verb", "May"));
|
||||||
|
</script>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p id="npgettext_sing">
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.write(interpolate(npgettext("search", "%s result", "%s results", 1), [1]));
|
||||||
|
</script>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p id="npgettext_plur">
|
||||||
|
<script type="text/javascript">
|
||||||
|
document.write(interpolate(npgettext("search", "%s result", "%s results", 455), [455]));
|
||||||
|
</script>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -167,6 +167,13 @@ class I18NTests(TestCase):
|
||||||
)
|
)
|
||||||
self.assertRedirects(response, '/en/translated/')
|
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):
|
def test_jsi18n(self):
|
||||||
"""The javascript_catalog can be deployed with language settings"""
|
"""The javascript_catalog can be deployed with language settings"""
|
||||||
for lang_code in ['es', 'fr', 'ru']:
|
for lang_code in ['es', 'fr', 'ru']:
|
||||||
|
@ -185,6 +192,13 @@ class I18NTests(TestCase):
|
||||||
# Message with context (msgctxt)
|
# Message with context (msgctxt)
|
||||||
self.assertContains(response, '"month name\\u0004May": "mai"', 1)
|
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):
|
def test_jsoni18n(self):
|
||||||
"""
|
"""
|
||||||
The json_catalog returns the language catalog and settings as JSON.
|
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.assertIn('DATETIME_FORMAT', data['formats'])
|
||||||
self.assertEqual(data['plural'], '(n != 1)')
|
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):
|
def test_jsi18n_with_missing_en_files(self):
|
||||||
"""
|
"""
|
||||||
The javascript_catalog shouldn't load the fallback language in the
|
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')
|
@override_settings(ROOT_URLCONF='view_tests.urls')
|
||||||
class JsI18NTestsMultiPackage(SimpleTestCase):
|
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.
|
settings.LANGUAGE_CODE and merge JS translation from several packages.
|
||||||
"""
|
"""
|
||||||
@modify_settings(INSTALLED_APPS={'append': ['view_tests.app1', 'view_tests.app2']})
|
@modify_settings(INSTALLED_APPS={'append': ['view_tests.app1', 'view_tests.app2']})
|
||||||
|
|
|
@ -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": <value of trans_txt Python variable>
|
||||||
|
# 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')
|
|
@ -72,19 +72,35 @@ urlpatterns = [
|
||||||
url(r'technical404/$', views.technical404, name="my404"),
|
url(r'technical404/$', views.technical404, name="my404"),
|
||||||
url(r'classbased404/$', views.Http404View.as_view()),
|
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
|
# i18n views
|
||||||
url(r'^i18n/', include('django.conf.urls.i18n')),
|
url(r'^i18n/', include('django.conf.urls.i18n')),
|
||||||
url(r'^jsi18n/$', i18n.javascript_catalog, js_info_dict),
|
url(r'^jsi18n/$', i18n.JavaScriptCatalog.as_view(packages=['view_tests'])),
|
||||||
url(r'^jsi18n/app1/$', i18n.javascript_catalog, js_info_dict_app1),
|
url(r'^jsi18n/app1/$', i18n.JavaScriptCatalog.as_view(packages=['view_tests.app1'])),
|
||||||
url(r'^jsi18n/app2/$', i18n.javascript_catalog, js_info_dict_app2),
|
url(r'^jsi18n/app2/$', i18n.JavaScriptCatalog.as_view(packages=['view_tests.app2'])),
|
||||||
url(r'^jsi18n/app5/$', i18n.javascript_catalog, js_info_dict_app5),
|
url(r'^jsi18n/app5/$', i18n.JavaScriptCatalog.as_view(packages=['view_tests.app5'])),
|
||||||
url(r'^jsi18n_english_translation/$', i18n.javascript_catalog, js_info_dict_english_translation),
|
url(r'^jsi18n_english_translation/$', i18n.JavaScriptCatalog.as_view(packages=['view_tests.app0'])),
|
||||||
url(r'^jsi18n_multi_packages1/$', i18n.javascript_catalog, js_info_dict_multi_packages1),
|
url(r'^jsi18n_multi_packages1/$',
|
||||||
url(r'^jsi18n_multi_packages2/$', i18n.javascript_catalog, js_info_dict_multi_packages2),
|
i18n.JavaScriptCatalog.as_view(packages=['view_tests.app1', 'view_tests.app2'])),
|
||||||
url(r'^jsi18n_admin/$', i18n.javascript_catalog, js_info_dict_admin),
|
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_template/$', views.jsi18n),
|
||||||
url(r'^jsi18n_multi_catalogs/$', views.jsi18n_multi_catalogs),
|
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
|
# Static views
|
||||||
url(r'^site_media/(?P<path>.*)$', static.serve, {'document_root': media_dir}),
|
url(r'^site_media/(?P<path>.*)$', static.serve, {'document_root': media_dir}),
|
||||||
|
|
|
@ -85,8 +85,16 @@ def jsi18n(request):
|
||||||
return render(request, 'jsi18n.html')
|
return render(request, 'jsi18n.html')
|
||||||
|
|
||||||
|
|
||||||
|
def old_jsi18n(request):
|
||||||
|
return render(request, 'old_jsi18n.html')
|
||||||
|
|
||||||
|
|
||||||
def jsi18n_multi_catalogs(request):
|
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'):
|
def raises_template_does_not_exist(request, path='i_dont_exist.html'):
|
||||||
|
|
Loading…
Reference in New Issue