Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added plurals and context support to Translation #40443

Merged
merged 4 commits into from
Aug 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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();
SkyLucilfer marked this conversation as resolved.
Show resolved Hide resolved
} 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