Software localization

FastAPI I18n Step by Step

Looking for ways to internationalize your FastAPI application using Python libraries? Our step-by-step FastAPI i18n tutorial can help!
Software localization blog category featured image | Phrase

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.

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:

HTML demo app | Phrase

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:

English demo app | Phrase

For German internationalization, go to the following URL:

http://localhost:8000/rental/de

The user interface should look like this:

German localized demo app | Phrase

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: