diff --git a/Source/Editor/CustomEditors/Editors/LocalizedStringEditor.cs b/Source/Editor/CustomEditors/Editors/LocalizedStringEditor.cs
new file mode 100644
index 000000000..0f93df70b
--- /dev/null
+++ b/Source/Editor/CustomEditors/Editors/LocalizedStringEditor.cs
@@ -0,0 +1,232 @@
+// Copyright (c) 2012-2021 Wojciech Figat. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using FlaxEditor.Content.Settings;
+using FlaxEditor.CustomEditors.Elements;
+using FlaxEditor.GUI.Tree;
+using FlaxEditor.Scripting;
+using FlaxEditor.Utilities;
+using FlaxEngine;
+using FlaxEngine.GUI;
+using Utils = FlaxEditor.Utilities.Utils;
+
+namespace FlaxEditor.CustomEditors.Editors
+{
+ ///
+ /// Default implementation of the inspector used to edit localized string properties.
+ ///
+ [CustomEditor(typeof(LocalizedString)), DefaultEditor]
+ public sealed class LocalizedStringEditor : GenericEditor
+ {
+ private TextBoxElement _idElement, _valueElement;
+
+ ///
+ public override DisplayStyle Style => DisplayStyle.Inline;
+
+ ///
+ public override void Initialize(LayoutElementsContainer layout)
+ {
+ base.Initialize(layout);
+
+ if (layout.Children.Count != 1)
+ return;
+ var propList = layout.Children[0] as PropertiesListElement;
+ if (propList == null || propList.Children.Count != 2)
+ return;
+ var idElement = propList.Children[0] as TextBoxElement;
+ var valueElement = propList.Children[1] as TextBoxElement;
+ if (idElement == null || valueElement == null)
+ return;
+ _idElement = idElement;
+ _valueElement = valueElement;
+
+ var attributes = Values.GetAttributes();
+ var multiLine = attributes?.FirstOrDefault(x => x is MultilineTextAttribute);
+ if (multiLine != null)
+ {
+ valueElement.TextBox.IsMultiline = true;
+ valueElement.TextBox.Height *= 3;
+ }
+
+ var selectString = new Button
+ {
+ Width = 16.0f,
+ Text = "...",
+ TooltipText = "Select localized text from Localization Settings...",
+ Parent = idElement.TextBox,
+ };
+ selectString.SetAnchorPreset(AnchorPresets.MiddleRight, false, true);
+ selectString.ButtonClicked += OnSelectStringClicked;
+
+ var addString = new Button
+ {
+ Width = 16.0f,
+ Text = "+",
+ TooltipText = "Add new localized text to Localization Settings (all used locales)",
+ Parent = _valueElement.TextBox,
+ };
+ addString.SetAnchorPreset(AnchorPresets.MiddleRight, false, true);
+ addString.ButtonClicked += OnAddStringClicked;
+ }
+
+ ///
+ internal override void RefreshInternal()
+ {
+ base.RefreshInternal();
+
+ if (_valueElement != null)
+ {
+ _valueElement.TextBox.WatermarkText = Localization.GetString(_idElement.Text);
+ }
+ }
+
+ ///
+ protected override void Deinitialize()
+ {
+ base.Deinitialize();
+
+ _idElement = null;
+ _valueElement = null;
+ }
+
+ private void OnSelectStringClicked(Button button)
+ {
+ var settings = GameSettings.Load();
+ if (settings?.LocalizedStringTables == null || settings.LocalizedStringTables.Length == 0)
+ {
+ MessageBox.Show("No valid localization settings setup.");
+ return;
+ }
+ Profiler.BeginEvent("LocalizedStringEditor.OnSelectStringClicked");
+ var allKeys = new HashSet();
+ for (int i = 0; i < settings.LocalizedStringTables.Length; i++)
+ {
+ var table = settings.LocalizedStringTables[i];
+ if (table && !table.WaitForLoaded())
+ {
+ var entries = table.Entries;
+ foreach (var e in entries)
+ allKeys.Add(e.Key);
+ }
+ }
+ var allKeysSorted = allKeys.ToList();
+ allKeysSorted.Sort();
+ var value = _idElement?.TextBox.Text;
+ var menu = Utils.CreateSearchPopup(out var searchBox, out var tree);
+ var idToNode = new TreeNode[allKeysSorted.Count];
+ for (var i = 0; i < allKeysSorted.Count; i++)
+ {
+ var key = allKeysSorted[i];
+ var node = new TreeNode
+ {
+ Text = key,
+ TooltipText = Localization.GetString(key),
+ Parent = tree,
+ };
+ if (key == value)
+ tree.Select(node);
+ idToNode[i] = node;
+ }
+ tree.SelectedChanged += delegate(List before, List after)
+ {
+ if (after.Count == 1)
+ {
+ menu.Hide();
+ _idElement.TextBox.SetTextAsUser(after[0].Text);
+ }
+ };
+ searchBox.TextChanged += delegate
+ {
+ if (tree.IsLayoutLocked)
+ return;
+ tree.LockChildrenRecursive();
+ var query = searchBox.Text;
+ for (int i = 0; i < idToNode.Length; i++)
+ {
+ var node = idToNode[i];
+ node.Visible = string.IsNullOrWhiteSpace(query) || QueryFilterHelper.Match(query, node.Text);
+ }
+ tree.UnlockChildrenRecursive();
+ menu.PerformLayout();
+ };
+ menu.Show(button, new Vector2(0, button.Height));
+ Profiler.EndEvent();
+ }
+
+ private void OnAddStringClicked(Button button)
+ {
+ var settings = GameSettings.Load();
+ if (settings?.LocalizedStringTables == null || settings.LocalizedStringTables.Length == 0)
+ {
+ MessageBox.Show("No valid localization settings setup.");
+ return;
+ }
+ Profiler.BeginEvent("LocalizedStringEditor.OnAddStringClicked");
+ var allKeys = new HashSet();
+ for (int i = 0; i < settings.LocalizedStringTables.Length; i++)
+ {
+ var table = settings.LocalizedStringTables[i];
+ if (table && !table.WaitForLoaded())
+ {
+ var entries = table.Entries;
+ foreach (var e in entries)
+ allKeys.Add(e.Key);
+ }
+ }
+ string newKey = null;
+ if (string.IsNullOrEmpty(_idElement.Text))
+ {
+ CustomEditor customEditor = this;
+ while (customEditor?.Values != null)
+ {
+ if (customEditor.Values.Info != ScriptMemberInfo.Null)
+ if (newKey == null)
+ newKey = customEditor.Values.Info.Name;
+ else
+ newKey = customEditor.Values.Info.Name + '.' + newKey;
+ else if (customEditor.Values[0] is SceneObject sceneObject)
+ if (newKey == null)
+ newKey = sceneObject.GetNamePath('.');
+ else
+ newKey = sceneObject.GetNamePath('.') + '.' + newKey;
+ else
+ break;
+ customEditor = customEditor.ParentEditor;
+ }
+ if (string.IsNullOrWhiteSpace(newKey))
+ newKey = Guid.NewGuid().ToString("N");
+ }
+ else
+ {
+ newKey = _idElement.Text;
+ }
+ if (allKeys.Contains(newKey))
+ {
+ Profiler.EndEvent();
+ if (_idElement.Text != newKey)
+ _idElement.TextBox.SetTextAsUser(newKey);
+ else
+ MessageBox.Show("Already added.");
+ return;
+ }
+ var newValue = _valueElement.Text;
+ Editor.Log(newKey + (newValue != null ? " = " + newValue : string.Empty));
+ var locales = settings.LocalizedStringTables.GroupBy(x => x.Locale);
+ foreach (var locale in locales)
+ {
+ var table = locale.First();
+ var entries = table.Entries;
+ if (table.Locale == "en")
+ entries[newKey] = new[] { newValue };
+ else
+ entries[newKey] = new[] { string.Empty };
+ table.Entries = entries;
+ table.Save();
+ }
+ _idElement.TextBox.SetTextAsUser(newKey);
+ Profiler.EndEvent();
+ }
+ }
+}