Browse Source

i18n: Make property paths and sections translatable

Co-Authored-By: Hugo Locurcio <[email protected]>
Haoyu Qiu 3 years ago
parent
commit
aaff2e3982

+ 3 - 1
editor/editor_feature_profile.cpp

@@ -33,6 +33,7 @@
 #include "core/os/dir_access.h"
 #include "editor/editor_settings.h"
 #include "editor_node.h"
+#include "editor_property_name_processor.h"
 #include "editor_scale.h"
 
 const char *EditorFeatureProfile::feature_names[FEATURE_MAX] = {
@@ -617,7 +618,8 @@ void EditorFeatureProfileManager::_class_list_item_selected() {
 			property->set_editable(0, true);
 			property->set_selectable(0, true);
 			property->set_checked(0, !edited->is_class_property_disabled(class_name, name));
-			property->set_text(0, name.capitalize());
+			property->set_text(0, EditorPropertyNameProcessor::get_singleton()->process_name(name));
+			property->set_tooltip(0, EditorPropertyNameProcessor::get_singleton()->make_tooltip_for_name(name));
 			property->set_metadata(0, name);
 			String icon_type = Variant::get_type_name(E->get().type);
 			property->set_icon(0, EditorNode::get_singleton()->get_class_icon(icon_type));

+ 8 - 5
editor/editor_inspector.cpp

@@ -36,6 +36,7 @@
 #include "dictionary_property_edit.h"
 #include "editor_feature_profile.h"
 #include "editor_node.h"
+#include "editor_property_name_processor.h"
 #include "editor_scale.h"
 #include "editor_settings.h"
 #include "multi_node_edit.h"
@@ -1551,11 +1552,11 @@ void EditorInspector::update_tree() {
 			if (dot != -1) {
 				String ov = name.right(dot);
 				name = name.substr(0, dot);
-				name = name.capitalize();
+				name = EditorPropertyNameProcessor::get_singleton()->process_name(name);
 				name += ov;
 
 			} else {
-				name = name.capitalize();
+				name = EditorPropertyNameProcessor::get_singleton()->process_name(name);
 			}
 		}
 
@@ -1565,7 +1566,7 @@ void EditorInspector::update_tree() {
 			String cat = path;
 
 			if (capitalize_paths) {
-				cat = cat.capitalize();
+				cat = EditorPropertyNameProcessor::get_singleton()->process_name(cat);
 			}
 
 			if (!filter.is_subsequence_ofi(cat) && !filter.is_subsequence_ofi(name) && property_prefix.to_lower().find(filter.to_lower()) == -1) {
@@ -1594,13 +1595,15 @@ void EditorInspector::update_tree() {
 					current_vbox->add_child(section);
 					sections.push_back(section);
 
+					String label = path_name;
 					if (capitalize_paths) {
-						path_name = path_name.capitalize();
+						label = EditorPropertyNameProcessor::get_singleton()->process_name(label);
 					}
 
 					Color c = sscolor;
 					c.a /= level;
-					section->setup(acc_path, path_name, object, c, use_folding);
+					section->setup(acc_path, label, object, c, use_folding);
+					section->set_tooltip(EditorPropertyNameProcessor::get_singleton()->make_tooltip_for_name(path_name));
 
 					VBoxContainer *vb = section->get_vbox();
 					item_path[acc_path] = vb;

+ 5 - 0
editor/editor_node.cpp

@@ -80,6 +80,7 @@
 #include "editor/editor_log.h"
 #include "editor/editor_plugin.h"
 #include "editor/editor_properties.h"
+#include "editor/editor_property_name_processor.h"
 #include "editor/editor_resource_picker.h"
 #include "editor/editor_resource_preview.h"
 #include "editor/editor_run_native.h"
@@ -5771,6 +5772,9 @@ int EditorNode::execute_and_show_output(const String &p_title, const String &p_p
 }
 
 EditorNode::EditorNode() {
+	EditorPropertyNameProcessor *epnp = memnew(EditorPropertyNameProcessor);
+	add_child(epnp);
+
 	Input::get_singleton()->set_use_accumulated_input(true);
 	Resource::_get_local_scene_func = _resource_get_edited_scene;
 
@@ -5970,6 +5974,7 @@ EditorNode::EditorNode() {
 	EDITOR_DEF("interface/editor/show_update_spinner", false);
 	EDITOR_DEF("interface/editor/update_continuously", false);
 	EDITOR_DEF("interface/editor/update_vital_only", false);
+	EDITOR_DEF("interface/editor/translate_properties", true);
 	EDITOR_DEF_RST("interface/scene_tabs/restore_scenes_on_load", false);
 	EDITOR_DEF_RST("interface/scene_tabs/show_thumbnail_on_hover", true);
 	EDITOR_DEF_RST("interface/inspector/capitalize_properties", true);

+ 121 - 0
editor/editor_property_name_processor.cpp

@@ -0,0 +1,121 @@
+/*************************************************************************/
+/*  editor_property_name_processor.cpp                                   */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* 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_property_name_processor.h"
+
+#include "editor_settings.h"
+
+EditorPropertyNameProcessor *EditorPropertyNameProcessor::singleton = nullptr;
+
+String EditorPropertyNameProcessor::_capitalize_name(const String &p_name) const {
+	String capitalized_string = p_name.capitalize();
+
+	// Fix the casing of a few strings commonly found in editor property/setting names.
+	for (Map<String, String>::Element *E = capitalize_string_remaps.front(); E; E = E->next()) {
+		capitalized_string = capitalized_string.replace(E->key(), E->value());
+	}
+
+	return capitalized_string;
+}
+
+String EditorPropertyNameProcessor::process_name(const String &p_name) const {
+	const String capitalized_string = _capitalize_name(p_name);
+	if (EDITOR_GET("interface/editor/translate_properties")) {
+		return TTRGET(capitalized_string);
+	}
+	return capitalized_string;
+}
+
+String EditorPropertyNameProcessor::make_tooltip_for_name(const String &p_name) const {
+	const String capitalized_string = _capitalize_name(p_name);
+	if (EDITOR_GET("interface/editor/translate_properties")) {
+		return capitalized_string;
+	}
+	return TTRGET(capitalized_string);
+}
+
+EditorPropertyNameProcessor::EditorPropertyNameProcessor() {
+	ERR_FAIL_COND(singleton != nullptr);
+	singleton = this;
+
+	// The following initialization is parsed in `editor/translations/extract.py` with a regex.
+	// The map name and value definition format should be kept synced with the regex.
+	capitalize_string_remaps["2d"] = "2D";
+	capitalize_string_remaps["3d"] = "3D";
+	capitalize_string_remaps["Adb"] = "ADB";
+	capitalize_string_remaps["Bptc"] = "BPTC";
+	capitalize_string_remaps["Bvh"] = "BVH";
+	capitalize_string_remaps["Csg"] = "CSG";
+	capitalize_string_remaps["Cpu"] = "CPU";
+	capitalize_string_remaps["Db"] = "dB";
+	capitalize_string_remaps["Dof"] = "DoF";
+	capitalize_string_remaps["Dpi"] = "DPI";
+	capitalize_string_remaps["Etc"] = "ETC";
+	capitalize_string_remaps["Fbx"] = "FBX";
+	capitalize_string_remaps["Fps"] = "FPS";
+	capitalize_string_remaps["Fov"] = "FOV";
+	capitalize_string_remaps["Fs"] = "FS";
+	capitalize_string_remaps["Fxaa"] = "FXAA";
+	capitalize_string_remaps["Ggx"] = "GGX";
+	capitalize_string_remaps["Gdscript"] = "GDScript";
+	capitalize_string_remaps["Gles 2"] = "GLES2";
+	capitalize_string_remaps["Gles 3"] = "GLES3";
+	capitalize_string_remaps["Gi Probe"] = "GI Probe";
+	capitalize_string_remaps["Hdr"] = "HDR";
+	capitalize_string_remaps["Hidpi"] = "hiDPI";
+	capitalize_string_remaps["Ik"] = "IK";
+	capitalize_string_remaps["Ios"] = "iOS";
+	capitalize_string_remaps["Kb"] = "KB";
+	capitalize_string_remaps["Msaa"] = "MSAA";
+	capitalize_string_remaps["Macos"] = "macOS";
+	capitalize_string_remaps["Opentype"] = "OpenType";
+	capitalize_string_remaps["Png"] = "PNG";
+	capitalize_string_remaps["Pvs"] = "PVS";
+	capitalize_string_remaps["Pvrtc"] = "PVRTC";
+	capitalize_string_remaps["S 3 Tc"] = "S3TC";
+	capitalize_string_remaps["Sdfgi"] = "SDFGI";
+	capitalize_string_remaps["Srgb"] = "sRGB";
+	capitalize_string_remaps["Ssao"] = "SSAO";
+	capitalize_string_remaps["Ssl"] = "SSL";
+	capitalize_string_remaps["Ssh"] = "SSH";
+	capitalize_string_remaps["Sdk"] = "SDK";
+	capitalize_string_remaps["Tcp"] = "TCP";
+	capitalize_string_remaps["Uv 1"] = "UV1";
+	capitalize_string_remaps["Uv 2"] = "UV2";
+	capitalize_string_remaps["Vram"] = "VRAM";
+	capitalize_string_remaps["Vsync"] = "V-Sync";
+	capitalize_string_remaps["Vector 2"] = "Vector2";
+	capitalize_string_remaps["Webrtc"] = "WebRTC";
+	capitalize_string_remaps["Websocket"] = "WebSocket";
+}
+
+EditorPropertyNameProcessor::~EditorPropertyNameProcessor() {
+	singleton = nullptr;
+}

+ 58 - 0
editor/editor_property_name_processor.h

@@ -0,0 +1,58 @@
+/*************************************************************************/
+/*  editor_property_name_processor.h                                     */
+/*************************************************************************/
+/*                       This file is part of:                           */
+/*                           GODOT ENGINE                                */
+/*                      https://godotengine.org                          */
+/*************************************************************************/
+/* Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur.                 */
+/* Copyright (c) 2014-2022 Godot Engine contributors (cf. AUTHORS.md).   */
+/*                                                                       */
+/* 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.                */
+/*************************************************************************/
+
+#ifndef EDITOR_PROPERTY_NAME_PROCESSOR_H
+#define EDITOR_PROPERTY_NAME_PROCESSOR_H
+
+#include "scene/main/node.h"
+
+class EditorPropertyNameProcessor : public Node {
+	GDCLASS(EditorPropertyNameProcessor, Node);
+
+	static EditorPropertyNameProcessor *singleton;
+
+	Map<String, String> capitalize_string_remaps;
+
+	String _capitalize_name(const String &p_name) const;
+
+public:
+	static EditorPropertyNameProcessor *get_singleton() { return singleton; }
+
+	// Capitalize & localize property path segments.
+	String process_name(const String &p_name) const;
+
+	// Make tooltip string for names processed by process_name().
+	String make_tooltip_for_name(const String &p_name) const;
+
+	EditorPropertyNameProcessor();
+	~EditorPropertyNameProcessor();
+};
+
+#endif // EDITOR_PROPERTY_NAME_PROCESSOR_H

+ 5 - 1
editor/editor_sectioned_inspector.cpp

@@ -29,7 +29,10 @@
 /*************************************************************************/
 
 #include "editor_sectioned_inspector.h"
+
+#include "editor_property_name_processor.h"
 #include "editor_scale.h"
+
 class SectionedInspectorFilter : public Object {
 	GDCLASS(SectionedInspectorFilter, Object);
 
@@ -266,7 +269,8 @@ void SectionedInspector::update_category_list() {
 			if (!section_map.has(metasection)) {
 				TreeItem *ms = sections->create_item(parent);
 				section_map[metasection] = ms;
-				ms->set_text(0, sectionarr[i].capitalize());
+				ms->set_text(0, EditorPropertyNameProcessor::get_singleton()->process_name(sectionarr[i]));
+				ms->set_tooltip(0, EditorPropertyNameProcessor::get_singleton()->make_tooltip_for_name(sectionarr[i]));
 				ms->set_metadata(0, metasection);
 				ms->set_selectable(0, false);
 			}

+ 3 - 1
editor/settings_config_dialog.cpp

@@ -35,6 +35,7 @@
 #include "editor_file_system.h"
 #include "editor_log.h"
 #include "editor_node.h"
+#include "editor_property_name_processor.h"
 #include "editor_scale.h"
 #include "editor_settings.h"
 #include "scene/gui/margin_container.h"
@@ -217,8 +218,9 @@ void EditorSettingsDialog::_update_shortcuts() {
 		} else {
 			section = shortcuts->create_item(root);
 
-			String item_name = section_name.capitalize();
+			String item_name = EditorPropertyNameProcessor::get_singleton()->process_name(section_name);
 			section->set_text(0, item_name);
+			section->set_tooltip(0, EditorPropertyNameProcessor::get_singleton()->make_tooltip_for_name(section_name));
 
 			if (collapsed.has(item_name)) {
 				section->set_collapsed(collapsed[item_name]);

+ 76 - 42
editor/translations/extract.py

@@ -2,6 +2,7 @@
 
 import fnmatch
 import os
+import re
 import shutil
 import subprocess
 import sys
@@ -31,6 +32,15 @@ for root, dirnames, filenames in os.walk("."):
 matches.sort()
 
 
+remaps = {}
+remap_re = re.compile(r'capitalize_string_remaps\["(.+)"\] = "(.+)";')
+with open("editor/editor_property_name_processor.cpp") as f:
+    for line in f:
+        m = remap_re.search(line)
+        if m:
+            remaps[m.group(1)] = m.group(2)
+
+
 unique_str = []
 unique_loc = {}
 main_po = """
@@ -52,6 +62,36 @@ msgstr ""
 """
 
 
+message_patterns = {
+    re.compile(r'RTR\("(([^"\\]|\\.)*)"\)'): False,
+    re.compile(r'TTR\("(([^"\\]|\\.)*)"\)'): False,
+    re.compile(r'TTRC\("(([^"\\]|\\.)*)"\)'): False,
+    re.compile(r'_initial_set\("([^"]+?)",'): True,
+    re.compile(r'GLOBAL_DEF(?:_RST)?\("([^".]+?)",'): True,
+    re.compile(r'EDITOR_DEF(?:_RST)?\("([^"]+?)",'): True,
+    re.compile(r'ADD_PROPERTY\(PropertyInfo\(Variant::[A-Z]+,\s*"([^"]+?)",'): True,
+    re.compile(r'ADD_GROUP\("([^"]+?)",'): False,
+}
+
+
+# See String::camelcase_to_underscore().
+capitalize_re = re.compile(r"(?<=\D)(?=\d)|(?<=\d)(?=\D([a-z]|\d))")
+
+
+def _process_editor_string(name):
+    # See String::capitalize().
+    # fmt: off
+    capitalized = " ".join(
+        part.title()
+        for part in capitalize_re.sub("_", name).replace("_", " ").split()
+    )
+    # fmt: on
+    # See EditorStringProcessor::process_string().
+    for key, value in remaps.items():
+        capitalized = capitalized.replace(key, value)
+    return capitalized
+
+
 def _write_translator_comment(msg, translator_comment):
     if translator_comment == "":
         return
@@ -149,11 +189,6 @@ def _extract_translator_comment(line, is_block_translator_comment):
 
 
 def process_file(f, fname):
-
-    global main_po, unique_str, unique_loc
-
-    patterns = ['RTR("', 'TTR("', 'TTRC("']
-
     l = f.readline()
     lc = 1
     reading_translator_comment = False
@@ -176,49 +211,48 @@ def process_file(f, fname):
             if not reading_translator_comment:
                 translator_comment = translator_comment[:-1]  # Remove extra \n at the end.
 
-        idx = 0
-        pos = 0
-
-        while not reading_translator_comment and pos >= 0:
-            pos = l.find(patterns[idx], pos)
-            if pos == -1:
-                if idx < len(patterns) - 1:
-                    idx += 1
-                    pos = 0
-                continue
-            pos += len(patterns[idx])
-
-            msg = ""
-            while pos < len(l) and (l[pos] != '"' or l[pos - 1] == "\\"):
-                msg += l[pos]
-                pos += 1
-
-            location = os.path.relpath(fname).replace("\\", "/")
-            if line_nb:
-                location += ":" + str(lc)
-
-            # Write translator comment.
-            _write_translator_comment(msg, translator_comment)
-            translator_comment = ""
+        if not reading_translator_comment:
+            for pattern, is_property_path in message_patterns.items():
+                for m in pattern.finditer(l):
+                    location = os.path.relpath(fname).replace("\\", "/")
+                    if line_nb:
+                        location += ":" + str(lc)
+
+                    msg = m.group(1)
+
+                    if is_property_path:
+                        for part in msg.split("/"):
+                            _add_message(_process_editor_string(part), location, translator_comment)
+                    else:
+                        _add_message(msg, location, translator_comment)
 
-            if not msg in unique_str:
-                main_po += "#: " + location + "\n"
-                main_po += 'msgid "' + msg + '"\n'
-                main_po += 'msgstr ""\n\n'
-                unique_str.append(msg)
-                unique_loc[msg] = [location]
-            elif not location in unique_loc[msg]:
-                # Add additional location to previous occurrence too
-                msg_pos = main_po.find('\nmsgid "' + msg + '"')
-                if msg_pos == -1:
-                    print("Someone apparently thought writing Python was as easy as GDScript. Ping Akien.")
-                main_po = main_po[:msg_pos] + " " + location + main_po[msg_pos:]
-                unique_loc[msg].append(location)
+            translator_comment = ""
 
         l = f.readline()
         lc += 1
 
 
+def _add_message(msg, location, translator_comment):
+    global main_po, unique_str, unique_loc
+
+    # Write translator comment.
+    _write_translator_comment(msg, translator_comment)
+
+    if not msg in unique_str:
+        main_po += "#: " + location + "\n"
+        main_po += 'msgid "' + msg + '"\n'
+        main_po += 'msgstr ""\n\n'
+        unique_str.append(msg)
+        unique_loc[msg] = [location]
+    elif not location in unique_loc[msg]:
+        # Add additional location to previous occurrence too
+        msg_pos = main_po.find('\nmsgid "' + msg + '"')
+        if msg_pos == -1:
+            print("Someone apparently thought writing Python was as easy as GDScript. Ping Akien.")
+        main_po = main_po[:msg_pos] + " " + location + main_po[msg_pos:]
+        unique_loc[msg].append(location)
+
+
 print("Updating the editor.pot template...")
 
 for fname in matches: