Browse Source

Added plurals and context support to Translation

SkyJJ 5 years ago
parent
commit
c0d837a2ea

+ 10 - 1
core/compressed_translation.cpp

@@ -212,7 +212,11 @@ bool PHashTranslation::_get(const StringName &p_name, Variant &r_ret) const {
 	return true;
 	return true;
 }
 }
 
 
-StringName PHashTranslation::get_message(const StringName &p_src_text) 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.");
+	}
+
 	int htsize = hash_table.size();
 	int htsize = hash_table.size();
 
 
 	if (htsize == 0) {
 	if (htsize == 0) {
@@ -267,6 +271,11 @@ StringName PHashTranslation::get_message(const StringName &p_src_text) const {
 	}
 	}
 }
 }
 
 
+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.");
+	return get_message(p_src_text, p_context);
+}
+
 void PHashTranslation::_get_property_list(List<PropertyInfo> *p_list) const {
 void PHashTranslation::_get_property_list(List<PropertyInfo> *p_list) const {
 	p_list->push_back(PropertyInfo(Variant::PACKED_INT32_ARRAY, "hash_table"));
 	p_list->push_back(PropertyInfo(Variant::PACKED_INT32_ARRAY, "hash_table"));
 	p_list->push_back(PropertyInfo(Variant::PACKED_INT32_ARRAY, "bucket_table"));
 	p_list->push_back(PropertyInfo(Variant::PACKED_INT32_ARRAY, "bucket_table"));

+ 2 - 1
core/compressed_translation.h

@@ -79,7 +79,8 @@ protected:
 	static void _bind_methods();
 	static void _bind_methods();
 
 
 public:
 public:
-	virtual StringName get_message(const StringName &p_src_text) const override; //overridable for other implementations
+	virtual StringName get_message(const StringName &p_src_text, const StringName &p_context = "") const override; //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 override;
 	void generate(const Ref<Translation> &p_from);
 	void generate(const Ref<Translation> &p_from);
 
 
 	PHashTranslation() {}
 	PHashTranslation() {}

+ 97 - 10
core/io/translation_loader_po.cpp

@@ -38,12 +38,16 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) {
 		STATUS_NONE,
 		STATUS_NONE,
 		STATUS_READING_ID,
 		STATUS_READING_ID,
 		STATUS_READING_STRING,
 		STATUS_READING_STRING,
+		STATUS_READING_CONTEXT,
+		STATUS_READING_PLURAL
 	};
 	};
 
 
 	Status status = STATUS_NONE;
 	Status status = STATUS_NONE;
 
 
 	String msg_id;
 	String msg_id;
 	String msg_str;
 	String msg_str;
+	String msg_context;
+	Vector<String> msgs_plural;
 	String config;
 	String config;
 
 
 	if (r_error) {
 	if (r_error) {
@@ -52,6 +56,9 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) {
 
 
 	Ref<Translation> translation = Ref<Translation>(memnew(Translation));
 	Ref<Translation> translation = Ref<Translation>(memnew(Translation));
 	int line = 1;
 	int line = 1;
+	int plural_forms = 0;
+	int plural_index = -1;
+	bool entered_context = false;
 	bool skip_this = false;
 	bool skip_this = false;
 	bool skip_next = false;
 	bool skip_next = false;
 	bool is_eof = false;
 	bool is_eof = false;
@@ -63,40 +70,107 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) {
 
 
 		// If we reached last line and it's not a content line, break, otherwise let processing that last loop
 		// If we reached last line and it's not a content line, break, otherwise let processing that last loop
 		if (is_eof && l.empty()) {
 		if (is_eof && l.empty()) {
-			if (status == STATUS_READING_ID) {
+			if (status == STATUS_READING_ID || status == STATUS_READING_CONTEXT || (status == STATUS_READING_PLURAL && plural_index != plural_forms - 1)) {
 				memdelete(f);
 				memdelete(f);
-				ERR_FAIL_V_MSG(RES(), "Unexpected EOF while reading 'msgid' at: " + path + ":" + itos(line));
+				ERR_FAIL_V_MSG(RES(), "Unexpected EOF while reading PO file at: " + path + ":" + itos(line));
 			} else {
 			} else {
 				break;
 				break;
 			}
 			}
 		}
 		}
 
 
-		if (l.begins_with("msgid")) {
+		if (l.begins_with("msgctxt")) {
+			if (status != STATUS_READING_STRING && status != STATUS_READING_PLURAL) {
+				memdelete(f);
+				ERR_FAIL_V_MSG(RES(), "Unexpected 'msgctxt', was expecting 'msgid_plural' or 'msgstr' before 'msgctxt' while parsing: " + path + ":" + itos(line));
+			}
+
+			// In PO file, "msgctxt" appears before "msgid". If we encounter a "msgctxt", we add what we have read
+			// and set "entered_context" to true to prevent adding twice.
+			if (!skip_this && msg_id != "") {
+				if (status == STATUS_READING_STRING) {
+					translation->add_message(msg_id, msg_str, msg_context);
+				} else if (status == STATUS_READING_PLURAL) {
+					if (plural_index != plural_forms - 1) {
+						memdelete(f);
+						ERR_FAIL_V_MSG(RES(), "Number of 'msgstr[]' doesn't match with number of plural forms: " + path + ":" + itos(line));
+					}
+					translation->add_plural_message(msg_id, msgs_plural, msg_context);
+				}
+			}
+			msg_context = "";
+			l = l.substr(7, l.length()).strip_edges();
+			status = STATUS_READING_CONTEXT;
+			entered_context = true;
+		}
+
+		if (l.begins_with("msgid_plural")) {
+			if (plural_forms == 0) {
+				memdelete(f);
+				ERR_FAIL_V_MSG(RES(), "PO file uses 'msgid_plural' but 'Plural-Forms' is invalid or missing in header: " + path + ":" + itos(line));
+			} else if (status != STATUS_READING_ID) {
+				memdelete(f);
+				ERR_FAIL_V_MSG(RES(), "Unexpected 'msgid_plural', was expecting 'msgid' before 'msgid_plural' while parsing: " + path + ":" + itos(line));
+			}
+			// We don't record the message in "msgid_plural" itself as tr_n(), TTRN(), RTRN() interfaces provide the plural string already.
+			// We just have to reset variables related to plurals for "msgstr[]" later on.
+			l = l.substr(12, l.length()).strip_edges();
+			plural_index = -1;
+			msgs_plural.clear();
+			msgs_plural.resize(plural_forms);
+			status = STATUS_READING_PLURAL;
+		} else if (l.begins_with("msgid")) {
 			if (status == STATUS_READING_ID) {
 			if (status == STATUS_READING_ID) {
 				memdelete(f);
 				memdelete(f);
 				ERR_FAIL_V_MSG(RES(), "Unexpected 'msgid', was expecting 'msgstr' while parsing: " + path + ":" + itos(line));
 				ERR_FAIL_V_MSG(RES(), "Unexpected 'msgid', was expecting 'msgstr' while parsing: " + path + ":" + itos(line));
 			}
 			}
 
 
 			if (msg_id != "") {
 			if (msg_id != "") {
-				if (!skip_this) {
-					translation->add_message(msg_id, msg_str);
+				if (!skip_this && !entered_context) {
+					if (status == STATUS_READING_STRING) {
+						translation->add_message(msg_id, msg_str, msg_context);
+					} else if (status == STATUS_READING_PLURAL) {
+						if (plural_index != plural_forms - 1) {
+							memdelete(f);
+							ERR_FAIL_V_MSG(RES(), "Number of 'msgstr[]' doesn't match with number of plural forms: " + path + ":" + itos(line));
+						}
+						translation->add_plural_message(msg_id, msgs_plural, msg_context);
+					}
 				}
 				}
 			} else if (config == "") {
 			} else if (config == "") {
 				config = msg_str;
 				config = msg_str;
+				// Record plural rule.
+				int p_start = config.find("Plural-Forms");
+				if (p_start != -1) {
+					int p_end = config.find("\n", p_start);
+					translation->set_plural_rule(config.substr(p_start, p_end - p_start));
+					plural_forms = translation->get_plural_forms();
+				}
 			}
 			}
 
 
 			l = l.substr(5, l.length()).strip_edges();
 			l = l.substr(5, l.length()).strip_edges();
 			status = STATUS_READING_ID;
 			status = STATUS_READING_ID;
+			// If we did not encounter msgctxt, we reset context to empty to reset it.
+			if (!entered_context) {
+				msg_context = "";
+			}
 			msg_id = "";
 			msg_id = "";
 			msg_str = "";
 			msg_str = "";
 			skip_this = skip_next;
 			skip_this = skip_next;
 			skip_next = false;
 			skip_next = false;
+			entered_context = false;
 		}
 		}
 
 
-		if (l.begins_with("msgstr")) {
+		if (l.begins_with("msgstr[")) {
+			if (status != STATUS_READING_PLURAL) {
+				memdelete(f);
+				ERR_FAIL_V_MSG(RES(), "Unexpected 'msgstr[]', was expecting 'msgid_plural' before 'msgstr[]' while parsing: " + path + ":" + itos(line));
+			}
+			plural_index++; // Increment to add to the next slot in vector msgs_plural.
+			l = l.substr(9, l.length()).strip_edges();
+		} else if (l.begins_with("msgstr")) {
 			if (status != STATUS_READING_ID) {
 			if (status != STATUS_READING_ID) {
 				memdelete(f);
 				memdelete(f);
-				ERR_FAIL_V_MSG(RES(), "Unexpected 'msgstr', was expecting 'msgid' while parsing: " + path + ":" + itos(line));
+				ERR_FAIL_V_MSG(RES(), "Unexpected 'msgstr', was expecting 'msgid' before 'msgstr' while parsing: " + path + ":" + itos(line));
 			}
 			}
 
 
 			l = l.substr(6, l.length()).strip_edges();
 			l = l.substr(6, l.length()).strip_edges();
@@ -108,7 +182,7 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) {
 				skip_next = true;
 				skip_next = true;
 			}
 			}
 			line++;
 			line++;
-			continue; //nothing to read or comment
+			continue; // Nothing to read or comment.
 		}
 		}
 
 
 		if (!l.begins_with("\"") || status == STATUS_NONE) {
 		if (!l.begins_with("\"") || status == STATUS_NONE) {
@@ -146,8 +220,12 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) {
 
 
 		if (status == STATUS_READING_ID) {
 		if (status == STATUS_READING_ID) {
 			msg_id += l;
 			msg_id += l;
-		} else {
+		} else if (status == STATUS_READING_STRING) {
 			msg_str += l;
 			msg_str += l;
+		} else if (status == STATUS_READING_CONTEXT) {
+			msg_context += l;
+		} else if (status == STATUS_READING_PLURAL && plural_index >= 0) {
+			msgs_plural.write[plural_index] = msgs_plural[plural_index] + l;
 		}
 		}
 
 
 		line++;
 		line++;
@@ -155,14 +233,23 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) {
 
 
 	memdelete(f);
 	memdelete(f);
 
 
+	// Add the last set of data from last iteration.
 	if (status == STATUS_READING_STRING) {
 	if (status == STATUS_READING_STRING) {
 		if (msg_id != "") {
 		if (msg_id != "") {
 			if (!skip_this) {
 			if (!skip_this) {
-				translation->add_message(msg_id, msg_str);
+				translation->add_message(msg_id, msg_str, msg_context);
 			}
 			}
 		} else if (config == "") {
 		} else if (config == "") {
 			config = msg_str;
 			config = msg_str;
 		}
 		}
+	} else if (status == STATUS_READING_PLURAL) {
+		if (!skip_this && msg_id != "") {
+			if (plural_index != plural_forms - 1) {
+				memdelete(f);
+				ERR_FAIL_V_MSG(RES(), "Number of 'msgstr[]' doesn't match with number of plural forms: " + path + ":" + itos(line));
+			}
+			translation->add_plural_message(msg_id, msgs_plural, msg_context);
+		}
 	}
 	}
 
 
 	ERR_FAIL_COND_V_MSG(config == "", RES(), "No config found in file: " + path + ".");
 	ERR_FAIL_COND_V_MSG(config == "", RES(), "No config found in file: " + path + ".");

+ 15 - 3
core/object.cpp

@@ -1432,12 +1432,23 @@ void Object::initialize_class() {
 	initialized = true;
 	initialized = true;
 }
 }
 
 
-StringName Object::tr(const StringName &p_message) const {
+StringName Object::tr(const StringName &p_message, const StringName &p_context) const {
 	if (!_can_translate || !TranslationServer::get_singleton()) {
 	if (!_can_translate || !TranslationServer::get_singleton()) {
 		return p_message;
 		return p_message;
 	}
 	}
+	return TranslationServer::get_singleton()->translate(p_message, p_context);
+}
 
 
-	return TranslationServer::get_singleton()->translate(p_message);
+String Object::tr_n(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const {
+	if (!_can_translate || !TranslationServer::get_singleton()) {
+		// 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 TranslationServer::get_singleton()->translate_plural(p_message, p_message_plural, p_n, p_context);
 }
 }
 
 
 void Object::_clear_internal_resource_paths(const Variant &p_var) {
 void Object::_clear_internal_resource_paths(const Variant &p_var) {
@@ -1578,7 +1589,8 @@ void Object::_bind_methods() {
 
 
 	ClassDB::bind_method(D_METHOD("set_message_translation", "enable"), &Object::set_message_translation);
 	ClassDB::bind_method(D_METHOD("set_message_translation", "enable"), &Object::set_message_translation);
 	ClassDB::bind_method(D_METHOD("can_translate_messages"), &Object::can_translate_messages);
 	ClassDB::bind_method(D_METHOD("can_translate_messages"), &Object::can_translate_messages);
-	ClassDB::bind_method(D_METHOD("tr", "message"), &Object::tr);
+	ClassDB::bind_method(D_METHOD("tr", "message", "context"), &Object::tr, DEFVAL(""));
+	ClassDB::bind_method(D_METHOD("tr_n", "message", "plural_message", "n", "context"), &Object::tr_n, DEFVAL(""));
 
 
 	ClassDB::bind_method(D_METHOD("is_queued_for_deletion"), &Object::is_queued_for_deletion);
 	ClassDB::bind_method(D_METHOD("is_queued_for_deletion"), &Object::is_queued_for_deletion);
 
 

+ 3 - 1
core/object.h

@@ -719,7 +719,9 @@ public:
 
 
 	virtual void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const;
 	virtual void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const;
 
 
-	StringName tr(const StringName &p_message) const; // translate message (internationalization)
+	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_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()
 	bool _is_queued_for_deletion = false; // set to true by SceneTree::queue_delete()
 	bool is_queued_for_deletion() const;
 	bool is_queued_for_deletion() const;

+ 331 - 83
core/translation.cpp

@@ -42,6 +42,41 @@
 // - https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
 // - https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
 // - https://lh.2xlibre.net/locales/
 // - 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[] = {
 static const char *locale_list[] = {
 	"aa", //  Afar
 	"aa", //  Afar
 	"aa_DJ", //  Afar (Djibouti)
 	"aa_DJ", //  Afar (Djibouti)
@@ -794,40 +829,114 @@ static const char *locale_renames[][2] = {
 
 
 ///////////////////////////////////////////////
 ///////////////////////////////////////////////
 
 
-Vector<String> Translation::_get_messages() const {
-	Vector<String> msgs;
-	msgs.resize(translation_map.size() * 2);
-	int idx = 0;
-	for (const Map<StringName, StringName>::Element *E = translation_map.front(); E; E = E->next()) {
-		msgs.set(idx + 0, E->key());
-		msgs.set(idx + 1, E->get());
-		idx += 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;
 	}
 	}
 
 
-	return msgs;
+	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 {
 Vector<String> Translation::_get_message_list() const {
-	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;
+	////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());
 	}
 	}
 
 
-	return msgs;
+	return v;
 }
 }
 
 
-void Translation::_set_messages(const Vector<String> &p_messages) {
-	int msg_count = p_messages.size();
-	ERR_FAIL_COND(msg_count % 2);
+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();
 
 
-	const String *r = p_messages.ptr();
+	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;
+}
 
 
-	for (int i = 0; i < msg_count; i += 2) {
-		add_message(r[i + 0], r[i + 1]);
+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();
+	}
+
+	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) {
 void Translation::set_locale(const String &p_locale) {
@@ -848,45 +957,143 @@ void Translation::set_locale(const String &p_locale) {
 	}
 	}
 }
 }
 
 
-void Translation::add_message(const StringName &p_src_text, const StringName &p_xlated_text) {
-	translation_map[p_src_text] = p_xlated_text;
+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);
+	}
+}
+
+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;
 }
 }
 
 
