Skip to content

Granit.Localization

Granit.Localization provides a modular, JSON-based internationalization system for .NET applications. Resources are embedded as JSON files, auto-discovered at startup, and served via IStringLocalizer<T>. Translation overrides live in a database (EF Core) and are cached in-memory with per-tenant isolation. A Roslyn source generator produces strongly-typed key constants at build time, eliminating magic strings.

  • DirectoryGranit.Localization/ Core abstractions, JSON resource loading, auto-discovery, override caching
    • Granit.Localization.EntityFrameworkCore EF Core persistence for translation overrides
    • Granit.Localization.Endpoints Minimal API (SPA bootstrapping + override CRUD)
    • Granit.Localization.SourceGenerator Roslyn incremental generator for type-safe keys
PackageRoleDepends on
Granit.LocalizationGranitLocalizationModule, JSON localizer, override store abstractionsGranit.Core
Granit.Localization.EntityFrameworkCoreGranitLocalizationOverridesDbContext, EfCoreLocalizationOverrideStoreGranit.Localization, Granit.Persistence
Granit.Localization.EndpointsAnonymous SPA endpoint, override CRUD endpointsGranit.Localization, Granit.Authorization
Granit.Localization.SourceGeneratorBuild-time LocalizationKeys class generation(analyzer, no runtime dependency)
graph TD
    L[Granit.Localization] --> CO[Granit.Core]
    EF[Granit.Localization.EntityFrameworkCore] --> L
    EF --> P[Granit.Persistence]
    EP[Granit.Localization.Endpoints] --> L
    EP --> A[Granit.Authorization]
    SG[Granit.Localization.SourceGenerator] -.->|build-time| L
[DependsOn(typeof(GranitLocalizationModule))]
public class AppModule : GranitModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.Configure<GranitLocalizationOptions>(options =>
{
// Register your application resource
options.Resources
.Add<AcmeLocalizationResource>("fr")
.AddJson(
typeof(AcmeLocalizationResource).Assembly,
"Acme.App.Localization.Acme");
// Add supported languages
options.Languages.Add(new LanguageInfo("nl", "Nederlands", "nl"));
options.Languages.Add(new LanguageInfo("de", "Deutsch", "de"));
});
}
}

Default languages registered by the module: fr, fr-CA, en (default), en-GB.

Each module registers a marker type and its embedded JSON files via GranitLocalizationOptions.Resources:

options.Resources
.Add<AcmeLocalizationResource>("fr") // default culture = "fr"
.AddJson(
typeof(AcmeLocalizationResource).Assembly,
"Acme.App.Localization.Acme") // embedded resource prefix
.AddBaseTypes(typeof(GranitLocalizationResource)); // inherit Granit keys

The LocalizationResourceStore holds all registered resources. Each entry is a LocalizationResourceInfo with:

PropertyDescription
ResourceTypeMarker type (empty class)
DefaultCultureFallback culture (e.g. "fr")
BaseTypesParent resources for key inheritance
JsonSourcesEmbedded JSON file locations

When EnableAutoDiscovery = true (default), loaded module assemblies are scanned for types decorated with [LocalizationResourceName]. Their embedded JSON files are registered automatically without explicit AddJson() calls.

Files follow the Granit convention: { "culture": "...", "texts": { ... } }. Nested keys are flattened with . separators.

Localization/Acme/en.json
{
"culture": "en",
"texts": {
"Patient": {
"Created": "Patient {0} created successfully.",
"NotFound": "Patient not found."
},
"Validation": {
"NissRequired": "National identification number is required."
}
}
}

Usage in code:

public class PatientHandler(IStringLocalizer<AcmeLocalizationResource> l)
{
public string GetMessage(string name) =>
l["Patient.Created", name];
}

Available languages are declared via GranitLocalizationOptions.Languages:

new LanguageInfo(
cultureName: "fr-CA",
displayName: "Francais (Canada)",
flagIcon: "ca",
isDefault: false)
PropertyDescription
CultureNameBCP 47 culture code ("fr", "en-GB")
DisplayNameUI-friendly name
FlagIconOptional icon identifier for the language selector
IsDefaultPre-selected language in the UI

The Languages list also drives SupportedUICultures for ASP.NET Core request localization (UseGranitRequestLocalization).

Overrides allow administrators to customize translations at runtime without redeploying. They are stored per resource, per culture, per tenant.

public interface ILocalizationOverrideStoreReader
{
Task<IReadOnlyDictionary<string, string>> GetOverridesAsync(
string resourceName, string culture,
CancellationToken cancellationToken = default);
}
public interface ILocalizationOverrideStoreWriter
{
Task SetOverrideAsync(
string resourceName, string culture, string key, string value,
CancellationToken cancellationToken = default);
Task RemoveOverrideAsync(
string resourceName, string culture, string key,
CancellationToken cancellationToken = default);
}

The CachedLocalizationOverrideStore wraps the EF Core store with an IMemoryCache layer. Cache keys are tenant-scoped (localization:{tenantId}:{resource}:{culture}) and invalidated on every write.

sequenceDiagram
    participant L as IStringLocalizer
    participant C as CachedLocalizationOverrideStore
    participant M as IMemoryCache
    participant EF as EfCoreLocalizationOverrideStore

    L->>C: GetOverridesAsync("Acme", "fr")
    C->>M: TryGetValue(cacheKey)
    alt Cache hit
        M-->>C: overrides
    else Cache miss
        C->>EF: GetOverridesAsync("Acme", "fr")
        EF-->>C: overrides
        C->>M: Set(cacheKey, overrides, TTL)
    end
    C-->>L: overrides

GET /localization?cultureName=fr returns all registered resources for the requested culture, plus the list of available languages. This endpoint is anonymous and includes cache headers (Cache-Control: public, max-age=3600, Vary: Accept-Language).

{
"currentCulture": "fr",
"resources": {
"Acme": {
"Patient.Created": "Patient {0} cree avec succes.",
"Patient.NotFound": "Patient introuvable."
},
"Granit": {
"EntityNotFound": "Entite introuvable."
}
},
"languages": [
{ "cultureName": "fr", "displayName": "Francais (France)", "flagIcon": "fr", "isDefault": false },
{ "cultureName": "en", "displayName": "English (United States)", "flagIcon": "us", "isDefault": true }
]
}

All override endpoints require the Localization.Overrides.Manage permission.

MethodRouteDescription
GET/localization/overrides?resourceName=X&cultureName=frList overrides for a resource/culture
PUT/localization/overrides/{resourceName}/{cultureName}/{key}Create or update an override
DELETE/localization/overrides/{resourceName}/{cultureName}/{key}Remove an override

If no ILocalizationOverrideStoreReader/Writer is registered (EF Core package not installed), all override endpoints return 501 Not Implemented.

The Granit.Localization.SourceGenerator package provides a Roslyn incremental generator that reads JSON localization files declared as <AdditionalFiles> and produces a LocalizationKeys class with nested constant string fields.

Acme.App.csproj
<ItemGroup>
<ProjectReference Include="..\Granit.Localization.SourceGenerator\Granit.Localization.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="Localization\Acme\en.json" />
</ItemGroup>

Given the JSON file from the example above, the generator produces:

LocalizationKeys.g.cs (auto-generated)
public static class LocalizationKeys
{
public static class Patient
{
public const string Created = "Patient.Created";
public const string NotFound = "Patient.NotFound";
}
public static class Validation
{
public const string NissRequired = "Validation.NissRequired";
}
}

Usage with IStringLocalizer:

string message = localizer[LocalizationKeys.Patient.Created, patientName];

Keys with the Resource:Key convention (e.g., "Granit:EntityNotFound") produce nested classes matching the resource prefix.

CategoryKey typesPackage
ModuleGranitLocalizationModule, GranitLocalizationEntityFrameworkCoreModule, GranitLocalizationEndpointsModule
CoreLocalizationResourceStore, LocalizationResourceInfo, LanguageInfoGranit.Localization
OptionsGranitLocalizationOptions (EnableAutoDiscovery, Resources, Languages, FormattingCultures)Granit.Localization
AbstractionsILocalizationOverrideStoreReader, ILocalizationOverrideStoreWriterGranit.Localization
CachingCachedLocalizationOverrideStore (internal)Granit.Localization
EF CoreGranitLocalizationOverridesDbContext, EfCoreLocalizationOverrideStore (internal)Granit.Localization.EntityFrameworkCore
EndpointsMapGranitLocalization(), MapGranitLocalizationOverrides()Granit.Localization.Endpoints
GeneratorLocalizationKeysGeneratorGranit.Localization.SourceGenerator