Italian proofing: po', qual è, È and un/un' grammar rules; more elisions in spelling (#1585)

* Italian proofing: po', qual è, È and un/un' grammar rules; more elisions in spelling

- grammar (works without a dictionary): un pò / un po -> un po'; qual'è -> qual è;
  E' -> È; un'altro -> un altro and un altra -> un'altra (also nessun, ciascun,
  qualcun, alcun), gender from the ending with masculine -ista/-ma/-cida... left alone;
  more emphatic doubles allowed (pian piano, poco poco, così così...)
- spelling: more elisions (dov'è, cos'è, anch'io, senz'altro, nessun'altra, n'ha...),
  truncations po'/mo'/be' accepted, pò and qual'è left to grammar (no double marks)
- tests: rule tests, grammar proptests, Italian suggestion proptest, It in mixed-span proptest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Contributors: credit lupant by their GitHub profile name

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Luigi Pantuosco
2026-10-14 19:50:35 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent 2c5249d13e
commit a3ec498f2a
6 changed files with 269 additions and 15 deletions
+3 -1
View File
@@ -84,8 +84,10 @@ display_name = "cyril265"
[people.webdevtodayjason]
display_name = "Jason Brashear"
sync_github_name = false
[people.Nael-Nathanael]
[people.lupant]
sync_github_name = true
[people.Nael-Nathanael]
[people.Losminek]
+211 -1
View File
@@ -9,6 +9,14 @@
//! - **Capital letter at the start of a sentence**, knowing Italian abbreviations (`ecc.`,
//! `dott.`, `prof.`, `sig.`), initials, ordinal numbers (`1. posto`) and ellipses.
//! - **Doubled punctuation** (`,,`, `..`, `;;`).
//! - **`po'`**: `un pò` and `un po` → `un po'` (a truncation of `poco`: apostrophe, no accent).
//! - **`qual è`**: `qual'è`, `qual'era` → `qual è`, `qual era` (a truncation, never elided).
//! - **`È`**: `E'` written for the verb → `È`.
//! - **`un` / `un'`**: the apostrophe only before a feminine word. `un'altro`, `un'anno` →
//! `un altro`, `un anno` (a masculine word in `-o`; `auto` and `eco` are feminine), and
//! `un altra`, `un amica` → `un'altra`, `un'amica` (a word in `-a`, except the masculine and
//! common-gender ones: `-ista`, `-ita`, `-ota`, `-cida`, `-ma`, `-eta`, `atleta`, `acrobata`…).
//! The same for `nessun`, `ciascun`, `qualcun` and `alcun`.
//!
//! No comma rule and no English rule (such as `a`/`an`) applies to Italian text.
@@ -17,9 +25,22 @@ use crate::{Issue, IssueKind, ProofLang, words};
/// Words Italian repeats on purpose: emphatic doubling, interjections, onomatopoeia.
const ALLOWED_REPEATS: &[&str] = &[
"sì", "no", "piano", "bene", "male", "ecco", "oh", "ah", "eh", "mh", "bah", "ehi", "urrà", "evviva", "ciao", "addio", "adagio", "forte", "lento",
"chiaro",
"chiaro", "pian", "poco", "quasi", "presto", "subito", "zitto", "via", "così", "tanto", "molto", "pure", "dai", "su", "avanti", "basta", "aiuto",
];
/// Indefinite articles (and the like) that elide to `un'` before a feminine word only.
const UN_FORMS: &[&str] = &["un", "nessun", "ciascun", "qualcun", "alcun"];
/// Feminine words in `-o` that start with a vowel (`un'auto`, `un'eco`).
const FEMININE_IN_O: &[&str] = &["auto", "autoradio", "eco", "ecografo"];
/// Endings of masculine or common-gender words in `-a` (`un artista`, `un idiota`, `un enigma`).
const MASCULINE_A_ENDINGS: &[&str] = &["ista", "ita", "ota", "cida", "ma", "eta", "nauta", "iatra", "arca"];
/// Words in `-a` after `un` that aren't feminine nouns: masculine or common-gender ones that fit
/// no ending above, and verbs and pronouns (`un ha`, `un una`).
const NOT_FEMININE_IN_A: &[&str] = &["acrobata", "autodidatta", "alfa", "omega", "ha", "una"];
/// Abbreviations written with a final period after which a sentence goes on in lower case
/// (lower case, without the period).
const ABBREVIATIONS: &[&str] = &[
@@ -210,6 +231,100 @@ fn doubled_punctuation(text: &str, v: &mut Vec<Issue>) {
}
}
/// Is `c` an apostrophe (straight or typographic)?
fn is_apostrophe(c: char) -> bool {
matches!(c, '\'' | '’')
}
/// `pò` (never accented) and `po` without its apostrophe → `po'`. `Po` (the river) is left alone.
fn po(text: &str, ws: &[(usize, usize)], v: &mut Vec<Issue>) {
for (a, b) in ws {
let Some(w) = text.get(*a..*b) else { continue };
let apostrophe = text.get(*b..).and_then(|r| r.chars().next()).is_some_and(is_apostrophe);
let fix = match w {
"pò" => "po'",
"Pò" => "Po'",
"po" if !apostrophe => "po'",
_ => continue,
};
v.push(issue(*a, *b, "Si scrive «po'», con l'apostrofo e senza accento".into(), vec![fix.into()]));
}
}
/// `qual'è` → `qual è` (any case): the fix when `word` is `qual` + apostrophe + `è`/`era`.
pub(crate) fn qual_elided(word: &str) -> Option<String> {
let head_end = word.char_indices().take(4).last().map(|(i, c)| i + c.len_utf8())?;
let (head, rest) = (word.get(..head_end)?, word.get(head_end..)?);
let tail = rest.strip_prefix(is_apostrophe)?;
let tail_ok = matches!(tail.to_lowercase().as_str(), "è" | "era");
(head.eq_ignore_ascii_case("qual") && tail_ok).then(|| format!("{head} {tail}"))
}
fn qual_e(text: &str, ws: &[(usize, usize)], v: &mut Vec<Issue>) {
for (a, b) in ws {
if let Some(fix) = text.get(*a..*b).and_then(qual_elided) {
v.push(issue(*a, *b, "«Qual è» si scrive senza apostrofo".into(), vec![fix]));
}
}
}
/// `E'` for the verb → `È`. A quoted letter (`la lettera 'E'`) is left alone.
fn e_apostrophe(text: &str, ws: &[(usize, usize)], v: &mut Vec<Issue>) {
for (a, b) in ws {
if text.get(*a..*b) != Some("E") {
continue;
}
let mut after = text.get(*b..).unwrap_or("").chars();
let Some(ap) = after.next().filter(|&c| is_apostrophe(c)) else { continue };
let ends = after.next().is_none_or(|c| c.is_whitespace());
let quoted = text.get(..*a).and_then(|t| t.chars().next_back()).is_some_and(|c| is_apostrophe(c) || c == '‘');
if ends && !quoted {
v.push(issue(*a, *b + ap.len_utf8(), "«È» si scrive con l'accento, non con l'apostrofo".into(), vec!["È".into()]));
}
}
}
/// Does `w` (lower case) start with a vowel or `h`, as the words `un'` elides before?
fn starts_with_vowel(w: &str) -> bool {
w.chars().next().is_some_and(|c| "aeiouhàèéìòóù".contains(c))
}
/// Is `w` (lower case) a feminine word in `-a` (`altra`, `amica`, `ora`), as far as the ending
/// tells?
fn feminine_in_a(w: &str) -> bool {
w.chars().count() >= 2 && w.ends_with('a') && !MASCULINE_A_ENDINGS.iter().any(|e| w.ends_with(e)) && !NOT_FEMININE_IN_A.contains(&w)
}
/// `un'` before a masculine word (`un'altro` → `un altro`), and `un` without the apostrophe
/// before a feminine one (`un altra` → `un'altra`). Only lower-case words: a name's gender isn't
/// in its ending.
fn un_apostrophe(text: &str, ws: &[(usize, usize)], v: &mut Vec<Issue>) {
for (a, b) in ws {
let Some(w) = text.get(*a..*b) else { continue };
let Some((head, tail)) = w.split_once(is_apostrophe) else { continue };
if UN_FORMS.contains(&head.to_lowercase().as_str())
&& tail.chars().all(|c| c.is_alphabetic() && c.is_lowercase())
&& starts_with_vowel(tail)
&& tail.ends_with('o')
&& !FEMININE_IN_O.contains(&tail)
{
v.push(issue(*a, *b, format!("Davanti a un nome maschile «{head}» non vuole l'apostrofo"), vec![format!("{head} {tail}")]));
}
}
for pair in ws.windows(2) {
let [(a0, b0), (a1, b1)] = pair else { continue };
let (Some(w0), Some(w1), Some(gap)) = (text.get(*a0..*b0), text.get(*a1..*b1), text.get(*b0..*a1)) else { continue };
if gap == " "
&& UN_FORMS.contains(&w0.to_lowercase().as_str())
&& w1.chars().all(|c| c.is_alphabetic() && c.is_lowercase())
&& starts_with_vowel(w1)
&& feminine_in_a(w1)
{
v.push(issue(*a0, *b1, format!("Davanti a un nome femminile si scrive «{w0}'», con l'apostrofo"), vec![format!("{w0}'{w1}")]));
}
}
}
/// Grammar issues in a paragraph of Italian text, in order.
pub fn check(text: &str) -> Vec<Issue> {
let ws: Vec<(usize, usize)> = words(text);
@@ -218,6 +333,10 @@ pub fn check(text: &str) -> Vec<Issue> {
spaces(text, &mut v);
sentence_capitals(text, &ws, &mut v);
doubled_punctuation(text, &mut v);
po(text, &ws, &mut v);
qual_e(text, &ws, &mut v);
e_apostrophe(text, &ws, &mut v);
un_apostrophe(text, &ws, &mut v);
v.sort_by_key(|i| (i.start, i.end));
v
}
@@ -250,6 +369,97 @@ mod tests {
assert!(check("Sì sì, bene bene :)").is_empty());
}
/// The fixes the rules offer for `t`, in order.
fn fixes(t: &str) -> Vec<(String, String)> {
check(t).into_iter().map(|i| (t.get(i.start..i.end).unwrap_or("").to_string(), i.suggestions.join("|"))).collect()
}
fn pair(a: &str, b: &str) -> (String, String) {
(a.to_string(), b.to_string())
}
#[test]
fn po_takes_an_apostrophe() {
assert_eq!(fixes("Dammi un pò di pane"), [pair("pò", "po'")]);
assert_eq!(fixes("Dammi un po di pane"), [pair("po", "po'")]);
assert_eq!(fixes("Pò di più"), [pair("Pò", "Po'")]);
assert!(fixes("Dammi un po' di pane, un po’ d'acqua").is_empty());
assert!(fixes("Il Po scorre a Torino").is_empty(), "the river");
}
#[test]
fn qual_e_has_no_apostrophe() {
assert_eq!(fixes("Dimmi qual'è il problema"), [pair("qual'è", "qual è")]);
assert_eq!(fixes("Qual’era la strada?"), [pair("Qual’era", "Qual era")]);
assert!(fixes("Qual è il problema? Quale era?").is_empty());
assert!(fixes("Quale'è").is_empty(), "not the truncation");
assert_eq!(qual_elided("qual'è").as_deref(), Some("qual è"));
assert_eq!(qual_elided("qua"), None);
assert_eq!(qual_elided(""), None);
}
#[test]
fn e_apostrophe_becomes_e_grave() {
assert_eq!(fixes("E' vero"), [pair("E'", "È")]);
assert_eq!(fixes("Lo so. E’ tardi"), [pair("E’", "È")]);
assert!(fixes("È vero. E poi?").is_empty());
assert!(fixes("La lettera 'E' è una vocale").is_empty(), "a quoted letter");
}
#[test]
fn un_apostrophe_follows_the_gender() {
assert_eq!(fixes("Ho visto un'altro amico"), [pair("un'altro", "un altro")]);
assert_eq!(fixes("È passato un’anno"), [pair("un’anno", "un anno")]);
assert_eq!(fixes("Prendo un altra strada"), [pair("un altra", "un'altra")]);
assert_eq!(fixes("Ho un amica a Roma"), [pair("un amica", "un'amica")]);
assert_eq!(fixes("Non c'è nessun altra via"), [pair("nessun altra", "nessun'altra")]);
for ok in [
"Un altro amico",
"Un'altra amica",
"Un'auto nuova",
"Un'eco lontana",
"Un artista",
"Un atleta",
"Un idiota",
"Un ipocrita",
"Un enigma",
"Un omicida",
"Un astronauta",
"Un acrobata",
"Un'ora",
"Un'idea",
"Un uomo",
"Un'Alfa",
"Un Andrea",
"Un ha",
] {
assert!(fixes(ok).is_empty(), "{ok}: {:?}", fixes(ok));
}
}
#[test]
fn more_emphatic_doubles_are_allowed() {
for ok in ["Pian piano", "Poco poco", "Zitto zitto", "Via via", "Così così", "Dai dai"] {
assert!(fixes(ok).is_empty(), "{ok}: {:?}", fixes(ok));
}
}
proptest::proptest! {
#[test]
fn never_panics(s in "\\PC{0,80}") {
for i in check(&s) {
proptest::prop_assert!(i.start <= i.end && i.end <= s.len());
}
}
#[test]
fn never_panics_on_italian_like_text(s in "[a-zàèéìòóùA-ZÈ ,.;:!?'’‘\"«»…\\-]{0,60}") {
for i in check(&s) {
proptest::prop_assert!(i.start <= i.end && i.end <= s.len() && s.is_char_boundary(i.start) && s.is_char_boundary(i.end));
}
}
}
fn test_dict_install() {
crate::it::test_dict::install();
}
+42 -7
View File
@@ -4,12 +4,14 @@
//! [`crate::hunspell`]: `it_IT.aff`/`it_IT.dic` from the OS, a package manager or
//! LibreOffice; it isn't bundled — the upstream file is GPL-licensed). Without it Italian
//! text gets no spelling marks. Elided forms check the word after the apostrophe
//! (`l'amico`, `un'idea`, `c'è`); hyphenated compounds are checked part by part; the user
//! dictionary applies as in English.
//! (`l'amico`, `un'idea`, `c'è`, `dov'è`, `senz'altro`); truncations keep their apostrophe
//! (`po'`, `mo'`); hyphenated compounds are checked part by part; the user dictionary applies
//! as in English. Forms a grammar rule explains better (`pò`, `qual'è`) are left to it.
//! - **Suggestions** tuned for Italian ([`suggest`]): missing accents first (`caffe` → `caffè`,
//! `perche` → `perché`), then the dictionary's `REP` confusions, a missing space, one-letter
//! typos, and a dictionary scan for anything further off.
//! - **Grammar** ([`grammar`]): cheap, reliable rules with Italian messages.
//! - **Grammar** ([`grammar`]): cheap, reliable rules with Italian messages, which work with
//! no dictionary installed.
pub mod grammar;
mod suggest;
@@ -84,12 +86,23 @@ fn is_latin_letter(c: char) -> bool {
}
/// Proclitics (and verb clippings) that elide before a vowel or `h`: the word after the
/// apostrophe is what gets checked (`l'amico`, `un'idea`, `c'è`, `m'hai`).
/// apostrophe is what gets checked (`l'amico`, `un'idea`, `c'è`, `m'hai`, `dov'è`, `anch'io`).
const PROCLITICS: &[&str] = &[
"l", "un", "una", "uno", "all", "dall", "dell", "nell", "sull", "coll", "pell", "c", "m", "t", "s", "v", "d", "j", "quest", "quell", "bell",
"sant", "grand", "poc",
"l", "un", "una", "uno", "all", "dall", "dell", "nell", "sull", "coll", "pell", "c", "m", "t", "s", "v", "d", "j", "n", "ch", "gl", "quest",
"quell", "bell", "buon", "sant", "grand", "poc", "nessun", "ciascun", "qualcun", "alcun", "dov", "cos", "com", "quand", "anch", "senz", "tutt",
"nient", "mezz", "sott", "tant", "quant", "dev",
];
/// Truncated words written with an apostrophe (`un po'`, `a mo' di`, `be'`): [`crate::words`]
/// trims the apostrophe, so spelling sees the bare word; [`grammar`] checks the apostrophe.
const TRUNCATIONS: &[&str] = &["po", "mo", "be"];
/// Misspellings a grammar rule reports with a clearer message and fix (`pò` → `po'`, `qual'è`
/// → `qual è`), so spelling leaves them alone rather than marking the word twice.
fn left_to_grammar(lower: &str) -> bool {
lower == "pò" || grammar::qual_elided(lower).is_some()
}
/// Is `word` spelled correctly in Italian? Numbers, single letters, short words in capitals
/// (acronyms: `RAI`, `TG`), words with digits and words in other scripts are accepted, and so
/// is every word when no Italian dictionary is installed.
@@ -114,6 +127,10 @@ pub fn is_correct_with(d: &AffixDict, word: &str) -> bool {
if !w.chars().any(is_latin_letter) {
return true; // other scripts: no dictionary
}
let lower = w.to_lowercase();
if TRUNCATIONS.contains(&lower.as_str()) || left_to_grammar(&lower) {
return true;
}
if w.contains('-') {
return w.split('-').all(|p| p.chars().count() <= 1 || known(d, p));
}
@@ -178,14 +195,32 @@ mod tests {
}
}
#[test]
fn elisions_and_truncations_are_correct() {
test_dict::install();
for w in ["dov'è", "Com'è", "cos'è", "anch'io", "senz'acqua", "tutt'uno", "quand'è", "nessun'amica", "n'ha", "po'", "po", "mo'"] {
assert!(is_correct(w), "{w}");
}
}
#[test]
fn misspellings_are_flagged() {
test_dict::install();
for w in ["caffé", "perche", "lbiro", "parlre", "amicaa", "citta", "quelo"] {
for w in ["caffé", "perche", "lbiro", "parlre", "amicaa", "citta", "quelo", "dov'lbiro", "xyz'casa"] {
assert!(!is_correct(w), "{w}");
}
}
#[test]
fn grammar_owned_forms_are_not_marked_twice() {
test_dict::install();
// `pò` and `qual'è` are reported by the grammar rules, with the fix; spelling stays quiet.
for w in ["pò", "Pò", "qual'è", "Qual’era"] {
assert!(is_correct(w), "{w}");
assert!(!grammar::check(w).is_empty(), "{w}: grammar reports it");
}
}
#[test]
fn user_dictionary_applies() {
test_dict::install();
+2 -1
View File
@@ -30,7 +30,7 @@ PFX R 0 ri .
";
/// Stems (the count line first, as in Hunspell files).
pub const DIC: &str = "96
pub const DIC: &str = "97
città
caffè
perché
@@ -127,6 +127,7 @@ a
per
me
quello
io
";
/// Make [`AFF`]/[`DIC`] the Italian dictionary (every test that needs one calls this; they all
+7 -2
View File
@@ -1298,14 +1298,19 @@ mod tests {
let _ = suggest_in(&s, 4, ProofLang::Pt);
}
#[test]
fn italian_suggestions_never_panic(s in "[a-zàèéìòóùA-ZÀÈÉÌÒÓÙ'’ -]{0,14}") {
let _ = suggest_in(&s, 4, ProofLang::It);
}
#[test]
fn turkish_suggestions_never_panic(s in "[a-zçğıöşüA-ZÇĞİÖŞÜ'’ -]{0,14}") {
let _ = suggest_in(&s, 4, ProofLang::Tr);
}
#[test]
fn mixed_spans_never_panic(s in "\\PC{0,60}", cuts in proptest::collection::vec((0usize..80, 0usize..80, 0u8..6), 0..6)) {
let langs = [ProofLang::En, ProofLang::Pl, ProofLang::Pt, ProofLang::Tr, ProofLang::Ru, ProofLang::Other];
fn mixed_spans_never_panic(s in "\\PC{0,60}", cuts in proptest::collection::vec((0usize..80, 0usize..80, 0u8..7), 0..6)) {
let langs = [ProofLang::En, ProofLang::Pl, ProofLang::Pt, ProofLang::It, ProofLang::Tr, ProofLang::Ru, ProofLang::Other];
let spans: Vec<(Range<usize>, ProofLang)> = cuts.iter().map(|&(a, b, l)| (a.min(b)..a.max(b), langs[l as usize % langs.len()])).collect();
for i in check_text(&s, &spans) {
proptest::prop_assert!(i.start <= i.end && i.end <= s.len());
+4 -3
View File
@@ -1,6 +1,6 @@
# Localization parity with Microsoft Word
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (Czech interface catalog completed, 2,447 entries; previously trivial: interface audit runs in CI again: size labels translated, 20 strings in every catalog, Read Aloud error race fixed, #1128; previously trivial: Review › Language reaches every language again — Set Proofing Language… and Set As Default, #860; previously minor: Italian proofing: it-IT spelling from the installed Hunspell dictionary, accent-first suggestions, conservative grammar with Italian messages; previously minor: Portuguese proofing; previously trivial: Turkish interface catalog completed, 2,325 entries, #774; previously minor: Hungarian interface added; previously trivial: sorting and table numbers follow the text's language, #836, #837; previously minor: Polish interface; Polish proofing — spelling, grammar, hyphenation — and a proofing language per run; previously minor: Turkish interface added; previously major: first version; catalogs measured from `crates/ui-egui/src/i18n`, Word's languages from its bundle listing) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** trivial (Italian grammar: `po'`, `qual è`, `È`, `un`/`un'` rules; more elisions and truncations in spelling; previously trivial: Czech interface catalog completed, 2,447 entries; previously trivial: interface audit runs in CI again: size labels translated, 20 strings in every catalog, Read Aloud error race fixed, #1128; previously trivial: Review › Language reaches every language again — Set Proofing Language… and Set As Default, #860; previously minor: Italian proofing: it-IT spelling from the installed Hunspell dictionary, accent-first suggestions, conservative grammar with Italian messages; previously minor: Portuguese proofing; previously trivial: Turkish interface catalog completed, 2,325 entries, #774; previously minor: Hungarian interface added; previously trivial: sorting and table numbers follow the text's language, #836, #837; previously minor: Polish interface; Polish proofing — spelling, grammar, hyphenation — and a proofing language per run; previously minor: Turkish interface added; previously major: first version; catalogs measured from `crates/ui-egui/src/i18n`, Word's languages from its bundle listing) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
Interface languages, script support and proofing per language. How translation works (catalog
format, adding a language, clean-room rule): `crates/ui-egui/src/i18n/mod.rs`.
@@ -102,7 +102,7 @@ other than envelopes (Letter, Legal, A4, B5 (JIS)) stay as they are (#1128).
| Russian (`ru`) | A Russian Hunspell dictionary installed on the system (`ru_RU` from the OS, a package manager, LibreOffice's `dict-ru` or the `WORDCRAFT_DICTIONARIES` folder; UTF-8, KOI8-R or Windows-1251; not bundled, so no Russian spelling marks without it), read by the same affix engine; `ё` optional (`елка`/`ёлка`, `еще`/`ещё`); hyphenated words whole, then by parts (`кто-то`, `по-русски`, `кое-где`, `северо-западный`, `ИИ-консультант`); solid compounds (`высокоэластичный`), `не` with adjectives, participles and verbal nouns (not verbs), productive prefixes (`ультра-`, `эко-`) and prefixed verbs; abbreviations with endings (`ГИПа`) and camel case left alone; our supplement of ~200 modern words with their declensions (`assets/spelling/ru-extra.txt`: `маркетплейс`, `промокод`, `интеграция`, `колеровать`) | `ё`/`е` first, then slips within letter groups (unstressed vowels, voiced and voiceless pairs, `ь`/`ъ`), the common confusions (`жы`, `чя`, `тся`/`ться`, doubled consonants, unpronounced consonants: `здраствуй` → `здравствуй`), a missing space (`незнаю` → `не знаю`; not a preposition before a verb), one typo, then a stem scan | Russian messages: repeated word, space before punctuation, double space, sentence capital (knows `т. е.`, `г.`, `руб.`, `ул.`, initials, numbers, dialogue dashes), possibly missing comma before `что`/`чтобы`/`если`/`хотя`/`поскольку`/`который` and before `потому что` (not after conjunctions and particles such as `и`, `только`, `даже`, `так`, `для того`; not after a preposition; not in `хотя бы`, `что-то`), doubled punctuation | The rules of Russian word division as code (no pattern file): a vowel on each side, two letters at least, `ь`/`ъ`/`й` kept with the letter before, a break after `ъ`, a consonant cluster divided after its first consonant |
| 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 |
| 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'è`, `dov'è`, `senz'altro`), truncations keep their apostrophe (`po'`, `mo'`), 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, `un pò` → `un po'`, `qual'è` → `qual è`, `E'` → `È`, `un'altro` → `un altro` and `un altra` → `un'altra` (gender from the ending, masculine `-ista`/`-ma`/`-cida`… left alone); these work without a dictionary; 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 |
| Any other | — (no marks) | — | — | the English patterns, as before |
@@ -121,7 +121,7 @@ Italian (`it`, #594) follows `it`, `it-IT` and `it-CH` system locales. Its catal
the others plus 51 strings that are wrapped in `tl!` but were in no catalog yet (File › Info
properties, the File › Home greeting, list levels, Equation tab gallery and tooltips). Ribbon tabs,
groups and command names use the names Italian Word users know (feature names, as `AGENTS.md`
allows); all other text is written from the meaning of the English. No Italian proofing yet.
allows); all other text is written from the meaning of the English. Italian proofing: see the table above.
## Sorting and numbers in the text's language
Sorts follow the alphabet of the text's language (`w:lang`, else the document default):
@@ -184,6 +184,7 @@ WordCraft keeps 2,54 cm).
| Date | Change | Summary |
|---|---|---|
| 2026-10-14 | minor | Italian grammar rules that work without a dictionary: `un pò`/`un po` → `un po'`, `qual'è` → `qual è`, `E'` → `È`, `un'` only before feminine words (`un'altro` → `un altro`, `un altra` → `un'altra`, also `nessun`/`ciascun`/`qualcun`/`alcun`); spelling accepts more elisions (`dov'è`, `anch'io`, `senz'altro`, `nessun'altra`) and truncations (`po'`, `mo'`) and leaves `pò`/`qual'è` to grammar so they aren't marked twice; more emphatic doubles allowed |
| 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 |