Internationalization
The Tee system supports multiple languages. This is implemented in the interface layer and uses Fluent files stored in the repository.
Key files
docs/SPECS/SPEC-04-I18N.mdsrc/interface/i18n.rslocales/<locale>/main.ftl
How locale is chosen
The locale is resolved in this order:
- User preference (if available).
localecookie.Accept-Languageheader.- Default locale (usually
en).
This logic is implemented in src/interface/i18n.rs.
Translator and templates
The Translator loads Fluent bundles at startup and provides a text method.
Handlers call the translator and pass localized strings into templates.
Templates do not perform translation lookups.
This keeps templates simple and avoids I/O during rendering.
If a key is missing, the translator falls back to the default locale and can return the key name as a last resort. This avoids crashing a page because of a missing translation.
Code example: loading bundles
From src/interface/i18n.rs:
#![allow(unused)]
fn main() {
pub fn load_from_disk<P: AsRef<Path>>(
root: P,
default_locale: &str,
required_locales: &[&str],
) -> Result<Self, anyhow::Error> {
let root = root.as_ref();
let mut bundles = HashMap::new();
for entry in fs::read_dir(root).context("reading locales directory")? {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let locale_code = entry.file_name().to_string_lossy().to_string();
let Some(langid) = parse_locale(&locale_code) else {
warn!(code = %locale_code, "skipping invalid locale directory name");
continue;
};
let path = entry.path().join("main.ftl");
if !path.exists() {
continue;
}
let bundle = load_bundle(&langid, &path)?;
bundles.insert(langid, bundle);
}
}
Code example: passing localized strings to templates
From src/interface/routes_web.rs:
#![allow(unused)]
fn main() {
fn build_layout_texts(translator: &Translator, locale: &Locale) -> LayoutTexts {
LayoutTexts {
brand: translator.text(&locale.0, "layout-brand", None),
nav_tasks: translator.text(&locale.0, "layout-nav-tasks", None),
theme_label: translator.text(&locale.0, "layout-theme-label", None),
theme_light: translator.text(&locale.0, "layout-theme-light", None),
theme_dark: translator.text(&locale.0, "layout-theme-dark", None),
theme_system: translator.text(&locale.0, "layout-theme-system", None),
locale_label: translator.text(&locale.0, "layout-locale-label", None),
}
}
}
Date and time formatting
The interface layer formats dates with helper functions like format_date
and format_datetime. These functions apply locale-aware formatting before
values reach the template.
Adding a new locale
Steps:
- Create a new folder under
locales/<lang>. - Add a
main.ftlfile with the required keys. - Restart the service to reload bundles.
Exercise
- Add a new translation key to all locale files.
- Remove the key from one locale and observe the fallback behavior.
Next: Theming and UI Assets.