installrs 0.1.0-rc13

Build self-contained software installers in plain Rust, with an optional native wizard GUI (Win32 / GTK3), component selection, progress, cancellation, and compression.
Documentation
<!-- markdownlint-configure-file { "MD013": { "line_length": 100 } } -->

# Internationalization

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/`](../example) installer,
which uses [`rust-i18n`](https://crates.io/crates/rust-i18n) plus
[`sys-locale`](https://crates.io/crates/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.

## 1. Add translation dependencies

```toml
[dependencies]
installrs = "0.1"
rust-i18n = "3"
sys-locale = "0.3"
anyhow = "1"
```

## 2. Wire up `rust-i18n`

Create a `strings.yml` next to your `Cargo.toml`:

```yaml
_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 string
```

Load it at the top of your installer lib:

```rust
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.

## 3. Detect the system locale

```rust
/// 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.

## 4. Let the user override the 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:

```rust
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.

## 5. The eager string capture gotcha

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:

```rust
// ✅ Correct order
init_locale();
show_language_dialog_if_needed();
rust_i18n::set_locale(&chosen);

let mut w = InstallerGui::wizard(&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::wizard(&t!("installer.title"));
// ...
w.run(i)?;

rust_i18n::set_locale(&chosen); // too late
```

If 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.

## 6. What to translate

Beyond the obvious (page titles, labels, button labels), don't forget:

### Button labels

Pass a localized `ButtonLabels` to `w.buttons(...)`:

```rust
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.

### Component labels and descriptions

```rust
i.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 and log strings emitted during install

`.status(...)` / `.log(...)` on builder ops show up in the progress page
and the log file:

```rust
i.file(source!("app.exe"), "app.exe")
    .status(t!("install.status.app"))
    .log(t!("install.log.app"))
    .install()?;
```

### Error messages

Native dialog helpers (`installrs::gui::error`, `confirm`, etc.) show
whatever text you pass. Localize the title and body:

```rust
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:

```rust
Err(anyhow::anyhow!("{}", t!("errors.disk_full")))
```

### Interpolated values

`rust-i18n` supports `%{name}` placeholders. Declare them in your YAML
and pass substitutions at the call site:

```yaml
confirm.install_to:
  en: "Install to %{dir}?"
  de: "In %{dir} installieren?"
  es: "¿Instalar en %{dir}?"
```

```rust
let dir: String = i.get_option("install-dir").unwrap_or_default();
t!("confirm.install_to", dir = dir)
```

## 7. Testing

`rust-i18n` responds to `LANG` in the environment via `sys-locale`, so:

```sh
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).

## See also

- [GUI Wizard]gui-wizard.md — the `ButtonLabels` struct and the
  eager-string-capture pattern referenced in §5.
- [Installer API]installer-api.md — component
  labels and descriptions that need localizing at registration time.
- [`example/`]../example — the repository's reference installer,
  translated into English, German, and Spanish.