Chapter 15 · Internationalization and time zones
15.2 Message catalogues: DjangoTranslation
DjangoTranslation holds one language’s translations: every catalogue Django finds for the language, in its own locale directory, in each installed application’s and in each directory of LOCALE_PATHS, layered in a TranslationCatalog that is searched top first, with the translations of LANGUAGE_CODE behind them as a fallback, unless the language is that one or its code begins "en". In the domain "django", a process makes one for each language the first time the language is asked for, and keeps it.
A lookup has to find a message in whichever of many files has it, and the files are ranked: a project can replace an application’s translation of a message, and an application Django’s. So Django reads all of a language’s catalogues once, into one object, with the files that should win on top. The lookup itself is told in Translating a message.
One object for each language
django keeps a dictionary, _translations, from a language’s code as it was given to its DjangoTranslation, and makes the object the first time the code is asked for. activate takes its object from there, as do the default translation, which answers when no language is active, and the fallbacks of the domain "django". All threads share the objects; a thread holds for itself only which one is active (The active language).
DjangoTranslation subclasses the standard library’s gettext and keeps its two parts: a catalogue, the dictionary a lookup searches, and a fallback, another translation asked for what the catalogue lacks. Unlike its parent, it reads many files into one catalogue.
Building a language’s translations
DjangoTranslation builds the object in this order:
- It starts with no catalogue and the Germanic plural formula, plural for every number but one, until the first catalogue found brings its own.
- In the domain
"django", the default, it ignores any directories passed as localedirs, warning “localedirs is ignored when domain is ‘django’.”; the comment says the"django"translations are cached at module level. ThenDjangoTranslationreads Django’s own catalogue from the._init_translation_catalog localedirectory beside the module the settings object’s__module__names:"django.conf", whereLazySettingsis defined, not the project’s settings module; sodjango/.conf/ locale/ - Given localedirs, it reads those directories. Otherwise
DjangoTranslationreads the._add_installed_apps_translations localedirectory of each application that has one, in the reverse ofINSTALLED_APPS. Until the application registry has imported every application’s configuration, it raisesAppRegistryNotReady, “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.” (Lazy translations). DjangoTranslationreads each directory of._add_local_translations LOCALE_PATHS, also in reverse.- If the language is
LANGUAGE_CODE, the domain"django", and nothing was found, it raisesOSError: withLANGUAGE_CODE = "xx", “No translation files found for default language xx.”. Any language that falls back to it fails too. DjangoTranslationputs the translations of._add_fallback LANGUAGE_CODEbehind.- If no file was found, the catalogue is an empty
TranslationCatalog, and every lookup goes to the fallback, if there is one.
Each catalogue read lands on top of those read before it, so the reversed orders put the first application listed above the later ones and the first directory of LOCALE_PATHS above everything.
Reading one directory
DjangoTranslation hands a directory to the standard library’s gettext, with the domain, the language’s locale name from to_locale, and fallback=True, so that a directory without the file gives a gettext rather than an error. The file is "<dir>/, but the standard library first normalizes the locale with locale.normalize and widens it: for "pt_BR" it tries "pt_BR, "pt_BR", "pt.ISO8859-1" and "pt". It returns the first file found, with any other it found chained behind it as fallbacks. It parses each file once and keeps it, by its path, in a dictionary of its own.
Merging: DjangoTranslation.merge and TranslationCatalog
DjangoTranslation passes over a result with no catalogue. The first catalogue found gives the object its plural formula and its header’s information, and becomes the bottom of a new TranslationCatalog; each later one goes to TranslationCatalog. Fallbacks the standard library chained behind a file go behind the object: for "pt-br", Django’s "pt_BR" catalogue is the catalogue, and its "pt" catalogue stands behind it, before the translations of LANGUAGE_CODE.
TranslationCatalog is a stack of dictionaries that answers as one, top first. Its docstring gives its purpose: to keep catalogues whose plural equations differ apart. A catalogue stores a plural message’s forms under the message and the form’s index, and an index means something only under its own file’s formula. So each dictionary keeps its formula beside it, and TranslationCatalog asks each for the form its own formula picks, raising KeyError when none has it.
The comment in TranslationCatalog says it merges a catalogue into the top dictionary when their plural functions are the same, and otherwise puts it on top as a dictionary of its own. At the pin the condition compares the new catalogue’s formula’s code object with the top dictionary’s formula, a function, and the two are never equal: every catalogue after the first goes on top as its own dictionary. The example’s Dutch translations hold two, the office’s above Django’s, though both files have the formula "nplurals=2; plural=(n != 1);". A lookup gives what a merge would have given, since the later catalogue wins either way; it tries one dictionary for each file.
The fallback
DjangoTranslation sets none for LANGUAGE_CODE itself, nor for a code beginning "en"; the comment’s reason for the second is that an English catalogue is empty, so an English lookup would always fall through to the default language. Django’s English catalogue holds nothing but its header. For the domain "django" the fallback is the cached translation of LANGUAGE_CODE; for another, a new DjangoTranslation of LANGUAGE_CODE in that domain, made each time. JavaScriptCatalog makes one of the domain "djangojs" for each request, with the locale directories of the packages it is asked for as localedirs (The language views: set_language and JavaScriptCatalog).
In a run with LANGUAGE_CODE = "de" and, in LOCALE_PATHS, a German catalogue that translates “Tide table” as “Gezeitentafel”, the Dutch translations gave “Gezeitentafel” from their fallback. In the example the fallback, "en", has nothing to give, and translation("nl") gave “Arrivals” as “Aankomsten” from the office’s catalogue, “January” as “januari” from Django’s own, and “Tide table” as it was:
"nl" in the example, top first. A lookup stops at the first catalogue that has the message; a message that none of them has, nor the translations of LANGUAGE_CODE behind them, comes back as it was given.Language codes and locale names
A directory of catalogues is named by a locale, "nl_BE", not a language code, "nl-be". django turns one into the other: the language in lower case, an underscore, then a region of two letters in capitals or a longer part with its first letter capital ("sr-latn" becomes "sr_Latn"), and any third part after a hyphen. to_language goes back, in lower case with a hyphen.
django lists where a catalogue may be: Django’s own directory, those of LOCALE_PATHS, and the locale directory of each application that has one. check_for_language searches them to tell whether a language has a catalogue at all (Choosing a language: LocaleMiddleware).
When the translations are thrown away
The facade connects two receivers of the autoreloader the first time a translation function is used with USE_I18N true (Translating a message). django has the reloader watch every .mo file under the locale directory where the process runs, under each application’s that is not Django’s own, and under each directory of LOCALE_PATHS. When one changes, translation_file_changed empties the standard library’s dictionary of parsed files and _translations, forgets the default translation, and replaces the holder of the active language, so that no thread has one active. It returns true, so the server goes on in the same process (The autoreloader).
When a test overrides INSTALLED_APPS, LANGUAGES or LOCALE_PATHS, receivers of setting_changed empty _translations. An override of LANGUAGE_CODE leaves it, and forgets only the default translation and the active one, so a language built before keeps the old default behind it (Overriding settings).