Skip to content

Integration — ASP.NET Core, Pragmatic Host, EF Core

How to wire Pragmatic.Internationalization into a web application: DI registration, the culture middleware, the fluent I18NBuilder, the translation endpoint, localized ProblemDetails, and EF Core persistence.

Any web app that must resolve the request culture and format/localize accordingly.

builder.Services.AddPragmaticInternationalization(options =>
{
options.DefaultUICulture = CultureCode.EnglishUS;
options.SupportedCultures = [CultureCode.EnglishUS, CultureCode.Italian];
});
var app = builder.Build();
app.UsePragmaticInternationalization(); // culture middleware, before routing

Or bind options from configuration (section "I18N" by default):

builder.Services.AddPragmaticInternationalization(builder.Configuration);

AddPragmaticInternationalization registers:

ServiceLifetimeRole
SystemConfigProvidersingletonfallback config provider (Priority 0) reading I18NOptions
I18NConfigResolverscopedmerges all II18NConfigProviders by priority
GlobalizationFormatterscopedculture-aware formatting, follows I18NContext.Current
JSON convertersall i18n types on ConfigureHttpJsonOptions (see below)

It returns an I18NBuilder for further fluent configuration (see below).

UsePragmaticInternationalization() adds I18NContextMiddleware, which per request:

  1. Resolves the configuration from the provider chain (I18NConfigResolver).
  2. Applies a request culture override: query string (options.QueryStringKey, default culture) first, then Accept-Language (quality-ordered best match) — both validated against SupportedCultures.
  3. Sets I18NContext (and thread cultures), opens an I18N.Context.Resolve activity.
  4. Captures and restores the previous thread cultures after the request — no culture leaks between requests on the same thread.

In a Pragmatic.Composition host — the middleware is auto-wired for you by InternationalizationStep (an IStartupStep with Order = 40, before routing).

await PragmaticApp.RunAsync(args, app =>
{
app.UseI18N(i18n =>
{
i18n.DefaultCulture(CultureCode.EnglishUS)
.Support(CultureCode.EnglishUS, CultureCode.Italian);
i18n.AddJsonTranslations("localization", watchForChanges: true);
i18n.LocalizeProblemDetails();
});
});

UseI18N calls AddPragmaticInternationalization() and hands you the same I18NBuilder. No manual UsePragmaticInternationalization() needed — the startup step adds the middleware.

MethodEffect
DefaultCulture(CultureCode)sets I18NOptions.DefaultUICulture
Support(params CultureCode[])sets I18NOptions.SupportedCultures
AddScope<TScope>()registers a typed ICultureScope default culture
AddConfigProvider<TProvider>()registers an II18NConfigProvider (scoped)
AddConfigProvider(II18NConfigProvider)registers a provider instance (singleton)
AddConfigProvider(Func<IServiceProvider, II18NConfigProvider>)registers a provider factory (scoped)
AddLocalizationProvider<TProvider>()registers an ILocalizationProvider (singleton)
AddLocalizationProvider(ILocalizationProvider)registers a provider instance
AddJsonTranslations(basePath?, watchForChanges)registers JsonLocalizationProvider (default path: I18NOptions.TranslationsPath, "translations")
LocalizeProblemDetails()replaces IErrorMessageResolver with LocalizedErrorMessageResolver

Configuration Providers (per-tenant / per-user culture)

Section titled “Configuration Providers (per-tenant / per-user culture)”

The culture must come from data — the tenant’s settings, the user profile — not from static options.

Implement II18NConfigProvider with a priority above 0; derive from CachedConfigProvider to get TTL caching (default 5 minutes) and invalidation for free:

public sealed class TenantConfigProvider(ITenantContext tenant) : CachedConfigProvider
{
public override int Priority => 100; // beats SystemConfigProvider (0)
protected override string GetCacheKey() => tenant.Id;
protected override I18NConfig? LoadConfiguration() =>
new() { DefaultUICulture = tenant.Culture }; // null properties = defer to lower priority
}
i18n.AddConfigProvider<TenantConfigProvider>();

