# How to Translate a Django Site the Standard Way | Vitra.ai

> Django's i18n is gettext underneath and it works well. The mistakes are lazy translation, untranslated database content and a missing locale prefix.

**Canonical URL**: https://www.vitra.ai/general/translate-a-django-site
**Source**: This is the Markdown rendering of https://www.vitra.ai/general/translate-a-django-site, generated at build time from that page.

---

4 min read

# 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.

[Samhitha J Bhatt](https://www.vitra.ai/author/samhitha)
Senior Product Manager , Vitra.ai
Updated Aug 17, 2026

![How to Translate a Django Site the Standard Way](https://www.vitra.ai/static/images/blog/translate-a-django-site.jpg)

Table of contents

[The framework is good; the gaps are predictable](#the-framework-is-good-the-gaps-are-predictable)

[Lazy versus eager translation](#lazy-versus-eager-translation)

[Database content is not covered](#database-content-is-not-covered)

[URL prefixes and locale middleware](#url-prefixes-and-locale-middleware)

[blocktrans and variables](#blocktrans-and-variables)

[Keeping catalogues current](#keeping-catalogues-current)

[Admin, emails and management commands](#admin-emails-and-management-commands)

[Formats as well as strings](#formats-as-well-as-strings)

[Where to start](#where-to-start)

[FAQ](#faq)

Contributors

[Samhitha J Bhatt](https://www.vitra.ai/author/samhitha)
Senior Product Manager

Subscribe to our newsletter

Subscribe

> **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](https://www.vitra.ai/general/internationalization-vs-localization) 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](https://www.vitra.ai/features/translation-memory) then means only genuinely new strings need work each release, and [quality control](https://www.vitra.ai/features/quality-control) checks that placeholders survived the round trip.

Setup and supported patterns are on the [Django integration](https://www.vitra.ai/integration/django) 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](https://www.vitra.ai/automotive/automotive-brochure-localization)
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.

[Samhitha J Bhatt](https://www.vitra.ai/author/samhitha)
Aug 18, 2026

Automotive

[Automotive Campaign Localization Across Markets](https://www.vitra.ai/automotive/automotive-campaign-localization)
Campaigns run through national companies and dealer networks, so one master becomes hundreds of files. Where the offer text and the disclaimers actually break.

[Samhitha J Bhatt](https://www.vitra.ai/author/samhitha)
Aug 18, 2026

Automotive

[Car Service Manual Translation for Technicians](https://www.vitra.ai/automotive/automotive-service-manual-translation)
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.

[Samhitha J Bhatt](https://www.vitra.ai/author/samhitha)
Aug 18, 2026

[View all posts](https://www.vitra.ai/blog/page/1)

---

## Structured data

```json
{
  "@context": "https://schema.org",
  "@type": "Article",
  "mainEntityOfPage": {
    "@type": "WebPage",
    "@id": "https://www.vitra.ai/general/translate-a-django-site"
  },
  "headline": "How to Translate a Django Site the Standard Way",
  "image": [
    {
      "@type": "ImageObject",
      "url": "https://www.vitra.ai/static/images/blog/translate-a-django-site.jpg"
    }
  ],
  "datePublished": "2026-08-17T00:00:00.000Z",
  "dateModified": "2026-08-17T00:00:00.000Z",
  "author": [
    {
      "@type": "Person",
      "name": "Samhitha J Bhatt"
    }
  ],
  "publisher": {
    "@type": "Organization",
    "name": "Vitra.ai",
    "logo": {
      "@type": "ImageObject",
      "url": "https://www.vitra.ai/static/images/vitra-v-logo.png"
    }
  },
  "description": "Django's i18n is gettext underneath and it works well. The mistakes are lazy translation, untranslated database content and a missing locale prefix."
}
```

```json
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    {
      "@type": "ListItem",
      "position": 1,
      "name": "Home",
      "item": "https://www.vitra.ai"
    },
    {
      "@type": "ListItem",
      "position": 2,
      "name": "General",
      "item": "https://www.vitra.ai/general"
    },
    {
      "@type": "ListItem",
      "position": 3,
      "name": "How to Translate a Django Site the Standard Way",
      "item": "https://www.vitra.ai/general/translate-a-django-site"
    }
  ]
}
```

```json
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "Does Django have built-in translation support?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "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."
      }
    },
    {
      "@type": "Question",
      "name": "When should gettext_lazy be used instead of gettext?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "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."
      }
    },
    {
      "@type": "Question",
      "name": "How do I get language-specific URLs in Django?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "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."
      }
    },
    {
      "@type": "Question",
      "name": "Does Django translate database content?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "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."
      }
    }
  ]
}
```
