Feature

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 in localStorage.
  • BlazOrbit.Localization.Server — for Blazor Server apps. Persists via an HTTP cookie and wires up RequestLocalizationOptions.

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

Program.cs
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

Program.cs
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.
MainLayout.razor
@* 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:

Greeting.razor
@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

MyApp.Translations/MyApp.Translations.csproj
<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

Program.cs
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:

  1. Reads the assembly that defines HomePage (e.g. MyApp).
  2. Looks up the mapped translations assembly (MyApp.Translations).
  3. Strips the source-assembly prefix from the type's full name (Pages.HomePage).
  4. Calls the inner factory with baseName = "MyApp.Translations.Pages.HomePage" and location = "MyApp.Translations".
  5. The standard ResourceManagerStringLocalizerFactory resolves the manifest at MyApp.Translations.Resources.Pages.HomePage.resources in MyApp.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:

MyApp.Wasm.csproj
<PropertyGroup>
  <SatelliteResourceLanguages>en;es</SatelliteResourceLanguages>
</PropertyGroup>