Internationalization (FastAPI i18n) is essential when developing applications for a global audience. Whether you're building an API for a multilingual website or supporting multiple locales in a SaaS product, handling translations efficiently ensures a seamless user experience.
This guide provides a practical, step-by-step approach to adding i18n support to your FastAPI app. No overcomplicated theory—just a working solution you can implement immediately.
By the end, you’ll have a fully localized FastAPI application that can handle multiple languages, pluralization, localized time, datetime formatting, currency localization, and user language preferences.
This guide assumes you're familiar with Python and FastAPI basics. You don’t need to be an expert, but you should know how to create a FastAPI app and run it.
We'll use Poetry for dependency management. You should also have Python 3.9+ installed.
poetry run uvicorn fastapi_i18n_demo.main:app --reload
Open your browser at 127.0.0.1:8000, and you should see:
{"message": "Hello, world!"}
Great!
Adding simple translations
To implement fastapi i18n, we need a way to translate text dynamically based on the user’s language. FastAPI doesn’t have built-in i18n support, so we’ll use Babel, a common Python library for managing translations.
Installing Babel
First, install Babel with Poetry by running the following command:
poetry add Babel
This will help us handle translations properly as we expand our app.
Setting up translation files
We’ll translate JSON files, storing one per language. Inside your project, create a locales folder:
mkdir -p locales/en locales/fr
Now, inside each language folder, create a messages.json file. These will store key-value pairs for our translations.
English translation file (locales/en/messages.json)
{ "greeting": "Hello, world!"}
French translation file (locales/fr/messages.json)
{ "greeting": "Bonjour, le monde!"}
We'll use literal translations in this tutorial. This way, our app can load and return the correct message strings based on the selected language.
Updating the FastAPI app to support translations
Now, modify main.py to load these translations and use them dynamically:
from fastapi import FastAPI, Queryimport jsonimport osapp = FastAPI()# Load translationstranslations = {}for lang in ["en", "fr"]: path = os.path.join("locales", lang, "messages.json") with open(path, encoding="utf-8") as f: translations[lang] = json.load(f)@app.get("/")def read_root(lang: str = Query("en", title="Language", description="Language code (en or fr)")): """ A simple endpoint that returns a localized greeting message. """ return {"message": translations.get(lang, translations["en"])["greeting"]}
The app loads all translations from the locales/ directory at startup.
The lang query parameter (?lang=en or ?lang=fr) determines which translation to use.
If an unsupported language is provided, it defaults to English.
Running and testing
Start the app:
poetry run uvicorn fastapi_i18n_demo.main:app --reload
French:http://127.0.0.1:8000/?lang=fr → {"message": "Bonjour, le monde!"}
Invalid language (defaults to English): http://127.0.0.1:8000/?lang=de → {"message": "Hello, world!"}
This is a basic yet functional FastAPI i18n setup.
Making the FastAPI i18n setup more robust
Right now, our fastapi i18n setup has some hardcoded values inside main.py. Specifically, the list of supported languages and the default locale are defined directly in the file. This isn't ideal because:
Configuration should be centralized – Keeping settings inside the main app file makes the code harder to manage as the project grows.
Easier updates – If we need to change supported languages or the default locale, we don’t want to modify the application logic.
Environment-based flexibility – If we want different settings for development and production, having a separate config system helps.
To solve this, we'll use Pydantic’s BaseSettings to create a proper configuration file.
Step 1: Install Pydantic
We'll need to install Pydantic-settings first:
poetry add pydantic-settings
Step 2: Create a config.py file
Inside fastapi_i18n_demo/, create a new file called config.py. This will store our fastapi i18n settings.
touch fastapi_i18n_demo/config.py
Now, open config.py and define the configuration class:
from pydantic_settings import BaseSettingsfrom typing import Listclass Settings(BaseSettings): """ Configuration settings for the FastAPI i18n setup. """ supported_locales: List[str] = ["en", "fr"] default_locale: str = "en" class Config: env_prefix = "APP_" # Prefix for environment variables# Create a settings instancesettings = Settings()
supported_locales lists all available languages.
default_locale defines the fallback language.
BaseSettings allows loading values from environment variables (useful in real-world deployment).
env_prefix = "APP_" means we can override these settings using environment variables like APP_SUPPORTED_LOCALES or APP_DEFAULT_LOCALE.
Step 3: Use the config in main.py
Now, update main.py to load the settings from config.py:
from fastapi import FastAPI, Queryimport jsonimport osfrom fastapi_i18n_demo.config import settingsapp = FastAPI()# Load translations dynamically based on config settingstranslations = {}for lang in settings.supported_locales: path = os.path.join("locales", lang, "messages.json") with open(path, encoding="utf-8") as f: translations[lang] = json.load(f)@app.get("/")def read_root( lang: str = Query( settings.default_locale, title="Language", description="Language code" )): """ A simple endpoint that returns a localized greeting message. """ return { "message": translations.get(lang, translations[settings.default_locale])[ "greeting" ] }
No more hardcoded locales – We load the list from config.py.
Cleaner and scalable – If we want to add new languages, we modify one file instead of searching through the code.
Supports environment variables – If we deploy this app in different environments, we can set values dynamically.
Step 4 (optional): Override settings with environment variables
By default, our app uses en and fr, but we can override this without changing the code.
Run the app with a different default locale:
APP_DEFAULT_LOCALE=fr poetry run uvicorn fastapi_i18n_demo.main:app --reload
Now, when you access the root endpoint without specifying ?lang=, it will default to French.
Handling pluralization in FastAPI i18n
So far, we’ve translated simple static texts, but real-world apps often need pluralization—handling different word forms depending on a number.
For example, we want to return:
"You have 0 new messages"
"You have 1 new message"
"You have 5 new messages"
This depends on the count parameter, which we’ll pass as a GET query parameter.
Step 1: Update translation files
Plural rules differ between languages. English has simple singular/plural forms, while some languages have more complex rules (like Russian or Arabic).
We’ll store pluralization patterns in messages.json using a {count} placeholder.
English (locales/en/messages.json)
{ "messages": { "one": "You have {count} new message", "other": "You have {count} new messages" }}
Update your main.py file with the following changes:
from fastapi import FastAPI, Queryfrom babel.core import Localeimport jsonimport osfrom fastapi_i18n_demo.config import settings# initial loadingdef get_plural_form(lang: str, count: int) -> str: """ Determines the correct pluralization form using Babel's Locale class. """ try: locale = Locale.parse(lang) # Load locale-specific plural rules plural_form = locale.plural_form(count) # Get the correct plural category return plural_form # Expected values: one, two, few, many, other except Exception: return "other" # Fallback to 'other' if something goes wrong# root endpoint here...@app.get("/messages")def get_messages( lang: str = Query( settings.default_locale, title="Language", description="Language code" ), count: int = Query(1, title="Count", description="Number of messages"),): """ Returns a localized message with correct pluralization. """ plural_form = get_plural_form(lang, count) messages_dict = translations.get(lang, translations[settings.default_locale])[ "messages" ] # Ensure we default to "other" if no exact match message_template = messages_dict.get(plural_form, messages_dict["other"]) return {"message": message_template.format(count=count)}
get_plural_form(lang, count)
Uses Babel’s Localeclass to determine the correct pluralization category (don’t forget to import it!)
Calls locale.plural_form(count) , which returns “one”, “other”, or other plural categories based on CLDR rules.
If an error occurs, it defaults to “other” to prevent crashes.
/messages endpoint
Accepts lang (language code) and count (number of messages).
Calls get_plural_form() to get the correct pluralization category.
Retrieves the correct translation from the JSON files based on the plural key (one, other, etc.).
Defaults to "other" if no match is found.
Brilliant, now our plurals are good to go!
Adding support for RTL (right-to-left) languages in FastAPI i18n
Some languages, like Hebrew (he), Arabic (ar), and Persian (fa), use right-to-left (RTL) writing. When adding a new language like Hebrew, we need to ensure that:
Translations are correctly stored in RTL format.
The FastAPI app handles RTL text properly.
Text direction is considered in responses if needed.
Step 1: Add Hebrew translations
Create a new folder for Hebrew translations:
mkdir -p locales/he
Then, create a messages.json file inside locales/he/ with proper Hebrew translations:
In Hebrew, the structure of the sentence is different, and {count} should remain in place so that FastAPI can replace it dynamically.
Step 2: Update config.py to support Hebrew
Modify settings inside config.py:
from pydantic_settings import BaseSettingsfrom typing import Listclass Settings(BaseSettings): """ Configuration settings for the FastAPI i18n setup. """ supported_locales: List[str] = ["en", "fr", "he"] # Added "he" default_locale: str = "en" class Config: env_prefix = "APP_"# Create a settings instancesettings = Settings()
This ensures that Hebrew is recognized as a valid language.
Step 3: Test the Hebrew translation
Now, restart the FastAPI app:
poetry run uvicorn fastapi_i18n_demo.main:app --reload
Test Hebrew responses, for example:
curl "http://127.0.0.1:8000/?lang=he"
Should give you:
{"message": "שלום, עולם!"}
Step 4: Handling RTL in responses (Optional)
By default, FastAPI does not modify text direction, but if we want to explicitly mark RTL text, we can include a direction indicator in our responses.
Modify read_root and get_messages to return text direction along with translations:
def get_text_direction(lang: str) -> str: """ Determines if a language is Right-to-Left (RTL) or Left-to-Right (LTR). """ rtl_languages = {"ar", "he", "fa", "ur"} # Common RTL languages return "rtl" if lang in rtl_languages else "ltr"@app.get("/")def read_root( lang: str = Query( settings.default_locale, title="Language", description="Language code" )): """ A simple endpoint that returns a localized greeting message. """ return { "message": translations.get(lang, translations[settings.default_locale])[ "greeting" ], "direction": get_text_direction(lang), # <===== }@app.get("/messages")def get_messages( lang: str = Query( settings.default_locale, title="Language", description="Language code" ), count: int = Query(1, title="Count", description="Number of messages"),): """ Returns a localized message with correct pluralization. """ plural_form = get_plural_form(lang, count) messages_dict = translations.get(lang, translations[settings.default_locale])[ "messages" ] # Ensure we default to "other" if no exact match message_template = messages_dict.get(plural_form, messages_dict["other"]) return { "message": message_template.format(count=count), "direction": get_text_direction(lang), # <===== }
This is it!
Adding datetime localization in FastAPI i18n
When working with international users, it’s important to display dates and times in the correct format for each locale. Different languages and regions use different date formats (e.g., MM/DD/YYYY in the US vs. DD/MM/YYYY in Europe).
To handle this, we’ll use Babel’s datetime formatting to properly localize dates.
Step 1: Create a localize_datetime function
Modify main.py to include a new function for localizing datetime values:
from fastapi import FastAPI, Queryfrom babel.core import Localefrom babel.dates import format_datetimefrom datetime import datetimeimport jsonimport osfrom fastapi_i18n_demo.config import settingsdef localize_datetime(lang: str) -> str: """ Returns the current datetime formatted according to the given locale. """ try: locale = Locale.parse(lang) # Parse locale now = datetime.now() # Get current time localized_time = format_datetime(now, locale=locale) # Format datetime return localized_time except Exception: return str( datetime.now() ) # Fallback: Return raw datetime if localization fails
localize_datetime(lang: str) -> str
Uses Babel’s Localeclass to parse the requested language.
Calls datetime.now() to get the current system time.
Uses format_datetime(now, locale=locale) to properly format the datetime based on the locale.
If something goes wrong (e.g., an unsupported language), it falls back to returning raw datetime as a string.
Step 2: Create a /datetime endpoint
Now, we add an endpoint to return the localized current datetime.
@app.get("/datetime")def get_localized_datetime( lang: str = Query( settings.default_locale, title="Language", description="Language code" ),): """ Returns the current datetime formatted according to the given locale. """ return {"datetime": localize_datetime(lang)}
Defines the /datetime endpoint, which returns the current time formatted based on the requested language.
Reads the lang query parameter, defaulting to the app’s configured locale.
Calls localize_datetime(lang) to format the datetime correctly for the given locale.
Returns a JSON response with the localized datetime string.
Formatting currencies in FastAPI i18n
Different regions display currencies in different formats. For example:
US Dollars:$1,234.56
French Euros:1 234,56 €
Japanese Yen:¥1,234
To ensure correct formatting, we use Babel’s currency formatting utilities.
Step 1: Create a function to format currencies
Modify main.py to include currency localization:
from babel.numbers import format_currencyDEFAULT_CURRENCIES = { "en": "USD", "fr": "EUR", "he": "ILS",}def localize_currency( amount: float, currency_code: str | None, lang: str, currency_digits: bool = True, format_type: str = "standard",) -> str: """ Formats a given amount into the correct currency format for a given locale. If no currency code is provided, a default is selected based on the locale. """ try: locale = lang # Babel can handle short locale codes like "en", "fr" # If no currency is provided, use the default for the given language if not currency_code: currency_code = DEFAULT_CURRENCIES.get(lang[:2], "USD") return format_currency( amount, currency_code, locale=locale, currency_digits=currency_digits, format_type=format_type, ) except Exception: return f"{amount} {currency_code or 'USD'}" # Fallback if localization fails
Uses Babel's format_currency() to format monetary values correctly based on the given locale.
DEFAULT_CURRENCIES dictionary provides a default currency for each language if none is specified.
Automatically selects a default currency by checking the first two letters of the locale (e.g., "fr" → "EUR").
Supports optional parameters:
currency_digits: Controls whether to show decimal places (useful for JPY).
format_type: Allows different currency display formats (e.g., standard, accounting).
Handles errors gracefully, falling back to a simple {amount} {currency} format if localization fails.
Step 2: Add a new endpoint for currency localization
Let's add a new endpoint now:
@app.get("/currency")def get_localized_currency( lang: str = Query( settings.default_locale, title="Language", description="Language code" ), amount: float = Query(1000.50, title="Amount", description="Monetary amount"), currency: str | None = Query( None, title="Currency", description="Currency code (optional, auto-selected if not provided)", ), currency_digits: bool = Query( True, title="Currency Digits", description="Whether to show decimal places (e.g., JPY has no cents)", ), format_type: str = Query( "standard", title="Format Type", description="Currency format type: standard, accounting, etc.", ),): """ Returns the given amount formatted as currency based on the requested locale. If no currency is provided, the default for that locale is used. """ return { "formatted_currency": localize_currency( amount, currency, lang, currency_digits, format_type ) }
Defines the /currency endpoint to return a formatted monetary value based on the given locale.
Accepts query parameters:
lang: The requested language (defaults to the app’s configured locale).
amount : The monetary value to format.
currency : The currency code (optional, defaults based on locale).
currency_digits : Controls whether to show decimal places (e.g., JPY usually has none).
format_type : Allows specifying the currency format style (standard, accounting, etc.).
Calls localize_currency() to properly format the amount according to the requested locale.
Returns a JSON response with the correctly formatted currency string.
Automatically detecting the user's preferred locale
Instead of requiring users to pass ?lang=xx in every request, we can automatically detect the preferred language from the Accept-Language HTTP header. This allows us to serve localized content without extra effort from the user.
To avoid writing custom parsing logic, we’ll use the langcodes package, which simplifies language detection and matching.
Step 1: Implement locale detection
Modify main.py to include a helper function for detecting the best language:
from fastapi import FastAPI, Query, Requestdef detect_best_locale(request: Request) -> str: """ Detects the user's preferred language based on the Accept-Language header. Strips regional codes (e.g., 'en-US' -> 'en') and matches the best available locale. """ accept_language = request.headers.get("Accept-Language", "") # Extract language codes from header and strip regions (e.g., 'en-US' -> 'en') parsed_languages = [tag.split("-")[0] for tag in accept_language.split(",")] # Find the first matching language from our supported locales for lang in parsed_languages: if lang in settings.supported_locales: return lang return settings.default_locale # Fallback if no match is found
This function extracts the preferred language from the Accept-Language HTTP header and matches it against the supported locales.
Retrieves the Accept-Language header from the request.
Splits the header into language tags and removes any regional codes (e.g., en-US becomes en).
Iterates through the extracted language codes and returns the first match from the supported locales.
If no match is found, falls back to the default locale.
This approach ensures that users receive content in their preferred language without needing to manually specify it.
Step 2: Adjust your endpoints
Now you'll need to adjust your endpoints to avoid immediately reading the default locale if the lang GET param is missing:
@app.get("/")def read_root( request: Request, lang: str = Query(None, title="Language", description="Language code (optional)"),): """ A simple endpoint that returns a localized greeting message. """ detected_lang = lang or detect_best_locale(request) return { "message": translations.get( detected_lang, translations[settings.default_locale] )["greeting"], "direction": get_text_direction(detected_lang), }
This endpoint returns a localized greeting message while automatically detecting the user's preferred language.
Accepts an optional lang query parameter for manual language selection.
If lang is not provided, it calls detect_best_locale(request) to determine the best-matching language from the request headers.
Retrieves the correct translation from the stored translations, falling back to the default locale if necessary.
Calls get_text_direction(detected_lang) to determine whether the language is left-to-right (LTR) or right-to-left (RTL) and includes this in the response.
Preserving the user's preferred locale
Once a user selects a language, we should remember their preference to avoid requiring ?lang=xx in every request. The best way to do this is by storing the selected locale in a cookie.
Step 1: Modify the / endpoint to set a language cookie
We update detect_best_locale to prioritize sources in the correct order:
Use the default locale if no valid language is found.
Check the lang query parameter. If it's valid, use it.
Check the stored cookie (preferred_locale). If it's valid, use it.
Check the Accept-Language header and extract the best match.
def detect_best_locale(request: Request, lang: str | None) -> str: """ Determines the user's preferred locale by checking: 1. Query parameter (`?lang=xx`) 2. Stored cookie (`preferred_locale`) 3. Accept-Language header 4. Default locale (fallback) """ # Step 1: Check if `lang` is provided and valid if lang and lang in settings.supported_locales: return lang # Step 2: Check if a language cookie is already set and valid preferred_locale = request.cookies.get("preferred_locale") if preferred_locale and preferred_locale in settings.supported_locales: return preferred_locale # Step 3: Check Accept-Language header and match best available locale accept_language = request.headers.get("Accept-Language", "") parsed_languages = [tag.split("-")[0] for tag in accept_language.split(",")] for detected_lang in parsed_languages: if detected_lang in settings.supported_locales: return detected_lang # Step 4: Fall back to default locale return settings.default_locale
Step 2: Create a separate function to store the detected locale
Instead of setting the cookie inside the endpoint, we define a dedicated function to handle language persistence. This function will:
Return the modified response
Create a response object
Set the preferred_locale cookie with the detected language
from fastapi.responses import JSONResponsedef store_locale(response: JSONResponse, lang: str) -> JSONResponse: """ Stores the selected locale in a cookie for 30 days. """ response.set_cookie(key="preferred_locale", value=lang, max_age=60 * 60 * 24 * 30) return response
Step 3: Modify the / endpoint to use store_locale
Now, the root endpoint only detects the locale, and we store it separately before returning the response.
@app.get("/")def read_root( request: Request, lang: str = Query(None, title="Language", description="Language code (optional)"),): """ A simple endpoint that returns a localized greeting message and ensures the preferred language is stored. """ detected_lang = detect_best_locale(request, lang) response = JSONResponse( { "message": translations.get( detected_lang, translations[settings.default_locale] )["greeting"], "direction": get_text_direction(detected_lang), } ) return store_locale(response, detected_lang)
How this works
detect_best_locale(request, lang) determines the correct language.
store_locale(response, detected_lang) ensures the preferred language is stored for future requests.
Final tweaks to FastAPI i18n
We'll move all i18n-related functions to a separate module and keep main.py focused only on defining endpoints.
Step 1: Create an i18n module
Inside your project, create a new directory i18n/ and add a file utils.py:
mkdir i18ntouch i18n/utils.py
Step 2: Move all i18n-related logic to i18n/utils.py
Move all localization functions, language detection, and currency/date formatting logic into i18n/utils.py:
from fastapi import Requestfrom fastapi.responses import JSONResponsefrom babel.core import Localefrom babel.dates import format_datetimefrom babel.numbers import format_currencyfrom datetime import datetimeimport jsonimport osfrom fastapi_i18n_demo.config import settingsDEFAULT_CURRENCIES = { "en": "USD", "fr": "EUR", "he": "ILS",}# Load translations dynamically based on config settingstranslations = {}for lang in settings.supported_locales: path = os.path.join("locales", lang, "messages.json") with open(path, encoding="utf-8") as f: translations[lang] = json.load(f)def store_locale(response: JSONResponse, lang: str) -> JSONResponse: """ Stores the selected locale in a cookie for 30 days. """ response.set_cookie(key="preferred_locale", value=lang, max_age=60 * 60 * 24 * 30) return responsedef detect_best_locale(request: Request, lang: str | None) -> str: """ Determines the user's preferred locale by checking: 1. Query parameter (`?lang=xx`) 2. Stored cookie (`preferred_locale`) 3. Accept-Language header 4. Default locale (fallback) """ if lang and lang in settings.supported_locales: return lang preferred_locale = request.cookies.get("preferred_locale") if preferred_locale and preferred_locale in settings.supported_locales: return preferred_locale accept_language = request.headers.get("Accept-Language", "") parsed_languages = [tag.split("-")[0] for tag in accept_language.split(",")] for detected_lang in parsed_languages: if detected_lang in settings.supported_locales: return detected_lang return settings.default_localedef localize_currency( amount: float, currency_code: str | None, lang: str, currency_digits: bool = True, format_type: str = "standard",) -> str: """ Formats a given amount into the correct currency format for a given locale. If no currency code is provided, a default is selected based on the locale. """ try: locale = lang if not currency_code: currency_code = DEFAULT_CURRENCIES.get(lang[:2], "USD") return format_currency( amount, currency_code, locale=locale, currency_digits=currency_digits, format_type=format_type, ) except Exception: return f"{amount} {currency_code or 'USD'}"def localize_datetime(lang: str) -> str: """ Returns the current datetime formatted according to the given locale. """ try: locale = Locale.parse(lang) now = datetime.now() return format_datetime(now, locale=locale) except Exception: return str(datetime.now())def get_text_direction(lang: str) -> str: """ Determines if a language is Right-to-Left (RTL) or Left-to-Right (LTR). """ rtl_languages = {"ar", "he", "fa", "ur"} return "rtl" if lang in rtl_languages else "ltr"def get_plural_form(lang: str, count: int) -> str: """ Determines the correct pluralization form using Babel's Locale class. """ try: locale = Locale.parse(lang) return locale.plural_form(count) except Exception: return "other"
Step 3: Update main.py to import from i18n.utils
Now, main.py will only contain FastAPI endpoints and import i18n-related functions from i18n.utils.
from fastapi import FastAPI, Query, Requestfrom fastapi.responses import JSONResponsefrom i18n.utils import ( detect_best_locale, store_locale, get_text_direction, get_plural_form, localize_datetime, localize_currency, translations,)from fastapi_i18n_demo.config import settingsapp = FastAPI()@app.get("/")def read_root( request: Request, lang: str = Query(None, title="Language", description="Language code (optional)"),): """ A simple endpoint that returns a localized greeting message and ensures the preferred language is stored. """ detected_lang = detect_best_locale(request, lang) response = JSONResponse( { "message": translations.get( detected_lang, translations[settings.default_locale] )["greeting"], "direction": get_text_direction(detected_lang), } ) return store_locale(response, detected_lang)@app.get("/messages")def get_messages( request: Request, lang: str = Query(None, title="Language", description="Language code (optional)"), count: int = Query(1, title="Count", description="Number of messages"),): """ Returns a localized message with correct pluralization. """ detected_lang = detect_best_locale(request, lang) plural_form = get_plural_form(detected_lang, count) messages_dict = translations.get( detected_lang, translations[settings.default_locale] )["messages"] message_template = messages_dict.get(plural_form, messages_dict["other"]) response = JSONResponse( { "message": message_template.format(count=count), "direction": get_text_direction(detected_lang), } ) return store_locale(response, detected_lang)@app.get("/datetime")def get_localized_datetime( request: Request, lang: str = Query(None, title="Language", description="Language code (optional)"),): """ Returns the current datetime formatted according to the given locale. """ detected_lang = detect_best_locale(request, lang) response = JSONResponse({"datetime": localize_datetime(detected_lang)}) return store_locale(response, detected_lang)@app.get("/currency")def get_localized_currency( request: Request, lang: str = Query(None, title="Language", description="Language code (optional)"), amount: float = Query(1000.50, title="Amount", description="Monetary amount"), currency: str | None = Query( None, title="Currency", description="Currency code (optional, auto-selected if not provided)", ), currency_digits: bool = Query( True, title="Currency Digits", description="Whether to show decimal places (e.g., JPY has no cents)", ), format_type: str = Query( "standard", title="Format Type", description="Currency format type: standard, accounting, etc.", ),): """ Returns the given amount formatted as currency based on the requested locale. If no currency is provided, the default for that locale is used. """ detected_lang = detect_best_locale(request, lang) response = JSONResponse( { "formatted_currency": localize_currency( amount, currency, detected_lang, currency_digits, format_type ) } ) return store_locale(response, detected_lang)
Brilliant job!
Simplifying FastAPI i18n with Lokalise
Translating all your application text is often the most time-consuming part of FastAPI internationalization. However, using a translation management system like Lokalise can streamline the process and save you from manually handling translations. Lokalise allows you to upload, manage, and download translations efficiently. You can collaborate with your team, order professional translations, take advantage of automatic translations, and many more.
Next, open the Lokalise website and navigate to the API tokens section in your profile. Generate a new read/write token, which will be used to interact with Lokalise from the command line.
Now, create a new Lokalise translation project with the "Web and mobile" type and set English as the base language. Choose other languages (French and Hebrew in our case) as target. Open the project settings and copy the project ID.
To upload your FastAPI translation files, run: lokalise2 file upload --token --project-id --lang_iso en --file locales/en/messages.json. Repeat this process for other locales.
After the upload is complete, go to your Lokalise dashboard and you will see all your translation keys and values. You can edit, delete, or add new translations easily using the web interface. Lokalise also provides powerful filtering tools, allowing you to quickly find untranslated keys.
Once your translations are finalized, you can download them back into your FastAPI project: lokalise2 file download --token --project-id --format json --dest locales/.
Lokalise supports multiple platforms and file formats. It also offers features like professional translation services, screenshot text recognition, and integrations with development tools. By using Lokalise, you can eliminate manual translation file management and keep your FastAPI i18n workflow efficient and scalable.
Conclusion
Internationalizing a FastAPI application doesn’t have to be complicated. By structuring translations properly, handling pluralization, datetime, and currency localization, and automatically detecting and persisting user preferences, we’ve built a robust and flexible i18n system.
We also explored Lokalise as a way to simplify translation management, making it easy to edit, update, and sync translations without manually handling JSON files. With the Lokalise API, tools like Babel for formatting, cookies for persistence, and FastAPI’s powerful query handling, your application can seamlessly serve content in multiple languages.
With this foundation in place, your FastAPI app is now fully ready for multilingual users. As you scale, you can expand translations, integrate external translation services, or even localize UI elements dynamically. The key is to automate as much as possible while maintaining full control over language behavior.
Now it’s time to ship your multilingual FastAPI app and make it accessible to a global audience!
Ilya is a lead of content/documentation/onboarding at Lokalise, an IT tutor and author, web developer, and ex-Microsoft/Cisco specialist. His primary programming languages are Ruby, JavaScript, Python, and Elixir. He enjoys coding, teaching people and learning new things. In his free time he writes educational posts, participates in OpenSource projects, goes in for sports and plays music.
Ilya is a lead of content/documentation/onboarding at Lokalise, an IT tutor and author, web developer, and ex-Microsoft/Cisco specialist. His primary programming languages are Ruby, JavaScript, Python, and Elixir. He enjoys coding, teaching people and learning new things. In his free time he writes educational posts, participates in OpenSource projects, goes in for sports and plays music.
An SRT file is a plain text file used to add subtitles to videos. It’s one of the simplest and most common formats out there. If you’ve ever turned on captions on a YouTube video, there’s a good chance it was using an SRT file behind the scenes. People use SRT files for all kinds of things: social media clips, online courses, interviews, films, you name it. They’re easy to make, easy to edit, and they work pretty much everywhere without hassle. In this post, we’ll
Libraries and frameworks to translate JavaScript apps
In our previous discussions, we explored localization strategies for backend frameworks like Rails and Phoenix. Today, we shift our focus to the front-end and talk about JavaScript translation and localization. The landscape here is packed with options, which makes many developers a
Syncing Lokalise translations with GitLab pipelines
In this guide, we’ll walk through building a fully automated translation pipeline using GitLab CI/CD and Lokalise. From upload to download, with tagging, version control, and merge requests. Here’s the high-level flow: Upload your source language files (e.g. English JSON files) to Lokalise from GitLab using a CI pipeline.Tag each uploaded key with your Git branch name. This helps keep translations isolated per feature or pull request