From e1f34a90d659a21ae6ecd6fcef882b8e1daaa38d Mon Sep 17 00:00:00 2001 From: witkowski01 Date: Wed, 14 Oct 2026 22:25:00 +0200 Subject: [PATCH] Proofing: a Dictionaries folder setting, and the dictionary each language found (#1063) (#1084) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit File › Options › Proofing gets a Dictionaries folder: a folder of Hunspell .aff/.dic files searched before the system locations, saved with the settings and applied at start-up. An app started from Finder or the Dock doesn't see WORDCRAFT_DICTIONARIES, so this is how it finds a dictionary kept in a folder of the user's own. Changing it reloads the Polish and Portuguese dictionaries and checks the text again. The pane lists the dictionary file each language found, or that none was found, so missing spelling marks are explained. Agents set it with file.options {"dictionariesFolder": path | "auto"}; a missing folder or a relative path is refused and changes nothing. Co-authored-by: Claude Opus 5.5 Co-authored-by: Brandon Thomas --- crates/engine/src/cmd/options.rs | 49 +++++++++++++++- crates/engine/src/session.rs | 5 ++ crates/engine/src/tests.rs | 52 +++++++++++++++-- crates/proof/src/hr/mod.rs | 7 +++ crates/proof/src/it/mod.rs | 7 +++ crates/proof/src/lib.rs | 63 +++++++++++++++++++- crates/proof/src/pl/mod.rs | 7 +++ crates/proof/src/pt/mod.rs | 7 +++ crates/proof/src/tr/mod.rs | 7 +++ crates/ui-egui/src/i18n/bg.tsv | 8 +++ crates/ui-egui/src/i18n/cs.tsv | 8 +++ crates/ui-egui/src/i18n/de.tsv | 8 +++ crates/ui-egui/src/i18n/es.tsv | 8 +++ crates/ui-egui/src/i18n/et.tsv | 8 +++ crates/ui-egui/src/i18n/fr.tsv | 8 +++ crates/ui-egui/src/i18n/hu.tsv | 8 +++ crates/ui-egui/src/i18n/it.tsv | 8 +++ crates/ui-egui/src/i18n/ja.tsv | 8 +++ crates/ui-egui/src/i18n/pl.tsv | 6 ++ crates/ui-egui/src/i18n/pt-br.tsv | 8 +++ crates/ui-egui/src/i18n/ru.tsv | 8 +++ crates/ui-egui/src/i18n/sr-latn.tsv | 8 +++ crates/ui-egui/src/i18n/sr.tsv | 8 +++ crates/ui-egui/src/i18n/tr.tsv | 8 +++ crates/ui-egui/src/i18n/uk.tsv | 8 +++ crates/ui-egui/src/i18n/vi.tsv | 8 +++ crates/ui-egui/src/i18n/zh-hans.tsv | 8 +++ crates/ui-egui/src/i18n/zh-hant.tsv | 8 +++ crates/ui-egui/src/lib.rs | 4 ++ crates/ui-egui/src/options.rs | 89 +++++++++++++++++++++++++++++ docs/localization-parity.md | 3 +- 31 files changed, 441 insertions(+), 9 deletions(-) diff --git a/crates/engine/src/cmd/options.rs b/crates/engine/src/cmd/options.rs index d9a96252e..a829f4c07 100644 --- a/crates/engine/src/cmd/options.rs +++ b/crates/engine/src/cmd/options.rs @@ -11,6 +11,8 @@ pub const PANES: [&str; 8] = ["general", "display", "proofing", "save", "languag /// Longest initials kept, in characters. pub const MAX_INITIALS_CHARS: usize = 9; +/// Longest dictionaries folder path taken, in characters. +const MAX_FOLDER_CHARS: usize = 4096; /// Longest word the custom dictionary takes, in characters. pub const MAX_WORD_CHARS: usize = 64; /// Most words one call may add or remove. @@ -20,7 +22,7 @@ pub fn specs() -> Vec { vec![ CommandSpec::new("file.options", "Options", "File", options) .params( - r#"{"pane"?: "general|display|proofing|save|language|accessibility|advanced|agents", "author"?: string, "initials"?: string, "units"?: "inches|centimeters|millimeters|points|picas|auto" (auto: the region's), "region"?: string (a locale tag like pl-PL, saved: the paper File › New starts on, the default units and the decimal separator; auto: the system's), "typingReplacesSelection"?: bool, "insertKeyOvertype"?: bool, "overtype"?: bool, "marks"?: {"tabs"?: bool, "spaces"?: bool, "paragraphs"?: bool, "hidden"?: bool}, "checkSpelling"?: bool, "markGrammar"?: bool, "ignoreUppercase"?: bool, "ignoreNumbers"?: bool, "ignoreInternet"?: bool, "addWords"?: [string], "removeWords"?: [string]} — with any setting it changes them and opens nothing; without, it opens File › Options (at `pane`). Returns the settings."#, + r#"{"pane"?: "general|display|proofing|save|language|accessibility|advanced|agents", "author"?: string, "initials"?: string, "units"?: "inches|centimeters|millimeters|points|picas|auto" (auto: the region's), "region"?: string (a locale tag like pl-PL, saved: the paper File › New starts on, the default units and the decimal separator; auto: the system's), "typingReplacesSelection"?: bool, "insertKeyOvertype"?: bool, "overtype"?: bool, "marks"?: {"tabs"?: bool, "spaces"?: bool, "paragraphs"?: bool, "hidden"?: bool}, "checkSpelling"?: bool, "markGrammar"?: bool, "ignoreUppercase"?: bool, "ignoreNumbers"?: bool, "ignoreInternet"?: bool, "dictionariesFolder"?: string (a folder of Hunspell .aff/.dic files searched first, saved; auto: the system locations only), "addWords"?: [string], "removeWords"?: [string]} — with any setting it changes them and opens nothing; without, it opens File › Options (at `pane`). Returns the settings."#, ) .pure(), CommandSpec::new("text.overtype", "Overtype", "File › Options › Advanced", |s, v| { @@ -113,6 +115,11 @@ fn options(s: &mut Session, v: &Value) -> CmdResult { } Some(_) => return Err(CmdError::Params("`region` must be a locale tag like pl-PL, or auto".into())), }; + let folder = match v.get("dictionariesFolder") { + None | Some(Value::Null) => None, + Some(Value::String(f)) => Some(dictionaries_folder(f)?), + Some(_) => return Err(CmdError::Params("`dictionariesFolder` must be a folder path, or auto".into())), + }; let mut flag_values = Vec::new(); for (k, f) in flags() { if let Some(b) = bool_param(v, k)? { @@ -139,6 +146,7 @@ fn options(s: &mut Session, v: &Value) -> CmdResult { || initials.is_some() || units.is_some() || region.is_some() + || folder.is_some() || !flag_values.is_empty() || !mark_values.is_empty() || check_spelling.is_some() @@ -156,6 +164,10 @@ fn options(s: &mut Session, v: &Value) -> CmdResult { if let Some(r) = region { s.prefs.region = r; } + if let Some(f) = folder { + use_dictionaries_folder(f.as_deref()); + s.prefs.dictionaries_folder = f; + } for (f, b) in flag_values { *f(&mut s.prefs) = b; } @@ -208,5 +220,40 @@ pub fn current(s: &Session) -> Value { "ignoreNumbers": pr.ignore_numbers, "ignoreInternet": pr.ignore_internet, "dictionary": wordcraft_proof::user_dictionary(), + "dictionariesFolder": pr.dictionaries_folder, + "dictionaries": wordcraft_proof::installed_dictionaries() + .into_iter() + .map(|(lang, dic)| json!({"language": lang.code(), "file": dic.map(|d| d.to_string_lossy().into_owned())})) + .collect::>(), }) } + +/// The `dictionariesFolder` setting: `auto` (or nothing) forgets it; otherwise an existing +/// folder, given as an absolute path or one starting with `~/`. +fn dictionaries_folder(f: &str) -> Result, CmdError> { + let f = f.trim(); + if f.is_empty() || f.eq_ignore_ascii_case("auto") { + return Ok(None); + } + if f.chars().count() > MAX_FOLDER_CHARS || f.chars().any(char::is_control) { + return Err(CmdError::Params("`dictionariesFolder` isn't a folder path".into())); + } + let home = std::env::var_os("HOME").filter(|h| !h.is_empty()).map(std::path::PathBuf::from); + let path = match (f.strip_prefix("~/"), home) { + (Some(rest), Some(h)) => h.join(rest), + _ => std::path::PathBuf::from(f), + }; + if !path.is_absolute() { + return Err(CmdError::Params(format!("`dictionariesFolder`: {f:?} isn't a full path"))); + } + if !path.is_dir() { + return Err(CmdError::Failed(format!("no folder at {}", path.display()))); + } + Ok(Some(path.to_string_lossy().into_owned())) +} + +/// Search `folder` for Hunspell dictionaries before the system locations, from now on (the saved +/// setting, applied when the app starts and when it changes). +pub fn use_dictionaries_folder(folder: Option<&str>) { + wordcraft_proof::set_dictionaries_folder(folder.map(std::path::PathBuf::from)); +} diff --git a/crates/engine/src/session.rs b/crates/engine/src/session.rs index c0ed2d808..5d6aacaae 100644 --- a/crates/engine/src/session.rs +++ b/crates/engine/src/session.rs @@ -508,6 +508,10 @@ pub struct Prefs { /// The region the user chose for new documents, units and the decimal separator (a locale /// tag like `pl-PL`, File › Options › Advanced › Region); `None` follows the system. pub region: Option, + /// A folder searched for Hunspell dictionaries before the system locations (File › Options › + /// Proofing, #1063): an app started from Finder or the Dock doesn't see + /// `WORDCRAFT_DICTIONARIES`. `None` searches the system locations only. + pub dictionaries_folder: Option, /// Formatting marks shown even while Show/Hide ¶ is off. pub marks: wordcraft_layout::display::FormattingMarks, /// Mark grammar errors as you type (with Check spelling as you type on). @@ -532,6 +536,7 @@ impl Default for Prefs { overtype: false, units: Default::default(), region: None, + dictionaries_folder: None, marks: Default::default(), mark_grammar: true, ignore_uppercase: true, diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 328a4a349..362811c72 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -4736,17 +4736,57 @@ fn note_options_per_section_and_convert_notes() { /// A tiny synthetic Polish Hunspell dictionary (written for these tests). Real dictionaries are /// installed on the system, never bundled; every Polish test installs this same one. fn polish_test_dictionary() { - let aff = "SET UTF-8\nTRY aioeznrwcyszżtkpdmłjlubąęgh\nMAP 2\nMAP oó\nMAP zż\nREP 2\nREP ż rz\nREP rz ż\n"; - let dic = "12\npłynie\nrzeka\na\nto\nwiem\nże\ntak\nw\nogóle\nzażółć\ngęślą\njaźń\n"; - wordcraft_proof::pl::install_dictionary(aff, dic).unwrap(); + wordcraft_proof::pl::install_dictionary(POLISH_TEST_AFF, POLISH_TEST_DIC).unwrap(); } +const POLISH_TEST_AFF: &str = "SET UTF-8\nTRY aioeznrwcyszżtkpdmłjlubąęgh\nMAP 2\nMAP oó\nMAP zż\nREP 2\nREP ż rz\nREP rz ż\n"; +const POLISH_TEST_DIC: &str = "12\npłynie\nrzeka\na\nto\nwiem\nże\ntak\nw\nogóle\nzażółć\ngęślą\njaźń\n"; + /// A tiny synthetic Portuguese Hunspell dictionary (written for these tests). Real dictionaries /// are installed on the system, never bundled; every Portuguese test installs this same one. fn portuguese_test_dictionary() { - let aff = "SET UTF-8\nFLAG UTF-8\nFORBIDDENWORD F\nTRY aeiousrntmdlcpvgqbzfhjxçãõáéíóúâêô\nMAP 2\nMAP aáã\nMAP eé\nREP 1\nREP seje seja\n"; - let dic = "11\nsaudade\ncoração\ne\nele\ndisse\nnão\né\nque\nseja\nfrequencia/F\nmuito\n"; - wordcraft_proof::pt::install_dictionary(aff, dic).unwrap(); + wordcraft_proof::pt::install_dictionary(PORTUGUESE_TEST_AFF, PORTUGUESE_TEST_DIC).unwrap(); +} + +const PORTUGUESE_TEST_AFF: &str = + "SET UTF-8\nFLAG UTF-8\nFORBIDDENWORD F\nTRY aeiousrntmdlcpvgqbzfhjxçãõáéíóúâêô\nMAP 2\nMAP aáã\nMAP eé\nREP 1\nREP seje seja\n"; +const PORTUGUESE_TEST_DIC: &str = "11\nsaudade\ncoração\ne\nele\ndisse\nnão\né\nque\nseja\nfrequencia/F\nmuito\n"; + +/// #1063: File › Options › Proofing › Dictionaries folder. A folder of Hunspell files is saved, +/// searched first and reported per language; a missing folder or a relative path is refused and +/// changes nothing. The folder holds the same test dictionaries the other tests install, so tests +/// running meanwhile see the same words; it stays set for the rest of the run for that reason. +#[test] +fn dictionaries_folder_option_is_saved_searched_and_reported() { + let dir = std::env::temp_dir().join(format!("wc-engine-dictionaries-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + for (name, aff, dic) in [("pl_PL", POLISH_TEST_AFF, POLISH_TEST_DIC), ("pt_BR", PORTUGUESE_TEST_AFF, PORTUGUESE_TEST_DIC)] { + std::fs::write(dir.join(format!("{name}.aff")), aff).unwrap(); + std::fs::write(dir.join(format!("{name}.dic")), dic).unwrap(); + } + let mut s = s(); + let missing = dir.join("no-such-folder"); + assert!(s.run("file.options", &json!({"dictionariesFolder": missing.to_string_lossy()})).is_err()); + assert!(s.run("file.options", &json!({"dictionariesFolder": "relative/dictionaries"})).is_err()); + assert!(s.run("file.options", &json!({"dictionariesFolder": 7})).is_err()); + assert_eq!(s.prefs.dictionaries_folder, None, "a refused folder changes nothing"); + let r = run(&mut s, "file.options", json!({"dictionariesFolder": dir.to_string_lossy()})); + assert_eq!(s.prefs.dictionaries_folder.as_deref(), Some(dir.to_string_lossy().as_ref())); + assert_eq!(r["dictionariesFolder"], json!(dir.to_string_lossy())); + let dicts = r["dictionaries"].as_array().unwrap(); + let file = |lang: &str| dicts.iter().find(|d| d["language"] == lang).map(|d| d["file"].clone()); + assert_eq!(file("pl"), Some(json!(dir.join("pl_PL.dic").to_string_lossy()))); + assert_eq!(file("pt"), Some(json!(dir.join("pt_BR.dic").to_string_lossy()))); + // Polish text is checked against the folder's dictionary. + run(&mut s, "document.setText", json!({"text": "rzeka płynie żeka"})); + run(&mut s, "select.all", json!({})); + run(&mut s, "review.language", json!({"lang": "pl-PL"})); + let issues = run(&mut s, "review.issues", json!({})); + let words: Vec<&str> = issues.as_array().unwrap().iter().filter(|i| i["kind"] == "spelling").filter_map(|i| i["text"].as_str()).collect(); + assert_eq!(words, ["żeka"], "{issues}"); + // Saved with the settings. + let prefs = serde_json::to_value(&s.prefs).unwrap(); + assert_eq!(prefs["dictionariesFolder"], json!(dir.to_string_lossy())); } /// Review › Language and the proofing commands work in Portuguese end to end: the language makes diff --git a/crates/proof/src/hr/mod.rs b/crates/proof/src/hr/mod.rs index 05b5a88b2..5bbb55bc6 100644 --- a/crates/proof/src/hr/mod.rs +++ b/crates/proof/src/hr/mod.rs @@ -60,6 +60,13 @@ pub fn reload_dictionary() { *DICT.write().unwrap_or_else(|e| e.into_inner()) = None; } +/// The `.dic` file of the Hunspell `hr_HR` dictionary found in the dictionaries folder or the +/// system locations now (File › Options › Proofing shows it, #1063); `None` when there is none. +/// A dictionary given with [`install_dictionary`] has no file and isn't reported. +pub fn installed_path() -> Option { + crate::hunspell::find(NAMES).map(|(_, dic)| dic) +} + /// Is a Croatian dictionary available (spelling is checked)? pub fn has_dictionary() -> bool { !dictionary().is_empty() diff --git a/crates/proof/src/it/mod.rs b/crates/proof/src/it/mod.rs index 41f63e489..7e1635599 100644 --- a/crates/proof/src/it/mod.rs +++ b/crates/proof/src/it/mod.rs @@ -66,6 +66,13 @@ pub fn reload_dictionary() { *DICT.write().unwrap_or_else(|e| e.into_inner()) = None; } +/// The `.dic` file of the Hunspell `it_IT` dictionary found in the dictionaries folder or the +/// system locations now (File › Options › Proofing shows it, #1063); `None` when there is none. +/// A dictionary given with [`install_dictionary`] has no file and isn't reported. +pub fn installed_path() -> Option { + crate::hunspell::find(NAMES).map(|(_, dic)| dic) +} + /// Is an Italian dictionary available (spelling is checked)? pub fn has_dictionary() -> bool { !dictionary().is_empty() diff --git a/crates/proof/src/lib.rs b/crates/proof/src/lib.rs index d644b62f9..d285c5ba4 100644 --- a/crates/proof/src/lib.rs +++ b/crates/proof/src/lib.rs @@ -125,7 +125,33 @@ pub fn ignore_word(w: &str) { } } -/// A number that changes whenever a word is added to or removed from the user dictionary. +/// Search `dir` for Hunspell dictionaries before the system locations (File › Options › +/// Proofing › Dictionaries folder, #1063); `None` clears it. The dictionaries load again from +/// the new search path, and text is checked again. +pub fn set_dictionaries_folder(dir: Option) { + hunspell::set_dictionary_dir(dir); + pl::reload_dictionary(); + pt::reload_dictionary(); + it::reload_dictionary(); + hr::reload_dictionary(); + tr::reload_dictionary(); + DICTIONARY_GENERATION.fetch_add(1, Ordering::Relaxed); +} + +/// Each language that reads an installed Hunspell dictionary, with the `.dic` file found for it +/// now (`None`: not installed, so the language has no spelling marks). +pub fn installed_dictionaries() -> Vec<(ProofLang, Option)> { + vec![ + (ProofLang::Pl, pl::installed_path()), + (ProofLang::Pt, pt::installed_path()), + (ProofLang::It, it::installed_path()), + (ProofLang::Hr, hr::installed_path()), + (ProofLang::Tr, tr::installed_path()), + ] +} + +/// A number that changes whenever a word is added to or removed from the user dictionary, or the +/// dictionaries folder changes. pub fn dictionary_generation() -> u64 { DICTIONARY_GENERATION.load(Ordering::Relaxed) } @@ -748,6 +774,41 @@ fn starts_consonant_sound(w: &str) -> bool { mod tests { use super::*; + /// #1063: the dictionaries folder is searched before the system locations; the dictionaries + /// in it are reported and used, and cached checks are dropped. The folder holds the same test + /// dictionaries the other tests install, so tests running meanwhile see the same words; it + /// stays set for the rest of the run for that reason. + #[test] + fn dictionaries_folder_is_searched_first() { + let dir = std::env::temp_dir().join(format!("wc-proof-dictionaries-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + for (name, aff, dic) in [ + ("pl_PL", pl::test_dict::AFF, pl::test_dict::DIC), + ("pt_BR", pt::test_dict::AFF, pt::test_dict::DIC), + ("it_IT", it::test_dict::AFF, it::test_dict::DIC), + ("hr_HR", hr::test_dict::AFF, hr::test_dict::DIC), + ] { + std::fs::write(dir.join(format!("{name}.aff")), aff).unwrap(); + std::fs::write(dir.join(format!("{name}.dic")), dic).unwrap(); + } + let before = dictionary_generation(); + set_dictionaries_folder(Some(dir.clone())); + assert!(dictionary_generation() > before, "cached checks are dropped"); + // Turkish has no test dictionary to put in the folder; it's listed after the others. + let found = installed_dictionaries(); + assert_eq!(found.last().map(|(lang, _)| *lang), Some(ProofLang::Tr)); + assert_eq!( + found[..4], + [ + (ProofLang::Pl, Some(dir.join("pl_PL.dic"))), + (ProofLang::Pt, Some(dir.join("pt_BR.dic"))), + (ProofLang::It, Some(dir.join("it_IT.dic"))), + (ProofLang::Hr, Some(dir.join("hr_HR.dic"))), + ] + ); + assert!(is_correct_in("chrząszcz", ProofLang::Pl) && !is_correct_in("chrzonszcz", ProofLang::Pl)); + } + #[test] fn dictionary_loads() { assert!(dict::Dictionary::en_us().len() > 100_000); diff --git a/crates/proof/src/pl/mod.rs b/crates/proof/src/pl/mod.rs index 74d8b8d2f..227253408 100644 --- a/crates/proof/src/pl/mod.rs +++ b/crates/proof/src/pl/mod.rs @@ -67,6 +67,13 @@ pub fn reload_dictionary() { *DICT.write().unwrap_or_else(|e| e.into_inner()) = None; } +/// The `.dic` file of the installed Polish dictionary, searching the dictionaries folder and +/// the system locations now (File › Options › Proofing shows it, #1063); `None` when there is +/// none. A dictionary given with [`install_dictionary`] has no file and isn't reported. +pub fn installed_path() -> Option { + crate::hunspell::find(NAMES).map(|(_, dic)| dic) +} + /// Is a Polish dictionary available (spelling is checked)? pub fn has_dictionary() -> bool { !dictionary().is_empty() diff --git a/crates/proof/src/pt/mod.rs b/crates/proof/src/pt/mod.rs index be16c74b8..d175f63ce 100644 --- a/crates/proof/src/pt/mod.rs +++ b/crates/proof/src/pt/mod.rs @@ -66,6 +66,13 @@ pub fn reload_dictionary() { *DICT.write().unwrap_or_else(|e| e.into_inner()) = None; } +/// The `.dic` file of the installed Portuguese dictionary, searching the dictionaries folder and +/// the system locations now (File › Options › Proofing shows it, #1063); `None` when there is +/// none. A dictionary given with [`install_dictionary`] has no file and isn't reported. +pub fn installed_path() -> Option { + crate::hunspell::find(NAMES).map(|(_, dic)| dic) +} + /// Is a Portuguese dictionary available (spelling is checked)? pub fn has_dictionary() -> bool { !dictionary().is_empty() diff --git a/crates/proof/src/tr/mod.rs b/crates/proof/src/tr/mod.rs index e0b6fab9c..2b18158ab 100644 --- a/crates/proof/src/tr/mod.rs +++ b/crates/proof/src/tr/mod.rs @@ -65,6 +65,13 @@ pub fn reload_dictionary() { *DICT.write().unwrap_or_else(|e| e.into_inner()) = None; } +/// The `.dic` file of the Hunspell `tr_TR` dictionary found in the dictionaries folder or the +/// system locations now (File › Options › Proofing shows it, #1063); `None` when there is none. +/// A dictionary given with [`install_dictionary`] has no file and isn't reported. +pub fn installed_path() -> Option { + crate::hunspell::find(NAMES).map(|(_, dic)| dic) +} + /// Is a Turkish dictionary available? pub fn has_dictionary() -> bool { !dictionary().is_empty() diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index f69f15126..105207382 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -2517,3 +2517,11 @@ Heading text Текст на заглавието Insert reference to: Вмъкване на препратка към: Reference type: Тип препратка: + +# Dictionaries folder (#1063) + Dictionaries Речници + Dictionaries folder: Папка с речници: + Not set: the system's usual places Не е зададена: обичайните места в системата + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Файловете .aff и .dic на Hunspell от тази папка (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) се използват преди системните. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: не е намерен речник diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index c674cf156..fb0fe633f 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2521,3 +2521,11 @@ preview Title Ukázka Heading text Text nadpisu Insert reference to: Vložit odkaz na: Reference type: Typ odkazu: + +# Dictionaries folder (#1063) + Dictionaries Slovníky + Dictionaries folder: Složka se slovníky: + Not set: the system's usual places Nenastaveno: obvyklá umístění v systému + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Soubory Hunspell .aff a .dic z této složky (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) mají přednost před systémovými. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: slovník nebyl nalezen diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 16aa52473..f59d53bd3 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2129,3 +2129,11 @@ Suggested Vorschläge Set Proofing Language… Sprache für die Korrekturhilfen festlegen… Language Preferences… Spracheinstellungen… + +# Dictionaries folder (#1063) + Dictionaries Wörterbücher + Dictionaries folder: Wörterbuchordner: + Not set: the system's usual places Nicht festgelegt: die üblichen Orte des Systems + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Hunspell-Dateien (.aff und .dic) in diesem Ordner (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) haben Vorrang vor denen des Systems. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: kein Wörterbuch gefunden diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index ff8c55bb5..4d91b4c42 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -2496,3 +2496,11 @@ preview Title Título Suggested Sugerencias Set Proofing Language… Establecer idioma de revisión… Language Preferences… Preferencias de idioma… + +# Dictionaries folder (#1063) + Dictionaries Diccionarios + Dictionaries folder: Carpeta de diccionarios: + Not set: the system's usual places Sin definir: las ubicaciones habituales del sistema + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Los archivos .aff y .dic de Hunspell de esta carpeta (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) se usan antes que los del sistema. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: no se encontró ningún diccionario diff --git a/crates/ui-egui/src/i18n/et.tsv b/crates/ui-egui/src/i18n/et.tsv index b62cfe6a5..c897c8a22 100644 --- a/crates/ui-egui/src/i18n/et.tsv +++ b/crates/ui-egui/src/i18n/et.tsv @@ -2164,3 +2164,11 @@ Suggested Soovitused Set Proofing Language… Määra õigekeelsuskontrolli keel… Language Preferences… Keele-eelistused… + +# Dictionaries folder (#1063) + Dictionaries Sõnastikud + Dictionaries folder: Sõnastike kaust: + Not set: the system's usual places Määramata: süsteemi tavapärased asukohad + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Selle kausta Hunspelli .aff- ja .dic-failid (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) on eelistatud süsteemi omadele. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: sõnastikku ei leitud diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index c7ee4e543..2802433da 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2138,3 +2138,11 @@ Suggested Suggestions Set Proofing Language… Définir la langue de vérification… Language Preferences… Préférences linguistiques… + +# Dictionaries folder (#1063) + Dictionaries Dictionnaires + Dictionaries folder: Dossier des dictionnaires : + Not set: the system's usual places Non défini : emplacements habituels du système + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Les fichiers Hunspell .aff et .dic de ce dossier (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) sont utilisés avant ceux du système. + {language}: {file} {language} : {file} + {language}: no dictionary found {language} : aucun dictionnaire trouvé diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 17406e917..32f06d9a6 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2526,3 +2526,11 @@ Heading text Címsor szövege Insert reference to: Hivatkozás beszúrása erre: Reference type: Hivatkozás típusa: + +# Dictionaries folder (#1063) + Dictionaries Szótárak + Dictionaries folder: Szótárak mappája: + Not set: the system's usual places Nincs beállítva: a rendszer szokásos helyei + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Az ebben a mappában lévő Hunspell .aff és .dic fájlok (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) elsőbbséget élveznek a rendszer fájljaival szemben. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: nem található szótár diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index a53c7ebef..1edcdbcd7 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2512,3 +2512,11 @@ preview Title Esempio Heading text Testo del titolo Insert reference to: Inserisci riferimento a: Reference type: Tipo di riferimento: + +# Dictionaries folder (#1063) + Dictionaries Dizionari + Dictionaries folder: Cartella dei dizionari: + Not set: the system's usual places Non impostata: i percorsi abituali del sistema + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. I file Hunspell .aff e .dic in questa cartella (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) hanno la precedenza su quelli di sistema. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: nessun dizionario trovato diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 378a05b79..4c120c1f7 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -2130,3 +2130,11 @@ Suggested おすすめ Set Proofing Language… 校正言語の設定… Language Preferences… 言語の設定… + +# Dictionaries folder (#1063) + Dictionaries 辞書 + Dictionaries folder: 辞書フォルダー: + Not set: the system's usual places 未設定: システムの標準の場所 + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. このフォルダーの Hunspell .aff と .dic ファイル (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) は、システムのものより優先して使われます。 + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: 辞書が見つかりません diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 2fb5ec15d..9661adf1b 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2521,3 +2521,9 @@ preview Title Tytuł Heading text Tekst nagłówka Insert reference to: Wstaw odwołanie do: Reference type: Typ odwołania: + Dictionaries Słowniki + Dictionaries folder: Folder słowników: + Not set: the system's usual places Nie ustawiono: standardowe miejsca w systemie + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Pliki Hunspell .aff i .dic z tego folderu (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) mają pierwszeństwo przed systemowymi. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: nie znaleziono słownika diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 59f2905f2..349fb8bd3 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -2142,3 +2142,11 @@ Suggested Sugestões Set Proofing Language… Definir idioma de revisão… Language Preferences… Preferências de idioma… + +# Dictionaries folder (#1063) + Dictionaries Dicionários + Dictionaries folder: Pasta de dicionários: + Not set: the system's usual places Não definida: os locais habituais do sistema + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Os arquivos .aff e .dic do Hunspell nesta pasta (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) são usados antes dos do sistema. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: nenhum dicionário encontrado diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 85c888ff4..1b7bfe6e4 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2527,3 +2527,11 @@ preview Title Пример Heading text Текст заголовка Insert reference to: Вставить ссылку на: Reference type: Тип ссылки: + +# Dictionaries folder (#1063) + Dictionaries Словари + Dictionaries folder: Папка словарей: + Not set: the system's usual places Не задана: стандартные расположения в системе + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Файлы Hunspell .aff и .dic из этой папки (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) используются раньше системных. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: словарь не найден diff --git a/crates/ui-egui/src/i18n/sr-latn.tsv b/crates/ui-egui/src/i18n/sr-latn.tsv index 38177fe9f..eb49545c7 100644 --- a/crates/ui-egui/src/i18n/sr-latn.tsv +++ b/crates/ui-egui/src/i18n/sr-latn.tsv @@ -2140,3 +2140,11 @@ Suggested Predlozi Set Proofing Language… Postavi jezik provere… Language Preferences… Jezičke postavke… + +# Dictionaries folder (#1063) + Dictionaries Rečnici + Dictionaries folder: Fascikla rečnika: + Not set: the system's usual places Nije podešeno: uobičajene lokacije u sistemu + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Hunspell .aff i .dic datoteke iz ove fascikle (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) koriste se pre sistemskih. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: rečnik nije pronađen diff --git a/crates/ui-egui/src/i18n/sr.tsv b/crates/ui-egui/src/i18n/sr.tsv index e11e2867b..8a5137902 100644 --- a/crates/ui-egui/src/i18n/sr.tsv +++ b/crates/ui-egui/src/i18n/sr.tsv @@ -2144,3 +2144,11 @@ Suggested Предлози Set Proofing Language… Постави језик провере… Language Preferences… Језичке поставке… + +# Dictionaries folder (#1063) + Dictionaries Речници + Dictionaries folder: Фасцикла речника: + Not set: the system's usual places Није подешено: уобичајене локације у систему + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Hunspell .aff и .dic датотеке из ове фасцикле (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) користе се пре системских. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: речник није пронађен diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index c2206d50c..a00f2e996 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2526,3 +2526,11 @@ preview Title Örnek Heading text Başlık metni Insert reference to: Başvuru ekle: Reference type: Başvuru türü: + +# Dictionaries folder (#1063) + Dictionaries Sözlükler + Dictionaries folder: Sözlük klasörü: + Not set: the system's usual places Ayarlanmadı: sistemin olağan konumları + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Bu klasördeki Hunspell .aff ve .dic dosyaları (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) sistemdekilerden önce kullanılır. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: sözlük bulunamadı diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index adde1b827..7abb7d00c 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -2142,3 +2142,11 @@ Suggested Пропоновані Set Proofing Language… Вибрати мову перевірки правопису… Language Preferences… Мовні параметри… + +# Dictionaries folder (#1063) + Dictionaries Словники + Dictionaries folder: Папка словників: + Not set: the system's usual places Не задано: стандартні розташування в системі + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Файли Hunspell .aff і .dic із цієї папки (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) використовуються раніше за системні. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: словник не знайдено diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 3ce84722c..b38a4091b 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2548,3 +2548,11 @@ # Ctrl+drag copies (#1046) Copy to Sao chép tới + +# Dictionaries folder (#1063) + Dictionaries Từ điển + Dictionaries folder: Thư mục từ điển: + Not set: the system's usual places Chưa đặt: các vị trí thông thường của hệ thống + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. Các tệp Hunspell .aff và .dic trong thư mục này (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) được dùng trước các tệp của hệ thống. + {language}: {file} {language}: {file} + {language}: no dictionary found {language}: không tìm thấy từ điển diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 39ce3c257..5717aa1b6 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2142,3 +2142,11 @@ Suggested 建议 Set Proofing Language… 设置校对语言… Language Preferences… 语言首选项… + +# Dictionaries folder (#1063) + Dictionaries 词典 + Dictionaries folder: 词典文件夹: + Not set: the system's usual places 未设置:系统的常用位置 + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. 此文件夹中的 Hunspell .aff 和 .dic 文件 (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) 优先于系统中的文件。 + {language}: {file} {language}:{file} + {language}: no dictionary found {language}:未找到词典 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 50026c454..f3aa21b86 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2131,3 +2131,11 @@ Suggested 建議 Set Proofing Language… 設定校訂語言… Language Preferences… 語言喜好設定… + +# Dictionaries folder (#1063) + Dictionaries 字典 + Dictionaries folder: 字典資料夾: + Not set: the system's usual places 未設定:系統的一般位置 + Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's. 此資料夾中的 Hunspell .aff 與 .dic 檔案 (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) 優先於系統中的檔案。 + {language}: {file} {language}:{file} + {language}: no dictionary found {language}:找不到字典 diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 3f63d2710..6f030d301 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -692,6 +692,10 @@ impl WordApp { } } self.session.prefs = std::mem::take(&mut self.ui.editing); + // The saved dictionaries folder (#1063) is searched from the start. + if let Some(f) = self.session.prefs.dictionaries_folder.as_deref() { + wordcraft_engine::cmd::options::use_dictionaries_folder(Some(f)); + } self.session.read_aloud.set_rate(self.ui.read_aloud_rate); self.session.read_aloud.skip_citations = self.ui.read_aloud_skip_citations; self.session.view.dark_mode = self.ui.dark_page; diff --git a/crates/ui-egui/src/options.rs b/crates/ui-egui/src/options.rs index 5f97efc29..635757eca 100644 --- a/crates/ui-egui/src/options.rs +++ b/crates/ui-egui/src/options.rs @@ -266,6 +266,76 @@ fn proofing(app: &mut WordApp, ui: &mut Ui) { ui.add_space(8.0); section(ui, "Custom dictionary"); custom_dictionary(app, ui); + ui.add_space(8.0); + section(ui, "Dictionaries"); + dictionaries(app, ui); +} + +/// Where the spelling dictionaries come from (#1063): a folder searched before the system's +/// usual places, saved with the settings, and the dictionary each language found. An app started +/// from Finder or the Dock doesn't see `WORDCRAFT_DICTIONARIES`, so this is how it finds a +/// dictionary kept in a folder of the user's own. +fn dictionaries(app: &mut WordApp, ui: &mut Ui) { + let saved = app.session.prefs.dictionaries_folder.clone().unwrap_or_default(); + let id = egui::Id::new("options_dictionaries_folder"); + let mut folder = ui.data(|d| d.get_temp::(id)).unwrap_or_else(|| saved.clone()); + ui.label(tl!("Dictionaries folder:")); + ui.horizontal(|ui| { + let r = ui.add(egui::TextEdit::singleline(&mut folder).desired_width(320.0).hint_text(tl!("Not set: the system's usual places"))); + let enter = r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); + let changed = folder.trim() != saved; + if ui.add_enabled(changed, egui::Button::new(tl!("Apply"))).clicked() || (enter && changed) { + let value = if folder.trim().is_empty() { "auto" } else { folder.trim() }; + match app.run("file.options", json!({"dictionariesFolder": value})) { + // Shown as saved (`~/` expanded). + Ok(_) => folder = app.session.prefs.dictionaries_folder.clone().unwrap_or_default(), + Err(e) => app.status(e), + } + } + if ui.add_enabled(!saved.is_empty(), egui::Button::new(tl!("Clear"))).clicked() + && app.run("file.options", json!({"dictionariesFolder": "auto"})).is_ok() + { + folder.clear(); + } + }); + ui.data_mut(|d| d.insert_temp(id, folder)); + note(ui, "Hunspell .aff and .dic files here (pl_PL, pt_BR, it_IT, hr_HR, tr_TR) are used before the system's."); + // The dictionary each language found, looked up again after the folder changes. + let found_id = egui::Id::new("options_dictionaries_found"); + let generation = wordcraft_engine::proof::dictionary_generation(); + let cached = ui.data(|d| d.get_temp::<(u64, Vec<(String, Option)>)>(found_id)).filter(|(g, _)| *g == generation); + let found = match cached { + Some((_, f)) => f, + None => { + let f: Vec<(String, Option)> = wordcraft_engine::proof::installed_dictionaries() + .into_iter() + .map(|(lang, dic)| (language_name(lang).to_string(), dic.map(|d| d.to_string_lossy().into_owned()))) + .collect(); + ui.data_mut(|d| d.insert_temp(found_id, (generation, f.clone()))); + f + } + }; + for (language, file) in found { + let line = match file { + Some(f) => crate::i18n::fmt(tl!("{language}: {file}"), &[("language", &language), ("file", &f)]), + None => crate::i18n::fmt(tl!("{language}: no dictionary found"), &[("language", &language)]), + }; + ui.add(egui::Label::new(RichText::new(line).small()).truncate()); + } +} + +/// The interface name of a language with a Hunspell dictionary. +fn language_name(lang: wordcraft_engine::proof::ProofLang) -> &'static str { + use wordcraft_engine::proof::ProofLang; + match lang { + ProofLang::Pl => tl!("Polish"), + ProofLang::Pt => tl!("Portuguese (Brazil)"), + ProofLang::It => tl!("Italian"), + ProofLang::Hr => tl!("Croatian"), + ProofLang::Tr => tl!("Turkish"), + // English uses its bundled word list; Bulgarian has no dictionary yet. + ProofLang::En | ProofLang::Bg | ProofLang::Other => "", + } } /// The custom dictionary: the words Add to Dictionary kept, each with a Remove button, and a @@ -686,6 +756,22 @@ mod tests { assert!(UiState::from_json(b"[1, 2]").is_none() && UiState::from_json(b"not json").is_none()); } + /// The dictionaries folder (#1063) is saved with the settings and searched again after a + /// restart, before any document is checked. + #[test] + fn the_dictionaries_folder_survives_a_restart() { + let dir = std::env::temp_dir().join(format!("wc-ui-dictionaries-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let mut a = app(); + a.run("file.options", json!({"dictionariesFolder": dir.to_string_lossy()})).unwrap(); + let saved = serde_json::to_vec(&a.prefs()).unwrap(); + wordcraft_engine::proof::hunspell::set_dictionary_dir(None); + let mut b = app(); + b.apply_prefs(UiState::from_json(&saved).unwrap()); + assert_eq!(b.session.prefs.dictionaries_folder.as_deref(), Some(dir.to_string_lossy().as_ref())); + assert_eq!(wordcraft_engine::proof::hunspell::search_dirs(None).first(), Some(&dir), "searched first"); + } + /// Settings saved before #838 kept the unit under `editing.units` (default inches): a unit /// the user chose carries over to `measurementUnits`; inches, junk or a newer value don't. #[test] @@ -732,6 +818,9 @@ mod tests { h.get_by_label("Ignore words in capitals").click(); h.run_steps(2); assert!(!h.state().session.prefs.ignore_uppercase); + // #1063: the dictionaries folder and the dictionary each language found. + assert!(h.query_by_label("Dictionaries folder:").is_some()); + assert!(h.query_by_label_contains("Polish: ").is_some(), "the Polish dictionary's line"); h.get_by_label("Advanced").click(); h.run_steps(4); h.get_by_label("Typing replaces selected text").click(); diff --git a/docs/localization-parity.md b/docs/localization-parity.md index 71174c779..c3d907622 100644 --- a/docs/localization-parity.md +++ b/docs/localization-parity.md @@ -99,7 +99,7 @@ other than envelopes (Letter, Legal, A4, B5 (JIS)) stay as they are (#1128). | Language | Spelling | Suggestions | Grammar | Hyphenation | |---|---|---|---|---| | English (`en`, any region; text with no language) | Moby word list (public domain, ~160k words) with inflection rules | edit distance | repeated word, `a`/`an`, sentence capital, space before punctuation, double space | Moby dictionary, our Liang patterns, heuristic | -| Polish (`pl`) | The Hunspell `pl_PL` dictionary installed on the system (OS package, Homebrew, LibreOffice, or the `WORDCRAFT_DICTIONARIES` folder; not bundled, so no Polish spelling marks without it), read at runtime by a Hunspell-style affix engine (`crates/proof/src/affix.rs`); names capitalized, compounds part by part, `Kennedy'ego` | missing diacritics first (`zolw` → `żółw`), the dictionary's confusion table (`żeka` → `rzeka`, `chuśtawka` → `huśtawka`), missing space (`napewno` → `na pewno`), one typo, then a stem scan within two typos | Polish messages: repeated word, space before punctuation, double space, sentence capital (knows `np.`, `r.`, initials, ordinals, dialogue dashes), missing comma before `że`/`który`/`ponieważ`/`gdyż`/`aby`/`żeby`/`jeśli`/`jeżeli`/`gdyby`/`lecz` (not after `mimo`, `chyba`, `tak`, prepositions…), `we`/`ze` (`we wtorek`, `ze szkoły`), doubled punctuation | TeX Polish patterns (Kołodziejska, Jackowski, Ryćko; MIT option) with their 20 exception words; a Polish syllable heuristic for letters outside the alphabet | +| Polish (`pl`) | The Hunspell `pl_PL` dictionary installed on the system (OS package, Homebrew, LibreOffice, the folder set in File › Options › Proofing › Dictionaries folder, or `WORDCRAFT_DICTIONARIES`; not bundled, so no Polish spelling marks without it; the same pane shows which file each language found), read at runtime by a Hunspell-style affix engine (`crates/proof/src/affix.rs`); names capitalized, compounds part by part, `Kennedy'ego` | missing diacritics first (`zolw` → `żółw`), the dictionary's confusion table (`żeka` → `rzeka`, `chuśtawka` → `huśtawka`), missing space (`napewno` → `na pewno`), one typo, then a stem scan within two typos | Polish messages: repeated word, space before punctuation, double space, sentence capital (knows `np.`, `r.`, initials, ordinals, dialogue dashes), missing comma before `że`/`który`/`ponieważ`/`gdyż`/`aby`/`żeby`/`jeśli`/`jeżeli`/`gdyby`/`lecz` (not after `mimo`, `chyba`, `tak`, prepositions…), `we`/`ze` (`we wtorek`, `ze szkoły`), doubled punctuation | TeX Polish patterns (Kołodziejska, Jackowski, Ryćko; MIT option) with their 20 exception words; a Polish syllable heuristic for letters outside the alphabet | | Portuguese (`pt`, any region) | The Hunspell `pt_BR` dictionary installed on the system — VERO and its derivatives (same places as Polish; not bundled, LGPL/MPL), read at runtime by the same affix engine, which reads its `FLAG UTF-8`, 106 flags, continuation classes and `FORBIDDENWORD` spellings; names capitalized, compounds part by part (`guarda-roupa`, `ex-marido`) | missing accents first (`coraçao` → `coração`, `saude` → `saúde`), the dictionary's confusion table (`seje` → `seja`, `excessão` → `exceção`), missing space (`nãoé` → `não é`), one typo, then an accent-folding stem scan | Portuguese messages: repeated word (the emphatic doubles are allowed), space before punctuation, double space, sentence capital (knows `etc.`, `pág.`, `Dr.`, initials, ordinals, dialogue dashes), doubled punctuation; no comma rule — `que` opens a clause only sometimes | TeX Portuguese patterns (Rezende, Almeida, Araujo, Benevides; BSD 3-clause) with their 2 exception words | | Italian (`it`, any region) | The Hunspell `it_IT` dictionary installed on the system (same places as Polish and Portuguese; not bundled — the upstream file is GPL-licensed), read at runtime by the same affix engine; elided forms check the word after the apostrophe (`l'amico`, `un'idea`, `c'è`), hyphenated compounds part by part | missing accents first (`caffe` → `caffè`, `perche` → `perché`), the dictionary's confusion table, missing space (`perme` → `per me`), one typo, then an accent-folding stem scan | Italian messages: repeated word (emphatic doubles like `piano piano` are allowed), space before punctuation, double space, sentence capital (knows `ecc.`, `dott.`, `prof.`, initials, ordinals), doubled punctuation; no comma rule | TeX Italian patterns (Beccari; MIT option), no exception words | | Turkish (`tr`, any region) | The Hunspell `tr_TR` dictionary installed on the system (same places as Polish; not bundled), read at runtime; proper nouns capitalized, apostrophe suffixes (`Ankara'da`, `WordCraft'ta`) and hyphenated compounds | missing diacritics / ASCII restoration first (`turkce` → `Türkçe`, `isik` → `ışık`), common confusion table (`yalnış` → `yanlış`, `herşey` → `her şey`), detached suffixes (`bende` → `ben de`, `geldinmi` → `geldi mi`), one typo | Turkish messages: repeated word (idiomatic reduplications allowed: `yavaş yavaş`), space before punctuation, double space, sentence capital (knows `vb.`, `vs.`, `prof.`, `dr.`), no comma before/after conjunctions (`ve`, `veya`, `de`), doubled punctuation | Turkish syllable rules (TDK rules); no external pattern file or word list needed | @@ -177,6 +177,7 @@ WordCraft keeps 2,54 cm). | Date | Change | Summary | |---|---|---| +| 2026-10-12 | minor | File › Options › Proofing › Dictionaries folder (#1063): a saved folder of Hunspell dictionaries searched first, so an app started from Finder or the Dock finds them without `WORDCRAFT_DICTIONARIES`; the pane lists the dictionary file each language found — Polish, Portuguese, Italian, Croatian, Turkish (`file.options` `dictionariesFolder`) | | 2026-10-12 | minor | Czech interface catalog completed: 386 entries added (2,447 total), including built-in style names, numbered families, equation gallery, watermarks, paper sizes and preview tiles; coverage test requires every Russian and Spanish key and retains Chinese coverage | | 2026-10-12 | minor | Italian proofing: it-IT spelling from the installed Hunspell dictionary (system `it_IT`, never bundled — upstream is GPL), accent-first suggestions, conservative grammar with Italian messages, TeX Italian hyphenation patterns (MIT option); Review › Language already offered Italian; an `it` interface writes it-IT documents | | 2026-10-13 | minor | Interface audit back in CI (#1128): Envelopes, Labels and Layout › Size labels go through translated patterns with the region's decimal separator (the old fixed-format paper rows, dead since #838, removed); 20 strings added to all 18 catalogs (status-bar messages, Reading Mode toolbar, Go To, Search's Suggested); citation style names and `bad parameters:` errors exempt; the Read Aloud player no longer draws a speech error before the frame has polled it, the audit's rare failure under load |