Chapter 15 · Internationalization and time zones
15.10 Making catalogues: makemessages and compilemessages
makemessages collects the messages that a project’s Python files, templates and JavaScript mark for translation into a .po file for each locale, and compilemessages compiles each .po file into the .mo catalogue Django reads. Both drive the GNU gettext programs; Django adds the choice of files and of where their messages go, and django, which rewrites a template as something xgettext can read.
A catalogue passes through three files. xgettext reads source files and writes a template of their messages, a .pot file, with every translation empty; msgmerge merges the template into a locale’s .po file, keeping what translators wrote there; msgfmt compiles the .po file into the .mo file. Each runs as a child process through popen_wrapper. A command first looks for each on PATH with find_command, and stops with a CommandError asking for GNU gettext 0.19 or newer when one is missing; the version is not checked.
makemessages, in order
The command’s Command.handle works in this order:
- It checks the options. The domain,
--domain, is"django", the default, or"djangojs"; the extensions are those of--extension, else"html","txt"and"py", or"js"for"djangojs". One of--locale,--excludeand--allis required. - It settles the locale directories, where the
.pofiles go, and the default among them. - It settles the locales: with
--all, each directory under the default locale directory whose name begins with two lower-case letters,--excludeignored; otherwise those of--locale, or that list when only--excludeis given, less those of--exclude. - It looks for msguniq, msgmerge and msgattrib, if there are locales, and xgettext.
Commandwrites a.build_potfiles .potfile into each locale directory that receives messages.- For each locale, it checks the name and writes a
.pofile under each.potfile’s directory. - It deletes the
.potfiles, after an error too, unless--keep-potis given.
In the "django" domain a file not ending .py, a .js file added with --extension too, is read as a template.
Where the messages go
A locale directory holds a directory for each locale, as "locale/. Run in a directory that has "conf/locale", as Django’s own package does, makemessages starts with that one candidate; otherwise with the directories of LOCALE_PATHS, then a locale directory where it runs. The first is the default, made if missing. makemessages sets BaseCommand false; run without settings, it has no LOCALE_PATHS and skips neither MEDIA_ROOT nor STATIC_ROOT.
Command walks the tree from where the command runs, passing over what matches an ignore pattern (those of --ignore, and "CVS", ".*", "*~" and "*.pyc" unless --no-default-ignore is given) and the directories MEDIA_ROOT and STATIC_ROOT name. Each locale directory it meets goes to the front of the candidates and is not descended into. A file goes to the first candidate whose parent directory’s path begins its own directory’s path: as the walk goes downwards, the nearest locale directory above it, though a file in an "office2/" would go to "office/locale/" too. So the office’s files go to "office/locale/". A file with none above it goes to the default; with no default, a message in it stops the command with a CommandError.
From files to a template of messages
Command handles one locale directory’s files together, making a BuildFile for each. A file of the "django" domain that is not Python is templatized: BuildFile rewrites it with django and writes the result beside it, its name with .py added, "arrivals. One xgettext then reads the group’s files, copies for templates, from a list in a temporary file. It is told the language, Python or JavaScript, and names of Django’s functions, some with the place of their arguments: "--keyword=pgettext: makes the first a context and the second the message. Command adds "--add-comments=Translators": as the documentation says, a comment beginning “Translators” on the line before a message appears with it in the .po file.
BuildFile puts each template’s path back where the #: lines name its copy, write_pot_file writes the .pot file, the copies are deleted, and msguniq is run over each .pot file.
A template rewritten as Python
django turns a Django template into text in which xgettext, reading Python, finds the template’s messages. It cuts the template into tokens with the template engine’s Lexer (From text to nodes: the lexer and the parser) and writes each anew. On the example’s template, "office/, it gives:
BBBB BBBBXXXXXXXXX
XXX ngettext(u'One ship expected.', u'%(counter)s ships expected.', count) SSS SSSS SSSSSSSSSPPPPPPPPPPP PPPPP PPPPPPPPPXXXX
XXXX
BBB BBBB BB BBBBB XXXXX X XXXXXX
BBBBBBXXXXX
A translation tag becomes the call xgettext knows: {% translate %} a call of gettext, or of pgettext with a context; {% blocktranslate %} the same, or with {% plural %} a call of ngettext (npgettext with a context), written at the end tag, with its variables as %(counter)s and a % in its text doubled. The rest, but comments, goes through blankout, which turns each character that is not whitespace into a letter: B for a tag, X for text, F for a filter, S and P for a block’s singular and plural. A variable outside a block leaves only its filters; a _("…"), in a tag, a variable or a filter’s argument, is copied as it stands. The whitespace stays, so the line numbers xgettext writes are the template’s, except below a {% comment %} with no line for translators that ends mid-line, which adds a line. {{ title }} left nothing: “Arrivals” is found in "office/, where it is marked.
A comment for translators becomes a Python comment: of a {% comment %} block, the lines from the last that begins “Translators”; a {# … #} comment beginning “Translators”, unless a tag or a variable follows it on its line, when it is dropped with a TranslatorCommentWarning. In these lines of the harbour’s berths, the first comment is kept and the second dropped, with a warning that it “wasn’t the last item on the line”:
{# Translators: a berth with no ship in it #}
<td>{% translate "Free" context "berth" %}</td>
<td>{# Translators: a service with no charge #}{% translate "Free" context "price" %}</td>
From a template of messages to each .po file
Command writes <locale directory>/. An existing file msgmerge updates in place, keeping its translations; the documentation notes that an entry may come out fuzzy, its translation inferred from strings translated before. A new file is the .pot file copied, and Command puts into its header the Plural-Forms line of Django’s own catalogue for the locale, where Django has one, so that it starts with its language’s plural formula. With --no-obsolete, msgattrib then removes the obsolete messages.
makemessages takes a locale name, "nl_BE", not a language code: a name it refuses, "nl-be" or "NL", is skipped, with a line proposing the name it guesses, "nl_BE" or "nl".
compilemessages
compilemessages looks for msgfmt, then gathers locale directories: "conf/locale" and locale where it runs, those of LOCALE_PATHS when the environment names a settings module, and each locale directory found by walking the tree, less those --ignore matches; with none, it stops with a CommandError. It compiles the .po files of the locales of --locale, else of every one found, less those of --exclude; where that leaves none, of every locale, the excluded too.
Command passes over a .po file whose .mo file is at least as new, refuses one that begins with a byte order mark, and gives up a locale directory when the first file, if it is to be compiled, is in a directory that cannot be written. Each remaining file goes to a thread pool that runs msgfmt with "--check-format": the documentation shows compilemessages failing where a translation’s format specifications differ from its message’s. --use-fuzzy adds "-f", which compiles fuzzy entries too. A failure does not stop the others; the command ends with one CommandError.
The .mo files are read when a language’s translations are made (Message catalogues: DjangoTranslation).