Skip to content

Commit

Permalink
Merge pull request #40443 from SkyLucilfer/PluralsSupport
Browse files Browse the repository at this point in the history
Added plurals and context support to Translation
  • Loading branch information
akien-mga authored Aug 25, 2020
2 parents e968109 + ce3461d commit 9d8f349
Show file tree
Hide file tree
Showing 25 changed files with 1,417 additions and 302 deletions.
11 changes: 10 additions & 1 deletion core/compressed_translation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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"));
Expand Down
3 changes: 2 additions & 1 deletion core/compressed_translation.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ class PHashTranslation : public Translation {
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() {}
Expand Down
110 changes: 99 additions & 11 deletions core/io/translation_loader_po.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
Expand All @@ -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) {
Expand Down Expand Up @@ -146,23 +221,36 @@ 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++;
}

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 + ".");
Expand Down
17 changes: 14 additions & 3 deletions core/object.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);

Expand Down
3 changes: 2 additions & 1 deletion core/object.h
Original file line number Diff line number Diff line change
Expand Up @@ -719,7 +719,8 @@ class Object {

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;
Expand Down
Loading

0 comments on commit 9d8f349

Please sign in to comment.