Sari la conținut

De ce t-stringuri

Patru moduri de a pune o valoare într-un mesaj traductibil, comparate pe același mesaj. Toate patru își denumesc substituenții și îi lasă pe traducători să îi reordoneze; ele diferă prin ce se întâmplă când o traducere este greșită, prin cât din programul tău poate atinge catalogul și prin cât costă adoptarea lor.

Tabelele vin primele, ca să găsești rândul care te interesează și să citești numai secțiunea din spatele lui.

Trei părți ating fiecare mesaj tradus

Un catalog este fișierul cu traduceri — .po cât timp îl editează oamenii, compilat în .mo pentru a fi încărcat de aplicație (tutorialul le parcurge pe amândouă). Trei părți ating fiecare mesaj: dezvoltatorul scrie șirul sursă, un traducător editează catalogul — adesea pe o platformă externă, departe de orice recenzie de cod — iar aplicația le randează împreună la rulare. Fiecare stil de formatare de mai jos răspunde diferit la aceeași întrebare: cât din limbajul de format ajunge sub controlul catalogului? În exemple, _ este numele convențional al funcției de traducere, iar tr este cel al acestei biblioteci.

Una lângă alta

Când un traducător face o greșeală. Un catalog trece prin multe mâini, iar cea mai mare parte din ce merge prost în el este accidentală:

%(name)s .format() flufl.i18n $name t"…"
O traducere pierde un substituent — ce se randează? valoarea dispare pe tăcute valoarea dispare pe tăcute valoarea dispare pe tăcute mesajul sursă, cu un avertisment (în mod implicit)
O traducere adaugă un substituent necunoscut — ce se randează? o excepție o excepție substituentul rămâne vizibil ca text mesajul sursă, cu un avertisment (în mod implicit)
O traducere reformatează un substituent — ce se randează? ce a cerut catalogul, sau o excepție dacă litera de tip nu se mai potrivește valorii ce a cerut catalogul inexprimabil în $-stringuri mesajul sursă, cu un avertisment
Sunt substituenții verificați la momentul randării? nu nu nu da (vezi mai jos)

Ce autoritate are catalogul. O traducere este date venite din afara depozitului tău, iar fiecare stil îi dă o cantitate diferită de putere:

%(name)s .format() flufl.i18n $name t"…"
De unde vin valorile? dintr-o mapare explicită din argumente explicite din variabilele locale și globale ale apelantului, plus extras opțional din valorile captate înăuntrul t-stringului
Poate catalogul să schimbe felul în care este formatată o valoare? da da nu nu
Poate catalogul să ajungă în interiorul obiectelor (acces la atribute)? nu da da, cu nume cu punct nu
Unde trăiește „limba curentă”? oriunde o pune aplicația oriunde o pune aplicația o stivă de coduri de limbă pe obiectul aplicație partajat o ContextVar, per task sau per cerere

Cât costă integrarea. Tot ce este mai sus vine pe gratis dacă uneltele se potrivesc; aici se poate să nu se potrivească:

%(name)s .format() flufl.i18n $name t"…"
Python minim oricare oricare 3.10 3.14
Maturitate biblioteca standard biblioteca standard lansare stabilă alpha
Folosește cataloage PO/MO obișnuite? da da da da
Are nevoie de un extractor de sursă propriu? nu nu nu da, deocamdată
Ce flag PO deduce Babel, pentru ca uneltele existente să valideze? python-format python-brace-format niciunul python-brace-format

Despre verificarea de la momentul randării: mesajelor la singular li se verifică o potrivire exactă a substituenților. Și mesajelor la plural li se verifică, față de regula de reuniune/intersecție care permite ca formele de plural ale unei limbi țintă să difere de cele ale sursei; verificarea mai strictă, formă cu formă, rulează atunci când cataloagele sunt compilate (Extragere).

Rândul cu flagul de format este despre validarea conștientă de substituenți, nu despre compatibilitatea cataloagelor. niciunul înseamnă că uneltele gettext standard citesc și compilează în continuare mesajul, dar msgfmt --check-format nu are nicio gramatică de substituenți $ pe care să o aplice.

Compatibilitate și maturitate

Primele două rânduri ale ultimului tabel sunt cele care decid adoptarea, așa că merită spuse pe șleau, nu ca celule de tabel.

