Skip to content

Commit 4e80190

Browse files
committed
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.
1 parent e882e42 commit 4e80190

File tree

9 files changed

+201
-303
lines changed

9 files changed

+201
-303
lines changed

core/io/translation_loader_po.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,16 @@
3131
#include "translation_loader_po.h"
3232

3333
#include "core/io/file_access.h"
34-
#include "core/string/translation_po.h"
34+
#include "core/string/translation.h"
3535

3636
Ref<Resource> TranslationLoaderPO::load_translation(Ref<FileAccess> f, Error *r_error) {
3737
if (r_error) {
3838
*r_error = ERR_FILE_CORRUPT;
3939
}
4040

4141
const String path = f->get_path();
42-
Ref<TranslationPO> translation = Ref<TranslationPO>(memnew(TranslationPO));
42+
Ref<Translation> translation;
43+
translation.instantiate();
4344
String config;
4445

4546
uint32_t magic = f->get_32();
@@ -229,7 +230,7 @@ Ref<Resource> TranslationLoaderPO::load_translation(Ref<FileAccess> f, Error *r_
229230
if (p_start != -1) {
230231
int p_end = config.find_char('\n', p_start);
231232
translation->set_plural_rules_override(config.substr(p_start, p_end - p_start));
232-
plural_forms = translation->get_plural_forms();
233+
plural_forms = translation->get_nplurals();
233234
}
234235
}
235236

core/io/translation_loader_po.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232

3333
#include "core/io/file_access.h"
3434
#include "core/io/resource_loader.h"
35-
#include "core/string/translation.h"
3635

3736
class TranslationLoaderPO : public ResourceFormatLoader {
3837
GDSOFTCLASS(TranslationLoaderPO, ResourceFormatLoader);

core/string/optimized_translation.cpp

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,27 @@ struct CompressedString {
4444

4545
void OptimizedTranslation::generate(const Ref<Translation> &p_from) {
4646
// This method compresses a Translation instance.
47-
// Right now, it doesn't handle context or plurals, so Translation subclasses using plurals or context (i.e TranslationPO) shouldn't be compressed.
47+
// Right now, it doesn't handle context or plurals.
4848
#ifdef TOOLS_ENABLED
4949
ERR_FAIL_COND(p_from.is_null());
50+
5051
List<StringName> keys;
51-
p_from->get_message_list(&keys);
52+
{
53+
List<StringName> raw_keys;
54+
p_from->get_message_list(&raw_keys);
55+
56+
for (const StringName &key : raw_keys) {
57+
const String key_str = key.operator String();
58+
int p = key_str.find_char(0x04);
59+
if (p == -1) {
60+
keys.push_back(key);
61+
} else {
62+
const String &msgctxt = key_str.substr(0, p);
63+
const String &msgid = key_str.substr(p + 1);
64+
WARN_PRINT(vformat("OptimizedTranslation does not support context, ignoring message '%s' with context '%s'.", msgid, msgctxt));
65+
}
66+
}
67+
}
5268

5369
int size = Math::larger_prime(keys.size());
5470

core/string/translation.cpp

Lines changed: 120 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -34,42 +34,102 @@
3434
#include "core/string/plural_rules.h"
3535
#include "core/string/translation_server.h"
3636

37+
void _check_for_incompatibility(const String &p_msgctxt, const String &p_msgid) {
38+
// Gettext PO and MO files use an empty untranslated string without context
39+
// to store metadata.
40+
if (p_msgctxt.is_empty() && p_msgid.is_empty()) {
41+
WARN_PRINT("Both context and the untranslated string are empty. This may cause issues with the translation system and external tools.");
42+
}
43+
44+
// The EOT character (0x04) is used as a separator between context and
45+
// untranslated string in the MO file format. This convention is also used
46+
// by `get_message_list()`.
47+
//
48+
// It's unusual to have this character in the context or untranslated
49+
// string. But it doesn't do any harm as long as you are aware of this when
50+
// using the relevant APIs and tools.
51+
if (p_msgctxt.contains_char(0x04)) {
52+
WARN_PRINT(vformat("Found EOT character (0x04) within context '%s'. This may cause issues with the translation system and external tools.", p_msgctxt));
53+
}
54+
if (p_msgid.contains_char(0x04)) {
55+
WARN_PRINT(vformat("Found EOT character (0x04) within untranslated string '%s'. This may cause issues with the translation system and external tools.", p_msgid));
56+
}
57+
}
58+
3759
Dictionary Translation::_get_messages() const {
3860
Dictionary d;
39-
for (const KeyValue<StringName, StringName> &E : translation_map) {
40-
d[E.key] = E.value;
61+
for (const KeyValue<MessageKey, Vector<StringName>> &E : translation_map) {
62+
const Array &storage_key = { E.key.msgctxt, E.key.msgid };
63+
64+
Array storage_value;
65+
storage_value.resize(E.value.size());
66+
for (int i = 0; i < E.value.size(); i++) {
67+
storage_value[i] = E.value[i];
68+
}
69+
d[storage_key] = storage_value;
4170
}
4271
return d;
4372
}
4473

45-
Vector<String> Translation::_get_message_list() const {
46-
Vector<String> msgs;
47-
msgs.resize(translation_map.size());
48-
int idx = 0;
49-
for (const KeyValue<StringName, StringName> &E : translation_map) {
50-
msgs.set(idx, E.key);
51-
idx += 1;
52-
}
74+
void Translation::_set_messages(const Dictionary &p_messages) {
75+
translation_map.clear();
5376

54-
return msgs;
77+
for (const KeyValue<Variant, Variant> &kv : p_messages) {
78+
switch (kv.key.get_type()) {
79+
// Old version, no context or plural support.
80+
case Variant::STRING_NAME: {
81+
const MessageKey msg_key = { StringName(), kv.key };
82+
_check_for_incompatibility(msg_key.msgctxt, msg_key.msgid);
83+
translation_map[msg_key] = { kv.value };
84+
} break;
85+
86+
// Current version.
87+
case Variant::ARRAY: {
88+
const Array &storage_key = kv.key;
89+
const MessageKey msg_key = { storage_key[0], storage_key[1] };
90+
91+
const Array &storage_value = kv.value;
92+
ERR_CONTINUE_MSG(storage_value.is_empty(), vformat("No translated strings for untranslated string '%s' with context '%s'.", msg_key.msgid, msg_key.msgctxt));
93+
94+
Vector<StringName> msgstrs;
95+
msgstrs.resize(storage_value.size());
96+
for (int i = 0; i < storage_value.size(); i++) {
97+
msgstrs.write[i] = storage_value[i];
98+
}
99+
100+
_check_for_incompatibility(msg_key.msgctxt, msg_key.msgid);
101+
translation_map[msg_key] = msgstrs;
102+
} break;
103+
104+
default: {
105+
WARN_PRINT(vformat("Invalid key type in messages dictionary: %s.", Variant::get_type_name(kv.key.get_type())));
106+
continue;
107+
}
108+
}
109+
}
55110
}
56111

57-
Vector<String> Translation::get_translated_message_list() const {
58-
Vector<String> msgs;
59-
msgs.resize(translation_map.size());
112+
Vector<String> Translation::_get_message_list() const {
113+
List<StringName> msgstrs;
114+
get_message_list(&msgstrs);
115+
116+
Vector<String> keys;
117+
keys.resize(msgstrs.size());
60118
int idx = 0;
61-
for (const KeyValue<StringName, StringName> &E : translation_map) {
62-
msgs.set(idx, E.value);
63-
idx += 1;
119+
for (const StringName &msgstr : msgstrs) {
120+
keys.write[idx++] = msgstr;
64121
}
65-
66-
return msgs;
122+
return keys;
67123
}
68124

69-
void Translation::_set_messages(const Dictionary &p_messages) {
70-
for (const KeyValue<Variant, Variant> &kv : p_messages) {
71-
translation_map[kv.key] = kv.value;
125+
Vector<String> Translation::get_translated_message_list() const {
126+
Vector<String> msgstrs;
127+
for (const KeyValue<MessageKey, Vector<StringName>> &E : translation_map) {
128+
for (const StringName &msgstr : E.value) {
129+
msgstrs.push_back(msgstr);
130+
}
72131
}
132+
return msgstrs;
73133
}
74134

75135
void Translation::set_locale(const String &p_locale) {
@@ -82,13 +142,21 @@ void Translation::set_locale(const String &p_locale) {
82142
}
83143

84144
void Translation::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) {
85-
translation_map[p_src_text] = p_xlated_text;
145+
_check_for_incompatibility(p_context, p_src_text);
146+
translation_map[{ p_context, p_src_text }] = { p_xlated_text };
86147
}
87148

88149
void Translation::add_plural_message(const StringName &p_src_text, const Vector<String> &p_plural_xlated_texts, const StringName &p_context) {
89-
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");
90150
ERR_FAIL_COND_MSG(p_plural_xlated_texts.is_empty(), "Parameter vector p_plural_xlated_texts passed in is empty.");
91-
translation_map[p_src_text] = p_plural_xlated_texts[0];
151+
152+
Vector<StringName> msgstrs;
153+
msgstrs.resize(p_plural_xlated_texts.size());
154+
for (int i = 0; i < p_plural_xlated_texts.size(); i++) {
155+
msgstrs.write[i] = p_plural_xlated_texts[i];
156+
}
157+
158+
_check_for_incompatibility(p_context, p_src_text);
159+
translation_map[{ p_context, p_src_text }] = msgstrs;
92160
}
93161

94162
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
97165
return ret;
98166
}
99167

100-
if (p_context != StringName()) {
101-
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");
102-
}
103-
104-
HashMap<StringName, StringName>::ConstIterator E = translation_map.find(p_src_text);
105-
if (!E) {
168+
const Vector<StringName> *msgstrs = translation_map.getptr({ p_context, p_src_text });
169+
if (msgstrs == nullptr) {
106170
return StringName();
107171
}
108172

109-
return E->value;
173+
DEV_ASSERT(!msgstrs->is_empty()); // Should be prevented when adding messages.
174+
return msgstrs->get(0);
110175
}
111176

112177
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
115180
return ret;
116181
}
117182

118-
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");
119-
return get_message(p_src_text);
120-
}
183+
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.");
121184

122-
void Translation::erase_message(const StringName &p_src_text, const StringName &p_context) {
123-
if (p_context != StringName()) {
124-
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");
185+
const Vector<StringName> *msgstrs = translation_map.getptr({ p_context, p_src_text });
186+
if (msgstrs == nullptr) {
187+
return StringName();
125188
}
126189

127-
translation_map.erase(p_src_text);
190+
const int index = _get_plural_rules()->evaluate(p_n);
191+
ERR_FAIL_INDEX_V_MSG(index, msgstrs->size(), StringName(), "Plural index returned or number of plural translations is not valid.");
192+
return msgstrs->get(index);
193+
}
194+
195+
void Translation::erase_message(const StringName &p_src_text, const StringName &p_context) {
196+
translation_map.erase({ p_context, p_src_text });
128197
}
129198

130199
void Translation::get_message_list(List<StringName> *r_messages) const {
131-
for (const KeyValue<StringName, StringName> &E : translation_map) {
132-
r_messages->push_back(E.key);
200+
for (const KeyValue<MessageKey, Vector<StringName>> &E : translation_map) {
201+
if (E.key.msgctxt.is_empty()) {
202+
r_messages->push_back(E.key.msgid);
203+
} else {
204+
// Separated by the EOT character. Compatible with the MO file format.
205+
r_messages->push_back(vformat("%s\x04%s", E.key.msgctxt, E.key.msgid));
206+
}
133207
}
134208
}
135209

@@ -175,6 +249,10 @@ String Translation::get_plural_rules_override() const {
175249
return plural_rules_override;
176250
}
177251

252+
int Translation::get_nplurals() const {
253+
return _get_plural_rules()->get_nplurals();
254+
}
255+
178256
void Translation::_bind_methods() {
179257
ClassDB::bind_method(D_METHOD("set_locale", "locale"), &Translation::set_locale);
180258
ClassDB::bind_method(D_METHOD("get_locale"), &Translation::get_locale);
@@ -195,7 +273,7 @@ void Translation::_bind_methods() {
195273
GDVIRTUAL_BIND(_get_message, "src_message", "context");
196274

197275
ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "messages", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_messages", "_get_messages");
198-
ADD_PROPERTY(PropertyInfo(Variant::STRING, "locale"), "set_locale", "get_locale");
276+
ADD_PROPERTY(PropertyInfo(Variant::STRING, "locale", PROPERTY_HINT_LOCALE_ID), "set_locale", "get_locale");
199277
ADD_PROPERTY(PropertyInfo(Variant::STRING, "plural_rules_override"), "set_plural_rules_override", "get_plural_rules_override");
200278
}
201279

core/string/translation.h

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,29 @@ class Translation : public Resource {
4141
RES_BASE_EXTENSION("translation");
4242

4343
String locale = "en";
44-
HashMap<StringName, StringName> translation_map;
44+
45+
struct MessageKey {
46+
StringName msgctxt;
47+
StringName msgid;
48+
49+
// Required to use this struct as a key in HashMap.
50+
static uint32_t hash(const MessageKey &p_key) {
51+
uint32_t h = hash_murmur3_one_32(HashMapHasherDefault::hash(p_key.msgctxt));
52+
return hash_fmix32(hash_murmur3_one_32(HashMapHasherDefault::hash(p_key.msgid), h));
53+
}
54+
bool operator==(const MessageKey &p_key) const {
55+
return msgctxt == p_key.msgctxt && msgid == p_key.msgid;
56+
}
57+
};
58+
59+
HashMap<MessageKey, Vector<StringName>, MessageKey> translation_map;
4560

4661
mutable PluralRules *plural_rules_cache = nullptr;
4762
String plural_rules_override;
4863

4964
virtual Vector<String> _get_message_list() const;
65+
66+
// For data storage.
5067
virtual Dictionary _get_messages() const;
5168
virtual void _set_messages(const Dictionary &p_messages);
5269

@@ -74,5 +91,8 @@ class Translation : public Resource {
7491
void set_plural_rules_override(const String &p_rules);
7592
String get_plural_rules_override() const;
7693

94+
// This method is not exposed to scripting intentionally. It is only used by TranslationLoaderPO and tests.
95+
int get_nplurals() const;
96+
7797
~Translation();
7898
};

0 commit comments

Comments
 (0)