Localization
Built-in culture management for both hosting models — a drop-in service, a culture selector component, and automatic persistence.
Which package to install
BlazOrbit.Localization.Wasm— for Blazor WebAssembly apps. Persists the chosen culture inlocalStorage.BlazOrbit.Localization.Server— for Blazor Server apps. Persists via an HTTP cookie and wires upRequestLocalizationOptions.
Both packages depend on the standard Microsoft.Extensions.Localization pipeline —
you still author .resx files the usual way. The packages only add the
persistence layer and a ready-made BOBCultureSelector component.
WASM setup
using System.Globalization;
using BlazOrbit.Localization.Wasm;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.Services.AddBlazOrbit();
builder.Services.AddBlazOrbitLocalizationWasm(opts =>
{
opts.SupportedCultures = [
new CultureInfo("en-US"),
new CultureInfo("es-ES"),
new CultureInfo("fr-FR")
];
opts.DefaultCulture = "en-US";
});
var host = builder.Build();
await host.UseBlazOrbitLocalizationWasm();
await host.RunAsync();UseBlazOrbitLocalizationWasm reads the persisted culture before any component
renders, so the first paint uses the right language.
Server setup
using System.Globalization;
using BlazOrbit.Localization.Server;
using Microsoft.AspNetCore.Localization;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddBlazOrbit();
builder.Services.AddBlazOrbitLocalizationServer(opts =>
{
opts.SupportedCultures = [
new CultureInfo("en-US"),
new CultureInfo("es-ES"),
new CultureInfo("fr-FR")
];
opts.DefaultCulture = "en-US";
});
var app = builder.Build();
app.UseRequestLocalization(
app.Services.GetRequiredService<IOptions<RequestLocalizationOptions>>().Value);
app.Run();
A CookieRequestCultureProvider is inserted at the front of the request
localization chain. A startup filter registers a
/blazor-ui/culture/{culture} endpoint the selector uses to set the cookie.
The BOBCultureSelector
Ships in both Localization packages. Two built-in variants:
BOBCultureSelectorVariant.Dropdown— a styled<select>with flags and culture names.BOBCultureSelectorVariant.Flags— one flag button per culture.
@* WASM *@
@using BlazOrbit.Components.Wasm
<BOBCultureSelector Variant="BOBCultureSelectorVariant.Dropdown" />
@* Server — same component, different namespace *@
@using BlazOrbit.Components.Server
<BOBCultureSelector Variant="BOBCultureSelectorVariant.Flags" />Using IStringLocalizer in components
Once localization is wired up, the standard ASP.NET approach works everywhere:
@inject IStringLocalizer<Greeting> L
<h1>@L["HelloWorld"]</h1>
<p>@L["WelcomeUser", user.Name]</p>
Organize the matching .resx files under Resources/Pages/Greeting.resx,
Greeting.es.resx, etc. — the ResourcesPath passed to the service
options dictates the root folder.
Manually switching culture
If you'd rather not use the selector, both persistence services (ILocalizationPersistence)
expose a SetStoredCultureAsync you can call from your own UI.
Sidecar translations projects
BlazOrbit ships its own strings in a separate package, BlazOrbit.Translations,
installed transitively by the localization integrations. You don't have to reference it explicitly —
adding BlazOrbit.Localization.Server or .Wasm pulls it in.
The same pattern is available for your own apps: instead of placing .resx files inside the
project that hosts your components, create a sidecar *.Translations class library and
register an assembly mapping. IStringLocalizer<T> still works with the original anchor
type — only the resource lookup is rerouted to the sidecar assembly.
Why split translations
- Strings change on a different cadence than code — translators can ship updates as patch versions of the translations package without touching the host app.
- The host assembly stays smaller and unaware of localization data.
- You can publish multiple translations packages (per-language, per-bounded-context) reusing the same anchor types.
Project layout
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>true</IsPackable>
<PackageId>MyApp.Translations</PackageId>
<RootNamespace>MyApp.Translations</RootNamespace>
<AssemblyName>MyApp.Translations</AssemblyName>
</PropertyGroup>
</Project>
Place the .resx files under Resources/<path>/<TypeName>.resx, mirroring
the namespace path of the anchor type relative to its source assembly. For an anchor
MyApp.Pages.HomePage in source assembly MyApp, the neutral resource lives at
Resources/Pages/HomePage.resx in the sidecar; the satellite cultures (HomePage.es.resx,
HomePage.fr-FR.resx) sit next to it. The .NET SDK packs each culture into its own
<culture>/MyApp.Translations.resources.dll automatically.
Wiring the assembly map
using BlazOrbit.Localization.Wasm;
builder.Services.AddBlazOrbitLocalizationWasm(opts =>
{
opts.SupportedCultures = [
new CultureInfo("en-US"),
new CultureInfo("es-ES"),
new CultureInfo("fr-FR")
];
opts.DefaultCulture = "en-US";
// Route IStringLocalizer<T> for types in MyApp to MyApp.Translations.dll.
opts.TranslationsAssemblies["MyApp"] = "MyApp.Translations";
});TranslationsAssemblies maps source-assembly simple names to translations-assembly simple
names. The default map already includes BlazOrbit's own entries; add one line per host assembly that
ships a sidecar. Anchor types from unmapped assemblies fall back to the standard
ResourceManagerStringLocalizerFactory behavior — they look up resources inside their own
assembly, the way Microsoft.Extensions.Localization does by default.
How the rerouting works
AddBlazOrbitLocalizationServer / AddBlazOrbitLocalizationWasm replace the default
IStringLocalizerFactory with ReroutedStringLocalizerFactory. When a component
injects IStringLocalizer<HomePage>, the factory:
- Reads the assembly that defines
HomePage(e.g.MyApp). - Looks up the mapped translations assembly (
MyApp.Translations). - Strips the source-assembly prefix from the type's full name (
Pages.HomePage). - Calls the inner factory with
baseName = "MyApp.Translations.Pages.HomePage"andlocation = "MyApp.Translations". - The standard
ResourceManagerStringLocalizerFactoryresolves the manifest atMyApp.Translations.Resources.Pages.HomePage.resourcesinMyApp.Translations.dll(and the matching satellite for non-neutral cultures).
No anchor types need to live in the sidecar — the marker is read from the original source assembly. You
can also register your own decorator if you want different rewriting rules; the public
ReroutedStringLocalizerFactory ctor takes any inner IStringLocalizerFactory and a
IReadOnlyDictionary<string, string> map.
Distribution
Ship the sidecar as its own NuGet package and reference it as a <PackageReference> from
your host package — consumers install only the host and get translations transitively. For non-shipping
sidecars (private apps, internal tools, the docs site), set <IsPackable>false</IsPackable> and use a
<ProjectReference> instead. Either way, the runtime surface is identical: the
*.Translations.dll just needs to be present beside the host at load time.
WASM size optimization
On Blazor WebAssembly, satellite assemblies are tree-shaken at publish time according to
SatelliteResourceLanguages. If your app only ships English and Spanish, declare it in the WASM
host csproj to drop unused cultures from the bundle:
<PropertyGroup>
<SatelliteResourceLanguages>en;es</SatelliteResourceLanguages>
</PropertyGroup>