How to implement .NET MAUI localization
Asked Answered
G

6

13

I'm unable to find any direction on implementing localization for a MAUI app. I've read some info about localizing Xamarin apps but am unable to translate it forward to MAUI even after extensive web searching.

Can anyone point me to a reference that I may have missed?

Grivation answered 1/3, 2022 at 22:14 Comment(2)
Please provide enough code so others can better understand or reproduce the problem.Cockerham
Are you referring to what is done (for Xamarin) via resx files? String and Image Localization in Xamarin. Then the question becomes how MAUI can reference resources in a resx, that changes dynamically based on language/culture. The resx files would probably be managed by .Net 6 as specified in Localization in .NET(learn.microsoft.com/en-us/dotnet/core/extensions/localization). But I'm not sure how MAUI would be pointed to the current file.Pacifically
S
17

Try this - Create standard resources

  • "Add New Item/Resource File" > MauiApp1/Resources
  • set name "AppRes.resx"
  • create second lang "AppRes.ru.resx"
  • add strings

explorer resource view

how use in XAML

[...] xmlns:res="clr-namespace:MauiApp1.Resources" 

<Button Text="{x:Static res:AppRes.Title}" />

use code

//get lang as "en"
string lang = Thread.CurrentThread.CurrentCulture.TwoLetterISOLanguageName;

//toggle lang
if(lang == "ru")
{
    Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("ru-RU");
    Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("ru-RU");
}
else
{
    Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");
    Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("en-US");
}

//get translated title
//using MauiApp1.Resources;
string title = AppRes.Title

And for update just reset app

(App.Current as App).MainPage = new AppShell();

That's All

UPD1: for restart from anywere

void Reset()
{
    (App.Current as App).MainPage.Dispatcher.Dispatch(() =>
    {
        // there some LoadLang method;
        (App.Current as App).MainPage = new AppShell();//REQUIRE RUN MAIN THREAD
    });


}
Scarificator answered 8/4, 2022 at 12:1 Comment(5)
This is a working solution BUT you have to create the resourcefile from windows, if you try it on mac, as there is no designer, an old xamarin.file gets created. I post the new file, and this should do the trick in my answerRochus
It doesn't work for me in main page. I needed to override method OnNavigatedTo and run InitializeComponent() for this page.Quadragesimal
@SilnyToJa try Reset() methodScarificator
It didn't work because of my mistake. MainPage was set to singleton ;)Quadragesimal
@Scarificator can you put content example for AppRes.resx file please?Denoting
R
11

Use Microsoft Extensions Localization package

Create Class For LocalizeExtension. Here AppStrings are ResourceFileName which you have given

[ContentProperty(nameof(Key))]
public class LocalizeExtension : IMarkupExtension
{
    IStringLocalizer<AppStrings> _localizer;

    public string Key { get; set; } = string.Empty;

    public LocalizeExtension()
    {
        _localizer = ServiceHelper.GetService<IStringLocalizer<AppStrings>>();
    }

    public object ProvideValue(IServiceProvider serviceProvider)
    {
        string localizedText = _localizer[Key];
        return localizedText;
    }

    object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) => ProvideValue(serviceProvider);
}

XAML

    <Button Text="{local:Localize Key}"/>

Check out this SampleApp for more details LocalizationDemo

Rodolphe answered 13/7, 2022 at 9:3 Comment(2)
This works great! Super easy to integrate into .NET Maui app.Shadrach
It works as intended but if you change the language it doesn't get updated liveJackknife
R
6

This answer is similar to Valliappan's except that it is more comprehensive and you do not need to check the github repo to connect the remaining dots. Also MAUI is highly evolving framework so hopefully this answer remains relevant for a while.

Step 1: Add Microsoft Extensions Localization Nuget package to your project

Step 2: Add one or more resource files (.resx) to your project. Give any name to the files - such as LocalizableStrings.fr-CA.resx. Normally this is added to the Resources/Strings folder but for some reason my Visual Studio mac edition complains about this location. If that happens, find another location - it doesn't matter.

Step 3: Add your keys and translations to your .resx file.

Step 4: Add Microsoft Extensions Dependency Injection nuget, if you haven't already.

