A type-safe, source-generated translation system for .NET. Define your translations in a simple text file, get strongly-typed C# code with full IntelliSense, and resolve translations at runtime with zero boxing of value types.
In your project file, reference the three Translation libraries:
<ItemGroup>
<!-- Core types (TranslationString, ITranslationProvider) -->
<ProjectReference Include="..\Translation.Abstractions\Translation.Abstractions.csproj" />
<!-- Source generator — loaded as an analyzer, not a runtime dependency -->
<ProjectReference Include="..\Translation.Generator\Translation.Generator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
<!-- Runtime services (TemplateEngine, JsonTranslationProvider) -->
<ProjectReference Include="..\Translation.Runtime\Translation.Runtime.csproj" />
</ItemGroup>Add a .tkeys file to your project with an @class directive that sets the generated class name.
Create Translations/App.tkeys:
@class App
# Navigation
[Nav]
Home : Home
Settings : Settings
# User-facing messages
[Messages]
Welcome : Welcome back, {userName|string}!
ItemCount : You have {count|int} items
OrderTotal : Total: {total:C2|decimal}Tell MSBuild about it:
<ItemGroup>
<AdditionalFiles Include="Translations\*.tkeys" />
</ItemGroup>The source generator creates a static class named App (matching the @class directive) with nested classes for each section:
// Simple keys — just use them as strings
string homeLabel = App.Nav.Home;
// "Home"
// Parameterized keys — call as methods
string greeting = App.Messages.Welcome("Marco");
// "Welcome back, Marco!"
string items = App.Messages.ItemCount(5);
// "You have 5 items"That's it. No setup needed for the default language — the fallback text from your .tkeys file is used automatically.
Create a JSON file with translated templates. The keys must match the Section.Key format:
Translations/localization.it-IT.json:
{
"Nav.Home": "Pagina iniziale",
"Nav.Settings": "Impostazioni",
"Messages.Welcome": "Bentornato, {userName}!",
"Messages.ItemCount": "Hai {count} articoli",
"Messages.OrderTotal": "Totale: {total:C2}"
}Note: translated templates use the same parameter names as your .tkeys file, but without the type (just {userName}, not {userName|string}). Translators can change or add format specifiers (e.g., {total:N2} instead of {total:C2}).
LocalizationContext is the central service that holds the active translation provider and notifies subscribers when it changes. It is designed for dependency injection.
First, implement ITranslationProviderFactory to tell the system how to load translations for a given culture:
public class JsonFileProviderFactory : ITranslationProviderFactory
{
private readonly string _basePath;
public JsonFileProviderFactory(string basePath) => _basePath = basePath;
public ITranslationProvider? Create(CultureInfo culture)
{
string path = Path.Combine(_basePath, $"localization.{culture.Name}.json");
return File.Exists(path) ? new JsonTranslationProvider(path) : null;
}
}Then register everything in your DI container:
var services = new ServiceCollection();
services.AddSingleton<ITranslationProviderFactory>(
new JsonFileProviderFactory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Translations")));
services.AddSingleton<LocalizationContext>();
services.AddSingleton<ILocalizationContext>(sp => sp.GetRequiredService<LocalizationContext>());The DI container automatically injects the factory into LocalizationContext. Consumers should depend on ILocalizationContext for easy mocking in unit tests.
Call SetCulture to switch the active language. The factory creates the appropriate provider, and all subscribers are notified:
// Switch to Italian — the factory loads Translations/localization.it-IT.json
_localization.SetCulture(new CultureInfo("it-IT"));
// Resolve a translation using the active provider
string greeting = App.Messages.Welcome("Marco").Resolve(_localization.Provider);
// "Bentornato, Marco!"
// Revert to fallback (default language from .tkeys)
_localization.SetCulture(null);Resolve() accepts an optional IFormatProvider so that formatted placeholders (like {total:C2}) render according to the correct locale:
var german = CultureInfo.GetCultureInfo("de-DE");
TranslationString price = App.Messages.OrderTotal(1234.50m);
string english = price.Resolve(); // "Total: $1,234.50"
string germanFmt = price.Resolve(formatProvider: german); // "Total: 1.234,50 €"
// With a translation provider + locale together
string full = price.Resolve(germanProvider, german); // "Bestellsumme: 1.234,50 €"For full control over how values are converted to strings (including object parameters), implement ICustomFormatter — see Runtime Configuration.
LocalizationContext.ProviderChanged is an IObservable<T> (BCL) that emits the current provider on subscription and again every time the language changes. Use it to keep your UI in sync.
Note: The library has no dependency on System.Reactive. To use
.Subscribe(Action<T>), addSystem.Reactiveto your project — or use a framework that already includes it (ReactiveUI, etc.).
// Observe a single translation — re-resolves automatically on language change
App.Messages.Welcome("Marco")
.Observe(localizationContext)
.Subscribe(text => welcomeLabel.Text = text);
// Or subscribe to all provider changes for custom logic
localizationContext.ProviderChanged
.Subscribe(provider => RefreshAllTranslations());flowchart LR
subgraph build [Build time]
A["App.tkeys\n@class App\n[Messages]\nWelcome : Hello,\n{name|str}!"]
B["Source Generator\npublic static\nclass App { ... }"]
A -- build --> B
end
subgraph runtime [Runtime]
C["Your Code\nApp.Messages\n.Welcome('Marco')"]
D["TranslationString\n.Resolve()"]
E["localization.it-IT.json\n{ 'Messages.Welcome':\n'Ciao, {name}!' }"]
F(["'Ciao, Marco!'"])
end
B -- use --> C
C --> D
D -- lookup --> E
D --> F
- File Format Reference — all the syntax details for
.tkeysfiles - Runtime Configuration — providers, DI, and advanced setup
- Architecture — how the source generator and type system work under the hood
- Future Scalability — planned format evolutions (metadata annotations, plural support)