Proč t-stringy¶
Čtyři způsoby, jak vložit hodnotu do přeložitelné zprávy, porovnané na téže zprávě. Všechny čtyři své zástupné symboly pojmenovávají a dovolují překladateli je přeuspořádat; liší se v tom, co se stane, když je překlad špatně, jak velké části vašeho programu katalog dosáhne a co stojí jejich zavedení.
Tabulky jsou na začátku, abyste našli řádek, který vás zajímá, a přečetli si jen ten oddíl, který za ním stojí.
Každé přeložené zprávy se dotýkají tři strany
Katalog je soubor s překlady — .po, dokud jej upravují lidé,
zkompilovaný do .mo, který načítá aplikace (tutoriál
projde oběma). Každé zprávy se dotýkají tři strany: vývojář píše
zdrojový řetězec, překladatel upravuje katalog — často na externí
platformě, daleko od jakéhokoli code review — a aplikace obojí
vykresluje dohromady za běhu. Každý styl formátování níže odpovídá na
tutéž otázku jinak: jak velkou část formátovacího jazyka smí katalog
ovládat? V příkladech je _ konvenční jméno překládací funkce a tr
je jméno z této knihovny.
Vedle sebe¶
Když překladatel udělá chybu. Katalog projde mnoha rukama a většina toho, co se v něm pokazí, je nechtěná:
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| Překlad zástupný symbol vypustí — co se vykreslí? | hodnota tiše zmizí | hodnota tiše zmizí | hodnota tiše zmizí | zdrojový text, s varováním (ve výchozím nastavení) |
| Překlad přidá neznámý zástupný symbol — co se vykreslí? | výjimka | výjimka | zástupný symbol zůstane viditelný jako text | zdrojový text, s varováním (ve výchozím nastavení) |
| Překlad zástupný symbol přeformátuje — co se vykreslí? | to, oč katalog požádal, nebo výjimka, pokud písmeno typu už k hodnotě nesedí | to, oč katalog požádal | v $-stringech nevyjádřitelné |
zdrojový text, s varováním |
| Kontrolují se zástupné symboly při vykreslování? | ne | ne | ne | ano (viz níže) |
Jakou pravomoc má katalog. Překlad jsou data zvenčí vašeho repozitáře a každý styl jim předává jinou míru moci:
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| Odkud pocházejí hodnoty? | explicitní mapování | explicitní argumenty | lokální a globální proměnné volajícího, plus volitelné extras |
hodnoty zachycené uvnitř t-stringu |
| Může katalog změnit způsob formátování hodnoty? | ano | ano | ne | ne |
| Může katalog sahat do objektů (přístup k atributům)? | ne | ano | ano, tečkovanými jmény | ne |
| Kde žije „aktuální jazyk“? | tam, kam si jej aplikace uloží | tam, kam si jej aplikace uloží | zásobník kódů jazyků na sdíleném objektu aplikace | ContextVar, pro každou úlohu či požadavek |
Co stojí integrace. Všechno výše je zadarmo, pokud sedí nástroje; tady sedět nemusí:
%(name)s |
.format() |
flufl.i18n $name |
t"…" |
|
|---|---|---|---|---|
| Minimální Python | libovolný | libovolný | 3.10 | 3.14 |
| Zralost | standardní knihovna | standardní knihovna | stabilní vydání | alfa |
| Používá obyčejné katalogy PO/MO? | ano | ano | ano | ano |
| Potřebuje vlastní extraktor zdrojů? | ne | ne | ne | ano, zatím |
| Jaký PO příznak odvodí Babel, aby existující nástroje validovaly? | python-format |
python-brace-format |
žádný | python-brace-format |
Ke kontrole při vykreslování: zprávy v jednotném čísle se kontrolují na přesnou shodu zástupných symbolů. Zprávy v množném čísle také — vůči pravidlu sjednocení a průniku, které dovoluje, aby se tvary množného čísla cílového jazyka lišily od zdrojových; přísnější kontrola po jednotlivých tvarech běží při kompilaci katalogů (Extrakce).
Řádek o formátovacím příznaku se týká validace znalé zástupných symbolů,
nikoli kompatibility katalogů. žádný znamená, že standardní nástroje
gettext zprávu stále přečtou a zkompilují, ale msgfmt --check-format
nemá žádnou gramatiku $-symbolů, kterou by mohl uplatnit.
Kompatibilita a zralost¶
První dva řádky poslední tabulky jsou ty, které rozhodují o zavedení, takže stojí za to je vyslovit naplno, a ne jen jako buňky.
%-formát a .format() jsou zabudované v Pythonu a nepotřebují vůbec
žádnou závislost. flufl.i18n je zralý balíček, vydaný
a používaný v produkci, který běží na Pythonu 3.10 a novějším.
gettext-tstrings je alfa a vyžaduje Python 3.14 nebo novější,
protože t-stringy jsou v 3.14 nová syntaxe — neexistuje žádný back-port
a ani existovat nemůže. Stabilní částí je jeho specifikace;
Python API se před 1.0 ještě může měnit.
Co nestojí ani jeden z nich, je kompatibilita katalogů. Všechny čtyři produkují obyčejné soubory POT/PO/MO, které už dnes přečte každý PO editor, každá překladatelská platforma i každý nástroj GNU gettext, takže volba níže je vratná způsobem, jakým by změna formátu katalogů nebyla. Migrace se věnuje přesunu existujícího projektu.
Oddíly níže ukazují každý kompromis podrobně, metodu po metodě.
%-formát¶
Co se může pokazit: poškozený zástupný symbol se za běhu změní ve výjimku, pokud jej dřív nezachytí validace katalogu.
Řetězec v katalogu nese syntaxi printf, včetně koncového písmene typu —
s v %(name)s — které lze snadno přehlédnout a snadno poškodit:
>>> "Hello %(name)" % {"name": "Ada"} # the trailing "s" was deleted
Traceback (most recent call last):
...
ValueError: incomplete format
Jednoznaková úprava v PO editoru se stane výjimkou za běhu, pokud ji
dřív nezachytí validace katalogu. GNU msgfmt --check-format právě tuhle
zachytí, ale jen u zpráv označených příznakem python-format, a jen
pokud katalog na cestě do vaší aplikace skutečně projde přes msgfmt.
str.format¶
Odstraňuje koncové písmeno typu a zachovává pojmenovaný, volně přeuspořádatelný zástupný symbol. To, co se může pokazit, se přesouvá na druhou stranu výměny: překlad získává moc nad vašimi objekty.
str.format je malý výrazový jazyk a jeho zavolání na řetězci znamená
předat tomu řetězci právo jej použít:
>>> "{name.__class__.__mro__}".format(name="Ada")
"(<class 'str'>, <class 'object'>)"
>>> settings.api_key = "sk-live-…"
>>> "{conf.api_key}".format(conf=settings)
'sk-live-…'
Teď ty literální řetězce nahraďte tím, co vrací _(). Pokud se překlad
Hello {name} vrátí jako {conf.api_key}, jeho vykreslení vypíše váš
API klíč — o tom, co se přečetlo, rozhodl katalog, ne váš kód. Katalog
není kód, ale cestuje jako data: na překladatelskou platformu, přes mnoho
rukou, zpět jako .po, zkompilovaný do .mo, někdy dodaný zcela zvnějšku
vašeho projektu. .format() dává každému kroku této cesty přístup
k atributům objektů, které předáváte.
$-stringy a flufl.i18n¶
from flufl.i18n import initialize
_ = initialize("example")
name = "Ada"
print(_("Hello $name")) # Hello Ada — the value came from the caller's locals
Standardní knihovna v string.Template poskytuje
interpolační jazyk $name, sama však není překladovým API.
flufl.i18n tento styl kombinuje s vyhledáváním v katalozích
gettext. Všimněte si, že hodnota se nikdy nepředává: flufl.i18n sestavuje
substituční prostor jmen z globálních a lokálních proměnných volajícího —
zprávě jsou dostupné všechny proměnné existující v místě volání. Volitelné
mapování extras má přednost před oběma. Syntaxe, kterou vidí překladatel,
nemá koncové písmeno typu ani formátovací specifikátor a zástupné symboly
zůstávají volně přeuspořádatelné.
Nedostupná substituce nevyvolá výjimku. Při name = "Ada" a bez nombre
v prostoru jmen volajícího se katalogový překlad Hello $nombre vykreslí
jako Hello $nombre: nevyřešený zástupný symbol zůstane viditelný. Toto
dokumentované chování zachová zbytek přeložené
zprávy, místo aby volání selhalo. Výjimky vyvolané při řešení atributu
nebo převodu hodnoty se stále mohou šířit dál.
flufl.i18n je v jednom podstatném ohledu schopnější než holý
string.Template. Jeho vlastní Template přijímá
tečkované zástupné symboly, jako je $settings.api_key, a jeho
translator tyto cesty řeší vůči hodnotám volajícího.
Přeložený zástupný symbol může pojmenovat kteroukoli dostupnou lokální
nebo globální proměnnou volajícího a tečkovanou syntaxí procházet její
atributy. To je pohodlné, když zpráva potřebuje atribut, zároveň to však
činí rámec volajícího součástí substitučního prostoru katalogu. Srovnání
zde popisuje flufl.i18n 6.0.0, nikoli každé možné použití
string.Template.
Odpovídá také na otázku, kterou zbylé dva formátovací styly nechávají zcela
na aplikaci: který jazyk právě platí a jak jej změnit. Objekt
aplikace drží zásobník jazyků, _.push(code) a _.pop()
s ním pohybují, with _.using(code): je vnořuje a strategie
najde katalog pro daný kód jazyka, takže aplikace sama nikdy nezachází
s objekty katalogu. Server, který musí během jediné jednotky práce vytvořit
text ve více než jednom jazyce — stránku pro čtenáře, oznámení pro někoho,
kdo má účet nastavený jinak —, je právě ten případ, kvůli němuž to existuje.
Zásobník žije na onom objektu aplikace, který sdílí celý proces. Dva překrývající se požadavky tedy sdílejí jeden zásobník a bloky, které nejsou striktně vnořené v čase, si navzájem podávají špatný jazyk:
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']
Tato knihovna zachovává tutéž schopnost — vazby se vnořují a rozvinují
stejným způsobem —, ale drží ji v ContextVar místo ve sdíleném zásobníku,
takže se výše uvedené prokládání vyřeší pro každou úlohu zvlášť. Ekvivalenty
najdete na stránce
Několik jazyků naráz. Co nedodává, je
vyhledání katalogu podle kódu jazyka: předáte objekt s překlady, což je
v běžném případě jediné volání gettext.translation(), a standardní knihovna
si rozparsovaný katalog uloží do cache.
t-stringy¶
Katalog stále vidí Hello {name} a zůstává obyčejným katalogem PO/MO.
Rozdíl je v tom, co překlad smí říct a kdo to kontroluje.
Tato knihovna validuje každý překlad vůči zástupným symbolům zdrojové
zprávy před vykreslením a přijímá holá jména — a nic jiného. Vůči
t"Hello {name}":
| Překlad obsahující | je odmítnut se zprávou |
|---|---|
{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 |
Odmítnutý neznamená spadlý: ve výchozím nastavení knihovna zaloguje varování a vykreslí zdrojový text, takže špatný katalog nikdy nepoloží aplikaci — tentýž kontrakt, který dodržuje sám gettext.
Formátování zůstává tam, kde bylo napsáno, v kódu:
:,.2f se do katalogu nikdy nedostane, takže jej žádný překlad nemůže
změnit a žádný překladatel se na něj nemusí dívat. Je to ovšem formát
pevný, nikoli lokalizovaný — volba číslic a oddělovačů podle jazyka je
úkol Babelu, ještě před voláním.
Ještě jeden rozdíl jsou nástroje: t-stringy jsou nová syntaxe, takže
jejich extrakce do .pot v současnosti vyžaduje extraktor, který
t-stringům rozumí, například ten, který tento balíček
poskytuje pro Babel.
Cena za omezení¶
Kromě požadavku na Python je cenou toho všeho jediné pravidlo: interpolace musí být prosté jméno.
To je skutečné omezení a je to totéž omezení, které produkuje záruky uvedené výše. Spolu s vázáním hodnot na straně zdroje a kontrolou zástupných symbolů za běhu brání tomu, aby řetězce z katalogu vyhodnocovaly výrazy, a udržuje jména zástupných symbolů srozumitelná pro toho, kdo je překládá.
F-string se takto použít vůbec nedá — než jej jakákoli knihovna uvidí, je už hotovým řetězcem, takže jeho překlad znamená překládat fragment. T-stringy (PEP 750) drží statický text a hodnoty odděleně a přitom zachovávají syntaxi podobnou f-stringům a explicitní vázání hodnot.
Jak sem Python dospěl — dva PEPy s odstupem deseti let a diskuse o standardní knihovně uzavřená bez odpovědi — vypráví s prameny stránka Pozadí.