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:
resx
files? String and Image Localization in Xamarin. Then the question becomes how MAUI can reference resources in aresx
, that changes dynamically based on language/culture. Theresx
files would probably be managed by .Net 6 as specified inLocalization 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