Zum Inhalt springen
Zum Inhalt springen

Referenz / CLI

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.

verbatra types

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

Mach aus einem vertippten Key oder einem vergessenen Interpolationsargument einen Compile-Fehler. types liest deinen Quellkatalog über den konfigurierten Format-Adapter und schreibt eine Deklarationsdatei: eine Union aller Keys und, pro Key, die Argumente, die dessen Nachricht nimmt. Der Befehl ruft keinen Provider auf, liest keinen API-Key und fasst nie eine Ziel-Locale-Datei an.

Synopsis

verbatra types [flags]

Beispiele

# write verbatra-types.d.ts from the source catalog
verbatra types

# write it where the app already imports from
verbatra types --out src/generated/messages.d.ts

# CI gate: exit 1 if the committed declaration is stale
verbatra types --check

# machine-readable result for a script
verbatra types --json

Flags

FlagArgumentStandardWirkung
--out<path>verbatra-types.d.tswohin die Deklaration geschrieben wird, relativ zum Arbeitsverzeichnis und innerhalb der Projektwurzel; fehlende Verzeichnisse werden angelegt
--checkkeinsausdie committete Deklaration Byte für Byte mit einer frischen Generierung vergleichen, ohne etwas zu schreiben; Exit 1, wenn sie veraltet ist oder fehlt

Außerdem nimmt der Befehl die globalen Flags --cwd, --config und --json.

Verhalten

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": { readonly "count": 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 erzeugt hat, in Dokumentreihenfolge, deshalb erzeugt derselbe Katalog immer dieselben Bytes. Die Argumente stammen aus den Platzhalter-Tokens, die der Adapter extrahiert hat; Nachrichten in next-intl-json und arb werden mit dem ICU-Parser des Adapters gelesen, sodass auch ein Argument deklariert wird, das nur einige Zweige verwenden. Geraten wird nichts, und nichts wird als any deklariert.

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)eine optionale Eigenschaft, name?:, oder eine optionale Tupel-Position am Ende ([number, VerbatraArgument?]); eines, das jeder Zweig verwendet, bleibt Pflicht
ein ICU-Argument vom Typ number, plural oder selectordinal 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 benannte, nummerierte und anonyme Argumente mischtVerbatraUnknownArguments, und der Key wird in der Ausgabe genannt
ein nummeriertes Argument unter Position 0 oder ab Position 64 (%0$s, {64})VerbatraUnknownArguments, und der Key wird in der Ausgabe genannt

Ein Format, dessen Platzhalter keinen Namen tragen (Apple .strings, Android strings.xml, positionale gettext-Konvertierungen), bekommt ein Tupel, nie erfundene Namen. Ein Name, der mit verschiedenen Typen vorkommt, bekommt deren Vereinigung: {d, date, short} neben einem schlichten {d} wird zu Date | number | string. Keys sind immer String-Literale in Anführungszeichen, ein Key mit Punkt, reserviertem Wort, führender Ziffer oder Anführungszeichen wird also wortwörtlich deklariert. Ein Key, den der Adapter von der Übersetzung ausgeschlossen hat (ein Blatt, das kein String ist, eine Android-Ressource mit translatable="false"), wird nie deklariert und in der Ausgabe genannt.

Ein Key, dessen eigener Name einen Punkt enthält

Der deklarierte Key wird so geschrieben, wie verbatra ihn in verbatra.lock.json schreibt, nicht in einer Lookup-Syntax, die deine i18n-Bibliothek versteht. Ein JSON-Katalog mit {"a.b": "..."} trägt den Key a\.b, bewusst verschieden vom verschachtelten Pfad a.b:

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

i18next kennt kein Escape für sein Key-Trennzeichen, t("a\\.b") schlägt diesen Key also nicht nach. Trägt dein Katalog solche Keys, setz keySeparator auf ein anderes Zeichen (oder bei einem flachen Katalog auf false) und bilde den deklarierten Key in deinem eigenen typisierten Wrapper auf den Laufzeit-Key ab.

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

Nur next-intl-json und arb prüfen die Nachrichtensyntax. In jedem anderen Format liefert ein kaputter Wert wie "Hello {name" kein Platzhalter-Token. Die Nachricht wird deshalb als argumentlos deklariert, und das Argument zu übergeben, das sie wirklich braucht, ist ein Compile-Fehler. Gemeldet wird das nicht, weil es nicht erkannt wurde. Reparier 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 so deklariert, wie sie geschrieben sind, und daneben der Basis-Key cart.item, den t("cart.item", { count }) nachschlägt; er verlangt count und jedes Argument, das eine der Formen nimmt. Auch die eigenen Typen von i18next akzeptieren beide Schreibweisen. Eine Ordinalgruppe wie place_ordinal_one deklariert place, und ein Basis-Key, den der Katalog schon enthält, behält seine eigene Deklaration. VerbatraPluralMessageKey listet die Keys, die der Adapter als Träger von Pluralformen markiert hat, also die mit Suffix, damit du darauf einschränken kannst.

