Localizely logo
  • Home
  • Getting started
  • How it works
  • Pricing
  • FAQ
Sign up

I18n questions · Localization files

How do I create or update a PO file? 

You do not write PO files from scratch. xgettext extracts the translatable strings from the sources into a .pot template, msginit turns the template into a .po file for one language, and msgmerge --update carries existing translations over to a fresh template after the code changed. msgfmt compiles the result to the binary .mo.

# 1. Extract a template from the sources
xgettext --from-code=UTF-8 -o locale/messages.pot src/*.py

# 2. Create a language file from the template
msginit --input=locale/messages.pot --locale=de --output-file=locale/de/LC_MESSAGES/messages.po

# 3. After code changes: regenerate the template, then merge it into each language
msgmerge --update --backup=none locale/de/LC_MESSAGES/messages.po locale/messages.pot

# 4. Compile for runtime
msgfmt --check -o locale/de/LC_MESSAGES/messages.mo locale/de/LC_MESSAGES/messages.po

xgettext has a default keyword list per language (gettext and _ for Python, but no _ for C); add --keyword=_ or --keyword=tr for a wrapper it does not know. msginit fills the header (language, charset, Plural-Forms) from the locale. msgmerge keeps every translation whose msgid still exists, pairs changed strings with the closest old translation and marks them #, fuzzy (--previous keeps the old text in #| lines), adds new strings with an empty msgstr, and comments out strings that disappeared as obsolete #~ entries. Fuzzy entries are skipped by msgfmt, so a translator has to review them and remove the flag.

Frameworks wrap the same tools. Django's django-admin makemessages -l de creates or updates locale/de/LC_MESSAGES/django.po and compilemessages builds the .mo; both need GNU gettext installed. Symfony's php bin/console translation:extract --force de updates its catalogs. WordPress projects use wp i18n make-pot . languages/my-plugin.pot.

Adding a string by hand is one block, and the msgid must match the string in the code exactly, or the lookup fails at runtime and the next msgmerge run marks the entry obsolete:

#: src/checkout.py:42
msgid "Your order has shipped."
msgstr "Ihre Bestellung wurde versandt."

An empty msgstr "" means untranslated. # plus a space starts a translator comment; #., #: and #, lines are written by the tools. The full entry syntax is in the gettext manual.

In Poedit the same steps are menu commands: File > New from POT/PO file creates a language file, Translation > Update from Source Code reruns the extraction, Translation > Update from POT File merges a template you received, and Translation > Compile to MO writes the binary.

Pitfalls:

  • Never edit a .mo file. It is generated; change the .po and compile again.
  • Do not put translations into the .pot; it is the template, and msgmerge discards its msgstr values.
  • Run msgfmt --check before shipping; it catches mismatched printf placeholders.

The format itself is described at PO file.

Published Oct 9, 2026 · Markdown version

Related localization file questions:

Supported file formats:

See the localization file formats Localizely supports
Localizely logo
About
Free tools
Boring stuff
  • Privacy Policy
  • Terms of Service
  • Cookie Policy
  • Compare
Newsletter
Copyrights 2026 © Localizely