Add translation domain

This commit is contained in:
Haoyu Qiu 2024-08-15 09:14:41 +08:00
parent 48403b5358
commit 68d494e24e
7 changed files with 379 additions and 116 deletions

View File

@ -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);

View File

@ -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<Translation> &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<Translation> &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<Translation> &E : translations) {
ERR_CONTINUE(E.is_null());
locales.push_back(E->get_locale());
}
return locales;
}
Ref<Translation> TranslationDomain::get_translation_object(const String &p_locale) const {
Ref<Translation> res;
int best_score = 0;
for (const Ref<Translation> &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<Translation> &p_translation) {
translations.insert(p_translation);
}
void TranslationDomain::remove_translation(const Ref<Translation> &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() {
}

View File

@ -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<Ref<Translation>> 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<Translation> get_translation_object(const String &p_locale) const;
void add_translation(const Ref<Translation> &p_translation);
void remove_translation(const Ref<Translation> &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

View File

@ -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<Translation> &E : translations) {
const Ref<Translation> &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<Translation> &p_translation) {
translations.insert(p_translation);
main_domain->add_translation(p_translation);
}
void TranslationServer::remove_translation(const Ref<Translation> &p_translation) {
translations.erase(p_translation);
main_domain->remove_translation(p_translation);
}
Ref<Translation> TranslationServer::get_translation_object(const String &p_locale) {
Ref<Translation> res;
int best_score = 0;
for (const Ref<Translation> &E : translations) {
const Ref<Translation> &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<Translation> &E : translations) {
const Ref<Translation> &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<TranslationDomain> TranslationServer::get_or_add_domain(const StringName &p_domain) {
if (p_domain == StringName()) {
return main_domain;
}
const Ref<TranslationDomain> *domain = custom_domains.getptr(p_domain);
if (domain) {
if (domain->is_valid()) {
return *domain;
}
ERR_PRINT("Bug (please report): Found invalid translation domain.");
}
Ref<TranslationDomain> 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<Translation> &E : translations) {
const Ref<Translation> &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<Translation> 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();
}

View File

@ -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<Ref<Translation>> translations;
Ref<TranslationDomain> main_domain;
HashMap<StringName, Ref<TranslationDomain>> custom_domains;
Ref<Translation> tool_translation;
Ref<Translation> property_translation;
Ref<Translation> 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<Translation> get_translation_object(const String &p_locale);
Vector<String> 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<TranslationDomain> get_or_add_domain(const StringName &p_domain);
void remove_domain(const StringName &p_domain);
void setup();
void clear();

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" ?>
<class name="TranslationDomain" inherits="RefCounted" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="../class.xsd">
<brief_description>
A self-contained collection of [Translation] resources.
</brief_description>
<description>
[TranslationDomain] is a self-contained collection of [Translation] resources. Translations can be added to or removed from it.
</description>
<tutorials>
</tutorials>
<methods>
<method name="add_translation">
<return type="void" />
<param index="0" name="translation" type="Translation" />
<description>
Adds a translation.
</description>
</method>
<method name="clear">
<return type="void" />
<description>
Removes all translations.
</description>
</method>
<method name="get_translation_object" qualifiers="const">
<return type="Translation" />
<param index="0" name="locale" type="String" />
<description>
Returns the [Translation] instance that best matches [param locale]. Returns [code]null[/code] if there are no matches.
</description>
</method>
<method name="remove_translation">
<return type="void" />
<param index="0" name="translation" type="Translation" />
<description>
Removes the given translation.
</description>
</method>
<method name="translate" qualifiers="const">
<return type="StringName" />
<param index="0" name="message" type="StringName" />
<param index="1" name="context" type="StringName" default="&amp;&quot;&quot;" />
<description>
Returns the current locale's translation for the given message and context.
</description>
</method>
<method name="translate_plural" qualifiers="const">
<return type="StringName" />
<param index="0" name="message" type="StringName" />
<param index="1" name="message_plural" type="StringName" />
<param index="2" name="n" type="int" />
<param index="3" name="context" type="StringName" default="&amp;&quot;&quot;" />
<description>
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.
</description>
</method>
</methods>
</class>

View File

@ -4,7 +4,8 @@
The server responsible for language translations.
</brief_description>
<description>
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.
</description>
<tutorials>
<link title="Internationalizing games">$DOCS_URL/tutorials/i18n/internationalizing_games.html</link>
@ -15,13 +16,13 @@
<return type="void" />
<param index="0" name="translation" type="Translation" />
<description>
Adds a [Translation] resource.
Adds a translation to the main translation domain.
</description>
</method>
<method name="clear">
<return type="void" />
<description>
Clears the server from all translations.
Removes all translations from the main translation domain.
</description>
</method>
<method name="compare_locales" qualifiers="const">
@ -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]).
</description>
</method>
<method name="get_or_add_domain">
<return type="TranslationDomain" />
<param index="0" name="domain" type="StringName" />
<description>
Returns the translation domain with the specified name. An empty translation domain will be created and added if it does not exist.
</description>
</method>
<method name="get_script_name" qualifiers="const">
<return type="String" />
<param index="0" name="script" type="String" />
@ -102,8 +110,14 @@
<return type="Translation" />
<param index="0" name="locale" type="String" />
<description>
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.
</description>
</method>
<method name="has_domain" qualifiers="const">
<return type="bool" />
<param index="0" name="domain" type="StringName" />
<description>
Returns [code]true[/code] if a translation domain with the specified name exists.
</description>
</method>
<method name="pseudolocalize" qualifiers="const">
@ -119,11 +133,19 @@
Reparses the pseudolocalization options and reloads the translation.
</description>
</method>
<method name="remove_domain">
<return type="void" />
<param index="0" name="domain" type="StringName" />
<description>
Removes the translation domain with the specified name.
[b]Note:[/b] Trying to remove the main translation domain is an error.
</description>
</method>
<method name="remove_translation">
<return type="void" />
<param index="0" name="translation" type="Translation" />
<description>
Removes the given translation from the server.
Removes the given translation from the main translation domain.
</description>
</method>
<method name="set_locale">
@ -146,7 +168,8 @@
<param index="0" name="message" type="StringName" />
<param index="1" name="context" type="StringName" default="&amp;&quot;&quot;" />
<description>
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.
</description>
</method>
<method name="translate_plural" qualifiers="const">
@ -156,8 +179,9 @@
<param index="2" name="n" type="int" />
<param index="3" name="context" type="StringName" default="&amp;&quot;&quot;" />
<description>
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.
</description>
</method>
</methods>