# General Translation Knowledge Base: ¿Cómo traduzco una aplicación de Flask o FastAPI?
URL: https://generaltranslation.com/es/kb/translate-python-app.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: Marca las cadenas visibles para el usuario, elige la configuración regional de cada solicitud (normalmente a partir de Accept-Language) y carga las traducciones correspondientes a esa configuración regional. Las aplicaciones de Flask suelen usar Flask-Babel con catálogos de gettext, mientras que las de FastAPI integran Babel en un middleware. La CLI de General Translation rellena esos catálogos, y sus SDK experimentales gt-flask y gt-fastapi traducen cadenas t() inline sin necesidad de catálogos.
Category: Internacionalización
Last updated: 2026-10-28
Authors: General Translation Team
Published: 2026-10-28

Ni Flask ni FastAPI incluyen soporte de traducción de serie, así que tendrás que añadir tres cosas: una forma de marcar las cadenas visibles para el usuario, una forma de elegir el idioma de cada solicitud y las traducciones para cada idioma que ofrezcas. La opción más consolidada son los catálogos de gettext de Babel; otra más reciente mantiene las cadenas inline y carga las traducciones como JSON. Sea cual sea el enfoque, el idioma está ligado a la solicitud, no al proceso, porque un mismo servidor atiende a la vez a usuarios en muchos idiomas.

## Qué necesita una aplicación web de Python traducida

* **Cadenas marcadas.** El código llama a una función como `gettext()` o [`t()`](/docs/python/reference/functions/t) en lugar de devolver una cadena sin más, y las plantillas usan `_()` o `{% trans %}`. El marcado es lo que permite a una herramienta localizar todas las cadenas y buscar su traducción.
* **Una configuración regional por solicitud.** Normalmente se obtiene de la cabecera `Accept-Language`, aunque un prefijo de URL, una cookie o un ajuste de la cuenta pueden sobrescribirla. Flask la guarda en el contexto de la solicitud; FastAPI gestiona las solicitudes de forma concurrente en un único bucle de eventos, así que la configuración regional se almacena en un `contextvars.ContextVar` que establece cada solicitud.
* **Traducciones para cada configuración regional.** gettext carga catálogos `.mo` compilados; los SDK de General Translation cargan un archivo JSON por configuración regional. En ambos casos, si falta una traducción, se recurre al texto de origen sin generar ningún error, de modo que las cadenas sin traducir aparecen en inglés en lugar de provocar un fallo.

## Opción 1: catálogos de gettext con Babel

