Skip to content

fix(ui): stop MudBlazor from pinning the culture on MAUI hybrid heads (ADR-027) - #411

Merged
ivanball merged 1 commit into
mainfrom
fix/mudblazor-culture-pin-hybrid
Sep 15, 2026
Merged

ivanball merged 1 commit into
mainfrom
fix/mudblazor-culture-pin-hybrid

Conversation

@ivanball

Copy link
Copy Markdown
Owner

Problem

On the ADC Android app (MAUI Blazor Hybrid), switching the language does nothing once a signed-in page has rendered: the preference PUT succeeds and the WebView reloads (visible in prod telemetry as five PUT auth/preferences 204s each followed by a fresh hub connection), yet every page still renders in the launch language. The web heads are unaffected.

Root cause

MudBlazor 9.7+ (AbstractLocalizationInterceptor.InvariantLanguageResourceLocalizer.ReadInvariant, introduced by MudBlazor/MudBlazor#13466) reads its built-in English strings by assigning CultureInfo.CurrentUICulture = InvariantCulture and then assigning the previous value back. Both writes go to an AsyncLocal, so the calling thread leaves the read carrying an explicit culture that beats DefaultThreadCurrentUICulture from then on. On a hybrid head the renderer dispatches on the process's main thread, which nothing ever resets, so the first MudBlazor chrome string a page renders (pager label, bell badge, dialog close button) pins the app to its launch language: MauiCultureApplier sets the defaults to es, reloads, and every render still resolves en-US. This is exactly the trap ADR-027 Decision 10 already forbids for our own code, re-introduced by the MudBlazor bump (9.6.0 -> 9.9.0, #220/#288). Verified: MudBlazor.dll is the only non-BCL assembly in the trimmed Android Release output that references set_CurrentUICulture, and a standalone repro shows the defaults switch no longer reaching the thread after the swap.

Anonymous landing pages render almost no MudBlazor chrome while signed-in pages do, which is why it read as "works signed out, not signed in". Web heads never see it: request localization sets the culture per request, and a browser reload discards the WASM runtime.

Fix

AddUIShared installs InvariantMudLocalizationInterceptor through MudBlazor's AddLocalizationInterceptor (replace semantics, so AddMudServices order does not matter). Same resolution order as MudBlazor's default (English UI culture or no MudLocalizer reads the built-ins; other cultures go through ResxMudLocalizer with a built-in fallback), but the built-ins are read through a ResourceManager under an explicit invariant culture, which is what MudBlazor's swap was for, with no culture assignment.

Tests

  • InvariantMudLocalizationInterceptorTests (6): DI replacement, built-in read leaves the ExecutionContext instance unchanged, Spanish via MudTranslations plus built-in fallback for unknown keys, argument formatting, neutral en handling.
  • Canary: asserts MudBlazor's DefaultLocalizationInterceptor still writes the AsyncLocal; when it starts failing, upstream fixed it and the replacement can be retired.
  • Full MMCA.Common.UI.Tests suite: 970 passed. FACTS drift check: up to date.

Docs

ADR-027 Decision 10 amendment is on a Website branch (docs/adr-027-mudblazor-culture-pin), pending push. CHANGELOG.md Unreleased entry included here.

Follow-ups

  • Consumers pick this up with the next release + version-bump sweep (/push-release); the ADC 1.8.5 Play build should wait for it.
  • Worth an upstream MudBlazor issue: the restore should not assign CurrentUICulture on hybrid hosts.

🤖 Generated with Claude Code

… (ADR-027)

MudBlazor 9.7+ reads its built-in English strings by assigning CurrentUICulture to the
invariant culture and then assigning the previous value back. Both writes land in an
AsyncLocal, so the calling thread leaves the read carrying an explicit culture. On a MAUI
Blazor Hybrid head the renderer dispatches on the main thread, which nothing resets, so the
first MudBlazor chrome string a page rendered pinned the app to its launch language:
MauiCultureApplier set the thread defaults and reloaded the WebView, and the pinned
AsyncLocal still won. Signed-in pages render that chrome (pagers, the bell badge) while the
anonymous landing pages do not, which is why it surfaced as "works signed out, not signed in".

AddUIShared now installs InvariantMudLocalizationInterceptor via MudBlazor's
AddLocalizationInterceptor (replace semantics, order-independent): same resolution order as
the default, with the built-ins read through a ResourceManager under an explicit invariant
culture and no culture assignment. A canary test pins the upstream behaviour so the
replacement is retired when MudBlazor stops assigning the culture.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@ivanball
ivanball merged commit f4becd6 into main Sep 15, 2026
15 checks passed
@ivanball ivanball mentioned this pull request Sep 15, 2026
ivanball added a commit that referenced this pull request Sep 15, 2026
Scorecard levers §5/§12/§24/§23/§27/§16/§13 (#412) plus the MudBlazor culture-pin fix for MAUI hybrid heads (#411).

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant