SEO//14 min de lecture

Python i18n : De gettext aux Flux de Travail de Traduction Modernes

Eray Gündoğmuş
Partager

Python i18n : De gettext aux Flux de Travail de Traduction Modernes

Python possède l'un des écosystèmes i18n les plus riches de tous les langages de programmation, porté par des décennies d'investissement de grandes applications multilingues — notamment le framework web Django déployé mondialement et l'utilisation répandue de Python dans le calcul scientifique et les outils de données qui doivent servir des communautés internationales.

Ce guide couvre l'ensemble du paysage i18n de Python : depuis l'approche classique gettext intégrée à la bibliothèque standard, en passant par la bibliothèque complète de données de locale Babel, jusqu'aux solutions spécifiques aux frameworks Django et Flask, et les approches modernes avec Fluent.

Options i18n de Python en un Coup d'Œil

Bibliothèque / ApprocheIdéale Pour
gettext (stdlib)Scripts et applications simples
BabelFormatage des nombres, dates, devises ; gestion des fichiers PO
Django i18nApplications web Django
Flask-BabelApplications web Flask
fluent.runtimeApplications utilisant le format Fluent de Mozilla
Babel + gettextLa plupart des services web Python en production

gettext : L'i18n Intégré de Python

La bibliothèque standard de Python inclut gettext, qui implémente l'API d'internationalisation GNU gettext. C'est la fondation sur laquelle de nombreuses bibliothèques de niveau supérieur sont construites.

Comment Fonctionne gettext

gettext utilise des fichiers .po (Portable Object) pour le stockage des traductions et les compile en fichiers binaires .mo (Machine Object) pour le chargement à l'exécution.

Flux de travail :

  1. Marquer les chaînes dans le code source avec _() ou gettext()
  2. Extraire les chaînes marquées avec xgettext ou pybabel vers un modèle .pot
  3. Créer des fichiers .po spécifiques à chaque locale à partir du modèle
  4. Les traducteurs remplissent les traductions
  5. Compiler les fichiers .po en fichiers .mo
  6. Charger à l'exécution selon la locale de l'utilisateur

Utilisation de Base de gettext

import gettext
import locale

def setup_i18n(lang: str) -> gettext.GNUTranslations:
    """Charge les traductions pour la langue donnée."""
    translation = gettext.translation(
        domain='messages',
        localedir='locales',
        languages=[lang],
        fallback=True  # Utilise le msgid (chaîne source) si non trouvé
    )
    return translation

# Configuration de l'application
trans = setup_i18n('fr')
_ = trans.gettext
ngettext = trans.ngettext  # Traduction avec gestion des pluriels

# Utilisation
print(_("Hello, world!"))
print(_("Welcome, %(name)s!") % {"name": "Alice"})

# Formes plurielles
count = 3
print(ngettext(
    "%(count)d item",    # forme singulière
    "%(count)d items",   # forme plurielle
    count                # le nombre qui détermine la forme
) % {"count": count})

Marquage des Chaînes pour l'Extraction

# Marquage direct avec _()
title = _("Dashboard")
error = _("An error occurred: %(message)s") % {"message": str(e)}

# Pour les chaînes devant être définies avant la configuration de l'i18n,
# utilisez un modèle de traduction différée :
def _(s):
    return s  # Aucun effet au moment de la définition

# Ces chaînes sont extraites mais non traduites jusqu'à l'exécution
ERROR_MESSAGES = {
    "not_found": _("Resource not found"),
    "unauthorized": _("You are not authorized to perform this action"),
}

# À l'exécution, traduire avec la vraie fonction _ :
def get_error_message(key: str, translation_func) -> str:
    return translation_func(ERROR_MESSAGES[key])

Babel : La Bibliothèque i18n Complète de Python

Babel étend gettext avec des données de locale complètes basées sur CLDR pour les nombres, devises, dates et plus encore. C'est la bibliothèque i18n la plus complète de Python.

pip install Babel

Formatage des Nombres et des Devises

from babel.numbers import format_number, format_currency, format_percent
from babel import Locale

# Objets Locale
en_us = Locale('en', 'US')
de_de = Locale('de', 'DE')
ja_jp = Locale('ja', 'JP')

# Formatage des nombres
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

# Formatage des devises
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 (sans décimales)

# Pourcentage
print(format_percent(0.8527, locale='en_US'))  # 85%
print(format_percent(0.8527, '#.##%', locale='de_DE'))  # 85,27%

Formatage des Dates et Heures

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)

# Formats de date
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

# Date et heure avec fuseau horaire
tz = get_timezone('Europe/Berlin')
print(format_datetime(dt, locale='de_DE', tzinfo=tz))

# Temps relatif (ex. « il y a 3 heures »)
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

Gestion des Fichiers PO avec Babel

Babel fournit pybabel, un outil en ligne de commande pour gérer les fichiers de traduction :

# 1. Extraire les chaînes traduisibles du code source Python
pybabel extract -F babel.cfg -o messages.pot .

# babel.cfg configure les fichiers à partir desquels extraire :
# [python: **.py]
# [jinja2: **/templates/**.html]

# 2. Initialiser une nouvelle langue (crée locales/fr/LC_MESSAGES/messages.po)
pybabel init -i messages.pot -d locales -l fr

# 3. Après des modifications dans le code source, mettre à jour les fichiers .po existants
pybabel update -i messages.pot -d locales

# 4. Compiler les fichiers .po en fichiers .mo pour l'utilisation à l'exécution
pybabel compile -d locales

Formes Plurielles dans Babel/gettext

# Dans le fichier .po (français) :
# 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 dispose d'un support i18n intégré complet, profondément intégré dans les templates, les modèles et l'ORM.

Configuration 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  # Formatage des nombres/dates selon la locale
USE_TZ = True

LOCALE_PATHS = [BASE_DIR / 'locale']

MIDDLEWARE = [
    'django.middleware.locale.LocaleMiddleware',  # Définit la langue depuis la requête
    # ... autres middleware
]

TEMPLATES = [{
    'OPTIONS': {
        'context_processors': [
            'django.template.context_processors.i18n',
            # ...
        ],
    },
}]

Traduction Django dans le Code Python

from django.utils.translation import gettext as _, ngettext, gettext_lazy as _lazy

# Traduction directe (évaluée immédiatement)
message = _("Welcome!")

# Traduction différée (évaluée à l'accès à la chaîne - pour les champs de modèles et attributs de classe)
class Article(models.Model):
    title = models.CharField(max_length=200)
    
    class Meta:
        verbose_name = _lazy("article")
        verbose_name_plural = _lazy("articles")

# Formes plurielles
def item_message(count: int) -> str:
    return ngettext(
        "You have %(count)d item",
        "You have %(count)d items",
        count
    ) % {"count": count}

# Formatage des chaînes - utiliser des paramètres nommés pour la traduisibilité
def welcome(name: str) -> str:
    return _("Welcome, %(name)s!") % {"name": name}

# Traduction sensible au contexte (même chaîne, sens différent)
from django.utils.translation import pgettext
month = pgettext("month name", "May")  # vs "May" comme verbe

Traduction dans les Templates Django

{% load i18n %}

{# Traduction simple #}
<h1>{% trans "Dashboard" %}</h1>

{# Traduction avec variables #}
{% blocktrans with name=user.first_name %}
  Welcome, {{ name }}!
{% endblocktrans %}

{# Formes plurielles dans les templates #}
{% blocktrans count count=items|length %}
  You have {{ count }} item.
{% plural %}
  You have {{ count }} items.
{% endblocktrans %}

{# Sélecteur de langue #}
{% get_available_languages as LANGUAGES %}
<ul>
  {% for lang_code, lang_name in LANGUAGES %}
    <li>
      <a href="/{{ lang_code }}/">{{ lang_name }}</a>
    </li>
  {% endfor %}
</ul>

Modèles d'URL i18n dans 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')),  # Point de terminaison du sélecteur de langue
]

urlpatterns += i18n_patterns(
    # Ces URLs reçoivent un préfixe de langue : /en/about/, /fr/about/
    path('about/', views.about, name='about'),
    path('products/', include('products.urls')),
    prefix_default_language=False,  # /about/ redirige vers /en/about/
)

Flask i18n avec Flask-Babel

Flask-Babel apporte la puissance de Babel aux applications 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. Vérifier le paramètre URL
    lang = request.args.get('lang')
    if lang:
        return lang
    
    # 2. Vérifier la préférence utilisateur en session/base de données
    if hasattr(g, 'current_user') and g.current_user.language:
        return g.current_user.language
    
    # 3. Utiliser l'en-tête 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)
    
    # Ces fonctions utilisent la locale courante automatiquement
    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}
    )

Approche Moderne : Fluent pour Python

Fluent de Mozilla est disponible en Python via fluent.runtime :

pip install fluent.runtime
from fluent.runtime import FluentBundle, FluentResource

# Charger et utiliser des fichiers 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

# Contenu du fichier 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))

Intégration avec le CI/CD

Les flux de travail i18n de Python s'intègrent naturellement avec les pipelines de localisation continue :

# .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: |
          # Envoyer messages.pot vers votre TMS
          curl -X POST https://api.better-i18n.com/upload \
            -F "file=@messages.pot" \
            -H "Authorization: Bearer ${{ secrets.BETTER_I18N_API_KEY }}"

Pour des modèles CI/CD i18n complets, voir automatisation des pipelines CI/CD i18n.

Bonnes Pratiques de Détection de Locale

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]:
    """Analyse l'en-tête Accept-Language en liste ordonnée de codes de langues."""
    locales = []
    for part in header.split(','):
        parts = part.strip().split(';')
        lang = parts[0].strip()
        # Extraire la qualité (q=0.9) ou utiliser 1.0 par défaut
        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))
    
    # Trier par qualité, la plus haute en premier
    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:
    """Trouve la locale compatible la mieux adaptée pour l'en-tête Accept-Language donné."""
    requested = parse_accept_language(accept_language)
    
    for requested_lang in requested:
        # Correspondance exacte d'abord
        if requested_lang in supported:
            return requested_lang
        
        # Correspondance sur la langue seule (en-US → en)
        base = requested_lang.split('-')[0]
        if base in supported:
            return base
        
        # Trouver toute locale compatible avec la même base
        for supported_locale in supported:
            if supported_locale.startswith(base + '-'):
                return supported_locale
    
    return 'en'  # Fallback par défaut

Pour une couverture plus approfondie des règles de pluralisation dans différentes langues, voir règles de pluralisation dans différentes langues. Pour un aperçu de l'écosystème de gestion des traductions, voir systèmes de gestion des traductions.


Rendez votre application mondiale avec better-i18n

better-i18n combine des traductions propulsées par l'IA, des flux de travail natifs git et une livraison CDN mondiale en une seule plateforme axée sur les développeurs. Arrêtez de gérer des feuilles de calcul et commencez à publier dans toutes les langues.

Commencer gratuitement → · Explorer les fonctionnalités · Lire la documentation

Comments

Loading comments...