I understand that it’s still early and some easy of use and quality of life features might be already planned.
But here is some things that would simplify my workflow a lot, and I draw from some years of experience building and using in-house Unity based tools in medium sized teams that worked more like what I will describe.
And I might have missed features that already exists, but I’ve read most of the docs and I think this is fair criticism, but please point out if I’ve missed anything!
Arrange localization entries in a tree structure, something like this:
Sepprate string tables can still exists. But in my opinion it’s really unnecessary having to separate entries in completely different tables to organize it all. Instead have everything in one place with categories and sub-categories that your can expand, minimize and so on.
Make the table editor slimmer or/and make it better fit the content, the rows does not need to be this big:
Make the data much more accessible in code. Having to specify stuff in the inspector is very unnecessary, clunky and slow in a lot of situations. I’ve solved this by having a small export script that makes everything available in code, then I use that straight in code with a reference to the table. It looks like this:
Here is the source code to that export extension if anyone would find is useful:
(I wrote it some time ago, do I don’t remember exactly how it all came together, but that should work)
Export Code
TableReferenceDrawer.cs
using UnityEditor;
using UnityEditor.Localization;
using UnityEngine;
using UnityEngine.Localization.Tables;
[CustomPropertyDrawer(typeof(TableReference))]
public class TableReferenceDrawer : PropertyDrawer
{
static GUIContent _Title = new GUIContent("Localization Table");
static GUIContent[] s_TableChoicesLabels;
static int _SelectedTableIndex = -1;
static GUIContent[] ProjectTableLabels
{
get
{
if (s_TableChoicesLabels == null)
{
var assetTables = LocalizationEditorSettings.GetStringTableCollections();
s_TableChoicesLabels = new GUIContent[assetTables.Count + 1];
s_TableChoicesLabels[0] = new GUIContent("None");
for (int i = 0; i < assetTables.Count; ++i)
{
s_TableChoicesLabels[i + 1] = new GUIContent(assetTables[i].TableCollectionName);
}
}
return s_TableChoicesLabels;
}
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var tableNameProp = property.FindPropertyRelative("m_TableCollectionName");
if (_SelectedTableIndex == -1 && string.IsNullOrWhiteSpace(tableNameProp.stringValue) == false)
_SelectedTableIndex = ArrayUtility.FindIndex(ProjectTableLabels, (e) => e.text == tableNameProp.stringValue);
if (_SelectedTableIndex == -1)
_SelectedTableIndex = 0;
EditorGUI.BeginProperty(position, label, property);
_SelectedTableIndex = EditorGUI.Popup(position, _Title, _SelectedTableIndex, ProjectTableLabels);
tableNameProp.stringValue = ProjectTableLabels[_SelectedTableIndex].text;
EditorGUI.EndProperty();
property.serializedObject.ApplyModifiedProperties();
}
}
ExportLocalizationExtension.cs:
using UnityEditor.Localization;
using UnityEngine;
[SerializeField]
[StringTableCollectionExtension]
public class ExportLocalizationExtension : CollectionExtension
{
public string TargetFolder;
}
ExportLocalizationExtensionProperyDrawer.cs:
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEditor.Localization;
using UnityEngine;
[CustomPropertyDrawer(typeof(ExportLocalizationExtension))]
public class ExportLocalizationExtensionProperyDrawer : PropertyDrawer
{
private SerializedProperty _TargetFolder;
private const int _StartOffset = 10;
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
if (_TargetFolder == null)
_TargetFolder = property.FindPropertyRelative("TargetFolder");
position.yMin += EditorGUIUtility.standardVerticalSpacing;
position.height = EditorGUIUtility.singleLineHeight;
GUI.Label(position, "Export key value pairs to .cs file");
position.y += position.height + EditorGUIUtility.standardVerticalSpacing;
EditorGUI.PropertyField(position, _TargetFolder);
position.y += position.height + EditorGUIUtility.standardVerticalSpacing;
if (GUI.Button(position, "Export"))
{
var target = GetActualObjectForSerializedProperty<ExportLocalizationExtension>(property, fieldInfo);
var collection = target.TargetCollection as StringTableCollection;
var sortedKeyEntries = collection.SharedData.Entries.OrderBy(e => e.Id);
var className = "Loc" + collection.TableCollectionName;
var fileContent = new List<string>();
fileContent.Add("public static class " + className + "\n{");
foreach (var item in sortedKeyEntries)
{
var line = " public const long " + item.Key + " = " + item.Id + ";";
fileContent.Add(line);
}
fileContent.Add("}");
File.WriteAllLines(Path.Combine(_TargetFolder.stringValue, className) + ".cs", fileContent);
AssetDatabase.Refresh();
}
}
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return _StartOffset + EditorGUIUtility.singleLineHeight * 3;
}
public static TObject GetActualObjectForSerializedProperty<TObject>(SerializedProperty property, System.Reflection.FieldInfo field) where TObject : class
{
try
{
if (property == null || field == null)
return null;
var serializedObject = property.serializedObject;
if (serializedObject == null)
{
return null;
}
var targetObject = serializedObject.targetObject;
var obj = field.GetValue(targetObject);
if (obj == null)
{
return null;
}
TObject actualObject = null;
if (obj.GetType().IsArray)
{
var index = Convert.ToInt32(new string(property.propertyPath.Where(char.IsDigit).ToArray()));
actualObject = ((TObject[])obj)[index];
}
else if (typeof(IList).IsAssignableFrom(obj.GetType()))
{
var index = Convert.ToInt32(new string(property.propertyPath.Where(char.IsDigit).ToArray()));
actualObject = ((IList)obj)[index] as TObject;
}
else
{
actualObject = obj as TObject;
}
return actualObject;
}
catch
{
return null;
}
}
}
I like the tool otherwise and I think it’s great that Unity is making it so openly with all source code available!
How would this look for each language?
We have a foldout view for the LocalizedString and LocalizedAsset however I think some users would disagree with this instead of the table editor as it seems to require more clicks to interact and edit each entry. We went with a spreadsheet layout as that is the most common way to present the localization data, a lot of people use google sheets and spreadsheets.
We do have plans for a complete UX review and a new table editor in the future. One of the ideas was a sidebar that would look like your table suggestion, you would select the ones you are interested in and then the panel on the right would show the selected. We would use the new Unity search system to let you filter the entries with various special filters.
Do you mean having them all in 1 asset or just the editor display? We separate them into tables per language so that we don’t need to load unnecessary data, we only load what is needed for that language. It means you can create separate language packs etc.
What are you trying to do via code that must be done through the inspector? It should be possible to do everything through code.
Code generation of ids is something we have planned for the future.
I sympathize a great deal with minimizing clicks! And the current solution minimizes some type of clicks. But I think having different tables increases the clicks again (not to mention all scrolling!) I guess this is game genre dependent and how strings naturally falls into categories and hierarches. In our case I would love to have everything in one view and just different categories. And I think that would be the case for most games. But I do understand that that might have a bit to do with how our project is structured. Here is how it worked at my previous studio:
The selected entry’s name is only editable in the settings panel. And i think that is good, changing the name of the entry is rarely done in my experience. Being able to jump around between all entries with category clicks and some small scrolls instead of big scrolls and switching between tables, feels a lot better for me.
It sound a bit like you guys are working on something similar to what I would like. Maybe my illustration above is enough to bridge this understanding?
Just in the editor to browse and inspect all entries easier. There is so much scrolling right now. The more I think about it the more I feel like a category based tree structure that is just for the working data in the editor would solve it!
Yeah, it was possible to do in code. No “hacking” needed. But having some simplification and/or tutorial of how to work with it just in code would be nice. Now that I have done it, is does not look that hard, but I remember digging a lot in code to find how to get the localized text without involving any reference in the inspector. The table reference might be needed. But getting the EntryId/KeyId without any classes, references or inspector work, that took some time to figure out. It’s just an id and I think it should be accessible in some global way, like my code generation. A big wrench in my machine was trying to marry this tool with DOTS and having this stuff burstable all the way up until the GUI code. I think that might be the best explanation on why I had feedback here.
Well then, excellent! That sound sweet! Maybe my way doing it can be a tiny little case study
I agree changing the entry name is not a common operation. We had this idea suggested by another user
A tree view on the left panel with selection like this on the right panel would be good. We need to do some actual UX research to see if this is true though.
It is possible to structure tables into categories through the group option
So this combined with the new layout could allow for a structure you describe.
Please do use the feedback section at the bottom of the docs if you feel its missing a sample or details. We do process these every week.
Alright, I might provide a bit of sample code! Great to know that feedback is getting through.
Small note on the UX research you need to do. I understand fully that you need to do the proper research and confirm how the UX should be designed. Just to add to that, the design I’ve described here was used in the localization tool that was build for the game Battlerite by Stunlock Studios, the studio I worked at before. Battlerite being a MOBA-isch game with tons of characters and spells and thus containing a ton of strings and our tool worked great for that. I mention this if that gives you guys a tiny little sample point of the validity of the design I advocate.
Anyway, good luck with it and thanks for the responsiveness!
That would be great! I will poke my old colleague who was the main author of the tool and still works there, and se if I can share some screenshots with you in a discussion. I’ll come back to you!
Edit, I’ll see if I can get more then screenshots, maybe a few clips or so.
So I’ve got some screenshots and comments from my old colleague. I was a bit mistaken about some details of the system used, but generally it’s what I’ve described. Here are a few images and gifs of how it works and a few comments from the creator.
Here is the basic layout of the tool. I remembered this being in Unity, but I was mistaken. But i suspect all of this can be made in Unity with little issue now days.
Interesting. It reminds me a little bit of poedit which is a tool we based an earlier version on.
This is our first string editor that was in 0.2
It does seem like a good idea to show the tables and entries like this instead of the popup we currently have. An advantage of this is we can then show multiple tables at once and even allow for dragging an entry from one table to another.
This does look like a fast way to select something, what happens if you click the wrong item? Do you need to press the reset? We plan to replace our current selection with a system built on Unity Quick Search.
We have a few ideas for improvements to the existing tables which would require a rethink in our editor. We want to combine string tables and asset tables into a single hybrid table that can have different types of entries, such as string and asset but also things like nested strings, platform variant entries(instead of using metadata) and combined entries such as a subtitles(string) with audio.
We also want to allow entries to have custom drawers and allow metadata to also draw on the entry, for example you may have a Translation Status metadata that colors the entry by the current state (Translated, Fuzzy, etc).
You may want to highlight all the entries that have syntax errors or missing translated values etc.
And we also want to make the metadata more visible instead of in a separate panel.
This will all be part of UX research in the future, once we have released 1.0.0.
Thanks for the suggestions so far, I have added them to our UX document for reference.
As seen in the GIF there is also a key search field just above, the most common practice for our Devs is to search for the key instead of manually clicking (like I did in the GIF). Building the new system on Unity Quick Search seems like a good idea!
When recording I also realized there was a bug in the code so the “Back” button wasn’t displayed correctly. The visuals and flow of this window is very outdated and is something that should be updated using the UIToolkit. And I think having it as a tree structure here would’ve been beneficial.
Understandibly its more tricky to design the Localization flow so that it fits all cases. As you mention different games might require other meta-data for their workflow.
When you mention combining string tables and assets, I come to think about having assets that are localization based as well. (Think a 3D object that says something in English that might need to be changed when playing in German. Or a skull that needs to be removed for release in China) Or was that something you meant as well?
I can envision Assets having tags as variants depending on what language (/or almost “version”) is being played.
Happy that we can share the solution we have and hopefully inspire some changes! ^^