Back to blog

A Developer's Glossary of Terms for Django I18n

2026-04-01 15 min read
A Developer's Glossary of Terms for Django I18n

If you’re working with Django's internationalization (i18n) system, you'll quickly run into a wall of jargon. Terms like gettext, .po files, msgid, and msgstr are the foundation of any multilingual Django app. Mixing them up causes real headaches.

This glossary is a decoder ring for the localization workflow. It’s a quick-reference guide for fast lookups when you’re deep in code and just need to remember the difference between i18n and l10n.

Your Quick-Reference Django I18n Glossary

A Django I18n Glossary table displaying internationalization terms, definitions, and their roles.

Django’s i18n system is powerful, but it has its own vocabulary. Getting these terms right isn't just academic. It directly affects your ability to build and maintain a multilingual app without pulling your hair out.

Knowing the terminology helps prevent common mistakes. You won't confuse a source string (msgid) with its translation (msgstr) or misunderstand how pluralization rules work.

Core I18n Concepts

This table is your cheat sheet. Use it as a starting point or a quick reminder when you need a fast definition for the most critical terms in the Django i18n world.

Term Brief Definition Role in Django I18n
I18n Internationalization. Preparing your code so it can support multiple languages. This is the architecture phase. You wrap user-facing strings in gettext functions so they can be discovered and translated later.
L10n Localization. The actual process of translating your app for a specific language and region. This is the implementation phase. You (or a tool) create language-specific .po files and provide the actual translated text.
Gettext The software package Django uses for all its translation services. It’s the engine under the hood. It provides the tools to find translatable strings and serves them up from .po and .mo files.
.po file A "Portable Object" file. This is a human-readable text file holding source strings and their translations. This is the file you or your translators will actually edit. All the translation work happens right here.
.mo file A "Machine Object" file. It's the compiled, binary version of a .po file. Django uses this optimized file at runtime to load translations quickly. You never edit this file directly; it’s generated for you.
msgid The "message ID." This is the original source string from your code (e.g., in English). It’s the text you wrapped in a function like _("Hello World"). It acts as the unique key for a translation.
msgstr The "message string." This is the translated version of a msgid. This is where the translation goes. It starts out empty and gets filled in for each language.

Understanding these seven terms will get you through 90% of the conversations and documentation you'll find. They are the foundation of everything else you'll do with Django's i18n tools.

The Foundation: Gettext and PO Files

Diagram illustrating the software localization process from source code to .po and .mo files.

When you localize a Django project, you're using the GNU gettext ecosystem. Django doesn't reinvent the wheel here. It builds on a toolset that has been the standard for open-source software translation for decades. This is why you'll see files and commands that aren't strictly Django-specific.

The entire workflow starts with one command: makemessages. Think of it as the starting pistol for any translation effort.

When you run python manage.py makemessages -l fr, Django spiders through your project. It hunts for any text you've wrapped in translation functions, like _() in your Python code or {% trans %} in your templates.

It gathers all these strings and organizes them into a .po file, which stands for Portable Object. This is a plain text, human-readable file where the translation work happens. A new file gets created for each language you target, like locale/fr/LC_MESSAGES/django.po for French.

The Anatomy of a PO File Entry

A .po file is a list of entries. Each entry pairs an original string from your code with its translation. The two most important fields are msgid and msgstr.

Let's look at a simple example. Imagine this snippet in your views.py:

from django.utils.translation import gettext as _

def my_view(request):
    message = _("Welcome to our website!")
    # ...

After running makemessages, your django.po file will contain this entry:

#. views.py:4
msgid "Welcome to our website!"
msgstr ""

Your task is to add the French translation:

#. views.py:4
msgid "Welcome to our website!"
msgstr "Bienvenue sur notre site web !"

Once you save this, you've created a translation. For a much deeper look into the structure and best practices for managing these files, check out our complete guide on gettext and .po files.

From PO to MO: The Final Step

