SirJohnK / LocalizationResourceManager.Maui

Enhanced .NET MAUI version of the XCT LocalizationResourceManager.
MIT License
143 stars 11 forks source link

How can I use multiple ResourceManagers? #19

Open santo998 opened 11 months ago

santo998 commented 11 months ago

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:

SirJohnK commented 11 months ago

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!

/Regards Johan

santo998 commented 11 months ago

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:

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

santo998 commented 11 months ago

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.

SirJohnK commented 11 months ago

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

/Johan

santo998 commented 11 months ago

@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.

SirJohnK commented 8 months ago

@santo998 , finally I have had the time to look into this! :sweat_smile:

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:

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

SirJohnK commented 7 months ago

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

santo998 commented 7 months ago

@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.

SirJohnK commented 7 months ago

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

/Johan

santo998 commented 7 months ago

@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...

2) 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.

SirJohnK commented 7 months ago

@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.

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

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

santo998 commented 7 months ago

@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

Wout-M commented 3 months ago

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.

SirJohnK commented 3 months ago

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:

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

Wout-M commented 3 months ago

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.

SirJohnK commented 3 months ago

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

SirJohnK commented 3 months ago

@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...

Wout-M commented 3 months ago

@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.

SirJohnK commented 2 months ago

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

Wout-M commented 2 months ago

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

Wout-M commented 2 months ago

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

SirJohnK commented 2 months ago

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