Merge pull request #40443 from SkyLucilfer/PluralsSupport

Added plurals and context support to Translation
This commit is contained in:
Rémi Verschelde 2020-08-25 12:11:56 +02:00 committed by GitHub
commit 9d8f3496e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1418 additions and 303 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);
@ -212,7 +214,9 @@ bool PHashTranslation::_get(const StringName &p_name, Variant &r_ret) const {
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 {
// p_context passed in is ignore. The use of context is not yet supported in PHashTranslation.
int htsize = hash_table.size();
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 {
// 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 {
p_list->push_back(PropertyInfo(Variant::PACKED_INT32_ARRAY, "hash_table"));
p_list->push_back(PropertyInfo(Variant::PACKED_INT32_ARRAY, "bucket_table"));

View file

@ -79,7 +79,8 @@ protected:
static void _bind_methods();
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);
PHashTranslation() {}

View file

@ -32,26 +32,34 @@
#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 {
STATUS_NONE,
STATUS_READING_ID,
STATUS_READING_STRING,
STATUS_READING_CONTEXT,
STATUS_READING_PLURAL,
};
Status status = STATUS_NONE;
String msg_id;
String msg_str;
String msg_context;
Vector<String> msgs_plural;
String config;
if (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;
bool entered_context = false;
bool skip_this = false;
bool skip_next = false;
bool is_eof = false;
@ -63,40 +71,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 (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);
ERR_FAIL_V_MSG(RES(), "Unexpected EOF while reading 'msgid' at: " + path + ":" + itos(line));
ERR_FAIL_V_MSG(RES(), "Unexpected EOF while reading PO file at: " + path + ":" + itos(line));
} else {
break;
}
}
if (l.begins_with("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) {
memdelete(f);
ERR_FAIL_V_MSG(RES(), "Unexpected 'msgid', was expecting 'msgstr' while parsing: " + path + ":" + itos(line));
}
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 == "") {
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();
status = STATUS_READING_ID;
// If we did not encounter msgctxt, we reset context to empty to reset it.
if (!entered_context) {
msg_context = "";
}
msg_id = "";
msg_str = "";
skip_this = skip_next;
skip_next = false;
entered_context = false;
}
if (l.begins_with("msgstr")) {
if (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) {
memdelete(f);
ERR_FAIL_V_MSG(RES(), "Unexpected 'msgstr', was expecting 'msgid' while parsing: " + path + ":" + itos(line));
ERR_FAIL_V_MSG(RES(), "Unexpected 'msgstr', was expecting 'msgid' before 'msgstr' while parsing: " + path + ":" + itos(line));
}
l = l.substr(6, l.length()).strip_edges();
@ -108,7 +183,7 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) {
skip_next = true;
}
line++;
continue; //nothing to read or comment
continue; // Nothing to read or comment.
}
if (!l.begins_with("\"") || status == STATUS_NONE) {
@ -146,8 +221,12 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) {
if (status == STATUS_READING_ID) {
msg_id += l;
} else {
} else if (status == STATUS_READING_STRING) {
msg_str += l;
} else if (status == STATUS_READING_CONTEXT) {
msg_context += l;
} else if (status == STATUS_READING_PLURAL && plural_index >= 0) {
msgs_plural.write[plural_index] = msgs_plural[plural_index] + l;
}
line++;
@ -155,14 +234,23 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) {
memdelete(f);
// Add the last set of data from last iteration.
if (status == STATUS_READING_STRING) {
if (msg_id != "") {
if (!skip_this) {
translation->add_message(msg_id, msg_str);
translation->add_message(msg_id, msg_str, msg_context);
}
} else if (config == "") {
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 + ".");

View file

@ -1432,12 +1432,22 @@ void Object::initialize_class() {
initialized = true;
}
StringName Object::tr(const StringName &p_message) const {
String Object::tr(const StringName &p_message, const StringName &p_context) const {
if (!_can_translate || !TranslationServer::get_singleton()) {
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;
}
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) {
@ -1578,7 +1588,8 @@ void Object::_bind_methods() {
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("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);

View file

@ -719,7 +719,8 @@ 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; // translate message (internationalization)
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()
bool is_queued_for_deletion() const;

View file

@ -794,17 +794,12 @@ static const char *locale_renames[][2] = {
///////////////////////////////////////////////
Vector<String> Translation::_get_messages() const {
Vector<String> msgs;
msgs.resize(translation_map.size() * 2);
int idx = 0;
Dictionary Translation::_get_messages() const {
Dictionary d;
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;
d[E->key()] = E->value();
}
return msgs;
return d;
}
Vector<String> Translation::_get_message_list() const {
@ -819,14 +814,11 @@ Vector<String> Translation::_get_message_list() const {
return msgs;
}
void Translation::_set_messages(const Vector<String> &p_messages) {
int msg_count = p_messages.size();
ERR_FAIL_COND(msg_count % 2);
const String *r = p_messages.ptr();
for (int i = 0; i < msg_count; i += 2) {
add_message(r[i + 0], r[i + 1]);
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()];
}
}
@ -848,11 +840,21 @@ void Translation::set_locale(const String &p_locale) {
}
}
void Translation::add_message(const StringName &p_src_text, const StringName &p_xlated_text) {
void Translation::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) {
translation_map[p_src_text] = p_xlated_text;
}
StringName Translation::get_message(const StringName &p_src_text) const {
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 (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();
@ -861,7 +863,16 @@ StringName Translation::get_message(const StringName &p_src_text) const {
return E->get();
}
void Translation::erase_message(const StringName &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 {
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 (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.erase(p_src_text);
}
@ -878,15 +889,17 @@ int Translation::get_message_count() const {
void Translation::_bind_methods() {
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("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_count"), &Translation::get_message_count);
ClassDB::bind_method(D_METHOD("_set_messages"), &Translation::_set_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");
}
@ -1020,11 +1033,35 @@ 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();
}
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.
if (!enabled) {
@ -1033,6 +1070,46 @@ 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.");
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, false);
}
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;
}
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, true, p_message_plural, p_n);
if (!res && fallback.length() >= 2) {
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;
}
return p_message_plural;
}
return res;
}
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
@ -1044,7 +1121,7 @@ StringName TranslationServer::translate(const StringName &p_message) const {
// logic, so be sure to propagate changes there when changing things here.
StringName res;
String lang = get_language_code(locale);
String lang = get_language_code(p_locale);
bool near_match = false;
for (const Set<Ref<Translation>>::Element *E = translations.front(); E; E = E->next()) {
@ -1052,7 +1129,7 @@ StringName TranslationServer::translate(const StringName &p_message) const {
ERR_FAIL_COND_V(t.is_null(), p_message);
String l = t->get_locale();
bool exact_match = (l == locale);
bool exact_match = (l == p_locale);
if (!exact_match) {
if (near_match) {
continue; // Only near-match once, but keep looking for exact matches.
@ -1062,7 +1139,13 @@ StringName TranslationServer::translate(const StringName &p_message) const {
}
}
StringName r = t->get_message(p_message);
StringName r;
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);
}
if (!r) {
continue;
}
@ -1075,44 +1158,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;
}
@ -1169,9 +1214,9 @@ void TranslationServer::set_tool_translation(const Ref<Translation> &p_translati
tool_translation = p_translation;
}
StringName TranslationServer::tool_translate(const StringName &p_message) const {
StringName TranslationServer::tool_translate(const StringName &p_message, const StringName &p_context) const {
if (tool_translation.is_valid()) {
StringName r = tool_translation->get_message(p_message);
StringName r = tool_translation->get_message(p_message, p_context);
if (r) {
return r;
}
@ -1179,13 +1224,27 @@ StringName TranslationServer::tool_translate(const StringName &p_message) const
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;
}
return p_message_plural;
}
void TranslationServer::set_doc_translation(const Ref<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()) {
StringName r = doc_translation->get_message(p_message);
StringName r = doc_translation->get_message(p_message, p_context);
if (r) {
return r;
}
@ -1193,16 +1252,32 @@ StringName TranslationServer::doc_translate(const StringName &p_message) const {
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;
}
return p_message_plural;
}
void TranslationServer::_bind_methods() {
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_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

@ -41,10 +41,9 @@ class Translation : public Resource {
String locale = "en";
Map<StringName, StringName> translation_map;
Vector<String> _get_message_list() const;
Vector<String> _get_messages() const;
void _set_messages(const Vector<String> &p_messages);
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();
@ -53,12 +52,13 @@ public:
void set_locale(const String &p_locale);
_FORCE_INLINE_ String get_locale() const { return locale; }
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 get_message_list(List<StringName> *r_messages) const;
int get_message_count() const;
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;
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() {}
};
@ -80,6 +80,8 @@ 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, bool plural, const String &p_message_plural = "", int p_n = 0) const;
static void _bind_methods();
public:
@ -90,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;
@ -98,7 +101,8 @@ public:
void add_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_locale_names();
@ -107,9 +111,11 @@ public:
static String get_language_code(const String &p_locale);
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);
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();

312
core/translation_po.cpp Normal file
View file

@ -0,0 +1,312 @@
/*************************************************************************/
/* 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 "core/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 "core/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

@ -4269,31 +4269,58 @@ String String::unquote() const {
}
#ifdef TOOLS_ENABLED
String TTR(const String &p_text) {
String TTR(const String &p_text, const String &p_context) {
if (TranslationServer::get_singleton()) {
return TranslationServer::get_singleton()->tool_translate(p_text);
return TranslationServer::get_singleton()->tool_translate(p_text, p_context);
}
return p_text;
}
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;
}
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.
const String text = p_text.dedent().strip_edges();
if (TranslationServer::get_singleton()) {
return TranslationServer::get_singleton()->doc_translate(text);
return TranslationServer::get_singleton()->doc_translate(text, p_context);
}
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;
}
return text_plural;
}
#endif
String RTR(const String &p_text) {
String RTR(const String &p_text, const String &p_context) {
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) {
return TranslationServer::get_singleton()->translate(p_text);
return TranslationServer::get_singleton()->translate(p_text, p_context);
} else {
return rtr;
}
@ -4301,3 +4328,20 @@ String RTR(const String &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;
}
return p_text_plural;
}

View file

@ -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).
#ifdef TOOLS_ENABLED
// 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.
#define TTRC(m_value) (m_value)
// Use to avoid parsing (for use later with C strings).
@ -419,13 +421,16 @@ String DTR(const String &);
#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
// 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 select_word(const String &p_s, int p_col, int &r_beg, int &r_end);

View file

@ -5,30 +5,41 @@
</brief_description>
<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 in a CSV file to write into a POT.
Below shows an example of a custom parser that extracts strings from a CSV file to write into a POT.
[codeblock]
tool
extends EditorTranslationParserPlugin
func parse_file(path, extracted_strings):
func parse_file(path, msgids, msgids_context_plural):
var file = File.new()
file.open(path, File.READ)
var text = file.get_as_text()
var split_strs = text.split(",", false, 0)
for s in split_strs:
extracted_strings.append(s)
msgids.append(s)
#print("Extracted string: " + s)
func get_recognized_extensions():
return ["csv"]
[/codeblock]
To add a translatable string associated with context or plural, add it to [code]msgids_context_plural[/code]:
[codeblock]
# 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:
[codeblock]
func parse_file(path, extracted_strings):
func parse_file(path, msgids, msgids_context_plural):
var res = ResourceLoader.load(path, "Script")
var text = res.get_source_code()
# Parsing logic.
@ -53,7 +64,9 @@
</return>
<argument index="0" name="path" type="String">
</argument>
<argument index="1" name="extracted_strings" type="Array">
<argument index="1" name="msgids" type="Array">
</argument>
<argument index="2" name="msgids_context_plural" type="Array">
</argument>
<description>
Override this method to define a custom parsing logic to extract the translatable strings.

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/localization_using_gettext.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

@ -37,15 +37,30 @@
EditorTranslationParser *EditorTranslationParser::singleton = nullptr;
Error EditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_extracted_strings) {
Error EditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) {
if (!get_script_instance())
return ERR_UNAVAILABLE;
if (get_script_instance()->has_method("parse_file")) {
Array extracted_strings;
get_script_instance()->call("parse_file", p_path, extracted_strings);
for (int i = 0; i < extracted_strings.size(); i++) {
r_extracted_strings->append(extracted_strings[i]);
Array ids;
Array ids_ctx_plural;
get_script_instance()->call("parse_file", p_path, ids, ids_ctx_plural);
// Add user's extracted translatable messages.
for (int i = 0; i < ids.size(); i++) {
r_ids->append(ids[i]);
}
// 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_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]);
id_ctx_plural.push_back(arr[1]);
id_ctx_plural.push_back(arr[2]);
r_ids_ctx_plural->append(id_ctx_plural);
}
return OK;
} else {
@ -69,7 +84,7 @@ void EditorTranslationParserPlugin::get_recognized_extensions(List<String> *r_ex
}
void EditorTranslationParserPlugin::_bind_methods() {
ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::NIL, "parse_file", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::ARRAY, "extracted_strings")));
ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::NIL, "parse_file", PropertyInfo(Variant::STRING, "path"), PropertyInfo(Variant::ARRAY, "msgids"), PropertyInfo(Variant::ARRAY, "msgids_context_plural")));
ClassDB::add_virtual_method(get_class_static(), MethodInfo(Variant::ARRAY, "get_recognized_extensions"));
}

View file

@ -41,7 +41,7 @@ protected:
static void _bind_methods();
public:
virtual Error parse_file(const String &p_path, Vector<String> *r_extracted_strings);
virtual Error parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural);
virtual void get_recognized_extensions(List<String> *r_extensions) const;
};

View file

@ -37,7 +37,7 @@ void PackedSceneEditorTranslationParserPlugin::get_recognized_extensions(List<St
ResourceLoader::get_recognized_extensions_for_type("PackedScene", r_extensions);
}
Error PackedSceneEditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_extracted_strings) {
Error PackedSceneEditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) {
// Parse specific scene Node's properties (see in constructor) that are auto-translated by the engine when set. E.g Label's text property.
// These properties are translated with the tr() function in the C++ code when being set or updated.
@ -71,8 +71,10 @@ Error PackedSceneEditorTranslationParserPlugin::parse_file(const String &p_path,
String extension = s->get_language()->get_extension();
if (EditorTranslationParser::get_singleton()->can_parse(extension)) {
Vector<String> temp;
EditorTranslationParser::get_singleton()->get_parser(extension)->parse_file(s->get_path(), &temp);
Vector<Vector<String>> ids_context_plural;
EditorTranslationParser::get_singleton()->get_parser(extension)->parse_file(s->get_path(), &temp, &ids_context_plural);
parsed_strings.append_array(temp);
r_ids_ctx_plural->append_array(ids_context_plural);
}
} else if (property_name == "filters") {
// Extract FileDialog's filters property with values in format "*.png ; PNG Images","*.gd ; GDScript Files".
@ -93,7 +95,7 @@ Error PackedSceneEditorTranslationParserPlugin::parse_file(const String &p_path,
}
}
r_extracted_strings->append_array(parsed_strings);
r_ids->append_array(parsed_strings);
return OK;
}

View file

@ -40,7 +40,7 @@ class PackedSceneEditorTranslationParserPlugin : public EditorTranslationParserP
Set<String> lookup_properties;
public:
virtual Error parse_file(const String &p_path, Vector<String> *r_extracted_strings) override;
virtual Error parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) override;
virtual void get_recognized_extensions(List<String> *r_extensions) const override;
PackedSceneEditorTranslationParserPlugin();

View file

@ -31,23 +31,25 @@
#include "pot_generator.h"
#include "core/error_macros.h"
#include "core/os/file_access.h"
#include "core/project_settings.h"
#include "editor_translation_parser.h"
#include "plugins/packed_scene_translation_parser_plugin.h"
POTGenerator *POTGenerator::singleton = nullptr;
//#define DEBUG_POT
#ifdef DEBUG_POT
void _print_all_translation_strings(const OrderedHashMap<String, Set<String>> &p_all_translation_strings) {
for (auto E_pair = p_all_translation_strings.front(); E_pair; E_pair = E_pair.next()) {
String msg = static_cast<String>(E_pair.key()) + " : ";
for (Set<String>::Element *E = E_pair.value().front(); E; E = E->next()) {
msg += E->get() + " ";
void POTGenerator::_print_all_translation_strings() {
for (auto E = all_translation_strings.front(); E; E = E.next()) {
Vector<MsgidData> v_md = all_translation_strings[E.key()];
for (int i = 0; i < v_md.size(); i++) {
print_line("++++++");
print_line("msgid: " + E.key());
print_line("context: " + v_md[i].ctx);
print_line("msgid_plural: " + v_md[i].plural);
for (Set<String>::Element *E = v_md[i].locations.front(); E; E = E->next()) {
print_line("location: " + E->get());
}
}
print_line(msg);
}
}
#endif
@ -65,27 +67,27 @@ void POTGenerator::generate_pot(const String &p_file) {
// Collect all translatable strings according to files order in "POT Generation" setting.
for (int i = 0; i < files.size(); i++) {
Vector<String> translation_strings;
Vector<String> msgids;
Vector<Vector<String>> msgids_context_plural;
String file_path = files[i];
String file_extension = file_path.get_extension();
if (EditorTranslationParser::get_singleton()->can_parse(file_extension)) {
EditorTranslationParser::get_singleton()->get_parser(file_extension)->parse_file(file_path, &translation_strings);
EditorTranslationParser::get_singleton()->get_parser(file_extension)->parse_file(file_path, &msgids, &msgids_context_plural);
} else {
ERR_PRINT("Unrecognized file extension " + file_extension + " in generate_pot()");
return;
}
// Store translation strings parsed in this iteration along with their corresponding source file - to write into POT later on.
for (int j = 0; j < translation_strings.size(); j++) {
all_translation_strings[translation_strings[j]].insert(file_path);
for (int j = 0; j < msgids_context_plural.size(); j++) {
Vector<String> entry = msgids_context_plural[j];
_add_new_msgid(entry[0], entry[1], entry[2], file_path);
}
for (int j = 0; j < msgids.size(); j++) {
_add_new_msgid(msgids[j], "", "", file_path);
}
}
#ifdef DEBUG_POT
_print_all_translation_strings(all_translation_strings);
#endif
_write_to_pot(p_file);
}
@ -119,37 +121,88 @@ void POTGenerator::_write_to_pot(const String &p_file) {
file->store_string(header);
for (OrderedHashMap<String, Set<String>>::Element E_pair = all_translation_strings.front(); E_pair; E_pair = E_pair.next()) {
String msg = E_pair.key();
for (OrderedHashMap<String, Vector<MsgidData>>::Element E_pair = all_translation_strings.front(); E_pair; E_pair = E_pair.next()) {
String msgid = E_pair.key();
Vector<MsgidData> v_msgid_data = E_pair.value();
for (int i = 0; i < v_msgid_data.size(); i++) {
String context = v_msgid_data[i].ctx;
String plural = v_msgid_data[i].plural;
const Set<String> &locations = v_msgid_data[i].locations;
// Write file locations.
for (Set<String>::Element *E = E_pair.value().front(); E; E = E->next()) {
file->store_line("#: " + E->get().trim_prefix("res://"));
}
// Write file locations.
for (Set<String>::Element *E = locations.front(); E; E = E->next()) {
file->store_line("#: " + E->get().trim_prefix("res://"));
}
// Split \\n and \n.
Vector<String> temp = msg.split("\\n");
Vector<String> msg_lines;
for (int i = 0; i < temp.size(); i++) {
msg_lines.append_array(temp[i].split("\n"));
if (i < temp.size() - 1) {
// Add \n.
msg_lines.set(msg_lines.size() - 1, msg_lines[msg_lines.size() - 1] + "\\n");
// Write context.
if (!context.empty()) {
file->store_line("msgctxt \"" + context + "\"");
}
// Write msgid.
_write_msgid(file, msgid, false);
// Write msgid_plural
if (!plural.empty()) {
_write_msgid(file, plural, true);
file->store_line("msgstr[0] \"\"");
file->store_line("msgstr[1] \"\"\n");
} else {
file->store_line("msgstr \"\"\n");
}
}
// Write msgid.
file->store_string("msgid ");
for (int i = 0; i < msg_lines.size(); i++) {
file->store_line("\"" + msg_lines[i] + "\"");
}
file->store_line("msgstr \"\"\n");
}
file->close();
}
void POTGenerator::_write_msgid(FileAccess *r_file, const String &p_id, bool p_plural) {
// Split \\n and \n.
Vector<String> temp = p_id.split("\\n");
Vector<String> msg_lines;
for (int i = 0; i < temp.size(); i++) {
msg_lines.append_array(temp[i].split("\n"));
if (i < temp.size() - 1) {
// Add \n.
msg_lines.set(msg_lines.size() - 1, msg_lines[msg_lines.size() - 1] + "\\n");
}
}
if (p_plural) {
r_file->store_string("msgid_plural ");
} else {
r_file->store_string("msgid ");
}
for (int i = 0; i < msg_lines.size(); i++) {
r_file->store_line("\"" + msg_lines[i] + "\"");
}
}
void POTGenerator::_add_new_msgid(const String &p_msgid, const String &p_context, const String &p_plural, const String &p_location) {
// Insert new location if msgid under same context exists already.
if (all_translation_strings.has(p_msgid)) {
Vector<MsgidData> &v_mdata = all_translation_strings[p_msgid];
for (int i = 0; i < v_mdata.size(); i++) {
if (v_mdata[i].ctx == p_context) {
if (!v_mdata[i].plural.empty() && !p_plural.empty() && v_mdata[i].plural != p_plural) {
WARN_PRINT("Redefinition of plural message (msgid_plural), under the same message (msgid) and context (msgctxt)");
}
v_mdata.write[i].locations.insert(p_location);
return;
}
}
}
// Add a new entry of msgid, context, plural and location - context and plural might be empty if the inserted msgid doesn't associated
// context or plurals.
MsgidData mdata;
mdata.ctx = p_context;
mdata.plural = p_plural;
mdata.locations.insert(p_location);
all_translation_strings[p_msgid].push_back(mdata);
}
POTGenerator *POTGenerator::get_singleton() {
if (!singleton) {
singleton = memnew(POTGenerator);

View file

@ -32,14 +32,29 @@
#define POT_GENERATOR_H
#include "core/ordered_hash_map.h"
#include "core/os/file_access.h"
#include "core/set.h"
//#define DEBUG_POT
class POTGenerator {
static POTGenerator *singleton;
// Stores all translatable strings and the source files containing them.
OrderedHashMap<String, Set<String>> all_translation_strings;
struct MsgidData {
String ctx;
String plural;
Set<String> locations;
};
// Store msgid as key and the additional data around the msgid - if it's under a context, has plurals and its file locations.
OrderedHashMap<String, Vector<MsgidData>> all_translation_strings;
void _write_to_pot(const String &p_file);
void _write_msgid(FileAccess *r_file, const String &p_id, bool p_plural);
void _add_new_msgid(const String &p_msgid, const String &p_context, const String &p_plural, const String &p_location);
#ifdef DEBUG_POT
void _print_all_translation_strings();
#endif
public:
static POTGenerator *get_singleton();

View file

@ -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_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 += "\n" + TTR("Click to show signals dock.");
}
Ref<Texture2D> icon_temp;
auto signal_temp = BUTTON_SIGNALS;
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) {
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) {
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);
}
}

View file

@ -33,6 +33,7 @@ matches.sort()
unique_str = []
unique_loc = {}
ctx_group = {} # Store msgctx, msg, and locations.
main_po = """
# LANGUAGE translation of the Godot Engine editor.
# Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur.
@ -52,6 +53,34 @@ msgstr ""
"""
def _write_message(msgctx, msg, msg_plural, location):
global main_po
main_po += "\n#: " + location + "\n"
if msgctx != "":
main_po += 'msgctxt "' + msgctx + '"\n'
main_po += 'msgid "' + msg + '"\n'
if msg_plural != "":
main_po += 'msgid_plural "' + msg_plural + '"\n'
main_po += 'msgstr[0] ""\n'
main_po += 'msgstr[1] ""\n'
else:
main_po += 'msgstr ""\n'
def _add_additional_location(msgctx, msg, location):
global main_po
# Add additional location to previous occurrence
msg_pos = -1
if msgctx != "":
msg_pos = main_po.find('\nmsgctxt "' + msgctx + '"\nmsgid "' + msg + '"')
else:
msg_pos = main_po.find('\nmsgid "' + msg + '"')
if msg_pos == -1:
print("Someone apparently thought writing Python was as easy as GDScript. Ping Akien.")
main_po = main_po[:msg_pos] + " " + location + main_po[msg_pos:]
def process_file(f, fname):
global main_po, unique_str, unique_loc
@ -60,10 +89,11 @@ def process_file(f, fname):
lc = 1
while l:
patterns = ['RTR("', 'TTR("', 'TTRC("']
patterns = ['RTR("', 'TTR("', 'TTRC("', 'TTRN("', 'RTRN("']
idx = 0
pos = 0
while pos >= 0:
# Loop until a pattern is found. If not, next line.
pos = l.find(patterns[idx], pos)
if pos == -1:
if idx < len(patterns) - 1:
@ -72,29 +102,64 @@ def process_file(f, fname):
continue
pos += len(patterns[idx])
# Read msg until "
msg = ""
while pos < len(l) and (l[pos] != '"' or l[pos - 1] == "\\"):
msg += l[pos]
pos += 1
# Read plural.
msg_plural = ""
if patterns[idx] in ['TTRN("', 'RTRN("']:
pos = l.find('"', pos + 1)
pos += 1
while pos < len(l) and (l[pos] != '"' or l[pos - 1] == "\\"):
msg_plural += l[pos]
pos += 1
# Read context.
msgctx = ""
pos += 1
read_ctx = False
while pos < len(l):
if l[pos] == ")":
break
elif l[pos] == '"':
read_ctx = True
break
pos += 1
pos += 1
if read_ctx:
while pos < len(l) and (l[pos] != '"' or l[pos - 1] == "\\"):
msgctx += l[pos]
pos += 1
# File location.
location = os.path.relpath(fname).replace("\\", "/")
if line_nb:
location += ":" + str(lc)
if not msg in unique_str:
main_po += "\n#: " + location + "\n"
main_po += 'msgid "' + msg + '"\n'
main_po += 'msgstr ""\n'
unique_str.append(msg)
unique_loc[msg] = [location]
elif not location in unique_loc[msg]:
# Add additional location to previous occurrence too
msg_pos = main_po.find('\nmsgid "' + msg + '"')
if msg_pos == -1:
print("Someone apparently thought writing Python was as easy as GDScript. Ping Akien.")
main_po = main_po[:msg_pos] + " " + location + main_po[msg_pos:]
unique_loc[msg].append(location)
if msgctx != "":
# If it's a new context or a new message within an existing context, then write new msgid.
# Else add location to existing msgid.
if not msgctx in ctx_group:
_write_message(msgctx, msg, msg_plural, location)
ctx_group[msgctx] = {msg: [location]}
elif not msg in ctx_group[msgctx]:
_write_message(msgctx, msg, msg_plural, location)
ctx_group[msgctx][msg] = [location]
elif not location in ctx_group[msgctx][msg]:
_add_additional_location(msgctx, msg, location)
ctx_group[msgctx][msg].append(location)
else:
if not msg in unique_str:
_write_message(msgctx, msg, msg_plural, location)
unique_str.append(msg)
unique_loc[msg] = [location]
elif not location in unique_loc[msg]:
_add_additional_location(msgctx, msg, location)
unique_loc[msg].append(location)
l = f.readline()
lc += 1
@ -102,7 +167,7 @@ def process_file(f, fname):
print("Updating the editor.pot template...")
for fname in matches:
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:

View file

@ -37,9 +37,11 @@ void GDScriptEditorTranslationParserPlugin::get_recognized_extensions(List<Strin
GDScriptLanguage::get_singleton()->get_recognized_extensions(r_extensions);
}
Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_extracted_strings) {
// Parse and match all GDScript function API that involves translation string.
// E.g get_node("Label").text = "something", var test = tr("something"), "something" will be matched and collected.
Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) {
// Extract all translatable strings using the parsed tree from GDSriptParser.
// The strategy is to find all ExpressionNode and AssignmentNode from the tree and extract strings if relevant, i.e
// Search strings in ExpressionNode -> CallNode -> tr(), set_text(), set_placeholder() etc.
// Search strings in AssignmentNode -> text = "__", hint_tooltip = "__" etc.
Error err;
RES loaded_res = ResourceLoader::load(p_path, "", false, &err);
@ -48,108 +50,302 @@ Error GDScriptEditorTranslationParserPlugin::parse_file(const String &p_path, Ve
return err;
}
ids = r_ids;
ids_ctx_plural = r_ids_ctx_plural;
Ref<GDScript> gdscript = loaded_res;
String source_code = gdscript->get_source_code();
Vector<String> parsed_strings;
// Search translation strings with RegEx.
regex.clear();
regex.compile(String("|").join(patterns));
Array results = regex.search_all(source_code);
_get_captured_strings(results, &parsed_strings);
// Special handling for FileDialog.
Vector<String> temp;
_parse_file_dialog(source_code, &temp);
parsed_strings.append_array(temp);
// Filter out / and +
String filter = "(?:\\\\\\n|\"[\\s\\\\]*\\+\\s*\")";
regex.clear();
regex.compile(filter);
for (int i = 0; i < parsed_strings.size(); i++) {
parsed_strings.set(i, regex.sub(parsed_strings[i], "", true));
GDScriptParser parser;
err = parser.parse(source_code, p_path, false);
if (err != OK) {
ERR_PRINT("Failed to parse with GDScript with GDScriptParser.");
return err;
}
r_extracted_strings->append_array(parsed_strings);
// Traverse through the parsed tree from GDScriptParser.
GDScriptParser::ClassNode *c = parser.get_tree();
_traverse_class(c);
return OK;
}
void GDScriptEditorTranslationParserPlugin::_parse_file_dialog(const String &p_source_code, Vector<String> *r_output) {
// FileDialog API has the form .filters = PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"]).
// First filter: Get "*.png ; PNG Images", "*.gd ; GDScript Files" from PackedStringArray.
regex.clear();
regex.compile(String("|").join(file_dialog_patterns));
Array results = regex.search_all(p_source_code);
Vector<String> temp;
_get_captured_strings(results, &temp);
String captured_strings = String(",").join(temp);
// Second filter: Get the texts after semicolon from "*.png ; PNG Images","*.gd ; GDScript Files".
String second_filter = "\"[^;]+;" + text + "\"";
regex.clear();
regex.compile(second_filter);
results = regex.search_all(captured_strings);
_get_captured_strings(results, r_output);
for (int i = 0; i < r_output->size(); i++) {
r_output->set(i, r_output->get(i).strip_edges());
void GDScriptEditorTranslationParserPlugin::_traverse_class(const GDScriptParser::ClassNode *p_class) {
for (int i = 0; i < p_class->members.size(); i++) {
const GDScriptParser::ClassNode::Member &m = p_class->members[i];
// There are 7 types of Member, but only class, function and variable can contain translatable strings.
switch (m.type) {
case GDScriptParser::ClassNode::Member::CLASS:
_traverse_class(m.m_class);
break;
case GDScriptParser::ClassNode::Member::FUNCTION:
_traverse_function(m.function);
break;
case GDScriptParser::ClassNode::Member::VARIABLE:
_read_variable(m.variable);
break;
default:
break;
}
}
}
void GDScriptEditorTranslationParserPlugin::_get_captured_strings(const Array &p_results, Vector<String> *r_output) {
Ref<RegExMatch> result;
for (int i = 0; i < p_results.size(); i++) {
result = p_results[i];
for (int j = 0; j < result->get_group_count(); j++) {
String s = result->get_string(j + 1);
// Prevent reading text with only spaces.
if (!s.strip_edges().empty()) {
r_output->push_back(s);
void GDScriptEditorTranslationParserPlugin::_traverse_function(const GDScriptParser::FunctionNode *p_func) {
_traverse_block(p_func->body);
}
void GDScriptEditorTranslationParserPlugin::_read_variable(const GDScriptParser::VariableNode *p_var) {
_assess_expression(p_var->initializer);
}
void GDScriptEditorTranslationParserPlugin::_traverse_block(const GDScriptParser::SuiteNode *p_suite) {
if (!p_suite) {
return;
}
const Vector<GDScriptParser::Node *> &statements = p_suite->statements;
for (int i = 0; i < statements.size(); i++) {
GDScriptParser::Node *statement = statements[i];
// Statements with Node type constant, break, continue, pass, breakpoint are skipped because they can't contain translatable strings.
switch (statement->type) {
case GDScriptParser::Node::VARIABLE:
_assess_expression(static_cast<GDScriptParser::VariableNode *>(statement)->initializer);
break;
case GDScriptParser::Node::IF: {
GDScriptParser::IfNode *if_node = static_cast<GDScriptParser::IfNode *>(statement);
_assess_expression(if_node->condition);
//FIXME : if the elif logic is changed in GDScriptParser, then this probably will have to change as well. See GDScriptParser::TreePrinter::print_if().
_traverse_block(if_node->true_block);
_traverse_block(if_node->false_block);
break;
}
case GDScriptParser::Node::FOR: {
GDScriptParser::ForNode *for_node = static_cast<GDScriptParser::ForNode *>(statement);
_assess_expression(for_node->list);
_traverse_block(for_node->loop);
break;
}
case GDScriptParser::Node::WHILE: {
GDScriptParser::WhileNode *while_node = static_cast<GDScriptParser::WhileNode *>(statement);
_assess_expression(while_node->condition);
_traverse_block(while_node->loop);
break;
}
case GDScriptParser::Node::MATCH: {
GDScriptParser::MatchNode *match_node = static_cast<GDScriptParser::MatchNode *>(statement);
_assess_expression(match_node->test);
for (int j = 0; j < match_node->branches.size(); j++) {
_traverse_block(match_node->branches[j]->block);
}
break;
}
case GDScriptParser::Node::RETURN:
_assess_expression(static_cast<GDScriptParser::ReturnNode *>(statement)->return_value);
break;
case GDScriptParser::Node::ASSERT:
_assess_expression((static_cast<GDScriptParser::AssertNode *>(statement))->condition);
break;
case GDScriptParser::Node::ASSIGNMENT:
_assess_assignment(static_cast<GDScriptParser::AssignmentNode *>(statement));
break;
default:
if (statement->is_expression()) {
_assess_expression(static_cast<GDScriptParser::ExpressionNode *>(statement));
}
break;
}
}
}
void GDScriptEditorTranslationParserPlugin::_assess_expression(GDScriptParser::ExpressionNode *p_expression) {
// Explore all ExpressionNodes to find CallNodes which contain translation strings, such as tr(), set_text() etc.
// tr() can be embedded quite deep within multiple ExpressionNodes so need to dig down to search through all ExpressionNodes.
if (!p_expression) {
return;
}
// ExpressionNode of type await, cast, get_node, identifier, literal, preload, self, subscript, unary are ignored as they can't be CallNode
// containing translation strings.
switch (p_expression->type) {
case GDScriptParser::Node::ARRAY: {
GDScriptParser::ArrayNode *array_node = static_cast<GDScriptParser::ArrayNode *>(p_expression);
for (int i = 0; i < array_node->elements.size(); i++) {
_assess_expression(array_node->elements[i]);
}
break;
}
case GDScriptParser::Node::ASSIGNMENT:
_assess_assignment(static_cast<GDScriptParser::AssignmentNode *>(p_expression));
break;
case GDScriptParser::Node::BINARY_OPERATOR: {
GDScriptParser::BinaryOpNode *binary_op_node = static_cast<GDScriptParser::BinaryOpNode *>(p_expression);
_assess_expression(binary_op_node->left_operand);
_assess_expression(binary_op_node->right_operand);
break;
}
case GDScriptParser::Node::CALL: {
GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(p_expression);
_extract_from_call(call_node);
for (int i = 0; i < call_node->arguments.size(); i++) {
_assess_expression(call_node->arguments[i]);
}
} break;
case GDScriptParser::Node::DICTIONARY: {
GDScriptParser::DictionaryNode *dict_node = static_cast<GDScriptParser::DictionaryNode *>(p_expression);
for (int i = 0; i < dict_node->elements.size(); i++) {
_assess_expression(dict_node->elements[i].key);
_assess_expression(dict_node->elements[i].value);
}
break;
}
case GDScriptParser::Node::TERNARY_OPERATOR: {
GDScriptParser::TernaryOpNode *ternary_op_node = static_cast<GDScriptParser::TernaryOpNode *>(p_expression);
_assess_expression(ternary_op_node->condition);
_assess_expression(ternary_op_node->true_expr);
_assess_expression(ternary_op_node->false_expr);
break;
}
default:
break;
}
}
void GDScriptEditorTranslationParserPlugin::_assess_assignment(GDScriptParser::AssignmentNode *p_assignment) {
// Extract the translatable strings coming from assignments. For example, get_node("Label").text = "____"
StringName assignee_name;
if (p_assignment->assignee->type == GDScriptParser::Node::IDENTIFIER) {
assignee_name = static_cast<GDScriptParser::IdentifierNode *>(p_assignment->assignee)->name;
} else if (p_assignment->assignee->type == GDScriptParser::Node::SUBSCRIPT) {
assignee_name = static_cast<GDScriptParser::SubscriptNode *>(p_assignment->assignee)->attribute->name;
}
if (assignment_patterns.has(assignee_name) && p_assignment->assigned_value->type == GDScriptParser::Node::LITERAL) {
// If the assignment is towards one of the extract patterns (text, hint_tooltip etc.), and the value is a string literal, we collect the string.
ids->push_back(static_cast<GDScriptParser::LiteralNode *>(p_assignment->assigned_value)->value);
} else if (assignee_name == fd_filters && p_assignment->assigned_value->type == GDScriptParser::Node::CALL) {
// FileDialog.filters accepts assignment in the form of PackedStringArray. For example,
// get_node("FileDialog").filters = PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"]).
GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(p_assignment->assigned_value);
if (call_node->arguments[0]->type == GDScriptParser::Node::ARRAY) {
GDScriptParser::ArrayNode *array_node = static_cast<GDScriptParser::ArrayNode *>(call_node->arguments[0]);
// Extract the name in "extension ; name" of PackedStringArray.
for (int i = 0; i < array_node->elements.size(); i++) {
_extract_fd_literals(array_node->elements[i]);
}
}
} else {
// If the assignee is not in extract patterns or the assigned_value is not Literal type, try to see if the assigned_value contains tr().
_assess_expression(p_assignment->assigned_value);
}
}
void GDScriptEditorTranslationParserPlugin::_extract_from_call(GDScriptParser::CallNode *p_call) {
// Extract the translatable strings coming from function calls. For example:
// tr("___"), get_node("Label").set_text("____"), get_node("LineEdit").set_placeholder("____").
StringName function_name = p_call->function_name;
// Variables for extracting tr() and tr_n().
Vector<String> id_ctx_plural;
id_ctx_plural.resize(3);
bool extract_id_ctx_plural = true;
if (function_name == tr_func) {
// Extract from tr(id, ctx).
for (int i = 0; i < p_call->arguments.size(); i++) {
if (p_call->arguments[i]->type == GDScriptParser::Node::LITERAL) {
id_ctx_plural.write[i] = static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[i])->value;
} else {
// Avoid adding something like tr("Flying dragon", var_context_level_1). We want to extract both id and context together.
extract_id_ctx_plural = false;
}
}
if (extract_id_ctx_plural) {
ids_ctx_plural->push_back(id_ctx_plural);
}
} else if (function_name == trn_func) {
// Extract from tr_n(id, plural, n, ctx).
Vector<int> indices;
indices.push_back(0);
indices.push_back(3);
indices.push_back(1);
for (int i = 0; i < indices.size(); i++) {
if (indices[i] >= p_call->arguments.size()) {
continue;
}
if (p_call->arguments[indices[i]]->type == GDScriptParser::Node::LITERAL) {
id_ctx_plural.write[i] = static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[indices[i]])->value;
} else {
extract_id_ctx_plural = false;
}
}
if (extract_id_ctx_plural) {
ids_ctx_plural->push_back(id_ctx_plural);
}
} else if (first_arg_patterns.has(function_name)) {
// Extracting argument with only string literals. In other words, not extracting something like set_text("hello " + some_var).
if (p_call->arguments[0]->type == GDScriptParser::Node::LITERAL) {
ids->push_back(static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[0])->value);
}
} else if (second_arg_patterns.has(function_name)) {
if (p_call->arguments[1]->type == GDScriptParser::Node::LITERAL) {
ids->push_back(static_cast<GDScriptParser::LiteralNode *>(p_call->arguments[1])->value);
}
} else if (function_name == fd_add_filter) {
// Extract the 'JPE Images' in this example - get_node("FileDialog").add_filter("*.jpg; JPE Images").
_extract_fd_literals(p_call->arguments[0]);
} else if (function_name == fd_set_filter && p_call->arguments[0]->type == GDScriptParser::Node::CALL) {
// FileDialog.set_filters() accepts assignment in the form of PackedStringArray. For example,
// get_node("FileDialog").set_filters( PackedStringArray(["*.png ; PNG Images","*.gd ; GDScript Files"])).
GDScriptParser::CallNode *call_node = static_cast<GDScriptParser::CallNode *>(p_call->arguments[0]);
if (call_node->arguments[0]->type == GDScriptParser::Node::ARRAY) {
GDScriptParser::ArrayNode *array_node = static_cast<GDScriptParser::ArrayNode *>(call_node->arguments[0]);
for (int i = 0; i < array_node->elements.size(); i++) {
_extract_fd_literals(array_node->elements[i]);
}
}
}
}
GDScriptEditorTranslationParserPlugin::GDScriptEditorTranslationParserPlugin() {
// Regex search pattern templates.
// The extra complication in the regex pattern is to ensure that the matching works when users write over multiple lines, use tabs etc.
const String dot = "\\.[\\s\\\\]*";
const String str_assign_template = "[\\s\\\\]*=[\\s\\\\]*\"" + text + "\"";
const String first_arg_template = "[\\s\\\\]*\\([\\s\\\\]*\"" + text + "\"[\\s\\S]*?\\)";
const String second_arg_template = "[\\s\\\\]*\\([\\s\\S]+?,[\\s\\\\]*\"" + text + "\"[\\s\\S]*?\\)";
void GDScriptEditorTranslationParserPlugin::_extract_fd_literals(GDScriptParser::ExpressionNode *p_expression) {
// Extract the name in "extension ; name".
// Common patterns.
patterns.push_back("tr" + first_arg_template);
patterns.push_back(dot + "text" + str_assign_template);
patterns.push_back(dot + "placeholder_text" + str_assign_template);
patterns.push_back(dot + "hint_tooltip" + str_assign_template);
patterns.push_back(dot + "set_text" + first_arg_template);
patterns.push_back(dot + "set_tooltip" + first_arg_template);
patterns.push_back(dot + "set_placeholder" + first_arg_template);
// Tabs and TabContainer API.
patterns.push_back(dot + "set_tab_title" + second_arg_template);
patterns.push_back(dot + "add_tab" + first_arg_template);
// PopupMenu API.
patterns.push_back(dot + "add_check_item" + first_arg_template);
patterns.push_back(dot + "add_icon_check_item" + second_arg_template);
patterns.push_back(dot + "add_icon_item" + second_arg_template);
patterns.push_back(dot + "add_icon_radio_check_item" + second_arg_template);
patterns.push_back(dot + "add_item" + first_arg_template);
patterns.push_back(dot + "add_multistate_item" + first_arg_template);
patterns.push_back(dot + "add_radio_check_item" + first_arg_template);
patterns.push_back(dot + "add_separator" + first_arg_template);
patterns.push_back(dot + "add_submenu_item" + first_arg_template);
patterns.push_back(dot + "set_item_text" + second_arg_template);
//patterns.push_back(dot + "set_item_tooltip" + second_arg_template); //no tr() behind this function. might be bug.
// FileDialog API - special case.
const String fd_text = "((?:[\\s\\\\]*\"(?:[^\"\\\\]|\\\\[\\s\\S])*(?:\"[\\s\\\\]*\\+[\\s\\\\]*\"(?:[^\"\\\\]|\\\\[\\s\\S])*)*\"[\\s\\\\]*,?)*)";
const String packed_string_array = "[\\s\\\\]*PackedStringArray[\\s\\\\]*\\([\\s\\\\]*\\[" + fd_text + "\\][\\s\\\\]*\\)";
file_dialog_patterns.push_back(dot + "add_filter[\\s\\\\]*\\(" + fd_text + "[\\s\\\\]*\\)");
file_dialog_patterns.push_back(dot + "filters[\\s\\\\]*=" + packed_string_array);
file_dialog_patterns.push_back(dot + "set_filters[\\s\\\\]*\\(" + packed_string_array + "[\\s\\\\]*\\)");
if (p_expression->type == GDScriptParser::Node::LITERAL) {
String arg_val = String(static_cast<GDScriptParser::LiteralNode *>(p_expression)->value);
PackedStringArray arr = arg_val.split(";", true);
if (arr.size() != 2) {
ERR_PRINT("Argument for setting FileDialog has bad format.");
return;
}
ids->push_back(arr[1].strip_edges());
}
}
GDScriptEditorTranslationParserPlugin::GDScriptEditorTranslationParserPlugin() {
assignment_patterns.insert("text");
assignment_patterns.insert("placeholder_text");
assignment_patterns.insert("hint_tooltip");
first_arg_patterns.insert("set_text");
first_arg_patterns.insert("set_tooltip");
first_arg_patterns.insert("set_placeholder");
first_arg_patterns.insert("add_tab");
first_arg_patterns.insert("add_check_item");
first_arg_patterns.insert("add_item");
first_arg_patterns.insert("add_multistate_item");
first_arg_patterns.insert("add_radio_check_item");
first_arg_patterns.insert("add_separator");
first_arg_patterns.insert("add_submenu_item");
second_arg_patterns.insert("set_tab_title");
second_arg_patterns.insert("add_icon_check_item");
second_arg_patterns.insert("add_icon_item");
second_arg_patterns.insert("add_icon_radio_check_item");
second_arg_patterns.insert("set_item_text");
}

View file

@ -31,23 +31,40 @@
#ifndef GDSCRIPT_TRANSLATION_PARSER_PLUGIN_H
#define GDSCRIPT_TRANSLATION_PARSER_PLUGIN_H
#include "core/set.h"
#include "editor/editor_translation_parser.h"
#include "modules/gdscript/gdscript_parser.h"
#include "modules/regex/regex.h"
class GDScriptEditorTranslationParserPlugin : public EditorTranslationParserPlugin {
GDCLASS(GDScriptEditorTranslationParserPlugin, EditorTranslationParserPlugin);
// Regex and search patterns that are used to match translation strings.
const String text = "((?:[^\"\\\\]|\\\\[\\s\\S])*(?:\"[\\s\\\\]*\\+[\\s\\\\]*\"(?:[^\"\\\\]|\\\\[\\s\\S])*)*)";
RegEx regex;
Vector<String> patterns;
Vector<String> file_dialog_patterns;
Vector<String> *ids;
Vector<Vector<String>> *ids_ctx_plural;
void _parse_file_dialog(const String &p_source_code, Vector<String> *r_output);
void _get_captured_strings(const Array &p_results, Vector<String> *r_output);
// List of patterns used for extracting translation strings.
StringName tr_func = "tr";
StringName trn_func = "tr_n";
Set<StringName> assignment_patterns;
Set<StringName> first_arg_patterns;
Set<StringName> second_arg_patterns;
// FileDialog patterns.
StringName fd_add_filter = "add_filter";
StringName fd_set_filter = "set_filters";
StringName fd_filters = "filters";
void _traverse_class(const GDScriptParser::ClassNode *p_class);
void _traverse_function(const GDScriptParser::FunctionNode *p_func);
void _traverse_block(const GDScriptParser::SuiteNode *p_suite);
void _read_variable(const GDScriptParser::VariableNode *p_var);
void _assess_expression(GDScriptParser::ExpressionNode *p_expression);
void _assess_assignment(GDScriptParser::AssignmentNode *p_assignment);
void _extract_from_call(GDScriptParser::CallNode *p_call);
void _extract_fd_literals(GDScriptParser::ExpressionNode *p_expression);
public:
virtual Error parse_file(const String &p_path, Vector<String> *r_extracted_strings) override;
virtual Error parse_file(const String &p_path, Vector<String> *r_ids, Vector<Vector<String>> *r_ids_ctx_plural) override;
virtual void get_recognized_extensions(List<String> *r_extensions) const override;
GDScriptEditorTranslationParserPlugin();