FeaturesPluginPricingResources
Change Language
Resources.po vs .mo vs .pot: WordPress Translation Files Explained

.po vs .mo vs .pot: WordPress Translation Files Explained

SimplePoTranslate TeamApril 21, 2026
.po vs .mo vs .pot: WordPress Translation Files Explained

You open the languages folder of a WordPress theme and find three files that look almost identical: twentytwentyfour.pot, twentytwentyfour-de_DE.po, and twentytwentyfour-de_DE.mo. Same theme, same language, three different extensions. Which one do you edit? Which one does WordPress actually load? And why does editing the wrong one make your translations silently disappear?

If you have ever asked which file matters in the po vs mo file debate, you are not alone. These three extensions are the backbone of every translated WordPress site, yet the difference between them trips up beginners and seasoned developers alike. Edit the wrong file and your German visitors still see English. Edit the right one but skip a step and nothing changes either.

This guide explains exactly what .pot, .po, and .mo files are, how they relate inside the GNU Gettext workflow, where they live in WordPress, and why you should never open a .mo file in a text editor. By the end you will know which file to touch and which to leave alone.

What Are .pot, .po, and .mo Files?

In short: a .pot file is the blank template, a .po file is your editable translation, and a .mo file is the compiled binary WordPress reads at runtime. They form a chain, and each link has exactly one job.

These three formats all come from GNU Gettext, the localization system WordPress, Drupal, and thousands of PHP and C projects rely on. Gettext separates the source strings a developer writes from the translations a translator provides, so the same codebase can speak dozens of languages without touching a single line of PHP.

Think of it like a recipe card. The .pot is the blank card with ingredient slots but no quantities. The .po is the card filled in for one specific dish. The .mo is the laminated, machine-scannable copy the kitchen actually uses during service. You write on the paper card; you never scribble on the laminated one.

The .pot File: The Master Template

A .pot file (Portable Object Template) contains every translatable string in a theme or plugin with the translations left empty. It is the master list developers ship so translators know exactly what needs translating. The msgstr lines are blank because no language has been chosen yet.

#: includes/checkout.php:42
msgid "Add to Cart"
msgstr ""

#: includes/account.php:108
msgid "Your order has been shipped to %s"
msgstr ""

Notice the empty msgstr "". A .pot is a template, not a translation. You copy it once per language and fill in the blanks. You rarely edit a .pot by hand; it is regenerated from source code whenever strings change.

The .po File: Your Human-Editable Translation

A .po file (Portable Object) is a copy of the .pot with the msgstr lines filled in for one language. This is the file translators and developers actually edit. It is plain text, human-readable, and version-control friendly.

#: includes/checkout.php:42
msgid "Add to Cart"
msgstr "In den Warenkorb"

#: includes/account.php:108
msgid "Your order has been shipped to %s"
msgstr "Ihre Bestellung wurde an %s versandt"

The msgid is the original English source string and must never change. The msgstr is your translation. The %s is a placeholder that must survive untouched into the translation — dropping or reordering it is the single most common cause of broken layouts, which we cover in depth in how to translate PO files without breaking code variables.

The .mo File: The Compiled Binary

A .mo file (Machine Object) is the compiled, binary version of your .po. WordPress cannot read .po files efficiently at runtime, so it loads the pre-compiled .mo instead. Opening a .mo in a text editor shows unreadable bytes — it is meant for machines, not people.

When WordPress calls load_textdomain() or load_theme_textdomain(), it looks for a .mo file, parses its binary hash table, and swaps every msgid for the matching msgstr on the fly. This binary format makes lookups fast even on sites with thousands of strings.

How Do the Three Files Relate in the Gettext Workflow?

They form a one-directional pipeline: source code becomes a .pot, the .pot becomes a per-language .po, and each .po compiles into a .mo. Translations always flow downhill.

Here is the full lifecycle in order:

  1. A developer wraps user-facing strings in Gettext functions like __() and _e() inside the PHP source.
  2. A scanning tool reads the source and generates the .pot template containing every wrapped string.
  3. A translator copies the .pot into a language-specific .po (for example de_DE.po) and fills in each msgstr.
  4. The finished .po is compiled into a binary .mo.
  5. WordPress loads the .mo at runtime and displays the translated site.