After you finish translation work in the human-readable .po files, you run one last command: compilemessages.

This command takes your .po files and compiles them into optimized, binary .mo (Machine Object) files. You never edit these files directly. Django's gettext machinery uses these compiled .mo files at runtime because reading a binary file is much faster than parsing a text file on every request.

This final compilation step is what makes your translations go "live" in the application.

Beyond Basic PO Files

Once you’ve got the hang of msgid and msgstr, you’re ready for the tricky parts. Simple key-value pairs are fine for static text, but real apps are full of gotchas. You'll find words that mean different things in different places, plural forms, and dynamic placeholders. Getting these wrong will crash your app or confuse your users.

Using Context to Disambiguate Strings

What happens when the same string needs different translations? Think about the word "Active". In one part of your app, it could be an order status. In another, it's a user's online status. A single translation won't work for both.

This is what msgctxt (message context) was designed for. It’s a gettext feature that lets you attach context to a string. This allows translators to provide distinct translations for identical msgid strings.

In Django, you use the pgettext function.

# In your models.py
from django.utils.translation import pgettext

class Order(models.Model):
    # ...
    status = models.CharField(
        max_length=20,
        help_text=pgettext("order status", "Active")
    )

# In another app's views.py
from django.utils.translation import pgettext

def user_profile(request):
    # ...
    user_status = pgettext("user status", "Active")
    # ...

When you run makemessages, Django generates two separate entries in your .po file, each with its own context.

#. models.py:6
msgctxt "order status"
msgid "Active"
msgstr ""

#. views.py:5
msgctxt "user status"
msgid "Active"
msgstr ""

Now, your translators have the information they need. For German, "order status" could be "Aktiv," while "user status" might be "Online." Without msgctxt, you'd be stuck with one or the other, forcing a compromise that hurts clarity. This is a non-negotiable tool for any serious glossary of terms for software localization.

Handling Plurals the Right Way

Pluralization is a classic point of failure. Nobody wants to see "You have 1 new messages." It's just bad grammar. Worse, trying to fix it with an if/else in your code is a trap. Different languages have wildly different plural rules. Polish, for example, has unique forms for 1, 2-4, and 5+ items.

Django’s solution is ngettext. It takes the singular string, the plural string, and a count.

# In a view
from django.utils.translation import ngettext

def inbox(request):
    unread_count = get_unread_count(request.user)
    message = ngettext(
        "You have %(count)d unread message.",
        "You have %(count)d unread messages.",
        unread_count
    ) % {'count': unread_count}
    # ...

This creates a special plural-aware entry in the .po file.

msgid "You have %(count)d unread message."
msgid_plural "You have %(count)d unread messages."
msgstr[0] ""
msgstr[1] ""

msgstr[0] is for the singular form, and msgstr[1] is for the plural. For languages with more rules, you’ll see msgstr[2], msgstr[3], and so on. Let the translator handle this. They know their language's grammar. Don't try to outsmart gettext with your own logic in Python; it will only lead to pain.

Don't Break Placeholders and HTML

Many translatable strings contain dynamic placeholders or HTML tags. Messing these up is one of the fastest ways to break your app.

CRITICAL: Placeholders like %(name)s or {user} and HTML tags like <a> or <strong> must be preserved exactly in the msgstr translation. If a translator changes or removes them, you're going to get KeyError exceptions or a broken UI.

Take this common example from a Django template:

{% blocktrans with user_name=user.get_full_name %}
  Welcome back, <strong>{{ user_name }}</strong>!
{% endblocktrans %}

The .po file entry will correctly include both the placeholder and the HTML.

msgid "Welcome back, <strong>%(user_name)s</strong>!"
msgstr ""

When this gets translated, the tags and placeholders have to come along for the ride. A proper Spanish translation looks like this:

msgid "Welcome back, <strong>%(user_name)s</strong>!"
msgstr "¡Bienvenido de nuevo, <strong>%(user_name)s</strong>!"

