Meta description: Need to change one UI phrase across Django code, templates, and .po files? Use exact search, regex, and PO-aware commands without missing variants.
You changed Add to Cart to Add to Basket. The code review is approved. The product team is happy. Then you load the app in French and still see the old copy in one modal, two validation errors, and a stale msgid in locale/fr/LC_MESSAGES/django.po.
That's the core search for phrase problem in Django. The phrase might live in Python, templates, JavaScript, fixtures, or gettext catalogs. Your editor's Find panel catches the obvious hits. It usually misses the messy ones.
A phrase matters because it's not just a bag of words. In phrase search, the exact word order is the unit, not the individual tokens, which is why exact matching narrows results and improves precision according to WorldCat phrase search documentation. In a Django app, that distinction shows up fast. Add to Cart is not the same maintenance problem as separate hits for add, to, and cart.
Finding a Needle in a Haystack
The first mistake is treating every text change like a plain text search.
In a real Django codebase, the same phrase can appear in all of these places:
- Template literal in
templates/shop/product_detail.html - Lazy translation call in Python via
gettext_lazy - Form validation message inside a form or serializer
msgidentry in multiple locale filesmsgstrtranslation that no longer matches the source wording
Here's the kind of spread you're dealing with:
from django.utils.translation import gettext_lazy as _
class CartForm(forms.Form):
submit_label = _("Add to Cart")
<button>{% translate "Add to Cart" %}</button>
#: templates/shop/product_detail.html:18
msgid "Add to Cart"
msgstr "Ajouter au panier"
You also hit awkward cases where the visible phrase is broken up by markup or placeholders:
<a href="#">
{% translate "Add" %} <strong>{% translate "to Cart" %}</strong>
</a>
That won't show up in a literal exact match for the full phrase.
Practical rule: Start with exact phrase search. Assume it's incomplete. Then widen the search on purpose.
When you treat search for phrase as code archaeology, the workflow gets better. You search the literal phrase first, then variations, then translation structure. That order saves time and avoids false confidence.
Your Command-Line Toolkit for Code Search
Your editor is fine for a quick pass. For whole-repo work, use the shell.

Use ripgrep first
rg is my default. It's fast, respects ignore files, and handles regex cleanly.
rg -n -F "Add to Cart" .
Useful variants:
rg -n -i "add to cart" .
rg -n -t py -t html -t po "Add to Cart" .
rg -n --glob '!**/node_modules/**' --glob '!**/__pycache__/**' "Add to Cart" .
Use it when you want raw speed and good defaults.
Use git grep inside tracked code
git grep is better when you only care about files Git knows about. That cuts noise from build artifacts and generated junk.
git grep -n "Add to Cart"
git grep -n -i "add to cart"
git grep -n, '*.py' '*.html' '*.po'
Use it when you're auditing code that will ship.
Keep grep for bare servers
grep is everywhere. It's not pleasant, but it's always there.
grep -RIn --exclude-dir=__pycache__ --exclude-dir=node_modules "Add to Cart" .
If you spend time in shell environments where these basics still feel rusty, this essential guide to Terminal for Mac users is a decent refresher on navigation and command-line habits.
Here's the short version.
| Tool | Best use | What it gets right | What it gets wrong |
|---|---|---|---|
rg |
Day-to-day repo search | Fast, regex-friendly, sane defaults | Not installed everywhere |
git grep |
Tracked source only | Repo-aware, low noise | Ignores untracked files |
grep |
Minimal environments | Universal availability | More flags, more cleanup |
For Django translation work, I usually combine them:
git grepfor tracked source.rgfor broader pattern hunts.greponly when I'm on a box without better tools.
If you want a command-centric workflow around translation jobs, the TranslateBot command reference is worth skimming because it keeps everything in CLI land instead of pushing you into a portal.
Search tracked files with
git grep. Search patterns withrg. Reach forgrepwhen the environment gives you no choice.
Using Regex to Find Phrase Variations
Literal matching breaks the moment copy isn't perfectly uniform.
You'll see all of these in old apps:
Sign upSign Upsign-upsign upSign up now
A better search for phrase flow uses regex the same way a database search uses controlled terms and variants. In systematic search methods, the workflow is to build a precise query from variants and syntax rather than guessing with one broad string, as described in this systematic-search methodology overview.
Start with case and punctuation
For most UI copy changes, this catches the common drift:
rg -n -i 'sign[ -]?up' .
That pattern matches:
Sign upsign upsign-upSign-Up
If spacing is inconsistent:
rg -n -i 'sign\s*[- ]?\s*up' .
Search around placeholders
Django translations often wrap placeholders that you can't break.
msgid "Welcome, %(name)s"
msgstr ""
If the phrase changes but the placeholder must stay, search for the structure, not just the visible words:
rg -n '%\(name\)s' locale
rg -n 'Welcome,\s+%\(name\)s' locale
Brace-style placeholders show up in mixed stacks or copied strings:
rg -n 'items?\s+\{count\}' .
A realistic .po example:
msgid "%(count)s item in your cart"
msgid_plural "%(count)s items in your cart"
msgstr[0] ""
msgstr[1] ""
If you're updating cart to basket, don't search only for the raw phrase. Search the placeholder-bearing variant too:
rg -n '%\(count\)s .*cart' locale
Know when regex is worse
Regex is great for variation. It's bad when you need exact legal or UX wording.
Use exact matching when:
- Copy must match exactly across product surfaces
- You're checking regressions from a specific text change
- You're auditing
msgidchurn aftermakemessages
Use regex when:
- Case drifts across old templates
- Hyphens and spacing vary
- Placeholders sit inside the phrase
A loose regex can explode your result set. That's the same failure mode you get in broad database searching. If the pattern returns noise, tighten it fast.
How to Search for a Phrase in PO Files
A .po file is not just text. It's a structured catalog.
That distinction matters because msgid "Add to Cart" and msgstr "Ajouter au panier" answer different questions. One tells you where the source phrase exists. The other tells you how that phrase was translated in a locale.

Search msgid when source copy changed
If product changed the canonical English phrase, search msgid first.
rg -n '^msgid "Add to Cart"$' locale
That tells you exactly which catalogs still carry the old source string.
If you want surrounding context:
rg -n -C 2 '^msgid "Add to Cart"$' locale
That usually shows the file reference comment and current translation.
Example:
#: templates/shop/product_detail.html:18
msgid "Add to Cart"
msgstr "Ajouter au panier"
Search msgstr when translation output looks wrong
When a locale displays the wrong visible text, target msgstr:
rg -n '^msgstr ".*panier.*"$' locale/fr/LC_MESSAGES/django.po
That's useful when translators used inconsistent terms and you need to standardize one locale without touching the source language.
Find untranslated and fuzzy entries
These two searches belong in every Django i18n playbook.
Untranslated entries:
rg -n '^msgstr ""$' locale
Fuzzy entries:
rg -n '^#, fuzzy' locale
And here's a realistic fuzzy block:
#: templates/shop/cart.html:42
#, fuzzy
msgid "Add to Basket"
msgstr "Ajouter au panier"
A fuzzy flag is a warning, not a translation strategy. You still need to review it before shipping.
Search
msgidfor source truth. Searchmsgstrfor locale behavior. Search#, fuzzyand emptymsgstrbefore every release.
Watch plural forms and multiline strings
Plural entries need their own search habit:
rg -n 'msgid_plural|msgstr\[[0-9]+\]' locale
Multiline strings also trip people up because the phrase may be split across lines:
msgid ""
"Add to Cart "
"for selected plan"
msgstr ""
A plain literal search may miss that. At that point, use context searches or a PO-aware editor.
For teams working directly with gettext files in code review, the TranslateBot PO file docs are useful because they stay focused on actual django.po workflows instead of generic localization theory.
One more thing matters here. Developer-facing translation isn't just about linguistic quality. A significant engineering risk is broken placeholders or malformed markup in .po files, which can crash rendering if your pipeline doesn't guard against them. That's why I treat gettext files as structured data first and text second.
Let the Code Find the Phrases for You
Manual search is still necessary. It just shouldn't be your only defense.
Once you already run makemessages, the better move is to let your tooling surface changed strings as part of the normal dev cycle. That shifts the job from hunting text to reviewing diffs.

A practical loop looks like this:
python manage.py makemessages -l fr
python manage.py translate --locale fr
python manage.py compilemessages
That works because new or changed msgid entries become visible in Git. You review the delta instead of rediscovering every phrase by hand.
What automation is good at
- Detecting new strings after template or Python changes
- Filling empty
msgstrentries consistently - Keeping translation work inside Git diffs
- Running in CI with the same rules every time
TranslateBot is one option here. It adds a manage.py translate workflow for Django projects, works on .po files, and exposes a Python API for automation if you want to wire checks into your own scripts.
What automation still won't solve
- Short UI labels with weak context
- Brand terminology disputes
- Plural edge cases
- Bad source copy
If you want a light primer on the language side behind machine-assisted workflows, this piece on understanding NLP concepts is useful background. The engineering question stays the same though. Can your pipeline translate safely without corrupting placeholders or markup?
That's the bar. If the tool can't preserve code-shaped content, don't put it in CI.
A Pre-Deploy Phrase Search Checklist
Before you merge a text change, run the boring checks. They catch the stuff that slips through review.

Run an exact tracked-file search Use
git grep -n "Add to Cart"to find literal leftovers in source you ship.Run one regex pass for variants
Userg -n -i 'add[ -]?to[ -]cart' .when casing, spacing, or punctuation may have drifted.Check gettext catalogs by field
Searchmsgid, thenmsgstr, then fuzzy and empty entries insidelocale/.Regenerate messages before reviewing translations
If you skipmakemessages, you're searching stale catalogs.Compile and load the app in one non-default locale
compilemessagescatches file-level issues. The browser catches wording and layout issues.
A phrase change isn't done when the English page looks right. It's done when the source, catalogs, and rendered UI all agree.
If your team wants to stop copy-pasting strings into portals, TranslateBot is worth a look. It keeps Django translation work in code, translates .po files through a manage.py command, and is built around preserving placeholders and HTML so you can review clean diffs instead of fixing broken catalogs by hand.