diff --git a/docs/features.md b/docs/features.md index b22c8c39..5fbf6731 100644 --- a/docs/features.md +++ b/docs/features.md @@ -217,10 +217,11 @@ composer added aren't checked. Turn spell checking off with **Check spelling** in Settings → General, which also says when there's nothing to check with. On Windows and Linux, the **Spelling** submenu also chooses your languages. -Flectar Mail starts with the languages your system is set to, and a word passes -when any of the chosen languages has it, so mail in two languages needs no -switching. On iOS it checks in your preferred languages, from Settings under -General, Language & Region, the same way. On macOS, the system picks the +Flectar Mail starts with the display language you chose in Settings, if any, +and the languages your system is set to, and a word passes when any of the +chosen languages has it, so mail in two languages needs no switching. On iOS it +checks in the same display language and your preferred languages, from Settings +under General, Language & Region, the same way. On macOS, the system picks the language of what you write from the ones set in System Settings, under Keyboard, Text Input. On Android, the spell checker chosen in Android's settings checks, usually the keyboard's, in the language set there. diff --git a/src/main.rs b/src/main.rs index 9d3cc26b..2778a217 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1175,6 +1175,7 @@ fn apply_language(app: &AppWindow, preference: &str) { language => language, }; number_format::set_language(selected); + spelling::apply_display_language(language_mode); if let Err(error) = slint::select_bundled_translation(selected) { eprintln!("could not select {selected} translation: {error}"); } diff --git a/src/spelling.rs b/src/spelling.rs index 02a70751..f494e859 100644 --- a/src/spelling.rs +++ b/src/spelling.rs @@ -26,7 +26,7 @@ use std::cell::{Cell, RefCell}; use std::ops::Range; use std::path::{Path, PathBuf}; use std::rc::Rc; -use std::sync::Arc; +use std::sync::{Arc, Mutex, PoisonError}; /// Whether the app has a checker of its own for this platform. A host may bring one too. #[cfg(desktop)] @@ -56,6 +56,10 @@ thread_local! { static SPELLING: RefCell>> = const { RefCell::new(None) }; } +/// The display language the user chose in Settings, such as "es", which is checked first when +/// they chose no spelling languages. `None` while the app follows the system's language. +static DISPLAY_LANGUAGE: Mutex> = Mutex::new(None); + /// A language spelling can be checked in. #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct Language { @@ -271,6 +275,23 @@ pub(crate) fn apply_settings(enabled: bool, languages: Option>) { } } +/// The display language as set in Settings: "system", or a code such as "es". Open messages are +/// checked again when that changes the languages checked by default. +pub(crate) fn apply_display_language(language: &str) { + let display = (language != "system").then(|| language.to_owned()); + let changed = { + let mut current = DISPLAY_LANGUAGE + .lock() + .unwrap_or_else(PoisonError::into_inner); + let changed = *current != display; + *current = display; + changed + }; + if changed && service().is_some_and(|spelling| spelling.chosen.borrow().is_none()) { + crate::composer::spelling_changed(); + } +} + /// Show the spelling settings in the main window's settings. pub(crate) fn apply_to_window(app: &crate::AppWindow) { let Some(spelling) = service() else { @@ -461,8 +482,8 @@ pub(crate) struct Checked { pub(crate) found: Vec>, } -/// The installed languages that are checked: those `chosen`, or the ones matching the system's -/// languages when the user chose none. +/// The installed languages that are checked: those `chosen`, or the ones matching the display +/// language and the system's languages when the user chose none. fn active_languages(installed: &[Language], chosen: Option<&[String]>) -> Vec { match chosen { Some(chosen) => chosen @@ -474,9 +495,40 @@ fn active_languages(installed: &[Language], chosen: Option<&[String]>) -> Vec Vec { - default_languages(installed, &sys_locale::get_locales().collect::>()) + let display = DISPLAY_LANGUAGE + .lock() + .unwrap_or_else(PoisonError::into_inner) + .clone(); + let locales = default_locales(display.as_deref(), sys_locale::get_locales().collect()); + default_languages(installed, &locales) +} + +/// The locales spelling starts with: the display language the user chose first, then the +/// system's. A system locale in the same language goes first in its place, so its region is +/// kept: English shown on a British system stays British English. +fn default_locales(display: Option<&str>, mut system: Vec) -> Vec { + let Some(display) = display else { + return system; + }; + let language = |locale: &str| { + locale + .split(['-', '_']) + .next() + .unwrap_or_default() + .to_ascii_lowercase() + }; + let wanted = language(display); + match system.iter().position(|locale| language(locale) == wanted) { + Some(index) => { + let same = system.remove(index); + system.insert(0, same); + } + None => system.insert(0, display.to_owned()), + } + system } /// The installed languages matching the system's, one for each of its languages: the same @@ -770,6 +822,31 @@ mod tests { assert_eq!(defaults(&["es"], &["es-MX"]), ["es"]); } + #[test] + fn the_display_language_the_user_chose_comes_first() { + let installed = installed(&["en_GB", "en_US", "es_ES", "es_MX", "ru_RU"]); + let starting = |display: Option<&str>, system: &[&str]| { + let system = system.iter().map(|locale| (*locale).to_owned()).collect(); + default_languages(&installed, &default_locales(display, system)) + }; + // Following the system, only its languages. + assert_eq!(starting(None, &["en-US"]), ["en_US"]); + // Spanish shown on an English system checks both, Spanish first. + assert_eq!(starting(Some("es"), &["en-US"]), ["es_ES", "en_US"]); + // English shown on a British system stays British English. + assert_eq!( + starting(Some("en"), &["es-ES", "en-GB"]), + ["en_GB", "es_ES"] + ); + // A display language without a dictionary changes nothing. + assert_eq!(starting(Some("zh_Hans"), &["en-US"]), ["en_US"]); + + apply_display_language("es"); + assert_eq!(*DISPLAY_LANGUAGE.lock().unwrap(), Some("es".to_owned())); + apply_display_language("system"); + assert_eq!(*DISPLAY_LANGUAGE.lock().unwrap(), None); + } + #[test] fn quotes_code_and_the_inserted_signature_are_left_out() { let signature = "\n\n-- \nDean Tirok";