Step 5: (Optional) Create Dependency Injection Helper class to be able to get services on-demand. Or re-use the one if you already have a way to retrieve injectable services.

namespace yourproject
{
    public static class ServiceHelper
    {
        public static TService GetService<TService>() => Current.GetService<TService>();

        public static IServiceProvider Current =>
#if WINDOWS
            MauiWinUIApplication.Current.Services;
#elif ANDROID
            MauiApplication.Current.Services;
#elif IOS || MACCATALYST
            MauiUIApplicationDelegate.Current.Services;
#else
            null;
#endif
    }
}

Step 6: Create a MarkupExtension. Detailed information can be found on Microsoft's site; however, here is the gist.

namespace yourproject.modules.localization //this namespace is important
{
    [ContentProperty(nameof(Key))]
    //give any name you want to this class; however,
    //you will use this name in XML like so: Text="{local:Localize hello_world}"
    public class LocalizeExtension: IMarkupExtension
    {
        //Generic LocalizableStrings name has to match your .resx filename
        private IStringLocalizer<LocalizableStrings> _localizer { get; }

        public string Key { get; set; } = string.Empty;

        public LocalizeExtension()
        {
            //you have to inject this like so because LocalizeExtension constructor 
            //has to be parameterless in order to be used in XML
            _localizer = ServiceHelper.GetService<IStringLocalizer<AppStrings>>();
        }

        public object ProvideValue(IServiceProvider serviceProvider)
        {
            string localizedText = _localizer[Key];
            return localizedText;
        }

        object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider) => ProvideValue(serviceProvider);
    }
}

Step 7: Go to MauiProgram and add couple of services to your services collections like so:

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder
            .UseMauiApp<EclypseApp>()
            ...
            .RegisterServices(); //register injectable services here

        return builder.Build();
    }


    private static MauiAppBuilder RegisterServices(this MauiAppBuilder mauiAppBuilder)
    {
        //this service is needed to inject IStringLocalizer into LocalizeExtension
        mauiAppBuilder.Services.AddLocalization();  

        //IStringLocalizer appears to be dependent on a logging service 
        mauiAppBuilder.Services.AddLogging();
        
        ... //register other services here
    }
}

Last step: Now in your XAML, you can use the MarkupExtension 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="..."
             xmlns:local="clr-namespace:yourproject.modules.localization" //use the same namespace as in Step 5
             >
    <VerticalStackLayout>
        <Label 
            Text="{local:Localize Key=a_key_in_your_resx_file}"
            VerticalOptions="Center" 
            HorizontalOptions="Center" />
    </VerticalStackLayout>
</ContentPage>

Cheers!

Reformer answered 28/9, 2022 at 16:50 Comment(4)
Thanks! I followed the steps, but AppStrings and LocalizableStrings identifiers are not foundDekko
I think AppStrings should be LocalizableStrings, and LocalizableStrings should be generated from the resx file. But that does not seem to happen for some reason.Dekko
It seems to work/compile after manually adding the following XML to the csproj file: <ItemGroup> <Compile Update="Resources\Strings\LocalizableStrings.Designer.cs"> <DesignTime>True</DesignTime> <AutoGen>True</AutoGen> <DependentUpon>LocalizableStrings.resx</DependentUpon> </Compile> </ItemGroup> <ItemGroup> <EmbeddedResource Update="Resources\Strings\LocalizableStrings.resx"> <Generator>ResXFileCodeGenerator</Generator> <LastGenOutput>LocalizableStrings.Designer.cs</LastGenOutput> </EmbeddedResource> </ItemGroup>Dekko
It looks like the files you added weren't included in the output. I think we need an intermediary step in the instructions above saying that make sure the files are not ignored by Visual Studio.Reformer
J
3

In this answer, while it does repeat what others have said, I wanted to start with the basic explanation of how localization works with resource strings and slowly progress to why you would use a third package and their XAML markup extensions and/or converters.

First, put your localized string (e.g. LBL_HELLO, LBL_WELCOME) in string resources. For instances, the following defines these strings in 3 languages (language default, French and German):

Resources/Strings/AppStrings.resx
LBL_HELLO    "Hello, World!"
LBL_WELCOME  "Welcome to .NET Multi-platform App UI"

