Skip to content
AZ Tools

Gettext .mo Catalogue Inspector

A .mo file is what msgfmt leaves behind after it compiles a .po, and it is the file that actually ships in locale/<lang>/LC_MESSAGES/. Once the .po is lost it is a binary nobody can read, even though the structure is small: a magic number, a revision, and the counts and offsets of two string tables, plus an optional hash table that readers are free to ignore. This inspector walks all of it and shows what is really inside. The magic number is 0x950412de and also its byte swap, 0xde120495 — that is how the file declares which byte order the rest of its words are written in, so a catalogue built for a big-endian machine reads exactly the same here as a little-endian one. The two things that hide in a .mo are both embedded NUL bytes, invisible in a text editor and easy to get wrong: a context-qualified entry stores msgctxt, a 0x04 byte and then the msgid in a single string, and a plural entry stores msgid and msgid_plural separated by a NUL with the translated forms NUL-joined on the other side. Both are split here, so contexts get their own column and plural forms are listed and numbered. The metadata entry — the one whose msgid is empty — is parsed into Project-Id-Version, Language, Content-Type, Plural-Forms and POT-Creation-Date, and every other string in the file is decoded with the charset that header declares rather than with a guess. When the header is missing or lies, the page says so instead of silently producing mojibake. The plural expression is parsed and evaluated for n = 0 to 20, so you can see which form each count selects, and the number of forms an entry carries is checked against nplurals — a mismatch is a real bug that gives the wrong string for some counts and nothing else reports it. It also calls out untranslated entries, translations that are byte-identical to their msgid, a %s or {} that the translation dropped, an originals table that is not sorted (the sort is what the binary search in C readers relies on) and a hash table with fewer slots than there are entries. It does not turn the catalogue back into a .po, and it cannot tell you whether a translation is good — only whether the file is structurally sound and internally consistent.

How to use

  1. Drop a .mo file onto the box — the compiled catalogue from locale/<lang>/LC_MESSAGES/, or anything msgfmt produced.
  2. Read the file header row first: byte order, revision, entry count, and where the two string tables and the hash table actually sit in the file.
  3. Check the metadata entry for the charset. Every string below is decoded with it, so a wrong or missing Content-Type is the first thing to fix.
  4. Look at the plural rule table for n = 0 to 20 to see which form each count selects, and compare the form count of each plural entry against nplurals.
  5. Scan the findings list, then use the filter to jump to a msgid, a translation or a context in the entry table.

Frequently asked questions

Why does the magic number appear as two different values?
There is only one magic number, 0x950412de, but a .mo file stores every 32-bit word in the byte order of the machine that compiled it, and the magic is stored the same way. A reader loads the first four bytes both ways: if they come out as 0x950412de read little-endian, the whole file is little-endian; if they come out that way read big-endian, the whole file is big-endian. That is the entire endianness declaration — there is no flag anywhere else — which is why a catalogue compiled on a big-endian host still works on a little-endian one. This page shows the raw four bytes next to the order it deduced, so you can check the deduction rather than trust it.
What is the 0x04 byte inside some msgids?
It is how msgctxt is stored. gettext has no separate field for a context, so msgfmt glues the context, a single 0x04 byte (ASCII EOT) and the msgid into one string, and pgettext looks the combined string up. That is why two entries can share the msgid "Open" and still be different entries: one is "menu\x04Open" and the other is "state\x04Open". In a text editor or a naive dump the separator is invisible and the two look like duplicates. This inspector splits on it and gives the context its own column, so entries that share a msgid line up next to each other and you can see which one a given call site will get.
How are plural forms stored, and what does nplurals have to match?
On the original side a plural entry is msgid, a NUL byte, then msgid_plural. On the translation side it is every translated form joined by NUL bytes. The header decides how many forms there should be: Plural-Forms carries nplurals and an expression in n that returns the index of the form to use. If an entry has fewer forms than nplurals promises, the counts that select the missing index get nothing back and gettext falls back to the untranslated msgid or msgid_plural — for some numbers only, which is why the bug survives testing. This page counts the forms of every plural entry and flags the ones that disagree with the header.
What happens when the Content-Type header is missing or wrong?
Nothing in the file marks its own encoding except that header, so a reader has to trust it. Python's gettext refuses the catalogue outright: an unfilled charset=CHARSET raises an unknown-encoding error, and bytes that are not valid in the declared charset raise a decoding error. This page is deliberately more forgiving — it falls back to UTF-8, decodes what it can, and tells you which of the two happened, because you usually opened the file precisely to find out why an application was showing garbage. A catalogue with no metadata entry at all has no charset, no language and no plural rule; gettext then treats the strings as ASCII and applies the Germanic n != 1 rule, which is fine until the first non-ASCII translation.
Does the order of the entries matter?
Yes, and this is the requirement most hand-written .mo writers miss. The gettext specification says the original strings must be sorted in byte order, because a reader is allowed to find an entry with a binary search over the originals table. The C implementation does exactly that when there is no usable hash table, so an unsorted catalogue silently fails to find some entries while python, which builds a dictionary from the whole file, reads it perfectly. That is a nasty class of bug: it works in your test script and not in the application. This page checks the sort and reports how many pairs are out of order.
What is the hash table for, and can I ignore it?
It is an optional lookup accelerator: msgfmt writes a hashpjw table with roughly a third more slots than there are entries and uses open addressing, so a reader can find a msgid without the binary search. Readers may ignore it entirely — python does — and a catalogue with a size of 0 is perfectly valid. What is not valid is a table with no more slots than there are entries, since open addressing needs at least one slot to stay free; that combination means the file was written by something that did not understand the format, so it is reported here. The size and offset shown come straight from the header words.

Related tools