Updated Translation architecture to have TranslationPO, did some commit fixes and updated class Reference.

This commit is contained in:
SkyJJ 2020-08-07 13:17:12 +02:00
parent 396f2eee82
commit 0ef758eaee
17 changed files with 599 additions and 305 deletions

View file

@ -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);
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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()

View file

@ -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;
return msgs;
}
int Translation::_get_plural_index(int p_n) const {
// Apply plural rule to a p_n passed in, and get a number between [0;number of plural forms)
Ref<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;
}
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;
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()];
}
if (bool(result)) {
return p_plural_rule.substr(first_ques_mark + 1, p_plural_rule.find(":") - (first_ques_mark + 1)).to_int();
}
String after_colon = p_plural_rule.substr(p_plural_rule.find(":") + 1, p_plural_rule.length());
return _get_plural_index(after_colon, p_input_name, p_input_value, r_expr);
}
void Translation::set_locale(const String &p_locale) {
@ -957,125 +840,50 @@ void Translation::set_locale(const String &p_locale) {
}
}
void Translation::set_plural_rule(const String &p_plural_rule) {
// Set plural_forms and plural_rule.
// p_plural_rule passed in has the form "Plural-Forms: nplurals=2; plural=(n >= 2);".
int first_semi_col = p_plural_rule.find(";");
plural_forms = p_plural_rule.substr(p_plural_rule.find("=") + 1, first_semi_col - (p_plural_rule.find("=") + 1)).to_int();
int expression_start = p_plural_rule.find("=", first_semi_col) + 1;
int second_semi_col = p_plural_rule.rfind(";");
plural_rule = p_plural_rule.substr(expression_start, second_semi_col - expression_start);
// Strip away '(' and ')' to ease evaluating the expression later on.
plural_rule = plural_rule.replacen("(", "");
plural_rule = plural_rule.replacen(")", "");
}
void Translation::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) {
HashMap<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)) {
if (p_context != StringName()) {
WARN_PRINT("Translation class doesn't handle context. Using context in get_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles context, such as TranslationPO class");
}
const Map<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 translation_map[p_context][p_src_text][0];
return E->get();
}
StringName Translation::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const {
ERR_FAIL_COND_V_MSG(p_n < 0, p_src_text, "N passed into translation to get a plural message should not be negative. For negative numbers, use singular translation please. Search \"gettext PO Plural Forms\" online for the documentation on translating negative numbers.");
if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) {
return StringName();
}
ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].empty(), StringName(), "Source text \"" + String(p_src_text) + "\" is registered but doesn't have a translation. Please check add_message() or add_plural_message() to make sure a translation is always added.");
// Return based on English plural rule if locale's plural rule is not registered (normally due to missing or invalid "Plural-Forms" in PO file header).
if (plural_forms <= 0) {
if (p_n == 1) {
return p_src_text;
} else {
return p_plural_text;
}
}
return translation_map[p_context][p_src_text][_get_plural_index(p_n)];
WARN_PRINT("Translation class doesn't handle plural messages. Calling get_plural_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles plurals, such as TranslationPO class");
return get_message(p_src_text);
}
void Translation::erase_message(const StringName &p_src_text, const StringName &p_context) {
if (!translation_map.has(p_context)) {
return;
if (p_context != StringName()) {
WARN_PRINT("Translation class doesn't handle context. Using context in erase_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles context, such as TranslationPO class");
}
translation_map[p_context].erase(p_src_text);
translation_map.erase(p_src_text);
}
void Translation::get_message_list(List<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);

View file

@ -31,9 +31,6 @@
#ifndef TRANSLATION_H
#define TRANSLATION_H
//#define DEBUG_TRANSLATION
#include "core/math/expression.h"
#include "core/resource.h"
class Translation : public Resource {
@ -42,23 +39,11 @@ class Translation : public Resource {
RES_BASE_EXTENSION("translation");
String locale = "en";
int plural_forms = 0; // 0 means no "Plural-Forms" is given in the PO header file. The min for all languages is 1.
String plural_rule;
Map<StringName, StringName> translation_map;
// TLDR: Maps context to a list of source strings and translated strings. In PO terms, maps msgctxt to a list of msgid and msgstr.
// The first key corresponds to context, and the second key (of the contained HashMap) corresponds to source string.
// The value Vector<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);
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;

311
core/translation_po.cpp Normal file
View file

@ -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);
}

92
core/translation_po.h Normal file
View file

@ -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

View file

@ -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;
}

View file

@ -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

View file

@ -6,6 +6,7 @@
<description>
Plugins are registered via [method EditorPlugin.add_translation_parser_plugin] method. To define the parsing and string extraction logic, override the [method parse_file] method in script.
Add the extracted strings to argument [code]msgids[/code] or [code]msgids_context_plural[/code] if context or plural is used.
When adding to [code]msgids_context_plural[/code], you must add the data using the format [code]["A", "B", "C"][/code], where [code]A[/code] represents the extracted string, [code]B[/code] represents the context, and [code]C[/code] represents the plural version of the extracted string. If you want to add only context but not plural, put [code]""[/code] for the plural slot. The idea is the same if you only want to add plural but not context. See the code below for concrete examples.
The extracted strings will be written into a POT file selected by user under "POT Generation" in "Localization" tab in "Project Settings" menu.
Below shows an example of a custom parser that extracts strings from a CSV file to write into a POT.
[codeblock]
@ -28,9 +29,12 @@
[/codeblock]
To add a translatable string associated with context or plural, add it to [code]msgids_context_plural[/code]:
[codeblock]
msgids_ctx_plural.append(["Test 1", "context", "test 1 plurals"]) # This will add a message with msgid "Test 1", msgctxt "context", and msgid_plural "test 1 plurals".
msgids_ctx_plural.append(["A test without context", "", "plurals"]) # This will add a message with msgid "A test without context" and msgid_plural "plurals".
msgids_ctx_plural.append(["Only with context", "a friendly context", ""]) # This will add a message with msgid "Only with context" and msgctxt "a friendly context".
# This will add a message with msgid "Test 1", msgctxt "context", and msgid_plural "test 1 plurals".
msgids_context_plural.append(["Test 1", "context", "test 1 plurals"])
# This will add a message with msgid "A test without context" and msgid_plural "plurals".
msgids_context_plural.append(["A test without context", "", "plurals"])
# This will add a message with msgid "Only with context" and msgctxt "a friendly context".
msgids_context_plural.append(["Only with context", "a friendly context", ""])
[/codeblock]
[b]Note:[/b] If you override parsing logic for standard script types (GDScript, C#, etc.), it would be better to load the [code]path[/code] argument using [method ResourceLoader.load]. This is because built-in scripts are loaded as [Resource] type, not [File] type.
For example:

View file

@ -486,13 +486,35 @@
</description>
</method>
<method name="tr" qualifiers="const">
<return type="StringName">
<return type="String">
</return>
<argument index="0" name="message" type="StringName">
</argument>
<argument index="1" name="context" type="StringName" default="&quot;&quot;">
</argument>
<description>
Translates a message using translation catalogs configured in the Project Settings.
Translates a message using translation catalogs configured in the Project Settings. An additional context could be used to specify the translation context.
Only works if message translation is enabled (which it is by default), otherwise it returns the [code]message[/code] unchanged. See [method set_message_translation].
See <link>https://docs.godotengine.org/en/latest/tutorials/i18n/internationalizing_games.html</link> for examples of the usage of this method.
</description>
</method>
<method name="tr_n" qualifiers="const">
<return type="String">
</return>
<argument index="0" name="message" type="StringName">
</argument>
<argument index="1" name="plural_message" type="StringName">
</argument>
<argument index="2" name="n" type="int">
</argument>
<argument index="3" name="context" type="StringName" default="&quot;&quot;">
</argument>
<description>
Translates a message involving plurals using translation catalogs configured in the Project Settings. An additional context could be used to specify the translation context.
Only works if message translation is enabled (which it is by default), otherwise it returns the [code]message[/code] or [code]plural_message[/code] unchanged. See [method set_message_translation].
The number [code]n[/code] is the number or quantity of the plural object. It will be used to guide the translation system to fetch the correct plural form for the selected language.
[b]Note:[/b] Negative and floating-point values usually represent physical entities for which singular and plural don't clearly apply. In such cases, use [method tr].
See <link>https://docs.godotengine.org/en/latest/tutorials/i18n/internationalizing_games.html</link> for examples of the usage of this method.
</description>
</method>
</methods>

View file

@ -18,8 +18,25 @@
</argument>
<argument index="1" name="xlated_message" type="StringName">
</argument>
<argument index="2" name="context" type="StringName" default="&quot;&quot;">
</argument>
<description>
Adds a message if nonexistent, followed by its translation.
An additional context could be used to specify the translation context or differentiate polysemic words.
</description>
</method>
<method name="add_plural_message">
<return type="void">
</return>
<argument index="0" name="src_message" type="StringName">
</argument>
<argument index="1" name="xlated_messages" type="PackedStringArray">
</argument>
<argument index="2" name="context" type="StringName" default="&quot;&quot;">
</argument>
<description>
Adds a message involving plural translation if nonexistent, followed by its translation.
An additional context could be used to specify the translation context or differentiate polysemic words.
</description>
</method>
<method name="erase_message">
@ -27,6 +44,8 @@
</return>
<argument index="0" name="src_message" type="StringName">
</argument>
<argument index="1" name="context" type="StringName" default="&quot;&quot;">
</argument>
<description>
Erases a message.
</description>
@ -36,6 +55,8 @@
</return>
<argument index="0" name="src_message" type="StringName">
</argument>
<argument index="1" name="context" type="StringName" default="&quot;&quot;">
</argument>
<description>
Returns a message's translation.
</description>
@ -54,6 +75,22 @@
Returns all the messages (keys).
</description>
</method>
<method name="get_plural_message" qualifiers="const">
<return type="StringName">
</return>
<argument index="0" name="src_message" type="StringName">
</argument>
<argument index="1" name="src_plural_message" type="StringName">
</argument>
<argument index="2" name="n" type="int">
</argument>
<argument index="3" name="context" type="StringName" default="&quot;&quot;">
</argument>
<description>
Returns a message's translation involving plurals.
The number [code]n[/code] is the number or quantity of the plural object. It will be used to guide the translation system to fetch the correct plural form for the selected language.
</description>
</method>
</methods>
<members>
<member name="locale" type="String" setter="set_locale" getter="get_locale" default="&quot;en&quot;">

View file

@ -50,6 +50,16 @@
Returns a locale's language and its variant (e.g. [code]"en_US"[/code] would return [code]"English (United States)"[/code]).
</description>
</method>
<method name="get_translation_object">
<return type="Translation">
</return>
<argument index="0" name="locale" type="String">
</argument>
<description>
Returns the [Translation] instance based on the [code]locale[/code] passed in.
It will return a [code]nullptr[/code] if there is no [Translation] instance that matches the [code]locale[/code].
</description>
</method>
<method name="remove_translation">
<return type="void">
</return>
@ -73,8 +83,26 @@
</return>
<argument index="0" name="message" type="StringName">
</argument>
<argument index="1" name="context" type="StringName" default="&quot;&quot;">
</argument>
<description>
Returns the current locale's translation for the given message (key).
Returns the current locale's translation for the given message (key) and context.
</description>
</method>
<method name="translate_plural" qualifiers="const">
<return type="StringName">
</return>
<argument index="0" name="message" type="StringName">
</argument>
<argument index="1" name="plural_message" type="StringName">
</argument>
<argument index="2" name="n" type="int">
</argument>
<argument index="3" name="context" type="StringName" default="&quot;&quot;">
</argument>
<description>
Returns the current locale's translation for the given message (key), plural_message and context.
The number [code]n[/code] is the number or quantity of the plural object. It will be used to guide the translation system to fetch the correct plural form for the selected language.
</description>
</method>
</methods>

View file

@ -54,7 +54,7 @@ Error EditorTranslationParserPlugin::parse_file(const String &p_path, Vector<Str
// Add user's collected translatable messages with context or plurals.
for (int i = 0; i < ids_ctx_plural.size(); i++) {
Array arr = ids_ctx_plural[i];
ERR_FAIL_COND_V_MSG(arr.size() != 3, ERR_INVALID_DATA, "Array entries written into msgids_ctx_plural of EditorTranslationParserPlugin parse_file() method should have the form [\"message\",\"context\",\"plural message\"]");
ERR_FAIL_COND_V_MSG(arr.size() != 3, ERR_INVALID_DATA, "Array entries written into `msgids_context_plural` in `parse_file()` method should have the form [\"message\", \"context\", \"plural message\"]");
Vector<String> id_ctx_plural;
id_ctx_plural.push_back(arr[0]);

View file

@ -271,7 +271,7 @@ bool SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
msg_temp += TTRN("Node is in one group.", "Node is in {num} groups.", num_groups).format(arr, "{num}");
}
if (num_connections >= 1 || num_groups >= 1) {
msg_temp += TTR("\nClick to show signals dock.");
msg_temp += "\n" + TTR("Click to show signals dock.");
}
Ref<Texture2D> icon_temp;

View file

@ -78,8 +78,6 @@ def _add_additional_location(msgctx, msg, location):
if msg_pos == -1:
print("Someone apparently thought writing Python was as easy as GDScript. Ping Akien.")
# NOTE FOR MENTORS: When I tested on my computer (windows) I need the extra \n#: to make the locations print line by line.
# but it worked before without \n# so I will leave it like before
main_po = main_po[:msg_pos] + " " + location + main_po[msg_pos:]
@ -171,7 +169,7 @@ print("Updating the editor.pot template...")
for fname in matches:
# NOTE FOR MENTORS: When I tested on windows I need to add encoding="utf8" at the end to be able to open the file.
# maybe on Linux there's no need.
with open(fname, "r") as f:
with open(fname, "r", encoding="utf8") as f:
process_file(f, fname)
with open("editor.pot", "w") as f: