Table des matières
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 / Approche | Idéale Pour |
|---|---|
| gettext (stdlib) | Scripts et applications simples |
| Babel | Formatage des nombres, dates, devises ; gestion des fichiers PO |
| Django i18n | Applications web Django |
| Flask-Babel | Applications web Flask |
| fluent.runtime | Applications utilisant le format Fluent de Mozilla |
| Babel + gettext | La 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 :
- Marquer les chaînes dans le code source avec
_()ougettext() - Extraire les chaînes marquées avec
xgettextoupybabelvers un modèle.pot - Créer des fichiers
.pospécifiques à chaque locale à partir du modèle - Les traducteurs remplissent les traductions
- Compiler les fichiers
.poen fichiers.mo - 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