UI Toolkit support?

Hello,

I and my team are trying the new Localisation package, and it looks awesome.
We use Addressables so it fits nicely in there, too.

However, we are using UI Toolkit for all of our UI needs, and it would be awesome to assign a StringReference to -say- a label text.

Is this something we should implement ourselves, or is it getting looked into by Unity devs?

Thank you!

1 Like

Hey,
It’s certainly on our roadmap but will be some time before we have an official integration.
We are talking with the UI toolkit team at the moment.
If you need something in the short term the you should look at adding your own integration, I can’t really give any firm dates for an integration yet.

In case sb needs to integrate these two packages I attach my working example you can start with. It handles runtime language changes too (this resets hierarchy tho).

UIDocumentLocalization.cs

This class assumes that you designate StringTable keys in label fields (as seen in Label, Button, etc) and start them all with ‘#’ char (so other labels will be left be)
Example: Imgur: The magic of the Internet

// void* src = https://gist.github.com/andrew-raphael-lukasik/72a4d3d14dd547a1d61ae9dc4c4513da
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.Localization;
using UnityEngine.Localization.Tables;
using UnityEngine.ResourceManagement.AsyncOperations;

// NOTE: this class assumes that you designate StringTable keys in label fields (as seen in Label, Button, etc)
// and start them all with '#' char (so other labels will be left be)
// example: https://i.imgur.com/H5RUIej.gif

[DisallowMultipleComponent]
[RequireComponent( typeof(UIDocument) )]
public class UIDocumentLocalization : MonoBehaviour
{

   [SerializeField] LocalizedStringTable _table = null;
   UIDocument _document;

   /// <summary> Executed after hierarchy is cloned fresh and translated. </summary>
   public event System.Action onCompleted = ()=>{};
  
  
   void OnEnable ()
   {
       if( _document==null )
           _document = gameObject.GetComponentInParent<UIDocument>( includeInactive:true );

       _table.TableChanged += OnTableChanged;
   }


   void OnDisable ()
   {
       _table.TableChanged -= OnTableChanged;
   }


   void OnTableChanged ( StringTable table )
   {
       var root = _document.rootVisualElement;
       root.Clear();
       _document.visualTreeAsset.CloneTree( root );

       var op = _table.GetTable();
       op.Completed -= OnTableLoaded;
       op.Completed += OnTableLoaded;
   }
  
   void OnTableLoaded ( AsyncOperationHandle<StringTable> op )
   {
       StringTable table = op.Result;
       var root = _document.rootVisualElement;

       LocalizeChildrenRecursively( root , table );
       onCompleted();

       root.MarkDirtyRepaint();
   }

   void Localize ( VisualElement next , StringTable table )
   {
       if( typeof(TextElement).IsInstanceOfType(next) )
       {
           TextElement textElement = (TextElement) next;
           string key = textElement.text;
           if( !string.IsNullOrEmpty(key) && key[0]=='#' )
           {
               key = key.TrimStart('#');
               StringTableEntry entry = table[ key ];
               if( entry!=null )
                   textElement.text = entry.LocalizedValue;
               else
                   Debug.LogWarning($"No {table.LocaleIdentifier.Code} translation for key: '{key}'");
           }
       }
   }

   void LocalizeChildrenRecursively ( VisualElement element , StringTable table )
   {
       VisualElement.Hierarchy elementHierarchy = element.hierarchy;
       int numChildren = elementHierarchy.childCount;
       for( int i=0 ; i<numChildren ; i++ )
       {
           VisualElement child = elementHierarchy.ElementAt( i );
           Localize( child , table );
       }
       for( int i=0 ; i<numChildren ; i++ )
       {
           VisualElement child = elementHierarchy.ElementAt( i );
           VisualElement.Hierarchy childHierarchy = child.hierarchy;
           int numGrandChildren = childHierarchy.childCount;
           if( numGrandChildren!=0 )
               LocalizeChildrenRecursively( child , table );
       }
   }

}

gist.github copy

Note: it will throw errors if addressables aren’t built yet (do that).

It’s not as ideal as native-like integration, but it gets job done for now. If you have better idea how to do it - please share it!

5 Likes

@andrew-lukasik the translations are applied, but it breaks callbacks registered for buttons.

I guess because of the tree cloning…

UPDATE:

I removed the tree cloning logic and storing the key in the viewDataKey property, to not have it reset every time you change the language.

...
            string key = textElement.viewDataKey;
            if( !string.IsNullOrEmpty(key) && key[0]=='_' )
            {
                key = key.TrimStart('_');
...

Though, I’m not sure how to solve another problem…

    void OnDisable ()
    {
        _table.TableChanged -= OnTableChanged;
    }

This doesn’t seem to be working. When you disable the script, and change the language, the table still tries to run the OnTableChanged handler.

This problem is addressed in the initial source already - although implicitly.

Arrange this in YourUiBindingClass.cs :

void OnEnable ()
{
    OnBind();// just in case of race condition
    _UIDocumentLocalization.Completed -= OnBind;
    _UIDocumentLocalization.Completed += OnBind;
}

void OnBind ()
{
    VisualElement yourRootVisualElement = _UIDocument.rootVisualElement;

    /* button etc. binding code goes here */
}

Yeah, I removed the cloning logic, so the Completed is not needed anymore - the bindings don’t break.

Is everything OK for you when you toggle the script on/off a few times and then switch the language?

The list of delegates kept growing for me, as the script doesn’t unsubscribe properly.

1 Like

Oh, you’re right, didn’t noticed that before! I will investigate it sometime soon but I’m surprised this handler isn’t being unsubscribed, like, at all.
I hope it’s my silly mistake somewhere and not custom event accessor shenanigans.

Not sure why but events stopped pilling up today and I can’t reproduce that behavior anymore. Maybe it was Unity version related? (on 2020.3.0f1 now)

Other than that I can see an exception when translation event is raised due to m_ChangeHandler being null in com.unity.localization@0.10.0-preview. To fix it just add “if( m_ChangeHandler!=null )” at line 139 in LocalizedTable.cs

stacktrace

NullReferenceException: Object reference not set to an instance of an object
UnityEngine.Localization.LocalizedTable`2[TTable,TEntry].AutomaticLoadingCompleted (UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle`1[TObject] loadOperation) (at Packages/com.unity.localization@0.10.0-preview/Runtime/Localized Reference/LocalizedTable.cs:140)
DelegateList`1[T].Invoke (T res) (at Library/PackageCache/com.unity.addressables@1.16.13/Runtime/ResourceManager/Util/DelegateList.cs:69)
UnityEngine.Debug:LogException(Exception)
DelegateList`1:Invoke(AsyncOperationHandle`1) (at Library/PackageCache/com.unity.addressables@1.16.13/Runtime/ResourceManager/Util/DelegateList.cs:73)
UnityEngine.Localization.LoadTableOperation`2:TableLoaded(AsyncOperationHandle`1)
DelegateList`1:Invoke(AsyncOperationHandle`1) (at Library/PackageCache/com.unity.addressables@1.16.13/Runtime/ResourceManager/Util/DelegateList.cs:69)
UnityEngine.ResourceManagement.Util.DelayedActionManager:LateUpdate() (at Library/PackageCache/com.unity.addressables@1.16.13/Runtime/ResourceManager/Util/DelayedActionManager.cs:159)
1 Like

@andrew-lukasik thanks for the script, I will modify it for my needs (custom, without using localization)

1 Like