Set Up Localization — i18n for 17 Cultures
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).
Key naming convention
Section titled “Key naming convention”Localization keys use PascalCase segments separated by . (dots) for
hierarchy, with an optional : (colon) namespace prefix for resource scoping
(e.g., Showcase:Admin:Users.Columns.Email). Two-letter acronyms stay
uppercase (AI, ID); three or more letters use PascalCase (Api, Url).
See the key naming convention reference for the full specification including standard sections, shared key namespaces, and complete examples.
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.
Positional placeholders ({0}, {1}) and built-in formatters (plural,
conditional) are supported. Reflection-based member access ({0.Name}) is
disabled for security — use positional arguments only.
{ "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/{version}/localization/overrides | List overrides by resource and culture |
PUT | /api/{version}/localization/overrides/{resource}/{culture}/{key} | Create or update an override |
DELETE | /api/{version}/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