Move context and plural support to Translation

- `TranslationPO` is now an empty class. It exists for compatibility.
- `OptimizedTranslation` stays the same, no context or plural support.
This commit is contained in:
Haoyu Qiu 2025-07-22 15:40:46 +08:00
parent e882e42e1b
commit 4e80190a46
9 changed files with 204 additions and 306 deletions

View file

@ -31,7 +31,7 @@
#include "translation_loader_po.h" #include "translation_loader_po.h"
#include "core/io/file_access.h" #include "core/io/file_access.h"
#include "core/string/translation_po.h" #include "core/string/translation.h"
Ref<Resource> TranslationLoaderPO::load_translation(Ref<FileAccess> f, Error *r_error) { Ref<Resource> TranslationLoaderPO::load_translation(Ref<FileAccess> f, Error *r_error) {
if (r_error) { if (r_error) {
@ -39,7 +39,8 @@ Ref<Resource> TranslationLoaderPO::load_translation(Ref<FileAccess> f, Error *r_
} }
const String path = f->get_path(); const String path = f->get_path();
Ref<TranslationPO> translation = Ref<TranslationPO>(memnew(TranslationPO)); Ref<Translation> translation;
translation.instantiate();
String config; String config;
uint32_t magic = f->get_32(); uint32_t magic = f->get_32();
@ -229,7 +230,7 @@ Ref<Resource> TranslationLoaderPO::load_translation(Ref<FileAccess> f, Error *r_
if (p_start != -1) { if (p_start != -1) {
int p_end = config.find_char('\n', p_start); int p_end = config.find_char('\n', p_start);
translation->set_plural_rules_override(config.substr(p_start, p_end - p_start)); translation->set_plural_rules_override(config.substr(p_start, p_end - p_start));
plural_forms = translation->get_plural_forms(); plural_forms = translation->get_nplurals();
} }
} }

View file

@ -32,7 +32,6 @@
#include "core/io/file_access.h" #include "core/io/file_access.h"
#include "core/io/resource_loader.h" #include "core/io/resource_loader.h"
#include "core/string/translation.h"
class TranslationLoaderPO : public ResourceFormatLoader { class TranslationLoaderPO : public ResourceFormatLoader {
GDSOFTCLASS(TranslationLoaderPO, ResourceFormatLoader); GDSOFTCLASS(TranslationLoaderPO, ResourceFormatLoader);

View file

@ -44,11 +44,27 @@ struct CompressedString {
void OptimizedTranslation::generate(const Ref<Translation> &p_from) { void OptimizedTranslation::generate(const Ref<Translation> &p_from) {
// This method compresses a Translation instance. // 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. // Right now, it doesn't handle context or plurals.
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
ERR_FAIL_COND(p_from.is_null()); ERR_FAIL_COND(p_from.is_null());
List<StringName> keys; List<StringName> keys;
p_from->get_message_list(&keys); {
List<StringName> raw_keys;
p_from->get_message_list(&raw_keys);
for (const StringName &key : raw_keys) {
const String key_str = key.operator String();
int p = key_str.find_char(0x04);
if (p == -1) {
keys.push_back(key);
} else {
const String &msgctxt = key_str.substr(0, p);
const String &msgid = key_str.substr(p + 1);
WARN_PRINT(vformat("OptimizedTranslation does not support context, ignoring message '%s' with context '%s'.", msgid, msgctxt));
}
}
}
int size = Math::larger_prime(keys.size()); int size = Math::larger_prime(keys.size());

View file

@ -34,42 +34,102 @@
#include "core/string/plural_rules.h" #include "core/string/plural_rules.h"
#include "core/string/translation_server.h" #include "core/string/translation_server.h"
void _check_for_incompatibility(const String &p_msgctxt, const String &p_msgid) {
// Gettext PO and MO files use an empty untranslated string without context
// to store metadata.
if (p_msgctxt.is_empty() && p_msgid.is_empty()) {
WARN_PRINT("Both context and the untranslated string are empty. This may cause issues with the translation system and external tools.");
}
// The EOT character (0x04) is used as a separator between context and
// untranslated string in the MO file format. This convention is also used
// by `get_message_list()`.
//
// It's unusual to have this character in the context or untranslated
// string. But it doesn't do any harm as long as you are aware of this when
// using the relevant APIs and tools.
if (p_msgctxt.contains_char(0x04)) {
WARN_PRINT(vformat("Found EOT character (0x04) within context '%s'. This may cause issues with the translation system and external tools.", p_msgctxt));
}
if (p_msgid.contains_char(0x04)) {
WARN_PRINT(vformat("Found EOT character (0x04) within untranslated string '%s'. This may cause issues with the translation system and external tools.", p_msgid));
}
}
Dictionary Translation::_get_messages() const { Dictionary Translation::_get_messages() const {
Dictionary d; Dictionary d;
for (const KeyValue<StringName, StringName> &E : translation_map) { for (const KeyValue<MessageKey, Vector<StringName>> &E : translation_map) {
d[E.key] = E.value; const Array &storage_key = { E.key.msgctxt, E.key.msgid };
Array storage_value;
storage_value.resize(E.value.size());
for (int i = 0; i < E.value.size(); i++) {
storage_value[i] = E.value[i];
}
d[storage_key] = storage_value;
} }
return d; return d;
} }
Vector<String> Translation::_get_message_list() const { void Translation::_set_messages(const Dictionary &p_messages) {
Vector<String> msgs; translation_map.clear();
msgs.resize(translation_map.size());
int idx = 0;
for (const KeyValue<StringName, StringName> &E : translation_map) {
msgs.set(idx, E.key);
idx += 1;
}
return msgs; for (const KeyValue<Variant, Variant> &kv : p_messages) {
switch (kv.key.get_type()) {
// Old version, no context or plural support.
case Variant::STRING_NAME: {
const MessageKey msg_key = { StringName(), kv.key };
_check_for_incompatibility(msg_key.msgctxt, msg_key.msgid);
translation_map[msg_key] = { kv.value };
} break;
// Current version.
case Variant::ARRAY: {
const Array &storage_key = kv.key;
const MessageKey msg_key = { storage_key[0], storage_key[1] };
const Array &storage_value = kv.value;
ERR_CONTINUE_MSG(storage_value.is_empty(), vformat("No translated strings for untranslated string '%s' with context '%s'.", msg_key.msgid, msg_key.msgctxt));
Vector<StringName> msgstrs;
msgstrs.resize(storage_value.size());
for (int i = 0; i < storage_value.size(); i++) {
msgstrs.write[i] = storage_value[i];
}
_check_for_incompatibility(msg_key.msgctxt, msg_key.msgid);
translation_map[msg_key] = msgstrs;
} break;
default: {
WARN_PRINT(vformat("Invalid key type in messages dictionary: %s.", Variant::get_type_name(kv.key.get_type())));
continue;
}
}
}
}
Vector<String> Translation::_get_message_list() const {
List<StringName> msgstrs;
get_message_list(&msgstrs);
Vector<String> keys;
keys.resize(msgstrs.size());
int idx = 0;
for (const StringName &msgstr : msgstrs) {
keys.write[idx++] = msgstr;
}
return keys;
} }
Vector<String> Translation::get_translated_message_list() const { Vector<String> Translation::get_translated_message_list() const {
Vector<String> msgs; Vector<String> msgstrs;
msgs.resize(translation_map.size()); for (const KeyValue<MessageKey, Vector<StringName>> &E : translation_map) {
int idx = 0; for (const StringName &msgstr : E.value) {
for (const KeyValue<StringName, StringName> &E : translation_map) { msgstrs.push_back(msgstr);
msgs.set(idx, E.value); }
idx += 1;
}
return msgs;
}
void Translation::_set_messages(const Dictionary &p_messages) {
for (const KeyValue<Variant, Variant> &kv : p_messages) {
translation_map[kv.key] = kv.value;
} }
return msgstrs;
} }
void Translation::set_locale(const String &p_locale) { void Translation::set_locale(const String &p_locale) {
@ -82,13 +142,21 @@ void Translation::set_locale(const String &p_locale) {
} }
void Translation::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) { 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; _check_for_incompatibility(p_context, p_src_text);
translation_map[{ p_context, p_src_text }] = { p_xlated_text };
} }
void Translation::add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_xlated_texts, const StringName &p_context) { 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.is_empty(), "Parameter vector p_plural_xlated_texts passed in is empty."); ERR_FAIL_COND_MSG(p_plural_xlated_texts.is_empty(), "Parameter vector p_plural_xlated_texts passed in is empty.");
translation_map[p_src_text] = p_plural_xlated_texts[0];
Vector<StringName> msgstrs;
msgstrs.resize(p_plural_xlated_texts.size());
for (int i = 0; i < p_plural_xlated_texts.size(); i++) {
msgstrs.write[i] = p_plural_xlated_texts[i];
}
_check_for_incompatibility(p_context, p_src_text);
translation_map[{ p_context, p_src_text }] = msgstrs;
} }
StringName Translation::get_message(const StringName &p_src_text, const StringName &p_context) const { StringName Translation::get_message(const StringName &p_src_text, const StringName &p_context) const {
@ -97,16 +165,13 @@ StringName Translation::get_message(const StringName &p_src_text, const StringNa
return ret; return ret;
} }
if (p_context != StringName()) { const Vector<StringName> *msgstrs = translation_map.getptr({ p_context, p_src_text });
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"); if (msgstrs == nullptr) {
}
HashMap<StringName, StringName>::ConstIterator E = translation_map.find(p_src_text);
if (!E) {
return StringName(); return StringName();
} }
return E->value; DEV_ASSERT(!msgstrs->is_empty()); // Should be prevented when adding messages.
return msgstrs->get(0);
} }
StringName Translation::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const { StringName Translation::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const {
@ -115,21 +180,30 @@ StringName Translation::get_plural_message(const StringName &p_src_text, const S
return ret; return ret;
} }
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"); 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 details on translating negative numbers.");
return get_message(p_src_text);
const Vector<StringName> *msgstrs = translation_map.getptr({ p_context, p_src_text });
if (msgstrs == nullptr) {
return StringName();
}
const int index = _get_plural_rules()->evaluate(p_n);
ERR_FAIL_INDEX_V_MSG(index, msgstrs->size(), StringName(), "Plural index returned or number of plural translations is not valid.");
return msgstrs->get(index);
} }
void Translation::erase_message(const StringName &p_src_text, const StringName &p_context) { void Translation::erase_message(const StringName &p_src_text, const StringName &p_context) {
if (p_context != StringName()) { translation_map.erase({ p_context, p_src_text });
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);
} }
void Translation::get_message_list(List<StringName> *r_messages) const { void Translation::get_message_list(List<StringName> *r_messages) const {
for (const KeyValue<StringName, StringName> &E : translation_map) { for (const KeyValue<MessageKey, Vector<StringName>> &E : translation_map) {
r_messages->push_back(E.key); if (E.key.msgctxt.is_empty()) {
r_messages->push_back(E.key.msgid);
} else {
// Separated by the EOT character. Compatible with the MO file format.
r_messages->push_back(vformat("%s\x04%s", E.key.msgctxt, E.key.msgid));
}
} }
} }
@ -175,6 +249,10 @@ String Translation::get_plural_rules_override() const {
return plural_rules_override; return plural_rules_override;
} }
int Translation::get_nplurals() const {
return _get_plural_rules()->get_nplurals();
}
void Translation::_bind_methods() { void Translation::_bind_methods() {
ClassDB::bind_method(D_METHOD("set_locale", "locale"), &Translation::set_locale); 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("get_locale"), &Translation::get_locale);
@ -195,7 +273,7 @@ void Translation::_bind_methods() {
GDVIRTUAL_BIND(_get_message, "src_message", "context"); GDVIRTUAL_BIND(_get_message, "src_message", "context");
ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "messages", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_messages", "_get_messages"); ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "messages", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_messages", "_get_messages");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "locale"), "set_locale", "get_locale"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "locale", PROPERTY_HINT_LOCALE_ID), "set_locale", "get_locale");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "plural_rules_override"), "set_plural_rules_override", "get_plural_rules_override"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "plural_rules_override"), "set_plural_rules_override", "get_plural_rules_override");
} }

View file

@ -41,12 +41,29 @@ class Translation : public Resource {
RES_BASE_EXTENSION("translation"); RES_BASE_EXTENSION("translation");
String locale = "en"; String locale = "en";
HashMap<StringName, StringName> translation_map;
struct MessageKey {
StringName msgctxt;
StringName msgid;
// Required to use this struct as a key in HashMap.
static uint32_t hash(const MessageKey &p_key) {
uint32_t h = hash_murmur3_one_32(HashMapHasherDefault::hash(p_key.msgctxt));
return hash_fmix32(hash_murmur3_one_32(HashMapHasherDefault::hash(p_key.msgid), h));
}
bool operator==(const MessageKey &p_key) const {
return msgctxt == p_key.msgctxt && msgid == p_key.msgid;
}
};
HashMap<MessageKey, Vector<StringName>, MessageKey> translation_map;
mutable PluralRules *plural_rules_cache = nullptr; mutable PluralRules *plural_rules_cache = nullptr;
String plural_rules_override; String plural_rules_override;
virtual Vector<String> _get_message_list() const; virtual Vector<String> _get_message_list() const;
// For data storage.
virtual Dictionary _get_messages() const; virtual Dictionary _get_messages() const;
virtual void _set_messages(const Dictionary &p_messages); virtual void _set_messages(const Dictionary &p_messages);
@ -74,5 +91,8 @@ public:
void set_plural_rules_override(const String &p_rules); void set_plural_rules_override(const String &p_rules);
String get_plural_rules_override() const; String get_plural_rules_override() const;
// This method is not exposed to scripting intentionally. It is only used by TranslationLoaderPO and tests.
int get_nplurals() const;
~Translation(); ~Translation();
}; };

View file

@ -30,198 +30,5 @@
#include "translation_po.h" #include "translation_po.h"
#include "core/string/plural_rules.h" // This file is intentionally left empty.
// It makes sure that `TranslationPO` exists, for compatibility.
#ifdef DEBUG_TRANSLATION_PO
#include "core/io/file_access.h"
void TranslationPO::print_translation_map() {
Error err;
Ref<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(get_plural_forms()));
file->store_line("Plural rule : " + get_plural_rule());
file->store_line("");
List<StringName> context_l;
translation_map.get_key_list(&context_l);
for (const StringName &ctx : context_l) {
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 (const StringName &id : id_l) {
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("");
}
}
}
#endif
Dictionary TranslationPO::_get_messages() const {
// Return translation_map as a Dictionary.
Dictionary d;
for (const KeyValue<StringName, HashMap<StringName, Vector<StringName>>> &E : translation_map) {
Dictionary d2;
for (const KeyValue<StringName, Vector<StringName>> &E2 : E.value) {
d2[E2.key] = E2.value;
}
d[E.key] = d2;
}
return d;
}
void TranslationPO::_set_messages(const Dictionary &p_messages) {
// Construct translation_map from a Dictionary.
for (const KeyValue<Variant, Variant> &kv : p_messages) {
const Dictionary &id_str_map = kv.value;
HashMap<StringName, Vector<StringName>> temp_map;
for (const KeyValue<Variant, Variant> &kv_id : id_str_map) {
StringName id = kv_id.key;
temp_map[id] = kv_id.value;
}
translation_map[kv.key] = temp_map;
}
}
Vector<String> TranslationPO::get_translated_message_list() const {
Vector<String> msgs;
for (const KeyValue<StringName, HashMap<StringName, Vector<StringName>>> &E : translation_map) {
if (E.key != StringName()) {
continue;
}
for (const KeyValue<StringName, Vector<StringName>> &E2 : E.value) {
for (const StringName &E3 : E2.value) {
msgs.push_back(E3);
}
}
}
return msgs;
}
Vector<String> TranslationPO::_get_message_list() const {
// Return all keys in translation_map.
List<StringName> msgs;
get_message_list(&msgs);
Vector<String> v;
for (const StringName &E : msgs) {
v.push_back(E);
}
return v;
}
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(vformat("Double translations for \"%s\" under the same context \"%s\" for locale \"%s\".\nThere should only be one unique translation for a given string under the same context.", String(p_src_text), String(p_context), get_locale()));
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() != _get_plural_rules()->get_nplurals(), vformat("Trying to add plural texts that don't match the required number of plural forms for locale \"%s\".", get_locale()));
HashMap<StringName, Vector<StringName>> &map_id_str = translation_map[p_context];
if (map_id_str.has(p_src_text)) {
WARN_PRINT(vformat("Double translations for \"%s\" under the same context \"%s\" for locale %s.\nThere should only be one unique translation for a given string under the same context.", p_src_text, p_context, get_locale()));
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 _get_plural_rules()->get_nplurals();
}
String TranslationPO::get_plural_rule() const {
return _get_plural_rules()->get_plural();
}
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].is_empty(), StringName(), vformat("Source text \"%s\" is registered but doesn't have a translation. Please report this bug.", String(p_src_text)));
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 (!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].is_empty(), StringName(), vformat("Source text \"%s\" is registered but doesn't have a translation. Please report this bug.", String(p_src_text)));
int plural_index = _get_plural_rules()->evaluate(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.");
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 {
// OptimizedTranslation uses this function to get the list of msgid.
// Return all the keys of translation_map under "" context.
for (const KeyValue<StringName, HashMap<StringName, Vector<StringName>>> &E : translation_map) {
if (E.key != StringName()) {
continue;
}
for (const KeyValue<StringName, Vector<StringName>> &E2 : E.value) {
r_messages->push_back(E2.key);
}
}
}
int TranslationPO::get_message_count() const {
int count = 0;
for (const KeyValue<StringName, HashMap<StringName, Vector<StringName>>> &E : translation_map) {
count += E.value.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);
}

View file

@ -30,41 +30,8 @@
#pragma once #pragma once
//#define DEBUG_TRANSLATION_PO
#include "core/string/translation.h" #include "core/string/translation.h"
class TranslationPO : public Translation { class TranslationPO : public Translation {
GDCLASS(TranslationPO, 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 matches 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 override;
Dictionary _get_messages() const override;
void _set_messages(const Dictionary &p_messages) override;
protected:
static void _bind_methods();
public:
Vector<String> get_translated_message_list() const override;
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;
int get_plural_forms() const;
String get_plural_rule() const;
#ifdef DEBUG_TRANSLATION_PO
void print_translation_map();
#endif
}; };

View file

@ -4,7 +4,8 @@
A language translation that maps a collection of strings to their individual translations. A language translation that maps a collection of strings to their individual translations.
</brief_description> </brief_description>
<description> <description>
[Translation]s are resources that can be loaded and unloaded on demand. They map a collection of strings to their individual translations, and they also provide convenience methods for pluralization. [Translation] maps a collection of strings to their individual translations, and also provides convenience methods for pluralization.
A [Translation] consists of messages. A message is identified by its context and untranslated string. Unlike [url=https://www.gnu.org/software/gettext/]gettext[/url], using an empty context string in Godot means not using any context.
</description> </description>
<tutorials> <tutorials>
<link title="Internationalizing games">$DOCS_URL/tutorials/i18n/internationalizing_games.html</link> <link title="Internationalizing games">$DOCS_URL/tutorials/i18n/internationalizing_games.html</link>
@ -48,7 +49,6 @@
<description> <description>
Adds a message involving plural translation if nonexistent, followed by its translation. 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. An additional context could be used to specify the translation context or differentiate polysemic words.
[b]Note:[/b] Plurals are only supported in [url=$DOCS_URL/tutorials/i18n/localization_using_gettext.html]gettext-based translations (PO)[/url], not CSV.
</description> </description>
</method> </method>
<method name="erase_message"> <method name="erase_message">
@ -76,7 +76,19 @@
<method name="get_message_list" qualifiers="const"> <method name="get_message_list" qualifiers="const">
<return type="PackedStringArray" /> <return type="PackedStringArray" />
<description> <description>
Returns all the messages (keys). Returns the keys of all messages, that is, the context and untranslated strings of each message.
[b]Note:[/b] If a message does not use a context, the corresponding element is the untranslated string. Otherwise, the corresponding element is the context and untranslated string separated by the EOT character ([code]U+0004[/code]). This is done for compatibility purposes.
[codeblock]
for key in translation.get_message_list():
var p = key.find("\u0004")
if p == -1:
var untranslated = key
print("Message %s" % untranslated)
else:
var context = key.substr(0, p)
var untranslated = key.substr(p + 1)
print("Message %s with context %s" % [untranslated, context])
[/codeblock]
</description> </description>
</method> </method>
<method name="get_plural_message" qualifiers="const"> <method name="get_plural_message" qualifiers="const">
@ -94,7 +106,7 @@
<method name="get_translated_message_list" qualifiers="const"> <method name="get_translated_message_list" qualifiers="const">
<return type="PackedStringArray" /> <return type="PackedStringArray" />
<description> <description>
Returns all the messages (translated text). Returns all the translated strings.
</description> </description>
</method> </method>
</methods> </methods>

View file

@ -33,7 +33,6 @@
#include "core/string/optimized_translation.h" #include "core/string/optimized_translation.h"
#include "core/string/plural_rules.h" #include "core/string/plural_rules.h"
#include "core/string/translation.h" #include "core/string/translation.h"
#include "core/string/translation_po.h"
#include "core/string/translation_server.h" #include "core/string/translation_server.h"
#ifdef TOOLS_ENABLED #ifdef TOOLS_ENABLED
@ -46,7 +45,8 @@
namespace TestTranslation { namespace TestTranslation {
TEST_CASE("[Translation] Messages") { TEST_CASE("[Translation] Messages") {
Ref<Translation> translation = memnew(Translation); Ref<Translation> translation;
translation.instantiate();
translation->set_locale("fr"); translation->set_locale("fr");
translation->add_message("Hello", "Bonjour"); translation->add_message("Hello", "Bonjour");
CHECK(translation->get_message("Hello") == "Bonjour"); CHECK(translation->get_message("Hello") == "Bonjour");
@ -71,8 +71,9 @@ TEST_CASE("[Translation] Messages") {
CHECK(messages.find("Hello3")); CHECK(messages.find("Hello3"));
} }
TEST_CASE("[TranslationPO] Messages with context") { TEST_CASE("[Translation] Messages with context") {
Ref<TranslationPO> translation = memnew(TranslationPO); Ref<Translation> translation;
translation.instantiate();
translation->set_locale("fr"); translation->set_locale("fr");
translation->add_message("Hello", "Bonjour"); translation->add_message("Hello", "Bonjour");
translation->add_message("Hello", "Salut", "friendly"); translation->add_message("Hello", "Salut", "friendly");
@ -90,11 +91,8 @@ TEST_CASE("[TranslationPO] Messages with context") {
List<StringName> messages; List<StringName> messages;
translation->get_message_list(&messages); translation->get_message_list(&messages);
// `get_message_count()` takes all contexts into account.
CHECK(translation->get_message_count() == 1); CHECK(translation->get_message_count() == 1);
// Only the default context is taken into account. CHECK(messages.size() == 1);
// Since "Hello" is now only present in a non-default context, it is not counted in the list of messages.
CHECK(messages.size() == 0);
translation->add_message("Hello2", "Bonjour2"); translation->add_message("Hello2", "Bonjour2");
translation->add_message("Hello2", "Salut2", "friendly"); translation->add_message("Hello2", "Salut2", "friendly");
@ -102,35 +100,35 @@ TEST_CASE("[TranslationPO] Messages with context") {
messages.clear(); messages.clear();
translation->get_message_list(&messages); translation->get_message_list(&messages);
// `get_message_count()` takes all contexts into account.
CHECK(translation->get_message_count() == 4); CHECK(translation->get_message_count() == 4);
// Only the default context is taken into account. CHECK(messages.size() == 4);
CHECK(messages.size() == 2);
// Messages are stored in a Map, don't assume ordering. // Messages are stored in a Map, don't assume ordering.
CHECK(messages.find("Hello2")); CHECK(messages.find("Hello2"));
CHECK(messages.find("Hello3")); CHECK(messages.find("Hello3"));
// Context and untranslated string are separated by EOT.
CHECK(messages.find("friendly\x04Hello2"));
} }
TEST_CASE("[TranslationPO] Plural messages") { TEST_CASE("[Translation] Plural messages") {
{ {
Ref<TranslationPO> translation = memnew(TranslationPO); Ref<Translation> translation;
translation.instantiate();
translation->set_locale("fr"); translation->set_locale("fr");
CHECK(translation->get_plural_forms() == 3); CHECK(translation->get_nplurals() == 3);
CHECK(translation->get_plural_rule() == "(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2)");
} }
{ {
Ref<TranslationPO> translation = memnew(TranslationPO); Ref<Translation> translation;
translation.instantiate();
translation->set_locale("invalid"); translation->set_locale("invalid");
CHECK(translation->get_plural_forms() == 2); CHECK(translation->get_nplurals() == 2);
CHECK(translation->get_plural_rule() == "(n != 1)");
} }
{ {
Ref<TranslationPO> translation = memnew(TranslationPO); Ref<Translation> translation;
translation.instantiate();
translation->set_plural_rules_override("Plural-Forms: nplurals=2; plural=(n >= 2);"); translation->set_plural_rules_override("Plural-Forms: nplurals=2; plural=(n >= 2);");
CHECK(translation->get_plural_forms() == 2); CHECK(translation->get_nplurals() == 2);
CHECK(translation->get_plural_rule() == "(n >= 2)");
PackedStringArray plurals; PackedStringArray plurals;
plurals.push_back("Il y a %d pomme"); plurals.push_back("Il y a %d pomme");
@ -146,7 +144,7 @@ TEST_CASE("[TranslationPO] Plural messages") {
} }
} }
TEST_CASE("[TranslationPO] Plural rules parsing") { TEST_CASE("[Translation] Plural rules parsing") {
ERR_PRINT_OFF; ERR_PRINT_OFF;
{ {
CHECK(PluralRules::parse("") == nullptr); CHECK(PluralRules::parse("") == nullptr);