Como funciona¶
Nada nesta página é necessário para usar a biblioteca — o tutorial e o guia cobrem isso. Esta página, em vez disso, reconstrói a biblioteca a partir dos primeiros princípios: o que uma t-string realmente é, como um msgid decorre dela, o que torna uma tradução válida e como a implementação faz toda essa verificação custar décimos de microssegundo. Leia-a se estiver com curiosidade, se quiser contribuir ou se planeja implementar a convenção por conta própria.
O que uma t-string realmente é¶
Uma f-string produz uma str, e a produz imediatamente — quando qualquer
função a recebe, o valor já foi interpolado e a frase está selada. Uma
t-string (PEP 750) tem a mesma sintaxe e a mesma avaliação imediata de suas
expressões, mas produz um tipo diferente:
>>> name = "Ada"
>>> f"Hello {name}!"
'Hello Ada!'
>>> t"Hello {name}!"
Template(strings=('Hello ', '!'), interpolations=(Interpolation('Ada', 'name', None, ''),))
Esse objeto Template guarda as partes de que um pipeline de catálogo
precisa, ainda separadas:
>>> 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— o texto literal ao redor das interpolações, em ordem.- Para cada interpolação: a expressão como texto-fonte (
'amount'), seu valor avaliado (1234.5) e qualquer conversão (!r) e especificação de formato (,.2f) — carregadas separadamente em vez de aplicadas.
Tudo o que esta biblioteca faz é um consumo disciplinado dessa estrutura. A linguagem já fez a única separação de que a i18n precisa — texto estático à parte dos valores —, então a biblioteca nunca analisa o seu código-fonte e nunca adivinha onde um valor está dentro de uma frase. O que resta são três decisões: como a estrutura vira uma chave de catálogo, o que uma tradução dessa chave pode dizer e como as duas renderizam juntas de volta.
Do template ao msgid¶
Um msgid — a chave pela qual um catálogo é indexado — é derivado apenas das
partes estáticas do template. Percorra strings e interpolations na
ordem da origem; escape as chaves de cada segmento literal ({ vira {{);
para cada interpolação, emita um token {name}, em que name é o texto da
expressão sem os espaços em volta. De t"Total: {amount:,.2f}":
strings ('Total: ', '')
interpolations expression 'amount' conversion None format_spec ',.2f'
msgid 'Total: {amount}'
Cada parte dessa regra tem um motivo:
- A expressão deve ser um nome simples —
str.isidentifier()é verdadeiro e não é uma palavra reservada do Python.t"Hello {user.name}"é rejeitada no ponto da chamada. Um msgid é uma chave: precisa sair idêntico em toda execução e toda extração, e é lido por quem traduz, então o marcador deve ser uma palavra estável e com significado — não um fragmento de código que convide o catálogo a virar uma linguagem de expressões. - A conversão e a especificação de formato nunca entram no msgid. Quem
traduz não deveria ter de ler
:,.2f, e nenhuma tradução deve poder alterá-la. O corolário vale a pena conhecer: apertar:,.2fpara:,.0fno seu código não muda msgid nenhum, então não invalida tradução alguma em idioma algum. A chave do catálogo acompanha o que a frase diz, não como o valor é formatado. - Um nome repetido deve repetir sua formatação exatamente.
t"{x:.2f} vs {x:.3f}"é rejeitada, porque as duas ocorrências colapsam no mesmo token{x}e o msgid não conseguiria mais dizer qual formatação uma renderização deve usar. - O msgid vazio nunca é consultado, porque o gettext o reserva para o
cabeçalho de metadados do próprio catálogo.
t""renderiza como""sem tocar no catálogo.
O conjunto completo de regras, incluindo casos extremos que esta página pula, está na SPEC §2.
O que uma tradução pode dizer¶
Um padrão que volta de um catálogo é analisado com string.Formatter — o
mesmo parser que str.format usa. A gramática é deliberadamente emprestada,
não inventada: um padrão que esta biblioteca aceita é um padrão que o
ecossistema mais amplo já entende. Em seguida, duas verificações se aplicam.
Forma: todo campo deve ser um {name} puro. Uma conversão ou
especificação de formato — incluindo a explicitamente vazia {name:} — é
rejeitada, assim como campos posicionais ({0}, {}) e nomes com espaços
dentro das chaves ({ name }). O último caso importa mais do que parece:
tanto str.format quanto o msgfmt do GNU rejeitam { name }, então
aceitá-lo aqui produziria catálogos que nenhuma outra ferramenta da cadeia
consegue validar.
Nomes: o conjunto de marcadores do padrão é comparado com o da origem. Para uma mensagem singular, todo nome da origem é obrigatório e nada além é permitido. Para uma mensagem plural, os dois ramos são mesclados:
- permitido = a união dos nomes dos dois ramos
- obrigatório = a interseção deles
Assim, diante de t"One file" / t"{n} files", o nome n é permitido em
uma tradução de qualquer das formas, mas não é obrigatório em nenhuma. Essa
assimetria é o que permite ao sistema de plural do idioma de destino diferir
do da origem — o japonês traduz os dois ramos com uma única forma que
provavelmente usa {n}; um idioma com mais formas que o inglês pode precisar
de {n} em uma forma na qual o inglês não tem nenhuma.
Nada disso é hipotético: o catálogo do próprio chrome deste site carrega a
mensagem plural Built {n} localized page / Built {n} localized pages —
dois ramos em inglês — e as edições do site traduzem essa única mensagem em
de uma a seis formas.
Nove dessas edições, na ordem das formas
| Catálogo | Formas | As traduções, na ordem das formas |
|---|---|---|
| Japonês | 1 | ローカライズ済みページを{n}件ビルドしました |
| Turco | 2 | {n} yerelleştirilmiş sayfa oluşturuldu — duas vezes, de forma idêntica: os substantivos turcos permanecem no singular depois de um numeral |
| Italiano | 2 | Generata {n} pagina localizzata · Generate {n} pagine localizzate — o particípio concorda em gênero e número |
| Letão | 3 | Izveidota {n} lokalizēta lapa · Izveidotas {n} lokalizētas lapas · Izveidots {n} lokalizētu lapu — a terceira forma é para zero apenas |
| Russo | 3 | Собрана {n} локализованная страница · Собраны {n} локализованные страницы · Собрано {n} локализованных страниц |
| Polonês | 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 — a segunda é um dual, para exatamente dois |
| Irlandês | 5 | Tógadh {n} leathanach logánaithe · Tógadh {n} leathanaigh logánaithe — um, dois, 3–6, 7–10 e o restante; o radical alterna, mas leathanach começa com l, letra em que nenhuma mutação irlandesa se escreve, de modo que várias formas coincidem |
| Árabe | 6 | entre elas, تم إنشاء صفحة مترجمة واحدة ({n}) para exatamente um e تم إنشاء {n} صفحات مترجمة para uns poucos |
Cada linha é uma entrada viva no i18n/*/LC_MESSAGES/site.po deste
repositório, renderizada pelo build multilíngue a cada release — e
um teste fixa esta tabela nesses catálogos, para que as duas coisas não possam
divergir.
Dentro desses limites, reordenar e repetir são deliberadamente livres. Ambos
são gramaticalmente necessários em idiomas reais, e restringir contagens de
ocorrência rejeitaria traduções corretas sem nenhum ganho de segurança: uma
tradução continua sem poder avaliar coisa alguma, porque não existe caminho
de avaliação — os marcadores são consultados por nome nos valores já
computados do template, nunca entregues a eval, getattr ou ao próprio
str.format.
Renderização¶
Renderizar um padrão validado é percorrer seus pedaços: emitir cada parte
literal e, para cada marcador, tomar o valor capturado da interpolação e
aplicar a conversão e a especificação de formato do lado da origem —
format(convert(value, conversion), format_spec). Duas garantias são
mantidas ao fazê-lo:
- Cada valor distinto é formatado no máximo uma vez por renderização,
mesmo quando a tradução repete um marcador. A repetição muda quantas vezes
o resultado é inserido, não quantas vezes o seu
__format__roda. - Nos plurais, um marcador lê o ramo que o definiu. Um nome presente nos
dois ramos lê o valor capturado pelo ramo que o idioma de origem
seleciona (
singularquandon == 1, senãoplural); um nome específico de um ramo sempre lê o próprio ramo, mesmo quando as regras de plural do idioma de destino o disponibilizaram em outra forma.
Quando a validação falha na hora de renderizar, a resposta se divide conforme
quem forneceu o padrão. Um padrão que saiu de um catálogo degrada: registra
um único aviso e renderiza o texto de origem, mantendo o contrato do gettext
de que um catálogo quebrado nunca derruba a aplicação
(o guia mostra os dois modos).
Um padrão que o chamador passou diretamente — CompiledTemplate.render —
sempre levanta exceção, porque não há texto de origem para o qual degradar;
a leniência existe para consultas ao catálogo, não para argumentos.
Os diagnósticos fazem parte do design¶
Um erro de marcador costuma cair diante de quem traduz, não de quem programa,
e muitas vezes em um arquivo em que o problema é invisível. Dizer
{name} is missing a alguém que consegue ver exatamente esses caracteres no
editor é um beco sem saída, então as mensagens são computadas com três
regras:
- Um nome contendo um caractere invisível — um espaço inseparável
produzido por um método de entrada, um espaço de largura zero — é impresso
com esse caractere substituído pelo seu ponto de código, no lugar exato:
{<U+00A0>name}. Quem lê precisa ver onde. - Um nome cujas letras misturam sistemas de escrita, o caso dos
homóglifos, é mostrado duas vezes — uma de forma legível, outra escapada —,
porque
{nаme}com umаcirílico é indistinguível de{name}impresso, e a forma escapada(nаme)é a única grafia que os distingue. - Todo o resto é mostrado como foi escrito.
{名前}e{café}são nomes comuns; escapá-los deixaria quem lê sem conseguir encontrar o que se quis dizer.
Pelo mesmo princípio, um marcador "ausente" que parece presente tem sua
ausência explicada — chaves de largura total de um método de entrada do leste
asiático, o dobramento {{name}} de uma ida e volta de escape, o nome fora
de quaisquer chaves. A
tabela de leitura de falhas
escrita para quem traduz
mostra cada uma dessas mensagens na íntegra.
O caminho quente¶
Tudo o que está acima acontece em cada string traduzida que uma aplicação renderiza, então a implementação é construída em torno de uma ideia: a validação nunca é pulada, então a validação deve ser o que fica em cache.
flowchart LR
T["t-string"] --> S{"estrutura<br>já vista?"}
S -- "acerto" --> G["consulta ao catálogo<br>pelo msgid em cache"]
S -- "falha" --> D["deriva o msgid,<br>guarda o plano"] --> G
G --> V{"padrão<br>já visto?"}
V -- "acerto" --> R["renderiza"]
V -- "falha" --> C["valida,<br>guarda o veredito"] --> R
Três caches, um por estágio:
- Um plano por estrutura de ponto de chamada. A tupla
stringsdo template — um objeto que o interpretador já construiu — é a chave do cache, então uma consulta não aloca nada. Em um acerto, a expressão, a conversão e a especificação de formato de cada interpolação ainda são comparadas com as registradas: dois pontos de chamada que compartilham o texto literal mas diferem na formatação (t"{x:.2f}"contrat"{x:.3f}") não podem colidir, e essa comparação é o preço de usar uma chave que o interpretador entrega de graça. - Um veredito por padrão. Na primeira vez que um catálogo responde com um dado padrão, ele é analisado e validado; o resultado — um plano de renderização compilado, ou um registro de invalidade — fica guardado no plano. Toda renderização posterior dessa mensagem o alcança em uma única consulta de dicionário. Padrões inválidos também são lembrados, e é por isso que uma entrada quebrada de catálogo avisa uma vez, e não a cada renderização.
- Um plano mesclado por par de plural, guardando os conjuntos de união/interseção para que a aritmética dos ramos aconteça uma vez por mensagem, não uma vez por chamada.
Todo cache é limitado, e nenhum retém valores interpolados — apenas
estrutura estática e texto de padrão. O resultado, medido por
benchmarks/runtime.py
no CPython 3.14.6, macOS 26 em um laptop arm64: cerca de 0,4 µs para uma
mensagem de um campo, incluindo a construção da própria t-string —
aproximadamente 2,7× um gettext(...).format(...) puro que não verifica nada.
Esses são os números de uma máquina — o script imprime seu interpretador e sua
plataforma no cabeçalho, então rode-o no hardware em que você de fato implanta
antes de tratar qualquer proporção como sua. O comentário no topo de
core.py
registra as medições individuais por trás desse número.
Reimplementando a convenção¶
Nada do que está acima é específico desta implementação: a convenção está registrada como a especificação v1, e sua suíte de conformidade legível por máquinas permite que um extrator, um plugin de IDE ou uma implementação em outra linguagem se verifique contra cada regra que esta página explicou. Esta implementação executa a suíte nos próprios testes, que é o que impede esta página, a especificação e o código de divergirem em silêncio.