diff --git a/Cargo.lock b/Cargo.lock
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3311,6 +3311,7 @@ dependencies = [
  "pollster 1.0.1",
  "rfd",
  "serde_json",
+ "sys-locale",
  "winit",
  "winresource",
  "zbus",
@@ -6145,6 +6146,15 @@ dependencies = [
  "syn 3.0.6",
 ]
 
+[[package]]
+name = "sys-locale"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "sysctl"
 version = "0.5.5"
diff --git a/Cargo.toml b/Cargo.toml
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -51,6 +51,8 @@ sha2 = "0.10"
 serde = { version = "1", features = ["derive", "rc"] }
 serde_json = { version = "1", features = ["float_roundtrip"] }
 log = "0.4"
+# the operating system's preferred languages, for the desktop "Auto" language
+sys-locale = "0.3"
 # All date/time conversion goes through chrono (no hand-rolled calendar code beyond the 400-year range extension; AGENTS.md).
 chrono = { version = "0.4.45", default-features = false, features = ["std"] }
 rayon = "1"
diff --git a/apps/lightcraft/Cargo.toml b/apps/lightcraft/Cargo.toml
--- a/apps/lightcraft/Cargo.toml
+++ b/apps/lightcraft/Cargo.toml
@@ -42,6 +42,8 @@ winit = { version = "0.30", default-features = false, features = ["x11"] }
 
 [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
 rfd = { workspace = true }
+# the system's preferred languages for the "Auto" interface language (`main`)
+sys-locale = { workspace = true }
 
 [target.'cfg(target_os = "linux")'.dependencies]
 zbus = { workspace = true }
diff --git a/apps/lightcraft/src/main.rs b/apps/lightcraft/src/main.rs
--- a/apps/lightcraft/src/main.rs
+++ b/apps/lightcraft/src/main.rs
@@ -193,6 +193,11 @@ fn main() -> eframe::Result {
     // what the arguments name, resolved now: an empty or relative argument never becomes a drive
     // root, and a whole drive or home folder is not walked (issue #374)
     let (files, launch_notices) = launch_imports(&files);
+    // "Auto" follows the system's preferred languages, as the web build follows the browser's:
+    // installed before the settings load, since a library without saved settings starts in it.
+    if let Some(language) = lightcraft_ui_egui::i18n::locale_from_tags(&sys_locale::get_locales().collect::<Vec<_>>()) {
+        lightcraft_ui_egui::i18n::set_default_language(language);
+    }
     let (prefs, prefs_warning, keep_prefs_file) = load_prefs(in_memory);
     // the saved language from the start, so a failed start is reported in it too
     if let Some(ui) = &prefs {
diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs
--- a/crates/ui-egui/src/i18n.rs
+++ b/crates/ui-egui/src/i18n.rs
@@ -193,23 +193,23 @@ thread_local! {
     static DEFAULT_LANGUAGE: std::cell::Cell<Option<Locale>> = const { std::cell::Cell::new(None) };
 }
 
-/// Install the language the "Auto" setting resolves to when nothing is stored. The browser host
-/// calls this once at startup with the browser's own languages ([`locale_from_tags`]); the
-/// desktop host doesn't: there Auto keeps following the environment. Must run before the UI state
-/// is first constructed.
+/// Install the language the "Auto" setting resolves to when nothing is stored. Each host calls
+/// this once at startup through [`locale_from_tags`]: the browser host with the browser's own
+/// languages, the desktop host with the operating system's preferred languages. Must run before
+/// the UI state is first constructed.
 pub fn set_default_language(language: Locale) {
     DEFAULT_LANGUAGE.with(|slot| slot.set(Some(language)));
 }
 
-/// The first of `tags` (BCP-47, most preferred first, as `navigator.languages` lists them) that
-/// names a shipped language; None when none does.
+/// The first of `tags` (BCP-47, most preferred first, as `navigator.languages` and the system's
+/// language list give them) that names a shipped language; None when none does.
 pub fn locale_from_tags(tags: &[String]) -> Option<Locale> {
     tags.iter().find_map(|tag| Locale::parse_tag(tag))
 }
 
 /// The UI language "Auto" resolves to: the environment's (`LIGHTCRAFT_LANGUAGE=zh-hans`, for
-/// headless runs), or the host-installed default ([`set_default_language`], the browser's languages
-/// on the web), else English.
+/// headless runs), or the host-installed default ([`set_default_language`]: the system's languages
+/// on the desktop, the browser's on the web), else English.
 pub fn default_language() -> Locale {
     std::env::var("LIGHTCRAFT_LANGUAGE")
         .ok()
diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs
--- a/crates/ui-egui/src/i18n/tests.rs
+++ b/crates/ui-egui/src/i18n/tests.rs
@@ -184,7 +184,7 @@ fn locale_tags_normalize() {
     assert_eq!(Locale::parse_tag("xx"), None);
 }
 
-/// The web host's "Auto" (issue #2085): the browser's `navigator.languages` in order, first
+/// The hosts' "Auto" (issue #2085): the browser's `navigator.languages`, or the system's languages, in order, first
 /// shipped language wins, unsupported tags are skipped, an empty or foreign list stays English
 /// ([`default_language`] fallback).
 #[test]
@@ -196,6 +196,9 @@ fn locale_from_tags_takes_the_first_shipped_language() {
     assert_eq!(locale_from_tags(&["zh-TW".into(), "en".into()]), Some(Locale::ZhHant));
     assert_eq!(locale_from_tags(&["ja_JP.UTF-8".into(), "en".into()]), Some(Locale::Ja));
     assert_eq!(locale_from_tags(&["pt".into(), "pt-BR".into()]), Some(Locale::PtBr));
+    // A language set up with a region it has no catalog of its own for (Russian in Slovakia).
+    assert_eq!(locale_from_tags(&["ru-SK".into(), "en-SK".into(), "sk-SK".into()]), Some(Locale::Ru));
+    assert_eq!(locale_from_tags(&["en-US".into(), "ru-RU".into()]), Some(Locale::En));
 }
 
 #[test]
diff --git a/docs/localization.md b/docs/localization.md
--- a/docs/localization.md
+++ b/docs/localization.md
@@ -113,6 +113,10 @@ in code without invalidating anyone's saved preference. `LIGHTCRAFT_LANGUAGE` ac
 locale looks like (`zh_Hans`, `zh-CN`, `en_US`, `ja_JP.UTF-8`) and falls back to the base language
 when a region has no dedicated entry.
 
+With no saved setting, the app starts in the first of the system's preferred languages it has a
+catalog for (the browser's languages in the web build), else English. `LIGHTCRAFT_LANGUAGE`
+overrides that, and a language chosen in Edit ▸ Language is saved and kept.
+
 ## Release notes
 
 Help ▸ What's New shows `docs/whats-new.md`, and each of its lines (a heading, or one wrapped line of a
diff --git a/docs/whats-new.md b/docs/whats-new.md
--- a/docs/whats-new.md
+++ b/docs/whats-new.md
@@ -1,5 +1,13 @@
 # What's new in LightCraft
 
+## 2026-10-27
+
+### The desktop app starts in the system's language
+- On macOS, Windows and Linux, a library with no saved settings now opens in the first of the system's preferred
+  languages that LightCraft has a catalog for (Russian for `ru-SK`, German for `de-AT`), as the web build follows the
+  browser's languages. Before, it opened in English unless `LIGHTCRAFT_LANGUAGE` was set. A language chosen in Edit ▸
+  Language is kept as before.
+
 ## 2026-10-24
 
 ### Relative-date filters and smart albums follow the clock
