Browse Source

Add translation preview in editor

Haoyu Qiu 11 months ago
parent
commit
8d93b6a54c

+ 31 - 2
core/string/translation_domain.cpp

@@ -287,7 +287,11 @@ void TranslationDomain::clear() {
 }
 }
 
 
 StringName TranslationDomain::translate(const StringName &p_message, const StringName &p_context) const {
 StringName TranslationDomain::translate(const StringName &p_message, const StringName &p_context) const {
-	const String &locale = TranslationServer::get_singleton()->get_locale();
+	if (!enabled) {
+		return p_message;
+	}
+
+	const String &locale = locale_override.is_empty() ? TranslationServer::get_singleton()->get_locale() : locale_override;
 	StringName res = get_message_from_translations(locale, p_message, p_context);
 	StringName res = get_message_from_translations(locale, p_message, p_context);
 
 
 	const String &fallback = TranslationServer::get_singleton()->get_fallback_locale();
 	const String &fallback = TranslationServer::get_singleton()->get_fallback_locale();
@@ -302,7 +306,11 @@ StringName TranslationDomain::translate(const StringName &p_message, const Strin
 }
 }
 
 
 StringName TranslationDomain::translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const {
 StringName TranslationDomain::translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const {
-	const String &locale = TranslationServer::get_singleton()->get_locale();
+	if (!enabled) {
+		return p_n == 1 ? p_message : p_message_plural;
+	}
+
+	const String &locale = locale_override.is_empty() ? TranslationServer::get_singleton()->get_locale() : locale_override;
 	StringName res = get_message_from_translations(locale, p_message, p_message_plural, p_n, p_context);
 	StringName res = get_message_from_translations(locale, p_message, p_message_plural, p_n, p_context);
 
 
 	const String &fallback = TranslationServer::get_singleton()->get_fallback_locale();
 	const String &fallback = TranslationServer::get_singleton()->get_fallback_locale();
@@ -319,6 +327,22 @@ StringName TranslationDomain::translate_plural(const StringName &p_message, cons
 	return res;
 	return res;
 }
 }
 
 
+String TranslationDomain::get_locale_override() const {
+	return locale_override;
+}
+
+void TranslationDomain::set_locale_override(const String &p_locale) {
+	locale_override = p_locale.is_empty() ? p_locale : TranslationServer::get_singleton()->standardize_locale(p_locale);
+}
+
+bool TranslationDomain::is_enabled() const {
+	return enabled;
+}
+
+void TranslationDomain::set_enabled(bool p_enabled) {
+	enabled = p_enabled;
+}
+
 bool TranslationDomain::is_pseudolocalization_enabled() const {
 bool TranslationDomain::is_pseudolocalization_enabled() const {
 	return pseudolocalization.enabled;
 	return pseudolocalization.enabled;
 }
 }
@@ -424,6 +448,10 @@ void TranslationDomain::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("clear"), &TranslationDomain::clear);
 	ClassDB::bind_method(D_METHOD("clear"), &TranslationDomain::clear);
 	ClassDB::bind_method(D_METHOD("translate", "message", "context"), &TranslationDomain::translate, DEFVAL(StringName()));
 	ClassDB::bind_method(D_METHOD("translate", "message", "context"), &TranslationDomain::translate, DEFVAL(StringName()));
 	ClassDB::bind_method(D_METHOD("translate_plural", "message", "message_plural", "n", "context"), &TranslationDomain::translate_plural, DEFVAL(StringName()));
 	ClassDB::bind_method(D_METHOD("translate_plural", "message", "message_plural", "n", "context"), &TranslationDomain::translate_plural, DEFVAL(StringName()));
+	ClassDB::bind_method(D_METHOD("get_locale_override"), &TranslationDomain::get_locale_override);
+	ClassDB::bind_method(D_METHOD("set_locale_override", "locale"), &TranslationDomain::set_locale_override);
+	ClassDB::bind_method(D_METHOD("is_enabled"), &TranslationDomain::is_enabled);
+	ClassDB::bind_method(D_METHOD("set_enabled", "enabled"), &TranslationDomain::set_enabled);
 
 
 	ClassDB::bind_method(D_METHOD("is_pseudolocalization_enabled"), &TranslationDomain::is_pseudolocalization_enabled);
 	ClassDB::bind_method(D_METHOD("is_pseudolocalization_enabled"), &TranslationDomain::is_pseudolocalization_enabled);
 	ClassDB::bind_method(D_METHOD("set_pseudolocalization_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_enabled);
 	ClassDB::bind_method(D_METHOD("set_pseudolocalization_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_enabled);
@@ -445,6 +473,7 @@ void TranslationDomain::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_pseudolocalization_suffix", "suffix"), &TranslationDomain::set_pseudolocalization_suffix);
 	ClassDB::bind_method(D_METHOD("set_pseudolocalization_suffix", "suffix"), &TranslationDomain::set_pseudolocalization_suffix);
 	ClassDB::bind_method(D_METHOD("pseudolocalize", "message"), &TranslationDomain::pseudolocalize);
 	ClassDB::bind_method(D_METHOD("pseudolocalize", "message"), &TranslationDomain::pseudolocalize);
 
 
+	ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "enabled"), "set_enabled", "is_enabled");
 	ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_enabled"), "set_pseudolocalization_enabled", "is_pseudolocalization_enabled");
 	ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_enabled"), "set_pseudolocalization_enabled", "is_pseudolocalization_enabled");
 	ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_accents_enabled"), "set_pseudolocalization_accents_enabled", "is_pseudolocalization_accents_enabled");
 	ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_accents_enabled"), "set_pseudolocalization_accents_enabled", "is_pseudolocalization_accents_enabled");
 	ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_double_vowels_enabled"), "set_pseudolocalization_double_vowels_enabled", "is_pseudolocalization_double_vowels_enabled");
 	ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_double_vowels_enabled"), "set_pseudolocalization_double_vowels_enabled", "is_pseudolocalization_double_vowels_enabled");

+ 9 - 0
core/string/translation_domain.h

@@ -49,6 +49,9 @@ class TranslationDomain : public RefCounted {
 		String suffix = "]";
 		String suffix = "]";
 	};
 	};
 
 
+	bool enabled = true;
+
+	String locale_override;
 	HashSet<Ref<Translation>> translations;
 	HashSet<Ref<Translation>> translations;
 	PseudolocalizationConfig pseudolocalization;
 	PseudolocalizationConfig pseudolocalization;
 
 
@@ -79,6 +82,12 @@ public:
 	StringName translate(const StringName &p_message, const StringName &p_context) const;
 	StringName translate(const StringName &p_message, const StringName &p_context) const;
 	StringName translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const;
 	StringName translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const;
 
 
+	String get_locale_override() const;
+	void set_locale_override(const String &p_locale);
+
+	bool is_enabled() const;
+	void set_enabled(bool p_enabled);
+
 	bool is_pseudolocalization_enabled() const;
 	bool is_pseudolocalization_enabled() const;
 	void set_pseudolocalization_enabled(bool p_enabled);
 	void set_pseudolocalization_enabled(bool p_enabled);
 	bool is_pseudolocalization_accents_enabled() const;
 	bool is_pseudolocalization_accents_enabled() const;

+ 0 - 11
core/string/translation_server.cpp

@@ -406,21 +406,10 @@ void TranslationServer::clear() {
 }
 }
 
 
 StringName TranslationServer::translate(const StringName &p_message, const StringName &p_context) const {
 StringName TranslationServer::translate(const StringName &p_message, const StringName &p_context) const {
-	if (!enabled) {
-		return p_message;
-	}
-
 	return main_domain->translate(p_message, p_context);
 	return main_domain->translate(p_message, p_context);
 }
 }
 
 
 StringName TranslationServer::translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const {
 StringName TranslationServer::translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const {
-	if (!enabled) {
-		if (p_n == 1) {
-			return p_message;
-		}
-		return p_message_plural;
-	}
-
 	return main_domain->translate_plural(p_message, p_message_plural, p_n, p_context);
 	return main_domain->translate_plural(p_message, p_message_plural, p_n, p_context);
 }
 }
 
 

+ 1 - 5
core/string/translation_server.h

@@ -47,8 +47,6 @@ class TranslationServer : public Object {
 
 
 	mutable HashMap<String, int> locale_compare_cache;
 	mutable HashMap<String, int> locale_compare_cache;
 
 
-	bool enabled = true;
-
 	static inline TranslationServer *singleton = nullptr;
 	static inline TranslationServer *singleton = nullptr;
 
 
 	static void _bind_methods();
 	static void _bind_methods();
@@ -96,11 +94,9 @@ class TranslationServer : public Object {
 public:
 public:
 	_FORCE_INLINE_ static TranslationServer *get_singleton() { return singleton; }
 	_FORCE_INLINE_ static TranslationServer *get_singleton() { return singleton; }
 
 
+	Ref<TranslationDomain> get_main_domain() const { return main_domain; }
 	Ref<TranslationDomain> get_editor_domain() const { return editor_domain; }
 	Ref<TranslationDomain> get_editor_domain() const { return editor_domain; }
 
 
-	void set_enabled(bool p_enabled) { enabled = p_enabled; }
-	_FORCE_INLINE_ bool is_enabled() const { return enabled; }
-
 	void set_locale(const String &p_locale);
 	void set_locale(const String &p_locale);
 	String get_locale() const;
 	String get_locale() const;
 	String get_fallback_locale() const;
 	String get_fallback_locale() const;

+ 18 - 0
doc/classes/TranslationDomain.xml

@@ -23,6 +23,12 @@
 				Removes all translations.
 				Removes all translations.
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="get_locale_override" qualifiers="const">
+			<return type="String" />
+			<description>
+				Returns the locale override of the domain. Returns an empty string if locale override is disabled.
+			</description>
+		</method>
 		<method name="get_translation_object" qualifiers="const">
 		<method name="get_translation_object" qualifiers="const">
 			<return type="Translation" />
 			<return type="Translation" />
 			<param index="0" name="locale" type="String" />
 			<param index="0" name="locale" type="String" />
@@ -44,6 +50,15 @@
 				Removes the given translation.
 				Removes the given translation.
 			</description>
 			</description>
 		</method>
 		</method>
+		<method name="set_locale_override">
+			<return type="void" />
+			<param index="0" name="locale" type="String" />
+			<description>
+				Sets the locale override of the domain.
+				If [param locale] is an empty string, locale override is disabled. Otherwise, [param locale] will be standardized to match known locales (e.g. [code]en-US[/code] would be matched to [code]en_US[/code]).
+				[b]Note:[/b] Calling this method does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] signal manually.
+			</description>
+		</method>
 		<method name="translate" qualifiers="const">
 		<method name="translate" qualifiers="const">
 			<return type="StringName" />
 			<return type="StringName" />
 			<param index="0" name="message" type="StringName" />
 			<param index="0" name="message" type="StringName" />
@@ -65,6 +80,9 @@
 		</method>
 		</method>
 	</methods>
 	</methods>
 	<members>
 	<members>
+		<member name="enabled" type="bool" setter="set_enabled" getter="is_enabled" default="true">
+			If [code]true[/code], translation is enabled. Otherwise, [method translate] and [method translate_plural] will return the input message unchanged regardless of the current locale.
+		</member>
 		<member name="pseudolocalization_accents_enabled" type="bool" setter="set_pseudolocalization_accents_enabled" getter="is_pseudolocalization_accents_enabled" default="true">
 		<member name="pseudolocalization_accents_enabled" type="bool" setter="set_pseudolocalization_accents_enabled" getter="is_pseudolocalization_accents_enabled" default="true">
 			Replace all characters with their accented variants during pseudolocalization.
 			Replace all characters with their accented variants during pseudolocalization.
 			[b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options.
 			[b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options.

+ 50 - 1
editor/editor_node.cpp

@@ -509,6 +509,8 @@ void EditorNode::_update_from_settings() {
 
 
 	ResourceImporterTexture::get_singleton()->update_imports();
 	ResourceImporterTexture::get_singleton()->update_imports();
 
 
+	_update_translations();
+
 #ifdef DEBUG_ENABLED
 #ifdef DEBUG_ENABLED
 	NavigationServer2D::get_singleton()->set_debug_navigation_edge_connection_color(GLOBAL_GET("debug/shapes/navigation/2d/edge_connection_color"));
 	NavigationServer2D::get_singleton()->set_debug_navigation_edge_connection_color(GLOBAL_GET("debug/shapes/navigation/2d/edge_connection_color"));
 	NavigationServer2D::get_singleton()->set_debug_navigation_geometry_edge_color(GLOBAL_GET("debug/shapes/navigation/2d/geometry_edge_color"));
 	NavigationServer2D::get_singleton()->set_debug_navigation_geometry_edge_color(GLOBAL_GET("debug/shapes/navigation/2d/geometry_edge_color"));
@@ -544,6 +546,23 @@ void EditorNode::_gdextensions_reloaded() {
 	EditorHelp::generate_doc(true, false);
 	EditorHelp::generate_doc(true, false);
 }
 }
 
 
+void EditorNode::_update_translations() {
+	Ref<TranslationDomain> main = TranslationServer::get_singleton()->get_main_domain();
+
+	main->clear();
+	TranslationServer::get_singleton()->load_translations();
+
+	if (main->is_enabled() && !main->get_loaded_locales().has(main->get_locale_override())) {
+		// Translations for the current preview locale is removed.
+		main->set_enabled(false);
+		main->set_locale_override(String());
+		scene_root->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
+		emit_signal(SNAME("preview_locale_changed"));
+	} else {
+		scene_root->propagate_notification(NOTIFICATION_TRANSLATION_CHANGED);
+	}
+}
+
 void EditorNode::_update_theme(bool p_skip_creation) {
 void EditorNode::_update_theme(bool p_skip_creation) {
 	if (!p_skip_creation) {
 	if (!p_skip_creation) {
 		theme = EditorThemeManager::generate_theme(theme);
 		theme = EditorThemeManager::generate_theme(theme);
@@ -4025,6 +4044,33 @@ void EditorNode::set_edited_scene_root(Node *p_scene, bool p_auto_add) {
 	}
 	}
 }
 }
 
 
+String EditorNode::get_preview_locale() const {
+	const Ref<TranslationDomain> &main_domain = TranslationServer::get_singleton()->get_main_domain();
+	return main_domain->is_enabled() ? main_domain->get_locale_override() : String();
+}
+
+void EditorNode::set_preview_locale(const String &p_locale) {
+	const String &prev_locale = get_preview_locale();
+	if (prev_locale == p_locale) {
+		return;
+	}
+
+	// Texts set in the editor could be identifiers that should never be translated.
+	// So we need to disable translation entirely.
+	Ref<TranslationDomain> main_domain = TranslationServer::get_singleton()->get_main_domain();
+	main_domain->set_enabled(!p_locale.is_empty());
+	main_domain->set_locale_override(p_locale);
+
+	if (prev_locale.is_empty() == p_locale.is_empty()) {
+		// Switching between different locales.
+		scene_root->propagate_notification(NOTIFICATION_TRANSLATION_CHANGED);
+	} else {
+		// Switching between on/off.
+		scene_root->set_auto_translate_mode(p_locale.is_empty() ? AUTO_TRANSLATE_MODE_DISABLED : AUTO_TRANSLATE_MODE_ALWAYS);
+	}
+	emit_signal(SNAME("preview_locale_changed"));
+}
+
 Dictionary EditorNode::_get_main_scene_state() {
 Dictionary EditorNode::_get_main_scene_state() {
 	Dictionary state;
 	Dictionary state;
 	state["scene_tree_offset"] = SceneTreeDock::get_singleton()->get_tree_editor()->get_scene_tree()->get_vscroll_bar()->get_value();
 	state["scene_tree_offset"] = SceneTreeDock::get_singleton()->get_tree_editor()->get_scene_tree()->get_vscroll_bar()->get_value();
@@ -7060,6 +7106,7 @@ void EditorNode::_bind_methods() {
 	ADD_SIGNAL(MethodInfo("scene_saved", PropertyInfo(Variant::STRING, "path")));
 	ADD_SIGNAL(MethodInfo("scene_saved", PropertyInfo(Variant::STRING, "path")));
 	ADD_SIGNAL(MethodInfo("scene_changed"));
 	ADD_SIGNAL(MethodInfo("scene_changed"));
 	ADD_SIGNAL(MethodInfo("scene_closed", PropertyInfo(Variant::STRING, "path")));
 	ADD_SIGNAL(MethodInfo("scene_closed", PropertyInfo(Variant::STRING, "path")));
+	ADD_SIGNAL(MethodInfo("preview_locale_changed"));
 }
 }
 
 
 static Node *_resource_get_edited_scene() {
 static Node *_resource_get_edited_scene() {
@@ -7354,7 +7401,7 @@ EditorNode::EditorNode() {
 	ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &EditorNode::_update_from_settings));
 	ProjectSettings::get_singleton()->connect("settings_changed", callable_mp(this, &EditorNode::_update_from_settings));
 	GDExtensionManager::get_singleton()->connect("extensions_reloaded", callable_mp(this, &EditorNode::_gdextensions_reloaded));
 	GDExtensionManager::get_singleton()->connect("extensions_reloaded", callable_mp(this, &EditorNode::_gdextensions_reloaded));
 
 
-	TranslationServer::get_singleton()->set_enabled(false);
+	TranslationServer::get_singleton()->get_main_domain()->set_enabled(false);
 	// Load settings.
 	// Load settings.
 	if (!EditorSettings::get_singleton()) {
 	if (!EditorSettings::get_singleton()) {
 		EditorSettings::create();
 		EditorSettings::create();
@@ -7767,6 +7814,8 @@ EditorNode::EditorNode() {
 	editor_main_screen->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 	editor_main_screen->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 
 
 	scene_root = memnew(SubViewport);
 	scene_root = memnew(SubViewport);
+	scene_root->set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
+	scene_root->set_translation_domain(StringName());
 	scene_root->set_embedding_subwindows(true);
 	scene_root->set_embedding_subwindows(true);
 	scene_root->set_disable_3d(true);
 	scene_root->set_disable_3d(true);
 	scene_root->set_disable_input(true);
 	scene_root->set_disable_input(true);

+ 4 - 0
editor/editor_node.h

@@ -615,6 +615,7 @@ private:
 	void _update_vsync_mode();
 	void _update_vsync_mode();
 	void _update_from_settings();
 	void _update_from_settings();
 	void _gdextensions_reloaded();
 	void _gdextensions_reloaded();
+	void _update_translations();
 
 
 	void _renderer_selected(int);
 	void _renderer_selected(int);
 	void _update_renderer_color();
 	void _update_renderer_color();
@@ -819,6 +820,9 @@ public:
 	void set_edited_scene_root(Node *p_scene, bool p_auto_add);
 	void set_edited_scene_root(Node *p_scene, bool p_auto_add);
 	Node *get_edited_scene() { return editor_data.get_edited_scene_root(); }
 	Node *get_edited_scene() { return editor_data.get_edited_scene_root(); }
 
 
+	String get_preview_locale() const;
+	void set_preview_locale(const String &p_locale);
+
 	void fix_dependencies(const String &p_for_file);
 	void fix_dependencies(const String &p_for_file);
 	int new_scene();
 	int new_scene();
 	Error load_scene(const String &p_scene, bool p_ignore_broken_deps = false, bool p_set_inherited = false, bool p_force_open_imported = false, bool p_silent_change_tab = false);
 	Error load_scene(const String &p_scene, bool p_ignore_broken_deps = false, bool p_set_inherited = false, bool p_force_open_imported = false, bool p_silent_change_tab = false);

+ 76 - 0
editor/gui/editor_translation_preview_button.cpp

@@ -0,0 +1,76 @@
+/**************************************************************************/
+/*  editor_translation_preview_button.cpp                                 */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "editor_translation_preview_button.h"
+
+#include "core/string/translation_server.h"
+#include "editor/editor_node.h"
+
+void EditorTranslationPreviewButton::_update() {
+	const String &locale = EditorNode::get_singleton()->get_preview_locale();
+
+	if (locale.is_empty()) {
+		hide();
+		return;
+	}
+
+	const String name = TranslationServer::get_singleton()->get_locale_name(locale);
+	set_text(vformat(TTR("Previewing: %s"), name == locale ? locale : name + " [" + locale + "]"));
+	show();
+}
+
+void EditorTranslationPreviewButton::pressed() {
+	EditorNode::get_singleton()->set_preview_locale(String());
+}
+
+void EditorTranslationPreviewButton::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_THEME_CHANGED: {
+			set_button_icon(get_editor_theme_icon(SNAME("Translation")));
+		} break;
+
+		case NOTIFICATION_TRANSLATION_CHANGED: {
+			_update();
+		} break;
+
+		case NOTIFICATION_READY: {
+			EditorNode::get_singleton()->connect("preview_locale_changed", callable_mp(this, &EditorTranslationPreviewButton::_update));
+		} break;
+	}
+}
+
+EditorTranslationPreviewButton::EditorTranslationPreviewButton() {
+	set_auto_translate_mode(AUTO_TRANSLATE_MODE_DISABLED);
+	set_tooltip_auto_translate_mode(AUTO_TRANSLATE_MODE_ALWAYS);
+	set_accessibility_name(TTRC("Disable Translation Preview"));
+	set_tooltip_text(TTRC("Previewing translation. Click to disable."));
+	set_focus_mode(FOCUS_NONE);
+	set_visible(false);
+}

+ 47 - 0
editor/gui/editor_translation_preview_button.h

@@ -0,0 +1,47 @@
+/**************************************************************************/
+/*  editor_translation_preview_button.h                                   */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "scene/gui/button.h"
+
+class EditorTranslationPreviewButton : public Button {
+	GDCLASS(EditorTranslationPreviewButton, Button);
+
+	void _update();
+
+protected:
+	virtual void pressed() override;
+
+	void _notification(int p_what);
+
+public:
+	EditorTranslationPreviewButton();
+};

+ 81 - 0
editor/gui/editor_translation_preview_menu.cpp

@@ -0,0 +1,81 @@
+/**************************************************************************/
+/*  editor_translation_preview_menu.cpp                                   */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "editor/gui/editor_translation_preview_menu.h"
+
+#include "core/string/translation_server.h"
+#include "editor/editor_node.h"
+
+void EditorTranslationPreviewMenu::_prepare() {
+	const String current_preview_locale = EditorNode::get_singleton()->get_preview_locale();
+
+	clear();
+	reset_size();
+
+	add_radio_check_item(TTRC("None"));
+	set_item_metadata(-1, "");
+	if (current_preview_locale.is_empty()) {
+		set_item_checked(-1, true);
+	}
+
+	const Vector<String> locales = TranslationServer::get_singleton()->get_loaded_locales();
+	if (!locales.is_empty()) {
+		add_separator();
+	}
+	for (const String &locale : locales) {
+		const String name = TranslationServer::get_singleton()->get_locale_name(locale);
+		add_radio_check_item(name == locale ? name : name + " [" + locale + "]");
+		set_item_auto_translate_mode(-1, AUTO_TRANSLATE_MODE_DISABLED);
+		set_item_metadata(-1, locale);
+		if (locale == current_preview_locale) {
+			set_item_checked(-1, true);
+		}
+	}
+}
+
+void EditorTranslationPreviewMenu::_pressed(int p_index) {
+	for (int i = 0; i < get_item_count(); i++) {
+		set_item_checked(i, i == p_index);
+	}
+	EditorNode::get_singleton()->set_preview_locale(get_item_metadata(p_index));
+}
+
+void EditorTranslationPreviewMenu::_notification(int p_what) {
+	switch (p_what) {
+		case NOTIFICATION_READY: {
+			connect("about_to_popup", callable_mp(this, &EditorTranslationPreviewMenu::_prepare));
+			connect("index_pressed", callable_mp(this, &EditorTranslationPreviewMenu::_pressed));
+		} break;
+	}
+}
+
+EditorTranslationPreviewMenu::EditorTranslationPreviewMenu() {
+	set_hide_on_checkable_item_selection(false);
+}

+ 46 - 0
editor/gui/editor_translation_preview_menu.h

@@ -0,0 +1,46 @@
+/**************************************************************************/
+/*  editor_translation_preview_menu.h                                     */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#pragma once
+
+#include "scene/gui/popup_menu.h"
+
+class EditorTranslationPreviewMenu : public PopupMenu {
+	GDCLASS(EditorTranslationPreviewMenu, PopupMenu);
+
+	void _prepare();
+	void _pressed(int p_index);
+
+protected:
+	void _notification(int p_what);
+
+public:
+	EditorTranslationPreviewMenu();
+};

+ 11 - 0
editor/plugins/canvas_item_editor_plugin.cpp

@@ -41,6 +41,8 @@
 #include "editor/editor_undo_redo_manager.h"
 #include "editor/editor_undo_redo_manager.h"
 #include "editor/gui/editor_run_bar.h"
 #include "editor/gui/editor_run_bar.h"
 #include "editor/gui/editor_toaster.h"
 #include "editor/gui/editor_toaster.h"
+#include "editor/gui/editor_translation_preview_button.h"
+#include "editor/gui/editor_translation_preview_menu.h"
 #include "editor/gui/editor_zoom_widget.h"
 #include "editor/gui/editor_zoom_widget.h"
 #include "editor/plugins/animation_player_editor_plugin.h"
 #include "editor/plugins/animation_player_editor_plugin.h"
 #include "editor/plugins/editor_context_menu_plugin.h"
 #include "editor/plugins/editor_context_menu_plugin.h"
@@ -5395,6 +5397,13 @@ CanvasItemEditor::CanvasItemEditor() {
 	controls_hb->add_child(zoom_widget);
 	controls_hb->add_child(zoom_widget);
 	zoom_widget->connect("zoom_changed", callable_mp(this, &CanvasItemEditor::_update_zoom));
 	zoom_widget->connect("zoom_changed", callable_mp(this, &CanvasItemEditor::_update_zoom));
 
 
+	EditorTranslationPreviewButton *translation_preview_button = memnew(EditorTranslationPreviewButton);
+	translation_preview_button->set_flat(true);
+	translation_preview_button->add_theme_constant_override("outline_size", Math::ceil(2 * EDSCALE));
+	translation_preview_button->add_theme_color_override("font_outline_color", Color(0, 0, 0));
+	translation_preview_button->add_theme_color_override(SceneStringName(font_color), Color(1, 1, 1));
+	controls_hb->add_child(translation_preview_button);
+
 	panner.instantiate();
 	panner.instantiate();
 	panner->set_callbacks(callable_mp(this, &CanvasItemEditor::_pan_callback), callable_mp(this, &CanvasItemEditor::_zoom_callback));
 	panner->set_callbacks(callable_mp(this, &CanvasItemEditor::_pan_callback), callable_mp(this, &CanvasItemEditor::_zoom_callback));
 
 
@@ -5675,6 +5684,8 @@ CanvasItemEditor::CanvasItemEditor() {
 		theme_menu->set_item_checked(i, i == theme_preview);
 		theme_menu->set_item_checked(i, i == theme_preview);
 	}
 	}
 
 
+	p->add_submenu_node_item(TTRC("Preview Translation"), memnew(EditorTranslationPreviewMenu));
+
 	main_menu_hbox->add_child(memnew(VSeparator));
 	main_menu_hbox->add_child(memnew(VSeparator));
 
 
 	// Contextual toolbars.
 	// Contextual toolbars.

+ 32 - 30
editor/plugins/node_3d_editor_plugin.cpp

@@ -44,6 +44,8 @@
 #include "editor/editor_undo_redo_manager.h"
 #include "editor/editor_undo_redo_manager.h"
 #include "editor/gui/editor_run_bar.h"
 #include "editor/gui/editor_run_bar.h"
 #include "editor/gui/editor_spin_slider.h"
 #include "editor/gui/editor_spin_slider.h"
+#include "editor/gui/editor_translation_preview_button.h"
+#include "editor/gui/editor_translation_preview_menu.h"
 #include "editor/plugins/animation_player_editor_plugin.h"
 #include "editor/plugins/animation_player_editor_plugin.h"
 #include "editor/plugins/gizmos/audio_listener_3d_gizmo_plugin.h"
 #include "editor/plugins/gizmos/audio_listener_3d_gizmo_plugin.h"
 #include "editor/plugins/gizmos/audio_stream_player_3d_gizmo_plugin.h"
 #include "editor/plugins/gizmos/audio_stream_player_3d_gizmo_plugin.h"
@@ -2908,6 +2910,23 @@ void Node3DEditorViewport::_project_settings_changed() {
 	viewport->set_anisotropic_filtering_level(anisotropic_filtering_level);
 	viewport->set_anisotropic_filtering_level(anisotropic_filtering_level);
 }
 }
 
 
+static void override_button_stylebox(Button *p_button, const Ref<StyleBox> p_stylebox) {
+	p_button->begin_bulk_theme_override();
+	p_button->add_theme_style_override(CoreStringName(normal), p_stylebox);
+	p_button->add_theme_style_override("normal_mirrored", p_stylebox);
+	p_button->add_theme_style_override(SceneStringName(hover), p_stylebox);
+	p_button->add_theme_style_override("hover_mirrored", p_stylebox);
+	p_button->add_theme_style_override("hover_pressed", p_stylebox);
+	p_button->add_theme_style_override("hover_pressed_mirrored", p_stylebox);
+	p_button->add_theme_style_override(SceneStringName(pressed), p_stylebox);
+	p_button->add_theme_style_override("pressed_mirrored", p_stylebox);
+	p_button->add_theme_style_override("focus", p_stylebox);
+	p_button->add_theme_style_override("focus_mirrored", p_stylebox);
+	p_button->add_theme_style_override("disabled", p_stylebox);
+	p_button->add_theme_style_override("disabled_mirrored", p_stylebox);
+	p_button->end_bulk_theme_override();
+}
+
 void Node3DEditorViewport::_notification(int p_what) {
 void Node3DEditorViewport::_notification(int p_what) {
 	switch (p_what) {
 	switch (p_what) {
 		case NOTIFICATION_READY: {
 		case NOTIFICATION_READY: {
@@ -3250,35 +3269,9 @@ void Node3DEditorViewport::_notification(int p_what) {
 
 
 			const Ref<StyleBox> &information_3d_stylebox = gui_base->get_theme_stylebox(SNAME("Information3dViewport"), EditorStringName(EditorStyles));
 			const Ref<StyleBox> &information_3d_stylebox = gui_base->get_theme_stylebox(SNAME("Information3dViewport"), EditorStringName(EditorStyles));
 
 
-			view_display_menu->begin_bulk_theme_override();
-			view_display_menu->add_theme_style_override(CoreStringName(normal), information_3d_stylebox);
-			view_display_menu->add_theme_style_override("normal_mirrored", information_3d_stylebox);
-			view_display_menu->add_theme_style_override(SceneStringName(hover), information_3d_stylebox);
-			view_display_menu->add_theme_style_override("hover_mirrored", information_3d_stylebox);
-			view_display_menu->add_theme_style_override("hover_pressed", information_3d_stylebox);
-			view_display_menu->add_theme_style_override("hover_pressed_mirrored", information_3d_stylebox);
-			view_display_menu->add_theme_style_override(SceneStringName(pressed), information_3d_stylebox);
-			view_display_menu->add_theme_style_override("pressed_mirrored", information_3d_stylebox);
-			view_display_menu->add_theme_style_override("focus", information_3d_stylebox);
-			view_display_menu->add_theme_style_override("focus_mirrored", information_3d_stylebox);
-			view_display_menu->add_theme_style_override("disabled", information_3d_stylebox);
-			view_display_menu->add_theme_style_override("disabled_mirrored", information_3d_stylebox);
-			view_display_menu->end_bulk_theme_override();
-
-			preview_camera->begin_bulk_theme_override();
-			preview_camera->add_theme_style_override(CoreStringName(normal), information_3d_stylebox);
-			preview_camera->add_theme_style_override("normal_mirrored", information_3d_stylebox);
-			preview_camera->add_theme_style_override(SceneStringName(hover), information_3d_stylebox);
-			preview_camera->add_theme_style_override("hover_mirrored", information_3d_stylebox);
-			preview_camera->add_theme_style_override("hover_pressed", information_3d_stylebox);
-			preview_camera->add_theme_style_override("hover_pressed_mirrored", information_3d_stylebox);
-			preview_camera->add_theme_style_override(SceneStringName(pressed), information_3d_stylebox);
-			preview_camera->add_theme_style_override("pressed_mirrored", information_3d_stylebox);
-			preview_camera->add_theme_style_override("focus", information_3d_stylebox);
-			preview_camera->add_theme_style_override("focus_mirrored", information_3d_stylebox);
-			preview_camera->add_theme_style_override("disabled", information_3d_stylebox);
-			preview_camera->add_theme_style_override("disabled_mirrored", information_3d_stylebox);
-			preview_camera->end_bulk_theme_override();
+			override_button_stylebox(view_display_menu, information_3d_stylebox);
+			override_button_stylebox(translation_preview_button, information_3d_stylebox);
+			override_button_stylebox(preview_camera, information_3d_stylebox);
 
 
 			frame_time_gradient->set_color(0, get_theme_color(SNAME("success_color"), EditorStringName(Editor)));
 			frame_time_gradient->set_color(0, get_theme_color(SNAME("success_color"), EditorStringName(Editor)));
 			frame_time_gradient->set_color(1, get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
 			frame_time_gradient->set_color(1, get_theme_color(SNAME("warning_color"), EditorStringName(Editor)));
@@ -5597,12 +5590,15 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p
 	vbox->set_offset(SIDE_LEFT, 10 * EDSCALE);
 	vbox->set_offset(SIDE_LEFT, 10 * EDSCALE);
 	vbox->set_offset(SIDE_TOP, 10 * EDSCALE);
 	vbox->set_offset(SIDE_TOP, 10 * EDSCALE);
 
 
+	HBoxContainer *hbox = memnew(HBoxContainer);
+	vbox->add_child(hbox);
+
 	view_display_menu = memnew(MenuButton);
 	view_display_menu = memnew(MenuButton);
 	view_display_menu->set_flat(false);
 	view_display_menu->set_flat(false);
 	view_display_menu->set_h_size_flags(0);
 	view_display_menu->set_h_size_flags(0);
 	view_display_menu->set_shortcut_context(this);
 	view_display_menu->set_shortcut_context(this);
 	view_display_menu->set_accessibility_name(TTRC("View"));
 	view_display_menu->set_accessibility_name(TTRC("View"));
-	vbox->add_child(view_display_menu);
+	hbox->add_child(view_display_menu);
 
 
 	view_display_menu->get_popup()->set_hide_on_checkable_item_selection(false);
 	view_display_menu->get_popup()->set_hide_on_checkable_item_selection(false);
 
 
@@ -5745,6 +5741,9 @@ Node3DEditorViewport::Node3DEditorViewport(Node3DEditor *p_spatial_editor, int p
 	ED_SHORTCUT("spatial_editor/instant_scale", TTRC("Begin Scale Transformation"));
 	ED_SHORTCUT("spatial_editor/instant_scale", TTRC("Begin Scale Transformation"));
 	ED_SHORTCUT("spatial_editor/collision_reposition", TTRC("Reposition Using Collisions"), KeyModifierMask::SHIFT | Key::G);
 	ED_SHORTCUT("spatial_editor/collision_reposition", TTRC("Reposition Using Collisions"), KeyModifierMask::SHIFT | Key::G);
 
 
+	translation_preview_button = memnew(EditorTranslationPreviewButton);
+	hbox->add_child(translation_preview_button);
+
 	preview_camera = memnew(CheckBox);
 	preview_camera = memnew(CheckBox);
 	preview_camera->set_text(TTR("Preview"));
 	preview_camera->set_text(TTR("Preview"));
 	// Using Control even on macOS to avoid conflict with Quick Open shortcut.
 	// Using Control even on macOS to avoid conflict with Quick Open shortcut.
@@ -9357,6 +9356,9 @@ Node3DEditor::Node3DEditor() {
 	p->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_origin", TTRC("View Origin")), MENU_VIEW_ORIGIN);
 	p->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_origin", TTRC("View Origin")), MENU_VIEW_ORIGIN);
 	p->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_grid", TTRC("View Grid"), Key::NUMBERSIGN), MENU_VIEW_GRID);
 	p->add_check_shortcut(ED_SHORTCUT("spatial_editor/view_grid", TTRC("View Grid"), Key::NUMBERSIGN), MENU_VIEW_GRID);
 
 
+	p->add_separator();
+	p->add_submenu_node_item(TTRC("Preview Translation"), memnew(EditorTranslationPreviewMenu));
+
 	p->add_separator();
 	p->add_separator();
 	p->add_shortcut(ED_SHORTCUT("spatial_editor/settings", TTRC("Settings...")), MENU_VIEW_CAMERA_SETTINGS);
 	p->add_shortcut(ED_SHORTCUT("spatial_editor/settings", TTRC("Settings...")), MENU_VIEW_CAMERA_SETTINGS);
 
 

+ 1 - 0
editor/plugins/node_3d_editor_plugin.h

@@ -243,6 +243,7 @@ private:
 
 
 	EditorSelection *editor_selection = nullptr;
 	EditorSelection *editor_selection = nullptr;
 
 
+	Button *translation_preview_button = nullptr;
 	CheckBox *preview_camera = nullptr;
 	CheckBox *preview_camera = nullptr;
 	SubViewportContainer *subviewport_container = nullptr;
 	SubViewportContainer *subviewport_container = nullptr;
 
 

+ 0 - 7
scene/main/node.cpp

@@ -197,13 +197,6 @@ void Node::_notification(int p_notification) {
 			data.is_auto_translate_dirty = true;
 			data.is_auto_translate_dirty = true;
 			data.is_translation_domain_dirty = true;
 			data.is_translation_domain_dirty = true;
 
 
-#ifdef TOOLS_ENABLED
-			// Don't translate UI elements when they're being edited.
-			if (is_part_of_edited_scene()) {
-				set_message_translation(false);
-			}
-#endif
-
 			if (data.input) {
 			if (data.input) {
 				add_to_group("_vp_input" + itos(get_viewport()->get_instance_id()));
 				add_to_group("_vp_input" + itos(get_viewport()->get_instance_id()));
 			}
 			}