Referência
CLI
O comando verbaly vive em @verbaly/compiler. Adicione-o às suas dependências de desenvolvimento para que npx verbaly funcione: os gestores de pacotes só ligam um comando de um pacote que você mesmo instalou, e os plugins dependem do compilador sem o instalar por você.
Comandos
14 comandos, na ordem em que o ciclo os usa. Cada um leva à sua seção.
Traduzir
translatePreenche as traduções que faltam com um provedor, marcadas como rascunho.reviewLista os rascunhos e as traduções desatualizadas, cada uma com o seu texto fonte, para que uma pessoa as aprove.exportArquivos para tradutores: XLIFF, CSV, PO, Android ou iOS.importTraz de volta os arquivos traduzidos, validados antes de entrar.
Scaffolding
verbaly init deixa um projeto montado num comando. Te dá uma config tipada (.ts se você tiver tsconfig, .mjs se não) e os seus catálogos: o de origem, mais um para cada idioma que você passar em --locales. Depois te diz o que falta ligar. Astro, Nuxt, Next.js, SvelteKit e Vite têm a sua própria integração; qualquer outro bundler usa @verbaly/unplugin.
npx verbaly init --locales es,pt- created: verbaly.config.ts, locales/en.json, locales/es.json, locales/pt.json
- detected: vite
- next steps: 1.
pnpm add verbaly @verbaly/vite - 2. add verbaly() to the plugins in vite.config
Reexecutar é seguro: uma config ou catálogo existente é reportado como “kept” e fica intacto.
Checagem de saúde
verbaly doctor é o irmão do init para projetos já rodando: inspeciona todo o setup e reporta cada achado com o comando exato que o corrige. Verde significa saudável; problemas saem com 1.
npx verbaly doctor- Correto config: verbaly.config.ts found
- Correto catalogs: 3 locales (en, es, pt) in locales/
- Correto plugin: @verbaly/vite installed for vite
- Aviso types: src/verbaly.d.ts is stale fix: run
npx verbaly typegen - Aviso orphans: 2 catalog keys are no longer referenced (old.title, hero.cta) fix: run
npx verbaly extract --pruneto drop them - Aviso outdated: 1 translation written for an older source text (es: hero.title) fix: update them, or keep them with
npx verbaly review --approve - Erro imports: 1 file imports t from a verbaly package, which never exports it (src/app.ts) fix: t comes from your instance (React:
const t = useT()) or fromvirtual:verbaly - Erro translations: 1 broken translation (es): present but not rendering what the source renders fix: run
npx verbaly checkto read what each one lost
Um comando para responder a “por que isto não traduz?”. Olha para quatro coisas.
- A sua montagem: a config, os catálogos, se cada código de idioma é um que o navegador consegue ler, se o modo de URL bate com as URLs que o
renderescreve, se está instalada a integração certa para o seu framework e se os tipos gerados estão atualizados onde o seu framework os lê,tsconfig.jsonincluído no Next.js. - Suas traduções: as que faltam, as quebradas, as desatualizadas, os avisos, as chaves que só uma tradução tem, as chaves escritas com dois textos e as chaves que nenhuma linha do seu código pede mais.
- O que o seu build não vê:
timportado de um pacote do verbaly, que nenhum bundler consegue resolver, umtcom outro nome, cujo texto nunca é extraído, um arquivo fora doincludeque escrevet`…`, que nenhum build traduz, e uma mensagem que ficou com um bloco de plural ou de formato como texto simples, com a forma que funciona. - Qualquer arquivo que não conseguiu ler, como aviso. O texto de um arquivo de código não é extraído, mas o arquivo pode continuar compilando normalmente no seu projeto. Um conflito de merge no
verbaly.locktambém é um aviso: os dois lados são lidos, e oextractos escreve no arquivo para você incluir no commit do merge.
Um relatório limpo quer dizer que o check também passa. O doctor falha em tudo aquilo em que o check falha, mais as falhas de montagem como esse import, e nunca em algo que constrói e renderiza bem.
Envolver texto fixo
verbaly wrap encontra texto fixo no seu JSX e o envolve em t`…` por você. Primeiro informa, e --write aplica. O que for ambíguo é listado para um humano em vez de adivinhado, e um arquivo onde t não está disponível fica intacto e é informado, então o projeto continua sempre a compilar.
Passar de outra biblioteca
verbaly migrate passa os catálogos que você já tem. O seu arquivo mantém a forma, aninhada ou plana, e a única coisa que tem de reescrever é a interpolação: {{name}} passa a {name}, porque uma chave duplicada é como o Verbaly escreve uma chave literal. Primeiro reporta, e --write aplica.
--plurals é a metade opcional: junta _one e _other numa só mensagem com variantes, que é o que dá a seleção de plural automática, e lá dentro o número se escreve já localizado. Também entende a forma antiga key + key_plural. Sem a flag, essas keys continuam funcionando tal como estão.
O que não vai adivinhar fica listado: um formato do i18next dentro das chaves, um valor {{- sem escape}}, um aninhamento com $t(), e uma junção que apagaria uma key que você já usa. Essas mensagens ficam exatamente como estavam.
Extração
verbaly extract varre seu código (.js/.ts/.jsx/.tsx, mais .svelte, .vue e .astro), escreve as mensagens novas no catálogo fonte, adiciona lacunas "" ao resto e regenera os tipos. Aponta qualquer arquivo que não conseguiu analisar em vez de parar nele. Cada texto fonte que seguiu o seu código ganha uma linha com o texto que tinha antes, então uma edição que você fez no código se lê como uma mudança, e uma feita no catálogo mostra o que ela desfez. Depois avisa, com arquivo e linha, o que de outra forma passaria despercebido: uma chave escrita com dois textos, um t com outro nome, uma mensagem que ficou com um bloco plural ou de formato como texto literal, e um valor que um tradutor veria como {_0}. Com --watch, cada aviso é dito uma vez, e de novo só se voltar.
Geração de tipos
verbaly typegen escreve os tipos TypeScript das suas mensagens e nada mais, a partir do texto que o seu build publica, onde o seu framework os lê: src/verbaly.d.ts num app Vite ou SvelteKit, .verbaly/types.d.ts no Next.js, e a pasta de tipos própria do Astro e do Nuxt. O extract, os servidores de desenvolvimento e o build escrevem o mesmo arquivo, então você só precisa dele onde nenhum rodou: um clone novo, ou um job de CI prestes a rodar o tsc.
Cobertura
verbaly status mostra num relance quanto está traduzido por idioma (es: 45/48 translated), quantas traduções automáticas ainda esperam revisão, quantas estão quebradas e quantas foram escritas para um texto fonte anterior. É apenas informativo e nunca falha; --json entrega os mesmos números a badges e ferramentas.
Pseudo-localização
verbaly pseudo preenche um catálogo de QA (en-XA por padrão) a partir do texto que o seu build publica no locale fonte: letras acentuadas, marcadores ⟦…⟧ e ~33% de padding. Texto que sai limpo no build pseudo está hardcoded; layouts que cortam se revelam antes de uma tradução real.
Params, blocos de variantes e tags sobrevivem verbatim, garantido pela mesma validação estrutural do translate. Re-executar regenera o catálogo completo.
Tradução de máquina
verbaly translate fecha o ciclo: escrever → extract → translate → check verde. O provider padrão usa Claude (@anthropic-ai/sdk como peer opcional + ANTHROPIC_API_KEY); batches de 20 por request, cada um dizendo ao modelo em quais arquivos fonte o texto vive, então ele traduz com contexto. Qualquer modelo atual do Claude funciona com translate.model ou --model, e um batch que o modelo recusa passa para um modelo de reserva em vez de voltar sem tradução.
Cada tradução é verificada: os placeholders e as tags precisam sobreviver intactos. Se um não sobrevive, a tradução é rejeitada e a entrada fica em "", então o check continua sinalizando até você corrigir.
Sem lock-in: conecte seu próprio provider no verbaly.config.ts. Em TypeScript, TranslateProvider tipa ele para você, e origins diz em quais arquivos do código cada texto aparece, então você traduz pensando no lugar onde ele é usado:
import type { TranslateProvider } from '@verbaly/compiler';
const provider: TranslateProvider = async ({ sourceLocale, targetLocale, messages, origins }) => ({
/* key → translation */
});
export default { translate: { provider } };Revise o que está pendente
A saída da máquina é um rascunho, não trabalho revisado. O translate registra tudo o que escreve em verbaly.lock, ao lado da sua config (commitado com seus catálogos, nunca editado à mão), e o verbaly review fecha o ciclo: leia os rascunhos e aprove. O mesmo arquivo lembra para qual texto fonte cada tradução foi escrita, desde o momento em que é escrita, então quando você muda um texto, as traduções dele também esperam no review, como desatualizadas: atualize-as, ou aprove se ainda valem. Cada uma aparece com o texto que é publicado ao lado da sua tradução, para você julgá-la ali mesmo, e --approve seguido de chaves aceita só essas.
Quer bloquear o merge até que um humano aprove? Adicione --drafts ao check do seu CI: ele continua falhando enquanto restarem traduções automáticas sem revisar, e --outdated faz o mesmo com as traduções escritas para um texto fonte anterior. Um arquivo de tradutor trazido com import conta como revisão por si só.
Tradutores e TMS
Humanos no ciclo? verbaly export escreve arquivos XLIFF 2.0, CSV ou gettext PO por idioma prontos para o tradutor e verbaly import os traz de volta, validados estruturalmente com o mesmo gate do translate. O fluxo completo, opções de TMS incluídas, vive em Trabalhar com tradutores.
verbaly import valida cada entrada, e rejeita e informa as quebradas. Entradas PO marcadas como fuzzy contam como não traduzidas, e as chaves importadas deixam de contar como rascunhos sem revisar, a menos que você passe --draft para um arquivo que ninguém leu ainda. O idioma de cada arquivo é lido como um dos seus, então o pt_BR do gettext preenche o seu pt-BR. Um arquivo de um idioma novo começa o seu catálogo com o código bem escrito, a menos que a sua config liste os seus locales: aí um idioma fora da lista é pulado com um aviso e código de saída 1, porque o seu app nunca o carregaria.
O export também escreve recursos nativos de mobile: --format android-xml para a estrutura res/ do Android e --format ios-strings para o Xcode. As keys sem tradução ficam de fora para o app cair no seu idioma fonte (--missing vale só para os formatos de tradutor). Todos os formatos escrevem em verbaly-export/ a menos que você aponte --out para outro lugar.
O que o gate verifica
check faz quatro perguntas. Cada mensagem está traduzida? Cada chave que o código escreve existe? Cada chave tem um só texto, para que nenhum lugar mostre palavras escritas para outro? E cada tradução consegue renderizar o que a fonte renderiza? Uma tradução preenchida não é automaticamente uma que funciona, então essas também fazem o check falhar, venham de onde vierem: um tradutor, uma máquina ou uma edição à mão. Cada build lista os mesmos problemas e termina, então rode o check no CI.
| A tradução | Por que falha |
|---|---|
perdeu um {param}, ou mudou o nome dele | o valor nunca chega ao texto |
perdeu um <em>, ou ganhou um | a ênfase ou a marca do link desaparece |
| transformou um bloco de plural em texto simples | uma única redação para qualquer quantidade |
tem um bloco de plural sem o caso other | cada quantidade que não lista aparece vazia |
Alguns achados são avisos: eles aparecem e o código de saída continua em 0, porque o texto ainda é mostrado.
- Um plural sem as formas que o idioma precisa: polonês e árabe pedem mais que o inglês.
- Um caso como
=0que o original tinha e a tradução deixou de fora, então essa quantidade cai emother. - Uma chave que só uma tradução tem. Onde o seu código a lê, o idioma fonte mostra a própria chave; onde nada a lê, ela só soma peso, que o
extract --pruneremove. - Um texto do seu catálogo fonte diferente do que está no seu código: o que vai para produção é o do código.
- Uma tradução escrita para um texto fonte anterior, lida contra o texto do seu código. Adicione
--outdatedaocheckpara que falhe, e ela aparece junto às falhas.
Um build roda o mesmo gate, e quando encontra avisos imprime uma única linha dizendo quantos são, e passa.
Falhe o que falhar, o relatório termina dizendo como corrigir: criar e preencher uma mensagem que ainda não tem tradução, corrigir a chave de uma que não está em nenhum catálogo, dar a cada texto sua própria chave quando uma chave tem dois, ou reparar a própria mensagem quando ela está quebrada.
Exemplo de CI
- run: pnpm install
- run: npx verbaly check --reporter github # each finding becomes a PR annotation
- run: pnpm buildCom --reporter github, cada achado aparece como anotação no pull request, no arquivo e na linha onde o texto vive. Os erros chegam como erros e os avisos como avisos, por isso um aviso nunca põe o job a vermelho. Sem ele você tem a lista de sempre, com o mesmo código de saída.
Renderização estática (SSG)
verbaly render é o que mata o piscar de texto por traduzir num site estático. Percorre o seu HTML construído e preenche cada elemento data-verbaly, uma vez por idioma, usando o runtime a sério: plurais, formatação do Intl, data-verbaly-args, atributos e texto rico da whitelist. Dentro de um idioma, os links para páginas que esse idioma também tem mantêm o seu prefixo, por isso quem chega fica. Levá-lo até lá é o --redirect, aqui abaixo.
- Cada idioma recebe o seu
<html lang>e<html dir>(locales da direita para a esquerda espelham correto); o locale fonte é preenchido in-place e o resto é espelhado paradist/<locale>/…. - Os atributos
data-verbalyficam no output, então a troca de idioma client-side continua funcionando sobre a página pré-renderizada. - Mensagens são escapadas como HTML (nunca injetadas), keys faltantes são reportadas e ficam intactas, re-execuções são idempotentes e atributos traduzidos passam pelos mesmos guards do runtime: URLs inseguras bloqueadas,
style/srcdocnunca são escritos. - Links nomeados: defina
render.linksemverbaly.config.*(oudata-verbaly-linkspor elemento) e as mensagens rich pré-renderizam elementos<a href>reais, atributos escapados, comjavascript:bloqueado. - SEO multi-idioma: defina
render.baseUrl(ou passe--base-url) e cada página recebe alternateshreflangrecíprocos;--sitemapescreve um sitemap i18n e--cleanremove páginas de idiomas que já não existem. - O sitemap lista apenas páginas que um buscador deveria indexar: uma que redireciona, ou que leva
noindex, fica de fora. Todas as páginas continuam sendo traduzidas e publicadas, então quem chegar a uma a recebe no seu idioma. Deixe mais de fora por nome comrender.exclude, uma lista de globs contra o caminho dentro da sua pasta de build. - Você usa outro data attribute?
--attributeaponta o renderer para ele, do mesmo jeito que o runtime recebe o seu, então os dois lados andam juntos. - Espelhos self-canonical:
rel="canonical"eog:urlsão reescritos para a URL própria de cada idioma, porque um canonical cruzado faria os buscadores ignorarem o hreflang. - Manda um visitante para o seu idioma.
--redirect(ourender.redirect) põe um script pequeno no topo da sua página inicial que o move antes de se desenhar seja o que for. Só dispara ali: um motor de busca que pede uma página profunda tem de receber essa página, não um desvio. Uma escolha guardada ganha sempre, a query e o hash vão com ele, e não pode entrar em ciclo, porque não faz nada se você já estiver nesse idioma. Acrescente{ on: 'all' }se você quiser que cada página do seu idioma de origem também encaminhe, estorageKeypara apontá-lo para a chave onde você salva a escolha. - Site servido a partir de uma subpasta?
--base /app(ourender.base) diz ao renderer onde o site começa, por isso os links dentro de cada idioma leem/app/es/docsem vez de apontarem para o nada. A mesma opção existe emlocaleFromPathelocalePath, por isso o runtime concorda com o build.
Flags
| Flag | Padrão | Descrição |
|---|---|---|
| --root | cwd | Raiz do projeto |
| --dir | locales | Diretório de catálogos |
| --source | en | Idioma fonte |
| --locales | dos arquivos | Só estes idiomas, sempre com o fonte incluído (no init: os que cria) |
| --prune | off | Remove chaves sem referências (só no extract) |
| --watch | off | Re-extrai quando os arquivos fonte mudam: extração viva para setups com webpack, Rspack e Rollup (só extract) |
| --locale | en-XA | Id do pseudo-locale (só pseudo) |
| --site | dist | Diretório do site compilado (só render) |
| --base | raiz do site | Subcaminho onde o site é servido (só render) |
| --redirect | off | Manda para o seu idioma quem chega à página inicial (só render) |
As flags são validadas por comando: uma flag que pertence a outro comando termina com um erro acionável em vez de ser ignorada em silêncio: translate --locale es avisa que você queria --locales.
Arquivo de config
As flags vencem verbaly.config.{js,mjs,ts,mts,json} na raiz do projeto, e o plugin recebe as mesmas opções. Todas elas, com o seu valor padrão, vivem em Configuração.
Agentes de código
Seu agente de código pode rodar esse mesmo ciclo sem tocar no terminal, através do servidor @verbaly/mcp. Esse canal tem página própria. Agentes de código cobre o servidor, suas ferramentas e recursos, a Agent Skill instalável e o índice llms.txt que este site serve.