-StringName Translation::get_message(const StringName &p_src_text) const {
-	const Map<StringName, StringName>::Element *E = translation_map.find(p_src_text);
-	if (!E) {
+String Translation::get_plural_rule() const {
+	return plural_rule;
+}
+
+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();
 		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 E->get();
+	return translation_map[p_context][p_src_text][0];
 }
 }
 
 
-void Translation::erase_message(const StringName &p_src_text) {
-	translation_map.erase(p_src_text);
+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)];
+}
+
+void Translation::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 Translation::get_message_list(List<StringName> *r_messages) const {
 void Translation::get_message_list(List<StringName> *r_messages) const {
-	for (const Map<StringName, StringName>::Element *E = translation_map.front(); E; E = E->next()) {
-		r_messages->push_back(E->key());
+	////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());
+		}
 	}
 	}
 }
 }
 
 
 int Translation::get_message_count() const {
 int Translation::get_message_count() const {
-	return translation_map.size();
+	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 Translation::_bind_methods() {
 void Translation::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_locale", "locale"), &Translation::set_locale);
 	ClassDB::bind_method(D_METHOD("set_locale", "locale"), &Translation::set_locale);
 	ClassDB::bind_method(D_METHOD("get_locale"), &Translation::get_locale);
 	ClassDB::bind_method(D_METHOD("get_locale"), &Translation::get_locale);
-	ClassDB::bind_method(D_METHOD("add_message", "src_message", "xlated_message"), &Translation::add_message);
-	ClassDB::bind_method(D_METHOD("get_message", "src_message"), &Translation::get_message);
-	ClassDB::bind_method(D_METHOD("erase_message", "src_message"), &Translation::erase_message);
+	ClassDB::bind_method(D_METHOD("add_message", "src_message", "xlated_message", "context"), &Translation::add_message, DEFVAL(""));
+	ClassDB::bind_method(D_METHOD("add_plural_message", "src_message", "xlated_messages", "context"), &Translation::add_plural_message, DEFVAL(""));
+	ClassDB::bind_method(D_METHOD("get_message", "src_message", "context"), &Translation::get_message, DEFVAL(""));
+	ClassDB::bind_method(D_METHOD("get_plural_message", "src_message", "src_plural_message", "n", "context"), &Translation::get_plural_message, DEFVAL(""));
+	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_list"), &Translation::_get_message_list);
 	ClassDB::bind_method(D_METHOD("get_message_count"), &Translation::get_message_count);
 	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("_set_messages"), &Translation::_set_messages);
 	ClassDB::bind_method(D_METHOD("_get_messages"), &Translation::_get_messages);
 	ClassDB::bind_method(D_METHOD("_get_messages"), &Translation::_get_messages);
 
 
-	ADD_PROPERTY(PropertyInfo(Variant::PACKED_STRING_ARRAY, "messages", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL), "_set_messages", "_get_messages");
+	ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "messages", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NOEDITOR | PROPERTY_USAGE_INTERNAL), "_set_messages", "_get_messages");
 	ADD_PROPERTY(PropertyInfo(Variant::STRING, "locale"), "set_locale", "get_locale");
 	ADD_PROPERTY(PropertyInfo(Variant::STRING, "locale"), "set_locale", "get_locale");
 }
 }
 
 
@@ -1024,7 +1231,7 @@ void TranslationServer::clear() {
 	translations.clear();
 	translations.clear();
 }
 }
 
 
-StringName TranslationServer::translate(const StringName &p_message) const {
+StringName TranslationServer::translate(const StringName &p_message, const StringName &p_context) const {
 	// Match given message against the translation catalog for the project locale.
 	// Match given message against the translation catalog for the project locale.
 
 
 	if (!enabled) {
 	if (!enabled) {
@@ -1033,6 +1240,48 @@ StringName TranslationServer::translate(const StringName &p_message) const {
 
 
 	ERR_FAIL_COND_V_MSG(locale.length() < 2, p_message, "Could not translate message as configured locale '" + locale + "' is invalid.");
 	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);
+
+	if (!res && fallback.length() >= 2) {
+		res = _get_message_from_translations(p_message, p_context, fallback);
+	}
+
+	if (!res) {
+		return p_message;
+	}
+
+	return res;
+}
+
+StringName TranslationServer::translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const {
+	if (!enabled) {
+		if (p_n == 1) {
+			return p_message;
+		} else {
+			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);
+
+	if (!res && fallback.length() >= 2) {
+		res = _get_message_from_translations(p_message, p_context, fallback, p_message_plural, p_n);
+	}
+
+	if (!res) {
+		if (p_n == 1) {
+			return p_message;
+		} else {
+			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 {
 	// Locale can be of the form 'll_CC', i.e. language code and regional code,
 	// 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'.
 	// 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
 	// To find the relevant translation, we look for those with locale starting
@@ -1044,7 +1293,7 @@ StringName TranslationServer::translate(const StringName &p_message) const {
 	// logic, so be sure to propagate changes there when changing things here.
 	// logic, so be sure to propagate changes there when changing things here.
 
 
 	StringName res;
 	StringName res;
-	String lang = get_language_code(locale);
+	String lang = get_language_code(p_locale);
 	bool near_match = false;
 	bool near_match = false;
 
 
 	for (const Set<Ref<Translation>>::Element *E = translations.front(); E; E = E->next()) {
 	for (const Set<Ref<Translation>>::Element *E = translations.front(); E; E = E->next()) {
@@ -1052,7 +1301,7 @@ StringName TranslationServer::translate(const StringName &p_message) const {
 		ERR_FAIL_COND_V(t.is_null(), p_message);
 		ERR_FAIL_COND_V(t.is_null(), p_message);
 		String l = t->get_locale();
 		String l = t->get_locale();
 
 
-		bool exact_match = (l == locale);
+		bool exact_match = (l == p_locale);
 		if (!exact_match) {
 		if (!exact_match) {
 			if (near_match) {
 			if (near_match) {
 				continue; // Only near-match once, but keep looking for exact matches.
 				continue; // Only near-match once, but keep looking for exact matches.
@@ -1062,7 +1311,13 @@ StringName TranslationServer::translate(const StringName &p_message) const {
 			}
 			}
 		}
 		}
 
 
-		StringName r = t->get_message(p_message);
+		StringName r;
+		if (p_n == -1) {
+			r = t->get_message(p_message, p_context);
+		} else {
+			r = t->get_plural_message(p_message, p_message_plural, p_n, p_context);
+		}
+
 		if (!r) {
 		if (!r) {
 			continue;
 			continue;
 		}
 		}
@@ -1075,44 +1330,6 @@ StringName TranslationServer::translate(const StringName &p_message) const {
 		}
 		}
 	}
 	}
 
 
-	if (!res && fallback.length() >= 2) {
-		// Try again with the fallback locale.
-		String fallback_lang = get_language_code(fallback);
-		near_match = 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(), p_message);
-			String l = t->get_locale();
-
-			bool exact_match = (l == fallback);
-			if (!exact_match) {
-				if (near_match) {
-					continue; // Only near-match once, but keep looking for exact matches.
-				}
-				if (get_language_code(l) != fallback_lang) {
-					continue; // Language code does not match.
-				}
-			}
-
-			StringName r = t->get_message(p_message);
-			if (!r) {
-				continue;
-			}
-			res = r;
-
-			if (exact_match) {
-				break;
-			} else {
-				near_match = true;
-			}
-		}
-	}
-
-	if (!res) {
-		return p_message;
-	}
-
 	return res;
 	return res;
 }
 }
 
 
@@ -1169,9 +1386,9 @@ void TranslationServer::set_tool_translation(const Ref<Translation> &p_translati
 	tool_translation = p_translation;
 	tool_translation = p_translation;
 }
 }
 
 
-StringName TranslationServer::tool_translate(const StringName &p_message) const {
+StringName TranslationServer::tool_translate(const StringName &p_message, const StringName &p_context) const {
 	if (tool_translation.is_valid()) {
 	if (tool_translation.is_valid()) {
-		StringName r = tool_translation->get_message(p_message);
+		StringName r = tool_translation->get_message(p_message, p_context);
 		if (r) {
 		if (r) {
 			return r;
 			return r;
 		}
 		}
@@ -1179,13 +1396,28 @@ StringName TranslationServer::tool_translate(const StringName &p_message) const
 	return p_message;
 	return p_message;
 }
 }
 
 
+StringName TranslationServer::tool_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const {
+	if (tool_translation.is_valid()) {
+		StringName r = tool_translation->get_plural_message(p_message, p_message_plural, p_n, p_context);
+		if (r) {
+			return r;
+		}
+	}
+
+	if (p_n == 1) {
+		return p_message;
+	} else {
+		return p_message_plural;
+	}
+}
+
 void TranslationServer::set_doc_translation(const Ref<Translation> &p_translation) {
 void TranslationServer::set_doc_translation(const Ref<Translation> &p_translation) {
 	doc_translation = p_translation;
 	doc_translation = p_translation;
 }
 }
 
 
-StringName TranslationServer::doc_translate(const StringName &p_message) const {
+StringName TranslationServer::doc_translate(const StringName &p_message, const StringName &p_context) const {
 	if (doc_translation.is_valid()) {
 	if (doc_translation.is_valid()) {
-		StringName r = doc_translation->get_message(p_message);
+		StringName r = doc_translation->get_message(p_message, p_context);
 		if (r) {
 		if (r) {
 			return r;
 			return r;
 		}
 		}
@@ -1193,6 +1425,21 @@ StringName TranslationServer::doc_translate(const StringName &p_message) const {
 	return p_message;
 	return p_message;
 }
 }
 
 
+StringName TranslationServer::doc_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const {
+	if (doc_translation.is_valid()) {
+		StringName r = doc_translation->get_plural_message(p_message, p_message_plural, p_n, p_context);
+		if (r) {
+			return r;
+		}
+	}
+
+	if (p_n == 1) {
+		return p_message;
+	} else {
+		return p_message_plural;
+	}
+}
+
 void TranslationServer::_bind_methods() {
 void TranslationServer::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("set_locale", "locale"), &TranslationServer::set_locale);
 	ClassDB::bind_method(D_METHOD("set_locale", "locale"), &TranslationServer::set_locale);
 	ClassDB::bind_method(D_METHOD("get_locale"), &TranslationServer::get_locale);
 	ClassDB::bind_method(D_METHOD("get_locale"), &TranslationServer::get_locale);
@@ -1200,6 +1447,7 @@ void TranslationServer::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_locale_name", "locale"), &TranslationServer::get_locale_name);
 	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"), &TranslationServer::translate);
+	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("add_translation", "translation"), &TranslationServer::add_translation);
 	ClassDB::bind_method(D_METHOD("remove_translation", "translation"), &TranslationServer::remove_translation);
 	ClassDB::bind_method(D_METHOD("remove_translation", "translation"), &TranslationServer::remove_translation);

+ 38 - 9
core/translation.h

@@ -31,6 +31,9 @@
 #ifndef TRANSLATION_H
 #ifndef TRANSLATION_H
 #define TRANSLATION_H
 #define TRANSLATION_H
 
 
+//#define DEBUG_TRANSLATION
+
+#include "core/math/expression.h"
 #include "core/resource.h"
 #include "core/resource.h"
 
 
 class Translation : public Resource {
 class Translation : public Resource {
@@ -39,12 +42,23 @@ class Translation : public Resource {
 	RES_BASE_EXTENSION("translation");
 	RES_BASE_EXTENSION("translation");
 
 
 	String locale = "en";
 	String locale = "en";
-	Map<StringName, 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;
+
+	// 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;
 	Vector<String> _get_message_list() const;
 
 
-	Vector<String> _get_messages() const;
-	void _set_messages(const Vector<String> &p_messages);
+	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<String> &p_input_name, const Array &p_input_value, Ref<Expression> &r_expr) const;
 
 
 protected:
 protected:
 	static void _bind_methods();
 	static void _bind_methods();
@@ -52,14 +66,24 @@ protected:
 public:
 public:
 	void set_locale(const String &p_locale);
 	void set_locale(const String &p_locale);
 	_FORCE_INLINE_ String get_locale() const { return 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);
-	virtual StringName get_message(const StringName &p_src_text) const; //overridable for other implementations
-	void erase_message(const StringName &p_src_text);
+	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 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;
 	void get_message_list(List<StringName> *r_messages) const;
 	int get_message_count() const;
 	int get_message_count() const;
 
 
+	int get_plural_forms() const;
+	String get_plural_rule() const;
+
+#ifdef DEBUG_TRANSLATION
+	void print_translation_map();
+#endif
+
 	Translation() {}
 	Translation() {}
 };
 };
 
 
@@ -80,6 +104,8 @@ class TranslationServer : public Object {
 	static TranslationServer *singleton;
 	static TranslationServer *singleton;
 	bool _load_translations(const String &p_from);
 	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;
+
 	static void _bind_methods();
 	static void _bind_methods();
 
 
 public:
 public:
@@ -98,7 +124,8 @@ public:
 	void add_translation(const Ref<Translation> &p_translation);
 	void add_translation(const Ref<Translation> &p_translation);
 	void remove_translation(const Ref<Translation> &p_translation);
 	void remove_translation(const Ref<Translation> &p_translation);
 
 
-	StringName translate(const StringName &p_message) const;
+	StringName translate(const StringName &p_message, const StringName &p_context = "") const;
+	StringName translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context = "") const;
 
 
 	static Vector<String> get_all_locales();
 	static Vector<String> get_all_locales();
 	static Vector<String> get_all_locale_names();
 	static Vector<String> get_all_locale_names();
@@ -107,9 +134,11 @@ public:
 	static String get_language_code(const String &p_locale);
 	static String get_language_code(const String &p_locale);
 
 
 	void set_tool_translation(const Ref<Translation> &p_translation);
 	void set_tool_translation(const Ref<Translation> &p_translation);
-	StringName tool_translate(const StringName &p_message) const;
+	StringName tool_translate(const StringName &p_message, const StringName &p_context = "") const;
+	StringName tool_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context = "") const;
 	void set_doc_translation(const Ref<Translation> &p_translation);
 	void set_doc_translation(const Ref<Translation> &p_translation);
-	StringName doc_translate(const StringName &p_message) const;
+	StringName doc_translate(const StringName &p_message, const StringName &p_context = "") const;
+	StringName doc_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context = "") const;
 
 
 	void setup();
 	void setup();
 
 

+ 54 - 7
core/ustring.cpp

@@ -4269,31 +4269,60 @@ String String::unquote() const {
 }
 }
 
 
 #ifdef TOOLS_ENABLED
 #ifdef TOOLS_ENABLED
-String TTR(const String &p_text) {
+String TTR(const String &p_text, const String &p_context) {
 	if (TranslationServer::get_singleton()) {
 	if (TranslationServer::get_singleton()) {
-		return TranslationServer::get_singleton()->tool_translate(p_text);
+		return TranslationServer::get_singleton()->tool_translate(p_text, p_context);
 	}
 	}
 
 
 	return p_text;
 	return p_text;
 }
 }
 
 
-String DTR(const String &p_text) {
+String TTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context) {
+	if (TranslationServer::get_singleton()) {
+		return TranslationServer::get_singleton()->tool_translate_plural(p_text, p_text_plural, p_n, p_context);
+	}
+
+	// Return message based on English plural rule if translation is not possible.
+	if (p_n == 1) {
+		return p_text;
+	} else {
+		return p_text_plural;
+	}
+}
+
+String DTR(const String &p_text, const String &p_context) {
 	// Comes straight from the XML, so remove indentation and any trailing whitespace.
 	// Comes straight from the XML, so remove indentation and any trailing whitespace.
 	const String text = p_text.dedent().strip_edges();
 	const String text = p_text.dedent().strip_edges();
 
 
 	if (TranslationServer::get_singleton()) {
 	if (TranslationServer::get_singleton()) {
-		return TranslationServer::get_singleton()->doc_translate(text);
+		return TranslationServer::get_singleton()->doc_translate(text, p_context);
 	}
 	}
 
 
 	return text;
 	return text;
 }
 }
+
+String DTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context) {
+	const String text = p_text.dedent().strip_edges();
+	const String text_plural = p_text_plural.dedent().strip_edges();
+
+	if (TranslationServer::get_singleton()) {
+		return TranslationServer::get_singleton()->doc_translate_plural(text, text_plural, p_n, p_context);
+	}
+
+	// Return message based on English plural rule if translation is not possible.
+	if (p_n == 1) {
+		return text;
+	} else {
+		return text_plural;
+	}
+}
 #endif
 #endif
 
 
-String RTR(const String &p_text) {
+String RTR(const String &p_text, const String &p_context) {
 	if (TranslationServer::get_singleton()) {
 	if (TranslationServer::get_singleton()) {
-		String rtr = TranslationServer::get_singleton()->tool_translate(p_text);
+		String rtr = TranslationServer::get_singleton()->tool_translate(p_text, p_context);
 		if (rtr == String() || rtr == p_text) {
 		if (rtr == String() || rtr == p_text) {
-			return TranslationServer::get_singleton()->translate(p_text);
+			return TranslationServer::get_singleton()->translate(p_text, p_context);
 		} else {
 		} else {
 			return rtr;
 			return rtr;
 		}
 		}
@@ -4301,3 +4330,21 @@ String RTR(const String &p_text) {
 
 
 	return p_text;
 	return p_text;
 }
 }
+
+String RTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context) {
+	if (TranslationServer::get_singleton()) {
+		String rtr = TranslationServer::get_singleton()->tool_translate_plural(p_text, p_text_plural, p_n, p_context);
+		if (rtr == String() || rtr == p_text || rtr == p_text_plural) {
+			return TranslationServer::get_singleton()->translate_plural(p_text, p_text_plural, p_n, p_context);
+		} else {
+			return rtr;
+		}
+	}
+
+	// Return message based on English plural rule if translation is not possible.
+	if (p_n == 1) {
+		return p_text;
+	} else {
+		return p_text_plural;
+	}
+}

+ 6 - 3
core/ustring.h

@@ -410,8 +410,10 @@ _FORCE_INLINE_ bool is_str_less(const L *l_ptr, const R *r_ptr) {
 // and doc translate for the class reference (DTR).
 // and doc translate for the class reference (DTR).
 #ifdef TOOLS_ENABLED
 #ifdef TOOLS_ENABLED
 // Gets parsed.
 // Gets parsed.
-String TTR(const String &);
-String DTR(const String &);
+String TTR(const String &p_text, const String &p_context = "");
+String TTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context = "");
+String DTR(const String &p_text, const String &p_context = "");
+String DTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context = "");
 // Use for C strings.
 // Use for C strings.
 #define TTRC(m_value) (m_value)
 #define TTRC(m_value) (m_value)
 // Use to avoid parsing (for use later with C strings).
 // Use to avoid parsing (for use later with C strings).
@@ -425,7 +427,8 @@ String DTR(const String &);
 #endif
 #endif
 
 
 // Runtime translate for the public node API.
 // Runtime translate for the public node API.
-String RTR(const String &);
+String RTR(const String &p_text, const String &p_context = "");
+String RTRN(const String &p_text, const String &p_text_plural, int p_n, const String &p_context = "");
 
 
 bool is_symbol(CharType c);
 bool is_symbol(CharType c);
 bool select_word(const String &p_s, int p_col, int &r_beg, int &r_end);
 bool select_word(const String &p_s, int p_col, int &r_beg, int &r_end);

+ 26 - 18
editor/scene_tree_editor.cpp

@@ -258,27 +258,35 @@ bool SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
 		int num_connections = p_node->get_persistent_signal_connection_count();
 		int num_connections = p_node->get_persistent_signal_connection_count();
 		int num_groups = p_node->get_persistent_group_count();
 		int num_groups = p_node->get_persistent_group_count();
 
 
+		String msg_temp;
+		if (num_connections >= 1) {
+			Array arr;
+			arr.push_back(num_connections);
+			msg_temp += TTRN("Node has one connection.", "Node has {num} connections.", num_connections).format(arr, "{num}");
+			msg_temp += " ";
+		}
+		if (num_groups >= 1) {
+			Array arr;
+			arr.push_back(num_groups);
+			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.");
+		}
+
+		Ref<Texture2D> icon_temp;
+		auto signal_temp = BUTTON_SIGNALS;
 		if (num_connections >= 1 && num_groups >= 1) {
 		if (num_connections >= 1 && num_groups >= 1) {
-			item->add_button(
-					0,
-					get_theme_icon("SignalsAndGroups", "EditorIcons"),
-					BUTTON_SIGNALS,
-					false,
-					vformat(TTR("Node has %s connection(s) and %s group(s).\nClick to show signals dock."), num_connections, num_groups));
+			icon_temp = get_theme_icon("SignalsAndGroups", "EditorIcons");
 		} else if (num_connections >= 1) {
 		} else if (num_connections >= 1) {
-			item->add_button(
-					0,
-					get_theme_icon("Signals", "EditorIcons"),
-					BUTTON_SIGNALS,
-					false,
-					vformat(TTR("Node has %s connection(s).\nClick to show signals dock."), num_connections));
+			icon_temp = get_theme_icon("Signals", "EditorIcons");
 		} else if (num_groups >= 1) {
 		} else if (num_groups >= 1) {
-			item->add_button(
-					0,
-					get_theme_icon("Groups", "EditorIcons"),
-					BUTTON_GROUPS,
-					false,
-					vformat(TTR("Node is in %s group(s).\nClick to show groups dock."), num_groups));
+			icon_temp = get_theme_icon("Groups", "EditorIcons");
+			signal_temp = BUTTON_GROUPS;
+		}
+
+		if (num_connections >= 1 || num_groups >= 1) {
+			item->add_button(0, icon_temp, signal_temp, false, msg_temp);
 		}
 		}
 	}
 	}