SirJohnK/LocalizationResourceManager.Maui

How can I use multiple ResourceManagers?

Opened this issue ยท 22 comments

I like to have a .resx file per ContentPage, so my MauiApp builder looks like:

var builder = MauiApp.CreateBuilder()
	.UseMauiApp<App>()
	.UseLocalizationResourceManager(settings =>
	{
		settings.RestoreLatestCulture(true);

		settings.AddResource(Vistas.Localizables.LocalizacionPage.ResourceManager);
		settings.AddResource(Vistas.Localizables.MainPage.ResourceManager);
		// ...
	})
	// ...

Problem is that all my views .resx files have a key named "Title".

So, I use ILocalizationResourceManager from my ViewModel and the translate extension from my views XAML, but I always get Title from LocalizacionPage's resource manager (the first one I registered on my MauiApp builder).

I mean, if I write this on MainPage's XAML:
<ContentPage Title="{loc:Translate Title}">
Title gets LocalizacionPage's resource manager "Title" key value, but I want MainPage's resource manager value.

Or if I go:

public MainViewModel(ILocalizationResourceManager localizador)
{
	_localizador = localizador;
	
	var titleTest = _localizador[nameof(Title)];
}

titleTest var gets LocalizacionPage's resource manager "Title" key value, but I want MainPage's resource manager value.

So, how could I tell ILocalizationResourceManager to use certain ResourceManager?
Or even better:

  • Tell once at XAML
  • Inject correct ResourceManager on my ViewModel (maybe it would be needed to specify my ViewModel type on the settings.AddResource(...) method)

Hi, @santo998!
This is not possible with how this library is built, but more important, not the way .RESX files are intended to be used!

  • Why not just have 1 .RESX file with all "Titles"?
  • Naming (Key) them after what they say, rather then only naming them by function. e.g. Key: "Title", Value: "Main Page" => Key:"MainPage", Value "Main Page" or Key:"MainPageTitle", Value "Main Page"
<ContentPage Title="{loc:Translate MainPageTitle}">
  • Having 1 .RESX file for each Page will create a lot of files when adding Languages. e..g 20 Pages in 10 Languages will be 200 files!
  • Please read my earlier Issue on this topic with recommendations at the end(!): Iissue 9

/Regards Johan

Hi @SirJohnK

This is not possible with how this library is built

Well, it can be acomplished modifying LocalizationResourceManager class.
It could store ResourceManagers on a Dictionary with a key provided when registering them, instead of a List ...

And, on its GetValue(...) method, instead of doing this:
var value = resources?.Select(resource => resource.GetString(text, CurrentCulture)).FirstOrDefault(output => output is not null);

It could search ResourceManager by key and then search the value. Adding a key parameter or a method overload.

but more important, not the way .RESX files are intended to be used!
This is debatible. In WinForms we had one .resx per Form, because you could localize its design. Here you also can.

I even feel it better having one .resx per view, as I use ResX Manager extension and it allows me to filter per file.
If I want to define shared resources, I define a single .resx, but it would contain only common stuff like "Accept", "Cancel" buttons, etc.

I don't see the issue of having 200 small files instead of one big one. It's total size will be a little higher, and compilation could take few milliseconds more, but JIT would benefict me loading only one view .resx at a time.
And yeah, there is a little maintenance task added (creating the .resx file, writing generated namespace property, etc.) but it can be automated using template when creating view.
But those are minor concerns.

If I use only one big .resx, then I would have lot of "trash" on my keys.
I like to use not only "Title", but also "BtnCounter.Text", "LblCount.Text" for example.
So those keys would become: "MainPage.Title", "MainPage.BtnCounter.Text", "MainPage.LblCount.Text".
It also adds maintenance issues, bigger than having multiple files.
Even it adds unneeded complexity. Not to mention if I localize some XAML design (size, position, etc.)

So, I really prefer having multiple files.

I have three options then:

  1. I would be happy if you change internal implementation as I said at the beginning of this message
  2. If that isn't possible, I would like you to:
  • Create a different implementation of ILocalizationResourceManager, or an implementation of similar interface, that allows register and search by a key
  • Or abstract the collection as generic parameter and make almost all LocalizationResourceManager's private fields protected, to let me inherit from it
  1. Make my own implementation to be able to search by key on both TranslateExtension and LocalizationResourceManager.GetValue(...)

I don't want the 3rd, because it defeats the purpose of using the library.

My ideal use cases would be:

For the XAML (IF POSSIBLE (adding some xmlns maybe)):
<ContentPage Title="{loc:Translate Title}">

If not possible:
<ContentPage Title="{loc:Translate MainPage.Title}">

When registering:
settings.AddResource(Vistas.Localizables.LocalizacionPage.ResourceManager, nameof(Vistas.Localizables.LocalizacionPage));

From ViewModel:

public MainViewModel(ILocalizationResourceManager localizador)
{
	_localizador = localizador;
	
	var titleTest = _localizador[nameof(Title), nameof(MainPage)]; // I don't care about parameters order
}

Even better if I could define ResourceManager file name once, setting some new ILocalizationResourceManager property, like this:

public MainViewModel(ILocalizationResourceManager localizador)
{
	_localizador = localizador;
	
	_localizador.PreferedResourceManager = nameof(MainPage);
	
	var titleTest = _localizador[nameof(Title)];
}

Maybe it can be implemented on another ways I'm not considering here.

/Regards Santiago

It could search by key on the desired ResourceManager file, and have a fallback option to search on others ResourceManagers if value isn't found.

It could get even better, because you could register the ResourceManager objects as Lazy<> so they will be load as needed.

If fallback option isn't specified, then it wouldn't search on others ResourceManagers and they won't be loaded until they are specified.

I need to think about this and come back to you, since I am of on vacation at the moment.

/Johan

@SirJohnK thank you so much!

I just played around with the code to test if it's possible, and the only problem is on TranslateExtension:
How to provide key from each view, to specify which ResourceManager use.

@santo998 , finally I have had the time to look into this! ๐Ÿ˜…

  • I made a draft for a solution in this branch: Specific_ResourceManager
  • With this you can register a ResourceManager with a Name/Key.
  • From XAML and Code you can then reference that Name/Key.

Register with Name/key:

.UseLocalizationResourceManager(settings =>
{
    settings.AddResource(AppResources.ResourceManager);
    settings.AddResource(MainPageResources.ResourceManager, "MainPage");
    settings.RestoreLatestCulture(true);
});

Reference specific ResourceManager in XAML:

<Button
    x:Name="ToggleLanguageBtn"
    Clicked="OnToggleLanguage"
    HorizontalOptions="Center"
    Text="{localization:Translate ToggleLanguage, ResourceManager=MainPage}" />

<Button
    x:Name="CounterBtn"
    Clicked="OnCounterClicked"
    HorizontalOptions="Center"
    SemanticProperties.Hint="{localization:Translate CounterBtnHint}"
    Text="{localization:TranslateBinding Count,
                                         TranslateFormat=ClickedManyTimes,
                                         TranslateOne=ClickedOneTime,
                                         TranslateZero=ClickMe,
                                         ResourceManager=MainPage}" />

Reference specific ResourceManager in Code:

public LocalizedString HelloWorld { get; }
public MainPage(ILocalizationResourceManager resourceManager)
{
    HelloWorld = new(() => $"{resourceManager["Hello", "MainPage"]}, {resourceManager.GetValue("World", "MainPage")}!");
}

The only issue/challenge with this solution is that you do not have the option to set ResourceManager once for the entire page. You need to reference the Name/Key for each translated text.

The reason for this is:

  • When LocalizationResourceManager.CurrentCulture is changed, the OnPropertyChanged event will be triggered and all texts will be updated/retrieved from the new CurrentCulture. (A key feature for this library)
  • This will be triggered for all Pages currently in the Navigation Stack.
  • If each Page retrieves resources from a specific ResourceManager we can not have a "CurrentResourceManager" in the LocalizationResourceManager, since it is handled and registered as a Singleton.

But maybe this is not a big issue and this solution is good enough!?

I would love if you have the time to look at this draft and hear what you think!

/Regards Johan

@santo998 , did you / will you have the time to review this? (Would really appreciate your input!)

@SirJohnK sorry, I was busy, but this was in my to do list.

How can I test it?

Like, is there a preview Nuget? Or I have to clone the repo?

I would like to test it on my test app first, and later review the code.

@santo998 , no problem! Just happy that you are willing to review this.

  • I have, with little testing(!), released a 1.3.0-alpha.1 prerelease version to NuGet that you can test. ๐Ÿ˜ƒ
  • I have some additional ideas for adjustments, but roughly this is my solution for this.

/Johan

@SirJohnK, I updated your library in my test project and used it a bit. Pretty awesome!

However, there are some "pain points":

  1. Like you described, we would have to specify the ResourceManager each time we localize a string in XAML.

In ViewModel, it can be avoided by creating a wrapper method where you hardcode the ResourceManager key, and then reference that method in all your ViewModel code.

I wonder if something like that could be done in XAML...

  1. There isn't IntelliSense in the XAML when setting the ResourceManager key. That can lead to runtime errors and slow development time a bit.

I wonder if those issues could be solved by creating another LocalizationResourceManager class that handles the multiple .resx localization.

I would split the LocalizationResourceManager class into three:

  1. One ResourceManager as a singleton use case.
  2. Multiple ResourceManagers use case.
  3. Common logic. Probably some logic can be reused if analyzed properly.

@santo998 , thanks! ๐Ÿ˜„

Yes, I anticipated the "pain points". ๐Ÿ˜ƒ

Like you described, we would have to specify the ResourceManager each time we localize a string in XAML.

I think I have found a pretty elegant solution to that.

  • By adding a ISpecificResourceManager interface, used on Xaml Pages/Views where a specific ResourceManager is wanted.
  • When the interface is added, the ResourceManager can be defined for the entire ViewElement.
  • Later, when the text/value is retrieved, as defined in Xaml, the interface is resolved and the ResourceManager is added for all text/values on the Page/View.
public partial class SpecificPage : ContentPage, ISpecificResourceManager
{
    public string ResourceManager => nameof(SpecificPage);

In ViewModel, it can be avoided by creating a wrapper method where you hardcode the ResourceManager key, and then reference that method in all your ViewModel code.

I think I have found a pretty elegant solution for this too!

  • When adding a keyed/with name ResourceManager, a keyed Specific version of ILocalizationResourceManager will be registered in DI.
  • The keyed Specific version of ILocalizationResourceManager can later be injected in your ViewModel or resolved anywhere it is needed and can be used exactly as the same as the non keyed ILocalizationResourceManager!
  • This requires upgrading the library to .NET 8, but that was overdue anyway...! ๐Ÿ˜†
public SpecificPage([FromKeyedServices(nameof(SpecificPage))] ILocalizationResourceManager resourceManager)

There isn't IntelliSense in the XAML when setting the ResourceManager key. That can lead to runtime errors and slow development time a bit.

This is tricky! Not sure it is easily solved. But to be fair, we do not have IntelliSense for any of the resource texts and with the new features, mentioned above, I do not think it will be a big issue.

I will soon release a alpha.2 prerelease of this for you to test!

/Regards Johan

@SirJohnK amazing!

It took me some readings to fully understand the solutions you encountered.

Pretty creative making the Page implement an ISpecificResourceManager interface.

Now the "pain points" are gone!

I would love to test your new version. I will wait.

Now I think your library can be massively adopted by the MAUI community!

Thank you in advance,
Santiago

Hi @SirJohnK ,

I came across your nuget package and saw at first that it didn't support separation of resource files and would only get the first resource out of all files so I tried to make my own version of it (that sadly only works in Debug mode and not in Release).

Then I saw this open issue and saw you created a new prerelease version a couple days ago and it seems to work nicely from very briefly trying it. How stable is that version in terms of crashing/memory leaks? Do you expect to keep the support for multiple resource files in the official version later on too? Like @santo998 , if there are any tests that I could do then I would love to do them since it looks quite promising.

Hi, @Wout-M !

Yes, the alpha-2 version is finally(!) out. It includes all the previous features and adds the ability to have specific named resources!

Theses resources can be referenced in a number of ways:

  • Directly from XAML.
  • Injected specific resource manager in ViewModel/Binding Context by name.
  • Selected specific resource manager for entire View/Page by implementing ISpecificResourceManager interface.
  • ISpecificResourceManager can also be implemented by adding the SpecificResourceManagerAttribute that uses source generator to specify resource manager for View/Page.

I think the alpha-2 version is stable and soon release ready! I just want to add some tests, comments and update the sample project more. All the above support will be in the official version.

Help testing is always welcome! Try testing it for your needs and please report back what those are and how it works.

/Regards Johan

Hey @SirJohnK,

I did some more testing and might have found a small issue when building the app in Release mode (for Android, I haven't tested if it's also the case for other platforms). It seems to still pick the resource from the first key it can find across all resources, even when a specific resource is specified on the page. In Debug mode it works fine and picks up the correct resource from the correct resource file.

I have multiple pages for an introduction in my app that look almost the same, but get their text from different resource files since they all have some slight different functionality, so the resource files for these pages all have a Title and SubText key. For example, a WelcomePage containing an introductory text and a PickLanguagePage where the user can pick the language.

The resources are registered as follows:

builder
    .UseMauiApp<App>()
    // Do other stuff
    .UseLocalizationResourceManager(settings =>
    {
        settings.AddResource(WelcomeResources.ResourceManager, nameof(WelcomeResources));
        settings.AddResource(PickLanguageResources.ResourceManager, nameof(PickLanguageResources));
        settings.SuppressTextNotFoundException(true, "'{0}' not found!");
        settings.RestoreLatestCulture(true);
    });

The pages have the specific resource key with the attribute/interface (I wanted to try both to be sure it wasn't related to the implementation of one of th methods):

[SpecificResourceManager(nameof(WelcomeResources))]
public partial class WelcomePage : ContentPage
{
    public WelcomePage(WelcomeViewModel vm)
    {
	InitializeComponent();
	BindingContext = vm;
    }
}
//[SpecificResourceManager(nameof(PickLanguageResources))]
public partial class PickLanguagePage : ContentPage, ISpecificResourceManager
{
	public PickLanguagePage(PickLanguageViewModel vm)
	{
		InitializeComponent();
		BindingContext = vm;
	}

    public string ResourceManager => nameof(PickLanguageResources);
}

In the page it's used like this:

<?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"
                      xmlns:localization="clr-namespace:LocalizationResourceManager.Maui;assembly=LocalizationResourceManager.Maui"
                      xmlns:view="clr-namespace:Build.Apps.Document.View.Views"
                      xmlns:vm="clr-namespace:Build.Apps.Document.ViewModel.ViewModels.PickLanguage;assembly=Build.Apps.Document.ViewModel"
                      x:Class="Build.Apps.Document.View.Pages.PickLanguagePage"
                      x:DataType="vm:PickLanguageViewModel"
                      Shell.NavBarIsVisible="False">
    <Grid RowDefinitions="3*,*,*"
          HorizontalOptions="FillAndExpand"
          VerticalOptions="FillAndExpand"
          Margin="0,0,0,30">
        <Image Source="Images/logo.png"
               HeightRequest="128"
               WidthRequest="128"
               HorizontalOptions="Center"
               VerticalOptions="Center"/>

        <VerticalStackLayout Grid.Row="1" 
                             Spacing="5"
                             Padding="30,30,30,0">
            <Label Text="{localization:Translate Title}"
                   FontSize="Title"
                   FontAttributes="Bold"/>

            <Label Text="{localization:Translate SubText, ResourceManager=PickLanguageResources}"/>
        </VerticalStackLayout>

        <VerticalStackLayout Grid.Row="2"
                             Spacing="30"
                             Padding="30"
                             VerticalOptions="End">
            <Picker ItemsSource="{Binding Languages}"
                               SelectedItem="{Binding SelectedLanguage}" 
                               ItemDisplayBinding="{Binding NativeName}"
                               Title="{localization:Translate PickLanguage}"/>

            <Button Text="{localization:Translate Next, ResourceManager=GeneralResources}"
                    Command="{Binding PickLanguageCommand}"
                    HeightRequest="50"
                    CornerRadius="100"/>
        </VerticalStackLayout>
    </Grid>
</ContentPage>

When building in Debug mode, the PickLanguagePage will display the texts correctly from the PickLanguageResources. When in Release mode, it will pick the first ones it can find, being the ones from WelcomeResources, unless I specify the ResourceManager.

So in the case above it would pick Title from WelcomeResources (even though it's supposed to take it from PickLanguageResources) and SubText from PickLanguageResources in Release, but both from PickLanguageResources in Debug.

I tried building it with or without the R8 linker to see if that meddled with something, but that doesn't seem to make a difference.

@Wout-M , nice find! ๐Ÿ‘ Will look into this asap. ๐Ÿ˜„

@Wout-M , I found the reason why it is not working in Release mode and it is NOT a fun one...seems like IRootObjectProvider that I resolve from the ServiceProvider to get parent view/page, is not available in Release mode. ๐Ÿ˜ž (Link to Github issue)
I even forked the Maui repo to see if I could understand why this is, but it's not that obvious. I need to think about some other solution, but it does not look bright at the moment...

@SirJohnK Oh no that sounds rough, but it would definitely also explain why my solution kept crashing when I used Release mode since I used the IRootObjectProvider there too (how did you even find that its that specific one cause I struggled so much trying to find what causes bugs in Release since you can't debug it then?). Seems like such an oversight on .NET MAUIs behalf, especially since the issue has been open for almost a year...

At the moment as a workaround I just specify the ResourceManager specifically for the translations where the keys would be in multiple resource files which seems to work fine apart from it being a little extra work.

@Wout-M , I think I found a solution! ๐Ÿ˜„

@SirJohnK I'll go try it out! I'll get back to you as soon as I can with what I find

@SirJohnK From some very quick tests it seems to work, nice find with the solution!

Great!๐Ÿ‘ Will try to release it soon!๐Ÿ˜… (Wanted to do some tests first)