diff --git a/crates/proof/src/tr/grammar.rs b/crates/proof/src/tr/grammar.rs index b3383f30..873e07c1 100644 --- a/crates/proof/src/tr/grammar.rs +++ b/crates/proof/src/tr/grammar.rs @@ -128,7 +128,7 @@ const ABBREVIATIONS: &[&str] = &[ const CONJUNCTIONS_NO_PRE_COMMA: &[&str] = &["ve", "veya", "yahut", "velev", "veyahut", "de", "da"]; /// Conjunctions that must not be followed by a comma. -const CONJUNCTIONS_NO_POST_COMMA: &[&str] = &["ve", "veya", "yahut", "veyahut", "hem", "ne", "ya", "gerek", "ister", "de", "da"]; +const CONJUNCTIONS_NO_POST_COMMA: &[&str] = &["ve", "veya", "yahut", "veyahut", "hem", "gerek", "ister"]; /// Check grammar and punctuation in a Turkish text block. pub fn check(text: &str) -> Vec { @@ -154,7 +154,7 @@ pub fn check_with(text: &str, capital_first: bool) -> Vec { let only_spaces = gap.chars().all(|c| c == ' ' || c == '\t'); let lower1 = w1.to_lowercase(); let lower2 = w2.to_lowercase(); - if only_spaces && lower1 == lower2 && !ALLOWED_REPEATS.contains(&lower1.as_str()) { + if only_spaces && w1.chars().all(char::is_alphabetic) && lower1 == lower2 && !ALLOWED_REPEATS.contains(&lower1.as_str()) { v.push(Issue { start: pair[1].0, end: pair[1].1, @@ -395,10 +395,27 @@ mod tests { assert_eq!(issues.len(), 1); assert!(issues[0].message.contains("virgül konulmaz")); - let text2 = "Bana kalırsa bu mesele de, haklı olan taraf bizdik."; + let text2 = "Elma ve, armut."; let issues2 = check(text2); assert_eq!(issues2.len(), 1); - assert!(issues2[0].message.contains("'de' bağlacından sonra virgül konulmaz")); + assert!(issues2[0].message.contains("'ve' bağlacından sonra virgül konulmaz")); + } + + /// `de`/`da`, `ne` and `ya` often take a comma after them in correct Turkish, so no mark + /// (craft-libs#488 review). + #[test] + fn a_comma_after_de_ne_or_ya_is_fine() { + for text in ["Ali de, Veli de geldi.", "Ne, sen mi geldin?", "Ya, bilmiyorum.", "Bu mesele de, haklı olan bizdik."] { + assert!(check(text).is_empty(), "{text}: {:?}", check(text)); + } + } + + /// Repeated numbers are not repeated words (craft-libs#488 review). + #[test] + fn repeated_numbers_are_not_flagged() { + for text in ["Skor 1 1 bitti.", "Toplam 100 100 değil.", "Oda A1 A1 değil."] { + assert!(check(text).is_empty(), "{text}: {:?}", check(text)); + } } #[test] diff --git a/crates/proof/src/tr/mod.rs b/crates/proof/src/tr/mod.rs index 967148d8..5472fbe2 100644 --- a/crates/proof/src/tr/mod.rs +++ b/crates/proof/src/tr/mod.rs @@ -78,12 +78,8 @@ fn check(d: &TrDict, word: &str, opts: &crate::Options) -> bool { } let w = word.trim_matches(|c: char| !c.is_alphanumeric() && c != '\'' && c != '’'); let w = w.trim_matches(['\'', '’']); - let n = w.chars().count(); - if n <= 1 || (opts.ignore_numbers && w.chars().any(|c| c.is_ascii_digit())) || !w.chars().any(char::is_alphabetic) { - return true; - } - // Check user dictionary first - if crate::in_user_dictionary(&suggest::turkish_to_lower(w)) || crate::in_user_dictionary(&w.to_lowercase()) { + // One letter, no letters, digits, capitals (Ignore words in UPPERCASE) or the user dictionary. + if crate::accepted_by_options(w, opts) || crate::in_user_dictionary(&suggest::turkish_to_lower(w)) { return true; } @@ -161,4 +157,15 @@ mod tests { assert!(crate::is_correct_in_with("2026", lang, &check_digits), "numbers alone stay correct"); assert!(is_correct("kitapp2"), "direct checks keep the defaults"); } + + /// Ignore words in UPPERCASE reaches Turkish's own entry points too, so `NATO` and `TBMM` are + /// right by default and checked when the option is off (craft-libs#495 review). + #[test] + fn turkish_ignores_capitals_unless_told_otherwise() { + test_dict::install(); + let check_caps = crate::Options { ignore_uppercase: false, ..crate::Options::default() }; + assert!(is_correct("TBMMX") && is_correct_with_options("TBMMX", &crate::Options::default())); + assert!(!is_correct_with_options("TBMMX", &check_caps)); + assert!(is_correct_with_options("KİTAP", &check_caps), "a word in Turkish capitals is still looked up"); + } } diff --git a/crates/proof/src/tr/suggest.rs b/crates/proof/src/tr/suggest.rs index 4f8bd68d..70cf6962 100644 --- a/crates/proof/src/tr/suggest.rs +++ b/crates/proof/src/tr/suggest.rs @@ -70,7 +70,7 @@ pub fn suggest_with(d: &TrDict, word: &str, max: usize) -> Vec { // 1. Check direct common replacement table for &(wrong, right) in COMMON_REPLACEMENTS { if lower == wrong && seen.insert(right.to_string()) { - suggestions.push(right.to_string()); + suggestions.push(match_casing(word, right)); if suggestions.len() >= max { return suggestions; } @@ -270,7 +270,8 @@ pub fn turkish_to_upper(s: &str) -> String { /// Match original casing on suggestion. fn match_casing(orig: &str, cand: &str) -> String { - if orig.chars().all(char::is_uppercase) { + let mut letters = orig.chars().filter(|c| c.is_alphabetic()).peekable(); + if letters.peek().is_some() && letters.all(char::is_uppercase) { turkish_to_upper(cand) } else if orig.chars().next().is_some_and(char::is_uppercase) { super::grammar::turkish_capitalize(cand) @@ -329,7 +330,10 @@ mod tests { fn two_words_get_only_the_tables_replacement() { let d = TrDict::from_hunspell("SET UTF-8\n", "2\nbirkaç\nsonra\n").unwrap(); assert_eq!(suggest_with(&d, "bir kaç", 3), vec!["birkaç".to_string()]); - assert_eq!(suggest_with(&d, "Bir kaç", 3), vec!["birkaç".to_string()]); + // craft-libs#487 review: the table keeps the word's case. + assert_eq!(suggest_with(&d, "Bir kaç", 3), vec!["Birkaç".to_string()]); + assert_eq!(suggest_with(&d, "BİR KAÇ", 3), vec!["BİRKAÇ".to_string()]); + assert_eq!(suggest_with(&d, "Herşey", 3), vec!["Her şey".to_string()]); assert!(suggest_with(&d, "sorna sorna", 3).is_empty()); }