Clearing Up the gettext vs gettext_lazy Confusion in Django (Understanding When Translation Happens)

When using Django i18n, you often find yourself unsure whether to use gettext() or gettext_lazy(). Most of the confusion comes from trying to memorize how the two behave.

The key idea is simple:

  • gettext translates now (eager evaluation)
  • gettext_lazy translates later (lazy evaluation)

With this single notion of evaluation timing, almost every case can be sorted out.


Why does the when of translation matter?



Django typically changes the language on a per‑request basis.

Flow comparing translation timing in Django

  • Middleware inspects the request and calls activate("ko") / activate("en") to activate the language for the current thread/context.
  • When rendering templates, forms, or the admin interface, the string must be translated into that language.

In other words, when you translate a string determines the outcome.


gettext(): Translate now and return the string

from django.utils.translation import gettext as _

def view(request):
    message = _("Welcome")   # Translated using the language active at this moment
    return HttpResponse(message)
  • When called inside a function/view (i.e., during request handling at runtime), it behaves as expected.
  • However, calling it at module import time can cause problems.

Common pitfall: Using gettext() in module constants

# app/constants.py
from django.utils.translation import gettext as _

WELCOME = _("Welcome")  # ❌ May become fixed to the language at server start/import time

In this scenario, WELCOME is frozen to the language that was active when the module was imported, even if the language changes later (especially in environments where imports happen only once).


gettext_lazy(): Return a lazy proxy that translates later



from django.utils.translation import gettext_lazy as _

WELCOME = _("Welcome")  # ✅ Returns a proxy object, not the actual string

gettext_lazy() typically returns a lazy proxy.

  • When a form/template/admin renders the value as a string,
  • It is translated using the language that is active at that rendering time.

One‑sentence summary: When the language is determined at rendering time, lazy is usually the right choice.


Where to use what: Practical rules

1) You’re immediately creating a response or output → gettext

  • In view/service logic where you build a string for a response or log entry.
from django.utils.translation import gettext as _

def signup_done(request):
    return JsonResponse({"message": _("Signup completed.")})

2) You’re defining class attributes, model meta, or form fields that may be evaluated at import time → gettext_lazy

  • Model verbose_name, help_text
  • Form field label, help_text
  • DRF serializer field label (for similar reasons)
  • Admin list_display descriptions – define early, render later
from django.db import models
from django.utils.translation import gettext_lazy as _

class Article(models.Model):
    title = models.CharField(_("title"), max_length=100)
    status = models.CharField(
        _("status"),
        max_length=20,
        choices=[
            ("draft", _("Draft")),
            ("published", _("Published")),
        ],
        help_text=_("Visibility of the article."),
    )

3) Reusable module‑level constants/choices → usually gettext_lazy

  • Reused constants are safest when kept lazy.
from django.utils.translation import gettext_lazy as _

STATUS_CHOICES = [
    ("draft", _("Draft")),
    ("published", _("Published")),
]

4) Strings sent to external systems (logs, third‑party APIs, headers, etc.) → gettext or force evaluation of lazy

Passing a lazy object directly can lead to unexpected types or serialization issues.

from django.utils.translation import gettext_lazy as _
from django.utils.encoding import force_str

msg = _("Welcome")
logger.info(force_str(msg))  # ✅ Convert to a real string before use

5) String concatenation/formatting is involved → consider lazy‑specific tools

Mixing lazy messages with f‑strings or str.format() can confuse the evaluation order. Django offers format_lazy for this purpose.

from django.utils.translation import gettext_lazy as _
from django.utils.text import format_lazy

title = format_lazy("{}: {}", _("Error"), _("Invalid token"))

Alternatively, using Python’s % formatting keeps translation strings tidy.

from django.utils.translation import gettext as _

message = _("Hello, %(name)s!") % {"name": user.username}

Three Most Common Mistakes

Mistake 1: Creating a fixed translation at module import with gettext()

  • If you have constants/choices, suspect lazy first.

Mistake 2: Putting a lazy object directly into JSON serialization or logs

  • Convert to a string with force_str() before use.

Mistake 3: Building translated strings with f‑strings

# ❌ Not recommended
_("Hello") + f" {user.username}"
  • The translation unit gets split (different word order per language) and the evaluation timing becomes more complex.
  • Instead, perform variable substitution inside the translation string.
# ✅ Recommended
_("Hello, %(name)s!") % {"name": user.username}

Tips to Reduce Confusion

The most effective way to reduce confusion is to standardize the meaning of _ based on the file type.

  • In models.py, forms.py, admin.py (files where definitions come first): from django.utils.translation import gettext_lazy as _
  • In views.py, services.py (files where execution comes first): from django.utils.translation import gettext as _

With this convention, lazy becomes the default in definition‑heavy files, which naturally reduces mistakes.


Quick Summary

  • Runtime logic that needs an immediate stringgettext
  • Definitions, meta, constants, choices, labels that will be rendered latergettext_lazy
  • Strings leaving the application → convert with force_str if needed
  • Lazy + formatting → use format_lazy or perform substitution inside the translation string

Once you adopt these guidelines, the conceptual confusion disappears and you can almost always pick the right function automatically.


Related posts - Problems and Solutions When Using gettext_lazy with JSON Keys