Normalize culture code casing in SetCultureInfo and SetPublishInfo - #23425
Conversation
|
Claude finished @ronaldbarendse's task in 3m 37s —— View job PR ReviewTarget: Closes the casing-mismatch gap in
Suggestions
ApprovedThe fix is correct, well-targeted, and carries no breaking changes. The |
There was a problem hiding this comment.
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
cultureinputs inSetCultureInfoandSetPublishInfoviaEnsureCultureCode()soCultureInfosandPublishCultureInfosstore canonical casing consistently. - Optimize
EnsureCultureCodeto useCultureInfo.GetCultureInfo(...)(cached) instead of allocatingnew 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. |
AndyButland
left a comment
There was a problem hiding this comment.
Thanks @ronaldbarendse, this looks good to me. I just extended it with some further automated tests.
|



Problem
Culture codes set on content via the low-level
SetCultureInfo/SetPublishInfoextensions are stored with whatever casing the caller provides. Since #19290 (which resolved #19287) normalizes the casing inSaveAndPublishandSetCultureName, 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.GetDocumentVariationDtosthen builds its rows fromAvailableCultures.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 onumbracoDocumentCultureVariation (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()inSetCultureInfoandSetPublishInfo, completing the approach started in #19290. Both collections now always store canonical casing, so any consumer (includingGetDocumentVariationDtos) is safe regardless of how the culture was originally cased.Also switches
EnsureCultureCodefromnew CultureInfo(culture)to the cachedCultureInfo.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 throwCultureNotFoundException).Testing
Existing
StringExtensionsTestsforEnsureCultureCodecontinue to pass. The scenario can be reproduced by setting a culture with non-canonical casing (e.g.content.SetCultureInfo("en-gb", ...)) and publishing.