FastAPI language translations I18n Step by Step
Since the beginning of 2020, FastAPI has been gaining momentum as one of the potential alternatives to Flask server for RESTful API. If you are an existing FastAPI user, you should be aware that it does not come with built-in internationalization, and that will likely not change soon, because internationalization strategies are application-dependent.
The following tutorial focuses on server-side i18n .
This tutorial will show you how to i18n your FastAPI web application easily using the following Python libraries:
- blob
- json
- fastapi
- uvicorn
- jinja2
- aiofiles
Setup
Before you get it started, feel free to check out our GitHub repository for the complete code used in this tutorial. It is highly recommended to create a virtual environment before you continue with the installation.
Python modules
First and foremost, run the following command in your terminal to install FastAPI.
pip install fastapi
You need to have an ASGI server to serve your application. The primary ASGI server for FastAPI is uvicorn. Run the following command to install the module:
pip install uvicorn
I am going to use the Jinja2 template engine for serving HTML files. For your information, Flask uses the same Jinja2 template engine. You can easily install it via the following command:
pip install jinja2
Furthermore, if your application serves any form of static files, you need to install the following modules:
pip install aiofiles
HTML Template
To keep things simple, this tutorial will use a stripped-down version of an existing apartment rental HTML template . It is made by W3.CSS and looks like this:
The final HTML code for the modified version is as follows. Create a new folder called templates and saved the code as index.html inside the folder. An interpolated string is defined using double curly brackets inside the HTML file. For example, you can define a variable called count inside the HTML as follows:
You received {{ count }} notifications.
Let’s say count is assigned to three. The interpolated string will be resolved as:
You received three notifications.
In this case, you should use the name of the unique keys in your language files.
<!DOCTYPE html>
<html>
<head>
<title>{{ webpage_title }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Raleway">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<style>
body,
h1,
h2,
h3,
h4,
h5,
h6 {
font-family: "Raleway", Arial, Helvetica, sans-serif
}
.mySlides {
display: none
}
</style>
</head>
<body class="w3-border-left w3-border-right">
<!-- Top menu on small screens -->
<header class="w3-bar w3-top w3-black w3-xlarge">
<span class="w3-bar-item">{{ for_rent }}</span>
</header>
<!-- !PAGE CONTENT! -->
<div class="w3-main w3-white">
<img src="{{ url_for('static', path='/diningroom.jpg') }}" style="width:100%;margin-bottom:-6px">
<div class="w3-container">
<h4><strong>{{ details }}</strong></h4>
<div class="w3-row w3-large">
<div class="w3-col s6">
<p><i class="fa fa-fw fa-male"></i> {{ max_people }}: 4</p>
<p><i class="fa fa-fw fa-bath"></i> {{ bathroom }}: 2</p>
<p><i class="fa fa-fw fa-bed"></i> {{ bedroom | plural_formatting(bedroom_value, locale) }}: {{
bedroom_value }}</p>
</div>
<div class="w3-col s6">
<p><i class="fa fa-fw fa-clock-o"></i> {{ check_in }}</p>
<p><i class="fa fa-fw fa-clock-o"></i> {{ check_out }}</p>
</div>
</div>
<hr>
<h4><strong>{{ amenities }}</strong></h4>
<div class="w3-row w3-large">
<div class="w3-col s6">
<p><i class="fa fa-fw fa-shower"></i> {{ shower }}</p>
<p><i class="fa fa-fw fa-wifi"></i> {{ wifi }}</p>
<p><i class="fa fa-fw fa-tv"></i> {{ television }}</p>
</div>
<div class="w3-col s6">
<p><i class="fa fa-fw fa-cutlery"></i> {{ kitchen }}</p>
<p><i class="fa fa-fw fa-thermometer"></i> {{ heating }}</p>
<p><i class="fa fa-fw fa-wheelchair"></i> {{ accessible }}</p>
</div>
</div>
<hr>
<h4><strong>{{ extra_info_title }}</strong></h4>
<p>{{ extra_info_text }}</p>
<p>{{ we_accept }}: <i class="fa fa-credit-card w3-large"></i> <i class="fa fa-cc-mastercard w3-large"></i>
<i class="fa fa-cc-amex w3-large"></i> <i class="fa fa-cc-cc-visa w3-large"></i><i
class="fa fa-cc-paypal w3-large"></i></p>
<hr>
</div>
<footer class="w3-container w3-padding-16" style="margin-top:32px">Powered by <a
href="https://www.w3schools.com/w3css/default.asp" title="W3.CSS" target="_blank"
class="w3-hover-text-green">w3.css</a></footer>
<!-- End page content -->
</div>
</body>
</html>
Language Files
The language files are based on a simple JSON format . Create a new languages folder. Inside the folder, make two new json files. The first one should be called en.json, representing the language file for English.
{
"webpage_title": "MyRental",
"for_rent": "For Rent",
"details": "Details",
"max_people": "Max people",
"bathroom": "Bathrooms",
"bedroom": "Bedrooms",
"check_in": "Check In: After 3PM",
"check_out": "Check Out: 12PM",
"amenities": "Amenities",
"shower": "Shower",
"wifi": "WiFi",
"television": "TV",
"kitchen": "Kitchen",
"heating": "Heating",
"accessible": "Accessible",
"extra_info_title": "Extra Info",
"extra_info_text": "Our apartment is really clean and we like to keep it that way. Enjoy the beautiful scenery around the building.",
"we_accept": "We accept"
}
The second file is called de.json and as the name implies, it contains the German translation of your application.
{
"webpage_title": "MyRental",
"for_rent": "Zu vermieten",
"details": "Einzelheiten",
"max_people": "Max Personen",
"bathroom": "Badezimmer",
"bedroom": "Schlafzimmer",
"check_in": "Check-in: Nach 15:00 Uhr",
"check_out": "Auschecken: 12 Uhr",
"amenities": "Ausstattung",
"shower": "Dusche",
"wifi": "WiFi",
"television": "TV",
"kitchen": "Küche",
"heating": "Heizung",
"accessible": "Zugänglich",
"extra_info_title": "Zusatzinformation",
"extra_info_text": "Unsere Wohnung ist sehr sauber und wir halten es gerne so. Genießen Sie die wunderschöne Landschaft rund um das Gebäude.",
"we_accept": "Wir akzeptieren"
}
Implementation
Import
Create a new file, and name it myapp_rental.py. Then, add the following import declarations at the top of your Python file:
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
import glob
import json
import os.path
Initialization
After that, initialize the following variables.
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
app_language = 'en'
languages = {}
Load Language Data
Instead of loading your language files manually one by one, we are going to utilize both the glob and json modules. In our use case, glob will search the languages folder and return a list of all the json files. Then, we are going to load it as json. The languages variable is a dictionary that holds all the loaded data.
language_list = glob.glob("languages/*.json")
for lang in language_list:
filename = os.path.basename(lang)
lang_code, ext = os.path.splitext(filename)
with open(lang, 'r', encoding='utf8') as file:
languages[lang_code] = json.load(file)
GET Route
The last step is to implement a GET operation. It should have a Path parameter called language. Our application will use this parameter as the language determiner. You should have a conditional check to determine if the language is valid or supported by your application. The route will return an HTMLResponse and a dictionary that holds the corresponding data.
@app.get("/rental/{language}", response_class=HTMLResponse)
async def rental(request: Request, language: str):
if(language not in languages):
language = app_language
result = {"request": request}
result.update(languages[language])
return templates.TemplateResponse("index.html", result)
Handling Dynamic Values
For dynamic values, you should update it manually to the result dictionary. For example, you have a variable called rent_per_month
, updated from time to time. Simply append it to the result variable as follows:
result.update(rent_per_month) = "89.9"
You can define it normally inside double curly brackets in your HTML file.
<p>Rent per month: {{ rent_per_month }} USD</p>
When the page is refreshed, the value will be updated automatically.
Handling Plurals
The development version of Jinja2 comes with support for i18n as an extension. You need to enable it via:
translations = get_gettext_translations()
env = jinja2.Environment(extensions=["jinja2.ext.i18n"])
env.install_gettext_translations(translations)
Afterwards, you can mark a sentence in your HTML file as translatable:
{% trans %}There are {{ bathroom_count }} bathrooms in the apartment.{% endtrans %}
Singular and plural forms should be separated using the pluralize tag:
{% trans count=list|length %}
There is {{ bathroom_count }} bathroom.
{% pluralize %}
There are {{ bathroom_count }} bathrooms.
{% endtrans %}
In fact, you can even use it together with other Python i18n libraries, such as gettext or Babel.
Run and Test
Save the file and run the following command to start your FastAPI server:
uvicorn myapp_rental:app --host="0.0.0.0" --port="8000"
Head over to the following URL:
http://localhost:8000/rental/en
You should see the following user interface: