From f922883aa50478bd74c860a2f7edb355d9a965ab Mon Sep 17 00:00:00 2001 From: Pablo Garcia Date: Fri, 7 Aug 2026 15:36:41 +0200 Subject: [PATCH 1/2] Determine Turkish casing from the locale name instead of collation TextInfo.NeedsTurkishCasing detected the Turkish dotted/dotless i casing rules by probing whether the culture collates U+0131 equal to 'I' under IgnoreCase. On Android the system ICU does not provide the collation data that probe depends on, so the check returned false for tr-TR and casing silently fell back to the non-Turkish path, making i.ToUpper() yield 'I' instead of U+0130. Fixes #106560 --- .../src/System/Globalization/TextInfo.Icu.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.Icu.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.Icu.cs index 0e04fb4dd1e9dd..3c5e215426e3a0 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.Icu.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.Icu.cs @@ -14,7 +14,20 @@ private static bool NeedsTurkishCasing(string localeName) { Debug.Assert(localeName != null); - return CultureInfo.GetCultureInfo(localeName).CompareInfo.Compare("\u0131", "I", CompareOptions.IgnoreCase) == 0; + // ICU applies the Turkish dotted/dotless "i" casing rules to the "tr" and "az" + // languages. This is determined from the locale name rather than by probing the + // collation tailoring, because some platforms (notably Android, which uses the + // system ICU) do not ship the collation data that probe relies on, which would + // silently fall back to non-Turkish casing. + ReadOnlySpan language = localeName.AsSpan(); + int separatorIndex = language.IndexOfAny('-', '_'); + if (separatorIndex >= 0) + { + language = language.Slice(0, separatorIndex); + } + + return language.Equals("tr", StringComparison.OrdinalIgnoreCase) || + language.Equals("az", StringComparison.OrdinalIgnoreCase); } internal unsafe void IcuChangeCase(char* src, int srcLen, char* dstBuffer, int dstBufferCapacity, bool bToUpper) From 8979933bce8141fbee12056e146a2630f7fcb11b Mon Sep 17 00:00:00 2001 From: Pablo Garcia Date: Fri, 7 Aug 2026 15:38:22 +0200 Subject: [PATCH 2/2] Enable the Turkish casing TextInfo cases on Android These ToLower/ToUpper cases were skipped on Android and LinuxBionic because the collation-based Turkish detection did not work there. The casing path no longer depends on collation data, so they can run. --- .../System/Globalization/TextInfoTests.cs | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/TextInfoTests.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/TextInfoTests.cs index 992beda98fc5ae..015ad986e9759d 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/TextInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/TextInfoTests.cs @@ -280,13 +280,9 @@ public static IEnumerable ToLower_TestData() foreach (string cultureName in GetTestLocales()) { - // Android has its own ICU, which doesn't work well with tr - if (!PlatformDetection.IsAndroid && !PlatformDetection.IsLinuxBionic) - { - yield return new object[] { cultureName, "I", "\u0131" }; - yield return new object[] { cultureName, "HI!", "h\u0131!" }; - yield return new object[] { cultureName, "HI\n\0H\u0130\t!", "h\u0131\n\0hi\u0009!" }; - } + yield return new object[] { cultureName, "I", "\u0131" }; + yield return new object[] { cultureName, "HI!", "h\u0131!" }; + yield return new object[] { cultureName, "HI\n\0H\u0130\t!", "h\u0131\n\0hi\u0009!" }; yield return new object[] { cultureName, "\u0130", "i" }; yield return new object[] { cultureName, "i", "i" }; @@ -294,7 +290,7 @@ public static IEnumerable ToLower_TestData() // ICU has special tailoring for the en-US-POSIX locale which treats "i" and "I" as different letters // instead of two letters with a case difference during collation. Make sure this doesn't confuse our - // casing implementation, which uses collation to understand if we need to do Turkish casing or not. + // casing implementation, which must apply Turkish casing only for the tr and az languages. if (!PlatformDetection.IsWindows && PlatformDetection.IsNotBrowser) { yield return new object[] { "en-US-POSIX", "I", "i" }; @@ -431,12 +427,8 @@ public static IEnumerable ToUpper_TestData() // Turkish i foreach (string cultureName in GetTestLocales()) { - // Android has its own ICU, which doesn't work well with tr - if (!PlatformDetection.IsAndroid && !PlatformDetection.IsLinuxBionic) - { - yield return new object[] { cultureName, "i", "\u0130" }; - yield return new object[] { cultureName, "H\u0131\n\0Hi\u0009!", "HI\n\0H\u0130\t!" }; - } + yield return new object[] { cultureName, "i", "\u0130" }; + yield return new object[] { cultureName, "H\u0131\n\0Hi\u0009!", "HI\n\0H\u0130\t!" }; yield return new object[] { cultureName, "\u0130", "\u0130" }; yield return new object[] { cultureName, "\u0131", "I" }; yield return new object[] { cultureName, "I", "I" }; @@ -444,7 +436,7 @@ public static IEnumerable ToUpper_TestData() // ICU has special tailoring for the en-US-POSIX locale which treats "i" and "I" as different letters // instead of two letters with a case difference during collation. Make sure this doesn't confuse our - // casing implementation, which uses collation to understand if we need to do Turkish casing or not. + // casing implementation, which must apply Turkish casing only for the tr and az languages. if (!PlatformDetection.IsWindows && PlatformDetection.IsNotBrowser) { yield return new object[] { "en-US-POSIX", "i", "I" };