diff --git a/core/string/translation_domain.cpp b/core/string/translation_domain.cpp index b44eb40366a..6a5e1b2af85 100644 --- a/core/string/translation_domain.cpp +++ b/core/string/translation_domain.cpp @@ -33,6 +33,170 @@ #include "core/string/translation.h" #include "core/string/translation_server.h" +struct _character_accent_pair { + const char32_t character; + const char32_t *accented_character; +}; + +static _character_accent_pair _character_to_accented[] = { + { 'A', U"Å" }, + { 'B', U"ß" }, + { 'C', U"Ç" }, + { 'D', U"Ð" }, + { 'E', U"É" }, + { 'F', U"F́" }, + { 'G', U"Ĝ" }, + { 'H', U"Ĥ" }, + { 'I', U"Ĩ" }, + { 'J', U"Ĵ" }, + { 'K', U"ĸ" }, + { 'L', U"Ł" }, + { 'M', U"Ḿ" }, + { 'N', U"й" }, + { 'O', U"Ö" }, + { 'P', U"Ṕ" }, + { 'Q', U"Q́" }, + { 'R', U"Ř" }, + { 'S', U"Ŝ" }, + { 'T', U"Ŧ" }, + { 'U', U"Ũ" }, + { 'V', U"Ṽ" }, + { 'W', U"Ŵ" }, + { 'X', U"X́" }, + { 'Y', U"Ÿ" }, + { 'Z', U"Ž" }, + { 'a', U"á" }, + { 'b', U"ḅ" }, + { 'c', U"ć" }, + { 'd', U"d́" }, + { 'e', U"é" }, + { 'f', U"f́" }, + { 'g', U"ǵ" }, + { 'h', U"h̀" }, + { 'i', U"í" }, + { 'j', U"ǰ" }, + { 'k', U"ḱ" }, + { 'l', U"ł" }, + { 'm', U"m̀" }, + { 'n', U"ή" }, + { 'o', U"ô" }, + { 'p', U"ṕ" }, + { 'q', U"q́" }, + { 'r', U"ŕ" }, + { 's', U"š" }, + { 't', U"ŧ" }, + { 'u', U"ü" }, + { 'v', U"ṽ" }, + { 'w', U"ŵ" }, + { 'x', U"x́" }, + { 'y', U"ý" }, + { 'z', U"ź" }, +}; + +String TranslationDomain::_get_override_string(const String &p_message) const { + String res; + for (int i = 0; i < p_message.length(); i++) { + if (pseudolocalization.skip_placeholders_enabled && _is_placeholder(p_message, i)) { + res += p_message[i]; + res += p_message[i + 1]; + i++; + continue; + } + res += '*'; + } + return res; +} + +String TranslationDomain::_double_vowels(const String &p_message) const { + String res; + for (int i = 0; i < p_message.length(); i++) { + if (pseudolocalization.skip_placeholders_enabled && _is_placeholder(p_message, i)) { + res += p_message[i]; + res += p_message[i + 1]; + i++; + continue; + } + res += p_message[i]; + if (p_message[i] == 'a' || p_message[i] == 'e' || p_message[i] == 'i' || p_message[i] == 'o' || p_message[i] == 'u' || + p_message[i] == 'A' || p_message[i] == 'E' || p_message[i] == 'I' || p_message[i] == 'O' || p_message[i] == 'U') { + res += p_message[i]; + } + } + return res; +}; + +String TranslationDomain::_replace_with_accented_string(const String &p_message) const { + String res; + for (int i = 0; i < p_message.length(); i++) { + if (pseudolocalization.skip_placeholders_enabled && _is_placeholder(p_message, i)) { + res += p_message[i]; + res += p_message[i + 1]; + i++; + continue; + } + const char32_t *accented = _get_accented_version(p_message[i]); + if (accented) { + res += accented; + } else { + res += p_message[i]; + } + } + return res; +} + +String TranslationDomain::_wrap_with_fakebidi_characters(const String &p_message) const { + String res; + char32_t fakebidiprefix = U'\u202e'; + char32_t fakebidisuffix = U'\u202c'; + res += fakebidiprefix; + // The fake bidi unicode gets popped at every newline so pushing it back at every newline. + for (int i = 0; i < p_message.length(); i++) { + if (p_message[i] == '\n') { + res += fakebidisuffix; + res += p_message[i]; + res += fakebidiprefix; + } else if (pseudolocalization.skip_placeholders_enabled && _is_placeholder(p_message, i)) { + res += fakebidisuffix; + res += p_message[i]; + res += p_message[i + 1]; + res += fakebidiprefix; + i++; + } else { + res += p_message[i]; + } + } + res += fakebidisuffix; + return res; +} + +String TranslationDomain::_add_padding(const String &p_message, int p_length) const { + String underscores = String("_").repeat(p_length * pseudolocalization.expansion_ratio / 2); + String prefix = pseudolocalization.prefix + underscores; + String suffix = underscores + pseudolocalization.suffix; + + return prefix + p_message + suffix; +} + +const char32_t *TranslationDomain::_get_accented_version(char32_t p_character) const { + if (!is_ascii_alphabet_char(p_character)) { + return nullptr; + } + + for (unsigned int i = 0; i < sizeof(_character_to_accented) / sizeof(_character_to_accented[0]); i++) { + if (_character_to_accented[i].character == p_character) { + return _character_to_accented[i].accented_character; + } + } + + return nullptr; +} + +bool TranslationDomain::_is_placeholder(const String &p_message, int p_index) const { + return p_index < p_message.length() - 1 && p_message[p_index] == '%' && + (p_message[p_index + 1] == 's' || p_message[p_index + 1] == 'c' || p_message[p_index + 1] == 'd' || + p_message[p_index + 1] == 'o' || p_message[p_index + 1] == 'x' || p_message[p_index + 1] == 'X' || p_message[p_index + 1] == 'f'); +} + StringName TranslationDomain::get_message_from_translations(const String &p_locale, const StringName &p_message, const StringName &p_context) const { StringName res; int best_score = 0; @@ -129,9 +293,9 @@ StringName TranslationDomain::translate(const StringName &p_message, const Strin } if (!res) { - return p_message; + return pseudolocalization.enabled ? pseudolocalize(p_message) : p_message; } - return res; + return pseudolocalization.enabled ? pseudolocalize(res) : res; } StringName TranslationDomain::translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const { @@ -152,6 +316,100 @@ StringName TranslationDomain::translate_plural(const StringName &p_message, cons return res; } +bool TranslationDomain::is_pseudolocalization_enabled() const { + return pseudolocalization.enabled; +} + +void TranslationDomain::set_pseudolocalization_enabled(bool p_enabled) { + pseudolocalization.enabled = p_enabled; +} + +bool TranslationDomain::is_pseudolocalization_accents_enabled() const { + return pseudolocalization.accents_enabled; +} + +void TranslationDomain::set_pseudolocalization_accents_enabled(bool p_enabled) { + pseudolocalization.accents_enabled = p_enabled; +} + +bool TranslationDomain::is_pseudolocalization_double_vowels_enabled() const { + return pseudolocalization.double_vowels_enabled; +} + +void TranslationDomain::set_pseudolocalization_double_vowels_enabled(bool p_enabled) { + pseudolocalization.double_vowels_enabled = p_enabled; +} + +bool TranslationDomain::is_pseudolocalization_fake_bidi_enabled() const { + return pseudolocalization.fake_bidi_enabled; +} + +void TranslationDomain::set_pseudolocalization_fake_bidi_enabled(bool p_enabled) { + pseudolocalization.fake_bidi_enabled = p_enabled; +} + +bool TranslationDomain::is_pseudolocalization_override_enabled() const { + return pseudolocalization.override_enabled; +} + +void TranslationDomain::set_pseudolocalization_override_enabled(bool p_enabled) { + pseudolocalization.override_enabled = p_enabled; +} + +bool TranslationDomain::is_pseudolocalization_skip_placeholders_enabled() const { + return pseudolocalization.skip_placeholders_enabled; +} + +void TranslationDomain::set_pseudolocalization_skip_placeholders_enabled(bool p_enabled) { + pseudolocalization.skip_placeholders_enabled = p_enabled; +} + +float TranslationDomain::get_pseudolocalization_expansion_ratio() const { + return pseudolocalization.expansion_ratio; +} + +void TranslationDomain::set_pseudolocalization_expansion_ratio(float p_ratio) { + pseudolocalization.expansion_ratio = p_ratio; +} + +String TranslationDomain::get_pseudolocalization_prefix() const { + return pseudolocalization.prefix; +} + +void TranslationDomain::set_pseudolocalization_prefix(const String &p_prefix) { + pseudolocalization.prefix = p_prefix; +} + +String TranslationDomain::get_pseudolocalization_suffix() const { + return pseudolocalization.suffix; +} + +void TranslationDomain::set_pseudolocalization_suffix(const String &p_suffix) { + pseudolocalization.suffix = p_suffix; +} + +StringName TranslationDomain::pseudolocalize(const StringName &p_message) const { + String message = p_message; + int length = message.length(); + if (pseudolocalization.override_enabled) { + message = _get_override_string(message); + } + + if (pseudolocalization.double_vowels_enabled) { + message = _double_vowels(message); + } + + if (pseudolocalization.accents_enabled) { + message = _replace_with_accented_string(message); + } + + if (pseudolocalization.fake_bidi_enabled) { + message = _wrap_with_fakebidi_characters(message); + } + + return _add_padding(message, length); +} + void TranslationDomain::_bind_methods() { ClassDB::bind_method(D_METHOD("get_translation_object", "locale"), &TranslationDomain::get_translation_object); ClassDB::bind_method(D_METHOD("add_translation", "translation"), &TranslationDomain::add_translation); @@ -159,6 +417,36 @@ void TranslationDomain::_bind_methods() { ClassDB::bind_method(D_METHOD("clear"), &TranslationDomain::clear); ClassDB::bind_method(D_METHOD("translate", "message", "context"), &TranslationDomain::translate, DEFVAL(StringName())); ClassDB::bind_method(D_METHOD("translate_plural", "message", "message_plural", "n", "context"), &TranslationDomain::translate_plural, DEFVAL(StringName())); + + ClassDB::bind_method(D_METHOD("is_pseudolocalization_enabled"), &TranslationDomain::is_pseudolocalization_enabled); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_enabled); + ClassDB::bind_method(D_METHOD("is_pseudolocalization_accents_enabled"), &TranslationDomain::is_pseudolocalization_accents_enabled); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_accents_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_accents_enabled); + ClassDB::bind_method(D_METHOD("is_pseudolocalization_double_vowels_enabled"), &TranslationDomain::is_pseudolocalization_double_vowels_enabled); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_double_vowels_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_double_vowels_enabled); + ClassDB::bind_method(D_METHOD("is_pseudolocalization_fake_bidi_enabled"), &TranslationDomain::is_pseudolocalization_fake_bidi_enabled); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_fake_bidi_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_fake_bidi_enabled); + ClassDB::bind_method(D_METHOD("is_pseudolocalization_override_enabled"), &TranslationDomain::is_pseudolocalization_override_enabled); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_override_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_override_enabled); + ClassDB::bind_method(D_METHOD("is_pseudolocalization_skip_placeholders_enabled"), &TranslationDomain::is_pseudolocalization_skip_placeholders_enabled); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_skip_placeholders_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_skip_placeholders_enabled); + ClassDB::bind_method(D_METHOD("get_pseudolocalization_expansion_ratio"), &TranslationDomain::get_pseudolocalization_expansion_ratio); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_expansion_ratio", "ratio"), &TranslationDomain::set_pseudolocalization_expansion_ratio); + ClassDB::bind_method(D_METHOD("get_pseudolocalization_prefix"), &TranslationDomain::get_pseudolocalization_prefix); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_prefix", "prefix"), &TranslationDomain::set_pseudolocalization_prefix); + ClassDB::bind_method(D_METHOD("get_pseudolocalization_suffix"), &TranslationDomain::get_pseudolocalization_suffix); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_suffix", "suffix"), &TranslationDomain::set_pseudolocalization_suffix); + ClassDB::bind_method(D_METHOD("pseudolocalize", "message"), &TranslationDomain::pseudolocalize); + + ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_enabled"), "set_pseudolocalization_enabled", "is_pseudolocalization_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_accents_enabled"), "set_pseudolocalization_accents_enabled", "is_pseudolocalization_accents_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_double_vowels_enabled"), "set_pseudolocalization_double_vowels_enabled", "is_pseudolocalization_double_vowels_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_fake_bidi_enabled"), "set_pseudolocalization_fake_bidi_enabled", "is_pseudolocalization_fake_bidi_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_override_enabled"), "set_pseudolocalization_override_enabled", "is_pseudolocalization_override_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_skip_placeholders_enabled"), "set_pseudolocalization_skip_placeholders_enabled", "is_pseudolocalization_skip_placeholders_enabled"); + ADD_PROPERTY(PropertyInfo(Variant::Type::FLOAT, "pseudolocalization_expansion_ratio"), "set_pseudolocalization_expansion_ratio", "get_pseudolocalization_expansion_ratio"); + ADD_PROPERTY(PropertyInfo(Variant::Type::STRING, "pseudolocalization_prefix"), "set_pseudolocalization_prefix", "get_pseudolocalization_prefix"); + ADD_PROPERTY(PropertyInfo(Variant::Type::STRING, "pseudolocalization_suffix"), "set_pseudolocalization_suffix", "get_pseudolocalization_suffix"); } TranslationDomain::TranslationDomain() { diff --git a/core/string/translation_domain.h b/core/string/translation_domain.h index 61399672174..55592d3b357 100644 --- a/core/string/translation_domain.h +++ b/core/string/translation_domain.h @@ -38,7 +38,28 @@ class Translation; class TranslationDomain : public RefCounted { GDCLASS(TranslationDomain, RefCounted); + struct PseudolocalizationConfig { + bool enabled = false; + bool accents_enabled = true; + bool double_vowels_enabled = false; + bool fake_bidi_enabled = false; + bool override_enabled = false; + bool skip_placeholders_enabled = true; + float expansion_ratio = 0.0; + String prefix = "["; + String suffix = "]"; + }; + HashSet> translations; + PseudolocalizationConfig pseudolocalization; + + String _get_override_string(const String &p_message) const; + String _double_vowels(const String &p_message) const; + String _replace_with_accented_string(const String &p_message) const; + String _wrap_with_fakebidi_characters(const String &p_message) const; + String _add_padding(const String &p_message, int p_length) const; + const char32_t *_get_accented_version(char32_t p_character) const; + bool _is_placeholder(const String &p_message, int p_index) const; protected: static void _bind_methods(); @@ -59,6 +80,27 @@ public: StringName translate(const StringName &p_message, const StringName &p_context) const; StringName translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const; + bool is_pseudolocalization_enabled() const; + void set_pseudolocalization_enabled(bool p_enabled); + bool is_pseudolocalization_accents_enabled() const; + void set_pseudolocalization_accents_enabled(bool p_enabled); + bool is_pseudolocalization_double_vowels_enabled() const; + void set_pseudolocalization_double_vowels_enabled(bool p_enabled); + bool is_pseudolocalization_fake_bidi_enabled() const; + void set_pseudolocalization_fake_bidi_enabled(bool p_enabled); + bool is_pseudolocalization_override_enabled() const; + void set_pseudolocalization_override_enabled(bool p_enabled); + bool is_pseudolocalization_skip_placeholders_enabled() const; + void set_pseudolocalization_skip_placeholders_enabled(bool p_enabled); + float get_pseudolocalization_expansion_ratio() const; + void set_pseudolocalization_expansion_ratio(float p_ratio); + String get_pseudolocalization_prefix() const; + void set_pseudolocalization_prefix(const String &p_prefix); + String get_pseudolocalization_suffix() const; + void set_pseudolocalization_suffix(const String &p_suffix); + + StringName pseudolocalize(const StringName &p_message) const; + TranslationDomain(); }; diff --git a/core/string/translation_server.cpp b/core/string/translation_server.cpp index c6b818a49b3..89b37d0b8a2 100644 --- a/core/string/translation_server.cpp +++ b/core/string/translation_server.cpp @@ -39,66 +39,6 @@ #include "main/main.h" #endif -struct _character_accent_pair { - const char32_t character; - const char32_t *accented_character; -}; - -static _character_accent_pair _character_to_accented[] = { - { 'A', U"Å" }, - { 'B', U"ß" }, - { 'C', U"Ç" }, - { 'D', U"Ð" }, - { 'E', U"É" }, - { 'F', U"F́" }, - { 'G', U"Ĝ" }, - { 'H', U"Ĥ" }, - { 'I', U"Ĩ" }, - { 'J', U"Ĵ" }, - { 'K', U"ĸ" }, - { 'L', U"Ł" }, - { 'M', U"Ḿ" }, - { 'N', U"й" }, - { 'O', U"Ö" }, - { 'P', U"Ṕ" }, - { 'Q', U"Q́" }, - { 'R', U"Ř" }, - { 'S', U"Ŝ" }, - { 'T', U"Ŧ" }, - { 'U', U"Ũ" }, - { 'V', U"Ṽ" }, - { 'W', U"Ŵ" }, - { 'X', U"X́" }, - { 'Y', U"Ÿ" }, - { 'Z', U"Ž" }, - { 'a', U"á" }, - { 'b', U"ḅ" }, - { 'c', U"ć" }, - { 'd', U"d́" }, - { 'e', U"é" }, - { 'f', U"f́" }, - { 'g', U"ǵ" }, - { 'h', U"h̀" }, - { 'i', U"í" }, - { 'j', U"ǰ" }, - { 'k', U"ḱ" }, - { 'l', U"ł" }, - { 'm', U"m̀" }, - { 'n', U"ή" }, - { 'o', U"ô" }, - { 'p', U"ṕ" }, - { 'q', U"q́" }, - { 'r', U"ŕ" }, - { 's', U"š" }, - { 't', U"ŧ" }, - { 'u', U"ü" }, - { 'v', U"ṽ" }, - { 'w', U"ŵ" }, - { 'x', U"x́" }, - { 'y', U"ý" }, - { 'z', U"ź" }, -}; - Vector TranslationServer::locale_script_info; HashMap TranslationServer::language_map; @@ -433,8 +373,7 @@ StringName TranslationServer::translate(const StringName &p_message, const Strin return p_message; } - const StringName res = main_domain->translate(p_message, p_context); - return pseudolocalization_enabled ? pseudolocalize(res) : res; + return main_domain->translate(p_message, p_context); } StringName TranslationServer::translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const { @@ -510,15 +449,15 @@ void TranslationServer::setup() { } fallback = GLOBAL_DEF("internationalization/locale/fallback", "en"); - pseudolocalization_enabled = GLOBAL_DEF("internationalization/pseudolocalization/use_pseudolocalization", false); - pseudolocalization_accents_enabled = GLOBAL_DEF("internationalization/pseudolocalization/replace_with_accents", true); - pseudolocalization_double_vowels_enabled = GLOBAL_DEF("internationalization/pseudolocalization/double_vowels", false); - pseudolocalization_fake_bidi_enabled = GLOBAL_DEF("internationalization/pseudolocalization/fake_bidi", false); - pseudolocalization_override_enabled = GLOBAL_DEF("internationalization/pseudolocalization/override", false); - expansion_ratio = GLOBAL_DEF("internationalization/pseudolocalization/expansion_ratio", 0.0); - pseudolocalization_prefix = GLOBAL_DEF("internationalization/pseudolocalization/prefix", "["); - pseudolocalization_suffix = GLOBAL_DEF("internationalization/pseudolocalization/suffix", "]"); - pseudolocalization_skip_placeholders_enabled = GLOBAL_DEF("internationalization/pseudolocalization/skip_placeholders", true); + main_domain->set_pseudolocalization_enabled(GLOBAL_DEF("internationalization/pseudolocalization/use_pseudolocalization", false)); + main_domain->set_pseudolocalization_accents_enabled(GLOBAL_DEF("internationalization/pseudolocalization/replace_with_accents", true)); + main_domain->set_pseudolocalization_double_vowels_enabled(GLOBAL_DEF("internationalization/pseudolocalization/double_vowels", false)); + main_domain->set_pseudolocalization_fake_bidi_enabled(GLOBAL_DEF("internationalization/pseudolocalization/fake_bidi", false)); + main_domain->set_pseudolocalization_override_enabled(GLOBAL_DEF("internationalization/pseudolocalization/override", false)); + main_domain->set_pseudolocalization_expansion_ratio(GLOBAL_DEF("internationalization/pseudolocalization/expansion_ratio", 0.0)); + main_domain->set_pseudolocalization_prefix(GLOBAL_DEF("internationalization/pseudolocalization/prefix", "[")); + main_domain->set_pseudolocalization_suffix(GLOBAL_DEF("internationalization/pseudolocalization/suffix", "]")); + main_domain->set_pseudolocalization_skip_placeholders_enabled(GLOBAL_DEF("internationalization/pseudolocalization/skip_placeholders", true)); #ifdef TOOLS_ENABLED ProjectSettings::get_singleton()->set_custom_property_info(PropertyInfo(Variant::STRING, "internationalization/locale/fallback", PROPERTY_HINT_LOCALE_ID, "")); @@ -567,11 +506,11 @@ StringName TranslationServer::doc_translate_plural(const StringName &p_message, } bool TranslationServer::is_pseudolocalization_enabled() const { - return pseudolocalization_enabled; + return main_domain->is_pseudolocalization_enabled(); } void TranslationServer::set_pseudolocalization_enabled(bool p_enabled) { - pseudolocalization_enabled = p_enabled; + main_domain->set_pseudolocalization_enabled(p_enabled); ResourceLoader::reload_translation_remaps(); @@ -581,14 +520,14 @@ void TranslationServer::set_pseudolocalization_enabled(bool p_enabled) { } void TranslationServer::reload_pseudolocalization() { - pseudolocalization_accents_enabled = GLOBAL_GET("internationalization/pseudolocalization/replace_with_accents"); - pseudolocalization_double_vowels_enabled = GLOBAL_GET("internationalization/pseudolocalization/double_vowels"); - pseudolocalization_fake_bidi_enabled = GLOBAL_GET("internationalization/pseudolocalization/fake_bidi"); - pseudolocalization_override_enabled = GLOBAL_GET("internationalization/pseudolocalization/override"); - expansion_ratio = GLOBAL_GET("internationalization/pseudolocalization/expansion_ratio"); - pseudolocalization_prefix = GLOBAL_GET("internationalization/pseudolocalization/prefix"); - pseudolocalization_suffix = GLOBAL_GET("internationalization/pseudolocalization/suffix"); - pseudolocalization_skip_placeholders_enabled = GLOBAL_GET("internationalization/pseudolocalization/skip_placeholders"); + main_domain->set_pseudolocalization_accents_enabled(GLOBAL_GET("internationalization/pseudolocalization/replace_with_accents")); + main_domain->set_pseudolocalization_double_vowels_enabled(GLOBAL_GET("internationalization/pseudolocalization/double_vowels")); + main_domain->set_pseudolocalization_fake_bidi_enabled(GLOBAL_GET("internationalization/pseudolocalization/fake_bidi")); + main_domain->set_pseudolocalization_override_enabled(GLOBAL_GET("internationalization/pseudolocalization/override")); + main_domain->set_pseudolocalization_expansion_ratio(GLOBAL_GET("internationalization/pseudolocalization/expansion_ratio")); + main_domain->set_pseudolocalization_prefix(GLOBAL_GET("internationalization/pseudolocalization/prefix")); + main_domain->set_pseudolocalization_suffix(GLOBAL_GET("internationalization/pseudolocalization/suffix")); + main_domain->set_pseudolocalization_skip_placeholders_enabled(GLOBAL_GET("internationalization/pseudolocalization/skip_placeholders")); ResourceLoader::reload_translation_remaps(); @@ -598,138 +537,7 @@ void TranslationServer::reload_pseudolocalization() { } StringName TranslationServer::pseudolocalize(const StringName &p_message) const { - String message = p_message; - int length = message.length(); - if (pseudolocalization_override_enabled) { - message = get_override_string(message); - } - - if (pseudolocalization_double_vowels_enabled) { - message = double_vowels(message); - } - - if (pseudolocalization_accents_enabled) { - message = replace_with_accented_string(message); - } - - if (pseudolocalization_fake_bidi_enabled) { - message = wrap_with_fakebidi_characters(message); - } - - StringName res = add_padding(message, length); - return res; -} - -StringName TranslationServer::tool_pseudolocalize(const StringName &p_message) const { - String message = p_message; - message = double_vowels(message); - message = replace_with_accented_string(message); - StringName res = "[!!! " + message + " !!!]"; - return res; -} - -String TranslationServer::get_override_string(String &p_message) const { - String res; - for (int i = 0; i < p_message.length(); i++) { - if (pseudolocalization_skip_placeholders_enabled && is_placeholder(p_message, i)) { - res += p_message[i]; - res += p_message[i + 1]; - i++; - continue; - } - res += '*'; - } - return res; -} - -String TranslationServer::double_vowels(String &p_message) const { - String res; - for (int i = 0; i < p_message.length(); i++) { - if (pseudolocalization_skip_placeholders_enabled && is_placeholder(p_message, i)) { - res += p_message[i]; - res += p_message[i + 1]; - i++; - continue; - } - res += p_message[i]; - if (p_message[i] == 'a' || p_message[i] == 'e' || p_message[i] == 'i' || p_message[i] == 'o' || p_message[i] == 'u' || - p_message[i] == 'A' || p_message[i] == 'E' || p_message[i] == 'I' || p_message[i] == 'O' || p_message[i] == 'U') { - res += p_message[i]; - } - } - return res; -}; - -String TranslationServer::replace_with_accented_string(String &p_message) const { - String res; - for (int i = 0; i < p_message.length(); i++) { - if (pseudolocalization_skip_placeholders_enabled && is_placeholder(p_message, i)) { - res += p_message[i]; - res += p_message[i + 1]; - i++; - continue; - } - const char32_t *accented = get_accented_version(p_message[i]); - if (accented) { - res += accented; - } else { - res += p_message[i]; - } - } - return res; -} - -String TranslationServer::wrap_with_fakebidi_characters(String &p_message) const { - String res; - char32_t fakebidiprefix = U'\u202e'; - char32_t fakebidisuffix = U'\u202c'; - res += fakebidiprefix; - // The fake bidi unicode gets popped at every newline so pushing it back at every newline. - for (int i = 0; i < p_message.length(); i++) { - if (p_message[i] == '\n') { - res += fakebidisuffix; - res += p_message[i]; - res += fakebidiprefix; - } else if (pseudolocalization_skip_placeholders_enabled && is_placeholder(p_message, i)) { - res += fakebidisuffix; - res += p_message[i]; - res += p_message[i + 1]; - res += fakebidiprefix; - i++; - } else { - res += p_message[i]; - } - } - res += fakebidisuffix; - return res; -} - -String TranslationServer::add_padding(const String &p_message, int p_length) const { - String underscores = String("_").repeat(p_length * expansion_ratio / 2); - String prefix = pseudolocalization_prefix + underscores; - String suffix = underscores + pseudolocalization_suffix; - - return prefix + p_message + suffix; -} - -const char32_t *TranslationServer::get_accented_version(char32_t p_character) const { - if (!is_ascii_alphabet_char(p_character)) { - return nullptr; - } - - for (unsigned int i = 0; i < sizeof(_character_to_accented) / sizeof(_character_to_accented[0]); i++) { - if (_character_to_accented[i].character == p_character) { - return _character_to_accented[i].accented_character; - } - } - - return nullptr; -} - -bool TranslationServer::is_placeholder(String &p_message, int p_index) const { - return p_index < p_message.length() - 1 && p_message[p_index] == '%' && - (p_message[p_index + 1] == 's' || p_message[p_index + 1] == 'c' || p_message[p_index + 1] == 'd' || - p_message[p_index + 1] == 'o' || p_message[p_index + 1] == 'x' || p_message[p_index + 1] == 'X' || p_message[p_index + 1] == 'f'); + return main_domain->pseudolocalize(p_message); } #ifdef TOOLS_ENABLED diff --git a/core/string/translation_server.h b/core/string/translation_server.h index 272fa1f11cc..a09230c0192 100644 --- a/core/string/translation_server.h +++ b/core/string/translation_server.h @@ -48,25 +48,6 @@ class TranslationServer : public Object { bool enabled = true; - bool pseudolocalization_enabled = false; - bool pseudolocalization_accents_enabled = false; - bool pseudolocalization_double_vowels_enabled = false; - bool pseudolocalization_fake_bidi_enabled = false; - bool pseudolocalization_override_enabled = false; - bool pseudolocalization_skip_placeholders_enabled = false; - float expansion_ratio = 0.0; - String pseudolocalization_prefix; - String pseudolocalization_suffix; - - StringName tool_pseudolocalize(const StringName &p_message) const; - String get_override_string(String &p_message) const; - String double_vowels(String &p_message) const; - String replace_with_accented_string(String &p_message) const; - String wrap_with_fakebidi_characters(String &p_message) const; - String add_padding(const String &p_message, int p_length) const; - const char32_t *get_accented_version(char32_t p_character) const; - bool is_placeholder(String &p_message, int p_index) const; - static TranslationServer *singleton; bool _load_translations(const String &p_from); String _standardize_locale(const String &p_locale, bool p_add_defaults) const; @@ -93,6 +74,8 @@ class TranslationServer : public Object { public: _FORCE_INLINE_ static TranslationServer *get_singleton() { return singleton; } + Ref get_editor_domain() const { return editor_domain; } + void set_enabled(bool p_enabled) { enabled = p_enabled; } _FORCE_INLINE_ bool is_enabled() const { return enabled; } diff --git a/doc/classes/TranslationDomain.xml b/doc/classes/TranslationDomain.xml index da6f2704bf3..5045f86260c 100644 --- a/doc/classes/TranslationDomain.xml +++ b/doc/classes/TranslationDomain.xml @@ -30,6 +30,13 @@ Returns the [Translation] instance that best matches [param locale]. Returns [code]null[/code] if there are no matches. + + + + + Returns the pseudolocalized string based on the [param message] passed in. + + @@ -57,4 +64,42 @@ + + + Replace all characters with their accented variants during pseudolocalization. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + Double vowels in strings during pseudolocalization to simulate the lengthening of text due to localization. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + If [code]true[/code], enables pseudolocalization for the project. This can be used to spot untranslatable strings or layout issues that may occur once the project is localized to languages that have longer strings than the source language. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + The expansion ratio to use during pseudolocalization. A value of [code]0.3[/code] is sufficient for most practical purposes, and will increase the length of each string by 30%. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + If [code]true[/code], emulate bidirectional (right-to-left) text when pseudolocalization is enabled. This can be used to spot issues with RTL layout and UI mirroring that will crop up if the project is localized to RTL languages such as Arabic or Hebrew. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + Replace all characters in the string with [code]*[/code]. Useful for finding non-localizable strings. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + Prefix that will be prepended to the pseudolocalized string. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + Skip placeholders for string formatting like [code]%s[/code] or [code]%f[/code] during pseudolocalization. Useful to identify strings which need additional control characters to display correctly. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + + Suffix that will be appended to the pseudolocalized string. + [b]Note:[/b] Updating this property does not automatically update texts in the scene tree. Please propagate the [constant MainLoop.NOTIFICATION_TRANSLATION_CHANGED] notification manually after you have finished modifying pseudolocalization related options. + + diff --git a/doc/classes/TranslationServer.xml b/doc/classes/TranslationServer.xml index 0a4965c36c1..69ca984f67d 100644 --- a/doc/classes/TranslationServer.xml +++ b/doc/classes/TranslationServer.xml @@ -125,12 +125,13 @@ Returns the pseudolocalized string based on the [param message] passed in. + [b]Note:[/b] This method always uses the main translation domain. - Reparses the pseudolocalization options and reloads the translation. + Reparses the pseudolocalization options and reloads the translation for the main translation domain. @@ -187,7 +188,7 @@ - If [code]true[/code], enables the use of pseudolocalization. See [member ProjectSettings.internationalization/pseudolocalization/use_pseudolocalization] for details. + If [code]true[/code], enables the use of pseudolocalization on the main translation domain. See [member ProjectSettings.internationalization/pseudolocalization/use_pseudolocalization] for details. diff --git a/main/main.cpp b/main/main.cpp index 36912c4fa39..5206e9b84c5 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -245,6 +245,7 @@ static MovieWriter *movie_writer = nullptr; static bool disable_vsync = false; static bool print_fps = false; #ifdef TOOLS_ENABLED +static bool editor_pseudolocalization = false; static bool dump_gdextension_interface = false; static bool dump_extension_api = false; static bool include_docs_in_extension_api_dump = false; @@ -649,6 +650,9 @@ void Main::print_help(const char *p_binary) { print_help_option("--fixed-fps ", "Force a fixed number of frames per second. This setting disables real-time synchronization.\n"); print_help_option("--delta-smoothing ", "Enable or disable frame delta smoothing [\"enable\", \"disable\"].\n"); print_help_option("--print-fps", "Print the frames per second to the stdout.\n"); +#ifdef TOOLS_ENABLED + print_help_option("--editor-pseudolocalization", "Enable pseudolocalization for the editor and the project manager.\n"); +#endif print_help_title("Standalone tools"); print_help_option("-s, --script