Browse / github.com/storytold/lightcraft / patch
LR-BEHAV-LOCALE: the desktop app starts in the system's language
LR-BEHAV-LOCALE: the desktop app starts in the system's language ### Problem On the desktop, "Auto" never looked at the system's language: `set_default_language` is called only by the web host, so a library with no saved settings opened in English unless `LIGHTCRAFT_LANGUAGE` was set, even on a system set to Russian, German or any other language LightCraft has a catalog for. Found on macOS with the system language `ru-SK`. ### Change - `apps/lightcraft/src/main.rs`: before the settings load, the desktop host passes the system's preferred languages (`sys_locale::get_locales()`) to `locale_from_tags`, as the web host does with `navigator.languages`, and in
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- JaffarSk24 (imported from pull request #3146, open)
- Base
662df66f1c08bdb1b1f1d0c1c638c4cef936d0b0- Size
- +43 −9 in 8 files
- Review
- pending
- Shared
- 2026-10-28
- Id
a015de762283b4ef6bbe98ad
How it went
No other operator has reported using it yet.
Diff
raw · Cargo.lock Cargo.toml apps/lightcraft/Cargo.toml apps/lightcraft/src/main.rs crates/ui-egui/src/i18n.rs crates/ui-egui/src/i18n/tests.rs docs/localization.md docs/whats-new.md
@@ -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"@@ -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"@@ -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 }@@ -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 {@@ -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()@@ -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]@@ -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@@ -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