Índice
Python i18n: De gettext a Flujos de Trabajo de Traducción Modernos
Python tiene uno de los ecosistemas i18n más ricos de cualquier lenguaje de programación, impulsado por décadas de inversión de grandes aplicaciones multilingües, especialmente el framework web Django desplegado globalmente y el amplio uso de Python en computación científica y herramientas de datos que deben servir a comunidades internacionales.
Esta guía cubre todo el panorama i18n de Python: desde el enfoque clásico gettext integrado en la librería estándar, pasando por la completa librería de datos de localización Babel, hasta las soluciones específicas para Django y Flask, y enfoques modernos usando Fluent.
Opciones de i18n en Python de un Vistazo
| Librería / Enfoque | Mejor Para |
|---|---|
| gettext (stdlib) | Scripts y aplicaciones simples |
| Babel | Formato de números, fechas, divisas; gestión de archivos PO |
| Django i18n | Aplicaciones web Django |
| Flask-Babel | Aplicaciones web Flask |
| fluent.runtime | Aplicaciones que usan el formato Fluent de Mozilla |
| Babel + gettext | La mayoría de servicios web Python en producción |
gettext: El i18n Integrado de Python
La librería estándar de Python incluye gettext, que implementa la API de internacionalización GNU gettext. Es la base sobre la que muchas librerías de nivel superior están construidas.
Cómo Funciona gettext
gettext usa archivos .po (Portable Object) para el almacenamiento de traducciones y los compila a archivos binarios .mo (Machine Object) para carga en tiempo de ejecución.
Flujo de trabajo:
- Marcar cadenas en el código fuente con
_()ogettext() - Extraer las cadenas marcadas con
xgettextopybabela una plantilla.pot - Crear archivos
.poespecíficos de cada localidad desde la plantilla - Los traductores rellenan las traducciones
- Compilar los archivos
.poa archivos.mo - Cargar en tiempo de ejecución según la localidad del usuario
Uso Básico de gettext
import gettext
import locale
def setup_i18n(lang: str) -> gettext.GNUTranslations:
"""Carga las traducciones para el idioma indicado."""
translation = gettext.translation(
domain='messages',
localedir='locales',
languages=[lang],
fallback=True # Usa el msgid (cadena original) si no se encuentra
)
return translation
# Configuración de la aplicación
trans = setup_i18n('fr')
_ = trans.gettext
ngettext = trans.ngettext # Traducción con soporte de plurales
# Uso
print(_("Hello, world!"))
print(_("Welcome, %(name)s!") % {"name": "Alice"})
# Formas plurales
count = 3
print(ngettext(
"%(count)d item", # forma singular
"%(count)d items", # forma plural
count # el número que determina la forma
) % {"count": count})
Marcado de Cadenas para Extracción
# Marcado directo con _()
title = _("Dashboard")
error = _("An error occurred: %(message)s") % {"message": str(e)}
# Para cadenas que deben definirse antes de configurar i18n,
# usa el patrón de traducción diferida:
def _(s):
return s # Sin efecto en tiempo de definición
# Estas cadenas se extraen pero no se traducen hasta el tiempo de ejecución
ERROR_MESSAGES = {
"not_found": _("Resource not found"),
"unauthorized": _("You are not authorized to perform this action"),
}
# En tiempo de ejecución, traduce con la función _ real:
def get_error_message(key: str, translation_func) -> str:
return translation_func(ERROR_MESSAGES[key])
Babel: La Librería i18n Completa de Python
Babel extiende gettext con datos de localización completos basados en CLDR para números, divisas, fechas y más. Es la librería i18n más completa de Python.
pip install Babel
Formato de Números y Divisas
from babel.numbers import format_number, format_currency, format_percent
from babel import Locale
# Objetos Locale
en_us = Locale('en', 'US')
de_de = Locale('de', 'DE')
ja_jp = Locale('ja', 'JP')
# Formato de números
amount = 1234567.89
print(format_number(amount, locale='en_US')) # 1,234,567.89
print(format_number(amount, locale='de_DE')) # 1.234.567,89
print(format_number(amount, locale='en_IN')) # 12,34,567.89
# Formato de divisas
print(format_currency(1234.56, 'USD', locale='en_US')) # $1,234.56
print(format_currency(1234.56, 'EUR', locale='de_DE')) # 1.234,56 €
print(format_currency(1234.56, 'JPY', locale='ja_JP')) # ¥1,235 (sin decimales)
# Porcentaje
print(format_percent(0.8527, locale='en_US')) # 85%
print(format_percent(0.8527, '#.##%', locale='de_DE')) # 85,27%
Formato de Fecha y Hora
from babel.dates import format_date, format_datetime, format_time, get_timezone
from datetime import datetime, date
dt = datetime(2024, 3, 15, 14, 30, 0)
d = date(2024, 3, 15)
# Formatos de fecha
print(format_date(d, locale='en_US')) # Mar 15, 2024
print(format_date(d, locale='de_DE')) # 15.03.2024
print(format_date(d, locale='ja_JP')) # 2024/03/15
print(format_date(d, format='full', locale='fr_FR')) # vendredi 15 mars 2024
# Fecha y hora con zona horaria
tz = get_timezone('Europe/Berlin')
print(format_datetime(dt, locale='de_DE', tzinfo=tz))
# Tiempo relativo (ej. "hace 3 horas")
from babel.dates import format_timedelta
from datetime import timedelta
delta = timedelta(hours=-3)
print(format_timedelta(delta, locale='en_US', add_direction=True)) # 3 hours ago
print(format_timedelta(delta, locale='fr_FR', add_direction=True)) # il y a 3 heures
Gestión de Archivos PO con Babel
Babel proporciona pybabel, una herramienta de línea de comandos para gestionar archivos de traducción:
# 1. Extraer cadenas traducibles del código fuente Python pybabel extract -F babel.cfg -o messages.pot . # babel.cfg configura de qué archivos extraer: # [python: **.py] # [jinja2: **/templates/**.html] # 2. Inicializar un nuevo idioma (crea locales/fr/LC_MESSAGES/messages.po) pybabel init -i messages.pot -d locales -l fr # 3. Tras realizar cambios en el código fuente, actualizar los archivos .po existentes pybabel update -i messages.pot -d locales # 4. Compilar los archivos .po a archivos .mo para uso en tiempo de ejecución pybabel compile -d locales
Formas Plurales en Babel/gettext
# En el archivo .po (francés):
# msgid "%(count)d item"
# msgid_plural "%(count)d items"
# msgstr[0] "%(count)d élément"
# msgstr[1] "%(count)d éléments"
# En Python:
from babel.support import Translations
translations = Translations.load('locales', ['fr'])
ngettext = translations.ngettext
for count in [0, 1, 2, 10]:
msg = ngettext("%(count)d item", "%(count)d items", count) % {"count": count}
print(msg)
Django i18n
Django tiene soporte i18n integrado y completo, profundamente integrado en plantillas, modelos y el ORM.
Configuración de Django i18n
# settings.py
LANGUAGE_CODE = 'en-us'
LANGUAGES = [
('en', 'English'),
('fr', 'Français'),
('de', 'Deutsch'),
('ja', '日本語'),
('ar', 'العربية'),
]
USE_I18N = True
USE_L10N = True # Formato de números/fechas según la localidad
USE_TZ = True
LOCALE_PATHS = [BASE_DIR / 'locale']
MIDDLEWARE = [
'django.middleware.locale.LocaleMiddleware', # Establece el idioma de la solicitud
# ... otros middleware
]
TEMPLATES = [{
'OPTIONS': {
'context_processors': [
'django.template.context_processors.i18n',
# ...
],
},
}]
Traducción Django en Código Python
from django.utils.translation import gettext as _, ngettext, gettext_lazy as _lazy
# Traducción directa (evaluada inmediatamente)
message = _("Welcome!")
# Traducción diferida (evaluada al acceder a la cadena - para campos de modelos y atributos de clase)
class Article(models.Model):
title = models.CharField(max_length=200)
class Meta:
verbose_name = _lazy("article")
verbose_name_plural = _lazy("articles")
# Formas plurales
def item_message(count: int) -> str:
return ngettext(
"You have %(count)d item",
"You have %(count)d items",
count
) % {"count": count}
# Formato de cadenas - usa parámetros nombrados para traducibilidad
def welcome(name: str) -> str:
return _("Welcome, %(name)s!") % {"name": name}
# Traducción sensible al contexto (misma cadena, diferente significado)
from django.utils.translation import pgettext
month = pgettext("month name", "May") # vs "May" como verbo
Traducción en Plantillas Django
{% load i18n %}
{# Traducción simple #}
<h1>{% trans "Dashboard" %}</h1>
{# Traducción con variables #}
{% blocktrans with name=user.first_name %}
Welcome, {{ name }}!
{% endblocktrans %}
{# Formas plurales en plantillas #}
{% blocktrans count count=items|length %}
You have {{ count }} item.
{% plural %}
You have {{ count }} items.
{% endblocktrans %}
{# Selector de idioma #}
{% get_available_languages as LANGUAGES %}
<ul>
{% for lang_code, lang_name in LANGUAGES %}
<li>
<a href="/{{ lang_code }}/">{{ lang_name }}</a>
</li>
{% endfor %}
</ul>
Patrones de URL i18n en Django
# urls.py
from django.conf.urls.i18n import i18n_patterns
from django.urls import path, include
urlpatterns = [
path('i18n/', include('django.conf.urls.i18n')), # Endpoint del selector de idioma
]
urlpatterns += i18n_patterns(
# Estas URLs obtienen un prefijo de idioma: /en/about/, /fr/about/
path('about/', views.about, name='about'),
path('products/', include('products.urls')),
prefix_default_language=False, # /about/ redirige a /en/about/
)
Flask i18n con Flask-Babel
Flask-Babel lleva el poder de Babel a las aplicaciones Flask:
pip install Flask-Babel
# app.py
from flask import Flask, g, request
from flask_babel import Babel, _, ngettext, format_currency, format_datetime
app = Flask(__name__)
app.config['BABEL_DEFAULT_LOCALE'] = 'en'
app.config['BABEL_DEFAULT_TIMEZONE'] = 'UTC'
app.config['BABEL_TRANSLATION_DIRECTORIES'] = 'translations'
def get_locale():
# 1. Verificar parámetro URL
lang = request.args.get('lang')
if lang:
return lang
# 2. Verificar preferencia del usuario en sesión/base de datos
if hasattr(g, 'current_user') and g.current_user.language:
return g.current_user.language
# 3. Usar cabecera Accept-Language
return request.accept_languages.best_match(['en', 'fr', 'de', 'ja'])
babel = Babel(app, locale_selector=get_locale)
@app.route('/products/<int:product_id>')
def product_detail(product_id):
product = Product.query.get_or_404(product_id)
# Estos usan la localidad actual automáticamente
price = format_currency(product.price, 'USD')
created = format_datetime(product.created_at, format='medium')
return render_template('product.html',
product=product,
price=price,
created=created,
title=_("Product: %(name)s") % {"name": product.name}
)
Enfoque Moderno: Fluent para Python
Fluent de Mozilla está disponible en Python a través de fluent.runtime:
pip install fluent.runtime
from fluent.runtime import FluentBundle, FluentResource
# Cargar y usar archivos Fluent
def create_bundle(locale: str, ftl_content: str) -> FluentBundle:
bundle = FluentBundle([locale])
resource = FluentResource(ftl_content)
errors = bundle.add_resource(resource)
if errors:
raise ValueError(f"FTL errors: {errors}")
return bundle
# Contenido del archivo FTL
en_ftl = """
welcome = Welcome to our app!
greeting = Hello, { $name }!
items =
{ $count ->
[0] No items
[one] { $count } item
*[other] { $count } items
}
"""
bundle = create_bundle("en-US", en_ftl)
def translate(bundle: FluentBundle, message_id: str, **kwargs) -> str:
msg = bundle.get_message(message_id)
if not msg or not msg.value:
return message_id
value, errors = bundle.format_pattern(msg.value, kwargs)
return value
print(translate(bundle, "welcome"))
print(translate(bundle, "greeting", name="Alice"))
print(translate(bundle, "items", count=0))
print(translate(bundle, "items", count=1))
print(translate(bundle, "items", count=5))
Integración con CI/CD
Los flujos de trabajo i18n de Python se integran de forma natural con los pipelines de localización continua:
# .github/workflows/i18n.yml
name: i18n
on:
push:
paths:
- '**.py'
- '**/templates/**.html'
jobs:
extract-and-update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Babel
run: pip install Babel
- name: Extract strings
run: pybabel extract -F babel.cfg -o messages.pot .
- name: Update translation files
run: pybabel update -i messages.pot -d locales
- name: Push to translation platform
run: |
# Subir messages.pot a tu TMS
curl -X POST https://api.better-i18n.com/upload \
-F "file=@messages.pot" \
-H "Authorization: Bearer ${{ secrets.BETTER_I18N_API_KEY }}"
Para patrones CI/CD i18n completos, consulta automatización de pipelines CI/CD i18n.
Mejores Prácticas de Detección de Localidad
from babel import Locale, UnknownLocaleError
from typing import Optional
SUPPORTED_LOCALES = ['en', 'fr', 'de', 'ja', 'ar', 'pt-BR']
def parse_accept_language(header: str) -> list[str]:
"""Analiza la cabecera Accept-Language en una lista ordenada de códigos de idioma."""
locales = []
for part in header.split(','):
parts = part.strip().split(';')
lang = parts[0].strip()
# Extraer calidad (q=0.9) o usar 1.0 por defecto
q = 1.0
for param in parts[1:]:
if param.strip().startswith('q='):
try:
q = float(param.strip()[2:])
except ValueError:
pass
locales.append((lang, q))
# Ordenar por calidad, mayor primero
locales.sort(key=lambda x: x[1], reverse=True)
return [lang for lang, _ in locales]
def negotiate_locale(accept_language: str, supported: list[str] = SUPPORTED_LOCALES) -> str:
"""Encuentra la localidad compatible más adecuada para la cabecera Accept-Language dada."""
requested = parse_accept_language(accept_language)
for requested_lang in requested:
# Coincidencia exacta primero
if requested_lang in supported:
return requested_lang
# Coincidencia solo de idioma (en-US → en)
base = requested_lang.split('-')[0]
if base in supported:
return base
# Encontrar cualquier localidad compatible con la misma base
for supported_locale in supported:
if supported_locale.startswith(base + '-'):
return supported_locale
return 'en' # Fallback por defecto
Para una cobertura más profunda de las reglas de pluralización en distintos idiomas, consulta reglas de pluralización en distintos idiomas. Para una visión general del ecosistema de gestión de traducciones, consulta sistemas de gestión de traducciones.
Lleva tu aplicación al mundo con better-i18n
better-i18n combina traducciones impulsadas por IA, flujos de trabajo nativos de git y entrega global por CDN en una plataforma enfocada en desarrolladores. Deja de gestionar hojas de cálculo y empieza a publicar en todos los idiomas.
Empieza gratis → · Explorar funcionalidades · Leer la documentación