From ea61d1fcb833e12747d9925dfcbf12be6c0aed97 Mon Sep 17 00:00:00 2001 From: Nikolai Nyegaard Date: Wed, 21 Oct 2026 12:01:39 +0200 Subject: [PATCH] Options: list the languages alphabetically by their own name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - English (and Auto where offered) stays first as the source language and fallback; the rest sort by native name with accents folded, so Čeština is under C and Magyar under M; Cyrillic and CJK names follow the Latin ones - The list used the registry order, which is the order the languages were added --- crates/ui-egui/src/i18n/mod.rs | 47 ++++++++++++++++++++++++++++++++++ crates/ui-egui/src/options.rs | 2 +- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 092e37684..69ad981ae 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -256,6 +256,16 @@ impl Lang { LANGUAGES.iter().map(Lang) } + /// Every registered language in the order a picker lists them: English first (the source + /// language and the fallback), then the rest by their own name with accents folded, so + /// Čeština sits under C rather than after Z. Names in other scripts follow the Latin ones in + /// code point order: Greek, Cyrillic, Arabic, then the CJK languages. + pub fn for_picker() -> Vec { + let mut rest: Vec = Lang::all().filter(|l| l.code() != Lang::EN.code()).collect(); + rest.sort_by_cached_key(|l| name_sort_key(l.name())); + std::iter::once(Lang::EN).chain(rest).collect() + } + fn catalog(self) -> &'static Catalog { self.0.catalog() } @@ -474,3 +484,40 @@ mod audit; pub(crate) mod pseudo; #[cfg(test)] mod tests; + +/// `name` lowercased with the Latin accents that occur in language names folded away, so a +/// byte-wise sort reads alphabetically. Other scripts are left as they are. +fn name_sort_key(name: &str) -> String { + name.chars() + .flat_map(char::to_lowercase) + .map(|c| match c { + 'à'..='å' | 'ā' | 'ă' | 'ą' => 'a', + 'ç' | 'ć' | 'č' => 'c', + 'è'..='ë' | 'ē' | 'ě' | 'ę' | 'ế' | 'ệ' => 'e', + 'ì'..='ï' | 'ī' => 'i', + 'ñ' | 'ń' | 'ň' => 'n', + 'ò'..='ö' | 'ø' | 'ő' => 'o', + 'ś' | 'š' => 's', + 'ù'..='ü' | 'ū' | 'ů' | 'ű' => 'u', + 'ý' | 'ÿ' => 'y', + 'ź' | 'ż' | 'ž' => 'z', + c => c, + }) + .collect() +} + +#[cfg(test)] +mod picker_order_tests { + use super::*; + + #[test] + fn picker_lists_english_first_then_names_alphabetically() { + let order = Lang::for_picker(); + assert_eq!(order.first().map(|l| l.code()), Some(Lang::EN.code())); + assert_eq!(order.len(), Lang::all().count()); + let keys: Vec = order[1..].iter().map(|l| name_sort_key(l.name())).collect(); + assert!(keys.windows(2).all(|w| w[0] <= w[1]), "{keys:?}"); + assert_eq!(name_sort_key("Čeština"), "cestina"); + assert_eq!(name_sort_key("Türkçe"), "turkce"); + } +} diff --git a/crates/ui-egui/src/options.rs b/crates/ui-egui/src/options.rs index 2907d7e04..c5e2f7023 100644 --- a/crates/ui-egui/src/options.rs +++ b/crates/ui-egui/src/options.rs @@ -634,7 +634,7 @@ fn language_picker(app: &mut WordApp, ui: &mut Ui) { if ui.selectable_label(app.ui.language == AUTO, auto_label.as_str()).clicked() { let _ = app.run("ui.language", json!({"value": AUTO})); } - for lang in Lang::all() { + for lang in Lang::for_picker() { if ui.selectable_label(app.ui.language == lang.code(), lang.name()).clicked() { let _ = app.run("ui.language", json!({"value": lang.code()})); }