verbatra types
Genera declaraciones de TypeScript para las claves de tu catálogo y los argumentos de sus mensajes, sin llamar a un proveedor ni leer una clave de API.
Página traducida automáticamente
Disponible a partir de 0.11.0
Una clave mal escrita es un fallo de búsqueda en tiempo de ejecución. Un argumento de interpolación olvidado es un marcador de posición renderizado tal cual en pantalla. types convierte ambas cosas en errores de compilación: lee tu catálogo de origen a través del adaptador de formato configurado y escribe un archivo de declaraciones con una unión de todas las claves y, por clave, los argumentos que toma su mensaje. No gasta nada: no se construye ningún proveedor, no se hace ninguna petición de red y no se lee ninguna clave de API, así que funciona en un checkout recién clonado, antes de que exista clave alguna.
Lee un archivo (tu catálogo de la locale de origen) y escribe un archivo (la declaración). Nunca toca un archivo de locale de destino.
Sinopsis
verbatra types [flags]Flags
| Flag | Argumento | Valor por defecto | Efecto |
|---|---|---|---|
--cwd | <path> | directorio actual | resolver la configuración y los archivos de locale desde este directorio |
--config | <path> | buscar una | cargar este archivo de configuración en lugar de buscar uno |
--out | <path> | verbatra-types.d.ts | dónde escribir la declaración, relativo al directorio de trabajo y dentro de él |
--check | ninguno | desactivado | informar si la declaración comprometida sigue vigente, sin escribir nada |
--json | ninguno | desactivado | imprime un envoltorio JSON en stdout que lleva el resultado bajo result |
Qué genera
A partir de un locales/en.json como este:
{
"app": { "title": "Verbatra" },
"greeting": "Hello {{name}}",
"cart": { "item_one": "{{count}} item", "item_other": "{{count}} items" }
}obtienes esto:
// Generated by verbatra from locales/en.json (i18next-json). Do not edit by hand.
// Re-run `verbatra types` after the source catalog changes.
/** An argument whose type the source catalog does not record. */
export type VerbatraArgument = string | number;
/** The arguments of a message that takes none: passing any argument is a type error. */
export type VerbatraNoArguments = Record<string, never>;
/**
* The arguments of a message verbatra could not analyse, so nothing is claimed about them.
* Every key carrying this type is listed by `verbatra types` when it generates this file.
*/
export type VerbatraUnknownArguments = Record<string, VerbatraArgument>;
/** Every key in the source catalog, mapped to the arguments its message takes. */
export interface VerbatraMessages {
"app.title": VerbatraNoArguments;
"greeting": { readonly "name": VerbatraArgument };
"cart.item_one": { readonly "count": VerbatraArgument };
"cart.item_other": { readonly "count": VerbatraArgument };
}
/** Every key in the source catalog. */
export type VerbatraMessageKey = keyof VerbatraMessages;
/** The arguments the message at `Key` takes. */
export type VerbatraMessageArguments<Key extends VerbatraMessageKey> =
VerbatraMessages[Key];
/** The keys the source catalog marks as carrying plural forms. */
export type VerbatraPluralMessageKey = "cart.item_one" | "cart.item_other";Tipa tu propia función de traducción contra ella y el compilador hace el resto:
import type { VerbatraMessageArguments, VerbatraMessageKey } from "./verbatra-types.js";
declare function t<Key extends VerbatraMessageKey>(
key: Key,
args: VerbatraMessageArguments<Key>,
): string;
t("greeting", { name: "Ada" }); // fine
t("greting", { name: "Ada" }); // error: not a key in the catalog
t("greeting", {}); // error: the name argument is required
t("app.title", { name: "Ada" }); // error: this message takes no argumentQué afirma y qué no
Las claves son exactamente lo que el adaptador de formato ya produjo al leer el catálogo, en orden del documento, y por eso el mismo catálogo produce siempre los mismos bytes. En la mayoría de los formatos, los argumentos vienen de los tokens de marcadores que el adaptador extrajo. En los dos formatos de mensajes ICU, next-intl-json y arb, cada mensaje se lee con el mismo parser ICU que usa el adaptador, así que un argumento que solo usan algunas ramas select o plural se declara igualmente, y cada argumento recibe su tipo según cómo lo formatea el mensaje. No se adivina nada.
| Tu mensaje | Qué se declara |
|---|---|
| sin marcadores de posición | VerbatraNoArguments, así que pasar cualquier cosa es un error de tipo |
marcadores con nombre ({{name}}, {name}, %(name)s) | una forma de objeto con esos nombres |
marcadores numerados o anónimos ({0}, %@, %1$d) | una tupla readonly, una posición por argumento |
un argumento que solo usan algunas ramas select o plural (next-intl-json, arb) | un miembro opcional, name?:, o para un argumento numerado una posición opcional de la tupla ([number, VerbatraArgument?]); uno que usan todas las ramas sigue siendo obligatorio |
un argumento ICU number, plural o selectordinal ({count, number}, {count, plural, ...}) en next-intl-json o arb | number |
un argumento ICU date o time ({d, date, short}) en next-intl-json o arb | Date | number |
un argumento ICU select ({gender, select, ...}) en next-intl-json o arb | string |
un formateador number de i18next ({{count, number}}) en i18next-json, ngx-translate-json o yaml | number |
un formateador datetime de i18next ({{d, datetime}}) en esos mismos tres formatos | Date | number |
una interpolación sin escapar de i18next ({{- name}}) | el nombre detrás del prefijo, name |
un argumento MessageFormat number, plural o choice ({0,number}) en properties | number |
una conversión printf numérica (%d, %1$d) | number |
una conversión printf de cadena (%s, %(name)s) | string |
cualquier otro marcador, incluido un argumento ICU simple ({name}) | VerbatraArgument, un alias de string | number |
sintaxis de mensaje inválida, que solo comprueban next-intl-json y arb | VerbatraUnknownArguments, y la clave se nombra en la salida |
| un mensaje que a la vez nombra y numera sus argumentos | VerbatraUnknownArguments, y la clave se nombra en la salida |
Una tupla solo puede omitir posiciones al final, así que un argumento numerado que solo usan algunas ramas se declara opcional únicamente si todas las posiciones que le siguen también lo son. En {0, select, a {{1}} other {x}} {2}, la posición 1 sigue siendo obligatoria, porque la sigue la posición obligatoria 2.
Los nombres de argumento vienen de los tokens de marcadores que el adaptador extrajo, así que un formato cuyos marcadores no llevan nombre (Apple .strings, Android strings.xml, conversiones posicionales de gettext) recibe una tupla en lugar de nombres inventados. Los tipos de argumento vienen solo de lo que tu catálogo registra de verdad: cómo formatea un mensaje ICU un argumento, o el formateador o la conversión que nombra un marcador. Cuando un formato nombra un argumento sin decir qué puede pasarse para él, verbatra declara string | number en vez de afirmar un tipo que el catálogo nunca llevó. Un nombre que aparece varias veces con tipos distintos se declara como la unión de todo lo que acepta cada uso: {d, date, short} junto a un {d} simple pasa a ser Date | number | string, y {n, plural, ...} junto a un {n} simple pasa a ser string | number. Nada se declara jamás como any.
Las claves se emiten siempre como literales de cadena entrecomillados, así que una clave con un punto, una palabra reservada, un dígito inicial, una comilla, o incluso la clave vacía, se declara tal cual en lugar de descartarse o volver a partirse. Una clave que el adaptador reportó como excluida de la traducción (una hoja suelta que no es una cadena, un recurso de Android con translatable="false") no se declara nunca, y se nombra en la salida para que veas qué quedó fuera.
Una clave cuyo propio nombre contiene un punto
La clave declarada se escribe como verbatra escribe la clave, en la misma forma que usa en verbatra.lock.json, y no es una sintaxis de búsqueda que entienda tu biblioteca de i18n. Un catálogo JSON con {"a.b": "..."} lleva una clave escrita a\.b, deliberadamente distinta de la ruta anidada a.b, y esa grafía escapada es la que declara el archivo:
export interface VerbatraMessages {
"a\\.b": VerbatraNoArguments;
}No esperes que un runtime resuelva esa grafía. i18next, por ejemplo, no tiene escape para su separador de claves, así que t("a\\.b") no es la forma en que busca una clave con un punto literal. Si tu catálogo lleva claves así, configura el runtime para que el punto no sea un separador (en i18next, pon keySeparator en otro carácter, o en false para un catálogo plano) y traduce la clave declarada a la clave que busca tu runtime en tu propio wrapper tipado.
El único caso en que la declaración se equivoca en lugar de solo callar
Solo next-intl-json y arb validan la sintaxis de mensaje. Para cualquier otro formato, un valor roto y no simplemente llano ("Hello {name", sin la llave de cierre) no produce ningún token de marcador, así que el mensaje se declara como si no tomara argumentos, y pasar el argumento que realmente quiere pasa a ser un error de compilación. Nada lo reporta, porque nada lo detectó. Lo que hay que arreglar es el catálogo, no la declaración.
Claves con plural
No se agrupa nada. i18next lleva el plural como sufijo de clave, así que cart.item_one y cart.item_other se declaran como las dos claves separadas que realmente son, porque esas son las dos claves que tu código busca de verdad. VerbatraPluralMessageKey lista justo las claves que el adaptador marcó como portadoras de formas plurales, por si quieres acotarte a ellas.
Dónde acaba la declaración
Por defecto acaba en verbatra-types.d.ts en la raíz de tu proyecto. Súbela al control de versiones. Es un artefacto versionado, no un borrador local, por dos razones: un compañero o un job de CI comprueba tipos contra ella sin tener que ejecutar verbatra antes, y --check necesita un archivo versionado con el que comparar. Nada la añade a .gitignore.
Pasa --out para dejarla donde tu aplicación ya importa, por ejemplo --out src/generated/messages.d.ts. Los directorios que falten se crean. Una ruta de salida se rechaza con TYPES_OUTPUT_CONFLICT cuando:
- no nombra ningún archivo,
- es absoluta,
- se sale del directorio de trabajo con
.., - no es un archivo TypeScript, es decir, su nombre no termina en
.ts,.mtso.cts, - nombra un archivo de locale configurado, de origen o de destino, para que la generación nunca sobrescriba un catálogo,
- es el archivo de bloqueo,
verbatra.lock.json, - es la caché local de memoria de traducción,
verbatra.cache.json, - es un archivo donde verbatra busca su configuración:
package.json,.verbatrarc,.verbatrarc.json,.verbatrarc.yaml,.verbatrarc.yml,.verbatrarc.js,.verbatrarc.cjs,.verbatrarc.ts,verbatra.config.js,verbatra.config.cjsoverbatra.config.ts, - es el archivo de configuración que la ejecución cargó de verdad, incluido uno que indicaste con
--config, - es un archivo existente que no empieza con la línea de cabecera
// Generated by verbatra fromque verbatra escribe al principio de cada declaración (se ignoran una marca de orden de bytes y líneas en blanco al inicio), o es demasiado grande para comprobarlo.
Los nombres de archivo se comparan sin distinguir mayúsculas de minúsculas, así que Verbatra.Config.ts también se rechaza. Todos los casos menos el último se rechazan antes de leer o escribir nada. El último solo lo comprueba una ejecución que fuera a escribir, y deja tu archivo intacto: elige otro --out, o borra el archivo si de verdad es una declaración antigua. --check nunca rechaza un archivo existente; solo compara.
Mantenerlo honesto en CI
--check no escribe nada y sale con 1 cuando la declaración versionada ya no coincide con lo que produciría una generación nueva. La comparación es byte a byte, sin reformatear, así que detecta una clave añadida al catálogo y nunca regenerada:
- run: npx verbatra types --checkPrepara una cosa antes de confiar en ello. La declaración se escribe siempre con finales de línea de tipo line feed y --check compara bytes exactos, así que un checkout que reescriba los finales de línea la deja permanentemente desactualizada sin que volver a ejecutar lo arregle. Fija los finales de línea en .gitattributes:
verbatra-types.d.ts text eol=lfEjecútalo junto a verbatra check en el mismo job. check detecta locales que se quedan atrás respecto a la fuente; types --check detecta tus tipos quedándose atrás respecto a ella.
Ejemplos
# escribir verbatra-types.d.ts a partir del catálogo de origen
verbatra types
# escribirlo donde la aplicación ya importa
verbatra types --out src/generated/messages.d.ts
# gate de CI: salir con 1 si la declaración versionada está obsoleta
verbatra types --check
# resultado legible por máquina para un script
verbatra types --jsonUna ejecución se ve así:
verbatra types
214 keys declared, 63 of them taking arguments, from locales/en.json
arguments not determined (1):
legal.notice invalid-message-syntax
excluded by the adapter (1): meta.version
wrote /app/verbatra-types.d.tsVuelve a ejecutarlo sin cambiar el catálogo y no reescribe nada:
verbatra types
214 keys declared, 63 of them taking arguments, from locales/en.json
unchanged /app/verbatra-types.d.tsY en modo de comprobación, una vez que el catálogo ha seguido adelante:
$ verbatra types --check
verbatra types
215 keys declared, 63 of them taking arguments, from locales/en.json
/app/verbatra-types.d.ts is out of date, re-run verbatra typesCódigos de salida
| Código | Significado |
|---|---|
0 | la declaración se generó, haya cambiado el archivo o no; o --check la encontró vigente |
1 | --check encontró obsoleta la declaración versionada |
2 | no pudo ejecutarse: un error de uso, un problema de configuración o de la fuente, o una ruta de salida rechazada |
Relacionado
verbatra extractes la otra mitad del ciclo: mete en el catálogo las claves que usa tu código, ytypeslas declara después.verbatra checkes el gate de CI para la deriva de locales, justo al lado de este para la deriva de tipos.- Formatos lista la sintaxis de marcadores de posición de cada formato, que es de donde salen los nombres de argumento.
- CI y códigos de salida cubre el envoltorio JSON y el contrato de códigos de salida.