Skip to content

Repository files navigation

DotNetBoost.MultiLingual

NuGet CI .NET License: MIT

Serve your content in every language, from the entity classes you already have.

You add one attribute to a property. DotNetBoost.MultiLingual stores each language's value for you, serves the right one for every request, and falls back when a translation is missing — with no extra table to design, no repository code and no service code.

public class Product
{
    public int Id { get; set; }
    public string Sku { get; set; } = "";     // ordinary column
    public decimal Price { get; set; }        // ordinary column

    [MultiLingual] public string? Name { get; set; }         // translated
    [MultiLingual] public string? Description { get; set; }  // translated
}
GET /api/products/1   Accept-Language: en  ->  { "name": "14-inch Laptop", … }
GET /api/products/1   Accept-Language: ar  ->  { "name": "حاسوب محمول 14 بوصة", … }
GET /api/products/1   Accept-Language: fr  ->  English, when there is no French yet

Reading and writing that product is still plain EF Core — Where(p => p.Name!.Contains(term)) filters on the translated text, in the database.

Works with .NET 8 and .NET 10, on SQL Server, PostgreSQL, SQLite or MongoDB.

Status: preview. The packages are on NuGet as 1.0.0-preview.1, so installing them needs --prerelease — every command below has it. The API may still change before 1.0.0.


Get started in 5 minutes

This builds a minimal web app that stores its content in a local SQLite file. Nothing else to install.

1. Create a project and add the packages

dotnet new web -n MyShop
cd MyShop
dotnet add package DotNetBoost.MultiLingual.Core --prerelease
dotnet add package DotNetBoost.MultiLingual.EntityFrameworkCore --prerelease
dotnet add package DotNetBoost.MultiLingual.API --prerelease
dotnet add package Microsoft.EntityFrameworkCore.Sqlite

2. Replace Program.cs with this

using DotNetBoost.MultiLingual.API;
using DotNetBoost.MultiLingual.Core.Attributes;
using DotNetBoost.MultiLingual.Core.Models;
using DotNetBoost.MultiLingual.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Store translations in SQLite through EF Core.
builder.Services.AddMultiLingual()
    .UseEntityFrameworkCore<AppDbContext>()
    .Build();

builder.Services.AddDbContext<AppDbContext>((sp, o) => o
    .UseSqlite("Data Source=shop.db")
    .UseMultiLingual(sp));              // attaches the interceptors

builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<MultiLingualExceptionHandler>();

var app = builder.Build();

// Create the database on first run. In a real app, use EF Core migrations instead.
using (var scope = app.Services.CreateScope())
{
    await scope.ServiceProvider.GetRequiredService<AppDbContext>().Database.EnsureCreatedAsync();
}

app.UseExceptionHandler();
app.UseMultiLingual();        // Accept-Language -> the language this request reads and writes in
app.MapLanguageEndpoints();   // adds GET/POST/… /api/languages

app.MapGet("/products", (AppDbContext db) => db.Products.ToListAsync());

app.MapPost("/products", async (Product product, AppDbContext db) =>
{
    db.Products.Add(product);
    await db.SaveChangesAsync();        // Name is stored for the request's language
    return product;
});

app.MapPut("/products/{id:int}/name", async (int id, NameUpdate body, AppDbContext db) =>
{
    var product = await db.Products.FindAsync(id);
    if (product is null)
        return Results.NotFound();

    product.Name = body.Name;           // this language only; the others are untouched
    await db.SaveChangesAsync();
    return Results.Ok(product);
});

app.Run();

// Your entity: one attribute per translatable property.
public class Product
{
    public int Id { get; set; }
    public string Sku { get; set; } = "";
    [MultiLingual] public string? Name { get; set; }
}

public record NameUpdate(string Name);

// Your EF Core context: add the two library tables, and call ApplyMultiLingual() LAST.
public class AppDbContext(DbContextOptions<AppDbContext> options)
    : DbContext(options), IMultiLingualDbContext
{
    public DbSet<Translation> Translations => Set<Translation>();
    public DbSet<Language>    Languages    => Set<Language>();
    public DbSet<Product>     Products     => Set<Product>();

    protected override void OnModelCreating(ModelBuilder modelBuilder)
        => modelBuilder.ApplyMultiLingual();
}

3. Run it and add a language

dotnet run --urls http://localhost:5080

In a second terminal. The first language created becomes the default:

curl -X POST http://localhost:5080/api/languages -H "Content-Type: application/json" -d '{"code":"en","name":"English"}'
curl -X POST http://localhost:5080/api/languages -H "Content-Type: application/json" -d '{"code":"ar","name":"العربية","direction":"rtl"}'
curl -X POST http://localhost:5080/api/languages -H "Content-Type: application/json" -d '{"code":"fr","name":"Français"}'

4. Write one product in two languages

curl -X POST http://localhost:5080/products -H "Content-Type: application/json" -H "Accept-Language: en" -d '{"sku":"LAPTOP-14","name":"14-inch Laptop"}'
curl -X PUT http://localhost:5080/products/1/name -H "Content-Type: application/json" -H "Accept-Language: ar" -d '{"name":"حاسوب محمول 14 بوصة"}'

5. Read it back in each one

curl http://localhost:5080/products -H "Accept-Language: ar"
[{"id":1,"sku":"LAPTOP-14","name":"حاسوب محمول 14 بوصة"}]
curl http://localhost:5080/products -H "Accept-Language: fr"
[{"id":1,"sku":"LAPTOP-14","name":"14-inch Laptop"}]

There is no French yet, so the read falls back to the default language. Nothing in the two product endpoints mentions a language: the values were filled in by the same SQL statement that loaded the row.

⚠️ The language endpoints allow anonymous access by default. They change what every client of your application sees, and one of them deletes content. Protect them before you deploy: app.MapLanguageEndpoints().RequireAuthorization("admin"). See Securing the endpoints.


Using it in your own code

Reading and writing translated properties needs no library call at all — it is the EF Core you already write. For everything else, inject IMultiLingualManager:

public class ProductService(AppDbContext db, IMultiLingualManager multiLingual)
{
    public async Task TranslateAsync(int id)
    {
        var product = await db.Products.FirstAsync(p => p.Id == id);

        multiLingual.For(product).Set("fr", p => p.Name, "Portable");    // add a language
        multiLingual.For(product).Clear("ar");                           // remove one

        await db.SaveChangesAsync();   // written with the entity, in the same transaction
    }
}
You want to… Call
know the request's language multiLingual.CurrentLanguage, DefaultLanguage, WasRequestedLanguageHonoured
set it outside a request (jobs, seeders) await multiLingual.UseLanguageAsync("ar")
read every stored language of an entity await multiLingual.For(product).GetAllAsync()
add, change or remove a translation .Set("fr", p => p.Name, "Chaise"), .Clear("fr")
find or administer languages multiLingual.Languages.FindAsync("ar"), CreateAsync(…), SetDefaultAsync(…), …
fill translated properties yourself (Dapper, MongoDB) await multiLingual.LocalizeAsync(products)

Set and Clear check the language straight away, so a mistake surfaces where it was made — and MultiLingualExceptionHandler turns it into problem details, so a controller needs no checks of its own. Full tour: reading and writing.


Using your existing database

The quick start uses EF Core with SQLite because that needs no setup. In a real app, use the database you already have:

You use… Package Register with
Entity Framework Core DotNetBoost.MultiLingual.EntityFrameworkCore .UseEntityFrameworkCore<AppDbContext>()
Dapper / plain ADO.NET DotNetBoost.MultiLingual.Dapper .UseDapper(sp => new SqlConnection(cs), migrateSchema: true)
MongoDB DotNetBoost.MultiLingual.MongoDb .UseMongoDb("mongodb://localhost:27017", "shop")

EF Core needs two extra DbSets on your context. Dapper and MongoDB have no change tracker to hook, so their users localize after a query and save translations explicitly — a few extra lines, the same rules. See the storage providers guide.

Already have a Products.Name column full of English? Then that content is a language. Set BaseLanguage = "en" and it stays exactly where it is, with every other language in the translations table falling back to it — nothing to import, no column dropped. See base-language mode.


What else it can do

Each feature is optional and switched on with one line on AddMultiLingual():

builder.Services.AddMultiLingual(o => o.BaseLanguage = "en")    // keep English in its own columns
    .UseEntityFrameworkCore<AppDbContext>()
    .AddJsonLocalization()                            // your own messages, in the same language
    .SynchronizeLanguagesThroughDistributedCache()    // several instances, one language list
    .WithLanguageCacheDuration(TimeSpan.FromMinutes(5))
    .Build();
I want to… Guide
Mark content translatable, and rename a class safely Declaring translatable content
Use SQL Server, PostgreSQL, Dapper or MongoDB Storage providers
Filter, sort and project on translated text, in SQL Translated queries
Keep one language in the entity's own columns Base-language mode
Add, deactivate or promote a language at runtime Languages
Expose and secure the language endpoints REST API
Return my own error messages in the caller's language Localized messages
Run on several servers (Redis) Languages → the cache
Give editors a web UI Dashboard
See every builder option Configuration reference
Know what this does not do Known limitations
Run the full demo stack (three APIs, dashboard, PostgreSQL, MongoDB) .NET Aspire
Understand how it works inside, or contribute Architecture

Try the full demo

samples/SampleApp is a complete Products and Categories API in base-language mode, so Products.Name is a real column you can read in any SQL client. Two more samples serve the same routes and the same bodies over the Dapper and MongoDB providers. If you have Docker running, one command starts all of it — plus PostgreSQL, MongoDB, a browser SQL client and the web dashboard:

dotnet run --project aspire/DotNetBoost.MultiLingual.AppHost

The API is then on http://localhost:5160, with its reference at /scalar. See Running everything with .NET Aspire.


Packages

Package What it's for
DotNetBoost.MultiLingual.Core Required. Attributes, the engine, IMultiLingualManager, the store contracts
DotNetBoost.MultiLingual.EntityFrameworkCore EF Core provider: mapping and interceptors, so queries and saves translate themselves
DotNetBoost.MultiLingual.Dapper Dapper provider, over the EF Core provider's own schema
DotNetBoost.MultiLingual.MongoDb MongoDB provider
DotNetBoost.MultiLingual.API ASP.NET Core: Accept-Language, language endpoints, problem details, OpenAPI
DotNetBoost.MultiLingual.Localization IStringLocalizer over JSON files, in the content's language

Contributing

Contributions are welcome. See CONTRIBUTING.md and the architecture overview.

License

MIT. See LICENSE.

About

Serve your .NET content in every language, from the entity classes you already have — one attribute, translated EF Core queries, automatic fallback. SQL Server, PostgreSQL, SQLite, MongoDB and Dapper.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages