Meta description: Confused about whether a file belongs in Git or .gitignore? Here's what a source file is in Django, and how it affects builds and localization.
You run python manage.py makemessages, Git lights up, and suddenly locale/ is full of files you didn't have five minutes ago. Your first instinct is often the wrong one. .po looks generated, so maybe it belongs next to .pyc in .gitignore.
That mistake causes a lot of churn.
If you treat translation files like throwaway build output, you lose review history, create mismatches between code and translations, and make deploys harder to reproduce. If you treat actual build artifacts like source, you commit noise and drift into your repo. Knowing what is a source file is how you draw that line.
Source File or Build Artifact? The .gitignore Dilemma
The common Django version of this problem looks like this:
python manage.py makemessages --locale fr
git status
Now you see changes like:
locale/fr/LC_MESSAGES/django.po
At the same time, your repo may also have things you already know to ignore:
__pycache__/
*.pyc
Those two categories aren't the same, even if a command created both at some point in your workflow.
What belongs in Git
A practical rule is to ask one question first. Which file is the editable record your team intends to maintain?
For Django projects, that usually includes:
- Python code like
models.py,views.py,urls.py - Templates like
templates/account/login.html - Static source assets that your team edits directly
- Translation source like
locale/fr/LC_MESSAGES/django.po
What usually doesn't belong in Git is tool output your team can rebuild from those files.
- Bytecode like
*.pyc - Cache directories like
__pycache__/ - Compiled translation binaries like
*.mo, if your deploy pipeline can build them - Other generated artifacts that don't carry the authoritative edit history
Practical rule: If your teammate fixes a bug by opening the file in an editor and committing the change, it's probably source. If a tool regenerates it from another file, it's probably a build artifact.
Where teams get burned
The bad workflow is easy to spot. Someone updates text in templates, forgets to commit the matching .po changes, and another developer pulls the branch and can't rebuild the same localization state. Or someone commits generated caches and every pull request turns into noise.
A clean .gitignore isn't about aesthetics. It's about preserving the files your team authors, and dropping the files your tools can recreate.
What Is a Source File Anyway?
A source file is the original, editable file from which another version is derived. In historical research, libraries and universities use the same idea for primary sources. They define them as original materials created by a participant or witness, such as letters, diaries, official documents, photographs, recordings, and artifacts, because those preserve evidence closest to the event itself, as described in Baylor University's guide to primary and source materials.
For software, keep the same mental model. A source file is the version humans maintain. A derived file is the version tools produce.

The working definition that helps in Django
Most definitions online get muddy because the term changes by domain. One explanation points out that source file gets blurred across programming files, design masters like PSD files, and academic submission files like LaTeX. For developers, the useful distinction is the one between what you write and what your tools build, especially in localization workflows, as noted in this discussion of the term's domain-specific meaning.
That distinction is the one you need in day-to-day Django work.
| File | Role | Edit by hand | Keep in Git |
|---|---|---|---|
app/models.py |
Python source | Yes | Yes |
templates/base.html |
Template source | Yes | Yes |
locale/fr/LC_MESSAGES/django.po |
Translation source | Yes | Yes |
__pycache__/views.cpython-*.pyc |
Python bytecode | No | No |
locale/fr/LC_MESSAGES/django.mo |
Compiled translation binary | No | Usually no |
The test that actually works
Use this when you're unsure:
- Openability test: If you expect a developer or translator to edit it, it's source.
- Authority test: If another file is generated from it, it's the source of truth.
- Rebuild test: If CI can recreate it from committed files, it's probably not source.
That last point matters because teams often confuse "created by a command" with "generated junk." makemessages creates .po files, but those files become your maintained translation source after generation.
Your Django Project Is Full of Source Files
You feel this fast on a real team. A pull request touches models.py, a template, and locale/fr/LC_MESSAGES/django.po. All three deserve review because all three change application behavior.
A Django codebase has more source files than many developers first assume. Python modules are the obvious ones, but templates, management command code, fixture definitions your team maintains by hand, and translation catalogs also sit on the authored side of the line. They are the files people read, edit, review, and blame in Git when something breaks.

