CLI-Referenzverbatra typesnew

verbatra types

Erzeuge TypeScript-Deklarationen für deine Katalog-Keys und deren Nachrichtenargumente, ohne einen Provider aufzurufen oder einen API-Key zu lesen.

Maschinell übersetzte Seite

Diese Seite wurde automatisch übersetzt und kann daher Fehler oder seltsame Formulierungen enthalten. Die englische Version ist die maßgebliche Quelle. Das englische Original lesen.

Verfügbar ab 0.11.0

Dafür wird verbatra 0.11.0 oder neuer benötigt. Frühere Versionen haben es nicht, überprüfe daher deine installierte Version mit verbatra --version und aktualisiere, falls sie älter ist.

Ein vertippter Key ist ein Lookup-Fehler zur Laufzeit. Ein vergessenes Interpolationsargument ist ein Platzhalter, der wörtlich auf dem Bildschirm landet. types macht aus beidem einen Compile-Fehler: Der Befehl liest deinen Quellkatalog über den konfigurierten Format-Adapter und schreibt eine Deklarationsdatei mit einer Union aller Keys und, pro Key, den Argumenten, die dessen Nachricht nimmt. Es kostet nichts: Es wird kein Provider konstruiert, keine Netzwerkanfrage gestellt und kein API-Key gelesen, es funktioniert also in einem frischen Checkout, bevor überhaupt ein Key existiert.

Er liest eine Datei (deinen Quell-Locale-Katalog) und schreibt eine Datei (die Deklaration). Eine Ziel-Locale-Datei fasst er nie an.

Aufruf

verbatra types [flags]

Flags

FlagArgumentStandardWirkung
--cwd<path>aktuelles VerzeichnisKonfiguration und Locale-Dateien aus diesem Verzeichnis auflösen
--config<path>eine suchendiese Konfigurationsdatei laden, statt eine zu suchen
--out<path>verbatra-types.d.tswohin die Deklaration geschrieben wird, relativ zum Arbeitsverzeichnis und innerhalb davon
--checkkeinsausmelden, ob die committete Deklaration noch aktuell ist, ohne etwas zu schreiben
--jsonkeinsauseinen JSON-Envelope auf stdout ausgeben, der das Ergebnis unter result trägt

Was erzeugt wird

Aus einer locales/en.json wie dieser:

{
  "app": { "title": "Verbatra" },
  "greeting": "Hello {{name}}",
  "cart": { "item_one": "{{count}} item", "item_other": "{{count}} items" }
}

wird das hier:

// 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";

Typisiere deine eigene Übersetzungsfunktion dagegen, den Rest erledigt der Compiler:

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 argument

Was behauptet wird und was nicht

Die Keys sind genau das, was der Format-Adapter beim Lesen des Katalogs ohnehin schon erzeugt hat, in Dokumentreihenfolge, und genau deshalb erzeugt derselbe Katalog immer dieselben Bytes. Bei den meisten Formaten stammen die Argumente aus den Platzhalter-Tokens, die der Adapter extrahiert hat. Bei den beiden ICU-Nachrichtenformaten, next-intl-json und arb, wird jede Nachricht mit demselben ICU-Parser gelesen, den der Adapter verwendet. Ein Argument, das nur einige select- oder plural-Zweige verwenden, wird deshalb trotzdem deklariert, und jedes Argument bekommt seinen Typ danach, wie die Nachricht es formatiert. Geraten wird nichts.

Deine NachrichtWas deklariert wird
keine PlatzhalterVerbatraNoArguments, jedes übergebene Argument ist also ein Typfehler
benannte Platzhalter ({{name}}, {name}, %(name)s)eine Objektform mit genau diesen Namen
nummerierte oder anonyme Platzhalter ({0}, %@, %1$d)ein readonly-Tupel, eine Position pro Argument
ein Argument, das nur einige select- oder plural-Zweige verwenden (next-intl-json, arb)ein optionales Mitglied, name?:, oder bei einem nummerierten Argument eine optionale Tupel-Position ([number, VerbatraArgument?]); eines, das jeder Zweig verwendet, bleibt Pflicht
ein ICU-Argument vom Typ number, plural oder selectordinal ({count, number}, {count, plural, ...}) in next-intl-json oder arbnumber
ein ICU-Argument vom Typ date oder time ({d, date, short}) in next-intl-json oder arbDate | number
ein ICU-Argument vom Typ select ({gender, select, ...}) in next-intl-json oder arbstring
ein i18next-Formatter number ({{count, number}}) in i18next-json, ngx-translate-json oder yamlnumber
ein i18next-Formatter datetime ({{d, datetime}}) in denselben drei FormatenDate | number
eine unescapte i18next-Interpolation ({{- name}})der Name hinter dem Präfix, name
ein MessageFormat-Argument vom Typ number, plural oder choice ({0,number}) in propertiesnumber
eine numerische printf-Konvertierung (%d, %1$d)number
eine printf-String-Konvertierung (%s, %(name)s)string
jeder andere Platzhalter, auch ein schlichtes ICU-Argument ({name})VerbatraArgument, ein Alias für string | number
ungültige Nachrichtensyntax, die nur next-intl-json und arb prüfenVerbatraUnknownArguments, und der Key wird in der Ausgabe genannt
eine Nachricht, die ihre Argumente zugleich benennt und nummeriertVerbatraUnknownArguments, und der Key wird in der Ausgabe genannt

Ein Tupel kann nur Positionen am Ende weglassen. Ein nummeriertes Argument, das nur einige Zweige verwenden, wird deshalb nur dann als optional deklariert, wenn jede Position danach ebenfalls optional ist. In {0, select, a {{1}} other {x}} {2} bleibt Position 1 Pflicht, weil ihr die Pflicht-Position 2 folgt.

Argumentnamen stammen aus den Platzhalter-Tokens, die der Adapter extrahiert hat. Ein Format, dessen Platzhalter gar keinen Namen tragen (Apple .strings, Android strings.xml, positionale gettext-Konvertierungen), bekommt deshalb ein Tupel statt erfundener Namen. Argumenttypen stammen ausschließlich daraus, was dein Katalog tatsächlich festhält: wie eine ICU-Nachricht ein Argument formatiert, oder welchen Formatter oder welche Konvertierung ein Platzhalter nennt. Wo ein Format ein Argument benennt, ohne zu sagen, was dafür übergeben werden darf, deklariert verbatra string | number, statt einen Typ zu behaupten, den der Katalog nie getragen hat. Ein Name, der mehrfach mit unterschiedlichen Typen vorkommt, wird als Vereinigung von allem deklariert, was die einzelnen Verwendungen akzeptieren: {d, date, short} neben einem schlichten {d} wird zu Date | number | string, und {n, plural, ...} neben einem schlichten {n} wird zu string | number. Nichts wird je als any deklariert.

Keys werden immer als Zeichenketten-Literale in Anführungszeichen ausgegeben. Ein Key mit einem Punkt, ein reserviertes Wort, eine führende Ziffer, ein Anführungszeichen oder ein leerer Key wird also wortwörtlich deklariert, statt verworfen oder neu aufgeteilt zu werden. Ein Key, den der Adapter als von der Übersetzung ausgeschlossen gemeldet hat (ein verirrtes Blatt, das kein String ist, eine Android-Ressource mit translatable="false"), wird nie deklariert und in der Ausgabe genannt, damit du siehst, was weggelassen wurde.

Ein Key, dessen eigener Name einen Punkt enthält

Der deklarierte Key wird so geschrieben, wie verbatra selbst den Key schreibt, in derselben Form wie in verbatra.lock.json. Das ist keine Lookup-Syntax, die deine i18n-Bibliothek versteht. Ein JSON-Katalog mit {"a.b": "..."} trägt einen Key, der a\.b geschrieben wird, bewusst verschieden vom verschachtelten Pfad a.b, und genau diese escapete Schreibweise deklariert die Datei:

export interface VerbatraMessages {
  "a\\.b": VerbatraNoArguments;
}

Erwarte nicht, dass eine Laufzeitbibliothek diese Schreibweise auflöst. i18next zum Beispiel kennt kein Escape für sein Key-Trennzeichen, t("a\\.b") ist also nicht der Weg, auf dem es einen Key mit wörtlichem Punkt nachschlägt. Trägt dein Katalog solche Keys, konfiguriere die Laufzeit so, dass der Punkt kein Trennzeichen ist (bei i18next setzt du keySeparator auf ein anderes Zeichen, oder bei einem flachen Katalog auf false), und bilde den deklarierten Key in deinem eigenen typisierten Wrapper auf den Key ab, den deine Laufzeit nachschlägt.

Der eine Fall, in dem die Deklaration falsch statt nur still ist

Nur next-intl-json und arb prüfen die Nachrichtensyntax. Bei jedem anderen Format liefert ein Wert, der kaputt und nicht bloß schlicht ist ("Hello {name", ohne schließende Klammer), überhaupt kein Platzhalter-Token. Die Nachricht wird deshalb als argumentlos deklariert, und das Argument zu übergeben, das sie eigentlich will, ist dann ein Compile-Fehler. Gemeldet wird das nicht, weil es nicht erkannt wurde. Reparieren musst du den Katalog, nicht die Deklaration.

Plural-Keys

Nichts wird zusammengefasst. i18next trägt den Plural als Key-Suffix, deshalb werden cart.item_one und cart.item_other als die zwei getrennten Keys deklariert, die sie wirklich sind, denn das sind die zwei Keys, die dein Code tatsächlich nachschlägt. VerbatraPluralMessageKey listet genau die Keys, die der Adapter als Träger von Pluralformen markiert hat, damit du bei Bedarf darauf einschränken kannst.

Wohin die Deklaration geht

Standardmäßig landet sie als verbatra-types.d.ts im Projektwurzelverzeichnis. Committe sie. Sie ist ein eingecheckter Artefakt, kein lokaler Zwischenstand, und zwar aus zwei Gründen: Eine Kollegin oder ein CI-Job typprüft dagegen, ohne vorher verbatra laufen lassen zu müssen, und --check braucht eine committete Datei zum Vergleichen. Nichts trägt sie in .gitignore ein.

