Cómo funciona¶
Nada de esta página es necesario para usar la biblioteca: el tutorial y la guía cubren eso. Esta página, en cambio, reconstruye la biblioteca desde los primeros principios: qué es realmente una t-string, cómo se deriva de ella un msgid, qué hace válida una traducción y cómo consigue la implementación que toda esa comprobación cueste décimas de microsegundo. Léela si tienes curiosidad, si quieres contribuir o si piensas implementar la convención por tu cuenta.
Qué es realmente una t-string¶
Una f-string produce un str, y lo produce de inmediato: para cuando
cualquier función la recibe, el valor ya se ha interpolado y la frase está
sellada. Una t-string (PEP 750) tiene la misma sintaxis y la misma
evaluación inmediata de sus expresiones, pero produce un tipo distinto:
>>> name = "Ada"
>>> f"Hello {name}!"
'Hello Ada!'
>>> t"Hello {name}!"
Template(strings=('Hello ', '!'), interpolations=(Interpolation('Ada', 'name', None, ''),))
Ese objeto Template conserva, todavía separadas, las partes que necesita un
pipeline de catálogos:
>>> 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— el texto literal alrededor de las interpolaciones, en orden.- Para cada interpolación: la expresión como texto fuente (
'amount'), su valor evaluado (1234.5) y cualquier conversión (!r) y especificación de formato (,.2f) — transportadas por separado en lugar de aplicadas.
Todo lo que hace esta biblioteca es un consumo disciplinado de esa estructura. El lenguaje ya hizo la única separación que necesita la i18n —el texto estático aparte de los valores—, así que la biblioteca nunca analiza tu código fuente ni adivina dónde se encuentra un valor dentro de una frase. Quedan tres decisiones: cómo la estructura se convierte en una clave de catálogo, qué puede decir una traducción de esa clave y cómo las dos se renderizan de nuevo juntas.
De la plantilla al msgid¶
Un msgid —la clave por la que se indexa un catálogo— se deriva únicamente de
las partes estáticas de la plantilla. Recorre strings e interpolations
en el orden del código fuente; escapa las llaves de cada segmento literal
({ pasa a ser {{); por cada interpolación, emite un token {name}, donde
name es el texto de la expresión sin los espacios en blanco que la rodean.
De t"Total: {amount:,.2f}":
strings ('Total: ', '')
interpolations expression 'amount' conversion None format_spec ',.2f'
msgid 'Total: {amount}'
Cada parte de esa regla tiene una razón:
- La expresión debe ser un nombre simple —
str.isidentifier()es verdadero y no es una palabra reservada de Python.t"Hello {user.name}"se rechaza en el punto de llamada. Un msgid es una clave: tiene que salir idéntico en cada ejecución y en cada extracción, y lo leen los traductores, así que el marcador debe ser una palabra estable y con significado, no un fragmento de código que invite al catálogo a convertirse en un lenguaje de expresiones. - La conversión y la especificación de formato nunca entran en el msgid.
Los traductores no deberían tener que leer
:,.2f, y ninguna traducción debería poder cambiarlo. Merece la pena conocer el corolario: ajustar:,.2fa:,.0fen tu código no cambia ningún msgid, así que no invalida ninguna traducción en ningún idioma. La clave del catálogo sigue lo que dice la frase, no cómo se formatea el valor. - Un nombre repetido debe repetir su formato exactamente.
t"{x:.2f} vs {x:.3f}"se rechaza, porque ambas apariciones colapsan en el mismo token{x}y el msgid ya no podría decir qué formato debe usar un renderizado. - El msgid vacío nunca se busca, porque gettext lo reserva para la
cabecera de metadatos del propio catálogo.
t""se renderiza como""sin tocar el catálogo.
El conjunto completo de reglas, incluidos los casos límite que esta página omite, es SPEC §2.
Qué puede decir una traducción¶
Un pattern que vuelve de un catálogo se analiza con string.Formatter, el
mismo parser que usa str.format. La gramática se toma prestada a propósito
en lugar de inventarse: un pattern que esta biblioteca acepta es uno que el
resto del ecosistema ya entiende. Después se aplican dos comprobaciones.
Forma: cada campo debe ser un {name} a secas. Una conversión o una
especificación de formato —incluida la explícitamente vacía {name:}— se
rechaza, igual que los campos posicionales ({0}, {}) y los nombres con
espacios dentro de las llaves ({ name }). Este último caso importa más de lo
que parece: tanto str.format como el msgfmt de GNU rechazan { name },
así que aceptarlo aquí produciría catálogos que ninguna otra herramienta de la
cadena puede validar.
Nombres: el conjunto de marcadores del pattern se compara con el del mensaje de origen. En un mensaje singular, todos los nombres del origen son obligatorios y no se permite nada más. En un mensaje plural, las dos ramas se combinan:
- permitidos = la unión de los nombres de ambas ramas
- obligatorios = su intersección
Así, frente a t"One file" / t"{n} files", el nombre n está permitido en
la traducción de cualquiera de las dos formas, pero no es obligatorio en
ninguna. Esa asimetría es lo que permite que el sistema de plurales del idioma
de destino difiera del de origen: el japonés traduce ambas ramas con una sola
forma que probablemente usa {n}; un idioma con más formas que el inglés
puede necesitar {n} en una forma donde el inglés no tiene ninguna.
Nada de eso es hipotético: el catálogo de interfaz de este mismo sitio contiene
el mensaje plural Built {n} localized page / Built {n} localized pages —dos
ramas en inglés— y las ediciones del sitio traducen ese único mensaje a entre
una y seis formas.
Nueve de esas ediciones, en orden de forma
| Catálogo | Formas | Las traducciones, en orden de forma |
|---|---|---|
| Japonés | 1 | ローカライズ済みページを{n}件ビルドしました |
| Turco | 2 | {n} yerelleştirilmiş sayfa oluşturuldu — dos veces, idéntica: los sustantivos turcos permanecen en singular tras un numeral |
| Italiano | 2 | Generata {n} pagina localizzata · Generate {n} pagine localizzate — el participio concuerda en género y número |
| Letón | 3 | Izveidota {n} lokalizēta lapa · Izveidotas {n} lokalizētas lapas · Izveidots {n} lokalizētu lapu — la tercera forma es solo para el cero |
| Ruso | 3 | Собрана {n} локализованная страница · Собраны {n} локализованные страницы · Собрано {n} локализованных страниц |
| Polaco | 3 | Zbudowano {n} zlokalizowaną stronę · Zbudowano {n} zlokalizowane strony · Zbudowano {n} zlokalizowanych stron |
| Esloveno | 4 | Zgrajena {n} lokalizirana stran · Zgrajeni {n} lokalizirani strani · Zgrajene {n} lokalizirane strani · Zgrajenih {n} lokaliziranih strani — la segunda es un dual, para exactamente dos |
| Irlandés | 5 | Tógadh {n} leathanach logánaithe · Tógadh {n} leathanaigh logánaithe — uno, dos, 3–6, 7–10 y el resto; la raíz alterna, pero leathanach empieza por l, letra sobre la que no se escribe ninguna mutación irlandesa, así que varias formas coinciden |
| Árabe | 6 | entre ellas تم إنشاء صفحة مترجمة واحدة ({n}) para exactamente uno y تم إنشاء {n} صفحات مترجمة para unos pocos |
Cada fila es una entrada viva en los i18n/*/LC_MESSAGES/site.po de este
repositorio, renderizada por la compilación multilingüe en cada
release — y una prueba fija esta tabla a esos catálogos, de modo que las dos no
pueden divergir.
Dentro de esos límites, reordenar y repetir queda deliberadamente sin
restricciones. Ambas cosas son gramaticalmente necesarias en idiomas reales, y
restringir el número de apariciones rechazaría traducciones correctas sin
ningún beneficio de seguridad: una traducción sigue sin poder evaluar nada,
porque no existe ninguna ruta de evaluación — los marcadores se buscan por
nombre entre los valores ya calculados de la plantilla, y nunca se pasan a
eval, getattr ni al propio str.format.
Renderizado¶
Renderizar un pattern validado es un recorrido por sus fragmentos: emite cada
parte literal y, para cada marcador, toma el valor capturado por la
interpolación y aplica la conversión y la especificación de formato del lado
del origen: format(convert(value, conversion), format_spec). Al hacerlo se
mantienen dos garantías:
- Cada valor distinto se formatea como mucho una vez por renderizado,
incluso cuando la traducción repite un marcador. La repetición cambia
cuántas veces se inserta el resultado, no cuántas veces se ejecuta tu
__format__. - En los plurales, un marcador lee la rama que lo definió. Un nombre
presente en ambas ramas lee el valor capturado por la rama que selecciona el
idioma de origen (
singularcuandon == 1,pluralen caso contrario); un nombre específico de una rama siempre lee su propia rama, incluso cuando las reglas de plural del idioma de destino lo hicieron disponible en otra forma.
Cuando la validación falla en el momento de renderizar, la respuesta depende
de quién suministró el pattern. Un pattern que salió de un catálogo se
degrada: se registra un aviso y se renderiza el texto de origen, manteniendo
el contrato de gettext de que un catálogo roto nunca tumba la aplicación
(la guía muestra ambos modos).
Un pattern que el llamante pasó directamente —CompiledTemplate.render—
siempre lanza una excepción, porque no hay texto de origen desde el que
degradar; la tolerancia existe para las búsquedas en catálogo, no para los
argumentos.
Los diagnósticos forman parte del diseño¶
Un error de marcadores suele aterrizar delante de un traductor, no de un
programador, y a menudo en un archivo donde el problema es invisible. Decir
{name} is missing a alguien que puede ver exactamente esos caracteres en su
editor es un callejón sin salida, así que los mensajes se calculan con tres
reglas:
- Un nombre que contiene un carácter invisible —un espacio de no
separación producido por un método de entrada, un espacio de ancho cero— se
imprime con ese carácter sustituido por su punto de código, en su posición:
{<U+00A0>name}. El lector necesita ver dónde. - Un nombre cuyas letras mezclan sistemas de escritura, el caso de los
homoglifos, se muestra dos veces —una legible, otra escapada—, porque
{nаme}con unaаcirílica es indistinguible de{name}sobre el papel, y la forma escapada(nаme)es la única grafía que los diferencia. - Todo lo demás se muestra tal como está escrito.
{名前}y{café}son nombres normales; escaparlos dejaría al lector sin poder encontrar lo que se quería decir.
Por el mismo principio, un marcador «ausente» que parece presente recibe una
explicación de su ausencia: llaves de ancho completo de un método de entrada
de Asia oriental, la duplicación {{name}} de una ronda de escapado, el
nombre fuera de cualquier llave. La
tabla de lectura de fallos escrita
para traductores muestra cada uno de estos mensajes literalmente.
La ruta caliente¶
Todo lo anterior ocurre en cada cadena traducida que renderiza una aplicación, así que la implementación se construye alrededor de una idea: la validación nunca se omite, así que lo que hay que cachear es la validación.
flowchart LR
T["t-string"] --> S{"¿estructura<br>ya vista?"}
S -- "acierto" --> G["búsqueda en el catálogo<br>por msgid cacheado"]
S -- "fallo" --> D["derivar el msgid,<br>cachear el plan"] --> G
G --> V{"¿pattern<br>ya visto?"}
V -- "acierto" --> R["renderizar"]
V -- "fallo" --> C["validar,<br>cachear el veredicto"] --> R
Tres cachés, una por etapa:
- Un plan por estructura de punto de llamada. La tupla
stringsde la plantilla —un objeto que el intérprete ya construyó— es la clave de la caché, así que una búsqueda no asigna memoria alguna. En un acierto, la expresión, la conversión y la especificación de formato de cada interpolación se comparan igualmente con las registradas: dos puntos de llamada que comparten el texto literal pero difieren en el formato (t"{x:.2f}"frente at"{x:.3f}") no deben colisionar, y esa comparación es el precio de usar una clave que el intérprete entrega gratis. - Un veredicto por pattern. La primera vez que un catálogo responde con un pattern dado, este se analiza y se valida; el resultado —un plan de renderizado compilado o un registro de invalidez— se conserva en el plan. Cada renderizado posterior de ese mensaje lo alcanza en una sola búsqueda de diccionario. Los patterns inválidos también se recuerdan, y por eso una entrada rota del catálogo avisa una vez y no en cada renderizado.
- Un plan combinado por pareja de plurales, que guarda los conjuntos de unión e intersección para que la aritmética de ramas ocurra una vez por mensaje, no una vez por llamada.
Todas las cachés están acotadas y ninguna retiene valores interpolados: solo
estructura estática y texto de patterns. El resultado, medido por
benchmarks/runtime.py
en CPython 3.14.6, macOS 26 sobre un portátil arm64: aproximadamente 0,4 µs
para un mensaje de un campo, incluida la construcción de la propia t-string,
en torno a 2,7 veces un simple gettext(...).format(...) que no comprueba
nada. Son las cifras de una sola máquina —el script imprime su intérprete y su
plataforma en la cabecera—, así que ejecútalo en el hardware en el que
realmente despliegas antes de dar cualquier proporción por tuya. El comentario
al principio de
core.py
recoge las mediciones individuales que hay detrás de esa forma.
Reimplementarla¶
Nada de lo anterior es específico de esta implementación: la convención está escrita como la especificación v1, y su suite de conformidad legible por máquinas permite que un extractor, un plugin de IDE o una implementación en otro lenguaje se compruebe contra cada regla que explica esta página. Esta implementación ejecuta la suite en sus propias pruebas, que es lo que evita que esta página, la especificación y el código se separen en silencio.