diff --git a/ROADMAP.md b/ROADMAP.md index bef690d..6cfce04 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -135,6 +135,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm | 2026-10-11 | Hide White Space (#645): `view.hideWhiteSpace` (View › Show checkbox, or double-click the gap between pages) collapses margins, headers and footers between pages to a thin line in Print Layout; screen only, page count unchanged | | 2026-10-11 | Fonts: Palatino Linotype and Book Antiqua fall back to Palatino (macOS) or TeX Gyre Pagella / P052 / URW Palladio (Linux) before generic serif (#670) | |---|---| +| 2026-10-12 | AutoCorrect capitals (#544): Correct TWo INitial CApitals (with exceptions), accidental cAPS LOCK, first letter of table cells; each undoable and switchable | | 2026-10-11 | Turkish (`tr`) interface completed (#774): 2,325 entries, every key of the Russian catalog (equation gallery, built-in style names, watermarks, paper sizes, preview tiles); the interface audit checks Turkish too | | 2026-10-12 | Navigation headings truncate within the resizable pane, with an ellipsis for long titles and full text on hover (#759) | | 2026-10-11 | Reading Mode: existing View ribbon button now opens a read-only, viewport-column layout with animated horizontal paging, transient display controls, heading navigation (Navigation pane) and resize anchoring; document layout and export unchanged | diff --git a/crates/engine/src/cmd/tools.rs b/crates/engine/src/cmd/tools.rs index 3e137f8..c7f5c95 100644 --- a/crates/engine/src/cmd/tools.rs +++ b/crates/engine/src/cmd/tools.rs @@ -71,12 +71,16 @@ pub struct AutoCorrectPrefs { pub enabled: bool, /// Replace text as you type (the replacement list). pub replace_text: bool, + /// Correct TWo INitial CApitals: "WOrd" → "Word" (not the exceptions, e.g. "IDs"). + pub two_initial_caps: bool, /// Capitalize the first letter of sentences. pub cap_sentences: bool, /// Capitalize the first letter of table cells. pub cap_cells: bool, /// Capitalize names of days. pub cap_days: bool, + /// Correct accidental usage of cAPS LOCK: "wORD" → "Word". + pub caps_lock: bool, /// AutoFormat As You Type: "straight quotes" with “smart quotes”. pub smart_quotes: bool, /// Fractions (1/2) with fraction characters (½). @@ -101,6 +105,8 @@ pub struct AutoCorrectPrefs { pub exceptions: Vec, /// Built-in abbreviations the user deleted from the exceptions. pub exceptions_removed: Vec, + /// The user's words that keep two initial capitals (besides the built-in ones). + pub initial_caps_exceptions: Vec, } impl Default for AutoCorrectPrefs { @@ -108,9 +114,11 @@ impl Default for AutoCorrectPrefs { AutoCorrectPrefs { enabled: true, replace_text: true, + two_initial_caps: true, cap_sentences: true, cap_cells: true, cap_days: true, + caps_lock: true, smart_quotes: true, fractions: true, ordinals: true, @@ -123,6 +131,7 @@ impl Default for AutoCorrectPrefs { removed: Vec::new(), exceptions: Vec::new(), exceptions_removed: Vec::new(), + initial_caps_exceptions: Vec::new(), } } } @@ -145,7 +154,7 @@ impl AutoCorrectPrefs { let mut seen = std::collections::HashSet::new(); self.entries.retain(|(a, b)| ok(a, MAX_FROM) && b.chars().count() <= MAX_TO && !b.contains(['\n', '\r']) && seen.insert(a.clone())); self.entries.truncate(MAX_ENTRIES); - for list in [&mut self.removed, &mut self.exceptions, &mut self.exceptions_removed] { + for list in [&mut self.removed, &mut self.exceptions, &mut self.exceptions_removed, &mut self.initial_caps_exceptions] { let mut seen = std::collections::HashSet::new(); list.retain(|a| ok(a, MAX_FROM) && seen.insert(a.clone())); list.truncate(MAX_ENTRIES); @@ -216,9 +225,11 @@ fn autocorrect_options(s: &mut Session, v: &Value) -> CmdResult { for (k, f) in [ ("enabled", &mut ac.enabled as &mut bool), ("replaceText", &mut ac.replace_text), + ("twoInitialCaps", &mut ac.two_initial_caps), ("capSentences", &mut ac.cap_sentences), ("capCells", &mut ac.cap_cells), ("capDays", &mut ac.cap_days), + ("capsLock", &mut ac.caps_lock), ("smartQuotes", &mut ac.smart_quotes), ("fractions", &mut ac.fractions), ("ordinals", &mut ac.ordinals), @@ -273,13 +284,31 @@ fn autocorrect_options(s: &mut Session, v: &Value) -> CmdResult { ac.exceptions_removed.push(e); } } + if let Some(w) = p::str(v, "addInitialCapsException") { + let w = w.trim(); + if bad(w, MAX_FROM) || w.contains(char::is_whitespace) { + return Err(CmdError::Params("an INitial CAps exception is one word, like \"IDs\"".into())); + } + if !INITIAL_CAPS_EXCEPTIONS.contains(&w) + && !ac.initial_caps_exceptions.iter().any(|e| e == w) + && ac.initial_caps_exceptions.len() < MAX_ENTRIES + { + ac.initial_caps_exceptions.push(w.to_string()); + } + } + if let Some(w) = p::str(v, "deleteInitialCapsException") { + let w = w.trim(); + ac.initial_caps_exceptions.retain(|e| e != w); + } let ac = &s.prefs.autocorrect; Ok(json!({ "enabled": ac.enabled, "replaceText": ac.replace_text, + "twoInitialCaps": ac.two_initial_caps, "capSentences": ac.cap_sentences, "capCells": ac.cap_cells, "capDays": ac.cap_days, + "capsLock": ac.caps_lock, "smartQuotes": ac.smart_quotes, "fractions": ac.fractions, "ordinals": ac.ordinals, @@ -291,6 +320,7 @@ fn autocorrect_options(s: &mut Session, v: &Value) -> CmdResult { "entries": ac.list().into_iter().map(|(a, b, builtin)| json!({"from": a, "to": b, "builtIn": builtin})).collect::>(), "user": ac.entries, "exceptions": ac.exception_list(), + "initialCapsExceptions": INITIAL_CAPS_EXCEPTIONS.iter().map(|e| e.to_string()).chain(ac.initial_caps_exceptions.iter().cloned()).collect::>(), })) } @@ -321,7 +351,7 @@ pub fn specs() -> Vec { .pure(), CommandSpec::new("tools.autocorrect", "AutoCorrect Options", "File › Options › Proofing", autocorrect_options) .params( - r#"{"enabled"?: bool (all of AutoCorrect), "replaceText"|"capSentences"|"capCells"|"capDays"|"smartQuotes"|"fractions"|"ordinals"|"dashes"|"links"|"bullets"|"numbering"|"borderLines"?: bool, "add"?: {"from": string, "to": string} (adds or replaces an entry), "delete"?: string (an entry's "from", built in or added), "addException"?: string, "deleteException"?: string} → options, entries and exceptions"#, + r#"{"enabled"?: bool (all of AutoCorrect), "replaceText"|"twoInitialCaps"|"capSentences"|"capCells"|"capDays"|"capsLock"|"smartQuotes"|"fractions"|"ordinals"|"dashes"|"links"|"bullets"|"numbering"|"borderLines"?: bool, "add"?: {"from": string, "to": string} (adds or replaces an entry), "delete"?: string (an entry's "from", built in or added), "addException"?: string, "deleteException"?: string, "addInitialCapsException"?: string (keeps TWo INitial CApitals, like "IDs"), "deleteInitialCapsException"?: string} → options, entries and exceptions"#, ) .pure(), CommandSpec::new("tools.customizeKeyboard", "Customize Keyboard", "File › Options", customize_keyboard) @@ -714,7 +744,7 @@ pub fn autocorrect_word(s: &mut Session, on_enter: bool) -> Result<(), CmdError> return Ok(()); } let ac = s.prefs.autocorrect.clone(); - let f = s.sel.focus.clone(); + let mut f = s.sel.focus.clone(); let Some(text) = s.doc.para_at(&f).map(|p| p.text.clone()) else { return Ok(()) }; let Some(before) = text.get(..f.off) else { return Ok(()) }; let Some(last) = before.chars().next_back() else { return Ok(()) }; @@ -724,10 +754,29 @@ pub fn autocorrect_word(s: &mut Session, on_enter: bool) -> Result<(), CmdError> let trigger = if on_enter { 0 } else { last.len_utf8() }; let Some(body) = before.get(..before.len() - trigger) else { return Ok(()) }; let start = body.rfind(char::is_whitespace).map(|i| i + 1).unwrap_or(0); - let Some(word) = body.get(start..).map(str::to_string) else { return Ok(()) }; + let Some(mut word) = body.get(start..).map(str::to_string) else { return Ok(()) }; if word.is_empty() { return Ok(()); } + // TWo INitial CApitals and cAPS LOCK come first, so "TEh" is still a replacement's typo. + if let Some((at, reps)) = case_fix(&ac, &word) { + let para = s.doc.para_mut(f.story, &f.path)?; + let letters = word.get(at..).unwrap_or("").chars().take(reps.len()).collect::(); + let mut end = start + at + letters.len(); + for (c, rep) in letters.chars().rev().zip(reps.iter().rev()) { + let from = end.saturating_sub(c.len_utf8()); + if rep.chars().ne(std::iter::once(c)) { + let props = para.props_of_char(from).clone(); + para.delete(from, end)?; + para.insert_text(from, rep, &props)?; + } + end = from; + } + let fixed: String = reps.concat(); + f.off = (f.off + fixed.len()).saturating_sub(letters.len()); + s.sel = Selection::caret(f.clone()); + word = format!("{}{fixed}{}", word.get(..at).unwrap_or(""), word.get(at + letters.len()..).unwrap_or("")); + } // A whole-word replacement, else "wait..." → "wait…". let found = if ac.replace_text { ac.replacement(&word) } else { None }; let (from, rep) = match found { @@ -804,6 +853,49 @@ pub fn autocorrect_word(s: &mut Session, on_enter: bool) -> Result<(), CmdError> Ok(()) } +/// Built-in words that keep their two initial capitals (plurals of two-letter abbreviations). +pub const INITIAL_CAPS_EXCEPTIONS: &[&str] = &["CDs", "IDs", "PCs", "TVs", "DJs", "GBs", "KBs", "MBs", "TBs", "VMs"]; + +/// Words with a lowercase first letter and the rest in capitals that aren't cAPS LOCK slips. +const CAPS_LOCK_EXCEPTIONS: &[&str] = &["iOS", "mRNA", "tRNA", "rRNA", "cDNA", "eSIM", "eSATA", "iSCSI", "vCPU"]; + +/// The capitalisation fix for a finished word: TWo INitial CApitals ("WOrd" → "Word") or cAPS +/// LOCK ("wORD" → "Word"). Returns the byte offset of the word's letters and each letter's +/// replacement. Only words of three or more letters, optionally in quotes or brackets. +fn case_fix(caps: &AutoCorrectPrefs, word: &str) -> Option<(usize, Vec)> { + if !caps.two_initial_caps && !caps.caps_lock { + return None; + } + let at = word.find(char::is_alphabetic)?; + let rest = word.get(at..)?; + let letters = rest.get(..rest.find(|c: char| !c.is_alphabetic()).unwrap_or(rest.len()))?; + let tail = rest.get(letters.len()..)?; + if !word.get(..at)?.chars().all(|c| "\"'“‘([".contains(c)) || !tail.chars().all(|c| "\"'”’)]".contains(c)) { + return None; + } + let cs: Vec = letters.chars().collect(); + let (&a, &b, more) = match cs.as_slice() { + [a, b, more @ ..] if !more.is_empty() => (a, b, more), + _ => return None, + }; + let one = |c: char| c.to_string(); + let lower = |c: char| c.to_lowercase().collect::(); + if caps.two_initial_caps + && a.is_uppercase() + && b.is_uppercase() + && more.iter().all(|c| c.is_lowercase()) + && !INITIAL_CAPS_EXCEPTIONS.contains(&letters) + && !caps.initial_caps_exceptions.iter().any(|e| e == letters) + { + return Some((at, std::iter::once(one(a)).chain(std::iter::once(lower(b))).chain(more.iter().map(|&c| one(c))).collect())); + } + if caps.caps_lock && a.is_lowercase() && b.is_uppercase() && more.iter().all(|c| c.is_uppercase()) && !CAPS_LOCK_EXCEPTIONS.contains(&letters) { + let up = a.to_uppercase().collect::(); + return Some((at, std::iter::once(up).chain(std::iter::once(lower(b))).chain(more.iter().map(|&c| lower(c))).collect())); + } + None +} + /// Byte length of an ordinal suffix to superscript ("1st" → 2), if `word` is a correct ordinal. fn ordinal_suffix(word: &str) -> Option { let digits = word.find(|c: char| !c.is_ascii_digit())?; diff --git a/crates/engine/src/tests_typing.rs b/crates/engine/src/tests_typing.rs index 48a5d92..8e03dd0 100644 --- a/crates/engine/src/tests_typing.rs +++ b/crates/engine/src/tests_typing.rs @@ -634,6 +634,61 @@ fn shift_f5_goes_back_through_the_last_edits() { assert!(Session::new(wordcraft_doc::Document::new()).run("edit.goBack", &json!({})).is_err(), "nothing edited yet"); } +#[test] +fn two_initial_capitals_are_corrected() { + let s = typed("the WOrd and THe end, not IDs or CDs. TV is fine "); + assert_eq!(s.doc.plain_text(StoryRef::Body), "The Word and The end, not IDs or CDs. TV is fine "); + // "TEh" loses its second capital, then the replacement list fixes the typo. + assert_eq!(typed("TEh ").doc.plain_text(StoryRef::Body), "The "); + // A user exception. + let mut s = typed(""); + run(&mut s, "tools.autocorrect", json!({"addInitialCapsException": "WCraft"})); + typ(&mut s, "WCraft "); + assert_eq!(s.doc.plain_text(StoryRef::Body), "WCraft "); +} + +#[test] +fn accidental_caps_lock_is_corrected() { + let s = typed("a wORD and hELLO there. iOS stays "); + assert_eq!(s.doc.plain_text(StoryRef::Body), "A Word and Hello there. iOS stays "); +} + +#[test] +fn capitalisation_fixes_undo_and_turn_off() { + let mut s = typed("WOrd"); + typ(&mut s, " "); + assert_eq!(s.doc.plain_text(StoryRef::Body), "Word "); + run(&mut s, "edit.undo", json!({})); + assert_eq!(s.doc.plain_text(StoryRef::Body), "WOrd "); + let mut s = typed("x wORD"); + typ(&mut s, " "); + run(&mut s, "edit.undo", json!({})); + assert_eq!(s.doc.plain_text(StoryRef::Body), "X wORD "); + let mut s = typed(""); + run(&mut s, "tools.autocorrect", json!({"twoInitialCaps": false, "capsLock": false})); + typ(&mut s, "x WOrd wORD "); + assert_eq!(s.doc.plain_text(StoryRef::Body), "X WOrd wORD "); +} + +#[test] +fn first_letter_of_a_table_cell_is_capitalised() { + let cell_text = |s: &Session, c: usize| { + s.doc.body.iter().find_map(|b| b.as_table()).and_then(|t| t.rows.first()?.cells.get(c)?.blocks.first()?.as_para().map(|p| p.text.clone())) + }; + let mut s = typed(""); + run(&mut s, "insert.table", json!({"rows": 1, "cols": 2})); + typ(&mut s, "apples and pears\tplums "); + assert_eq!(cell_text(&s, 0).as_deref(), Some("Apples and pears")); + assert_eq!(cell_text(&s, 1).as_deref(), Some("Plums ")); + run(&mut s, "edit.undo", json!({})); + assert_eq!(cell_text(&s, 1).as_deref(), Some("plums ")); + let mut s = typed(""); + run(&mut s, "tools.autocorrect", json!({"capCells": false})); + run(&mut s, "insert.table", json!({"rows": 1, "cols": 1})); + typ(&mut s, "plums "); + assert_eq!(cell_text(&s, 0).as_deref(), Some("plums ")); +} + /// File › Options › Advanced, "Typing replaces selected text" (#485): on (Word's default) the /// typed text replaces the selection; off, it goes in front of the selection and keeps it. #[test] diff --git a/crates/ui-egui/src/dialogs_review.rs b/crates/ui-egui/src/dialogs_review.rs index f74a413..263c9b5 100644 --- a/crates/ui-egui/src/dialogs_review.rs +++ b/crates/ui-egui/src/dialogs_review.rs @@ -336,9 +336,11 @@ impl AutoCorrectForm { let o = &self.options; json!({ "replaceText": o.replace_text, + "twoInitialCaps": o.two_initial_caps, "capSentences": o.cap_sentences, "capCells": o.cap_cells, "capDays": o.cap_days, + "capsLock": o.caps_lock, "smartQuotes": o.smart_quotes, "fractions": o.fractions, "ordinals": o.ordinals, @@ -410,6 +412,7 @@ fn autocorrect_ui(app: &mut WordApp, ui: &mut Ui, f: &mut AutoCorrectForm) -> bo fn autocorrect_tab(app: &mut WordApp, ui: &mut Ui, f: &mut AutoCorrectForm) { let o = &mut f.options; + ui.checkbox(&mut o.two_initial_caps, tl!("Correct TWo INitial CApitals")); ui.checkbox(&mut o.cap_sentences, tl!("Capitalize first letter of sentences")); ui.horizontal(|ui| { ui.add_space(18.0); @@ -423,6 +426,7 @@ fn autocorrect_tab(app: &mut WordApp, ui: &mut Ui, f: &mut AutoCorrectForm) { let o = &mut f.options; ui.checkbox(&mut o.cap_cells, tl!("Capitalize first letter of table cells")); ui.checkbox(&mut o.cap_days, tl!("Capitalize names of days")); + ui.checkbox(&mut o.caps_lock, tl!("Correct accidental usage of cAPS LOCK key")); ui.checkbox(&mut o.replace_text, tl!("Replace text as you type")); egui::Grid::new("ac_boxes").num_columns(2).spacing(vec2(8.0, 2.0)).show(ui, |ui| { ui.label(tl!("Replace:")); diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 996f6f9..1e84700 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -2058,3 +2058,5 @@ Reading Column Width Šířka sloupce pro čtení Reading Line Spacing Řádkování pro čtení Reading Text Size Velikost textu pro čtení + Correct TWo INitial CApitals Opravovat DVě POčáteční VElká písmena + Correct accidental usage of cAPS LOCK key Opravovat náhodné použití klávesy cAPS LOCK diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 915d278..35c7f83 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -2052,3 +2052,5 @@ Reading Column Width Spaltenbreite beim Lesen Reading Line Spacing Zeilenabstand beim Lesen Reading Text Size Textgröße beim Lesen + Correct TWo INitial CApitals ZWei GRoßbuchstaben am WOrtanfang korrigieren + Correct accidental usage of cAPS LOCK key Unbeabsichtigtes Verwenden der fESTSTELLTASTE korrigieren diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 4034d2b..0985894 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1468,6 +1468,8 @@ Border lines Líneas de borde Capitalize first letter of sentences Poner en mayúscula la primera letra de las oraciones Capitalize first letter of table cells Poner en mayúscula la primera letra de las celdas + Correct TWo INitial CApitals Corregir DOs MAyúsculas SEguidas + Correct accidental usage of cAPS LOCK key Corregir el uso accidental de bLOQ mAYÚS Capitalize names of days Poner en mayúscula los nombres de los días Case changes Cambios de mayúsculas Character level Nivel de carácter diff --git a/crates/ui-egui/src/i18n/et.tsv b/crates/ui-egui/src/i18n/et.tsv index fd1fb16..b6be242 100644 --- a/crates/ui-egui/src/i18n/et.tsv +++ b/crates/ui-egui/src/i18n/et.tsv @@ -1447,6 +1447,8 @@ Border lines Äärisjooned Capitalize first letter of sentences Lausete esitäht suureks Capitalize first letter of table cells Tabelilahtrite esitäht suureks + Correct TWo INitial CApitals Paranda KAks SUurt algustähte + Correct accidental usage of cAPS LOCK key Paranda kogemata sisse jäänud cAPS LOCK Capitalize names of days Nädalapäevade nimed suure algustähega Case changes Tähesuuruse muutused Character level Märgi tasandil diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 9e5f797..8a7f1e7 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -2061,3 +2061,5 @@ Reading Column Width Largeur de colonne de lecture Reading Line Spacing Interligne de lecture Reading Text Size Taille du texte de lecture + Correct TWo INitial CApitals Corriger DEux MAjuscules EN début de mot + Correct accidental usage of cAPS LOCK key Corriger l’utilisation accidentelle de la touche vERR. mAJ diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 82e7429..e076dc0 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2451,3 +2451,5 @@ Reading Column Width Olvasási oszlopszélesség Reading Line Spacing Olvasási sorköz Reading Text Size Olvasási szövegméret + Correct TWo INitial CApitals KÉt KEzdő NAgybetű javítása + Correct accidental usage of cAPS LOCK key A cAPS LOCK billentyű véletlen használatának javítása diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 84bc2af..aa2a09f 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -2437,3 +2437,5 @@ preview Title Esempio Reading Column Width Larghezza colonna di lettura Reading Line Spacing Interlinea di lettura Reading Text Size Dimensione testo di lettura + Correct TWo INitial CApitals Correggi DUe MAiuscole INiziali + Correct accidental usage of cAPS LOCK key Correggi l'uso accidentale del tasto bLOC mAIUSC diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 037ee24..1c49d88 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1428,6 +1428,8 @@ Border lines 罫線 Capitalize first letter of sentences 文の先頭文字を大文字にする Capitalize first letter of table cells 表のセルの先頭文字を大文字にする + Correct TWo INitial CApitals 先頭 2 文字の大文字を修正する + Correct accidental usage of cAPS LOCK key Caps Lock キーの押し間違いを修正する Capitalize names of days 曜日の先頭文字を大文字にする Case changes 大文字/小文字の変更 Character level 文字単位 diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 43249f4..91cdbb5 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -2094,3 +2094,5 @@ Reading Column Width Szerokość kolumny do czytania Reading Line Spacing Interlinia do czytania Reading Text Size Rozmiar tekstu do czytania + Correct TWo INitial CApitals Poprawiaj DWie POczątkowe WIelkie litery + Correct accidental usage of cAPS LOCK key Poprawiaj przypadkowe użycie klawisza cAPS LOCK diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 392c3f2..00b4fa9 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1440,6 +1440,8 @@ Border lines Linhas de borda Capitalize first letter of sentences Primeira letra das frases em maiúscula Capitalize first letter of table cells Primeira letra das células em maiúscula + Correct TWo INitial CApitals Corrigir DUas INiciais MAiúsculas + Correct accidental usage of cAPS LOCK key Corrigir o uso acidental da tecla cAPS LOCK Capitalize names of days Nomes dos dias em maiúscula Case changes Alterações de maiúsculas Character level Nível de caractere diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index ab441c6..f6ac816 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -2037,6 +2037,8 @@ preview Title Пример Border lines Линии границ Capitalize first letter of sentences Делать первые буквы предложений прописными Capitalize first letter of table cells Делать первые буквы ячеек таблиц прописными + Correct TWo INitial CApitals Исправлять ДВе ПРописные буквы в начале слова + Correct accidental usage of cAPS LOCK key Устранять последствия случайного нажатия cAPS LOCK Capitalize names of days Названия дней с прописной буквы Case changes Изменения регистра Character level На уровне символов diff --git a/crates/ui-egui/src/i18n/sr-latn.tsv b/crates/ui-egui/src/i18n/sr-latn.tsv index 1aab2b3..755e723 100644 --- a/crates/ui-egui/src/i18n/sr-latn.tsv +++ b/crates/ui-egui/src/i18n/sr-latn.tsv @@ -1438,6 +1438,8 @@ Border lines Linije ivica Capitalize first letter of sentences Veliko prvo slovo rečenice Capitalize first letter of table cells Veliko prvo slovo u ćelijama tabele + Correct TWo INitial CApitals Ispravi DVa POčetna velika slova + Correct accidental usage of cAPS LOCK key Ispravi slučajno uključen cAPS LOCK Capitalize names of days Veliko slovo u nazivima dana Case changes Promene veličine slova Character level Nivo znaka diff --git a/crates/ui-egui/src/i18n/sr.tsv b/crates/ui-egui/src/i18n/sr.tsv index 2f5fb99..e52ceaa 100644 --- a/crates/ui-egui/src/i18n/sr.tsv +++ b/crates/ui-egui/src/i18n/sr.tsv @@ -1442,6 +1442,8 @@ Border lines Линије ивица Capitalize first letter of sentences Велико прво слово реченице Capitalize first letter of table cells Велико прво слово у ћелијама табеле + Correct TWo INitial CApitals Исправи ДВа ПОчетна велика слова + Correct accidental usage of cAPS LOCK key Исправи случајно укључен cAPS LOCK Capitalize names of days Велико слово у називима дана Case changes Промене величине слова Character level Ниво знака diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index ad2074a..dd84ba9 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2039,6 +2039,8 @@ preview Title Örnek Border lines Kenarlık çizgileri Capitalize first letter of sentences Cümlelerin ilk harfini büyük yap Capitalize first letter of table cells Tablo hücrelerinin ilk harfini büyük yap + Correct TWo INitial CApitals İKi BAşharfi büyük olan sözcükleri düzelt + Correct accidental usage of cAPS LOCK key cAPS LOCK tuşunun yanlışlıkla kullanımını düzelt Capitalize names of days Gün adlarını büyük harfle başlat Case changes Büyük/küçük harf değişiklikleri Character level Karakter düzeyi diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index f4829bc..bc82b66 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1440,6 +1440,8 @@ Border lines Лінії меж Capitalize first letter of sentences Перша літера речення велика Capitalize first letter of table cells Перша літера в клітинках таблиці велика + Correct TWo INitial CApitals Виправляти ДВі ВЕликі літери на початку слова + Correct accidental usage of cAPS LOCK key Виправляти випадкове натискання cAPS LOCK Capitalize names of days Назви днів із великої літери Case changes Зміни регістру Character level Рівень символів diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 86a1d77..ee6bbe4 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1440,6 +1440,8 @@ Border lines 边框线 Capitalize first letter of sentences 句首字母大写 Capitalize first letter of table cells 表格单元格首字母大写 + Correct TWo INitial CApitals 更正前两个字母连续大写 + Correct accidental usage of cAPS LOCK key 更正误按 Caps Lock 键造成的大小写错误 Capitalize names of days 星期名称首字母大写 Case changes 大小写更改 Character level 字符级 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 0138e34..fafd53e 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1429,6 +1429,8 @@ Border lines 框線 Capitalize first letter of sentences 句首字母大寫 Capitalize first letter of table cells 表格儲存格首字母大寫 + Correct TWo INitial CApitals 更正前兩個字母連續大寫 + Correct accidental usage of cAPS LOCK key 更正意外使用 Caps Lock 鍵造成的大小寫錯誤 Capitalize names of days 星期名稱首字母大寫 Case changes 大小寫變更 Character level 字元層級 diff --git a/docs/typing-parity.md b/docs/typing-parity.md index 8e64485..6acbd8b 100644 --- a/docs/typing-parity.md +++ b/docs/typing-parity.md @@ -1,6 +1,6 @@ # Typing parity with Word -> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (right-to-left letters typed into an empty paragraph make it right-to-left, #713; previously minor: editing options: typing replaces selection, overtype, #485; previously minor: Move Paragraph Up/Down, Promote/Demote, Go Back and Alt+X keys, #343) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 +> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-12 · **Change:** minor (Correct TWo INitial CApitals, cAPS LOCK and table-cell capitals, #544; previously trivial: complex-script caret and Backspace, #604; Move Paragraph Up/Down, Promote/Demote, Go Back and Alt+X keys, #343) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4 What Word does when you type, observed black-box (same keystrokes typed into Word and WordCraft, results read back paragraph by paragraph) and pinned by tests in @@ -63,6 +63,18 @@ Applied when a word is finished by a space, punctuation **or Enter** (`enter_fin after abbreviations (`e.g.`, `Dr.`, …) or initials (`J.`), and not for mixed-case words (`iPhone`) or single-letter labels (`a.`). `teh`→`the`, `i`→`I`, `(c)`→`©`, `(tm)`→`™`, `wait...`→`wait…`, smart quotes (`autocorrect_as_you_type`). +- Correct TWo INitial CApitals: a finished word of three or more letters with two capitals and + the rest lowercase loses its second capital (`WOrd`→`Word`, `TEh`→`The`); not the exceptions + (`IDs`, `CDs`, `PCs`, `TVs`, … plus the user's, `tools.autocorrect {"addInitialCapsException"}`) + (`two_initial_capitals_are_corrected`). +- Correct accidental usage of cAPS LOCK: `wORD`→`Word` (three or more letters; not `iOS`, + `mRNA`, …). Word also switches Caps Lock off; we only correct the word + (`accidental_caps_lock_is_corrected`). +- Capitalize first letter of table cells: the first word typed in a cell is capitalised like a + sentence start, under its own option (`first_letter_of_a_table_cell_is_capitalised`). +- Each fix is one undo step right after it, and each has its own switch (`twoInitialCaps`, + `capsLock`, `capCells` on `tools.autocorrect`; on by default) + (`capitalisation_fixes_undo_and_turn_off`). - `1/2 1/4 3/4` → `½ ¼ ¾`; `1st 22nd 13th` get superscript suffixes (`ordinals_become_superscript`). - `word -- word` → en dash, `word - word` → en dash, `word--word` → em dash (`dashes`). - Web addresses become links; following text isn't linked (`web_addresses_become_links`). @@ -128,7 +140,9 @@ Word. - Moving paragraphs with Track Changes on isn't recorded as a tracked move; whole table rows don't move yet (a selection across cells is refused). Go Back's places aren't mapped through later edits (they are clamped to the document). -- Not yet compared: Correct TWo INitial CApitals; the rows marked ¹ above. +- Not yet compared: keystroke-by-keystroke comparison of Correct TWo INitial CApitals, cAPS LOCK + and table-cell capitals (implemented from Word's AutoCorrect options; Word's exception list + not compared); the rows marked ¹ above. ## How to compare @@ -144,6 +158,7 @@ Word. | Date | Change | Summary | |---|---|---| +| 2026-10-12 | minor | AutoCorrect: Correct TWo INitial CApitals, accidental cAPS LOCK, capitalize first letter of table cells (#544) | | 2026-10-11 | minor | Text direction: right-to-left letters typed into an empty paragraph make it right-to-left; existing paragraphs keep theirs (#713) | | 2026-10-11 | trivial | Grammar proofing (not AutoCorrect) no longer suggests capitalising a sentence after `...` or `…` (`proof::tests::ellipses_do_not_start_sentences`) | | 2026-10-11 | minor | AutoFormat tables from `+---+`, arrow bullets, smileys inside words, undo after a border line (#546) |