Generated outputs#

One run can write runtime JSON and compile-time bindings. All examples on this page are generated from the checked-in quick-start catalog before the documentation site is built.

Runtime JSON#

Globetrotter writes one versioned JSON document per language. Its translations object maps fully qualified keys to literal text or templates:

translations_en.jsongenerated
{
  "version": "1",
  "template_engine": "handlebars",
  "language": "en",
  "translations": {
    "app.account.greeting": {
      "template": "Welcome back, {{name}}!"
    },
    "app.cart.summary": {
      "template": "You have {{count}} items in your cart."
    },
    "app.navigation.sign_out": {
      "literal": "Sign out"
    }
  }
}

The key set is shared across language files, so an application can choose a file at runtime without changing the lookup contract. Values without arguments remain literals; values with Handlebars placeholders remain templates for the application to render.

The generated files contain data, not a runtime dependency on Globetrotter. Load them with the JSON and templating libraries already used by your application.

TypeScript#

The TypeScript generator writes the structure and template argument contracts derived from the same catalog:

translations.tsgenerated
//
// AUTOGENERATED. DO NOT EDIT.
// generated by globetrotter v0.0.15.
//
export type Translations = {
    readonly "app.account.greeting": (values: {
        readonly "name": string;
    }) => string;
    readonly "app.cart.summary": (values: {
        readonly "count": number;
    }) => string;
    readonly "app.navigation.sign_out": string;
};

Import the generated definitions from application code, but keep the JSON as the source of runtime text. This lets the compiler check keys and argument shapes without embedding every translation in the JavaScript bundle.

Rust#

The Rust generator writes typed translation keys:

translations.rsgenerated
//
// AUTOGENERATED. DO NOT EDIT.
// generated by globetrotter v0.0.15.
//

#[derive(
    Debug,
    Clone,
    PartialEq,
    Eq,
    PartialOrd,
    Ord,
    ::serde::Serialize,
    ::serde::Deserialize,
)]
#[serde(untagged)]
pub enum Translation<'a> {
    AppAccountGreeting { #[serde(rename = "name")] name: &'a str },
    AppCartSummary { #[serde(rename = "count")] count: i64 },
    AppNavigationSignOut {},
}
impl<'a> Translation<'a> {
    pub fn key(&self) -> &'static str {
        match self {
            Self::AppAccountGreeting { .. } => "app.account.greeting",
            Self::AppCartSummary { .. } => "app.cart.summary",
            Self::AppNavigationSignOut { .. } => "app.navigation.sign_out",
        }
    }
}

The generated file can be included from a build script or committed at a stable source path. The repository’s examples/example-rust shows a complete build.rs workflow.

Typst#

Typst output writes one .typ module with the same translations as the JSON files plus the argument types of every key. It carries data only: the Typst runtime package imports it, validates calls, and renders the templates. Configure one path or a list:

version: 1
config:
  languages: [en, de]
  engine: handlebars
  inputs: [translations.toml]
  outputs:
    typst: generated/translations.typ

For a catalog with a greeting template and a plain literal, the module contains:

#let version = "1"

#let engine = "handlebars"

#let languages = ("en", "de")

#let arguments = (
  "greeting": ("name": "string"),
  "plain": (:),
)

#let translations = (
  "en": (
    "greeting": "Hello {{name}}!",
    "plain": "Hello world",
  ),
  "de": (
    "greeting": "Hallo {{name}}!",
    "plain": "Hallo Welt",
  ),
)
NameValue
versionFormat version, shared with the JSON output.
engineThe configured template engine name, or none.
languagesLanguage codes in configured order.
argumentsKey to a dictionary of argument names and their catalog types: any, string, number, integer, float, isodatetime, or boolean.
translationsLanguage to a dictionary of key and source string, with the same keys as arguments.

Keys and argument names are dictionary strings, so dots and dashes need no escaping. A key without arguments is a literal that a runtime returns verbatim; every other key is a template for engine. A runtime uses arguments to reject unknown keys and missing, unexpected, or mistyped arguments before rendering, so a faulty call fails the document’s compilation. Missing translations follow the JSON output: they fail generation in strict mode and become placeholder text otherwise.

From a Rust build script#

The library’s typst feature is enabled by default. A build dependency that disables default features lists it explicitly:

[build-dependencies]
globetrotter = { version = "0", default-features = false, features = ["rust", "typst"] }

