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