Wohin die Deklaration geht

Standardmäßig landet sie als verbatra-types.d.ts im Wurzelverzeichnis deines Projekts. Committe sie: Eine Kollegin oder ein CI-Job typprüft dagegen, ohne verbatra laufen zu lassen, und --check braucht eine committete Datei zum Vergleichen. Nichts trägt sie in .gitignore ein.

Ein Ausgabepfad wird mit TYPES_OUTPUT_CONFLICT zurückgewiesen, wenn er:

  • keine Datei benennt, auch wenn er auf ein Pfadtrennzeichen endet wie generated/ (ein leeres --out scheitert früher, mit INVALID_OUT),
  • absolut ist,
  • mit .. aus der Projektwurzel 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,
  • die Herkunftsdatei verbatra.provenance.json ist,
  • der Übersetzungsspeicher-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 Konfigurationsdatei ist, die der Lauf tatsächlich geladen hat, auch eine, die du mit --config angegeben hast,
  • die Glossardatei ist, auf die glossary in deiner Konfiguration zeigt,
  • 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, und die Prüfungen laufen nach dem Auflösen symbolischer Links noch einmal, sodass ein verlinktes Verzeichnis die Deklaration weder aus der Projektwurzel hinaus noch auf eine geschützte Datei tragen kann. Die letzte Prüfung läuft nur, wenn ein Lauf schreiben würde, und lässt deine Datei unangetastet; --check weist eine bestehende Datei nie zurück. Ein Pfad, der alle Prüfungen besteht, kann beim Schreiben trotzdem scheitern (ein schreibgeschütztes Verzeichnis, eine volle Platte): Der Lauf bricht dann mit TYPES_UNWRITABLE ab und nennt die Datei und den Dateisystem-Code, etwa EACCES.

Ehrlich bleiben in der CI

Lass types --check im selben Job neben verbatra check laufen: check fängt Locales ab, die hinter der Quelle zurückbleiben, types --check deine Typen. Der Vergleich geht Byte für Byte, und die Deklaration wird immer mit Line-Feed-Zeilenenden geschrieben. Leg die Zeilenenden deshalb in .gitattributes fest, sonst lässt ein Checkout, der sie umschreibt, die Datei dauerhaft veraltet zurück:

verbatra-types.d.ts text eol=lf

JSON-Ausgabe

Schema: types-envelope.json validiert den ganzen Envelope; Schemas listet die übrigen auf.

Mit --json trägt der Envelope ein Ergebnis:

interface GenerateTypesResult {
  path: string;               // absolute path of the declaration file
  sourcePath: string;         // the source catalog, relative to the project root
  keys: number;               // how many keys are declared
  withArguments: number;      // keys taking at least one determined argument
  unresolved: { key: string; reason: "invalid-message-syntax" | "mixed-argument-styles" | "argument-index-out-of-range" }[];
  excluded: string[];         // keys the adapter excluded, never declared
  plural: string[];           // keys carrying plural forms
  written: boolean;           // false under --check and when the file already matched
  stale: boolean;             // the file on disk differed when the run started
  missing: boolean;           // no file existed when the run started
  check: boolean;             // whether this was a --check run
}

Rückmeldung im Terminal

Die Zusammenfassung geht auf stdout; eine Spinner-Zeile und nach dem Schreiben ein Hinweis, die Datei zu committen, gehen auf stderr (--check schlägt nichts vor, und --quiet lässt beides weg). Ein erzeugender Lauf, ein unveränderter zweiter Lauf und ein veraltetes --check:

Ausgabe
verbatra: generating the declarations... done (0.2s)
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 verbatra-types.d.ts
next: commit verbatra-types.d.ts (verbatra types --check compares against it in CI)

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

verbatra types
  215 keys declared, 63 of them taking arguments, from locales/en.json
  verbatra-types.d.ts is out of date, re-run verbatra types

Farbe, Spinner und die Variablen dazu findest du unter Terminalausgabe.

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 oder fehlend befunden (is missing, run verbatra types to create it)
2konnte nicht laufen: ein Verwendungsfehler, ein Problem mit Konfiguration oder Quelle, ein zurückgewiesener Ausgabepfad (TYPES_OUTPUT_CONFLICT) oder eine Deklaration, die nicht geschrieben werden konnte (TYPES_UNWRITABLE)

Den Vertrag, dem jeder Befehl folgt, findest du unter Exit-Codes.

Siehe auch

  • verbatra extract trägt die Keys, die dein Code benutzt, in den Katalog ein; types deklariert sie dann.
  • verbatra check ist das CI-Gate für Locale-Drift, direkt neben diesem für Typ-Drift.
  • Formate listet die Platzhaltersyntax jedes Formats.
  • Exit-Codes und JSON-Ausgabe behandelt den Envelope und den Exit-Code-Vertrag.
Auf GitHub bearbeiten

Auf dieser Seite