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:
- Session value —
request.session[LANGUAGE_SESSION_KEY](if set). - Cookie —
django_languagecookie sent by the browser. Accept-Languageheader — from the WebSocket handshake.LANGUAGE_CODE— your default insettings.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:
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:
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¶
- Django context in Reflex — how
LANGUAGE_CODEand other context-processor keys reachself.request. - Custom middleware in events — the underlying middleware that activates the language.