FastAPI Jinja2 Translation
This tutorial is somewhat of a followup to another tutorial that does the same thing, except it doesn’t specify how to use it with Jinja templates.
Setup
This tutorial assumes you already have a FastAPI project and you just want to add translation to it. For the sake of this tutorial I will be making a new minimal project.
First we need to create a virtual environment to install the packages, do this using python -m venv .venv && source .venv/bin/activate
. Next, make a new requirements.txt
file in the root of the project and add these packages:
fastapi
jinja2 # templaes
starlette # sessions stuff
itsdangerous # required by starlette, I think?
babel # translation tools
uvicorn # running the project
And run pip install -r requirements.txt
.
Now that we have our dependencies installed, we can setup our app, our templates, and the translation folders, your project should looks something like this.
.
├── app
│ ├── i18n.py
│ ├── main.py
│ └── middleware.py
├── babel.cfg
├── requirements.txt
├── templates
│ └── home.html
└── translations
i18n
First, let’s setup the app/i18n.py
file. Here we will define our main TranslationWrapper
class, the set_locale
function, and the _
function, which wraps the gettext
function, for convenience.
import gettext
from pathlib import Path
from fastapi import Request
class TranslationWrapper:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
cls._instance.init_translation()
return cls._instance
def init_translation(self):
lang = "en" # Default language
locales_dir = Path(__file__).parent.parent / "translations"
self.translations = gettext.translation(
"messages",
localedir=locales_dir,
languages=[lang],
fallback=True
)
self.translations.install()
def gettext(self, message: str) -> str:
return self.translations.gettext(message)
async def set_locale(request: Request, lang: str = "en"):
translation_wrapper = TranslationWrapper()
locales_dir = Path(__file__).parent.parent / "translations"
print(f"Setting language to: {lang}")
translation_wrapper.translations = gettext.translation(
"messages", localedir=locales_dir, languages=[lang], fallback=True
)
translation_wrapper.translations.install()
def _(message: str) -> str:
translation_wrapper = TranslationWrapper()
return translation_wrapper.gettext(message)
Notice how I do locales_dir = Path(__file__).parent.parent / "translations"
, using .parent
twice, this is because my file is inside the app
directory, if it was in the same folder as the translations folder you would just do .parent
once.
The Middleware
Next is setting up our custom middleware that ensures that:
- The appropriate language is set for each request based on session data or browser preferences.
- The translation function (
_
) and current language are available in all Jinja2 templates.
Open your app/middleware.py
file and add the following:
from fastapi import Request
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi.templating import Jinja2Templates
from .i18n import set_locale, _
class LanguageMiddleware(BaseHTTPMiddleware):
def __init__(self, app, templates: Jinja2Templates):
super().__init__(app)
self.templates = templates
async def dispatch(self, request: Request, call_next):
lang = request.session.get('language') or request.headers.get("Accept-Language", "en")
await set_locale(request, lang)
self.templates.env.globals['_'] = _
self.templates.env.globals['lang'] = lang
response = await call_next(request)
return response
The Main File
Obviously this will depend on your project, but here I will provide an example app/main.py
with a home route and a set-lang
function to set the language for the session.
from fastapi import FastAPI, Request, HTTPException, Response
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from starlette.middleware.sessions import SessionMiddleware
from .middleware import LanguageMiddleware
from .i18n import _
app = FastAPI()
templates = Jinja2Templates(directory="templates")
app.add_middleware(LanguageMiddleware, templates=templates)
app.add_middleware(SessionMiddleware, secret_key="your-totally-secret-key")
@app.get("/", response_class=HTMLResponse)
async def home(request: Request):
user = request.session.get('user')
return templates.TemplateResponse("home.html", {"request": request})
@app.post("/set-lang/{lang}")
async def set_language(lang: str, request: Request, response: Response):
supported_langs = ['en', 'ar']
if lang not in supported_langs:
raise HTTPException(status_code=400, detail=f"Invalid language. Only {', '.join(supported_langs)} are supported.")
request.session['language'] = lang
referer = request.headers.get('Referer')
response = RedirectResponse(referer or "/")
return response
Make sure you change the supported_langs
variable.
Example Template
We setup the middleware in a way that in all template files you have access to two things, the _
function, in which you will wrap pieces of text you want to translate, and the lang
variable which will tell you what language is currently set by the user, this will be changeable with our /set-lang
route.
<!doctype html>
<html>
<head>
<title>Test App</title>
</head>
<body class="p-0">
<h1>{{ _("Hello") }}</h1>
{% if lang == 'ar' %}
<a href="#" onclick="setLanguage('en')">English</a>
{% else %}
<a href="#" onclick="setLanguage('ar')">Arabic</a>
{% endif %}
</body>
<script>
function setLanguage(lang) {
fetch("/set-lang/" + lang, {
method: "POST",
}).then(() => {
location.reload();
});
}
</script>
</html>
Translation
Finally, we can translate our app. First, we need to tell babel where to look for things to translate. Open babel.cfg
and add the following:
[python: **.py, **/**.py]
[jinja2: **/templates/**.html]
keywords = _
I don’t think this needs explaining.
Now, we will generate our messages.pot
, which will hold all of our pieces of text that we want to translate, this will look in all html files in the templates folder and find anything wrapped in the _
function. To generate it we run:
pybabel extract -F babel.cfg -o translations/messages.pot .
Next, we will initialize a translation folder for the languages we want, let’s do Arabic for example.
Warning: doing this will overwrite any existing translation files for the specified language
pybabel init -i translations/messages.pot -d translations -l ar
You should have a file called translations/ar/LC_MESSAGES/messages.po
, in this file you will write your translations for the language. For example, here is the translation for the home.html
page:
# metadata stuff...
#: templates/home.html:7
msgid "Hello"
msgstr "مرحبا"
And finally, before running your application you have to compile the .po
file, as follows:
pybabel compile -d translations
Enjoy
Everything should be done now, you can run your applications from it’s root directory using:
uvicorn app.main:app --reload --host localhost --port 8000
And that should be it, what is suggest you do is making a run.sh
file that compiles the translations and then runs the app to avoid having to remember doing that every time.
If you found any problem in this please contact me, info in the about page.