11

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?

Brad Taylor
  • 111
  • 1
  • 1
  • 5
  • Please provide enough code so others can better understand or reproduce the problem. – Dragonthoughts Mar 02 '22 at 15:29
  • Are you referring to what is done (for Xamarin) via `resx` files? [String and Image Localization in Xamarin](https://learn.microsoft.com/en-us/xamarin/xamarin-forms/app-fundamentals/localization/text?pivots=windows). 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`(https://learn.microsoft.com/en-us/dotnet/core/extensions/localization). But I'm not sure how MAUI would be pointed to the current file. – ToolmakerSteve Mar 11 '22 at 22:30

6 Answers6

14

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


}
mdimai666
  • 699
  • 8
  • 14
  • 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 answer – innom Nov 29 '22 at 14:40
  • It doesn't work for me in main page. I needed to override method OnNavigatedTo and run InitializeComponent() for this page. – Silny ToJa Jan 12 '23 at 19:03
  • @SilnyToJa try Reset() method – mdimai666 Jan 13 '23 at 07:02
  • It didn't work because of my mistake. MainPage was set to singleton ;) – Silny ToJa Jan 13 '23 at 09:50
  • @mdimai666 can you put content example for AppRes.resx file please? – knocte Apr 19 '23 at 06:53
10

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

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!

deniz
  • 725
  • 10
  • 13
  • Thanks! I followed the steps, but AppStrings and LocalizableStrings identifiers are not found – chtenb May 30 '23 at 08:24
  • 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. – chtenb May 30 '23 at 08:30
  • It seems to work/compile after manually adding the following XML to the csproj file: True True LocalizableStrings.resx ResXFileCodeGenerator LocalizableStrings.Designer.cs – chtenb May 30 '23 at 08:33
  • 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. – deniz Jun 01 '23 at 14:23
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.

Mephisztoe
  • 3,276
  • 7
  • 34
  • 48
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.

innom
  • 770
  • 4
  • 19
  • 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. – Wolfgang Schreurs Mar 18 '23 at 01:26
  • 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). – Wolfgang Schreurs Mar 18 '23 at 01:33
1

First, put your localized string (e.g. LBL_HELLO, LBL_WELCOME) in string resources. The following defines strings for 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:maui_localize_resx.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 react to language changes. This is unfortunate because the underlying string resource actually does change its string value in response to localization changes. One would have to reopen the page, or, even reopen the app before they see localization changes.

A simple solution is to 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 CultureInfo.CurrentUICulture and then emit an appropriate OnPropertyChanged:

CultureInfo newCulture = new CultureInfo("fr-FR");
CultureInfo.CurrentUICulture = newCulture;
CultureInfo.CurrentCulture = newCulture; // Optional
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 = ServiceHelper.GetService<IStringLocalizer<AppStrings>>();
public IStringLocalizer Localizer => _localizer;

In XAML:

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

To change language, update CurrentUICulture, but, now, we just only need to raise OnPropertyChanged(nameof(Localizer)) signal:

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

If your app has multiple pages, rather than implementing a Localizer for every page, we can move the code out of your page's ViewModel into a LocalizationManager.Current singleton ViewModel that you can call in your code, e.g.

public partial class LocalizationManager : ObservableObject
{
    private static LocalizationManager _current;
    public static LocalizationManager Current => _current ??= new LocalizationManager();
    public FlowDirection FlowDirection => Culture.TextInfo.IsRightToLeft ? FlowDirection.RightToLeft : FlowDirection.LeftToRight;
    private IStringLocalizer _localizer = ServiceHelper.GetService<IStringLocalizer<AppStrings>>();
    public IStringLocalizer Localizer => _localizer;
    public CultureInfo Culture
    {
        get => CultureInfo.CurrentUICulture;
        set
        {
            if (value.Name == CultureInfo.CurrentUICulture.Name) return;
            CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = value;
            OnPropertyChanged(nameof(FlowDirection));
            OnPropertyChanged(nameof(Localizer));
        }
    }
}

Then, in your XAML, you can bind to that LocalizationManager.Current singleton anywhere using {x:Static} with the LocalizationManager.Current.Culture setter broadcasting resource string changes to all your pages at once:

<ContentPage FlowDirection="{Binding FlowDirection,Source={x:Static local:LocalizationManager.Current}}">
    <Label Text="{Binding Localizer[LBL_HELLO],Source={x:Static local:LocalizationManager.Current}}"/>
    <Label Text="{Binding Localizer[LBL_WELCOME],Source={x:Static local:LocalizationManager.Current}}"/>
</ContentPage>

The above XAML is quite ugly to look at, so, we can use markup extensions to refactor the XAML to:

<ContentPage FlowDirection="{local:Localize FlowDirection}">
    <Label Text="{local:Localize LBL_HELLO}"/>
    <Label Text="{local:Localize LBL_WELCOME}"/>
</ContentPage>

The Localize markup extension, basically, implements a direct Binding against LocalizationManager.Current singleton and its member Localize[Path]:

[ContentProperty(nameof(Path))]
public class LocalizeExtension : IMarkupExtension<BindingBase>
{
    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(
            Path == "FlowDirection" ? Path : $"Localizer[{Path}]",
            Mode, Converter, ConverterParameter, StringFormat, LocalizationManager.Current);
}

A cool feature of the LocalizationManager.Current implementation is the implementation of Culture as a property. This means, we can create a TwoWay binding to it and set change the languages from XAML. For instance, assume you have the following in your ViewModel:

private List<CultureInfo> _languages = new List<CultureInfo>()
{
    new CultureInfo("en-US"),
    new CultureInfo("fr-FR"),
    new CultureInfo("de-DE")
}
public List<CultureInfo> Languages => _languages;

For UI, let's say we provided flags as flag_us.svg, flag_fr.svg and flag_de.svg. CollectionView can be used to combine Languages, LocalizationManager.Current.Culture to build a language switcher:

<CollectionView ItemsSource="{Binding Languages}"
                SelectedItem="{local:Localize Culture,Mode=TwoWay}"
                SelectionMode="Single">
    <CollectionView.ItemTemplate>
        <DataTemplate>
            <Image Source="{Binding TwoLetterISOLanguageName,StringFormat='flag_{0}.png'}"
                   WidthRequest="32"
                   HeightRequest="32"/>
        </DataTemplate>
    </CollectionView.ItemTemplate>
</CollectionView>

To demonstrate some of these ideas, I've localized the starter MAUI app in multiple different ways in a GitHub repo https://github.com/stephenquan/maui-localize-test

Stephen Quan
  • 21,481
  • 4
  • 88
  • 75