diff --git a/distribution/changelog.txt b/distribution/changelog.txt index 8a4091a47e..409b43a495 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -5,6 +5,7 @@ - Feature: [#18744] Cheat to allow using a regular path as a queue path. - Improved: [#18749] Ability to have 4 active awards for more than one month in a row. - Improved: [#18826] [Plugin] Added all actions and their documentation to plugin API. +- Improved: [#18945] Languages can now fall back to other languages than English. - Fix: [#18467] “Selected only” Object Selection filter is active in Track Designs Manager, and cannot be toggled. - Fix: [#18905] Ride Construction window theme is not applied correctly. - Fix: [#18911] Mini Golf station does not draw correctly from all angles. diff --git a/src/openrct2/localisation/Language.cpp b/src/openrct2/localisation/Language.cpp index 777ae28413..7f0eb9d86e 100644 --- a/src/openrct2/localisation/Language.cpp +++ b/src/openrct2/localisation/Language.cpp @@ -23,32 +23,33 @@ // clang-format off const language_descriptor LanguagesDescriptors[LANGUAGE_COUNT] = { - { "", "", "", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_UNDEFINED - { "ar-EG", "Arabic (experimental)", "Arabic (experimental)", FAMILY(&TTFFamilySansSerif), true }, // LANGUAGE_ARABIC - { "ca-ES", "Catalan", u8"Català", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_CATALAN - { "zh-CN", "Chinese (Simplified)", "Chinese (Simplified)", FAMILY(&TTFFamilyChineseSimplified), false }, // LANGUAGE_CHINESE_SIMPLIFIED - { "zh-TW", "Chinese (Traditional)", "Chinese (Traditional)", FAMILY(&TTFFamilyChineseTraditional), false }, // LANGUAGE_CHINESE_TRADITIONAL - { "cs-CZ", "Czech", u8"Čeština", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_CZECH - { "da-DK", "Danish", "Dansk", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_DANISH - { "de-DE", "German", "Deutsch", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_GERMAN - { "en-GB", "English (UK)", "English (UK)", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_ENGLISH_UK - { "en-US", "English (US)", "English (US)", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_ENGLISH_US - { "eo-ZZ", "Esperanto", "Esperanto", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_ESPERANTO - { "es-ES", "Spanish", u8"Español", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_SPANISH - { "fr-FR", "French", u8"Français", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_FRENCH - { "it-IT", "Italian", "Italiano", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_ITALIAN - { "ja-JP", "Japanese", "Japanese", FAMILY(&TTFFamilyJapanese), false }, // LANGUAGE_JAPANESE - { "ko-KR", "Korean", "Korean", FAMILY(&TTFFamilyKorean), false }, // LANGUAGE_KOREAN - { "hu-HU", "Hungarian", "Magyar", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_HUNGARIAN - { "nl-NL", "Dutch", "Nederlands", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_DUTCH - { "nb-NO", "Norwegian", "Norsk", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_NORWEGIAN - { "pl-PL", "Polish", "Polski", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_POLISH - { "pt-BR", "Portuguese (BR)", u8"Português (BR)", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_PORTUGUESE_BR - { "ru-RU", "Russian", u8"Русский", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_RUSSIAN - { "fi-FI", "Finnish", "Suomi", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_FINNISH - { "sv-SE", "Swedish", "Svenska", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_SWEDISH - { "tr-TR", "Turkish", "Türkçe", FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_TURKISH - { "vi-VN", "Vietnamese", "Vietnamese", FAMILY(&TTFFamilySansSerif), false }, // LANGUAGE_VIETNAMESE + { "", "", "", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_UNDEFINED + { "ar-EG", "Arabic (experimental)", "Arabic (experimental)", LANGUAGE_UNDEFINED, FAMILY(&TTFFamilySansSerif), true }, // LANGUAGE_ARABIC + { "ca-ES", "Catalan", u8"Català", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_CATALAN + { "zh-CN", "Chinese (Simplified)", "Chinese (Simplified)", LANGUAGE_UNDEFINED, FAMILY(&TTFFamilyChineseSimplified), false }, // LANGUAGE_CHINESE_SIMPLIFIED + { "zh-TW", "Chinese (Traditional)", "Chinese (Traditional)", LANGUAGE_UNDEFINED, FAMILY(&TTFFamilyChineseTraditional), false }, // LANGUAGE_CHINESE_TRADITIONAL + { "cs-CZ", "Czech", u8"Čeština", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_CZECH + { "da-DK", "Danish", "Dansk", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_DANISH + { "de-DE", "German", "Deutsch", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_GERMAN + { "en-GB", "English (UK)", "English (UK)", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_ENGLISH_UK + { "en-US", "English (US)", "English (US)", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_ENGLISH_US + { "eo-ZZ", "Esperanto", "Esperanto", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_ESPERANTO + { "es-ES", "Spanish", u8"Español", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_SPANISH + { "fr-FR", "French", u8"Français", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_FRENCH + { "fr-CA", "French (CA)", u8"Français (CA)", LANGUAGE_FRENCH, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_FRENCH_CA + { "it-IT", "Italian", "Italiano", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_ITALIAN + { "ja-JP", "Japanese", "Japanese", LANGUAGE_UNDEFINED, FAMILY(&TTFFamilyJapanese), false }, // LANGUAGE_JAPANESE + { "ko-KR", "Korean", "Korean", LANGUAGE_UNDEFINED, FAMILY(&TTFFamilyKorean), false }, // LANGUAGE_KOREAN + { "hu-HU", "Hungarian", "Magyar", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_HUNGARIAN + { "nl-NL", "Dutch", "Nederlands", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_DUTCH + { "nb-NO", "Norwegian", "Norsk", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_NORWEGIAN + { "pl-PL", "Polish", "Polski", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_POLISH + { "pt-BR", "Portuguese (BR)", u8"Português (BR)", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_PORTUGUESE_BR + { "ru-RU", "Russian", u8"Русский", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_RUSSIAN + { "fi-FI", "Finnish", "Suomi", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_FINNISH + { "sv-SE", "Swedish", "Svenska", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_SWEDISH + { "tr-TR", "Turkish", "Türkçe", LANGUAGE_UNDEFINED, FAMILY_OPENRCT2_SPRITE, false }, // LANGUAGE_TURKISH + { "vi-VN", "Vietnamese", "Vietnamese", LANGUAGE_UNDEFINED, FAMILY(&TTFFamilySansSerif), false }, // LANGUAGE_VIETNAMESE }; // clang-format on diff --git a/src/openrct2/localisation/Language.h b/src/openrct2/localisation/Language.h index 75d4071141..20221ca5c6 100644 --- a/src/openrct2/localisation/Language.h +++ b/src/openrct2/localisation/Language.h @@ -30,6 +30,7 @@ enum LANGUAGE_ESPERANTO, LANGUAGE_SPANISH, LANGUAGE_FRENCH, + LANGUAGE_FRENCH_CA, LANGUAGE_ITALIAN, LANGUAGE_JAPANESE, LANGUAGE_KOREAN, @@ -75,6 +76,7 @@ struct language_descriptor const char* locale; const utf8* english_name; const utf8* native_name; + uint8_t fallback; #if !defined(NO_TTF) TTFontFamily const* font_family; #else diff --git a/src/openrct2/localisation/LanguagePack.cpp b/src/openrct2/localisation/LanguagePack.cpp index a82707245a..bda03db897 100644 --- a/src/openrct2/localisation/LanguagePack.cpp +++ b/src/openrct2/localisation/LanguagePack.cpp @@ -9,6 +9,7 @@ #include "LanguagePack.h" +#include "../Context.h" #include "../common.h" #include "../core/FileStream.h" #include "../core/Memory.hpp" @@ -18,6 +19,7 @@ #include "../core/StringReader.h" #include "Language.h" #include "Localisation.h" +#include "LocalisationService.h" #include #include @@ -580,6 +582,12 @@ namespace LanguagePackFactory return languagePack; } + std::unique_ptr FromLanguageId(uint16_t id) + { + auto path = OpenRCT2::GetContext()->GetLocalisationService().GetLanguagePath(id); + return LanguagePack::FromFile(id, path.c_str()); + } + std::unique_ptr FromText(uint16_t id, const utf8* text) { auto languagePack = LanguagePack::FromText(id, text); diff --git a/src/openrct2/localisation/LanguagePack.h b/src/openrct2/localisation/LanguagePack.h index b6e63859ea..f5b8b9e8be 100644 --- a/src/openrct2/localisation/LanguagePack.h +++ b/src/openrct2/localisation/LanguagePack.h @@ -33,5 +33,6 @@ struct ILanguagePack namespace LanguagePackFactory { std::unique_ptr FromFile(uint16_t id, const utf8* path); + std::unique_ptr FromLanguageId(uint16_t id); std::unique_ptr FromText(uint16_t id, const utf8* text); } // namespace LanguagePackFactory diff --git a/src/openrct2/localisation/LocalisationService.cpp b/src/openrct2/localisation/LocalisationService.cpp index cbb810202d..6716223312 100644 --- a/src/openrct2/localisation/LocalisationService.cpp +++ b/src/openrct2/localisation/LocalisationService.cpp @@ -11,6 +11,7 @@ #include "../Context.h" #include "../PlatformEnvironment.h" +#include "../core/Guard.hpp" #include "../core/Path.hpp" #include "../interface/Fonts.h" #include "../object/ObjectManager.h" @@ -40,10 +41,9 @@ LocalisationService::~LocalisationService() = default; const char* LocalisationService::GetString(StringId id) const { - const char* result = nullptr; if (id == STR_EMPTY) { - result = ""; + return ""; } else if (id >= BASE_OBJECT_STRING_ID && id < BASE_OBJECT_STRING_ID + MAX_OBJECT_CACHED_STRINGS) { @@ -53,24 +53,21 @@ const char* LocalisationService::GetString(StringId id) const return _objectStrings[index].c_str(); } - result = "(unallocated string)"; + return "(unallocated string)"; } else if (id != STR_NONE) { - if (_languageCurrent != nullptr) + for (const auto& language : _loadedLanguages) { - result = _languageCurrent->GetString(id); - } - if (result == nullptr && _languageFallback != nullptr) - { - result = _languageFallback->GetString(id); - } - if (result == nullptr) - { - result = "(undefined string)"; + const auto result = language->GetString(id); + if (result != nullptr) + return result; } + + return "(undefined string)"; } - return result; + + return nullptr; } std::string LocalisationService::GetLanguagePath(uint32_t languageId) const @@ -98,49 +95,71 @@ void LocalisationService::OpenLanguage(int32_t id) throw std::invalid_argument("id was undefined"); } - std::string filename; - if (id != LANGUAGE_ENGLISH_UK) - { - filename = GetLanguagePath(LANGUAGE_ENGLISH_UK); - _languageFallback = LanguagePackFactory::FromFile(LANGUAGE_ENGLISH_UK, filename.c_str()); - } - - filename = GetLanguagePath(id); - _languageCurrent = LanguagePackFactory::FromFile(id, filename.c_str()); - if (_languageCurrent != nullptr) + auto preferredLanguage = LanguagePackFactory::FromLanguageId(id); + if (preferredLanguage != nullptr) { _currentLanguage = id; + _languageOrder.emplace_back(id); + _loadedLanguages.emplace_back(std::move(preferredLanguage)); TryLoadFonts(*this); } else { throw std::runtime_error("Unable to open language " + std::to_string(id)); } + + auto checkLanguage = LanguagesDescriptors[id].fallback; + while (checkLanguage != LANGUAGE_UNDEFINED) + { + _languageOrder.emplace_back(checkLanguage); + auto fallbackLanguagePack = LanguagePackFactory::FromLanguageId(checkLanguage); + if (fallbackLanguagePack != nullptr) + { + _loadedLanguages.emplace_back(std::move(fallbackLanguagePack)); + } + + checkLanguage = LanguagesDescriptors[checkLanguage].fallback; + } + + if (id != LANGUAGE_ENGLISH_UK) + { + _languageOrder.emplace_back(LANGUAGE_ENGLISH_UK); + auto englishLanguagePack = LanguagePackFactory::FromLanguageId(LANGUAGE_ENGLISH_UK); + if (englishLanguagePack != nullptr) + { + _loadedLanguages.emplace_back(std::move(englishLanguagePack)); + } + else + { + throw std::runtime_error("Unable to open the English language file!"); + } + } } void LocalisationService::CloseLanguages() { - _languageFallback = nullptr; - _languageCurrent = nullptr; + _languageOrder.clear(); + _loadedLanguages.clear(); _currentLanguage = LANGUAGE_UNDEFINED; } std::tuple LocalisationService::GetLocalisedScenarioStrings( const std::string& scenarioFilename) const { - auto result0 = _languageCurrent->GetScenarioOverrideStringId(scenarioFilename.c_str(), 0); - auto result1 = _languageCurrent->GetScenarioOverrideStringId(scenarioFilename.c_str(), 1); - auto result2 = _languageCurrent->GetScenarioOverrideStringId(scenarioFilename.c_str(), 2); + Guard::Assert(!_loadedLanguages.empty()); + auto result0 = _loadedLanguages[0]->GetScenarioOverrideStringId(scenarioFilename.c_str(), 0); + auto result1 = _loadedLanguages[0]->GetScenarioOverrideStringId(scenarioFilename.c_str(), 1); + auto result2 = _loadedLanguages[0]->GetScenarioOverrideStringId(scenarioFilename.c_str(), 2); return std::make_tuple(result0, result1, result2); } StringId LocalisationService::GetObjectOverrideStringId(std::string_view legacyIdentifier, uint8_t index) const { - if (_languageCurrent == nullptr) + if (_loadedLanguages.empty()) { return STR_NONE; } - return _languageCurrent->GetObjectOverrideStringId(legacyIdentifier, index); + return _loadedLanguages[0]->GetObjectOverrideStringId(legacyIdentifier, index); } StringId LocalisationService::AllocateObjectString(const std::string& target) @@ -176,6 +195,11 @@ void LocalisationService::FreeObjectString(StringId stringId) } } +const std::vector& LocalisationService::GetLanguageOrder() const +{ + return _languageOrder; +} + int32_t LocalisationService_GetCurrentLanguage() { const auto& localisationService = GetContext()->GetLocalisationService(); diff --git a/src/openrct2/localisation/LocalisationService.h b/src/openrct2/localisation/LocalisationService.h index 30cf879d28..d7b76b4c52 100644 --- a/src/openrct2/localisation/LocalisationService.h +++ b/src/openrct2/localisation/LocalisationService.h @@ -34,8 +34,8 @@ namespace OpenRCT2::Localisation const std::shared_ptr _env; int32_t _currentLanguage{}; bool _useTrueTypeFont{}; - std::unique_ptr _languageFallback; - std::unique_ptr _languageCurrent; + std::vector _languageOrder; + std::vector> _loadedLanguages; std::stack _availableObjectStringIds; std::vector _objectStrings; @@ -66,6 +66,7 @@ namespace OpenRCT2::Localisation void CloseLanguages(); StringId AllocateObjectString(const std::string& target); void FreeObjectString(StringId stringId); + const std::vector& GetLanguageOrder() const; }; } // namespace OpenRCT2::Localisation diff --git a/src/openrct2/object/StringTable.cpp b/src/openrct2/object/StringTable.cpp index 09464e31ec..ad0cda0682 100644 --- a/src/openrct2/object/StringTable.cpp +++ b/src/openrct2/object/StringTable.cpp @@ -9,6 +9,7 @@ #include "StringTable.h" +#include "../Context.h" #include "../core/IStream.hpp" #include "../core/Json.hpp" #include "../core/String.hpp" @@ -154,8 +155,8 @@ void StringTable::SetString(ObjectStringID id, uint8_t language, const std::stri void StringTable::Sort() { - auto targetLanguage = LocalisationService_GetCurrentLanguage(); - std::sort(_strings.begin(), _strings.end(), [targetLanguage](const StringTableEntry& a, const StringTableEntry& b) -> bool { + const auto& languageOrder = OpenRCT2::GetContext()->GetLocalisationService().GetLanguageOrder(); + std::sort(_strings.begin(), _strings.end(), [languageOrder](const StringTableEntry& a, const StringTableEntry& b) -> bool { if (a.Id == b.Id) { if (a.LanguageId == b.LanguageId) @@ -163,22 +164,16 @@ void StringTable::Sort() return String::Compare(a.Text, b.Text, true) < 0; } - if (a.LanguageId == targetLanguage) + for (const auto& language : languageOrder) { - return true; - } - if (b.LanguageId == targetLanguage) - { - return false; - } - - if (a.LanguageId == LANGUAGE_ENGLISH_UK) - { - return true; - } - if (b.LanguageId == LANGUAGE_ENGLISH_UK) - { - return false; + if (a.LanguageId == language) + { + return true; + } + if (b.LanguageId == language) + { + return false; + } } return a.LanguageId < b.LanguageId; diff --git a/src/openrct2/platform/Platform.Win32.cpp b/src/openrct2/platform/Platform.Win32.cpp index 9e735b4bc6..0c2d189710 100644 --- a/src/openrct2/platform/Platform.Win32.cpp +++ b/src/openrct2/platform/Platform.Win32.cpp @@ -609,6 +609,7 @@ namespace Platform { L"eo", LANGUAGE_ESPERANTO }, { L"es", LANGUAGE_SPANISH }, { L"fr", LANGUAGE_FRENCH }, + { L"fr-CA", LANGUAGE_FRENCH_CA }, { L"it", LANGUAGE_ITALIAN }, { L"ja", LANGUAGE_JAPANESE }, { L"ko", LANGUAGE_KOREAN },