Skip to content

Fix TextInfo.ToTitleCase Dutch IJ digraph and U+2019 apostrophe handling - #133042

Open
tarekgh wants to merge 2 commits into
dotnet:mainfrom
tarekgh:fix-dutch-ij-neutral-titlecase
Open

Fix TextInfo.ToTitleCase Dutch IJ digraph and U+2019 apostrophe handling#133042
tarekgh wants to merge 2 commits into
dotnet:mainfrom
tarekgh:fix-dutch-ij-neutral-titlecase

Conversation

@tarekgh

@tarekgh tarekgh commented Sep 1, 2026

Copy link
Copy Markdown
Member

This PR fixes two independent TextInfo.ToTitleCase bugs.

1. Dutch IJ digraph for the neutral nl culture

TextInfo.ToTitleCase special-cases Dutch to capitalize the ij digraph to IJ. The check gated this on CultureName.StartsWith("nl-"), which matches specific cultures (nl-NL, nl-BE, ...) but never the neutral Dutch culture, whose name is exactly "nl" with no trailing -. As a result the neutral culture fell through to the generic path:

CultureInfo.GetCultureInfo("nl-NL").TextInfo.ToTitleCase("de ijsvogel"); // "De IJsvogel"
CultureInfo.GetCultureInfo("nl").TextInfo.ToTitleCase("de ijsvogel");    // "De Ijsvogel"  (wrong)

Fix

Match the neutral name exactly or the nl- prefix:

string cultureName = CultureName;
bool isDutchCulture = cultureName.StartsWith("nl", StringComparison.OrdinalIgnoreCase) &&
    (cultureName.Length == 2 || cultureName[2] == '-');

The ij digraph rule is a property of the Dutch language, not of a specific region, so every nl* culture (neutral and specific alike) should apply it.

Tests

Added neutral nl cases to the existing DutchTitleCaseInfo_TestData theory in TextInfoTests, including the issue's repro ("de ijsvogel" -> "De IJsvogel").

Fixes #133032.

2. U+2019 (and related quotes) treated as apostrophes

ToTitleCase only treated the ASCII apostrophe ' (U+0027) as an in-word character. The typographic curly apostrophe (U+2019) and similar characters have punctuation categories that are classified as word separators, so the letter after them was treated as the start of a new word:

CultureInfo.GetCultureInfo("en").TextInfo.ToTitleCase("Grandma’s pictures");
// before: "Grandma’S Pictures"   after: "Grandma’s Pictures"

Fix

Introduce an IsApostrophe helper and use it in the word scan. It covers the ASCII apostrophe plus the characters that share Unicode's Word_Break=MidNumLet property and are used as apostrophes:

private static bool IsApostrophe(char c)
{
    return c is '\'' or '\u2019' or '\u2018' or '\uFF07';
}
  • U+2019 RIGHT SINGLE QUOTATION MARK (the curly apostrophe)
  • U+2018 LEFT SINGLE QUOTATION MARK
  • U+FF07 FULLWIDTH APOSTROPHE

Modifier-letter apostrophes such as U+02BC already work because their category (Lm) is not a word separator. Dot/colon-style MidLetter joiners (., :, ·, ...) are intentionally left out of scope to avoid changing titlecasing of dotted tokens.

Tests

Added a ToTitleCase_Apostrophe theory covering U+0027, U+2019, U+2018, and U+FF07, plus a control case ("Grandma-s" -> "Grandma-S") confirming genuine separators still end the word.

Fixes #133034.

Performance

No impact. The Dutch check remains a single StartsWith over a 2-character literal plus one length/index check, evaluated once per call. IsApostrophe is a small constant-time comparison replacing the previous single-character equality in the same loop position.

Note

This PR description was drafted with GitHub Copilot.

ToTitleCase gated the Dutch ij->IJ digraph titlecasing on
CultureName.StartsWith("nl-"), which never matched the neutral Dutch
culture whose name is exactly "nl". As a result
GetCultureInfo("nl").TextInfo.ToTitleCase("de ijsvogel") returned
"De Ijsvogel" instead of "De IJsvogel".

Match the neutral name exactly or the "nl-" prefix. The check remains a
single StartsWith over a 2-char literal plus a length/index check,
evaluated once per call, so there is no performance impact.

Added neutral nl cases to the existing DutchTitleCaseInfo_TestData theory.

Fixes dotnet#133032.
Copilot AI lite review requested due to automatic review settings September 1, 2026 20:22
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

Pull request overview

Fixes Dutch TextInfo.ToTitleCase handling for the neutral Dutch culture ("nl") by treating it as Dutch for the IJ digraph titlecasing rule, aligning behavior with specific Dutch cultures like "nl-NL".

Changes:

  • Update the Dutch-culture detection in TextInfo.ToTitleCase to match "nl" and "nl-*" (case-insensitive).
  • Extend existing Dutch titlecasing test data to cover the neutral "nl" culture, including a repro-style phrase case.
File summaries
File Description
src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.cs Broadens the Dutch-culture gate so the IJ digraph rule applies to neutral "nl" in addition to "nl-*" cultures.
src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/TextInfoTests.cs Adds neutral "nl" cases to existing Dutch titlecasing theory data, covering both token and sentence inputs.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 0
  • Review effort level: Lite

TextInfo.ToTitleCase only treated the ASCII apostrophe (U+0027) as an in-word character, so U+2019 (typographic curly apostrophe) and similar characters were classified as word separators, producing output like 'Grandma'S Pictures'. Add an IsApostrophe helper covering U+0027, U+2019, U+2018 and U+FF07 (all Word_Break=MidNumLet) and add tests.

Fixes dotnet#133034
Copilot AI review requested due to automatic review settings September 1, 2026 23:27
@tarekgh tarekgh changed the title Fix TextInfo.ToTitleCase Dutch IJ digraph for neutral nl culture Fix TextInfo.ToTitleCase Dutch IJ digraph and U+2019 apostrophe handling Sep 1, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The behavioral changes are narrowly scoped, match the intended scenarios, and are covered by new targeted tests.

Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines +858 to +864
// Characters that behave like an apostrophe within a word (e.g. contractions such
// as "can't" or possessives such as "Grandma's") and therefore must not be treated
// as a word separator during titlecasing. These share Unicode's Word_Break=MidNumLet
// property with the ASCII apostrophe (U+0027):
// U+2019 RIGHT SINGLE QUOTATION MARK - the typographic curly apostrophe
// U+2018 LEFT SINGLE QUOTATION MARK
// U+FF07 FULLWIDTH APOSTROPHE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TextInfo.ToTitleCase does not handle U+2019 correctly TextInfo.ToTitleCase mishandles Dutch IJ digraph on neutral nl culture

2 participants