CP4.2: review — German and Bulgarian are spelled from their pack; pack languages are checked in any script

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2
This commit is contained in:
Martin BallandClaude Opus 5.5 committed 2026-11-01 22:34:32 +00:00
1 parent afbdede88e
commit 2495fad496
4 files changed
+62 -12

No files matched your search

+18 -5
View File
@@ -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<PackLocale> {
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<PackLocale> {
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),
}
}
}
+36 -5
View File
@@ -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<std::path::PathBuf> {
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<String> {
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<String> {
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]
+1 -1
View File
@@ -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));
+7 -1
View File
@@ -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)]