Gettext PO files

This guide summarizes the essentials of working with gettext catalogs and shows how to edit PO files comfortably with BabelEdit. It focuses on general gettext and BabelEdit usage — no framework- or language-specific details, so you can follow along no matter your tech stack.

See Working with PO files below for how to create PO files from a POT template and how to compile a PO file into an MO file.

Creating a new project

Start BabelEdit and select a gettext PO files project:

Start a new gettext PO file project
Selecting a po-file / gettext project

Adding files

Next drop your language files onto the dialog:

Add po files to the project
Adding po files to the project

Setting the source language

The source language is used for machine translation. It's the language that is supposed to be filled with the text you want to translate into other languages.

Setting the source language for PO files
Setting the source language for PO files

With PO files, you have 2 choices depending on how you use translation IDs. For more details about the differences, see Unique IDs vs string literals below.

Option 1: Using unique IDs

Unique IDs are IDs like main.greeting. They contain an identifier for the translation. When using unique IDs, you need a separate file for each language.

In this case, select one of the languages in the upper part of the dialog.

Option 2: Using string literals

String literals are strings like "Hello World!". In this case, the translation is the source text.

In this case, select the language in the lower part of the dialog.

PO‑specific features

Fuzzy translations

gettext marks strings as "fuzzy" if there are slight changes in a translation ID. For example, changing "Hello World" to "Hello World!" usually marks the entry as “fuzzy”. Bigger changes like "Hello John Doe" will likely create a new ID and remove the old one during merging.

BabelEdit displays fuzzy translations with a warning icon:

Warning about a changed ID
PO file: Fuzzy flag

Review the translation, then click the triangle icon to clear the flag. You can toggle it while it’s visible. After you switch to another entry, a cleared flag won’t be shown again.

Pluralization

When your messages use ngettext() (or similar), BabelEdit provides separate fields for singular and plural forms:

Pluralization in PO files
Pluralization in PO files

Comments

BabelEdit supports comments in PO files. Use xgettext with --add-comments to extract comments from your source code. They are displayed above the text fields.

Adding new translations

You can’t create new message IDs directly in BabelEdit because gettext’s command‑line tools would remove them during merging. To add new translations, add them in your source code and extract them again.

Unique IDs vs string literals

You have two approaches:

  • Use a unique ID for each translatable string. E.g. main.greeting.
  • Use a string literal as the ID. E.g. "Hello World!"

The first option is more stable. Changing the text does not lose the link to existing translations. In addition, a descriptive ID gives translators helpful context (for example: main.greeting clearly tells where it’s used).

gettext can detect small changes and will mark affected entries as “fuzzy,” which is a friendly nudge to double‑check and confirm them. If the text changes significantly, the previous translation may no longer apply and a new entry will typically be created.

For example, changing "Hello World" to "Hello World!" usually just sets the entry to “fuzzy.” But changing it to "Hello John Doe" will likely create a new ID and remove the old one during merging.

Using string literals can be quicker when converting an existing project because you only need to wrap strings with a marker (often _()). Creating unique IDs takes a bit more setup time.

AspectUnique IDsString Literals
Examplemain.welcome-screen.greetingHello World!
ContextProvides explicit context: e.g. greeting on the welcome screen. Makes it easy to group related translations, such as main.welcome-screen.description.No inherent context — the same string could appear anywhere in the app.
UpdatingChanging the source text in the primary language does not affect other translations.Changing the source text may break or invalidate existing translations.
Integrating into an existing appRequires defining IDs for all strings and exporting them into a file.Very simple: just wrap existing strings with a marker function (e.g. _()).
FilesNeeds a .po/.mo file for each language.Needs a .po/.mo file only for the translations.

While both options are valid, we recommend using unique IDs since maintenance is easier.

Working with PO files

File types and workflow

Gettext uses three file types that work together in a simple cycle you repeat whenever source texts change:

  1. POT — Portable Object Template
  • Created by extracting translatable strings from your source code with tooling such as xgettext.
  • Do not hand‑edit; regenerate when source texts change.
  1. PO — Portable Object (one per language)
  • Human‑readable translation files created from the POT.
  • Updated by merging new/changed messages from the POT with msgmerge.
  1. MO — Machine Object (one per language)
  • Compiled, fast binary catalogs generated from PO files with msgfmt.

Typical loop:

  • Extract → POT (with xgettext)
  • Initialize or update → PO (with msginit for first creation, msgmerge for updates)
  • Edit the PO files
  • Compile → MO (with msgfmt)

Creating and updating translation files (generic)

  • Extract messages from your source code to a POT file (example):

    • xgettext --from-code=UTF-8 --add-comments -o myapp.pot <your source files>
    • With --add-comments, comment blocks above marked strings are copied into the POT to give translators context.
    • Many tools initially set the charset in the header to CHARSET. Replace it with UTF-8 in the header line Content-Type: text/plain; charset=CHARSET.
  • Create initial PO files for each language (run once per language):

    • msginit --locale de.UTF-8 --input myapp.pot --output locale/de/LC_MESSAGES/myapp.po
  • Update existing PO files when source texts change:

    • Regenerate the POT: xgettext ... -o myapp.pot
    • Merge into PO files: msgmerge --update locale/de/LC_MESSAGES/myapp.po myapp.pot
    • After merging, review entries marked as “fuzzy” — they indicate approximate matches that need confirmation.
  • Compile PO files into MO catalogs used at runtime:

    • msgfmt locale/de/LC_MESSAGES/myapp.po --output-file=locale/de/LC_MESSAGES/myapp.mo

Standard folder structure

Gettext looks for compiled catalogs in a well‑known layout. For a text domain named myapp and the languages de and fr, the following folder structure is expected:

  • locale
    • de
      • LC_MESSAGES
        • myapp.po
        • myapp.mo
    • fr
      • LC_MESSAGES
        • myapp.po
        • myapp.mo

Notes:

  • LC_MESSAGES is the mandatory directory name used by gettext.
  • Languages can be simple (de) or include region/encoding (de_DE, de_DE.UTF-8).
  • Use UTF‑8 for source and translation files where possible.
  • For production, it's sufficient to deploy the MO files only.
  • It makes sense to keep the .po files in the same structure so that they are easy to find and update.

Tutorials

If you work with PHP, try our step‑by‑step tutorial: Translation with gettext + PHP.