diff options
Diffstat (limited to 'core')
-rw-r--r-- | core/compressed_translation.cpp | 8 | ||||
-rw-r--r-- | core/io/translation_loader_po.cpp | 5 | ||||
-rw-r--r-- | core/object.cpp | 5 | ||||
-rw-r--r-- | core/object.h | 3 | ||||
-rw-r--r-- | core/translation.cpp | 313 | ||||
-rw-r--r-- | core/translation.h | 45 | ||||
-rw-r--r-- | core/translation_po.cpp | 311 | ||||
-rw-r--r-- | core/translation_po.h | 92 | ||||
-rw-r--r-- | core/ustring.cpp | 9 | ||||
-rw-r--r-- | core/ustring.h | 2 |
10 files changed, 499 insertions, 294 deletions
diff --git a/core/compressed_translation.cpp b/core/compressed_translation.cpp index 7f0078ae2e..a92275565d 100644 --- a/core/compressed_translation.cpp +++ b/core/compressed_translation.cpp @@ -43,6 +43,8 @@ struct _PHashTranslationCmp { }; void PHashTranslation::generate(const Ref<Translation> &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<StringName> 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 1a70f9272d..d8ddb213c3 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> translation = Ref<Translation>(memnew(Translation)); + Ref<TranslationPO> translation = Ref<TranslationPO>(memnew(TranslationPO)); int line = 1; int plural_forms = 0; int plural_index = -1; diff --git a/core/object.cpp b/core/object.cpp index 9d38f36009..67c605c39b 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 b4e6fe0fa6..f9a12da8f6 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<String> *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 ea29b93319..8c8ca06740 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<StringName> 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<StringName, Vector<StringName>> &inner_map = translation_map[ctx]; - - List<StringName> 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<StringName> 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<StringName, Vector<StringName>> &id_str_map = translation_map[ctx]; - - Dictionary d2; - List<StringName> 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<StringName, StringName>::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<Variant> 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<StringName, Vector<StringName>> temp_map; - List<Variant> 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<String> 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<StringName> msgs; - get_message_list(&msgs); - - Vector<String> v; - for (auto E = msgs.front(); E; E = E->next()) { - v.push_back(E->get()); + Vector<String> msgs; + msgs.resize(translation_map.size()); + int idx = 0; + for (const Map<StringName, StringName>::Element *E = translation_map.front(); E; E = E->next()) { + msgs.set(idx, E->key()); + idx += 1; } - return v; -} - -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<Expression> expr; - expr.instance(); - - Vector<String> 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; + return msgs; } -int Translation::_get_plural_index(const String &p_plural_rule, const Vector<String> &p_input_name, const Array &p_input_value, Ref<Expression> &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; - } - - if (bool(result)) { - return p_plural_rule.substr(first_ques_mark + 1, p_plural_rule.find(":") - (first_ques_mark + 1)).to_int(); +void Translation::_set_messages(const Dictionary &p_messages) { + List<Variant> keys; + p_messages.get_key_list(&keys); + for (auto E = keys.front(); E; E = E->next()) { + translation_map[E->get()] = p_messages[E->get()]; } - - 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<StringName, Vector<StringName>> &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<String> &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<StringName, Vector<StringName>> &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<String> &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)) { - return StringName(); + 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"); } - 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]; -} -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)) { + const Map<StringName, StringName>::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 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 E->get(); +} - return translation_map[p_context][p_src_text][_get_plural_index(p_n)]; +StringName Translation::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const { + 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<StringName> *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<StringName> 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<StringName> 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<StringName, StringName>::Element *E = translation_map.front(); E; E = E->next()) { + r_messages->push_back(E->key()); } } int Translation::get_message_count() const { - List<StringName> 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<Translation> &p_translation translations.erase(p_translation); } +Ref<Translation> TranslationServer::get_translation_object(const String &p_locale) { + Ref<Translation> res; + String lang = get_language_code(p_locale); + bool near_match_found = false; + + for (const Set<Ref<Translation>>::Element *E = translations.front(); E; E = E->next()) { + const Ref<Translation> &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<Translation> &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 b4329c0ef7..cba25a434f 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; - - // 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<StringName> 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<StringName, HashMap<StringName, Vector<StringName>>> translation_map; - - Vector<String> _get_message_list() const; - - Dictionary _get_messages() const; - void _set_messages(const Dictionary &p_messages); + Map<StringName, StringName> translation_map; - int _get_plural_index(int p_n) const; - int _get_plural_index(const String &p_plural_rule, const Vector<String> &p_input_name, const Array &p_input_value, Ref<Expression> &r_expr) const; + virtual Vector<String> _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<String> &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<String> &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<StringName> *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<StringName> *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<Translation> 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 0000000000..eb363d623f --- /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<StringName> 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<StringName, Vector<StringName>> &inner_map = translation_map[ctx]; + + List<StringName> 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<StringName> 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<StringName, Vector<StringName>> &id_str_map = translation_map[ctx]; + + Dictionary d2; + List<StringName> 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<Variant> 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<StringName, Vector<StringName>> temp_map; + List<Variant> 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<String> TranslationPO::_get_message_list() const { + // Return all keys in translation_map. + + List<StringName> msgs; + get_message_list(&msgs); + + Vector<String> 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<StringName, Vector<StringName>> &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<String> &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<StringName, Vector<StringName>> &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<StringName> *r_messages) const { + // PHashTranslation uses this function to get the list of msgid. + // Return all the keys of translation_map under "" context. + + List<StringName> 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<StringName> 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<StringName> 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 0000000000..06d23d5247 --- /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<StringName> 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<StringName, HashMap<StringName, Vector<StringName>>> 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<String> equi_tests; + Vector<String> input_name; + mutable Ref<Expression> 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<String> _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<StringName> *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<String> &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 5a310c7f95..9d2d938eaf 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 a82a42c81f..7a1c1a5232 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 |