A .po file is a catalog of source strings with their translations, and the build step that turns it into a binary catalog is msgfmt. With the checking switch on, that program refuses a file for a short list of reasons: the format placeholders of a string and its translation disagree, the number of plural lines does not fit the header, the header has no usable plural rule, an escape sequence is unknown, the same key is defined twice, or the declared character set is not a real one. Some things that look like errors are accepted without a word, an empty translation and an entry marked fuzzy among them. This guide goes through each rejection, shows what it looks like when it happens, and lists the parts of a .po file that a translator, human or machine, has to leave exactly as they came.
What the switch actually runs
The manual describes --check as the combination of three narrower checks: one for format strings, one for the header entry and one that looks for domain directives clashing with the chosen output name (GNU gettext manual, msgfmt). A separate option, for keyboard accelerators, exists but is not part of that bundle. Everything below belongs to the first two checks, plus the syntax errors that the reader of the file raises before any check starts.
The checks are only as informed as the file. The format check, for example, does not guess; it looks at the flags on the comment line that begins with #,. We ran msgfmt --check on a small catalog and then on edited copies of it, and every rejection below was seen in that run, with the message texts recorded in the saved test notes. The behaviour belongs to the release we used and may differ in other versions.
Placeholders must match, but only where a flag says so
For a string that feeds a printf-style call, the manual wants source and translation to hold equal counts of % specifiers whose types agree, and it runs this test when the entry carries a c-format or possible-c-format flag. It lists the mismatches it diagnoses, such as %d against %s, and says that positional parameters are handled too (GNU gettext manual, msgfmt).
In our run, putting %d where the source had %s produced a message that the specifications for argument one are not the same, and deleting the placeholder altogether produced a message that the number of specifications does not match. Both stopped the build.
Two consequences follow:
- The same mistake in an entry without a format flag compiled without any message. The error then shows up later, as a crash or a garbled line in the running program, which is worse than a failed build.
- A translator must not remove the flag to silence the error. The manual says that such a fix would be undone the next time the catalog is merged, because the extraction tool sets the flag from the source code, and that the flags should not be added by hand (GNU gettext manual, sticky flags). It also admits that the tool's decision about a string is imperfect and that a programmer can override it, so a false alarm is a bug report for the developer, not a reason to edit the flag line.
Plural lines must fit the header
A plural entry has a msgid_plural line and then one msgstr[n] line per form (GNU gettext manual, plural entries). How many forms exist is not decided in the entry. It is declared once, in the header entry, identifiable by its empty msgid. The manual shows the line as a field named Plural-Forms holding an nplurals count and a plural expression, says nplurals is a decimal count of forms, and says the expression, written in C syntax with n as the only variable, must always give a value from zero up to one less than that count (GNU gettext manual, plural forms).
Three breaks follow from that, and we reproduced all of them:
- Too few lines. Deleting
msgstr[1]from a catalog whose header says two forms gives a fatal message that the header declares two forms but some messages have only one. - A header copied from the wrong language. Changing the declared count to three, with entries that have two lines, gives the same kind of message the other way round: three declared, two found. A rule copied from another language triggers this exactly.
- No usable rule. Removing the
Plural-Formsline while plural entries remain gives two fatal messages, one for the missingplural=expression and one for the missingnplurals=value.
The same manual page lists the standard lines per language family. Bulgarian, English and German use two forms with the rule n != 1, and Japanese uses a single form with plural=0 (GNU gettext manual, plural forms). The practical lesson is that the header is a statement about the language, so it is written from the language's rule, never inferred from how many lines a draft happened to contain. The header page adds that the field is optional and only needed when the file has plural forms, which can be found by searching for msgid_plural (GNU gettext manual, header entry).
The header, the character set and the escapes
The header entry carries the encoding in its Content-Type line, and the manual says the placeholder CHARSET has to be replaced with the encoding used, or with UTF-8, because msgmerge and msgfmt rely on it (GNU gettext manual, header entry). Our run agrees: leaving the literal word CHARSET made msgfmt stop, complaining that the charset it found cannot be used as an encoding name. Missing descriptive fields such as Last-Translator, by contrast, only produced warnings, and the file still compiled.
Each string sits inside double quotes and uses backslash escapes, which the manual mentions in passing while telling translators that their editor normally takes care of quoting (GNU gettext manual, entries). The manual page we read does not list which escapes are valid, so this part rests on our test alone: a backslash plus a letter that names no escape, placed in both the source string and the translation, made the reader report an invalid control sequence for each. A Windows path or a regular expression pasted into a message is the usual way to hit this.
Duplicates, context and what is not an error
The manual explains that several entries may share the same source string as long as each has its own context, and that an empty context is not the same as no context line (GNU gettext manual, entries with context). Two entries with the same source string and the same context are a different matter. In our run a second definition of one key was rejected with a message that points back to the location of the first. Choosing which copy survives means judging the translations, so a repair that drops one silently is guessing.
Then the cases that look like errors and are not. An entry with an empty translation is simply counted as untranslated: our statistics line reported it as such and the exit status stayed zero. An entry flagged fuzzy is skipped for the output. The manual says the flag shows that the translation might not be correct, that only a translator can decide, and that the tool which merges catalogs sets it after a fuzzy match (GNU gettext manual, workflow flags). The switch that includes fuzzy entries in the output is described as usually wrong, because those are the messages no human has validated (GNU gettext manual, msgfmt). In our test a fuzzy entry with a wrong placeholder compiled cleanly, and the statistics counted it as fuzzy. A green build therefore says nothing about a fuzzy entry, and that is a reason to count them.
What to leave untouched when translating
Putting the pages together gives a short list of what belongs to the translator and what does not. According to the manual, other tools generate and maintain the source string and the automatic comments, and what the translator really owns is the msgstr plus the translator comments (GNU gettext manual, entries). In practice:
msgid,msgid_pluralandmsgctxtare the keys. A changed character turns the entry into a new, untranslated one at the next merge.- Comments and references, including the
#,flag line and the#|previous-string lines, are information for tools and people. A note to the translator is a wish about style, not text to translate. - Placeholders such as
%s,%d,%1$sor{name}are copied exactly. The words around them move; the placeholders themselves do not change. - Plural slots follow the header. A language with another count of forms means a new header, and then every plural entry carries exactly that many lines.
- The header entry stays as it is, apart from the plural rule and the language fields that genuinely describe the new language.
- Fuzzy and format flags are not edited by the translator.
Then run the check on the result and read the output, not just the exit status, because warnings about header fields go through with status zero.
A check that cannot fail proves nothing, so try it on a file you broke on purpose. Delete one msgstr[n] line, change one placeholder letter, double a key; each should turn the build red. If the build stays green after a deliberate break, the entry was probably not flagged, which is the case worth fixing first. For the same reasoning applied to plural rules in another message format, see how ICU messages and CLDR categories treat the count of forms.
When this does not apply
The subject here is GNU msgfmt and the PO format as the GNU gettext manual describes it. Other catalog formats, other implementations of the format, and checks in editors or web platforms may behave differently. The test was a handful of edits to one small file with one release of the tool on one platform, so it shows that those rejections exist, not how often each one occurs in real catalogs. Most manual pages came from an archive copy, since the live site did not answer on the day of reading, and the text may have changed since. Duplicate keys, unknown escapes and the behaviour of the empty-translation and fuzzy cases come from our test, not from a manual page, and other versions may differ. Newline mismatches between source and translation, and the handling of obsolete entries, were not examined here.
Tools for the two jobs
Two of our skills work on exactly this file type, and both return the whole catalog and nothing else. The po-translate skill fills in the translations of a catalog for a target language. It keeps keys, comments, flags, placeholders and obsolete entries as they were, writes a plural line for every form the target language needs, and sets the plural rule in the header for that language. The po-file-repair skill takes a catalog that will not build and changes only what blocks the build: quoting and escapes, placeholder types, plural line counts against the header, charset and duplicate entries. Where a fix would need new words or a choice between two translations, it marks the entry or refuses instead of inventing text.
Read this post as Markdown: /blog/gettext-po-msgfmt-check-errors.md · Atom feed.