Resources/Strings/AppStrings.fr.resx
LBL_HELLO    "Salut, le monde !"
LBL_WELCOME  "Bienvenue dans .NET Multi-platform App UI"

Resources/Strings/AppStrings.de.resx
LBL_HELLO    "Hallo, Programmierwelt!"
LBL_WELCOME  "Willkommen bei .NET Multi-platform App UI"

Now refer to the string resources in the XAML markup. Use xmlns:resx to declare resx namespace then use {x:Static} to refer to the resource.

<ContentPage xmlns:resx="clr-namespace:MySampleApp.Resources.Strings">
    <Label Text="{x:Static resx:AppStrings.LBL_HELLO}"/>
    <Label Text="{x:Static resx:AppStrings.LBL_WELCOME}"/>
</ContentPage>

Because the above uses the {x:Static} markup extension, it will never change, and will not automatically react to language changes whilst the app is running. This is unfortunate because the underlying string resource actually does support culture lookup to retrieve the correct localized string. One would have to reopen the page, or, even reopen the app before they see localization changes.

A simple solution is to move access to your resource strings to your ViewModel. Also, with the ViewModel, we can consider adding Right-To-Left support, e.g.

public FlowDirection FlowDirection
    => CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft
        ? FlowDirection.RightToLeft
        : FlowDirection.LeftToRight;
public string LBL_HELLO
    => Resources.Strings.AppStrings.LBL_HELLO;
public string LBL_WELCOME
    => Resources.Strings.AppStrings.LBL_WELCOME;

In XAML, we bind to the strings and the FlowDirection:

<ContentPage FlowDirection="{Binding FlowDirection}">
    <Label Text="{Binding LBL_HELLO}"/>
    <Label Text="{Binding LBL_WELCOME}"/>
</ContentPage>

Now to do a localization change, update both CultureInfo.CurrentUICulture and CultureInfo.CurrentCulture and then emit appropriate OnPropertyChanged:

CultureInfo newCulture = new CultureInfo("fr-FR");
CultureInfo.CurrentUICulture = newCulture; // needed to point to the new resource strings
CultureInfo.CurrentCulture = newCulture; // needed for localize currency, dates and decimals
OnPropertyChanged(nameof(FlowDirection));
OnPropertyChanged(nameof(LBL_HELLO));
OnPropertyChanged(nameof(LBL_WELCOME));

With this approach, an OnPropertyChanged() signal is required for each resource string.

To get around this, we can install the 3rd party Microsoft.Extensions.Localization NuGet packages which has a convenient IStringLocalizer for accessing your resource strings. If publicize that via your ViewModel, you can localize any string with it:

public FlowDirection FlowDirection
    => CultureInfo.CurrentUICulture.TextInfo.IsRightToLeft
        ? FlowDirection.RightToLeft
        : FlowDirection.LeftToRight;
private IStringLocalizer _localizer;
public IStringLocalizer Localizer
    => _localizer ??= IPlatformApplication.Current.Services.GetService<IStringLocalizer<Resources.Strings.AppStrings>>();

In XAML:

<ContentPage FlowDirection="{Binding FlowDirection}">
    <Label Text="{Binding Localizer[LBL_HELLO]}"/>
    <Label Text="{Binding Localizer[LBL_WELCOME]}"/>
</ContentPage>

To change language, update CurrentUICulture and CurrentCulture, but, now, we just only need to raise OnPropertyChanged(nameof(Localizer)) signal to have all your strings updated:

CultureInfo newCulture = new CultureInfo("fr-FR");
CultureInfo.CurrentUICulture = newCulture;
CultureInfo.CurrentCulture = newCulture;
OnPropertyChanged(nameof(FlowDirection));
OnPropertyChanged(nameof(Localizer));

To reduce the amount of code in your C# code-behind, there are numerous 3rd party libraries that come with a localization manager and supporting markup extensions and/or converters to help you get the maximum localization experience with minimal code.

One thing these libraries have in common is an app-wide singleton that black boxes setting and broadcasting changes to CultureInfo.CurrentUICulture and CultureInfo.CurrentCulture. They react to those changes in convenient markup extensions. So you only need to specify the name of your string resource and it will take care of the rest.

