This guide explains how to provide translated strings at runtime, configure providers, and integrate with dependency injection.
When you use a generated translation like App.Messages.Welcome("Marco"), here's what happens:
- A
TranslationStringobject is created, holding the key ("Messages.Welcome") and the parameter ("Marco") - When it's converted to a
string(implicitly or via.Resolve()), the system checks for a translation provider - If a provider is supplied and has a template for the key, the template is formatted with the parameters
- If no provider is supplied or the key isn't found, the fallback text from the
.lockeysfile is used
flowchart TD
A([TranslationString created]) --> B{Provider given?}
B -- No --> F1["Use fallback text\n'Welcome back, Marco!'"]
B -- Yes --> C{Key found?}
C -- No --> F2["Use fallback text\n'Welcome back, Marco!'"]
C -- Yes --> D["Format template\n'Bentornato, Marco!'"]
LocalizationContext is the central runtime service that holds the active ITranslationProvider and notifies subscribers when it changes. It is designed for dependency injection and supports reactive UI patterns.
using Deck.Dev.Translation.Runtime;
var ctx = new LocalizationContext();
// Set a provider directly
ctx.Provider = new JsonTranslationProvider("Translations/it.json");
// Resolve translations using the provider
TranslationString greeting = App.Messages.Welcome("Marco");
string text = greeting.Resolve(ctx.Provider);
// "Bentornato, Marco!"For automatic provider resolution, supply an ITranslationProviderFactory at construction time:
using System.Globalization;
using Deck.Dev.Translation.Abstractions;
using Deck.Dev.Translation.Runtime;
var factory = new MyProviderFactory();
var ctx = new LocalizationContext(factory);
// Switch to Italian — the factory creates the provider
ctx.SetCulture(new CultureInfo("it"));
// ctx.Provider is now set, ctx.CurrentCulture is "it"
// Revert to fallback (default language)
ctx.SetCulture(null);
// ctx.Provider is null, ctx.CurrentCulture is nullCalling SetCulture without a factory (on the parameterless constructor) throws InvalidOperationException.
using System.Globalization;
using System.IO;
using Deck.Dev.Translation.Abstractions;
using Deck.Dev.Translation.Runtime;
public class JsonFileProviderFactory : ITranslationProviderFactory
{
private readonly string _basePath;
public JsonFileProviderFactory(string basePath) => _basePath = basePath;
public ITranslationProvider? Create(CultureInfo culture)
{
string jsonPath = Path.Combine(_basePath, $"{culture.TwoLetterISOLanguageName}.json");
return File.Exists(jsonPath) ? new JsonTranslationProvider(jsonPath) : null;
}
}Register LocalizationContext and its dependencies in your DI container:
var services = new ServiceCollection();
services.AddSingleton<ITranslationProviderFactory>(
new JsonFileProviderFactory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Translations")));
services.AddSingleton<LocalizationContext>();LocalizationContext has a constructor that accepts ITranslationProviderFactory, so the DI container will automatically inject it.
LocalizationContext exposes a ProviderChanged observable (backed by a BehaviorSubject) that emits the current provider on subscription and again every time the provider changes. This enables reactive UI patterns.
The TranslationString.Observe(context) extension method returns an IObservable<string> that re-resolves the translation whenever the provider changes:
using Deck.Dev.Translation.Runtime;
TranslationString greeting = App.Messages.Welcome("Marco");
// Emits immediately with current translation, then again on every provider change
greeting.Observe(ctx)
.Subscribe(text => Console.WriteLine(text));You can also subscribe directly to ProviderChanged for custom logic:
ctx.ProviderChanged
.Subscribe(provider =>
{
// Re-resolve all translations, update UI, etc.
});You can pass a specific provider to a single .Resolve() call:
var italianProvider = new JsonTranslationProvider("Translations/it.json");
var germanProvider = new JsonTranslationProvider("Translations/de.json");
TranslationString greeting = App.Messages.Welcome("Marco");
string italian = greeting.Resolve(italianProvider);
// "Bentornato, Marco!"
string german = greeting.Resolve(germanProvider);
// "Willkommen zurück, Marco!"This is useful for scenarios where you need multiple languages simultaneously (e.g., sending emails in the recipient's language).
The built-in JsonTranslationProvider loads translations from a flat JSON file. Keys use the Section.Key format:
{
"Nav.Home": "Pagina iniziale",
"Nav.Settings": "Impostazioni",
"Messages.Welcome": "Bentornato, {userName}!",
"Messages.ItemCount": "Hai {count} articoli",
"Messages.OrderTotal": "Totale: {total:C2}"
}- Use the same parameter names as in the
.lockeysfile —{userName}, not{name}or{0} - Format specifiers are optional — if omitted, the default from the
.lockeysfile is used - Translators can change format specifiers — e.g., use
{total:N2}instead of{total:C2}if the target locale formats currency differently - Escaped braces — use
{{and}}for literal{and}in translated text
Source file:
Price : Total: {price:C2|decimal}English translation (en.json):
{
"Price": "Total: {price:C2}"
}German translation (de.json):
{
"Price": "Gesamt: {price:N2} EUR"
}The German translation uses N2 (plain number) instead of C2 (currency symbol) because it appends "EUR" manually.
Implement ITranslationProvider to load translations from any source — database, API, resource files, etc.:
using System.Diagnostics.CodeAnalysis;
using Deck.Dev.Translation.Abstractions;
public class DatabaseTranslationProvider : ITranslationProvider
{
private readonly Dictionary<string, string> _cache;
public DatabaseTranslationProvider(string locale, IDbConnection db)
{
// Load all translations for this locale into memory
_cache = db.Query("SELECT Key, Template FROM Translations WHERE Locale = @locale",
new { locale })
.ToDictionary(r => r.Key, r => r.Template);
}
public bool TryGetTemplate(string key, [NotNullWhen(true)] out string? template)
{
return _cache.TryGetValue(key, out template);
}
}Then use it via LocalizationContext:
ctx.Provider = new DatabaseTranslationProvider("it", dbConnection);If you never set a provider, everything still works — you just get the fallback text from your .lockeys file. This is the expected behavior during development: you write your keys and fallback text, use the generated API, and add actual translations later.
// No provider set — fallback text is used
string msg = App.Messages.Welcome("Marco");
// "Welcome back, Marco!"
// This is perfectly fine for development and for the default language| Member | Description |
|---|---|
Key |
The translation key (e.g., "Messages.Welcome") |
Resolve(provider?) |
Returns the translated and formatted string. Uses the given provider, or uses the fallback text. |
ToString() |
Same as Resolve() |
implicit operator string |
Lets you use a TranslationString anywhere a string is expected |
| Member | Description |
|---|---|
LocalizationContext() |
Creates a context without a factory. Provider can be set directly but SetCulture is limited to null. |
LocalizationContext(ITranslationProviderFactory) |
Creates a context with a factory, enabling SetCulture. |
Provider |
The active ITranslationProvider. Setting it notifies all ProviderChanged subscribers. |
CurrentCulture |
The culture last passed to SetCulture, or null. |
ProviderChanged |
IObservable<ITranslationProvider?> — emits on subscription and on every Provider change. |
SetCulture(CultureInfo?) |
Switches the active culture via the factory. Pass null to revert to fallback. |
Dispose() |
Completes the ProviderChanged observable. |
| Member | Description |
|---|---|
Create(CultureInfo) |
Creates an ITranslationProvider for the given culture, or returns null. |
| Constructor | Description |
|---|---|
JsonTranslationProvider(string filePath) |
Loads translations from a JSON file on disk |
JsonTranslationProvider(Dictionary<string, string> translations) |
Uses an in-memory dictionary (useful for testing) |
| Method | Description |
|---|---|
TranslationString.Observe(LocalizationContext) |
Extension method returning IObservable<string> that re-resolves on provider change |