Localization
Axiom localization wraps Microsoft.Extensions.Localization with a JSON based, file provider system. Define translation files per culture, register resource types, and resolve strings through the standard IStringLocalizer<T> with automatic parent culture fallback and support for dynamic runtime updates.
Two packages are involved:
dotnet add package Allegory.Axiom.Localization.Abstractions
dotnet add package Allegory.Axiom.LocalizationThe key terms to understand before diving in:
| Concept | Description |
|---|---|
| Resource | A marker class (MyAppResource) that identifies a named set of translations. |
| Translation file | A JSON file named after a culture code (e.g. en.json, tr-TR.json). |
| Default culture | The fallback culture used when no translation exists for the current culture chain. |
| Path | A virtual directory path resolved through the file provider. Multiple paths merge at the key level. |
| File provider | The source of translation files. Configured separately via FileProviderOptions. |
// 1. Register the file provider containing your translation files
builder.Services.Configure<FileProviderOptions>(options =>
{
options.AddEmbedded<MyAppPackage>();
});
// 2. Map your resource marker to a default culture and translation directory
builder.Services.Configure<LocalizationOptions>(options =>
{
options.Resources.Add<MyAppResource>(
defaultCulture: "en",
paths: "/Resources/Localization");
});
// 3. Inject IStringLocalizer<T> and resolve strings
public class OrderService(IStringLocalizer<MyAppResource> localizer) : ITransientService
{
public string Greet(string name) => localizer["welcome", name].Value;
}Translation Files
Translation files are JSON files named after a culture code, placed inside one or more registered directories.
Resources/
en.json
en-US.json
fr.json
tr.jsonEach file is a flat key-value map:
{
"greeting": "Hello!",
"farewell": "Goodbye!",
"welcome": "Welcome, {0}!"
}String formatting uses {0}, {1}, etc. passed as arguments when indexing the localizer.
Resource Registration
Resources are registered through LocalizationOptions in your application package. Each resource has a marker type (or name string), default culture and one or more translation directories.
internal sealed class MyAppPackage : IConfigureApplication
{
public static Task ConfigureAsync(IHostApplicationBuilder builder)
{
// Register embedded file provider that contains your translation files
builder.Services.Configure<FileProviderOptions>(options =>
{
options.AddEmbedded<MyAppPackage>();
});
// Register the localization resource
builder.Services.Configure<LocalizationOptions>(options =>
{
options.Resources.Add<MyAppResource>(
defaultCulture: "en",
paths: "/Resources/Localization");
// It maps resources with exception codes.
options.MapExceptionCode<MyAppResource>("App.Exceptions");
});
return Task.CompletedTask;
}
}MyAppResource is a plain marker class used to identify the resource:
public class MyAppResource { }You can specify multiple directories. All translation files are combined into a single set. If the same culture has duplicate keys, the values from files in later directories take precedence and override earlier ones.
options.Resources.Add<MyAppResource>(
defaultCulture: "en",
paths:
[
"/Resources/Localization/Base",
"/Resources/Localization/Overrides"
]);INFO
For registering embedded resources correctly, follow the Embedded Resources Setup section.
Resolving Strings
Inject IStringLocalizer<T> where T is your resource marker class:
public class OrderService(IStringLocalizer<MyAppResource> localizer) : ITransientService
{
public string GetGreeting(string name)
=> localizer["welcome", name].Value;
}IStringLocalizer<T> is backed by AxiomStringLocalizer for registered resources. Unregistered types fall back to the standard ResourceManagerStringLocalizer.
Culture Fallback
When a key is not found for the current culture, Axiom walks up the culture hierarchy until the key is found or the default culture is reached.
Given default culture en and current culture tr-TR:
tr-TR.json → tr.json → en.json (default)If the key exists in tr.json but not tr-TR.json, the tr.json value is returned. If not found in any Turkish culture file, the en.json value is used. If not found anywhere, the key itself is returned and ResourceNotFound is true.
CultureInfo.CurrentUICulture = new CultureInfo("tr-TR");
var result = localizer["greeting"];
// Checks: tr-TR.json → tr.json → en.json (default)Formatted Strings
Pass arguments after the key:
// en.json: { "welcome": "Welcome, {0}!" }
var result = localizer["welcome", "Alice"];
// result.Value → "Welcome, Alice!"GetAllStrings
Retrieve all strings for the current culture, optionally including parent cultures:
// Current culture only
var strings = localizer.GetAllStrings(includeParentCultures: false);
// Current culture + all parent cultures up to default
// Keys found in child cultures take priority over parents
var allStrings = localizer.GetAllStrings(includeParentCultures: true);When includeParentCultures: true, the hierarchy is walked from specific to general. The first occurrence of each key wins no duplicates.
Multiple Directories
Split translations across multiple directories and compose them per resource:
/Localization/
Base/
en.json ← shared strings
tr.json
Module/
en.json ← module-specific strings, override base if same key
tr.jsonoptions.Resources.Add<MyAppResource>("en", "/Localization/Base", "/Localization/Module");Directories are processed in order. Later directories take precedence for duplicate keys within the same culture file.
You can also add paths to an existing resource after initial registration:
options.Resources.Get<MyAppResource>().AddPaths("/Localization/Plugin");Dynamic Translations
IAxiomStringLocalizer exposes Translations, a ConcurrentDictionary<string, ConcurrentDictionary<string, string>> keyed by culture name. You can add or override translations at runtime:
var localizer = (IAxiomStringLocalizer) factory.Create(typeof(MyAppResource));
localizer.Translations["en"]["dynamic-key"] = "Dynamic value";All instances backed by the same resource share the same Translations dictionary a change in one is visible in all. Changes are in-memory only and not persisted.
Embedded Resources Setup
When serving translation files from embedded resources, configure your .csproj to generate the manifest and embed the files:
<PropertyGroup>
<GenerateEmbeddedFilesManifest>true</GenerateEmbeddedFilesManifest>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" />
</ItemGroup>
<ItemGroup>
<None Remove="Resources\Localization\**" />
<EmbeddedResource Include="Resources\Localization\**" />
</ItemGroup>Then register the embedded provider in FileProviderOptions:
builder.Services.Configure<FileProviderOptions>(options =>
{
options.AddEmbedded<MyAppPackage>();
});See File Providers for full provider configuration options.
WARNING
Be careful with file provider ordering. If any registered file provider (embedded, physical, or custom) has files from the same path as your localization directories, it will completely replace those files rather than merge with them. This is different from passing multiple paths to options.Resources.Add, which merges translations at the key level. To safely compose translations from multiple sources, use multiple localization paths instead of multiple file providers pointing at the same directory.