craft-proof english — Australian English also takes a word in SCOWL's en-GB list, and without SCOWL every en_AU source is read before any en_GB one (#496 review)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Martin BallandClaude Opus 5.5 committed 2026-11-02 23:23:09 +00:00
1 parent 54ac5fbc4c
commit 7b64b78cb0
2 files changed
+75 -8

No files matched your search

+55 -8
View File
@@ -91,13 +91,29 @@ fn names(lang: ProofLang) -> &'static [&'static str] {
match lang {
ProofLang::EnGb => &["en_GB", "en-GB"],
ProofLang::EnCa => &["en_CA", "en-CA"],
// Australian English's own, else a British one: the British-spelling dictionary it read
// before it had its own (a system en_GB on a build without SCOWL).
// Australian English's own, else a British one (a system en_GB on a build without SCOWL):
// [`exact_candidates`] reads every en_AU source before any en_GB one.
ProofLang::EnAu => &["en_AU", "en-AU", "en_GB", "en-GB"],
_ => &["en_US", "en-US"],
}
}
/// The Hunspell pairs of `lang` without SCOWL, from `find` (a folder search for some names,
/// [`crate::hunspell::candidates_exact`]): its own names first, and only then Australian English's
/// British fallback, so every en_AU source (a dictionaries folder, a pack, the system) comes before
/// any en_GB one.
fn exact_candidates(lang: ProofLang, find: impl Fn(&[&str]) -> Vec<(PathBuf, PathBuf)>) -> Vec<(PathBuf, PathBuf)> {
let mut out = find(own_names(lang));
if lang == ProofLang::EnAu {
for c in find(&names(lang)[own_names(lang).len()..]) {
if !out.iter().any(|(_, d)| *d == c.1) {
out.push(c);
}
}
}
out
}
/// The names [`names`] gives that outrank the dialect's own embedded SCOWL dictionary: its own pair
/// only, not Australian English's British fallback (a user's `en_GB` is for British text).
fn own_names(lang: ProofLang) -> &'static [&'static str] {
@@ -217,7 +233,7 @@ fn load(lang: ProofLang) -> (Loaded, Option<(PathBuf, String)>) {
let candidates = if crate::scowl::dict(lang).is_some() {
crate::hunspell::user_candidates(own_names(lang))
} else {
crate::hunspell::candidates_exact(names(lang))
exact_candidates(lang, crate::hunspell::candidates_exact)
};
if candidates.is_empty() {
log::info!("English spelling: no Hunspell {} dictionary to read first; using SCOWL or the built-in word list", key(lang));
@@ -300,10 +316,16 @@ fn pick(found: Loaded, lang: ProofLang) -> Speller {
if let Some((d, dic)) = found {
return Speller::Hunspell(d, dic);
}
match crate::scowl::dict(lang) {
Some(d) => Speller::Scowl(vec![d], crate::scowl::is_british(lang)),
None => Speller::List,
let Some(d) = crate::scowl::dict(lang) else { return Speller::List };
let mut ds = vec![d];
// SCOWL's en-AU list keeps one spelling per word (`enquiry`, not `inquiry`); the British list
// has the rest, which Australian English takes. Suggestions come from the first, en-AU.
if lang == ProofLang::EnAu
&& let Some(gb) = crate::scowl::dict(ProofLang::EnGb)
{
ds.push(gb);
}
Speller::Scowl(ds, crate::scowl::is_british(lang))
}
/// [`speller`] for English with no dialect ([`crate::is_correct`]): a SCOWL word of any English
@@ -631,11 +653,11 @@ mod tests {
let fallback = pick(None, lang);
if crate::scowl::dict(lang).is_some() {
assert!(
matches!(&fallback, Speller::Scowl(ds, _) if ds.len() == 1 && std::ptr::eq(ds[0], crate::scowl::dict(lang).unwrap())),
matches!(&fallback, Speller::Scowl(ds, _) if ds.len() == if lang == ProofLang::EnAu { 2 } else { 1 } && std::ptr::eq(ds[0], crate::scowl::dict(lang).unwrap())),
"{lang:?}"
);
assert!(fallback.knows("21st") && !fallback.knows("becuase"), "{lang:?}");
assert_eq!(fallback.knows("colour"), lang != ProofLang::En, "{lang:?}: its own dictionary only");
assert_eq!(fallback.knows("colour"), lang != ProofLang::En, "{lang:?}: its own dictionary (en-AU: and en-GB's)");
} else {
assert!(matches!(fallback, Speller::List), "{lang:?}");
}
@@ -671,6 +693,31 @@ mod tests {
let _ = std::fs::remove_dir_all(&dir);
}
/// Without SCOWL, every en_AU source comes before any en_GB one (a dictionaries folder's, an
/// installed pack's, the system's): a user's en_GB doesn't outrank a system en_AU.
#[test]
fn australian_english_reads_every_en_au_before_any_en_gb() {
let dir = std::env::temp_dir().join(format!("wc-proof-en-au-order-{}", std::process::id()));
for sub in ["user", "system"] {
std::fs::create_dir_all(dir.join(sub)).unwrap();
}
let write = |sub: &str, name: &str| {
std::fs::write(dir.join(sub).join(format!("{name}.aff")), "SET UTF-8\n").unwrap();
std::fs::write(dir.join(sub).join(format!("{name}.dic")), "1\nzzyzx\n").unwrap();
};
write("user", "en_GB");
write("system", "en_AU");
let (user, system) = (vec![dir.join("user")], vec![dir.join("system")]);
let find = |n: &[&str]| crate::hunspell::ordered(n, &user, &[], &system, &[]);
let stems = |v: Vec<(PathBuf, PathBuf)>| -> Vec<String> {
v.iter().filter_map(|(_, d)| d.file_stem().map(|s| s.to_string_lossy().into_owned())).collect()
};
assert_eq!(stems(exact_candidates(ProofLang::EnAu, find)), ["en_AU", "en_GB"]);
assert_eq!(stems(exact_candidates(ProofLang::EnGb, find)), ["en_GB"]);
assert_eq!(stems(exact_candidates(ProofLang::En, find)), Vec::<String>::new());
let _ = std::fs::remove_dir_all(&dir);
}
/// craft-libs#482 review: a load that started before a reload (the dictionaries folder
/// changed) serves its own check but isn't kept, so the next check reads the new folder; a
/// load after it is kept, and shared with the Englishes that read the same file.
+20
View File
@@ -2571,6 +2571,26 @@ mod tests {
assert!(check_spelling_in(text, gb).is_empty(), "{:?}", check_spelling_in(text, gb));
}
/// Review follow-up: SCOWL's en-AU list keeps one spelling per word, so Australian English also
/// takes a word the en-GB list has (`enquiry`, `guerrilla`, `liquorice`); American-only
/// spellings, wrong in both, stay wrong. Suggestions still come from the en-AU list.
#[cfg_attr(not(craft_proof_data), ignore = "needs the Language data (CRAFT_I18N_DIR)")]
#[test]
fn australian_english_also_takes_british_words() {
crate::hold_dictionary_tests();
let (au, gb) = (ProofLang::EnAu, ProofLang::EnGb);
for w in ["enquiry", "enquire", "guerrilla", "guerrillas", "euthanise", "liveable", "liquorice", "veranda", "burka", "Anthea"] {
assert!(is_correct_in(w, gb), "en-GB: {w}");
assert!(is_correct_in(w, au), "en-AU: {w}");
}
for w in ["analyze", "catalyze", "color", "center", "defense", "zzyzxq"] {
assert!(!is_correct_in(w, gb), "en-GB: {w}");
assert!(!is_correct_in(w, au), "en-AU: {w}");
}
assert!(is_correct_in("billabong", au) && !is_correct_in("billabong", gb), "en-AU still has its own");
assert!(suggest_in("billabon", 5, au).iter().any(|s| s == "billabong"), "{:?}", suggest_in("billabon", 5, au));
}
/// English spelling reads SCOWL 2026 through the affix engine: one dictionary per English,
/// its notice embedded, and its ordinals (`COMPOUNDRULE`) understood.
#[cfg_attr(not(craft_proof_data), ignore = "needs the Language data (CRAFT_I18N_DIR)")]