DocsRendering and delivery
Localization
Translate server-rendered pages with build-validated Fluent catalogs, request-local locale selection, explicit localized URLs, fallback chains, and locale-aware plurals.
Configure supported locales
Localization is opt-in. Add an i18n object to rahti.config.json; a project without one scans no catalogs and adds no localization middleware. Locale names are canonical Unicode identifiers, and the default and every fallback must appear in the supported list.
{
"i18n": {
"locales": ["en", "es", "es-NI"],
"default": "en",
"fallbacks": {
"es": ["en"],
"es-NI": ["es", "en"]
},
"dir": "lang"
}
}| Field | Meaning |
|---|---|
| locales | Supported Unicode locale identifiers. Non-empty and unique after canonicalization. |
| default | The locale used when negotiation finds no supported match. |
| fallbacks | Ordered per-locale lookup chains. A chain cannot repeat or contain its source. |
| dir | Catalog root, relative to the project. Defaults to lang. |
Write Fluent catalogs
Each configured locale owns a directory of .ftl files. Files may be nested; the build watches, parses, and embeds all of them in the application binary. Deployments and native packages do not read translation files at runtime.
lang/
├── en/
│ ├── app.ftl
│ └── validation.ftl
└── es/
├── app.ftl
└── validation.ftlwelcome = Welcome, { $name }!
cart-items =
{ $count ->
[one] One item
*[other] { $count } items
}Choose the locale from the URL
Put localized pages below a root segment named exactly [locale] or [lang]. The concrete segment is authoritative: /es/account uses Spanish, while an unsupported /fr/account answers 404. An Accept-Language header never replaces a locale already present in the URL.
src/app/[locale]/page.rs
src/app/[locale]/account/page.rsUnprefixed routes negotiate Accept-Language and then fall back to the configured default. A common entry route redirects once to a canonical, explicit locale URL:
use axum::response::Redirect;
use rahti::i18n::localized_path;
pub async fn page() -> Redirect {
Redirect::temporary(&localized_path("/"))
}Translate during the request
Pages, components, layouts, metadata functions, error boundaries, API handlers, and RPCs all read the same request-task-local locale. Concurrent requests cannot change one another's language.
use rahti::i18n::{Argument, locale, t, t_with};
let current = locale();
let heading = t_with("welcome", &[("name", Argument::from("Ada"))]);
let items = t_with("cart-items", &[("count", Argument::from(3_u32))]);
let label = t("navigation-account");| Helper | Result |
|---|---|
| locale() | The current locale and its language direction. |
| t(key) | A message without named arguments. |
| t_with(key, arguments) | A formatted message with string or numeric arguments and locale plural rules. |
| localized_path(path) | Prefixes a path with the active locale without duplicating an existing supported prefix. |
Lookup walks the active locale, its configured fallback chain, and finally the default locale. A message missing everywhere renders its key. Translation results are ordinary strings, so html! escapes them like any other runtime value; catalogs are never trusted raw HTML.
Set document language and direction
pub fn layout(children: Html) -> Html {
let locale = rahti::i18n::locale();
html! {
<!DOCTYPE html>
<html lang=@{locale.as_str()} dir=@{locale.direction()}>
<head>/* the normal root head */</head>
<body><slot /></body>
</html>
}
}Common right-to-left language subtags return rtl; all others return ltr. The localization layer surrounds metadata and layouts, so a metadata() function may call t. Localized dynamic responses also carry Content-Language.
Authentication, RPCs, and page caching
- Authentication policies stay application-relative:
/accountprotects both/en/accountand/es/account, while generated sign-in and post-authentication redirects retain the locale. - Page RPCs post to the current localized URL and inherit its request locale. Browser-only PulsePoint expressions receive only the translated strings the server explicitly seeds into them.
- Locale-prefixed URLs naturally partition page-cache keys. A request whose locale came from
Accept-Languagebypasses page caching; redirect to an explicit locale URL before caching.
Export localized pages
When [locale] or [lang] is a page's only dynamic segment, static generation expands it once for every configured locale. A route such as src/app/[locale]/about/page.rs produces /en/about and /es/about automatically.
Pages with another dynamic segment still declare complete concrete paths, including the locale:
pub async fn static_paths() -> Vec<&'static str> {
vec!["/en/products/1", "/es/products/1"]
}