Set up localization
Granit.Localization provides a modular JSON-based localization system that
integrates with IStringLocalizer<T>. Each package embeds its own translations,
supports culture fallback via CultureInfo.Parent, and allows runtime
overrides from the database without redeployment.
Prerequisites
Section titled “Prerequisites”- A .NET 10 project with Granit module system configured
- Familiarity with
IStringLocalizer<T>fromMicrosoft.Extensions.Localization - For runtime overrides: a PostgreSQL (or other EF Core-supported) database
Step 1 — Install packages
Section titled “Step 1 — Install packages”# Core localizationdotnet add package Granit.Localization
# Source-generated type-safe keys (optional but recommended)dotnet add package Granit.Localization.SourceGenerator
# EF Core override store (optional -- runtime corrections)dotnet add package Granit.Localization.EntityFrameworkCore
# HTTP endpoint for SPA clients (optional)dotnet add package Granit.Localization.EndpointsStep 2 — Create JSON resource files
Section titled “Step 2 — Create JSON resource files”Each JSON file contains translations for a single culture. Place them under
Localization/{ResourceName}/:
src/MyApp/ Localization/ MyApp/ en.json en-GB.json fr.json fr-CA.json nl.json de.json es.json it.json pt.json pt-BR.json zh.json ja.json pl.json tr.json ko.json sv.json cs.jsonFile format
Section titled “File format”{ "culture": "en", "texts": { "Patient:NotFound": "Patient not found.", "Validation": { "Required": "This field is required.", "MaxLength": "Maximum {0} characters." } }}Regional variants
Section titled “Regional variants”Regional files (fr-CA.json, en-GB.json, pt-BR.json) should only contain
keys that differ from the base language. The .NET native fallback
(CultureInfo.Parent) resolves automatically: fr-CA -> fr -> default
culture.
There is no need for an en-US.json file since en.json is already US English
(native fallback: en-US -> en).
Embed as resources
Section titled “Embed as resources”Mark the files as embedded resources in your .csproj:
<ItemGroup> <EmbeddedResource Include="Localization\**\*.json" /></ItemGroup>Step 3 — Define a resource marker class
Section titled “Step 3 — Define a resource marker class”Each localization resource is represented by an empty marker class:
using Granit.Localization.Attributes;
[LocalizationResourceName("MyApp")][InheritResource(typeof(GranitLocalizationResource))]public sealed class MyAppResource;The [InheritResource] attribute lets your app inherit base error messages
from GranitLocalizationResource (Granit:EntityNotFound,
Granit:ValidationError, etc.) without redefining them.
Step 4 — Register localization
Section titled “Step 4 — Register localization”[DependsOn(typeof(GranitLocalizationModule))]public sealed class MyAppModule : GranitModule{ public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.Configure<GranitLocalizationOptions>(options => { options.Resources .Add<MyAppResource>(defaultCulture: "en") .AddJson( typeof(MyAppModule).Assembly, "MyApp.Localization.MyApp") .AddBaseTypes(typeof(GranitLocalizationResource));
options.DefaultResourceType = typeof(MyAppResource);
options.Languages.Add(new LanguageInfo("en", "English", isDefault: true)); options.Languages.Add(new LanguageInfo("fr", "Francais")); options.Languages.Add(new LanguageInfo("nl", "Nederlands")); options.Languages.Add(new LanguageInfo("de", "Deutsch")); }); }}builder.Services.AddGranitLocalization();
builder.Services.Configure<GranitLocalizationOptions>(options =>{ options.Resources .Add<MyAppResource>(defaultCulture: "en") .AddJson(typeof(Program).Assembly, "MyApp.Localization.MyApp") .AddBaseTypes(typeof(GranitLocalizationResource));
options.DefaultResourceType = typeof(MyAppResource); options.Languages.Add(new LanguageInfo("en", "English", isDefault: true)); options.Languages.Add(new LanguageInfo("fr", "Francais"));});Step 5 — Configure request localization
Section titled “Step 5 — Configure request localization”Use Granit.Localization.Endpoints to auto-configure the ASP.NET Core
RequestLocalizationMiddleware:
app.UseGranitRequestLocalization();This reads the Languages list from GranitLocalizationOptions and sets
SupportedCultures, SupportedUICultures, and DefaultRequestCulture
accordingly.
Step 6 — Use IStringLocalizer
Section titled “Step 6 — Use IStringLocalizer”Inject IStringLocalizer<T> in any service or handler:
public sealed class PatientService(IStringLocalizer<MyAppResource> localizer){ public string GetNotFoundMessage(Guid id) => localizer["Patient:NotFound", id];}Culture fallback chain
Section titled “Culture fallback chain”Key resolution follows this order:
- Current UI culture (
CultureInfo.CurrentUICulture) — e.g.,fr-BE - Parent culture (
CultureInfo.Parent) — e.g.,fr - Ancestors up to
CultureInfo.InvariantCulture - Default culture of the resource (
defaultCultureinAdd<T>()) - Parent resources (inheritance chain)
Pluralization
Section titled “Pluralization”SmartFormat.NET provides automatic pluralization based on CLDR rules:
{ "culture": "en", "texts": { "Files:Count": "{0:No file|One file|{} files}" }}var zero = localizer["Files:Count", 0]; // "No file"var one = localizer["Files:Count", 1]; // "One file"var many = localizer["Files:Count", 42]; // "42 files"Source-generated keys
Section titled “Source-generated keys”The Granit.Localization.SourceGenerator package eliminates magic strings by
generating compile-time constants from your JSON files.
Configuration
Section titled “Configuration”Declare JSON files as AdditionalFiles in the consuming project’s .csproj:
<ItemGroup> <AdditionalFiles Include="Localization/**/*.json" /></ItemGroup>Generated code
Section titled “Generated code”From this JSON:
{ "culture": "en", "texts": { "Patient:NotFound": "Patient not found.", "Patient:Created": "Patient {0} created." }}The source generator produces:
public static class LocalizationKeys{ public static class Patient { public const string NotFound = "Patient:NotFound"; public const string Created = "Patient:Created"; }}Use the constants with full IDE autocompletion:
// Before -- magic stringstring message = localizer["Patient:NotFound"];
// After -- type-safe constantstring message = localizer[LocalizationKeys.Patient.NotFound];Runtime overrides (database)
Section titled “Runtime overrides (database)”Granit.Localization.EntityFrameworkCore enables runtime translation
corrections without redeployment. Overrides are stored in PostgreSQL and cached
in memory (5-minute TTL by default).
Set up the override store
Section titled “Set up the override store”[DependsOn(typeof(GranitLocalizationEntityFrameworkCoreModule))]public sealed class AppModule : GranitModule { }builder.AddGranitLocalizationEntityFrameworkCore(options => options.UseNpgsql(connectionString));Run the migration:
dotnet ef migrations add AddLocalizationOverridesdotnet ef database updateManage overrides programmatically
Section titled “Manage overrides programmatically”public sealed class TranslationAdminService( ILocalizationOverrideStoreReader storeReader, ILocalizationOverrideStoreWriter storeWriter){ public async Task CorrectTranslationAsync(CancellationToken cancellationToken) { // Override "Patient" with "Beneficiary" for French await storeWriter.SetOverrideAsync( "MyApp", "fr", "Patient.Title", "Beneficiaire", cancellationToken); }
public async Task RevertAsync(CancellationToken cancellationToken) { await storeWriter.RemoveOverrideAsync( "MyApp", "fr", "Patient.Title", cancellationToken); }}Override CRUD endpoints
Section titled “Override CRUD endpoints”Granit.Localization.Endpoints exposes admin endpoints protected by
Localization.Overrides.Manage:
| Method | Route | Description |
|---|---|---|
GET | /api/granit/localization/overrides | List overrides by resource and culture |
PUT | /api/granit/localization/overrides/{resource}/{culture}/{key} | Create or update an override |
DELETE | /api/granit/localization/overrides/{resource}/{culture}/{key} | Remove an override |
app.MapGranitLocalizationOverrides();Multi-tenancy
Section titled “Multi-tenancy”Overrides are isolated by tenant via ICurrentTenant. Each tenant sees only
its own overrides. Host-level overrides (TenantId = null) apply globally.
Supported cultures
Section titled “Supported cultures”Granit supports 17 cultures out of the box: 14 base languages plus 3 regional variants.
| Base languages (14) | Regional variants (3) |
|---|---|
| en, fr, nl, de, es, it, pt, zh, ja, pl, tr, ko, sv, cs | fr-CA, en-GB, pt-BR |
Every src/*/Localization/**/*.json must exist for all 17 cultures. Regional
files only contain keys that differ from the base.
Next steps
Section titled “Next steps”- Create document templates for culture-specific template rendering
- Configure blob storage — blob storage error messages are resolved via localization JSON files
- Localization reference for the complete API surface