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?

An infrastructure package helping you translate „things“. Install corresponding sub-packages to help you translate Winforms & WPF applications – but easy. 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.

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:

C#

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

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

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

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

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

}

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

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

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

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

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