Translation files#

Translation sources are TOML. Each table path is a translation key; its fields are spoken-language codes and, optionally, an argument declaration.

translations.toml
[account.greeting]
en = "Welcome back, {{name}}!"
de = "Willkommen zurück, {{name}}!"
fr = "Bon retour, {{name}} !"
arguments = { name = "string" }

[cart.summary]
en = "You have {{count}} items in your cart."
de = "Du hast {{count}} Artikel in deinem Warenkorb."
fr = "Vous avez {{count}} articles dans votre panier."
arguments = { count = "number" }

[navigation.sign_out]
en = "Sign out"
de = "Abmelden"
fr = "Se déconnecter"

Keys#

Nested TOML tables become dotted keys. This table:

[account.greeting]
en = "Welcome back"
de = "Willkommen zurück"

defines account.greeting before any input prefix is applied. In the quick-start config, prefix: app makes the generated key app.account.greeting.

Keep keys about meaning rather than English wording. navigation.sign_out survives a copy edit; navigation.click_here does not describe why the text exists.

Languages#

Language fields contain ordinary TOML strings, including multiline strings when the translation needs line breaks:

[legal.notice]
en = """
Read the terms before continuing.
Changes take effect immediately.
"""
de = """
Lies die Bedingungen, bevor du fortfährst.
Änderungen gelten sofort.
"""

The config’s languages list defines the expected set. With strict: true, a missing or empty translation prevents generation. Without strict mode, the same condition is reported as a warning.

Template arguments#

Declare placeholders in an arguments table:

[account.greeting]
en = "Welcome back, {{name}}!"
de = "Willkommen zurück, {{name}}!"
arguments = { name = "string" }

Supported types, with the type each generator emits for them, are:

TypeMeaningRustTypeScript
anyThe caller may provide any value.serde_json::Valueany
stringText.&strstring
numberA numeric value, treated like integer.i64number
integerA whole number.i64number
floatA number that may have a fractional part.f64number
booleanA true or false value.boolboolean
isodatetimeAn ISO 8601 date-time string.&strstring

The generated Rust enum derives only the comparison traits all of its fields support: a float argument removes Eq and Ord, and an any argument removes PartialOrd and Ord.

An array shorthand declares untyped arguments:

arguments = ["name"]

Prefer the typed table when a useful type is known. With a configured engine, globetrotter checks that every declared argument is used by at least one language, that every placeholder is declared, and that a value substituted in one language is substituted in every language. An argument that only selects a wording, as in {{#if my_condition}}…{{else}}…{{/if}}, may be left out of a language that has no such distinction; a condition whose branches are identical is reported instead.

Formatting#

Format catalogs in place:

globetrotter format

By default keys are sorted ascending. Use --order descending to reverse the order, or verify formatting without modifying files:

globetrotter format --check

The formatter preserves comments, so explanations for translators can stay beside the relevant key.

Local lint exceptions#

Every lint finding has a stable code. Suppress one code for a key, only when the divergence is intentional, by listing it with the lint: prefix:

[product.proper_name]
en = "Globetrotter"
de = "Globetrotter"
allow = ["lint:identical-languages"]

The prefix is required. A bare identical-languages is rejected rather than silently ignored, and the namespace keeps the list open to non-lint directives later without a name ever meaning two things.

An allow applies to the table it is written on and to every key nested under it, so a group or a whole file can share one exception:

# Applies to every key in this file.
allow = ["lint:duplicate"]

[checkout]
# Applies to every key under `checkout`.
allow = ["lint:missing-language"]

[checkout.submit]
en = "Continue"

allow = "lint:all" silences every lint for the keys it covers and should be rare; a specific code records the reason more clearly and allows other checks to keep working.

To suppress a code for a whole build rather than one file, use the config file’s allow list — see configuration.

Next: generated outputs and linting.