The source files you work on every week
In a typical Django app, these usually count as source files:
models.pybecause schema and domain rules start thereviews.pybecause request handling lives thereurls.pybecause routing decisions are authored there- Templates because they define rendered output
forms.py,admin.py,tasks.py, and custom management commands because they contain application logiclocale/*/LC_MESSAGES/django.pobecause translators and developers edit them directly- Static authoring files if your team maintains the originals in the repo
The practical test is simple. If a developer or translator is expected to change the file and submit that change through code review, treat it like source.
A small example:
from django.db import models
from django.utils.translation import gettext_lazy as _
class Invoice(models.Model):
status = models.CharField(
max_length=32,
verbose_name=_("status"),
)
That _('status') call starts in Python source, but it does not stay there. Django's i18n tooling extracts that string into a translation catalog, and the catalog becomes another maintained source file in the same feature's lifecycle.
Why .po files belong in the same category
Teams sometimes treat .po files like exports because makemessages created the first version. That breaks down in practice. After extraction, the .po file becomes the place where translation decisions live, where reviewers catch bad phrasing, and where merge conflicts show two people changed the same user-facing string.
Here's a realistic example:
msgid ""
msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Language: fr\n"
#: billing/templates/billing/invoice.html:18
#, python-format
msgid "Hello %(name)s, your invoice is ready."
msgstr "Bonjour %(name)s, votre facture est prête."
#: billing/models.py:42
msgid "status"
msgstr "statut"
#: billing/templates/billing/summary.html:12
#, python-format
msgid "%(count)s invoice"
msgid_plural "%(count)s invoices"
msgstr[0] "%(count)s facture"
msgstr[1] "%(count)s factures"
This file is source because your team edits it on purpose. It is readable, diffable, and reviewable. If you want a quick refresher on the structure, this guide to the PO file format in Django localization workflows is a useful reference.
That distinction matters in Git. A noisy .po diff may still contain the actual business change, such as a revised legal label or a pluralization fix for a billing screen. If reviewers dismiss it as generated output, bad translations ship.
A quick visual helps if you're explaining this to someone else on the team.
Your app runs from maintained inputs. In Django, that includes code, templates, and translation catalogs.
The Localization Loop From .po Source to .mo Binary
A common Django mistake shows up right after a translation edit. Someone updates django.po, reloads the page, still sees the old string, and starts questioning the template, the locale middleware, or the deployment. The missing step is usually simpler. Django is still reading the compiled catalog.

The workflow has two distinct file types. .po is the source your team reads and edits. .mo is the machine-oriented output Django loads at runtime. That split matters because it affects what you review in Git, what you regenerate in CI, and what you should never hand-edit on a server.
The loop in commands
python manage.py makemessages --locale fr
python manage.py compilemessages
If you're using an automated translator in the middle, the flow becomes:
python manage.py makemessages --locale fr
python manage.py translate --locale fr
python manage.py compilemessages
makemessages extracts translatable strings into .po files. A translator, developer, or automation step updates the entries. compilemessages then turns those text catalogs into .mo files for runtime use.
The compiler analogy is useful, but the practical rule is what matters day to day. Edit the human-readable source. Build the runtime artifact. Ship the built result your deployment process expects.
For teams automating this pipeline, it helps to know the exact structure of headers, references, and plural forms in a PO file format guide for Django localization workflows. Badly formed entries often surface late, usually when compilemessages runs in CI instead of on a laptop.
I handle translation assets with the same discipline used for buildable infrastructure definitions. The source belongs in version control, the generated output belongs in the build pipeline, and the result should be reproducible on any machine. The same reasoning behind infrastructure as code best practices applies here.
Keep the boundary clean: edit
.po, build.mo, run the app. Don't edit.moby hand, and don't expect translation changes to appear before recompiling.
How This Impacts Your Git History and CI/CD Pipeline
Once you treat .po files as source, a lot of workflow decisions stop being fuzzy.
Translation changes belong in pull requests. Reviewers can see string changes next to the code that introduced them. Releases stay reproducible because the repo contains the source state for code and localization together. One source on editable masters notes that source files often stay within a limited chain of people because recipients can alter the work, but for team development the right move is to commit those sources with clear ownership rules so the project can be rebuilt consistently, as discussed in this glossary entry on source file handoff and control.
What works in practice
A sane pipeline usually looks like this:
- During development: run
makemessageswhenever UI strings change - In review: diff
.pochanges like any other text file - In CI: validate files, then build runtime artifacts
- At deploy time: compile translations as part of the artifact build
That approach lines up well with the same discipline you already use for infra definitions. If your team cares about reproducibility, drift control, and reviewable changes, the principles in these infrastructure as code best practices apply almost one-to-one to localization assets.
What doesn't work
A few patterns create pain fast:
- Ignoring
.pofiles: You lose history and break reproducibility. - Committing only
.mofiles: You keep binaries without the editable source. - Translating outside Git: You force reviewers to trust a portal snapshot.
- Running ad hoc commands on laptops: You get state nobody else can rebuild.
If you want your team to review generated changes cleanly, it also helps to tighten up commit hygiene. This article on commands in Git is a good reminder that localization diffs deserve the same discipline as code diffs.
For automation, one valid option is TranslateBot. It runs as a Django management command and writes translations back into .po files, which fits naturally into a repo-first workflow.
Best Practices for Healthy Source Files
You don't need a grand system here. You need habits that prevent broken translations and messy diffs.

Keep the text safe
Translation files are still source code in the sense that bad edits can break runtime behavior.
- Use UTF-8 everywhere: Your
.pofiles should stay text-safe across editors and CI. - Preserve placeholders:
%(name)s,%s, and similar tokens must survive translation intact. - Watch HTML fragments: If a string contains tags, translators and tools need to preserve them exactly.
Bad:
msgid "Hello %(name)s"
msgstr "Bonjour %name"
Good:
msgid "Hello %(name)s"
msgstr "Bonjour %(name)s"
Give translators context
Short strings are where quality falls apart. "Open", "Close", "Archive", "Post" can all mean different things depending on the screen.
Use translator comments and message context where needed:
from django.utils.translation import gettext_lazy as _
from django.utils.translation import pgettext_lazy
button_label = pgettext_lazy("verb for opening a document", "Open")
status_label = _("Archived")
You can also add comments near marked strings so extraction keeps useful context.
Keep files reviewable
The healthiest translation source files are boring to review.
- Commit
.powith the feature change: Don't batch unrelated locale updates later. - Avoid manual formatting churn: Let one toolchain own ordering and wrapping.
- Review fuzzy and plural entries carefully: Those are where subtle mistakes hide.
- Write copy for translation: Short, context-free UI fragments are harder to translate well. This guide on writing for translation is worth circulating to anyone who ships product copy.
The practical takeaway is small but important. Treat django.po with the same care you give models.py. It's source. It deserves review, ownership, and a clean path through CI.
If your team wants to stop copy-pasting strings into portals, TranslateBot fits neatly into the Django workflow you already have. Run makemessages, fill or update .po entries with a management command, review the diff in Git, then compile for deploy.