If the translator accidentally deletes the <strong> tags or modifies %(user_name)s, your template will either render unstyled text or crash. Automated tools like TranslateBot have a huge advantage here. They are specifically programmed to protect these elements, avoiding a common cause of broken translations.

Controlling Quality with a Project Glossary

Manual translation is slow and inconsistent. One developer translates a term, a colleague chooses a different word three months later, and users get a confusing experience. AI translation fixes the speed problem, but it can be just as inconsistent without clear instructions.

This is where a project-specific glossary of terms makes a difference. With TranslateBot, this glossary is a simple Markdown file named TRANSLATING.md that you commit to your repository. It's a set of rules for the AI model, ensuring key terminology is handled correctly every time. Your translation rules live right alongside your code, under version control.

Building Your TRANSLATING.md File

The goal here is to give the AI clear, unambiguous rules for brand names, technical jargon, or any string that needs special handling. This prevents the model from making unhelpful "creative" choices when you need precision.

A good TRANSLATING.md file has two types of entries:

Here’s a practical template you can adapt for your project:

# Translation Rules for My Awesome App

This file contains rules for translating this project.
TranslateBot uses these instructions to ensure consistency.

## General Rules

- **Django**: Do not translate. It's a proper name.
- **TranslateBot**: Do not translate. This is our product name.

## Specific Language Rules

### For French (fr):

- "Sign in": Always translate as "Se connecter".
- "Dashboard": Always translate as "Tableau de bord".

### For German (de):

- "User": Translate as "Benutzer", not "Anwender".

This simple file gives you control over your project's glossary of terms. For other workflow improvements, check out our advice on working effectively with translations.

Why a Version-Controlled Glossary Is Now Standard

Using a glossary isn't just nice-to-have; it's a core part of modern development. While 49.2% of websites still use English, that number is shrinking as non-English mobile usage grows. According to W3Techs' historical trends, the web is becoming more multilingual every year.

A TRANSLATING.md file is a developer-centric solution to this problem. Instead of logging into a separate SaaS portal to manage terminology, you define the rules right next to your code. This fits into any Git-based workflow, making your translation rules as reviewable and reproducible as any other part of your application.

Integrating i18n into Your CI/CD Pipeline

Localization shouldn't be a frantic, manual task before a release. That approach is a recipe for slow-downs and human error. Instead, translation should be an automated, predictable part of your development workflow, just like running tests or linting. This is where wiring i18n into your CI/CD pipeline makes a difference.

By automating the process, you guarantee that every new piece of text gets captured and translated consistently. It keeps your translations in sync with your codebase, creating a repeatable process managed entirely within Git. Forget about logging into third-party portals or emailing .po files back and forth.

A Developer-Centric Workflow

The goal is to automate the makemessages -> translate -> compilemessages sequence. A developer-centric workflow keeps you in the tools you already use: your editor, Git, and your CI provider. This is a massive departure from SaaS platforms that pull you into a separate web app.

At its core, the automated process is straightforward and Git-based.

Flowchart illustrating the automated translation process from code to AI translation.

The flow is simple. You write code, a TRANSLATING.md file provides context to the AI, and a bot handles the translation work automatically. The entire localization process stays inside the repository, making it transparent and simple to manage.

Sample GitHub Actions Workflow

You can set this up with a basic GitHub Actions workflow. This action triggers on a push, runs the necessary Django commands, and uses TranslateBot to manage the AI translation step. The result is a fresh commit with updated .po files, ready for you to review in a pull request.

Here’s a sample workflow .yml file:

name: Translate PO Files

on:
  push:
    branches: [ main ]

jobs:
  translate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.11'

      # Install dependencies, including TranslateBot
      - name: Install dependencies
        run: pip install django translatebot

      # Find new strings to translate
      - name: Run makemessages
        run: python manage.py makemessages --all

      # Translate new strings using your glossary
      - name: Run translatebot
        run: translatebot
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}

      # Compile translations for production
      - name: Run compilemessages
        run: python manage.py compilemessages

      # Commit the updated .po files
      - name: Commit translations
        run: |
          git config --global user.name 'GitHub Actions'
          git config --global user.email 'actions@github.com'
          git add .
          git commit -m "chore: update translations" || echo "No changes to commit"
          git push

