Internationalization Case Study: The Django Framework
When discussing internationalization, it's easy to get caught up in theoretical debates of best practices and implementation strategies. In this case study, we'll cut through that by taking a look at how the Django framework, a full-stack web development framework written in Python, provides internationalization options and documentation to developers. In doing so, we'll develop an understanding of the specific components of internationalization that require developer attention and discover how a widely used web framework thinks about supporting this work, then discuss how to apply the same philosophy in your own work. Specifically, we'll cover
- how internationalization encompasses translation, localized formatting, time zone support, and globally available deployment;
- how the Django framework supports developer efforts in these areas; and
- reproducible practices for development in other frameworks.
This is not a Django tutorial, and the concepts will be relevant to your website or application regardless of which languages and frameworks you build it with. I chose to discuss Django because of my own familiarity with the framework and because Django's core developers chose to invest in internationalization support as a first-party package within the framework rather than creating it as an add-on. This suggests that their development philosophy considers internationalization as a core feature rather than a late-stage addition for web development projects, which is exactly the approach that we should seek to understand and emulate.
I've written about localization for developers with WonderProxy before, as well as an argument for localization in support of business goals. The internationalization work that developers do in support of localization provides critical human and business benefits by multiplying the audience for your application and supporting the needs of users around the world. This case study assumes you're already sold on investing developer time on internationalizing your application.
Definitions
The Django documentation provides excellent, concise definitions for internationalization and localization.
- internationalization: Preparing the software for localization. Usually done by developers.
- localization: Writing the translations and local formats. Usually done by translators.
We'll use these definitions for the rest of this case study.
I18N Task 1: Translations
Django supports a fairly standard architecture for translatable strings. As a developer, you use your normal strings, wrapped in a function, throughout both backend code and templates. In each app, you add a locales
folder, which contains string files that map the default for each string to its translation in each supported language. There is no automated translation provided by the framework, only the ability to load hard-coded strings prepared for you by a translator. That said, Django makes a few interesting decisions in the implementation details.
One such decision is that strings are referenced by the original string rather than a semantic key. That means that the internationalization framework looks up the string "Hello, World!"
by its contents rather than a developer-specified variable name, like helloWorld
. In my opinion, this is a good choice as it makes the code and templates more readable to developers. The downside of this choice is that if you change the value of a string, it now must be changed in two places, which could potentially cause errors.
By convention, Django developers tend to alias the translation function to save keystrokes and keep code concise and readable. The conventional abbreviation is accomplished during import, such as from django.utils.translation import gettext_lazy as _
. Then, all strings that require translation are simply loaded as _("Hello, World!")
. This highlights the need for an internationalization framework to be as minimally impactful as possible on developer workflows.
Another place that internationalization should be lightweight is in application size so as to minimize server load and maximize page speed. Django translations work towards this goal by compressing translation strings for deployment. Message files are structured plain text with a .po extension. Django then uses the django-admin compilemessages
command to compile these files into .mo files. These binary files are optimized for use by the gettext
function family.
Finally, Django helps developers support both translation and application speed by providing lazy-loading options for translations. While gettext
performs the translation as soon as the function is called, gettext_lazy
waits until the string is going to be rendered before translating it, which can in some circumstances avoid needless translation activity.
I18N Task 2: Formatting
Just as words need to be translated to various languages, so to do different cultures use different formats and units for numerical measurements like weights and currency. Django can automatically format these values everywhere from forms to templates using its localization framework. The formatting is selected automatically based on the locale in the request.
Django uses templates for server-side rendering. In this approach, a view function passes data into a template, which is then rendered into HTML and sent to the user. Just like lazy loading of string translations, number formatting takes place in this last step before the rendered page is sent to the user.
There are two ways to format a number in a template, but regardless of the method, the template must include {% load l10n %}
at the beginning of the file. If only a couple of numbers need formatting, Django enforces localization with {{ value|localize }}
. The pipe character is the filter in a template tag; basically it passes the value on its left into the function on its right and then renders the result. To unlocalize a value that otherwise would be given a localized format, a similar unlocalize
function exists that can be applied in the same manner. For larger sections, localization blocks format all values within them. Here's an example:
{% localize on %}
<tr>
<td>{{ value }}</td>
<td>{{ value }}</td>
<td>{{ value }}</td>
</tr>
{% endlocalize %}
For forms, localized formatting is again a per-field value. The localize
variable is optional in form field creation and defaults to False
when not present. Between this and the template behavior, we see a pretty straightforward way of handling localized values. A programmer needs the ability to turn on and off formatting for broad swaths of the application and for individual values. Value formatting should be turned off by default but require minimal configuration and code to turn on. The Django framework has found a straightforward way to meet these needs, and your application's stack should hopefully offer something similar.
I18N Task 3: Time Zones
Working with datetimes across multiple time zones is many a programmer's biggest fear. After all, though I was born when programmers were racing to protect their applications against the Y2K bug, even still I've heard tell of it. Dates, times, and time zones are some of the most complex sources of edge cases and weird bugs that programmers regularly have to deal with. As such, Django, like most other frameworks, provides a robust library that abstracts away most of the messiness to make it possible, if not fun, to coordinate your application across time zones. That said, even Django relies on prior work in handling datetimes, in this case the pytz
library, which comes with its own set of behavioral quirks.
In the FAQ, the documentation writers strongly recommend using timezone-aware datetimes, even if an application doesn't explicitly support multiple time zones. The time zone support doubles as support for events like daylight savings time and leap days that can knock out an otherwise well-functioning application. However, even using timezone-aware datetimes does not fully insulate a programmer from running into related bugs. Introducing naive math rather than only operating in protected datetime objects can still break the system, as can inconsistencies between the application and third party services with incompatible handling of datetime objects.
As for takeaways for your own internationalization efforts, I'd advise following Django's example and avoid implementing your own datetime system if at all possible. Due to the vast number of edge cases, just put a locale into a library and be done with it.
A Note on Settings
Django provides two settings: USE_I18N
to enable the translation system and USE_L10N
to enable the data formatting system. By default, these are set to True
in the settings file generated by the django-admin startproject
command. These settings exist so that projects that don't use internationalization features of the framework can disable them for performance optimization, but the fact that they are enabled by default suggests that the Django core developers believe most projects big enough to care about these optimizations are also big enough to be worth internationalizing.
Django also handles some time zone information in its settings. USE_TZ
is set to True
in the default settings file. This option instructs Django to handle datetimes in a timezone-aware manner throughout the entire application.
I18N Task 4: Global Deployment
Deploying a Django application is a varied and complex process well outside the scope of this case study. However, one thing that Django supports is the global availability of localized content. Though it's generally a good idea to use a CDN or other non-Django application to deploy static assets around the world, modern cloud infrastructure allows Django developers to handle huge amounts of traffic from across the globe.
Django is a framework for creating monolithic web applications. This means that they are designed to be developed and deployed as a single entity. However, in the past decade Django has adapted to the needs of modern websites and fully supports the use of elastic cloud services to scale up and down a deployment in response to traffic and to ensure worldwide availability.
As you internationalize an application with an eye towards deployment, you'll want to make sure that localized content is loading quickly in the correct locations. When you're done reading this case study, check out how WonderProxy can help test your application.
Hook-Driven Development
Every pattern in Django internationalization relies on using built-in settings and included libraries to abstract away as much as possible from the developer to minimize the burden of preparing an application for localization. As a play on test-driven development and similar philosophies, one could think about Django's approach to providing internationalization support as a kind of hook-driven development. This is particularly evident in how Django handles accessing localized content through specific URL patterns.
A common method of presenting localized content is to internationalize URL patterns by including a language code or locale right after the root of the URL, often with a single locale specified as the default. For example, a site with an English default but support for Spanish might serve its about page, localized as appropriate, at both example.com/about and example.com/es/about (or example.com/es/sobre if the URL patterns are also translated, which Django supports). Django supports the programmer by providing functions that apply language codes to whole blocks of URL patterns and automatically load the appropriate translations and formats, that is, Django provides a hook for programmers to internationalize URL patterns. Similarly, the programmer supports the end user by providing the option to use a URL to specify which localized version of the application they would like to receive, a kind of non-programmatic hook.
While this isn't a precise use of the concept of hooking, it's a useful way to think about building internationalization options into developer tools and end-user applications. As we defined at the beginning of the case study, internationalization is the work done by developers to deliver localized content; internationalization is a hook for localization. Taking a hook-driven development approach to internationalizing an application should naturally lead to implementing the most common aspects of internationalization and deploying to users worldwide. In most stacks, you're not starting from scratch; seek out internationalization hooks at the beginning of the development process and build the application naturally around the tools that the framework provides.
In Conclusion
Internationalization is how a programmer prepares their application to support localization efforts. Frameworks like Django implement developer tools to assist with internationalization and, in doing so, demonstrate exactly what responsibilities a programmer must bear in preparing their application. Internationalization is not a single subject, it concerns supporting at least three visible aspects — translation of strings, formatting of numbers, and localizing of datetimes — as well as one invisible aspect — globally available deployment. Each of these is accessible to both the developer and then the user using hooks, whether they be function calls or URL patterns. Using the right tools makes this difficult but valuable process minimally burdensome and tightly integrated with the rest of the application development process.
Further Reading
If you're interested in further details of how to implement an internationalized application in Django, I encourage you to take a look at the following pieces of documentation: