From 724d541856e8f288246a60c5bd2e0425c9435fc7 Mon Sep 17 00:00:00 2001 From: Martin Ball Date: Mon, 2 Nov 2026 01:06:58 +0000 Subject: [PATCH] =?UTF-8?q?CP6.13:=20craft-libs#495=20review=20=E2=80=94?= =?UTF-8?q?=20tr=20grammar:=20Turkish=20case=20folding=20(=C4=B0st.,=20I?= =?UTF-8?q?=C5=9F=C4=B1k=20=C4=B1=C5=9F=C4=B1k),=20initials=20(A.=C5=9E.,?= =?UTF-8?q?=20T.C.)=20end=20no=20sentence,=20and=20a=20repeated=20word=20w?= =?UTF-8?q?ith=20an=20apostrophe=20(Ankara'da=20Ankara'da)=20is=20marked?= =?UTF-8?q?=20again?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01FypgCX8LsYYE8fsj4byBm2 --- crates/proof/src/tr/grammar.rs | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/crates/proof/src/tr/grammar.rs b/crates/proof/src/tr/grammar.rs index 873e07c1..81ba93c2 100644 --- a/crates/proof/src/tr/grammar.rs +++ b/crates/proof/src/tr/grammar.rs @@ -10,6 +10,7 @@ //! `ne ... ne` conjunctions should not have commas immediately before or after them. //! - **Doubled punctuation** (`..`, `,,`, `;;`). +use super::suggest::turkish_to_lower; use crate::{Issue, IssueKind, ProofLang, words}; /// Turkish reduplications and emphatic word pairings that are intentionally repeated. @@ -120,8 +121,8 @@ const ALLOWED_REPEATS: &[&str] = &[ /// Turkish abbreviations that end with a period and after which lower case may continue. const ABBREVIATIONS: &[&str] = &[ "vb", "vs", "vd", "bkz", "bknz", "ör", "örnek", "mad", "md", "s", "sf", "sn", "dk", "sa", "yy", "m", "km", "cm", "mm", "gr", "kg", "mg", "dr", - "prof", "doç", "yrd", "av", "ist", "ank", "izm", "cad", "sok", "mah", "apt", "no", "tel", "faks", "tl", "kr", "krş", "a.ş", "ltd", "şti", "t.c", - "tbmm", "meb", "möt", "mö", "ms", + "prof", "doç", "yrd", "av", "ist", "ank", "izm", "cad", "sok", "mah", "apt", "no", "tel", "faks", "tl", "kr", "krş", "ltd", "şti", "tbmm", "meb", + "möt", "mö", "ms", ]; /// Conjunctions that must not be preceded by a comma. @@ -152,9 +153,11 @@ pub fn check_with(text: &str, capital_first: bool) -> Vec { None => continue, }; let only_spaces = gap.chars().all(|c| c == ' ' || c == '\t'); - let lower1 = w1.to_lowercase(); - let lower2 = w2.to_lowercase(); - if only_spaces && w1.chars().all(char::is_alphabetic) && lower1 == lower2 && !ALLOWED_REPEATS.contains(&lower1.as_str()) { + let lower1 = turkish_to_lower(w1); + let lower2 = turkish_to_lower(w2); + // Words, not numbers or codes (`1 1`, `A1 A1`); `Ankara'da Ankara'da` is one. + let word = w1.chars().any(char::is_alphabetic) && !w1.chars().any(|c| c.is_numeric()); + if only_spaces && word && lower1 == lower2 && !ALLOWED_REPEATS.contains(&lower1.as_str()) { v.push(Issue { start: pair[1].0, end: pair[1].1, @@ -226,7 +229,7 @@ pub fn check_with(text: &str, capital_first: bool) -> Vec { // 3. Comma before or after conjunctions (`ali, ve veli` or `ve, veli`) for (i, &(w_start, w_end)) in ws.iter().enumerate() { let word = match text.get(w_start..w_end) { - Some(w) => w.to_lowercase(), + Some(w) => turkish_to_lower(w), None => continue, }; // Check comma before conjunction @@ -293,11 +296,14 @@ pub fn check_with(text: &str, capital_first: bool) -> Vec { // number before a period is an ordinal (`5. sınıf`, `21. yüzyıl`). let spaced = gap.rfind(['.', '?', '!']).and_then(|m| gap.get(m + 1..)).is_some_and(|rest| rest.contains(char::is_whitespace)); let ordinal = trimmed.ends_with('.') && prev_word.chars().any(|c| c.is_ascii_digit()); + // An initial (`A.Ş.`, `T.C.`, `M. Kemal`) ends no sentence. + let initial = trimmed.starts_with('.') && prev_word.chars().count() == 1; if (trimmed.ends_with('.') || trimmed.ends_with('?') || trimmed.ends_with('!')) && spaced && !ordinal + && !initial && !trimmed.contains("...") - && !ABBREVIATIONS.contains(&prev_word.to_lowercase().as_str()) + && !ABBREVIATIONS.contains(&turkish_to_lower(prev_word).as_str()) && let Some(w) = text.get(w_start..w_end) && w.chars().next().is_some_and(char::is_lowercase) { @@ -410,6 +416,18 @@ mod tests { } } + /// craft-libs#495 review: initials (`A.Ş.`, `T.C.`) and `İst.` end no sentence, and Turkish + /// case folding finds `Işık ışık`; a word with an apostrophe is still a repeat. + #[test] + fn turkish_case_and_initials() { + for text in ["Şirket A.Ş. bugün açıldı.", "O T.C. vatandaşı.", "Adres İst. caddesi."] { + assert!(check(text).is_empty(), "{text}: {:?}", check(text)); + } + for text in ["Işık ışık yandı.", "Ankara'da Ankara'da kaldık."] { + assert_eq!(check(text).iter().filter(|i| i.message == "Yinelenen sözcük").count(), 1, "{text}"); + } + } + /// Repeated numbers are not repeated words (craft-libs#488 review). #[test] fn repeated_numbers_are_not_flagged() {