diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 12a7d8d0..58e9cf02 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -414,7 +414,7 @@ fn insert_block(app: &mut CadApp, name: &str) { fn language(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) { egui::Window::new(crate::tl!("Interface Language")).id(egui::Id::new("interface_language")).open(open).resizable(false).show(ctx, |ui| { egui::ComboBox::from_id_salt("interface_language_choice").selected_text(app.ui.interface_language.name()).show_ui(ui, |ui| { - for preference in crate::i18n::Preference::ALL { + for preference in crate::i18n::Preference::for_picker() { if ui.selectable_label(app.ui.interface_language == preference, preference.name()).clicked() { let _ = app.run("ui.language", serde_json::json!({"lang": preference.code()})); ui.ctx().request_repaint(); diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index a0bf590d..c0ce1d25 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -68,6 +68,23 @@ impl Preference { Self::Turkish, ]; + /// Every preference in the order the Interface Language dialog lists them: Auto, then English + /// (the source language), then the rest by the language's own name with accents folded, so + /// Čeština sits under C and Magyar under M. Names in other scripts follow the Latin ones in + /// code point order: Greek, Cyrillic, Arabic, then the CJK languages. The order follows the + /// registry's native name even where the dialog shows an English label instead (Korean + /// without a Hangul face), so the list does not reorder when a font appears. + pub fn for_picker() -> Vec { + let mut rest: Vec = Self::ALL.into_iter().filter(|p| !matches!(p, Self::Auto | Self::English)).collect(); + rest.sort_by_cached_key(|p| name_sort_key(p.native_name())); + [Self::Auto, Self::English].into_iter().chain(rest).collect() + } + + /// The language's own name from the registry, whatever the dialog currently shows for it. + fn native_name(self) -> &'static str { + LANGUAGES.iter().find(|l| l.code.eq_ignore_ascii_case(self.code())).map_or_else(|| self.name(), |l| l.name) + } + /// The codes a preference can be given as, for messages ("auto, en or uk"). pub fn codes() -> String { let codes: Vec<&str> = Self::ALL.iter().map(|p| p.code()).collect(); @@ -377,3 +394,43 @@ macro_rules! tf { #[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_auto_and_english_first_then_names_alphabetically() { + let order = Preference::for_picker(); + assert_eq!(&order[..2], &[Preference::Auto, Preference::English]); + assert_eq!(order.len(), Preference::ALL.len()); + let keys: Vec = order[2..].iter().map(|p| name_sort_key(p.native_name())).collect(); + assert!(keys.windows(2).all(|w| w[0] <= w[1]), "{keys:?}"); + let pos = |p: Preference| order.iter().position(|q| *q == p).unwrap_or(usize::MAX); + assert!(pos(Preference::Czech) < pos(Preference::German), "Čeština sorts under C"); + assert!(pos(Preference::Italian) < pos(Preference::Hungarian), "Magyar sorts under M"); + assert!(pos(Preference::Turkish) < pos(Preference::Greek), "Latin names come before Greek"); + assert_eq!(name_sort_key("Türkçe"), "turkce"); + } +} diff --git a/crates/ui-egui/src/options/dialog.rs b/crates/ui-egui/src/options/dialog.rs index aba07311..66367038 100644 --- a/crates/ui-egui/src/options/dialog.rs +++ b/crates/ui-egui/src/options/dialog.rs @@ -467,7 +467,7 @@ fn preferences(app: &CadApp, ui: &mut egui::Ui, changes: &mut Map ui.label(crate::tl!("Interface language")); let mut lang = app.ui.interface_language; egui::ComboBox::from_id_salt("options_language").selected_text(lang.name()).show_ui(ui, |ui| { - for p in crate::i18n::Preference::ALL { + for p in crate::i18n::Preference::for_picker() { ui.selectable_value(&mut lang, p, p.name()); } });