This workflow ensures every push to main triggers an automatic translation update. For more advanced setups, check out our guide on setting up CI for your Django project. This approach turns localization from a painful chore into a seamless, automated part of your engineering practice.

Why Standardized Terms Matter

Consistent terminology isn't a "nice to have". It's a core part of shipping a professional product. When "Dashboard" is translated three different ways across your app, you create confusion. You also erode user trust and make your application feel poorly assembled.

Getting this right means creating a single source of truth for your project's language. This prevents the linguistic drift that plagues many internationalized apps and ensures your product speaks with one clear voice in every language.

A Lesson from Other Fields

This isn't a new problem. For decades, large international bodies have relied on standardized terminology to prevent ambiguity. The OECD Glossary of Statistical Terms, for instance, contains over 6,700 definitions to ensure statisticians worldwide interpret data the same way.

The lesson for a developer is simple. Maintaining a glossary is a serious engineering practice, not an afterthought. Your project's glossary, even if it starts as a simple TRANSLATING.md file, serves the same purpose. It's the engineering document that prevents ambiguity and ensures everyone, developers, translators, and AI, is speaking the same language.

TRANSLATING.md as an Engineering Doc

When you treat your translation glossary like any other engineering document, you get practical benefits.

A well-maintained TRANSLATING.md file turns localization from a chaotic, error-prone mess into a predictable part of your development cycle. It’s the difference between an app that feels cobbled together and one that was built for a global audience.

Common Questions and Sticking Points

You've got the theory down, but getting i18n right in practice always brings up tricky questions. Here are the most common issues we see developers hit when they're in the trenches with Django's translation system.

What’s the Real Difference Between I18n and L10n?

People often use these terms interchangeably, but they represent two different stages of work.

Think of it this way: i18n is wiring the house for electricity. L10n is plugging in the lamps for each room.

How Do I Stop a Term from Being Translated?

This is a classic problem for brand names, feature names, or technical terms like "Django" that should never change. The most foolproof way is to add an explicit rule to your TRANSLATING.md glossary.

A simple entry like "TranslateBot: Do not translate." gives the AI a direct command. This is far more reliable than just hoping the model figures out your intent. By committing this file to version control, you ensure the rule is enforced consistently on every translation run.

Can I Use This Glossary Approach for JavaScript Translations, Too?

Yes. Django’s i18n system extends to JavaScript code using the JavaScriptCatalog view. The workflow is almost identical.

First, you tell Django to pull strings from your JavaScript files by running python manage.py makemessages -d djangojs. This will generate a separate djangojs.po file for your frontend code.

From there, everything works the same. A tool like TranslateBot will process djangojs.po just like it does the main django.po file. It uses the same project-level TRANSLATING.md glossary, ensuring a term is translated the same way whether it appears in your Python views or your React components.

Why Is a Glossary File Better Than Just Adding Comments to the .po File?

While you can add translator comments (#.) directly into .po files for context, a central TRANSLATING.md glossary is a better solution for consistency and automation.

A comment in a .po file is tied to a single msgid. If the same term shows up somewhere else without a comment, that context is lost. A glossary file creates a global rule for the entire project. It's a single source of truth that guarantees a term like "Dashboard" is always handled the same way, everywhere. This makes your localization process more predictable and less vulnerable to human error.


Ready to stop copy-pasting translations and automate your Django i18n workflow? TranslateBot integrates directly into your project with a single command. It uses your version-controlled glossary to deliver consistent, high-quality translations without leaving your terminal. Check out the docs and get started for free at https://translatebot.dev.

Stop editing .po files manually

TranslateBot automates Django translations with AI. One command, all your languages, pennies per translation.