Add this output to the same Outputs builder used for Rust or JSON, then run the executor as in examples/example-rust/build.rs:

let outputs = Outputs::new()
    .with_typst(globetrotter::typst::OutputConfig::new([
        out_dir.join("translations.typ"),
    ]));
let config = Config::new("pdf")
    .with_languages([Language::En, Language::De])
    .with_template_engine(TemplateEngine::Handlebars)
    .with_input(Input::new("translations.toml"))
    .with_outputs(outputs);

The module can be embedded with include_str!(concat!(env!("OUT_DIR"), "/translations.typ")) and handed to an embedded Typst compiler at the path its document imports.

Rendering with the Typst runtime#

The repository ships a small pure-Typst package in runtime/typst/globetrotter, versioned with the module format it reads. It is not published to Typst Universe: vendor its lib.typ, or copy the directory to {data-dir}/typst/packages/local/globetrotter/0.1.0 and import @local/globetrotter:0.1.0.

#import "@local/globetrotter:0.1.0": translator
#import "generated/translations.typ" as catalog

#let t = translator(catalog, "en", helpers: (
  pluralize: (word, count) => if count == 1 { word } else { word + "s" },
))

#t("greeting", name: "Ada")   // "Hello Ada!"
#t("cart.items", count: 3)    // "3 items" for "{{count}} {{pluralize \"item\" count}}"

translator(module, lang, helpers: (:)) returns t(key, ..arguments), which renders a str. Arguments are named. A key without arguments returns its literal verbatim; a module generated with engine: none renders only literals. Helpers are Typst functions that receive the evaluated parameters positionally and return a str, number, boolean, or none; close over anything else they need, such as the language.

Every faulty call panics and fails the document’s compilation: an unknown language or key, a positional value, a missing or unexpected argument, an argument of the wrong type, an unknown helper, and any unsupported template construct. Messages name the key and the construct.

Declared typeAccepted Typst types
string, isodatetimestr
number, integerint, like the other generated bindings
floatint or float
booleanbool
anyevery type

The runtime implements a Handlebars subset:

ConstructBehavior
{{path}}, {{{path}}}The value as plain text; neither form escapes HTML.
{{[arg-name]}}, {{user.name}}Segment literals and dictionary fields of an argument.
{{#if path}}…{{else}}…{{/if}}, {{#unless path}}…{{/unless}}Handlebars truthiness: none, false, zero, "", and () are false.
{{! comment }}, {{!-- comment --}}Removed.
{{name param …}}A registered helper, called with paths and string or number literals.

Numbers render with an ASCII minus sign, booleans as true or false, and none as empty text. {{#each}}, {{else if}}, partials, whitespace control, this, ../, @ data variables, subexpressions, hash arguments, and \{{ escapes panic. Keep catalogs that Typst documents render within this subset; check_templates validates full Handlebars syntax, not the subset.

The examples/example-typst directory generates a module, lints its document’s usages, and renders it with a registered helper.

Run task test:typst to compile the generated fixture module, the runtime’s construct and panic tests, and the example. It needs the typst CLI on PATH, which mise install provides; normal Rust tests do not invoke it.

Usage scanning#

globetrotter lint scans Typst documents like other sources. Calls such as #t("greeting", name: "Ada") keep their keys alive, and t("status." + kind) follows the configured dynamic policy. Every call with a literal key is also checked for an undefined key and for named arguments that contradict the declaration, including calls in branches the document never renders; calls: deny makes those findings errors. The default functions recognize t, $t, and translate; list the translator’s name when you bind it differently:

usages:
  roots: [./document]
  functions: [tr]
  calls: deny

See Linting for the recognized call shapes and call checks for the argument rules.

Commit or generate?#

Both approaches are valid:

  • Commit generated files when consumers should build without the generator installed. CI should regenerate them and fail on a diff.
  • Generate during the build when every consumer already has a controlled build environment. Keep the build script deterministic and rerun it when catalogs change.

Do not mix ownership. If a file is generated, its header and repository workflow should make that clear; application code should never patch it by hand.

Preview without writing#

Use a dry run to validate config, parse catalogs, and exercise generation without changing output files:

globetrotter --dry-run

For large catalogs, --max-keys N provides a deliberately partial debugging run. Globetrotter emits a warning when truncation is active so partial output cannot be mistaken for a full build.