diff --git a/crates/proof/src/english.rs b/crates/proof/src/english.rs index d3dc5d74..0c2c2e13 100644 --- a/crates/proof/src/english.rs +++ b/crates/proof/src/english.rs @@ -191,13 +191,17 @@ fn key(lang: ProofLang) -> &'static str { names(lang).first().copied().unwrap_or("en_US") } -/// Read the first Hunspell pair of `lang`'s own that loads, trying each name in each folder -/// ([`crate::hunspell::candidates`]; with SCOWL embedded, the dictionaries folder only: +/// Read the first Hunspell pair of `lang`'s own that loads, trying each name in each folder and +/// an installed pack named exactly for it ([`crate::hunspell::candidates_exact`]: never another +/// dialect's file or pack; with SCOWL embedded, the dictionaries folder only: /// [`crate::hunspell::user_candidates`]), with the first failure before it (the file `lang` /// would rather use), for [`note_error`]. fn load(lang: ProofLang) -> (Loaded, Option<(PathBuf, String)>) { - let candidates = - if crate::scowl::dict(lang).is_some() { crate::hunspell::user_candidates(names(lang)) } else { crate::hunspell::candidates(names(lang)) }; + let candidates = if crate::scowl::dict(lang).is_some() { + crate::hunspell::user_candidates(names(lang)) + } else { + crate::hunspell::candidates_exact(names(lang)) + }; if candidates.is_empty() { log::info!("English spelling: no Hunspell {} dictionary to read first; using SCOWL or the built-in word list", key(lang)); return (None, None); diff --git a/crates/proof/src/hunspell.rs b/crates/proof/src/hunspell.rs index cc430374..5537a93c 100644 --- a/crates/proof/src/hunspell.rs +++ b/crates/proof/src/hunspell.rs @@ -199,6 +199,15 @@ fn ordered(names: &[&str], dirs: &[PathBuf], region_packs: &[PathBuf], system: & out } +/// [`candidates`] without another region's or dialect's dictionary: the dictionaries folders, +/// then an installed pack named exactly by one of `names` (`en_GB` → `en-gb`), then the system's +/// folders, `names` only. English uses it: en-GB never falls back to an `en_US` file or pack. +pub fn candidates_exact(names: &[&str]) -> Vec<(PathBuf, PathBuf)> { + let lo = names.first().copied().map(lo_ext_dirs); + let (dirs, system) = search_tiers(lo.as_deref()); + ordered(names, &dirs, &crate::packs::exact_spelling_dirs(names), &system, &[]) +} + /// [`candidates`] in the folders the user keeps dictionaries in for WordCraft ([`user_dirs`]) /// only, not the system locations. pub fn user_candidates(names: &[&str]) -> Vec<(PathBuf, PathBuf)> { diff --git a/crates/proof/src/lib.rs b/crates/proof/src/lib.rs index 832f78df..4f0eb8f5 100644 --- a/crates/proof/src/lib.rs +++ b/crates/proof/src/lib.rs @@ -1321,6 +1321,10 @@ mod tests { /// installed English Language pack, then the system's Hunspell, then the built-in list. #[test] fn an_english_pack_comes_after_the_dictionaries_folder() { + // With SCOWL embedded, English reads only the dictionaries folder before it. + if scowl_embedded() { + return; + } let _guard = DICTIONARY_TESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner); set_dictionaries_folder(Some(shared_test_folder())); let root = std::env::temp_dir().join(format!("wc-proof-en-pack-{}", std::process::id())); @@ -1328,24 +1332,24 @@ mod tests { let (dir, packs) = (root.join("dictionaries"), root.join("language-packs")); let aff: &[u8] = b"SET UTF-8\nSFX S Y 1\nSFX S 0 s .\n"; let pack = write_pack(&packs, "en-us", &[("spelling.aff", aff), ("spelling.dic", b"2\nzzpackword\nhouse/S\n")]); + let gb_pack = write_pack(&packs, "en-gb", &[("spelling.aff", aff), ("spelling.dic", b"2\nzzgbword\nhouse/S\n")]); packs::set_packs_dir(Some(packs.clone())); let english: &[(&str, &[u8], &[u8])] = &[("en_US", aff, b"2\nzzfolderword\nhouse/S\n")]; folder_with_english(&dir, english); - if english_data() { - assert_eq!(english_source(ProofLang::En), Some(EnglishSource::Embedded)); - } else { - assert_eq!(english_source(ProofLang::En), Some(EnglishSource::Hunspell(dir.join("en_US.dic"))), "the folder first"); - assert!(is_correct_in("zzfolderword", ProofLang::En) && !is_correct_in("zzpackword", ProofLang::En)); - } + assert_eq!(english_source(ProofLang::En), Some(EnglishSource::Hunspell(dir.join("en_US.dic"))), "the folder first"); + assert!(is_correct_in("zzfolderword", ProofLang::En) && !is_correct_in("zzpackword", ProofLang::En)); + // Each English its own: the folder's en_US never stands in for en-GB, which takes its pack. + assert_eq!(english_source(ProofLang::EnGb), Some(EnglishSource::Hunspell(gb_pack.join("spelling.dic")))); drop_english_files(&dir, english); - if !english_data() { - assert_eq!(english_source(ProofLang::En), Some(EnglishSource::Hunspell(pack.join("spelling.dic"))), "then the pack"); - assert!(is_correct_in("zzpackword", ProofLang::En) && is_correct_in("houses", ProofLang::En)); - } + assert_eq!(english_source(ProofLang::En), Some(EnglishSource::Hunspell(pack.join("spelling.dic"))), "then the pack"); + assert!(is_correct_in("zzpackword", ProofLang::En) && is_correct_in("houses", ProofLang::En)); + // Neither English takes the other dialect's pack: without its own, en-CA has none. + std::fs::remove_dir_all(&gb_pack).unwrap(); + packs::reload(); + assert_ne!(english_source(ProofLang::EnGb), Some(EnglishSource::Hunspell(pack.join("spelling.dic"))), "never en-us for en-GB"); + assert_ne!(english_source(ProofLang::EnCa), Some(EnglishSource::Hunspell(pack.join("spelling.dic"))), "never en-us for en-CA"); packs::set_packs_dir(None); - if !english_data() { - assert_ne!(english_source(ProofLang::En), Some(EnglishSource::Hunspell(pack.join("spelling.dic"))), "a removed pack is not used"); - } + assert_ne!(english_source(ProofLang::En), Some(EnglishSource::Hunspell(pack.join("spelling.dic"))), "a removed pack is not used"); set_dictionaries_folder(Some(shared_test_folder())); let _ = std::fs::remove_dir_all(&root); } @@ -2253,20 +2257,6 @@ mod tests { assert_eq!(suggest_in("programmes", 5, us).first().map(String::as_str), Some("programs")); } - /// Does en-GB read a list with the British spellings: the Moby list (embedded or built in) or a - /// British Hunspell dictionary? Without the Language data, a machine with only a US Hunspell - /// dictionary (GitHub's Ubuntu runners) spells en-GB with it, by design, and marks `goitre`. - fn has_british_source() -> bool { - match english_source(ProofLang::EnGb) { - Some(EnglishSource::Embedded | EnglishSource::CoreList) => true, - Some(EnglishSource::Hunspell(dic)) => { - let stem = dic.file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or_default(); - !["en_US", "en-US", "en_CA", "en-CA"].contains(&stem.as_str()) - } - None => false, - } - } - /// More British spellings that must stay right in en-GB and en-AU (#1281): -re, single l, ae, /// with their inflections. #[test] @@ -2275,9 +2265,6 @@ mod tests { let _guard = crate::DICTIONARY_TESTS.lock().unwrap_or_else(std::sync::PoisonError::into_inner); let gb = ProofLang::EnGb; assert_eq!(ProofLang::from_tag(Some("en-AU")), gb); - if !has_british_source() { - return; - } for w in [ "goitre", "goitres", diff --git a/crates/proof/src/packs.rs b/crates/proof/src/packs.rs index 2cf34a2c..f29fa3ae 100644 --- a/crates/proof/src/packs.rs +++ b/crates/proof/src/packs.rs @@ -468,6 +468,25 @@ pub(crate) fn spelling_dirs(names: &[&str]) -> (Vec, Vec) { all.into_iter().partition(in_region) } +/// The folders of the installed packs with a spelling dictionary whose locale is exactly one of +/// `names` (`en_GB` → `en-gb`), in that order: no other region, no language alone. +pub(crate) fn exact_spelling_dirs(names: &[&str]) -> Vec { + if read().dir.is_none() { + return Vec::new(); + } + let scan = scan(); + let mut out: Vec = Vec::new(); + for n in names { + let l = n.to_ascii_lowercase().replace('_', "-"); + if let Some((p, _)) = scan.packs.iter().find(|(p, h)| h.spelling && p.locale == l) + && !out.contains(&p.path) + { + out.push(p.path.clone()); + } + } + out +} + /// The pack locales of `names`' own region: the first name's (`pt_BR` → `pt-br`) and the language /// alone (`pt`) when it is written in the same script. pub(crate) fn region_locales(names: &[&str]) -> Vec {