The PO file format: msgid, msgstr, and everything gettext adds around them
GNU gettext predates almost every other localization tool still in use, and its file format — PO, short for Portable Object — is still the default choice across open-source software. It is a plain-text format built around one idea: a source string (msgid) paired with its translation (msgstr). Everything else in the format exists to make that one pairing survive real-world translation work.
This article covers the format at the level of the specification: what each entry looks like, how plurals and disambiguation work, and why teams that could pick anything still pick this.
The basic entry
A PO file is a sequence of entries, each with an optional set of comment lines followed by a msgid/msgstr pair. Whitespace and line breaks inside strings are preserved literally, including in the source, so extracted text has to match the runtime string exactly.
#: src/dialogue/tutorial.c:42 #, c-format msgid "Press %s to open your inventory." msgstr "%s を押してインベントリを開く。"
Plural forms
English has two plural forms; many languages have more, and some have fewer meaningful distinctions than English implies. gettext handles this with a header that defines the language's plural rule as a small C-like expression, and a msgid_plural / msgstr[n] block per entry that needs pluralization.
The Plural-Forms header declares how many forms exist and which integer expression selects each one at runtime. A translator only fills in msgstr[0], msgstr[1], and so on — the count and the selection logic are already decided by the header, not guessed per string.
"Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "%d item found" msgid_plural "%d items found" msgstr[0] "%d 件のアイテムが見つかりました" msgstr[1] "%d 件のアイテムが見つかりました"
msgctxt: disambiguating identical source text
The same English word often means different things depending on where it appears — "Close" as a verb on a button versus "Close" as an adjective describing proximity. Because gettext keys translations by the source string itself, two identical msgid values would collide without a way to tell them apart.
msgctxt (message context) solves this: it is an extra field before msgid that scopes the entry, so "Close"|button and "Close"|distance become two independent, independently translatable entries even though the English text is identical.
msgctxt "button" msgid "Close" msgstr "閉じる" msgctxt "distance" msgid "Close" msgstr "近い"
Comments, fuzzy entries, and flags
Every line above a msgid that starts with a specific comment marker carries a distinct kind of information, and tools rely on the marker to route it correctly:
- # — translator comments, written by hand and preserved across re-extraction
- #. — extracted comments, written by the developer in source code for the translator's benefit
- #: — reference comments, listing the file and line the string was extracted from
- #, — flags, such as c-format (marks a printf-style string so tools validate placeholders) or fuzzy
The POT template and per-language PO files
A project maintains one POT file (Portable Object Template) — the extracted set of source strings with empty msgstr values — and one PO file per target language, each derived from the template. When source text changes, the template is regenerated and merged into every language's PO file: unchanged entries are left alone, changed entries are marked fuzzy so a human reviews them, and removed entries are kept as obsolete (commented out) rather than deleted outright, in case they come back.
This merge workflow is the reason gettext has stayed the default in open-source projects: it is entirely file-based, diffs cleanly in version control, and the fuzzy/obsolete mechanism gives a translator a clear, bounded list of work after every source change instead of a full re-translation.