Skip to content

i18n & translations

Django's translation system works inside Reflex events too. You use the same gettext / gettext_lazy / _() calls, the same .po files, the same makemessages / compilemessages commands. The DjangoEventBridge activates the right language before every event runs, based on the same priority Django uses for HTTP requests.

This page covers the setup and the patterns. For the underlying mechanics, see How Reflex events get a request.


How the language is chosen

For every Reflex event, the bridge picks a language in this order:

  1. Session valuerequest.session[LANGUAGE_SESSION_KEY] (if set).
  2. Cookiedjango_language cookie sent by the browser.
  3. Accept-Language header — from the WebSocket handshake.
  4. LANGUAGE_CODE — your default in settings.py.

Once chosen, the bridge calls translation.activate(lang) and your handler can use _(...) normally. The reactive DjangoUserState.language variable reflects whatever was active.


Django setup

Enable i18n in settings.py:

USE_I18N = True
USE_TZ = True

LANGUAGE_CODE = "en"

LANGUAGES = [
    ("en", "English"),
    ("ar", "Arabic"),
    ("fr", "French"),
]

LOCALE_PATHS = [BASE_DIR / "locale"]

MIDDLEWARE = [
    "django.middleware.security.SecurityMiddleware",
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.common.CommonMiddleware",
    "django.middleware.csrf.CsrfViewMiddleware",
    "django.contrib.auth.middleware.AuthenticationMiddleware",
    "django.middleware.locale.LocaleMiddleware",   # <-- here
    "django.contrib.messages.middleware.MessageMiddleware",
    "django.middleware.clickjacking.XFrameOptionsMiddleware",
    "reflex_django.streaming_middleware.AsyncStreamingMiddleware",
]

Then create the locale directories and extract strings:

mkdir -p locale/ar locale/fr
python manage.py makemessages -l ar
python manage.py makemessages -l fr
# edit locale/<lang>/LC_MESSAGES/django.po, fill in translations
python manage.py compilemessages

Translating strings in Reflex pages

Use Django's gettext_lazy (aliased as _):

# shop/views.py
from django.utils.translation import gettext_lazy as _
import reflex as rx
from reflex_django import template


@template(route="/", title=_("Home"))
def index() -> rx.Component:
    return rx.vstack(
        rx.heading(_("Welcome")),
        rx.text(_("Browse our catalog and sign in to place orders.")),
    )

gettext_lazy is "lazy" — it doesn't translate until rendering time. That's important because Reflex compiles components ahead of time. Use gettext_lazy for strings inside component trees; use plain gettext (or _()) only for strings inside @rx.event handlers that run on demand.


Translating event-handler output

from django.utils.translation import gettext as _


class CheckoutState(AppState):
    @rx.event
    async def submit(self):
        try:
            ...
            return rx.toast.success(_("Order placed successfully."))
        except Exception:
            return rx.toast.error(_("Something went wrong. Please try again."))

Inside a handler, the request's language is already active (the bridge handled it). Plain gettext works.


Exposing the language to the UI

reflex-django mirrors the active language onto DjangoUserState:

from reflex_django import DjangoUserState

def language_badge():
    return rx.text("Language: ", DjangoUserState.language)


def app_layout(content):
    return rx.vstack(
        rx.html(
            rx.fragment(),
            dir=rx.cond(DjangoUserState.language_bidi, "rtl", "ltr"),
        ),
        content,
    )

language is the active code (e.g. "en", "ar"). language_bidi is True for RTL languages — useful for layout direction.

If you don't need these vars, set:

REFLEX_DJANGO_MIRROR_LANGUAGE = False

Switching language at runtime

Django ships a set_language view that updates the session. Mount it at a Django-owned URL:

# config/urls.py
from django.urls import path
from django.views.i18n import set_language

urlpatterns = [
    path("i18n/setlang/", set_language, name="set_language"),
    ...
]

urlpatterns += [
    reflex_mount(
        app_name="shop",
        django_prefix=("/admin", "/i18n"),
        ...
    ),
]

Now you can switch from inside a Reflex page using a form post:

def language_switcher() -> rx.Component:
    return rx.hstack(
        rx.link("English", href="/i18n/setlang/?language=en&next=/"),
        rx.link("العربية",  href="/i18n/setlang/?language=ar&next=/"),
        rx.link("Français", href="/i18n/setlang/?language=fr&next=/"),
    )

The user clicks a link, Django updates the session, and the next page load (or next Reflex event after a small refresh) uses the new language.

For a slicker version that uses a Reflex handler:

class LangState(AppState):
    @rx.event
    async def switch(self, lang: str):
        from django.utils.translation import activate
        self.session["_language"] = lang
        await self.session.asave()
        activate(lang)
        return rx.redirect(self.request.path)

RTL layouts

For Arabic, Hebrew, and other RTL languages, the bridge sets request.LANGUAGE_BIDI = True automatically. You can use it directly:

def app_shell(content):
    return rx.box(
        content,
        direction=rx.cond(DjangoUserState.language_bidi, "rtl", "ltr"),
    )

For more sophisticated RTL handling (flipping icons, mirroring layouts), see the Radix Themes docs or wrap your components with a custom direction provider.


Lazy vs eager translation cheat sheet

Where you use it Use
Component literals (rx.heading(...)) from django.utils.translation import gettext_lazy as _
Event handler return values, toast messages, exception text from django.utils.translation import gettext as _
Inside @template(title=...) gettext_lazy
Validators, model verbose_name gettext_lazy

If in doubt, use gettext_lazy. Plain gettext is faster but only safe where translation is guaranteed to happen at call time (not capture-and-render later).


Disabling i18n on Reflex events

If you want the bridge to skip language activation:

REFLEX_DJANGO_ACTIVATE_LANGUAGE_ON_EVENT = False

Useful for backend-only states that never produce user-visible strings (data sync, telemetry).


Common bumps

Strings show up untranslated You probably ran makemessages but forgot compilemessages. Always compile after editing .po files.

Translation is wrong (stuck on default) Check the bridge's selection: open the page, log the value of DjangoUserState.language. If it doesn't match what you expect, check the cookie / session / Accept-Language.

_("...") strings inside components don't translate Use gettext_lazy, not gettext. Components are compiled ahead of time; gettext runs immediately and captures the bootstrap language, not the per-request language.

RTL layout doesn't apply Confirm LocaleMiddleware is in MIDDLEWARE. Use rx.cond(DjangoUserState.language_bidi, "rtl", "ltr").


Where to go next


Next: HTTP APIs alongside Reflex →