From f58b237275e65af69a8ba833ec3bb903c470e54d Mon Sep 17 00:00:00 2001 From: Nikolai Nyegaard Date: Sat, 24 Oct 2026 07:43:13 +0200 Subject: [PATCH] Settings: list the languages alphabetically by their own name (#720) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - English 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 - Lang::for_picker holds the order; the picker used the registry order, which is the order the languages were added --- crates/ui-egui/src/i18n/lang.rs | 32 +++++++++++++++++++++++++++ crates/ui-egui/src/i18n/tests.rs | 17 +++++++++++++- crates/ui-egui/src/settings_dialog.rs | 2 +- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/i18n/lang.rs b/crates/ui-egui/src/i18n/lang.rs index 7a26a40b..b6328759 100644 --- a/crates/ui-egui/src/i18n/lang.rs +++ b/crates/ui-egui/src/i18n/lang.rs @@ -275,6 +275,17 @@ impl Lang { LANGUAGES.iter().map(Lang) } + /// Every registered language in the order the Settings 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: Cyrillic, then the CJK languages. The order follows the + /// native name even where the picker shows a CJK language's English name instead. + pub fn for_picker() -> Vec { + let mut rest: Vec = Lang::all().filter(|l| l.code() != "en").collect(); + rest.sort_by_cached_key(|l| name_sort_key(l.name())); + std::iter::once(Lang::EN).chain(rest).collect() + } + pub fn name(self) -> &'static str { self.0.name } @@ -355,3 +366,24 @@ fn chinese(tag: &str) -> Option { None } } + +/// `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. +pub(super) 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() +} diff --git a/crates/ui-egui/src/i18n/tests.rs b/crates/ui-egui/src/i18n/tests.rs index b7735e70..c9e3dc2f 100644 --- a/crates/ui-egui/src/i18n/tests.rs +++ b/crates/ui-egui/src/i18n/tests.rs @@ -4,7 +4,7 @@ use std::collections::{BTreeSet, HashSet}; use super::catalog::{Catalog, unescape}; -use super::lang::{plural_czech, plural_east_slavic, plural_polish}; +use super::lang::{name_sort_key, plural_czech, plural_east_slavic, plural_polish}; use super::{LANGUAGES, Lang, LangInfo, lang_from_tag, message, tr, trn}; /// Each catalog's rows as (line number, context, source, translation), split exactly as the @@ -547,3 +547,18 @@ fn complete_catalogs_translate_package_formats() { } } } + +#[test] +fn picker_lists_english_first_then_names_alphabetically() { + let order = Lang::for_picker(); + assert_eq!(order.first().map(|l| l.code()), Some("en")); + 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:?}"); + let pos = |code: &str| order.iter().position(|l| l.code() == code).unwrap_or_else(|| panic!("{code} registered")); + assert!(pos("cs") < pos("de"), "Čeština sorts under C, not after Z"); + assert!(pos("fr") < pos("hu"), "Magyar sorts under M, not under its code"); + assert!(pos("pt-BR") < pos("ru"), "Latin names come before Cyrillic"); + assert!(pos("ru") < pos("ja"), "Cyrillic names come before CJK"); + assert_eq!(name_sort_key("Čeština"), "cestina"); +} diff --git a/crates/ui-egui/src/settings_dialog.rs b/crates/ui-egui/src/settings_dialog.rs index 6e8f4199..c4eab485 100644 --- a/crates/ui-egui/src/settings_dialog.rs +++ b/crates/ui-egui/src/settings_dialog.rs @@ -123,7 +123,7 @@ impl LauncherUi { let width = ui.available_width(); egui::ComboBox::from_id_salt("language").selected_text(Settings::language_label(&lang_code, installed)).width(width).show_ui(ui, |ui| { ui.selectable_value(&mut lang_code, Settings::LANGUAGE_AUTO.to_string(), tl!("Auto")); - for lang in crate::i18n::Lang::all() { + for lang in crate::i18n::Lang::for_picker() { ui.selectable_value(&mut lang_code, lang.code().to_string(), Settings::language_label(lang.code(), installed)); } });