Internationalization and Translation Services
Django's internationalization (i18n) system provides a robust engine for managing translation catalogs and switching languages at runtime. The implementation relies on a lazy-loading proxy that selects between a real translation engine and a dummy one, combined with a sophisticated merging system that aggregates translations from across the entire project.
The Translation Engine Proxy
The entry point for translation services is the Trans class in django/utils/translation/__init__.py. This class acts as a proxy, deferring the selection of the actual translation implementation until the first translation function is called.
The engine selection is governed by the USE_I18N setting:
- If
USE_I18NisTrue, it loadsdjango.utils.translation.trans_real. - If
USE_I18NisFalse, it loadsdjango.utils.translation.trans_null.
class Trans:
def __getattr__(self, real_name):
from django.conf import settings
if settings.USE_I18N:
from django.utils.translation import trans_real as trans
# ... connects autoreload signals ...
else:
from django.utils.translation import trans_null as trans
# Cache the function on this instance for performance
setattr(self, real_name, getattr(trans, real_name))
return getattr(trans, real_name)
When USE_I18N is enabled, the proxy also connects to Django's autoreloader via autoreload_started and file_changed signals, ensuring that translation catalogs are reloaded when .mo files change on disk.
Catalog Aggregation and Merging
The core of the real translation engine is DjangoTranslation, located in django/utils/translation/trans_real.py. This class extends Python's standard gettext.GNUTranslations but adds the ability to merge multiple translation sources into a single object.
The Merging Process
When a DjangoTranslation object is initialized for a specific language, it aggregates translations in a specific order:
- Django Core: Loads translations from Django's own
localedirectory. - Installed Apps: Iterates through
INSTALLED_APPSand merges translations from each app'slocale/directory. - Local Translations: Merges translations from paths defined in the
LOCALE_PATHSsetting.
The _add_installed_apps_translations method ensures that the application registry is ready before attempting to load app-specific translations:
def _add_installed_apps_translations(self):
try:
app_configs = reversed(apps.get_app_configs())
except AppRegistryNotReady:
raise AppRegistryNotReady(
"The translation infrastructure cannot be initialized before the "
"apps registry is ready. Check that you don't make non-lazy "
"gettext calls at import time."
)
for app_config in app_configs:
localedir = os.path.join(app_config.path, "locale")
if os.path.exists(localedir):
translation = self._new_gnu_trans(localedir)
self.merge(translation)
Handling Pluralization Rules
Because different apps might provide translations for languages with different pluralization rules, Django uses the TranslationCatalog class to manage the internal _catalog dictionary.
Instead of a simple dictionary, TranslationCatalog maintains a list of catalogs and their corresponding pluralization functions. When merge() is called on DjangoTranslation, it checks if the new catalog's pluralization logic matches the existing one. If it doesn't, it stores them separately to ensure correct plural forms are retrieved for each source.
def update(self, trans):
# Merge if plural function is the same as the top catalog, else prepend.
if trans.plural.__code__ == self._plurals[0]:
self._catalogs[0].update(trans._catalog)
else:
self._catalogs.insert(0, trans._catalog.copy())
self._plurals.insert(0, trans.plural)
Runtime Language Switching
Django allows developers to temporarily change the active language using the override context manager in django/utils/translation/__init__.py. This is essential for tasks like generating localized emails or reversing URLs in a specific language.
The override class captures the current language in __enter__ and restores it in __exit__:
class override(ContextDecorator):
def __enter__(self):
self.old_language = get_language()
if self.language is not None:
activate(self.language)
else:
deactivate_all()
def __exit__(self, exc_type, exc_value, traceback):
if self.old_language is None:
deactivate_all()
# ... restores the old language ...
Real-World Usage: URL Translation
A primary use case for override is found in django/urls/base.py, where it is used to translate a URL path into a different language:
# From django/urls/base.py
def translate_url(url, lang_code):
# ... logic to find the view ...
with override(lang_code):
try:
url = reverse(to_be_reversed, args=match.args, kwargs=match.kwargs)
except NoReverseMatch:
pass
return url
Translation Fallbacks
If a translation is missing in the requested language, DjangoTranslation manages fallbacks. By default, it falls back to the language defined in settings.LANGUAGE_CODE.
The ngettext implementation in DjangoTranslation demonstrates this fallback logic: if a pluralized string is not found in the current TranslationCatalog, it attempts to use the _fallback object before defaulting to the original English strings.
def ngettext(self, msgid1, msgid2, n):
try:
tmsg = self._catalog.plural(msgid1, n)
except KeyError:
if self._fallback:
return self._fallback.ngettext(msgid1, msgid2, n)
# Default to original strings if no translation or fallback exists
return msgid1 if n == 1 else msgid2
return tmsg
This layered approach—from app-specific catalogs to project-wide fallbacks—ensures that the system remains functional even when translations are incomplete.