From e79dde1cbb0e9a97c17bb5d535809efbcee1d965 Mon Sep 17 00:00:00 2001 From: Angad Kambli Date: Sun, 8 Aug 2021 20:37:57 +0530 Subject: [PATCH] add pseudolocalization to Godot --- core/string/translation.cpp | 260 +++++++++++++++++++++++++++++- core/string/translation.h | 27 ++++ doc/classes/ProjectSettings.xml | 28 ++++ doc/classes/TranslationServer.xml | 18 +++ editor/editor_settings.cpp | 7 +- 5 files changed, 335 insertions(+), 5 deletions(-) diff --git a/core/string/translation.cpp b/core/string/translation.cpp index 153f0190fd..07a6e395c3 100644 --- a/core/string/translation.cpp +++ b/core/string/translation.cpp @@ -923,6 +923,66 @@ void Translation::_bind_methods() { /////////////////////////////////////////////// +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"ź" }, +}; + bool TranslationServer::is_locale_valid(const String &p_locale) { const char **ptr = locale_list; @@ -1095,10 +1155,10 @@ StringName TranslationServer::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 TranslationServer::translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const { @@ -1211,7 +1271,18 @@ void TranslationServer::setup() { } else { set_locale(OS::get_singleton()->get_locale()); } + 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); + #ifdef TOOLS_ENABLED { String options = ""; @@ -1252,10 +1323,10 @@ StringName TranslationServer::tool_translate(const StringName &p_message, const if (tool_translation.is_valid()) { StringName r = tool_translation->get_message(p_message, p_context); if (r) { - return r; + return editor_pseudolocalization ? tool_pseudolocalize(r) : r; } } - return p_message; + return editor_pseudolocalization ? tool_pseudolocalize(p_message) : p_message; } StringName TranslationServer::tool_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const { @@ -1300,6 +1371,181 @@ StringName TranslationServer::doc_translate_plural(const StringName &p_message, return p_message_plural; } +bool TranslationServer::is_pseudolocalization_enabled() const { + return pseudolocalization_enabled; +} + +void TranslationServer::set_pseudolocalization_enabled(bool p_enabled) { + pseudolocalization_enabled = p_enabled; + + if (OS::get_singleton()->get_main_loop()) { + OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_TRANSLATION_CHANGED); + } + ResourceLoader::reload_translation_remaps(); +} + +void TranslationServer::set_editor_pseudolocalization(bool p_enabled) { + editor_pseudolocalization = 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"); + + if (OS::get_singleton()->get_main_loop()) { + OS::get_singleton()->get_main_loop()->notification(MainLoop::NOTIFICATION_TRANSLATION_CHANGED); + } + ResourceLoader::reload_translation_remaps(); +} + +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.size(); 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.size(); 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.size(); 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.size(); 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(String &p_message, int p_length) const { + String res; + String prefix = pseudolocalization_prefix; + String suffix; + for (int i = 0; i < p_length * expansion_ratio / 2; i++) { + prefix += "_"; + suffix += "_"; + } + suffix += pseudolocalization_suffix; + res += prefix; + res += p_message; + res += suffix; + return res; +} + +const char32_t *TranslationServer::get_accented_version(char32_t p_character) const { + if (!((p_character >= 'a' && p_character <= 'z') || (p_character >= 'A' && p_character <= 'Z'))) { + 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_message[p_index] == '%' && p_index < p_message.size() - 1 && + (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'); +} + void TranslationServer::_bind_methods() { ClassDB::bind_method(D_METHOD("set_locale", "locale"), &TranslationServer::set_locale); ClassDB::bind_method(D_METHOD("get_locale"), &TranslationServer::get_locale); @@ -1316,6 +1562,12 @@ void TranslationServer::_bind_methods() { ClassDB::bind_method(D_METHOD("clear"), &TranslationServer::clear); ClassDB::bind_method(D_METHOD("get_loaded_locales"), &TranslationServer::get_loaded_locales); + + ClassDB::bind_method(D_METHOD("is_pseudolocalization_enabled"), &TranslationServer::is_pseudolocalization_enabled); + ClassDB::bind_method(D_METHOD("set_pseudolocalization_enabled", "enabled"), &TranslationServer::set_pseudolocalization_enabled); + ClassDB::bind_method(D_METHOD("reload_pseudolocalization"), &TranslationServer::reload_pseudolocalization); + ClassDB::bind_method(D_METHOD("pseudolocalize", "message"), &TranslationServer::pseudolocalize); + ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_enabled"), "set_pseudolocalization_enabled", "is_pseudolocalization_enabled"); } void TranslationServer::load_translations() { diff --git a/core/string/translation.h b/core/string/translation.h index 72a828227e..4f179ac0fe 100644 --- a/core/string/translation.h +++ b/core/string/translation.h @@ -77,6 +77,26 @@ 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; + bool editor_pseudolocalization = 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(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); @@ -104,6 +124,13 @@ 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; + StringName pseudolocalize(const StringName &p_message) const; + + bool is_pseudolocalization_enabled() const; + void set_pseudolocalization_enabled(bool p_enabled); + void set_editor_pseudolocalization(bool p_enabled); + void reload_pseudolocalization(); + static Vector get_all_locales(); static Vector get_all_locale_names(); static bool is_locale_valid(const String &p_locale); diff --git a/doc/classes/ProjectSettings.xml b/doc/classes/ProjectSettings.xml index fbd257cdba..1d9be0ab32 100644 --- a/doc/classes/ProjectSettings.xml +++ b/doc/classes/ProjectSettings.xml @@ -775,6 +775,34 @@ If non-empty, this locale will be used when running the project from the editor. + + Double vowels in strings during pseudolocalization to simulate the lengthening of text due to localization. + + + 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%. + + + 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. + + + Replace all characters in the string with [code]*[/code]. Useful for finding non-localizable strings. + + + Prefix that will be prepended to the pseudolocalized string. + + + Replace all characters with their accented variants during pseudolocalization. + + + 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. + + + Suffix that will be appended to the pseudolocalized string. + + + 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] This property is only read when the project starts. To toggle pseudolocalization at run-time, use [member TranslationServer.pseudolocalization_enabled] instead. + Force layout direction and text writing direction to RTL for all locales. diff --git a/doc/classes/TranslationServer.xml b/doc/classes/TranslationServer.xml index 664cb3e2e3..5e98cb60bc 100644 --- a/doc/classes/TranslationServer.xml +++ b/doc/classes/TranslationServer.xml @@ -60,6 +60,19 @@ It will return a [code]nullptr[/code] if there is no [Translation] instance that matches the [code]locale[/code]. + + + + + Returns the pseudolocalized string based on the [code]p_message[/code] passed in. + + + + + + Reparses the pseudolocalization options and reloads the translation. + + @@ -106,6 +119,11 @@ + + + If [code]true[/code], enables the use of pseudolocalization. See [member ProjectSettings.internationalization/pseudolocalization/use_pseudolocalization] for details. + + diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp index 0667d7116f..34d167e54c 100644 --- a/editor/editor_settings.cpp +++ b/editor/editor_settings.cpp @@ -374,6 +374,11 @@ void EditorSettings::_load_defaults(Ref p_extra_config) { _initial_set("interface/editor/display_scale", 0); // Display what the Auto display scale setting effectively corresponds to. float scale = get_auto_display_scale(); + + _initial_set("interface/editor/enable_debugging_pseudolocalization", false); + set_restart_if_changed("interface/editor/enable_debugging_pseudolocalization", true); + // Use pseudolocalization in editor. + hints["interface/editor/display_scale"] = PropertyInfo(Variant::INT, "interface/editor/display_scale", PROPERTY_HINT_ENUM, vformat("Auto (%d%%),75%%,100%%,125%%,150%%,175%%,200%%,Custom", Math::round(scale * 100)), PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED); _initial_set("interface/editor/custom_display_scale", 1.0f); hints["interface/editor/custom_display_scale"] = PropertyInfo(Variant::FLOAT, "interface/editor/custom_display_scale", PROPERTY_HINT_RANGE, "0.5,3,0.01", PROPERTY_USAGE_DEFAULT | PROPERTY_USAGE_RESTART_IF_CHANGED); @@ -955,11 +960,11 @@ fail: } void EditorSettings::setup_language() { + TranslationServer::get_singleton()->set_editor_pseudolocalization(get("interface/editor/enable_debugging_pseudolocalization")); String lang = get("interface/editor/editor_language"); if (lang == "en") { return; // Default, nothing to do. } - // Load editor translation for configured/detected locale. EditorTranslationList *etl = _editor_translations; while (etl->data) {