Pourquoi les t-strings¶
Quatre façons d'insérer une valeur dans un message traduisible, comparées sur la même phrase. Toutes les quatre nomment leurs marqueurs et laissent un traducteur les réordonner ; elles diffèrent par ce qui arrive quand une traduction est fausse, par la part de votre programme que le catalogue peut atteindre, et par ce que leur adoption coûte.
Les tableaux viennent d'abord, pour que vous puissiez repérer la ligne qui vous intéresse et ne lire que la section qui se trouve derrière.
Trois parties touchent chaque message traduit
Un catalogue est le fichier des traductions — .po tant que des
humains l'éditent, compilé en .mo pour que l'application le charge (le
tutoriel parcourt les deux). Trois parties touchent chaque
message : le développeur écrit la chaîne source, un traducteur
édite le catalogue — souvent sur une plateforme externe, loin de toute
revue de code — et l'application rend les deux ensemble à l'exécution.
Chaque style de formatage ci-dessous répond différemment à la même
question : quelle part du langage de formatage le catalogue
contrôle-t-il ? Dans les exemples, _ est le nom conventionnel de la
fonction de traduction, et tr celui de cette bibliothèque.
Comparaison¶
Quand un traducteur se trompe. Un catalogue passe entre beaucoup de mains, et l'essentiel de ce qui y tourne mal est accidentel :
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| Une traduction supprime un marqueur — que rend-on ? | la valeur disparaît silencieusement | la valeur disparaît silencieusement | la valeur disparaît silencieusement | le message source, avec un avertissement (par défaut) |
| Une traduction ajoute un marqueur inconnu — que rend-on ? | une exception | une exception | le marqueur reste visible comme texte | le message source, avec un avertissement (par défaut) |
| Une traduction reformate un marqueur — que rend-on ? | ce que le catalogue a demandé, ou une exception si la lettre de type ne convient plus à la valeur | ce que le catalogue a demandé | inexprimable dans les chaînes $ |
le message source, avec un avertissement |
| Les marqueurs sont-ils vérifiés au moment du rendu ? | non | non | non | oui (voir ci-dessous) |
Quelle autorité détient le catalogue. Une traduction est une donnée venue de l'extérieur de votre dépôt, et chaque style lui confie une quantité de pouvoir différente :
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| D'où viennent les valeurs ? | un mapping explicite | des arguments explicites | les variables locales et globales de l'appelant, plus un extras facultatif |
les valeurs capturées dans la t-string |
| Le catalogue peut-il changer le formatage d'une valeur ? | oui | oui | non | non |
| Le catalogue peut-il fouiller les objets (accès aux attributs) ? | non | oui | oui, avec des noms à points | non |
| Où vit « la langue courante » ? | là où l'application la met | là où l'application la met | une pile de codes de langue sur l'objet application partagé | une ContextVar, par tâche ou par requête |
Ce que coûte l'intégration. Tout ce qui précède est gratuit si l'outillage convient ; c'est ici qu'il pourrait ne pas convenir :
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| Python minimum | n'importe lequel | n'importe lequel | 3.10 | 3.14 |
| Maturité | bibliothèque standard | bibliothèque standard | version stable | alpha |
| Utilise des catalogues PO/MO ordinaires ? | oui | oui | oui | oui |
| Nécessite un extracteur de code source personnalisé ? | non | non | non | oui, actuellement |
| Quel flag PO Babel déduit-il, pour la validation par les outils existants ? | python-format |
python-brace-format |
aucun | python-brace-format |
Sur la vérification au rendu : les messages au singulier sont vérifiés pour une correspondance exacte des marqueurs. Les messages au pluriel le sont aussi, selon la règle union/intersection qui permet aux formes plurielles d'une langue cible de différer de celles de la source ; la vérification plus stricte, forme par forme, s'exécute à la compilation des catalogues (Extraction).
La ligne sur l'indicateur de format concerne la validation qui tient compte des
marqueurs, pas la compatibilité du catalogue. aucun signifie que les outils
gettext standard peuvent toujours lire et compiler le message, mais que
msgfmt --check-format n'a pas de grammaire de marqueurs $ à appliquer.
Compatibilité et maturité¶
Les deux premières lignes du dernier tableau sont celles qui décident de l'adoption : elles méritent d'être énoncées clairement plutôt que rangées dans des cellules.
Le format % et .format() sont intégrés à Python et n'exigent aucune
dépendance. flufl.i18n est un paquet mature, publié et utilisé
en production, qui fonctionne sur Python 3.10 et suivants. gettext-tstrings
est en alpha et exige Python 3.14 ou plus récent, parce que les
t-strings sont une syntaxe nouvelle de la 3.14 — il n'en existe pas de
rétroportage et il ne peut pas en exister. Sa spécification en est
la partie stable ; l'API Python peut encore bouger avant la 1.0.
Ce qu'aucune des quatre ne coûte, c'est la compatibilité des catalogues. Toutes produisent des fichiers POT/PO/MO ordinaires que chaque éditeur PO, chaque plateforme de traduction et chaque outil GNU gettext lit déjà, si bien que le choix ci-dessous est réversible d'une façon qu'un changement de format de catalogue ne serait pas. Migration traite le déplacement d'un projet existant.
Les sections qui suivent détaillent chaque compromis, une méthode à la fois.
Format %¶
Ce qui peut mal tourner : un marqueur endommagé devient une exception à l'exécution, à moins que la validation du catalogue ne l'attrape avant.
La chaîne du catalogue contient la syntaxe printf, notamment une lettre de type
finale — le s de %(name)s — facile à ignorer et facile à endommager :
>>> "Hello %(name)" % {"name": "Ada"} # the trailing "s" was deleted
Traceback (most recent call last):
...
ValueError: incomplete format
Un caractère supprimé dans un éditeur PO devient une exception à l'exécution, à
moins que la validation du catalogue ne l'attrape avant. GNU
msgfmt --check-format attrape bien celle-ci, mais seulement pour les messages
portant le flag python-format, et seulement si le catalogue passe réellement
par msgfmt en chemin vers votre application.
str.format¶
Il supprime la lettre de type finale tout en conservant un marqueur nommé et librement réordonnable. Ce qui peut mal tourner passe de l'autre côté de l'échange : la traduction gagne du pouvoir sur vos objets.
str.format est un petit langage d'expressions : l'appliquer à une chaîne
autorise cette chaîne à l'utiliser.
>>> "{name.__class__.__mro__}".format(name="Ada")
"(<class 'str'>, <class 'object'>)"
>>> settings.api_key = "sk-live-…"
>>> "{conf.api_key}".format(conf=settings)
'sk-live-…'
Remplacez maintenant ces chaînes littérales par ce que renvoie _(). Si une
traduction de Hello {name} revient sous la forme {conf.api_key}, la rendre
imprime votre clé d'API — c'est le catalogue, pas votre code, qui a décidé de
ce qui était lu. Un catalogue n'est pas du code, mais il voyage comme donnée :
plateforme de traduction, plusieurs mains, retour en .po, compilation en
.mo, parfois import depuis un tiers extérieur au projet. .format() donne à
chaque étape de ce trajet la possibilité d'accéder aux attributs des objets
fournis.
Chaînes $ et flufl.i18n¶
from flufl.i18n import initialize
_ = initialize("example")
name = "Ada"
print(_("Hello $name")) # Hello Ada — the value came from the caller's locals
Le module standard string.Template fournit le langage
d'interpolation $name, mais ne constitue pas en lui-même une API de
traduction. flufl.i18n associe ce style à la recherche dans les
catalogues gettext. Remarquez que la valeur n'est jamais passée en argument :
flufl.i18n construit l'espace de noms de substitution à partir des variables
globales et locales de l'appelant — toutes les variables qui existent au point
d'appel sont disponibles pour le message. Un mapping extras facultatif prend
le pas sur les deux. La syntaxe destinée aux traducteurs ne comporte ni lettre
de type finale ni spécificateur de format, et les marqueurs restent librement
réordonnables.
Une substitution indisponible ne lève pas d'exception. Avec name = "Ada" et
sans nombre dans l'espace de noms de l'appelant, une traduction de catalogue
Hello $nombre donne Hello $nombre : le marqueur non résolu reste visible.
Ce comportement documenté préserve le reste du message
traduit au lieu de faire échouer l'appel. Les exceptions levées pendant la
résolution d'un attribut ou la conversion d'une valeur peuvent néanmoins se
propager.
Sur un point pertinent, flufl.i18n est plus puissant qu'un
string.Template brut. Son Template personnalisé accepte
des marqueurs avec des points, comme $settings.api_key, et son
traducteur résout ces chemins sur les valeurs de l'appelant. Un
marqueur traduit peut désigner toute variable locale ou globale disponible de
l'appelant et, avec la syntaxe à points, parcourir ses attributs. C'est
pratique lorsqu'un message a besoin d'un attribut, tout en faisant du cadre de
l'appelant une partie de l'espace de noms de substitution du catalogue. La
comparaison faite ici décrit flufl.i18n 6.0.0, pas tous les usages possibles
de string.Template.
Il répond aussi à une question que les deux autres styles de formatage
laissent entièrement à l'application : quelle langue est courante, et
comment en changer. Un objet application conserve une
pile de langues, _.push(code) et _.pop() la déplacent, with _.using(code):
s'imbrique, et une stratégie trouve le catalogue correspondant à un
code de langue, si bien que l'application ne manipule jamais d'objets catalogue.
Un serveur qui doit produire du texte dans plusieurs langues au cours d'une même
unité de travail — une page pour le lecteur, une notification pour quelqu'un
dont le compte est réglé autrement — est précisément le cas pour lequel cela
existe.
La pile vit sur cet objet application, que tout le processus partage. Deux requêtes qui se chevauchent partagent donc une seule pile, et des blocs qui ne sont pas strictement imbriqués dans le temps se refilent la mauvaise langue :
async def greet(code, delay):
with _.using(code):
await asyncio.sleep(delay)
return _("Hello $name")
async def main():
return await asyncio.gather(greet("fr", 0.01), greet("ja", 0.02))
>>> asyncio.run(main()) # "fr" entered first and left first, so it read "ja" off the top
['こんにちは Ada', 'Bonjour Ada']
Cette bibliothèque conserve la même capacité — les liaisons s'imbriquent et se
dénouent de la même façon — dans une ContextVar plutôt que dans une pile
partagée, si bien que l'entrelacement ci-dessus se résout par tâche. Les
équivalents figurent sur
Plusieurs langues à la fois. Ce qu'elle
ne fournit pas, c'est la recherche du catalogue à partir d'un code de langue :
vous passez un objet translations, qui dans le cas courant tient en un seul
appel à gettext.translation(), et la bibliothèque standard met en cache le
catalogue analysé.
t-strings¶
Le catalogue voit toujours Hello {name} et reste un catalogue PO/MO ordinaire.
La différence tient à ce qu'une traduction a le droit de dire, et à qui le
vérifie.
Cette bibliothèque valide chaque traduction par rapport aux marqueurs du
message source avant le rendu, et n'accepte que des noms simples, rien
d'autre. Face à t"Hello {name}" :
| Une traduction contenant | est rejetée avec |
|---|---|
{name.__class__.__mro__} |
placeholder {name.__class__.__mro__} must be a plain name, copied from the source message unchanged |
{name!r} |
placeholder {name} adds formatting; write {name} on its own, because the source message decides how the value is formatted |
{0} |
placeholder {0} must be a plain name, copied from the source message unchanged |
{nombre} |
translation does not match the source placeholders: {name} is missing; {nombre} is not in the source message |
Rejetée ne veut pas dire plantée : par défaut, la bibliothèque journalise un avertissement et rend le message source, si bien qu'un mauvais catalogue ne fait jamais tomber l'application — le contrat que gettext lui-même respecte.
Le formatage reste là où il a été écrit, dans le code :
:,.2f n'atteint jamais le catalogue : aucune traduction ne peut le modifier
et aucun traducteur n'a à le regarder. C'est cependant un format fixe, pas un
format localisé — choisir les chiffres et les séparateurs par langue est
le travail de Babel, avant l'appel.
Une dernière différence concerne l'outillage : les t-strings sont une syntaxe
nouvelle, donc les extraire vers un .pot demande aujourd'hui un extracteur
qui les comprend, comme celui que ce paquet
fournit pour Babel.
Le coût de la restriction¶
Au-delà de l'exigence sur Python, le prix de tout cela tient en une règle : une interpolation doit être un nom simple.
C'est une véritable contrainte, et c'est la contrainte même qui produit les garanties ci-dessus. Avec la liaison des valeurs côté source et la vérification des marqueurs à l'exécution, elle empêche les chaînes du catalogue d'évaluer des expressions et conserve aux noms de marqueurs un sens pour la personne qui les traduit.
Une f-string ne peut pas du tout être utilisée ainsi : lorsqu'une bibliothèque la reçoit, c'est déjà une chaîne terminée, donc la traduire revient à traduire un fragment. Les t-strings (PEP 750) gardent le texte statique et les valeurs séparés, tout en conservant une syntaxe proche des f-strings et une liaison explicite des valeurs.
Comment Python en est arrivé là — deux PEP à dix ans d'écart, et la discussion sur la bibliothèque standard close sans réponse — est raconté, sources à l'appui, sur la page Contexte.