Search Unity

Localization for UI Toolkit?

Discussion in 'UI Toolkit' started by Kichang-Kim, May 3, 2020.

  1. Kichang-Kim

    Kichang-Kim

    Joined:
    Oct 19, 2010
    Posts:
    1,011
  2. BinaryCats

    BinaryCats

    Joined:
    Feb 8, 2016
    Posts:
    317
    I haven't used Unity's localisation; But I am making my own localisation tool so I'm interested in this topic.

    What I think you mean to ask is if the localisation package will work with UI elements, with labels and buttons and what not. I'm sure it would be possible to work with it using C# to grab the required text, and change the text on a element to that acquired text.

    It looks like Localization's are stored as Assets in the project (much like my own). It would be fabulous if Unity's UI Toolkit could like assets into UXML (other than resource loading, I guess).

    I am facing the dilemma now of if I should extend all of Unity's Visual Elements for my own plugin; it feels like I should, but no plan how as of yet.
     
    RKar likes this.
  3. DonLoquacious

    DonLoquacious

    Joined:
    Feb 24, 2013
    Posts:
    1,667
    Localization in my experience depends just as much on how you receive and want to process the files from the person/people/agency you hire to do your localization for you, how you want to set up the localization table used in memory, and when/where/how you want the "current language" to be tracked and changed by the user, as it does on what you're applying that data to. In this case (UI Elements), you can query through all labels and get whatever properties you like from them, so you can identify them by name, or make a new tag for "localization-name" or something in order to work as an identifier. You then just replace whatever text is in that label with your localized version.

    Add-ons that extend VisualElement/Label to automate this would simply be wiring into your own localization table (probably a dictionary of strings, dictionary of audio files, dictionary of images), which is really based on how you use assets in your project. I can't imagine a "one size fits all" solution to this- though ScriptableObjects could probably create some kind of a standard, like localization for UGUI does, I probably wouldn't even bother using it since ScriptableObjects wouldn't be how I'd be getting the translated assets from the company I hire for localizing the game.

    *shrugs*
     
    Last edited: May 4, 2020
  4. bdovaz

    bdovaz

    Joined:
    Dec 10, 2011
    Posts:
    1,051
    If they develop something it should be extensible enough so that if I want to provide the data I can implement it my way. For example: get the data from one or several *.json, *.csv, ...
     
  5. Rotark

    Rotark

    Joined:
    Mar 21, 2013
    Posts:
    6
    I saw that at 1:13:26 in this webinar from December 2020 the Localization team says they don't have an eta on UI Builder Localization support but that they are working on it with the localization team. Does anyone have an update on this? Thank you!
     
    LuisAlfredo92 likes this.
  6. phil-Unity

    phil-Unity

    Unity UI Lead Developer

    Joined:
    Nov 23, 2012
    Posts:
    1,226
    Short answer is its on the roadmap of things we need to do for localization but we've not finalized a timeline for its support.

    Longer answer is our current objectives are to get the Localization package stable and a 1.0 version released. Once that has been concluded we will pivot to determine what the next requirements for localization are and then meet those needs (UIToolkit being one of the higher priorities).
     
    Onaewe, kayy, orb_9 and 4 others like this.
  7. Rotark

    Rotark

    Joined:
    Mar 21, 2013
    Posts:
    6
    Thank you so much, Phil! If you need testers to try and get the packages working together, I'd be happy to try and be of assistance!
     
    phil-Unity likes this.
  8. tinix

    tinix

    Joined:
    Jul 4, 2016
    Posts:
    1
    Our team uses the UI toolkit and we would like to use the Localization package, it looks very good. Any update on when we can expect the UI toolkit integration? Thanks.
     
  9. JamesSozoLabs

    JamesSozoLabs

    Joined:
    Jan 10, 2022
    Posts:
    14
    My team is also super keen on using UI Toolkit, but having to create a custom solution for Localization is not that ideal.
    Looking forward to hearing more about Unity's Localization support for this.
     
    Hadrien-ESTELA likes this.
  10. Hadrien-ESTELA

    Hadrien-ESTELA

    Joined:
    Nov 10, 2016
    Posts:
    6
    2 years after the first post there is still no localization solution inside UIToolkit or even a road map with an approximative date?
     
  11. TPXP

    TPXP

    Joined:
    Apr 23, 2022
    Posts:
    1
    Hello there, we're also looking for a clean way to localize strings and assets in the UI builder. Strings are first thing, but localizing assets like background images could also help us a lot. Has the Unity team made progress on the UI Builder integration? If not, do you have any suggestions on ways to achieve this with the UI builder? I'm worried using a MonoBehaviour might cause flickering between non-localized and localized strings when the UI is being loaded.
     
  12. bilalakil

    bilalakil

    Joined:
    Jan 28, 2018
    Posts:
    77
    Hey all, thought I'd share my interim solution (fingers crossed for proper integration from Unity soon).

    Notes:
    - My code uses a different code style to that which Unity does. Season to taste
    - Set Windows > Asset Management > Localization Scene Controls > Active Locale so you can see resolved text in-editor
    - You must change Localization Smart Format error actions to NOT throw exceptions, otherwise UI Builder may fail to load your UXML files or... EXPLOSIONS. e.g. here's what I've set them to:
    upload_2022-6-4_22-58-29.png

    Code (CSharp):
    1. using UnityEngine.Localization;
    2. using UnityEngine.Localization.Settings;
    3. using UnityEngine.UIElements;
    4.  
    5. namespace FrankenStorm.UI
    6. {
    7.     public class LocalisedLabel : TextElement
    8.     {
    9.         public new class UxmlFactory : UxmlFactory<LocalisedLabel, UxmlTraits> { }
    10.         public new class UxmlTraits : VisualElement.UxmlTraits
    11.         {
    12.             readonly UxmlStringAttributeDescription _table = new() { name = "table" };
    13.             readonly UxmlStringAttributeDescription _tableEntry = new() { name = "table-entry" };
    14.  
    15.             public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
    16.             {
    17.                 base.Init(ve, bag, cc);
    18.                 var label = (LocalisedLabel)ve;
    19.                 label.Table = _table.GetValueFromBag(bag, cc);
    20.                 label.TableEntry = _tableEntry.GetValueFromBag(bag, cc);
    21.                 label.Refresh();
    22.             }
    23.         }
    24.  
    25.  
    26.         const string USSClassName = "fs-localised-label";
    27.  
    28.  
    29.         public string Table { get; set; }
    30.         public string TableEntry { get; set; }
    31.         public LocalizedString LocalisedString { get; private set; }
    32.  
    33.         public LocalisedLabel() : this(string.Empty, string.Empty) { }
    34.  
    35.         public LocalisedLabel(string table, string tableEntry)
    36.         {
    37.             AddToClassList(USSClassName);
    38.  
    39.             LocalisedString = new LocalizedString();
    40.             LocalisedString.StringChanged += HandleStringChanged;
    41.  
    42.             Table = table;
    43.             TableEntry = tableEntry;
    44.  
    45.             Refresh();
    46.         }
    47.  
    48.         public void Refresh()
    49.         {
    50.             LocalisedString.TableReference = string.IsNullOrEmpty(Table)
    51.                 ? LocalizationSettings.StringDatabase.DefaultTable
    52.                 : Table;
    53.             LocalisedString.TableEntryReference = TableEntry;
    54.         }
    55.  
    56.         void HandleStringChanged(string val) =>
    57.             text = val;
    58.     }
    59. }
    Note that I've got this class in a namespace. You don't need to do that, but if you do, then you do need to add an assembly attribute to make UI Builder aware of your namespace. e.g. I've got this in my UI assembly's AssemblyInfo.cs:

    Code (CSharp):
    1. #if UNITY_EDITOR
    2. using UnityEditor.UIElements;
    3.  
    4. [assembly: UxmlNamespacePrefix("FrankenStorm.UI", "fs")]
    5. #endif
    This works fine with smart strings, just access the
    LocalisedString
    directly and set variables as you desire, e.g.:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Localization.SmartFormat.PersistentVariables;
    3. using UnityEngine.UIElements;
    4.  
    5. namespace FrankenStorm.UI
    6. {
    7.     [RequireComponent(typeof(UIDocument))]
    8.     public class UIController : MonoBehaviour
    9.     {
    10.         UIDocument _doc;
    11.         LocalisedLabel _test;
    12.  
    13.         public void Awake()
    14.         {
    15.             _doc = GetComponent<UIDocument>();
    16.             _test = _doc.rootVisualElement.Q<LocalisedLabel>("test");
    17.         }
    18.  
    19.         public void Update()
    20.         {
    21.             _test.LocalisedString.Remove("SecondsIn");
    22.             _test.LocalisedString.Add("SecondsIn", new IntVariable { Value = (int)Time.time });
    23.             _test.LocalisedString.RefreshString();
    24.         }
    25.     }
    26. }
    I've not tested this much. If anyone sees any red flags, please share with me!

    EDIT: AssemblyInfo.cs
    UnityEditor.UIElements
    needed preprocessor conditions.
     
    Last edited: Jun 5, 2022
    CunningFox146, Sluggy, dlorre and 2 others like this.
  13. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,281
    Try this instead of recreating the variable each time:

    Code (csharp):
    1. [RequireComponent(typeof(UIDocument))]
    2. public class UIController : MonoBehaviour
    3. {
    4.     UIDocument _doc;
    5.     LocalisedLabel _test;
    6.     IntVariable m_Time;
    7.  
    8.     public void Awake()
    9.     {
    10.         _doc = GetComponent<UIDocument>();
    11.         _test = _doc.rootVisualElement.Q<LocalisedLabel>("test");
    12.     }
    13.  
    14.     public void Update()
    15.     {
    16.         if (m_Time == null)
    17.         {
    18.             m_Time = new IntVariable();
    19.             _test.LocalisedString.Add("SecondsIn", m_Time);
    20.         }
    21.  
    22.         m_Time.Value = (int)Time.time; // Will trigger the string to update
    23.     }
    24. }
     
    dlorre and bilalakil like this.
  14. bilalakil

    bilalakil

    Joined:
    Jan 28, 2018
    Posts:
    77
    Ah great tip, thanks Karl! Didn't cross my mind to re-use to reference

    EDIT from the future: Even with this approach, it still seems required to call
    RefreshString
    at least once, once the variable is added to the string the first time. Otherwise the string seems to remain in an erroneous state.
     
    Last edited: Aug 15, 2022
    karl_jones likes this.
  15. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    699
    I had already posted my code here but using StringChanged and making the LocalizedString public are two nice improvements:

    Code (csharp):
    1.  
    2. using System;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using System.Text;
    6. using System.Threading.Tasks;
    7. using UnityEngine;
    8. using UnityEngine.Localization;
    9. using UnityEngine.Localization.Settings;
    10. using UnityEngine.Localization.Tables;
    11. using UnityEngine.UIElements;
    12.  
    13. namespace UnityEngine.UIElements
    14. {
    15.     internal class LocalizedLabel : TextElement
    16.     {
    17.         public new class UxmlFactory : UxmlFactory<LocalizedLabel, UxmlTraits> { }
    18.  
    19.         public new class UxmlTraits : TextElement.UxmlTraits
    20.         {
    21.             UxmlStringAttributeDescription m_LocaleKey = new UxmlStringAttributeDescription { name = "locale-key" };
    22.  
    23.             public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
    24.             {
    25.                 get { yield break; }
    26.             }
    27.  
    28.             public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
    29.             {
    30.  
    31.                 base.Init(ve, bag, cc);
    32.                 var ll = ve as LocalizedLabel;
    33.                 ll.LocaleKey = m_LocaleKey.GetValueFromBag(bag, cc);
    34.                 ll.initLabel();
    35.             }
    36.         }
    37.         public string LocaleKey { get; set; }
    38.         public LocalizedString LocalizedString { get; private set; }
    39.  
    40.         public new static readonly string ussClassName = "localized-label";
    41.  
    42.         public LocalizedLabel()
    43.         {
    44.             AddToClassList(ussClassName);
    45.             LocalizedString = new LocalizedString();
    46.         }
    47.  
    48.         public LocalizedLabel(string localeKey) : this()
    49.         {
    50.             this.LocaleKey = localeKey;
    51.             initLabel();
    52.         }
    53.  
    54.  
    55.         private void initLabel()
    56.         {
    57.             var p = LocaleKey.IndexOf('/');
    58.             if (p > 0 && p != (LocaleKey.Length - 1) && LocaleKey.Length > 2)
    59.             {
    60.                 LocalizedString.TableReference = LocaleKey.Substring(0, p);
    61.                 LocalizedString.TableEntryReference = LocaleKey.Substring(p + 1);
    62.                 RegisterCallback<GeometryChangedEvent>(initChange) ;
    63.             }
    64.         }
    65.  
    66.         private void initChange(GeometryChangedEvent evt)
    67.         {
    68.             LocalizedString.StringChanged += stringChanged;
    69.             UnregisterCallback<GeometryChangedEvent>(initChange);  
    70.         }
    71.  
    72.         private void stringChanged(string value)
    73.         {
    74.             text = value;
    75.         }
    76.     }
    77. }
    78.  
    79.  
     
    Last edited: Jun 29, 2022
    lilacsky824 and karl_jones like this.
  16. dlorre

    dlorre

    Joined:
    Apr 12, 2020
    Posts:
    699
    I've fixed the warning: "SendMessage cannot be called during Awake, CheckConsistency, or OnValidate" by using a GeometryChangedEvent callback.
     
    karl_jones likes this.
  17. yonicstudios

    yonicstudios

    Joined:
    Jul 15, 2018
    Posts:
    7
    Another option would be to use Asset Localization instead of String Localization.

    You can use Localized Property Variants (Unity 2020.3 or later) or Component Localizers to change the Tree View Asset of the UIDocument component at runtime, and have one Tree View Asset for each locale. This is useful for situations in which the layout is altered with the language change or you want a more fine control of the visual elements of the document like animations. I would suggest the latter unless you have a lot of different UI Documents to deal with in a lot of languages.

    For Component Localizers, it only requires 2 lines of code and adding the LocalizeUIDocumentEvent component to the UIDocument you want to localize:

    Code (CSharp):
    1. public class LocalizeUIDocumentEvent : LocalizedAssetEvent<VisualTreeAsset, LocalizedVisualTreeAsset, UnityEvent<VisualTreeAsset>>{}
    (It might be necessary to replace the derived class to LocalizedAssetEvent<VisualTreeAsset> depending on the version of your Localization package and editor, the documentation is a bit outdated)

    Code (CSharp):
    1. [Serializable]
    2. public class LocalizedVisualTreeAsset: LocalizedAsset<VisualTreeAsset> {}
    Note that you mustn't set the Visual Tree Asset directly. Instead, make a setter function that does the cleanup for the previous Visual Tree, and the initialization for the new one.



    Plus, you can combine this with the LocalizedLabel from above.
     
    Last edited: Jul 14, 2022
    dlorre and karl_jones like this.
  18. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,281
    We do have a prototype based on a new bindings feature for UI Toolkit. Unfortunately, it will require Unity 2023 and above as that's the version the feature should land in.
     
    Last edited: Jul 14, 2022
    brainwipe, CodeSmile, avacio and 2 others like this.
  19. brainwipe

    brainwipe

    Joined:
    Aug 21, 2017
    Posts:
    78
  20. curtispelissier

    curtispelissier

    Joined:
    Jul 26, 2019
    Posts:
    39
  21. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,281
    Hi,
    The work is still ongoing with an estimate of landing during the 2023 cycle. We are currently waiting for the UI Toolkit binding improvements to land, once that is completed then we need to apply some changes to the package.

    Edit(June 1): The bindings changes landed in 2023.2!
    We are now working on the integration, expect to see something later in the year
     
    Last edited: Jun 1, 2023
  22. Onigiri

    Onigiri

    Joined:
    Aug 10, 2014
    Posts:
    482
    Hi @karl_jones. Could you give us a small example of how to bind to
    LocalizedString with new system?
     
  23. karl_jones

    karl_jones

    Unity Technologies

    Joined:
    May 5, 2015
    Posts:
    8,281
    The work is done now, just waiting on some reviews and then we will prepare the 1.5 release. The LocalizedString class is now a Custom binding so it should be very simple to use.
    E.g

    Code (csharp):
    1. label.SetBinding("text", localizedString);
    The uxml would look like:
    Code (csharp):
    1. <ui:UXML xmlns:ui="UnityEngine.UIElements">
    2.         <ui:Label text="Label">
    3.             <Bindings>
    4.                 <UnityEngine.Localization.LocalizedString property="text" table="General Test Data" entry="My Entry">
    5.                     <variables>
    6.                         <UnityEngine.Localization.LocalVariable name="counter">
    7.                             <UnityEngine.Localization.SmartFormat.PersistentVariables.IntVariable value="2" />
    8.                         </UnityEngine.Localization.LocalVariable>
    9.                     </variables>
    10.                 </UnityEngine.Localization.LocalizedString>
    11.             </Bindings>
    12.         </ui:Label>
    13.     </ui:UXML>
     
  24. Onigiri

    Onigiri

    Joined:
    Aug 10, 2014
    Posts:
    482
    Thanks, I'll wait for new release then
     
    corp_unity_czy and karl_jones like this.
  25. brainwipe

    brainwipe

    Joined:
    Aug 21, 2017
    Posts:
    78
    Smart! Thank you, Karl. That's precisely what I was after. Not quite as fancy shmancy as .NET's resx but good enough for what I need.
     
    karl_jones likes this.
  26. zhuli

    zhuli

    Joined:
    Feb 17, 2014
    Posts:
    1
    Hello, I'd like to share my custom solution, which is very easy to work with: custom styles! It's not perfect but works for simple cases.

    Like this in uxml:

    <ui:Label text="Leader Board" style="--localize-text: 'text-leader-board'" />
    <ui:Button text="Play" style="--localize-text:'text-play'" />

    'text-leader-board' and 'text-play' is your localization keys. Use a C# script to get all TextElement in a UIDocument and poll custom styles, if there is a style for '--localize-text', replace the text with a localization table lookup. Example C# code, might need some changes to work for you:

    Code (CSharp):
    1.  
    2. using UnityEngine;
    3. using UnityEngine.Localization.Settings;
    4. using UnityEngine.UIElements;
    5.  
    6. public class UILocalizationStyle:MonoBehaviour
    7. {
    8.     [SerializeField]
    9.     private UIDocument _document;
    10.  
    11.     private static readonly CustomStyleProperty<string> CustomStyleProperty = new CustomStyleProperty<string>("--localize-text");
    12.  
    13.     private void OnEnable()
    14.     {
    15.         foreach (var textElement in _document.rootVisualElement.Query<TextElement>().ToList())
    16.         {
    17.             LocalizeTextElement(textElement);
    18.             textElement.RegisterCallback<CustomStyleResolvedEvent>(OnStyleResolved);
    19.         }
    20.     }
    21.  
    22.     private void OnStyleResolved(CustomStyleResolvedEvent evt)
    23.     {
    24.         LocalizeTextElement(evt.target as TextElement);
    25.     }
    26.  
    27.     private void LocalizeTextElement(TextElement textElement)
    28.     {
    29.         Debug.Assert(textElement != null);
    30.         if (textElement.customStyle.TryGetValue(CustomStyleProperty, out var key))
    31.         {
    32.             if (!string.IsNullOrEmpty(key))
    33.             {
    34.                 string text = LocalizationSettings.StringDatabase.GetLocalizedString(key);
    35.                 if (!string.IsNullOrEmpty(text))
    36.                 {
    37.                     textElement.text = text;
    38.                 }
    39.             }
    40.         }
    41.     }
    42. }
    43.  
    Also, Unity has just provided an official solution.

    Update:
    Updated the script at Dec. 28 2023
     
    Last edited: Dec 28, 2023
    karl_jones likes this.