[Babel](https://babel.pocoo.org/en/latest/cmdline.html) extrae las cadenas marcadas a una plantilla POT, crea y actualiza un catálogo PO por idioma y los compila en archivos `.mo`. Además, da formato a fechas, números y monedas para cualquier configuración regional.

### Flask con Flask-Babel

[Flask-Babel](https://python-babel.github.io/flask-babel/) (4.0.0) integra Babel con las solicitudes y las plantillas Jinja de Flask. Solo tienes que proporcionarle un `locale_selector` que devuelva la configuración regional de cada solicitud:

```python title="app.py"
from flask import Flask, render_template, request
from flask_babel import Babel, gettext, ngettext


def select_locale():
    # La mejor coincidencia entre la cabecera Accept-Language y los idiomas que incluyes
    return request.accept_languages.best_match(["en", "es", "pl"])


app = Flask(__name__)
babel = Babel(app, locale_selector=select_locale)


@app.route("/inbox/<int:count>")
def inbox(count):
    return {
        "title": gettext("Inbox"),
        # ngettext selecciona la forma plural según la configuración regional activa
        "unread": ngettext("You have %(num)d unread message.", "You have %(num)d unread messages.", count),
    }


@app.route("/")
def home():
    return render_template("home.html", name="Alice")
```

En las plantillas, las cadenas se marcan con `_()`, por ejemplo `{{ _("Welcome back, %(name)s!", name=name) }}`. Un archivo `babel.cfg` le indica a `pybabel` dónde buscar, y los catálogos se gestionan con cuatro comandos:

```bash
# babel.cfg contiene [python: **.py] y [jinja2: **/templates/**.html]
pybabel extract -F babel.cfg -o messages.pot .
pybabel init -i messages.pot -d translations -l es    # una vez por idioma
pybabel update -i messages.pot -d translations        # tras cada extracción, para todos los idiomas
pybabel compile -d translations
```

Con el catálogo en español generado por la CLI de General Translation, una solicitud con `Accept-Language: es-MX,es;q=0.9` devolvió &quot;Bandeja de entrada&quot; y &quot;Tienes 5 mensajes sin leer.&quot;. Con el catálogo en polaco creado pero vacío, una solicitud en polaco devolvió todas las cadenas en inglés, sin mostrar ninguna advertencia.

### FastAPI con Babel

FastAPI no cuenta con un equivalente oficial de Flask-Babel. Existen paquetes de terceros como [fastapi-babel](https://pypi.org/project/fastapi-babel/) y [starlette-babel](https://pypi.org/project/starlette-babel/), aunque también puedes cargar tú mismo los catálogos de Babel en un middleware. Esta versión lee el mismo directorio `translations` y guarda el catálogo de cada solicitud en una `ContextVar`:

```python title="main.py"
from contextvars import ContextVar
from functools import lru_cache

from babel.core import negotiate_locale
from babel.support import Translations
from fastapi import FastAPI, Request

SUPPORTED = ["en", "es", "pl"]
current = ContextVar("translations")


@lru_cache
def load(locale: str) -> Translations:
    # Lee translations/<locale>/LC_MESSAGES/messages.mo, compilado por pybabel
    return Translations.load("translations", [locale])


def _(message: str) -> str:
    return current.get().gettext(message)


def preferred_locales(header: str) -> list[str]:
    """Accept-Language tags, most preferred first, without q=0 entries."""
    weighted = []
    for part in header.split(","):
        tag, sep, param = part.strip().partition(";")
        try:
            q = float(param.strip().removeprefix("q=")) if sep else 1.0
        except ValueError:
            q = 0.0
        # q=0 significa «no aceptable», así que se omite
        if tag and q > 0:
            weighted.append((q, tag.replace("-", "_")))
    # sorted() es estable, así que las etiquetas con el mismo peso conservan el orden de la cabecera
    return [tag for q, tag in sorted(weighted, key=lambda pair: -pair[0])]


app = FastAPI()


@app.middleware("http")
async def select_locale(request: Request, call_next):
    preferred = preferred_locales(request.headers.get("accept-language", ""))
    current.set(load(negotiate_locale(preferred, SUPPORTED) or "en"))
    return await call_next(request)


@app.get("/inbox/{count}")
def inbox(count: int):
    t = current.get()
    return {
        "title": _("Inbox"),
        "unread": t.ngettext("You have %(num)d unread message.", "You have %(num)d unread messages.", count) % {"num": count},
    }
```

Al ejecutarlo con Uvicorn, `Accept-Language: es-MX,es;q=0.9,en;q=0.8` devolvió las cadenas en español, `de,en;q=0.5` recurrió al inglés como alternativa, `en;q=0.5,es` devolvió español y `es;q=0,en` devolvió inglés. `negotiate_locale` hace coincidir `es_MX` con `es`. `preferred_locales` ordena las etiquetas según su peso `q` y descarta las que tienen `q=0`, que indica un idioma que el usuario no acepta.

### Lo que la opción de Babel deja en tus manos

Babel gestiona catálogos, pero no escribe traducciones. Cada cadena nueva o modificada queda a la espera de que alguien rellene su `msgstr` en cada idioma (incluidas las tres formas de plural del polaco) y, después, de que se ejecute `pybabel compile` y se haga un despliegue. (Consulta [¿Cómo traduzco archivos PO y POT?](/kb/translate-po-files) para conocer el formato y las comprobaciones que detectan entradas defectuosas, y [General Translation vs. Babel y Flask-Babel](/kb/general-translation-vs-flask-babel) para ver qué cubre cada herramienta). Las aplicaciones de Django siguen el mismo patrón con sus propios comandos. (Consulta [General Translation vs. el framework de traducción de Django](/kb/general-translation-vs-django-i18n)).

## Elegir la configuración regional para cada solicitud

`Accept-Language` enumera los idiomas del usuario con pesos `q` opcionales, y tu tarea es hacerlos coincidir con los idiomas que ofreces, con un valor predeterminado para todo lo demás. Tanto `best_match()` de Werkzeug sobre `request.accept_languages` como `negotiate_locale` de Babel recurren a `es` cuando reciben una etiqueta regional como `es-MX`. Da prioridad a una elección explícita sobre la cabecera: un prefijo de URL `/es/` para las páginas que indexan los motores de búsqueda, o una cookie que establece el selector de idioma. (Consulta [¿Cómo detecto el idioma del navegador de un usuario?](/kb/detect-user-language) para ver el orden completo de señales). Los servidores Node.js resuelven el mismo problema con `AsyncLocalStorage`. (Consulta [¿Cómo traduzco una aplicación Node.js?](/kb/translate-node-app)).

## Compara los enfoques

| Enfoque                                              | Cómo escribes las cadenas                                           | Configuración regional por solicitud        | Quién escribe las traducciones          | Estado                                                                    |
| ---------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------- | --------------------------------------- | ------------------------------------------------------------------------- |
| Flask-Babel con catálogos PO                         | `gettext`, `ngettext`, Jinja `_()`                                  | `locale_selector`                           | Tú o tus traductores, por catálogo      | Estable                                                                   |
| Babel en un middleware de FastAPI                    | `gettext` mediante un `ContextVar`                                  | Tu middleware                               | Tú o tus traductores, por catálogo      | Estable, pero lo montas tú                                                |
| API de traducción automática por solicitud           | Cadenas sin procesar enviadas en tiempo de ejecución                | Tú pasas el idioma de destino               | La API, en cada solicitud               | Añade latencia y coste en cada llamada, salvo que uses caché              |
| Catálogos de Babel más la CLI de General Translation | Igual que con Babel                                                 | Igual que con Babel                         | General Translation, antes de desplegar | La CLI es estable; Babel no cambia                                        |
| `gt-flask` o `gt-fastapi` de General Translation     | [`t()`](/docs/python/reference/functions/t) inline con mensajes ICU | Coincidencia de `Accept-Language` integrada | General Translation, antes de desplegar | Experimental (0.1.0); las traducciones cargadas caducan a los 60 segundos |

Usa Flask-Babel, o Babel en un middleware de FastAPI, si ya tienes catálogos o traductores. Añade la CLI de General Translation a esa configuración si quieres que todos los catálogos se completen en todos los idiomas con cada cambio, sin tener que esperar a una ronda de traducción. Prueba `gt-flask` o `gt-fastapi` en un prototipo en el que prefieras mantener las cadenas inline sin archivos de catálogo, y fija la versión. Recurre a una API de traducción automática en el momento de la solicitud solo para contenido que únicamente existe en tiempo de ejecución, como las publicaciones de los usuarios.

## Cómo traduce General Translation las aplicaciones de Flask y FastAPI

### Conservar Babel y traducir los catálogos

La CLI de General Translation, `gt`, lee un catálogo PO en inglés y genera los catálogos de los demás idiomas donde Babel espera encontrarlos. Crea el catálogo en inglés una sola vez con `pybabel init -l en`, actualízalo después de cada extracción con `pybabel update -l en` y, por último, haz que `gt.config.json` apunte a él:

```json title="gt.config.json"
{
  "defaultLocale": "en",
  "locales": ["es", "pl"],
  "files": {
    "pot": {
      "include": ["translations/[locale]/LC_MESSAGES/messages.po"],
      "transformationFormat": "PO"
    }
  }
}
```

Según lo comprobado con `gt` 2.26.11, la CLI asigna el catálogo en inglés de `translations/en/LC_MESSAGES` a las carpetas `es` y `pl` situadas junto a él. Ejecuta [`npx gt translate`](/docs/cli/reference/commands/translate) y, a continuación, `pybabel compile -d translations`. General Translation rellena todas las formas de plural de cada idioma de destino y comprueba que cada forma plural conserve los marcadores de posición, como `%(num)d`. Un glosario y los prompts personalizados de un [grupo de contexto](/docs/platform/dashboard/guides/defining-context-for-translations) mantienen la coherencia terminológica con tus otros proyectos. La CLI se ejecuta en Node.js, así que la máquina donde la ejecutes necesita Node; tu aplicación sigue cargando los catálogos compilados sin depender de General Translation en tiempo de ejecución.

### O usa gt-flask y gt-fastapi

Los SDK de Python de General Translation, `gt-flask` y `gt-fastapi` (0.1.0 en PyPI, del repositorio [gt-python](https://github.com/generaltranslation/gt-python)), son experimentales y pueden cambiar de una versión a otra. Requieren Python 3.10 o posterior. Las cadenas se mantienen en tu código como mensajes ICU que se pasan a [`t`](/docs/python/reference/functions/t), con los valores como argumentos con nombre, y [`initialize_gt`](/docs/python/reference/functions/initialize-gt) configura la coincidencia de `Accept-Language`. Esta aplicación Flask carga las traducciones desde archivos JSON locales:

```python title="app.py"
import json
from pathlib import Path

from flask import Flask
from gt_flask import initialize_gt, t, get_locale


def load_translations(locale: str) -> dict[str, str]:
    # Archivos que la CLI de gt escribe en la ruta de files.gt.output
    path = Path("translations") / f"{locale}.json"
    return json.loads(path.read_text()) if path.exists() else {}


app = Flask(__name__)
# En Flask, pasa aquí los locales para que las traducciones se carguen al arrancar
initialize_gt(app, default_locale="en", locales=["es", "fr"], load_translations=load_translations)


@app.route("/")
def index():
    return {
        "message": t("Hello, {name}!", name="Alice"),
        "unread": t("{count, plural, one {You have # unread message.} other {You have # unread messages.}}", count=3),
        "locale": get_locale(),
    }
```

Para FastAPI, importa desde `gt_fastapi`, crea `FastAPI()` y usa `@app.get`; en este caso, [`initialize_gt`](/docs/python/reference/functions/initialize-gt) añade un middleware HTTP y carga las traducciones durante el ciclo de vida (lifespan) de la aplicación, así que basta con los locales definidos en `gt.config.json`. Con el archivo `translations/es.json` que generó la CLI de General Translation, ambas aplicaciones respondieron así:

```text title="Output"
Accept-Language: es-MX,es;q=0.9   {"locale": "es", "message": "¡Hola, Alice!", "unread": "Tienes 3 mensajes sin leer."}
Accept-Language: de                {"locale": "en", "message": "Hello, Alice!", "unread": "You have 3 unread messages."}
```

La CLI genera esos archivos. Añade `gt-flask` o `gt-fastapi` a `requirements.txt` o `pyproject.toml` para que la CLI lo detecte, y define la ruta de salida en `gt.config.json`:

```json title="gt.config.json"
{
  "defaultLocale": "en",
  "locales": ["es", "fr"],
  "files": { "gt": { "output": "translations/[locale].json" } }
}
```

[`npx gt translate`](/docs/cli/reference/commands/translate) analiza tus archivos `.py` en busca de llamadas a [`t()`](/docs/python/reference/functions/t) (omitiendo entornos virtuales, pruebas y migraciones), las traduce y escribe un archivo por configuración regional. En esta aplicación, [`npx gt validate`](/docs/cli/reference/commands/validate) detectó 2 entradas traducibles para `gt-flask` sin llamar a la API. Si prefieres cargarlas desde la CDN de General Translation, configura `projectId` y `cacheUrl` en la configuración (consulta [Elegir cómo se entregan las traducciones](/docs/python/guides/configuring#delivery)). Los SDK no requieren clave de API, y con archivos locales no se hacen llamadas de red en tiempo de ejecución.

En la versión 0.1.0, [`t()`](/docs/python/reference/functions/t) lee las traducciones de una caché en memoria que caduca a los 60 segundos y que no se recarga en esa ruta de lectura. Por eso, en un servidor de larga duración, una vez que caduca la caché se devuelven las cadenas de origen hasta que el cargador vuelve a ejecutarse. (Consulta las [notas sobre la precarga](/docs/python/guides/configuring#preload)). Mientras ningún lanzamiento lo corrija, usa catálogos de Babel generados por la CLI para el tráfico de producción y reserva `gt-flask` y `gt-fastapi` para prototipos.

## Preguntas frecuentes

### ¿gt-flask reemplaza el formato de fechas y monedas de Flask-Babel?

No. En la versión 0.1.0, `gt-flask` y `gt-fastapi` exportan funciones auxiliares de traducción y de configuración regional ([`t`](/docs/python/reference/functions/t), [`get_locale`](/docs/python/reference/functions/get-locale), [`declare_var`](/docs/python/reference/functions/declare-var) y algunas más), por lo que debes seguir usando `format_date` y `format_currency` de Babel para dar formato según la configuración regional.

### ¿Cómo permito que los usuarios elijan un idioma en lugar de usar Accept-Language?

Con Flask-Babel, lee una cookie o un segmento de la URL en tu `locale_selector`. Con los SDK de General Translation, pasa un callback [`get_locale`](/docs/python/reference/functions/get-locale) a [`initialize_gt`](/docs/python/reference/functions/initialize-gt); este sustituye a la coincidencia basada en la cabecera, así que tendrás que devolver tú mismo una configuración regional compatible. (Consulta [Detección de la configuración regional de la solicitud](/docs/python/guides/detecting-locale)).

### ¿Cómo traduzco contenido de bases de datos o publicaciones de usuarios?

Los catálogos y [`t()`](/docs/python/reference/functions/t) se encargan de las cadenas de tu código. El contenido que solo existe en tiempo de ejecución requiere una llamada de traducción en el momento en que se crea o se solicita, por ejemplo al endpoint [Translate content](/docs/platform/openapi/reference/translation/translate-content) de General Translation (`POST /v2/projects/{projectId}/translate`), y los resultados se guardan junto al original.

## Conclusiones clave

* Flask y FastAPI necesitan tres elementos adicionales: cadenas marcadas, una configuración regional por solicitud y traducciones para cada configuración regional; FastAPI guarda la configuración regional en un `ContextVar`.
* En Flask, Flask-Babel se encarga de todo con un `locale_selector` y los comandos `pybabel extract`, [`init`](/docs/cli/reference/commands/init), `update` y `compile`; FastAPI carga las `Translations` de Babel en un middleware.
* Si un catálogo está vacío o no existe, se recurre al inglés sin ningún aviso, así que lo que queda por hacer es completar cada `msgstr`.
* La CLI de General Translation completa los catálogos PO de Babel con `"pot"` y `"transformationFormat": "PO"`, incluidas las formas de plural, y tu aplicación sigue cargando los catálogos compilados como siempre.
* Los paquetes experimentales `gt-flask` y `gt-fastapi` 0.1.0 de General Translation traducen cadenas inline de [`t()`](/docs/python/reference/functions/t) a partir de los archivos JSON que genera la CLI; en Flask, pasa `locales` a [`initialize_gt`](/docs/python/reference/functions/initialize-gt) y ten en cuenta la caducidad de la caché a los 60 segundos antes de atender tráfico en producción.

## Related questions

- [General Translation frente a Babel y Flask-Babel: ¿cuál deberías usar?](https://generaltranslation.com/es/kb/general-translation-vs-flask-babel.mdx): Babel da formato a fechas, números y monedas para cualquier configuración regional y gestiona catálogos de gettext, y Flask-Babel lo integra con Flask, pero ninguno de los dos traduce tus cadenas. La CLI de General Translation rellena los archivos PO de Babel para cada idioma, formas plurales incluidas, para que puedas seguir usando Flask-Babel y dejar de traducir catálogos a mano.
- [¿Cómo traduzco archivos PO y POT?](https://generaltranslation.com/es/kb/translate-po-files.mdx): Rellena cada msgstr, incluidas todas las formas plurales que pide la cabecera Plural-Forms, y deja msgid, msgctxt y los marcadores de posición sin cambios. Ejecuta msgfmt --check para verificar las cabeceras, el número de formas plurales y los marcadores de posición en las entradas con una opción de formato, y comprueba por separado que ningún msgstr esté vacío. La CLI de General Translation genera un archivo PO traducido para cada idioma a partir de tu plantilla POT.
- [¿Cómo detecto el idioma del navegador de un usuario?](https://generaltranslation.com/es/kb/detect-user-language.mdx): Lee la lista ordenada de idiomas del navegador (navigator.languages en JavaScript o la cabecera Accept-Language en el servidor), ordenada por valor q, y busca la coincidencia con tus configuraciones regionales compatibles, con una configuración regional de respaldo. Safari y el modo incógnito de Chrome solo indican un idioma, y la geolocalización por IP revela la ubicación, no el idioma.
- [¿Cómo traduzco una aplicación de Node.js?](https://generaltranslation.com/es/kb/translate-node-app.mdx): Detecta el idioma de cada solicitud, normalmente a partir de su cabecera Accept-Language, y devuelve todas las cadenas en ese idioma. gt-node de General Translation vincula la configuración regional a cada solicitud con withGT y traduce las cadenas con getGT, a partir de las traducciones que npx gt translate --publish genera antes de desplegar.
- [General Translation frente al framework de traducción de Django: ¿necesitas ambos?](https://generaltranslation.com/es/kb/general-translation-vs-django-i18n.mdx): Usa el framework de traducción de Django y General Translation de forma conjunta. Django se encarga de la extracción de mensajes, la selección del idioma y el renderizado. General Translation rellena sus archivos PO, incluidas las formas plurales, con la terminología de tu producto y la revisión en el panel de control. Tus plantillas y patrones de URL siguen igual, y las traducciones se actualizan a la par que tu código.

## Previous and next questions

- Previous question: [¿Cómo traduzco los títulos SEO y las metadescripciones?](https://generaltranslation.com/es/kb/translate-seo-metadata.mdx)
- Next question: [¿Qué es la memoria de traducción y sigue siendo necesaria con la IA?](https://generaltranslation.com/es/kb/what-is-translation-memory.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/es/kb/sitemap.md) for every article.
