Rystem.Localization
10.0.8
See the version list below for details.
dotnet add package Rystem.Localization --version 10.0.8
NuGet\Install-Package Rystem.Localization -Version 10.0.8
<PackageReference Include="Rystem.Localization" Version="10.0.8" />
<PackageVersion Include="Rystem.Localization" Version="10.0.8" />
<PackageReference Include="Rystem.Localization" />
paket add Rystem.Localization --version 10.0.8
#r "nuget: Rystem.Localization, 10.0.8"
#:package Rystem.Localization@10.0.8
#addin nuget:?package=Rystem.Localization&version=10.0.8
#tool nuget:?package=Rystem.Localization&version=10.0.8
Rystem.Localization
Rystem.Localization is a strongly typed localization layer built on top of RepositoryFramework.
It does not use .resx files or per-key lookups. Instead, each language stores one full object of type T, keyed by language code, and the package resolves the right object at runtime from an in-memory snapshot.
Installation
dotnet add package Rystem.Localization
Architecture
The package is small and repository-centric:
- define a localization model
T - register a repository-backed or query-backed source for
T - warm the app up with
WarmUpAsync() - let
ILanguages<T>load all language rows into memory - resolve the current language through
IRepositoryLocalizer<T>or directTinjection
Runtime lookup uses:
CultureInfo.CurrentUICulture.TwoLetterISOLanguageName- then
en - then the first loaded language
Define the localization model
A language is one full object graph.
public sealed class TheDictionary
{
public string Value { get; set; } = string.Empty;
public TheFirstPage TheFirstPage { get; set; } = new();
public TheSecondPage TheSecondPage { get; set; } = new();
}
public sealed class TheFirstPage
{
public string Title { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
}
public sealed class TheSecondPage
{
public FormattedString Title { get; set; } = "Title {0}";
}
The repository key is expected to be the language code, typically the two-letter ISO form such as en or it.
Registration with Repository Framework
The most grounded path is AddLocalizationWithRepositoryFramework<T>(...).
This is how the sample app wires it in src/Localization/Tests/Rystem.Localization.Test.App/Repository/ServiceCollectionExtensions.cs.
services.AddLocalizationWithRepositoryFramework<TheDictionary>(builder =>
{
builder.WithInMemory(name: "localization");
},
name: "localization",
storageWarmup: async serviceProvider =>
{
var repository = serviceProvider.GetRequiredService<IRepository<TheDictionary, string>>();
await repository.InsertAsync("it", new TheDictionary
{
Value = "Valore",
TheFirstPage = new TheFirstPage
{
Title = "Titolo",
Description = "Descrizione"
},
TheSecondPage = new TheSecondPage
{
Title = "Titolo {0}"
}
});
await repository.InsertAsync("en", new TheDictionary
{
Value = "Value",
TheFirstPage = new TheFirstPage
{
Title = "Title",
Description = "Description"
},
TheSecondPage = new TheSecondPage
{
Title = "Title {0}"
}
});
});
What this registration adds:
- the underlying
IRepository<T, string> - named
ILanguages<T>as a singleton - named
IRepositoryLocalizer<T>as a singleton - direct transient
Tinjection vialocalizer.Instance - warm-up hooks for optional storage seeding and language-cache loading
Query-backed registration
The package also exposes:
services.AddLocalizationWithQueryFramework<TheDictionary>(builder =>
{
// configure a query source here
});
This API is intended for read-only localization sources.
Important caveat: the current implementation looks inconsistent here. The registration path adds query services, but the warm-up loader still resolves IFactory<IRepository<T, string>>. There is no sample app using this path, so treat it as a less-proven option until it is cleaned up.
Also note that, unlike the repository-backed path, the current query-backed registration does not add direct T injection.
Warm-up is required
You need to warm the host up manually.
The sample app does this in src/Localization/Tests/Rystem.Localization.Test.App/Program.cs:
var app = builder.Build();
await app.Services.WarmUpAsync();
Without warm-up, the in-memory language snapshot never loads.
Culture management is manual
This package does not ship ASP.NET Core request-localization middleware, culture providers, or built-in culture selection.
You are expected to set CultureInfo.CurrentCulture and CultureInfo.CurrentUICulture yourself.
The sample app uses a custom middleware that reads a lang cookie:
public sealed class LocalizationMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (context.Request.Cookies.TryGetValue("lang", out var cookieLanguage))
{
var language = cookieLanguage;
CultureInfo.CurrentCulture = new CultureInfo(language ?? "en");
CultureInfo.CurrentUICulture = new CultureInfo(language ?? "en");
}
await next(context);
}
}
Core abstractions
IRepositoryLocalizer<T>
public interface IRepositoryLocalizer<T>
{
T Instance { get; }
}
Instance resolves the current language object every time you access it.
ILanguages<T>
public interface ILanguages<T>
{
RystemLocalizationFiles<T> Localizer { get; }
}
This exposes the full loaded language snapshot.
RystemLocalizationFiles<T>
public sealed class RystemLocalizationFiles<T>
{
public Dictionary<string, T> AllLanguages { get; set; } = [];
}
FormattedString
FormattedString is the package's simple formatted-message helper.
public sealed class FormattedString
{
public required string Value { get; init; }
public string this[params object[] parameters] => string.Format(Value, parameters);
}
Usage:
var title = localizer.Instance.TheSecondPage.Title["something"];
Consuming localizations
The sample Blazor app injects both the localizer and the direct T instance in src/Localization/Tests/Rystem.Localization.Test.App/Components/_Imports.razor:
@inject IRepositoryLocalizer<TheDictionary> Localizer
@inject TheDictionary InstanceOfLocalizer
And uses them in src/Localization/Tests/Rystem.Localization.Test.App/Components/Pages/Home.razor:
<h2>@Localizer.Instance.Value</h2>
<h3>@Localizer.Instance.TheFirstPage.Title</h3>
<h5>@Localizer.Instance.TheSecondPage.Title["something"]</h5>
<h2>@InstanceOfLocalizer.Value</h2>
Difference between IRepositoryLocalizer<T> and direct T
IRepositoryLocalizer<T>.Instanceresolves from the current culture at access time- injected
Tis resolved once when the consuming service or component is created
If culture can change during the lifetime of the consumer, prefer IRepositoryLocalizer<T>.
Fallback behavior
RepositoryLocalizer<T> resolves languages in this order:
- current UI culture's two-letter code
en- the first loaded language
Examples:
it-ITresolves throughiten-USresolves throughen
This also means storing keys like en-US is not enough by itself, because the resolver only uses the two-letter code.
Named localizers
Both registration methods accept name, which can be a string or Enum through AnyOf<string?, Enum>?.
That lets you host multiple independent localization sets.
public enum LocalizationSet
{
Main,
Admin
}
services.AddLocalizationWithRepositoryFramework<TheDictionary>(builder =>
{
builder.WithInMemory(name: LocalizationSet.Admin);
}, LocalizationSet.Admin);
Resolve named localizers through the factory layer:
public sealed class AdminService
{
private readonly IRepositoryLocalizer<TheDictionary> _localizer;
public AdminService(IFactory<IRepositoryLocalizer<TheDictionary>> factory)
=> _localizer = factory.Create(LocalizationSet.Admin);
}
Important caveats
This is snapshot caching, not live synchronization
Warm-up loads all languages into memory once. Later repository updates do not automatically refresh AllLanguages.
No built-in ASP.NET localization pipeline
You are responsible for setting the current culture yourself.
Language keys are effectively two-letter keys
Lookup uses TwoLetterISOLanguageName, so the most reliable storage keys are values like en, it, and fr.
Query-backed registration looks weaker than the repository path
Because the warm-up implementation currently resolves a repository factory, AddLocalizationWithQueryFramework<T>(...) should be treated cautiously until the source is aligned.
Warm-up failures are not ideal
Languages<T>.WarmUpAsync() throws when no languages are found, but broader warm-up handling elsewhere in the repo can swallow failures. So do not rely on a clean startup exception as the only safety mechanism.
Grounded by sample and source files
src/Localization/Tests/Rystem.Localization.Test.App/Program.cssrc/Localization/Tests/Rystem.Localization.Test.App/Repository/ServiceCollectionExtensions.cssrc/Localization/Tests/Rystem.Localization.Test.App/Components/_Imports.razorsrc/Localization/Tests/Rystem.Localization.Test.App/Components/Pages/Home.razorsrc/Localization/Rystem.Localization/ServiceCollectionExtensions/RepositoryFrameworkLocalizationServiceCollectionExtensions.cssrc/Localization/Rystem.Localization/Services/Languages.cssrc/Localization/Rystem.Localization/Services/RepositoryLocalizer.cs
Use this package when you want repository-backed, strongly typed localized object graphs rather than .resx resources or key/value string lookups.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Rystem.DependencyInjection (>= 10.0.8)
- Rystem.RepositoryFramework.Abstractions (>= 10.0.8)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 10.1.0-beta.3 | 51 | 8/26/2026 |
| 10.1.0-beta.2 | 47 | 8/26/2026 |
| 10.0.8 | 60,706 | 5/13/2026 |
| 10.0.7 | 118 | 3/26/2026 |
| 10.0.6 | 433,481 | 3/3/2026 |
| 10.0.5 | 127 | 2/22/2026 |
| 10.0.4 | 127 | 2/9/2026 |
| 10.0.3 | 147,931 | 1/28/2026 |
| 10.0.1 | 209,085 | 11/12/2025 |
| 9.1.3 | 240 | 9/2/2025 |
| 9.1.2 | 764,483 | 5/29/2025 |
| 9.1.1 | 97,817 | 5/2/2025 |
| 9.0.32 | 186,659 | 4/15/2025 |
| 9.0.31 | 5,824 | 4/2/2025 |
| 9.0.30 | 88,863 | 3/26/2025 |
| 9.0.29 | 9,012 | 3/18/2025 |
| 9.0.28 | 212 | 3/17/2025 |
| 9.0.27 | 236 | 3/16/2025 |
| 9.0.26 | 215 | 3/16/2025 |