Skip to content

Latest commit

 

History

History
280 lines (201 loc) · 9.38 KB

File metadata and controls

280 lines (201 loc) · 9.38 KB

Runtime Configuration

This guide explains how to provide translated strings at runtime, configure providers, and integrate with dependency injection.

How translation resolution works

When you use a generated translation like App.Messages.Welcome("Marco"), here's what happens:

  1. A TranslationString object is created, holding the key ("Messages.Welcome") and the parameter ("Marco")
  2. When it's converted to a string (implicitly or via .Resolve()), the system checks for a translation provider
  3. If a provider is supplied and has a template for the key, the template is formatted with the parameters
  4. If no provider is supplied or the key isn't found, the fallback text from the .lockeys file 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!'"]
Loading

LocalizationContext

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.

Basic usage

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!"

Using SetCulture with a provider factory

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 null

Calling SetCulture without a factory (on the parameterless constructor) throws InvalidOperationException.

Implementing ITranslationProviderFactory

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;
    }
}

Dependency injection

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.

Reactive extensions

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.

Observing a single translation

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));

Subscribing to provider changes

You can also subscribe directly to ProviderChanged for custom logic:

ctx.ProviderChanged
    .Subscribe(provider =>
    {
        // Re-resolve all translations, update UI, etc.
    });

Per-call provider

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).

JSON translation files

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}"
}

Rules for translated templates

  1. Use the same parameter names as in the .lockeys file — {userName}, not {name} or {0}
  2. Format specifiers are optional — if omitted, the default from the .lockeys file is used
  3. Translators can change format specifiers — e.g., use {total:N2} instead of {total:C2} if the target locale formats currency differently
  4. Escaped braces — use {{ and }} for literal { and } in translated text

Example: format specifier override

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.

Building a custom provider

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);

No provider? No problem

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

API reference

TranslationString

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

LocalizationContext

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.

ITranslationProviderFactory

Member Description
Create(CultureInfo) Creates an ITranslationProvider for the given culture, or returns null.

JsonTranslationProvider

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)

TranslationReactive

Method Description
TranslationString.Observe(LocalizationContext) Extension method returning IObservable<string> that re-resolves on provider change