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 .lockeys file to your project. The file name becomes the generated class name.
Create Translations/App.lockeys:
# 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\*.lockeys" />
</ItemGroup>The source generator creates a static class named App (matching the file name) 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 .lockeys file is used automatically.
Create a JSON file with translated templates. The keys must match the Section.Key format:
Translations/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 .lockeys 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, $"{culture.TwoLetterISOLanguageName}.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>();The DI container automatically injects the factory into LocalizationContext.
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/it.json
_localization.SetCulture(new CultureInfo("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 .lockeys)
_localization.SetCulture(null);LocalizationContext.ProviderChanged is an observable that emits the current provider on subscription and again every time the language changes. Use it to keep your UI in sync:
// 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.lockeys\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["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
.lockeysfiles - 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)