Skip to main content

i18n en Flask: cree una aplicación multilingüe con Flask-Babel

De los fundamentos de Gettext a producción: internacionalice su aplicación Flask con Flask-Babel, archivos PO y traducción automatizada mediante IA.

1

Instalar Flask-Babel

Flask-Babel es la extensión estándar de internacionalización para Flask. Integra GNU Gettext con Flask y las plantillas Jinja2, y ofrece de serie funciones de traducción, selección regional y compatibilidad con zonas horarias.

Flask-Babel envuelve Babel —la biblioteca de i18n para Python— y lo integra en el ciclo de solicitudes de Flask. Le proporciona gettext(), ngettext() y lazy_gettext(), además de detección automática desde las cabeceras del navegador.
Terminal
pip install Flask-Babel
2

Configurar Babel

Cree un archivo babel.cfg para indicar a pybabel dónde buscar cadenas traducibles e inicialice Flask-Babel con una función selectora que determine qué idioma servir en cada solicitud.

babel.cfg
# babel.cfg — tells pybabel where to find translatable strings
[python: **.py]
[jinja2: **/templates/**.html]
extensions=jinja2.ext.autoescape,jinja2.ext.with_
app.py
from flask import Flask, request
from flask_babel import Babel

app = Flask(__name__)
app.config['BABEL_DEFAULT_LOCALE'] = 'en'
app.config['BABEL_DEFAULT_TIMEZONE'] = 'UTC'
# Directory where translations live (default: "translations")
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'

def get_locale():
    # 1. Check URL parameter or session
    # 2. Fall back to browser Accept-Language header
    return request.accept_languages.best_match(['en', 'de', 'ja', 'es', 'fr'])

babel = Babel(app, locale_selector=get_locale)
La función locale_selector se llama en cada solicitud. Si devuelve una configuración sin archivo .mo compilado, Flask-Babel recurre silenciosamente a la predeterminada. No lanza ningún error: las cadenas solo aparecen sin traducir.
3

Marcar cadenas para traducir

Envuelva cada cadena dirigida al usuario con gettext() en Python y _() en plantillas Jinja2. Utilice lazy_gettext() para cadenas definidas al cargar el módulo —como etiquetas de formularios y configuración— que deban traducirse después, al recibir una solicitud.

app.py
from flask_babel import gettext, ngettext, lazy_gettext

# In views — gettext() for immediate translation
@app.route('/')
def index():
    flash(gettext('Your profile has been updated.'))
    return render_template('index.html',
        title=gettext('Home'))

# In forms/config — lazy_gettext() for deferred translation
class LoginForm(FlaskForm):
    username = StringField(lazy_gettext('Username'))
    password = PasswordField(lazy_gettext('Password'))
    submit = SubmitField(lazy_gettext('Sign In'))
templates/index.html
{# In Jinja2 templates, use _() shorthand for gettext #}
<h1>{{ _('Welcome to our app') }}</h1>

<p>{{ _('Hello, %(name)s!', name=user.name) }}</p>

<footer>
  {{ _('Copyright %(year)s Example Corp.', year=2026) }}
</footer>
Utilice _() en las plantillas Jinja2 en vez de gettext(): es la abreviatura estándar y mantiene limpias las plantillas. Flask-Babel registra automáticamente _() como global de Jinja2.
4

Extraer mensajes

Ejecute pybabel extract para buscar cadenas traducibles en el código fuente y las plantillas. Crea un archivo .pot (Portable Object Template). Después, inicialice catálogos para cada idioma de destino o actualice los existentes cuando cambien las cadenas de origen.

Terminal
# Extract translatable strings from source code
pybabel extract -F babel.cfg -k lazy_gettext -o messages.pot .

# Initialize a new language (first time only)
pybabel init -i messages.pot -d translations -l de
pybabel init -i messages.pot -d translations -l ja
pybabel init -i messages.pot -d translations -l es

# Update existing catalogs when source strings change
pybabel update -i messages.pot -d translations
Ejecute siempre pybabel update —no init— después de la primera extracción. Ejecutar init sobre un directorio de idioma existente sobrescribe todas sus traducciones. update combina las cadenas nuevas y conserva las existentes.
5

Traducir archivos PO

Abra los archivos .po generados y rellene los valores msgstr de cada msgid. Los PO son texto sin formato: puede editarlos directamente, usar un editor como Poedit o automatizar la traducción con IA.

translations/de/LC_MESSAGES/messages.po
# translations/de/LC_MESSAGES/messages.po
msgid "Welcome to our app"
msgstr "Willkommen in unserer App"

msgid "Hello, %(name)s!"
msgstr "Hallo, %(name)s!"

msgid "Your profile has been updated."
msgstr "Ihr Profil wurde aktualisiert."

msgid "Username"
msgstr "Benutzername"

msgid "Password"
msgstr "Passwort"

msgid "Sign In"
msgstr "Anmelden"
Los archivos PO incluyen comentarios de contexto —líneas que empiezan por #:— que indican dónde se utiliza cada cadena. Los traductores los emplean para comprender el contexto. Consérvelos: pybabel extract los genera automáticamente.
6

Compilar traducciones

Compile los archivos .po como binarios .mo mediante pybabel compile. Flask-Babel lee .mo durante la ejecución; no puede leer .po directamente. Debe volver a compilar después de cada actualización.

Terminal
# Compile .po files to binary .mo files (required at runtime)
pybabel compile -d translations

# Flask-Babel reads .mo files, not .po files.
# You MUST compile after every translation update.
Si las traducciones no aparecen después de editar un .po, casi con total seguridad olvidó ejecutar pybabel compile. Es el problema más habitual de Flask-Babel. Añada ese paso al script de implementación para evitarlo en producción.
7

Gestionar plurales y variables

Utilice ngettext() para cadenas sensibles al plural. Recibe una forma singular, otra plural y el número. Babel aplica automáticamente la regla correcta: el inglés tiene 2 formas, el ruso 3, el árabe 6 y el japonés 1.

app.py
from flask_babel import ngettext

@app.route('/cart')
def cart():
    count = len(session.get('cart_items', []))
    message = ngettext(
        '%(num)d item in your cart',     # singular
        '%(num)d items in your cart',    # plural
        count                             # determines which form
    )
    return render_template('cart.html', message=message)
Plural forms in .po files
# English: 2 forms (nplurals=2)
msgid "%(num)d item in your cart"
msgid_plural "%(num)d items in your cart"
msgstr[0] "%(num)d item in your cart"
msgstr[1] "%(num)d items in your cart"

# German: 2 forms (nplurals=2)
msgid "%(num)d item in your cart"
msgid_plural "%(num)d items in your cart"
msgstr[0] "%(num)d Artikel in Ihrem Warenkorb"
msgstr[1] "%(num)d Artikel in Ihrem Warenkorb"

# Japanese: 1 form (nplurals=1)
msgid "%(num)d item in your cart"
msgid_plural "%(num)d items in your cart"
msgstr[0] "カートに%(num)d個の商品があります"

# Russian: 3 forms (nplurals=3)
msgid "%(num)d item in your cart"
msgid_plural "%(num)d items in your cart"
msgstr[0] "%(num)d товар в вашей корзине"
msgstr[1] "%(num)d товара в вашей корзине"
msgstr[2] "%(num)d товаров в вашей корзине"
Nunca utilice if count == 1 para la lógica de plural. Idiomas como el francés consideran singular el 0, y el ruso y el árabe tienen formas que no existen en inglés. Deje la selección a ngettext() y las reglas CLDR de Babel.
8

Añadir el cambio de configuración regional

Cree un selector de idioma que guarde la elección del usuario en la sesión de Flask. Actualice locale_selector para comprobar primero la sesión y recurrir después a la detección del navegador.

app.py
from flask import session, redirect, url_for, request
from flask_babel import refresh

@app.route('/set-language/<lang>')
def set_language(lang):
    session['lang'] = lang
    refresh()  # Force Flask-Babel to re-read the locale
    return redirect(request.referrer or url_for('index'))

# Update get_locale to check session first
def get_locale():
    # 1. Explicit user choice (stored in session)
    if 'lang' in session:
        return session['lang']
    # 2. Browser Accept-Language header
    return request.accept_languages.best_match(
        ['en', 'de', 'ja', 'es', 'fr']
    )
templates/components/language_switcher.html
{# Language switcher component #}
<nav class="language-switcher">
  {% for lang, name in [('en','English'),('de','Deutsch'),
                         ('ja','日本語'),('es','Español'),
                         ('fr','Français')] %}
    <a href="{{ url_for('set_language', lang=lang) }}"
       class="{{ 'active' if get_locale() == lang }}">
      {{ name }}
    </a>
  {% endfor %}
</nav>
Tras cambiar la región de la sesión, llame a flask_babel.refresh() para obligar a Flask-Babel a leerla de nuevo en la solicitud actual. Sin refresh(), la anterior persiste hasta la siguiente solicitud.
9

Automatizar traducciones

Cuando termine de configurar Flask-Babel, traduzca sus archivos PO con IA. Automatice en CI/CD el ciclo extraer-traducir-compilar para mantener las traducciones sincronizadas con el código fuente.

Terminal
# Translate your PO files with AI directly from your IDE
# or use the CLI in CI/CD:
npx i18n-agent translate translations/de/LC_MESSAGES/messages.po \
  --source-lang en --target-lang de

# Bulk translate all languages at once:
npx i18n-agent translate messages.pot --lang de,ja,es,fr

# Then compile:
pybabel compile -d translations
Traduzca de forma incremental: cuando añada cadenas y ejecute pybabel update, traduzca solo las entradas nuevas sin traducir —valores msgstr vacíos— en vez de volver a generar todo. Así conserva las traducciones revisadas por personas.

Extra: respaldo regional inteligente con flask-babel-locale-chain

De forma predeterminada, Flask-Babel pasa directamente a la configuración regional predeterminada cuando no está disponible la preferida. Un usuario pt-BR que solo dispone de pt-PT ve inglés en vez de portugués. flask-babel-locale-chain añade cadenas configurables para que las regiones relacionadas se encadenen con naturalidad.

app.py
# pip install flask-babel-locale-chain
from flask_babel_locale_chain import LocaleChain

# Define fallback chains: pt-BR falls back to pt before en
locale_chain = LocaleChain({
    'pt-BR': ['pt-BR', 'pt', 'en'],
    'pt-PT': ['pt-PT', 'pt', 'en'],
    'zh-Hant': ['zh-Hant', 'zh-Hans', 'en'],
    'en-GB': ['en-GB', 'en', 'en-US'],
})

def get_locale():
    requested = request.accept_languages.best_match(
        ['en', 'pt-BR', 'pt', 'zh-Hant', 'zh-Hans']
    )
    # Returns the best available locale from the chain
    return locale_chain.resolve(requested)
flask-babel-locale-chain es un paquete Python de código abierto. Véalo en GitHub: github.com/i18n-agent/flask-babel-locale-chain.

Automatizar la calidad de la traducción

Detecte las claves ausentes y los marcadores rotos antes de publicar con i18n-validate. Pruebe la interfaz con pseudotraducciones mediante i18n-pseudo antes de recibir las reales.

Errores habituales

Olvidar compilar .po como .mo

Flask-Babel lee archivos .mo compilados, no .po. Si las traducciones no aparecen tras editar los .po, ejecute pybabel compile -d translations. Añada este paso a su script de implementación.

Utilizar gettext() a nivel de módulo

gettext() requiere un contexto de solicitud. Si define cadenas traducidas a nivel de módulo —atributos de clase o constantes—, utilice lazy_gettext(). Aplaza la traducción hasta que la cadena se renderiza en una solicitud.

La extracción omite cadenas

pybabel extract solo analiza los archivos que coinciden con los patrones de babel.cfg. Si no extrae cadenas, compruebe que los patrones coincidan con su estructura, añada -k lazy_gettext para las llamadas diferidas y asegúrese de que las plantillas Jinja2 tengan la extensión correcta (.html o .jinja2).

Errores de codificación en archivos PO

Los archivos PO deben utilizar UTF-8. Si ve UnicodeDecodeError, compruebe la cabecera Content-Type del .po: debe indicar charset=UTF-8. Algunos editores guardan con otras codificaciones; verifíquelo siempre después de editar.

Estructura de archivos recomendada

Project Structure
my-flask-app/
├── app.py                          # Flask app with Babel config
├── babel.cfg                       # Extraction config
├── messages.pot                    # Template (extracted strings)
├── translations/
│   ├── de/
│   │   └── LC_MESSAGES/
│   │       ├── messages.po         # German translations (editable)
│   │       └── messages.mo         # Compiled binary (generated)
│   ├── ja/
│   │   └── LC_MESSAGES/
│   │       ├── messages.po
│   │       └── messages.mo
│   └── es/
│       └── LC_MESSAGES/
│           ├── messages.po
│           └── messages.mo
├── templates/
│   ├── base.html
│   ├── index.html
│   └── components/
│       └── language_switcher.html
├── requirements.txt
└── venv/

Pruebe i18n Agent ahora

Arrastre y suelte aquí su archivo de traducción

JSON, YAML, PO, XML, CSV, Markdown, Properties

o haga clic para seleccionar

Idiomas de destino

No es necesario registrarsePresupuesto al instante

Preguntas frecuentes sobre i18n en Flask