---
title: "How do I create or update a PO file?"
description: "Extract a `.pot` template with `xgettext` or your framework's command, create a language file from it with `msginit`, and after code changes refresh each `.po` with `msgmerge --update`. Compile to `.mo` with `msgfmt`. Edit `.po` files by hand or in Poedit, never the `.mo`."
category: localization-files
date: 2026-10-09
canonical: https://localizely.com/i18n-questions/localization-files/how-do-i-create-or-update-a-po-file/
---

# 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`.

```sh
# 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`](https://www.gnu.org/software/gettext/manual/html_node/msgmerge-Invocation.html) 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:

```po
#: 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](https://www.gnu.org/software/gettext/manual/html_node/PO-File-Entries.html).

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](/po-file/).

Source: https://localizely.com/i18n-questions/localization-files/how-do-i-create-or-update-a-po-file/
