SEO//14 min de lectura

Python i18n: De gettext a Flujos de Trabajo de Traducción Modernos

Eray Gündoğmuş
Compartir

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 / EnfoqueMejor Para
gettext (stdlib)Scripts y aplicaciones simples
BabelFormato de números, fechas, divisas; gestión de archivos PO
Django i18nAplicaciones web Django
Flask-BabelAplicaciones web Flask
fluent.runtimeAplicaciones que usan el formato Fluent de Mozilla
Babel + gettextLa 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:

  1. Marcar cadenas en el código fuente con _() o gettext()
  2. Extraer las cadenas marcadas con xgettext o pybabel a una plantilla .pot
  3. Crear archivos .po específicos de cada localidad desde la plantilla
  4. Los traductores rellenan las traducciones
  5. Compilar los archivos .po a archivos .mo
  6. 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

Comments

Loading comments...