skip to content

In Django, how does the JavaScriptCatalog view make translations available to front-end JavaScript, and what should you watch out for?

level: middleimportance: nice to knowfreq 22%

answer

  1. a view in django.views.i18n
  2. the djangojs domain
  3. gettext, ngettext, interpolate in the browser
  4. rebuilt from .mo on every request

basics

~20 s

django.views.i18n.JavaScriptCatalog is a view that returns a script defining gettext(), ngettext(), pgettext(), interpolate() and the active language's djangojs catalog. Watch that it is regenerated on every request, so cache it, and that it must sit inside i18n_patterns when your URLs use them.

solid answer

~40 s

You route `JavaScriptCatalog.as_view()` (from `django.views.i18n`) at a URL such as `jsi18n/` and load it with a `<script>` tag before your own code. It renders the **active language's** catalog for the `djangojs` domain, from every installed app plus `LOCALE_PATHS` by default, or only the apps named in `packages`. It also defines browser-side `gettext`, `ngettext`, `pgettext`, `npgettext`, `interpolate`, `get_format`, `pluralidx` and `gettext_noop`. Watch out for three things. It rebuilds the catalog from `.mo` files on every request, so wrap it in `cache_page` with a version-dependent `key_prefix`. If your root URLconf uses `i18n_patterns()`, the view must be inside it too, or the catalog is built for the wrong language. And strings in `.js` files must be extracted into the `djangojs` domain, not the Python one. `JSONCatalog` returns the same data as JSON for other client libraries.

code

python · 16 lines
python
from django.conf.urls.i18n import i18n_patterns
from django.urls import path
from django.views.decorators.cache import cache_page
from django.views.i18n import JavaScriptCatalog

RELEASE = "2026.09.3"  # bump when translations change

urlpatterns = i18n_patterns(
    path(
        "jsi18n/",
        cache_page(86400, key_prefix=f"jsi18n-{RELEASE}")(
            JavaScriptCatalog.as_view(packages=["shop"])
        ),
        name="javascript-catalog",
    ),
)

go deeper

for a junior

Recall that JavaScriptCatalog is a URL you include with a script tag, after which gettext() works in JavaScript.

for a middle

Explain the djangojs domain, the packages option, the functions it defines, and why it follows the active language.

for a senior

Cache it per release and language, place it inside i18n_patterns, and keep the payload small with packages.

for a principal

Decide between Django's catalog views and a front-end i18n pipeline based on who owns translations and how the SPA is built.

## The problem it solves Browser code has no access to Python's `gettext` or to compiled `.mo` files, and you don't want to ship every translation of a large project to the client. Django's answer is a view that turns the relevant part of the catalog into JavaScript. It gives the front end the same API names the back end uses. ## Wiring it up ```python from django.urls import path from django.views.i18n import JavaScriptCatalog urlpatterns = [ path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"), ] ``` ```django <script src="{% url 'javascript-catalog' %}"></script> <script src="{% static 'shop/cart.js' %}"></script> ``` The catalog script must load before any code that calls `gettext()`. ## What the view produces | Attribute | Default | Meaning | |---|---|---| | `domain` | `"djangojs"` | which gettext domain to read; JavaScript strings live apart from the Python `django` domain | | `packages` | `None` | app names (`AppConfig.name`) whose `locale/` catalogs to include; `None` means all installed apps. `LOCALE_PATHS` is always included | The language is whatever is **active** for the request. The browser then gets these functions: - `gettext(msgid)`, `ngettext(singular, plural, count)`, `pgettext(context, msgid)`, `npgettext(context, singular, plural, count)`; - `interpolate(fmt, obj, named)`, which fills `%s` placeholders from an array, or `%(name)s` placeholders from an object when `named` is `true`; - `get_format(name)` for locale formats, `pluralidx(count)` for the plural index, and `gettext_noop(msgid)`. ```javascript const n = cart.items.length; const text = interpolate( ngettext("%(count)s item in your cart", "%(count)s items in your cart", n), { count: n }, true ); ``` When `packages` lists several apps and they translate the same string differently, the app listed **later** wins. ## Things to watch out for 1. **It is regenerated on every request.** The view reads `.mo` files and builds the script each time. The output only changes when translations do, so cache it: - server side with `cache_page(...)`, whose keys already include the active language when `USE_I18N` is on, plus a `key_prefix` that changes with each release; - client side with conditional-GET headers (`last_modified` or ETags); - or by pre-generating the catalogs as static files at build time with a third-party package. 2. **`i18n_patterns` placement.** If the root URLconf uses `i18n_patterns()`, the catalog view must be wrapped in it too. Otherwise the language prefix is not applied to the catalog request, and the catalog can be generated for the wrong language. 3. **Extraction is per domain.** Strings in `.js` files are extracted into `djangojs.po` with `makemessages -d djangojs` and compiled like any other catalog. Forgetting that gives a catalog that loads fine but translates nothing. 4. **Payload size.** With `packages=None` every app's `djangojs` strings ship to every page. A focused view, `JavaScriptCatalog.as_view(packages=["shop"])`, keeps it small. 5. **Keep sentences whole.** The JavaScript `interpolate()` runs regular-expression substitutions. Use it for placeholders, not for gluing fragments of a sentence together. ## How it picks the language The view calls `get_language()` at the start of each request and builds a translation object for that language and domain. It does not read the page that included it. The **catalog request itself** must therefore resolve to the shopper's language, through the same URL prefix, cookie or `Accept-Language` rules as any other request. That is why the `i18n_patterns` placement matters. It is also why a catalog URL cached by a CDN without regard to language can serve French text to German shoppers. Put the language in the URL, or make sure the cache varies by it. ## `JSONCatalog` for other client libraries `django.views.i18n.JSONCatalog` takes the same `domain` and `packages` options but returns JSON with `catalog`, `formats` and `plural` keys. It suits a single-page app that brings its own i18n library and only needs the data. The same caching and `i18n_patterns` advice applies.

  • The French storefront loads the catalog script, but every JavaScript string stays English. What do you check?
    Check that the `.js` strings were extracted into the `djangojs` domain and that `djangojs.mo` was compiled for French. Check that the app is included in `packages`, or that `packages` is left at the default. Check that the catalog URL sits inside `i18n_patterns` when the site uses language prefixes, so French is active for that request. Finally, check that a stale cached copy isn't being served.
  • When would you choose JSONCatalog over JavaScriptCatalog?
    When the front end brings its own i18n library, as a single-page app often does, and only needs the data: the `catalog`, the locale `formats` and the `plural` expression. It avoids loading Django's global JavaScript helper functions, and it still uses the same `domain` and `packages` options and the same per-language behaviour.

saying these in an interview costs you the question

  • JavaScriptCatalog reads the Python django domain by default.
  • The catalog is built once at startup, so caching is pointless.
  • The view can stay outside i18n_patterns even when the site uses language prefixes.
  • Strings in .js files are extracted by the normal makemessages run with no domain option.
  • JavaScriptCatalog ships all languages at once and the browser picks one.