Referencia
CLI
El comando verbaly vive en @verbaly/compiler. Añádelo a tus dependencias de desarrollo para que npx verbaly funcione: los gestores de paquetes solo enlazan un comando de un paquete que instalaste tú, y los plugins dependen del compilador sin instalártelo.
Comandos
14 comandos, en el orden en que los usa el ciclo. Cada uno lleva a su sección.
Traducir
translateRellena las traducciones que faltan con un proveedor, marcadas como borrador.reviewLista los borradores y las traducciones desactualizadas, cada una con su texto fuente, para que una persona las apruebe.exportArchivos para traductores: XLIFF, CSV, PO, Android o iOS.importTrae de vuelta los archivos traducidos, validados antes de entrar.
Scaffolding
verbaly init deja un proyecto montado en un comando. Te da una config tipada (.ts si tienes tsconfig, .mjs si no) y tus catálogos: el fuente, más uno por cada idioma que pases en --locales. Después te dice qué queda por conectar. Astro, Nuxt, Next.js, SvelteKit y Vite tienen su propia integración; cualquier otro 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
Re-ejecutarlo es seguro: una config o catálogo existente se reporta como “kept” y queda intacto.
Chequeo de salud
verbaly doctor es el hermano de init para proyectos ya andando: inspecciona todo el setup y reporta cada hallazgo con el comando exacto que lo arregla. Verde significa sano; los problemas salen con 1.
npx verbaly doctor- Correcto config: verbaly.config.ts found
- Correcto catalogs: 3 locales (en, es, pt) in locales/
- Correcto 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 - Error 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 - Error translations: 1 broken translation (es): present but not rendering what the source renders fix: run
npx verbaly checkto read what each one lost
Un comando para responder a «¿por qué no se traduce esto?». Mira cuatro cosas.
- Tu montaje: la config, los catálogos, si cada código de idioma es uno que el navegador sabe leer, si el modo de URL cuadra con las URLs que escribe
render, si está instalada la integración que toca para tu framework y si los tipos generados están al día donde tu framework los lee,tsconfig.jsonincluido en Next.js. - Tus traducciones: las que faltan, las rotas, las desactualizadas, los avisos, las claves que solo tiene una traducción, las claves escritas con dos textos y las claves que ya ninguna línea de tu código pide.
- Lo que tu build no ve:
timportado desde un paquete de verbaly, que ningún bundler puede resolver, untcon otro nombre, cuyo texto nunca se extrae, un fichero fuera deincludeque escribet`…`, que ningún build traduce, y un mensaje que se dejó un bloque de plural o de formato como texto plano, con la forma que funciona. - Cualquier archivo que no pudo leer, como aviso. El texto de un archivo de código no se extrae, pero el archivo puede seguir construyendo bien en tu proyecto. Un conflicto de merge en
verbaly.locktambién es un aviso: se leen los dos lados, yextractlos escribe en el archivo para que lo incluyas en el commit del merge.
Un informe limpio quiere decir que check también pasa. doctor falla en todo lo que falla check, más los fallos de montaje como ese import, y nunca en algo que construye y renderiza bien.
Envolver texto hardcodeado
verbaly wrap encuentra texto hardcodeado en tu JSX y lo envuelve en t`…` por ti. Primero informa, y --write aplica. Lo ambiguo se lista para un humano en vez de adivinarlo, y un fichero donde t no está disponible se deja intacto y se informa, así el proyecto siempre sigue compilando.
Pasar desde otra librería
verbaly migrate pasa los catálogos que ya tienes. Tu fichero conserva su forma, anidada o plana, y lo único que tiene que reescribir es la interpolación: {{name}} pasa a {name}, porque una llave doblada es como Verbaly escribe una llave literal. Primero reporta, y --write aplica.
--plurals es la mitad opcional: fusiona _one y _other en un solo mensaje con variantes, que es lo que da la selección de plural automática, y dentro el número se imprime ya localizado. También entiende la forma antigua key + key_plural. Sin el flag, esas keys siguen funcionando tal cual.
Lo que no va a adivinar te lo lista: un formato de i18next dentro de las llaves, un valor {{- sin escapar}}, un anidado con $t(), y una fusión que pisaría una key que ya usas. Esos mensajes se quedan exactamente como estaban.
Extracción
verbaly extract escanea tu código (.js/.ts/.jsx/.tsx, más .svelte, .vue y .astro), escribe los mensajes nuevos en el catálogo fuente, añade huecos "" al resto y regenera los tipos. Nombra cualquier archivo que no pudo parsear en vez de detenerse en él. Cada texto fuente que siguió a tu código recibe una línea con el texto que tenía antes, así una edición que hiciste en el código se lee como un cambio, y una hecha en el catálogo muestra lo que deshizo. Después avisa, con archivo y línea, de lo que si no pasaría desapercibido: una clave escrita con dos textos, un t con otro nombre, un mensaje que se quedó con un bloque plural o de formato como texto literal, y un valor que un traductor vería como {_0}. Con --watch, cada aviso se dice una vez, y otra solo si vuelve.
Generación de tipos
verbaly typegen escribe los tipos de TypeScript de tus mensajes y nada más, desde el texto que publica tu build, donde tu framework los lee: src/verbaly.d.ts en una app de Vite o SvelteKit, .verbaly/types.d.ts en Next.js, y la carpeta de tipos propia de Astro y de Nuxt. extract, los servidores de desarrollo y el build escriben el mismo fichero, así que solo lo necesitas donde ninguno corrió: un clon fresco, o un job de CI a punto de correr tsc.
Cobertura
verbaly status muestra de un vistazo cuánto está traducido por idioma (es: 45/48 translated), cuántas traducciones automáticas siguen esperando revisión, cuántas están rotas y cuántas se escribieron para un texto fuente anterior. Es solo informativo y nunca falla; --json da los mismos números a badges y herramientas.
Pseudo-localización
verbaly pseudo llena un catálogo de QA (en-XA por defecto) desde el texto que publica tu build en el locale fuente: letras acentuadas, marcadores ⟦…⟧ y ~33% de padding. El texto que sale limpio en el build pseudo está hardcodeado; los layouts que se cortan se delatan antes de que llegue una traducción real.
Params, bloques de variantes y tags sobreviven verbatim, garantizados por la misma validación estructural de translate. Re-ejecutar regenera el catálogo completo.
Traducción máquina
verbaly translate cierra el ciclo: escribir → extract → translate → check en verde. El provider por defecto usa Claude (@anthropic-ai/sdk como peer opcional + ANTHROPIC_API_KEY); batches de 20 por request, cada uno diciéndole al modelo en qué archivos fuente vive el texto, así traduce con contexto. Cualquier modelo actual de Claude funciona con translate.model o --model, y un batch que el modelo rechaza pasa a un modelo de respaldo en vez de volver sin traducir.
Cada traducción se verifica: los placeholders y las etiquetas deben sobrevivir intactos. Si uno no lo hace, la traducción se rechaza y la entrada queda en "", así que check la sigue marcando hasta arreglarla.
Sin lock-in: conecta tu propio provider en verbaly.config.ts. En TypeScript, TranslateProvider lo tipa por ti, y origins te dice en qué archivos del código aparece cada texto, así lo traduces para el sitio donde se usa:
import type { TranslateProvider } from '@verbaly/compiler';
const provider: TranslateProvider = async ({ sourceLocale, targetLocale, messages, origins }) => ({
/* key → translation */
});
export default { translate: { provider } };Revisa lo pendiente
La salida de la máquina es un borrador, no trabajo revisado. translate registra todo lo que escribe en verbaly.lock, junto a tu config (committeado con tus catálogos, jamás editado a mano), y verbaly review cierra el ciclo: lee los borradores y apruébalos. El mismo archivo recuerda para qué texto fuente se escribió cada traducción, desde el momento en que se escribe, así que cuando cambias un texto, sus traducciones también esperan en review, como desactualizadas: actualízalas, o apruébalas si siguen valiendo. Cada una aparece con el texto que se publica al lado de su traducción, para que la juzgues ahí mismo, y --approve seguido de claves acepta solo esas.
¿Quieres bloquear el merge hasta que un humano firme? Añade --drafts al check de tu CI: sigue fallando mientras queden traducciones a máquina sin revisar, y --outdated hace lo mismo con las traducciones escritas para un texto fuente anterior. Un archivo de traductor traído con import cuenta como revisión por sí solo.
Traductores y TMS
¿Humanos en el ciclo? verbaly export escribe archivos XLIFF 2.0, CSV o gettext PO por idioma listos para el traductor y verbaly import los trae de vuelta, validados estructuralmente con el mismo gate que translate. El flujo completo, opciones de TMS incluidas, vive en Trabajar con traductores.
verbaly import valida cada entrada, y rechaza e informa las rotas. Las entradas PO marcadas como fuzzy cuentan como sin traducir, y las claves importadas dejan de contar como borradores sin revisar, salvo que pases --draft para un archivo que nadie ha leído aún. El idioma de cada archivo se lee como uno de los tuyos, así que el pt_BR de gettext llena tu pt-BR. Un archivo de un idioma nuevo empieza su catálogo con el código bien escrito, salvo que tu config liste sus locales: entonces un idioma fuera de la lista se salta con un aviso y código de salida 1, porque tu app nunca lo cargaría.
Export también escribe recursos nativos de móvil: --format android-xml para la estructura res/ de Android y --format ios-strings para Xcode. Las keys sin traducir se omiten para que la app caiga a tu idioma fuente (--missing aplica solo a los formatos de traductor). Todos los formatos escriben en verbaly-export/ salvo que apuntes --out a otro sitio.
Qué revisa el gate
check hace cuatro preguntas. ¿Está traducido cada mensaje? ¿Existe cada clave que escribe el código? ¿Tiene cada clave un solo texto, para que ningún sitio muestre palabras escritas para otro? ¿Y puede cada traducción renderizar lo que renderiza su fuente? Una traducción rellena no es automáticamente una que funciona, así que estas también hacen fallar check, vengan de donde vengan: un traductor, una máquina o una edición a mano. Cada build lista los mismos problemas y termina, así que corre check en el CI.
| La traducción | Por qué falla |
|---|---|
perdió un {param}, o le cambió el nombre | el valor nunca llega al texto |
perdió un <em>, o ganó uno | el énfasis o la marca del enlace desaparece |
| convirtió un bloque de plural en texto plano | una sola redacción para cualquier cantidad |
tiene un bloque de plural sin caso other | cada cantidad que no lista se muestra vacía |
Algunos hallazgos son advertencias: se imprimen y el código de salida sigue en 0, porque el texto igual se muestra.
- Un plural al que le faltan formas que el idioma necesita: el polaco y el árabe piden más que el inglés.
- Un caso como
=0que el original tenía y la traducción dejó afuera, así que esa cantidad cae enother. - Una clave que solo tiene una traducción. Donde tu código la lee, el idioma fuente muestra la clave misma; donde nada la lee, solo suma peso, que
extract --prunequita. - Un texto de tu catálogo fuente distinto del que está en tu código: el que se publica es el del código.
- Una traducción escrita para un texto fuente anterior, leída contra el texto de tu código. Añade
--outdatedacheckpara que falle, y aparece junto a los fallos.
Un build corre el mismo gate, y cuando encuentra advertencias imprime una sola línea que dice cuántas hay, y pasa.
Falle lo que falle, el informe termina diciéndote cómo arreglarlo: crear y rellenar un mensaje que todavía no tiene traducción, corregir la clave de uno que no vive en ningún catálogo, dar a cada texto su propia clave donde una clave tiene dos, o reparar el mensaje mismo cuando está roto.
Ejemplo de CI
- run: pnpm install
- run: npx verbaly check --reporter github # each finding becomes a PR annotation
- run: pnpm buildCon --reporter github, cada hallazgo aparece como anotación en la pull request, en el archivo y la línea donde vive el texto. Los errores llegan como errores y los avisos como avisos, así que un aviso nunca pone el job en rojo. Sin él tienes la lista de siempre, con el mismo código de salida.
Render estático (SSG)
verbaly render es lo que mata el parpadeo de texto sin traducir en un sitio estático. Recorre tu HTML construido y rellena cada elemento data-verbaly, una vez por idioma, usando el runtime de verdad: plurales, formato de Intl, data-verbaly-args, atributos y texto rico de la whitelist. Dentro de un idioma, los enlaces a páginas que ese idioma también tiene conservan su prefijo, así que quien llega se queda. Llevarlo hasta ahí es --redirect, aquí abajo.
- Cada idioma recibe su
<html lang>y<html dir>(los locales de derecha a izquierda se espejan correcto); el locale fuente se llena in-place y el resto se espeja adist/<locale>/…. - Los atributos
data-verbalyquedan en el output, así que el cambio de idioma client-side sigue funcionando sobre la página pre-renderizada. - Los mensajes se escapan como HTML (nunca se inyectan), las keys faltantes se reportan y quedan intactas, las re-ejecuciones son idempotentes y los atributos traducidos pasan por los mismos guards que el runtime: URLs inseguras bloqueadas,
style/srcdocnunca se escriben. - Links nombrados: define
render.linksenverbaly.config.*(odata-verbaly-linkspor elemento) y los mensajes rich pre-renderizan elementos<a href>reales, atributos escapados, conjavascript:bloqueado. - SEO multi-idioma: pon
render.baseUrl(o pasa--base-url) y cada página recibe alternateshreflangrecíprocos;--sitemapescribe un sitemap i18n y--cleanborra páginas de idiomas que ya no existen. - El sitemap lista solo páginas que un buscador debería indexar: una que redirige, o que lleva
noindex, se queda fuera. Todas las páginas se siguen traduciendo y publicando, así que quien aterrice en una la recibe en su idioma. Deja fuera más por nombre conrender.exclude, una lista de globs contra la ruta dentro de tu carpeta de build. - ¿Usas otro data attribute?
--attributeapunta el renderer a ese, igual que lo recibe el runtime, así los dos lados van a la par. - Espejos self-canonical:
rel="canonical"yog:urlse reescriben a la URL propia de cada idioma, porque un canonical cruzado haría que los buscadores ignoren el hreflang. - Manda a un visitante a su idioma.
--redirect(orender.redirect) pone un script pequeño arriba de tu portada que lo mueve antes de que se dibuje nada. Solo salta ahí: un buscador que pide una página profunda tiene que recibir esa página, no un desvío. Una elección guardada siempre gana, la query y el hash se van con él, y no puede entrar en bucle, porque no hace nada si ya estás en ese idioma. Añade{ on: 'all' }si quieres que también enrute cada página de tu idioma fuente, ystorageKeypara apuntarlo a la clave donde guardas la elección. - ¿Sitio servido desde una subcarpeta?
--base /app(orender.base) le dice al renderer dónde empieza el sitio, así los enlaces dentro de cada idioma leen/app/es/docsen vez de apuntar a la nada. La misma opción existe enlocaleFromPathylocalePath, así que el runtime está de acuerdo con el build.
Flags
| Flag | Por defecto | Descripción |
|---|---|---|
| --root | cwd | Raíz del proyecto |
| --dir | locales | Directorio de catálogos |
| --source | en | Idioma fuente |
| --locales | de los archivos | Solo estos idiomas, siempre con el fuente incluido (en init: los que crea) |
| --prune | off | Elimina claves sin referencias (solo extract) |
| --watch | off | Re-extrae cuando cambian los archivos fuente: extracción viva para setups con webpack, Rspack y Rollup (solo extract) |
| --locale | en-XA | Id del pseudo-locale (solo pseudo) |
| --site | dist | Directorio del sitio compilado (solo render) |
| --base | raíz del sitio | Subruta bajo la que se sirve el sitio (solo render) |
| --redirect | off | Manda a su idioma a quien llega a la página de inicio (solo render) |
Las flags se validan por comando: una flag que pertenece a otro comando termina con un error accionable en vez de ignorarse en silencio: translate --locale es te dice que querías --locales.
Archivo de config
Los flags ganan sobre verbaly.config.{js,mjs,ts,mts,json} en la raíz del proyecto, y el plugin toma las mismas opciones. Todas ellas, con su valor por defecto, viven en Configuración.
Agentes de código
Tu agente de código puede correr este mismo ciclo sin tocar la terminal, a través del servidor @verbaly/mcp. Ese canal tiene página propia. Agentes de código cubre el servidor, sus herramientas y recursos, la Agent Skill instalable y el índice llms.txt que sirve este sitio.