Software localization
FastAPI 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. In fact, we have covered quite a few tutorials on it in the past.
- Website i18n with Django REST Framework and django-parler
- Explore all i18n advantages of Babel for your Python app
- How to translate Python applications with the GNU gettext module
This tutorial will show you how to i18n your FastAPI web application easily using the following Python libraries:
- glob
- json
- fastapi
- uvicorn
- jinja2
- aiofiles
- babel
Let's start installing the necessary modules.
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
If you intend to pluralize your application, you can leverage on pluralization support in babel module
pip install babel
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> <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> <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 }}: 1</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. I have created two separate key-value pairs for bedroom and bedroom_plural. It will be used later on for pluralization.
{ "webpage_title": "MyRental", "for_rent": "For Rent", "details": "Details", "max_people": "Max people", "bathroom": "Bathrooms", "bedroom": "Bedroom", "bedroom_plural": "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", "bedroom_plural": "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 from babel.plural import PluralRule import glob import json
Initialization
After that, initialize the following variables.
app = FastAPI() app.mount("/static", StaticFiles(directory="static"), name="static") templates = Jinja2Templates(directory="templates") default_fallback = 'en' languages = {} plural_rule = PluralRule({'one': 'n in 0..1'}
plural_rule variable represents the PluralRule object from Babel. In this case, it will return:
- one – when the value is 0 or 1
- other – when the value is not tagged under one
At the time of this writing, Babel only supports the following tags:
- zero
- one
- two
- few
- many
- other
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 = lang.split('\\') lang_code = filename[1].split('.')[0] with open(lang, 'r', encoding='utf8') as file: languages[lang_code] = json.load(file)
Handling Plurals
For pluralization, you can combine the power of Babel's PluralRule and Jinja2's filters. Continue to append the following code inside myapp_rental.py file.
# custom filters for Jinja2 def plural_formatting(key_value, input, locale): key = '' for i in languages[locale]: if(key_value == languages[locale][i]): key = i break if not key: return key_value plural_key = f"{key}_plural" if(plural_rule(input) != 'one' and plural_key in languages[locale]): key = plural_key return languages[locale][key]
It will look for the respective plural key from languages dictionary and return the corresponding value. Next, you need to assign the filter to Jinja's Environment.
# assign filter to Jinja2 templates.env.filters['plural_formatting'] = plural_formatting
Besides that, the development version of Jinja2 also comes with support for i18n as an extension. You can use this instead of Babel. Enable it via:
translations = get_gettext_translations() env = jinja2.Environment(extensions=["jinja2.ext.i18n"]) env.install_gettext_translations(translations)
get_gettext_translations represents the custom function which returns a translator for the current locale. For example, you can use the find function from gettext module (gettext.find).
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.
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/{locale}", response_class=HTMLResponse) async def rental(request: Request, locale: str): if(locale not in languages): locale = default_fallback result = {"request": request} result.update(languages[locale]) result.update({'locale': locale, 'bedroom_value': 2}) return templates.TemplateResponse("index.html", result)
Handling Dynamic Values
For other 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 = {"request": request} result.update(languages[language]) result.update({'locale': locale, 'bedroom_value': 2}) result.update({'rent_per_month': 300}) # dynamic values ...
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.
Run and Test
Save the file and run the following command to start your FastAPI server:
uvicorn myapp_rental:app
Head over to the following URL:
http://localhost:8000/rental/en
You should see the following user interface:
For German internationalization, go to the following URL:
http://localhost:8000/rental/de
The user interface should look like this:
Conclusion
A round of applause for completing this i18n tutorial! By now, you should have a working multilingual rental web application. Now, if you want to streamline your i18n process, sign up for Phrase, the most reliable localization and translation management platform that comes with a 14-day free trial.
Phrase will equip you with everything you need to:
- Build production-ready integrations with your development workflow,
- Invite as many users as you wish to collaborate on your projects,
- Edit and convert localization files online with more context for higher translation quality
Ready for a deep dive into different topics surrounding APIs? Here are some cool guides we've put together for you: