How to Translate a Django Site the Standard Way
Django's i18n is gettext underneath and it works well. The mistakes are lazy translation, untranslated database content and a missing locale prefix.

Quick answer — Django ships a complete internationalisation framework built on gettext: template tags, lazy translation for module-level strings, locale middleware and URL prefixes. What it does not translate is anything stored in your database.
The framework is good; the gaps are predictable
{% trans %} and {% blocktrans %} in templates, gettext and gettext_lazy in Python, makemessages to extract and compilemessages to build. It is mature and it works.
Three things go wrong, and they go wrong on nearly every project.
Lazy versus eager translation
A string evaluated at import time — a model field's verbose_name, a form label, a choices tuple — is resolved before the request knows what language it is. Use gettext_lazy there and gettext inside views. Get this backwards and the site shows whichever language was active when the process started, for everybody, until it restarts. It is the single most confusing Django i18n bug because it looks intermittent.
Database content is not covered
Django's i18n translates code and templates. It does not touch anything an editor typed into the admin — product names, page bodies, category titles. Those need either a translatable-field package or a translation layer over the rendered output. Teams routinely finish the framework work, see the site still mostly in English, and only then discover the split.
URL prefixes and locale middleware
i18n_patterns puts a language code in the path and LocaleMiddleware picks the active language from it. That produces the distinct, crawlable URLs search engines need.
Without the prefix, language comes from a cookie or a header and every language shares one URL, which is invisible to search. Check the order of middleware too; LocaleMiddleware has to sit after session and before common.
blocktrans and variables
{% blocktrans %} handles interpolation and plurals. Variables inside it must be simple names, not attribute lookups, which is a constraint people hit immediately and work around by assigning first with {% with %}.
Plurals use {% plural %} inside the block, and the target language's form count comes from its .po header rather than from English.
Keeping catalogues current
makemessages merges rather than overwrites, so existing translations survive and changed strings are marked fuzzy. Run it in CI so a developer adding a string cannot forget.
translation memory then means only genuinely new strings need work each release, and quality control checks that placeholders survived the round trip.
Setup and supported patterns are on the Django integration page.
Admin, emails and management commands
The Django admin is translated by the framework, but your own admin customisations are not unless you wrapped their strings.
Emails sent from management commands or Celery tasks have no request, so the active language is whatever the settings default is.
Pass the recipient's language explicitly and activate it around the render. Otherwise every notification goes out in the default language regardless of who receives it.
Formats as well as strings
Django's USE_L10N handles date, number and currency formatting per locale. Turning it on changes how existing pages render, so do it early in the project rather than after content has been reviewed against the old format.
Where to start
Audit which of your user-facing text is in templates and which is in the database. That ratio decides whether this is a framework job or a content job.
FAQ
Does Django have built-in translation support? Yes, a full gettext-based framework with template tags, lazy translation, locale middleware and URL prefixes. It covers code and templates but not content stored in the database.
When should gettext_lazy be used instead of gettext? For anything evaluated at import time — model verbose names, form labels, choices tuples. Using the eager version there freezes the language at process start for every user, which looks like an intermittent bug.
How do I get language-specific URLs in Django? With i18n_patterns and LocaleMiddleware, which puts a language code in the path. Without the prefix, language comes from a cookie and every language shares one URL, which search engines cannot index separately.
Does Django translate database content? No. Product names, page bodies and anything typed into the admin need either a translatable-field package or a translation layer over the rendered output. Teams often discover this after finishing the framework work.
Our blog
Lastest blog posts
Tool and strategies modern teams need to help their companies grow.

Automotive
Automotive Brochure Localization by Market
A car brochure is a spec grid, a legal footer and a photo library, all market-specific. What actually has to change, and why the layout decides the schedule.

Automotive
Automotive Campaign Localization Across Markets
Campaigns run through national companies and dealer networks, so one master becomes hundreds of files. Where the offer text and the disclaimers actually break.

Automotive
Car Service Manual Translation for Technicians
A workshop manual is read mid-repair by someone with the car on a lift. What that demands of procedures, torque figures and fault codes, in every language.