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.
USE_I18N = TrueLANGUAGE_CODE = "en"LANGUAGES = [ ("en", "English"), ("ar", "العربية"), ("fr", "Français"),]LOCALE_PATHS = ["locale"]MIDDLEWARE = [ "buraq.contrib.i18n.middleware.LocaleMiddleware", # ... other middleware]from buraq.urls import path, i18n_patternsfrom buraq.contrib.i18n.views import set_languageimport views
urlpatterns = [ path("/i18n/set_language", set_language), # language switcher endpoint i18n_patterns( path("/", views.home, name="home"), path("/about", views.about, name="about"), ),]from buraq.shortcuts import renderfrom buraq.urls import reversefrom buraq.utils.translation import gettext as _, get_language, get_language_bidi, get_language_switch_urlsfrom 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 })<!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># Extract strings → translate → compileburaq makemessages -l ar -l fr# edit locale/ar/LC_MESSAGES/django.po ...buraq compilemessages1. Enable i18n in settings
Section titled “1. Enable i18n in settings”USE_I18N = TrueLANGUAGE_CODE = "en" # default language
LANGUAGES = [ ("en", "English"), ("ar", "العربية"), ("fr", "Français"), ("es", "Español"),]
LOCALE_PATHS = [ "locale", # relative to your project root]2. Add LocaleMiddleware
Section titled “2. Add LocaleMiddleware”MIDDLEWARE = [ "buraq.contrib.i18n.middleware.LocaleMiddleware", # ... other middleware]3. Create locale directory
Section titled “3. Create locale directory”myproject/├── locale/│ ├── ar/│ │ └── LC_MESSAGES/│ ├── fr/│ │ └── LC_MESSAGES/│ └── es/│ └── LC_MESSAGES/Marking Strings for Translation
Section titled “Marking Strings for Translation”Full function reference
Section titled “Full function reference”| 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,)
# Simplemessage = _("Welcome to Buraq")
# Lazy — evaluated when the string is rendered, not at import timeverbose_name = _l("Post")
# Pluralizationlabel = ngettext("%(count)d item", "%(count)d items", count) % {"count": count}
# Lazy pluralization — used in model Metaverbose_name_plural = ngettext_lazy("%(count)d post", "%(count)d posts", count)
# Context disambiguation — same word, different meaningaction = pgettext("verb", "Post") # "Post" as in submitnoun = pgettext("noun", "Post") # "Post" as in blog post
# Lazy context translation — for class-level stringslabel = pgettext_lazy("verb", "Post")
# Context + pluralizationmsg = npgettext("basket", "%(n)d item", "%(n)d items", n) % {"n": n}
# Lazy context + pluralizationmsg = npgettext_lazy("basket", "%(n)d item", "%(n)d items", n)In views
Section titled “In views”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)In models
Section titled “In models”from buraq import modelsfrom 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")In forms
Section titled “In forms”from buraq.forms import ModelForm, CharFieldfrom 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"]In templates (Jinja2)
Section titled “In templates (Jinja2)”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 %}>Extracting Messages
Section titled “Extracting Messages”Extract all marked strings into .po files:
# Single localeburaq makemessages -l ar
# Multiple locales at onceburaq makemessages -l ar -l fr -l esThis creates or updates locale/<lang>/LC_MESSAGES/django.po for each locale.
Translating
Section titled “Translating”Open the generated .po file and fill in the msgstr values:
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 عناصر"Compiling Messages
Section titled “Compiling Messages”Compile .po files into binary .mo files (fast dictionary lookup):
buraq compilemessagesThis generates locale/<lang>/LC_MESSAGES/django.mo for every locale.
Language Detection
Section titled “Language Detection”LocaleMiddleware detects the active language in this order:
- URL prefix —
/ar/aboutactivates Arabic - Cookie —
buraq_language=arcookie Accept-Languageheader — browser’s preferred languageLANGUAGE_CODE— fallback from settings
Switching language via cookie
Section titled “Switching language via cookie”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 responseURL-Based Language Switcher
Section titled “URL-Based Language Switcher”LocaleMiddleware automatically detects a language prefix in the URL (e.g. /ar/about → Arabic) and strips it before routing, so your views stay clean.
i18n_patterns()
Section titled “i18n_patterns()”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.
from buraq.urls import path, i18n_patternsfrom 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"set_language view
Section titled “set_language view”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) → sameSwitcher in templates
Section titled “Switcher in templates”Pass lang_switcher from your view:
from buraq.utils.translation import get_language_switch_urls, get_languagefrom 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>RTL Support
Section titled “RTL Support”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") # Trueget_language_bidi("fr") # Falseoverride() — 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 tooasync def send_email_in_user_language(user): with override(user.language): subject = _("You have a new message") await send_email(user.email, subject)Utility Functions
Section titled “Utility Functions”check_for_language()
Section titled “check_for_language()”Returns True if the given language code is in LANGUAGES:
from buraq.utils.translation import check_for_language
check_for_language("ar") # Truecheck_for_language("xx") # Falseto_locale()
Section titled “to_locale()”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"translate_url()
Section titled “translate_url()”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": "العربية"}, ...]deactivate_all()
Section titled “deactivate_all()”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)Settings Reference
Section titled “Settings Reference”| 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) |
File Structure
Section titled “File Structure”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.moTranslatable model fields
Section titled “Translatable model fields”To store translated field values (e.g. a title or content field with a separate value per language) in the database, see Translatable Models.