Localizing UI Dropdown Options

What’s the recommended way to localize a UI Dropdown component. I have a Settings screen with a Graphics Quality Dropdown with High, Medium and Low options. I’d like to use the StringTable for each of these options. I haven’t been able to find any examples of how to do this.

I’ve been thinking about writing a LocalizeDropdown component similar to the built-in LocalizeString component. But before I attempt that, I wanted to see if anyone has crossed this bridge before or has suggestions for me.

2 Likes

For the moment your approach is the correct one. We will have better ways to do this in the future.

jamesor, can you share with us the LocalizeDropdown component that you wrote?

For anyone coming here to find a solution:

Probably not the best code, but it allows you to add localized string and texture options in the Unity inspector. These then get used to populate the dropdown and to update all options if the locale changes

Currently (with my limited testing), the only bug I have right now is that the initial CaptionSprite does not show after initial population of the dropdown.

I’ll probably improve on this as my needs change (and there is no official solution yet).
Any feedback is also welcome (be it on style or the logic itself)

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;

namespace Utilities.Localization
{
    [RequireComponent(typeof(TMP_Dropdown))]
    [AddComponentMenu("Localization/Localize dropdown")]
    public class LocalizeDropdown : MonoBehaviour
    {
        // Fields
        // =======
        public List<LocalizedDropdownOption> options;

        public int selectedOptionIndex = 0;

        // Properties
        // ===========
        private TMP_Dropdown Dropdown => GetComponent<TMP_Dropdown>();
        
        // Methods
        // ========
        private IEnumerator Start()
        {
            yield return PopulateDropdown();
        }
        
        private void OnEnable() => LocalizationSettings.SelectedLocaleChanged += UpdateDropdownOptions;
        
        private void OnDisable() => LocalizationSettings.SelectedLocaleChanged -= UpdateDropdownOptions;

        private void OnDestroy() => LocalizationSettings.SelectedLocaleChanged -= UpdateDropdownOptions;

        private IEnumerator PopulateDropdown()
        {
            // Clear any options that might be present
            Dropdown.ClearOptions();
            Dropdown.onValueChanged.RemoveListener(UpdateSelectedOptionIndex);
            
            for (var i = 0; i < options.Count; ++i)
            {
                var option = options[i];
                var localizedText = string.Empty;
                Sprite localizedSprite = null;
                    
                // If the option has text, fetch the localized version
                if (!option.text.IsEmpty)
                {
                    var localizedTextHandle = option.text.GetLocalizedString();
                    yield return localizedTextHandle;

                    localizedText = localizedTextHandle.Result;
                    
                    // If this is the selected item, also update the caption text
                    if (i == selectedOptionIndex)
                    {
                        UpdateSelectedText(localizedText);
                    }
                }

                // If the option has a sprite, fetch the localized version
                if (!option.sprite.IsEmpty)
                {
                    var localizedSpriteHandle = option.sprite.LoadAssetAsync();
                    yield return localizedSpriteHandle;
                    
                    localizedSprite = localizedSpriteHandle.Result;
                    
                    // If this is the selected item, also update the caption text
                    if (i == selectedOptionIndex)
                    {
                        UpdateSelectedSprite(localizedSprite);
                    }
                }
                
                // Finally add the option with the localized content
                Dropdown.options.Add(new TMP_Dropdown.OptionData(localizedText, localizedSprite));
            }

            // Update selected option, to make sure the correct option can be displayed in the caption
            Dropdown.value = selectedOptionIndex;
            Dropdown.onValueChanged.AddListener(UpdateSelectedOptionIndex);
        }

        private void UpdateDropdownOptions(Locale locale)
        {
            // Updating all options in the dropdown
            // Assumes that this list is the same as the options passed on in the inspector window
            for (var i = 0; i < Dropdown.options.Count; ++i)
            {
                var optionI = i;
                var option = options[i];

                // Update the text 
                if (!option.text.IsEmpty)
                {
                    var localizedTextHandle = option.text.GetLocalizedString(locale);
                    localizedTextHandle.Completed += (handle) =>
                    {
                        Dropdown.options[optionI].text = handle.Result;

                        // If this is the selected item, also update the caption text
                        if (optionI == selectedOptionIndex)
                        {
                            UpdateSelectedText(handle.Result);
                        }
                    };
                }

                // Update the sprite
                if (!option.sprite.IsEmpty)
                {
                    var localizedSpriteHandle = option.sprite.LoadAssetAsync();
                    localizedSpriteHandle.Completed += (handle) =>
                    {
                        Dropdown.options[optionI].image = localizedSpriteHandle.Result;

                        // If this is the selected item, also update the caption sprite
                        if (optionI == selectedOptionIndex)
                        {
                            UpdateSelectedSprite(localizedSpriteHandle.Result);
                        }
                    };
                }
            }
        }

        private void UpdateSelectedOptionIndex(int index) => selectedOptionIndex = index;

        private void UpdateSelectedText(string text)
        {
            if (Dropdown.captionText != null)
            {
                Dropdown.captionText.text = text;
            }
        }
        
        private void UpdateSelectedSprite(Sprite sprite)
        {
            if (Dropdown.captionImage != null)
            {
                Dropdown.captionImage.sprite = sprite;
            }
        }
    }

    [Serializable]
    public class LocalizedDropdownOption
    {
        public LocalizedString text;

        public LocalizedSprite sprite;
    }
}

EDIT: Updated script (Changed the code style a bit, applied some feedback and fixed a bug)

3 Likes

Hi.
Thanks for sharing.
:wink:

var localizedSpriteHandle = LocalizationSettings.AssetDatabase
                        .GetLocalizedAssetAsync<Texture2D>(option.sprite.TableReference, option.sprite.TableEntryReference);

You can just do

var localizedSpriteHandle = option.sprite.LoadAssetAsync();

It basically does the same thing.

Have you tried using a LocalizedSprite instead of LocalizedTexture?

Is this a bug?

Thanks for taking the time to reply :slight_smile:

Didn’t know that was possible (still exploring the package), so I’ll change that

I have not, but will try. I used the Texture because the table entry and my assets are Texture2D

I think it’s a bug in my own code (the intention is to have it show up immediatly), but haven’t had the time yet do take a decent look at it.
I’ll (probably) be doing that this weekend

1 Like

There is a small issue in your wonderful code. The selectedOptionIndex is set to 0 which prevents from setting the dropdown value from Prefs at start.

So I changed it like that and it works perfectly now:

EDIT: I also changed OnEnable so that a reenabled dropdown would update if the locale has been changed while it was disabled.

// https://discussions.unity.com/t/792432

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using TMPro;
using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;

namespace Utilities.Localization
{
    [RequireComponent(typeof(TMP_Dropdown))]
    [AddComponentMenu("Localization/Localize Dropdown")]
    public class LocalizeDropdown : MonoBehaviour
    {
        // Fields
        // =======
        public List<LocalizedDropdownOption> options;

        public int selectedOptionIndex = 0;

        private Locale currentLocale = null;

        // Properties
        // ===========
        private TMP_Dropdown Dropdown => GetComponent<TMP_Dropdown>();

        // Methods
        // ========
        private IEnumerator Start()
        {
            yield return PopulateDropdown();
        }

        private void OnEnable()
        {
            var locale = LocalizationSettings.SelectedLocale;
            if (currentLocale != null && locale != currentLocale)
            {
                UpdateDropdownOptions(locale);
                currentLocale = locale;
            }
            LocalizationSettings.SelectedLocaleChanged += UpdateDropdownOptions;
        }

        private void OnDisable() => LocalizationSettings.SelectedLocaleChanged -= UpdateDropdownOptions;

        private void OnDestroy() => LocalizationSettings.SelectedLocaleChanged -= UpdateDropdownOptions;

        private IEnumerator PopulateDropdown()
        {
            // Clear any options that might be present
            selectedOptionIndex = Dropdown.value;

            Dropdown.ClearOptions();
            Dropdown.onValueChanged.RemoveListener(UpdateSelectedOptionIndex);

            for (var i = 0; i < options.Count; ++i)
            {
                var option = options[i];
                var localizedText = string.Empty;
                Sprite localizedSprite = null;

                // If the option has text, fetch the localized version
                if (!option.text.IsEmpty)
                {
                    var localizedTextHandle = option.text.GetLocalizedString();
                    yield return localizedTextHandle;

                    localizedText = localizedTextHandle.Result;

                    // If this is the selected item, also update the caption text
                    if (i == selectedOptionIndex)
                    {
                        UpdateSelectedText(localizedText);
                    }
                }

                // If the option has a sprite, fetch the localized version
                if (!option.sprite.IsEmpty)
                {
                    var localizedSpriteHandle = option.sprite.LoadAssetAsync();
                    yield return localizedSpriteHandle;

                    localizedSprite = localizedSpriteHandle.Result;

                    // If this is the selected item, also update the caption text
                    if (i == selectedOptionIndex)
                    {
                        UpdateSelectedSprite(localizedSprite);
                    }
                }

                // Finally add the option with the localized content
                Dropdown.options.Add(new TMP_Dropdown.OptionData(localizedText, localizedSprite));
            }

            // Update selected option, to make sure the correct option can be displayed in the caption
            Dropdown.value = selectedOptionIndex;
            Dropdown.onValueChanged.AddListener(UpdateSelectedOptionIndex);
            currentLocale = LocalizationSettings.SelectedLocale;

        }

        private void UpdateDropdownOptions(Locale locale)
        {
            // Updating all options in the dropdown
            // Assumes that this list is the same as the options passed on in the inspector window
            for (var i = 0; i < Dropdown.options.Count; ++i)
            {
                var optionI = i;
                var option = options[i];

                // Update the text
                if (!option.text.IsEmpty)
                {
                    var localizedTextHandle = option.text.GetLocalizedString(locale);
                    localizedTextHandle.Completed += (handle) =>
                    {
                        Dropdown.options[optionI].text = handle.Result;

                        // If this is the selected item, also update the caption text
                        if (optionI == selectedOptionIndex)
                        {
                            UpdateSelectedText(handle.Result);
                        }
                    };
                }

                // Update the sprite
                if (!option.sprite.IsEmpty)
                {
                    var localizedSpriteHandle = option.sprite.LoadAssetAsync();
                    localizedSpriteHandle.Completed += (handle) =>
                    {
                        Dropdown.options[optionI].image = localizedSpriteHandle.Result;

                        // If this is the selected item, also update the caption sprite
                        if (optionI == selectedOptionIndex)
                        {
                            UpdateSelectedSprite(localizedSpriteHandle.Result);
                        }
                    };
                }
            }
        }

        private void UpdateSelectedOptionIndex(int index) => selectedOptionIndex = index;

        private void UpdateSelectedText(string text)
        {
            if (Dropdown.captionText != null)
            {
                Dropdown.captionText.text = text;
            }
        }

        private void UpdateSelectedSprite(Sprite sprite)
        {
            if (Dropdown.captionImage != null)
            {
                Dropdown.captionImage.sprite = sprite;
            }
        }
    }

    [Serializable]
    public class LocalizedDropdownOption
    {
        public LocalizedString text;

        public LocalizedSprite sprite;
    }
}



...
1 Like

Keep getting this error:

Error CS1061 ‘string’ does not contain a definition for ‘Result’ and no accessible extension method ‘Result’ accepting a first argument of type ‘string’ could be found (are you missing a using directive or an assembly reference?)

We changed some APi a few versions ago.

For asynchronous you should now use GetLocalizedStringAsync instead of GetLocalizedString.


For some reasons I got an empty DropDown. Is it possible to fix it somehow?

Unity 2020.3.24f1

ERROR

A table with the same key menu already exists. Something went wrong during preloading.
UnityEngine.Localization.PreloadDatabaseOperation2<UnityEngine.Localization.Tables.StringTable, UnityEngine.Localization.Tables.StringTableEntry>:LoadTableContents (UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle1<System.Collections.Generic.IList1<UnityEngine.Localization.Tables.StringTable>>) DelegateList1<UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle1<System.Collections.Generic.IList1<UnityEngine.Localization.Tables.StringTable>>>:Invoke (UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle1<System.Collections.Generic.IList1<UnityEngine.Localization.Tables.StringTable>>) (at Library/PackageCache/com.unity.addressables@1.19.9/Runtime/ResourceManager/Util/DelegateList.cs:69)
UnityEngine.ResourceManagement.ChainOperationTypelessDepedency1<System.Collections.Generic.IList1<UnityEngine.Localization.Tables.StringTable>>:OnWrappedCompleted (UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle1<System.Collections.Generic.IList1<UnityEngine.Localization.Tables.StringTable>>)

Could you try running the addressable analyzers?
https://discussions.unity.com/t/829393

