fredag den 20. maj 2022

Exploring the capabilities of .NET MAUI part 1: Localization in .NET MAUI

Recently I have been following the development of .NET MAUI, which is going to replace Xamarin.Native/Xamarin.Forms, and thought that I might as well document some of my findings here.

Note: As of writing this, you have to install the Visual Studio Preview in order to develop .NET MAUI apps.


To demonstrate how to localize your MAUI app, I will be using the default .NET MAUI App template, as it already provides a simple app that is a good candidate for illustrating something simple like localization.




After creating a new project from a template, I like to build and run it as is, just to ensure that everything is working ok. The default MAUI template app is just a basic hello world with a button that increases a counter and updates the "Current count..." label text to reflect the counter, it looks like this:


We need to add a few resource files that contain the localized strings, and I chose to create a new folder in the Resources directory of my project, called Strings, so as to contain the localization strings (is handy if you support a lot of languages) to which I add one Resources File per language I want to support.


I want to support English and Danish in my app, so after adding adding AppResource.resx for the default English localization, and AppResource.da.resx for the Danish localization, we should have something similar to this:


Let's start with making the default language resource file - Visual Studio provides us with an easy way to insert strings to this resource file, so now we just want to move all the strings from the default MAUI template app into our default AppResource.resx file, like so:


After moving all the strings, we can now access the strings by referencing the resource file's namespace in xaml and access the strings directly, like so:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="localization_demo.MainPage"
             xmlns:res="clr-namespace:localization_demo.Resources.Strings"> <!-- here we declare the namespace that contains the strings -->

    <ScrollView>
        <VerticalStackLayout Spacing="25" Padding="30">

            <Label 
                Text="{x:Static res:AppResource.Hello_World}"
                SemanticProperties.HeadingLevel="Level1"
                FontSize="32"
                HorizontalOptions="Center" />

            <Label 
                Text="{x:Static res:AppResource.Welcome}"
                SemanticProperties.HeadingLevel="Level1"
                SemanticProperties.Description="{x:Static res:AppResource.Welcome_Desc}"
                FontSize="18"
                HorizontalOptions="Center" />

            <Label 
                Text=""
                FontSize="18"
                FontAttributes="Bold"
                x:Name="CounterLabel"
                HorizontalOptions="Center" />

            <Button 
                Text="{x:Static res:AppResource.Click_me}"
                FontAttributes="Bold"
                SemanticProperties.Hint="{x:Static res:AppResource.Click_me_Hint}"
                Clicked="OnCounterClicked"
                HorizontalOptions="Center" />

            <Image
                Source="dotnet_bot.png"
                SemanticProperties.Description="{x:Static res:AppResource.dotnet_bot_png_Desc}"
                WidthRequest="250"
                HeightRequest="310"
                HorizontalOptions="Center" />

        </VerticalStackLayout>
    </ScrollView>
</ContentPage>


The CounterLabel text is empty in the xaml because I set it programmatically in the MainPage constructor:

using localization_demo.Resources.Strings;
namespace localization_demo;

public partial class MainPage : ContentPage
{
	int count = 0;

	public MainPage()
	{
		InitializeComponent();
		UpdateCounterLabelText(); // set CounterLabel text
	}

	private void UpdateCounterLabelText()
    {
		CounterLabel.Text = $"{AppResource.Current_count} {count}";
	}

	private void OnCounterClicked(object sender, EventArgs e)
	{
		count++;
		UpdateCounterLabelText();

		SemanticScreenReader.Announce(CounterLabel.Text);
	}
}


We can add the same strings to the AppResource.da.resx file and translate them like so:


Which enables us to switch the resource using the Thread.CurrentThread.CurrentCulture and Thread.CurrentThread.CurrentUICulture APIs provided by .NET. For illustrative purposes, I modified the AppShell constructor to change locale to Danish:

public AppShell()
{
    // change locale to da-DK:
    Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("da-DK");
    Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("da-DK");
    InitializeComponent();
}



Now you can create a language selection screen to let the user pick their preferred language, or even let .NET automatically pick the current language of the system the app is running on!

Incidentally, this is what my next post will cover, since I wanted to keep this one short.

Edit: The project can now be found on my GitHub