Rystem.Localization 10.0.8

There is a newer prerelease version of this package available.
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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Rystem.Localization" Version="10.0.8" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Rystem.Localization" Version="10.0.8" />
                    
Directory.Packages.props
<PackageReference Include="Rystem.Localization" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Rystem.Localization --version 10.0.8
                    
#r "nuget: Rystem.Localization, 10.0.8"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Rystem.Localization@10.0.8
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Rystem.Localization&version=10.0.8
                    
Install as a Cake Addin
#tool nuget:?package=Rystem.Localization&version=10.0.8
                    
Install as a Cake Tool

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:

  1. define a localization model T
  2. register a repository-backed or query-backed source for T
  3. warm the app up with WarmUpAsync()
  4. let ILanguages<T> load all language rows into memory
  5. resolve the current language through IRepositoryLocalizer<T> or direct T injection

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 T injection via localizer.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>.Instance resolves from the current culture at access time
  • injected T is 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:

  1. current UI culture's two-letter code
  2. en
  3. the first loaded language

Examples:

  • it-IT resolves through it
  • en-US resolves through en

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.cs
  • src/Localization/Tests/Rystem.Localization.Test.App/Repository/ServiceCollectionExtensions.cs
  • src/Localization/Tests/Rystem.Localization.Test.App/Components/_Imports.razor
  • src/Localization/Tests/Rystem.Localization.Test.App/Components/Pages/Home.razor
  • src/Localization/Rystem.Localization/ServiceCollectionExtensions/RepositoryFrameworkLocalizationServiceCollectionExtensions.cs
  • src/Localization/Rystem.Localization/Services/Languages.cs
  • src/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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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