When the developer adds new strings later, the .pot is regenerated, the new entries are merged into each existing .po (keeping old translations), the translator fills the gaps, and the .po is recompiled to .mo. The cycle repeats for the life of the project.

The critical takeaway: you edit the .po, then compile to .mo. You never edit the .mo directly, and you never translate inside the .pot. If you want the deeper end-to-end picture, the ultimate guide to WordPress localization walks through the entire pipeline with examples.

Naming Conventions and Where the Files Live

WordPress is strict about file names. Get the naming wrong and your perfectly translated .mo will simply be ignored, because WordPress looks for an exact match based on the text domain and locale.

The naming pattern for theme and plugin translations is:

# Pattern: textdomain-locale.po / .mo
twentytwentyfour-de_DE.po      # German (Germany) translation
twentytwentyfour-de_DE.mo      # compiled binary WordPress loads
twentytwentyfour-fr_FR.po      # French (France)
twentytwentyfour.pot           # template, no locale suffix

The textdomain matches the string passed as the second argument to Gettext functions like __( 'Add to Cart', 'twentytwentyfour' ). The locale is a WordPress locale code such as de_DE, fr_FR, or pt_BR. The .pot template carries no locale suffix because it is language-neutral.

Location matters just as much as naming. WordPress searches a few predictable paths:

  • Theme translations ship in the theme's own wp-content/themes/your-theme/languages/ folder.
  • Plugin translations ship in wp-content/plugins/your-plugin/languages/.
  • Translations from translate.wordpress.org and user overrides land in the global wp-content/languages/themes/ and wp-content/languages/plugins/ directories, which take priority and survive updates.

Put a correctly named de_DE.mo in the right folder and German visitors see German. Put it one folder too deep, or misspell the text domain, and WordPress quietly falls back to English with no error message. If your translations are not appearing, a naming or path mismatch is the usual culprit, and the troubleshooting guide for missing translations covers every common cause.

Why You Should Never Edit a .mo File Directly

Because a .mo is a compiled binary, there is no safe way to edit it by hand — and any change you force in will be overwritten the next time the .po is recompiled. The .po is the source of truth; the .mo is a disposable build artifact.

This single rule explains a huge share of "my translation changed but the site didn't update" support tickets. Someone tweaks a string, saves the .po, and forgets to recompile the .mo. WordPress keeps loading the old binary, so the new wording never appears. The fix is always: edit .po, recompile to .mo, reload.

There is a second reason the rule matters: .mo files are not diff-friendly in version control. Because they are binary, a tiny wording change produces an opaque blob in your Git history that no reviewer can read. Keeping the .po as the tracked source of truth and treating the .mo as a generated artifact keeps your repository reviewable and your translations auditable.

Doing this manually across dozens of languages is tedious and error-prone. This is where a cloud workflow saves real time. Tools like SimplePoTranslate translate your .po strings and hand back a single ZIP containing the matching .po and .mo together — already compiled, correctly named, ready to drop into wp-content/languages/. There is nothing to compile manually and no chance of a stale .mo.

It also applies Syntax Locking, which freezes every placeholder like %s, %1$s, and {name}, plus HTML tags, before translation. Your variables survive intact, so you never ship a .mo that breaks a layout. And because it runs entirely in the cloud, there is no extra plugin weighing down your site — you upload a file and download finished, compiled translations.

Putting It All Together

Once the po vs mo file distinction clicks, the whole WordPress translation system stops feeling mysterious. The .pot is the developer's template, the .po is the translator's editable working copy, and the .mo is the compiled binary WordPress actually serves to visitors. Translations flow in one direction — template to .po to .mo — and you only ever hand-edit the middle one.

Remember the three rules that prevent ninety percent of translation headaches: never translate inside a .pot, always recompile the .mo after editing a .po, and match the textdomain-locale naming exactly. Follow those and your translated strings will appear every time.

Ready to stop wrestling with compiling and naming translation files by hand? Try SimplePoTranslate free — no credit card required. Upload your .po or .pot and get a ready-to-use .po + .mo bundle back on the free tier in minutes.