Skip to content

Internationalization (i18n)

Buraq’s i18n system lets you translate your application into multiple languages using the same .po/.mo file format as Django — so existing Django translation files work without modification.

Under the hood it uses Python’s built-in gettext (pure dictionary lookups, microsecond speed) and Babel for message extraction and locale data.


settings.py
USE_I18N = True
LANGUAGE_CODE = "en"
LANGUAGES = [
("en", "English"),
("ar", "العربية"),
("fr", "Français"),
]
LOCALE_PATHS = ["locale"]
MIDDLEWARE = [
"buraq.contrib.i18n.middleware.LocaleMiddleware",
# ... other middleware
]
urls.py
from buraq.urls import path, i18n_patterns
from buraq.contrib.i18n.views import set_language
import views
urlpatterns = [
path("/i18n/set_language", set_language), # language switcher endpoint
i18n_patterns(
path("/", views.home, name="home"),
path("/about", views.about, name="about"),
),
]
views.py
from buraq.shortcuts import render
from buraq.urls import reverse
from buraq.utils.translation import gettext as _, get_language, get_language_bidi, get_language_switch_urls
from buraq.urls import reverse
async def home(request):
return await render(request, "home.html", {
"title": _("Welcome to Buraq"),
"lang": get_language(),
"bidi": get_language_bidi(),
"lang_switcher": get_language_switch_urls(request),
"about_url": reverse("about"), # → "/ar/about" when lang=ar
})
templates/base.html
<!DOCTYPE html>
<html lang="{{ lang }}" {% if bidi %}dir="rtl"{% endif %}>
<head>
<title>{{ title }}</title>
</head>
<body>
<!-- Language switcher -->
<form method="post" action="/i18n/set_language">
<input type="hidden" name="next" value="{{ request.url.path }}">
<select name="language" onchange="this.form.submit()">
{% for l in lang_switcher %}
<option value="{{ l.code }}" {% if l.code == lang %}selected{% endif %}>
{{ l.name }}
</option>
{% endfor %}
</select>
</form>
{% block content %}{% endblock %}
</body>
</html>
Terminal window
# Extract strings → translate → compile
buraq makemessages -l ar -l fr
# edit locale/ar/LC_MESSAGES/django.po ...
buraq compilemessages

settings.py
USE_I18N = True
LANGUAGE_CODE = "en" # default language
LANGUAGES = [
("en", "English"),
("ar", "العربية"),
("fr", "Français"),
("es", "Español"),
]
LOCALE_PATHS = [
"locale", # relative to your project root
]
MIDDLEWARE = [
"buraq.contrib.i18n.middleware.LocaleMiddleware",
# ... other middleware
]
myproject/
├── locale/
│ ├── ar/
│ │ └── LC_MESSAGES/
│ ├── fr/
│ │ └── LC_MESSAGES/
│ └── es/
│ └── LC_MESSAGES/

Function Lazy variant Use for
gettext(msg) gettext_lazy(msg) Simple string translation
ngettext(singular, plural, n) ngettext_lazy(singular, plural, n) Pluralization
pgettext(ctx, msg) pgettext_lazy(ctx, msg) Context-disambiguated translation
npgettext(ctx, singular, plural, n) npgettext_lazy(ctx, singular, plural, n) Context + pluralization
gettext_noop(msg) Mark for extraction, translate later

Immediate functions translate at call time — use in views and functions. Lazy variants return a proxy that translates when converted to str() — use in class bodies (models, forms) where the active language isn’t known at import time.

from buraq.utils.translation import (
gettext as _,
gettext_lazy as _l,
ngettext,
ngettext_lazy,
pgettext,
pgettext_lazy,
npgettext,
npgettext_lazy,
)
# Simple
message = _("Welcome to Buraq")
# Lazy — evaluated when the string is rendered, not at import time
verbose_name = _l("Post")
# Pluralization
label = ngettext("%(count)d item", "%(count)d items", count) % {"count": count}
# Lazy pluralization — used in model Meta
verbose_name_plural = ngettext_lazy("%(count)d post", "%(count)d posts", count)
# Context disambiguation — same word, different meaning
action = pgettext("verb", "Post") # "Post" as in submit
noun = pgettext("noun", "Post") # "Post" as in blog post
# Lazy context translation — for class-level strings
label = pgettext_lazy("verb", "Post")
# Context + pluralization
msg = npgettext("basket", "%(n)d item", "%(n)d items", n) % {"n": n}
# Lazy context + pluralization
msg = npgettext_lazy("basket", "%(n)d item", "%(n)d items", n)
from buraq.utils.translation import gettext as _
from buraq.shortcuts import render
async def dashboard(request):
context = {
"title": _("Dashboard"),
"welcome": _("Welcome back!"),
}
return await render(request, "dashboard.html", context)
from buraq import models
from buraq.utils.translation import gettext_lazy as _l, ngettext_lazy
class Post(models.Model):
title = models.CharField(max_length=200)
content = models.TextField()
class Meta:
verbose_name = _l("Post")
verbose_name_plural = _l("Posts")
from buraq.forms import ModelForm, CharField
from buraq.utils.translation import gettext_lazy as _l
class PostForm(ModelForm):
title = CharField(label=_l("Title"), max_length=200)
class Meta:
model = Post
fields = ["title", "content"]

When USE_I18N = True, Buraq automatically registers these as Jinja2 globals — no need to pass them from every view:

Global What it is
_("string") Alias for gettext
gettext("string") Translate a string
ngettext(s, p, n) Pluralized translation
pgettext(ctx, s) Context-disambiguated translation
get_language() Returns the active language code
get_language_bidi() Returns True for RTL languages
<h1>{{ _("Dashboard") }}</h1>
<p>{{ ngettext("%(n)d result", "%(n)d results", count) % {"n": count} }}</p>
<html lang="{{ get_language() }}" {% if get_language_bidi() %}dir="rtl"{% endif %}>

Extract all marked strings into .po files:

Terminal window
# Single locale
buraq makemessages -l ar
# Multiple locales at once
buraq makemessages -l ar -l fr -l es

This creates or updates locale/<lang>/LC_MESSAGES/django.po for each locale.


Open the generated .po file and fill in the msgstr values:

locale/ar/LC_MESSAGES/django.po
msgid "Welcome to Buraq"
msgstr "مرحبًا بك في بُراق"
msgid "Dashboard"
msgstr "لوحة التحكم"
msgid "%(count)d item"
msgid_plural "%(count)d items"
msgstr[0] "%(count)d عنصر"
msgstr[1] "%(count)d عناصر"

Compile .po files into binary .mo files (fast dictionary lookup):

Terminal window
buraq compilemessages

This generates locale/<lang>/LC_MESSAGES/django.mo for every locale.


LocaleMiddleware detects the active language in this order:

  1. URL prefix/ar/about activates Arabic
  2. Cookieburaq_language=ar cookie
  3. Accept-Language header — browser’s preferred language
  4. LANGUAGE_CODE — fallback from settings
from buraq.shortcuts import redirect
async def set_language(request):
lang = (await request.form()).get("language", "en")
response = redirect(request.headers.get("referer", "/"))
response.set_cookie("buraq_language", lang, max_age=60 * 60 * 24 * 365)
return response

LocaleMiddleware automatically detects a language prefix in the URL (e.g. /ar/about → Arabic) and strips it before routing, so your views stay clean.

Use i18n_patterns() to declare which URL groups are language-prefixed. This tells Buraq to track those route names so reverse() can prepend the correct language prefix when generating links.

urls.py
from buraq.urls import path, i18n_patterns
from buraq.contrib.i18n.views import set_language
urlpatterns = [
# This route is NOT language-prefixed — always at /i18n/set_language
path("/i18n/set_language", set_language),
# Everything inside i18n_patterns() is served under /{lang}/
i18n_patterns(
path("/", views.home, name="home"),
path("/about", views.about, name="about"),
path("/posts", include("posts.urls")),
),
]

To serve the default language without a URL prefix (e.g. /about instead of /en/about), pass prefix_default_language=False. See URL configuration for details.

Routes inside i18n_patterns() are still registered at their plain paths — LocaleMiddleware handles prefix detection and stripping transparently:

URL visited Language activated Path the router sees
/about default (LANGUAGE_CODE) /about
/ar/about ar /about
/fr/about fr /about

reverse() — language-aware URL generation

Section titled “reverse() — language-aware URL generation”

For routes registered via i18n_patterns(), reverse() automatically prepends the active language prefix — no request needed, just like Django:

from buraq.urls import reverse
async def some_view(request):
# Active language: "ar"
link = reverse("post_detail", pk=42) # → "/ar/posts/42"
link = reverse("home") # → "/ar/"
# Active language: "en" (default) — no prefix added
link = reverse("about") # → "/about"

Redirects the user to the same page in a different language by rewriting the URL prefix:

