verbatra types
Génère des déclarations TypeScript pour les clés de ton catalogue et les arguments de leurs messages, sans appeler de fournisseur ni lire de clé d'API.
Page traduite automatiquement
Disponible à partir de 0.11.0
Une clé mal orthographiée, c'est un échec de recherche à l'exécution. Un argument d'interpolation oublié, c'est un marqueur affiché tel quel à l'écran. types transforme les deux en erreurs de compilation : la commande lit ton catalogue source via l'adaptateur de format configuré et écrit un fichier de déclarations contenant une union de toutes les clés et, par clé, les arguments que son message prend. Cela ne coûte rien : aucun fournisseur n'est construit, aucune requête réseau n'est faite et aucune clé d'API n'est lue, donc cela marche sur un dépôt fraîchement cloné, avant même qu'une clé existe.
Elle lit un fichier (le catalogue de ta locale source) et écrit un fichier (la déclaration). Elle ne touche jamais à un fichier de locale cible.
Synopsis
verbatra types [flags]Options
| Option | Argument | Valeur par défaut | Effet |
|---|---|---|---|
--cwd | <path> | répertoire courant | résoudre la configuration et les fichiers de locale depuis ce répertoire |
--config | <path> | en chercher une | charger ce fichier de configuration au lieu d'en chercher un |
--out | <path> | verbatra-types.d.ts | où écrire la déclaration, relativement au répertoire de travail et à l'intérieur de celui-ci |
--check | aucun | désactivé | signaler si la déclaration versionnée est toujours à jour, sans rien écrire |
--json | aucun | désactivé | afficher une enveloppe JSON sur stdout portant le résultat sous result |
Ce qui est généré
À partir d'un locales/en.json comme celui-ci :
{
"app": { "title": "Verbatra" },
"greeting": "Hello {{name}}",
"cart": { "item_one": "{{count}} item", "item_other": "{{count}} items" }
}tu obtiens ceci :
// 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";Type ta propre fonction de traduction là-dessus, le compilateur fait le reste :
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 argumentCe qui est affirmé et ce qui ne l'est pas
Les clés sont exactement ce que l'adaptateur de format avait déjà produit en lisant le catalogue, dans l'ordre du document, et c'est pourquoi le même catalogue produit toujours les mêmes octets. Pour la plupart des formats, les arguments viennent des jetons de marqueurs que l'adaptateur a extraits. Pour les deux formats de messages ICU, next-intl-json et arb, chaque message est lu avec le même parseur ICU que celui de l'adaptateur : un argument que seules certaines branches select ou plural utilisent est donc quand même déclaré, et chaque argument reçoit son type selon la façon dont le message le formate. Rien n'est deviné.
| Ton message | Ce qui est déclaré |
|---|---|
| aucun marqueur | VerbatraNoArguments, donc passer quoi que ce soit est une erreur de type |
des marqueurs nommés ({{name}}, {name}, %(name)s) | une forme d'objet portant ces noms |
des marqueurs numérotés ou anonymes ({0}, %@, %1$d) | un tuple readonly, une position par argument |
un argument que seules certaines branches select ou plural utilisent (next-intl-json, arb) | un membre optionnel, name?:, ou pour un argument numéroté une position de tuple optionnelle ([number, VerbatraArgument?]) ; un argument utilisé par toutes les branches reste obligatoire |
un argument ICU number, plural ou selectordinal ({count, number}, {count, plural, ...}) dans next-intl-json ou arb | number |
un argument ICU date ou time ({d, date, short}) dans next-intl-json ou arb | Date | number |
un argument ICU select ({gender, select, ...}) dans next-intl-json ou arb | string |
un formateur i18next number ({{count, number}}) dans i18next-json, ngx-translate-json ou yaml | number |
un formateur i18next datetime ({{d, datetime}}) dans ces trois mêmes formats | Date | number |
une interpolation i18next non échappée ({{- name}}) | le nom derrière le préfixe, name |
un argument MessageFormat number, plural ou choice ({0,number}) dans properties | number |
une conversion printf numérique (%d, %1$d) | number |
une conversion printf de chaîne (%s, %(name)s) | string |
tout autre marqueur, y compris un argument ICU simple ({name}) | VerbatraArgument, un alias de string | number |
une syntaxe de message invalide, que seuls next-intl-json et arb vérifient | VerbatraUnknownArguments, et la clé est nommée dans la sortie |
| un message qui à la fois nomme et numérote ses arguments | VerbatraUnknownArguments, et la clé est nommée dans la sortie |
Un tuple ne peut omettre que des positions à sa fin : un argument numéroté que seules certaines branches utilisent n'est donc déclaré optionnel que si toutes les positions qui le suivent le sont aussi. Dans {0, select, a {{1}} other {x}} {2}, la position 1 reste obligatoire, parce que la position obligatoire 2 la suit.
Les noms d'arguments viennent des jetons de marqueurs que l'adaptateur a extraits : un format dont les marqueurs ne portent aucun nom (Apple .strings, Android strings.xml, conversions gettext positionnelles) reçoit donc un tuple plutôt que des noms inventés. Les types d'arguments viennent uniquement de ce que ton catalogue enregistre vraiment : la façon dont un message ICU formate un argument, ou le formateur ou la conversion que nomme un marqueur. Quand un format nomme un argument sans dire ce qu'on peut lui passer, verbatra déclare string | number au lieu d'affirmer un type que le catalogue n'a jamais porté. Un nom utilisé plusieurs fois avec des types différents est déclaré comme l'union de tout ce qu'accepte chaque utilisation : {d, date, short} à côté d'un simple {d} devient Date | number | string, et {n, plural, ...} à côté d'un simple {n} devient string | number. Rien n'est jamais déclaré en any.
Les clés sont toujours émises comme des littéraux de chaîne entre guillemets : une clé portant un point, un mot réservé, un chiffre en tête, un guillemet, ou même la clé vide, est donc déclarée telle quelle plutôt qu'abandonnée ou redécoupée. Une clé que l'adaptateur a signalée comme exclue de la traduction (une feuille isolée qui n'est pas une chaîne, une ressource Android avec translatable="false") n'est jamais déclarée, et elle est nommée dans la sortie pour que tu voies ce qui a été laissé de côté.
Une clé dont le nom contient lui-même un point
La clé déclarée est écrite comme verbatra écrit lui-même la clé, sous la même forme que dans verbatra.lock.json, et ce n'est pas une syntaxe de recherche que ta bibliothèque i18n comprend. Un catalogue JSON contenant {"a.b": "..."} porte une clé écrite a\.b, délibérément distincte du chemin imbriqué a.b, et c'est cette graphie échappée que le fichier déclare :
export interface VerbatraMessages {
"a\\.b": VerbatraNoArguments;
}Ne compte pas sur un runtime pour résoudre cette graphie. i18next, par exemple, n'a pas d'échappement pour son séparateur de clés : t("a\\.b") n'est donc pas la façon dont il cherche une clé contenant un point littéral. Si ton catalogue porte de telles clés, configure le runtime pour que le point ne soit pas un séparateur (pour i18next, règle keySeparator sur un autre caractère, ou sur false pour un catalogue plat) et fais correspondre la clé déclarée à celle que cherche ton runtime dans ton propre wrapper typé.
Le seul cas où la déclaration se trompe au lieu de simplement se taire
Seuls next-intl-json et arb valident la syntaxe des messages. Pour tout autre format, une valeur cassée et pas seulement ordinaire ("Hello {name", sans l'accolade fermante) ne produit aucun jeton de marqueur : le message est donc déclaré comme ne prenant aucun argument, et passer l'argument qu'il veut vraiment devient une erreur de compilation. Rien ne le signale, parce que rien ne l'a détecté. Ce qu'il faut corriger, c'est le catalogue, pas la déclaration.
Clés au pluriel
Rien n'est fusionné. i18next porte le pluriel en suffixe de clé, donc cart.item_one et cart.item_other sont déclarées comme les deux clés distinctes qu'elles sont réellement, puisque ce sont les deux clés que ton code cherche vraiment. VerbatraPluralMessageKey liste précisément les clés que l'adaptateur a marquées comme portant des formes plurielles, si tu veux t'y restreindre.
Où atterrit la déclaration
Par défaut elle atterrit dans verbatra-types.d.ts à la racine de ton projet. Versionne-la. C'est un artefact versionné, pas un brouillon local, pour deux raisons : un collègue ou un job de CI vérifie les types dessus sans avoir à lancer verbatra d'abord, et --check a besoin d'un fichier versionné auquel se comparer. Rien ne l'ajoute au .gitignore.
Passe --out pour la mettre là où ton application importe déjà, par exemple --out src/generated/messages.d.ts. Les répertoires manquants sont créés. Un chemin de sortie est refusé avec TYPES_OUTPUT_CONFLICT quand il :
- ne nomme aucun fichier,
- est absolu,
- sort du répertoire de travail avec
.., - n'est pas un fichier TypeScript, c'est-à-dire que son nom ne se termine pas par
.ts,.mtsou.cts, - nomme un fichier de locale configuré, source ou cible, pour que la génération n'écrase jamais un catalogue,
- est le fichier de verrouillage
verbatra.lock.json, - est le cache local de mémoire de traduction
verbatra.cache.json, - est un fichier où verbatra cherche sa configuration :
package.json,.verbatrarc,.verbatrarc.json,.verbatrarc.yaml,.verbatrarc.yml,.verbatrarc.js,.verbatrarc.cjs,.verbatrarc.ts,verbatra.config.js,verbatra.config.cjsouverbatra.config.ts, - est le fichier de configuration que l'exécution a réellement chargé, y compris celui que tu as indiqué avec
--config, - est un fichier existant qui ne commence pas par la ligne d'en-tête
// Generated by verbatra fromque verbatra écrit en tête de chaque déclaration (une marque d'ordre des octets et des lignes vides en tête sont ignorées), ou est trop volumineux pour être vérifié.
Les noms de fichiers sont comparés sans tenir compte de la casse : Verbatra.Config.ts est donc refusé aussi. Tous les cas sauf le dernier sont refusés avant toute lecture ou écriture. Le dernier n'est vérifié que par une exécution qui écrirait, et il laisse ton fichier intact : choisis un autre --out, ou supprime le fichier s'il s'agit vraiment d'une ancienne déclaration. --check ne refuse jamais un fichier existant ; il se contente de comparer.
Rester honnête en CI
--check n'écrit rien et sort avec 1 quand la déclaration versionnée ne correspond plus à ce qu'une génération fraîche produirait. La comparaison se fait octet par octet, sans reformatage : elle attrape donc une clé ajoutée au catalogue et jamais régénérée.
- run: npx verbatra types --checkMets une chose en place avant de t'y fier. La déclaration est toujours écrite avec des fins de ligne line feed et --check compare des octets exacts : un checkout qui réécrit les fins de ligne la laisse donc périmée en permanence, sans qu'une nouvelle exécution puisse y remédier. Fixe les fins de ligne dans .gitattributes :
verbatra-types.d.ts text eol=lfLance-la à côté de verbatra check dans le même job. check attrape les locales qui prennent du retard sur la source ; types --check attrape tes types qui en prennent.
Exemples
# écrire verbatra-types.d.ts à partir du catalogue source
verbatra types
# l'écrire là où l'application importe déjà
verbatra types --out src/generated/messages.d.ts
# garde-fou CI : sortie 1 si la déclaration versionnée est périmée
verbatra types --check
# résultat lisible par une machine, pour un script
verbatra types --jsonUne exécution ressemble à ceci :
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.tsRelance-la sans toucher au catalogue et elle ne réécrit rien :
verbatra types
214 keys declared, 63 of them taking arguments, from locales/en.json
unchanged /app/verbatra-types.d.tsEt en mode vérification, une fois que le catalogue a avancé :
$ 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 typesCodes de sortie
| Code | Signification |
|---|---|
0 | la déclaration a été générée, que le fichier ait changé ou non ; ou --check l'a trouvée à jour |
1 | --check a trouvé la déclaration versionnée périmée |
2 | n'a pas pu s'exécuter : une erreur d'utilisation, un problème de configuration ou de source, ou un chemin de sortie refusé |
Voir aussi
verbatra extractest l'autre moitié de la boucle : elle ajoute au catalogue les clés que ton code utilise, ettypesles déclare ensuite.verbatra checkest le garde-fou CI pour la dérive des locales, juste à côté de celui-ci pour la dérive des types.- Formats liste la syntaxe de marqueurs de chaque format, d'où viennent les noms d'arguments.
- CI et codes de sortie couvre l'enveloppe JSON et le contrat des codes de sortie.