rskibbe.I18n – Translate / create multilanguage .NET Apps

rskibbe.I18n - Multilanguage apps - but easy - Translating WPF and Windows forms apps
rskibbe.I18n – Multilanguage apps – but easy – Translating WPF and Windows forms apps

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!

💡 In a hurry?: No problem, just navigate to the points of interest by using the table of contents from above. If you need a quick example, or an example download, scroll to the bottom.

++ 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 rskibbe.I18n for Winforms and WPF Translation
Setting up rskibbe.I18n for Winforms and WPF Translation

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

Quick example for rskibbe.I18n translating Winforms and WPF applications
Quick example for rskibbe.I18n translating Winforms and WPF applications

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

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

Downloads

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert