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 |