# 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? {#sec-cd5e4068943f} Django typically changes the language on a per‑request basis. ![Flow comparing translation timing in Django](/media/editor_temp/6/29f9dbe8-44b7-47ed-bef6-4cc0cc62bb79.png) * 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 {#sec-9ff53b7dc3ad} ```python 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 {#sec-7c331db329fb} ```python # 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 {#sec-a90df9111315} ```python 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 {#sec-4a8a3cbed930} ### 1) You’re *immediately* creating a response or output → `gettext` {#sec-197c23c83765} * In view/service logic where you build a string for a response or log entry. ```python 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` {#sec-b545464ef5b7} * 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* ```python 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` {#sec-9ea28af08fa3} * **Reused constants** are safest when kept lazy. ```python 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 {#sec-f04525bb1d99} Passing a lazy object directly can lead to unexpected types or serialization issues. ```python 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 {#sec-67d42a88924b} Mixing lazy messages with f‑strings or `str.format()` can confuse the evaluation order. Django offers `format_lazy` for this purpose. ```python 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. ```python from django.utils.translation import gettext as _ message = _("Hello, %(name)s!") % {"name": user.username} ``` --- ## Three Most Common Mistakes {#sec-273d53d27224} ### Mistake 1: Creating a *fixed* translation at module import with `gettext()` {#sec-7f4b5a25d9d0} * If you have *constants/choices*, suspect lazy first. ### Mistake 2: Putting a lazy object directly into JSON serialization or logs {#sec-7a741de5c1d8} * Convert to a string with `force_str()` before use. ### Mistake 3: Building translated strings with f‑strings {#sec-76316b53a754} ```python # ❌ 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. ```python # ✅ Recommended _("Hello, %(name)s!") % {"name": user.username} ``` --- ## Tips to Reduce Confusion {#sec-79205bb8a9a7} 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 {#sec-3aa43e207283} * **Runtime logic that needs an immediate string** → `gettext` * **Definitions, meta, constants, choices, labels that will be rendered later** → `gettext_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 as a JSON key](/ko/whitedec/2025/4/26/gettext-lazy-json-key-problem-solution/)