Mit --out legst du sie dorthin, wo deine App ohnehin schon importiert, zum Beispiel --out src/generated/messages.d.ts. Fehlende Verzeichnisse werden angelegt. Ein Ausgabepfad wird mit TYPES_OUTPUT_CONFLICT zurückgewiesen, wenn er:

  • keine Datei benennt,
  • absolut ist,
  • mit .. aus dem Arbeitsverzeichnis herausklettert,
  • keine TypeScript-Datei ist, sein Name also nicht auf .ts, .mts oder .cts endet,
  • eine konfigurierte Locale-Datei benennt, Quelle oder Ziel, damit die Generierung nie einen Katalog überschreibt,
  • die Lock-Datei verbatra.lock.json ist,
  • der Translation-Memory-Cache verbatra.cache.json ist,
  • eine Datei ist, in der verbatra nach seiner Konfiguration sucht: package.json, .verbatrarc, .verbatrarc.json, .verbatrarc.yaml, .verbatrarc.yml, .verbatrarc.js, .verbatrarc.cjs, .verbatrarc.ts, verbatra.config.js, verbatra.config.cjs oder verbatra.config.ts,
  • die Config-Datei ist, die der Lauf tatsächlich geladen hat, auch eine, die du mit --config angegeben hast,
  • eine bestehende Datei ist, die nicht mit der Kopfzeile // Generated by verbatra from beginnt, die verbatra an den Anfang jeder Deklaration schreibt (eine vorangestellte Byte-Order-Mark und Leerzeilen werden ignoriert), oder zu groß ist, um das zu prüfen.

Dateinamen werden ohne Rücksicht auf Groß- und Kleinschreibung verglichen, Verbatra.Config.ts wird also ebenfalls zurückgewiesen. Alle Fälle außer dem letzten werden zurückgewiesen, bevor irgendetwas gelesen oder geschrieben wird. Den letzten prüft nur ein Lauf, der schreiben würde, und deine Datei bleibt dabei unangetastet: Wähle ein anderes --out, oder lösche die Datei, wenn sie wirklich eine alte Deklaration ist. --check weist eine bestehende Datei nie zurück, es vergleicht nur.

Ehrlich bleiben in der CI

--check schreibt nichts und endet mit 1, wenn die committete Deklaration nicht mehr dem entspricht, was eine frische Generierung erzeugen würde. Der Vergleich geht Byte für Byte, ohne Neuformatierung, und fängt damit einen Key ab, der zum Katalog hinzugefügt und nie neu generiert wurde:

- run: npx verbatra types --check

Richte eine Sache ein, bevor du dich darauf verlässt. Die Deklaration wird immer mit Line-Feed-Zeilenenden geschrieben, und --check vergleicht exakte Bytes. Ein Checkout, der Zeilenenden umschreibt, lässt sie also dauerhaft veraltet aussehen, ohne dass ein erneuter Lauf das beheben kann. Nagle die Zeilenenden in .gitattributes fest:

verbatra-types.d.ts text eol=lf

Lass ihn im selben Job neben verbatra check laufen. check fängt Locales ab, die hinter der Quelle zurückbleiben; types --check fängt deine Typen ab, die hinter ihr zurückbleiben.

Beispiele

# verbatra-types.d.ts aus dem Quellkatalog schreiben
verbatra types

# dorthin schreiben, wo die App ohnehin schon importiert
verbatra types --out src/generated/messages.d.ts

# CI-Gate: Exit 1, wenn die committete Deklaration veraltet ist
verbatra types --check

# maschinenlesbares Ergebnis für ein Skript
verbatra types --json

Ein Lauf sieht so aus:

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.ts

Lass ihn ohne Änderung am Katalog noch einmal laufen, und er schreibt nichts neu:

verbatra types
  214 keys declared, 63 of them taking arguments, from locales/en.json
  unchanged /app/verbatra-types.d.ts

Und im Check-Modus, sobald der Katalog weitergezogen ist:

$ 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 types

Exit-Codes

CodeBedeutung
0die Deklaration wurde erzeugt, egal ob sich die Datei geändert hat; oder --check hat sie als aktuell befunden
1--check hat die committete Deklaration als veraltet befunden
2konnte nicht laufen: ein Verwendungsfehler, ein Problem mit Konfiguration oder Quelle, oder ein zurückgewiesener Ausgabepfad

Verwandt

  • verbatra extract ist die andere Hälfte des Kreislaufs: Es trägt die Keys, die dein Code benutzt, in den Katalog ein, und types deklariert sie dann.
  • verbatra check ist das CI-Gate für Locale-Drift, direkt neben diesem hier für Typ-Drift.
  • Formate listet die Platzhaltersyntax jedes Formats, und genau daraus stammen die Argumentnamen.
  • CI und Exit-Codes behandelt den JSON-Envelope und den Exit-Code-Vertrag.
Edit on GitHub