InstallRS doesn't ship a translation system of its own — it defers to
whatever you plug in for your installer crate. This guide walks through
the pattern used by the repository's example/ installer,
which uses rust-i18n plus
sys-locale for automatic
locale detection.
The same pattern works for any translation library you prefer — the interesting bits are where to set the locale, which strings the installer needs translated, and how to handle the pre-wizard language selector.
[dependencies]
installrs = "0.1"
rust-i18n = "3"
sys-locale = "0.3"
anyhow = "1"Create a strings.yml next to your Cargo.toml:
_version: 2
installer.title:
en: "My App Installer"
de: "Mein-App-Installer"
es: "Instalador de Mi App"
wizard.back:
en: "< Back"
de: "< Zurück"
es: "< Atrás"
wizard.next:
en: "Next >"
de: "Weiter >"
es: "Siguiente >"
# ... one entry per translatable stringLoad it at the top of your installer lib:
use anyhow::Result;
use installrs::{source, Installer};
use rust_i18n::t;
// Load translations from any .yml files in this directory, English fallback.
rust_i18n::i18n!(".", fallback = "en");The i18n! macro reads every .yml file next to Cargo.toml at build
time and embeds the strings into your crate — no external files to ship.
/// Detect and apply the system locale, falling back to English.
fn init_locale() {
let locale = sys_locale::get_locale().unwrap_or_else(|| "en".to_string());
// Use just the language prefix (e.g. "de-DE" → "de").
let lang = locale.split('-').next().unwrap_or("en");
rust_i18n::set_locale(lang);
}Call this as the first thing in your install() / uninstall()
functions — before registering components or building the wizard — so
every subsequent t!() call returns the right language.
Users on multilingual systems or in situations where the OS locale is
wrong deserve a way to pick. Use the built-in
installrs::gui::choose_language modal before the wizard is built:
pub fn install(i: &mut Installer) -> Result<()> {
init_locale();
// In GUI mode, let the user pick a language before we build the wizard.
// Skip in headless mode — there's no GUI to show a dialog in.
if !std::env::args().any(|a| a == "--headless") {
let choices: &[(&str, &str)] = &[
("en", "English"),
("es", "Español"),
("de", "Deutsch"),
];
let default = rust_i18n::locale().to_string();
if let Some(code) = installrs::gui::choose_language(
&t!("installer.language.title"),
&t!("installer.language.prompt"),
choices,
Some(&default),
)? {
rust_i18n::set_locale(&code);
}
}
// ... now build the wizard with strings in the chosen locale
}Important: the dialog's own title and prompt are taken from the
already-detected locale set by init_locale(). Make sure those
installer.language.* keys have translations for every language you
support, or the pre-wizard dialog will show unlocalized fallback text.
The wizard builder captures all page strings eagerly when you call
the .welcome(...), .license(...), etc. methods. That means the locale
must be final before you start chaining wizard page methods:
// ✅ Correct order
init_locale();
show_language_dialog_if_needed();
rust_i18n::set_locale(&chosen);
let mut w = InstallerGui::new(&t!("installer.title")); // chosen locale
w.welcome(&t!("installer.welcome.title"), &t!("installer.welcome.message"));
// ...
w.run(i)?;
// ❌ Wrong order — strings captured in the detected locale, not the chosen one
let mut w = InstallerGui::new(&t!("installer.title"));
// ...
w.run(i)?;
rust_i18n::set_locale(&chosen); // too lateIf you need to switch the language after the wizard has been built (e.g.
a combo box on the first page), you'd have to exit and rebuild — or
restructure to put all localized content inside callbacks that re-read
t!() each time they fire. For most installers, the pre-wizard picker
is fine.
Beyond the obvious (page titles, labels, button labels), don't forget:
Pass a localized ButtonLabels to w.buttons(...):
w.buttons(installrs::gui::ButtonLabels {
back: t!("wizard.back").into(),
next: t!("wizard.next").into(),
install: t!("wizard.install").into(),
uninstall: t!("wizard.uninstall").into(),
finish: t!("wizard.finish").into(),
cancel: t!("wizard.cancel").into(),
});Without this, buttons render as English defaults ("< Back", "Next >", etc.) regardless of your page-string translations.
i.add_component(
"core",
t!("components.core"), // label
t!("components.core_desc"), // description
10,
).required();The wizard's components page pulls these strings from the registered components, so they must be localized at registration time.
.status(...) / .log(...) on builder ops show up in the progress page
and the log file:
i.file(source!("app.exe"), "app.exe")
.status(t!("install.status.app"))
.log(t!("install.log.app"))
.install()?;Native dialog helpers (installrs::gui::error, confirm, etc.) show
whatever text you pass. Localize the title and body:
installrs::gui::error(
&t!("errors.install_failed.title"),
&t!("errors.install_failed.message"),
)?;Anyhow errors that propagate out of your install callback end up on the error page (if you registered one) or in a native error dialog. If those errors might be user-facing, construct them with localized strings:
Err(anyhow::anyhow!("{}", t!("errors.disk_full")))rust-i18n supports %{name} placeholders. Declare them in your YAML
and pass substitutions at the call site:
confirm.install_to:
en: "Install to %{dir}?"
de: "In %{dir} installieren?"
es: "¿Instalar en %{dir}?"let dir: String = i.option("install-dir").unwrap_or_default();
t!("confirm.install_to", dir = dir)rust-i18n responds to LANG in the environment via sys-locale, so:
LANG=de_DE.UTF-8 ./my-installer # German
LANG=es_ES.UTF-8 ./my-installer # Spanish
LANG=en_US.UTF-8 ./my-installer # English (or unset)Or force a locale programmatically in a test path — rust_i18n::set_locale("de").
Verify each translation by walking through every page; the common
mistakes are missing keys (show up as "installer.welcome.title"
verbatim instead of the translated string) and missing language codes
in a particular .yml entry (fall back to the configured fallback
language, often English — worth checking in a German-only run).
- GUI Wizard — the
ButtonLabelsstruct and the eager-string-capture pattern referenced in §5. - Installer API — component labels and descriptions that need localizing at registration time.
example/— the repository's reference installer, translated into English, German, and Spanish.