Em produção¶
O tutorial executa o ciclo uma vez, sozinho, em um programa com uma única mensagem. Em um projeto real o ciclo continua girando: mensagens mudam depois de já terem sido traduzidas, quem traduz trabalha em outro lugar e no próprio ritmo, e um catálogo compilado é distribuído com cada release. Esta página é essa prática — o que fica no repositório, o que viaja, o que o CI precisa barrar e onde o runtime vincula um idioma.
No fim das contas são seis verificações, então aqui estão elas primeiro; cada seção abaixo monta uma delas.
pybabel update --checkpassa — nenhuma mensagem mudou sem que os catálogos ficassem sabendo.pybabel compilecondiciona o build ao seu código de saída.- As entradas
fuzzyrestantes são intencionais — cada uma renderiza como texto de origem até que quem traduz a confirme. - A suíte de testes renderiza cada idioma distribuído uma vez com
strict=True. - O artefato de produção contém arquivos
.moe nenhum Babel. - O logger
gettext_tstringsestá direcionado ao monitoramento.
A forma de um projeto¶
myapp/
├── babel.cfg
├── pyproject.toml
├── src/
│ └── myapp/
└── locales/
├── messages.pot
├── ja/LC_MESSAGES/messages.po
└── de/LC_MESSAGES/messages.po
Versione babel.cfg, o template .pot e cada .po — eles são as fontes do
build de tradução, e seus diffs são a forma de revisar mudanças de tradução.
Os arquivos .mo compilados são artefatos de build: produza-os no CI ou na
hora de empacotar, em vez de versioná-los, para que um .po e seu .mo
jamais possam discordar sobre o que é distribuído.
Um arquivo tem um papel em cada direção: o .pot leva suas mensagens para
fora, até quem traduz; os arquivos .po trazem as traduções de volta. O
restante desta página é o que se move entre eles.
flowchart LR
code["código-fonte<br>chamadas com t-strings"] -->|"pybabel extract"| pot["messages.pot"]
pot -->|"pybabel update"| po["um .po por idioma"]
po --> tr["tradutor(a)<br>ou plataforma"]
tr --> po
po -->|"pybabel compile (CI)"| mo["arquivos .mo"]
mo --> app["aplicação<br>em tempo de execução"]
O ciclo depois da primeira tradução¶
O pybabel init do tutorial normalmente roda uma única vez, quando um idioma
é adicionado. Daí em diante, o ciclo de trabalho é extrair → atualizar →
traduzir → compilar, e seu centro é o pybabel update, que incorpora um template
recém-extraído aos catálogos existentes sem descartar as traduções que já
estão neles.
Suponha que a saudação Hello {name} — já traduzida como
こんにちは {name} — seja reescrita no código como Welcome back, {name}.
Extraia e atualize:
$ pybabel extract -F babel.cfg -c "Translators:" -o locales/messages.pot .
extracting messages from app.py (encoding="utf-8")
writing PO template file to locales/messages.pot
$ pybabel update -i locales/messages.pot -d locales
updating catalog locales/ja/LC_MESSAGES/messages.po based on locales/messages.pot
O catálogo japonês agora contém:
#. gettext-tstrings
#: app.py:4
#, fuzzy, python-brace-format
msgid "Welcome back, {name}"
msgstr "こんにちは {name}"
O Babel notou que o novo msgid se parece com um que foi removido e o
emparelhou com a tradução antiga — mas marcou o par como fuzzy: um palpite
da máquina à espera de um humano. A marca muda o que é compilado. O
pybabel compile exclui as entradas fuzzy do .mo, então, até que quem traduz confirme o
par, a aplicação renderiza o novo texto em inglês em vez de um japonês
desatualizado:
$ pybabel compile -d locales
compiling catalog locales/ja/LC_MESSAGES/messages.po to locales/ja/LC_MESSAGES/messages.mo
$ python app.py
Welcome back, Ada
Uma mensagem alterada, portanto, degrada da mesma forma que uma quebrada —
para o idioma de origem, nunca para uma tradução desatualizada. A parte de
quem traduz nesse ciclo é revisar o msgstr e apagar a marca fuzzy; a
compilação seguinte recolhe a entrada.
Os nomes dos marcadores fazem parte da identidade da mensagem
O msgid é a chave do catálogo, e o nome do marcador está dentro dele —
então renomear uma variável no código (name → user_name) muda o msgid
e manda a tradução dessa mensagem em todos os idiomas de volta ao ciclo
fuzzy. Dê às variáveis interpoladas nomes que sejam palavras
compreensíveis para quem traduz, e renomeie-as apenas por um bom motivo.
A formatação é a imagem espelhada: !r e :.2f não fazem parte do
msgid, então apertar {amount:,.2f}
para {amount:,.0f} não muda nada em catálogo nenhum. Reescrever a
frase, claro, é uma mudança real — esse é o ciclo acima.
O que o CI barra¶
Três falhas merecem um build vermelho: os catálogos ficaram para trás em relação ao código, uma tradução quebrou um marcador, ou uma entrada quebrada escapou até o tempo de execução. Um passo por falha:
- run: pybabel extract -F babel.cfg -c "Translators:" -o locales/messages.pot .
- run: pybabel update -i locales/messages.pot -d locales --check
- run: pybabel compile -d locales
- run: pytest
pybabel update --check não reescreve nada e sai com código diferente de zero
quando um catálogo está desatualizado em relação ao template recém-extraído —
a guarda contra fazer merge de código cujas mensagens ninguém re-extraiu.
pybabel compile executa as verificações de marcadores do Babel e do
verificador registrado
deste pacote.
Babel 2.18.0: --check não consegue barrar um catálogo que usa contextos
No Babel 2.18.0, pybabel update --check relata todo catálogo que
contenha um msgctxt como desatualizado, a cada execução, por mais em dia
que ele esteja. Um portão que falha permanentemente é pior que portão
nenhum, porque a equipe o desliga — então, se você usa pgettext ou
npgettext de alguma forma, substitua este passo em vez de conviver com
ele. Ler o template e cada catálogo com babel.messages.pofile.read_po e
comparar {(m.context, m.id) for m in catalog if m.id} é a verificação
inteira, e é o que o próprio build deste site faz. A causa está
descrita em Armadilhas.
Verifique o código de saída, não o log
pybabel compile relata cada erro de marcador, sai com código diferente
de zero — e mesmo assim escreve o .mo. Um pipeline que compila e
depois copia locales/ para uma imagem distribui o catálogo quebrado, a
menos que a saída diferente de zero de fato o interrompa. Deixar o passo
derrubar o build, como acima, é a correção inteira.
A última linha é a sua suíte de testes comum, com um hábito a mais: em algum lugar dela, renderize ao menos uma mensagem por idioma distribuído através de um tradutor estrito —
import gettext
from gettext_tstrings import Translator
def test_catalogs_render(language: str) -> None:
translations = gettext.translation("messages", localedir="locales", languages=[language])
_ = Translator(translations, strict=True)
name = "Ada"
assert _(t"Welcome back, {name}")
— porque strict=True levanta exceção onde a produção recairia em silêncio,
e uma renderização em tempo de execução é a única verificação que vê o
catálogo exatamente como a aplicação o verá, .mo e tudo.
Trabalhando com tradutores e plataformas¶
O arquivo .po é o formato de intercâmbio de todo o mundo gettext, e é por
isso que esta biblioteca o reutiliza: entregar a tradução significa entregar
um arquivo, seja o destinatário um colega com um editor de PO ou uma
plataforma como Weblate ou Crowdin. Três coisas fazem essa entrega funcionar
bem:
Diga para que serve a mensagem. Um comentário no código viaja com a
mensagem — é isso que a flag -c "Translators:" coleta:
from gettext_tstrings import tr
name = "Ada"
# Translators: shown on the dashboard right after sign-in
print(tr(t"Welcome back, {name}"))
#. Translators: shown on the dashboard right after sign-in
#. gettext-tstrings
#: app.py:5
#, python-brace-format
msgid "Welcome back, {name}"
msgstr ""
Quem traduz vê esse comentário no editor, ao lado da mensagem, do outro lado
do mundo. É a alavanca de qualidade mais barata de todo o fluxo de trabalho.
Para uma palavra que é homônima de si mesma — o botão "Open" versus o estado
"Open" — dê à mensagem um contexto com
pgettext, que vira um msgctxt visível no catálogo.
Deixe a plataforma validar os marcadores. Toda mensagem extraída de uma
t-string carrega a flag python-brace-format, e essa única linha é o que liga
o controle de qualidade de marcadores em ferramentas que você não controla —
o Weblate documenta a verificação, plataformas comerciais amarram as suas à
mesma flag, e msgfmt --check-format a impõe em qualquer pipeline GNU. Os
detalhes, e o que o verificador incluído captura além deles, estão na
página de extração.
Confie na rede de segurança exatamente até onde ela alcança. O que quer que volte de uma plataforma ainda é dado entrando no seu build; os portões de CI acima são o que transforma "a plataforma provavelmente verificou isto" em "isto não pode ser distribuído quebrado".
Vincular um idioma em tempo de execução¶
Tudo até aqui produz catálogos. A decisão restante é onde a aplicação seleciona um deles. Vincule uma vez por escopo de um idioma — o processo em uma CLI, a requisição em um serviço web.
Uma ferramenta de linha de comando ou aplicação desktop lê o ambiente de
quem a usa uma vez, na inicialização. Não passar languages= deixa a
biblioteca padrão negociar a partir de LANGUAGE, LC_ALL,
LC_MESSAGES e LANG; fallback=True devolve um catálogo nulo — texto
de origem — em vez de levantar exceção quando nenhum deles corresponde a
um catálogo que você distribui.
Uma aplicação web decide por requisição. Carregue cada catálogo uma vez
no import e vincule o negociado ao contexto antes de a view rodar —
set_translations é local ao contexto,
então requisições concorrentes em idiomas diferentes nunca veem o vínculo
umas das outras.
import gettext
from flask import Flask, request
from gettext_tstrings import set_translations, tr
LANGUAGES = ("en", "ja", "de")
CATALOGS = {
language: gettext.translation(
"messages", localedir="locales", languages=[language], fallback=True
)
for language in LANGUAGES
}
app = Flask(__name__)
@app.before_request
def bind_language() -> None:
language = request.accept_languages.best_match(LANGUAGES) or "en"
set_translations(CATALOGS[language])
@app.get("/")
def home() -> str:
name = "Ada"
return tr(t"Welcome back, {name}")
Sob frameworks assíncronos — FastAPI, Starlette e qualquer outro ASGI —,
envolva a requisição em
use_translations: o vínculo vive em um
ContextVar, que a troca de tarefas assíncronas preserva por requisição.
import gettext
from fastapi import FastAPI, Request
from gettext_tstrings import tr, use_translations
LANGUAGES = ("en", "ja", "de")
CATALOGS = {
language: gettext.translation(
"messages", localedir="locales", languages=[language], fallback=True
)
for language in LANGUAGES
}
app = FastAPI()
@app.middleware("http")
async def bind_language(request: Request, call_next):
language = negotiate_language(request.headers.get("accept-language"), LANGUAGES)
with use_translations(CATALOGS[language]):
return await call_next(request)
negotiate_language representa o seu parsing de Accept-Language — a
maioria dos frameworks, ou seus ecossistemas, fornece um; o que importa
aqui é o vínculo em torno de call_next.
Dois hábitos de execução completam o quadro. Strings criadas no momento do
import — o rótulo de um formulário, o nome de exibição de um enum — não devem
capturar o idioma que estivesse ativo durante o import; defina-as com
lazy_gettext e elas serão renderizadas no
idioma ativo no uso. E direcione o logger gettext_tstrings para algum
lugar que um humano olhe: seus avisos são o modo leniente relatando uma
tradução que escapou de todos os portões, uma linha por mensagem quebrada em
vez de uma por renderização.
Distribuição¶
A produção precisa do pacote, dos arquivos .mo e de nada mais. O Babel é
uma dependência de desenvolvimento e de CI — mantenha gettext-tstrings[babel]
fora da imagem de produção e instale ali o pacote puro; a renderização roda
apenas com a biblioteca padrão. Compile os catálogos no mesmo build que
produz o artefato implantado, para que os arquivos .mo dentro dele sejam
exatamente os .po revisados, e nada compilado no laptop de alguém jamais
seja distribuído.
Como eles viajam depende do que você implanta. Uma wheel os carrega como dados
de pacote, o que significa que os catálogos precisam ficar dentro do
diretório do pacote — src/myapp/locales/, não um locales/ no topo — e é
preciso dizer ao backend de build que inclua arquivos que o .gitignore
normalmente esconde:
Leia-os de volta através do pacote, e não por um caminho relativo à árvore de código, que deixa de existir no instante em que a wheel é instalada:
import gettext
from importlib.resources import as_file, files
with as_file(files("myapp") / "locales") as localedir:
translations = gettext.translation("messages", localedir=localedir, languages=["ja"])
Uma imagem de contêiner tem a tarefa mais fácil: compile durante o estágio de build e copie o resultado, deixando o Babel para trás nesse estágio.
FROM python:3.14-slim AS build
COPY . /src
RUN cd /src && python -m pip install ".[babel]" \
&& pybabel compile -d src/myapp/locales
FROM python:3.14-slim
COPY --from=build /src /src
RUN python -m pip install /src # no [babel]: rendering needs the stdlib only
Antes de um release, o checklist a que esta página se reduz:
pybabel update --checkpassa — nenhuma mensagem mudou sem que os catálogos ficassem sabendo.pybabel compilecondiciona o build ao seu código de saída.- As entradas
fuzzyrestantes são intencionais — cada uma renderiza como texto de origem até que quem traduz a confirme. - A suíte de testes renderiza cada idioma distribuído uma vez com
strict=True. - O artefato de produção contém arquivos
.moe nenhum Babel. - O logger
gettext_tstringsestá direcionado ao monitoramento.
Próximos passos¶
- Extração — a referência para a metade de ferramentas desta página: opções de mapeamento, nomes de função personalizados, modo estrito e cada verificador.
- Guia — a metade de execução: plurais, contextos, strings preguiçosas e os modos de falha em detalhe.
- Como funciona — por que o msgid tem a forma que tem, e o que a validação de fato verifica.