<ContentPage xmlns:i18n="clr-namespace:Toolkit.Maui.Localization;assembly=Toolkit.Maui.Localization">
    <VerticalStackLayout>
        <Label Text="{i18n:Localize LBL_HELLO}" />
        <Label Text="{i18n:Localize LBL_WELCOME}" />
    </VerticalStackLayout>
</ContentPage>

If you were to implement the above yourself, the LocalizationManager and the LocalizeExtension could be implemented as follows:

// LocalizationManager.cs
public class LocalizationManager : INotifyPropertyChanged
{
    public CultureInfo Culture
    {
        get => CultureInfo.CurrentUICulture;
        set
        {
            if (CultureInfo.CurrentUICulture.Name == value.Name
                && CultureInfo.CurrentCulture.Name == value.Name)
            {
                return;
            }
            value.ClearCachedData();
            CultureInfo.CurrentUICulture = value;
            CultureInfo.CurrentCulture = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Culture)));
            CultureChanged?.Invoke(this, value);
        }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    public event EventHandler<CultureInfo> CultureChanged;
}
// LocalizeExtension.cs
[ContentProperty(nameof(Path))]
public class LocalizeExtension : IMarkupExtension<BindingBase>, INotifyPropertyChanged
{
    private LocalizationManager lm;
    public LocalizationManager LM
        => lm ??= IPlatformApplication.Current.Services.GetService<LocalizationManager>();
    private IStringLocalizer _localizer;
    public IStringLocalizer Localizer
        => _localizer ??= IPlatformApplication.Current.Services.GetService<AppStrings>>();
    public string Path { get; set; } = ".";
    public BindingMode Mode { get; set; } = BindingMode.OneWay;
    public IValueConverter Converter { get; set; } = null;
    public string ConverterParameter { get; set; } = null;
    public string StringFormat { get; set; } = null;
    public object ProvideValue(IServiceProvider serviceProvider)
        => (this as IMarkupExtension<BindingBase>).ProvideValue(serviceProvider);
    BindingBase IMarkupExtension<BindingBase>.ProvideValue(IServiceProvider serviceProvider)
        => new Binding($"Localizer[{Path}]", Mode, Converter, ConverterParameter, StringFormat, this);
    public LocalizeExtension()
        => LM.CultureChanged += OnCultureChanged;
    ~LocalizeExtension()
        => LM.CultureChanged -= OnCultureChanged;
    private void OnCultureChanged(object sender, CultureInfo e)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Localizer)));
    public event PropertyChangedEventHandler PropertyChanged;
}

There are numerous libraries out there for you to check out:

Jimmyjimsonweed answered 7/8, 2023 at 5:36 Comment(0)
C
2

Have a look at the .NET MAUI Reference Application for .NET 6 "Podcast App" you can find here: https://github.com/microsoft/dotnet-podcasts

It makes use of a resource file that contains localizable strings for the UI.

Maybe that helps you.

Ceasar answered 13/3, 2022 at 11:56 Comment(4)
Not a good example. It's just English...Liter
It's a fine example of how to implement localization. The next step would be to add more resource files.Independence
This is a link to a solution, not a solution itself.Audit
Check this link github.com/umeshkamble/LangChangePoll
R
1

This works as stated in the first answer, however, if you are working from mac, you cannot just create a resource file, as it will create an old xamarin resources file which you cant use in maui.

Follow the steps from the top answer, but paste this into your created resources file (from mac) and override all:

<root>
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>


  <data name="Login" xml:space="preserve">
    <value>Login</value>
  </data>


</root>

This file contains one string (at the very bottom) saying "login". You can just add data to this file and it will work.

Rochus answered 29/11, 2022 at 14:42 Comment(2)
Also, ensure the related *.Designer.cs file has its build action set to Compile. By default it seems to set the build action is set to BundleResource in Visual Studio for Mac version 17.5.2.Wellbred
Likewise the *.resx file should have a build action of EmbeddedResource (seems this is also BundleResource by default in Visual Studio for Mac version 17.5.2).Wellbred

© 2022 - 2024 — McMap. All rights reserved.