%-format și .format() sunt încorporate în Python și nu cer nicio dependență. flufl.i18n este un pachet matur, lansat și folosit în producție, care rulează pe Python 3.10 și mai nou. gettext-tstrings este un alpha și cere Python 3.14 sau mai nou, pentru că t-stringurile sunt sintaxă nouă în 3.14 — nu există un back-port și nici nu poate exista. Specificația lui este partea stabilă; API-ul Python se mai poate mișca înainte de 1.0.

Ce nu costă niciunul dintre ele este compatibilitatea cataloagelor. Toate patru produc fișiere POT/PO/MO obișnuite, pe care orice editor de PO, platformă de traducere și unealtă GNU gettext le citește deja, așa că alegerea de mai jos este reversibilă într-un fel în care schimbarea formatului de catalog nu ar fi. Migrare acoperă mutarea unui proiect existent.

Secțiunile de mai jos arată fiecare compromis în detaliu, metodă cu metodă.

%-format

_("Hello %(name)s") % {"name": name}

Ce poate merge prost: un substituent stricat devine o excepție la rulare, dacă validarea catalogului nu îl prinde mai întâi.

Șirul din catalog poartă sintaxă printf, inclusiv o literă de tip la final — s-ul din %(name)s — ușor de trecut cu vederea și ușor de stricat:

>>> "Hello %(name)" % {"name": "Ada"}  # the trailing "s" was deleted
Traceback (most recent call last):
  ...
ValueError: incomplete format

O modificare de un caracter într-un editor PO devine o excepție la rulare, dacă nu o prinde întâi validarea catalogului. GNU msgfmt --check-format chiar o prinde pe aceasta, dar numai pentru mesajele marcate python-format și numai dacă în drumul său către aplicație catalogul trece într-adevăr prin msgfmt.

str.format

_("Hello {name}").format(name=name)

Elimină litera de tip de la final, păstrând totodată un substituent cu nume, liber reordonabil. Ce poate merge prost se mută pe cealaltă parte a schimbului: traducerea capătă putere asupra obiectelor tale.

str.format este un mic limbaj de expresii, iar a-l apela pe un șir înseamnă a-i da acelui șir dreptul să îl folosească:

>>> "{name.__class__.__mro__}".format(name="Ada")
"(<class 'str'>, <class 'object'>)"
>>> settings.api_key = "sk-live-…"
>>> "{conf.api_key}".format(conf=settings)
'sk-live-…'

Acum înlocuiește acele șiruri literale cu orice returnează _(). Dacă o traducere a lui Hello {name} se întoarce ca {conf.api_key}, randarea ei îți tipărește cheia de API — catalogul, nu codul tău, a decis ce anume s-a citit. Un catalog nu este cod, dar călătorește ca datele: afară către o platformă de traducere, prin mai multe mâini, înapoi ca .po, compilat într-un .mo, uneori adus din afara proiectului tău cu totul. .format() dă fiecărui pas al acelei călătorii acces la atributele obiectelor pe care le transmiți.

$-stringuri și flufl.i18n

from flufl.i18n import initialize

_ = initialize("example")

name = "Ada"
print(_("Hello $name"))  # Hello Ada — the value came from the caller's locals

string.Template din biblioteca standard furnizează limbajul de interpolare $name, dar nu este el însuși un API de traducere. flufl.i18n combină acel stil cu căutarea în cataloage gettext. Observă că valoarea nu este niciodată transmisă: flufl.i18n construiește spațiul de nume al substituțiilor din globalele și localele apelantului — orice variabile există la punctul de apel sunt disponibile mesajului. O mapare extras opțională are întâietate față de amândouă. Sintaxa lui văzută de traducător nu are literă de tip la final și nici specificator de format, iar substituenții rămân liber reordonabili.

O substituție indisponibilă nu ridică o excepție. Cu name = "Ada" și fără niciun nombre în spațiul de nume al apelantului, o traducere din catalog a lui Hello $nombre se randează ca Hello $nombre: substituentul nerezolvat rămâne vizibil. Acel comportament documentat păstrează restul mesajului tradus în loc să facă apelul să eșueze. Excepțiile ridicate în timpul rezolvării unui atribut sau al conversiei unei valori se pot propaga totuși.

flufl.i18n este mai capabil decât un string.Template gol într-un fel relevant. Template-ul lui propriu acceptă substituenți cu punct, precum $settings.api_key, iar translator-ul lui rezolvă acele căi față de valorile apelantului. Un substituent tradus poate numi orice locală sau globală disponibilă a apelantului și, cu sintaxa cu punct, îi poate parcurge atributele. Asta este comod atunci când un mesaj are nevoie de un atribut, dar face totodată cadrul apelantului parte din spațiul de nume al substituțiilor catalogului. Comparația de aici descrie flufl.i18n 6.0.0, nu orice utilizare posibilă a lui string.Template.

El răspunde totodată la o întrebare pe care celelalte două stiluri de formatare o lasă în întregime aplicației: care limbă este cea curentă și cum se schimbă. Un obiect aplicație ține o stivă de limbi, _.push(code) și _.pop() o mișcă, with _.using(code): se imbrichează, iar o strategie găsește catalogul pentru un cod de limbă, așa încât aplicația nu manevrează niciodată obiecte de catalog. Un server care trebuie să producă text în mai multe limbi în cadrul unei singure unități de lucru — o pagină pentru cititor, o notificare pentru cineva al cărui cont este setat altfel — este exact cazul pentru care există asta.

Stiva trăiește pe acel obiect aplicație, pe care întregul proces îl împarte. Două cereri care se suprapun împart deci o singură stivă, iar blocurile care nu sunt strict imbricate în timp își dau unul altuia limba greșită:

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']

Biblioteca de față păstrează aceeași capabilitate — legările se imbrichează și se desfac la fel — într-o ContextVar în loc de o stivă partajată, așa că întrepătrunderea de mai sus se rezolvă per task. Echivalentele se află pe Mai multe limbi deodată. Ce nu oferă este căutarea catalogului după codul de limbă: tu treci un obiect translations, care în cazul obișnuit înseamnă un singur apel gettext.translation(), iar biblioteca standard ține în cache catalogul deja parsat.

t-stringuri

tr(t"Hello {name}")

Catalogul vede tot Hello {name} și rămâne un catalog PO/MO obișnuit. Diferența este ce anume are voie să spună o traducere, și cine verifică asta.

Biblioteca de față validează fiecare traducere față de substituenții mesajului sursă înainte de randare, și acceptă nume goale și nimic altceva. Față de t"Hello {name}":

O traducere care conține este respinsă cu
{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

Respins nu înseamnă căzut: în mod implicit biblioteca jurnalizează un avertisment și randează mesajul sursă, așa că un catalog prost nu doboară niciodată aplicația — același contract pe care îl ține gettext însuși.

Formatarea rămâne acolo unde a fost scrisă, în cod:

amount = 1234.5
tr(t"Total: {amount:,.2f}")  # msgid is "Total: {amount}"

:,.2f nu ajunge niciodată la catalog, așa că nicio traducere nu îl poate schimba și niciun traducător nu trebuie să se uite la el. Este însă un format fix, nu unul localizat — alegerea cifrelor și a separatorilor pentru fiecare limbă este treaba lui Babel, dinaintea apelului.

Încă o diferență ține de unelte: t-stringurile sunt sintaxă nouă, așa că extragerea lor într-un .pot cere deocamdată un extractor care știe de t-stringuri, precum cel pe care acest pachet îl oferă pentru Babel.

Costul restricției

Dincolo de cerința de versiune Python, prețul a toate acestea este o singură regulă: o interpolare trebuie să fie un nume simplu.

tr(t"Hello {user.name}")  # raises InvalidTemplateError at the call site
name = user.name  # compute it first
tr(t"Hello {name}")

Aceasta este o constrângere reală, și este chiar constrângerea care produce garanțiile de mai sus. Împreună cu legarea valorilor dinspre sursă și cu verificarea substituenților la rulare, ea împiedică șirurile din catalog să evalueze expresii și păstrează numele substituenților pline de înțeles pentru persoana care le traduce.

Un f-string nu poate fi folosit deloc în acest fel — până când vreo bibliotecă apucă să vadă unul, el este deja un șir terminat, așa că a-l traduce înseamnă a traduce un fragment. T-stringurile (PEP 750) țin separate textul static și valorile, păstrând totodată o sintaxă asemănătoare f-stringurilor și legarea explicită a valorilor.

Cum a ajuns Python aici — două PEP-uri la zece ani distanță și discuția din biblioteca standard care s-a închis fără un răspuns — este povestit, cu surse, în Context.