NO errors found by analyzer

 private IEnumerator PopulateDropdown()
        {
            // Clear any options that might be present
            selectedOptionIndex = Dropdown.value;
            Dropdown.ClearOptions();
            Dropdown.onValueChanged.RemoveListener(UpdateSelectedOptionIndex);
            for (var i = 0; i < options.Count; ++i)
            {
               //THIS CODE EXECTUES ONLY ONCE
             }

I found that inside the FOR cycle the iterations runs only once, even if you have like 10 “options” added.

What does the Localize Dropdown script look like?

it is exact copy of the script from dlorre:

the only thing i’ve changed is GetLocalizedString to GetLocalizedStringAsync in lines where were errors.

Based on previous answers I wrote my own simple script that works:

using System;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;

namespace UVH.Localization
{
    [RequireComponent(typeof(Dropdown))]
    [AddComponentMenu("Localization/Localize Dropdown")]
    public class LocalizeDropdown : MonoBehaviour
    {

        [Serializable]
        public class LocalizedDropdownOption
        {
            public LocalizedString text;

            public LocalizedSprite sprite; //not implemented yet!
        }

        public List<LocalizedDropdownOption> options;
        public int selectedOptionIndex = 0;
        private Locale currentLocale = null;
        private Dropdown Dropdown => GetComponent<Dropdown>();


        private void Start()
        {
            getLocale();
            UpdateDropdown(currentLocale);
            LocalizationSettings.SelectedLocaleChanged += UpdateDropdown;
        }


        private void OnEnable()=> LocalizationSettings.SelectedLocaleChanged += UpdateDropdown;
        private void OnDisable()=> LocalizationSettings.SelectedLocaleChanged -= UpdateDropdown;
        void OnDestroy() => LocalizationSettings.SelectedLocaleChanged -= UpdateDropdown;



        private void getLocale()
        {
            var locale = LocalizationSettings.SelectedLocale;
            if (currentLocale != null && locale != currentLocale)
            {
                currentLocale = locale;
            }
        }


        private void UpdateDropdown(Locale locale)
        {
            selectedOptionIndex = Dropdown.value;
            Dropdown.ClearOptions();

            for (int i = 0; i < options.Count; i++)
            {
                //sprite functionality isn't ready yet.
                Sprite localizedSprite = null;

                String localizedText = options[i].text.GetLocalizedString();
                Dropdown.options.Add(new Dropdown.OptionData(localizedText, localizedSprite));
            }

            Dropdown.value = selectedOptionIndex;
            Dropdown.RefreshShownValue();
        }

    }
}
2 Likes

It looks like you subscribe to locale changed twice. In Start and OnEnable, that will mean it does the callback twice. Otherwise it looks fine.

I created my own Dropdown Localizer.
This is as addiction of the TMP_Dropdown component. Instead of options in the Dropdown component you can add the LocalizedString options in this script.
Any feedback is welcome!

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.Settings;
using TMPro;

public class LocalizeDropdown : MonoBehaviour
{
    [SerializeField] private List<LocalizedString> dropdownOptions;
    private TMP_Dropdown tmpDropdown;

    private void Awake()
    {
        List<TMP_Dropdown.OptionData> tmpDropdownOptions = new List<TMP_Dropdown.OptionData>();
        for (int i = 0; i < dropdownOptions.Count; i++)
        {
            tmpDropdownOptions.Add(new TMP_Dropdown.OptionData(dropdownOptions[i].GetLocalizedString()));
        }
        if (!tmpDropdown) tmpDropdown = GetComponent<TMP_Dropdown>();
        tmpDropdown.options = tmpDropdownOptions;
    }

    Locale currentLocale;
    private void ChangedLocale(Locale newLocale)
    {
        if (currentLocale == newLocale) return;
        currentLocale = newLocale;
        List<TMP_Dropdown.OptionData> tmpDropdownOptions = new List<TMP_Dropdown.OptionData>();
        for (int i = 0; i < dropdownOptions.Count; i++)
        {
            tmpDropdownOptions.Add(new TMP_Dropdown.OptionData(dropdownOptions[i].GetLocalizedString()));
        }
        tmpDropdown.options = tmpDropdownOptions;
    }

    private void Update()
    {
        LocalizationSettings.SelectedLocaleChanged += ChangedLocale;
    }
}
10 Likes

Have you tried using the Localized Property Variants feature for dropdowns? It will let you localize any field and works with dropdowns :wink:
https://docs.unity3d.com/Packages/com.unity.localization@1.0/manual/LocalizedPropertyVariants.html

Did you know how to hide the Default Dropdown Localizer which on top right in Game window?
It a dropdown button to change localized language,and I want to undisplay that.
Very thanks.

It can be disabled in the Unity preferences under Localization.