Path: blob/master/core/string/translation_domain.cpp
10277 views
/**************************************************************************/1/* translation_domain.cpp */2/**************************************************************************/3/* This file is part of: */4/* GODOT ENGINE */5/* https://godotengine.org */6/**************************************************************************/7/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */8/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */9/* */10/* Permission is hereby granted, free of charge, to any person obtaining */11/* a copy of this software and associated documentation files (the */12/* "Software"), to deal in the Software without restriction, including */13/* without limitation the rights to use, copy, modify, merge, publish, */14/* distribute, sublicense, and/or sell copies of the Software, and to */15/* permit persons to whom the Software is furnished to do so, subject to */16/* the following conditions: */17/* */18/* The above copyright notice and this permission notice shall be */19/* included in all copies or substantial portions of the Software. */20/* */21/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */22/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */23/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */24/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */25/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */26/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */27/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */28/**************************************************************************/2930#include "translation_domain.h"3132#include "core/string/translation.h"33#include "core/string/translation_server.h"3435struct _character_accent_pair {36const char32_t character;37const char32_t *accented_character;38};3940static _character_accent_pair _character_to_accented[] = {41{ 'A', U"Å" },42{ 'B', U"ß" },43{ 'C', U"Ç" },44{ 'D', U"Ð" },45{ 'E', U"É" },46{ 'F', U"F́" },47{ 'G', U"Ĝ" },48{ 'H', U"Ĥ" },49{ 'I', U"Ĩ" },50{ 'J', U"Ĵ" },51{ 'K', U"ĸ" },52{ 'L', U"Ł" },53{ 'M', U"Ḿ" },54{ 'N', U"й" },55{ 'O', U"Ö" },56{ 'P', U"Ṕ" },57{ 'Q', U"Q́" },58{ 'R', U"Ř" },59{ 'S', U"Ŝ" },60{ 'T', U"Ŧ" },61{ 'U', U"Ũ" },62{ 'V', U"Ṽ" },63{ 'W', U"Ŵ" },64{ 'X', U"X́" },65{ 'Y', U"Ÿ" },66{ 'Z', U"Ž" },67{ 'a', U"á" },68{ 'b', U"ḅ" },69{ 'c', U"ć" },70{ 'd', U"d́" },71{ 'e', U"é" },72{ 'f', U"f́" },73{ 'g', U"ǵ" },74{ 'h', U"h̀" },75{ 'i', U"í" },76{ 'j', U"ǰ" },77{ 'k', U"ḱ" },78{ 'l', U"ł" },79{ 'm', U"m̀" },80{ 'n', U"ή" },81{ 'o', U"ô" },82{ 'p', U"ṕ" },83{ 'q', U"q́" },84{ 'r', U"ŕ" },85{ 's', U"š" },86{ 't', U"ŧ" },87{ 'u', U"ü" },88{ 'v', U"ṽ" },89{ 'w', U"ŵ" },90{ 'x', U"x́" },91{ 'y', U"ý" },92{ 'z', U"ź" },93};9495String TranslationDomain::_get_override_string(const String &p_message) const {96String res;97for (int i = 0; i < p_message.length(); i++) {98if (pseudolocalization.skip_placeholders_enabled && _is_placeholder(p_message, i)) {99res += p_message[i];100res += p_message[i + 1];101i++;102continue;103}104res += '*';105}106return res;107}108109String TranslationDomain::_double_vowels(const String &p_message) const {110String res;111for (int i = 0; i < p_message.length(); i++) {112if (pseudolocalization.skip_placeholders_enabled && _is_placeholder(p_message, i)) {113res += p_message[i];114res += p_message[i + 1];115i++;116continue;117}118res += p_message[i];119if (p_message[i] == 'a' || p_message[i] == 'e' || p_message[i] == 'i' || p_message[i] == 'o' || p_message[i] == 'u' ||120p_message[i] == 'A' || p_message[i] == 'E' || p_message[i] == 'I' || p_message[i] == 'O' || p_message[i] == 'U') {121res += p_message[i];122}123}124return res;125}126127String TranslationDomain::_replace_with_accented_string(const String &p_message) const {128String res;129for (int i = 0; i < p_message.length(); i++) {130if (pseudolocalization.skip_placeholders_enabled && _is_placeholder(p_message, i)) {131res += p_message[i];132res += p_message[i + 1];133i++;134continue;135}136const char32_t *accented = _get_accented_version(p_message[i]);137if (accented) {138res += accented;139} else {140res += p_message[i];141}142}143return res;144}145146String TranslationDomain::_wrap_with_fakebidi_characters(const String &p_message) const {147String res;148char32_t fakebidiprefix = U'\u202e';149char32_t fakebidisuffix = U'\u202c';150res += fakebidiprefix;151// The fake bidi unicode gets popped at every newline so pushing it back at every newline.152for (int i = 0; i < p_message.length(); i++) {153if (p_message[i] == '\n') {154res += fakebidisuffix;155res += p_message[i];156res += fakebidiprefix;157} else if (pseudolocalization.skip_placeholders_enabled && _is_placeholder(p_message, i)) {158res += fakebidisuffix;159res += p_message[i];160res += p_message[i + 1];161res += fakebidiprefix;162i++;163} else {164res += p_message[i];165}166}167res += fakebidisuffix;168return res;169}170171String TranslationDomain::_add_padding(const String &p_message, int p_length) const {172String underscores = String("_").repeat(p_length * pseudolocalization.expansion_ratio / 2);173String prefix = pseudolocalization.prefix + underscores;174String suffix = underscores + pseudolocalization.suffix;175176return prefix + p_message + suffix;177}178179const char32_t *TranslationDomain::_get_accented_version(char32_t p_character) const {180if (!is_ascii_alphabet_char(p_character)) {181return nullptr;182}183184for (unsigned int i = 0; i < std::size(_character_to_accented); i++) {185if (_character_to_accented[i].character == p_character) {186return _character_to_accented[i].accented_character;187}188}189190return nullptr;191}192193bool TranslationDomain::_is_placeholder(const String &p_message, int p_index) const {194return p_index < p_message.length() - 1 && p_message[p_index] == '%' &&195(p_message[p_index + 1] == 's' || p_message[p_index + 1] == 'c' || p_message[p_index + 1] == 'd' ||196p_message[p_index + 1] == 'o' || p_message[p_index + 1] == 'x' || p_message[p_index + 1] == 'X' || p_message[p_index + 1] == 'f');197}198199StringName TranslationDomain::get_message_from_translations(const String &p_locale, const StringName &p_message, const StringName &p_context) const {200StringName res;201int best_score = 0;202203for (const Ref<Translation> &E : translations) {204ERR_CONTINUE(E.is_null());205int score = TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale());206if (score > 0 && score >= best_score) {207const StringName r = E->get_message(p_message, p_context);208if (!r) {209continue;210}211res = r;212best_score = score;213if (score == 10) {214break; // Exact match, skip the rest.215}216}217}218219return res;220}221222StringName TranslationDomain::get_message_from_translations(const String &p_locale, const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const {223StringName res;224int best_score = 0;225226for (const Ref<Translation> &E : translations) {227ERR_CONTINUE(E.is_null());228int score = TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale());229if (score > 0 && score >= best_score) {230const StringName r = E->get_plural_message(p_message, p_message_plural, p_n, p_context);231if (!r) {232continue;233}234res = r;235best_score = score;236if (score == 10) {237break; // Exact match, skip the rest.238}239}240}241242return res;243}244245PackedStringArray TranslationDomain::get_loaded_locales() const {246PackedStringArray locales;247for (const Ref<Translation> &E : translations) {248ERR_CONTINUE(E.is_null());249const String &locale = E->get_locale();250if (!locales.has(locale)) {251locales.push_back(locale);252}253}254return locales;255}256257// Translation objects that could potentially be used for the given locale.258HashSet<Ref<Translation>> TranslationDomain::get_potential_translations(const String &p_locale) const {259HashSet<Ref<Translation>> res;260261for (const Ref<Translation> &E : translations) {262ERR_CONTINUE(E.is_null());263264if (TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale()) > 0) {265res.insert(E);266}267}268return res;269}270271Ref<Translation> TranslationDomain::get_translation_object(const String &p_locale) const {272Ref<Translation> res;273int best_score = 0;274275for (const Ref<Translation> &E : translations) {276ERR_CONTINUE(E.is_null());277278int score = TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale());279if (score > 0 && score >= best_score) {280res = E;281best_score = score;282if (score == 10) {283break; // Exact match, skip the rest.284}285}286}287return res;288}289290void TranslationDomain::add_translation(const Ref<Translation> &p_translation) {291translations.insert(p_translation);292}293294void TranslationDomain::remove_translation(const Ref<Translation> &p_translation) {295translations.erase(p_translation);296}297298void TranslationDomain::clear() {299translations.clear();300}301302StringName TranslationDomain::translate(const StringName &p_message, const StringName &p_context) const {303if (!enabled) {304return p_message;305}306307const String &locale = locale_override.is_empty() ? TranslationServer::get_singleton()->get_locale() : locale_override;308StringName res = get_message_from_translations(locale, p_message, p_context);309310const String &fallback = TranslationServer::get_singleton()->get_fallback_locale();311if (!res && fallback.length() >= 2) {312res = get_message_from_translations(fallback, p_message, p_context);313}314315if (!res) {316return pseudolocalization.enabled ? pseudolocalize(p_message) : p_message;317}318return pseudolocalization.enabled ? pseudolocalize(res) : res;319}320321StringName TranslationDomain::translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const {322if (!enabled) {323return p_n == 1 ? p_message : p_message_plural;324}325326const String &locale = locale_override.is_empty() ? TranslationServer::get_singleton()->get_locale() : locale_override;327StringName res = get_message_from_translations(locale, p_message, p_message_plural, p_n, p_context);328329const String &fallback = TranslationServer::get_singleton()->get_fallback_locale();330if (!res && fallback.length() >= 2) {331res = get_message_from_translations(fallback, p_message, p_message_plural, p_n, p_context);332}333334if (!res) {335if (p_n == 1) {336return p_message;337}338return p_message_plural;339}340return res;341}342343String TranslationDomain::get_locale_override() const {344return locale_override;345}346347void TranslationDomain::set_locale_override(const String &p_locale) {348locale_override = p_locale.is_empty() ? p_locale : TranslationServer::get_singleton()->standardize_locale(p_locale);349}350351bool TranslationDomain::is_enabled() const {352return enabled;353}354355void TranslationDomain::set_enabled(bool p_enabled) {356enabled = p_enabled;357}358359bool TranslationDomain::is_pseudolocalization_enabled() const {360return pseudolocalization.enabled;361}362363void TranslationDomain::set_pseudolocalization_enabled(bool p_enabled) {364pseudolocalization.enabled = p_enabled;365}366367bool TranslationDomain::is_pseudolocalization_accents_enabled() const {368return pseudolocalization.accents_enabled;369}370371void TranslationDomain::set_pseudolocalization_accents_enabled(bool p_enabled) {372pseudolocalization.accents_enabled = p_enabled;373}374375bool TranslationDomain::is_pseudolocalization_double_vowels_enabled() const {376return pseudolocalization.double_vowels_enabled;377}378379void TranslationDomain::set_pseudolocalization_double_vowels_enabled(bool p_enabled) {380pseudolocalization.double_vowels_enabled = p_enabled;381}382383bool TranslationDomain::is_pseudolocalization_fake_bidi_enabled() const {384return pseudolocalization.fake_bidi_enabled;385}386387void TranslationDomain::set_pseudolocalization_fake_bidi_enabled(bool p_enabled) {388pseudolocalization.fake_bidi_enabled = p_enabled;389}390391bool TranslationDomain::is_pseudolocalization_override_enabled() const {392return pseudolocalization.override_enabled;393}394395void TranslationDomain::set_pseudolocalization_override_enabled(bool p_enabled) {396pseudolocalization.override_enabled = p_enabled;397}398399bool TranslationDomain::is_pseudolocalization_skip_placeholders_enabled() const {400return pseudolocalization.skip_placeholders_enabled;401}402403void TranslationDomain::set_pseudolocalization_skip_placeholders_enabled(bool p_enabled) {404pseudolocalization.skip_placeholders_enabled = p_enabled;405}406407float TranslationDomain::get_pseudolocalization_expansion_ratio() const {408return pseudolocalization.expansion_ratio;409}410411void TranslationDomain::set_pseudolocalization_expansion_ratio(float p_ratio) {412pseudolocalization.expansion_ratio = p_ratio;413}414415String TranslationDomain::get_pseudolocalization_prefix() const {416return pseudolocalization.prefix;417}418419void TranslationDomain::set_pseudolocalization_prefix(const String &p_prefix) {420pseudolocalization.prefix = p_prefix;421}422423String TranslationDomain::get_pseudolocalization_suffix() const {424return pseudolocalization.suffix;425}426427void TranslationDomain::set_pseudolocalization_suffix(const String &p_suffix) {428pseudolocalization.suffix = p_suffix;429}430431StringName TranslationDomain::pseudolocalize(const StringName &p_message) const {432if (p_message.is_empty()) {433return p_message;434}435436String message = p_message;437int length = message.length();438if (pseudolocalization.override_enabled) {439message = _get_override_string(message);440}441442if (pseudolocalization.double_vowels_enabled) {443message = _double_vowels(message);444}445446if (pseudolocalization.accents_enabled) {447message = _replace_with_accented_string(message);448}449450if (pseudolocalization.fake_bidi_enabled) {451message = _wrap_with_fakebidi_characters(message);452}453454return _add_padding(message, length);455}456457void TranslationDomain::_bind_methods() {458ClassDB::bind_method(D_METHOD("get_translation_object", "locale"), &TranslationDomain::get_translation_object);459ClassDB::bind_method(D_METHOD("add_translation", "translation"), &TranslationDomain::add_translation);460ClassDB::bind_method(D_METHOD("remove_translation", "translation"), &TranslationDomain::remove_translation);461ClassDB::bind_method(D_METHOD("clear"), &TranslationDomain::clear);462ClassDB::bind_method(D_METHOD("translate", "message", "context"), &TranslationDomain::translate, DEFVAL(StringName()));463ClassDB::bind_method(D_METHOD("translate_plural", "message", "message_plural", "n", "context"), &TranslationDomain::translate_plural, DEFVAL(StringName()));464ClassDB::bind_method(D_METHOD("get_locale_override"), &TranslationDomain::get_locale_override);465ClassDB::bind_method(D_METHOD("set_locale_override", "locale"), &TranslationDomain::set_locale_override);466ClassDB::bind_method(D_METHOD("is_enabled"), &TranslationDomain::is_enabled);467ClassDB::bind_method(D_METHOD("set_enabled", "enabled"), &TranslationDomain::set_enabled);468469ClassDB::bind_method(D_METHOD("is_pseudolocalization_enabled"), &TranslationDomain::is_pseudolocalization_enabled);470ClassDB::bind_method(D_METHOD("set_pseudolocalization_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_enabled);471ClassDB::bind_method(D_METHOD("is_pseudolocalization_accents_enabled"), &TranslationDomain::is_pseudolocalization_accents_enabled);472ClassDB::bind_method(D_METHOD("set_pseudolocalization_accents_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_accents_enabled);473ClassDB::bind_method(D_METHOD("is_pseudolocalization_double_vowels_enabled"), &TranslationDomain::is_pseudolocalization_double_vowels_enabled);474ClassDB::bind_method(D_METHOD("set_pseudolocalization_double_vowels_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_double_vowels_enabled);475ClassDB::bind_method(D_METHOD("is_pseudolocalization_fake_bidi_enabled"), &TranslationDomain::is_pseudolocalization_fake_bidi_enabled);476ClassDB::bind_method(D_METHOD("set_pseudolocalization_fake_bidi_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_fake_bidi_enabled);477ClassDB::bind_method(D_METHOD("is_pseudolocalization_override_enabled"), &TranslationDomain::is_pseudolocalization_override_enabled);478ClassDB::bind_method(D_METHOD("set_pseudolocalization_override_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_override_enabled);479ClassDB::bind_method(D_METHOD("is_pseudolocalization_skip_placeholders_enabled"), &TranslationDomain::is_pseudolocalization_skip_placeholders_enabled);480ClassDB::bind_method(D_METHOD("set_pseudolocalization_skip_placeholders_enabled", "enabled"), &TranslationDomain::set_pseudolocalization_skip_placeholders_enabled);481ClassDB::bind_method(D_METHOD("get_pseudolocalization_expansion_ratio"), &TranslationDomain::get_pseudolocalization_expansion_ratio);482ClassDB::bind_method(D_METHOD("set_pseudolocalization_expansion_ratio", "ratio"), &TranslationDomain::set_pseudolocalization_expansion_ratio);483ClassDB::bind_method(D_METHOD("get_pseudolocalization_prefix"), &TranslationDomain::get_pseudolocalization_prefix);484ClassDB::bind_method(D_METHOD("set_pseudolocalization_prefix", "prefix"), &TranslationDomain::set_pseudolocalization_prefix);485ClassDB::bind_method(D_METHOD("get_pseudolocalization_suffix"), &TranslationDomain::get_pseudolocalization_suffix);486ClassDB::bind_method(D_METHOD("set_pseudolocalization_suffix", "suffix"), &TranslationDomain::set_pseudolocalization_suffix);487ClassDB::bind_method(D_METHOD("pseudolocalize", "message"), &TranslationDomain::pseudolocalize);488489ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "enabled"), "set_enabled", "is_enabled");490ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_enabled"), "set_pseudolocalization_enabled", "is_pseudolocalization_enabled");491ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_accents_enabled"), "set_pseudolocalization_accents_enabled", "is_pseudolocalization_accents_enabled");492ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_double_vowels_enabled"), "set_pseudolocalization_double_vowels_enabled", "is_pseudolocalization_double_vowels_enabled");493ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_fake_bidi_enabled"), "set_pseudolocalization_fake_bidi_enabled", "is_pseudolocalization_fake_bidi_enabled");494ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_override_enabled"), "set_pseudolocalization_override_enabled", "is_pseudolocalization_override_enabled");495ADD_PROPERTY(PropertyInfo(Variant::Type::BOOL, "pseudolocalization_skip_placeholders_enabled"), "set_pseudolocalization_skip_placeholders_enabled", "is_pseudolocalization_skip_placeholders_enabled");496ADD_PROPERTY(PropertyInfo(Variant::Type::FLOAT, "pseudolocalization_expansion_ratio"), "set_pseudolocalization_expansion_ratio", "get_pseudolocalization_expansion_ratio");497ADD_PROPERTY(PropertyInfo(Variant::Type::STRING, "pseudolocalization_prefix"), "set_pseudolocalization_prefix", "get_pseudolocalization_prefix");498ADD_PROPERTY(PropertyInfo(Variant::Type::STRING, "pseudolocalization_suffix"), "set_pseudolocalization_suffix", "get_pseudolocalization_suffix");499}500501TranslationDomain::TranslationDomain() {502}503504505