Skip to content

Instantly share code, notes, and snippets.

@andrew-raphael-lukasik
Last active November 4, 2024 06:03
Show Gist options
  • Save andrew-raphael-lukasik/72a4d3d14dd547a1d61ae9dc4c4513da to your computer and use it in GitHub Desktop.
Save andrew-raphael-lukasik/72a4d3d14dd547a1d61ae9dc4c4513da to your computer and use it in GitHub Desktop.
Text localization script for UIDocument (UI Toolkit @ Unity)

pattern to follow

// 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)

Programming workflow:

Binding UIDocument, normally, happened at OnEnable but since StringTable can be (re)loaded at any time then we need to switch to using events.

GetComponent<UIDocumentLocalization>().onCompleted += (root) => Debug.Log("let's bind a freshly localized UI!");

I added a big remainder so ppl not miss this:

Screenshot 2023-08-28 001053

using UnityEngine;
using UnityEngine.UIElements;

[DisallowMultipleComponent]
[RequireComponent( typeof(UIDocumentLocalization) )]
public class MainMenuController : MonoBehaviour
{
    void OnEnable () => GetComponent<UIDocumentLocalization>().onCompleted += Bind;
    void OnDisable () => GetComponent<UIDocumentLocalization>().onCompleted -= Bind;
    void Bind ( VisualElement root )
    {
        /* bind here */
    }
}

Works well with:

/// void* src = https://gist.github.com/andrew-raphael-lukasik/72a4d3d14dd547a1d61ae9dc4c4513da
///
/// Copyright (C) 2022 Andrzej Rafał Łukasik (also known as: Andrew Raphael Lukasik)
///
/// This program is free software: you can redistribute it and/or modify
/// it under the terms of the GNU General Public License as published by
/// the Free Software Foundation, version 3 of the License.
///
/// This program is distributed in the hope that it will be useful,
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
/// See the GNU General Public License for details https://www.gnu.org/licenses/
///
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.Localization;
using UnityEngine.Localization.Tables;
using UnityEngine.ResourceManagement.AsyncOperations;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.UIElements;
#endif
// 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
[HelpURL("https://gist.github.com/andrew-raphael-lukasik/72a4d3d14dd547a1d61ae9dc4c4513da")]
[DisallowMultipleComponent]
[RequireComponent(typeof(UIDocument))]
public class UIDocumentLocalization : MonoBehaviour
{
[SerializeField] LocalizedStringTable _table = null;
UIDocument _uiDocument;
/// <summary> Executed after hierarchy is cloned fresh and translated. </summary>
public event System.Action<VisualElement> onCompleted = ( VisualElement root ) =>
{
#if DEBUG
Debug.Log($"{nameof(UIDocumentLocalization)}: {nameof(UIDocument)} translated");
#endif
};
void OnEnable ()
{
if( _uiDocument == null )
_uiDocument = GetComponent<UIDocument>();
_table.TableChanged += OnTableChanged;
}
void OnDisable ()
{
_table.TableChanged -= OnTableChanged;
}
void OnTableChanged ( StringTable table )
{
_uiDocument.rootVisualElement.Clear();
_uiDocument.visualTreeAsset.CloneTree(_uiDocument.rootVisualElement);
#if DEBUG
Debug.Log($"{nameof(UIDocumentLocalization)}: {nameof(StringTable)} changed, {nameof(VisualTreeAsset)} has been cloned anew" , _uiDocument);
#endif
var op = _table.GetTableAsync();
if( op.IsDone )
{
OnTableLoaded(op);
}
else
{
op.Completed -= OnTableLoaded;
op.Completed += OnTableLoaded;
}
}
void OnTableLoaded ( AsyncOperationHandle<StringTable> op )
{
StringTable table = op.Result;
LocalizeChildrenRecursively(_uiDocument.rootVisualElement , table);
_uiDocument.rootVisualElement.MarkDirtyRepaint();
onCompleted(_uiDocument.rootVisualElement);
}
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);
}
}
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}'");
}
}
}
#if UNITY_EDITOR
[CustomEditor(typeof(UIDocumentLocalization))]
public class MyEditor : Editor
{
public override VisualElement CreateInspectorGUI ()
{
var ROOT = new VisualElement();
var LABEL = new Label($"- Remember -<br> Use <color=\"yellow\">{nameof(onCompleted)}</color> event instead of <color=\"yellow\">OnEnable()</color><br>to localize and bind this document correctly.");
{
var style = LABEL.style;
style.minHeight = EditorGUIUtility.singleLineHeight * 3;
style.backgroundColor = new Color(1f , 0.121f , 0 , 0.2f);
style.borderBottomLeftRadius = style.borderBottomRightRadius = style.borderTopLeftRadius = style.borderTopRightRadius = 6;
style.unityTextAlign = TextAnchor.MiddleCenter;
}
ROOT.Add(LABEL);
InspectorElement.FillDefaultInspector(ROOT , this.serializedObject , this);
return ROOT;
}
}
#endif
}
@andrew-raphael-lukasik
Copy link
Author

andrew-raphael-lukasik commented Aug 27, 2023

Hello @Morigun @VladTempest !
Thank you for your feedback, these are fair observations indeed. And because of that I added a big red box in the Inspector window to remind everyone (myself included!) to use onCompleted event and not OnEnable() method here.

Screenshot 2023-08-28 001053

using UnityEngine;
using UnityEngine.UIElements;

[DisallowMultipleComponent]
[RequireComponent( typeof(UIDocumentLocalization) )]
public class MainMenuController : MonoBehaviour
{
    void OnEnable () => GetComponent<UIDocumentLocalization>().onCompleted += Bind;
    void OnDisable () => GetComponent<UIDocumentLocalization>().onCompleted -= Bind;
    void Bind ( VisualElement root )
    {
        /* bind here */
    }
}

@DaniilVdovin
Copy link

@shadowbiz
Copy link

shadowbiz commented Jun 7, 2024

You have to change Script Execution Order in Project Settings to prevent UIDocumentLocalization's OnEnable firing before your Controller's OnEnable to ensure your Bind method will trigger properly

@andrew-raphael-lukasik
Copy link
Author

andrew-raphael-lukasik commented Jun 7, 2024

Hi @shadowbiz !
You can modify Script Execution Order from C# directly with [DefaultExecutionOrder( 100 )] attribute if you ever need that.
I, personally, never observer an issue with it in this context but I can definitely add this attribute to this template if you guys keep reporting it as an issue.

@JohnJohnSartain
Copy link

Maybe I'm doing it wrong but I added this method so that per event firing I can reload specific portions of the UI that need to be localized.

        public void LocalizeElement(VisualElement element)
        {
            var op = _table.GetTableAsync();
            if (op.IsDone)
            {
                LocalizeChildrenRecursively(element, op.Result);
                element.MarkDirtyRepaint();
            }
            else
            {
                op.Completed += (completedOp) =>
                {
                    LocalizeChildrenRecursively(element, completedOp.Result);
                    element.MarkDirtyRepaint();
                };
            }
        }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment