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
13 comandos, en el orden en que los usa el ciclo. Cada uno lleva a su sección.
Traducir
Scaffolding
verbaly init deja el proyecto listo en un comando: una config tipada (.ts si tienes tsconfig, .mjs si no), tu catálogo fuente más uno por cada --locales, y los siguientes pasos según lo que encuentre: Astro, Nuxt, Next.js, SvelteKit y Vite tienen cada uno su integración, y 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:
pnpm add -D @verbaly/vite
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: verbaly.d.ts is stale fix: run
npx verbaly extract - Aviso orphans: 2 catalog keys are no longer referenced (old.title, hero.cta) fix: run
npx verbaly extract --pruneto drop them - 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 “¿por qué esto no se traduce?” Revisa tu config y tus catálogos, si está instalada la integración que tu framework necesita, si verbaly.d.ts está al día, las keys huérfanas y la salud de tus traducciones: las que faltan, las rotas y los avisos. También caza dos cosas que el gate del build no puede ver: t importado de un paquete de verbaly, que ningún bundler resuelve, y un mensaje que se quedó con un bloque plural o de formato como texto literal. También nombra cualquier archivo que no pudo parsear, como aviso: sus textos no se extraen, pero el archivo puede compilar igual en tu proyecto. Un reporte sano significa un build que pasa: doctor falla en todo lo que falla check, más fallos de setup como esos dos, y jamás en algo que compila y renderiza.
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 verbaly.d.ts. Avisa cuando un mensaje se quedó con un bloque plural o de formato como texto literal, y nombra cualquier archivo que no pudo parsear en vez de detenerse en él.
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 y cuántas están rotas. 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 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.
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 los borradores
La salida de la máquina es un borrador, no trabajo revisado. translate registra todo lo que escribe en locales/.verbaly-drafts.json (committeado con tus catálogos, jamás editado a mano), y verbaly review cierra el ciclo: lee los borradores y apruébalos.
¿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. 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.
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 dos preguntas, no una. ¿Está traducido cada mensaje? ¿Y puede cada traducción mostrar lo que muestra el original? Una traducción llena no es automáticamente una que funciona, así que esto también detiene el build, venga de donde venga: de una persona, de una máquina o de una edición a mano.
| 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 |
Dos hallazgos son advertencias: se imprimen, el código de salida sigue en 0, porque el texto igual se muestra. Uno es un plural al que le faltan formas que el idioma necesita, porque el polaco y el árabe piden más que el inglés. El otro es un caso puntual como =0 que el original tenía y la traducción dejó afuera, así que esa cantidad ahora cae en other.
Falle lo que falle, el reporte cierra con el paso que repara ese fallo: generar y rellenar cuando a un mensaje aún le falta la traducción, corregir la key cuando esa key no vive en ningún catálogo, y arreglar 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 el pull request, apuntando al archivo y la línea donde vive el texto: los errores como errores, las advertencias como advertencias, así que una advertencia nunca pone el job en rojo. Sin eso obtienes la lista en texto plano, con el mismo código de salida.
Render estático (SSG)
verbaly render mata el parpadeo de texto sin traducir en sitios estáticos: recorre tu HTML construido y rellena cada elemento data-verbaly por idioma usando el runtime de verdad: plurales, formato con Intl, data-verbaly-args, traducción de 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 ahí se queda ahí. Llevarlo hasta ahí es --redirect, más 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 al visitante a su idioma:
--redirect(orender.redirect) pone un script diminuto arriba de tu página de inicio que lo mueve antes de que se dibuje nada. Solo salta ahí, a propósito: un buscador que pide una página profunda tiene que recibir esa página y no un desvío. Una elección guardada siempre gana, la query y el hash van con él, y nunca puede entrar en bucle porque no hace nada cuando ya estás en ese idioma. Usa{ on: 'all' }si quieres que enrute cada página del 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 | Default | Descripción |
|---|---|---|
| --root | cwd | Raíz del proyecto |
| --dir | locales | Directorio de catálogos |
| --source | en | Idioma fuente |
| --locales | de los archivos | Idiomas extra, separados por coma |
| --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.