Internationalization in Javascript Code

Adding translations to JavaScript poses some problems:

  • JavaScript code doesn’t have access to a gettext implementation.
  • JavaScript code doesn’t have access to .po or .mo files; they need to be delivered by the server.
  • The translation catalogs for JavaScript should be kept as small as possible.
    Django provides an integrated solution for these problems: It passes the translations into JavaScript, so you can call gettext, etc., from within JavaScript.

The Javascript_Catalog View

The main solution to these problems is the django.views.i18n.javascript_catalog() view, which sends out a JavaScript code library with functions that mimic the gettext interface, plus an array of translation strings.

Those translation strings are taken from applications or Django core, according to what you specify in either the info_dict or the URL. Paths listed in LOCALE_PATHS are also included.

You hook it up like this:

from django.views.i18n import javascript_catalog

js_info_dict = {
    'packages': ('your.app.package',),
}

urlpatterns = [
    url(r'^jsi18n/$', javascript_catalog, js_info_dict),
]

Each string in packages should be in Python dotted-package syntax (the same format as the strings in INSTALLED_APPS) and should refer to a package that contains a locale directory. If you specify multiple packages, all those catalogs are merged into one catalog. This is useful if you have JavaScript that uses strings from different applications.

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.

By default, the view uses the djangojs gettext domain. This can be changed by altering the domain argument.

You can make the view dynamic by putting the packages into the URL pattern:

urlpatterns = [
    url(r'^jsi18n/(?P<packages>\S+?)/$', javascript_catalog),
]

With this, you specify the packages as a list of package names delimited by ‘+’ signs in the URL. This is especially useful if your pages use code from 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 INSTALLED_APPS setting.

The JavaScript translations found in the paths listed in the LOCALE_PATHS setting are also always included. To keep consistency with the translations lookup order algorithm used for Python and templates, the directories listed in LOCALE_PATHS have the highest precedence with the ones appearing first having higher precedence than the ones appearing later.

Using The Javascript Translation Catalog

To use the catalog, just pull in the dynamically generated script like this:

<script type="text/javascript" src="{% url ‘django.views.i18n.javascript_catalog’ %}"></script>

This uses reverse URL lookup to find the URL of the JavaScript catalog view. When the catalog is loaded, your JavaScript code can use the standard gettext interface to access it:

document.write(gettext('this is to be translated'));
There is also an `ngettext` interface:

var object_cnt = 1 // or 0, or 2, or 3, ...
s = ngettext('literal for the singular case', 
      'literal for the plural case', object_cnt);

and even a string interpolation function:

function interpolate(fmt, obj, named);

The interpolation syntax is borrowed from Python, so the interpolate function supports both positional and named interpolation:

  • Positional interpolation: obj contains a JavaScript array object whose elements values are then sequentially interpolated in their corresponding fmt placeholders in the same order they appear. For example:
      fmts = ngettext('There is %s object. Remaining: %s', 
               'There are %s objects. Remaining: %s', 11);
      s = interpolate(fmts, [11, 20]);
      // s is 'There are 11 objects. Remaining: 20'
    
  • Named interpolation: This mode is selected by passing the optional boolean named parameter as true. obj contains a JavaScript object or associative array. For example:
      d = {
          count: 10,
          total: 50
      };
    
      fmts = ngettext('Total: %(total)s, there is %(count)s object', 
        'there are %(count)s of a total of %(total)s objects', d.count);
      s = interpolate(fmts, d, true);
    

You shouldn’t go over the top with string interpolation, though: this is still JavaScript, so the code has to make repeated regular-expression substitutions. This isn’t as fast as string interpolation in Python, so keep it to those cases where you really need it (for example, in conjunction with ngettext to produce proper pluralization).

Note On Performance

The 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.

Server-side caching will reduce CPU load. It’s easily implemented with the cache_page() decorator. To trigger cache invalidation when your translations change, provide a version-dependent key 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

# 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)

Client-side caching will save bandwidth and make your site load faster. If you’re using ETags (USE_ETAGS = True), you’re already covered. Otherwise, you can apply conditional decorators. In the following example, the cache is invalidated 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

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)

You can even pre-generate the JavaScript catalog as part of your deployment procedure and serve it as a static file.