# GET /i18n/set_language?language=ar&next=/about → redirect to /ar/about
# POST /i18n/set_language (form fields: language, next) → same

Pass lang_switcher from your view:

from buraq.utils.translation import get_language_switch_urls, get_language
from buraq.shortcuts import render
async def my_view(request):
context = {
"lang_switcher": get_language_switch_urls(request),
"current_language": get_language(),
}
return await render(request, "base.html", context)

Link-based switcher:

<ul class="lang-switcher">
{% for lang in lang_switcher %}
<li>
<a href="{{ lang.url }}"
{% if lang.code == current_language %}class="active"{% endif %}
{% if lang.get("bidi") %}dir="rtl"{% endif %}>
{{ lang.name }}
</a>
</li>
{% endfor %}
</ul>

Select/form-based switcher:

<form method="post" action="/i18n/set_language">
<input type="hidden" name="next" value="{{ request.url.path }}">
<select name="language" onchange="this.form.submit()">
{% for lang in lang_switcher %}
<option value="{{ lang.code }}"
{% if lang.code == current_language %}selected{% endif %}>
{{ lang.name }}
</option>
{% endfor %}
</select>
</form>

Use get_language_bidi() to detect right-to-left languages (Arabic, Hebrew, Persian, Urdu, etc.) and set dir="rtl" on your root element:

from buraq.utils.translation import get_language_bidi, get_language
async def my_view(request):
context = {
"lang": get_language(),
"bidi": get_language_bidi(),
}
return await render(request, "base.html", context)
<!DOCTYPE html>
<html lang="{{ lang }}" {% if bidi %}dir="rtl"{% endif %}>

Or check a specific language:

from buraq.utils.translation import get_language_bidi
get_language_bidi("ar") # True
get_language_bidi("fr") # False

override() — Temporarily Switch Language

Section titled “override() — Temporarily Switch Language”

Use the override context manager in tests, management commands, or background tasks:

from buraq.utils.translation import override, gettext as _
with override("ar"):
print(_("Hello")) # → "مرحبا"
# Works in async code too
async def send_email_in_user_language(user):
with override(user.language):
subject = _("You have a new message")
await send_email(user.email, subject)

Returns True if the given language code is in LANGUAGES:

from buraq.utils.translation import check_for_language
check_for_language("ar") # True
check_for_language("xx") # False

Converts a language code to locale format for use with locale directories:

from buraq.utils.translation import to_locale
to_locale("en") # → "en"
to_locale("en-us") # → "en_US"
to_locale("zh-hans") # → "zh_Hans"

Rewrites a URL path to a different language prefix:

from buraq.utils.translation import translate_url
translate_url("/about", "ar") # → "/ar/about"
translate_url("/ar/about", "fr") # → "/fr/about"
translate_url("/ar/about", "en") # → "/about" (en is default, no prefix)

get_supported_languages() / get_language_info_list()

Section titled “get_supported_languages() / get_language_info_list()”
from buraq.utils.translation import get_supported_languages, get_language_info_list
get_supported_languages()
# → ["en", "ar", "fr"]
get_language_info_list()
# → [{"code": "en", "name": "English"}, {"code": "ar", "name": "العربية"}, ...]

Disables translation entirely for the current async context — all translation calls return the original string unchanged. Useful in management commands or background tasks that should not translate output:

from buraq.utils.translation import deactivate_all, deactivate, gettext as _
token = deactivate_all()
_("Hello") # → "Hello" (untranslated)
deactivate(token)
_("Hello") # → "مرحبا" (translated again)

Setting Default Description
USE_I18N True Enable the i18n system
LANGUAGE_CODE "en" Default language
LANGUAGES (9 built-in) List of (code, name) tuples
LOCALE_PATHS [] Directories containing locale files
LANGUAGE_COOKIE_NAME "buraq_language" Cookie name for language preference
LANGUAGE_COOKIE_AGE 31536000 Cookie lifetime in seconds (1 year)

After running buraq makemessages and buraq compilemessages:

myproject/
├── locale/
│ ├── django.pot # master template (auto-generated)
│ ├── ar/
│ │ └── LC_MESSAGES/
│ │ ├── django.po # translate this
│ │ └── django.mo # compiled binary (auto-generated)
│ └── fr/
│ └── LC_MESSAGES/
│ ├── django.po
│ └── django.mo

To store translated field values (e.g. a title or content field with a separate value per language) in the database, see Translatable Models.