En este caso vamos a crear los endpoints de tal forma que el usuario pueda enviar en el request, el idioma seleccionado. También podría ser por una query en la base de datos dónde indique el idioma o por cabecera Accept-Language.

La estructura más limpia es con archivos de traducción JSON:
locales/ es.json en.json fr.json de.json
Cada archivo con las cadenas traducidas:
locales/es.json
json
{
"email": {
"welcome": {
"subject": "Bienvenido a La web",
"body": "Hola {name}, bienvenido a la comunidad de la web."
},
"libro_vendido": {
"subject": "Tu libro ha sido vendido",
"body": "Hola {name}, tu libro '{titulo}' ha sido vendido."
}
}
}
locales/en.json
json
{
"email": {
"welcome": {
"subject": "Welcome to My Web",
"body": "Hi {name}, welcome to the book community."
},
"libro_vendido": {
"subject": "Your book has been sold",
"body": "Hi {name}, your book '{titulo}' has been sold."
}
}
}
Luego un helper en config/i18n.py:
python
import json
import os
from typing import Optional
_translations = {}
def load_translations():
locales_dir = "locales"
for filename in os.listdir(locales_dir):
if filename.endswith(".json"):
lang = filename.replace(".json", "")
with open(f"{locales_dir}/{filename}", "r", encoding="utf-8") as f:
_translations[lang] = json.load(f)
def t(key: str, lang: str = "es", **kwargs) -> str:
keys = key.split(".")
result = _translations.get(lang, _translations.get("es", {}))
for k in keys:
result = result.get(k, key)
if not isinstance(result, dict):
break
if isinstance(result, str):
return result.format(**kwargs)
return key
Y lo usas así en cualquier email:
python
from config.i18n import t
subject = t("email.welcome.subject", lang=user.lang)
body = t("email.welcome.body", lang=user.lang, name=user.name)
Y en el startup cargas las traducciones:
python
@asynccontextmanager
async def lifespan(app: FastAPI):
load_translations()
await init_redis()
...
Y si quieres utilizar cabecera HTTP:
Cambia el parámetro por el header Accept-Language:
python
from fastapi import Header
@articles.post("/add_book")
async def add_book(
...,
accept_language: Optional[str] = Header(default="es")
):
lang = accept_language.split("-")[0].split(",")[0].lower() # "es-ES" → "es"
subject = t("email.welcome.subject", lang=lang)
body = t("email.welcome.body", lang=lang, name=user.name)
El .split("-")[0] convierte es-ES o en-US en es o en para que coincida con tus archivos JSON.
Si quieres reutilizarlo en toda la app sin repetirlo en cada endpoint, ponlo como dependencia:
python
# config/i18n.py
def get_lang(accept_language: Optional[str] = Header(default="es")) -> str:
return accept_language.split("-")[0].split(",")[0].lower()
# En cualquier endpoint
from config.i18n import get_lang
@articles.post("/add_book")
async def add_book(
...,
lang: str = Depends(get_lang)
):
subject = t("email.welcome.subject", lang=lang)
Así en todos los endpoints solo añades lang: str = Depends(get_lang) y ya tienes el idioma sin repetir lógica.

Ingeniero en Informática, Investigador, me encanta crear cosas o arreglarlas y darles una nueva vida. Escritor y poeta. Más de 20 APPs publicadas y un libro en Amazon.