Localization
Write a Module's messages in English and Persian, and run each HTTP request in the culture its client's Accept-Language header names.
PouryaKit.Localization lets a Module answer in English or Persian. A Feature injects IStringLocalizer<T> for a type of its own and reads each message by its key, from .resx files beside that type's file: the neutral file holds English and the .fa.resx file Persian. Each HTTP request runs in the culture its client's Accept-Language header names (What the Kit guarantees), and that culture flows through the Gateway into the Operation's handlers.
It is optional and adds no NuGet package: it references PouryaKit.Cqrs and the ASP.NET Core shared framework only. A Host that answers in English alone leaves it out, as both of the Proving Sample's Hosts do.
A delivery runs in the process's culture
A Notification Handler, an Integration Event Handler or a delayed or recurring command runs outside any HTTP request, so it does not read the culture of the request that caused it. What else stays unlocalized is under Limits and What the Kit guarantees.
When to use it
- A Module's messages must read in English and Persian.
- You read the request culture from the client's
Accept-Languageheader. - A cached query must answer each culture's callers in their own language.
- You test a localized message over HTTP.
How it works
Register it in the Composition Root
AddPouryaKitLocalization() on the Module's builder records the Module's assembly, registers the shared framework's localization with English and Persian, and puts the request localization middleware ahead of everything the application adds. The Host calls nothing in its pipeline.
Put the messages beside a marker type
Declare an empty internal sealed class first in its file, and put its .resx files beside it, named after it. The SDK compiles them into the Module's assembly and a Persian satellite assembly; there is no generated class.
Read a message by its key
Inject IStringLocalizer<T> for the marker type and read messages["Key"] in the request's culture. What a missing entry answers is under A Module writing localized messages.
In the Kit's tests
The test Module's greeting: a marker type, a query and a handler that reads its message in the request's culture.
// The Stub Module's localized messages: StubMessages.resx in English, StubMessages.fa.resx in Persian, beside this file.
internal sealed class StubMessages;
internal sealed record GreetQuery : Query<string>;
internal sealed class GreetQueryHandler(IStringLocalizer<StubMessages> messages)
: QueryHandler<GreetQuery, string>
{
public override Task<Result<string>> Handle(
GreetQuery query,
CancellationToken cancellationToken
) => Task.FromResult<Result<string>>(messages["Greeting"].Value);
}Guarantees and failures
| You want to know | Read |
|---|---|
| What the registration adds, and in which order | Registering |
| How the messages are named, compiled and found | A Module writing localized messages |
| Which header values select Persian, and what else is localized | What the Kit guarantees |
| What stays in English | Limits |
Next steps
Reference
PouryaKit.Localization: a Module writes its messages in English and Persian, and each HTTP request runs in the culture its client's Accept-Language header names, English by default. Namespace PouryaKit.Localization. It references PouryaKit.Cqrs and the ASP.NET Core shared framework only, so it adds no NuGet package. It is optional: a Host that answers in English alone does not reference it, as neither of the Proving Sample's Hosts does.
Registering
In the Composition Root of each Module that writes localized messages:
using PouryaKit.Localization;
module.AddPouryaKitLocalization();- On a Module's builder it names no assembly: it records the assembly of the Module's definition, where its Features live, for the Host, as every Kit registration that names Feature assemblies does (modules.md). An application without Modules calls it on its own services, naming its Feature assembly:
builder.Services.AddPouryaKitLocalization(typeof(Program).Assembly). - It registers the shared framework's localization (
IStringLocalizer<T>), English and Persian as the supported cultures with English the default, and the request localization middleware, which a startup filter puts ahead of every middleware the application adds. The Host calls nothing in its pipeline. A second call, from another Module or the Host, adds no second culture or middleware.
A Module writing localized messages
A Feature injects IStringLocalizer<T> for a type of its own and reads a message by its key. The messages sit in .resx files beside that type's file, named after it: the neutral file holds English, the .fa.resx file Persian. The SDK compiles them into the Module's assembly and a fa satellite assembly; there is no designer or generated class.
Greetings/
GreetingMessages.cs internal sealed class GreetingMessages;
GreetingMessages.resx Greeting = Welcome
GreetingMessages.fa.resx Greeting = خوش آمدیدusing Microsoft.Extensions.Localization;
using PouryaKit.Cqrs;
using PouryaKit.Domain;
internal sealed class GreetingMessages;
internal sealed record GreetQuery : Query<string>;
internal sealed class GreetQueryHandler(IStringLocalizer<GreetingMessages> messages) : QueryHandler<GreetQuery, string>
{
public override Task<Result<string>> Handle(GreetQuery query, CancellationToken cancellationToken) =>
Task.FromResult<Result<string>>(messages["Greeting"].Value);
}- Keep the marker type first in its file: the SDK names the compiled resources after the first class in the
.csfile the.resxsits beside, and the localizer looks them up byT's full name. - A key with no Persian entry falls back to English, and a key with no entry at all returns the key itself (
ResourceNotFoundis true). - An Error a handler returns may carry a localized message the same way; its code stays the same in every culture.
What the Kit guarantees
- A request whose
Accept-Languageheader prefers Persian,faor a regional variant such asfa-IR, over the other cultures the Host supports, by quality and then by listed order, runs in Persian (en;q=0.1, fa;q=1does), and its Feature's localizer reads the Persian message. A request with no header,enor a culture the Host does not support runs in English, and so does one whose only Persian value has quality 0 (fa;q=0) or comes after 16 other values: the provider reads the first 16 values, not the framework's default of three. The header is the only source: no query string or cookie changes the culture. - The culture flows through the Gateway into the Operation's handlers, which run in the request's culture.
- A cached query (caching.md) is stored per UI culture, so each culture's callers read their own answer.
- The Kit's own Error messages stay English in every culture.
Limits
- English and Persian only, fixed; there are no options to bind.
- A delivery (a Notification Handler, an Integration Event Handler, a delayed or recurring command) runs outside an HTTP request, in the process's culture, so it does not read the publishing request's culture.
- FluentValidation's built-in messages follow the request's culture too, and FluentValidation ships Persian ones; a message a Module's validator sets itself is its own to localize.
Testing
Send the request with the header and assert the message, as the Proving Sample's tests do against its Stub Module:
using var client = app.Client("caller");
client.DefaultRequestHeaders.Add("Accept-Language", "fa");
var message = await client.GetFromJsonAsync<string>("/stub/greeting", TestContext.Current.CancellationToken);
Assert.Equal("خوش آمدید", message);Send a plain-text mail from a Notification Handler through one contract, on the Host's MailKit sender configured once from validated options.
Reverse proxy
A YARP Host of its own process in front of a system's Hosts: routes and clusters from configuration, service discovery, a liveness probe, and one trace from the client to the Operation.