Funktionsweise¶
Nichts auf dieser Seite ist nötig, um die Bibliothek zu benutzen — das decken Tutorial und Anleitung ab. Diese Seite baut die Bibliothek stattdessen aus ersten Prinzipien wieder auf: was eine t-string wirklich ist, wie daraus eine msgid herausfällt, was eine Übersetzung gültig macht und wie die Implementierung all dieses Prüfen Zehntel einer Mikrosekunde kosten lässt. Lies sie aus Neugier, wenn du beitragen möchtest oder wenn du die Konvention selbst implementieren willst.
Was eine t-string wirklich ist¶
Ein f-string erzeugt einen str, und zwar sofort — wenn irgendeine Funktion
ihn erhält, ist der Wert bereits interpoliert und der Satz versiegelt. Eine
t-string (PEP 750) hat dieselbe Syntax und dieselbe eifrige Auswertung
ihrer Ausdrücke, erzeugt aber einen anderen Typ:
>>> name = "Ada"
>>> f"Hello {name}!"
'Hello Ada!'
>>> t"Hello {name}!"
Template(strings=('Hello ', '!'), interpolations=(Interpolation('Ada', 'name', None, ''),))
Dieses Template-Objekt bewahrt die Teile, die eine Katalog-Pipeline
braucht, weiterhin getrennt:
>>> template = t"Total: {amount:,.2f}"
>>> template.strings
('Total: ', '')
>>> template.interpolations[0].expression
'amount'
>>> template.interpolations[0].value
1234.5
>>> template.interpolations[0].format_spec
',.2f'
strings— der literale Text um die Interpolationen, in Reihenfolge.- Pro Interpolation: der Ausdruck als Quelltext (
'amount'), sein ausgewerteter Wert (1234.5) sowie eine etwaige Konvertierung (!r) und Formatspezifikation (,.2f) — getrennt mitgeführt statt angewendet.
Alles, was diese Bibliothek tut, ist ein diszipliniertes Konsumieren dieser Struktur. Die Sprache hat die eine Trennung, die i18n braucht — statischer Text getrennt von Werten —, bereits vollzogen; die Bibliothek parst also nie deinen Quellcode und rät nie, wo in einem Satz ein Wert sitzt. Übrig bleiben drei Entscheidungen: wie die Struktur zu einem Katalogschlüssel wird, was eine Übersetzung dieses Schlüssels sagen darf und wie beides wieder zusammen rendert.
Vom Template zur msgid¶
Eine msgid — der Schlüssel, über den ein Katalog indiziert ist — wird allein
aus den statischen Teilen des Templates abgeleitet. Gehe strings und
interpolations in Quellreihenfolge durch; maskiere in jedem Literalsegment
die Klammern ({ wird {{); gib für jede Interpolation ein Token {name}
aus, wobei name der Ausdruckstext ohne umgebende Leerzeichen ist. Aus
t"Total: {amount:,.2f}":
strings ('Total: ', '')
interpolations expression 'amount' conversion None format_spec ',.2f'
msgid 'Total: {amount}'
Jeder Teil dieser Regel hat einen Grund:
- Der Ausdruck muss ein einfacher Name sein —
str.isidentifier()ist wahr, und er ist kein Python-Schlüsselwort.t"Hello {user.name}"wird an der Aufrufstelle abgelehnt. Eine msgid ist ein Schlüssel: Sie muss bei jedem Lauf und jeder Extraktion identisch herauskommen, und sie wird von Übersetzenden gelesen — der Platzhalter muss also ein stabiles, bedeutungstragendes Wort sein, kein Codefragment, das den Katalog einlädt, zu einer Ausdruckssprache zu werden. - Konvertierung und Formatspezifikation gelangen nie in die msgid.
Übersetzende sollen kein
:,.2flesen müssen, und keine Übersetzung soll es ändern können. Die Konsequenz ist es wert, gewusst zu werden: Wer im Code:,.2fzu:,.0fverschärft, ändert keine msgid und macht damit in keiner Sprache eine Übersetzung ungültig. Der Katalogschlüssel folgt dem, was der Satz sagt, nicht der Formatierung des Werts. - Ein wiederholter Name muss seine Formatierung exakt wiederholen.
t"{x:.2f} vs {x:.3f}"wird abgelehnt, weil beide Vorkommen in dasselbe Token{x}zusammenfallen und die msgid nicht mehr sagen könnte, mit welcher Formatierung ein Rendern arbeiten soll. - Die leere msgid wird nie nachgeschlagen, weil gettext sie für den
Metadaten-Header des Katalogs reserviert.
t""rendert als"", ohne den Katalog zu berühren.
Das vollständige Regelwerk, einschließlich der Randfälle, die diese Seite auslässt, ist SPEC §2.
Was eine Übersetzung sagen darf¶
Ein Pattern, das aus einem Katalog zurückkommt, wird mit string.Formatter
geparst — demselben Parser, den str.format verwendet. Die Grammatik ist
bewusst geliehen statt erfunden: Ein Pattern, das diese Bibliothek
akzeptiert, versteht das weitere Ökosystem bereits. Dann greifen zwei
Prüfungen.
Form: Jedes Feld muss ein bloßes {name} sein. Eine Konvertierung oder
Formatspezifikation — auch die explizit leere {name:} — wird abgelehnt,
ebenso Positionsfelder ({0}, {}) und mit Leerzeichen gepolsterte Namen
({ name }). Der letzte Fall wiegt schwerer, als er aussieht: str.format
und GNU msgfmt lehnen { name } beide ab; es hier zu akzeptieren, ergäbe
Kataloge, die kein anderes Werkzeug der Kette validieren kann.
Namen: Die Platzhaltermenge des Patterns wird mit der der Quelle verglichen. Für eine Singular-Nachricht ist jeder Quellname erforderlich, und nichts anderes ist erlaubt. Für eine Plural-Nachricht werden die beiden Zweige zusammengeführt:
- erlaubt = die Vereinigung der Namen beider Zweige
- erforderlich = ihre Schnittmenge
Gegen t"One file" / t"{n} files" ist der Name n also in einer
Übersetzung beider Formen erlaubt, aber in keiner erforderlich. Diese
Asymmetrie erlaubt es dem Pluralsystem einer Zielsprache, vom Quellsystem
abzuweichen — Japanisch übersetzt beide Zweige mit einer Form, die
vermutlich {n} verwendet; eine Sprache mit mehr Formen als Englisch
braucht {n} womöglich in einer Form, wo Englisch keine hat.
Nichts davon ist hypothetisch: Der Katalog für das Seitengerüst dieser
Website führt selbst die Plural-Nachricht Built {n} localized page /
Built {n} localized pages — zwei englische Zweige — und die Sprachausgaben
der Website übersetzen diese eine Nachricht in eine bis sechs Formen.
Neun dieser Ausgaben, in der Reihenfolge der Formen
| Katalog | Formen | Die Übersetzungen, in der Reihenfolge der Formen |
|---|---|---|
| Japanisch | 1 | ローカライズ済みページを{n}件ビルドしました |
| Türkisch | 2 | {n} yerelleştirilmiş sayfa oluşturuldu — zweimal, identisch: türkische Substantive bleiben nach einem Zahlwort im Singular |
| Italienisch | 2 | Generata {n} pagina localizzata · Generate {n} pagine localizzate — das Partizip kongruiert in Genus und Numerus |
| Lettisch | 3 | Izveidota {n} lokalizēta lapa · Izveidotas {n} lokalizētas lapas · Izveidots {n} lokalizētu lapu — die dritte Form gilt allein der Null |
| Russisch | 3 | Собрана {n} локализованная страница · Собраны {n} локализованные страницы · Собрано {n} локализованных страниц |
| Polnisch | 3 | Zbudowano {n} zlokalizowaną stronę · Zbudowano {n} zlokalizowane strony · Zbudowano {n} zlokalizowanych stron |
| Slowenisch | 4 | Zgrajena {n} lokalizirana stran · Zgrajeni {n} lokalizirani strani · Zgrajene {n} lokalizirane strani · Zgrajenih {n} lokaliziranih strani — die zweite ist ein Dual, für genau zwei |
| Irisch | 5 | Tógadh {n} leathanach logánaithe · Tógadh {n} leathanaigh logánaithe — eins, zwei, 3–6, 7–10 und der Rest; der Stamm wechselt, doch leathanach beginnt mit l, das keine irische Mutation schreibt, sodass mehrere Formen zusammenfallen |
| Arabisch | 6 | darunter تم إنشاء صفحة مترجمة واحدة ({n}) für genau eins und تم إنشاء {n} صفحات مترجمة für wenige |
Jede Zeile ist ein echter Eintrag in i18n/*/LC_MESSAGES/site.po dieses
Repositorys, gerendert vom mehrsprachigen Build bei jedem
Release — und ein Test heftet diese Tabelle an jene Kataloge, sodass beide
nicht auseinanderdriften können.
Innerhalb dieser Grenzen sind Umstellung und Wiederholung bewusst
unbeschränkt. Beides ist in echten Sprachen grammatisch notwendig, und ein
Begrenzen der Vorkommen würde korrekte Übersetzungen ohne jeden
Sicherheitsgewinn ablehnen: Eine Übersetzung kann trotzdem nichts
auswerten, weil kein Auswertungspfad existiert — Platzhalter werden per
Name in den bereits berechneten Werten des Templates nachgeschlagen und nie
an eval, getattr oder str.format selbst gereicht.
Rendern¶
Ein validiertes Pattern zu rendern ist ein Durchlauf über seine Stücke: Gib
jeden Literalteil aus, und nimm für jeden Platzhalter den erfassten Wert der
Interpolation und wende die quellseitige Konvertierung und
Formatspezifikation an — format(convert(value, conversion), format_spec).
Zwei Garantien bleiben dabei gewahrt:
- Jeder eigenständige Wert wird höchstens einmal pro Rendern formatiert,
auch wenn die Übersetzung einen Platzhalter wiederholt. Wiederholung
ändert, wie oft das Ergebnis eingefügt wird, nicht, wie oft dein
__format__läuft. - Bei Pluralformen liest ein Platzhalter den Zweig, der ihn definiert
hat. Ein in beiden Zweigen vorhandener Name liest den Wert, den der von
der Quellsprache gewählte Zweig erfasst hat (
singularbein == 1, sonstplural); ein zweigspezifischer Name liest immer seinen eigenen Zweig, auch wenn die Pluralregeln der Zielsprache ihn in einer anderen Form verfügbar gemacht haben.
Schlägt die Validierung beim Rendern fehl, richtet sich die Antwort danach,
wer das Pattern geliefert hat. Ein Pattern, das aus einem Katalog kam,
degradiert: eine Warnung ins Log, gerendert wird der Quelltext — das wahrt
gettexts Vertrag, dass ein defekter Katalog die Anwendung nie mitreißt
(die Anleitung zeigt beide Modi).
Ein Pattern, das der Aufrufer direkt übergeben hat —
CompiledTemplate.render — löst immer eine Exception aus, denn es gibt
keinen Quelltext, auf den degradiert werden könnte; Nachsicht gilt
Katalogsuchen, nicht Argumenten.
Diagnostik ist Teil des Designs¶
Ein Platzhalterfehler landet meist vor einer übersetzenden Person, nicht vor
einer programmierenden, und oft in einer Datei, in der das Problem
unsichtbar ist. Jemandem, der genau diese Zeichen in seinem Editor sehen
kann, {name} is missing zu sagen, ist eine Sackgasse; die Meldungen folgen
deshalb drei Regeln:
- Ein Name mit einem unsichtbaren Zeichen — ein geschütztes Leerzeichen
aus einer Eingabemethode, ein Leerzeichen der Breite null — wird mit
diesem Zeichen als Codepoint gedruckt, an Ort und Stelle:
{<U+00A0>name}. Die lesende Person muss sehen, wo. - Ein Name, dessen Buchstaben Schriftsysteme mischen, der
Homoglyphen-Fall, erscheint doppelt — einmal lesbar, einmal maskiert —,
denn
{nаme}mit kyrillischemаist im Druckbild von{name}nicht zu unterscheiden, und die maskierte Form(nаme)ist die einzige Schreibweise, die beide auseinanderhält. - Alles andere erscheint wie geschrieben.
{名前}und{café}sind gewöhnliche Namen; sie zu maskieren, ließe die lesende Person nicht mehr finden, was gemeint war.
Nach demselben Prinzip bekommt ein „fehlender“ Platzhalter, der vorhanden
aussieht, sein Fehlen erklärt — vollbreite Klammern aus einer
ostasiatischen Eingabemethode, ein durch eine Maskierungs-Rundreise
verdoppeltes {{name}}, der Name außerhalb jeder Klammer. Die für
Übersetzende geschriebene
Fehlertabelle zeigt jede dieser
Meldungen im Wortlaut.
Der Hot Path¶
Alles Obige geschieht bei jedem übersetzten String, den eine Anwendung rendert; die Implementierung ist deshalb um einen Gedanken herum gebaut: Validierung wird nie übersprungen, also muss die Validierung das sein, was gecacht wird.
flowchart LR
T["t-string"] --> S{"Struktur<br>schon gesehen?"}
S -- "Treffer" --> G["Katalogsuche<br>über gecachte msgid"]
S -- "kein Treffer" --> D["msgid ableiten,<br>Plan cachen"] --> G
G --> V{"Pattern<br>schon gesehen?"}
V -- "Treffer" --> R["rendern"]
V -- "kein Treffer" --> C["validieren,<br>Verdikt cachen"] --> R
Drei Caches, einer pro Stufe:
- Ein Plan pro Aufrufstellen-Struktur. Das
strings-Tupel des Templates — ein Objekt, das der Interpreter ohnehin gebaut hat — ist der Cache-Schlüssel, sodass ein Lookup nichts alloziert. Bei einem Treffer werden Ausdruck, Konvertierung und Formatspezifikation jeder Interpolation trotzdem mit den aufgezeichneten verglichen: Zwei Aufrufstellen, die den Literaltext teilen, sich aber in der Formatierung unterscheiden (t"{x:.2f}"gegent"{x:.3f}"), dürfen nicht kollidieren, und dieser Vergleich ist der Preis eines Schlüssels, den der Interpreter gratis hereinreicht. - Ein Verdikt pro Pattern. Beim ersten Mal, dass ein Katalog mit einem bestimmten Pattern antwortet, wird es geparst und validiert; das Ergebnis — ein kompilierter Renderplan oder ein Vermerk der Ungültigkeit — bleibt am Plan erhalten. Jedes spätere Rendern dieser Nachricht erreicht es mit einem einzigen Dictionary-Lookup. Auch ungültige Patterns werden gemerkt — deshalb warnt ein defekter Katalogeintrag einmal statt bei jedem Rendern.
- Ein zusammengeführter Plan pro Pluralpaar, der die Vereinigungs- und Schnittmengen hält, sodass die Zweigarithmetik einmal pro Nachricht stattfindet, nicht einmal pro Aufruf.
Jeder Cache ist begrenzt, und keiner behält interpolierte Werte — nur
statische Struktur und Patterntext. Das Ergebnis, gemessen von
benchmarks/runtime.py
auf CPython 3.14.6, macOS 26 auf einem arm64-Laptop: rund 0,4 µs für eine
Nachricht mit einem Feld, den Bau der t-string selbst eingeschlossen — etwa
das 2,7-Fache eines schlichten gettext(...).format(...), das nichts prüft.
Das sind die Zahlen einer einzigen Maschine — das Skript gibt seinen
Interpreter und seine Plattform im Kopf seiner Ausgabe aus, also lass es auf
der Hardware laufen, auf der du tatsächlich deployst, bevor du irgendein
Verhältnis für deines hältst. Der Kommentar am Kopf von
core.py
hält die Einzelmessungen hinter dieser Gestalt fest.
Selbst implementieren¶
Nichts davon ist dieser Implementierung eigen: Die Konvention ist als Spezifikation v1 niedergeschrieben, und ihre maschinenlesbare Konformitätssuite lässt einen Extraktor, ein IDE-Plugin oder eine Implementierung in einer anderen Sprache sich gegen jede Regel prüfen, die diese Seite erklärt hat. Diese Implementierung führt die Suite in ihren eigenen Tests aus — genau das bewahrt diese Seite, die Spezifikation und den Code davor, still auseinanderzudriften.