Tellurian.Localization 1.5.0

Prefix Reserved
dotnet add package Tellurian.Localization --version 1.5.0
                    
NuGet\Install-Package Tellurian.Localization -Version 1.5.0
                    
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="Tellurian.Localization" Version="1.5.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Tellurian.Localization" Version="1.5.0" />
                    
Directory.Packages.props
<PackageReference Include="Tellurian.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 Tellurian.Localization --version 1.5.0
                    
#r "nuget: Tellurian.Localization, 1.5.0"
                    
#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 Tellurian.Localization@1.5.0
                    
#: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=Tellurian.Localization&version=1.5.0
                    
Install as a Cake Addin
#tool nuget:?package=Tellurian.Localization&version=1.5.0
                    
Install as a Cake Tool

Tellurian.Localization

This library is helpful for .NET developers that creates applications intended for an international market.

Objectives

This library is developed to support the follwing scenarios:

  • A unified model for retriving translations from a varity of language resource types.
  • A centralised language translation service. It is posible to have all resources in one project, also accessible from GUI and other usage.
  • Provide consistent ways in code to retrieve translations.
  • Easy to add new language translations, eventually with help from AI.
  • Full control over translations, which is specially important in applications targeting special domain areas where terminology is important.

Cross-platform Considerations

When devloping cross-platform applications, it is important that localisation behaves consistent on each platform.

.NET 10 uses the International Components for Unicode (ICU) which is supportet on both Windows and Linux including macOS. This ensure consistent behaviour. Minor differences may occur depending on what version of ICU that is installed on the machine the app runs.

Invariant Globalisation

In order for applications to use localized resources, invariant globalisation must be turned off. This can be declared in the project file. If omitted,the default behaviour is that invariant globaisation is off, so you don't need to declare it explicit as in the example below.

<PropertyGroup>
    <InvariantGlobalization>false</InvariantGlobalization>
</PropertyGroup>

Resource Providers

This library has an extensible model for adding new sources of language resources, Resource Providers. This library implements three resource providers;

  • ResxResourceProvider for getting resources from .NET RESX-files. This provider uses standard .NET mechanism - the Resource Manager class.
  • MarkdownResourceProvider gets resources in form of markdown files with a naming convenstion resourcename.language/culture.md. Markdown files should be structure in a base-folder, but can contain sub-folders.
  • ObjectResourceProvider uses reflection to find the string property whose name matches the language (the culture's two-letter code, matched case-insensitively — so both EN and en resolve), and returns the text of that property. This is useful for example when you load an object from a database with columns for each supported language.

You can add new providers, for example getting trainslations online or in other file formats.

Fallback Behaviour

Fallback is handled by .NET, not by this library. Providers resolve translations from CultureInfo.CurrentUICulture, and the framework walks the culture's parent chain (e.g. sv-SEsv → the assembly's neutral language) automatically. The neutral language is configured with <NeutralLanguage> in the project file (NeutralResourcesLanguageAttribute), en-GB in this library.

How each provider behaves when no translation exists:

  • ResxResourceProvider - the .NET ResourceManager probes sv-SEsv → neutral, then returns the resource key if nothing is found.
  • MarkdownResourceProvider - tries {key}.{lang}.md, then the suffixless {key}.md, then returns the resource key. Set Settings.NeutralLanguage to the language the suffixless files are written in, and the culture-specific file is looked for only for the other languages — for the neutral one it could only ever miss.
  • ObjectResourceProvider - reads the property matching the culture's two-letter code (case-insensitively), returning an empty result if there is none.

Supported Languages

.NET Localization needs to know what languages the application support. This is configured using the Language record:

public record Language(string TwoLetterCode, bool IsFullySupported)
{
    public string? CultureCode { get; init; }
    public bool CapitalizesNouns { get; init; } = false;
}

Properties:

  • TwoLetterCode - The ISO 639-1 two letter language code, e.g. en, sv, de.
  • IsFullySupported - Indicates if the language has complete translations.
  • CultureCode - Optional culture specifier, e.g. GB for British English (en-GB).
  • CapitalizesNouns - Indicates if the language capitalizes nouns (e.g. German).

The default/fallback language is not marked on Language; it is the assembly's neutral language set via <NeutralLanguage> in the project file. See Fallback Behaviour above.

Example:

var languages = new List<Language>
{
    new("en", true) { CultureCode = "GB" },        // British English
    new("sv", true) { CultureCode = "SE" },        // Swedish
    new("de", false) { CapitalizesNouns = true },  // German (partial support)
};

Code Structure

Core Interfaces

Interface Description
ILanguageService Provides information about supported languages and the fallback language.
IResourceProvider Retrieves translations from a specific source (RESX, Markdown, Object).
ISynchronousResourceProvider Implemented by providers whose lookups are genuinely in-memory (RESX, Object), exposing a synchronous GetTranslation. File/HTTP-backed providers do not implement it, so they can't be called synchronously by mistake.
IResourceProviderGroup Manages multiple resource providers of the same type.

Key Classes

Class Description
Language Record representing a supported language with its properties.
LanguageService Implementation of ILanguageService.
TextContent Record containing the translated text, file suffix, and last modified timestamp.
Settings Configuration class for dependency injection setup.
ResxResourceProvider Provides translations from .NET RESX files via ResourceManager.
ResxResourceProviders Groups multiple RESX providers for different resource types.
MarkdownResourceProvider Provides translations from markdown files.
ObjectResourceProvider Provides translations from object properties using reflection.

TextContent

All resource providers return a TextContent record:

public record TextContent(string Text, string FileSuffix, DateTimeOffset? LastModified = null);
  • Text - The translated string.
  • FileSuffix - Source format (.resx, .md, .obj).
  • LastModified - Timestamp for cache invalidation (mainly used by Markdown provider).

Dependency Injection Setup

Configuration

The library uses Settings for configuration:

public class Settings
{
    public IEnumerable<Language> Languages { get; set; } = [];
    public IEnumerable<string> ResxTypeNames { get; set; } = [];
    public string? MarkdownFilesBasePath { get; set; }
    public string? NeutralLanguage { get; set; }
}

Registration in Program.cs

using Microsoft.Extensions.Options;
using Tellurian.Localization;
using Tellurian.Localization.DependencyInjection;

var builder = WebApplication.CreateBuilder(args);

// Configure settings
builder.Services.Configure<Settings>(options =>
{
    options.Languages = new List<Language>
    {
        new("en", true) { CultureCode = "GB" },
        new("sv", true) { CultureCode = "SE" },
        new("de", false)
    };
    // Type names for RESX resources (fully qualified)
    options.ResxTypeNames = new[]
    {
        "MyApp.Resources.Labels, MyApp",
        "MyApp.Resources.Messages, MyApp"
    };
    // Base path for markdown files
    options.MarkdownFilesBasePath = "Content/Translations";
    // The language the suffixless markdown files are written in
    options.NeutralLanguage = "en";
});

// Register localization services
var options = builder.Services.BuildServiceProvider()
    .GetRequiredService<IOptions<Settings>>();
builder.Services.AddTellurianLocalization(options);

Blazor WebAssembly: AddTellurianLocalization registers the file-based MarkdownResourceProvider, which has no file-system access in the browser. Register the providers individually instead and use AddHttpMarkdownResourceProvider for markdown (it fetches over HTTP relative to the app base address):

builder.Services.AddLanguageService(languages);
builder.Services.AddResxResourceProviders([typeof(Labels)]);
builder.Services.AddHttpMarkdownResourceProvider("Content", neutralLanguage: "en");
builder.Services.AddObjectResourceProvider();

Retrieving Services

The providers are registered as keyed singletons:

// Get language service
var languageService = serviceProvider.GetRequiredService<ILanguageService>();

// Get RESX providers group
var resxProviders = serviceProvider.GetRequiredKeyedService<IResourceProviderGroup>("Resx");

// Get Markdown provider
var markdownProvider = serviceProvider.GetRequiredKeyedService<IResourceProvider>("Markdown");

// Get Object provider
var objectProvider = serviceProvider.GetRequiredKeyedService<IResourceProvider>("Object");

Usage Examples

Using the Language Service

public class MyService(ILanguageService languageService)
{
    public void ShowSupportedLanguages()
    {
        var languages = languageService.GetSupportedLanguages();

        foreach (var lang in languages)
        {
            Console.WriteLine($"{lang.TwoLetterCode}: Fully supported = {lang.IsFullySupported}");
        }
    }

    public bool IsLanguageSupported(CultureInfo culture)
    {
        return languageService.SupportsLanguage(culture);
    }
}

Using RESX Resources

RESX files are compiled into satellite assemblies by .NET. The ResxResourceProvider uses the standard ResourceManager to retrieve translations.

Because RESX (and Object) lookups are genuinely in-memory, IResourceProviderGroup exposes both a synchronous Translated<T> and an asynchronous TranslatedAsync<T>. Use the synchronous overload from synchronous contexts (e.g. Razor component markup) to avoid sync-over-async; use the async overload where you are already in an async flow.

public class TranslationService(
    [FromKeyedServices("Resx")] IResourceProviderGroup resxProviders)
{
    public string GetLabel(string key)
    {
        // Synchronous, uses CultureInfo.CurrentUICulture automatically
        var content = resxProviders.Translated<Labels>(key);
        return content.Text;
    }

    public async Task<string> GetLabelAsync(string key, CultureInfo culture)
    {
        // Asynchronous overload
        var content = await resxProviders.TranslatedAsync<Labels>(key, culture);
        return content.Text;
    }
}

Using Markdown Resources

public class ContentService(
    [FromKeyedServices("Markdown")] IResourceProvider markdownProvider)
{
    public async Task<string> GetPageContent(string resourceKey)
    {
        var content = await markdownProvider.GetTranslationAsync(
            resourceKey,
            CultureInfo.CurrentUICulture);
        return content.Text;
    }
}

Using Object Resources

Useful for objects loaded from a database with language-specific columns:

// Example: Entity with language properties. Property names match the two-letter
// language code and are resolved case-insensitively, so conventional C# casing
// (EN, SV, DE) works just as well as lowercase (en, sv, de).
public class ProductDescription
{
    public int Id { get; set; }
    public string EN { get; set; } = "";  // English description
    public string SV { get; set; } = "";  // Swedish description
    public string DE { get; set; } = "";  // German description
}

public class ProductService(
    [FromKeyedServices("Object")] IResourceProvider objectProvider)
{
    public async Task<string> GetDescription(ProductDescription product)
    {
        // Returns the property value matching CurrentUICulture.TwoLetterISOLanguageName
        var content = await objectProvider.GetTranslationAsync(
            product,
            CultureInfo.CurrentUICulture);
        return content.Text;
    }
}

Using Extension Methods

The library provides convenient extension methods:

// Uses CultureInfo.CurrentUICulture automatically
var content = await resourceProvider.Translated("MyResourceKey");

// For objects with language properties
var content = await objectProvider.Translate(myDatabaseEntity);

Markdown File Organization

Naming Convention

Markdown files follow the pattern: {resourcekey}.{language}.md

Examples: English is assumed to be the neutral language that is used as fallback if specific translation is missing.

  • welcome.md - English welcome content
  • welcome.sv.md - Swedish welcome content
  • about-us.md - English "about us" content
  • about-us.sv.md - Swedish "about us" content

Path Mapping

Resource keys with hyphens are converted to directory paths:

  • Key help-faq-intro maps to file path help/faq/intro.{lang}.md

Example folder structure:

Content/Translations/
├── welcome.md 
├── welcome.sv.md
├── welcome.de.md
├── about-us.md
├── about-us.sv.md
├── about-us.de.md
└── help/
    └── faq/
        ├── intro.md
        └── intro.sv.md
        └── intro.de.md

Fallback Behavior

The Markdown provider searches in this order:

  1. {path}.{TwoLetterISOLanguageName}.md (e.g., welcome.sv.md)
  2. {path}.md (default file without language suffix)
  3. Returns the resource key if no file is found

Step 1 is skipped when the requested culture is the Settings.NeutralLanguage, since step 2 already holds that language's content. This matters most for HttpMarkdownResourceProvider, where the miss costs a round trip and a 404 for every lookup — and fails outright when the client is offline, so a progressive web app would show nothing at all in its neutral language.

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 (1)

Showing the top 1 NuGet packages that depend on Tellurian.Localization:

Package Downloads
Tellurian.Trains.Schedules.Model

Import operations for layouts, timetables and schedules.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.5.0 102 8/28/2026
1.4.1 355 6/22/2026
1.4.0 120 6/16/2026
1.3.0 113 6/16/2026
1.2.0 131 6/16/2026
1.1.0 128 6/8/2026
1.0.3 141 3/22/2026
1.0.2 121 3/22/2026
1.0.1 136 1/1/2026
1.0.0 476 12/8/2025

1.5.0: The markdown providers can be told which language the suffixless files are written in, through the new Settings.NeutralLanguage or the neutralLanguage argument on AddMarkdownResourceProvider/AddHttpMarkdownResourceProvider. For that language the culture-specific file is no longer looked for, since the suffixless file already holds its content. HttpMarkdownResourceProvider previously spent a round trip and a 404 on that lookup for every translation, and in an offline progressive web app the failed request left the neutral language with no content at all. Leaving the setting unset keeps the previous two-step lookup, so the change is behaviour-neutral for existing callers.
1.4.1: ObjectResourceProvider now matches the language property case-insensitively. CultureInfo.TwoLetterISOLanguageName is always lowercase (en, sv, da, …), so the previous case-sensitive reflection lookup missed conventional C# property names (EN, SV, DA). Callers can now use either casing. Behaviour-neutral for objects that already used lowercase property names.
1.4.0: Removed the Language.IsFallback property. Fallback is handled entirely by .NET — providers resolve from CultureInfo.CurrentUICulture and the framework walks the culture parent chain down to the assembly neutral language (<NeutralLanguage>). No explicit fallback language needs to be marked. Behaviour-neutral, since nothing consulted IsFallback after FallbackLangauge was removed.
1.3.0: Removed the unused ILanguageService.FallbackLangauge property. It was never consulted by any provider, so the change is behaviour-neutral. RESX fallback is governed by the assembly neutral language (NeutralResourcesLanguageAttribute / <NeutralLanguage>); markdown and object providers fall back per provider. Language.IsFallback remains for callers' own use.
1.2.0: Added synchronous resource lookup. In-memory providers (resx, object) now implement the new ISynchronousResourceProvider with a GetTranslation method. On IResourceProviderGroup the synchronous lookup is Translated<T> and the async one is renamed to TranslatedAsync<T> (was Translated<T>). File/HTTP-backed providers remain async-only. This lets resx be translated from synchronous contexts (e.g. component markup) without sync-over-async.
1.1.0: IResourceProviderGroup.Translated<T>/Provider<T> now take the resource type (e.g. Translated<Labels>) — the previous 'where T : ResxResourceProvider' constraint made them uncallable. Language.IsFallback is now 'init' so the fallback language can be set explicitly. Fixed inverted string.HasValue extension.