Every text a user sees comes from the locale files through t!. The compiler checks that every locale file has the key and that the arguments match the placeholders, so a missing translation is a build error, not a blank label.
Locale files
locales/<lang>.json holds nested keys. All files must have the same keys.
{
"stock": {
"products": "Products",
"saved": "Product {code} saved."
}
}t!
Screen::new(t!("stock.products"))
ui.toast(t!("stock.saved", code = p.code));t! returns a Text: a key with its arguments, resolved in the user's language when the page renders. Typos fail at compile time:
error: t!: key `stock.prodcts` is missing in locales/tr.json
= help: add it to every file in locales/Labels of fields and choices
Field labels use the key <Model>.<field>, such as Product.code. Choice labels use <Choice>.<Variant>. When a key is missing the label falls back to #[label("...")] or a readable form of the name. zeyn check reports keys that some languages have and others lack.
{
"Product": { "code": "Code", "name": "Product name" },
"Unit": { "Piece": "Piece", "Kg": "Kilogram" }
}Registering the languages
let i18n = I18n::new("en")
.catalog("en", include_str!("../locales/en.json"))
.and_then(|i| i.catalog("tr", include_str!("../locales/tr.json")))?;
App::new(t!("app.name")).i18n(i18n)The first argument of I18n::new is the default language. With more than one catalog the shell shows a language picker. The framework's own texts (grid, forms, shell) ship in English and Turkish.
Texts outside screens
ctx.t(&text)turns aTextinto aStringin the user's language, such as for an email.Text::english()gives the English text for logs and tests.Text::from("literal")is a text that is never translated, such as a product code.
See Time and languages for how the language is chosen and how numbers, money and dates are formatted.