From ac65e2172b16313ca712a275217b417fcf2fa394 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 8 Oct 2026 16:29:36 +0000 Subject: [PATCH] [patch] Title-case [IsTitleCase] values with invariant rules, not the thread culture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit IsTitleCaseAttribute lowercased with invariant rules but title-cased with CultureInfo.CurrentCulture, so "Istanbul" validated under en-US and failed under tr-TR and az, where "i" title-cases to "İ". It now title-cases with CultureInfo.InvariantCulture, like the rest of the casing attributes. The test project turns off the invariant globalization ktsu.Sdk enables, so the culture test can set tr-TR and az at all. CA1304 and CA1307, which that switches on, are suppressed there: SemanticStringTests calls the culture-default overloads deliberately. Fixes ktsu-dev/Semantics#309 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Co2rZq6e7TjwF2FMUzNoXu --- .../Attributes/Casing/IsTitleCaseAttribute.cs | 5 +++-- Semantics.Test/CasingValidatorsTests.cs | 21 +++++++++++++++++++ Semantics.Test/Semantics.Test.csproj | 7 +++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/Semantics.Strings/Validation/Attributes/Casing/IsTitleCaseAttribute.cs b/Semantics.Strings/Validation/Attributes/Casing/IsTitleCaseAttribute.cs index 9a81628d..93ed2086 100644 --- a/Semantics.Strings/Validation/Attributes/Casing/IsTitleCaseAttribute.cs +++ b/Semantics.Strings/Validation/Attributes/Casing/IsTitleCaseAttribute.cs @@ -39,8 +39,9 @@ protected override ValidationResult ValidateValue(string value) return ValidationResult.Success(); } - // Use TextInfo.ToTitleCase and compare with original - string titleCase = CultureInfo.CurrentCulture.TextInfo.ToTitleCase(value.ToLowerInvariant()); + // Title-case with invariant rules, as the lowercasing does, so the result does not depend + // on the thread culture (Turkish title-cases "istanbul" to "İstanbul") + string titleCase = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(value.ToLowerInvariant()); bool isValid = string.Equals(value, titleCase, StringComparison.Ordinal); return isValid ? ValidationResult.Success() diff --git a/Semantics.Test/CasingValidatorsTests.cs b/Semantics.Test/CasingValidatorsTests.cs index 014da889..cc41539b 100644 --- a/Semantics.Test/CasingValidatorsTests.cs +++ b/Semantics.Test/CasingValidatorsTests.cs @@ -2,6 +2,7 @@ namespace ktsu.Semantics.Test; +using System.Globalization; using ktsu.Semantics.Strings; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -66,6 +67,26 @@ public void TitleCase_Valid_Invalid_Empty() Assert.AreEqual("", empty.WeakString); } + [TestMethod] + [DataRow("en-US")] + [DataRow("tr-TR")] + [DataRow("az-Latn-AZ")] + public void TitleCase_DoesNotDependOnThreadCulture(string cultureName) + { + CultureInfo original = CultureInfo.CurrentCulture; + try + { + CultureInfo.CurrentCulture = new CultureInfo(cultureName); + TitleCaseString valid = SemanticString.Create("Istanbul Is Lovely"); + Assert.AreEqual("Istanbul Is Lovely", valid.WeakString); + Assert.ThrowsExactly(() => SemanticString.Create("istanbul")); + } + finally + { + CultureInfo.CurrentCulture = original; + } + } + [TestMethod] public void SentenceCase_Valid_Invalid_Empty() { diff --git a/Semantics.Test/Semantics.Test.csproj b/Semantics.Test/Semantics.Test.csproj index a1edefde..c9919f97 100644 --- a/Semantics.Test/Semantics.Test.csproj +++ b/Semantics.Test/Semantics.Test.csproj @@ -10,6 +10,13 @@ true true $(NoWarn);MSTEST0032 + + false + $(NoWarn);CA1304;CA1307