diff --git a/core/config/project_settings.cpp b/core/config/project_settings.cpp
index c9b615fb0ad..6db8100f593 100644
--- a/core/config/project_settings.cpp
+++ b/core/config/project_settings.cpp
@@ -1203,6 +1203,8 @@ ProjectSettings::ProjectSettings() {
singleton = this;
GLOBAL_DEF_BASIC("application/config/name", "");
+ GLOBAL_DEF_BASIC("application/config/name_localized", Dictionary());
+ custom_prop_info["application/config/name_localized"] = PropertyInfo(Variant::DICTIONARY, "application/config/name_localized", PROPERTY_HINT_LOCALIZABLE_STRING);
GLOBAL_DEF_BASIC("application/config/description", "");
custom_prop_info["application/config/description"] = PropertyInfo(Variant::STRING, "application/config/description", PROPERTY_HINT_MULTILINE_TEXT);
GLOBAL_DEF_BASIC("application/run/main_scene", "");
diff --git a/core/core_constants.cpp b/core/core_constants.cpp
index 63e7323f7a3..ea8db7d2940 100644
--- a/core/core_constants.cpp
+++ b/core/core_constants.cpp
@@ -588,6 +588,7 @@ void register_global_constants() {
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_INT_IS_POINTER);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_ARRAY_TYPE);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_LOCALE_ID);
+ BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_LOCALIZABLE_STRING);
BIND_CORE_ENUM_CONSTANT(PROPERTY_HINT_MAX);
BIND_CORE_ENUM_CONSTANT(PROPERTY_USAGE_NONE);
diff --git a/core/object/object.h b/core/object/object.h
index b5be1cf0e7a..3f7c58238ec 100644
--- a/core/object/object.h
+++ b/core/object/object.h
@@ -95,6 +95,7 @@ enum PropertyHint {
PROPERTY_HINT_ARRAY_TYPE,
PROPERTY_HINT_INT_IS_POINTER,
PROPERTY_HINT_LOCALE_ID,
+ PROPERTY_HINT_LOCALIZABLE_STRING,
PROPERTY_HINT_MAX,
// When updating PropertyHint, also sync the hardcoded list in VisualScriptEditorVariableEdit
};
diff --git a/doc/classes/@GlobalScope.xml b/doc/classes/@GlobalScope.xml
index 17cb50d1a4e..134df25aae1 100644
--- a/doc/classes/@GlobalScope.xml
+++ b/doc/classes/@GlobalScope.xml
@@ -2513,7 +2513,10 @@
Hints that a string property is a locale code. Editing it will show a locale dialog for picking language and country.
-
+
+ Hints that a dictionary property is string translation map. Dictionary keys are locale codes and, values are translated strings.
+
+
diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml
index be2c1ad3725..e368d017af8 100644
--- a/doc/classes/ProjectSettings.xml
+++ b/doc/classes/ProjectSettings.xml
@@ -226,6 +226,9 @@
The project's name. It is used both by the Project Manager and by exporters. The project name can be translated by translating its value in localization files. The window title will be set to match the project name automatically on startup.
[b]Note:[/b] Changing this value will also change the user data folder's path if [member application/config/use_custom_user_dir] is [code]false[/code]. After renaming the project, you will no longer be able to access existing data in [code]user://[/code] unless you rename the old folder to match the new project name. See [url=$DOCS_URL/tutorials/io/data_paths.html]Data paths[/url] in the documentation for more information.
+
+ Translations of the project's name. This setting is used by OS tools to translate application name on Android, iOS and macOS.
+
Specifies a file to override project settings. For example: [code]user://custom_settings.cfg[/code]. See "Overriding" in the [ProjectSettings] class description at the top for more information.
[b]Note:[/b] Regardless of this setting's value, [code]res://override.cfg[/code] will still be read to override the project settings.
diff --git a/editor/editor_properties.cpp b/editor/editor_properties.cpp
index 68a3fabe1ea..be858ff898f 100644
--- a/editor/editor_properties.cpp
+++ b/editor/editor_properties.cpp
@@ -3738,8 +3738,13 @@ EditorProperty *EditorInspectorDefaultPlugin::get_editor_for_property(Object *p_
} break;
case Variant::DICTIONARY: {
- EditorPropertyDictionary *editor = memnew(EditorPropertyDictionary);
- return editor;
+ if (p_hint == PROPERTY_HINT_LOCALIZABLE_STRING) {
+ EditorPropertyLocalizableString *editor = memnew(EditorPropertyLocalizableString);
+ return editor;
+ } else {
+ EditorPropertyDictionary *editor = memnew(EditorPropertyDictionary);
+ return editor;
+ }
} break;
case Variant::ARRAY: {
EditorPropertyArray *editor = memnew(EditorPropertyArray);
diff --git a/editor/editor_properties_array_dict.cpp b/editor/editor_properties_array_dict.cpp
index 61261af6084..302cc9c28ce 100644
--- a/editor/editor_properties_array_dict.cpp
+++ b/editor/editor_properties_array_dict.cpp
@@ -742,7 +742,7 @@ void EditorPropertyDictionary::_property_changed(const String &p_property, Varia
emit_changed(get_edited_property(), dict, "", true);
- dict = dict.duplicate(); // Duplicate, so undo/redo works better\.
+ dict = dict.duplicate(); // Duplicate, so undo/redo works better.
object->set_dict(dict);
}
}
@@ -805,7 +805,7 @@ void EditorPropertyDictionary::_change_type_menu(int p_index) {
emit_changed(get_edited_property(), dict, "", false);
- dict = dict.duplicate(); // Duplicate, so undo/redo works better\.
+ dict = dict.duplicate(); // Duplicate, so undo/redo works better.
object->set_dict(dict);
update_property();
}
@@ -814,7 +814,7 @@ void EditorPropertyDictionary::update_property() {
Variant updated_val = get_edited_object()->get(get_edited_property());
if (updated_val.get_type() == Variant::NIL) {
- edit->set_text("Dictionary (Nil)"); // This provides symmetry with the array property.
+ edit->set_text(TTR("Dictionary (Nil)")); // This provides symmetry with the array property.
edit->set_pressed(false);
if (vbox) {
set_bottom_editor(nullptr);
@@ -826,7 +826,7 @@ void EditorPropertyDictionary::update_property() {
Dictionary dict = updated_val;
- edit->set_text("Dictionary (size " + itos(dict.size()) + ")");
+ edit->set_text(vformat(TTR("Dictionary (size %d)"), dict.size()));
bool unfolded = get_edited_object()->editor_is_section_unfolded(get_edited_property());
if (edit->is_pressed() != unfolded) {
@@ -1144,6 +1144,7 @@ void EditorPropertyDictionary::update_property() {
if (vbox) {
set_bottom_editor(nullptr);
memdelete(vbox);
+ button_add_item = nullptr;
vbox = nullptr;
}
}
@@ -1216,3 +1217,226 @@ EditorPropertyDictionary::EditorPropertyDictionary() {
change_type->connect("id_pressed", callable_mp(this, &EditorPropertyDictionary::_change_type_menu));
changing_type_index = -1;
}
+
+///////////////////// LOCALIZABLE STRING ///////////////////////////
+
+void EditorPropertyLocalizableString::_property_changed(const String &p_property, Variant p_value, const String &p_name, bool p_changing) {
+ if (p_property.begins_with("indices")) {
+ int index = p_property.get_slice("/", 1).to_int();
+ Dictionary dict = object->get_dict();
+ Variant key = dict.get_key_at_index(index);
+ dict[key] = p_value;
+
+ emit_changed(get_edited_property(), dict, "", true);
+
+ dict = dict.duplicate(); // Duplicate, so undo/redo works better.
+ object->set_dict(dict);
+ }
+}
+
+void EditorPropertyLocalizableString::_add_locale_popup() {
+ locale_select->popup_locale_dialog();
+}
+
+void EditorPropertyLocalizableString::_add_locale(const String &p_locale) {
+ Dictionary dict = object->get_dict();
+
+ object->set_new_item_key(p_locale);
+ object->set_new_item_value(String());
+ dict[object->get_new_item_key()] = object->get_new_item_value();
+
+ emit_changed(get_edited_property(), dict, "", false);
+
+ dict = dict.duplicate(); // Duplicate, so undo/redo works better.
+ object->set_dict(dict);
+ update_property();
+}
+
+void EditorPropertyLocalizableString::_remove_item(Object *p_button, int p_index) {
+ Dictionary dict = object->get_dict();
+
+ Variant key = dict.get_key_at_index(p_index);
+ dict.erase(key);
+
+ emit_changed(get_edited_property(), dict, "", false);
+
+ dict = dict.duplicate(); // Duplicate, so undo/redo works better.
+ object->set_dict(dict);
+ update_property();
+}
+
+void EditorPropertyLocalizableString::update_property() {
+ Variant updated_val = get_edited_object()->get(get_edited_property());
+
+ if (updated_val.get_type() == Variant::NIL) {
+ edit->set_text(TTR("Localizable String (Nil)")); // This provides symmetry with the array property.
+ edit->set_pressed(false);
+ if (vbox) {
+ set_bottom_editor(nullptr);
+ memdelete(vbox);
+ vbox = nullptr;
+ }
+ return;
+ }
+
+ Dictionary dict = updated_val;
+
+ edit->set_text(vformat(TTR("Localizable String (size %d)"), dict.size()));
+
+ bool unfolded = get_edited_object()->editor_is_section_unfolded(get_edited_property());
+ if (edit->is_pressed() != unfolded) {
+ edit->set_pressed(unfolded);
+ }
+
+ if (unfolded) {
+ updating = true;
+
+ if (!vbox) {
+ vbox = memnew(VBoxContainer);
+ add_child(vbox);
+ set_bottom_editor(vbox);
+
+ property_vbox = memnew(VBoxContainer);
+ property_vbox->set_h_size_flags(SIZE_EXPAND_FILL);
+ vbox->add_child(property_vbox);
+
+ paginator = memnew(EditorPaginator);
+ paginator->connect("page_changed", callable_mp(this, &EditorPropertyLocalizableString::_page_changed));
+ vbox->add_child(paginator);
+ } else {
+ // Queue children for deletion, deleting immediately might cause errors.
+ for (int i = property_vbox->get_child_count() - 1; i >= 0; i--) {
+ property_vbox->get_child(i)->queue_delete();
+ }
+ }
+
+ int size = dict.size();
+
+ int max_page = MAX(0, size - 1) / page_length;
+ page_index = MIN(page_index, max_page);
+
+ paginator->update(page_index, max_page);
+ paginator->set_visible(max_page > 0);
+
+ int offset = page_index * page_length;
+
+ int amount = MIN(size - offset, page_length);
+
+ dict = dict.duplicate();
+
+ object->set_dict(dict);
+
+ for (int i = 0; i < amount; i++) {
+ String prop_name;
+ Variant key;
+ Variant value;
+
+ prop_name = "indices/" + itos(i + offset);
+ key = dict.get_key_at_index(i + offset);
+ value = dict.get_value_at_index(i + offset);
+
+ EditorProperty *prop = memnew(EditorPropertyText);
+
+ prop->set_object_and_property(object.ptr(), prop_name);
+ int remove_index = 0;
+
+ String cs = key.get_construct_string();
+ prop->set_label(cs);
+ prop->set_tooltip(cs);
+ remove_index = i + offset;
+
+ prop->set_selectable(false);
+ prop->connect("property_changed", callable_mp(this, &EditorPropertyLocalizableString::_property_changed));
+ prop->connect("object_id_selected", callable_mp(this, &EditorPropertyLocalizableString::_object_id_selected));
+
+ HBoxContainer *hbox = memnew(HBoxContainer);
+ property_vbox->add_child(hbox);
+ hbox->add_child(prop);
+ prop->set_h_size_flags(SIZE_EXPAND_FILL);
+ Button *edit = memnew(Button);
+ edit->set_icon(get_theme_icon(SNAME("Remove"), SNAME("EditorIcons")));
+ hbox->add_child(edit);
+ edit->connect("pressed", callable_mp(this, &EditorPropertyLocalizableString::_remove_item), varray(edit, remove_index));
+
+ prop->update_property();
+ }
+
+ if (page_index == max_page) {
+ button_add_item = memnew(Button);
+ button_add_item->set_text(TTR("Add Translation"));
+ button_add_item->set_icon(get_theme_icon(SNAME("Add"), SNAME("EditorIcons")));
+ button_add_item->connect("pressed", callable_mp(this, &EditorPropertyLocalizableString::_add_locale_popup));
+ property_vbox->add_child(button_add_item);
+ }
+
+ updating = false;
+
+ } else {
+ if (vbox) {
+ set_bottom_editor(nullptr);
+ memdelete(vbox);
+ button_add_item = nullptr;
+ vbox = nullptr;
+ }
+ }
+}
+
+void EditorPropertyLocalizableString::_object_id_selected(const StringName &p_property, ObjectID p_id) {
+ emit_signal(SNAME("object_id_selected"), p_property, p_id);
+}
+
+void EditorPropertyLocalizableString::_notification(int p_what) {
+ switch (p_what) {
+ case NOTIFICATION_THEME_CHANGED:
+ case NOTIFICATION_ENTER_TREE: {
+ if (Object::cast_to