diff --git a/core/io/translation_loader_po.cpp b/core/io/translation_loader_po.cpp index 080c196d68b6..e9863934bb31 100644 --- a/core/io/translation_loader_po.cpp +++ b/core/io/translation_loader_po.cpp @@ -33,25 +33,33 @@ #include "core/os/file_access.h" #include "core/translation.h" -RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { +RES TranslationLoaderPO::load_translation(FileAccess *f, bool p_use_context, Error *r_error) { enum Status { STATUS_NONE, STATUS_READING_ID, STATUS_READING_STRING, + STATUS_READING_CONTEXT, }; Status status = STATUS_NONE; String msg_id; String msg_str; + String msg_context; String config; if (r_error) { *r_error = ERR_FILE_CORRUPT; } - Ref translation = Ref(memnew(Translation)); + Ref translation; + if (p_use_context) { + translation = Ref(memnew(ContextTranslation)); + } else { + translation.instance(); + } int line = 1; + bool entered_context = false; bool skip_this = false; bool skip_next = false; bool is_eof = false; @@ -63,14 +71,31 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { // If we reached last line and it's not a content line, break, otherwise let processing that last loop if (is_eof && l.empty()) { - if (status == STATUS_READING_ID) { + if (status == STATUS_READING_ID || status == STATUS_READING_CONTEXT) { memdelete(f); - ERR_FAIL_V_MSG(RES(), "Unexpected EOF while reading 'msgid' at: " + path + ":" + itos(line)); + ERR_FAIL_V_MSG(RES(), "Unexpected EOF while reading PO file at: " + path + ":" + itos(line)); } else { break; } } + if (l.begins_with("msgctxt")) { + if (status != STATUS_READING_STRING) { + memdelete(f); + ERR_FAIL_V_MSG(RES(), "Unexpected 'msgctxt', was expecting 'msgstr' before 'msgctxt' while parsing: " + path + ":" + itos(line)); + } + + // In PO file, "msgctxt" appears before "msgid". If we encounter a "msgctxt", we add what we have read + // and set "entered_context" to true to prevent adding twice. + if (!skip_this && msg_id != "") { + translation->add_context_message(msg_id, msg_str, msg_context); + } + msg_context = ""; + l = l.substr(7, l.length()).strip_edges(); + status = STATUS_READING_CONTEXT; + entered_context = true; + } + if (l.begins_with("msgid")) { if (status == STATUS_READING_ID) { memdelete(f); @@ -78,8 +103,8 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { } if (msg_id != "") { - if (!skip_this) { - translation->add_message(msg_id, msg_str); + if (!skip_this && !entered_context) { + translation->add_context_message(msg_id, msg_str, msg_context); } } else if (config == "") { config = msg_str; @@ -87,16 +112,21 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { l = l.substr(5, l.length()).strip_edges(); status = STATUS_READING_ID; + // If we did not encounter msgctxt, we reset context to empty to reset it. + if (!entered_context) { + msg_context = ""; + } msg_id = ""; msg_str = ""; skip_this = skip_next; skip_next = false; + entered_context = false; } if (l.begins_with("msgstr")) { if (status != STATUS_READING_ID) { memdelete(f); - ERR_FAIL_V_MSG(RES(), "Unexpected 'msgstr', was expecting 'msgid' while parsing: " + path + ":" + itos(line)); + ERR_FAIL_V_MSG(RES(), "Unexpected 'msgstr', was expecting 'msgid' before 'msgstr' while parsing: " + path + ":" + itos(line)); } l = l.substr(6, l.length()).strip_edges(); @@ -108,7 +138,7 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { skip_next = true; } line++; - continue; //nothing to read or comment + continue; // Nothing to read or comment. } if (!l.begins_with("\"") || status == STATUS_NONE) { @@ -146,8 +176,10 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { if (status == STATUS_READING_ID) { msg_id += l; - } else { + } else if (status == STATUS_READING_STRING) { msg_str += l; + } else if (status == STATUS_READING_CONTEXT) { + msg_context += l; } line++; @@ -155,10 +187,11 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { memdelete(f); + // Add the last set of data from last iteration. if (status == STATUS_READING_STRING) { if (msg_id != "") { if (!skip_this) { - translation->add_message(msg_id, msg_str); + translation->add_context_message(msg_id, msg_str, msg_context); } } else if (config == "") { config = msg_str; @@ -197,7 +230,7 @@ RES TranslationLoaderPO::load(const String &p_path, const String &p_original_pat FileAccess *f = FileAccess::open(p_path, FileAccess::READ); ERR_FAIL_COND_V_MSG(!f, RES(), "Cannot open file '" + p_path + "'."); - return load_translation(f, r_error); + return load_translation(f, false, r_error); } void TranslationLoaderPO::get_recognized_extensions(List *p_extensions) const { diff --git a/core/io/translation_loader_po.h b/core/io/translation_loader_po.h index af6cdd6e6025..812f256084b3 100644 --- a/core/io/translation_loader_po.h +++ b/core/io/translation_loader_po.h @@ -37,7 +37,7 @@ class TranslationLoaderPO : public ResourceFormatLoader { public: - static RES load_translation(FileAccess *f, Error *r_error = nullptr); + static RES load_translation(FileAccess *f, bool p_use_context, Error *r_error = nullptr); virtual RES load(const String &p_path, const String &p_original_path = "", Error *r_error = nullptr); virtual void get_recognized_extensions(List *p_extensions) const; virtual bool handles_type(const String &p_type) const; diff --git a/core/translation.cpp b/core/translation.cpp index 2d0cb230f286..16384c15fe79 100644 --- a/core/translation.cpp +++ b/core/translation.cpp @@ -870,9 +870,24 @@ void Translation::set_locale(const String &p_locale) { } } +void Translation::add_context_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) { + if (p_context != StringName()) { + WARN_PRINT("Translation class doesn't handle context."); + } + add_message(p_src_text, p_xlated_text); +} + +StringName Translation::get_context_message(const StringName &p_src_text, const StringName &p_context) const { + if (p_context != StringName()) { + WARN_PRINT("Translation class doesn't handle context."); + } + return get_message(p_src_text); +} + void Translation::add_message(const StringName &p_src_text, const StringName &p_xlated_text) { translation_map[p_src_text] = p_xlated_text; } + StringName Translation::get_message(const StringName &p_src_text) const { if (get_script_instance()) { return get_script_instance()->call("_get_message", p_src_text); @@ -923,6 +938,32 @@ Translation::Translation() : /////////////////////////////////////////////// +void ContextTranslation::add_context_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) { + if (p_context == StringName()) { + add_message(p_src_text, p_xlated_text); + } else { + context_translation_map[p_context][p_src_text] = p_xlated_text; + } +} + +StringName ContextTranslation::get_context_message(const StringName &p_src_text, const StringName &p_context) const { + if (p_context == StringName()) { + return get_message(p_src_text); + } + + const Map>::Element *context = context_translation_map.find(p_context); + if (!context) { + return StringName(); + } + const Map::Element *message = context->get().find(p_src_text); + if (!message) { + return StringName(); + } + return message->get(); +} + +/////////////////////////////////////////////// + bool TranslationServer::is_locale_valid(const String &p_locale) { const char **ptr = locale_list; @@ -1202,9 +1243,9 @@ void TranslationServer::set_tool_translation(const Ref &p_translati tool_translation = p_translation; } -StringName TranslationServer::tool_translate(const StringName &p_message) const { +StringName TranslationServer::tool_translate(const StringName &p_message, const StringName &p_context) const { if (tool_translation.is_valid()) { - StringName r = tool_translation->get_message(p_message); + StringName r = tool_translation->get_context_message(p_message, p_context); if (r) { return r; } diff --git a/core/translation.h b/core/translation.h index 528bd38e4fa2..6bd98e1e23d1 100644 --- a/core/translation.h +++ b/core/translation.h @@ -60,9 +60,23 @@ class Translation : public Resource { void get_message_list(List *r_messages) const; int get_message_count() const; + // Not exposed to scripting. For easy usage of `ContextTranslation`. + virtual void add_context_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context); + virtual StringName get_context_message(const StringName &p_src_text, const StringName &p_context) const; + Translation(); }; +class ContextTranslation : public Translation { + GDCLASS(ContextTranslation, Translation); + + Map> context_translation_map; + +public: + virtual void add_context_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context); + virtual StringName get_context_message(const StringName &p_src_text, const StringName &p_context) const; +}; + class TranslationServer : public Object { GDCLASS(TranslationServer, Object); @@ -107,7 +121,7 @@ class TranslationServer : public Object { static String get_language_code(const String &p_locale); void set_tool_translation(const Ref &p_translation); - StringName tool_translate(const StringName &p_message) const; + StringName tool_translate(const StringName &p_message, const StringName &p_context) const; void set_doc_translation(const Ref &p_translation); StringName doc_translate(const StringName &p_message) const; diff --git a/core/ustring.cpp b/core/ustring.cpp index 2bcf8dd54ec8..62721fdd88b5 100644 --- a/core/ustring.cpp +++ b/core/ustring.cpp @@ -4493,9 +4493,9 @@ String String::unquote() const { } #ifdef TOOLS_ENABLED -String TTR(const String &p_text) { +String TTR(const String &p_text, const String &p_context) { if (TranslationServer::get_singleton()) { - return TranslationServer::get_singleton()->tool_translate(p_text); + return TranslationServer::get_singleton()->tool_translate(p_text, p_context); } return p_text; @@ -4519,7 +4519,7 @@ String DTR(const String &p_text) { String RTR(const String &p_text) { if (TranslationServer::get_singleton()) { - String rtr = TranslationServer::get_singleton()->tool_translate(p_text); + String rtr = TranslationServer::get_singleton()->tool_translate(p_text, StringName()); if (rtr == String() || rtr == p_text) { return TranslationServer::get_singleton()->translate(p_text); } else { diff --git a/core/ustring.h b/core/ustring.h index 286bf8d7de64..a10c011e1d99 100644 --- a/core/ustring.h +++ b/core/ustring.h @@ -423,7 +423,7 @@ _FORCE_INLINE_ bool is_str_less(const L *l_ptr, const R *r_ptr) { // and doc translate for the class reference (DTR). #ifdef TOOLS_ENABLED // Gets parsed. -String TTR(const String &); +String TTR(const String &p_text, const String &p_context = ""); String DTR(const String &); // Use for C strings. #define TTRC(m_value) (m_value) diff --git a/editor/editor_translation.cpp b/editor/editor_translation.cpp index f64adcf0a1cc..5aba8324befd 100644 --- a/editor/editor_translation.cpp +++ b/editor/editor_translation.cpp @@ -62,7 +62,7 @@ void load_editor_translations(const String &p_locale) { FileAccessMemory *fa = memnew(FileAccessMemory); fa->open_custom(data.ptr(), data.size()); - Ref tr = TranslationLoaderPO::load_translation(fa); + Ref tr = TranslationLoaderPO::load_translation(fa, true); if (tr.is_valid()) { tr->set_locale(etl->lang); @@ -87,7 +87,7 @@ void load_doc_translations(const String &p_locale) { FileAccessMemory *fa = memnew(FileAccessMemory); fa->open_custom(data.ptr(), data.size()); - Ref tr = TranslationLoaderPO::load_translation(fa); + Ref tr = TranslationLoaderPO::load_translation(fa, false); if (tr.is_valid()) { tr->set_locale(dtl->lang); diff --git a/editor/translations/extract.py b/editor/translations/extract.py index ac94fbcb3a44..15573e602644 100755 --- a/editor/translations/extract.py +++ b/editor/translations/extract.py @@ -43,6 +43,7 @@ unique_str = [] unique_loc = {} +ctx_group = {} # Store msgctx, msg, and locations. main_po = """ # LANGUAGE translation of the Godot Engine editor. # Copyright (c) 2007-2022 Juan Linietsky, Ariel Manzur. @@ -62,15 +63,16 @@ """ +# Regex "(?P(?:[^"\\]|\\.)*)" creates a group named `name` that matches a string. 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, + re.compile(r'RTR\("(?P(?:[^"\\]|\\.)*)"\)'): False, + re.compile(r'TTR\("(?P(?:[^"\\]|\\.)*)"(?:, "(?P(?:[^"\\]|\\.)*)")?\)'): False, + re.compile(r'TTRC\("(?P(?:[^"\\]|\\.)*)"\)'): False, + re.compile(r'_initial_set\("(?P[^"]+?)",'): True, + re.compile(r'GLOBAL_DEF(?:_RST)?\("(?P[^".]+?)",'): True, + re.compile(r'EDITOR_DEF(?:_RST)?\("(?P[^"]+?)",'): True, + re.compile(r'ADD_PROPERTY\(PropertyInfo\(Variant::[A-Z]+,\s*"(?P[^"]+?)",'): True, + re.compile(r'ADD_GROUP\("(?P[^"]+?)",'): False, } @@ -92,12 +94,37 @@ def _process_editor_string(name): return capitalized -def _write_translator_comment(msg, translator_comment): +def _write_message(msgctx, msg, location): + global main_po + main_po += "#: " + location + "\n" + if msgctx != "": + main_po += 'msgctxt "' + msgctx + '"\n' + main_po += 'msgid "' + msg + '"\n' + main_po += 'msgstr ""\n\n' + + +def _add_additional_location(msgctx, msg, location): + global main_po + # Add additional location to previous occurrence. + if msgctx != "": + msg_pos = main_po.find('\nmsgctxt "' + msgctx + '"\nmsgid "' + msg + '"') + else: + 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:] + + +def _write_translator_comment(msgctx, msg, translator_comment): if translator_comment == "": return global main_po - msg_pos = main_po.find('\nmsgid "' + msg + '"') + if msgctx != "": + msg_pos = main_po.find('\nmsgctxt "' + msgctx + '"\nmsgid "' + msg + '"') + else: + msg_pos = main_po.find('\nmsgid "' + msg + '"') # If it's a new message, just append comment to the end of PO file. if msg_pos == -1: @@ -218,39 +245,48 @@ def process_file(f, fname): if line_nb: location += ":" + str(lc) - msg = m.group(1) + groups = m.groupdict("") + msg = groups.get("message", "") + msgctx = groups.get("context", "") if is_property_path: for part in msg.split("/"): - _add_message(_process_editor_string(part), location, translator_comment) + _add_message(_process_editor_string(part), msgctx, location, translator_comment) else: - _add_message(msg, location, translator_comment) - + _add_message(msg, msgctx, location, translator_comment) translator_comment = "" l = f.readline() lc += 1 -def _add_message(msg, location, translator_comment): +def _add_message(msg, msgctx, 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) + _write_translator_comment(msgctx, msg, translator_comment) + translator_comment = "" + + if msgctx != "": + # If it's a new context or a new message within an existing context, then write new msgid. + # Else add location to existing msgid. + if not msgctx in ctx_group: + _write_message(msgctx, msg, location) + ctx_group[msgctx] = {msg: [location]} + elif not msg in ctx_group[msgctx]: + _write_message(msgctx, msg, location) + ctx_group[msgctx][msg] = [location] + elif not location in ctx_group[msgctx][msg]: + _add_additional_location(msgctx, msg, location) + ctx_group[msgctx][msg].append(location) + else: + if not msg in unique_str: + _write_message(msgctx, msg, location) + unique_str.append(msg) + unique_loc[msg] = [location] + elif not location in unique_loc[msg]: + _add_additional_location(msgctx, msg, location) + unique_loc[msg].append(location) print("Updating the editor.pot template...")