.NET MAUI : Write multilingual apps easily
If you need a solution to create multilingual applications without the hassle of implementing all kinds of code, I have good news for you. I’ve migrated my Xamarin.Forms package to .NET MAUI, and you can easily build multilingual applications with MAUI. No need to restart the application, the language takes effect immediately and works on all platforms. You can also use it from XAML, and C# code. It can handle multiple resource files at the same time. It can store the last language set by the user, and the next time you restart it, it will use the same language as the last time the user set it. But instead of letters, watch this video to see what exactly it can do:
Let’s get started
This project is available on GitHub: https://github.com/banditoth/MAUI.Packages
To start with, I’ve put together a demo app for you for the sake of demonstration. There is no extra functionality, just 4 buttons. Three of them are to change the language of the application, and one of them is to give us a pop-up window. There is no logic to it yet, but together we will build on the article.
Create translation files
The first thing you will need are files containing multilingual translations. If you are familiar with Xamarin Forms, I will not surprise you: we will use files with RESX extension. By default, all MAUI applications include a directory called Resources. In this directory, I create a directory called Translations next to the Fonts, Images, and Raw directories, where I put the translations used by the application.
Decide what the default language of your application should be. This is important because the default language is not indicated by a language code at the end of the filename. This language (and file) will be the one that the application will use as fallback, so that if, say, the application can’t find the text in the Hungarian or German translations, it can print it out in that language. In my case, the default language of the application will be English, so I didn’t specify it in the filename.
I’ve created translations for each language with this data:
<data name="WelcomeMessage" xml:space="preserve">
<value>The quick brown fox jumps over the lazy dog</value>
</data>
<data name="AlertTitle" xml:space="preserve">
<value>Important notice</value>
</data>
<data name="AlertContent" xml:space="preserve">
<value>.NET Bot is cute!</value>
</data>
<data name="AlertConfirm" xml:space="preserve">
<value>Agree</value>
</data>
Since ResX files are set by default to generate a code-behind file from their data, this automatic generation should be disabled for all ResX files other than the default language.
This can be easily done on windows, just click on the file (in this case the German translation) and in the properties window, under Custom tool, clear the ResxFileCodeGenerator value. If you are using Visual Studio on a Mac, you cannot delete this from the interface, so you have to manually remove the option in the project file.
For Visual Studio for mac, find the name of the files containing the translation in the project and delete the following lines:
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>AppTranslations.de.Designer.cs</LastGenOutput>
If you did everything right, you will only have these files. If there are any ‘.Designer’ files left in the Solution, feel free to delete them. The important thing is that when you compile the application, they are not recreated by Visual Studio
Install banditoth.MAUI.Multilanguage
Click on your project with right, then select ‘Manage NuGet Packages’. Search for banditoth.MAUI.Multilanguage in the search bar, and install the result with the exact same name of your search text 🙂 This project is available on GitHub by the way, so if you are experiencing issues with it, you can report over here: https://github.com/banditoth/MAUI.Packages
Initialise the component
Let’s navigate to your MauiProgram.cs file, and add some init logic in it!
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.ConfigureMultilanguage(config =>
{
// Set the source of the translations
// You can use multiple resource managers by calling UseResource multiple times.
config.UseResource(AppTranslations.ResourceManager);
// If the app is not storing last used culture, this culture will be used by default
config.UseDefaultCulture(new System.Globalization.CultureInfo("en-US"));
// Determines whether the app should store the last used culture
config.StoreLastUsedCulture(true);
// Determines whether the app should throw an exception if a translation is not found.
config.ThrowExceptionIfTranslationNotFound(false);
// You can set custom translation not found text by calling this method
config.SetTranslationNotFoundText("Transl_Not_Found:", appendTranslationKey: true);
});
return builder.Build();
}
Use it in code
Usage in XAML
In order to use our translations in XAML files, we need to declare the tool’s namespace in XAML. Go to the top of your XAML file, and make the following declaration:
xmlns:multilanguage="clr-namespace:banditoth.MAUI.Multilanguage;assembly=banditoth.MAUI.Multilanguage"
After this declaration you can access the translation tool, so let change this code:
<Label
Text="This text should be multilingual"
FontSize="18"
HorizontalOptions="Center" />
to this:
<Label
Text="{multilanguage:Translation Key=WelcomeMessage}"
FontSize="18"
HorizontalOptions="Center" />
Usage in C# code
Wherever we need to pass multilingual texts to or from another method, the ITranslator interface comes in handy. This interface is registered in the MAUI ‘s dependency container, so it can be retrieved from the container by constructor injection for example.
private readonly ITranslator translator;
public MainPage(ITranslator translator)
{
InitializeComponent();
this.translator = translator;
}
Unfortunately currently .NET MAUI is not supporting Constructor injection for Shell, but you can request any service from anywhere by adding this little work around to App.xaml.cs (but its definitely not nice):
public App(IServiceProvider serviceProvider)
{
InitializeComponent();
ServiceProvider = serviceProvider;
MainPage = new AppShell();
}
public static IServiceProvider ServiceProvider { get; private set; }
The localized string can be retreived like this (This example shows an Alert dialog for the user in the correct language)
void DisplayAlertButton_Clicked(System.Object sender, System.EventArgs e)
{
_ = DisplayAlert(translator.GetTranslation("AlertTitle"),
translator.GetTranslation("AlertContent"),
translator.GetTranslation("AlertConfirm"));
}
Change language runtime
To change language, you need to call the SetCurrentCulture method with the desired CultureInfo, like this:
void ChangeToEnglishButton_Clicked(System.Object sender, System.EventArgs e)
{
translator.SetCurrentCulture(new CultureInfo("en"));
}
void ChangeToGermanButton_Clicked(System.Object sender, System.EventArgs e)
{
translator.SetCurrentCulture(new CultureInfo("de"));
}
void ChangeToHungarianButton_Clicked(System.Object sender, System.EventArgs e)
{
translator.SetCurrentCulture(new CultureInfo("hu"));
}
17 Comments
Leave a Reply Cancel reply
This site uses Akismet to reduce spam. Learn how your comment data is processed.
Hi, I am András,
I am a seasoned software engineer from Budapest, Hungary with a strong focus on mobile app development using .NET MAUI and Xamarin.Forms. My expertise also extends to website building for my happy customers and other complex system designing. I am passionate about developing well-organized, maintainable software solutions.
Pont ezt kerestem, én is most váltok MAUI-ra. Köszi!
Thanks for your feedback! 🙂
Great work – thank you!
[…] I am currently working on a students project, comparing native iOS development with MAUI (Multi-platform App UI). When searching for a simple solution to make the app react to changes to a devices language settings, I found this great NuGet package from András Tóth. […]
I have a question – this is in regard to using your package with SemanticProperties.Description for accessibility / screenreaders:
If I use it in this way, it works perfectly:
SemanticProperties.Description=”{multilanguage:Translation Key=Minutes}”
I would like to use it with a Binding to a property in a viewmodel and String interpolation. This works:
SemanticProperties.Description=”{Binding LengthInMinutes, StringFormat='{0} minutes’}”
But I can not use the translation instead of the fixed text like so:
SemanticProperties.Description=”{Binding LengthInMinutes, StringFormat='{0} {multilanguage:Translation Key=Minutes}’}”
The translation is simply ignored in this case. Is there a way to do this?
Hey Lilly, thanks for sharing your use-case.
It is currently not possible with the current version of the plugin, but I will start thinking on a solution for it.
Stay tuned for the updates!
bandi
I got a Label, how do i change the string in the XAML via C#?
I tried this but it treats it like a String not like a Binding?!
Label_Prompt.Text=”{multilanguage: Translation Key = strPasswordsDoNotMatch}”;
Label_Prompt.Text=”{multilanguage: Translation Key = strPasswordIsToShort}”;
How u can help me 🙂
Hey Fabi, thanks for your question.
Make sure you have dependency injected an ITranslator instance, like this:
private readonly ITranslator translator;
public MainPage(ITranslator translator)
{
InitializeComponent();
this.translator = translator;
}
If you do not know what is dependency injection, check out this video by Gerald explaining it in details: https://www.youtube.com/watch?v=tTJetZj3vg0
Use the translator instance like this: Label_Prompt.Text = translator.GetTranslation(“strPasswordsDoNotMatch”);
Hi,
running into an issue where i have no idea what i’m missing following your instructions:
Could not find the resource “TotalTrackMaui.Resources.Translations.tt_language.en.resources” among the resources “TotalTrackMaui.Resources.Translations.tt_language.resources”, “TotalTrackMaui.Send.Templates.BackupDbEmail.html”, “TotalTrackMaui.Send.Templates.OAuthResponse.html”, “TotalTrackMaui.App.xaml”, “TotalTrackMaui.AppShell.xaml”, “TotalTrackMaui.Platforms.Windows.App.xaml”, “TotalTrackMaui.Resources.Styles.Colors.xaml”, “TotalTrackMaui.Resources.Styles.Styles.xaml”, “TotalTrackMaui.Views.AboutPage.xaml”, “TotalTrackMaui.Views.AmmoRecipesPage.xaml”, … embedded in the assembly “TotalTrackMaui”, nor among the resources in any satellite assemblies for the specified culture. Perhaps the resources were embedded with an incorrect name.
it took me some time to understand what is the cause:
1. creating a resource file called tt_language.en.resx is being translated to tt_language_en.resx
to circumvent this issue the error already indicated the direction.
1. Create a folder under resources/translations/tt_language
2. create the resource file en.resx
This creates the correct namespace being TotalTrackMaui.Resources.Translations.tt_language.en.resources
Ignore – i had to fiddle with it to make it work. Removed the folder and moved the files back to include tt_language.en.resx and now it works.
When working with the appshell i notice that adding the ‘xmlns:multilanguage=”clr-namespace:banditoth.MAUI.Multilanguage;assembly=banditoth.MAUI.Multilanguage”‘ and setting
is throwing an error.
TotalTrackMaui/TotalTrackMaui/Microsoft.Maui.Controls.SourceGen/Microsoft.Maui.Controls.SourceGen.CodeBehindGenerator/AppShell.xaml.sg.cs(67,67): Error CS0246: The type or namespace name ‘Translation’ could not be found (are you missing a using directive or an assembly reference?) (CS0246) (TotalTrackMaui)
Is that because the compiler doesn’t know about this resource yet?
Does this mean that flyout menu items need to be changed further down in the code?
As i’m using the AppShell i quite don’t understand what you mean with using the serviceProvider to get the service. serviceProvider.GetService( … ) needs something to find the actual resource.
How does this work?
public static IServiceProvider ServiceProvider { get; private set; }
private static ITranslator _Translator;
public static ITranslator Translator
{
get
{
return _Translator;
}
}
public App(IServiceProvider serviceProvider)
{
InitializeComponent();
ServiceProvider = serviceProvider;
var trans = serviceProvider.GetService(typeof(ITranslator));
_Translator = trans as ITranslator;
Translator.GetTranslation(“”);
…
}
Is this what you meant?
Hello.
Is it possible to use the translation in the following context?
<Label Text="{multilanguage:Translation Key=<>}” />
For the keys that do not need a dynamic value, it is working properly:
I’ve tried to do the translation in the ViewModel:
item.ValueOneTranslated = _translator.GetTranslation(item.ValueOne);
and then use it like this:
( I am using the translation inside a CollectionView )
This is working properly when the page is first rendered, but if I switch the language from a button, then the translation is not working. The SetCurrentCulture is triggered, the “static” translations are working as expected, but the method that is populating the ValueOneTranslated property of the object is not triggered anymore, so the translation is not working..
Is there any way to “fix” this small issue?
Providing binding from code behind is not a feature right now, but its a great idea! Thanks for sharing!
Stay tuned on this
This is working great, thanks!
Hi,
I did as suggested in the previous post in App.xaml.cs:
public partial class App : Application
{
private static ITranslator _translator;
public static ITranslator Translator
{
get
{
return _translator;
}
}
public App(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
var trans = serviceProvider.GetService(typeof(ITranslator));
_translator = trans as ITranslator;
_translator.GetTranslation(“”);
_translator.SetCurrentCulture(CultureInfo.CurrentUICulture);
…}
I have Resources folder with Translations subfolder and files AppTranslations.fr.resx ; AppTranslations.resx are inside
if you add to App.xaml.cs
AppDomain.CurrentDomain.FirstChanceException += CurrentDomain_FirstChanceException;
and
private void CurrentDomain_FirstChanceException(object sender, FirstChanceExceptionEventArgs e)
{
Logger.Fatal($”FirstChanceException! Details: {sender}; {e?.Exception}”);
}
you will catch the exception
– Exception {System.IO.FileNotFoundException} System.IO.FileNotFoundException
+ base {System.IO.IOException} System.IO.IOException
FileName “MyProjectName.resources” string
banditoth.MAUI.Multilanguage version 1.0.5
VS 2022 Version 17.6.5
Please advise.
Hey Irina,
Please fill out a bug in the MAUI.Packages repo with some repro code.
Thanks, bandi