From ec860ffe4a1684511283438780419a189f7f59f1 Mon Sep 17 00:00:00 2001 From: Haoyu Qiu Date: Sun, 9 Nov 2025 16:17:04 +0800 Subject: [PATCH] Add methods for querying loaded `Translation` instances --- core/string/translation_domain.cpp | 94 ++++++++++++++++----- core/string/translation_domain.h | 13 ++- core/string/translation_server.cpp | 55 +++++++++--- core/string/translation_server.h | 8 ++ doc/classes/TranslationDomain.xml | 31 ++++++- doc/classes/TranslationServer.xml | 31 ++++++- editor/editor_node.cpp | 9 +- tests/core/string/test_translation.h | 32 +++---- tests/core/string/test_translation_server.h | 66 +++++++++------ 9 files changed, 257 insertions(+), 82 deletions(-) diff --git a/core/string/translation_domain.cpp b/core/string/translation_domain.cpp index 056d3fcb05c..e7b713d4f09 100644 --- a/core/string/translation_domain.cpp +++ b/core/string/translation_domain.cpp @@ -32,6 +32,7 @@ #include "core/string/translation.h" #include "core/string/translation_server.h" +#include "core/variant/typed_array.h" struct _character_accent_pair { const char32_t character; @@ -252,27 +253,7 @@ PackedStringArray TranslationDomain::get_loaded_locales() const { return locales; } -bool TranslationDomain::has_translation_for_locale(const String &p_locale) const { - for (const Ref &E : translations) { - if (E->get_locale() == p_locale) { - return true; - } - } - return false; -} - -// Translation objects that could potentially be used for the given locale. -HashSet> TranslationDomain::get_potential_translations(const String &p_locale) const { - HashSet> res; - - for (const Ref &E : translations) { - if (TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale()) > 0) { - res.insert(E); - } - } - return res; -} - +#ifndef DISABLE_DEPRECATED Ref TranslationDomain::get_translation_object(const String &p_locale) const { Ref res; int best_score = 0; @@ -289,6 +270,7 @@ Ref TranslationDomain::get_translation_object(const String &p_local } return res; } +#endif void TranslationDomain::add_translation(const Ref &p_translation) { ERR_FAIL_COND_MSG(p_translation.is_null(), "Invalid translation provided."); @@ -303,6 +285,69 @@ void TranslationDomain::clear() { translations.clear(); } +const HashSet> TranslationDomain::get_translations() const { + return translations; +} + +HashSet> TranslationDomain::find_translations(const String &p_locale, bool p_exact) const { + HashSet> res; + if (p_exact) { + for (const Ref &E : translations) { + if (E->get_locale() == p_locale) { + res.insert(E); + } + } + } else { + for (const Ref &E : translations) { + if (TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale()) > 0) { + res.insert(E); + } + } + } + return res; +} + +bool TranslationDomain::has_translation(const Ref &p_translation) const { + return translations.has(p_translation); +} + +bool TranslationDomain::has_translation_for_locale(const String &p_locale, bool p_exact) const { + if (p_exact) { + for (const Ref &E : translations) { + if (E->get_locale() == p_locale) { + return true; + } + } + } else { + for (const Ref &E : translations) { + if (TranslationServer::get_singleton()->compare_locales(p_locale, E->get_locale()) > 0) { + return true; + } + } + } + return false; +} + +TypedArray TranslationDomain::get_translations_bind() const { + TypedArray res; + res.reserve(translations.size()); + for (const Ref &E : translations) { + res.push_back(E); + } + return res; +} + +TypedArray TranslationDomain::find_translations_bind(const String &p_locale, bool p_exact) const { + const HashSet> &found = find_translations(p_locale, p_exact); + + TypedArray res; + res.reserve(found.size()); + for (const Ref &E : found) { + res.push_back(E); + } + return res; +} + StringName TranslationDomain::translate(const StringName &p_message, const StringName &p_context) const { if (!enabled) { return p_message; @@ -459,10 +504,17 @@ StringName TranslationDomain::pseudolocalize(const StringName &p_message) const } void TranslationDomain::_bind_methods() { +#ifndef DISABLE_DEPRECATED ClassDB::bind_method(D_METHOD("get_translation_object", "locale"), &TranslationDomain::get_translation_object); +#endif + 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("get_translations"), &TranslationDomain::get_translations_bind); + ClassDB::bind_method(D_METHOD("has_translation_for_locale", "locale", "exact"), &TranslationDomain::has_translation_for_locale); + ClassDB::bind_method(D_METHOD("has_translation", "translation"), &TranslationDomain::has_translation); + ClassDB::bind_method(D_METHOD("find_translations", "locale", "exact"), &TranslationDomain::find_translations_bind); 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("get_locale_override"), &TranslationDomain::get_locale_override); diff --git a/core/string/translation_domain.h b/core/string/translation_domain.h index f95ff631dcf..986e538405c 100644 --- a/core/string/translation_domain.h +++ b/core/string/translation_domain.h @@ -71,16 +71,25 @@ public: 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; - bool has_translation_for_locale(const String &p_locale) const; - HashSet> get_potential_translations(const String &p_locale) const; public: + // These two methods are public for easier TranslationServer bindings. + TypedArray get_translations_bind() const; + TypedArray find_translations_bind(const String &p_locale, bool p_exact) const; + +#ifndef DISABLE_DEPRECATED Ref get_translation_object(const String &p_locale) const; +#endif void add_translation(const Ref &p_translation); void remove_translation(const Ref &p_translation); void clear(); + bool has_translation(const Ref &p_translation) const; + const HashSet> get_translations() const; + HashSet> find_translations(const String &p_locale, bool p_exact) const; + bool has_translation_for_locale(const String &p_locale, bool p_exact) const; + 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; diff --git a/core/string/translation_server.cpp b/core/string/translation_server.cpp index 6d12a6e1e02..890338a08ed 100644 --- a/core/string/translation_server.cpp +++ b/core/string/translation_server.cpp @@ -36,6 +36,7 @@ #include "core/os/main_loop.h" #include "core/os/os.h" #include "core/string/locales.h" +#include "core/variant/typed_array.h" void TranslationServer::init_locale_info() { // Init locale info. @@ -503,9 +504,27 @@ void TranslationServer::remove_translation(const Ref &p_translation main_domain->remove_translation(p_translation); } +#ifndef DISABLE_DEPRECATED Ref TranslationServer::get_translation_object(const String &p_locale) { return main_domain->get_translation_object(p_locale); } +#endif + +TypedArray TranslationServer::get_translations() const { + return main_domain->get_translations_bind(); +} + +TypedArray TranslationServer::find_translations(const String &p_locale, bool p_exact) const { + return main_domain->find_translations_bind(p_locale, p_exact); +} + +bool TranslationServer::has_translation(const Ref &p_translation) const { + return main_domain->has_translation(p_translation); +} + +bool TranslationServer::has_translation_for_locale(const String &p_locale, bool p_exact) const { + return main_domain->has_translation_for_locale(p_locale, p_exact); +} void TranslationServer::clear() { main_domain->clear(); @@ -576,21 +595,27 @@ void TranslationServer::setup() { String TranslationServer::get_tool_locale() { #ifdef TOOLS_ENABLED if (Engine::get_singleton()->is_editor_hint() || Engine::get_singleton()->is_project_manager_hint()) { - if (editor_domain->has_translation_for_locale(locale)) { + if (editor_domain->has_translation_for_locale(locale, true)) { return locale; } return "en"; - } else { -#else - { -#endif - // Look for best matching loaded translation. - Ref t = main_domain->get_translation_object(locale); - if (t.is_null()) { - return fallback; - } - return t->get_locale(); } +#endif + + Ref res; + int best_score = 0; + + for (const Ref &E : main_domain->get_translations()) { + int score = TranslationServer::get_singleton()->compare_locales(locale, E->get_locale()); + if (score > 0 && score >= best_score) { + res = E; + best_score = score; + if (score == 10) { + return locale; // Exact match. + } + } + } + return res.is_valid() ? res->get_locale() : fallback; } bool TranslationServer::is_pseudolocalization_enabled() const { @@ -676,7 +701,15 @@ void TranslationServer::_bind_methods() { ClassDB::bind_method(D_METHOD("add_translation", "translation"), &TranslationServer::add_translation); ClassDB::bind_method(D_METHOD("remove_translation", "translation"), &TranslationServer::remove_translation); + +#ifndef DISABLE_DEPRECATED ClassDB::bind_method(D_METHOD("get_translation_object", "locale"), &TranslationServer::get_translation_object); +#endif + + ClassDB::bind_method(D_METHOD("get_translations"), &TranslationServer::get_translations); + ClassDB::bind_method(D_METHOD("find_translations", "locale", "exact"), &TranslationServer::find_translations); + ClassDB::bind_method(D_METHOD("has_translation_for_locale", "locale", "exact"), &TranslationServer::has_translation_for_locale); + ClassDB::bind_method(D_METHOD("has_translation", "translation"), &TranslationServer::has_translation); 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); diff --git a/core/string/translation_server.h b/core/string/translation_server.h index 56729bf176c..0528f6e2d2e 100644 --- a/core/string/translation_server.h +++ b/core/string/translation_server.h @@ -110,7 +110,15 @@ public: String get_locale() const; void set_fallback_locale(const String &p_locale); String get_fallback_locale() const; + +#ifndef DISABLE_DEPRECATED Ref get_translation_object(const String &p_locale); +#endif + + bool has_translation(const Ref &p_translation) const; + TypedArray get_translations() const; + TypedArray find_translations(const String &p_locale, bool p_exact) const; + bool has_translation_for_locale(const String &p_locale, bool p_exact) const; Vector get_all_languages() const; String get_language_name(const String &p_language) const; diff --git a/doc/classes/TranslationDomain.xml b/doc/classes/TranslationDomain.xml index 3f5873b2a1d..aac39e3fba8 100644 --- a/doc/classes/TranslationDomain.xml +++ b/doc/classes/TranslationDomain.xml @@ -23,19 +23,48 @@ Removes all translations. + + + + + + Returns the [Translation] instances that match [param locale] (see [method TranslationServer.compare_locales]). If [param exact] is [code]true[/code], only instances whose locale exactly equals [param locale] will be returned. + + Returns the locale override of the domain. Returns an empty string if locale override is disabled. - + Returns the [Translation] instance that best matches [param locale]. Returns [code]null[/code] if there are no matches. + + + + Returns all available [Translation] instances as added by [method add_translation]. + + + + + + + Returns [code]true[/code] if this translation domain contains the given [param translation]. + + + + + + + + Returns [code]true[/code] if there are any [Translation] instances that match [param locale] (see [method TranslationServer.compare_locales]). If [param exact] is [code]true[/code], only instances whose locale exactly equals [param locale] are considered. + + diff --git a/doc/classes/TranslationServer.xml b/doc/classes/TranslationServer.xml index bc12eee7b28..fb2622cdfa6 100644 --- a/doc/classes/TranslationServer.xml +++ b/doc/classes/TranslationServer.xml @@ -33,6 +33,14 @@ Compares two locales and returns a similarity score between [code]0[/code] (no match) and [code]10[/code] (full match). + + + + + + Returns the [Translation] instances in the main translation domain that match [param locale] (see [method compare_locales]). If [param exact] is [code]true[/code], only instances whose locale exactly equals [param locale] will be returned. + + @@ -128,13 +136,19 @@ [b]Note:[/b] When called from an exported project returns the same value as [method get_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 all available [Translation] instances in the main translation domain as added by [method add_translation]. + + @@ -142,6 +156,21 @@ Returns [code]true[/code] if a translation domain with the specified name exists. + + + + + Returns [code]true[/code] if the main translation domain contains the given [param translation]. + + + + + + + + Returns [code]true[/code] if there are any [Translation] instances in the main translation domain that match [param locale] (see [method compare_locales]). If [param exact] is [code]true[/code], only instances whose locale exactly equals [param locale] are considered. + + diff --git a/editor/editor_node.cpp b/editor/editor_node.cpp index d21f346ad7a..5cb4f7d1b5c 100644 --- a/editor/editor_node.cpp +++ b/editor/editor_node.cpp @@ -585,10 +585,9 @@ void EditorNode::_update_translations() { if (main->is_enabled()) { // Check for the exact locale. - // `get_potential_translations("zh_CN")` could return translations for "zh". - if (main->has_translation_for_locale(main->get_locale_override())) { + if (main->has_translation_for_locale(main->get_locale_override(), true)) { // The set of translation resources for the current locale changed. - const HashSet> translations = main->get_potential_translations(main->get_locale_override()); + const HashSet> translations = main->find_translations(main->get_locale_override(), false); if (translations != tracked_translations) { _translation_resources_changed(); } @@ -609,7 +608,7 @@ void EditorNode::_translation_resources_changed() { const Ref main = TranslationServer::get_singleton()->get_main_domain(); if (main->is_enabled()) { - const HashSet> translations = main->get_potential_translations(main->get_locale_override()); + const HashSet> translations = main->find_translations(main->get_locale_override(), false); tracked_translations.reserve(translations.size()); for (const Ref &translation : translations) { translation->connect_changed(callable_mp(this, &EditorNode::_queue_translation_notification)); @@ -957,7 +956,7 @@ void EditorNode::_notification(int p_what) { // Remember the selected locale to preview node translations. const String preview_locale = EditorSettings::get_singleton()->get_project_metadata("editor_metadata", "preview_locale", String()); - if (!preview_locale.is_empty() && TranslationServer::get_singleton()->get_loaded_locales().has(preview_locale)) { + if (!preview_locale.is_empty() && TranslationServer::get_singleton()->has_translation_for_locale(preview_locale, true)) { set_preview_locale(preview_locale); } diff --git a/tests/core/string/test_translation.h b/tests/core/string/test_translation.h index 3469400c3b1..d2f2bd16702 100644 --- a/tests/core/string/test_translation.h +++ b/tests/core/string/test_translation.h @@ -228,37 +228,37 @@ TEST_CASE("[TranslationCSV] CSV import") { CHECK(result == OK); CHECK(gen_files.size() == 4); - TranslationServer *ts = TranslationServer::get_singleton(); - + Ref td = TranslationServer::get_singleton()->get_or_add_domain("godot.test"); for (const String &file : gen_files) { Ref translation = ResourceLoader::load(file); CHECK(translation.is_valid()); - ts->add_translation(translation); + td->add_translation(translation); } - ts->set_locale("en"); + td->set_locale_override("en"); - // `tr` can be called on any Object, we reuse TranslationServer for convenience. - CHECK(ts->tr("GOOD_MORNING") == "Good Morning"); - CHECK(ts->tr("GOOD_EVENING") == "Good Evening"); + CHECK(td->translate("GOOD_MORNING", StringName()) == "Good Morning"); + CHECK(td->translate("GOOD_EVENING", StringName()) == "Good Evening"); - ts->set_locale("de"); + td->set_locale_override("de"); - CHECK(ts->tr("GOOD_MORNING") == "Guten Morgen"); - CHECK(ts->tr("GOOD_EVENING") == "Good Evening"); // Left blank in CSV, should source from 'en'. + CHECK(td->translate("GOOD_MORNING", StringName()) == "Guten Morgen"); + CHECK(td->translate("GOOD_EVENING", StringName()) == "Good Evening"); // Left blank in CSV, should source from 'en'. - ts->set_locale("ja"); + td->set_locale_override("ja"); - CHECK(ts->tr("GOOD_MORNING") == String::utf8("おはよう")); - CHECK(ts->tr("GOOD_EVENING") == String::utf8("こんばんは")); + CHECK(td->translate("GOOD_MORNING", StringName()) == String::utf8("おはよう")); + CHECK(td->translate("GOOD_EVENING", StringName()) == String::utf8("こんばんは")); /* FIXME: This passes, but triggers a chain reaction that makes test_viewport * and test_text_edit explode in a billion glittery Unicode particles. - ts->set_locale("fa"); + td->set_locale_override("fa"); - CHECK(ts->tr("GOOD_MORNING") == String::utf8("صبح بخیر")); - CHECK(ts->tr("GOOD_EVENING") == String::utf8("عصر بخیر")); + CHECK(td->translate("GOOD_MORNING", String()) == String::utf8("صبح بخیر")); + CHECK(td->translate("GOOD_EVENING", String()) == String::utf8("عصر بخیر")); */ + + TranslationServer::get_singleton()->remove_domain("godot.test"); } #endif // TOOLS_ENABLED diff --git a/tests/core/string/test_translation_server.h b/tests/core/string/test_translation_server.h index 87c9c9c0ba0..e7cf57c2b3b 100644 --- a/tests/core/string/test_translation_server.h +++ b/tests/core/string/test_translation_server.h @@ -36,39 +36,55 @@ namespace TestTranslationServer { TEST_CASE("[TranslationServer] Translation operations") { + Ref td = TranslationServer::get_singleton()->get_or_add_domain("godot.test"); + CHECK(td->get_translations().is_empty()); + Ref t1 = memnew(Translation); - t1->set_locale("uk"); + t1->set_locale("uk"); // Ukrainian. t1->add_message("Good Morning", String(U"Добрий ранок")); + td->add_translation(t1); + CHECK(td->get_translations().size() == 1); + CHECK(td->has_translation_for_locale("uk", true)); + CHECK(td->has_translation_for_locale("uk", false)); + CHECK_FALSE(td->has_translation_for_locale("uk_UA", true)); + CHECK(td->has_translation_for_locale("uk_UA", false)); + CHECK(td->find_translations("uk", false).size() == 1); + CHECK(td->find_translations("uk", true).size() == 1); + CHECK(td->find_translations("uk_UA", false).size() == 1); + CHECK(td->find_translations("uk_UA", true).size() == 0); Ref t2 = memnew(Translation); - t2->set_locale("uk"); - t2->add_message("Hello Godot", String(U"你好戈多")); + t2->set_locale("uk_UA"); // Ukrainian in Ukraine. + t2->add_message("Hello Godot", String(U"Привіт, Годо.")); + td->add_translation(t2); + CHECK(td->get_translations().size() == 2); + CHECK(td->has_translation_for_locale("uk", true)); + CHECK(td->has_translation_for_locale("uk", false)); + CHECK(td->has_translation_for_locale("uk_UA", true)); + CHECK(td->has_translation_for_locale("uk_UA", false)); + CHECK(td->find_translations("uk", false).size() == 2); + CHECK(td->find_translations("uk", true).size() == 1); + CHECK(td->find_translations("uk_UA", false).size() == 2); + CHECK(td->find_translations("uk_UA", true).size() == 1); - TranslationServer *ts = TranslationServer::get_singleton(); + td->set_locale_override("uk"); + CHECK(td->translate("Good Morning", StringName()) == String::utf8("Добрий ранок")); - // Adds translation for UK locale for the first time. - int l_count_before = ts->get_loaded_locales().size(); - ts->add_translation(t1); - int l_count_after = ts->get_loaded_locales().size(); - CHECK(l_count_after > l_count_before); + td->remove_translation(t1); + CHECK(td->get_translations().size() == 1); + CHECK_FALSE(td->has_translation_for_locale("uk", true)); + CHECK(td->has_translation_for_locale("uk", false)); + CHECK(td->has_translation_for_locale("uk_UA", true)); + CHECK(td->has_translation_for_locale("uk_UA", false)); + CHECK(td->find_translations("uk", true).size() == 0); + CHECK(td->find_translations("uk", false).size() == 1); + CHECK(td->find_translations("uk_UA", true).size() == 1); + CHECK(td->find_translations("uk_UA", false).size() == 1); - // Adds translation for UK locale again. - ts->add_translation(t2); - CHECK_EQ(ts->get_loaded_locales().size(), l_count_after); - - // Removing that translation. - ts->remove_translation(t2); - CHECK_EQ(ts->get_loaded_locales().size(), l_count_after); - - CHECK(ts->get_translation_object("uk").is_valid()); - - ts->set_locale("uk"); - CHECK(ts->translate("Good Morning") == String::utf8("Добрий ранок")); - - ts->remove_translation(t1); - CHECK(ts->get_translation_object("uk").is_null()); // If no suitable Translation object has been found - the original message should be returned. - CHECK(ts->translate("Good Morning") == "Good Morning"); + CHECK(td->translate("Good Morning", StringName()) == "Good Morning"); + + TranslationServer::get_singleton()->remove_domain("godot.test"); } TEST_CASE("[TranslationServer] Locale operations") {