rskibbe.I18n – Translate / create multilanguage .NET Apps

Inhaltsverzeichnis
What is rskibbe.I18n?
rskibbe.I18n is an infrastructure package helping you translate „things“, trying to be as easy as possbile. If you want, you can extend it by installing corresponding sub-packages making your translation work with like Winforms, WPF apps and more – even easier. Multi-Language / internationalized apps – here we come!
++ Update 01.11.2024 ++
It was about time – taking a look at the nice resonance the project and its sub-packages got – to finally put an icon alongside the NuGet package. I also introducted some internal changes which are (for now) only PoC (Proof of Concept) things that aren’t finalized and can be ignored. The main thing was: I finally wanted an icon :3, the other stuff is work in progress and will be finished as soon as I will find the time to do so.
++ Update 04.09.2023 ++
With the „rskibbe.I18n“ update version 1.0.14 I made the project more compatible by providing it as a .NET Standard 2 library. There was a little unimportant improvement as well, but as said – not really important. This way, the library should be even more accessible and useful. Thanks to everybody using it!
What were your thoughts designing it?
When I first started designing / thinking about „rskibbe.I18n“ I was like: „Oh god, I just want to have something simple“. I imagined something like a normal (real life person) translator, which you could just ask like: „Hey Translator, can you tell me the meaning of <insert something identifying here>“
It’s a basic thing for me: You ask your translator for a translation – period.
It worked pretty fast at first, but then the usual everyday developer life kicked in and I was like:
- What if I want to make it „just work“ for Winforms, WPF, or even a Console app?
- What if I want to provide different translation source types (file-, web-, database-based, or even „InMemory“)?
- What if I want to have an easy configuration for the translations / languages?
- What about live-updates when changing the language, instead of restarting? Configurable?
- How to easily distribute it, without having some sort of usual download link?
- and much more..
After some hours of programming, thinking, refactoring and repeating – you can now install different tools.
How to get started?
The following steps will help you getting started with translating / creating your multilanguage apps – easily. This package and its sub-packages aims to help you, to create a translator for your individual usecase. Whether it’ll be a Winforms app using like the older Ini file format, or a brandnew WPF app with a more modern approach like JSON or web-request based translations.
Installation
Use the NuGet Package Manager of Visual Studio to install the base package (and corresponding sub-packages if wanted/needed). The easiest way to access the Manager, is by clicking the „Extra“ menu tool strip menu item, or by right-clicking your project inside the project explorer. Then click something like „Manage NuGet Packages“. There you should be easily able to search for „rskibbe.I18n“ and see all possible packages.
Or you can use the following NuGet Package Manager Console command for the newest version:
Install-Package rskibbe.I18n
Namespaces
After installation, just go ahead and import the most important namespace – making the builder available:
// somewhere high up inside your file using rskibbe.I18n.Models;
' somewhere high up inside your file Imports rskibbe.I18n.Models
From there, you can build your own Translator by using the TranslatorBuilder.
Setup

Setting up the translator should be done as early as possible, like in some sort of bootstrapping process, or before the first Form „InitializeComponent“ ’n‘ stuff.
Simplest – with autodetection
If you just want to „make it work“, you can easily create your Translator in 1-2 simple lines. This will use the internal autodetection feature to estimate, which techniques to use. For this, it will prioritize finding a suitable ILanguagesLoader & ITranslationTablesLoader in the following order (Assemblies):
- Executing Assembly
if you for example defined your own Loaders and to lazily use them – like InMemory ones
- rskibbe.I18n.Json
Keep in mind that this is an additional package with prepared implementations you could install
- rskibbe.I18n.Ini
see the Json one..
- rskibbe.I18n.Web ⚠️
Not available yet, for like (REST) API or general web based loading calls
- rskibbe.I18n.Database ⚠️
Not available yet, for like (mySQL, SQLServer, etc.)
So basically, you could for example just install the rskibbe.I18n.Json package, to use the JSON tools through autodetection:
// create and save the instance for later, DI, etc. var translator = Translator.Builder .Build(); // don't forget to store it in static Property // to make other helpers available Translator.Instance = translator; // or just chain the StoreInstace method // then "var instance = " is optional for sure var translator = Translator.Builder .Build() .StoreInstance();
' create and save the instance for later, DI, etc. Dim translator = Translator.Builder _ .Build() ' don't forget to store it in static Property ' to make other helpers available Translator.Instance = translator ' or just chain the StoreInstace method ' then "var instance = " is optional for sure Dim translator = Translator.Builder _ .Build() _ .StoreInstance()
Explicitly specifying premade loaders
You can tell the Translator „how to load the available languages“ and „how to load the translations“ by explicitly providing implementations of ILanguagesLoader / ITranslationTableLoader. You can use the generic approach, if you want the translator to instantiate the loaders for you, or the instance based approach – therefore providing an instance to the corresponding method.
// don't forget the imports somewhere above.. using rskibbe.I18n.Json; // create and save the instance for later, DI, etc. var translator = Translator.Builder .WithLanguagesLoader<JsonLanguagesLoader>() .WithTranslationTableLoader<JsonTranslationTableLoader>() .Build() .StoreInstance();
' don't forget the imports somewhere above.. Imports rskibbe.I18n.Json ' create and save the instance for later, DI, etc. Dim translator = Translator.Builder _ .WithLanguagesLoader(Of JsonLanguagesLoader)() _ .WithTranslationTableLoader(Of JsonTranslationTableLoader)() _ .Build() _ .StoreInstance()
Providing your own loaders
The above can also be done by providing custom implementations of ILanguagesLoader / ITranslationTableLoader.
// create and save the instance for later, DI, etc. var translator = Translator.Builder .WithLanguagesLoader<InMemoryLanguagesLoader>() .WithTranslationTableLoader<InMemoryTranslationTableLoader>() .Build() .StoreInstance();
' create and save the instance for later, DI, etc. Dim translator = Translator.Builder _ .WithLanguagesLoader(Of InMemoryLanguagesLoader)() _ .WithTranslationTableLoader(Of InMemoryTranslationTableLoader)() _ .Build() _ .StoreInstance()
InMemoryLanguagesLoader implementation:
public class InMemoryLanguagesLoader : ILanguagesLoader { public List<ILanguage> AllLanguages { get; set; } public InMemoryLanguagesLoader() { AllLanguages = new List<ILanguage>() { new Language("Deutsch", "de-DE"), new Language("English", "en-US") }; } public async Task CreateLanguageAsync(ILanguage language) { var languages = await LoadLanguagesAsync(); var existingLanguage = languages.SingleOrDefault(x => x.Iso == language.Iso); if (existingLanguage != null) throw new DuplicateLanguageException(language); AllLanguages.Add(language); } public Task<List<ILanguage>> LoadLanguagesAsync() { var languages = AllLanguages .Select(x => (ILanguage) x.Clone()) .ToList(); return Task.FromResult(languages); } }
Public Class InMemoryLanguagesLoader Inherits ILanguagesLoader Public Property AllLanguages As List(Of ILanguage) Public Sub New() AllLanguages = New List(Of ILanguage)() From { New Language("Deutsch", "de-DE"), New Language("English", "en-US") } End Sub Public Async Function CreateLanguageAsync(ByVal language As ILanguage) As Task Dim languages = Await LoadLanguagesAsync() Dim existingLanguage = languages.SingleOrDefault(Function(x) x.Iso = language.Iso) If existingLanguage IsNot Nothing Then Throw New DuplicateLanguageException(language) End If AllLanguages.Add(language) End Function Public Function LoadLanguagesAsync() As Task(Of List(Of ILanguage)) Dim languages = AllLanguages.Select(Function(x) CType(x.Clone(), ILanguage)).ToList() Return Task.FromResult(languages) End Function End Class
InMemoryTranslationTableLoader implementation:
public class InMemoryTranslationTableLoader : ITranslationTableLoader { public List<ITranslationTable> AllTranslationTables { get; set; } public InMemoryTranslationTableLoader() { var german = new Language("Deutsch", "de-DE"); var english = new Language("English", "en-US"); var germanTranslationTable = new TranslationTable(german) .AddTranslation("namespaceOne.key1", "Schlüssel 1") .AddTranslation("namespaceTwo.key2", "Schlüssel 2") .AddTranslation("btnEnglish", "Englisch") .AddTranslation("btnGerman", "Deutsch"); var englishTranslationTable = new TranslationTable(english) .AddTranslation("namespaceOne.key1", "Key 1") .AddTranslation("namespaceTwo.key2", "Key 2") .AddTranslation("btnEnglish", "English") .AddTranslation("btnGerman", "German"); AllTranslationTables = new List<ITranslationTable>() { germanTranslationTable, englishTranslationTable }; } public Task<List<ITranslationTable>> LoadTranslationTablesAsync(params string[] isoCodes) { var requestedTranslationTables = AllTranslationTables .Where(x => isoCodes.Contains(x.Language.Iso)) .Select(x => (ITranslationTable)x.Clone()) .ToList(); return Task.FromResult(requestedTranslationTables); } /// <inheritdoc/> public Task CreateTranslationAsync(ITranslation translation) { var translationTable = AllTranslationTables.SingleOrDefault(x => x.Language == translation.Language); if (translationTable == null) throw new TranslationTableNotAvailableException(translation.Language); var translationExists = translationTable.Keys.Contains(translation.Key); if (translationExists) throw new DuplicateTranslationException(translation.Key); translationTable.AddTranslation(translation.Key, translation.Value); return Task.CompletedTask; } }
Public Class InMemoryTranslationTableLoader Inherits ITranslationTableLoader Public Property AllTranslationTables As List(Of ITranslationTable) Public Sub New() Dim german = New Language("Deutsch", "de-DE") Dim english = New Language("English", "en-US") Dim germanTranslationTable = New TranslationTable(german) _ .AddTranslation("namespaceOne.key1", "Schlüssel 1") _ .AddTranslation("namespaceTwo.key2", "Schlüssel 2") _ .AddTranslation("btnEnglish", "Englisch") _ .AddTranslation("btnGerman", "Deutsch") Dim englishTranslationTable = New TranslationTable(english) _ .AddTranslation("namespaceOne.key1", "Key 1") _ .AddTranslation("namespaceTwo.key2", "Key 2") _ .AddTranslation("btnEnglish", "English") _ .AddTranslation("btnGerman", "German") AllTranslationTables = New List(Of ITranslationTable)() From { germanTranslationTable, englishTranslationTable } End Sub Public Function LoadTranslationTablesAsync(ParamArray isoCodes As String()) As Task(Of List(Of ITranslationTable)) Dim requestedTranslationTables = AllTranslationTables _ .Where(Function(x) isoCodes.Contains(x.Language.Iso)) _ .Select(Function(x) CType(x.Clone(), ITranslationTable)) _ .ToList() Return Task.FromResult(requestedTranslationTables) End Function Public Function CreateTranslationAsync(ByVal translation As ITranslation) As Task Dim translationTable = AllTranslationTables.SingleOrDefault(Function(x) x.Language = translation.Language) If translationTable Is Nothing Then Throw New TranslationTableNotAvailableException(translation.Language) Dim translationExists = translationTable.Keys.Contains(translation.Key) If translationExists Then Throw New DuplicateTranslationException(translation.Key) translationTable.AddTranslation(translation.Key, translation.Value) Return Task.CompletedTask End Function End Class
Preparation
Depending on your use case, you need to like execute the loading of the available languages by a call to the Translator LoadLanguagesAsync() method. Otherwise, you won’t know, what languages are available, right?
Something like a Form Load EventHandler could be the right place, remember to use async/await if wanted. You can then see the available languages inside the Languages-Property.
private async void Form1_Load(object sender, EventArgs e) { // the next line, or if you have a direct reference, just use your reference of the translator.. await Translator.Instance.LoadLanguagesAsync(); // the available languages can be accessed // Translator.Instance.Languages // prefill UI, etc.. }
Private Async Sub Form1_Load(sender As Object, e As EventArgs) ' the next line, or if you have a direct reference, just use your reference of the translator.. Await Translator.Instance.LoadLanguagesAsync() ' the available languages can be accessed ' Translator.Instance.Languages ' prefill UI, etc.. End Sub
If you’ve used the WithLanguage method, it will automatically set the corresponding language after loading the languages.
The package also provides a basic DefaultLanguageItemViewModel or a LanguageItemViewModel as base to work on. This can help filling ComboBoxes, etc.
Usage
After all setup and preparation steps are completed, you can then start using the Translator like this:
// get the current language // var currentLanguage = Translator.Instance.Language // change the current language // using an implementation of ILanguage / the iso code string // if you try to set it to the same language twice, the call will be ignored await Translator.Instance.ChangeLanguageAsync("de-DE"); // get a specific translation var translation = Translator.Instance.Translate("theKey"); MessageBox.Show(translation.Value);
' get the current language ' Dim currentLanguage = Translator.Instance.Language ' change the current language ' using an implementation of ILanguage / the iso code string ' if you try to set it to the same language twice, the call will be ignored Await Translator.Instance.ChangeLanguageAsync("de-DE") ' get a specific translation Dim translation = Translator.Instance.Translate("theKey") MessageBox.Show(translation.Value)
Quick example

Here’s a quick example without talking to much, go into the specific sections of this article to get more details.
Step 1 – Install the package(s)
Open the NuGet Package Manager and execute the following commands (or install it by GUI). We will prepare our app to be translated by a file-based JSON approach:
Install-Package rskibbe.I18n Install-Package rskibbe.I18n.Json
Step 2 – Initialize the Translator
Call this code inside your bootstrapping Program.cs/vb file, or put it inside the constructor of the first Form (before the InitializeComponent call). Works for WPF as well.. You can then specify the desired loaders explicitly, or make them be recognized and instantiated by the autodetection feature.
Translator.Builder //.WithLanguagesLoader<JsonLanguagesLoader>() //.WithTranslationTableLoader<JsonTranslationTableLoader>() //.WithLanguage("en-US") //.WithUpdates() .Build() .StoreInstance();
Translator.Builder _ //.WithLanguagesLoader<JsonLanguagesLoader>() _ //.WithTranslationTableLoader<JsonTranslationTableLoader>() _ //.WithLanguage("en-US") _ //.WithUpdates() _ .Build() _ .StoreInstance()
Step 3 – Creating the file structure
As we are using a file-based approach in our usecase, we now need to configure the folder and file structure (inside your folder, where the .exe file will reside):
- i18n
- json
- languages.json
- en-US_English.json
- de-DE_Deutsch.json
- json
Providing the available languages
In the next step, we are going to create a simple languages.json file, which provides 2 languages:
[{"iso": "de-DE", "name": "Deutsch"},{"iso": "en-US", "name": "English"}]
Supplying some actual translations
The translations of our example, will reside in the corresponding files as seen in the file structure from above.
{"btnEnglish":"English", "btnGerman": "German"}
{"btnEnglish":"Englisch", "btnGerman": "Deutsch"}
Step 4 – Load languages and go!
Now you could tell the translator to actually look for the available languages, we specified above. This could be done inside an async Form Load handler, or anything similar. If you’ve used/chained the „WithLanguage“ function from above, the language will be set after loading has completed.
public class Form1 { public Form1() { // Translator initialization from step 2 InitializeComponent(); Load += Form1_Load; } private async void Form1_Load(object sender, EventArgs e) { await Translator.Instance.LoadLanguagesAsync(); // after the languages have been loaded, the translator // set the language automatically, if WithLanguage("en-US") // has been used during initialization // if not, you could just trigger the initial language // change/set now // await Translator.Instance.ChangeLanguageAsync("en-US"); // you could now get a specific translation by // (keep in mind, that on using WithLanguage, you // would actually need to like listen to the initial // LanguageChanged event, to get the translation) ITranslation translation = Translator.Instance.Translate("btnEnglish"); // translation.Value -> English in en-US, Englisch in de-DE } }
Public Class Form1 Sub New() ' Translator initialization from step 2 InitializeComponent() AddHandler Load, AddressOf Form1_Load End Sub Private Async Sub Form1_Load(sender As Object, e As EventArgs) Await Translator.Instance.LoadLanguagesAsync() ' after the languages have been loaded, the translator ' set the language automatically, if WithLanguage("en-US") ' has been used during initialization ' if not, you could just trigger the initial language ' change/set now ' Await Translator.Instance.ChangeLanguageAsync("en-US") ' you could now get a specific translation by ' (keep in mind, that on using WithLanguage, you ' would actually need to like listen to the initial ' LanguageChanged event, to get the translation) Dim translation As ITranslation = Translator.Instance.Translate("btnEnglish") ' translation.Value -> English in en-US, Englisch in de-DE End Sub End Class
Optional – Further language changes at runtime
If you want the translator to support additional language changes at runtime, just chain the „WithUpdates()“ method on initialization. This behaviour is disabled for efficiency by default (not everyone needs it.. and the typical thing is mostly, to just restart your app..).