Updated Translation architecture to have TranslationPO, did some commit fixes and updated class Reference.
This commit is contained in:
parent
396f2eee82
commit
0ef758eaee
|
@ -43,6 +43,8 @@ struct _PHashTranslationCmp {
|
|||
};
|
||||
|
||||
void PHashTranslation::generate(const Ref<Translation> &p_from) {
|
||||
// This method compresses a Translation instance.
|
||||
// Right now it doesn't handle context or plurals, so Translation subclasses using plurals or context (i.e TranslationPO) shouldn't be compressed.
|
||||
#ifdef TOOLS_ENABLED
|
||||
List<StringName> keys;
|
||||
p_from->get_message_list(&keys);
|
||||
|
@ -213,9 +215,7 @@ bool PHashTranslation::_get(const StringName &p_name, Variant &r_ret) const {
|
|||
}
|
||||
|
||||
StringName PHashTranslation::get_message(const StringName &p_src_text, const StringName &p_context) const {
|
||||
if (String(p_context) != "") {
|
||||
WARN_PRINT("The use of context is not yet supported in PHashTranslation.");
|
||||
}
|
||||
// p_context passed in is ignore. The use of context is not yet supported in PHashTranslation.
|
||||
|
||||
int htsize = hash_table.size();
|
||||
|
||||
|
@ -272,7 +272,7 @@ StringName PHashTranslation::get_message(const StringName &p_src_text, const Str
|
|||
}
|
||||
|
||||
StringName PHashTranslation::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const {
|
||||
WARN_PRINT("The use of plurals translation is not yet supported in PHashTranslation.");
|
||||
// The use of plurals translation is not yet supported in PHashTranslation.
|
||||
return get_message(p_src_text, p_context);
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
|
||||
#include "core/os/file_access.h"
|
||||
#include "core/translation.h"
|
||||
#include "core/translation_po.h"
|
||||
|
||||
RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) {
|
||||
enum Status {
|
||||
|
@ -39,7 +40,7 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) {
|
|||
STATUS_READING_ID,
|
||||
STATUS_READING_STRING,
|
||||
STATUS_READING_CONTEXT,
|
||||
STATUS_READING_PLURAL
|
||||
STATUS_READING_PLURAL,
|
||||
};
|
||||
|
||||
Status status = STATUS_NONE;
|
||||
|
@ -54,7 +55,7 @@ RES TranslationLoaderPO::load_translation(FileAccess *f, Error *r_error) {
|
|||
*r_error = ERR_FILE_CORRUPT;
|
||||
}
|
||||
|
||||
Ref<Translation> translation = Ref<Translation>(memnew(Translation));
|
||||
Ref<TranslationPO> translation = Ref<TranslationPO>(memnew(TranslationPO));
|
||||
int line = 1;
|
||||
int plural_forms = 0;
|
||||
int plural_index = -1;
|
||||
|
|
|
@ -1432,7 +1432,7 @@ void Object::initialize_class() {
|
|||
initialized = true;
|
||||
}
|
||||
|
||||
StringName Object::tr(const StringName &p_message, const StringName &p_context) const {
|
||||
String Object::tr(const StringName &p_message, const StringName &p_context) const {
|
||||
if (!_can_translate || !TranslationServer::get_singleton()) {
|
||||
return p_message;
|
||||
}
|
||||
|
@ -1444,9 +1444,8 @@ String Object::tr_n(const StringName &p_message, const StringName &p_message_plu
|
|||
// Return message based on English plural rule if translation is not possible.
|
||||
if (p_n == 1) {
|
||||
return p_message;
|
||||
} else {
|
||||
return p_message_plural;
|
||||
}
|
||||
return p_message_plural;
|
||||
}
|
||||
return TranslationServer::get_singleton()->translate_plural(p_message, p_message_plural, p_n, p_context);
|
||||
}
|
||||
|
|
|
@ -719,8 +719,7 @@ public:
|
|||
|
||||
virtual void get_argument_options(const StringName &p_function, int p_idx, List<String> *r_options) const;
|
||||
|
||||
StringName tr(const StringName &p_message, const StringName &p_context = "") const; // translate message (internationalization)
|
||||
////I'm returning as String here because when I test the API, if I return StringName, I need to wrap it with String() to use format string, which is inconvenient.
|
||||
String tr(const StringName &p_message, const StringName &p_context = "") const; // translate message (internationalization)
|
||||
String tr_n(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context = "") const;
|
||||
|
||||
bool _is_queued_for_deletion = false; // set to true by SceneTree::queue_delete()
|
||||
|
|
|
@ -42,41 +42,6 @@
|
|||
// - https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
|
||||
// - https://lh.2xlibre.net/locales/
|
||||
|
||||
#ifdef DEBUG_TRANSLATION
|
||||
void Translation::print_translation_map() {
|
||||
Error err;
|
||||
FileAccess *file = FileAccess::open("translation_map_print_test.txt", FileAccess::WRITE, &err);
|
||||
if (err != OK) {
|
||||
ERR_PRINT("Failed to open translation_map_print_test.txt");
|
||||
return;
|
||||
}
|
||||
|
||||
file->store_line("NPlural : " + String::num_int64(this->get_plural_forms()));
|
||||
file->store_line("Plural rule : " + this->get_plural_rule());
|
||||
file->store_line("");
|
||||
|
||||
List<StringName> context_l;
|
||||
translation_map.get_key_list(&context_l);
|
||||
for (auto E = context_l.front(); E; E = E->next()) {
|
||||
StringName ctx = E->get();
|
||||
file->store_line(" ===== Context: " + String::utf8(String(ctx).utf8()) + " ===== ");
|
||||
const HashMap<StringName, Vector<StringName>> &inner_map = translation_map[ctx];
|
||||
|
||||
List<StringName> id_l;
|
||||
inner_map.get_key_list(&id_l);
|
||||
for (auto E2 = id_l.front(); E2; E2 = E2->next()) {
|
||||
StringName id = E2->get();
|
||||
file->store_line("msgid: " + String::utf8(String(id).utf8()));
|
||||
for (int i = 0; i < inner_map[id].size(); i++) {
|
||||
file->store_line("msgstr[" + String::num_int64(i) + "]: " + String::utf8(String(inner_map[id][i]).utf8()));
|
||||
}
|
||||
file->store_line("");
|
||||
}
|
||||
}
|
||||
file->close();
|
||||
}
|
||||
#endif
|
||||
|
||||
static const char *locale_list[] = {
|
||||
"aa", // Afar
|
||||
"aa_DJ", // Afar (Djibouti)
|
||||
|
@ -830,113 +795,31 @@ static const char *locale_renames[][2] = {
|
|||
///////////////////////////////////////////////
|
||||
|
||||
Dictionary Translation::_get_messages() const {
|
||||
// Return translation_map as a Dictionary.
|
||||
|
||||
Dictionary d;
|
||||
|
||||
List<StringName> context_l;
|
||||
translation_map.get_key_list(&context_l);
|
||||
for (auto E = context_l.front(); E; E = E->next()) {
|
||||
StringName ctx = E->get();
|
||||
const HashMap<StringName, Vector<StringName>> &id_str_map = translation_map[ctx];
|
||||
|
||||
Dictionary d2;
|
||||
List<StringName> id_l;
|
||||
id_str_map.get_key_list(&id_l);
|
||||
// Save list of id and strs associated with a context in a temporary dictionary.
|
||||
for (auto E2 = id_l.front(); E2; E2 = E2->next()) {
|
||||
StringName id = E2->get();
|
||||
d2[id] = id_str_map[id];
|
||||
}
|
||||
|
||||
d[ctx] = d2;
|
||||
for (const Map<StringName, StringName>::Element *E = translation_map.front(); E; E = E->next()) {
|
||||
d[E->key()] = E->value();
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
void Translation::_set_messages(const Dictionary &p_messages) {
|
||||
// Construct translation_map from a Dictionary.
|
||||
|
||||
List<Variant> context_l;
|
||||
p_messages.get_key_list(&context_l);
|
||||
for (auto E = context_l.front(); E; E = E->next()) {
|
||||
StringName ctx = E->get();
|
||||
const Dictionary &id_str_map = p_messages[ctx];
|
||||
|
||||
HashMap<StringName, Vector<StringName>> temp_map;
|
||||
List<Variant> id_l;
|
||||
id_str_map.get_key_list(&id_l);
|
||||
for (auto E2 = id_l.front(); E2; E2 = E2->next()) {
|
||||
StringName id = E2->get();
|
||||
temp_map[id] = id_str_map[id];
|
||||
}
|
||||
|
||||
translation_map[ctx] = temp_map;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<String> Translation::_get_message_list() const {
|
||||
////This one I'm really not sure what the use case of this function is. So I just follow what it does before.
|
||||
// Return all keys in translation_map.
|
||||
|
||||
List<StringName> msgs;
|
||||
get_message_list(&msgs);
|
||||
|
||||
Vector<String> v;
|
||||
for (auto E = msgs.front(); E; E = E->next()) {
|
||||
v.push_back(E->get());
|
||||
Vector<String> msgs;
|
||||
msgs.resize(translation_map.size());
|
||||
int idx = 0;
|
||||
for (const Map<StringName, StringName>::Element *E = translation_map.front(); E; E = E->next()) {
|
||||
msgs.set(idx, E->key());
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
return v;
|
||||
return msgs;
|
||||
}
|
||||
|
||||
int Translation::_get_plural_index(int p_n) const {
|
||||
// Apply plural rule to a p_n passed in, and get a number between [0;number of plural forms)
|
||||
|
||||
Ref<Expression> expr;
|
||||
expr.instance();
|
||||
|
||||
Vector<String> input_name;
|
||||
input_name.push_back("n");
|
||||
|
||||
Array input_val;
|
||||
input_val.push_back(p_n);
|
||||
|
||||
int result = _get_plural_index(plural_rule, input_name, input_val, expr);
|
||||
ERR_FAIL_COND_V_MSG(result < 0, 0, "_get_plural_index() returns a negative number after evaluating a plural rule expression.");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int Translation::_get_plural_index(const String &p_plural_rule, const Vector<String> &p_input_name, const Array &p_input_value, Ref<Expression> &r_expr) const {
|
||||
// Evaluate recursively until we find the first condition that is true.
|
||||
// Some examples of p_plural_rule passed in can have the form:
|
||||
// "n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5" (Arabic)
|
||||
// "n >= 2" (French)
|
||||
// "n != 1" (English)
|
||||
|
||||
// Parse expression.
|
||||
int first_ques_mark = p_plural_rule.find("?");
|
||||
String equi_test = p_plural_rule.substr(0, first_ques_mark);
|
||||
Error err = r_expr->parse(equi_test, p_input_name);
|
||||
ERR_FAIL_COND_V_MSG(err != OK, p_input_value[0], "Cannot parse expression. Error: " + r_expr->get_error_text());
|
||||
|
||||
// Evaluate expression.
|
||||
Variant result = r_expr->execute(p_input_value);
|
||||
ERR_FAIL_COND_V_MSG(r_expr->has_execute_failed(), p_input_value[0], "Cannot evaluate expression.");
|
||||
|
||||
// Base case of recursion. Variant result will either map to a bool or an integer, in both cases returning it will give the correct plural index.
|
||||
if (first_ques_mark == -1) {
|
||||
return result;
|
||||
void Translation::_set_messages(const Dictionary &p_messages) {
|
||||
List<Variant> keys;
|
||||
p_messages.get_key_list(&keys);
|
||||
for (auto E = keys.front(); E; E = E->next()) {
|
||||
translation_map[E->get()] = p_messages[E->get()];
|
||||
}
|
||||
|
||||
if (bool(result)) {
|
||||
return p_plural_rule.substr(first_ques_mark + 1, p_plural_rule.find(":") - (first_ques_mark + 1)).to_int();
|
||||
}
|
||||
|
||||
String after_colon = p_plural_rule.substr(p_plural_rule.find(":") + 1, p_plural_rule.length());
|
||||
return _get_plural_index(after_colon, p_input_name, p_input_value, r_expr);
|
||||
}
|
||||
|
||||
void Translation::set_locale(const String &p_locale) {
|
||||
|
@ -957,125 +840,50 @@ void Translation::set_locale(const String &p_locale) {
|
|||
}
|
||||
}
|
||||
|
||||
void Translation::set_plural_rule(const String &p_plural_rule) {
|
||||
// Set plural_forms and plural_rule.
|
||||
// p_plural_rule passed in has the form "Plural-Forms: nplurals=2; plural=(n >= 2);".
|
||||
|
||||
int first_semi_col = p_plural_rule.find(";");
|
||||
plural_forms = p_plural_rule.substr(p_plural_rule.find("=") + 1, first_semi_col - (p_plural_rule.find("=") + 1)).to_int();
|
||||
|
||||
int expression_start = p_plural_rule.find("=", first_semi_col) + 1;
|
||||
int second_semi_col = p_plural_rule.rfind(";");
|
||||
plural_rule = p_plural_rule.substr(expression_start, second_semi_col - expression_start);
|
||||
// Strip away '(' and ')' to ease evaluating the expression later on.
|
||||
plural_rule = plural_rule.replacen("(", "");
|
||||
plural_rule = plural_rule.replacen(")", "");
|
||||
}
|
||||
|
||||
void Translation::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) {
|
||||
HashMap<StringName, Vector<StringName>> &map_id_str = translation_map[p_context];
|
||||
|
||||
if (map_id_str.has(p_src_text)) {
|
||||
WARN_PRINT("Double translations for \"" + String(p_src_text) + "\" under the same context \"" + String(p_context) + "\" for locale \"" + get_locale() + "\".\nThere should only be one unique translation for a given string under the same context.");
|
||||
map_id_str[p_src_text].set(0, p_xlated_text);
|
||||
} else {
|
||||
map_id_str[p_src_text].push_back(p_xlated_text);
|
||||
}
|
||||
translation_map[p_src_text] = p_xlated_text;
|
||||
}
|
||||
|
||||
void Translation::add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_texts, const StringName &p_context) {
|
||||
ERR_FAIL_COND_MSG(p_plural_texts.size() != plural_forms, "Trying to add plural texts that don't match the required number of plural forms for locale \"" + get_locale() + "\"");
|
||||
|
||||
HashMap<StringName, Vector<StringName>> &map_id_str = translation_map[p_context];
|
||||
|
||||
if (map_id_str.has(p_src_text)) {
|
||||
WARN_PRINT("Double translations for \"" + p_src_text + "\" under the same context \"" + p_context + "\" for locale " + get_locale() + ".\nThere should only be one unique translation for a given string under the same context.");
|
||||
map_id_str[p_src_text].clear();
|
||||
}
|
||||
|
||||
for (int i = 0; i < p_plural_texts.size(); i++) {
|
||||
map_id_str[p_src_text].push_back(p_plural_texts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
int Translation::get_plural_forms() const {
|
||||
return plural_forms;
|
||||
}
|
||||
|
||||
String Translation::get_plural_rule() const {
|
||||
return plural_rule;
|
||||
void Translation::add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_xlated_texts, const StringName &p_context) {
|
||||
WARN_PRINT("Translation class doesn't handle plural messages. Calling add_plural_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles plurals, such as TranslationPO class");
|
||||
ERR_FAIL_COND_MSG(p_plural_xlated_texts.empty(), "Parameter vector p_plural_xlated_texts passed in is empty.");
|
||||
translation_map[p_src_text] = p_plural_xlated_texts[0];
|
||||
}
|
||||
|
||||
StringName Translation::get_message(const StringName &p_src_text, const StringName &p_context) const {
|
||||
if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) {
|
||||
if (p_context != StringName()) {
|
||||
WARN_PRINT("Translation class doesn't handle context. Using context in get_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles context, such as TranslationPO class");
|
||||
}
|
||||
|
||||
const Map<StringName, StringName>::Element *E = translation_map.find(p_src_text);
|
||||
if (!E) {
|
||||
return StringName();
|
||||
}
|
||||
ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].empty(), StringName(), "Source text \"" + String(p_src_text) + "\" is registered but doesn't have a translation. Please check add_message() or add_plural_message() to make sure a translation is always added.");
|
||||
|
||||
return translation_map[p_context][p_src_text][0];
|
||||
return E->get();
|
||||
}
|
||||
|
||||
StringName Translation::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const {
|
||||
ERR_FAIL_COND_V_MSG(p_n < 0, p_src_text, "N passed into translation to get a plural message should not be negative. For negative numbers, use singular translation please. Search \"gettext PO Plural Forms\" online for the documentation on translating negative numbers.");
|
||||
|
||||
if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) {
|
||||
return StringName();
|
||||
}
|
||||
ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].empty(), StringName(), "Source text \"" + String(p_src_text) + "\" is registered but doesn't have a translation. Please check add_message() or add_plural_message() to make sure a translation is always added.");
|
||||
|
||||
// Return based on English plural rule if locale's plural rule is not registered (normally due to missing or invalid "Plural-Forms" in PO file header).
|
||||
if (plural_forms <= 0) {
|
||||
if (p_n == 1) {
|
||||
return p_src_text;
|
||||
} else {
|
||||
return p_plural_text;
|
||||
}
|
||||
}
|
||||
|
||||
return translation_map[p_context][p_src_text][_get_plural_index(p_n)];
|
||||
WARN_PRINT("Translation class doesn't handle plural messages. Calling get_plural_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles plurals, such as TranslationPO class");
|
||||
return get_message(p_src_text);
|
||||
}
|
||||
|
||||
void Translation::erase_message(const StringName &p_src_text, const StringName &p_context) {
|
||||
if (!translation_map.has(p_context)) {
|
||||
return;
|
||||
if (p_context != StringName()) {
|
||||
WARN_PRINT("Translation class doesn't handle context. Using context in erase_message() on a Translation instance is probably a mistake. \nUse a derived Translation class that handles context, such as TranslationPO class");
|
||||
}
|
||||
|
||||
translation_map[p_context].erase(p_src_text);
|
||||
translation_map.erase(p_src_text);
|
||||
}
|
||||
|
||||
void Translation::get_message_list(List<StringName> *r_messages) const {
|
||||
////This is the function that PHashTranslation uses to get the list of msgid.
|
||||
////Right now I just return the msgid list under "" context, and make no changes to PHashTranslation at all.
|
||||
////So PHashTranslation will be functioning like last time, it will not handle context and plurals translation.
|
||||
|
||||
// Return all the keys of translation_map under "" context.
|
||||
|
||||
List<StringName> context_l;
|
||||
translation_map.get_key_list(&context_l);
|
||||
|
||||
for (auto E = context_l.front(); E; E = E->next()) {
|
||||
if (String(E->get()) != "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<StringName> msgid_l;
|
||||
translation_map[E->get()].get_key_list(&msgid_l);
|
||||
|
||||
for (auto E2 = msgid_l.front(); E2; E2 = E2->next()) {
|
||||
r_messages->push_back(E2->get());
|
||||
}
|
||||
for (const Map<StringName, StringName>::Element *E = translation_map.front(); E; E = E->next()) {
|
||||
r_messages->push_back(E->key());
|
||||
}
|
||||
}
|
||||
|
||||
int Translation::get_message_count() const {
|
||||
List<StringName> context_l;
|
||||
translation_map.get_key_list(&context_l);
|
||||
|
||||
int count = 0;
|
||||
for (auto E = context_l.front(); E; E = E->next()) {
|
||||
count += translation_map[E->get()].size();
|
||||
}
|
||||
return count;
|
||||
return translation_map.size();
|
||||
}
|
||||
|
||||
void Translation::_bind_methods() {
|
||||
|
@ -1088,8 +896,6 @@ void Translation::_bind_methods() {
|
|||
ClassDB::bind_method(D_METHOD("erase_message", "src_message", "context"), &Translation::erase_message, DEFVAL(""));
|
||||
ClassDB::bind_method(D_METHOD("get_message_list"), &Translation::_get_message_list);
|
||||
ClassDB::bind_method(D_METHOD("get_message_count"), &Translation::get_message_count);
|
||||
ClassDB::bind_method(D_METHOD("get_plural_forms"), &Translation::get_plural_forms);
|
||||
ClassDB::bind_method(D_METHOD("get_plural_rule"), &Translation::get_plural_rule);
|
||||
ClassDB::bind_method(D_METHOD("_set_messages"), &Translation::_set_messages);
|
||||
ClassDB::bind_method(D_METHOD("_get_messages"), &Translation::_get_messages);
|
||||
|
||||
|
@ -1227,6 +1033,30 @@ void TranslationServer::remove_translation(const Ref<Translation> &p_translation
|
|||
translations.erase(p_translation);
|
||||
}
|
||||
|
||||
Ref<Translation> TranslationServer::get_translation_object(const String &p_locale) {
|
||||
Ref<Translation> res;
|
||||
String lang = get_language_code(p_locale);
|
||||
bool near_match_found = false;
|
||||
|
||||
for (const Set<Ref<Translation>>::Element *E = translations.front(); E; E = E->next()) {
|
||||
const Ref<Translation> &t = E->get();
|
||||
ERR_FAIL_COND_V(t.is_null(), nullptr);
|
||||
String l = t->get_locale();
|
||||
|
||||
// Exact match.
|
||||
if (l == p_locale) {
|
||||
return t;
|
||||
}
|
||||
|
||||
// If near match found, keep that match, but keep looking to try to look for perfect match.
|
||||
if (get_language_code(l) == lang && !near_match_found) {
|
||||
res = t;
|
||||
near_match_found = true;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
void TranslationServer::clear() {
|
||||
translations.clear();
|
||||
}
|
||||
|
@ -1240,10 +1070,10 @@ StringName TranslationServer::translate(const StringName &p_message, const Strin
|
|||
|
||||
ERR_FAIL_COND_V_MSG(locale.length() < 2, p_message, "Could not translate message as configured locale '" + locale + "' is invalid.");
|
||||
|
||||
StringName res = _get_message_from_translations(p_message, p_context, locale);
|
||||
StringName res = _get_message_from_translations(p_message, p_context, locale, false);
|
||||
|
||||
if (!res && fallback.length() >= 2) {
|
||||
res = _get_message_from_translations(p_message, p_context, fallback);
|
||||
res = _get_message_from_translations(p_message, p_context, fallback, false);
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
|
@ -1257,31 +1087,29 @@ StringName TranslationServer::translate_plural(const StringName &p_message, cons
|
|||
if (!enabled) {
|
||||
if (p_n == 1) {
|
||||
return p_message;
|
||||
} else {
|
||||
return p_message_plural;
|
||||
}
|
||||
return p_message_plural;
|
||||
}
|
||||
|
||||
ERR_FAIL_COND_V_MSG(locale.length() < 2, p_message, "Could not translate message as configured locale '" + locale + "' is invalid.");
|
||||
|
||||
StringName res = _get_message_from_translations(p_message, p_context, locale, p_message_plural, p_n);
|
||||
StringName res = _get_message_from_translations(p_message, p_context, locale, true, p_message_plural, p_n);
|
||||
|
||||
if (!res && fallback.length() >= 2) {
|
||||
res = _get_message_from_translations(p_message, p_context, fallback, p_message_plural, p_n);
|
||||
res = _get_message_from_translations(p_message, p_context, fallback, true, p_message_plural, p_n);
|
||||
}
|
||||
|
||||
if (!res) {
|
||||
if (p_n == 1) {
|
||||
return p_message;
|
||||
} else {
|
||||
return p_message_plural;
|
||||
}
|
||||
return p_message_plural;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
StringName TranslationServer::_get_message_from_translations(const StringName &p_message, const StringName &p_context, const String &p_locale, const String &p_message_plural, int p_n) const {
|
||||
StringName TranslationServer::_get_message_from_translations(const StringName &p_message, const StringName &p_context, const String &p_locale, bool plural, const String &p_message_plural, int p_n) const {
|
||||
// Locale can be of the form 'll_CC', i.e. language code and regional code,
|
||||
// e.g. 'en_US', 'en_GB', etc. It might also be simply 'll', e.g. 'en'.
|
||||
// To find the relevant translation, we look for those with locale starting
|
||||
|
@ -1312,7 +1140,7 @@ StringName TranslationServer::_get_message_from_translations(const StringName &p
|
|||
}
|
||||
|
||||
StringName r;
|
||||
if (p_n == -1) {
|
||||
if (!plural) {
|
||||
r = t->get_message(p_message, p_context);
|
||||
} else {
|
||||
r = t->get_plural_message(p_message, p_message_plural, p_n, p_context);
|
||||
|
@ -1406,9 +1234,8 @@ StringName TranslationServer::tool_translate_plural(const StringName &p_message,
|
|||
|
||||
if (p_n == 1) {
|
||||
return p_message;
|
||||
} else {
|
||||
return p_message_plural;
|
||||
}
|
||||
return p_message_plural;
|
||||
}
|
||||
|
||||
void TranslationServer::set_doc_translation(const Ref<Translation> &p_translation) {
|
||||
|
@ -1435,9 +1262,8 @@ StringName TranslationServer::doc_translate_plural(const StringName &p_message,
|
|||
|
||||
if (p_n == 1) {
|
||||
return p_message;
|
||||
} else {
|
||||
return p_message_plural;
|
||||
}
|
||||
return p_message_plural;
|
||||
}
|
||||
|
||||
void TranslationServer::_bind_methods() {
|
||||
|
@ -1446,11 +1272,12 @@ void TranslationServer::_bind_methods() {
|
|||
|
||||
ClassDB::bind_method(D_METHOD("get_locale_name", "locale"), &TranslationServer::get_locale_name);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("translate", "message"), &TranslationServer::translate);
|
||||
ClassDB::bind_method(D_METHOD("translate", "message", "context"), &TranslationServer::translate, DEFVAL(""));
|
||||
ClassDB::bind_method(D_METHOD("translate_plural", "message", "plural_message", "n", "context"), &TranslationServer::translate_plural, DEFVAL(""));
|
||||
|
||||
ClassDB::bind_method(D_METHOD("add_translation", "translation"), &TranslationServer::add_translation);
|
||||
ClassDB::bind_method(D_METHOD("remove_translation", "translation"), &TranslationServer::remove_translation);
|
||||
ClassDB::bind_method(D_METHOD("get_translation_object", "locale"), &TranslationServer::get_translation_object);
|
||||
|
||||
ClassDB::bind_method(D_METHOD("clear"), &TranslationServer::clear);
|
||||
|
||||
|
|
|
@ -31,9 +31,6 @@
|
|||
#ifndef TRANSLATION_H
|
||||
#define TRANSLATION_H
|
||||
|
||||
//#define DEBUG_TRANSLATION
|
||||
|
||||
#include "core/math/expression.h"
|
||||
#include "core/resource.h"
|
||||
|
||||
class Translation : public Resource {
|
||||
|
@ -42,23 +39,11 @@ class Translation : public Resource {
|
|||
RES_BASE_EXTENSION("translation");
|
||||
|
||||
String locale = "en";
|
||||
int plural_forms = 0; // 0 means no "Plural-Forms" is given in the PO header file. The min for all languages is 1.
|
||||
String plural_rule;
|
||||
Map<StringName, StringName> translation_map;
|
||||
|
||||
// TLDR: Maps context to a list of source strings and translated strings. In PO terms, maps msgctxt to a list of msgid and msgstr.
|
||||
// The first key corresponds to context, and the second key (of the contained HashMap) corresponds to source string.
|
||||
// The value Vector<StringName> in the second map stores the translated strings. Index 0, 1, 2 matches msgstr[0], msgstr[1], msgstr[2]... in the case of plurals.
|
||||
// Otherwise index 0 mathes to msgstr in a singular translation.
|
||||
// Strings without context have "" as first key.
|
||||
HashMap<StringName, HashMap<StringName, Vector<StringName>>> translation_map;
|
||||
|
||||
Vector<String> _get_message_list() const;
|
||||
|
||||
Dictionary _get_messages() const;
|
||||
void _set_messages(const Dictionary &p_messages);
|
||||
|
||||
int _get_plural_index(int p_n) const;
|
||||
int _get_plural_index(const String &p_plural_rule, const Vector<String> &p_input_name, const Array &p_input_value, Ref<Expression> &r_expr) const;
|
||||
virtual Vector<String> _get_message_list() const;
|
||||
virtual Dictionary _get_messages() const;
|
||||
virtual void _set_messages(const Dictionary &p_messages);
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
|
@ -66,23 +51,14 @@ protected:
|
|||
public:
|
||||
void set_locale(const String &p_locale);
|
||||
_FORCE_INLINE_ String get_locale() const { return locale; }
|
||||
void set_plural_rule(const String &p_plural_rule);
|
||||
|
||||
void add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context = "");
|
||||
void add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_texts, const StringName &p_context = "");
|
||||
virtual void add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context = "");
|
||||
virtual void add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_xlated_texts, const StringName &p_context = "");
|
||||
virtual StringName get_message(const StringName &p_src_text, const StringName &p_context = "") const; //overridable for other implementations
|
||||
virtual StringName get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context = "") const;
|
||||
void erase_message(const StringName &p_src_text, const StringName &p_context = "");
|
||||
|
||||
void get_message_list(List<StringName> *r_messages) const;
|
||||
int get_message_count() const;
|
||||
|
||||
int get_plural_forms() const;
|
||||
String get_plural_rule() const;
|
||||
|
||||
#ifdef DEBUG_TRANSLATION
|
||||
void print_translation_map();
|
||||
#endif
|
||||
virtual void erase_message(const StringName &p_src_text, const StringName &p_context = "");
|
||||
virtual void get_message_list(List<StringName> *r_messages) const;
|
||||
virtual int get_message_count() const;
|
||||
|
||||
Translation() {}
|
||||
};
|
||||
|
@ -104,7 +80,7 @@ class TranslationServer : public Object {
|
|||
static TranslationServer *singleton;
|
||||
bool _load_translations(const String &p_from);
|
||||
|
||||
StringName _get_message_from_translations(const StringName &p_message, const StringName &p_context, const String &p_locale, const String &p_message_plural = "", int p_n = -1) const;
|
||||
StringName _get_message_from_translations(const StringName &p_message, const StringName &p_context, const String &p_locale, bool plural, const String &p_message_plural = "", int p_n = 0) const;
|
||||
|
||||
static void _bind_methods();
|
||||
|
||||
|
@ -116,6 +92,7 @@ public:
|
|||
|
||||
void set_locale(const String &p_locale);
|
||||
String get_locale() const;
|
||||
Ref<Translation> get_translation_object(const String &p_locale);
|
||||
|
||||
String get_locale_name(const String &p_locale) const;
|
||||
|
||||
|
|
|
@ -0,0 +1,311 @@
|
|||
/*************************************************************************/
|
||||
/* translation_po.cpp */
|
||||
/*************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/*************************************************************************/
|
||||
/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
|
||||
/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/*************************************************************************/
|
||||
|
||||
#include "translation_po.h"
|
||||
#include "os/file_access.h"
|
||||
|
||||
#ifdef DEBUG_TRANSLATION_PO
|
||||
void TranslationPO::print_translation_map() {
|
||||
Error err;
|
||||
FileAccess *file = FileAccess::open("translation_map_print_test.txt", FileAccess::WRITE, &err);
|
||||
if (err != OK) {
|
||||
ERR_PRINT("Failed to open translation_map_print_test.txt");
|
||||
return;
|
||||
}
|
||||
|
||||
file->store_line("NPlural : " + String::num_int64(this->get_plural_forms()));
|
||||
file->store_line("Plural rule : " + this->get_plural_rule());
|
||||
file->store_line("");
|
||||
|
||||
List<StringName> context_l;
|
||||
translation_map.get_key_list(&context_l);
|
||||
for (auto E = context_l.front(); E; E = E->next()) {
|
||||
StringName ctx = E->get();
|
||||
file->store_line(" ===== Context: " + String::utf8(String(ctx).utf8()) + " ===== ");
|
||||
const HashMap<StringName, Vector<StringName>> &inner_map = translation_map[ctx];
|
||||
|
||||
List<StringName> id_l;
|
||||
inner_map.get_key_list(&id_l);
|
||||
for (auto E2 = id_l.front(); E2; E2 = E2->next()) {
|
||||
StringName id = E2->get();
|
||||
file->store_line("msgid: " + String::utf8(String(id).utf8()));
|
||||
for (int i = 0; i < inner_map[id].size(); i++) {
|
||||
file->store_line("msgstr[" + String::num_int64(i) + "]: " + String::utf8(String(inner_map[id][i]).utf8()));
|
||||
}
|
||||
file->store_line("");
|
||||
}
|
||||
}
|
||||
file->close();
|
||||
}
|
||||
#endif
|
||||
|
||||
Dictionary TranslationPO::_get_messages() const {
|
||||
// Return translation_map as a Dictionary.
|
||||
|
||||
Dictionary d;
|
||||
|
||||
List<StringName> context_l;
|
||||
translation_map.get_key_list(&context_l);
|
||||
for (auto E = context_l.front(); E; E = E->next()) {
|
||||
StringName ctx = E->get();
|
||||
const HashMap<StringName, Vector<StringName>> &id_str_map = translation_map[ctx];
|
||||
|
||||
Dictionary d2;
|
||||
List<StringName> id_l;
|
||||
id_str_map.get_key_list(&id_l);
|
||||
// Save list of id and strs associated with a context in a temporary dictionary.
|
||||
for (auto E2 = id_l.front(); E2; E2 = E2->next()) {
|
||||
StringName id = E2->get();
|
||||
d2[id] = id_str_map[id];
|
||||
}
|
||||
|
||||
d[ctx] = d2;
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
void TranslationPO::_set_messages(const Dictionary &p_messages) {
|
||||
// Construct translation_map from a Dictionary.
|
||||
|
||||
List<Variant> context_l;
|
||||
p_messages.get_key_list(&context_l);
|
||||
for (auto E = context_l.front(); E; E = E->next()) {
|
||||
StringName ctx = E->get();
|
||||
const Dictionary &id_str_map = p_messages[ctx];
|
||||
|
||||
HashMap<StringName, Vector<StringName>> temp_map;
|
||||
List<Variant> id_l;
|
||||
id_str_map.get_key_list(&id_l);
|
||||
for (auto E2 = id_l.front(); E2; E2 = E2->next()) {
|
||||
StringName id = E2->get();
|
||||
temp_map[id] = id_str_map[id];
|
||||
}
|
||||
|
||||
translation_map[ctx] = temp_map;
|
||||
}
|
||||
}
|
||||
|
||||
Vector<String> TranslationPO::_get_message_list() const {
|
||||
// Return all keys in translation_map.
|
||||
|
||||
List<StringName> msgs;
|
||||
get_message_list(&msgs);
|
||||
|
||||
Vector<String> v;
|
||||
for (auto E = msgs.front(); E; E = E->next()) {
|
||||
v.push_back(E->get());
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
int TranslationPO::_get_plural_index(int p_n) const {
|
||||
// Get a number between [0;number of plural forms).
|
||||
|
||||
input_val.clear();
|
||||
input_val.push_back(p_n);
|
||||
|
||||
Variant result;
|
||||
for (int i = 0; i < equi_tests.size(); i++) {
|
||||
Error err = expr->parse(equi_tests[i], input_name);
|
||||
ERR_FAIL_COND_V_MSG(err != OK, 0, "Cannot parse expression. Error: " + expr->get_error_text());
|
||||
|
||||
result = expr->execute(input_val);
|
||||
ERR_FAIL_COND_V_MSG(expr->has_execute_failed(), 0, "Cannot evaluate expression.");
|
||||
|
||||
// Last expression. Variant result will either map to a bool or an integer, in both cases returning it will give the correct plural index.
|
||||
if (i + 1 == equi_tests.size()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (bool(result)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
ERR_FAIL_V_MSG(0, "Unexpected. Function should have returned. Please report this bug.");
|
||||
}
|
||||
|
||||
void TranslationPO::_cache_plural_tests(const String &p_plural_rule) {
|
||||
// Some examples of p_plural_rule passed in can have the form:
|
||||
// "n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5" (Arabic)
|
||||
// "n >= 2" (French) // When evaluating the last, esp careful with this one.
|
||||
// "n != 1" (English)
|
||||
int first_ques_mark = p_plural_rule.find("?");
|
||||
if (first_ques_mark == -1) {
|
||||
equi_tests.push_back(p_plural_rule.strip_edges());
|
||||
return;
|
||||
}
|
||||
|
||||
String equi_test = p_plural_rule.substr(0, first_ques_mark).strip_edges();
|
||||
equi_tests.push_back(equi_test);
|
||||
|
||||
String after_colon = p_plural_rule.substr(p_plural_rule.find(":") + 1, p_plural_rule.length());
|
||||
_cache_plural_tests(after_colon);
|
||||
}
|
||||
|
||||
void TranslationPO::set_plural_rule(const String &p_plural_rule) {
|
||||
// Set plural_forms and plural_rule.
|
||||
// p_plural_rule passed in has the form "Plural-Forms: nplurals=2; plural=(n >= 2);".
|
||||
|
||||
int first_semi_col = p_plural_rule.find(";");
|
||||
plural_forms = p_plural_rule.substr(p_plural_rule.find("=") + 1, first_semi_col - (p_plural_rule.find("=") + 1)).to_int();
|
||||
|
||||
int expression_start = p_plural_rule.find("=", first_semi_col) + 1;
|
||||
int second_semi_col = p_plural_rule.rfind(";");
|
||||
plural_rule = p_plural_rule.substr(expression_start, second_semi_col - expression_start);
|
||||
|
||||
// Setup the cache to make evaluating plural rule faster later on.
|
||||
plural_rule = plural_rule.replacen("(", "");
|
||||
plural_rule = plural_rule.replacen(")", "");
|
||||
_cache_plural_tests(plural_rule);
|
||||
expr.instance();
|
||||
input_name.push_back("n");
|
||||
}
|
||||
|
||||
void TranslationPO::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) {
|
||||
HashMap<StringName, Vector<StringName>> &map_id_str = translation_map[p_context];
|
||||
|
||||
if (map_id_str.has(p_src_text)) {
|
||||
WARN_PRINT("Double translations for \"" + String(p_src_text) + "\" under the same context \"" + String(p_context) + "\" for locale \"" + get_locale() + "\".\nThere should only be one unique translation for a given string under the same context.");
|
||||
map_id_str[p_src_text].set(0, p_xlated_text);
|
||||
} else {
|
||||
map_id_str[p_src_text].push_back(p_xlated_text);
|
||||
}
|
||||
}
|
||||
|
||||
void TranslationPO::add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_xlated_texts, const StringName &p_context) {
|
||||
ERR_FAIL_COND_MSG(p_plural_xlated_texts.size() != plural_forms, "Trying to add plural texts that don't match the required number of plural forms for locale \"" + get_locale() + "\"");
|
||||
|
||||
HashMap<StringName, Vector<StringName>> &map_id_str = translation_map[p_context];
|
||||
|
||||
if (map_id_str.has(p_src_text)) {
|
||||
WARN_PRINT("Double translations for \"" + p_src_text + "\" under the same context \"" + p_context + "\" for locale " + get_locale() + ".\nThere should only be one unique translation for a given string under the same context.");
|
||||
map_id_str[p_src_text].clear();
|
||||
}
|
||||
|
||||
for (int i = 0; i < p_plural_xlated_texts.size(); i++) {
|
||||
map_id_str[p_src_text].push_back(p_plural_xlated_texts[i]);
|
||||
}
|
||||
}
|
||||
|
||||
int TranslationPO::get_plural_forms() const {
|
||||
return plural_forms;
|
||||
}
|
||||
|
||||
String TranslationPO::get_plural_rule() const {
|
||||
return plural_rule;
|
||||
}
|
||||
|
||||
StringName TranslationPO::get_message(const StringName &p_src_text, const StringName &p_context) const {
|
||||
if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) {
|
||||
return StringName();
|
||||
}
|
||||
ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].empty(), StringName(), "Source text \"" + String(p_src_text) + "\" is registered but doesn't have a translation. Please report this bug.");
|
||||
|
||||
return translation_map[p_context][p_src_text][0];
|
||||
}
|
||||
|
||||
StringName TranslationPO::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const {
|
||||
ERR_FAIL_COND_V_MSG(p_n < 0, StringName(), "N passed into translation to get a plural message should not be negative. For negative numbers, use singular translation please. Search \"gettext PO Plural Forms\" online for the documentation on translating negative numbers.");
|
||||
|
||||
// If the query is the same as last time, return the cached result.
|
||||
if (p_n == last_plural_n && p_context == last_plural_context && p_src_text == last_plural_key) {
|
||||
return translation_map[p_context][p_src_text][last_plural_mapped_index];
|
||||
}
|
||||
|
||||
if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) {
|
||||
return StringName();
|
||||
}
|
||||
ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].empty(), StringName(), "Source text \"" + String(p_src_text) + "\" is registered but doesn't have a translation. Please report this bug.");
|
||||
|
||||
if (translation_map[p_context][p_src_text].size() == 1) {
|
||||
WARN_PRINT("Source string \"" + String(p_src_text) + "\" doesn't have plural translations. Use singular translation API for such as tr(), TTR() to translate \"" + String(p_src_text) + "\"");
|
||||
return translation_map[p_context][p_src_text][0];
|
||||
}
|
||||
|
||||
int plural_index = _get_plural_index(p_n);
|
||||
ERR_FAIL_COND_V_MSG(plural_index < 0 || translation_map[p_context][p_src_text].size() < plural_index + 1, StringName(), "Plural index returned or number of plural translations is not valid. Please report this bug.");
|
||||
|
||||
// Cache result so that if the next entry is the same, we can return directly.
|
||||
// _get_plural_index(p_n) can get very costly, especially when evaluating long plural-rule (Arabic)
|
||||
last_plural_key = p_src_text;
|
||||
last_plural_context = p_context;
|
||||
last_plural_n = p_n;
|
||||
last_plural_mapped_index = plural_index;
|
||||
|
||||
return translation_map[p_context][p_src_text][plural_index];
|
||||
}
|
||||
|
||||
void TranslationPO::erase_message(const StringName &p_src_text, const StringName &p_context) {
|
||||
if (!translation_map.has(p_context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
translation_map[p_context].erase(p_src_text);
|
||||
}
|
||||
|
||||
void TranslationPO::get_message_list(List<StringName> *r_messages) const {
|
||||
// PHashTranslation uses this function to get the list of msgid.
|
||||
// Return all the keys of translation_map under "" context.
|
||||
|
||||
List<StringName> context_l;
|
||||
translation_map.get_key_list(&context_l);
|
||||
|
||||
for (auto E = context_l.front(); E; E = E->next()) {
|
||||
if (String(E->get()) != "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<StringName> msgid_l;
|
||||
translation_map[E->get()].get_key_list(&msgid_l);
|
||||
|
||||
for (auto E2 = msgid_l.front(); E2; E2 = E2->next()) {
|
||||
r_messages->push_back(E2->get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int TranslationPO::get_message_count() const {
|
||||
List<StringName> context_l;
|
||||
translation_map.get_key_list(&context_l);
|
||||
|
||||
int count = 0;
|
||||
for (auto E = context_l.front(); E; E = E->next()) {
|
||||
count += translation_map[E->get()].size();
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
void TranslationPO::_bind_methods() {
|
||||
ClassDB::bind_method(D_METHOD("get_plural_forms"), &TranslationPO::get_plural_forms);
|
||||
ClassDB::bind_method(D_METHOD("get_plural_rule"), &TranslationPO::get_plural_rule);
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*************************************************************************/
|
||||
/* translation_po.h */
|
||||
/*************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/*************************************************************************/
|
||||
/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
|
||||
/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/*************************************************************************/
|
||||
|
||||
#ifndef TRANSLATION_PO_H
|
||||
#define TRANSLATION_PO_H
|
||||
|
||||
//#define DEBUG_TRANSLATION_PO
|
||||
|
||||
#include "core/math/expression.h"
|
||||
#include "translation.h"
|
||||
|
||||
class TranslationPO : public Translation {
|
||||
GDCLASS(TranslationPO, Translation);
|
||||
|
||||
// TLDR: Maps context to a list of source strings and translated strings. In PO terms, maps msgctxt to a list of msgid and msgstr.
|
||||
// The first key corresponds to context, and the second key (of the contained HashMap) corresponds to source string.
|
||||
// The value Vector<StringName> in the second map stores the translated strings. Index 0, 1, 2 matches msgstr[0], msgstr[1], msgstr[2]... in the case of plurals.
|
||||
// Otherwise index 0 mathes to msgstr in a singular translation.
|
||||
// Strings without context have "" as first key.
|
||||
HashMap<StringName, HashMap<StringName, Vector<StringName>>> translation_map;
|
||||
|
||||
int plural_forms = 0; // 0 means no "Plural-Forms" is given in the PO header file. The min for all languages is 1.
|
||||
String plural_rule;
|
||||
|
||||
// Cache temporary variables related to _get_plural_index() to make it faster
|
||||
Vector<String> equi_tests;
|
||||
Vector<String> input_name;
|
||||
mutable Ref<Expression> expr;
|
||||
mutable Array input_val;
|
||||
mutable StringName last_plural_key;
|
||||
mutable StringName last_plural_context;
|
||||
mutable int last_plural_n = -1; // Set it to an impossible value at the beginning.
|
||||
mutable int last_plural_mapped_index = 0;
|
||||
|
||||
void _cache_plural_tests(const String &p_plural_rule);
|
||||
int _get_plural_index(int p_n) const;
|
||||
|
||||
Vector<String> _get_message_list() const override;
|
||||
Dictionary _get_messages() const override;
|
||||
void _set_messages(const Dictionary &p_messages) override;
|
||||
|
||||
protected:
|
||||
static void _bind_methods();
|
||||
|
||||
public:
|
||||
void get_message_list(List<StringName> *r_messages) const override;
|
||||
int get_message_count() const override;
|
||||
void add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context = "") override;
|
||||
void add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_xlated_texts, const StringName &p_context = "") override;
|
||||
StringName get_message(const StringName &p_src_text, const StringName &p_context = "") const override;
|
||||
StringName get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context = "") const override;
|
||||
void erase_message(const StringName &p_src_text, const StringName &p_context = "") override;
|
||||
|
||||
void set_plural_rule(const String &p_plural_rule);
|
||||
int get_plural_forms() const;
|
||||
String get_plural_rule() const;
|
||||
|
||||
#ifdef DEBUG_TRANSLATION_PO
|
||||
void print_translation_map();
|
||||
#endif
|
||||
|
||||
TranslationPO() {}
|
||||
};
|
||||
|
||||
#endif // TRANSLATION_PO_H
|
|
@ -4285,9 +4285,8 @@ String TTRN(const String &p_text, const String &p_text_plural, int p_n, const St
|
|||
// Return message based on English plural rule if translation is not possible.
|
||||
if (p_n == 1) {
|
||||
return p_text;
|
||||
} else {
|
||||
return p_text_plural;
|
||||
}
|
||||
return p_text_plural;
|
||||
}
|
||||
|
||||
String DTR(const String &p_text, const String &p_context) {
|
||||
|
@ -4312,9 +4311,8 @@ String DTRN(const String &p_text, const String &p_text_plural, int p_n, const St
|
|||
// Return message based on English plural rule if translation is not possible.
|
||||
if (p_n == 1) {
|
||||
return text;
|
||||
} else {
|
||||
return text_plural;
|
||||
}
|
||||
return text_plural;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
@ -4344,7 +4342,6 @@ String RTRN(const String &p_text, const String &p_text_plural, int p_n, const St
|
|||
// Return message based on English plural rule if translation is not possible.
|
||||
if (p_n == 1) {
|
||||
return p_text;
|
||||
} else {
|
||||
return p_text_plural;
|
||||
}
|
||||
return p_text_plural;
|
||||
}
|
||||
|
|
|
@ -421,7 +421,9 @@ String DTRN(const String &p_text, const String &p_text_plural, int p_n, const St
|
|||
|
||||
#else
|
||||
#define TTR(m_value) (String())
|
||||
#define TTRN(m_value) (String())
|
||||
#define DTR(m_value) (String())
|
||||
#define DTRN(m_value) (String())
|
||||
#define TTRC(m_value) (m_value)
|
||||
#define TTRGET(m_value) (m_value)
|
||||
#endif
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
<description>
|
||||
Plugins are registered via [method EditorPlugin.add_translation_parser_plugin] method. To define the parsing and string extraction logic, override the [method parse_file] method in script.
|
||||
Add the extracted strings to argument [code]msgids[/code] or [code]msgids_context_plural[/code] if context or plural is used.
|
||||
When adding to [code]msgids_context_plural[/code], you must add the data using the format [code]["A", "B", "C"][/code], where [code]A[/code] represents the extracted string, [code]B[/code] represents the context, and [code]C[/code] represents the plural version of the extracted string. If you want to add only context but not plural, put [code]""[/code] for the plural slot. The idea is the same if you only want to add plural but not context. See the code below for concrete examples.
|
||||
The extracted strings will be written into a POT file selected by user under "POT Generation" in "Localization" tab in "Project Settings" menu.
|
||||
Below shows an example of a custom parser that extracts strings from a CSV file to write into a POT.
|
||||
[codeblock]
|
||||
|
@ -28,9 +29,12 @@
|
|||
[/codeblock]
|
||||
To add a translatable string associated with context or plural, add it to [code]msgids_context_plural[/code]:
|
||||
[codeblock]
|
||||
msgids_ctx_plural.append(["Test 1", "context", "test 1 plurals"]) # This will add a message with msgid "Test 1", msgctxt "context", and msgid_plural "test 1 plurals".
|
||||
msgids_ctx_plural.append(["A test without context", "", "plurals"]) # This will add a message with msgid "A test without context" and msgid_plural "plurals".
|
||||
msgids_ctx_plural.append(["Only with context", "a friendly context", ""]) # This will add a message with msgid "Only with context" and msgctxt "a friendly context".
|
||||
# This will add a message with msgid "Test 1", msgctxt "context", and msgid_plural "test 1 plurals".
|
||||
msgids_context_plural.append(["Test 1", "context", "test 1 plurals"])
|
||||
# This will add a message with msgid "A test without context" and msgid_plural "plurals".
|
||||
msgids_context_plural.append(["A test without context", "", "plurals"])
|
||||
# This will add a message with msgid "Only with context" and msgctxt "a friendly context".
|
||||
msgids_context_plural.append(["Only with context", "a friendly context", ""])
|
||||
[/codeblock]
|
||||
[b]Note:[/b] If you override parsing logic for standard script types (GDScript, C#, etc.), it would be better to load the [code]path[/code] argument using [method ResourceLoader.load]. This is because built-in scripts are loaded as [Resource] type, not [File] type.
|
||||
For example:
|
||||
|
|
|
@ -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="""">
|
||||
</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="""">
|
||||
</argument>
|
||||
<description>
|
||||
Translates a message involving plurals using translation catalogs configured in the Project Settings. An additional context could be used to specify the translation context.
|
||||
Only works if message translation is enabled (which it is by default), otherwise it returns the [code]message[/code] or [code]plural_message[/code] unchanged. See [method set_message_translation].
|
||||
The number [code]n[/code] is the number or quantity of the plural object. It will be used to guide the translation system to fetch the correct plural form for the selected language.
|
||||
[b]Note:[/b] Negative and floating-point values usually represent physical entities for which singular and plural don't clearly apply. In such cases, use [method tr].
|
||||
See <link>https://docs.godotengine.org/en/latest/tutorials/i18n/internationalizing_games.html</link> for examples of the usage of this method.
|
||||
</description>
|
||||
</method>
|
||||
</methods>
|
||||
|
|
|
@ -18,8 +18,25 @@
|
|||
</argument>
|
||||
<argument index="1" name="xlated_message" type="StringName">
|
||||
</argument>
|
||||
<argument index="2" name="context" type="StringName" default="""">
|
||||
</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="""">
|
||||
</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="""">
|
||||
</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="""">
|
||||
</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="""">
|
||||
</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=""en"">
|
||||
|
|
|
@ -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="""">
|
||||
</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="""">
|
||||
</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>
|
||||
|
|
|
@ -54,7 +54,7 @@ Error EditorTranslationParserPlugin::parse_file(const String &p_path, Vector<Str
|
|||
// Add user's collected translatable messages with context or plurals.
|
||||
for (int i = 0; i < ids_ctx_plural.size(); i++) {
|
||||
Array arr = ids_ctx_plural[i];
|
||||
ERR_FAIL_COND_V_MSG(arr.size() != 3, ERR_INVALID_DATA, "Array entries written into msgids_ctx_plural of EditorTranslationParserPlugin parse_file() method should have the form [\"message\",\"context\",\"plural message\"]");
|
||||
ERR_FAIL_COND_V_MSG(arr.size() != 3, ERR_INVALID_DATA, "Array entries written into `msgids_context_plural` in `parse_file()` method should have the form [\"message\", \"context\", \"plural message\"]");
|
||||
|
||||
Vector<String> id_ctx_plural;
|
||||
id_ctx_plural.push_back(arr[0]);
|
||||
|
|
|
@ -271,7 +271,7 @@ bool SceneTreeEditor::_add_nodes(Node *p_node, TreeItem *p_parent) {
|
|||
msg_temp += TTRN("Node is in one group.", "Node is in {num} groups.", num_groups).format(arr, "{num}");
|
||||
}
|
||||
if (num_connections >= 1 || num_groups >= 1) {
|
||||
msg_temp += TTR("\nClick to show signals dock.");
|
||||
msg_temp += "\n" + TTR("Click to show signals dock.");
|
||||
}
|
||||
|
||||
Ref<Texture2D> icon_temp;
|
||||
|
|
|
@ -78,8 +78,6 @@ def _add_additional_location(msgctx, msg, location):
|
|||
|
||||
if msg_pos == -1:
|
||||
print("Someone apparently thought writing Python was as easy as GDScript. Ping Akien.")
|
||||
# NOTE FOR MENTORS: When I tested on my computer (windows) I need the extra \n#: to make the locations print line by line.
|
||||
# but it worked before without \n# so I will leave it like before
|
||||
main_po = main_po[:msg_pos] + " " + location + main_po[msg_pos:]
|
||||
|
||||
|
||||
|
@ -171,7 +169,7 @@ print("Updating the editor.pot template...")
|
|||
for fname in matches:
|
||||
# NOTE FOR MENTORS: When I tested on windows I need to add encoding="utf8" at the end to be able to open the file.
|
||||
# maybe on Linux there's no need.
|
||||
with open(fname, "r") as f:
|
||||
with open(fname, "r", encoding="utf8") as f:
|
||||
process_file(f, fname)
|
||||
|
||||
with open("editor.pot", "w") as f:
|
||||
|
|
Loading…
Reference in New Issue