I18NConfigResolver merges all providers highest-priority-first; each null property in an I18NConfig defers to the next provider down, ending at SystemConfigProvider (the options). Suggested priority convention: request 300 · user 200 · tenant 100 · system 0 (only the system provider is built in; the others are yours).

A SPA/frontend needs the same translations the backend knows, without duplicating JSON files.

app.MapPragmaticTranslations(); // GET /api/i18n/{culture}
app.MapPragmaticTranslations("/api/translations", cacheDurationSeconds: 600);

GET /api/i18n/it returns all merged translations from every registered ILocalizationProvider (via CompositeLocalizationProvider) for that culture, as a flat key → value map. Supports ?prefix=error.validation for lazy-loading a subset per boundary/page. Responses carry Cache-Control: public, max-age=300 (configurable). Returns 500 if no localization provider is registered.

Domain errors (Result failures) must reach the client in the caller’s language.

i18n.LocalizeProblemDetails();

Translation keys follow the error’s MessageKey (or, when resolving from a bare code, error. + the code lowercased with _.):

{
"error.room.unavailable": "Room is not available for the selected dates",
"error.room.unavailable.title": "Room Not Available"
}

LocalizedErrorMessageResolver resolves ProblemDetails detail from the error’s message key and title from {messageKey}.title, in the current request culture, with {param} placeholders interpolated from Error.Parameters ("Cannot exceed {limit} nights" + Parameters["limit"] = 14"Cannot exceed 14 nights"). Missing keys fall back to the error’s own message.

Registered automatically on ASP.NET Core’s JSON options by AddPragmaticInternationalization. For a custom JsonSerializerOptions:

var options = new JsonSerializerOptions();
options.AddPragmaticInternationalization();

Covered types (plus their nullable variants): Money ({ "amount": 99.99, "currency": "USD" }), CurrencyCode, LanguageCode, CountryCode, CultureCode (as ISO/BCP-47 strings), LocalizedString (as a { "en": "...", "it": "..." } map).

DataAnnotations for money DTOs — no registration needed:

public sealed record CreateInvoiceRequest
{
[Required, PositiveMoney] // Amount > 0 (null passes; add [Required])
public Money? Total { get; init; }
[NonNegativeMoney] // Amount >= 0
public Money? Discount { get; init; }
[SupportedCurrency("EUR", "USD")] // whitelist (Money or CurrencyCode)
public CurrencyCode Currency { get; init; }
}

Either per-context:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyPragmaticInternationalization();
}

or convention-based (EF Core 6+):

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.ApplyPragmaticInternationalizationConventions();
}
CLR typeColumn mapping
LocalizedStringJSON string ({"en":"...","it":"..."}), empty on null/empty
CurrencyCode / CurrencyCode?varchar(3) ISO code

Money has no automatic converter — a single-column mapping would lose either precision or the currency. Map it as two columns; MoneyConfiguration.DefaultPrecision (19) and MoneyConfiguration.DefaultScale (4) are the recommended constants:

builder.OwnsOne(i => i.Total, money =>
{
money.Property(m => m.Amount).HasPrecision(MoneyConfiguration.DefaultPrecision,
MoneyConfiguration.DefaultScale);
money.Property(m => m.Currency).HasMaxLength(3);
});

The module emits OpenTelemetry-friendly signals, all named under Pragmatic.Internationalization:

  • ActivitySource — the middleware opens an I18N.Context.Resolve activity per request, tagged with the resolved cultures.
  • Meter — counters pragmatic.i18n.key_lookups and pragmatic.i18n.missing_keys track translation lookups and misses at runtime (StringLocalizer).

Enable richer tracing with I18NOptions.EnableDiagnostics = true, and subscribe your OTel setup to the Pragmatic.Internationalization source/meter names.

  • Getting Started — culture context, Money, formatting
  • Translation Keys — the generated T class and localization providers
  • Troubleshooting — diagnostics and common failures
  • Pragmatic.Temporal.Internationalization — culture-aware formatting for the Temporal types (LocalDateTime, ZonedDateTime); LocalDate/LocalTime work out of the box via DateOnly/TimeOnly conversions