A gettext PO file is a plain text file that works like a translation dictionary. It connects original text strings (like "Welcome back") to their translated versions (like "Bienvenido de nuevo"). This lets you support multiple languages in your Django app without hardcoding translations into your source code.
The Role of a PO File in Django i18n
If you've built a multilingual Django app, you've used the GNU gettext system. The .po file is the heart of that workflow.
Think of it as a contract between you and your translators. You define the English source text, and they fill in the blanks for every other language. This separation is critical. It allows you to refactor code without destroying translations, and it lets translators work without touching a Python file.
The system uses three key file types:
- POT (Portable Object Template): This is the master blueprint. When you run
makemessages, Django scans your code for translatable strings and dumps them into a.potfile. It contains the original text (msgid) but no translations. You usually don't commit this file to version control. - PO (Portable Object): This is the file you and your translators actually use. It's a copy of the template for a specific language, like
de/LC_MESSAGES/django.pofor German. Translators open this file and add translated text into themsgstrfields. You always commit.pofiles to Git, as they are the source of truth for your translations. - MO (Machine Object): This is the compiled, binary version of a
.pofile. When you runcompilemessages, Django converts the human-readable.pofile into a machine-optimized.mofile. Your server uses this binary for fast translation lookups. You should always add*.moto your.gitignore.
An Open-Source Standard Since 1995
This system isn't a new Django invention. It’s been a cornerstone of software localization since the GNU gettext toolset was first introduced in 1995.
Originally developed for the GNU Translation Project, .po files became the standard for open-source software. By 2000, they were used in over 80% of Linux distributions. You can read more about the history of gettext PO/POT files. This long, battle-tested history is why gettext is so deeply integrated into frameworks like Django.
The workflow is straightforward. You mark strings, makemessages extracts them into .po files, a person or service translates them, and compilemessages prepares them for your app. The foundation is solid, but the manual steps can become a big time sink.
When you first open a .po file, it can look intimidating. But once you see the pattern, it's quite simple. It’s a plain text file that acts like a script: metadata at the top, followed by one translation unit after another.
Let's break down a typical entry from a Django project's django.po file.
#. Translators: This is a welcome message on the user's dashboard.
#: templates/dashboard.html:5
#, python-format
msgid "Welcome back, %(username)s!"
msgstr "¡Bienvenido de nuevo, %(username)s!"
Each block is a self-contained translation unit. The comments (#. and #:) and flags (, python-format) give translators and tools context, like where the string came from and what placeholders it uses. The msgid is the original string from your code, and the msgstr is where the translation goes.
The Header: Metadata for Your Translations
Every gettext po file starts with a special entry where msgid is an empty string. This isn't a mistake; it's the file's header, containing crucial metadata.
msgid ""
msgstr ""
"Project-Id-Version: My Awesome Django App 1.0\n"
"PO-Revision-Date: 2024-05-10 14:00+0000\n"
"Language-Team: Spanish <team@example.com>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
Pay close attention to the Plural-Forms directive. It tells gettext how to handle pluralization rules for this language. The rule nplurals=2; plural=(n != 1); works for languages like Spanish and English, which have two forms: singular (when n is 1) and plural (for everything else). A language like Polish has a much more complex rule. Getting this wrong will break your plural translations.
The .po file acts as the central hub, connecting your source code to the final translated experience.

It’s the bridge that turns developer code into a polished, multilingual product. Here are the most common elements you'll see inside a PO file.
Key PO File Directives and Their Purpose
| Directive / Element | Description | Example |
|---|---|---|
#. comment |
A comment for translators, usually written by developers. | #. Translators: A short greeting. |
#: path:line |
A reference to the source file and line number where the string was found. | #: views.py:123 |
, flag |
A flag indicating special properties, like fuzzy or python-format. |
, python-format |
msgctxt |
A "message context" to disambiguate identical strings. | msgctxt "button label" |
msgid |
The original, untranslated string from your source code. | msgid "Save Changes" |
msgstr |
The translated string. Empty msgstr means untranslated. |
msgstr "Guardar Cambios" |
msgid_plural |
The plural form of the source string. | msgid_plural "%(count)s items" |
msgstr[n] |
The translated plural forms, where n is an index (0, 1, 2...). |
msgstr[0] "%(count)s elemento" |
Knowing these directives makes reading and debugging .po files much easier.
Core Translation Units: msgid and msgstr
The real work happens in the translation pairs.
msgid(Message ID): This is the source string, copied exactly from your code. It acts as the unique key for that piece of text.msgstr(Message String): This is where the translation goes. If it's empty, Django falls back to the originalmsgidtext.
These key-value pairs are the foundation of the gettext system. A 2026 Sloccount study estimated that .po files facilitate $15 billion annually in localization value for open-source projects. They make extracting text from code a zero-cost operation. You can learn more from the official gettext system overview.
Handling Context and Placeholders
Real-world apps aren't just static text. Your .po file has features for handling tricky parts.
When a source string changes,
makemessagesdoesn't know if the old translation is still valid. It makes a guess and marks the entry with a# fuzzyflag. This is a signal for a human to review the translation.
You can also add contextual markers to avoid ambiguity.
msgctxt(Message Context): Imagine you have the word "Open" as both a button label and an order status. You can add context (e.g.,msgctxt "button") to create two separate translation entries.- Placeholders: Dynamic values like
%(name)sor{count}are preserved in themsgid. Translators must include the exact same placeholder in themsgstrto prevent runtime errors.
For more on managing these files, check out our guide on working with PO files in your projects.
The Standard makemessages and compilemessages Workflow
If you've dealt with internationalization in a Django project, you've used two management commands: makemessages and compilemessages. This two-step process is the foundation of Django's i18n system.
It's a predictable process, but it's also manual. You have to run these commands every time you add or change text. Forget a step, and you'll ship untranslated strings to production.

Step 1: Generating PO Files with makemessages
The makemessages command scans your project for any text marked for translation.
It looks for strings wrapped in functions like gettext() (or _()) and template tags like {% trans %}. It gathers every unique string and uses them to create new .po files or update existing ones.
To generate files for Spanish (es), you’d run this command:
python manage.py makemessages -l es
This command does a few things:
- Creates a
locale/es/LC_MESSAGES/directory if it doesn't exist. - Generates a
django.pofile inside that directory. - If
django.poalready exists, it adds newmsgidentries and comments out old ones no longer in use.
The result is a .po file with your new source strings, each with an empty msgstr waiting for translation. This is where the manual work usually begins.
Step 2: Compiling PO Files with compilemessages
Once your .po files have translations, they’re still just text files. Django can't use them directly at runtime because parsing them on every request would be too slow. This is where compilemessages comes in.
This command converts your .po files into optimized, binary .mo (Machine Object) files. The .mo format is compact and indexed, which the gettext system can read very quickly.
To compile all .po files, run:
python manage.py compilemessages
This command finds every django.po file in your locale directories and creates a corresponding django.mo file beside it.
Your web server only reads the
.mofile. A classic mistake is translating a.pofile, deploying it, and wondering why translations don't appear. If you forgetcompilemessages, all your work stays invisible.
This compile step is why you should add *.mo to your .gitignore. They are compiled artifacts, not source files. They should be generated fresh during your deployment process.
Common PO File Problems and How to Fix Them
A .po file is a plain text file that must be perfectly structured. A single misplaced character can break your deployment pipeline or ruin the user experience. Let's cover the most common problems and how to solve them.
Broken Placeholders and Runtime Errors
This is a classic problem. Your template uses a placeholder for a username, but after translation, the page crashes with a ValueError or KeyError.
# In your Python code or template
_("Welcome, %(username)s!")
# A broken translation in your PO file
msgid "Welcome, %(username)s!"
msgstr "Bienvenido!" # ❌ Oops, the placeholder is missing.
When Django tries to render this string, it has a value for username but no %(username)s placeholder in the translated msgstr. This causes a runtime error.
The fix involves two steps:
- Always use named placeholders (
%(name)s) over positional ones (%s). They don't depend on word order, so translators can rearrange sentences freely. - Use a linter or a modern translation tool. Good tools automatically flag translations where placeholders in
msgstrdon't matchmsgid.
Character Encoding Mismatches
You translate your django.po file, deploy, and discover your Spanish translations are now gibberish like Bienvenido de nuevo, Sebastián.
This is a sign of an encoding mismatch. The gettext system relies on the Content-Type header in your .po file, which should always be UTF-8.
"Content-Type: text/plain; charset=UTF-8\n"
If your text editor saves the file in a different encoding, or if this header is wrong, non-ASCII characters get mangled. Configure your editor to save files as UTF-8 without BOM and check that the header in every .po file specifies charset=UTF-8.
Git Merge Conflicts
If two developers translate the same django.po file on different branches, you'll get a painful merge conflict. Git sees line-based changes, but a .po file is structured data. A makemessages run can reorder entries or change line numbers, creating a huge diff that’s nearly impossible to resolve by hand.
The problem is that Git treats a
gettext po filelike code, but it's really data. Two developers adding different translations creates a branching nightmare.
Here are a few ways to escape this mess:
- Serialize your workflow. Designate one person or an automated process to handle all translation updates.
- Use a translation management service. Platforms like Crowdin or Transifex manage merge logic on their end. You just pull down the final
.pofile. - Automate with a bot. Use a tool like TranslateBot to run translations serially in a CI/CD pipeline. The bot becomes the single editor, so human-generated conflicts don't happen.
The Pain of Fuzzy Entries
You fix a typo in a source string, run makemessages, and your translation is now marked "fuzzy".
# Original string
msgid "Creat an account"
# After fixing the typo in code
#: path/to/your/file.py:42
#, fuzzy
#| msgid "Creat an account"
msgid "Create an account"
msgstr "Crear una cuenta"
The makemessages command sees "Create an account" as a new string. It helpfully copies the old, similar translation and marks it with #, fuzzy.
A fuzzy entry is a warning: this translation is untrusted and will not be displayed. It’s a signal for a human to review it. These entries pile up fast, and if you forget to resolve them, parts of your app will silently revert to English.
The only solution is a dedicated review step. Before running compilemessages, search your .po files for #, fuzzy. Review each one, confirm the translation is still correct for the new msgid, and remove the fuzzy flag.
Automating PO File Translations with TranslateBot
Translating a .po file by hand is slow and repetitive. If you're a solo dev or a small team, every hour spent copy-pasting strings into Google Translate is an hour you’re not building features.
This is what sends developers to big SaaS platforms like Crowdin or Lokalise, which often feel like overkill for a growing Django project.
There’s a developer-first way. You can automate the process right in your terminal. That’s what TranslateBot was built for. It’s a command-line tool that hooks directly into your Django i18n workflow.

This diagram shows the entire workflow. A single command translates new strings for all your languages right inside your project’s locale directory. You stay in your terminal, focused on code.
A Workflow Built for Developers
TranslateBot doesn't require learning a new system. It snaps into the standard Django i18n process. After you run makemessages, you just run one more command.
translatebot
That’s it. This command triggers an automated workflow:
- Finds Your Files: TranslateBot scans your
localedirectory for everydjango.pofile. - Analyzes for Changes: It identifies only the new or changed entries, keeping API costs low.
- Translates with AI: It sends these new strings to a large language model for translation.
- Writes Back: The translated
msgstrvalues are written directly back into the correct.pofile.
The process happens locally. No web portals or complex config files. You get fully translated .po files, ready for compilemessages.
Smart Features That Prevent Breakage
A big fear with automated translation is that it will break the app. TranslateBot has safeguards to stop these problems.
Placeholder preservation is non-negotiable. The tool correctly handles all placeholder formats used in Django:
%(name)s(Named placeholders)%sor%d(Positional placeholders){count}(Format string syntax)- HTML tags like
<strong>or<a href="...">
It guarantees that if a msgid has a placeholder, the translated msgstr will have the exact same one. This prevents the ValueError and KeyError exceptions common in manual translation.
Context-aware translations with a glossary give you fine-grained control. You can create a simple TRANSLATING.md file in your project root to act as a glossary.
This Markdown file lets you give the AI specific instructions. For example:
# Glossary
- "Branch" -> always translate as "sucursal" in Spanish, not "rama".
- "Ship" -> when referring to order fulfillment, use "enviar".
TranslateBot feeds this glossary to the language model with every request. This ensures your app’s terminology is translated consistently. This simple file gives you control without leaving your editor.
Clean Diffs and a Git-Friendly Workflow
This approach fits cleanly into version control. Since TranslateBot just edits your .po files, the changes are just code. You get a clean, reviewable diff in Git.
Translation updates become a predictable part of your development cycle. You can see exactly which translations were added in a pull request and get them reviewed. This solves the merge conflict nightmare from multiple people editing .po files by hand.
For a deeper look at this process, you can read our guide on how to automate your Django .po file translation workflow.
Integrating PO File Automation into Your CI/CD Pipeline
Manual translation workflows are a recipe for mistakes. It's too easy to forget a command before deploying. This is where a tool like TranslateBot really helps, especially when wired into your CI/CD pipeline.
Instead of relying on developers to remember a sequence of commands, the pipeline takes over. Every time you push code, your CI server can run the full translation cycle automatically. This guarantees your gettext po file translations are always in sync with your source code.
A Practical GitHub Actions Workflow
Setting this up with GitHub Actions is straightforward. You need a workflow file that runs the Django i18n commands, with translatebot in the middle.
Here’s a sample workflow you can drop into .github/workflows/translate.yml:
name: Update PO File Translations
on:
push:
branches:
- main # Or 'master'
jobs:
translate:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install django translatebot-ai
- name: Run makemessages
run: |
python manage.py makemessages --all
- name: Run TranslateBot
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
translatebot
- name: Run compilemessages
run: |
python manage.py compilemessages -l '*'
- name: Create Pull Request with updated translations
uses: peter-evans/create-pull-request@v6
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore(i18n): update PO file translations"
title: "Automated PO File Translation Updates"
body: |
This PR was automatically generated by the translate workflow.
It contains updated translations for new or changed source strings.
branch: "chore/i18n-updates"
This simple file automates the entire process.
How It Works Step-by-Step
This workflow runs on every push to your main branch. First, it runs makemessages to find new strings. Then, translatebot fills in the missing msgstr entries. Finally, compilemessages builds the binary .mo files.
The magic is in the last step. Instead of committing directly to
main, the workflow opens a new pull request with the translation changes. This gives you a final chance to review everything the AI generated.
This reviewable diff makes the process trustworthy. It provides a human checkpoint, ensuring that automated changes get reviewed before they go live. You can spot awkward phrasing, approve the PR, and merge. The process is transparent, version-controlled, and requires almost zero manual work.
You can find more instructions in our guide on configuring CI for your Django projects.
Your Gettext PO File Questions, Answered
If you're new to Django's internationalization system, a few common questions pop up right away. Here are the straight answers.
What's the Difference Between PO, POT, and MO Files?
These three file types are steps in a simple workflow.
.POT (Portable Object Template): The master blueprint.
makemessagesscans your code and creates this template file. It contains every original string (msgid) but no translations..PO (Portable Object): Where the work happens. It’s a copy of the .POT file for a specific language (like
de/LC_MESSAGES/django.po). You or a tool fill in the emptymsgstrvalues here..MO (Machine Object): The finished product. It's a compiled, binary version of your .PO file. Django reads these fast, optimized .MO files to look up translations at runtime.
You create a .POT template, copy it to a .PO file for translation, and then compilemessages turns it into a .MO file for Django to use.
Should I Commit MO Files to My Git Repository?
No. You should add *.mo to your .gitignore file.
MO files are compiled artifacts, like .pyc files. Your source control should only track the source of truth, which is the .PO file. The .MO files should be generated on your server as part of your deployment process by running compilemessages.
How Do I Handle Plural Forms in a PO File?
Django handles this well. When you use ngettext in Python or {% blocktrans count %} in templates, makemessages creates special entries in your .PO file.
Instead of a single msgid, you'll see both a msgid (for the singular form) and a msgid_plural (for the plural form). You provide translations for each case in msgstr[0], msgstr[1], and so on.
The number of plural forms depends on the language. English has two (one, other), but Polish has three or four. The Plural-Forms rule in the .PO file header, which makemessages sets up for you, defines this.
Stop wasting time on manual copy-pasting. TranslateBot automates your Django localization workflow with a single command, right in your terminal. Get started for free and ship multilingual features faster. Check it out at https://translatebot.dev.