mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-28 20:37:10 -06:00
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:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
2c5249d13e
commit
a3ec498f2a
@@ -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]
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in New Issue
Block a user