Preferences: list the languages alphabetically by their own name

- 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
This commit is contained in:
Nikolai Nyegaard
2026-10-21 23:14:17 +07:00
committed by bflatastic
parent cb0a7d9b31
commit 48263b339d
4 changed files with 51 additions and 4 deletions
+47
View File
@@ -312,6 +312,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<Lang> {
let mut rest: Vec<Lang> = 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()
}
pub fn name(self) -> &'static str {
self.0.name
}
@@ -596,3 +606,40 @@ pub fn tn(n: u64, one: &str, other: &str) -> String {
#[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<String> = 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");
}
}
+1 -1
View File
@@ -365,7 +365,7 @@ pub fn mac_layout(app: &VectorcraftApp, bar: &MenuBar, lang: Lang) -> Layout {
/// with the `interfaceLanguage` preference checked.
fn language_node(app: &VectorcraftApp, lang: Lang) -> Node {
let mut children = vec![Node::Item(native_item(app, "Automatic", "app.language", json!({"lang": "auto"}))), Node::Separator];
children.extend(Lang::all().map(|l| Node::Item(native_item(app, l.name(), "app.language", json!({"lang": l.code()})))));
children.extend(Lang::for_picker().into_iter().map(|l| Node::Item(native_item(app, l.name(), "app.language", json!({"lang": l.code()})))));
Node::Submenu { label: tr(lang, "Language").into(), children }
}
+2 -2
View File
@@ -348,8 +348,8 @@ fn category_fields(ui: &mut egui::Ui, d: &mut Dialog, cat: &str) {
// `auto` plus every registered language, by its own name.
labeled(ui, sp.label, |ui| {
let cur = v.as_str().unwrap_or("auto");
let codes: Vec<&str> = std::iter::once("auto").chain(crate::i18n::Lang::all().map(|l| l.code())).collect();
let labels: Vec<&str> = std::iter::once(tl!("Auto")).chain(crate::i18n::Lang::all().map(|l| l.name())).collect();
let codes: Vec<&str> = std::iter::once("auto").chain(crate::i18n::Lang::for_picker().into_iter().map(|l| l.code())).collect();
let labels: Vec<&str> = std::iter::once(tl!("Auto")).chain(crate::i18n::Lang::for_picker().into_iter().map(|l| l.name())).collect();
let cur_label = codes.iter().position(|c| c.eq_ignore_ascii_case(cur)).and_then(|i| labels.get(i).copied()).unwrap_or(cur);
if let Some(i) = widgets::dropdown(ui, sp.key, cur_label, &labels, 260.0)
&& let Some(code) = codes.get(i)
+1 -1
View File
@@ -135,7 +135,7 @@ fn language_and_appearance_are_in_the_app_menu() {
assert_eq!(ids(submenu(app_menu, "Language"))[..2], ["app.language", "---"], "Automatic, then the languages");
let langs: Vec<(String, Option<bool>)> = items(submenu(app_menu, "Language")).iter().map(|it| (it.label.clone(), it.checked)).collect();
let mut want = vec![("Automatic".to_string(), Some(false))];
want.extend(Lang::all().map(|l| (l.name().to_string(), Some(l.code() == "ja"))));
want.extend(Lang::for_picker().into_iter().map(|l| (l.name().to_string(), Some(l.code() == "ja"))));
assert_eq!(langs, want);
// In every UI language the languages keep their own names.
for ui in Lang::all() {