Skip to content

Normalize culture code casing in SetCultureInfo and SetPublishInfo - #23425

Merged
AndyButland merged 6 commits into
v17/devfrom
v17/bugfix/normalize-culture-code-casing
Jul 20, 2026
Merged

AndyButland merged 6 commits into
v17/devfrom
v17/bugfix/normalize-culture-code-casing

Conversation

@ronaldbarendse

Copy link
Copy Markdown
Contributor

Problem

Culture codes set on content via the low-level SetCultureInfo / SetPublishInfo extensions are stored with whatever casing the caller provides. Since #19290 (which resolved #19287) normalizes the casing in SaveAndPublish and SetCultureName, but not in these two setters, a programmatic caller (e.g. Umbraco Deploy importing older, lower-cased culture codes) can end up with the draft (CultureInfos) and published (PublishCultureInfos) collections holding the same culture with different casing.

DocumentRepository.GetDocumentVariationDtos then builds its rows from AvailableCultures.Union(PublishedCultures) using the default (ordinal, case-sensitive) comparer, so {da-dk} ∪ {da-DK} yields two entries that both resolve (case-insensitively) to the same language id, producing a duplicate row and a unique constraint violation on umbracoDocumentCultureVariation (nodeId, languageId).

This is the root cause behind umbraco/Umbraco.Deploy.Issues#280 and umbraco/Umbraco.Deploy.Issues#271.

Fix

Normalize the culture code at the input boundary by calling EnsureCultureCode() in SetCultureInfo and SetPublishInfo, completing the approach started in #19290. Both collections now always store canonical casing, so any consumer (including GetDocumentVariationDtos) is safe regardless of how the culture was originally cased.

Also switches EnsureCultureCode from new CultureInfo(culture) to the cached CultureInfo.GetCultureInfo(culture) to avoid an allocation per culture on hot paths such as content materialization (these setters are also used when reading content from the database). Behaviour is identical for all valid, hyphenated culture codes and for invalid codes (both throw CultureNotFoundException).

Testing

Existing StringExtensionsTests for EnsureCultureCode continue to pass. The scenario can be reproduced by setting a culture with non-canonical casing (e.g. content.SetCultureInfo("en-gb", ...)) and publishing.

Copilot AI review requested due to automatic review settings July 20, 2026 10:06
@claude

claude Bot commented Jul 20, 2026 •

Copy link
Copy Markdown

Claude finished @ronaldbarendse's task in 3m 37s —— View job


PR Review

Target: origin/v17/dev · Based on commit: 60832b3

Closes the casing-mismatch gap in SetCultureInfo / SetPublishInfo by normalising the culture code at the input boundary via EnsureCultureCode(), completing the approach begun in #19290. Also replaces new CultureInfo(culture) with the cached CultureInfo.GetCultureInfo(culture) in EnsureCultureCode itself.

  • Other changes: Callers that previously stored non-canonical culture codes (e.g. "en-gb" from Umbraco Deploy restoring older data) will now silently receive "en-GB" in CultureInfos/PublishCultureInfos — intentional and correct.

Suggestions

  • src/Umbraco.Core/Models/ContentRepositoryExtensions.cs:46: No new test covers the normalisation flowing through SetCultureInfo / SetPublishInfo. The existing EnsureCultureCode_ReturnsExpectedResult validates the string extension in isolation, but a unit test calling SetCultureInfo(content, "en-gb", ...) and asserting CultureInfos contains "en-GB" would pin the exact bug scenario and guard against the fix being accidentally dropped. (inline comment posted)

Approved

The fix is correct, well-targeted, and carries no breaking changes. The CultureInfo.GetCultureInfo caching improvement is a nice bonus on hot content-materialisation paths. The only gap is the missing regression test for the normalisation path — worth adding, but not a blocker.

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.

Pull request overview

This PR prevents duplicate culture entries caused by inconsistent culture-code casing by normalizing culture codes at the lowest-level setters used by the repository/content materialization paths.

Changes:

  • Normalize culture inputs in SetCultureInfo and SetPublishInfo via EnsureCultureCode() so CultureInfos and PublishCultureInfos store canonical casing consistently.
  • Optimize EnsureCultureCode to use CultureInfo.GetCultureInfo(...) (cached) instead of allocating new CultureInfo(...) per call.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
src/Umbraco.Core/Models/ContentRepositoryExtensions.cs Normalizes culture codes when setting draft and published culture info, preventing casing-based duplicates downstream.
src/Umbraco.Core/Extensions/StringExtensions.Culture.cs Switches culture normalization to cached CultureInfo.GetCultureInfo to avoid allocations while preserving behavior.

Comment thread src/Umbraco.Core/Models/ContentRepositoryExtensions.cs
@claude claude Bot added area/backend category/performance Fixes for performance (generally cpu or memory) fixes labels Jul 20, 2026

@AndyButland AndyButland 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.

Thanks @ronaldbarendse, this looks good to me. I just extended it with some further automated tests.

@AndyButland
AndyButland enabled auto-merge (squash) July 20, 2026 11:23
@sonarqubecloud

Copy link
Copy Markdown

@AndyButland
AndyButland merged commit 49040f6 into v17/dev Jul 20, 2026
29 of 30 checks passed
@AndyButland
AndyButland deleted the v17/bugfix/normalize-culture-code-casing branch July 20, 2026 13:03
@KevinJump KevinJump mentioned this pull request Sep 7, 2026
13 tasks done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/backend category/performance Fixes for performance (generally cpu or memory) fixes release/17.7.0 release/18.2.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants