From e882e42e1be615b0a4aefeaeb0bb52d7a6aa3fa0 Mon Sep 17 00:00:00 2001 From: Haoyu Qiu Date: Mon, 21 Jul 2025 17:59:14 +0800 Subject: [PATCH] Add default plural rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This makes the PO loader correctly handle the situation where the optional `Plural-Forms` header field does not exist. The `Translation` class and its subclasses always have access to valid plural rules via `_get_plural_rules()`. Plural rules are prioritized: 1. `Translation.plural_rules_override` 2. `TranslationServer.get_plural_rules(locale)` 3. The English plural rules: `nplurals=2; plurals=(n != 1)` Co-Authored-By: Pāvels Nadtočajevs <7645683+bruvzg@users.noreply.github.com> --- core/io/translation_loader_po.cpp | 4 +-- core/string/locales.h | 47 ++++++++++++++++++++++++ core/string/translation.cpp | 54 ++++++++++++++++++++++++++++ core/string/translation.h | 12 ++++++- core/string/translation_po.cpp | 24 +++---------- core/string/translation_po.h | 7 ---- core/string/translation_server.cpp | 42 +++++++++++++++++----- core/string/translation_server.h | 16 +++++---- doc/classes/Translation.xml | 4 +++ doc/classes/TranslationServer.xml | 7 ++++ tests/core/string/test_translation.h | 46 ++++++++++++++++-------- 11 files changed, 202 insertions(+), 61 deletions(-) diff --git a/core/io/translation_loader_po.cpp b/core/io/translation_loader_po.cpp index 35773109a97..fcab40e653f 100644 --- a/core/io/translation_loader_po.cpp +++ b/core/io/translation_loader_po.cpp @@ -112,7 +112,7 @@ Ref TranslationLoaderPO::load_translation(Ref f, Error *r_ int p_start = config.find("Plural-Forms"); if (p_start != -1) { int p_end = config.find_char('\n', p_start); - translation->set_plural_rule(config.substr(p_start, p_end - p_start)); + translation->set_plural_rules_override(config.substr(p_start, p_end - p_start)); } } else { uint32_t str_start = 0; @@ -228,7 +228,7 @@ Ref TranslationLoaderPO::load_translation(Ref f, Error *r_ int p_start = config.find("Plural-Forms"); if (p_start != -1) { int p_end = config.find_char('\n', p_start); - translation->set_plural_rule(config.substr(p_start, p_end - p_start)); + translation->set_plural_rules_override(config.substr(p_start, p_end - p_start)); plural_forms = translation->get_plural_forms(); } } diff --git a/core/string/locales.h b/core/string/locales.h index f80e07789b7..6f1f5819094 100644 --- a/core/string/locales.h +++ b/core/string/locales.h @@ -1194,3 +1194,50 @@ static const char *script_list[][2] = { { "Zanabazar Square", "Zanb" }, { nullptr, nullptr } }; + +// Plural rules. +// Reference: +// - https://github.com/unicode-org/cldr/blob/main/common/supplemental/plurals.xml +static const char *plural_rules[][2] = { + { "bm bo dz hnj id ig ii in ja jbo jv jw kde kea km ko lkt lo ms my nqo osa root sah ses sg su th to tpi vi wo yo yue zh", "nplurals=1; plural=0;" }, + { "am as bn doi fa gu hi kn pcm zu", "nplurals=2; plural=(n==0 || n==1);" }, + { "ff hy kab", "nplurals=2; plural=(n > 1);" }, + { "ast de en et fi fy gl ia io ji lij nl sc sv sw ur yi", "nplurals=2; plural=(n != 1);" }, + { "si", "nplurals=2; plural=(n > 1);" }, + { "ak bho csw guw ln mg nso pa ti wa", "nplurals=2; plural=(n > 1);" }, + { "tzm", "nplurals=2; plural=(n<=1 || (n>=11 && n<=99));" }, + { "af an asa az bal bem bez bg brx ce cgg chr ckb dv ee el eo eu fo fur gsw ha haw hu jgo jmc ka kaj kcg kk kkj kl ks ksb ku ky lb lg mas mgo ml mn mr nah nb nd ne nn nnh no nr ny nyn om or os pap ps rm rof rwk saq sd sdh seh sn so sq ss ssy st syr ta te teo tig tk tn tr ts ug uz ve vo vun wae xh xog", "nplurals=2; plural=(n != 1);" }, + { "da", "nplurals=2; plural=(n != 1);" }, + { "is", "nplurals=2; plural=(n%10==1 && n%100!=11);" }, + { "mk", "nplurals=2; plural=(n%10==1 && n%100!=11);" }, + { "ceb fil tl", "nplurals=2; plural=(n==1 || n==2 || n==3 || (n%10!=4 && n%10!=6 && n%10!=9));" }, + { "lv prg", "nplurals=3; plural=(n%10==0 || (n%100>=11 && n%100<=19) ? 0 : n%10==1 && n%100!=11 ? 1 : 2);" }, + { "lag", "nplurals=3; plural=(n==0 ? 0 : (n==0 || n==1) && n!=0 ? 1 : 2);" }, + { "blo", "nplurals=3; plural=(n==0 ? 0 : n==1 ? 1 : 2);" }, + { "ksh", "nplurals=3; plural=(n==0 ? 0 : n==1 ? 1 : 2);" }, + { "he iw", "nplurals=3; plural=(n==1 ? 0 : n==2 ? 1 : 2);" }, + { "iu naq sat se sma smi smj smn sms", "nplurals=3; plural=(n==1 ? 0 : n==2 ? 1 : 2);" }, + { "shi", "nplurals=3; plural=(n==0 || n==1 ? 0 : n>=2 && n<=10 ? 1 : 2);" }, + { "mo ro", "nplurals=3; plural=(n==1 ? 0 : n==0 || (n!=1 && n%100>=1 && n%100<=19) ? 1 : 2);" }, + { "bs hr sh sr", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);" }, + { "fr", "nplurals=3; plural=(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2);" }, + { "pt", "nplurals=3; plural=((n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2);" }, + { "ca it lld pt_PT scn vec", "nplurals=3; plural=(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2);" }, + { "es", "nplurals=3; plural=(n == 1 ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2);" }, + { "gd", "nplurals=4; plural=(n==1 || n==11 ? 0 : n==2 || n==12 ? 1 : (n>=3 && n<=10) || (n>=13 && n<=19) ? 2 : 3);" }, + { "sl", "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n%100<=4 ? 2 : 3);" }, + { "dsb hsb", "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && n%100<=4 ? 2 : 3);" }, + { "cs sk", "nplurals=3; plural=(n==1 ? 0 : n>=2 && n<=4 ? 1 : 2);" }, + { "pl", "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);" }, + { "be", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);" }, + { "lt", "nplurals=3; plural=(n%10==1 && (n%100<11 || n%100>19) ? 0 : n%10>=2 && n%10<=9 && (n%100<11 || n%100>19) ? 1 : 2);" }, + { "ru uk", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<12 || n%100>14) ? 1 : 2);" }, + { "br", "nplurals=5; plural=(n%10==1 && n%100!=11 && n%100!=71 && n%100!=91 ? 0 : n%10==2 && n%100!=12 && n%100!=72 && n%100!=92 ? 1 : ((n%10>=3 && n%10<=4) || n%10==9) && (n%100<10 || n%100>19) && (n%100<70 || n%100>79) && (n%100<90 || n%100>99) ? 2 : n!=0 && n%1000000==0 ? 3 : 4);" }, + { "mt", "nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n==0 || (n%100>=3 && n%100<=10) ? 2 : n%100>=11 && n%100<=19 ? 3 : 4);" }, + { "ga", "nplurals=5; plural=(n==1 ? 0 : n==2 ? 1 : n>=3 && n<=6 ? 2 : n>=7 && n<=10 ? 3 : 4);" }, + { "gv", "nplurals=4; plural=(n%10==1 ? 0 : n%10==2 ? 1 : n%100==0 || n%100==20 || n%100==40 || n%100==60 || n%100==80 ? 2 : 3);" }, + { "kw", "nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n%100==2 || n%100==22 || n%100==42 || n%100==62 || n%100==82 || (n%1000==0 && ((n%100000>=1000 && n%100000<=20000) || n%100000==40000 || n%100000==60000 || n%100000==80000)) || (n!=0 && n%1000000==100000) ? 2 : n%100==3 || n%100==23 || n%100==43 || n%100==63 || n%100==83 ? 3 : n!=1 && (n%100==1 || n%100==21 || n%100==41 || n%100==61 || n%100==81) ? 4 : 5);" }, + { "ar ars", "nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);" }, + { "cy", "nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n==3 ? 3 : n==6 ? 4 : 5);" }, + { nullptr, nullptr }, +}; diff --git a/core/string/translation.cpp b/core/string/translation.cpp index d7b4d86c28f..7fcfc13229b 100644 --- a/core/string/translation.cpp +++ b/core/string/translation.cpp @@ -31,6 +31,7 @@ #include "translation.h" #include "core/os/thread.h" +#include "core/string/plural_rules.h" #include "core/string/translation_server.h" Dictionary Translation::_get_messages() const { @@ -73,6 +74,11 @@ void Translation::_set_messages(const Dictionary &p_messages) { void Translation::set_locale(const String &p_locale) { locale = TranslationServer::get_singleton()->standardize_locale(p_locale); + + if (plural_rules_cache && plural_rules_override.is_empty()) { + memdelete(plural_rules_cache); + plural_rules_cache = nullptr; + } } void Translation::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) { @@ -131,6 +137,44 @@ int Translation::get_message_count() const { return translation_map.size(); } +PluralRules *Translation::_get_plural_rules() const { + if (plural_rules_cache) { + return plural_rules_cache; + } + + if (!plural_rules_override.is_empty()) { + plural_rules_cache = PluralRules::parse(plural_rules_override); + } + + if (!plural_rules_cache) { + // Locale's default plural rules. + const String &default_rule = TranslationServer::get_singleton()->get_plural_rules(locale); + if (!default_rule.is_empty()) { + plural_rules_cache = PluralRules::parse(default_rule); + } + + // Use English plural rules as a fallback. + if (!plural_rules_cache) { + plural_rules_cache = PluralRules::parse("nplurals=2; plural=(n != 1);"); + } + } + + DEV_ASSERT(plural_rules_cache != nullptr); + return plural_rules_cache; +} + +void Translation::set_plural_rules_override(const String &p_rules) { + plural_rules_override = p_rules; + if (plural_rules_cache) { + memdelete(plural_rules_cache); + plural_rules_cache = nullptr; + } +} + +String Translation::get_plural_rules_override() const { + return plural_rules_override; +} + void Translation::_bind_methods() { ClassDB::bind_method(D_METHOD("set_locale", "locale"), &Translation::set_locale); ClassDB::bind_method(D_METHOD("get_locale"), &Translation::get_locale); @@ -144,10 +188,20 @@ void Translation::_bind_methods() { ClassDB::bind_method(D_METHOD("get_message_count"), &Translation::get_message_count); ClassDB::bind_method(D_METHOD("_set_messages", "messages"), &Translation::_set_messages); ClassDB::bind_method(D_METHOD("_get_messages"), &Translation::_get_messages); + ClassDB::bind_method(D_METHOD("set_plural_rules_override", "rules"), &Translation::set_plural_rules_override); + ClassDB::bind_method(D_METHOD("get_plural_rules_override"), &Translation::get_plural_rules_override); GDVIRTUAL_BIND(_get_plural_message, "src_message", "src_plural_message", "n", "context"); GDVIRTUAL_BIND(_get_message, "src_message", "context"); ADD_PROPERTY(PropertyInfo(Variant::DICTIONARY, "messages", PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR | PROPERTY_USAGE_INTERNAL), "_set_messages", "_get_messages"); ADD_PROPERTY(PropertyInfo(Variant::STRING, "locale"), "set_locale", "get_locale"); + ADD_PROPERTY(PropertyInfo(Variant::STRING, "plural_rules_override"), "set_plural_rules_override", "get_plural_rules_override"); +} + +Translation::~Translation() { + if (plural_rules_cache) { + memdelete(plural_rules_cache); + plural_rules_cache = nullptr; + } } diff --git a/core/string/translation.h b/core/string/translation.h index 3e445bedcb0..3136d8420f1 100644 --- a/core/string/translation.h +++ b/core/string/translation.h @@ -33,6 +33,8 @@ #include "core/io/resource.h" #include "core/object/gdvirtual.gen.inc" +class PluralRules; + class Translation : public Resource { GDCLASS(Translation, Resource); OBJ_SAVE_TYPE(Translation); @@ -41,6 +43,9 @@ class Translation : public Resource { String locale = "en"; HashMap translation_map; + mutable PluralRules *plural_rules_cache = nullptr; + String plural_rules_override; + virtual Vector _get_message_list() const; virtual Dictionary _get_messages() const; virtual void _set_messages(const Dictionary &p_messages); @@ -48,6 +53,8 @@ class Translation : public Resource { protected: static void _bind_methods(); + PluralRules *_get_plural_rules() const; + GDVIRTUAL2RC(StringName, _get_message, StringName, StringName); GDVIRTUAL4RC(StringName, _get_plural_message, StringName, StringName, int, StringName); @@ -64,5 +71,8 @@ public: virtual int get_message_count() const; virtual Vector get_translated_message_list() const; - Translation() {} + void set_plural_rules_override(const String &p_rules); + String get_plural_rules_override() const; + + ~Translation(); }; diff --git a/core/string/translation_po.cpp b/core/string/translation_po.cpp index 569efe594cc..3eaecbf5344 100644 --- a/core/string/translation_po.cpp +++ b/core/string/translation_po.cpp @@ -131,13 +131,6 @@ Vector TranslationPO::_get_message_list() const { return v; } -void TranslationPO::set_plural_rule(const String &p_plural_rule) { - if (plural_rules) { - memdelete(plural_rules); - } - plural_rules = PluralRules::parse(p_plural_rule); -} - void TranslationPO::add_message(const StringName &p_src_text, const StringName &p_xlated_text, const StringName &p_context) { HashMap> &map_id_str = translation_map[p_context]; @@ -150,8 +143,7 @@ void TranslationPO::add_message(const StringName &p_src_text, const StringName & } void TranslationPO::add_plural_message(const StringName &p_src_text, const Vector &p_plural_xlated_texts, const StringName &p_context) { - ERR_FAIL_NULL_MSG(plural_rules, "Plural rules are not set. Please call set_plural_rule() before calling add_plural_message()."); - ERR_FAIL_COND_MSG(p_plural_xlated_texts.size() != plural_rules->get_nplurals(), vformat("Trying to add plural texts that don't match the required number of plural forms for locale \"%s\".", get_locale())); + ERR_FAIL_COND_MSG(p_plural_xlated_texts.size() != _get_plural_rules()->get_nplurals(), vformat("Trying to add plural texts that don't match the required number of plural forms for locale \"%s\".", get_locale())); HashMap> &map_id_str = translation_map[p_context]; @@ -166,11 +158,11 @@ void TranslationPO::add_plural_message(const StringName &p_src_text, const Vecto } int TranslationPO::get_plural_forms() const { - return plural_rules ? plural_rules->get_nplurals() : 0; + return _get_plural_rules()->get_nplurals(); } String TranslationPO::get_plural_rule() const { - return plural_rules ? plural_rules->get_plural() : String(); + return _get_plural_rules()->get_plural(); } StringName TranslationPO::get_message(const StringName &p_src_text, const StringName &p_context) const { @@ -184,14 +176,13 @@ StringName TranslationPO::get_message(const StringName &p_src_text, const String StringName TranslationPO::get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context) const { ERR_FAIL_COND_V_MSG(p_n < 0, StringName(), "N passed into translation to get a plural message should not be negative. For negative numbers, use singular translation please. Search \"gettext PO Plural Forms\" online for the documentation on translating negative numbers."); - ERR_FAIL_NULL_V_MSG(plural_rules, StringName(), "Plural rules are not set. Please call set_plural_rule() before calling get_plural_message()."); if (!translation_map.has(p_context) || !translation_map[p_context].has(p_src_text)) { return StringName(); } ERR_FAIL_COND_V_MSG(translation_map[p_context][p_src_text].is_empty(), StringName(), vformat("Source text \"%s\" is registered but doesn't have a translation. Please report this bug.", String(p_src_text))); - int plural_index = plural_rules->evaluate(p_n); + int plural_index = _get_plural_rules()->evaluate(p_n); ERR_FAIL_COND_V_MSG(plural_index < 0 || translation_map[p_context][p_src_text].size() < plural_index + 1, StringName(), "Plural index returned or number of plural translations is not valid. Please report this bug."); return translation_map[p_context][p_src_text][plural_index]; @@ -234,10 +225,3 @@ void TranslationPO::_bind_methods() { ClassDB::bind_method(D_METHOD("get_plural_forms"), &TranslationPO::get_plural_forms); ClassDB::bind_method(D_METHOD("get_plural_rule"), &TranslationPO::get_plural_rule); } - -TranslationPO::~TranslationPO() { - if (plural_rules) { - memdelete(plural_rules); - plural_rules = nullptr; - } -} diff --git a/core/string/translation_po.h b/core/string/translation_po.h index ab7d0e7b8f1..dd0bc3c602b 100644 --- a/core/string/translation_po.h +++ b/core/string/translation_po.h @@ -34,8 +34,6 @@ #include "core/string/translation.h" -class PluralRules; - class TranslationPO : public Translation { GDCLASS(TranslationPO, Translation); @@ -46,8 +44,6 @@ class TranslationPO : public Translation { // Strings without context have "" as first key. HashMap>> translation_map; - PluralRules *plural_rules = nullptr; - Vector _get_message_list() const override; Dictionary _get_messages() const override; void _set_messages(const Dictionary &p_messages) override; @@ -65,13 +61,10 @@ public: StringName get_plural_message(const StringName &p_src_text, const StringName &p_plural_text, int p_n, const StringName &p_context = "") const override; void erase_message(const StringName &p_src_text, const StringName &p_context = "") override; - void set_plural_rule(const String &p_plural_rule); int get_plural_forms() const; String get_plural_rule() const; #ifdef DEBUG_TRANSLATION_PO void print_translation_map(); #endif - - ~TranslationPO(); }; diff --git a/core/string/translation_server.cpp b/core/string/translation_server.cpp index ea9c103e430..d9e72ea808b 100644 --- a/core/string/translation_server.cpp +++ b/core/string/translation_server.cpp @@ -36,15 +36,6 @@ #include "core/os/os.h" #include "core/string/locales.h" -Vector TranslationServer::locale_script_info; - -HashMap TranslationServer::language_map; -HashMap TranslationServer::script_map; -HashMap TranslationServer::locale_rename_map; -HashMap TranslationServer::country_name_map; -HashMap TranslationServer::variant_map; -HashMap TranslationServer::country_rename_map; - void TranslationServer::init_locale_info() { // Init locale info. language_map.clear(); @@ -113,6 +104,18 @@ void TranslationServer::init_locale_info() { } idx++; } + + // Init plural rules. + plural_rules_map.clear(); + idx = 0; + while (plural_rules[idx][0] != nullptr) { + const Vector rule_locs = String(plural_rules[idx][0]).split(" "); + const String rule = String(plural_rules[idx][1]); + for (const String &l : rule_locs) { + plural_rules_map[l] = rule; + } + idx++; + } } TranslationServer::Locale::operator String() const { @@ -305,6 +308,26 @@ String TranslationServer::get_locale_name(const String &p_locale) const { return name; } +String TranslationServer::get_plural_rules(const String &p_locale) const { + const String *rule = plural_rules_map.getptr(p_locale); + if (rule) { + return *rule; + } + + Locale l = Locale(*this, p_locale, false); + if (!l.country.is_empty()) { + rule = plural_rules_map.getptr(l.language + "_" + l.country); + if (rule) { + return *rule; + } + } + rule = plural_rules_map.getptr(l.language); + if (rule) { + return *rule; + } + return String(); +} + Vector TranslationServer::get_all_languages() const { Vector languages; @@ -584,6 +607,7 @@ void TranslationServer::_bind_methods() { ClassDB::bind_method(D_METHOD("get_country_name", "country"), &TranslationServer::get_country_name); ClassDB::bind_method(D_METHOD("get_locale_name", "locale"), &TranslationServer::get_locale_name); + ClassDB::bind_method(D_METHOD("get_plural_rules", "locale"), &TranslationServer::get_plural_rules); ClassDB::bind_method(D_METHOD("translate", "message", "context"), &TranslationServer::translate, DEFVAL(StringName())); ClassDB::bind_method(D_METHOD("translate_plural", "message", "plural_message", "n", "context"), &TranslationServer::translate_plural, DEFVAL(StringName())); diff --git a/core/string/translation_server.h b/core/string/translation_server.h index 48632953ce5..f904f074b89 100644 --- a/core/string/translation_server.h +++ b/core/string/translation_server.h @@ -62,7 +62,7 @@ class TranslationServer : public Object { String default_country; HashSet supported_countries; }; - static Vector locale_script_info; + static inline Vector locale_script_info; struct Locale { String language; @@ -82,12 +82,13 @@ class TranslationServer : public Object { Locale(const TranslationServer &p_server, const String &p_locale, bool p_add_defaults); }; - static HashMap language_map; - static HashMap script_map; - static HashMap locale_rename_map; - static HashMap country_name_map; - static HashMap country_rename_map; - static HashMap variant_map; + static inline HashMap language_map; + static inline HashMap script_map; + static inline HashMap locale_rename_map; + static inline HashMap country_name_map; + static inline HashMap country_rename_map; + static inline HashMap variant_map; + static inline HashMap plural_rules_map; void init_locale_info(); @@ -113,6 +114,7 @@ public: String get_country_name(const String &p_country) const; String get_locale_name(const String &p_locale) const; + String get_plural_rules(const String &p_locale) const; PackedStringArray get_loaded_locales() const; diff --git a/doc/classes/Translation.xml b/doc/classes/Translation.xml index 2d365b32a87..c57c29e68bb 100644 --- a/doc/classes/Translation.xml +++ b/doc/classes/Translation.xml @@ -102,5 +102,9 @@ The locale of the translation. + + The plural rules string to enforce. See [url=https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html]GNU gettext[/url] for examples and more info. + If empty or invalid, default plural rules from [method TranslationServer.get_plural_rules] are used. The English plural rules are used as a fallback. + diff --git a/doc/classes/TranslationServer.xml b/doc/classes/TranslationServer.xml index f30a1da014c..ee05b8091b5 100644 --- a/doc/classes/TranslationServer.xml +++ b/doc/classes/TranslationServer.xml @@ -92,6 +92,13 @@ Returns the translation domain with the specified name. An empty translation domain will be created and added if it does not exist. + + + + + Returns the default plural rules for the [param locale]. + + diff --git a/tests/core/string/test_translation.h b/tests/core/string/test_translation.h index cf0a496634b..35740e547c3 100644 --- a/tests/core/string/test_translation.h +++ b/tests/core/string/test_translation.h @@ -112,22 +112,38 @@ TEST_CASE("[TranslationPO] Messages with context") { } TEST_CASE("[TranslationPO] Plural messages") { - Ref translation = memnew(TranslationPO); - translation->set_locale("fr"); - translation->set_plural_rule("Plural-Forms: nplurals=2; plural=(n >= 2);"); - CHECK(translation->get_plural_forms() == 2); + { + Ref translation = memnew(TranslationPO); + translation->set_locale("fr"); + CHECK(translation->get_plural_forms() == 3); + CHECK(translation->get_plural_rule() == "(n == 0 || n == 1) ? 0 : n != 0 && n % 1000000 == 0 ? 1 : 2)"); + } - PackedStringArray plurals; - plurals.push_back("Il y a %d pomme"); - plurals.push_back("Il y a %d pommes"); - translation->add_plural_message("There are %d apples", plurals); - ERR_PRINT_OFF; - // This is invalid, as the number passed to `get_plural_message()` may not be negative. - CHECK(vformat(translation->get_plural_message("There are %d apples", "", -1), -1) == ""); - ERR_PRINT_ON; - CHECK(vformat(translation->get_plural_message("There are %d apples", "", 0), 0) == "Il y a 0 pomme"); - CHECK(vformat(translation->get_plural_message("There are %d apples", "", 1), 1) == "Il y a 1 pomme"); - CHECK(vformat(translation->get_plural_message("There are %d apples", "", 2), 2) == "Il y a 2 pommes"); + { + Ref translation = memnew(TranslationPO); + translation->set_locale("invalid"); + CHECK(translation->get_plural_forms() == 2); + CHECK(translation->get_plural_rule() == "(n != 1)"); + } + + { + Ref translation = memnew(TranslationPO); + translation->set_plural_rules_override("Plural-Forms: nplurals=2; plural=(n >= 2);"); + CHECK(translation->get_plural_forms() == 2); + CHECK(translation->get_plural_rule() == "(n >= 2)"); + + PackedStringArray plurals; + plurals.push_back("Il y a %d pomme"); + plurals.push_back("Il y a %d pommes"); + translation->add_plural_message("There are %d apples", plurals); + ERR_PRINT_OFF; + // This is invalid, as the number passed to `get_plural_message()` may not be negative. + CHECK(vformat(translation->get_plural_message("There are %d apples", "", -1), -1) == ""); + ERR_PRINT_ON; + CHECK(vformat(translation->get_plural_message("There are %d apples", "", 0), 0) == "Il y a 0 pomme"); + CHECK(vformat(translation->get_plural_message("There are %d apples", "", 1), 1) == "Il y a 1 pomme"); + CHECK(vformat(translation->get_plural_message("There are %d apples", "", 2), 2) == "Il y a 2 pommes"); + } } TEST_CASE("[TranslationPO] Plural rules parsing") {