From 2495fad4968375d9e6edf9c7580e55761a4bf204 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Sun, 1 Nov 2026 18:20:23 +0000 Subject: [PATCH] =?UTF-8?q?CP4.2:=20review=20=E2=80=94=20German=20and=20Bu?= =?UTF-8?q?lgarian=20are=20spelled=20from=20their=20pack;=20pack=20languag?= =?UTF-8?q?es=20are=20checked=20in=20any=20script?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- crates/proof/src/lang.rs | 23 +++++++++++++++----- crates/proof/src/lib.rs | 41 ++++++++++++++++++++++++++++++----- crates/proof/src/pack_lang.rs | 2 +- crates/proof/src/spelling.rs | 8 ++++++- 4 files changed, 62 insertions(+), 12 deletions(-) diff --git a/crates/proof/src/lang.rs b/crates/proof/src/lang.rs index e1b8f9444..c09d6f4dc 100644 --- a/crates/proof/src/lang.rs +++ b/crates/proof/src/lang.rs @@ -193,18 +193,31 @@ impl ProofLang { } } + /// The locale whose Hunspell dictionary (dictionaries folder, Language pack, system) spells + /// this language through the shared affix engine: a [`ProofLang::Pack`] language's own, and + /// German's and Bulgarian's, which have hyphenation (and Bulgarian rules) of their own but no + /// dictionary slot. `None` for the languages with a slot of their own, English and `Other`. + pub(crate) fn spelling_locale(self) -> Option { + match self { + ProofLang::Pack(l) => Some(l), + ProofLang::De => PackLocale::new("de-DE"), + ProofLang::Bg => PackLocale::new("bg-BG"), + _ => None, + } + } + /// The locale of a [`ProofLang::Pack`] language. pub fn pack_locale(self) -> Option { if let ProofLang::Pack(l) = self { Some(l) } else { None } } - /// Does WordCraft check spelling in this language? For a [`ProofLang::Pack`] language: is a - /// dictionary for it available now (read on first use, cached until the dictionaries folder or + /// Does WordCraft check spelling in this language? For a [`ProofLang::Pack`] language, German + /// and Bulgarian: is a dictionary for it available now (read on first use, cached until the dictionaries folder or /// the packs change)? pub fn is_proofed(self) -> bool { - match self { - ProofLang::Pack(l) => crate::pack_lang::has_dictionary(l), - _ => Self::PROOFED.contains(&self), + match self.spelling_locale() { + Some(l) => crate::pack_lang::has_dictionary(l), + None => Self::PROOFED.contains(&self), } } } diff --git a/crates/proof/src/lib.rs b/crates/proof/src/lib.rs index 88024fb85..924502cdc 100644 --- a/crates/proof/src/lib.rs +++ b/crates/proof/src/lib.rs @@ -322,11 +322,11 @@ pub(crate) fn reload_dictionaries() { DICTIONARY_GENERATION.fetch_add(1, Ordering::Relaxed); } -/// The `.dic` file a [`ProofLang::Pack`] language's spelling is read from now (the dictionaries +/// The `.dic` file a [`ProofLang::Pack`] language's (or German's or Bulgarian's) spelling is read from now (the dictionaries /// folder, an installed Language pack or the system's); `None` when it has none (unchecked) or /// `lang` isn't a pack language. Reads the dictionary once (it is cached afterwards). pub fn pack_dictionary_file(lang: ProofLang) -> Option { - pack_lang::dictionary_file(lang.pack_locale()?) + pack_lang::dictionary_file(lang.spelling_locale()?) } /// Each language that reads an installed Hunspell dictionary, with the `.dic` file found for it @@ -343,7 +343,7 @@ pub fn dictionary_load_error(lang: ProofLang) -> Option { if matches!(lang, ProofLang::En | ProofLang::EnGb | ProofLang::EnCa) { return english::load_error(lang); } - if let ProofLang::Pack(l) = lang { + if let Some(l) = lang.spelling_locale() { return pack_lang::load_error(l); } hunspell::load_error(language(lang)?.slot.name()).map(|(_, reason)| reason) @@ -842,7 +842,7 @@ pub fn is_correct_in_with(word: &str, lang: ProofLang, opts: &Options) -> bool { if lang.is_english() { return is_correct_english(word, lang, opts); } - if let ProofLang::Pack(l) = lang { + if let Some(l) = lang.spelling_locale() { return accepted_by_options(word, opts) || pack_lang::is_correct(word, l, opts); } // A language WordCraft doesn't proof (Bulgarian, German, any other) has no spelling marks. @@ -856,7 +856,7 @@ pub fn suggest_in(word: &str, max: usize, lang: ProofLang) -> Vec { if lang.is_english() { return suggest_english(word, max, lang); } - if let ProofLang::Pack(l) = lang { + if let Some(l) = lang.spelling_locale() { return pack_lang::suggest(word, max, l); } language(lang).map_or_else(Vec::new, |l| (l.suggest)(word, max, lang)) @@ -1496,6 +1496,37 @@ mod tests { let _ = std::fs::remove_dir_all(&root); } + /// Review of #485: German and Bulgarian, which have hyphenation of their own but no + /// dictionary slot, are spelled from their Language pack (or a Hunspell dictionary) too. + #[test] + fn german_and_bulgarian_are_spelled_from_their_packs() { + 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-de-bg-packs-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&root); + let hyphen_before = hyphen::hyphenate_word_in("Silbentrennung", &hyphen::Limits::default(), ProofLang::De); + write_pack(&root, "de", &[("spelling.aff", b"SET UTF-8\n"), ("spelling.dic", "2\nHaus\nund\n".as_bytes())]); + write_pack(&root, "bg", &[("spelling.aff", b"SET UTF-8\n"), ("spelling.dic", "1\nкъща\n".as_bytes())]); + packs::set_packs_dir(Some(root.clone())); + let de = ProofLang::from_tag(Some("de-AT")); + assert_eq!(de, ProofLang::De); + assert!(is_correct_in("Haus", de) && !is_correct_in("Hauz", de)); + assert!(suggest_in("Hauz", 5, de).contains(&"Haus".to_string())); + assert_eq!(check_text("Haus und Hauz", &[(0..13, de)]).len(), 1); + assert!(de.is_proofed() && has_dictionary(Some("de-CH"))); + assert_eq!(pack_dictionary_file(de), Some(root.join("de").join("spelling.dic"))); + assert!(is_correct_in("къща", ProofLang::Bg) && !is_correct_in("къшта", ProofLang::Bg), "any script, not only Latin"); + assert_eq!( + hyphen::hyphenate_word_in("Silbentrennung", &hyphen::Limits::default(), ProofLang::De), + hyphen_before, + "German keeps its own hyphenation" + ); + packs::set_packs_dir(None); + assert!(is_correct_in("Hauz", de) || pack_dictionary_file(de).is_some(), "no dictionary: unchecked"); + set_dictionaries_folder(Some(shared_test_folder())); + let _ = std::fs::remove_dir_all(&root); + } + /// At most [`pack_lang::MAX_LOADED`] pack-language dictionaries stay loaded; others load again /// when used. #[test] diff --git a/crates/proof/src/pack_lang.rs b/crates/proof/src/pack_lang.rs index 44dde95b6..1a1f4485e 100644 --- a/crates/proof/src/pack_lang.rs +++ b/crates/proof/src/pack_lang.rs @@ -144,7 +144,7 @@ pub(crate) fn is_correct(word: &str, locale: PackLocale, opts: &crate::Options) if d.is_empty() { return true; } - let Some(w) = crate::spelling::word_to_check(&d, word, opts) else { return true }; + let Some(w) = crate::spelling::word_to_check_in(&d, word, opts) else { return true }; let known = |w: &str| d.check(w) || crate::in_user_dictionary(&w.to_lowercase()); if w.contains('-') && !known(w) { return w.split('-').all(|p| p.chars().count() <= 1 || known(p)); diff --git a/crates/proof/src/spelling.rs b/crates/proof/src/spelling.rs index ab5e520e9..f4d65fa47 100644 --- a/crates/proof/src/spelling.rs +++ b/crates/proof/src/spelling.rs @@ -15,6 +15,12 @@ fn latin(c: char) -> bool { /// letter or has no letters, it has digits or is a short word in capitals (an acronym) and the /// options skip those, or it is in another script. pub(crate) fn word_to_check<'a>(d: &AffixDict, word: &'a str, opts: &Options) -> Option<&'a str> { + word_to_check_in(d, word, opts).filter(|w| w.chars().any(latin)) +} + +/// [`word_to_check`] in any script: the start of a Language pack language's check, whose +/// dictionary may be Cyrillic, Greek or any other script. +pub(crate) fn word_to_check_in<'a>(d: &AffixDict, word: &'a str, opts: &Options) -> Option<&'a str> { if d.is_empty() { return None; } @@ -27,7 +33,7 @@ pub(crate) fn word_to_check<'a>(d: &AffixDict, word: &'a str, opts: &Options) -> if opts.ignore_uppercase && w.chars().all(|c| !c.is_lowercase()) && n <= 6 { return None; } - w.chars().any(latin).then_some(w) + Some(w) } #[cfg(test)]