diff --git a/core/compressed_translation.cpp b/core/compressed_translation.cpp index 7f0078ae2ec..a92275565de 100644 --- a/core/compressed_translation.cpp +++ b/core/compressed_translation.cpp @@ -43,6 +43,8 @@ struct _PHashTranslationCmp { }; void PHashTranslation::generate(const Ref &p_from) { + // This method compresses a Translation instance. + // Right now it doesn't handle context or plurals, so Translation subclasses using plurals or context (i.e TranslationPO) shouldn't be compressed. #ifdef TOOLS_ENABLED List keys; p_from->get_message_list(&keys); @@ -213,9 +215,7 @@ bool PHashTranslation::_get(const StringName &p_name, Variant &r_ret) const { } StringName PHashTranslation::get_message(const StringName &p_src_text, const StringName &p_context) const { - if (String(p_context) != "") { - WARN_PRINT("The use of context is not yet supported in PHashTranslation."); - } + // p_context passed in is ignore. The use of context is not yet supported in PHashTranslation. int htsize = hash_table.size(); @@ -272,7 +272,7 @@ StringName PHashTranslation::get_message(const StringName &p_src_text, const Str } StringName PHashTranslation::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const { - WARN_PRINT("The use of plurals translation is not yet supported in PHashTranslation."); + // The use of plurals translation is not yet supported in PHashTranslation. return get_message(p_src_text, p_context); } diff --git a/core/io/translation_loader_po.cpp b/core/io/translation_loader_po.cpp index 1a70f9272de..d8ddb213c31 100644 --- a/core/io/translation_loader_po.cpp +++ b/core/io/translation_loader_po.cpp @@ -32,6 +32,7 @@ #include "core/os/file_access.h" #include "core/translation.h" +#include "core/translation_po.h" RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { enum Status { @@ -39,7 +40,7 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { STATUS_READING_ID, STATUS_READING_STRING, STATUS_READING_CONTEXT, - STATUS_READING_PLURAL + STATUS_READING_PLURAL, }; Status status = STATUS_NONE; @@ -54,7 +55,7 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) { *r_error = ERR_FILE_CORRUPT; } - Ref translation = Ref(memnew(Translation)); + Ref translation = Ref(memnew(TranslationPO)); int line = 1; int plural_forms = 0; int plural_index = -1; diff --git a/core/object.cpp b/core/object.cpp index 9d38f360091..67c605c39ba 100644 --- a/core/object.cpp +++ b/core/object.cpp @@ -1432,7 +1432,7 @@ void Object::initialize_class() { initialized = true; } -StringName Object::tr(const StringName &p_message, const StringName &p_context) const { +String Object::tr(const StringName &p_message, const StringName &p_context) const { if (!_can_translate || !TranslationServer::get_singleton()) { return p_message; } @@ -1444,9 +1444,8 @@ String Object::tr_n(const StringName &p_message, const StringName &p_message_plu // Return message based on English plural rule if translation is not possible. if (p_n == 1) { return p_message; - } else { - return p_message_plural; } + return p_message_plural; } return TranslationServer::get_singleton()->translate_plural(p_message, p_message_plural, p_n, p_context); } diff --git a/core/object.h b/core/object.h index b4e6fe0fa61..f9a12da8f62 100644 --- a/core/object.h +++ b/core/object.h @@ -719,8 +719,7 @@ public: virtual void get_argument_options(const StringName &p_function, int p_idx, List *r_options) const; - StringName tr(const StringName &p_message, const StringName &p_context = "") const; // translate message (internationalization) - ////I'm returning as String here because when I test the API, if I return StringName, I need to wrap it with String() to use format string, which is inconvenient. + String tr(const StringName &p_message, const StringName &p_context = "") const; // translate message (internationalization) String tr_n(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context = "") const; bool _is_queued_for_deletion = false; // set to true by SceneTree::queue_delete() diff --git a/core/translation.cpp b/core/translation.cpp index ea29b933196..8c8ca06740d 100644 --- a/core/translation.cpp +++ b/core/translation.cpp @@ -42,41 +42,6 @@ // - https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes // - https://lh.2xlibre.net/locales/ -#ifdef DEBUG_TRANSLATION -void Translation::print_translation_map() { - Error err; - FileAccess *file = FileAccess::open("translation_map_print_test.txt", FileAccess::WRITE, &err); - if (err != OK) { - ERR_PRINT("Failed to open translation_map_print_test.txt"); - return; - } - - file->store_line("NPlural : " + String::num_int64(this->get_plural_forms())); - file->store_line("Plural rule : " + this->get_plural_rule()); - file->store_line(""); - - List context_l; - translation_map.get_key_list(&context_l); - for (auto E = context_l.front(); E; E = E->next()) { - StringName ctx = E->get(); - file->store_line(" ===== Context: " + String::utf8(String(ctx).utf8()) + " ===== "); - const HashMap> &inner_map = translation_map[ctx]; - - List id_l; - inner_map.get_key_list(&id_l); - for (auto E2 = id_l.front(); E2; E2 = E2->next()) { - StringName id = E2->get(); - file->store_line("msgid: " + String::utf8(String(id).utf8())); - for (int i = 0; i < inner_map[id].size(); i++) { - file->store_line("msgstr[" + String::num_int64(i) + "]: " + String::utf8(String(inner_map[id][i]).utf8())); - } - file->store_line(""); - } - } - file->close(); -} -#endif - static const char *locale_list[] = { "aa", // Afar "aa_DJ", // Afar (Djibouti) @@ -830,113 +795,31 @@ static const char *locale_renames[][2] = { /////////////////////////////////////////////// Dictionary Translation::_get_messages() const { - // Return translation_map as a Dictionary. - Dictionary d; - - List context_l; - translation_map.get_key_list(&context_l); - for (auto E = context_l.front(); E; E = E->next()) { - StringName ctx = E->get(); - const HashMap> &id_str_map = translation_map[ctx]; - - Dictionary d2; - List id_l; - id_str_map.get_key_list(&id_l); - // Save list of id and strs associated with a context in a temporary dictionary. - for (auto E2 = id_l.front(); E2; E2 = E2->next()) { - StringName id = E2->get(); - d2[id] = id_str_map[id]; - } - - d[ctx] = d2; + for (const Map::Element *E = translation_map.front(); E; E = E->next()) { + d[E->key()] = E->value(); } - return d; } -void Translation::_set_messages(const Dictionary &p_messages) { - // Construct translation_map from a Dictionary. - - List context_l; - p_messages.get_key_list(&context_l); - for (auto E = context_l.front(); E; E = E->next()) { - StringName ctx = E->get(); - const Dictionary &id_str_map = p_messages[ctx]; - - HashMap> temp_map; - List id_l; - id_str_map.get_key_list(&id_l); - for (auto E2 = id_l.front(); E2; E2 = E2->next()) { - StringName id = E2->get(); - temp_map[id] = id_str_map[id]; - } - - translation_map[ctx] = temp_map; - } -} - Vector Translation::_get_message_list() const { - ////This one I'm really not sure what the use case of this function is. So I just follow what it does before. - // Return all keys in translation_map. - - List msgs; - get_message_list(&msgs); - - Vector v; - for (auto E = msgs.front(); E; E = E->next()) { - v.push_back(E->get()); + Vector msgs; + msgs.resize(translation_map.size()); + int idx = 0; + for (const Map::Element *E = translation_map.front(); E; E = E->next()) { + msgs.set(idx, E->key()); + idx += 1; } - return v; + return msgs; } -int Translation::_get_plural_index(int p_n) const { - // Apply plural rule to a p_n passed in, and get a number between [0;number of plural forms) - - Ref expr; - expr.instance(); - - Vector input_name; - input_name.push_back("n"); - - Array input_val; - input_val.push_back(p_n); - - int result = _get_plural_index(plural_rule, input_name, input_val, expr); - ERR_FAIL_COND_V_MSG(result < 0, 0, "_get_plural_index() returns a negative number after evaluating a plural rule expression."); - - return result; -} - -int Translation::_get_plural_index(const String &p_plural_rule, const Vector &p_input_name, const Array &p_input_value, Ref &r_expr) const { - // Evaluate recursively until we find the first condition that is true. - // Some examples of p_plural_rule passed in can have the form: - // "n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5" (Arabic) - // "n >= 2" (French) - // "n != 1" (English) - - // Parse expression. - int first_ques_mark = p_plural_rule.find("?"); - String equi_test = p_plural_rule.substr(0, first_ques_mark); - Error err = r_expr->parse(equi_test, p_input_name); - ERR_FAIL_COND_V_MSG(err != OK, p_input_value[0], "Cannot parse expression. Error: " + r_expr->get_error_text()); - - // Evaluate expression. - Variant result = r_expr->execute(p_input_value); - ERR_FAIL_COND_V_MSG(r_expr->has_execute_failed(), p_input_value[0], "Cannot evaluate expression."); - - // Base case of recursion. Variant result will either map to a bool or an integer, in both cases returning it will give the correct plural index. - if (first_ques_mark == -1) { - return result; +void Translation::_set_messages(const Dictionary &p_messages) { + List keys; + p_messages.get_key_list(&keys); + for (auto E = keys.front(); E; E = E->next()) { + translation_map[E->get()] = p_messages[E->get()]; } - - if (bool(result)) { - return p_plural_rule.substr(first_ques_mark + 1, p_plural_rule.find(":") - (first_ques_mark + 1)).to_int(); - } - - String after_colon = p_plural_rule.substr(p_plural_rule.find(":") + 1, p_plural_rule.length()); - return _get_plural_index(after_colon, p_input_name, p_input_value, r_expr); } void Translation::set_locale(const String &p_locale) { @@ -957,125 +840,50 @@ void Translation::set_locale(const String &p_locale) { } } -void Translation::set_plural_rule(const String &p_plural_rule) { - // Set plural_forms and plural_rule. - // p_plural_rule passed in has the form "Plural-Forms: nplurals=2; plural=(n >= 2);". - - int first_semi_col = p_plural_rule.find(";"); - plural_forms = p_plural_rule.substr(p_plural_rule.find("=") + 1, first_semi_col - (p_plural_rule.find("=") + 1)).to_int(); - - int expression_start = p_plural_rule.find("=", first_semi_col) + 1; - int second_semi_col = p_plural_rule.rfind(";"); - plural_rule = p_plural_rule.substr(expression_start, second_semi_col - expression_start); - // Strip away '(' and ')' to ease evaluating the expression later on. - plural_rule = plural_rule.replacen("(", ""); - plural_rule = plural_rule.replacen(")", ""); -} - void Translation::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) { - HashMap> &map_id_str = translation_map[p_context]; - - if (map_id_str.has(p_src_text)) { - WARN_PRINT("Double translations for \"" + String(p_src_text) + "\" under the same context \"" + String(p_context) + "\" for locale \"" + get_locale() + "\".\nThere should only be one unique translation for a given string under the same context."); - map_id_str[p_src_text].set(0, p_xlated_text); - } else { - map_id_str[p_src_text].push_back(p_xlated_text); - } + translation_map[p_src_text] = p_xlated_text; } -void Translation::add_plural_message(const StringName &p_src_text, const Vector &p_plural_texts, const StringName &p_context) { - ERR_FAIL_COND_MSG(p_plural_texts.size() != plural_forms, "Trying to add plural texts that don't match the required number of plural forms for locale \"" + get_locale() + "\""); - - HashMap> &map_id_str = translation_map[p_context]; - - if (map_id_str.has(p_src_text)) { - WARN_PRINT("Double translations for \"" + p_src_text + "\" under the same context \"" + p_context + "\" for locale " + get_locale() + ".\nThere should only be one unique translation for a given string under the same context."); - map_id_str[p_src_text].clear(); - } - - for (int i = 0; i < p_plural_texts.size(); i++) { - map_id_str[p_src_text].push_back(p_plural_texts[i]); - } -} - -int Translation::get_plural_forms() const { - return plural_forms; -} - -String Translation::get_plural_rule() const { - return plural_rule; +void Translation::add_plural_message(const StringName &p_src_text, const Vector &p_plural_xlated_texts, const StringName &p_context) { + WARN_PRINT("Translation class doesn't handle plural messages. Calling add_plural_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles plurals, such as TranslationPO class"); + ERR_FAIL_COND_MSG(p_plural_xlated_texts.empty(), "Parameter vector p_plural_xlated_texts passed in is empty."); + translation_map[p_src_text] = p_plural_xlated_texts[0]; } StringName Translation::get_message(const StringName &p_src_text, const StringName &p_context) const { - if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) { + if (p_context != StringName()) { + WARN_PRINT("Translation class doesn't handle context. Using context in get_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles context, such as TranslationPO class"); + } + + const Map::Element *E = translation_map.find(p_src_text); + if (!E) { return StringName(); } - ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].empty(), StringName(), "Source text \"" + String(p_src_text) + "\" is registered but doesn't have a translation. Please check add_message() or add_plural_message() to make sure a translation is always added."); - return translation_map[p_context][p_src_text][0]; + return E->get(); } StringName Translation::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const { - ERR_FAIL_COND_V_MSG(p_n < 0, p_src_text, "N passed into translation to get a plural message should not be negative. For negative numbers, use singular translation please. Search \"gettext PO Plural Forms\" online for the documentation on translating negative numbers."); - - if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) { - return StringName(); - } - ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].empty(), StringName(), "Source text \"" + String(p_src_text) + "\" is registered but doesn't have a translation. Please check add_message() or add_plural_message() to make sure a translation is always added."); - - // Return based on English plural rule if locale's plural rule is not registered (normally due to missing or invalid "Plural-Forms" in PO file header). - if (plural_forms <= 0) { - if (p_n == 1) { - return p_src_text; - } else { - return p_plural_text; - } - } - - return translation_map[p_context][p_src_text][_get_plural_index(p_n)]; + WARN_PRINT("Translation class doesn't handle plural messages. Calling get_plural_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles plurals, such as TranslationPO class"); + return get_message(p_src_text); } void Translation::erase_message(const StringName &p_src_text, const StringName &p_context) { - if (!translation_map.has(p_context)) { - return; + if (p_context != StringName()) { + WARN_PRINT("Translation class doesn't handle context. Using context in erase_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles context, such as TranslationPO class"); } - translation_map[p_context].erase(p_src_text); + translation_map.erase(p_src_text); } void Translation::get_message_list(List *r_messages) const { - ////This is the function that PHashTranslation uses to get the list of msgid. - ////Right now I just return the msgid list under "" context, and make no changes to PHashTranslation at all. - ////So PHashTranslation will be functioning like last time, it will not handle context and plurals translation. - - // Return all the keys of translation_map under "" context. - - List context_l; - translation_map.get_key_list(&context_l); - - for (auto E = context_l.front(); E; E = E->next()) { - if (String(E->get()) != "") { - continue; - } - - List msgid_l; - translation_map[E->get()].get_key_list(&msgid_l); - - for (auto E2 = msgid_l.front(); E2; E2 = E2->next()) { - r_messages->push_back(E2->get()); - } + for (const Map::Element *E = translation_map.front(); E; E = E->next()) { + r_messages->push_back(E->key()); } } int Translation::get_message_count() const { - List context_l; - translation_map.get_key_list(&context_l); - - int count = 0; - for (auto E = context_l.front(); E; E = E->next()) { - count += translation_map[E->get()].size(); - } - return count; + return translation_map.size(); } void Translation::_bind_methods() { @@ -1088,8 +896,6 @@ void Translation::_bind_methods() { ClassDB::bind_method(D_METHOD("erase_message", "src_message", "context"), &Translation::erase_message, DEFVAL("")); ClassDB::bind_method(D_METHOD("get_message_list"), &Translation::_get_message_list); ClassDB::bind_method(D_METHOD("get_message_count"), &Translation::get_message_count); - ClassDB::bind_method(D_METHOD("get_plural_forms"), &Translation::get_plural_forms); - ClassDB::bind_method(D_METHOD("get_plural_rule"), &Translation::get_plural_rule); ClassDB::bind_method(D_METHOD("_set_messages"), &Translation::_set_messages); ClassDB::bind_method(D_METHOD("_get_messages"), &Translation::_get_messages); @@ -1227,6 +1033,30 @@ void TranslationServer::remove_translation(const Ref &p_translation translations.erase(p_translation); } +Ref TranslationServer::get_translation_object(const String &p_locale) { + Ref res; + String lang = get_language_code(p_locale); + bool near_match_found = false; + + for (const Set>::Element *E = translations.front(); E; E = E->next()) { + const Ref &t = E->get(); + ERR_FAIL_COND_V(t.is_null(), nullptr); + String l = t->get_locale(); + + // Exact match. + if (l == p_locale) { + return t; + } + + // If near match found, keep that match, but keep looking to try to look for perfect match. + if (get_language_code(l) == lang && !near_match_found) { + res = t; + near_match_found = true; + } + } + return res; +} + void TranslationServer::clear() { translations.clear(); } @@ -1240,10 +1070,10 @@ StringName TranslationServer::translate(const StringName &p_message, const Strin ERR_FAIL_COND_V_MSG(locale.length() < 2, p_message, "Could not translate message as configured locale '" + locale + "' is invalid."); - StringName res = _get_message_from_translations(p_message, p_context, locale); + StringName res = _get_message_from_translations(p_message, p_context, locale, false); if (!res && fallback.length() >= 2) { - res = _get_message_from_translations(p_message, p_context, fallback); + res = _get_message_from_translations(p_message, p_context, fallback, false); } if (!res) { @@ -1257,31 +1087,29 @@ StringName TranslationServer::translate_plural(const StringName &p_message, cons if (!enabled) { if (p_n == 1) { return p_message; - } else { - return p_message_plural; } + return p_message_plural; } ERR_FAIL_COND_V_MSG(locale.length() < 2, p_message, "Could not translate message as configured locale '" + locale + "' is invalid."); - StringName res = _get_message_from_translations(p_message, p_context, locale, p_message_plural, p_n); + StringName res = _get_message_from_translations(p_message, p_context, locale, true, p_message_plural, p_n); if (!res && fallback.length() >= 2) { - res = _get_message_from_translations(p_message, p_context, fallback, p_message_plural, p_n); + res = _get_message_from_translations(p_message, p_context, fallback, true, p_message_plural, p_n); } if (!res) { if (p_n == 1) { return p_message; - } else { - return p_message_plural; } + return p_message_plural; } return res; } -StringName TranslationServer::_get_message_from_translations(const StringName &p_message, const StringName &p_context, const String &p_locale, const String &p_message_plural, int p_n) const { +StringName TranslationServer::_get_message_from_translations(const StringName &p_message, const StringName &p_context, const String &p_locale, bool plural, const String &p_message_plural, int p_n) const { // Locale can be of the form 'll_CC', i.e. language code and regional code, // e.g. 'en_US', 'en_GB', etc. It might also be simply 'll', e.g. 'en'. // To find the relevant translation, we look for those with locale starting @@ -1312,7 +1140,7 @@ StringName TranslationServer::_get_message_from_translations(const StringName &p } StringName r; - if (p_n == -1) { + if (!plural) { r = t->get_message(p_message, p_context); } else { r = t->get_plural_message(p_message, p_message_plural, p_n, p_context); @@ -1406,9 +1234,8 @@ StringName TranslationServer::tool_translate_plural(const StringName &p_message, if (p_n == 1) { return p_message; - } else { - return p_message_plural; } + return p_message_plural; } void TranslationServer::set_doc_translation(const Ref &p_translation) { @@ -1435,9 +1262,8 @@ StringName TranslationServer::doc_translate_plural(const StringName &p_message, if (p_n == 1) { return p_message; - } else { - return p_message_plural; } + return p_message_plural; } void TranslationServer::_bind_methods() { @@ -1446,11 +1272,12 @@ void TranslationServer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_locale_name", "locale"), &TranslationServer::get_locale_name); - ClassDB::bind_method(D_METHOD("translate", "message"), &TranslationServer::translate); + ClassDB::bind_method(D_METHOD("translate", "message", "context"), &TranslationServer::translate, DEFVAL("")); ClassDB::bind_method(D_METHOD("translate_plural", "message", "plural_message", "n", "context"), &TranslationServer::translate_plural, DEFVAL("")); ClassDB::bind_method(D_METHOD("add_translation", "translation"), &TranslationServer::add_translation); ClassDB::bind_method(D_METHOD("remove_translation", "translation"), &TranslationServer::remove_translation); + ClassDB::bind_method(D_METHOD("get_translation_object", "locale"), &TranslationServer::get_translation_object); ClassDB::bind_method(D_METHOD("clear"), &TranslationServer::clear); diff --git a/core/translation.h b/core/translation.h index b4329c0ef79..cba25a434f0 100644 --- a/core/translation.h +++ b/core/translation.h @@ -31,9 +31,6 @@ #ifndef TRANSLATION_H #define TRANSLATION_H -//#define DEBUG_TRANSLATION - -#include "core/math/expression.h" #include "core/resource.h" class Translation : public Resource { @@ -42,23 +39,11 @@ class Translation : public Resource { RES_BASE_EXTENSION("translation"); String locale = "en"; - int plural_forms = 0; // 0 means no "Plural-Forms" is given in the PO header file. The min for all languages is 1. - String plural_rule; + Map translation_map; - // TLDR: Maps context to a list of source strings and translated strings. In PO terms, maps msgctxt to a list of msgid and msgstr. - // The first key corresponds to context, and the second key (of the contained HashMap) corresponds to source string. - // The value Vector in the second map stores the translated strings. Index 0, 1, 2 matches msgstr[0], msgstr[1], msgstr[2]... in the case of plurals. - // Otherwise index 0 mathes to msgstr in a singular translation. - // Strings without context have "" as first key. - HashMap>> translation_map; - - Vector _get_message_list() const; - - Dictionary _get_messages() const; - void _set_messages(const Dictionary &p_messages); - - int _get_plural_index(int p_n) const; - int _get_plural_index(const String &p_plural_rule, const Vector &p_input_name, const Array &p_input_value, Ref &r_expr) const; + virtual Vector _get_message_list() const; + virtual Dictionary _get_messages() const; + virtual void _set_messages(const Dictionary &p_messages); protected: static void _bind_methods(); @@ -66,23 +51,14 @@ protected: public: void set_locale(const String &p_locale); _FORCE_INLINE_ String get_locale() const { return locale; } - void set_plural_rule(const String &p_plural_rule); - void add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context = ""); - void add_plural_message(const StringName &p_src_text, const Vector &p_plural_texts, const StringName &p_context = ""); + virtual void add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context = ""); + virtual void add_plural_message(const StringName &p_src_text, const Vector &p_plural_xlated_texts, const StringName &p_context = ""); virtual StringName get_message(const StringName &p_src_text, const StringName &p_context = "") const; //overridable for other implementations virtual StringName get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context = "") const; - void erase_message(const StringName &p_src_text, const StringName &p_context = ""); - - void get_message_list(List *r_messages) const; - int get_message_count() const; - - int get_plural_forms() const; - String get_plural_rule() const; - -#ifdef DEBUG_TRANSLATION - void print_translation_map(); -#endif + virtual void erase_message(const StringName &p_src_text, const StringName &p_context = ""); + virtual void get_message_list(List *r_messages) const; + virtual int get_message_count() const; Translation() {} }; @@ -104,7 +80,7 @@ class TranslationServer : public Object { static TranslationServer *singleton; bool _load_translations(const String &p_from); - StringName _get_message_from_translations(const StringName &p_message, const StringName &p_context, const String &p_locale, const String &p_message_plural = "", int p_n = -1) const; + StringName _get_message_from_translations(const StringName &p_message, const StringName &p_context, const String &p_locale, bool plural, const String &p_message_plural = "", int p_n = 0) const; static void _bind_methods(); @@ -116,6 +92,7 @@ public: void set_locale(const String &p_locale); String get_locale() const; + Ref get_translation_object(const String &p_locale); String get_locale_name(const String &p_locale) const; diff --git a/core/translation_po.cpp b/core/translation_po.cpp new file mode 100644 index 00000000000..eb363d623f9 --- /dev/null +++ b/core/translation_po.cpp @@ -0,0 +1,311 @@ +/*************************************************************************/ +/* translation_po.cpp */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 "translation_po.h" +#include "os/file_access.h" + +#ifdef DEBUG_TRANSLATION_PO +void TranslationPO::print_translation_map() { + Error err; + FileAccess *file = FileAccess::open("translation_map_print_test.txt", FileAccess::WRITE, &err); + if (err != OK) { + ERR_PRINT("Failed to open translation_map_print_test.txt"); + return; + } + + file->store_line("NPlural : " + String::num_int64(this->get_plural_forms())); + file->store_line("Plural rule : " + this->get_plural_rule()); + file->store_line(""); + + List context_l; + translation_map.get_key_list(&context_l); + for (auto E = context_l.front(); E; E = E->next()) { + StringName ctx = E->get(); + file->store_line(" ===== Context: " + String::utf8(String(ctx).utf8()) + " ===== "); + const HashMap> &inner_map = translation_map[ctx]; + + List id_l; + inner_map.get_key_list(&id_l); + for (auto E2 = id_l.front(); E2; E2 = E2->next()) { + StringName id = E2->get(); + file->store_line("msgid: " + String::utf8(String(id).utf8())); + for (int i = 0; i < inner_map[id].size(); i++) { + file->store_line("msgstr[" + String::num_int64(i) + "]: " + String::utf8(String(inner_map[id][i]).utf8())); + } + file->store_line(""); + } + } + file->close(); +} +#endif + +Dictionary TranslationPO::_get_messages() const { + // Return translation_map as a Dictionary. + + Dictionary d; + + List context_l; + translation_map.get_key_list(&context_l); + for (auto E = context_l.front(); E; E = E->next()) { + StringName ctx = E->get(); + const HashMap> &id_str_map = translation_map[ctx]; + + Dictionary d2; + List id_l; + id_str_map.get_key_list(&id_l); + // Save list of id and strs associated with a context in a temporary dictionary. + for (auto E2 = id_l.front(); E2; E2 = E2->next()) { + StringName id = E2->get(); + d2[id] = id_str_map[id]; + } + + d[ctx] = d2; + } + + return d; +} + +void TranslationPO::_set_messages(const Dictionary &p_messages) { + // Construct translation_map from a Dictionary. + + List context_l; + p_messages.get_key_list(&context_l); + for (auto E = context_l.front(); E; E = E->next()) { + StringName ctx = E->get(); + const Dictionary &id_str_map = p_messages[ctx]; + + HashMap> temp_map; + List id_l; + id_str_map.get_key_list(&id_l); + for (auto E2 = id_l.front(); E2; E2 = E2->next()) { + StringName id = E2->get(); + temp_map[id] = id_str_map[id]; + } + + translation_map[ctx] = temp_map; + } +} + +Vector TranslationPO::_get_message_list() const { + // Return all keys in translation_map. + + List msgs; + get_message_list(&msgs); + + Vector v; + for (auto E = msgs.front(); E; E = E->next()) { + v.push_back(E->get()); + } + + return v; +} + +int TranslationPO::_get_plural_index(int p_n) const { + // Get a number between [0;number of plural forms). + + input_val.clear(); + input_val.push_back(p_n); + + Variant result; + for (int i = 0; i < equi_tests.size(); i++) { + Error err = expr->parse(equi_tests[i], input_name); + ERR_FAIL_COND_V_MSG(err != OK, 0, "Cannot parse expression. Error: " + expr->get_error_text()); + + result = expr->execute(input_val); + ERR_FAIL_COND_V_MSG(expr->has_execute_failed(), 0, "Cannot evaluate expression."); + + // Last expression. Variant result will either map to a bool or an integer, in both cases returning it will give the correct plural index. + if (i + 1 == equi_tests.size()) { + return result; + } + + if (bool(result)) { + return i; + } + } + + ERR_FAIL_V_MSG(0, "Unexpected. Function should have returned. Please report this bug."); +} + +void TranslationPO::_cache_plural_tests(const String &p_plural_rule) { + // Some examples of p_plural_rule passed in can have the form: + // "n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5" (Arabic) + // "n >= 2" (French) // When evaluating the last, esp careful with this one. + // "n != 1" (English) + int first_ques_mark = p_plural_rule.find("?"); + if (first_ques_mark == -1) { + equi_tests.push_back(p_plural_rule.strip_edges()); + return; + } + + String equi_test = p_plural_rule.substr(0, first_ques_mark).strip_edges(); + equi_tests.push_back(equi_test); + + String after_colon = p_plural_rule.substr(p_plural_rule.find(":") + 1, p_plural_rule.length()); + _cache_plural_tests(after_colon); +} + +void TranslationPO::set_plural_rule(const String &p_plural_rule) { + // Set plural_forms and plural_rule. + // p_plural_rule passed in has the form "Plural-Forms: nplurals=2; plural=(n >= 2);". + + int first_semi_col = p_plural_rule.find(";"); + plural_forms = p_plural_rule.substr(p_plural_rule.find("=") + 1, first_semi_col - (p_plural_rule.find("=") + 1)).to_int(); + + int expression_start = p_plural_rule.find("=", first_semi_col) + 1; + int second_semi_col = p_plural_rule.rfind(";"); + plural_rule = p_plural_rule.substr(expression_start, second_semi_col - expression_start); + + // Setup the cache to make evaluating plural rule faster later on. + plural_rule = plural_rule.replacen("(", ""); + plural_rule = plural_rule.replacen(")", ""); + _cache_plural_tests(plural_rule); + expr.instance(); + input_name.push_back("n"); +} + +void TranslationPO::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) { + HashMap> &map_id_str = translation_map[p_context]; + + if (map_id_str.has(p_src_text)) { + WARN_PRINT("Double translations for \"" + String(p_src_text) + "\" under the same context \"" + String(p_context) + "\" for locale \"" + get_locale() + "\".\nThere should only be one unique translation for a given string under the same context."); + map_id_str[p_src_text].set(0, p_xlated_text); + } else { + map_id_str[p_src_text].push_back(p_xlated_text); + } +} + +void TranslationPO::add_plural_message(const StringName &p_src_text, const Vector &p_plural_xlated_texts, const StringName &p_context) { + ERR_FAIL_COND_MSG(p_plural_xlated_texts.size() != plural_forms, "Trying to add plural texts that don't match the required number of plural forms for locale \"" + get_locale() + "\""); + + HashMap> &map_id_str = translation_map[p_context]; + + if (map_id_str.has(p_src_text)) { + WARN_PRINT("Double translations for \"" + p_src_text + "\" under the same context \"" + p_context + "\" for locale " + get_locale() + ".\nThere should only be one unique translation for a given string under the same context."); + map_id_str[p_src_text].clear(); + } + + for (int i = 0; i < p_plural_xlated_texts.size(); i++) { + map_id_str[p_src_text].push_back(p_plural_xlated_texts[i]); + } +} + +int TranslationPO::get_plural_forms() const { + return plural_forms; +} + +String TranslationPO::get_plural_rule() const { + return plural_rule; +} + +StringName TranslationPO::get_message(const StringName &p_src_text, const StringName &p_context) const { + if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) { + return StringName(); + } + ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].empty(), StringName(), "Source text \"" + String(p_src_text) + "\" is registered but doesn't have a translation. Please report this bug."); + + return translation_map[p_context][p_src_text][0]; +} + +StringName TranslationPO::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const { + ERR_FAIL_COND_V_MSG(p_n < 0, StringName(), "N passed into translation to get a plural message should not be negative. For negative numbers, use singular translation please. Search \"gettext PO Plural Forms\" online for the documentation on translating negative numbers."); + + // If the query is the same as last time, return the cached result. + if (p_n == last_plural_n && p_context == last_plural_context && p_src_text == last_plural_key) { + return translation_map[p_context][p_src_text][last_plural_mapped_index]; + } + + if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) { + return StringName(); + } + ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].empty(), StringName(), "Source text \"" + String(p_src_text) + "\" is registered but doesn't have a translation. Please report this bug."); + + if (translation_map[p_context][p_src_text].size() == 1) { + WARN_PRINT("Source string \"" + String(p_src_text) + "\" doesn't have plural translations. Use singular translation API for such as tr(), TTR() to translate \"" + String(p_src_text) + "\""); + return translation_map[p_context][p_src_text][0]; + } + + int plural_index = _get_plural_index(p_n); + ERR_FAIL_COND_V_MSG(plural_index < 0 || translation_map[p_context][p_src_text].size() < plural_index + 1, StringName(), "Plural index returned or number of plural translations is not valid. Please report this bug."); + + // Cache result so that if the next entry is the same, we can return directly. + // _get_plural_index(p_n) can get very costly, especially when evaluating long plural-rule (Arabic) + last_plural_key = p_src_text; + last_plural_context = p_context; + last_plural_n = p_n; + last_plural_mapped_index = plural_index; + + return translation_map[p_context][p_src_text][plural_index]; +} + +void TranslationPO::erase_message(const StringName &p_src_text, const StringName &p_context) { + if (!translation_map.has(p_context)) { + return; + } + + translation_map[p_context].erase(p_src_text); +} + +void TranslationPO::get_message_list(List *r_messages) const { + // PHashTranslation uses this function to get the list of msgid. + // Return all the keys of translation_map under "" context. + + List context_l; + translation_map.get_key_list(&context_l); + + for (auto E = context_l.front(); E; E = E->next()) { + if (String(E->get()) != "") { + continue; + } + + List msgid_l; + translation_map[E->get()].get_key_list(&msgid_l); + + for (auto E2 = msgid_l.front(); E2; E2 = E2->next()) { + r_messages->push_back(E2->get()); + } + } +} + +int TranslationPO::get_message_count() const { + List context_l; + translation_map.get_key_list(&context_l); + + int count = 0; + for (auto E = context_l.front(); E; E = E->next()) { + count += translation_map[E->get()].size(); + } + return count; +} + +void TranslationPO::_bind_methods() { + ClassDB::bind_method(D_METHOD("get_plural_forms"), &TranslationPO::get_plural_forms); + ClassDB::bind_method(D_METHOD("get_plural_rule"), &TranslationPO::get_plural_rule); +} diff --git a/core/translation_po.h b/core/translation_po.h new file mode 100644 index 00000000000..06d23d52474 --- /dev/null +++ b/core/translation_po.h @@ -0,0 +1,92 @@ +/*************************************************************************/ +/* translation_po.h */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 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 TRANSLATION_PO_H +#define TRANSLATION_PO_H + +//#define DEBUG_TRANSLATION_PO + +#include "core/math/expression.h" +#include "translation.h" + +class TranslationPO : public Translation { + GDCLASS(TranslationPO, Translation); + + // TLDR: Maps context to a list of source strings and translated strings. In PO terms, maps msgctxt to a list of msgid and msgstr. + // The first key corresponds to context, and the second key (of the contained HashMap) corresponds to source string. + // The value Vector in the second map stores the translated strings. Index 0, 1, 2 matches msgstr[0], msgstr[1], msgstr[2]... in the case of plurals. + // Otherwise index 0 mathes to msgstr in a singular translation. + // Strings without context have "" as first key. + HashMap>> translation_map; + + int plural_forms = 0; // 0 means no "Plural-Forms" is given in the PO header file. The min for all languages is 1. + String plural_rule; + + // Cache temporary variables related to _get_plural_index() to make it faster + Vector equi_tests; + Vector input_name; + mutable Ref expr; + mutable Array input_val; + mutable StringName last_plural_key; + mutable StringName last_plural_context; + mutable int last_plural_n = -1; // Set it to an impossible value at the beginning. + mutable int last_plural_mapped_index = 0; + + void _cache_plural_tests(const String &p_plural_rule); + int _get_plural_index(int p_n) const; + + Vector _get_message_list() const override; + Dictionary _get_messages() const override; + void _set_messages(const Dictionary &p_messages) override; + +protected: + static void _bind_methods(); + +public: + void get_message_list(List *r_messages) const override; + int get_message_count() const override; + void add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context = "") override; + void add_plural_message(const StringName &p_src_text, const Vector &p_plural_xlated_texts, const StringName &p_context = "") override; + StringName get_message(const StringName &p_src_text, const StringName &p_context = "") const override; + StringName get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context = "") const override; + void erase_message(const StringName &p_src_text, const StringName &p_context = "") override; + + void set_plural_rule(const String &p_plural_rule); + int get_plural_forms() const; + String get_plural_rule() const; + +#ifdef DEBUG_TRANSLATION_PO + void print_translation_map(); +#endif + + TranslationPO() {} +}; + +#endif // TRANSLATION_PO_H diff --git a/core/ustring.cpp b/core/ustring.cpp index 5a310c7f954..9d2d938eafe 100644 --- a/core/ustring.cpp +++ b/core/ustring.cpp @@ -4285,9 +4285,8 @@ String TTRN(const String &p_text, const String &p_text_plural, int p_n, const St // Return message based on English plural rule if translation is not possible. if (p_n == 1) { return p_text; - } else { - return p_text_plural; } + return p_text_plural; } String DTR(const String &p_text, const String &p_context) { @@ -4312,9 +4311,8 @@ String DTRN(const String &p_text, const String &p_text_plural, int p_n, const St // Return message based on English plural rule if translation is not possible. if (p_n == 1) { return text; - } else { - return text_plural; } + return text_plural; } #endif @@ -4344,7 +4342,6 @@ String RTRN(const String &p_text, const String &p_text_plural, int p_n, const St // Return message based on English plural rule if translation is not possible. if (p_n == 1) { return p_text; - } else { - return p_text_plural; } + return p_text_plural; } diff --git a/core/ustring.h b/core/ustring.h index a82a42c81f5..7a1c1a5232f 100644 --- a/core/ustring.h +++ b/core/ustring.h @@ -421,7 +421,9 @@ String DTRN(const String &p_text, const String &p_text_plural, int p_n, const St #else #define TTR(m_value) (String()) +#define TTRN(m_value) (String()) #define DTR(m_value) (String()) +#define DTRN(m_value) (String()) #define TTRC(m_value) (m_value) #define TTRGET(m_value) (m_value) #endif diff --git a/doc/classes/EditorTranslationParserPlugin.xml b/doc/classes/EditorTranslationParserPlugin.xml index eaa678a25a7..f5204e7babf 100644 --- a/doc/classes/EditorTranslationParserPlugin.xml +++ b/doc/classes/EditorTranslationParserPlugin.xml @@ -6,6 +6,7 @@ Plugins are registered via [method EditorPlugin.add_translation_parser_plugin] method. To define the parsing and string extraction logic, override the [method parse_file] method in script. Add the extracted strings to argument [code]msgids[/code] or [code]msgids_context_plural[/code] if context or plural is used. + When adding to [code]msgids_context_plural[/code], you must add the data using the format [code]["A", "B", "C"][/code], where [code]A[/code] represents the extracted string, [code]B[/code] represents the context, and [code]C[/code] represents the plural version of the extracted string. If you want to add only context but not plural, put [code]""[/code] for the plural slot. The idea is the same if you only want to add plural but not context. See the code below for concrete examples. The extracted strings will be written into a POT file selected by user under "POT Generation" in "Localization" tab in "Project Settings" menu. Below shows an example of a custom parser that extracts strings from a CSV file to write into a POT. [codeblock] @@ -28,9 +29,12 @@ [/codeblock] To add a translatable string associated with context or plural, add it to [code]msgids_context_plural[/code]: [codeblock] - msgids_ctx_plural.append(["Test 1", "context", "test 1 plurals"]) # This will add a message with msgid "Test 1", msgctxt "context", and msgid_plural "test 1 plurals". - msgids_ctx_plural.append(["A test without context", "", "plurals"]) # This will add a message with msgid "A test without context" and msgid_plural "plurals". - msgids_ctx_plural.append(["Only with context", "a friendly context", ""]) # This will add a message with msgid "Only with context" and msgctxt "a friendly context". + # This will add a message with msgid "Test 1", msgctxt "context", and msgid_plural "test 1 plurals". + msgids_context_plural.append(["Test 1", "context", "test 1 plurals"]) + # This will add a message with msgid "A test without context" and msgid_plural "plurals". + msgids_context_plural.append(["A test without context", "", "plurals"]) + # This will add a message with msgid "Only with context" and msgctxt "a friendly context". + msgids_context_plural.append(["Only with context", "a friendly context", ""]) [/codeblock] [b]Note:[/b] If you override parsing logic for standard script types (GDScript, C#, etc.), it would be better to load the [code]path[/code] argument using [method ResourceLoader.load]. This is because built-in scripts are loaded as [Resource] type, not [File] type. For example: diff --git a/doc/classes/Object.xml b/doc/classes/Object.xml index ca6b624359d..aa6111df6c1 100644 --- a/doc/classes/Object.xml +++ b/doc/classes/Object.xml @@ -486,13 +486,35 @@ - + + + - Translates a message using translation catalogs configured in the Project Settings. + Translates a message using translation catalogs configured in the Project Settings. An additional context could be used to specify the translation context. Only works if message translation is enabled (which it is by default), otherwise it returns the [code]message[/code] unchanged. See [method set_message_translation]. + See https://docs.godotengine.org/en/latest/tutorials/i18n/internationalizing_games.html for examples of the usage of this method. + + + + + + + + + + + + + + + Translates a message involving plurals using translation catalogs configured in the Project Settings. An additional context could be used to specify the translation context. + Only works if message translation is enabled (which it is by default), otherwise it returns the [code]message[/code] or [code]plural_message[/code] unchanged. See [method set_message_translation]. + The number [code]n[/code] is the number or quantity of the plural object. It will be used to guide the translation system to fetch the correct plural form for the selected language. + [b]Note:[/b] Negative and floating-point values usually represent physical entities for which singular and plural don't clearly apply. In such cases, use [method tr]. + See https://docs.godotengine.org/en/latest/tutorials/i18n/internationalizing_games.html for examples of the usage of this method. diff --git a/doc/classes/Translation.xml b/doc/classes/Translation.xml index 11245195bfc..1989a63362a 100644 --- a/doc/classes/Translation.xml +++ b/doc/classes/Translation.xml @@ -18,8 +18,25 @@ + + Adds a message if nonexistent, followed by its translation. + An additional context could be used to specify the translation context or differentiate polysemic words. + + + + + + + + + + + + + Adds a message involving plural translation if nonexistent, followed by its translation. + An additional context could be used to specify the translation context or differentiate polysemic words. @@ -27,6 +44,8 @@ + + Erases a message. @@ -36,6 +55,8 @@ + + Returns a message's translation. @@ -54,6 +75,22 @@ Returns all the messages (keys). + + + + + + + + + + + + + Returns a message's translation involving plurals. + The number [code]n[/code] is the number or quantity of the plural object. It will be used to guide the translation system to fetch the correct plural form for the selected language. + + diff --git a/doc/classes/TranslationServer.xml b/doc/classes/TranslationServer.xml index aaf7a4d160d..3369663af66 100644 --- a/doc/classes/TranslationServer.xml +++ b/doc/classes/TranslationServer.xml @@ -50,6 +50,16 @@ Returns a locale's language and its variant (e.g. [code]"en_US"[/code] would return [code]"English (United States)"[/code]). + + + + + + + Returns the [Translation] instance based on the [code]locale[/code] passed in. + It will return a [code]nullptr[/code] if there is no [Translation] instance that matches the [code]locale[/code]. + + @@ -73,8 +83,26 @@ + + - Returns the current locale's translation for the given message (key). + Returns the current locale's translation for the given message (key) and context. + + + + + + + + + + + + + + + Returns the current locale's translation for the given message (key), plural_message and context. + The number [code]n[/code] is the number or quantity of the plural object. It will be used to guide the translation system to fetch the correct plural form for the selected language. diff --git a/editor/editor_translation_parser.cpp b/editor/editor_translation_parser.cpp index e6642927962..7a90d200003 100644 --- a/editor/editor_translation_parser.cpp +++ b/editor/editor_translation_parser.cpp @@ -54,7 +54,7 @@ Error EditorTranslationParserPlugin::parse_file(const String &p_path, Vector id_ctx_plural; id_ctx_plural.push_back(arr[0]); diff --git a/editor/scene_tree_editor.cpp b/editor/scene_tree_editor.cpp index ca173653a86..a62448169da 100644 --- a/editor/scene_tree_editor.cpp +++ b/editor/scene_tree_editor.cpp @@ -271,7 +271,7 @@ bool SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) { msg_temp += TTRN("Node is in one group.", "Node is in {num} groups.", num_groups).format(arr, "{num}"); } if (num_connections >= 1 || num_groups >= 1) { - msg_temp += TTR("\nClick to show signals dock."); + msg_temp += "\n" + TTR("Click to show signals dock."); } Ref icon_temp; diff --git a/editor/translations/extract.py b/editor/translations/extract.py index 42a078b3e30..5ca3d8c0ed3 100755 --- a/editor/translations/extract.py +++ b/editor/translations/extract.py @@ -78,8 +78,6 @@ def _add_additional_location(msgctx, msg, location): if msg_pos == -1: print("Someone apparently thought writing Python was as easy as GDScript. Ping Akien.") - # NOTE FOR MENTORS: When I tested on my computer (windows) I need the extra \n#: to make the locations print line by line. - # but it worked before without \n# so I will leave it like before main_po = main_po[:msg_pos] + " " + location + main_po[msg_pos:] @@ -171,7 +169,7 @@ print("Updating the editor.pot template...") for fname in matches: # NOTE FOR MENTORS: When I tested on windows I need to add encoding="utf8" at the end to be able to open the file. # maybe on Linux there's no need. - with open(fname, "r") as f: + with open(fname, "r", encoding="utf8") as f: process_file(f, fname) with open("editor.pot", "w") as f: