Xamarin.Forms change UI language at runtime (XAML)
Asked Answered
H

2

8

I am using Strings.resx, Strings.de.resx, etc. to localize Xamarin.Forms app. I need to be able to change interface language at run time, and it (allmost) works.

Xamarin generates static class Strings in namespace MyProject.Resources from resource files, and I use those values to display strings on UI. When doing it from code, it works flawlessly:

await DisplayAlert(Strings.lblConfirmDelete, Strings.lblDeleteMessage, Strings.lblOK, Strings.lblCancel));

Problem is - not all attributes defined this way from XAML are updated when I change UI culture during runtime. Buttons, Labels, Entry properties (Placeholder etc.) change as they should, but PageTitle, Toolbaritems, and some other properties remain in previous language.

I presume that some of these are populated when Page is first created, and are not updated on culture (and UI culture) change. So, basically, I need a way to combine {DynamicResource ...} with values from resources. I know that DynamicResource is ment to be used with Resource dictionary, but that is not a good way to store language translations for localization.

I tried

Text="{DynamicResource {x:Static lr:Strings.lblAddNew}}"

also not working.

Is there a way of refreshing page dynamicaly?

I also tried calling

global::Xamarin.Forms.Xaml.Extensions.LoadFromXaml(this, typeof(MainListPage));

from Appearing event for that page, but that also does not work.

Any ideas?

Part of XAML file

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:MyProject.View"
    xmlns:rs="clr-namespace:MMPI"
    x:Class="MyProject.MainListPage"
    xmlns:lr="clr-namespace:MyProject.Resources"

    Title="{x:Static lr:Strings.appName}"
    >

<ContentPage.ToolbarItems>
    <ToolbarItem 
     Name="New" 
     Order="Primary" 
     Priority="0"
     Text="{x:Static lr:Strings.lblAddNew}" 
     Clicked="New_Clicked"
>

Happ answered 7/6, 2017 at 10:42 Comment(1)
Does this answer your question? Re-evaluate all values in xaml page calculated by a markup-extensionSaulsauls
C
9

When i encountered that challenge in a project I resolved it by using a simple class ResourceLoader and making use of INotifyPropertyChanged.

You can access the Instanceproperty from anywhere and change the culture. All String that are bound to the index would update.

The ResourceManager instance injected into the constructor must be set up appropriately.

public class ResourceLoader : INotifyPropertyChanged
{
    private readonly ResourceManager manager;
    private CultureInfo cultureInfo;

    public ResourceLoader(ResourceManager resourceManager)
    {
        this.manager = resourceManager;
        Instance = this;
        this.cultureInfo = CultureInfo.CurrentUICulture;
    }

    public static ResourceLoader Instance { get; private set; }

    public string GetString(string resourceName)
    {
        string stringRes = this.manager.GetString(resourceName, this.cultureInfo);
        return stringRes;
    }

    public string this[string key] => this.GetString(key);

    public void SetCultureInfo(CultureInfo cultureInfo)
    {
        this.cultureInfo = cultureInfo;
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null));
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

To display the localized strings in your application you need to bind through the indexer like so:

<Label Text="{Binding [Test], Source={x:Static ResourceLoader.Instance}}" />

Since it is now bound it should update when you call ResourceLoader.SetCultureInfo because the Item[] 'PropertyName' is causing bound controls to re-fetch the values to their bound keys.


Update

I just tested it if i was talking bogus and for some reason the property changed didn't work. I've added a different approach below, which is close to what i'm using in production i urge you to add some kind of weak reference 'caching' instead of the simple list holding all the string resources (otherwise they will be kept forever)

I'm keeping above for reference.

public class ResourceLoader
{

    public ResourceLoader(ResourceManager resourceManager)
    {
        this.manager = resourceManager;
        Instance = this;
        this.cultureInfo = CultureInfo.CurrentUICulture;
    }

    private readonly ResourceManager manager;
    private CultureInfo cultureInfo;

    private readonly List<StringResource> resources = new List<StringResource>();

    public static ResourceLoader Instance { get; private set; }

    public StringResource this[string key] {
        get { return this.GetString(key); }
    }

    public StringResource GetString(string resourceName)
    {
        string stringRes = this.manager.GetString(resourceName, this.cultureInfo);
        var stringResource = new StringResource(resourceName, stringRes);
        this.resources.Add(stringResource);
        return stringResource;
    }

    public void SetCultureInfo(CultureInfo cultureInfo)
    {
        this.cultureInfo = cultureInfo;
        foreach (StringResource stringResource in this.resources) {
            stringResource.Value = this.manager.GetString(stringResource.Key, cultureInfo);
        }
    }

}

StringResource:

public class StringResource : INotifyPropertyChanged
{

    public StringResource(string key, string value)
    {
        this.Key = key;
        this.Value = value;
    }

    private string value;

    public string Key { get; }

    public string Value {
        get { return this.value; }
        set {
            this.value = value;
            this.OnPropertyChanged();
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
    {
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

}

XAML Binding

<Label  Text="{Binding [ResourceKey].Value, Mode=OneWay, Source={x:Static local:ResourceLoader.Instance}}" 
           />

Update 2

Came across this link where they implemented it similarly to my first approach. Maybe you can give it a try.


Update 3

Fixed the first approach. Both are working now. What was needed was this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(null)); instead of this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(Item[]));

Capone answered 7/6, 2017 at 15:48 Comment(2)
Thank you for your fast response. I'm sorry, but I don't quite follow. If I create an instance of ResourceLoader class using the ResourceManager from auto-generated file (Strings.Designer.cs), what am I supposed to do next? Could you please give me an example usage in a couple of lines? Nothing too elaborate, just 1-2-3 steps.Happ
i've checked what i proposed and updated my answer with a different - properly working approachCapone
H
0

I solved it very similar to @woelliJ . I just wanted to have key as strongly types from static class and binding should be in code behind.

ITranslationService is singleton from static variable. It is very close like @woelliJ .

[ContentProperty("Text")]
public sealed class TranslateExtension : IMarkupExtension<BindingBase>
{
    private readonly ITranslationService? _translationService;

    public TranslateExtension()
    {
        _translationService = Mobile.App.TranslationService;
    }

    public string? Text { get; set; }

    public BindingBase ProvideValue(IServiceProvider serviceProvider)
    {
        var translationItem = _translationService[Text];

        var binding = new Binding
        {
            Mode = BindingMode.OneWay,
            Path = $"Value",
            Source = translationItem,
        };

        return binding;
    }

    object IMarkupExtension.ProvideValue(IServiceProvider serviceProvider)
    {
        return (this as IMarkupExtension<BindingBase>).ProvideValue(serviceProvider);
    }
}

[AddINotifyPropertyChangedInterface]
public class TranslationItem
{
    public string? Key { get; set; }
    public string? Value { get; set; }
}

Then label would be like this

<Label FontSize="Title" Text="{services:Translate Text={x:Static models:M.AboutTestInfoTitle}}" />
Haggadah answered 2/8, 2021 at 11:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.