From 68d494e24e2d6704ae95b1d00fa91b440311e8c3 Mon Sep 17 00:00:00 2001 From: Haoyu Qiu Date: Thu, 15 Aug 2024 09:14:41 +0800 Subject: [PATCH] Add translation domain --- core/register_core_types.cpp | 1 + core/string/translation_domain.cpp | 165 +++++++++++++++++++++++++++++ core/string/translation_domain.h | 65 ++++++++++++ core/string/translation_server.cpp | 153 +++++++++----------------- core/string/translation_server.h | 12 ++- doc/classes/TranslationDomain.xml | 59 +++++++++++ doc/classes/TranslationServer.xml | 40 +++++-- 7 files changed, 379 insertions(+), 116 deletions(-) create mode 100644 core/string/translation_domain.cpp create mode 100644 core/string/translation_domain.h create mode 100644 doc/classes/TranslationDomain.xml diff --git a/core/register_core_types.cpp b/core/register_core_types.cpp index c866ff04152..3a578d01a6b 100644 --- a/core/register_core_types.cpp +++ b/core/register_core_types.cpp @@ -233,6 +233,7 @@ void register_core_types() { GDREGISTER_CLASS(MainLoop); GDREGISTER_CLASS(Translation); + GDREGISTER_CLASS(TranslationDomain); GDREGISTER_CLASS(OptimizedTranslation); GDREGISTER_CLASS(UndoRedo); GDREGISTER_CLASS(TriangleMesh); diff --git a/core/string/translation_domain.cpp b/core/string/translation_domain.cpp new file mode 100644 index 00000000000..b44eb40366a --- /dev/null +++ b/core/string/translation_domain.cpp @@ -0,0 +1,165 @@ +/**************************************************************************/ +/* translation_domain.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "translation_domain.h" + +#include "core/string/translation.h" +#include "core/string/translation_server.h" + +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; + + for (const Ref &E : translations) { + ERR_CONTINUE(E.is_null()); + int score = TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale()); + if (score > 0 && score >= best_score) { + const StringName r = E->get_message(p_message, p_context); + if (!r) { + continue; + } + res = r; + best_score = score; + if (score == 10) { + break; // Exact match, skip the rest. + } + } + } + + return res; +} + +StringName 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 { + StringName res; + int best_score = 0; + + for (const Ref &E : translations) { + ERR_CONTINUE(E.is_null()); + int score = TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale()); + if (score > 0 && score >= best_score) { + const StringName r = E->get_plural_message(p_message, p_message_plural, p_n, p_context); + if (!r) { + continue; + } + res = r; + best_score = score; + if (score == 10) { + break; // Exact match, skip the rest. + } + } + } + + return res; +} + +PackedStringArray TranslationDomain::get_loaded_locales() const { + PackedStringArray locales; + for (const Ref &E : translations) { + ERR_CONTINUE(E.is_null()); + locales.push_back(E->get_locale()); + } + return locales; +} + +Ref TranslationDomain::get_translation_object(const String &p_locale) const { + Ref res; + int best_score = 0; + + for (const Ref &E : translations) { + ERR_CONTINUE(E.is_null()); + + int score = TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale()); + if (score > 0 && score >= best_score) { + res = E; + best_score = score; + if (score == 10) { + break; // Exact match, skip the rest. + } + } + } + return res; +} + +void TranslationDomain::add_translation(const Ref &p_translation) { + translations.insert(p_translation); +} + +void TranslationDomain::remove_translation(const Ref &p_translation) { + translations.erase(p_translation); +} + +void TranslationDomain::clear() { + translations.clear(); +} + +StringName TranslationDomain::translate(const StringName &p_message, const StringName &p_context) const { + const String &locale = TranslationServer::get_singleton()->get_locale(); + StringName res = get_message_from_translations(locale, p_message, p_context); + + const String &fallback = TranslationServer::get_singleton()->get_fallback_locale(); + if (!res && fallback.length() >= 2) { + res = get_message_from_translations(fallback, p_message, p_context); + } + + if (!res) { + return p_message; + } + return res; +} + +StringName TranslationDomain::translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context) const { + const String &locale = TranslationServer::get_singleton()->get_locale(); + StringName res = get_message_from_translations(locale, p_message, p_message_plural, p_n, p_context); + + const String &fallback = TranslationServer::get_singleton()->get_fallback_locale(); + if (!res && fallback.length() >= 2) { + res = get_message_from_translations(fallback, p_message, p_message_plural, p_n, p_context); + } + + if (!res) { + if (p_n == 1) { + return p_message; + } + return p_message_plural; + } + return res; +} + +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); + ClassDB::bind_method(D_METHOD("remove_translation", "translation"), &TranslationDomain::remove_translation); + 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())); +} + +TranslationDomain::TranslationDomain() { +} diff --git a/core/string/translation_domain.h b/core/string/translation_domain.h new file mode 100644 index 00000000000..61399672174 --- /dev/null +++ b/core/string/translation_domain.h @@ -0,0 +1,65 @@ +/**************************************************************************/ +/* translation_domain.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TRANSLATION_DOMAIN_H +#define TRANSLATION_DOMAIN_H + +#include "core/object/ref_counted.h" + +class Translation; + +class TranslationDomain : public RefCounted { + GDCLASS(TranslationDomain, RefCounted); + + HashSet> translations; + +protected: + static void _bind_methods(); + +public: + // Methods in this section are not intended for scripting. + StringName get_message_from_translations(const String &p_locale, const StringName &p_message, const StringName &p_context) const; + StringName 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; + PackedStringArray get_loaded_locales() const; + +public: + Ref get_translation_object(const String &p_locale) const; + + void add_translation(const Ref &p_translation); + void remove_translation(const Ref &p_translation); + void clear(); + + 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; + + TranslationDomain(); +}; + +#endif // TRANSLATION_DOMAIN_H diff --git a/core/string/translation_server.cpp b/core/string/translation_server.cpp index d4aa1523409..ae822b43fb4 100644 --- a/core/string/translation_server.cpp +++ b/core/string/translation_server.cpp @@ -404,69 +404,36 @@ String TranslationServer::get_locale() const { return locale; } +String TranslationServer::get_fallback_locale() const { + return fallback; +} + PackedStringArray TranslationServer::get_loaded_locales() const { - PackedStringArray locales; - for (const Ref &E : translations) { - const Ref &t = E; - ERR_FAIL_COND_V(t.is_null(), PackedStringArray()); - String l = t->get_locale(); - - locales.push_back(l); - } - - return locales; + return main_domain->get_loaded_locales(); } void TranslationServer::add_translation(const Ref &p_translation) { - translations.insert(p_translation); + main_domain->add_translation(p_translation); } void TranslationServer::remove_translation(const Ref &p_translation) { - translations.erase(p_translation); + main_domain->remove_translation(p_translation); } Ref TranslationServer::get_translation_object(const String &p_locale) { - Ref res; - int best_score = 0; - - for (const Ref &E : translations) { - const Ref &t = E; - ERR_FAIL_COND_V(t.is_null(), nullptr); - String l = t->get_locale(); - - int score = compare_locales(p_locale, l); - if (score > 0 && score >= best_score) { - res = t; - best_score = score; - if (score == 10) { - break; // Exact match, skip the rest. - } - } - } - return res; + return main_domain->get_translation_object(p_locale); } void TranslationServer::clear() { - translations.clear(); + main_domain->clear(); } StringName TranslationServer::translate(const StringName &p_message, const StringName &p_context) const { - // Match given message against the translation catalog for the project locale. - if (!enabled) { return p_message; } - StringName res = _get_message_from_translations(p_message, p_context, locale, false); - - if (!res && fallback.length() >= 2) { - res = _get_message_from_translations(p_message, p_context, fallback, false); - } - - if (!res) { - return pseudolocalization_enabled ? pseudolocalize(p_message) : p_message; - } - + const StringName res = main_domain->translate(p_message, p_context); return pseudolocalization_enabled ? pseudolocalize(res) : res; } @@ -478,51 +445,7 @@ StringName TranslationServer::translate_plural(const StringName &p_message, cons return p_message_plural; } - StringName res = _get_message_from_translations(p_message, p_context, locale, true, p_message_plural, p_n); - - if (!res && fallback.length() >= 2) { - res = _get_message_from_translations(p_message, p_context, fallback, true, p_message_plural, p_n); - } - - if (!res) { - if (p_n == 1) { - return p_message; - } - return p_message_plural; - } - - return res; -} - -StringName TranslationServer::_get_message_from_translations(const StringName &p_message, const StringName &p_context, const String &p_locale, bool plural, const String &p_message_plural, int p_n) const { - StringName res; - int best_score = 0; - - for (const Ref &E : translations) { - const Ref &t = E; - ERR_FAIL_COND_V(t.is_null(), p_message); - String l = t->get_locale(); - - int score = compare_locales(p_locale, l); - if (score > 0 && score >= best_score) { - StringName r; - if (!plural) { - r = t->get_message(p_message, p_context); - } else { - r = t->get_plural_message(p_message, p_message_plural, p_n, p_context); - } - if (!r) { - continue; - } - res = r; - best_score = score; - if (score == 10) { - break; // Exact match, skip the rest. - } - } - } - - return res; + return main_domain->translate_plural(p_message, p_message_plural, p_n, p_context); } TranslationServer *TranslationServer::singleton = nullptr; @@ -549,6 +472,34 @@ bool TranslationServer::_load_translations(const String &p_from) { return false; } +bool TranslationServer::has_domain(const StringName &p_domain) const { + if (p_domain == StringName()) { + return true; + } + return custom_domains.has(p_domain); +} + +Ref TranslationServer::get_or_add_domain(const StringName &p_domain) { + if (p_domain == StringName()) { + return main_domain; + } + const Ref *domain = custom_domains.getptr(p_domain); + if (domain) { + if (domain->is_valid()) { + return *domain; + } + ERR_PRINT("Bug (please report): Found invalid translation domain."); + } + Ref new_domain = memnew(TranslationDomain); + custom_domains[p_domain] = new_domain; + return new_domain; +} + +void TranslationServer::remove_domain(const StringName &p_domain) { + ERR_FAIL_COND_MSG(p_domain == StringName(), "Cannot remove main translation domain."); + custom_domains.erase(p_domain); +} + void TranslationServer::setup() { String test = GLOBAL_DEF("internationalization/locale/test", ""); test = test.strip_edges(); @@ -595,24 +546,11 @@ String TranslationServer::get_tool_locale() { { #endif // Look for best matching loaded translation. - String best_locale = "en"; - int best_score = 0; - - for (const Ref &E : translations) { - const Ref &t = E; - ERR_FAIL_COND_V(t.is_null(), best_locale); - String l = t->get_locale(); - - int score = compare_locales(locale, l); - if (score > 0 && score >= best_score) { - best_locale = l; - best_score = score; - if (score == 10) { - break; // Exact match, skip the rest. - } - } + Ref t = main_domain->get_translation_object(locale); + if (t.is_null()) { + return "en"; } - return best_locale; + return t->get_locale(); } } @@ -925,6 +863,10 @@ void TranslationServer::_bind_methods() { ClassDB::bind_method(D_METHOD("remove_translation", "translation"), &TranslationServer::remove_translation); ClassDB::bind_method(D_METHOD("get_translation_object", "locale"), &TranslationServer::get_translation_object); + ClassDB::bind_method(D_METHOD("has_domain", "domain"), &TranslationServer::has_domain); + ClassDB::bind_method(D_METHOD("get_or_add_domain", "domain"), &TranslationServer::get_or_add_domain); + ClassDB::bind_method(D_METHOD("remove_domain", "domain"), &TranslationServer::remove_domain); + ClassDB::bind_method(D_METHOD("clear"), &TranslationServer::clear); ClassDB::bind_method(D_METHOD("get_loaded_locales"), &TranslationServer::get_loaded_locales); @@ -947,5 +889,6 @@ void TranslationServer::load_translations() { TranslationServer::TranslationServer() { singleton = this; + main_domain.instantiate(); init_locale_info(); } diff --git a/core/string/translation_server.h b/core/string/translation_server.h index bb285ab19ca..1271abec99d 100644 --- a/core/string/translation_server.h +++ b/core/string/translation_server.h @@ -32,6 +32,7 @@ #define TRANSLATION_SERVER_H #include "core/string/translation.h" +#include "core/string/translation_domain.h" class TranslationServer : public Object { GDCLASS(TranslationServer, Object); @@ -39,7 +40,9 @@ class TranslationServer : public Object { String locale = "en"; String fallback; - HashSet> translations; + Ref main_domain; + HashMap> custom_domains; + Ref tool_translation; Ref property_translation; Ref doc_translation; @@ -70,8 +73,6 @@ class TranslationServer : public Object { bool _load_translations(const String &p_from); String _standardize_locale(const String &p_locale, bool p_add_defaults) const; - StringName _get_message_from_translations(const StringName &p_message, const StringName &p_context, const String &p_locale, bool plural, const String &p_message_plural = "", int p_n = 0) const; - static void _bind_methods(); struct LocaleScriptInfo { @@ -99,6 +100,7 @@ public: void set_locale(const String &p_locale); String get_locale() const; + String get_fallback_locale() const; Ref get_translation_object(const String &p_locale); Vector get_all_languages() const; @@ -144,6 +146,10 @@ public: StringName extractable_translate(const StringName &p_message, const StringName &p_context = "") const; StringName extractable_translate_plural(const StringName &p_message, const StringName &p_message_plural, int p_n, const StringName &p_context = "") const; + bool has_domain(const StringName &p_domain) const; + Ref get_or_add_domain(const StringName &p_domain); + void remove_domain(const StringName &p_domain); + void setup(); void clear(); diff --git a/doc/classes/TranslationDomain.xml b/doc/classes/TranslationDomain.xml new file mode 100644 index 00000000000..8079685885d --- /dev/null +++ b/doc/classes/TranslationDomain.xml @@ -0,0 +1,59 @@ + + + + A self-contained collection of [Translation] resources. + + + [TranslationDomain] is a self-contained collection of [Translation] resources. Translations can be added to or removed from it. + + + + + + + + + Adds a translation. + + + + + + Removes all translations. + + + + + + + Returns the [Translation] instance that best matches [param locale]. Returns [code]null[/code] if there are no matches. + + + + + + + Removes the given translation. + + + + + + + + Returns the current locale's translation for the given message and context. + + + + + + + + + + Returns the current locale's translation for the given message, plural message and context. + The number [param n] is the number or quantity of the plural object. It will be used to guide the translation system to fetch the correct plural form for the selected language. + + + + diff --git a/doc/classes/TranslationServer.xml b/doc/classes/TranslationServer.xml index db1a65278c9..39afa2c0d2c 100644 --- a/doc/classes/TranslationServer.xml +++ b/doc/classes/TranslationServer.xml @@ -4,7 +4,8 @@ The server responsible for language translations. - The server that manages all language translations. Translations can be added to or removed from it. + The translation server is the API backend that manages all language translations. + Translations are stored in [TranslationDomain]s, which can be accessed by name. The most commonly used translation domain is the main translation domain, which always exists and can be accessed using an empty [StringName]. The translation server provides wrapper methods for accessing the master translation domain directly, without having to fetch the main translation domain first. $DOCS_URL/tutorials/i18n/internationalizing_games.html @@ -15,13 +16,13 @@ - Adds a [Translation] resource. + Adds a translation to the main translation domain. - Clears the server from all translations. + Removes all translations from the main translation domain. @@ -84,6 +85,13 @@ Returns a locale's language and its variant (e.g. [code]"en_US"[/code] would return [code]"English (United States)"[/code]). + + + + + Returns the translation domain with the specified name. An empty translation domain will be created and added if it does not exist. + + @@ -102,8 +110,14 @@ - Returns the [Translation] instance based on the [param locale] passed in. - It will return [code]null[/code] if there is no [Translation] instance that matches the [param locale]. + Returns the [Translation] instance that best matches [param locale] in the main translation domain. Returns [code]null[/code] if there are no matches. + + + + + + + Returns [code]true[/code] if a translation domain with the specified name exists. @@ -119,11 +133,19 @@ Reparses the pseudolocalization options and reloads the translation. + + + + + Removes the translation domain with the specified name. + [b]Note:[/b] Trying to remove the main translation domain is an error. + + - Removes the given translation from the server. + Removes the given translation from the main translation domain. @@ -146,7 +168,8 @@ - Returns the current locale's translation for the given message (key) and context. + Returns the current locale's translation for the given message and context. + [b]Note:[/b] This method always uses the main translation domain. @@ -156,8 +179,9 @@ - Returns the current locale's translation for the given message (key), plural message and context. + Returns the current locale's translation for the given message, plural message and context. The number [param n] is the number or quantity of the plural object. It will be used to guide the translation system to fetch the correct plural form for the selected language. + [b]Note:[/b] This method always uses the main translation domain.